SnagBadgerA little help for every snag.

Architecture enforcement#

pnpm verify runs pnpm check:architecture against the checkout and pnpm test:architecture against isolated filesystem/package fixtures, after the strict format/lint/type/build/baseline/rule checks. All authored JS/TS variants are scanned. Prototypes receive the same checks.

One explicit policy#

tooling/architecture/policy.mts describes actual module roots, names, source entries, exact built export maps, private trees, permitted dependency directions, runtime environments, and dependencies that must load on demand. The lint rule and graph share this policy and the TypeScript resolver. Future foundation paths remain illustrative; the manifest does not require empty packages.

The current direction is embedding -> reporting/capture and reporting -> capture. Capture cannot depend on reporting, including its UI. Embedding must defer capture and reporting/browser. This encodes ownership/loading constraints, not product interfaces or an obligation to add dependencies to the empty packages.

Support trees currently contain docs and tooling. New root files, support trees (for example examples or CI), extensions, and conventional names require explicit registration. Support trees do not waive naming, package registration, imports, or cycles. Packages anywhere require a module registration. Only listed generated and dependency directories, Git metadata, and TypeScript build info are excluded.

Paths and exports#

Use lowercase kebab-case directory/file basenames, with .test or .spec for tests. Registered tool suffixes include .config, .build, .browser, .base, and .d. Catalog tests use .snag.md and may start with a numeric filename prefix for deterministic ordering. README.md, AGENTS.md, GLOSSARY.md, SKILL.md and existing dotfiles are explicit exceptions. Unknown dotfiles and suffixes fail. React symbol names may remain PascalCase. Symlinked authored source is rejected to avoid hidden ownership.

JavaScript exports must match registered types and import targets exactly, without extra subpaths or conditions. Optional CSS entries use exact registered relative .css string targets, without extra conditions; their source entries still count as UI for runtime boundary enforcement. Each source entry must exist with exact case. External callers must resolve to supported source entries. Package-name deep imports fail even if they target public source. Relative imports to public source and same-module private imports are valid.

Resolution uses actual files, inherited tsconfig aliases, and workspace entries. Aliases take precedence when they redirect workspace names. Slash styles are normalized; resolved spelling is checked against the filesystem, including on case-insensitive Windows. Invalid/unresolved local paths fail. Lint alone is not the unresolved-import check.

Dependencies and loading#

Static imports, re-exports, import types, import-equals, literal import() and require() contribute edges. Computed arguments fail: use a literal. The one registered exception is the deferred trusted project-module loader in tooling/testing/plan.mts, using the exact projectModuleUrl identifier. It is restricted to that tooling path; eager imports, require(), different identifiers, and module-source exceptions still fail. Project modules execute trusted local code and are outside the workspace dependency graph. File and module cycles include type/deferred edges; module cycles also include workspace dependencies declared in package manifests.

Runtime traversal starts at every supported entry and follows transitive helpers. Declaration-level import type / export type edges load nothing. With the required verbatimModuleSyntax setting, inline import { type View } and export { type View } retain empty runtime declarations and eagerly evaluate the target module, so they are runtime edges. Static imports and top-level dynamic imports, including immediately invoked functions, are eager. Imports in other functions are deferred. Deferred edges allow portable entries to reach optional browser/UI code; top-level import() does not. Portable/browser/UI entries cannot reach server code even lazily; server entries cannot reach browser/UI code. CSS and JSX source count as UI.

Classify external runtime dependencies in externalEnvironments when adding them. React/React DOM are UI; Node built-ins are server-only. Unknown external runtime dependencies fail closed, including deferred ones. Longest matching module zones refine the default environment, separating browser/UI source from portable entries.

This source graph does not evaluate arbitrary JavaScript, infer calls to separately declared functions at initialization, inspect external packages' transitive contents, or interpret CSS imports/HTML script tags. Review initialization calls and external classifications. Retain #26's built-consumer/selective-loading checks for actual runtime and bundler behavior.

Evolve the policy#

Move source and update its root/entries/exports/private zones together. Register new packages with their dependencies/environments, and update workspace, TypeScript, build and lint configuration without relaxing strictness. Add path exceptions individually. Do not allowlist a prototype or disable checks. Changes to accepted foundation responsibilities need a recorded decision.

Add passing/failing isolated fixtures in tooling/architecture/check.test.mts for each new category or meaningful regression. Tests create real package trees and tsconfig files, assert diagnostics, and remove only their own fixtures. An actual module move demonstrates policy evolution. Run pnpm test:architecture, then pnpm verify. Diagnostics identify naming/layout, packages/exports, private and unsupported imports, casing/resolution, dependency direction, computed imports, runtime boundaries, eager optional dependencies, external classification, and file/module cycles.