@ontrails/warden 1.0.0-beta.30 → 1.0.0-beta.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +23 -1
  3. package/bin/warden.ts +1 -0
  4. package/package.json +9 -9
  5. package/src/cli.ts +160 -14
  6. package/src/command.ts +48 -5
  7. package/src/config.ts +9 -0
  8. package/src/drift.ts +116 -7
  9. package/src/fix.ts +8 -2
  10. package/src/formatters.ts +3 -8
  11. package/src/index.ts +32 -2
  12. package/src/project-context.ts +286 -3
  13. package/src/rules/ast.ts +4 -0
  14. package/src/rules/duplicate-exported-symbol.ts +211 -0
  15. package/src/rules/duplicate-public-contract.ts +2 -1
  16. package/src/rules/governed-symbol-residue.ts +131 -0
  17. package/src/rules/implementation-returns-result.ts +1 -3
  18. package/src/rules/index.ts +44 -3
  19. package/src/rules/metadata.ts +108 -14
  20. package/src/rules/no-legacy-cli-alias-export.ts +247 -0
  21. package/src/rules/no-retired-cross-vocabulary.ts +19 -9
  22. package/src/rules/permit-governance.ts +1 -1
  23. package/src/rules/public-export-example-coverage.ts +5 -0
  24. package/src/rules/public-internal-deep-imports.ts +3 -63
  25. package/src/rules/registry-names.ts +12 -2
  26. package/src/rules/retired-vocabulary.ts +396 -0
  27. package/src/rules/signal-graph-coaching.ts +32 -3
  28. package/src/rules/surface-overlay-coherence.ts +262 -0
  29. package/src/rules/{surface-facet-coherence.ts → surface-trailhead-coherence.ts} +46 -42
  30. package/src/rules/trail-fork-coaching.ts +1 -1
  31. package/src/rules/trailhead-override-divergence.ts +356 -0
  32. package/src/rules/types.ts +84 -4
  33. package/src/rules/webhook-route-collision.ts +64 -1
  34. package/src/trails/duplicate-exported-symbol.trail.ts +48 -0
  35. package/src/trails/duplicate-public-contract.trail.ts +1 -1
  36. package/src/trails/governed-symbol-residue.trail.ts +24 -0
  37. package/src/trails/index.ts +6 -1
  38. package/src/trails/no-legacy-cli-alias-export.trail.ts +41 -0
  39. package/src/trails/schema.ts +39 -0
  40. package/src/trails/surface-overlay-coherence.trail.ts +24 -0
  41. package/src/trails/{surface-facet-coherence.trail.ts → surface-trailhead-coherence.trail.ts} +5 -5
  42. package/src/trails/trail-fork-coaching.trail.ts +1 -1
  43. package/src/trails/trailhead-override-divergence.trail.ts +47 -0
  44. package/src/trails/wrap-rule.ts +10 -0
  45. package/src/workspaces.ts +30 -69
package/CHANGELOG.md CHANGED
@@ -1,5 +1,110 @@
1
1
  # @ontrails/warden
2
2
 
3
+ ## 1.0.0-beta.39
4
+
5
+ ### Patch Changes
6
+
7
+ - [`f42ca6e`](https://github.com/outfitter-dev/trails/commit/f42ca6e40b29155acec446e5bf44e52e014466bd): Hard cutover: the CLI consumes `cli` bindings from the app-authored surfaces overlay. Scalar bindings behave identically to the removed cliAliases (parity-tested) — the binding name splits on `.` into a transparent synonym command path for exactly one trail. List bindings arrive as command groups: each expanded member trail gets a group-prefixed route that dispatches the member trail with its identity preserved, and a singleton list stays a group. Expansion is fail-fast boundary validation: a scalar binding resolving to zero or multiple trails, or a group with an empty member union, is a `ValidationError` naming the binding. `DeriveTopoGraphOptions.cliAliases`, the `cliAliases`/`trailsCliAliases` app-module export convention, and the per-kind compile lift are deleted; `deriveCliCommands`/`createProgram` take `overlays` instead of `aliases`, and both topo-graph derivation pipelines expand the same bindings through one shared helper so runtime CLI routes and lock routes come from one semantic. A leftover legacy export is now a Warden error (`no-legacy-cli-alias-export`) naming the `surfaceOverlay({ cli: { ... } })` rewrite.
8
+
9
+ This is a breaking API removal shipped under the lockstep beta patch convention (pre-1.0 hard-cutover posture, zero external adoption); the removed options have no deprecation window by design.
10
+
11
+ - [`bafde1f`](https://github.com/outfitter-dev/trails/commit/bafde1fc8172abb8d8617f69a3c7a70667626d10): Fresh derivations now collect app-module overlays through the shared channel compile uses. `@ontrails/adapter-kit` exports `resolveTrailsOverlays()`, the one reader of an app module's `trailsOverlays` export; the compile-path fresh app lease and Warden's fresh topo loading both go through it, making per-namespace drift asymmetry structurally impossible. Warden drift checks (`checkDrift` now accepts derive options carrying overlays) and the topo-aware rule context graph derive with the same overlays the committed lock embeds, so rules like `surface-overlay-coherence` fire on standard runs. Stale drift results name the drifted overlay namespaces (`DriftResult.driftedOverlayNamespaces`) and point at `trails compile` as the remediation.
12
+ - [`b077fb7`](https://github.com/outfitter-dev/trails/commit/b077fb7ba6d9724cac6f0e59bc3fec9aec28984c): Add the export-restructure Regrade class family (TRL-1210). `export-restructure:cli-aliases` inverts legacy `cliAliases`/`trailsCliAliases` exports into `surfaceOverlay({ cli })` bindings inside the module's `trailsOverlays` export — adding the `@ontrails/core` import, deleting the legacy export, and routing anything it cannot prove safe (computed keys, spreads, in-module `aliases:` references) to `needs-review` with the exact target shape named. `export-restructure:mcp-trailheads` projects call-site MCP trailhead maps into `surfaceOverlay({ mcp })` group bindings: it rewrites in place when the same module exports `trailsOverlays`, and otherwise emits a classified `needs-review` handoff naming the module-overlay target while the call-site map stays as the richer-metadata override-in-context. Warden's fix-class union grows to `'export-restructure' | 'term-rewrite'`, `no-legacy-cli-alias-export` now advertises the `export-restructure` class, and `loadWardenRegradeClasses` supersedes `loadWardenTermRewriteClasses` (still exported) as the full Warden-routed class loader. Class-mode Regrade also gains the full plan lifecycle: `trails regrade plan --type class --class-ids ...` writes a `.trails/regrade/<slug>.json` plan carrying class ids, scope, and intent, `regrade check` re-runs the dry run and gates on outstanding rewrites or review, and `regrade apply` applies and graduates the plan to `.trails/regrade/history/<slug>-<hash>.json` — the same plan → check → apply → history evidence trail vocabulary regrades already had, now available to structural transforms. The class family ships for downstream apps bridging the pre-1.0 gap, so pre-cutover alias exports and trailhead maps migrate mechanically instead of by hand.
13
+ - [`81373bc`](https://github.com/outfitter-dev/trails/commit/81373bc5e980bb06d56fb06af4f0986f72e318c7): Wave-2 MCP cutover to the app-authored `surfaces` overlay. The overlay's `mcp` bindings are now the authored, lockable default for the MCP surface: a list binding derives one grouped trailhead tool (member selection in `{ trail, input }`, member identity preserved in `{ trail, output }`, deterministic derived description), and a scalar binding derives an additional tool synonym whose MCP-safe name is published verbatim and must expand to exactly one trail. `deriveMcpTools`/`createServer` accept the new `overlays` option; `@ontrails/core` gains `expandMcpSurfaceBindings` and `deriveMcpTrailheadDescription`.
14
+
15
+ The call-site `CreateServerOptions.trailheads` map survives as permanent override-in-context design, not a compatibility bridge: when both channels are present, the call-site map wins at runtime. Warden's new `trailhead-override-divergence` rule (warn) names both sides when a call-site map's binding names or member selectors diverge from the authored overlay default.
16
+
17
+ Topographer now derives `graph.trailheads` from the overlay's `mcp` list bindings in both `deriveTopoGraph` and the store-side graph build, so trailhead facts flow from compiled locks into Wayfinder reads for the first time. The never-wired `DeriveTopoGraphOptions.trailheads` option and the `TopoGraphTrailheadDeclaration`/`TopoGraphTrailheadTrailSelector` types are removed — a beta-window hard cutover of an option no caller could reach; author the equivalent `mcp` list binding in `surfaceOverlay({ mcp })` instead.
18
+
19
+ - [`820b4ad`](https://github.com/outfitter-dev/trails/commit/820b4ad9c40ea383b3c489a05fe7e4b2328e324f): Add `surfaceOverlay` — the shared surface-naming schema (scalar binding = synonym, list binding = grouped entry, singleton list stays a group) with app-authored/adapter-derived overlay provenance enforced at collection and consumption, and the `surface-overlay-coherence` Warden rule. MCP tool-name derivation moves to `@ontrails/core` (`deriveMcpToolName`) so the surface and governance read one projection; `@ontrails/mcp`'s `deriveToolName` now delegates to it. The coherence rule activates on standard warden runs once fresh derivations collect app-module overlays through the shared compile channel (TRL-1209, next in this stack).
20
+ - [`58db715`](https://github.com/outfitter-dev/trails/commit/58db715209442604fe58f5004fec37426c5969b1): `duplicate-public-contract` now includes contour anchoring in the normalized contract fingerprint, so factory CRUD trails derived against different contours (for example two tables' `delete` trails that both normalize to `{ id } → void` with the same intent) are no longer flagged as duplicates. Genuine duplicates — identical facts with the same or no contour anchoring — still warn, and the diagnostic message names contours among the shared facts.
21
+ - [`7cd0576`](https://github.com/outfitter-dev/trails/commit/7cd0576db8bc421cfd441c126f884e425e2254af): `signal-graph-coaching` no longer flags store-derived table signals (`created`/`updated`/`removed`) that have no consumers. Store resources advertise those signals as available capability, so leaving them unconsumed is a legitimate steady state for store-backed apps. Non-store produced signals without consumers still warn, and dead-signal coaching (no producer and no consumer) is unchanged.
22
+ - [`b9e82a3`](https://github.com/outfitter-dev/trails/commit/b9e82a33546356c93fbc302fb934a83f19f1c2c5): Webhook ingress v2 (TRL-1194, absorbing TRL-1174 and TRL-1175): store-verified, per-endpoint webhook ingress becomes framework-expressible. `webhook()` accepts dynamic path segments (`path: '/hooks/:endpoint'`) whose values are delivered as envelope fields, opt-in `rawBody: true` delivery (a non-JSON body is no longer a surface-level failure — the trail owns payload interpretation), an allowlisted `headers` list delivered lowercased, and `resources` that make `verify` resource-capable: the HTTP surface resolves the declared resources into a context for the verifier and releases them afterwards, so signature checks can reach stores holding per-endpoint secrets. Envelope-mode ingress responds 202 Accepted; classic static webhooks keep their exact-match, JSON-gated, 200 behavior. Core exports `parseWebhookPathParams`, `matchWebhookPath`, `webhookPathPatternsOverlap`, and `createResources`. The `webhook-route-collision` Warden rule now also flags dynamic patterns that overlap other webhook or derived routes, not just exact method/path duplicates.
23
+
24
+ ## 1.0.0-beta.38
25
+
26
+ ### Patch Changes
27
+
28
+ - [`a105127`](https://github.com/outfitter-dev/trails/commit/a105127e5662ed9a6c245125f791fb0182da3f5e): Add the `@ontrails/cloudflare` adapter collection with its first two service subpaths. `@ontrails/cloudflare/workers` exports `createWorkersHandler`, a materializer producing the `{ fetch(request, env, ctx) }` Worker export on the shared HTTP fetch kernel, with an env bridge that re-resolves env-bound resources whenever a new Worker `env` arrives so no resource instance serves a request with a stale env. `@ontrails/cloudflare/kv` exports `cloudflareKv`, a resource definition wrapping a KV namespace binding (`get`/`put`/`delete`/`list` with TTL options) plus an in-memory `createMemoryKv` mock so `testAll` runs configuration-free.
29
+
30
+ `@ontrails/core` now guards the default trail context fields: `requestId` falls back to `crypto.randomUUID()` when the `Bun` global is absent, and `cwd`/`env` fall back to `'/'`/`{}` when `process` is absent, so trail execution works on runtimes like Cloudflare Workers.
31
+
32
+ `@ontrails/warden` registers the `@ontrails/cloudflare` public barrel in the repo-local `public-export-example-coverage` policy, requiring `@example` TSDoc coverage on `createWorkersHandler` and `cloudflareKv`.
33
+
34
+ ## 1.0.0-beta.37
35
+
36
+ ## 1.0.0-beta.36
37
+
38
+ ### Patch Changes
39
+
40
+ - [`6e63e48`](https://github.com/outfitter-dev/trails/commit/6e63e483617b84cb6868d0c4d58d5b5a8d3b9ed2): Complete the v1 grouped surface-entry vocabulary cutover from facet to trailhead, including Regrade dogfood support for governed string literal renames and composed AST rewrite application.
41
+
42
+ ## 1.0.0-beta.35
43
+
44
+ ### Patch Changes
45
+
46
+ - [`1f5659b`](https://github.com/outfitter-dev/trails/commit/1f5659bfe39568f7bbee0503ace8b6e562d3f899): Add the `duplicate-exported-symbol` Warden rule to warn when multiple first-party packages define the same exported symbol name.
47
+
48
+ ## 1.0.0-beta.34
49
+
50
+ ## 1.0.0-beta.33
51
+
52
+ ### Patch Changes
53
+
54
+ - [`fc002d5`](https://github.com/outfitter-dev/trails/commit/fc002d5669f4303427e99f45f9998fd0b0172bdb): Add governed AST identifier rename helpers and Warden residue detection for
55
+ active vocabulary symbol transitions.
56
+ - [`6ca0d8f`](https://github.com/outfitter-dev/trails/commit/6ca0d8f776801eee71ddd86cb88c198eaf5815fd): Add a typed governed-vocabulary transition registry that Warden owns and Regrade
57
+ can consume for migration planning.
58
+
59
+ ## 1.0.0-beta.32
60
+
61
+ ### Patch Changes
62
+
63
+ - 3e5c0fc: Export shared diagnostic base types from core and align governance diagnostic
64
+ severity vocabulary across adapter checks, permits, and Warden.
65
+ - f3c4fef: Export a shared `escapeRegExp` helper from core and migrate first-party callers off local copies.
66
+ - cb0a9d8: Export shared workspace package discovery helpers from core and migrate first-party discovery callers.
67
+ - fe72b84: Fold remaining Regrade and Warden scan-target surfaces onto the shared path-scope vocabulary.
68
+ - Updated dependencies [3e5c0fc]
69
+ - Updated dependencies [f3c4fef]
70
+ - Updated dependencies [cb0a9d8]
71
+ - Updated dependencies [8db145e]
72
+ - Updated dependencies [2b819f4]
73
+ - Updated dependencies [21c6dda]
74
+ - Updated dependencies [860ef32]
75
+ - Updated dependencies [fe72b84]
76
+ - @ontrails/adapter-kit@1.0.0-beta.32
77
+ - @ontrails/core@1.0.0-beta.32
78
+ - @ontrails/permits@1.0.0-beta.32
79
+ - @ontrails/topographer@1.0.0-beta.32
80
+ - @ontrails/config@1.0.0-beta.32
81
+ - @ontrails/cli@1.0.0-beta.32
82
+ - @ontrails/store@1.0.0-beta.32
83
+
84
+ ## 1.0.0-beta.31
85
+
86
+ ### Patch Changes
87
+
88
+ - ee9f3ae: Let Warden fix capabilities declare downstream scan targets and have Regrade
89
+ honor those targets for Warden-backed term-rewrite classes.
90
+
91
+ Dogfood the first safe facet-to-trailhead prose rewrite through project-local
92
+ Warden rules and Regrade.
93
+
94
+ - a0126d9: Add Warden `scope.exclude` globs through project config and the Trails CLI
95
+ wrapper so governance runs can exclude local notes, scratch space, and generated
96
+ state without dropping durable skills or plugin assets from scope.
97
+ - 6a26a08: Rename Warden governance scope controls from jurisdiction ignore settings to `scope.exclude` across config, CLI, and Trails surfaces.
98
+ - Updated dependencies [4cd5d4e]
99
+ - Updated dependencies [38907cc]
100
+ - @ontrails/core@1.0.0-beta.31
101
+ - @ontrails/adapter-kit@1.0.0-beta.31
102
+ - @ontrails/cli@1.0.0-beta.31
103
+ - @ontrails/config@1.0.0-beta.31
104
+ - @ontrails/permits@1.0.0-beta.31
105
+ - @ontrails/store@1.0.0-beta.31
106
+ - @ontrails/topographer@1.0.0-beta.31
107
+
3
108
  ## 1.0.0-beta.30
4
109
 
5
110
  ### Patch Changes
package/README.md CHANGED
@@ -47,6 +47,28 @@ Warden uses the shared Trails project-root resolver when a caller does not pass
47
47
 
48
48
  This is the right home for repo-specific migration checks or governance that has not earned a place in `@ontrails/warden` itself.
49
49
 
50
+ ## Scope
51
+
52
+ Use `warden.scope.exclude` in `trails.config.*` to keep generated, scratch, or local planning paths outside Warden governance while leaving durable project paths in scope.
53
+
54
+ ```json
55
+ {
56
+ "warden": {
57
+ "scope": {
58
+ "exclude": [".scratch/**", ".agents/notes/**"]
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ For a single run, pass one or more root-relative globs:
65
+
66
+ ```bash
67
+ warden --scope-exclude '.scratch/**' --scope-exclude '.agents/notes/**'
68
+ ```
69
+
70
+ Scope is a governance boundary, not a migration scan boundary. Regrade uses its own `scope.exclude` / `--exclude` controls for migration plans.
71
+
50
72
  A rule module may export `rule`, `rules`, `sourceRule`, `sourceRules`, `topoRule`, or `topoRules`. Rules without explicit metadata receive default repo-local source-static or topo-aware metadata so short migration rules can run without extra ceremony. Project-aware source rules that provide `checkWithContext()` default to repo-local project-static metadata.
51
73
 
52
74
  ```typescript
@@ -149,7 +171,7 @@ This is the same factory used internally to build all built-in rule trails.
149
171
  | --- | --- |
150
172
  | `runWarden(options?)` | Run all rules and drift checks, return a report |
151
173
  | `formatWardenReport(report)` | Human-readable report |
152
- | `checkDrift(rootDir, topo?)` | Check if the lock file matches the current topo |
174
+ | `checkDrift(rootDir, topo?, options?)` | Check if the lock file matches the current topo; pass `{ overlays }` from the app module so the comparison graph carries the overlay content compile embeds. Stale results name `driftedOverlayNamespaces` when overlays diverge |
153
175
  | `wardenRules` | Registry of all built-in rules |
154
176
  | `builtinWardenRuleMetadata` | Tier, scope, lifecycle, and invariant metadata for built-in rules |
155
177
  | `getWardenRuleMetadata(ruleOrName)` | Resolve inline or built-in metadata for a Warden rule |
package/bin/warden.ts CHANGED
@@ -19,6 +19,7 @@ Options:
19
19
  --fail-on <value> error or warning
20
20
  --format <value> summary, github, or json
21
21
  --drafts <value> include, exclude, or only
22
+ --scope-exclude <glob> Root-relative path glob Warden should not govern
22
23
  --lock <value> auto, cached, refresh, or skip
23
24
  --no-lock-mutation Do not write lock artifacts
24
25
  --strict Fail on warnings
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/warden",
3
- "version": "1.0.0-beta.30",
3
+ "version": "1.0.0-beta.39",
4
4
  "bin": {
5
5
  "warden": "./bin/warden.ts"
6
6
  },
@@ -28,21 +28,21 @@
28
28
  "clean": "rm -rf dist *.tsbuildinfo"
29
29
  },
30
30
  "dependencies": {
31
- "@ontrails/adapter-kit": "^1.0.0-beta.30",
32
- "@ontrails/cli": "^1.0.0-beta.30",
33
- "@ontrails/config": "^1.0.0-beta.30",
34
- "@ontrails/permits": "^1.0.0-beta.30",
35
- "@ontrails/store": "^1.0.0-beta.30",
31
+ "@ontrails/adapter-kit": "^1.0.0-beta.39",
32
+ "@ontrails/cli": "^1.0.0-beta.39",
33
+ "@ontrails/config": "^1.0.0-beta.39",
34
+ "@ontrails/permits": "^1.0.0-beta.39",
35
+ "@ontrails/store": "^1.0.0-beta.39",
36
36
  "oxc-parser": "^0.121.0",
37
37
  "oxc-resolver": "11.19.1",
38
38
  "oxc-walker": "^1.0.0",
39
39
  "zod": "^4.3.5"
40
40
  },
41
41
  "devDependencies": {
42
- "@ontrails/testing": "^1.0.0-beta.30"
42
+ "@ontrails/testing": "^1.0.0-beta.39"
43
43
  },
44
44
  "peerDependencies": {
45
- "@ontrails/core": "^1.0.0-beta.30",
46
- "@ontrails/topographer": "^1.0.0-beta.30"
45
+ "@ontrails/core": "^1.0.0-beta.39",
46
+ "@ontrails/topographer": "^1.0.0-beta.39"
47
47
  }
48
48
  }
package/src/cli.ts CHANGED
@@ -8,10 +8,18 @@
8
8
  import { isAbsolute, relative, resolve } from 'node:path';
9
9
 
10
10
  import { resolveTrailsProjectRoot } from '@ontrails/config';
11
- import type { Topo } from '@ontrails/core';
11
+ import {
12
+ getContourReferences,
13
+ matchesAnyPathGlob,
14
+ resolveSurfaceOverlayBindings,
15
+ surfaceBindingsFromLockOverlays,
16
+ } from '@ontrails/core';
17
+ import type { SurfaceBindings, Topo } from '@ontrails/core';
12
18
  import { deriveTopoGraph } from '@ontrails/topographer';
13
- import type { TopoGraph } from '@ontrails/topographer';
14
- import { getContourReferences } from '@ontrails/core';
19
+ import type {
20
+ TopoGraph,
21
+ TopoGraphOverlayRegistration,
22
+ } from '@ontrails/topographer';
15
23
 
16
24
  import type {
17
25
  EffectiveWardenConfig,
@@ -20,6 +28,7 @@ import type {
20
28
  WardenDepth,
21
29
  WardenFailOn,
22
30
  WardenFormat,
31
+ WardenScope,
23
32
  WardenLockMode,
24
33
  } from './config.js';
25
34
  import { runWardenAdapterChecks } from './adapter-check.js';
@@ -27,11 +36,12 @@ import { resolveWardenConfig } from './config.js';
27
36
  import { isDraftMarkedFile } from './draft.js';
28
37
  import { applySafeFixesToSource, hasSafeFixEdits } from './fix.js';
29
38
  import type { DriftResult } from './drift.js';
30
- import { checkDrift } from './drift.js';
39
+ import { checkDrift, staleDriftMessage } from './drift.js';
31
40
  import { loadProjectWardenRules } from './project-rules.js';
32
41
  import type { ProjectWardenRules } from './project-rules.js';
33
42
  import {
34
43
  collectProjectDocumentationImportResolutions,
44
+ collectProjectExportedSymbolDefinitions,
35
45
  collectProjectImportResolutions,
36
46
  collectPublicWorkspaces,
37
47
  } from './project-context.js';
@@ -56,10 +66,12 @@ import {
56
66
  isWardenSourceScanTarget,
57
67
  } from './rules/scan.js';
58
68
  import type {
69
+ AuthoredMcpSurfaceBindingSet,
59
70
  ProjectAwareWardenRule,
60
71
  ProjectContext,
61
72
  TopoAwareWardenRule,
62
73
  WardenDiagnostic,
74
+ WardenExportedSymbolDefinition,
63
75
  WardenGuidanceLink,
64
76
  WardenRule,
65
77
  WardenRuleTier,
@@ -74,6 +86,12 @@ export interface WardenTopoTarget {
74
86
  readonly graph?: TopoGraph | undefined;
75
87
  /** Stable app/topo label used to tag topo-aware diagnostics. */
76
88
  readonly name?: string | undefined;
89
+ /**
90
+ * App-module overlay registrations collected through the shared
91
+ * `resolveTrailsOverlays` channel, so fresh drift and topo-aware rule
92
+ * derivations carry the same overlays the committed lock embeds.
93
+ */
94
+ readonly overlays?: readonly TopoGraphOverlayRegistration[] | undefined;
77
95
  /** Resolved topo module to inspect. */
78
96
  readonly topo: Topo;
79
97
  }
@@ -105,6 +123,8 @@ export interface WardenRunOptions {
105
123
  readonly fix?: boolean | undefined;
106
124
  /** Output format requested by the caller. */
107
125
  readonly format?: WardenFormat | undefined;
126
+ /** Root-relative source paths that Warden should not govern. */
127
+ readonly scope?: WardenScope | undefined;
108
128
  /** Lockfile mode requested by the caller. */
109
129
  readonly lock?: WardenLockMode | undefined;
110
130
  /** Suppress lockfile mutation for CI/pre-push callers. */
@@ -260,6 +280,29 @@ const filterSourceFilesByDraftMode = (
260
280
  draftModeIncludesFile(sourceFile.filePath, drafts)
261
281
  );
262
282
 
283
+ const rootRelativeScopePath = (rootDir: string, filePath: string): string =>
284
+ relative(rootDir, filePath).replaceAll('\\', '/');
285
+
286
+ const filterSourceFilesByScope = (
287
+ sourceFiles: readonly SourceFile[],
288
+ rootDir: string,
289
+ scope: WardenScope
290
+ ): readonly SourceFile[] => {
291
+ if (scope.exclude.length === 0) {
292
+ return sourceFiles;
293
+ }
294
+
295
+ return sourceFiles.filter(
296
+ (sourceFile) =>
297
+ !matchesAnyPathGlob(
298
+ rootRelativeScopePath(rootDir, sourceFile.filePath),
299
+ scope.exclude
300
+ )
301
+ );
302
+ };
303
+
304
+ const EMPTY_WARDEN_SCOPE: WardenScope = { exclude: [] };
305
+
263
306
  const collectDevPermitTestFiles = (dir: string): readonly string[] => {
264
307
  const glob = new Bun.Glob('**/*.ts');
265
308
  let matches: IterableIterator<string>;
@@ -328,6 +371,9 @@ interface SourceFile {
328
371
  }
329
372
 
330
373
  interface MutableProjectContext {
374
+ authoredMcpSurfaceBindingSets:
375
+ | readonly AuthoredMcpSurfaceBindingSet[]
376
+ | undefined;
331
377
  contourReferencesByName: Map<string, Set<string>>;
332
378
  crudTableIds: Set<string>;
333
379
  composeTargetTrailIds: Set<string>;
@@ -338,6 +384,10 @@ interface MutableProjectContext {
338
384
  knownTrailIds: Set<string>;
339
385
  topoTrailIds: Set<string>;
340
386
  importResolutionsByFile: Map<string, readonly WardenImportResolution[]>;
387
+ exportedSymbolDefinitionsByName: Map<
388
+ string,
389
+ readonly WardenExportedSymbolDefinition[]
390
+ >;
341
391
  documentedImportResolutionsByFile: Map<
342
392
  string,
343
393
  readonly WardenImportResolution[]
@@ -349,6 +399,7 @@ interface MutableProjectContext {
349
399
  }
350
400
 
351
401
  const createMutableProjectContext = (): MutableProjectContext => ({
402
+ authoredMcpSurfaceBindingSets: undefined,
352
403
  composeTargetTrailIds: new Set<string>(),
353
404
  contourReferencesByName: new Map<string, Set<string>>(),
354
405
  crudCoverageByEntity: new Map<string, Set<string>>(),
@@ -357,6 +408,7 @@ const createMutableProjectContext = (): MutableProjectContext => ({
357
408
  string,
358
409
  readonly WardenImportResolution[]
359
410
  >(),
411
+ exportedSymbolDefinitionsByName: new Map(),
360
412
  importResolutionsByFile: new Map<string, readonly WardenImportResolution[]>(),
361
413
  knownContourIds: new Set<string>(),
362
414
  knownResourceIds: new Set<string>(),
@@ -386,6 +438,9 @@ const addContourReferenceTargets = (
386
438
  };
387
439
 
388
440
  const toProjectContext = (context: MutableProjectContext): ProjectContext => ({
441
+ ...(context.authoredMcpSurfaceBindingSets === undefined
442
+ ? {}
443
+ : { authoredMcpSurfaceBindingSets: context.authoredMcpSurfaceBindingSets }),
389
444
  ...(context.contourReferencesByName.size > 0
390
445
  ? {
391
446
  contourReferencesByName: new Map(
@@ -427,6 +482,12 @@ const toProjectContext = (context: MutableProjectContext): ProjectContext => ({
427
482
  context.documentedImportResolutionsByFile,
428
483
  }
429
484
  : {}),
485
+ ...(context.exportedSymbolDefinitionsByName.size > 0
486
+ ? {
487
+ exportedSymbolDefinitionsByName:
488
+ context.exportedSymbolDefinitionsByName,
489
+ }
490
+ : {}),
430
491
  ...(context.onTargetSignalIds.size > 0
431
492
  ? { onTargetSignalIds: context.onTargetSignalIds }
432
493
  : {}),
@@ -799,6 +860,7 @@ const collectFileImportResolutions = (
799
860
  context: MutableProjectContext
800
861
  ): void => {
801
862
  const resolutionsByFile = collectProjectImportResolutions({
863
+ publicWorkspaces: context.publicWorkspaces,
802
864
  rootDir,
803
865
  sourceFiles,
804
866
  });
@@ -813,6 +875,7 @@ const collectFileDocumentedImportResolutions = (
813
875
  context: MutableProjectContext
814
876
  ): void => {
815
877
  const resolutionsByFile = collectProjectDocumentationImportResolutions({
878
+ publicWorkspaces: context.publicWorkspaces,
816
879
  rootDir,
817
880
  sourceFiles,
818
881
  });
@@ -821,19 +884,78 @@ const collectFileDocumentedImportResolutions = (
821
884
  }
822
885
  };
823
886
 
887
+ const collectFileExportedSymbolDefinitions = (
888
+ rootDir: string,
889
+ sourceFiles: readonly SourceFile[],
890
+ context: MutableProjectContext
891
+ ): void => {
892
+ const definitionsByName = collectProjectExportedSymbolDefinitions({
893
+ publicWorkspaces: context.publicWorkspaces,
894
+ rootDir,
895
+ sourceFiles,
896
+ });
897
+ for (const [name, definitions] of definitionsByName) {
898
+ context.exportedSymbolDefinitionsByName.set(name, definitions);
899
+ }
900
+ };
901
+
902
+ /**
903
+ * Resolve per-app authored `mcp` surface bindings across the run's topo
904
+ * targets, so project-aware source rules can compare call-site surface
905
+ * options against the authored default of the app they belong to.
906
+ *
907
+ * Prefers the serialized graph overlays when a target carries a precomputed
908
+ * graph; otherwise reads the app-module overlay registrations. Invalid
909
+ * overlays are skipped here — `surface-overlay-coherence` reports them on
910
+ * the topo-aware path.
911
+ */
912
+ const collectAuthoredMcpSurfaceBindingSets = (
913
+ topoTargets: readonly WardenTopoTarget[]
914
+ ): readonly AuthoredMcpSurfaceBindingSet[] | undefined => {
915
+ const sets: AuthoredMcpSurfaceBindingSet[] = [];
916
+ for (const target of topoTargets) {
917
+ let bindings: SurfaceBindings | undefined;
918
+ try {
919
+ const graphOverlays = target.graph?.overlays;
920
+ bindings =
921
+ graphOverlays === undefined
922
+ ? resolveSurfaceOverlayBindings(target.overlays)?.mcp
923
+ : surfaceBindingsFromLockOverlays(graphOverlays)?.mcp;
924
+ } catch {
925
+ continue;
926
+ }
927
+ if (bindings === undefined) {
928
+ continue;
929
+ }
930
+ sets.push({
931
+ appName: target.name ?? target.topo.name,
932
+ bindings,
933
+ trailIds: [...target.topo.trails.keys()],
934
+ });
935
+ }
936
+ return sets.length > 0 ? sets : undefined;
937
+ };
938
+
824
939
  const buildProjectContext = (
825
940
  sourceFiles: readonly SourceFile[],
826
941
  rootDir: string,
827
- appTopos: readonly Topo[] = []
942
+ appTopos: readonly Topo[] = [],
943
+ scope: WardenScope = EMPTY_WARDEN_SCOPE,
944
+ authoredMcpSurfaceBindingSets:
945
+ | readonly AuthoredMcpSurfaceBindingSet[]
946
+ | undefined = undefined
828
947
  ): ProjectContext => {
829
948
  const context = createMutableProjectContext();
949
+ context.authoredMcpSurfaceBindingSets = authoredMcpSurfaceBindingSets;
830
950
  const typeScriptSourceFiles = sourceFiles.filter(
831
951
  (sourceFile) => sourceFile.kind === 'typescript'
832
952
  );
833
953
  const documentationSourceFiles = sourceFiles.filter(
834
954
  (sourceFile) => sourceFile.kind === 'documentation'
835
955
  );
836
- context.publicWorkspaces = collectPublicWorkspaces(rootDir);
956
+ context.publicWorkspaces = collectPublicWorkspaces(rootDir, {
957
+ exclude: scope.exclude,
958
+ });
837
959
 
838
960
  if (appTopos.length > 0) {
839
961
  for (const appTopo of appTopos) {
@@ -857,6 +979,7 @@ const buildProjectContext = (
857
979
  documentationSourceFiles,
858
980
  context
859
981
  );
982
+ collectFileExportedSymbolDefinitions(rootDir, typeScriptSourceFiles, context);
860
983
 
861
984
  return toProjectContext(context);
862
985
  };
@@ -1015,6 +1138,7 @@ const topoRuleFailureDiagnostic = (
1015
1138
  const lintTopo = async (
1016
1139
  appTopo: Topo,
1017
1140
  graph: TopoGraph | undefined,
1141
+ overlays: readonly TopoGraphOverlayRegistration[] | undefined,
1018
1142
  extraTopoRules: readonly TopoAwareWardenRule[],
1019
1143
  selector: WardenRuleSelector
1020
1144
  ): Promise<readonly WardenDiagnostic[]> => {
@@ -1025,7 +1149,7 @@ const lintTopo = async (
1025
1149
  ].filter((rule) => isSelectedTopoRule(rule, selector));
1026
1150
  let contextGraph: TopoGraph;
1027
1151
  try {
1028
- contextGraph = graph ?? deriveTopoGraph(appTopo);
1152
+ contextGraph = graph ?? deriveTopoGraph(appTopo, { overlays });
1029
1153
  } catch (error) {
1030
1154
  for (const rule of rules) {
1031
1155
  diagnostics.push(topoRuleFailureDiagnostic(rule, error));
@@ -1110,6 +1234,7 @@ const lintTopoTargets = async (
1110
1234
  const topoDiagnostics = await lintTopo(
1111
1235
  target.topo,
1112
1236
  target.graph,
1237
+ target.overlays,
1113
1238
  extraTopoRules,
1114
1239
  selector
1115
1240
  );
@@ -1145,6 +1270,7 @@ interface WardenLintResult {
1145
1270
  const lintFiles = async (
1146
1271
  rootDir: string,
1147
1272
  drafts: EffectiveWardenConfig['drafts'],
1273
+ scope: EffectiveWardenConfig['scope'],
1148
1274
  topoTargets: readonly WardenTopoTarget[],
1149
1275
  extraTopoRules: readonly TopoAwareWardenRule[],
1150
1276
  extraSourceRules: readonly WardenRule[],
@@ -1159,14 +1285,17 @@ const lintFiles = async (
1159
1285
  };
1160
1286
  }
1161
1287
 
1162
- const sourceFiles = filterSourceFilesByDraftMode(
1163
- await loadSourceFiles(rootDir),
1164
- drafts
1288
+ const sourceFiles = filterSourceFilesByScope(
1289
+ filterSourceFilesByDraftMode(await loadSourceFiles(rootDir), drafts),
1290
+ rootDir,
1291
+ scope
1165
1292
  );
1166
1293
  const context = buildProjectContext(
1167
1294
  sourceFiles,
1168
1295
  rootDir,
1169
- topoTargets.map((target) => target.topo)
1296
+ topoTargets.map((target) => target.topo),
1297
+ scope,
1298
+ collectAuthoredMcpSurfaceBindingSets(topoTargets)
1170
1299
  );
1171
1300
  const allDiagnostics: WardenDiagnostic[] = [
1172
1301
  ...lintSourceFiles(sourceFiles, context, extraSourceRules, selector),
@@ -1244,11 +1373,18 @@ const checkDriftForTopoTargets = async (
1244
1373
  topoTargets: readonly WardenTopoTarget[]
1245
1374
  ): Promise<DriftResult> => {
1246
1375
  if (topoTargets.length <= 1) {
1247
- return checkDrift(rootDir, topoTargets[0]?.topo);
1376
+ const [target] = topoTargets;
1377
+ return checkDrift(
1378
+ rootDir,
1379
+ target?.topo,
1380
+ target === undefined ? undefined : { overlays: target.overlays }
1381
+ );
1248
1382
  }
1249
1383
 
1250
1384
  const driftResults = await Promise.all(
1251
- topoTargets.map((target) => checkDrift(rootDir, target.topo))
1385
+ topoTargets.map((target) =>
1386
+ checkDrift(rootDir, target.topo, { overlays: target.overlays })
1387
+ )
1252
1388
  );
1253
1389
  const committedHashes = new Set(
1254
1390
  driftResults.map((result) => result.committedHash)
@@ -1282,9 +1418,17 @@ const checkDriftForTopoTargets = async (
1282
1418
  }
1283
1419
 
1284
1420
  const currentHash = aggregateDriftHash(topoTargets, driftResults);
1421
+ const driftedOverlayNamespaces = [
1422
+ ...new Set(
1423
+ driftResults.flatMap((result) => result.driftedOverlayNamespaces ?? [])
1424
+ ),
1425
+ ].toSorted();
1285
1426
  return {
1286
1427
  committedHash,
1287
1428
  currentHash,
1429
+ ...(driftedOverlayNamespaces.length === 0
1430
+ ? {}
1431
+ : { driftedOverlayNamespaces }),
1288
1432
  stale: committedHash !== null && committedHash !== currentHash,
1289
1433
  };
1290
1434
  };
@@ -1339,6 +1483,7 @@ const buildCliConfigLayer = (options: WardenRunOptions): WardenConfigLayer => ({
1339
1483
  ...(options.drafts ? { drafts: options.drafts } : {}),
1340
1484
  ...(options.failOn ? { failOn: options.failOn } : {}),
1341
1485
  ...(options.format ? { format: options.format } : {}),
1486
+ ...(options.scope ? { scope: options.scope } : {}),
1342
1487
  ...(options.lock ? { lock: options.lock } : {}),
1343
1488
  ...(options.noLockMutation === undefined
1344
1489
  ? {}
@@ -1476,6 +1621,7 @@ export const runWarden = async (
1476
1621
  ? await lintFiles(
1477
1622
  rootDir,
1478
1623
  effectiveConfig.drafts,
1624
+ effectiveConfig.scope,
1479
1625
  topoTargets,
1480
1626
  [...projectRules.topoRules, ...(options.extraTopoRules ?? [])],
1481
1627
  [...projectRules.sourceRules, ...(options.extraSourceRules ?? [])],
@@ -1602,7 +1748,7 @@ const formatDriftSection = (drift: DriftResult | null): string[] => {
1602
1748
  return [`Drift: blocked (${drift.blockedReason})`, ''];
1603
1749
  }
1604
1750
  const label = drift.stale
1605
- ? 'Drift: trails.lock is stale (regenerate with `trails compile`)'
1751
+ ? `Drift: ${staleDriftMessage(drift)}`
1606
1752
  : 'Drift: clean';
1607
1753
  return [label, ''];
1608
1754
  };