@ontrails/warden 1.0.0-beta.32 → 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 +56 -0
- package/README.md +1 -1
- package/package.json +9 -9
- package/src/cli.ts +120 -10
- package/src/command.ts +25 -5
- package/src/drift.ts +116 -7
- package/src/fix.ts +8 -2
- package/src/formatters.ts +3 -8
- package/src/index.ts +30 -2
- package/src/project-context.ts +255 -1
- 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/index.ts +43 -3
- package/src/rules/metadata.ts +105 -11
- package/src/rules/no-legacy-cli-alias-export.ts +247 -0
- package/src/rules/no-retired-cross-vocabulary.ts +19 -9
- package/src/rules/public-export-example-coverage.ts +5 -0
- 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} +45 -41
- package/src/rules/trailhead-override-divergence.ts +356 -0
- package/src/rules/types.ts +58 -1
- 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} +4 -4
- package/src/trails/trailhead-override-divergence.trail.ts +47 -0
- package/src/trails/wrap-rule.ts +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
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
|
+
|
|
3
59
|
## 1.0.0-beta.32
|
|
4
60
|
|
|
5
61
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -171,7 +171,7 @@ This is the same factory used internally to build all built-in rule trails.
|
|
|
171
171
|
| --- | --- |
|
|
172
172
|
| `runWarden(options?)` | Run all rules and drift checks, return a report |
|
|
173
173
|
| `formatWardenReport(report)` | Human-readable report |
|
|
174
|
-
| `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 |
|
|
175
175
|
| `wardenRules` | Registry of all built-in rules |
|
|
176
176
|
| `builtinWardenRuleMetadata` | Tier, scope, lifecycle, and invariant metadata for built-in rules |
|
|
177
177
|
| `getWardenRuleMetadata(ruleOrName)` | Resolve inline or built-in metadata for a Warden rule |
|
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 {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
getContourReferences,
|
|
13
|
+
matchesAnyPathGlob,
|
|
14
|
+
resolveSurfaceOverlayBindings,
|
|
15
|
+
surfaceBindingsFromLockOverlays,
|
|
16
|
+
} from '@ontrails/core';
|
|
17
|
+
import type { SurfaceBindings, Topo } from '@ontrails/core';
|
|
13
18
|
import { deriveTopoGraph } from '@ontrails/topographer';
|
|
14
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
TopoGraph,
|
|
21
|
+
TopoGraphOverlayRegistration,
|
|
22
|
+
} from '@ontrails/topographer';
|
|
15
23
|
|
|
16
24
|
import type {
|
|
17
25
|
EffectiveWardenConfig,
|
|
@@ -28,11 +36,12 @@ import { resolveWardenConfig } from './config.js';
|
|
|
28
36
|
import { isDraftMarkedFile } from './draft.js';
|
|
29
37
|
import { applySafeFixesToSource, hasSafeFixEdits } from './fix.js';
|
|
30
38
|
import type { DriftResult } from './drift.js';
|
|
31
|
-
import { checkDrift } from './drift.js';
|
|
39
|
+
import { checkDrift, staleDriftMessage } from './drift.js';
|
|
32
40
|
import { loadProjectWardenRules } from './project-rules.js';
|
|
33
41
|
import type { ProjectWardenRules } from './project-rules.js';
|
|
34
42
|
import {
|
|
35
43
|
collectProjectDocumentationImportResolutions,
|
|
44
|
+
collectProjectExportedSymbolDefinitions,
|
|
36
45
|
collectProjectImportResolutions,
|
|
37
46
|
collectPublicWorkspaces,
|
|
38
47
|
} from './project-context.js';
|
|
@@ -57,10 +66,12 @@ import {
|
|
|
57
66
|
isWardenSourceScanTarget,
|
|
58
67
|
} from './rules/scan.js';
|
|
59
68
|
import type {
|
|
69
|
+
AuthoredMcpSurfaceBindingSet,
|
|
60
70
|
ProjectAwareWardenRule,
|
|
61
71
|
ProjectContext,
|
|
62
72
|
TopoAwareWardenRule,
|
|
63
73
|
WardenDiagnostic,
|
|
74
|
+
WardenExportedSymbolDefinition,
|
|
64
75
|
WardenGuidanceLink,
|
|
65
76
|
WardenRule,
|
|
66
77
|
WardenRuleTier,
|
|
@@ -75,6 +86,12 @@ export interface WardenTopoTarget {
|
|
|
75
86
|
readonly graph?: TopoGraph | undefined;
|
|
76
87
|
/** Stable app/topo label used to tag topo-aware diagnostics. */
|
|
77
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;
|
|
78
95
|
/** Resolved topo module to inspect. */
|
|
79
96
|
readonly topo: Topo;
|
|
80
97
|
}
|
|
@@ -354,6 +371,9 @@ interface SourceFile {
|
|
|
354
371
|
}
|
|
355
372
|
|
|
356
373
|
interface MutableProjectContext {
|
|
374
|
+
authoredMcpSurfaceBindingSets:
|
|
375
|
+
| readonly AuthoredMcpSurfaceBindingSet[]
|
|
376
|
+
| undefined;
|
|
357
377
|
contourReferencesByName: Map<string, Set<string>>;
|
|
358
378
|
crudTableIds: Set<string>;
|
|
359
379
|
composeTargetTrailIds: Set<string>;
|
|
@@ -364,6 +384,10 @@ interface MutableProjectContext {
|
|
|
364
384
|
knownTrailIds: Set<string>;
|
|
365
385
|
topoTrailIds: Set<string>;
|
|
366
386
|
importResolutionsByFile: Map<string, readonly WardenImportResolution[]>;
|
|
387
|
+
exportedSymbolDefinitionsByName: Map<
|
|
388
|
+
string,
|
|
389
|
+
readonly WardenExportedSymbolDefinition[]
|
|
390
|
+
>;
|
|
367
391
|
documentedImportResolutionsByFile: Map<
|
|
368
392
|
string,
|
|
369
393
|
readonly WardenImportResolution[]
|
|
@@ -375,6 +399,7 @@ interface MutableProjectContext {
|
|
|
375
399
|
}
|
|
376
400
|
|
|
377
401
|
const createMutableProjectContext = (): MutableProjectContext => ({
|
|
402
|
+
authoredMcpSurfaceBindingSets: undefined,
|
|
378
403
|
composeTargetTrailIds: new Set<string>(),
|
|
379
404
|
contourReferencesByName: new Map<string, Set<string>>(),
|
|
380
405
|
crudCoverageByEntity: new Map<string, Set<string>>(),
|
|
@@ -383,6 +408,7 @@ const createMutableProjectContext = (): MutableProjectContext => ({
|
|
|
383
408
|
string,
|
|
384
409
|
readonly WardenImportResolution[]
|
|
385
410
|
>(),
|
|
411
|
+
exportedSymbolDefinitionsByName: new Map(),
|
|
386
412
|
importResolutionsByFile: new Map<string, readonly WardenImportResolution[]>(),
|
|
387
413
|
knownContourIds: new Set<string>(),
|
|
388
414
|
knownResourceIds: new Set<string>(),
|
|
@@ -412,6 +438,9 @@ const addContourReferenceTargets = (
|
|
|
412
438
|
};
|
|
413
439
|
|
|
414
440
|
const toProjectContext = (context: MutableProjectContext): ProjectContext => ({
|
|
441
|
+
...(context.authoredMcpSurfaceBindingSets === undefined
|
|
442
|
+
? {}
|
|
443
|
+
: { authoredMcpSurfaceBindingSets: context.authoredMcpSurfaceBindingSets }),
|
|
415
444
|
...(context.contourReferencesByName.size > 0
|
|
416
445
|
? {
|
|
417
446
|
contourReferencesByName: new Map(
|
|
@@ -453,6 +482,12 @@ const toProjectContext = (context: MutableProjectContext): ProjectContext => ({
|
|
|
453
482
|
context.documentedImportResolutionsByFile,
|
|
454
483
|
}
|
|
455
484
|
: {}),
|
|
485
|
+
...(context.exportedSymbolDefinitionsByName.size > 0
|
|
486
|
+
? {
|
|
487
|
+
exportedSymbolDefinitionsByName:
|
|
488
|
+
context.exportedSymbolDefinitionsByName,
|
|
489
|
+
}
|
|
490
|
+
: {}),
|
|
456
491
|
...(context.onTargetSignalIds.size > 0
|
|
457
492
|
? { onTargetSignalIds: context.onTargetSignalIds }
|
|
458
493
|
: {}),
|
|
@@ -849,13 +884,69 @@ const collectFileDocumentedImportResolutions = (
|
|
|
849
884
|
}
|
|
850
885
|
};
|
|
851
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
|
+
|
|
852
939
|
const buildProjectContext = (
|
|
853
940
|
sourceFiles: readonly SourceFile[],
|
|
854
941
|
rootDir: string,
|
|
855
942
|
appTopos: readonly Topo[] = [],
|
|
856
|
-
scope: WardenScope = EMPTY_WARDEN_SCOPE
|
|
943
|
+
scope: WardenScope = EMPTY_WARDEN_SCOPE,
|
|
944
|
+
authoredMcpSurfaceBindingSets:
|
|
945
|
+
| readonly AuthoredMcpSurfaceBindingSet[]
|
|
946
|
+
| undefined = undefined
|
|
857
947
|
): ProjectContext => {
|
|
858
948
|
const context = createMutableProjectContext();
|
|
949
|
+
context.authoredMcpSurfaceBindingSets = authoredMcpSurfaceBindingSets;
|
|
859
950
|
const typeScriptSourceFiles = sourceFiles.filter(
|
|
860
951
|
(sourceFile) => sourceFile.kind === 'typescript'
|
|
861
952
|
);
|
|
@@ -888,6 +979,7 @@ const buildProjectContext = (
|
|
|
888
979
|
documentationSourceFiles,
|
|
889
980
|
context
|
|
890
981
|
);
|
|
982
|
+
collectFileExportedSymbolDefinitions(rootDir, typeScriptSourceFiles, context);
|
|
891
983
|
|
|
892
984
|
return toProjectContext(context);
|
|
893
985
|
};
|
|
@@ -1046,6 +1138,7 @@ const topoRuleFailureDiagnostic = (
|
|
|
1046
1138
|
const lintTopo = async (
|
|
1047
1139
|
appTopo: Topo,
|
|
1048
1140
|
graph: TopoGraph | undefined,
|
|
1141
|
+
overlays: readonly TopoGraphOverlayRegistration[] | undefined,
|
|
1049
1142
|
extraTopoRules: readonly TopoAwareWardenRule[],
|
|
1050
1143
|
selector: WardenRuleSelector
|
|
1051
1144
|
): Promise<readonly WardenDiagnostic[]> => {
|
|
@@ -1056,7 +1149,7 @@ const lintTopo = async (
|
|
|
1056
1149
|
].filter((rule) => isSelectedTopoRule(rule, selector));
|
|
1057
1150
|
let contextGraph: TopoGraph;
|
|
1058
1151
|
try {
|
|
1059
|
-
contextGraph = graph ?? deriveTopoGraph(appTopo);
|
|
1152
|
+
contextGraph = graph ?? deriveTopoGraph(appTopo, { overlays });
|
|
1060
1153
|
} catch (error) {
|
|
1061
1154
|
for (const rule of rules) {
|
|
1062
1155
|
diagnostics.push(topoRuleFailureDiagnostic(rule, error));
|
|
@@ -1141,6 +1234,7 @@ const lintTopoTargets = async (
|
|
|
1141
1234
|
const topoDiagnostics = await lintTopo(
|
|
1142
1235
|
target.topo,
|
|
1143
1236
|
target.graph,
|
|
1237
|
+
target.overlays,
|
|
1144
1238
|
extraTopoRules,
|
|
1145
1239
|
selector
|
|
1146
1240
|
);
|
|
@@ -1200,7 +1294,8 @@ const lintFiles = async (
|
|
|
1200
1294
|
sourceFiles,
|
|
1201
1295
|
rootDir,
|
|
1202
1296
|
topoTargets.map((target) => target.topo),
|
|
1203
|
-
scope
|
|
1297
|
+
scope,
|
|
1298
|
+
collectAuthoredMcpSurfaceBindingSets(topoTargets)
|
|
1204
1299
|
);
|
|
1205
1300
|
const allDiagnostics: WardenDiagnostic[] = [
|
|
1206
1301
|
...lintSourceFiles(sourceFiles, context, extraSourceRules, selector),
|
|
@@ -1278,11 +1373,18 @@ const checkDriftForTopoTargets = async (
|
|
|
1278
1373
|
topoTargets: readonly WardenTopoTarget[]
|
|
1279
1374
|
): Promise<DriftResult> => {
|
|
1280
1375
|
if (topoTargets.length <= 1) {
|
|
1281
|
-
|
|
1376
|
+
const [target] = topoTargets;
|
|
1377
|
+
return checkDrift(
|
|
1378
|
+
rootDir,
|
|
1379
|
+
target?.topo,
|
|
1380
|
+
target === undefined ? undefined : { overlays: target.overlays }
|
|
1381
|
+
);
|
|
1282
1382
|
}
|
|
1283
1383
|
|
|
1284
1384
|
const driftResults = await Promise.all(
|
|
1285
|
-
topoTargets.map((target) =>
|
|
1385
|
+
topoTargets.map((target) =>
|
|
1386
|
+
checkDrift(rootDir, target.topo, { overlays: target.overlays })
|
|
1387
|
+
)
|
|
1286
1388
|
);
|
|
1287
1389
|
const committedHashes = new Set(
|
|
1288
1390
|
driftResults.map((result) => result.committedHash)
|
|
@@ -1316,9 +1418,17 @@ const checkDriftForTopoTargets = async (
|
|
|
1316
1418
|
}
|
|
1317
1419
|
|
|
1318
1420
|
const currentHash = aggregateDriftHash(topoTargets, driftResults);
|
|
1421
|
+
const driftedOverlayNamespaces = [
|
|
1422
|
+
...new Set(
|
|
1423
|
+
driftResults.flatMap((result) => result.driftedOverlayNamespaces ?? [])
|
|
1424
|
+
),
|
|
1425
|
+
].toSorted();
|
|
1319
1426
|
return {
|
|
1320
1427
|
committedHash,
|
|
1321
1428
|
currentHash,
|
|
1429
|
+
...(driftedOverlayNamespaces.length === 0
|
|
1430
|
+
? {}
|
|
1431
|
+
: { driftedOverlayNamespaces }),
|
|
1322
1432
|
stale: committedHash !== null && committedHash !== currentHash,
|
|
1323
1433
|
};
|
|
1324
1434
|
};
|
|
@@ -1638,7 +1748,7 @@ const formatDriftSection = (drift: DriftResult | null): string[] => {
|
|
|
1638
1748
|
return [`Drift: blocked (${drift.blockedReason})`, ''];
|
|
1639
1749
|
}
|
|
1640
1750
|
const label = drift.stale
|
|
1641
|
-
?
|
|
1751
|
+
? `Drift: ${staleDriftMessage(drift)}`
|
|
1642
1752
|
: 'Drift: clean';
|
|
1643
1753
|
return [label, ''];
|
|
1644
1754
|
};
|
package/src/command.ts
CHANGED
|
@@ -12,8 +12,10 @@ import type {
|
|
|
12
12
|
CliFlagValueAlias,
|
|
13
13
|
CliFlagValueAliasDeclaration,
|
|
14
14
|
} from '@ontrails/cli';
|
|
15
|
+
import { resolveTrailsOverlays } from '@ontrails/adapter-kit';
|
|
15
16
|
import type { Topo } from '@ontrails/core';
|
|
16
17
|
import { AmbiguousError, NotFoundError } from '@ontrails/core';
|
|
18
|
+
import type { TopoGraphOverlayRegistration } from '@ontrails/topographer';
|
|
17
19
|
import {
|
|
18
20
|
loadTrailsConfigValue,
|
|
19
21
|
resolveTrailsProjectRoot,
|
|
@@ -682,12 +684,22 @@ const resolveNamedAppModulePath = (
|
|
|
682
684
|
: resolveFilesystemModulePath(rootDir, matched);
|
|
683
685
|
};
|
|
684
686
|
|
|
685
|
-
|
|
687
|
+
interface LoadedWardenTopoModule {
|
|
688
|
+
/** App-module overlay registrations read through the shared channel. */
|
|
689
|
+
readonly overlays?: readonly TopoGraphOverlayRegistration[] | undefined;
|
|
690
|
+
readonly topo: Topo;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const importTopoFromModulePath = async (
|
|
694
|
+
modulePath: string
|
|
695
|
+
): Promise<LoadedWardenTopoModule> => {
|
|
686
696
|
const loaded = (await import(pathToFileURL(modulePath).href)) as Record<
|
|
687
697
|
string,
|
|
688
698
|
unknown
|
|
689
699
|
>;
|
|
690
|
-
|
|
700
|
+
const topo = extractTopo(modulePath, loaded);
|
|
701
|
+
const overlays = resolveTrailsOverlays(loaded, modulePath);
|
|
702
|
+
return overlays === undefined ? { topo } : { overlays, topo };
|
|
691
703
|
};
|
|
692
704
|
|
|
693
705
|
const topoLoadDiagnostic = ({
|
|
@@ -759,9 +771,11 @@ export const resolveWardenTopoTargets = async ({
|
|
|
759
771
|
for (const appName of apps) {
|
|
760
772
|
try {
|
|
761
773
|
const modulePath = resolveNamedAppModulePath(rootDir, appName);
|
|
774
|
+
const loaded = await importTopoFromModulePath(modulePath);
|
|
762
775
|
topos.push({
|
|
763
776
|
name: appName,
|
|
764
|
-
|
|
777
|
+
overlays: loaded.overlays,
|
|
778
|
+
topo: loaded.topo,
|
|
765
779
|
});
|
|
766
780
|
} catch (error) {
|
|
767
781
|
diagnostics.push(
|
|
@@ -778,10 +792,16 @@ export const resolveWardenTopoTargets = async ({
|
|
|
778
792
|
|
|
779
793
|
try {
|
|
780
794
|
const modulePath = resolveDiscoveredModulePath(rootDir);
|
|
781
|
-
const
|
|
795
|
+
const loaded = await importTopoFromModulePath(modulePath);
|
|
782
796
|
return {
|
|
783
797
|
diagnostics,
|
|
784
|
-
topos: [
|
|
798
|
+
topos: [
|
|
799
|
+
{
|
|
800
|
+
name: loaded.topo.name,
|
|
801
|
+
overlays: loaded.overlays,
|
|
802
|
+
topo: loaded.topo,
|
|
803
|
+
},
|
|
804
|
+
],
|
|
785
805
|
};
|
|
786
806
|
} catch (error) {
|
|
787
807
|
if (error instanceof NotFoundError) {
|
package/src/drift.ts
CHANGED
|
@@ -15,14 +15,33 @@ import {
|
|
|
15
15
|
ValidationError,
|
|
16
16
|
} from '@ontrails/core';
|
|
17
17
|
import {
|
|
18
|
+
collectTopoGraphOverlays,
|
|
18
19
|
createTopoStore,
|
|
19
20
|
deriveTopoGraph,
|
|
20
21
|
deriveTopoGraphHash,
|
|
21
22
|
isTopoArtifactRegenerationError,
|
|
22
23
|
readLockManifest,
|
|
24
|
+
readTopoGraph,
|
|
23
25
|
readTrailsLock,
|
|
24
26
|
} from '@ontrails/topographer';
|
|
25
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
DeriveTopoGraphOptions,
|
|
29
|
+
LockManifest,
|
|
30
|
+
TopoGraphOverlays,
|
|
31
|
+
} from '@ontrails/topographer';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Derive options `checkDrift` accepts so the fresh comparison graph carries
|
|
35
|
+
* the same app-module overlays the committed lock embeds.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* import type { CheckDriftOptions } from '@ontrails/warden';
|
|
40
|
+
*
|
|
41
|
+
* const options: CheckDriftOptions = { overlays: lease.overlays };
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export type CheckDriftOptions = Pick<DeriveTopoGraphOptions, 'overlays'>;
|
|
26
45
|
|
|
27
46
|
/**
|
|
28
47
|
* Result of a drift check comparing committed trails.lock against the current state.
|
|
@@ -36,6 +55,12 @@ export interface DriftResult {
|
|
|
36
55
|
readonly committedHash: string | null;
|
|
37
56
|
/** Hash computed from the current trail topology */
|
|
38
57
|
readonly currentHash: string;
|
|
58
|
+
/**
|
|
59
|
+
* Overlay namespaces whose committed facts differ from the freshly derived
|
|
60
|
+
* facts, sorted lexicographically. Present only when the lock is stale and
|
|
61
|
+
* the caller supplied a topo plus derive options.
|
|
62
|
+
*/
|
|
63
|
+
readonly driftedOverlayNamespaces?: readonly string[] | undefined;
|
|
39
64
|
}
|
|
40
65
|
|
|
41
66
|
interface BlockedLockRead {
|
|
@@ -91,14 +116,89 @@ const isBlockedLockRead = (
|
|
|
91
116
|
): result is BlockedLockRead =>
|
|
92
117
|
result !== null && 'kind' in result && result.kind === 'blocked-lock-read';
|
|
93
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Read the committed lock's embedded graph overlays, tolerating both the v4
|
|
121
|
+
* root `trails.lock` envelope and the legacy `.trails/` artifact layout.
|
|
122
|
+
*/
|
|
123
|
+
const readCommittedGraphOverlays = async (
|
|
124
|
+
rootDir: string
|
|
125
|
+
): Promise<TopoGraphOverlays | undefined> => {
|
|
126
|
+
const committedGraph =
|
|
127
|
+
(await readTopoGraph({ dir: rootDir })) ??
|
|
128
|
+
(await readTopoGraph({ dir: deriveTrailsDir({ rootDir }) }));
|
|
129
|
+
return committedGraph?.overlays;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Name the overlay namespaces whose committed facts drifted from the freshly
|
|
134
|
+
* derived facts. Compares canonical JSON per namespace across the union of
|
|
135
|
+
* committed and current namespaces; returns a sorted list.
|
|
136
|
+
*/
|
|
137
|
+
const collectDriftedOverlayNamespaces = async (
|
|
138
|
+
rootDir: string,
|
|
139
|
+
topo: Topo,
|
|
140
|
+
options: CheckDriftOptions
|
|
141
|
+
): Promise<readonly string[]> => {
|
|
142
|
+
let committed: TopoGraphOverlays | undefined;
|
|
143
|
+
try {
|
|
144
|
+
committed = await readCommittedGraphOverlays(rootDir);
|
|
145
|
+
} catch {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
const current = collectTopoGraphOverlays(topo, options.overlays);
|
|
149
|
+
const namespaces = new Set([
|
|
150
|
+
...Object.keys(committed ?? {}),
|
|
151
|
+
...Object.keys(current ?? {}),
|
|
152
|
+
]);
|
|
153
|
+
return [...namespaces]
|
|
154
|
+
.filter(
|
|
155
|
+
(namespace) =>
|
|
156
|
+
JSON.stringify(committed?.[namespace]) !==
|
|
157
|
+
JSON.stringify(current?.[namespace])
|
|
158
|
+
)
|
|
159
|
+
.toSorted();
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Format a stale drift result into one human-readable sentence.
|
|
164
|
+
*
|
|
165
|
+
* Names the drifted overlay namespaces when the drift check identified them,
|
|
166
|
+
* and always points at `trails compile` as the remediation.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```ts
|
|
170
|
+
* import { staleDriftMessage } from './drift.js';
|
|
171
|
+
*
|
|
172
|
+
* staleDriftMessage({
|
|
173
|
+
* committedHash: 'aaa',
|
|
174
|
+
* currentHash: 'bbb',
|
|
175
|
+
* driftedOverlayNamespaces: ['surfaces'],
|
|
176
|
+
* stale: true,
|
|
177
|
+
* });
|
|
178
|
+
* // => 'trails.lock is stale — drifted overlay namespaces: surfaces (regenerate with `trails compile`)'
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
export const staleDriftMessage = (drift: DriftResult): string => {
|
|
182
|
+
const namespaces = drift.driftedOverlayNamespaces;
|
|
183
|
+
const detail =
|
|
184
|
+
namespaces !== undefined && namespaces.length > 0
|
|
185
|
+
? ` — drifted overlay namespaces: ${namespaces.join(', ')}`
|
|
186
|
+
: '';
|
|
187
|
+
return `trails.lock is stale${detail} (regenerate with \`trails compile\`)`;
|
|
188
|
+
};
|
|
189
|
+
|
|
94
190
|
/**
|
|
95
191
|
* Check whether the committed trails.lock is stale compared to the current topology.
|
|
96
192
|
*
|
|
97
193
|
* When no topo is provided, returns a clean result (no drift detectable without runtime info).
|
|
194
|
+
* When a topo is provided, `options.overlays` carries the app-module overlay
|
|
195
|
+
* registrations so the fresh comparison graph embeds the same namespaced
|
|
196
|
+
* facts the compile path writes into the committed lock.
|
|
98
197
|
*/
|
|
99
198
|
export const checkDrift = async (
|
|
100
199
|
rootDir: string,
|
|
101
|
-
topo?: Topo | undefined
|
|
200
|
+
topo?: Topo | undefined,
|
|
201
|
+
options?: CheckDriftOptions | undefined
|
|
102
202
|
): Promise<DriftResult> => {
|
|
103
203
|
try {
|
|
104
204
|
const lockManifest = await readCommittedLockManifest(rootDir);
|
|
@@ -127,15 +227,24 @@ export const checkDrift = async (
|
|
|
127
227
|
const currentHash =
|
|
128
228
|
topo === undefined
|
|
129
229
|
? (readStoredHash() ?? 'unknown')
|
|
130
|
-
: deriveTopoGraphHash(deriveTopoGraph(topo));
|
|
230
|
+
: deriveTopoGraphHash(deriveTopoGraph(topo, options));
|
|
231
|
+
const stale =
|
|
232
|
+
topoArtifact !== null &&
|
|
233
|
+
currentHash !== 'unknown' &&
|
|
234
|
+
topoArtifact.sha256 !== currentHash;
|
|
235
|
+
const driftedOverlayNamespaces =
|
|
236
|
+
stale && topo !== undefined && options !== undefined
|
|
237
|
+
? await collectDriftedOverlayNamespaces(rootDir, topo, options)
|
|
238
|
+
: undefined;
|
|
131
239
|
|
|
132
240
|
return {
|
|
133
241
|
committedHash: topoArtifact?.sha256 ?? null,
|
|
134
242
|
currentHash,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
243
|
+
...(driftedOverlayNamespaces === undefined ||
|
|
244
|
+
driftedOverlayNamespaces.length === 0
|
|
245
|
+
? {}
|
|
246
|
+
: { driftedOverlayNamespaces }),
|
|
247
|
+
stale,
|
|
139
248
|
};
|
|
140
249
|
} catch (error) {
|
|
141
250
|
if (
|
package/src/fix.ts
CHANGED
|
@@ -95,16 +95,22 @@ export const applySafeFixesToSource = (
|
|
|
95
95
|
const applied: WardenDiagnostic[] = [];
|
|
96
96
|
const skipped: WardenDiagnostic[] = [];
|
|
97
97
|
const edits: ResolvedEdit[] = [];
|
|
98
|
+
const seenEdits = new Set<string>();
|
|
98
99
|
|
|
99
100
|
for (const diagnostic of diagnostics) {
|
|
100
101
|
if (hasSafeFixEdits(diagnostic)) {
|
|
101
102
|
applied.push(diagnostic);
|
|
102
103
|
for (const edit of diagnostic.fix.edits) {
|
|
103
|
-
|
|
104
|
+
const resolvedEdit = {
|
|
104
105
|
end: edit.end,
|
|
105
106
|
replacement: edit.replacement,
|
|
106
107
|
start: edit.start,
|
|
107
|
-
}
|
|
108
|
+
};
|
|
109
|
+
const key = `${String(resolvedEdit.start)}\0${String(resolvedEdit.end)}\0${resolvedEdit.replacement}`;
|
|
110
|
+
if (!seenEdits.has(key)) {
|
|
111
|
+
seenEdits.add(key);
|
|
112
|
+
edits.push(resolvedEdit);
|
|
113
|
+
}
|
|
108
114
|
}
|
|
109
115
|
} else {
|
|
110
116
|
skipped.push(diagnostic);
|