@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.
- package/CHANGELOG.md +105 -0
- package/README.md +23 -1
- package/bin/warden.ts +1 -0
- package/package.json +9 -9
- package/src/cli.ts +160 -14
- package/src/command.ts +48 -5
- package/src/config.ts +9 -0
- package/src/drift.ts +116 -7
- package/src/fix.ts +8 -2
- package/src/formatters.ts +3 -8
- package/src/index.ts +32 -2
- package/src/project-context.ts +286 -3
- package/src/rules/ast.ts +4 -0
- package/src/rules/duplicate-exported-symbol.ts +211 -0
- package/src/rules/duplicate-public-contract.ts +2 -1
- package/src/rules/governed-symbol-residue.ts +131 -0
- package/src/rules/implementation-returns-result.ts +1 -3
- package/src/rules/index.ts +44 -3
- package/src/rules/metadata.ts +108 -14
- package/src/rules/no-legacy-cli-alias-export.ts +247 -0
- package/src/rules/no-retired-cross-vocabulary.ts +19 -9
- package/src/rules/permit-governance.ts +1 -1
- package/src/rules/public-export-example-coverage.ts +5 -0
- package/src/rules/public-internal-deep-imports.ts +3 -63
- package/src/rules/registry-names.ts +12 -2
- package/src/rules/retired-vocabulary.ts +396 -0
- package/src/rules/signal-graph-coaching.ts +32 -3
- package/src/rules/surface-overlay-coherence.ts +262 -0
- package/src/rules/{surface-facet-coherence.ts → surface-trailhead-coherence.ts} +46 -42
- package/src/rules/trail-fork-coaching.ts +1 -1
- package/src/rules/trailhead-override-divergence.ts +356 -0
- package/src/rules/types.ts +84 -4
- package/src/rules/webhook-route-collision.ts +64 -1
- package/src/trails/duplicate-exported-symbol.trail.ts +48 -0
- package/src/trails/duplicate-public-contract.trail.ts +1 -1
- package/src/trails/governed-symbol-residue.trail.ts +24 -0
- package/src/trails/index.ts +6 -1
- package/src/trails/no-legacy-cli-alias-export.trail.ts +41 -0
- package/src/trails/schema.ts +39 -0
- package/src/trails/surface-overlay-coherence.trail.ts +24 -0
- package/src/trails/{surface-facet-coherence.trail.ts → surface-trailhead-coherence.trail.ts} +5 -5
- package/src/trails/trail-fork-coaching.trail.ts +1 -1
- package/src/trails/trailhead-override-divergence.trail.ts +47 -0
- package/src/trails/wrap-rule.ts +10 -0
- 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.
|
|
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.
|
|
32
|
-
"@ontrails/cli": "^1.0.0-beta.
|
|
33
|
-
"@ontrails/config": "^1.0.0-beta.
|
|
34
|
-
"@ontrails/permits": "^1.0.0-beta.
|
|
35
|
-
"@ontrails/store": "^1.0.0-beta.
|
|
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.
|
|
42
|
+
"@ontrails/testing": "^1.0.0-beta.39"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@ontrails/core": "^1.0.0-beta.
|
|
46
|
-
"@ontrails/topographer": "^1.0.0-beta.
|
|
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
|
|
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 {
|
|
14
|
-
|
|
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 =
|
|
1163
|
-
await loadSourceFiles(rootDir),
|
|
1164
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
?
|
|
1751
|
+
? `Drift: ${staleDriftMessage(drift)}`
|
|
1606
1752
|
: 'Drift: clean';
|
|
1607
1753
|
return [label, ''];
|
|
1608
1754
|
};
|