@ontrails/warden 1.0.0-beta.19 → 1.0.0-beta.21

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 CHANGED
@@ -1,5 +1,42 @@
1
1
  # @ontrails/warden
2
2
 
3
+ ## 1.0.0-beta.21
4
+
5
+ ### Minor Changes
6
+
7
+ - 5be032c: Add the repo-local `public-export-example-coverage` Warden rule (and its `publicExportExampleCoverageTrail` wrapper), graduating `scripts/check-public-api-examples.ts` into governed rule coverage. The rule anchors to this repository's five public surface package barrels via the rule module's own on-disk location, so it stays silent in consumer repositories.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [99523f2]
12
+ - Updated dependencies [3caa263]
13
+ - @ontrails/core@1.0.0-beta.21
14
+ - @ontrails/permits@1.0.0-beta.21
15
+ - @ontrails/topographer@1.0.0-beta.21
16
+ - @ontrails/adapter-kit@1.0.0-beta.21
17
+ - @ontrails/cli@1.0.0-beta.21
18
+ - @ontrails/store@1.0.0-beta.21
19
+
20
+ ## 1.0.0-beta.20
21
+
22
+ ### Minor Changes
23
+
24
+ - 8bc0708: Add surface facet coherence diagnostics for selector overlap, visibility widening acknowledgements, dynamic selectors, and description hygiene.
25
+
26
+ ### Patch Changes
27
+
28
+ - 851a2a3: Derive trail caller and blaze input types from the authored input schema while keeping one public input contract.
29
+ - 6901776: Add a Warden rule and safe-fix metadata for rewriting retired cross vocabulary to compose vocabulary.
30
+ - Updated dependencies [851a2a3]
31
+ - Updated dependencies [eee1307]
32
+ - Updated dependencies [b248d4a]
33
+ - @ontrails/core@1.0.0-beta.20
34
+ - @ontrails/store@1.0.0-beta.20
35
+ - @ontrails/topographer@1.0.0-beta.20
36
+ - @ontrails/adapter-kit@1.0.0-beta.20
37
+ - @ontrails/cli@1.0.0-beta.20
38
+ - @ontrails/permits@1.0.0-beta.20
39
+
3
40
  ## 1.0.0-beta.19
4
41
 
5
42
  ### Major Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/warden",
3
- "version": "1.0.0-beta.19",
3
+ "version": "1.0.0-beta.21",
4
4
  "bin": {
5
5
  "warden": "./bin/warden.ts"
6
6
  },
@@ -28,20 +28,20 @@
28
28
  "clean": "rm -rf dist *.tsbuildinfo"
29
29
  },
30
30
  "dependencies": {
31
- "@ontrails/adapter-kit": "^1.0.0-beta.19",
32
- "@ontrails/cli": "^1.0.0-beta.19",
33
- "@ontrails/permits": "^1.0.0-beta.19",
34
- "@ontrails/store": "^1.0.0-beta.19",
31
+ "@ontrails/adapter-kit": "^1.0.0-beta.21",
32
+ "@ontrails/cli": "^1.0.0-beta.21",
33
+ "@ontrails/permits": "^1.0.0-beta.21",
34
+ "@ontrails/store": "^1.0.0-beta.21",
35
35
  "oxc-parser": "^0.121.0",
36
36
  "oxc-resolver": "11.19.1",
37
37
  "zod": "^4.3.5"
38
38
  },
39
39
  "devDependencies": {
40
- "@ontrails/config": "^1.0.0-beta.19",
41
- "@ontrails/testing": "^1.0.0-beta.19"
40
+ "@ontrails/config": "^1.0.0-beta.21",
41
+ "@ontrails/testing": "^1.0.0-beta.21"
42
42
  },
43
43
  "peerDependencies": {
44
- "@ontrails/core": "^1.0.0-beta.19",
45
- "@ontrails/topographer": "^1.0.0-beta.19"
44
+ "@ontrails/core": "^1.0.0-beta.21",
45
+ "@ontrails/topographer": "^1.0.0-beta.21"
46
46
  }
47
47
  }
package/src/index.ts CHANGED
@@ -188,6 +188,7 @@ export {
188
188
  noLegacyLayerImportsTrail,
189
189
  noNativeErrorResultTrail,
190
190
  noRedundantResultErrorWrapTrail,
191
+ noRetiredCrossVocabularyTrail,
191
192
  noSyncResultAssumptionTrail,
192
193
  noThrowInDetourRecoverTrail,
193
194
  noThrowInImplementationTrail,
@@ -199,6 +200,7 @@ export {
199
200
  permitGovernanceTrail,
200
201
  preferSchemaInferenceTrail,
201
202
  projectAwareRuleInput,
203
+ publicExportExampleCoverageTrail,
202
204
  publicInternalDeepImportsTrail,
203
205
  publicOutputSchemaTrail,
204
206
  publicUnionOutputDiscriminantsTrail,
@@ -214,6 +216,7 @@ export {
214
216
  scheduledDestroyIntentTrail,
215
217
  signalGraphCoachingTrail,
216
218
  staticResourceAccessorPreferenceTrail,
219
+ surfaceFacetCoherenceTrail,
217
220
  unmaterializedActivationSourceTrail,
218
221
  topoAwareRuleInput,
219
222
  unreachableDetourShadowingTrail,
@@ -22,6 +22,7 @@ import { noLegacyLayerImports } from './no-legacy-layer-imports.js';
22
22
  import { noDirectImplementationCall } from './no-direct-implementation-call.js';
23
23
  import { noNativeErrorResult } from './no-native-error-result.js';
24
24
  import { noRedundantResultErrorWrap } from './no-redundant-result-error-wrap.js';
25
+ import { noRetiredCrossVocabulary } from './no-retired-cross-vocabulary.js';
25
26
  import { noSyncResultAssumption } from './no-sync-result-assumption.js';
26
27
  import { noThrowInDetourRecover } from './no-throw-in-detour-recover.js';
27
28
  import { noThrowInImplementation } from './no-throw-in-implementation.js';
@@ -31,6 +32,7 @@ import { orphanedSignal } from './orphaned-signal.js';
31
32
  import { ownerProjectionParity } from './owner-projection-parity.js';
32
33
  import { permitGovernance } from './permit-governance.js';
33
34
  import { preferSchemaInference } from './prefer-schema-inference.js';
35
+ import { publicExportExampleCoverage } from './public-export-example-coverage.js';
34
36
  import { publicInternalDeepImports } from './public-internal-deep-imports.js';
35
37
  import { publicOutputSchema } from './public-output-schema.js';
36
38
  import { publicUnionOutputDiscriminants } from './public-union-output-discriminants.js';
@@ -44,6 +46,7 @@ import { resourceMockCoverage } from './resource-mock-coverage.js';
44
46
  import { scheduledDestroyIntent } from './scheduled-destroy-intent.js';
45
47
  import { signalGraphCoaching } from './signal-graph-coaching.js';
46
48
  import { staticResourceAccessorPreference } from './static-resource-accessor-preference.js';
49
+ import { surfaceFacetCoherence } from './surface-facet-coherence.js';
47
50
  import {
48
51
  forkWithoutPreservedBlaze,
49
52
  markerSchemaUnsupported,
@@ -124,6 +127,7 @@ export { noLegacyLayerImports } from './no-legacy-layer-imports.js';
124
127
  export { noDirectImplementationCall } from './no-direct-implementation-call.js';
125
128
  export { noNativeErrorResult } from './no-native-error-result.js';
126
129
  export { noRedundantResultErrorWrap } from './no-redundant-result-error-wrap.js';
130
+ export { noRetiredCrossVocabulary } from './no-retired-cross-vocabulary.js';
127
131
  export { noSyncResultAssumption } from './no-sync-result-assumption.js';
128
132
  export { implementationReturnsResult } from './implementation-returns-result.js';
129
133
  export { noThrowInDetourRecover } from './no-throw-in-detour-recover.js';
@@ -145,6 +149,7 @@ export { resourceMockCoverage } from './resource-mock-coverage.js';
145
149
  export { scheduledDestroyIntent } from './scheduled-destroy-intent.js';
146
150
  export { signalGraphCoaching } from './signal-graph-coaching.js';
147
151
  export { staticResourceAccessorPreference } from './static-resource-accessor-preference.js';
152
+ export { surfaceFacetCoherence } from './surface-facet-coherence.js';
148
153
  export {
149
154
  forkWithoutPreservedBlaze,
150
155
  markerSchemaUnsupported,
@@ -186,6 +191,7 @@ export const wardenRules: ReadonlyMap<string, WardenRule> = new Map<
186
191
  [onReferencesExist.name, onReferencesExist],
187
192
  [orphanedSignal.name, orphanedSignal],
188
193
  [ownerProjectionParity.name, ownerProjectionParity],
194
+ [publicExportExampleCoverage.name, publicExportExampleCoverage],
189
195
  [publicInternalDeepImports.name, publicInternalDeepImports],
190
196
  [resourceDeclarations.name, resourceDeclarations],
191
197
  [readIntentFires.name, readIntentFires],
@@ -196,6 +202,7 @@ export const wardenRules: ReadonlyMap<string, WardenRule> = new Map<
196
202
  [resourceMockCoverage.name, resourceMockCoverage],
197
203
  [preferSchemaInference.name, preferSchemaInference],
198
204
  [staticResourceAccessorPreference.name, staticResourceAccessorPreference],
205
+ [surfaceFacetCoherence.name, surfaceFacetCoherence],
199
206
  [validDescribeRefs.name, validDescribeRefs],
200
207
  [noDevPermitInSource.name, noDevPermitInSource],
201
208
  [noDestructuredCompose.name, noDestructuredCompose],
@@ -203,6 +210,7 @@ export const wardenRules: ReadonlyMap<string, WardenRule> = new Map<
203
210
  [noLegacyLayerImports.name, noLegacyLayerImports],
204
211
  [noNativeErrorResult.name, noNativeErrorResult],
205
212
  [noRedundantResultErrorWrap.name, noRedundantResultErrorWrap],
213
+ [noRetiredCrossVocabulary.name, noRetiredCrossVocabulary],
206
214
  [noSyncResultAssumption.name, noSyncResultAssumption],
207
215
  [implementationReturnsResult.name, implementationReturnsResult],
208
216
  [noThrowInDetourRecover.name, noThrowInDetourRecover],
@@ -87,6 +87,7 @@ const concernByRuleName: Partial<Record<string, WardenRuleConcern>> = {
87
87
  'no-direct-implementation-call': 'composition',
88
88
  'no-native-error-result': 'results',
89
89
  'no-redundant-result-error-wrap': 'results',
90
+ 'no-retired-cross-vocabulary': 'composition',
90
91
  'no-sync-result-assumption': 'results',
91
92
  'no-throw-in-detour-recover': 'results',
92
93
  'no-throw-in-implementation': 'results',
@@ -104,6 +105,7 @@ const concernByRuleName: Partial<Record<string, WardenRuleConcern>> = {
104
105
  'scheduled-destroy-intent': 'lifecycle',
105
106
  'signal-graph-coaching': 'signals',
106
107
  'static-resource-accessor-preference': 'resources',
108
+ 'surface-facet-coherence': 'meta',
107
109
  'unmaterialized-activation-source': 'lifecycle',
108
110
  'valid-detour-contract': 'results',
109
111
  'version-gap': 'lifecycle',
@@ -295,6 +297,18 @@ const builtinWardenRuleMetadataInput = {
295
297
  'Result error pass-throughs preserve the original Result boundary.',
296
298
  tier: 'source-static',
297
299
  },
300
+ 'no-retired-cross-vocabulary': {
301
+ fix: { class: 'term-rewrite', safety: 'safe' },
302
+ invariant:
303
+ 'Retired cross composition vocabulary does not remain in downstream source after the beta.19 compose cutover.',
304
+ lifecycle: {
305
+ retireWhen:
306
+ 'Downstream beta.19 cross-to-compose migration window closes and supported apps have adopted compose vocabulary.',
307
+ state: 'temporary',
308
+ },
309
+ scope: 'external',
310
+ tier: 'source-static',
311
+ },
298
312
  'no-sync-result-assumption': {
299
313
  ...durableExternal,
300
314
  invariant:
@@ -391,6 +405,12 @@ const builtinWardenRuleMetadataInput = {
391
405
  scope: 'advisory',
392
406
  tier: 'source-static',
393
407
  },
408
+ 'public-export-example-coverage': {
409
+ ...durableRepoLocal,
410
+ invariant:
411
+ 'Public API barrel exports carry leading @example TSDoc coverage.',
412
+ tier: 'source-static',
413
+ },
394
414
  'public-internal-deep-imports': {
395
415
  invariant: 'Cross-package imports stay on package-owned public exports.',
396
416
  lifecycle: { state: 'durable' },
@@ -513,6 +533,27 @@ const builtinWardenRuleMetadataInput = {
513
533
  scope: 'advisory',
514
534
  tier: 'source-static',
515
535
  },
536
+ 'surface-facet-coherence': {
537
+ ...durableExternal,
538
+ guidance: {
539
+ docs: [
540
+ {
541
+ label: 'Surface Facets ADR',
542
+ path: 'docs/adr/drafts/20260603-surface-facets-shape-dense-topos.md',
543
+ },
544
+ ],
545
+ steps: [
546
+ 'Keep facet selectors as explicit string literals or literal arrays when possible.',
547
+ 'Ensure each public trail belongs to one facet owner.',
548
+ 'Record explicit visibility-widening acceptance and stable-description metadata when a facet intentionally widens visibility.',
549
+ ],
550
+ summary:
551
+ 'Keep surface facet maps reviewable before they reach MCP projection.',
552
+ },
553
+ invariant:
554
+ 'Surface facet maps avoid selector overlap, hidden visibility widening, and drift-prone dynamic selectors.',
555
+ tier: 'source-static',
556
+ },
516
557
  'unmaterialized-activation-source': {
517
558
  ...durableExternal,
518
559
  invariant:
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Flags retired `cross` composition vocabulary after the beta.19 compose cutover.
3
+ *
4
+ * Exact authored terms that have a mechanical successor carry safe
5
+ * `term-rewrite` edits. Larger identifiers and type prefixes remain
6
+ * review-required because the text-only rule cannot prove the intended symbol
7
+ * boundary.
8
+ */
9
+ import { resolve, sep } from 'node:path';
10
+
11
+ import type { WardenDiagnostic, WardenFix, WardenRule } from './types.js';
12
+
13
+ const RULE_NAME = 'no-retired-cross-vocabulary';
14
+
15
+ const SAFE_REWRITES = [
16
+ { from: 'ctx.cross', to: 'ctx.compose' },
17
+ { from: 'crossInput', to: 'composeInput' },
18
+ { from: 'crosses', to: 'composes' },
19
+ ] as const;
20
+
21
+ const REVIEW_TERMS = ['Cross', 'cross'] as const;
22
+
23
+ const IDENTIFIER_CHAR = /[$0-9A-Z_a-z]/u;
24
+
25
+ const ALLOWED_PATH_SUFFIXES: readonly string[] = [
26
+ '/docs/migration/cross-to-compose.md',
27
+ '/docs/releases/beta15-to-beta19.md',
28
+ '/docs/adr/0049-composition-is-compose-not-cross.md',
29
+ '/packages/warden/src/rules/no-retired-cross-vocabulary.ts',
30
+ '/packages/warden/src/rules/metadata.ts',
31
+ '/packages/warden/src/trails/no-retired-cross-vocabulary.trail.ts',
32
+ '/scripts/vocab-cutover-rewrite.ts',
33
+ ];
34
+
35
+ interface SafeRewriteMatch {
36
+ readonly from: string;
37
+ readonly index: number;
38
+ readonly to: string;
39
+ }
40
+
41
+ interface ReviewMatch {
42
+ readonly index: number;
43
+ readonly term: (typeof REVIEW_TERMS)[number];
44
+ }
45
+
46
+ const normalizePath = (filePath: string): string =>
47
+ resolve(filePath).split(sep).join('/');
48
+
49
+ const isAllowedFile = (filePath: string): boolean => {
50
+ const normalized = normalizePath(filePath);
51
+ return ALLOWED_PATH_SUFFIXES.some((suffix) => normalized.endsWith(suffix));
52
+ };
53
+
54
+ const isIdentifierChar = (value: string): boolean =>
55
+ value !== '' && IDENTIFIER_CHAR.test(value);
56
+
57
+ const isReviewPrefix = (
58
+ sourceCode: string,
59
+ index: number,
60
+ term: string
61
+ ): boolean => !(term === 'cross' && sourceCode.startsWith('crosses', index));
62
+
63
+ const isStandaloneSpan = (
64
+ sourceCode: string,
65
+ start: number,
66
+ end: number
67
+ ): boolean => {
68
+ const before = start === 0 ? '' : (sourceCode[start - 1] ?? '');
69
+ const after = sourceCode[end] ?? '';
70
+ return !(isIdentifierChar(before) || isIdentifierChar(after));
71
+ };
72
+
73
+ const findSafeRewriteMatches = (
74
+ sourceCode: string
75
+ ): readonly SafeRewriteMatch[] => {
76
+ const matches: SafeRewriteMatch[] = [];
77
+ for (const rewrite of SAFE_REWRITES) {
78
+ let fromIndex = 0;
79
+ while (fromIndex < sourceCode.length) {
80
+ const index = sourceCode.indexOf(rewrite.from, fromIndex);
81
+ if (index === -1) {
82
+ break;
83
+ }
84
+ const end = index + rewrite.from.length;
85
+ if (isStandaloneSpan(sourceCode, index, end)) {
86
+ matches.push({ ...rewrite, index });
87
+ }
88
+ fromIndex = end;
89
+ }
90
+ }
91
+ return matches.toSorted((a, b) => a.index - b.index);
92
+ };
93
+
94
+ const findReviewMatches = (sourceCode: string): readonly ReviewMatch[] => {
95
+ const safeSpans = findSafeRewriteMatches(sourceCode).map((match) => ({
96
+ end: match.index + match.from.length,
97
+ start: match.index,
98
+ }));
99
+ const isInsideSafeSpan = (index: number): boolean =>
100
+ safeSpans.some((span) => index >= span.start && index < span.end);
101
+
102
+ const matches: ReviewMatch[] = [];
103
+ for (const term of REVIEW_TERMS) {
104
+ let fromIndex = 0;
105
+ while (fromIndex < sourceCode.length) {
106
+ const index = sourceCode.indexOf(term, fromIndex);
107
+ if (index === -1) {
108
+ break;
109
+ }
110
+ const end = index + term.length;
111
+ if (!isInsideSafeSpan(index)) {
112
+ const before = index === 0 ? '' : (sourceCode[index - 1] ?? '');
113
+ const after = sourceCode[end] ?? '';
114
+ if (
115
+ !isIdentifierChar(before) &&
116
+ isIdentifierChar(after) &&
117
+ isReviewPrefix(sourceCode, index, term)
118
+ ) {
119
+ matches.push({ index, term });
120
+ }
121
+ }
122
+ fromIndex = end;
123
+ }
124
+ }
125
+ return matches.toSorted((a, b) => a.index - b.index);
126
+ };
127
+
128
+ const lineForOffset = (sourceCode: string, offset: number): number => {
129
+ let line = 1;
130
+ for (let i = 0; i < offset; i += 1) {
131
+ if (sourceCode.codePointAt(i) === 10) {
132
+ line += 1;
133
+ }
134
+ }
135
+ return line;
136
+ };
137
+
138
+ const safeFix = (match: SafeRewriteMatch): WardenFix => ({
139
+ class: 'term-rewrite',
140
+ edits: [
141
+ {
142
+ end: match.index + match.from.length,
143
+ replacement: match.to,
144
+ start: match.index,
145
+ },
146
+ ],
147
+ reason: `Retired composition vocabulary '${match.from}' has a mechanical beta.19 replacement '${match.to}'.`,
148
+ safety: 'safe',
149
+ });
150
+
151
+ const reviewFix = (term: string): WardenFix => ({
152
+ class: 'term-rewrite',
153
+ reason: `Retired composition vocabulary '${term}' appears in a larger or ambiguous form. Review before migrating to compose vocabulary.`,
154
+ safety: 'review',
155
+ });
156
+
157
+ const safeMessage = (match: SafeRewriteMatch): string =>
158
+ `Retired composition vocabulary '${match.from}' should be '${match.to}' after the beta.19 compose cutover.`;
159
+
160
+ const reviewMessage = (term: string): string =>
161
+ `Retired composition vocabulary '${term}' needs review before migrating to compose vocabulary.`;
162
+
163
+ export const noRetiredCrossVocabulary: WardenRule = {
164
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
165
+ if (isAllowedFile(filePath)) {
166
+ return [];
167
+ }
168
+
169
+ const reviewMatches = findReviewMatches(sourceCode);
170
+ if (reviewMatches.length > 0) {
171
+ return reviewMatches.map((match) => ({
172
+ filePath,
173
+ fix: reviewFix(match.term),
174
+ line: lineForOffset(sourceCode, match.index),
175
+ message: reviewMessage(match.term),
176
+ rule: RULE_NAME,
177
+ severity: 'error',
178
+ }));
179
+ }
180
+
181
+ return findSafeRewriteMatches(sourceCode).map((match) => ({
182
+ filePath,
183
+ fix: safeFix(match),
184
+ line: lineForOffset(sourceCode, match.index),
185
+ message: safeMessage(match),
186
+ rule: RULE_NAME,
187
+ severity: 'error',
188
+ }));
189
+ },
190
+ description:
191
+ 'Disallow retired cross composition vocabulary after the beta.19 compose cutover.',
192
+ name: RULE_NAME,
193
+ severity: 'error',
194
+ };