@ontrails/warden 1.0.0-beta.41 → 1.0.0-beta.43

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 (34) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/package.json +10 -10
  3. package/src/adapter-check.ts +1 -1
  4. package/src/cli.ts +81 -5
  5. package/src/index.ts +17 -4
  6. package/src/regrade-history.ts +599 -0
  7. package/src/rules/cli-command-route-coherence.ts +6 -6
  8. package/src/rules/draft-visible-debt.ts +1 -1
  9. package/src/rules/duplicate-exported-symbol.ts +4 -43
  10. package/src/rules/governed-symbol-residue.ts +146 -53
  11. package/src/rules/governed-vocabulary-permutation-watch.ts +76 -0
  12. package/src/rules/index.ts +17 -6
  13. package/src/rules/layer-field-name-drift.ts +2 -2
  14. package/src/rules/{library-projection-coherence.ts → library-render-coherence.ts} +11 -14
  15. package/src/rules/metadata.ts +51 -14
  16. package/src/rules/no-retired-cross-vocabulary.ts +0 -1
  17. package/src/rules/{owner-projection-parity.ts → owner-render-parity.ts} +18 -18
  18. package/src/rules/public-internal-deep-imports.ts +1 -3
  19. package/src/rules/public-output-schema.ts +1 -1
  20. package/src/rules/registry-names.ts +6 -4
  21. package/src/rules/retired-vocabulary.ts +775 -41
  22. package/src/rules/surface-overlay-coherence.ts +1 -1
  23. package/src/rules/trail-versioning-source.ts +1 -1
  24. package/src/rules/trailhead-override-divergence.ts +4 -4
  25. package/src/rules/types.ts +51 -5
  26. package/src/trails/governed-vocabulary-permutation-watch.trail.ts +16 -0
  27. package/src/trails/index.ts +3 -2
  28. package/src/trails/layer-field-name-drift.trail.ts +1 -1
  29. package/src/trails/{library-projection-coherence.trail.ts → library-render-coherence.trail.ts} +6 -6
  30. package/src/trails/{owner-projection-parity.trail.ts → owner-render-parity.trail.ts} +4 -4
  31. package/src/trails/public-output-schema.trail.ts +1 -1
  32. package/src/trails/run.ts +44 -2
  33. package/src/trails/schema.ts +41 -0
  34. package/src/trails/wrap-rule.ts +44 -2
@@ -147,7 +147,7 @@ const cliCollisionDiagnostics = (
147
147
  routeByName.set(segment, { kind: 'canonical', trailId: entry.id });
148
148
  }
149
149
  for (const route of entry.cli?.routes ?? []) {
150
- // Surface-sourced alias routes are the projections of the overlay
150
+ // Surface-sourced alias routes are the renderings of the overlay
151
151
  // bindings themselves (TRL-1207), so a binding can never "shadow"
152
152
  // one — only canonical paths and trail-owned aliases are real
153
153
  // entries a binding name could collide with.
@@ -1070,7 +1070,7 @@ export const markerSchemaUnsupported: WardenRule = {
1070
1070
  return diagnostics;
1071
1071
  },
1072
1072
  description:
1073
- 'Reject versioned trail schema constructs that cannot be projected into stable marker contracts.',
1073
+ 'Reject versioned trail schema constructs that cannot be rendered into stable marker contracts.',
1074
1074
  name: MARKER_SCHEMA_UNSUPPORTED,
1075
1075
  severity: 'error',
1076
1076
  };
@@ -5,11 +5,11 @@
5
5
  * The overlay is the authored, lockable default; a call-site trailhead map
6
6
  * is a supported override-in-context that wins at runtime. Divergence between
7
7
  * the two is legal but must be visible: an agent reading the committed lock
8
- * would otherwise trust grouped entries the running surface does not project.
8
+ * would otherwise trust grouped entries the running surface does not render.
9
9
  *
10
10
  * Rule-kind note: no Warden rule kind currently sees both the serialized
11
11
  * graph overlays and source ASTs directly. This rule is the closest honest
12
- * shape — a project-aware source rule whose `ProjectContext` carries
12
+ * shape — a render-aware source rule whose `ProjectContext` carries
13
13
  * per-app authored `mcp` binding sets resolved from the run's topo targets
14
14
  * (graph overlays when available, else app-module overlay registrations).
15
15
  * A call-site map is attributed to an app only when one of its literal
@@ -256,7 +256,7 @@ const diagnoseTrailheadMap = (
256
256
  sourceCode,
257
257
  filePath,
258
258
  trailheadMap,
259
- `App "${set.appName}"'s surfaceOverlay mcp binding "${name}" is not carried by this call-site trailhead map, so it will not be projected at runtime while the committed lock still advertises it. Add "${name}" to the call-site map or remove it from the authored overlay.`
259
+ `App "${set.appName}"'s surfaceOverlay mcp binding "${name}" is not carried by this call-site trailhead map, so it will not be rendered at runtime while the committed lock still advertises it. Add "${name}" to the call-site map or remove it from the authored overlay.`
260
260
  )
261
261
  );
262
262
  }
@@ -271,7 +271,7 @@ const diagnoseTrailheadMap = (
271
271
  */
272
272
  export const trailheadOverrideDivergence: ProjectAwareWardenRule = {
273
273
  check(): readonly WardenDiagnostic[] {
274
- // Without project context there are no authored bindings to compare
274
+ // Without render context there are no authored bindings to compare
275
275
  // against; stay silent instead of guessing.
276
276
  return [];
277
277
  },
@@ -11,6 +11,7 @@ import type { TopoGraph } from '@ontrails/topography';
11
11
  import type { WardenDepth } from '../config.js';
12
12
  import type { WardenImportResolution } from '../resolve.js';
13
13
  import type { WardenPublicWorkspace } from '../workspaces.js';
14
+ import type { GovernedVocabularyHistoryProvenance } from './retired-vocabulary.js';
14
15
 
15
16
  /**
16
17
  * Severity level for warden diagnostics.
@@ -83,7 +84,7 @@ export interface WardenGuidanceLink {
83
84
  }
84
85
 
85
86
  /**
86
- * Structured remediation guidance that can be rendered for humans or projected
87
+ * Structured remediation guidance that can be rendered for humans or rendered
87
88
  * into agent-facing manifests without scraping diagnostic prose.
88
89
  */
89
90
  export interface WardenGuidance {
@@ -143,7 +144,7 @@ export interface WardenFixEdit {
143
144
  }
144
145
 
145
146
  /**
146
- * Source targets a fix class can inspect when projected into downstream tools.
147
+ * Source targets a fix class can inspect when rendered into downstream tools.
147
148
  *
148
149
  * Warden itself decides which committed files it scans. This metadata is for
149
150
  * consumers such as Regrade that need to derive a narrower collection before
@@ -179,7 +180,7 @@ export interface WardenFix {
179
180
  }
180
181
 
181
182
  /**
182
- * Per-rule fix capability, projected into the guide/manifest.
183
+ * Per-rule fix capability, rendered into the guide/manifest.
183
184
  *
184
185
  * Declares that a rule can emit {@link WardenFix} metadata and the default
185
186
  * safety for its fixes, so `warden --help`, the guide, and agent surfaces can
@@ -191,7 +192,7 @@ export interface WardenFixCapability {
191
192
  readonly class: WardenFixClass;
192
193
  /** Default safety for fixes this rule emits. */
193
194
  readonly safety: WardenFixSafety;
194
- /** Downstream scan targets for tools that project this fix capability. */
195
+ /** Downstream scan targets for tools that render this fix capability. */
195
196
  readonly scanTargets?: WardenFixScanTargets | undefined;
196
197
  }
197
198
 
@@ -213,7 +214,7 @@ export interface WardenRuleMetadata {
213
214
  readonly tier: WardenRuleTier;
214
215
  /** Structured remediation guidance for diagnostics emitted by this rule. */
215
216
  readonly guidance?: WardenGuidance | undefined;
216
- /** Declares that this rule can emit fix metadata, for guide projection. */
217
+ /** Declares that this rule can emit fix metadata, for guide rendering. */
217
218
  readonly fix?: WardenFixCapability | undefined;
218
219
  }
219
220
 
@@ -280,6 +281,10 @@ export interface WardenRule {
280
281
  readonly description: string;
281
282
  /** Optional inline classification. Built-ins are classified by registry. */
282
283
  readonly metadata?: WardenRuleMetadata | undefined;
284
+ /** Source file kinds this rule explicitly supports beyond TypeScript. */
285
+ readonly sourceKinds?:
286
+ | readonly ('documentation' | 'text' | 'typescript')[]
287
+ | undefined;
283
288
  /** Run the rule against source code and return any diagnostics */
284
289
  readonly check: (
285
290
  sourceCode: string,
@@ -304,6 +309,16 @@ export interface AuthoredMcpSurfaceBindingSet {
304
309
  * Options for compose-file rules that need knowledge of all trail IDs in a project.
305
310
  */
306
311
  export interface ProjectContext {
312
+ /** Whether this project owns the governed transition registry and must prove completed migrations. */
313
+ readonly governedVocabularyHistoryRequired?: boolean | undefined;
314
+ /** Validated committed Regrade history evidence keyed by governed transition. */
315
+ readonly governedVocabularyHistoryByTransitionId?:
316
+ | ReadonlyMap<string, GovernedVocabularyHistoryEvidence>
317
+ | undefined;
318
+ /** Invalid committed history artifacts observed while building context. */
319
+ readonly governedVocabularyHistoryIssues?:
320
+ | readonly GovernedVocabularyHistoryIssue[]
321
+ | undefined;
307
322
  /**
308
323
  * Per-app authored `mcp` surface bindings, resolved from the project's
309
324
  * topo targets (the serialized graph overlays when available, else the
@@ -368,10 +383,41 @@ export interface ProjectContext {
368
383
  readonly crudCoverageByEntity?: ReadonlyMap<string, ReadonlySet<string>>;
369
384
  }
370
385
 
386
+ export interface GovernedVocabularyHistoryEvidence {
387
+ readonly caseSensitive: boolean;
388
+ readonly id: string;
389
+ readonly latestFormObservations: readonly GovernedVocabularyHistoryFormObservation[];
390
+ readonly path: string;
391
+ readonly provenance?: GovernedVocabularyHistoryProvenance;
392
+ readonly runCount: number;
393
+ readonly transitionId: string;
394
+ }
395
+
396
+ /** One vocabulary-form observation preserved in committed Regrade history. */
397
+ export interface GovernedVocabularyHistoryFormObservation {
398
+ readonly disposition: string;
399
+ readonly form: string;
400
+ readonly line: number;
401
+ readonly path: string;
402
+ readonly reason: string;
403
+ readonly scopeTier?: 'in-scope' | 'policy-classified' | undefined;
404
+ readonly verdict: 'applied' | 'deferred' | 'modified' | 'skipped';
405
+ }
406
+
407
+ export interface GovernedVocabularyHistoryIssue {
408
+ readonly message: string;
409
+ readonly path: string;
410
+ readonly transitionId?: string;
411
+ }
412
+
371
413
  /**
372
414
  * A project-aware rule that requires knowledge of all trail IDs.
373
415
  */
374
416
  export interface ProjectAwareWardenRule extends WardenRule {
417
+ /** Run project-wide validation once, independently of any source file. */
418
+ readonly checkProject?: (
419
+ context: ProjectContext
420
+ ) => readonly WardenDiagnostic[];
375
421
  /** Run the rule with project-level context */
376
422
  readonly checkWithContext: (
377
423
  sourceCode: string,
@@ -0,0 +1,16 @@
1
+ import { governedVocabularyPermutationWatch } from '../rules/governed-vocabulary-permutation-watch.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const governedVocabularyPermutationWatchTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'packages/example/src/trails/play.ts',
10
+ sourceCode: 'const entityId = "track";\n',
11
+ },
12
+ name: 'Project history watch runs at the project boundary',
13
+ },
14
+ ],
15
+ rule: governedVocabularyPermutationWatch,
16
+ });
@@ -17,12 +17,13 @@ export { exampleValidTrail } from './example-valid.trail.js';
17
17
  export { firesDeclarationsTrail } from './fires-declarations.trail.js';
18
18
  export { forkWithoutPreservedImplementationTrail } from './fork-without-preserved-implementation.trail.js';
19
19
  export { governedSymbolResidueTrail } from './governed-symbol-residue.trail.js';
20
+ export { governedVocabularyPermutationWatchTrail } from './governed-vocabulary-permutation-watch.trail.js';
20
21
  export { implementationReturnsResultTrail } from './implementation-returns-result.trail.js';
21
22
  export { incompleteAccessorForStandardOpTrail } from './incomplete-accessor-for-standard-op.trail.js';
22
23
  export { incompleteCrudTrail } from './incomplete-crud.trail.js';
23
24
  export { intentPropagationTrail } from './intent-propagation.trail.js';
24
25
  export { layerFieldNameDriftTrail } from './layer-field-name-drift.trail.js';
25
- export { libraryProjectionCoherenceTrail } from './library-projection-coherence.trail.js';
26
+ export { libraryRenderCoherenceTrail } from './library-render-coherence.trail.js';
26
27
  export { markerSchemaUnsupportedTrail } from './marker-schema-unsupported.trail.js';
27
28
  export { missingVisibilityTrail } from './missing-visibility.trail.js';
28
29
  export { missingReconcileTrail } from './missing-reconcile.trail.js';
@@ -40,7 +41,7 @@ export { noThrowInDetourRecoverTrail } from './no-throw-in-detour-recover.trail.
40
41
  export { noThrowInImplementationTrail } from './no-throw-in-implementation.trail.js';
41
42
  export { noTopLevelSurfaceTrail } from './no-top-level-surface.trail.js';
42
43
  export { orphanedSignalTrail } from './orphaned-signal.trail.js';
43
- export { ownerProjectionParityTrail } from './owner-projection-parity.trail.js';
44
+ export { ownerRenderParityTrail } from './owner-render-parity.trail.js';
44
45
  export { pendingForceTrail } from './pending-force.trail.js';
45
46
  export { permitGovernanceTrail } from './permit-governance.trail.js';
46
47
  export { preferSchemaInferenceTrail } from './prefer-schema-inference.trail.js';
@@ -21,7 +21,7 @@ const collides = LAYER_FIELD_RESERVED_NAMES.has('all');
21
21
  filePath: 'packages/cli/src/build.ts',
22
22
  line: 1,
23
23
  message:
24
- 'layer-field-name-drift: surface-local reserved name set "META_FLAG_CANDIDATES" can make layer input fields project differently across surfaces. Import LAYER_FIELD_RESERVED_NAMES from @ontrails/core instead.',
24
+ 'layer-field-name-drift: surface-local reserved name set "META_FLAG_CANDIDATES" can make layer input fields render differently across surfaces. Import LAYER_FIELD_RESERVED_NAMES from @ontrails/core instead.',
25
25
  rule: 'layer-field-name-drift',
26
26
  severity: 'error',
27
27
  },
@@ -1,7 +1,7 @@
1
1
  import { Result, topo, trail } from '@ontrails/core';
2
2
  import { z } from 'zod';
3
3
 
4
- import { libraryProjectionCoherence } from '../rules/library-projection-coherence.js';
4
+ import { libraryRenderCoherence } from '../rules/library-render-coherence.js';
5
5
  import { wrapTopoRule } from './wrap-rule.js';
6
6
 
7
7
  const output = z.object({ ok: z.boolean() });
@@ -18,7 +18,7 @@ const kebab = trail('widget-ping', {
18
18
  output,
19
19
  });
20
20
 
21
- export const libraryProjectionCoherenceTrail = wrapTopoRule({
21
+ export const libraryRenderCoherenceTrail = wrapTopoRule({
22
22
  examples: [
23
23
  {
24
24
  expected: {
@@ -27,17 +27,17 @@ export const libraryProjectionCoherenceTrail = wrapTopoRule({
27
27
  filePath: '<topo>',
28
28
  line: 1,
29
29
  message:
30
- 'Library projection export collision on "widgetPing": trails "widget-ping", "widget.ping" derive the same package export. Rename one trail or add a library export override before materializing the generated package.',
31
- rule: 'library-projection-coherence',
30
+ 'Library rendering export collision on "widgetPing": trails "widget-ping", "widget.ping" derive the same package export. Rename one trail or add a library export override before materializing the generated package.',
31
+ rule: 'library-render-coherence',
32
32
  severity: 'error',
33
33
  },
34
34
  ],
35
35
  },
36
36
  input: {
37
- topo: topo('library-projection-coherence', { dotted, kebab }),
37
+ topo: topo('library-render-coherence', { dotted, kebab }),
38
38
  },
39
39
  name: 'Library export collision',
40
40
  },
41
41
  ],
42
- rule: libraryProjectionCoherence,
42
+ rule: libraryRenderCoherence,
43
43
  });
@@ -1,9 +1,9 @@
1
1
  import { fileURLToPath } from 'node:url';
2
2
 
3
- import { ownerProjectionParity } from '../rules/owner-projection-parity.js';
3
+ import { ownerRenderParity } from '../rules/owner-render-parity.js';
4
4
  import { wrapRule } from './wrap-rule.js';
5
5
 
6
- export const ownerProjectionParityTrail = wrapRule({
6
+ export const ownerRenderParityTrail = wrapRule({
7
7
  examples: [
8
8
  {
9
9
  expected: { diagnostics: [] },
@@ -19,8 +19,8 @@ export const httpMethodByIntent = {
19
19
  write: 'POST',
20
20
  } as const satisfies Record<Intent, 'GET' | 'POST' | 'DELETE'>;`,
21
21
  },
22
- name: 'HTTP method projection covers core intent values',
22
+ name: 'HTTP method rendering covers core intent values',
23
23
  },
24
24
  ],
25
- rule: ownerProjectionParity,
25
+ rule: ownerRenderParity,
26
26
  });
@@ -39,7 +39,7 @@ export const publicOutputSchemaTrail = wrapTopoRule({
39
39
  filePath: '<topo>',
40
40
  line: 1,
41
41
  message:
42
- 'Trail "report.missing" is visible to public MCP/HTTP surface projection but does not declare an output schema. Add an explicit output schema, or mark the trail visibility as internal if it is composition-only.',
42
+ 'Trail "report.missing" is visible to public MCP/HTTP surface rendering but does not declare an output schema. Add an explicit output schema, or mark the trail visibility as internal if it is composition-only.',
43
43
  rule: 'public-output-schema',
44
44
  severity: 'error',
45
45
  },
package/src/trails/run.ts CHANGED
@@ -9,12 +9,20 @@
9
9
  import type { Intent, Topo } from '@ontrails/core';
10
10
  import { run } from '@ontrails/core';
11
11
 
12
- import { wardenTopoRules } from '../rules/index.js';
13
- import type { WardenDiagnostic } from '../rules/types.js';
12
+ import { wardenRules, wardenTopoRules } from '../rules/index.js';
13
+ import type {
14
+ GovernedVocabularyHistoryEvidence,
15
+ GovernedVocabularyHistoryIssue,
16
+ ProjectAwareWardenRule,
17
+ WardenDiagnostic,
18
+ WardenRule,
19
+ } from '../rules/types.js';
14
20
  import type { WardenImportResolution } from '../resolve.js';
15
21
  import type { WardenPublicWorkspace } from '../workspaces.js';
22
+ import { projectAwareRuleInput } from './schema.js';
16
23
  import type { RuleOutput } from './schema.js';
17
24
  import { wardenTopo } from './topo.js';
25
+ import { buildProjectContext } from './wrap-rule.js';
18
26
 
19
27
  /**
20
28
  * Run all file-scoped warden rule trails for a given file and collect diagnostics.
@@ -34,6 +42,9 @@ const appendDiagnostics = (
34
42
  type TrailIntentMap = Readonly<Record<string, Intent>>;
35
43
 
36
44
  interface ProjectRuleOptions {
45
+ readonly governedVocabularyHistories?: readonly GovernedVocabularyHistoryEvidence[];
46
+ readonly governedVocabularyHistoryIssues?: readonly GovernedVocabularyHistoryIssue[];
47
+ readonly governedVocabularyHistoryRequired?: boolean;
37
48
  readonly entityReferencesByName?: Readonly<Record<string, readonly string[]>>;
38
49
  readonly composeTargetTrailIds?: readonly string[];
39
50
  readonly crudTableIds?: readonly string[];
@@ -55,6 +66,9 @@ interface ProjectRuleOptions {
55
66
  }
56
67
 
57
68
  const PROJECT_OPTION_KEYS = [
69
+ 'governedVocabularyHistories',
70
+ 'governedVocabularyHistoryIssues',
71
+ 'governedVocabularyHistoryRequired',
58
72
  'entityReferencesByName',
59
73
  'composeTargetTrailIds',
60
74
  'crudTableIds',
@@ -135,6 +149,34 @@ export const runWardenTrails = async (
135
149
  return allDiagnostics;
136
150
  };
137
151
 
152
+ const isProjectAwareRule = (rule: WardenRule): rule is ProjectAwareWardenRule =>
153
+ 'checkWithContext' in rule;
154
+
155
+ /**
156
+ * Run project-wide built-in Warden diagnostics once for one project context.
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * const diagnostics = runProjectWardenRules({
161
+ * governedVocabularyHistories: histories,
162
+ * });
163
+ * ```
164
+ */
165
+ export const runProjectWardenRules = (
166
+ options?: ProjectRuleOptions
167
+ ): readonly WardenDiagnostic[] => {
168
+ const context = buildProjectContext(
169
+ projectAwareRuleInput.parse(buildRuleInput('<project>', '', options))
170
+ );
171
+ const diagnostics: WardenDiagnostic[] = [];
172
+ for (const rule of wardenRules.values()) {
173
+ if (isProjectAwareRule(rule) && rule.checkProject !== undefined) {
174
+ appendDiagnostics(diagnostics, rule.checkProject(context));
175
+ }
176
+ }
177
+ return diagnostics;
178
+ };
179
+
138
180
  /**
139
181
  * Run the built-in topo-aware warden rule trails once against a resolved topo.
140
182
  *
@@ -119,6 +119,27 @@ export const authoredMcpSurfaceBindingSetSchema = z.object({
119
119
  .describe('Trail ids registered in the owning app topo'),
120
120
  });
121
121
 
122
+ const governedVocabularyHistoryObservationSchema = z.object({
123
+ disposition: z.string(),
124
+ form: z.string(),
125
+ line: z.number().int().positive(),
126
+ path: z.string(),
127
+ reason: z.string(),
128
+ scopeTier: z.enum(['in-scope', 'policy-classified']).optional(),
129
+ verdict: z.enum(['applied', 'deferred', 'modified', 'skipped']),
130
+ });
131
+
132
+ const governedVocabularyHistoryEvidenceSchema = z.object({
133
+ caseSensitive: z.boolean(),
134
+ id: z.string(),
135
+ latestFormObservations: z
136
+ .array(governedVocabularyHistoryObservationSchema)
137
+ .readonly(),
138
+ path: z.string(),
139
+ runCount: z.number().int().nonnegative(),
140
+ transitionId: z.string(),
141
+ });
142
+
122
143
  export const projectAwareRuleInput = ruleInput.extend({
123
144
  authoredMcpSurfaceBindingSets: z
124
145
  .array(authoredMcpSurfaceBindingSetSchema)
@@ -153,6 +174,26 @@ export const projectAwareRuleInput = ruleInput.extend({
153
174
  .record(z.string(), z.array(exportedSymbolDefinitionSchema))
154
175
  .optional()
155
176
  .describe('Public exported symbol definitions grouped by exported name'),
177
+ governedVocabularyHistories: z
178
+ .array(governedVocabularyHistoryEvidenceSchema)
179
+ .readonly()
180
+ .optional()
181
+ .describe('Validated committed Regrade histories by governed transition'),
182
+ governedVocabularyHistoryIssues: z
183
+ .array(
184
+ z.object({
185
+ message: z.string(),
186
+ path: z.string(),
187
+ transitionId: z.string().optional(),
188
+ })
189
+ )
190
+ .readonly()
191
+ .optional()
192
+ .describe('Invalid committed Regrade history artifacts'),
193
+ governedVocabularyHistoryRequired: z
194
+ .boolean()
195
+ .optional()
196
+ .describe('Whether the project must prove completed governed migrations'),
156
197
  importResolutionsByFile: z
157
198
  .record(z.string(), z.array(importResolutionSchema))
158
199
  .optional()
@@ -50,7 +50,47 @@ const buildRuleMeta = (rule: WardenRule | TopoAwareWardenRule) => {
50
50
  };
51
51
  };
52
52
 
53
- const buildProjectContext = (input: ProjectAwareRuleInput): ProjectContext => ({
53
+ const buildGovernedHistoryContext = (
54
+ input: ProjectAwareRuleInput
55
+ ): Pick<
56
+ ProjectContext,
57
+ | 'governedVocabularyHistoryByTransitionId'
58
+ | 'governedVocabularyHistoryIssues'
59
+ | 'governedVocabularyHistoryRequired'
60
+ > => ({
61
+ ...(input.governedVocabularyHistories
62
+ ? {
63
+ governedVocabularyHistoryByTransitionId: new Map(
64
+ input.governedVocabularyHistories.map((history) => [
65
+ history.transitionId,
66
+ history,
67
+ ])
68
+ ),
69
+ }
70
+ : {}),
71
+ ...(input.governedVocabularyHistoryIssues
72
+ ? {
73
+ governedVocabularyHistoryIssues:
74
+ input.governedVocabularyHistoryIssues.map((issue) => ({
75
+ message: issue.message,
76
+ path: issue.path,
77
+ ...(issue.transitionId === undefined
78
+ ? {}
79
+ : { transitionId: issue.transitionId }),
80
+ })),
81
+ }
82
+ : {}),
83
+ ...(input.governedVocabularyHistoryRequired === undefined
84
+ ? {}
85
+ : {
86
+ governedVocabularyHistoryRequired:
87
+ input.governedVocabularyHistoryRequired,
88
+ }),
89
+ });
90
+
91
+ export const buildProjectContext = (
92
+ input: ProjectAwareRuleInput
93
+ ): ProjectContext => ({
54
94
  ...(input.authoredMcpSurfaceBindingSets
55
95
  ? { authoredMcpSurfaceBindingSets: input.authoredMcpSurfaceBindingSets }
56
96
  : {}),
@@ -98,6 +138,7 @@ const buildProjectContext = (input: ProjectAwareRuleInput): ProjectContext => ({
98
138
  ),
99
139
  }
100
140
  : {}),
141
+ ...buildGovernedHistoryContext(input),
101
142
  ...(input.publicWorkspaces
102
143
  ? { publicWorkspaces: new Map(Object.entries(input.publicWorkspaces)) }
103
144
  : {}),
@@ -151,10 +192,11 @@ export function wrapRule(
151
192
  RuleOutput
152
193
  >['examples'],
153
194
  implementation: (input: ProjectAwareRuleInput) => {
195
+ const context = buildProjectContext(input);
154
196
  const diagnostics = projectAwareRule.checkWithContext(
155
197
  input.sourceCode,
156
198
  input.filePath,
157
- buildProjectContext(input)
199
+ context
158
200
  );
159
201
  return Result.ok({ diagnostics: [...diagnostics] });
160
202
  },