@ontrails/warden 1.0.0-beta.24 → 1.0.0-beta.29

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 (61) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +35 -1
  3. package/package.json +10 -9
  4. package/src/ast.ts +109 -0
  5. package/src/cli.ts +44 -4
  6. package/src/command.ts +24 -34
  7. package/src/drift.ts +24 -8
  8. package/src/index.ts +9 -0
  9. package/src/project-rules.ts +290 -0
  10. package/src/resolve.ts +8 -7
  11. package/src/rules/ast.ts +837 -6
  12. package/src/rules/cli-command-route-coherence.ts +177 -0
  13. package/src/rules/composes-declarations.ts +210 -77
  14. package/src/rules/context-no-surface-types.ts +15 -12
  15. package/src/rules/contour-exists.ts +7 -7
  16. package/src/rules/dead-internal-trail.ts +11 -4
  17. package/src/rules/dead-public-trail.ts +258 -0
  18. package/src/rules/duplicate-public-contract.ts +91 -0
  19. package/src/rules/error-mapping-completeness.ts +5 -3
  20. package/src/rules/example-valid.ts +6 -12
  21. package/src/rules/fires-declarations.ts +41 -64
  22. package/src/rules/implementation-returns-result.ts +208 -76
  23. package/src/rules/incomplete-crud.ts +20 -19
  24. package/src/rules/index.ts +15 -0
  25. package/src/rules/layer-field-name-drift.ts +12 -6
  26. package/src/rules/library-projection-coherence.ts +100 -0
  27. package/src/rules/metadata.ts +102 -1
  28. package/src/rules/no-destructured-compose.ts +16 -14
  29. package/src/rules/no-native-error-result.ts +15 -8
  30. package/src/rules/no-redundant-result-error-wrap.ts +82 -29
  31. package/src/rules/no-sync-result-assumption.ts +70 -68
  32. package/src/rules/no-top-level-surface.ts +46 -64
  33. package/src/rules/on-references-exist.ts +1 -1
  34. package/src/rules/owner-projection-parity.ts +10 -13
  35. package/src/rules/public-export-example-coverage.ts +29 -20
  36. package/src/rules/public-internal-deep-imports.ts +2 -2
  37. package/src/rules/read-intent-fires.ts +8 -8
  38. package/src/rules/registry-names.ts +10 -0
  39. package/src/rules/resource-declarations.ts +20 -31
  40. package/src/rules/resource-exists.ts +2 -2
  41. package/src/rules/resource-id-grammar.ts +2 -2
  42. package/src/rules/resource-mock-coverage.ts +2 -2
  43. package/src/rules/static-resource-accessor-preference.ts +26 -34
  44. package/src/rules/surface-facet-coherence.ts +21 -29
  45. package/src/rules/trail-fork-coaching.ts +616 -0
  46. package/src/rules/trail-versioning-source.ts +56 -78
  47. package/src/rules/types.ts +2 -0
  48. package/src/rules/unreachable-detour-shadowing.ts +16 -21
  49. package/src/rules/valid-describe-refs.ts +14 -12
  50. package/src/rules/warden-export-symmetry.ts +42 -35
  51. package/src/rules/warden-rules-use-ast.ts +168 -50
  52. package/src/trails/cli-command-route-coherence.trail.ts +47 -0
  53. package/src/trails/dead-public-trail.trail.ts +31 -0
  54. package/src/trails/duplicate-public-contract.trail.ts +47 -0
  55. package/src/trails/error-mapping-completeness.trail.ts +1 -0
  56. package/src/trails/index.ts +5 -0
  57. package/src/trails/library-projection-coherence.trail.ts +43 -0
  58. package/src/trails/schema.ts +4 -0
  59. package/src/trails/trail-fork-coaching.trail.ts +42 -0
  60. package/src/trails/warden-rules-use-ast.trail.ts +19 -0
  61. package/src/trails/wrap-rule.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,107 @@
1
1
  # @ontrails/warden
2
2
 
3
+ ## 1.0.0-beta.29
4
+
5
+ ### Patch Changes
6
+
7
+ - @ontrails/adapter-kit@1.0.0-beta.29
8
+ - @ontrails/cli@1.0.0-beta.29
9
+ - @ontrails/config@1.0.0-beta.29
10
+ - @ontrails/core@1.0.0-beta.29
11
+ - @ontrails/permits@1.0.0-beta.29
12
+ - @ontrails/store@1.0.0-beta.29
13
+ - @ontrails/topographer@1.0.0-beta.29
14
+
15
+ ## 1.0.0-beta.28
16
+
17
+ ### Patch Changes
18
+
19
+ - @ontrails/adapter-kit@1.0.0-beta.28
20
+ - @ontrails/cli@1.0.0-beta.28
21
+ - @ontrails/config@1.0.0-beta.28
22
+ - @ontrails/core@1.0.0-beta.28
23
+ - @ontrails/permits@1.0.0-beta.28
24
+ - @ontrails/store@1.0.0-beta.28
25
+ - @ontrails/topographer@1.0.0-beta.28
26
+
27
+ ## 1.0.0-beta.27
28
+
29
+ ### Patch Changes
30
+
31
+ - @ontrails/adapter-kit@1.0.0-beta.27
32
+ - @ontrails/cli@1.0.0-beta.27
33
+ - @ontrails/config@1.0.0-beta.27
34
+ - @ontrails/core@1.0.0-beta.27
35
+ - @ontrails/permits@1.0.0-beta.27
36
+ - @ontrails/store@1.0.0-beta.27
37
+ - @ontrails/topographer@1.0.0-beta.27
38
+
39
+ ## 1.0.0-beta.26
40
+
41
+ ### Patch Changes
42
+
43
+ - 1307568: Centralize Trails config module path conventions, move local config overrides to root `trails.config.local.*`, scaffold the matching gitignore entries, and load project-local Warden rules from `.trails/rules.ts` or `.trails/rules/`.
44
+ - ef09e46: Add shared Trails project-root discovery helpers and use them in Warden so nested
45
+ cwd invocations still load root `trails.config.*` and project-local
46
+ `.trails/rules*` governance.
47
+ - 38cd9d6: Add a shared Trails config file loader that treats `trails.config.ts` as the natural primary while supporting JSON, JSONC, YAML, and TOML peer formats. Release and Warden config loading now consume the same loader and local overrides can be authored as data files.
48
+ - f8403c4: Collapse normal topo compilation onto one root `trails.lock` envelope that embeds the TopoGraph, hash, and summary while keeping legacy `.trails/trails.lock` plus `.trails/topo.lock` readers for migration compatibility.
49
+ - ff48e41: Harden project-local rule loading: Warden now discovers `.trails/rules.ts` and direct `.trails/rules/*.ts` files only, reports duplicate project-local rule ids, and emits a migration diagnostic for the retired `trails/warden/rules` location.
50
+ - Updated dependencies [1307568]
51
+ - Updated dependencies [ef09e46]
52
+ - Updated dependencies [38cd9d6]
53
+ - Updated dependencies [f8403c4]
54
+ - Updated dependencies [371d19e]
55
+ - @ontrails/config@1.0.0-beta.26
56
+ - @ontrails/core@1.0.0-beta.26
57
+ - @ontrails/topographer@1.0.0-beta.26
58
+ - @ontrails/adapter-kit@1.0.0-beta.26
59
+ - @ontrails/cli@1.0.0-beta.26
60
+ - @ontrails/permits@1.0.0-beta.26
61
+ - @ontrails/store@1.0.0-beta.26
62
+
63
+ ## 1.0.0-beta.25
64
+
65
+ ### Patch Changes
66
+
67
+ - a9fdbc7: Clarify surface accommodation doctrine in MCP surface facet metadata and Warden trail-fork coaching guidance.
68
+ - f8fd6ca: Add OXC Walker-backed AST facade helpers for parent-aware traversal, scope-aware traversal, source locations, and safe source edits.
69
+ - 0fcc42b: Add `dead-public-trail` and `duplicate-public-contract` Warden coaching rules so exported public trails stay anchored and duplicate surface contracts become visible drift.
70
+ - c36aca9: Preserve existing Result error boundaries directly and widen Warden pass-through
71
+ coaching beyond trail blazes.
72
+ - f556559: Adds curated typed AST node guards to the public `@ontrails/warden/ast` helper
73
+ surface so source consumers can narrow common OXC node shapes without assertion
74
+ casts.
75
+ - 6250729: Expands the public AST guard/accessor surface and migrates Warden/Regrade AST
76
+ consumers onto the typed helpers instead of rule-local node-field casts.
77
+ - d73c38e: Warn when Warden rules add raw AST node-field casts where a typed accessor exists.
78
+ - a8e4dc3: Clean up the Wayfinder navigation grammar before RC, including explicit pattern/query/file selectors, target-bound dependency and impact flags, drift-first provenance fields, stricter fires declaration diagnostics, and updated operator dogfood coverage.
79
+ - a4f9cf6: Reserve the `shift` error category and `WorkspaceShiftError` before the stable
80
+ cutover so surface mappings can distinguish moved-workspace retry verdicts.
81
+ Update Warden's error-mapping completeness examples to cover the reserved
82
+ category.
83
+ - 00c0cf8: Add Warden governance for CLI command route and alias coherence.
84
+ - b313c58: Add library projection coherence governance so Warden reports generated library export collisions and stale projection targets before packages materialize.
85
+ - f245fa0: Add advisory trail-fork coaching so Warden can warn when a trail may be hiding
86
+ several capabilities behind one branching action or operation input.
87
+ - f1e6efa: Recognize module-local helper functions that receive the trail context when checking declared compose usage.
88
+ - caff950: `implementation-returns-result` now recognizes conditional returns whose branches are all recognized Result expressions — both `return cond ? Result.err(...) : Result.ok(...)` statements (including branches that are Result helpers or Result-bound variables) and concise ternary blaze bodies. Previously the idiomatic two-branch ternary was flagged as an error.
89
+ - df13faf: Tighten Wayfinder navigation review findings: carry adapter filters into included adapter facts, keep live-source outline off the operator MCP surface until host-root binding exists, make unknown MCP resources protocol errors, route secondary Wayfinder graph populations through unified flags, and keep undeclared string `ctx.compose()` calls as Warden errors.
90
+ - Updated dependencies [c36aca9]
91
+ - Updated dependencies [3befcf1]
92
+ - Updated dependencies [f1e6efa]
93
+ - Updated dependencies [a4f9cf6]
94
+ - Updated dependencies [9bcf34e]
95
+ - Updated dependencies [f7d97fc]
96
+ - Updated dependencies [59d10da]
97
+ - Updated dependencies [d9c6e50]
98
+ - @ontrails/core@1.0.0-beta.25
99
+ - @ontrails/topographer@1.0.0-beta.25
100
+ - @ontrails/cli@1.0.0-beta.25
101
+ - @ontrails/adapter-kit@1.0.0-beta.25
102
+ - @ontrails/permits@1.0.0-beta.25
103
+ - @ontrails/store@1.0.0-beta.25
104
+
3
105
  ## 1.0.0-beta.24
4
106
 
5
107
  ### Patch Changes
package/README.md CHANGED
@@ -37,6 +37,39 @@ Rules cover several families:
37
37
 
38
38
  When adding or auditing rules, follow [Warden Rules](../../docs/contributing/warden-rules.md): name the invariant, import owner-held framework data, choose the narrowest Warden tier, and collapse families only when the data model, traversal, and diagnostic shape are shared.
39
39
 
40
+ ## Project-local rules
41
+
42
+ Projects can carry local Warden rules in `.trails/rules.ts` or direct `.trails/rules/*.ts` files. `runWarden()` and `trails warden` load those files by default for lint runs, then run those rules alongside the built-in registries. Drift-only runs do not import project-local rule modules. Embedders that need only built-in or explicitly provided rules can pass `projectRules: false`.
43
+
44
+ Warden does not recursively discover nested `.trails/rules` files. Use nested files as private helpers and re-export from a direct entrypoint when a local rule grows. Rule ids must be unique across every project-local module. The retired `trails/warden/rules` location reports a migration diagnostic instead of loading.
45
+
46
+ Warden uses the shared Trails project-root resolver when a caller does not pass `rootDir` or `--root-dir`, so commands launched from nested directories still load the nearest root `trails.config.*` and its `.trails/rules*` files. An explicit root always wins over discovery.
47
+
48
+ This is the right home for repo-specific migration checks or governance that has not earned a place in `@ontrails/warden` itself.
49
+
50
+ 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
+
52
+ ```typescript
53
+ export const rule = {
54
+ name: 'local-contract-check',
55
+ severity: 'error',
56
+ description: 'Local contract examples keep their migration marker.',
57
+ check(sourceCode, filePath) {
58
+ return sourceCode.includes('deprecatedMarker')
59
+ ? [
60
+ {
61
+ filePath,
62
+ line: 1,
63
+ message: 'Replace deprecatedMarker before release.',
64
+ rule: 'local-contract-check',
65
+ severity: 'error',
66
+ },
67
+ ]
68
+ : [];
69
+ },
70
+ };
71
+ ```
72
+
40
73
  ## Drift detection
41
74
 
42
75
  Warden integrates with `@ontrails/topographer` to detect when the topo has changed without updating the lock file:
@@ -124,12 +157,13 @@ This is the same factory used internally to build all built-in rule trails.
124
157
  | `wardenTopo` | `Topo` of all built-in rule trails (one per rule) |
125
158
  | `runWardenTrails(filePath, sourceCode, options?)` | Dispatch file-scoped rule trails for a file, collect diagnostics |
126
159
  | `runTopoAwareWardenTrails(topo)` | Dispatch built-in topo-aware rule trails once for a resolved topo |
160
+ | `loadProjectWardenRules(rootDir)` | Load rule modules from `.trails/rules.ts` or direct `.trails/rules/*.ts` children |
127
161
  | `formatGitHubAnnotations(report)` | GitHub Actions annotation format |
128
162
  | `formatJson(report)` | Machine-readable JSON |
129
163
  | `formatSummary(report)` | Compact summary line |
130
164
  | `wrapRule(rule)` | Wrap a custom rule as a trail (same factory used for all built-in rule trails) |
131
165
 
132
- AST parser helpers are exported from `@ontrails/warden/ast`, not the root runtime barrel. The stable authoring surface includes `parse`, `walk`, `walkScope`, `offsetToLine`, `findTrailDefinitions`, `findBlazeBodies`, `findContourDefinitions`, `isBlazeCall`, and string-literal helpers.
166
+ AST parser helpers are exported from `@ontrails/warden/ast`, not the root runtime barrel. The stable authoring surface includes `parse`, `walk`, `walkScope`, `walkWithParents`, `walkWithScopeContext`, `offsetToLine`, `offsetToLineColumn`, source-edit helpers, `findTrailDefinitions`, `findBlazeBodies`, `findContourDefinitions`, `isBlazeCall`, and string-literal helpers.
133
167
 
134
168
  `runWarden({ tier })` can narrow a run to `source-static`, `project-static`, `topo-aware`, `drift`, or `advisory`. Omit `tier` for the default full run.
135
169
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/warden",
3
- "version": "1.0.0-beta.24",
3
+ "version": "1.0.0-beta.29",
4
4
  "bin": {
5
5
  "warden": "./bin/warden.ts"
6
6
  },
@@ -28,20 +28,21 @@
28
28
  "clean": "rm -rf dist *.tsbuildinfo"
29
29
  },
30
30
  "dependencies": {
31
- "@ontrails/adapter-kit": "^1.0.0-beta.24",
32
- "@ontrails/cli": "^1.0.0-beta.24",
33
- "@ontrails/permits": "^1.0.0-beta.24",
34
- "@ontrails/store": "^1.0.0-beta.24",
31
+ "@ontrails/adapter-kit": "^1.0.0-beta.29",
32
+ "@ontrails/cli": "^1.0.0-beta.29",
33
+ "@ontrails/config": "^1.0.0-beta.29",
34
+ "@ontrails/permits": "^1.0.0-beta.29",
35
+ "@ontrails/store": "^1.0.0-beta.29",
35
36
  "oxc-parser": "^0.121.0",
36
37
  "oxc-resolver": "11.19.1",
38
+ "oxc-walker": "^1.0.0",
37
39
  "zod": "^4.3.5"
38
40
  },
39
41
  "devDependencies": {
40
- "@ontrails/config": "^1.0.0-beta.24",
41
- "@ontrails/testing": "^1.0.0-beta.24"
42
+ "@ontrails/testing": "^1.0.0-beta.29"
42
43
  },
43
44
  "peerDependencies": {
44
- "@ontrails/core": "^1.0.0-beta.24",
45
- "@ontrails/topographer": "^1.0.0-beta.24"
45
+ "@ontrails/core": "^1.0.0-beta.29",
46
+ "@ontrails/topographer": "^1.0.0-beta.29"
46
47
  }
47
48
  }
package/src/ast.ts CHANGED
@@ -10,19 +10,128 @@ export {
10
10
  findContourDefinitions,
11
11
  findStringLiterals,
12
12
  findTrailDefinitions,
13
+ applySourceEdits,
14
+ createSourceEdit,
15
+ getNodeAlternate,
16
+ getNodeArgument,
17
+ getNodeArguments,
18
+ getNodeBody,
19
+ getNodeBodyNode,
20
+ getNodeBodyStatements,
21
+ getNodeCallee,
22
+ getNodeCases,
23
+ getNodeComputed,
24
+ getNodeConsequent,
25
+ getNodeDeclaration,
26
+ getNodeDeclarations,
27
+ getNodeDiscriminant,
28
+ getNodeElements,
29
+ getNodeExported,
30
+ getNodeExportKind,
31
+ getNodeExpression,
32
+ getNodeId,
33
+ getNodeImported,
34
+ getNodeInit,
35
+ getNodeKey,
36
+ getNodeKind,
37
+ getNodeLeft,
38
+ getNodeLocal,
39
+ getNodeName,
40
+ getNodeObject,
41
+ getNodeOperator,
42
+ getNodeParam,
43
+ getNodeParams,
44
+ getNodeProperties,
45
+ getNodeProperty,
46
+ getNodeReturnType,
47
+ getNodeRight,
48
+ getNodeSource,
49
+ getNodeSpecifiers,
50
+ getNodeSuperClass,
51
+ getNodeTest,
52
+ getNodeTypeAnnotation,
53
+ getNodeValue,
54
+ getNodeValueNode,
13
55
  getStringValue,
56
+ identifierName,
57
+ isArrayExpression,
58
+ isAssignmentPattern,
59
+ isAstNode,
60
+ isBinaryExpression,
61
+ isBlockStatement,
14
62
  isBlazeCall,
63
+ isCallExpression,
64
+ isClassMember,
65
+ isDeclarationWithId,
66
+ isExportAllDeclaration,
67
+ isExportDeclaration,
68
+ isExportDefaultDeclaration,
69
+ isExportNamedDeclaration,
70
+ isExportSpecifier,
71
+ isExpressionStatement,
72
+ isFunctionLike,
73
+ isIdentifier,
74
+ isImportDeclaration,
75
+ isImportSpecifier,
76
+ isMemberExpression,
77
+ isObjectExpression,
78
+ isProgram,
79
+ isProperty,
80
+ isRestElement,
81
+ isReturnStatement,
15
82
  isStringLiteral,
83
+ isUnaryExpression,
84
+ isVariableDeclaration,
85
+ isVariableDeclarator,
86
+ offsetToLineColumn,
16
87
  offsetToLine,
17
88
  parse,
89
+ parseWithDiagnostics,
90
+ validateSourceEdits,
18
91
  walk,
92
+ walkWithParents,
93
+ walkWithScopeContext,
19
94
  walkScope,
20
95
  } from './rules/ast.js';
21
96
  export type {
97
+ ArrayExpressionNode,
98
+ AssignmentPatternNode,
99
+ AstFieldProjection,
100
+ AstParentContext,
22
101
  AstNode,
102
+ AstParseDiagnostic,
103
+ AstParseDiagnosticLabel,
104
+ AstParseResult,
105
+ AstScopeContext,
106
+ AstScopeDeclaration,
107
+ BinaryExpressionNode,
108
+ BlockStatementNode,
109
+ CallExpressionNode,
110
+ ClassMemberNode,
23
111
  ContourDefinition,
112
+ CuratedAstNode,
113
+ DeclarationWithIdNode,
114
+ ExportDeclarationNode,
115
+ ExportSpecifierNode,
116
+ ExpressionStatementNode,
24
117
  FindContourDefinitionsOptions,
25
118
  FrameworkNamespaceContext,
119
+ FunctionLikeNode,
120
+ IdentifierNode,
121
+ ImportDeclarationNode,
122
+ ImportSpecifierNode,
123
+ MemberExpressionNode,
124
+ ObjectExpressionNode,
125
+ ProgramNode,
126
+ PropertyNode,
127
+ RestElementNode,
128
+ ReturnStatementNode,
129
+ SourceEdit,
130
+ SourceLocation,
26
131
  StringLiteralMatch,
132
+ StringLiteralNode,
27
133
  TrailDefinition,
134
+ UnaryExpressionNode,
135
+ VariableDeclarationNode,
136
+ VariableDeclaratorNode,
28
137
  } from './rules/ast.js';
package/src/cli.ts CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { isAbsolute, relative, resolve } from 'node:path';
9
9
 
10
+ import { resolveTrailsProjectRoot } from '@ontrails/config';
10
11
  import type { Topo } from '@ontrails/core';
11
12
  import { deriveTopoGraph } from '@ontrails/topographer';
12
13
  import type { TopoGraph } from '@ontrails/topographer';
@@ -27,16 +28,18 @@ import { isDraftMarkedFile } from './draft.js';
27
28
  import { applySafeFixesToSource, hasSafeFixEdits } from './fix.js';
28
29
  import type { DriftResult } from './drift.js';
29
30
  import { checkDrift } from './drift.js';
31
+ import { loadProjectWardenRules } from './project-rules.js';
32
+ import type { ProjectWardenRules } from './project-rules.js';
30
33
  import {
31
34
  collectProjectDocumentationImportResolutions,
32
35
  collectProjectImportResolutions,
33
36
  collectPublicWorkspaces,
34
37
  } from './project-context.js';
35
38
  import {
39
+ collectComposeTargetTrailIds,
36
40
  collectContourDefinitionIds,
37
41
  collectContourReferenceTargetsByName,
38
42
  collectCrudTableIds as collectCrudTableIdsFromAst,
39
- collectComposeTargetTrailIds,
40
43
  collectOnTargetSignalIds as collectOnTargetSignalIdsFromAst,
41
44
  collectReconcileTableIds as collectReconcileTableIdsFromAst,
42
45
  collectResourceDefinitionIds,
@@ -119,6 +122,15 @@ export interface WardenRunOptions {
119
122
  * lint rule dispatch. `lintOnly` and `driftOnly` remain compatibility shims.
120
123
  */
121
124
  readonly tier?: WardenRuleTier | undefined;
125
+ /**
126
+ * Load project-local Warden rules from `.trails/rules.ts` or `.trails/rules/`.
127
+ *
128
+ * Enabled by default for normal runs so a Trails app can carry migration or
129
+ * repo-local governance with the project instead of shipping it from
130
+ * `@ontrails/warden`. Set to `false` for narrow tests or embedders that need
131
+ * only built-in and explicitly provided rules.
132
+ */
133
+ readonly projectRules?: boolean | undefined;
122
134
  /**
123
135
  * App topology for drift detection. When provided, enables real topology
124
136
  * drift comparison and unlocks the topo-aware rule dispatch path.
@@ -324,6 +336,7 @@ interface MutableProjectContext {
324
336
  knownResourceIds: Set<string>;
325
337
  knownSignalIds: Set<string>;
326
338
  knownTrailIds: Set<string>;
339
+ topoTrailIds: Set<string>;
327
340
  importResolutionsByFile: Map<string, readonly WardenImportResolution[]>;
328
341
  documentedImportResolutionsByFile: Map<
329
342
  string,
@@ -352,6 +365,7 @@ const createMutableProjectContext = (): MutableProjectContext => ({
352
365
  onTargetSignalIds: new Set<string>(),
353
366
  publicWorkspaces: new Map(),
354
367
  reconcileTableIds: new Set<string>(),
368
+ topoTrailIds: new Set<string>(),
355
369
  trailIntentsById: new Map<string, 'destroy' | 'read' | 'write'>(),
356
370
  });
357
371
 
@@ -401,6 +415,9 @@ const toProjectContext = (context: MutableProjectContext): ProjectContext => ({
401
415
  knownResourceIds: context.knownResourceIds,
402
416
  knownSignalIds: context.knownSignalIds,
403
417
  knownTrailIds: context.knownTrailIds,
418
+ ...(context.topoTrailIds.size > 0
419
+ ? { topoTrailIds: context.topoTrailIds }
420
+ : {}),
404
421
  ...(context.importResolutionsByFile.size > 0
405
422
  ? { importResolutionsByFile: context.importResolutionsByFile }
406
423
  : {}),
@@ -635,6 +652,7 @@ const collectTopoKnownIds = (
635
652
 
636
653
  for (const id of appTopo.trails.keys()) {
637
654
  context.knownTrailIds.add(id);
655
+ context.topoTrailIds.add(id);
638
656
  }
639
657
 
640
658
  for (const id of appTopo.resources.keys()) {
@@ -854,6 +872,23 @@ const createOptionsDiagnostic = (message: string): WardenDiagnostic => ({
854
872
  severity: 'error',
855
873
  });
856
874
 
875
+ const emptyProjectWardenRules = (): ProjectWardenRules => ({
876
+ diagnostics: [],
877
+ sourceRules: [],
878
+ topoRules: [],
879
+ });
880
+
881
+ const loadProjectRulesForRun = async (
882
+ rootDir: string,
883
+ options: WardenRunOptions,
884
+ runLint: boolean
885
+ ): Promise<ProjectWardenRules> => {
886
+ if (!runLint || options.projectRules === false) {
887
+ return emptyProjectWardenRules();
888
+ }
889
+ return loadProjectWardenRules(rootDir);
890
+ };
891
+
857
892
  interface WardenRuleSelector {
858
893
  readonly depth?: WardenDepth | undefined;
859
894
  readonly tier?: WardenRuleTier | undefined;
@@ -1411,7 +1446,10 @@ export const applySafeFixesToFiles = async (
1411
1446
  export const runWarden = async (
1412
1447
  options: WardenRunOptions = {}
1413
1448
  ): Promise<WardenReport> => {
1414
- const rootDir = resolve(options.rootDir ?? process.cwd());
1449
+ const { rootDir } = resolveTrailsProjectRoot({
1450
+ explicitRootDir: options.rootDir,
1451
+ startDir: process.cwd(),
1452
+ });
1415
1453
  const { diagnostics: configDiagnostics, effectiveConfig } =
1416
1454
  resolveWardenConfig({
1417
1455
  cli: buildCliConfigLayer(options),
@@ -1433,13 +1471,14 @@ export const runWarden = async (
1433
1471
  } satisfies WardenRuleSelector;
1434
1472
  const runLint = shouldRunLint(options);
1435
1473
  const runDrift = shouldRunDrift(options, effectiveConfig);
1474
+ const projectRules = await loadProjectRulesForRun(rootDir, options, runLint);
1436
1475
  const lintResult = runLint
1437
1476
  ? await lintFiles(
1438
1477
  rootDir,
1439
1478
  effectiveConfig.drafts,
1440
1479
  topoTargets,
1441
- options.extraTopoRules ?? [],
1442
- options.extraSourceRules ?? [],
1480
+ [...projectRules.topoRules, ...(options.extraTopoRules ?? [])],
1481
+ [...projectRules.sourceRules, ...(options.extraSourceRules ?? [])],
1443
1482
  selector
1444
1483
  )
1445
1484
  : { diagnostics: [], sourceFiles: [] };
@@ -1447,6 +1486,7 @@ export const runWarden = async (
1447
1486
 
1448
1487
  const rawDiagnostics = [
1449
1488
  ...configDiagnostics,
1489
+ ...projectRules.diagnostics,
1450
1490
  ...optionDiagnostics,
1451
1491
  ...lintResult.diagnostics,
1452
1492
  ...adapterDiagnostics,
package/src/command.ts CHANGED
@@ -14,6 +14,10 @@ import type {
14
14
  } from '@ontrails/cli';
15
15
  import type { Topo } from '@ontrails/core';
16
16
  import { AmbiguousError, NotFoundError } from '@ontrails/core';
17
+ import {
18
+ loadTrailsConfigValue,
19
+ resolveTrailsProjectRoot,
20
+ } from '@ontrails/config';
17
21
 
18
22
  import type {
19
23
  WardenConfigInput,
@@ -57,13 +61,6 @@ interface MutableWardenConfigLayer {
57
61
  noLockMutation?: boolean | undefined;
58
62
  }
59
63
 
60
- const CONFIG_CANDIDATES = [
61
- 'trails.config.ts',
62
- 'trails.config.mts',
63
- 'trails.config.js',
64
- 'trails.config.mjs',
65
- ] as const;
66
-
67
64
  const diagnostic = ({
68
65
  filePath = '<warden-cli>',
69
66
  message,
@@ -176,7 +173,7 @@ interface WardenValueAliasTarget {
176
173
  readonly configKey: AliasConfigKey;
177
174
  }
178
175
 
179
- const wardenAliasSpecs = [
176
+ const wardenAliasSpecs: readonly WardenAliasSpec[] = [
180
177
  {
181
178
  aliases: true,
182
179
  choices: wardenFormatValues,
@@ -203,7 +200,7 @@ const wardenAliasSpecs = [
203
200
  configKey: 'drafts',
204
201
  flagName: 'drafts',
205
202
  },
206
- ] satisfies readonly WardenAliasSpec[];
203
+ ];
207
204
 
208
205
  const wardenValueAliasTargets: readonly WardenValueAliasTarget[] =
209
206
  wardenAliasSpecs.flatMap((spec) =>
@@ -491,12 +488,7 @@ const findConfigPath = (
491
488
  };
492
489
  }
493
490
 
494
- const candidate = CONFIG_CANDIDATES.map((entry) =>
495
- resolve(rootDir, entry)
496
- ).find((entry) => existsSync(entry));
497
- return candidate === undefined
498
- ? { diagnostics: [] }
499
- : { configPath: candidate, diagnostics: [] };
491
+ return { diagnostics: [] };
500
492
  };
501
493
 
502
494
  const isRecord = (value: unknown): value is Record<string, unknown> =>
@@ -529,14 +521,6 @@ const extractWardenConfig = (value: unknown): WardenConfigInput | undefined =>
529
521
  ? (value['warden'] as WardenConfigInput)
530
522
  : undefined;
531
523
 
532
- const importConfigModule = async (
533
- configPath: string
534
- ): Promise<Record<string, unknown>> => {
535
- const url = pathToFileURL(configPath);
536
- url.searchParams.set('t', Date.now().toString());
537
- return (await import(url.href)) as Record<string, unknown>;
538
- };
539
-
540
524
  export const loadWardenConfig = async ({
541
525
  configPath,
542
526
  env = {},
@@ -547,29 +531,32 @@ export const loadWardenConfig = async ({
547
531
  readonly rootDir: string;
548
532
  }): Promise<WardenConfigLoadResult> => {
549
533
  const located = findConfigPath(rootDir, configPath);
550
- if (located.configPath === undefined) {
534
+ if (configPath !== undefined && located.configPath === undefined) {
551
535
  return located;
552
536
  }
553
537
 
554
538
  try {
555
- const mod = await importConfigModule(located.configPath);
556
- const exported = mod['default'] ?? mod;
539
+ const loaded = await loadTrailsConfigValue({ configPath, rootDir });
540
+ const exported = loaded.value;
541
+ if (exported === undefined) {
542
+ return located;
543
+ }
557
544
  if (isResolvableConfig(exported)) {
558
545
  const resolved = await exported.resolve({ cwd: rootDir, env });
559
546
  if (isResultLike(resolved)) {
560
547
  if (resolved.isOk()) {
561
548
  return {
562
549
  config: extractWardenConfig(resolved.value),
563
- configPath: located.configPath,
550
+ configPath: loaded.configPath,
564
551
  diagnostics: located.diagnostics,
565
552
  };
566
553
  }
567
554
  return {
568
- configPath: located.configPath,
555
+ configPath: loaded.configPath,
569
556
  diagnostics: [
570
557
  ...located.diagnostics,
571
558
  diagnostic({
572
- filePath: located.configPath,
559
+ filePath: loaded.configPath ?? rootDir,
573
560
  message: `Failed to resolve Warden config: ${errorMessage(resolved.error)}`,
574
561
  rule: 'warden-config',
575
562
  }),
@@ -578,22 +565,22 @@ export const loadWardenConfig = async ({
578
565
  }
579
566
  return {
580
567
  config: extractWardenConfig(resolved),
581
- configPath: located.configPath,
568
+ configPath: loaded.configPath,
582
569
  diagnostics: located.diagnostics,
583
570
  };
584
571
  }
585
572
  return {
586
573
  config: extractWardenConfig(exported),
587
- configPath: located.configPath,
574
+ configPath: loaded.configPath,
588
575
  diagnostics: located.diagnostics,
589
576
  };
590
577
  } catch (error) {
591
578
  return {
592
- configPath: located.configPath,
579
+ configPath: located.configPath ?? configPath,
593
580
  diagnostics: [
594
581
  ...located.diagnostics,
595
582
  diagnostic({
596
- filePath: located.configPath,
583
+ filePath: located.configPath ?? rootDir,
597
584
  message: `Failed to load Warden config: ${errorMessage(error)}`,
598
585
  rule: 'warden-config',
599
586
  }),
@@ -906,7 +893,10 @@ export const runWardenCommand = async ({
906
893
  env = {},
907
894
  }: RunWardenCommandOptions): Promise<WardenCommandResult> => {
908
895
  const parsed = parseWardenCommandArgs(args);
909
- const rootDir = resolve(cwd, parsed.rootDir ?? '.');
896
+ const { rootDir } = resolveTrailsProjectRoot({
897
+ explicitRootDir: parsed.rootDir,
898
+ startDir: cwd,
899
+ });
910
900
  const loadedConfig = await loadWardenConfig({
911
901
  configPath: parsed.configPath,
912
902
  env,
package/src/drift.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Topo lock drift detection.
2
+ * Trails lock drift detection.
3
3
  *
4
- * Compares the `topo.lock` artifact hash listed in `trails.lock` against a
5
- * freshly generated TopoGraph hash to detect when the trail topology has
6
- * changed without updating the artifact family.
4
+ * Compares the committed `trails.lock` TopoGraph hash against a freshly
5
+ * generated TopoGraph hash to detect when the trail topology changed without
6
+ * updating the committed resolved truth.
7
7
  */
8
8
 
9
9
  import { existsSync, statSync } from 'node:fs';
@@ -20,6 +20,7 @@ import {
20
20
  deriveTopoGraphHash,
21
21
  isTopoArtifactRegenerationError,
22
22
  readLockManifest,
23
+ readTrailsLock,
23
24
  } from '@ontrails/topographer';
24
25
  import type { LockManifest } from '@ontrails/topographer';
25
26
 
@@ -57,11 +58,26 @@ const blockedLockRead = (reason: string): BlockedLockRead => ({
57
58
  const readCommittedLockManifest = async (
58
59
  rootDir: string
59
60
  ): Promise<BlockedLockRead | LockManifest | null> => {
60
- const trailsDir = deriveTrailsDir({ rootDir });
61
61
  try {
62
- return existsSync(rootDir) && statSync(rootDir).isDirectory()
63
- ? await readLockManifest({ dir: trailsDir })
64
- : null;
62
+ if (!(existsSync(rootDir) && statSync(rootDir).isDirectory())) {
63
+ return null;
64
+ }
65
+ const rootLock = await readTrailsLock({ dir: rootDir });
66
+ if (rootLock !== null) {
67
+ return {
68
+ artifacts: [
69
+ {
70
+ path: 'topo.lock',
71
+ role: 'topo',
72
+ sha256: rootLock.topoGraphHash,
73
+ },
74
+ ],
75
+ scope: rootLock.scope,
76
+ summary: rootLock.summary,
77
+ version: 3,
78
+ };
79
+ }
80
+ return await readLockManifest({ dir: deriveTrailsDir({ rootDir }) });
65
81
  } catch (error) {
66
82
  if (isTopoArtifactRegenerationError(error)) {
67
83
  return blockedLockRead(error.message);