@ontrails/warden 1.0.0-beta.4 → 1.0.0-beta.42

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 (279) hide show
  1. package/CHANGELOG.md +837 -13
  2. package/README.md +133 -26
  3. package/bin/warden.ts +51 -0
  4. package/package.json +27 -6
  5. package/src/adapter-check.ts +136 -0
  6. package/src/cli.ts +1570 -105
  7. package/src/command.ts +986 -0
  8. package/src/config.ts +193 -0
  9. package/src/draft.ts +22 -0
  10. package/src/drift.ts +233 -23
  11. package/src/fix.ts +126 -0
  12. package/src/formatters.ts +78 -13
  13. package/src/guide.ts +245 -0
  14. package/src/index.ts +247 -15
  15. package/src/project-context.ts +446 -0
  16. package/src/project-rules.ts +290 -0
  17. package/src/resolve.ts +531 -0
  18. package/src/rules/activation-orphan.ts +97 -0
  19. package/src/rules/captured-kernel.ts +375 -0
  20. package/src/rules/circular-refs.ts +150 -0
  21. package/src/rules/cli-command-route-coherence.ts +177 -0
  22. package/src/rules/composes-declarations.ts +839 -0
  23. package/src/rules/context-no-surface-types.ts +79 -15
  24. package/src/rules/dead-internal-trail.ts +161 -0
  25. package/src/rules/dead-public-trail.ts +258 -0
  26. package/src/rules/draft-file-marking.ts +155 -0
  27. package/src/rules/draft-visible-debt.ts +82 -0
  28. package/src/rules/duplicate-exported-symbol.ts +211 -0
  29. package/src/rules/duplicate-public-contract.ts +137 -0
  30. package/src/rules/entity-exists.ts +254 -0
  31. package/src/rules/entity-ids.ts +15 -0
  32. package/src/rules/error-mapping-completeness.ts +290 -0
  33. package/src/rules/example-valid.ts +395 -0
  34. package/src/rules/fires-declarations.ts +740 -0
  35. package/src/rules/governed-symbol-residue.ts +438 -0
  36. package/src/rules/implementation-returns-result.ts +1409 -166
  37. package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
  38. package/src/rules/incomplete-crud.ts +583 -0
  39. package/src/rules/index.ts +277 -10
  40. package/src/rules/intent-propagation.ts +125 -0
  41. package/src/rules/layer-field-name-drift.ts +102 -0
  42. package/src/rules/library-projection-coherence.ts +100 -0
  43. package/src/rules/metadata.ts +871 -0
  44. package/src/rules/missing-reconcile.ts +97 -0
  45. package/src/rules/missing-visibility.ts +111 -0
  46. package/src/rules/no-destructured-compose.ts +196 -0
  47. package/src/rules/no-dev-permit-in-source.ts +99 -0
  48. package/src/rules/no-direct-implementation-call.ts +12 -7
  49. package/src/rules/no-legacy-cli-alias-export.ts +247 -0
  50. package/src/rules/no-legacy-layer-imports.ts +211 -0
  51. package/src/rules/no-native-error-result.ts +118 -0
  52. package/src/rules/no-redundant-result-error-wrap.ts +384 -0
  53. package/src/rules/no-retired-cross-vocabulary.ts +204 -0
  54. package/src/rules/no-sync-result-assumption.ts +1141 -98
  55. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  56. package/src/rules/no-throw-in-implementation.ts +15 -8
  57. package/src/rules/no-top-level-surface.ts +371 -0
  58. package/src/rules/on-references-exist.ts +194 -0
  59. package/src/rules/orphaned-signal.ts +149 -0
  60. package/src/rules/owner-projection-parity.ts +143 -0
  61. package/src/rules/permit-governance.ts +25 -0
  62. package/src/rules/public-export-example-coverage.ts +573 -0
  63. package/src/rules/public-internal-deep-imports.ts +456 -0
  64. package/src/rules/public-output-schema.ts +29 -0
  65. package/src/rules/public-union-output-discriminants.ts +150 -0
  66. package/src/rules/read-intent-fires.ts +188 -0
  67. package/src/rules/reference-exists.ts +97 -0
  68. package/src/rules/registry-names.ts +167 -0
  69. package/src/rules/resolved-import-boundary.ts +146 -0
  70. package/src/rules/resource-declarations.ts +697 -0
  71. package/src/rules/resource-exists.ts +181 -0
  72. package/src/rules/resource-id-grammar.ts +65 -0
  73. package/src/rules/resource-mock-coverage.ts +115 -0
  74. package/src/rules/retired-vocabulary.ts +633 -0
  75. package/src/rules/scan.ts +38 -25
  76. package/src/rules/scheduled-destroy-intent.ts +44 -0
  77. package/src/rules/signal-graph-coaching.ts +220 -0
  78. package/src/rules/source/composition.ts +165 -0
  79. package/src/rules/source/drafts.ts +164 -0
  80. package/src/rules/source/entities.ts +618 -0
  81. package/src/rules/source/pragmas.ts +45 -0
  82. package/src/rules/source/resources.ts +64 -0
  83. package/src/rules/source/signals.ts +397 -0
  84. package/src/rules/source/stores.ts +310 -0
  85. package/src/rules/specs.ts +9 -5
  86. package/src/rules/static-resource-accessor-preference.ts +653 -0
  87. package/src/rules/surface-overlay-coherence.ts +262 -0
  88. package/src/rules/surface-trailhead-coherence.ts +366 -0
  89. package/src/rules/trail-fork-coaching.ts +625 -0
  90. package/src/rules/trail-versioning-source.ts +1076 -0
  91. package/src/rules/trail-versioning-topo.ts +172 -0
  92. package/src/rules/trailhead-override-divergence.ts +356 -0
  93. package/src/rules/types.ts +354 -8
  94. package/src/rules/unmaterialized-activation-source.ts +85 -0
  95. package/src/rules/unreachable-detour-shadowing.ts +339 -0
  96. package/src/rules/valid-describe-refs.ts +162 -32
  97. package/src/rules/valid-detour-contract.ts +78 -0
  98. package/src/rules/warden-export-symmetry.ts +540 -0
  99. package/src/rules/warden-rules-use-ast.ts +1109 -0
  100. package/src/rules/webhook-route-collision.ts +306 -0
  101. package/src/trails/activation-orphan.trail.ts +84 -0
  102. package/src/trails/captured-kernel.trail.ts +108 -0
  103. package/src/trails/circular-refs.trail.ts +29 -0
  104. package/src/trails/cli-command-route-coherence.trail.ts +47 -0
  105. package/src/trails/composes-declarations.trail.ts +22 -0
  106. package/src/trails/context-no-surface-types.trail.ts +21 -0
  107. package/src/trails/dead-internal-trail.trail.ts +26 -0
  108. package/src/trails/dead-public-trail.trail.ts +31 -0
  109. package/src/trails/deprecation-without-guidance.trail.ts +21 -0
  110. package/src/trails/draft-file-marking.trail.ts +16 -0
  111. package/src/trails/draft-visible-debt.trail.ts +16 -0
  112. package/src/trails/duplicate-exported-symbol.trail.ts +48 -0
  113. package/src/trails/duplicate-public-contract.trail.ts +47 -0
  114. package/src/trails/entity-exists.trail.ts +21 -0
  115. package/src/trails/error-mapping-completeness.trail.ts +30 -0
  116. package/src/trails/example-valid.trail.ts +25 -0
  117. package/src/trails/fires-declarations.trail.ts +23 -0
  118. package/src/trails/fork-without-preserved-implementation.trail.ts +31 -0
  119. package/src/trails/governed-symbol-residue.trail.ts +24 -0
  120. package/src/trails/implementation-returns-result.trail.ts +20 -0
  121. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  122. package/src/trails/incomplete-crud.trail.ts +39 -0
  123. package/src/trails/index.ts +89 -0
  124. package/src/trails/intent-propagation.trail.ts +30 -0
  125. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  126. package/src/trails/library-projection-coherence.trail.ts +43 -0
  127. package/src/trails/marker-schema-unsupported.trail.ts +23 -0
  128. package/src/trails/missing-reconcile.trail.ts +33 -0
  129. package/src/trails/missing-visibility.trail.ts +22 -0
  130. package/src/trails/no-destructured-compose.trail.ts +44 -0
  131. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  132. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  133. package/src/trails/no-legacy-cli-alias-export.trail.ts +41 -0
  134. package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
  135. package/src/trails/no-native-error-result.trail.ts +18 -0
  136. package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
  137. package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
  138. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  139. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  140. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  141. package/src/trails/no-top-level-surface.trail.ts +43 -0
  142. package/src/trails/on-references-exist.trail.ts +21 -0
  143. package/src/trails/orphaned-signal.trail.ts +36 -0
  144. package/src/trails/owner-projection-parity.trail.ts +26 -0
  145. package/src/trails/pending-force.trail.ts +21 -0
  146. package/src/trails/permit-governance.trail.ts +51 -0
  147. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  148. package/src/trails/public-export-example-coverage.trail.ts +16 -0
  149. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  150. package/src/trails/public-output-schema.trail.ts +55 -0
  151. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  152. package/src/trails/read-intent-fires.trail.ts +20 -0
  153. package/src/trails/reference-exists.trail.ts +25 -0
  154. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  155. package/src/trails/resource-declarations.trail.ts +25 -0
  156. package/src/trails/resource-exists.trail.ts +27 -0
  157. package/src/trails/resource-id-grammar.trail.ts +39 -0
  158. package/src/trails/resource-mock-coverage.trail.ts +40 -0
  159. package/src/trails/run.ts +160 -0
  160. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  161. package/src/trails/schema.ts +237 -0
  162. package/src/trails/signal-graph-coaching.trail.ts +77 -0
  163. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  164. package/src/trails/surface-overlay-coherence.trail.ts +24 -0
  165. package/src/trails/surface-trailhead-coherence.trail.ts +25 -0
  166. package/src/trails/topo.ts +6 -0
  167. package/src/trails/trail-fork-coaching.trail.ts +42 -0
  168. package/src/trails/trailhead-override-divergence.trail.ts +47 -0
  169. package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
  170. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  171. package/src/trails/valid-describe-refs.trail.ts +18 -0
  172. package/src/trails/valid-detour-contract.trail.ts +71 -0
  173. package/src/trails/version-gap.trail.ts +35 -0
  174. package/src/trails/version-pinned-compose.trail.ts +23 -0
  175. package/src/trails/version-without-examples.trail.ts +38 -0
  176. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  177. package/src/trails/warden-rules-use-ast.trail.ts +64 -0
  178. package/src/trails/webhook-route-collision.trail.ts +50 -0
  179. package/src/trails/wrap-rule.ts +224 -0
  180. package/src/workspaces.ts +199 -0
  181. package/.turbo/turbo-build.log +0 -1
  182. package/.turbo/turbo-lint.log +0 -3
  183. package/.turbo/turbo-typecheck.log +0 -1
  184. package/dist/cli.d.ts +0 -46
  185. package/dist/cli.d.ts.map +0 -1
  186. package/dist/cli.js +0 -218
  187. package/dist/cli.js.map +0 -1
  188. package/dist/drift.d.ts +0 -26
  189. package/dist/drift.d.ts.map +0 -1
  190. package/dist/drift.js +0 -27
  191. package/dist/drift.js.map +0 -1
  192. package/dist/formatters.d.ts +0 -29
  193. package/dist/formatters.d.ts.map +0 -1
  194. package/dist/formatters.js +0 -87
  195. package/dist/formatters.js.map +0 -1
  196. package/dist/index.d.ts +0 -26
  197. package/dist/index.d.ts.map +0 -1
  198. package/dist/index.js +0 -26
  199. package/dist/index.js.map +0 -1
  200. package/dist/rules/ast.d.ts +0 -41
  201. package/dist/rules/ast.d.ts.map +0 -1
  202. package/dist/rules/ast.js +0 -161
  203. package/dist/rules/ast.js.map +0 -1
  204. package/dist/rules/context-no-surface-types.d.ts +0 -12
  205. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  206. package/dist/rules/context-no-surface-types.js +0 -96
  207. package/dist/rules/context-no-surface-types.js.map +0 -1
  208. package/dist/rules/implementation-returns-result.d.ts +0 -13
  209. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  210. package/dist/rules/implementation-returns-result.js +0 -277
  211. package/dist/rules/implementation-returns-result.js.map +0 -1
  212. package/dist/rules/index.d.ts +0 -15
  213. package/dist/rules/index.d.ts.map +0 -1
  214. package/dist/rules/index.js +0 -34
  215. package/dist/rules/index.js.map +0 -1
  216. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  217. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  218. package/dist/rules/no-direct-impl-in-route.js +0 -47
  219. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  220. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  221. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  222. package/dist/rules/no-direct-implementation-call.js +0 -39
  223. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  224. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  225. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  226. package/dist/rules/no-sync-result-assumption.js +0 -98
  227. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  228. package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
  229. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  230. package/dist/rules/no-throw-in-detour-target.js +0 -87
  231. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  232. package/dist/rules/no-throw-in-implementation.d.ts +0 -9
  233. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  234. package/dist/rules/no-throw-in-implementation.js +0 -34
  235. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  236. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  237. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  238. package/dist/rules/prefer-schema-inference.js +0 -86
  239. package/dist/rules/prefer-schema-inference.js.map +0 -1
  240. package/dist/rules/scan.d.ts +0 -8
  241. package/dist/rules/scan.d.ts.map +0 -1
  242. package/dist/rules/scan.js +0 -32
  243. package/dist/rules/scan.js.map +0 -1
  244. package/dist/rules/specs.d.ts +0 -29
  245. package/dist/rules/specs.d.ts.map +0 -1
  246. package/dist/rules/specs.js +0 -192
  247. package/dist/rules/specs.js.map +0 -1
  248. package/dist/rules/structure.d.ts +0 -13
  249. package/dist/rules/structure.d.ts.map +0 -1
  250. package/dist/rules/structure.js +0 -142
  251. package/dist/rules/structure.js.map +0 -1
  252. package/dist/rules/types.d.ts +0 -52
  253. package/dist/rules/types.d.ts.map +0 -1
  254. package/dist/rules/types.js +0 -2
  255. package/dist/rules/types.js.map +0 -1
  256. package/dist/rules/valid-describe-refs.d.ts +0 -7
  257. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  258. package/dist/rules/valid-describe-refs.js +0 -51
  259. package/dist/rules/valid-describe-refs.js.map +0 -1
  260. package/dist/rules/valid-detour-refs.d.ts +0 -6
  261. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  262. package/dist/rules/valid-detour-refs.js +0 -116
  263. package/dist/rules/valid-detour-refs.js.map +0 -1
  264. package/src/__tests__/cli.test.ts +0 -198
  265. package/src/__tests__/drift.test.ts +0 -74
  266. package/src/__tests__/formatters.test.ts +0 -157
  267. package/src/__tests__/implementation-returns-result.test.ts +0 -129
  268. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  269. package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
  270. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  271. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  272. package/src/__tests__/rules.test.ts +0 -227
  273. package/src/__tests__/valid-describe-refs.test.ts +0 -60
  274. package/src/rules/ast.ts +0 -213
  275. package/src/rules/no-direct-impl-in-route.ts +0 -81
  276. package/src/rules/no-throw-in-detour-target.ts +0 -150
  277. package/src/rules/valid-detour-refs.ts +0 -187
  278. package/tsconfig.json +0 -9
  279. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,155 @@
1
+ import { isDraftId } from '@ontrails/core';
2
+
3
+ import { isDraftMarkedFile } from '../draft.js';
4
+ import { collectFrameworkDraftPrefixConstantOffsets } from './source/drafts.js';
5
+ import { hasIgnoreCommentOnLine, splitSourceLines } from './source/pragmas.js';
6
+ import { findStringLiterals, offsetToLine, parse } from '@ontrails/source';
7
+ import type { StringLiteralMatch } from '@ontrails/source';
8
+ import type { WardenDiagnostic, WardenRule } from './types.js';
9
+
10
+ const messageForMissingMarker = (draftId: string): string =>
11
+ `Draft id "${draftId}" appears in source, but the file is not draft-marked. ` +
12
+ 'Rename it with an _draft. prefix or a .draft. trailing segment.';
13
+
14
+ const makeDiagnostic = (
15
+ sourceCode: string,
16
+ filePath: string,
17
+ start: number,
18
+ message: string,
19
+ severity: WardenDiagnostic['severity']
20
+ ): WardenDiagnostic => ({
21
+ filePath,
22
+ line: offsetToLine(sourceCode, start),
23
+ message,
24
+ rule: 'draft-file-marking',
25
+ severity,
26
+ });
27
+
28
+ const collectDraftMatches = (
29
+ sourceCode: string,
30
+ filePath: string,
31
+ ast: NonNullable<ReturnType<typeof parse>>
32
+ ): StringLiteralMatch[] => {
33
+ const frameworkConstantOffsets = collectFrameworkDraftPrefixConstantOffsets(
34
+ ast,
35
+ filePath
36
+ );
37
+ const lines = splitSourceLines(sourceCode);
38
+ return findStringLiterals(ast, (value) => isDraftId(value)).filter(
39
+ (match) => {
40
+ if (frameworkConstantOffsets.has(match.start)) {
41
+ return false;
42
+ }
43
+ if (
44
+ hasIgnoreCommentOnLine(lines, offsetToLine(sourceCode, match.start))
45
+ ) {
46
+ return false;
47
+ }
48
+ return true;
49
+ }
50
+ );
51
+ };
52
+
53
+ const draftMissingMarkerDiagnostic = (
54
+ sourceCode: string,
55
+ filePath: string,
56
+ ast: NonNullable<ReturnType<typeof parse>>
57
+ ): WardenDiagnostic | null => {
58
+ const draftMatches = collectDraftMatches(sourceCode, filePath, ast);
59
+ if (!draftMatches.length || isDraftMarkedFile(filePath)) {
60
+ return null;
61
+ }
62
+
63
+ const [first] = draftMatches;
64
+ if (!first) {
65
+ return null;
66
+ }
67
+
68
+ return makeDiagnostic(
69
+ sourceCode,
70
+ filePath,
71
+ first.start,
72
+ messageForMissingMarker(first.value),
73
+ 'error'
74
+ );
75
+ };
76
+
77
+ const draftMarkedWithoutIdsDiagnostic = (
78
+ filePath: string,
79
+ ast: NonNullable<ReturnType<typeof parse>>
80
+ ): WardenDiagnostic | null => {
81
+ // Deciding whether the file's `_draft.` marker is still warranted is a
82
+ // question about *all* draft ids present in source, not just the unsuppressed
83
+ // ones. Pragma-suppressed ids still justify a draft-marked filename — a user
84
+ // intentionally silencing them has not removed the draft content. We
85
+ // therefore filter only the framework-constant declarations (which are not
86
+ // draft ids at all) and bypass the pragma filter that `collectDraftMatches`
87
+ // applies.
88
+ const frameworkConstantOffsets = collectFrameworkDraftPrefixConstantOffsets(
89
+ ast,
90
+ filePath
91
+ );
92
+ const unsuppressedDraftIds = findStringLiterals(ast, (value) =>
93
+ isDraftId(value)
94
+ ).filter((match) => !frameworkConstantOffsets.has(match.start));
95
+
96
+ if (unsuppressedDraftIds.length > 0) {
97
+ return null;
98
+ }
99
+
100
+ if (!isDraftMarkedFile(filePath)) {
101
+ return null;
102
+ }
103
+
104
+ return {
105
+ filePath,
106
+ line: 1,
107
+ message:
108
+ 'File is draft-marked but no longer contains draft ids. Remove the draft filename marker or finish the promotion cleanup.',
109
+ rule: 'draft-file-marking',
110
+ severity: 'warn',
111
+ };
112
+ };
113
+
114
+ const collectDraftFileMarkingDiagnostics = (
115
+ sourceCode: string,
116
+ filePath: string,
117
+ ast: NonNullable<ReturnType<typeof parse>>
118
+ ): WardenDiagnostic[] => {
119
+ const missingMarkerDiagnostic = draftMissingMarkerDiagnostic(
120
+ sourceCode,
121
+ filePath,
122
+ ast
123
+ );
124
+ if (missingMarkerDiagnostic) {
125
+ return [missingMarkerDiagnostic];
126
+ }
127
+
128
+ const markedWithoutIdsDiagnostic = draftMarkedWithoutIdsDiagnostic(
129
+ filePath,
130
+ ast
131
+ );
132
+ if (markedWithoutIdsDiagnostic) {
133
+ return [markedWithoutIdsDiagnostic];
134
+ }
135
+
136
+ return [];
137
+ };
138
+
139
+ /**
140
+ * Ensures files containing draft ids are visibly marked as draft-bearing files.
141
+ */
142
+ export const draftFileMarking: WardenRule = {
143
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
144
+ const ast = parse(filePath, sourceCode);
145
+ if (!ast) {
146
+ return [];
147
+ }
148
+
149
+ return collectDraftFileMarkingDiagnostics(sourceCode, filePath, ast);
150
+ },
151
+ description:
152
+ 'Require draft-bearing files to use _draft.* or *.draft.* filename markers.',
153
+ name: 'draft-file-marking',
154
+ severity: 'error',
155
+ };
@@ -0,0 +1,82 @@
1
+ import { isDraftId } from '@ontrails/core';
2
+
3
+ import { collectFrameworkDraftPrefixConstantOffsets } from './source/drafts.js';
4
+ import { hasIgnoreCommentOnLine, splitSourceLines } from './source/pragmas.js';
5
+ import { findStringLiterals, offsetToLine, parse } from '@ontrails/source';
6
+ import type { WardenDiagnostic, WardenRule } from './types.js';
7
+
8
+ const createDiagnostic = (
9
+ sourceCode: string,
10
+ filePath: string,
11
+ match: { start: number; value: string }
12
+ ): WardenDiagnostic => ({
13
+ filePath,
14
+ line: offsetToLine(sourceCode, match.start),
15
+ message:
16
+ `Draft id "${match.value}" is still visible debt. ` +
17
+ 'Established surfaces, lock export, and OpenAPI generation will reject it until it is promoted.',
18
+ rule: 'draft-visible-debt',
19
+ severity: 'warn',
20
+ });
21
+
22
+ const isSuppressedMatch = (
23
+ match: { start: number },
24
+ sourceCode: string,
25
+ lines: readonly string[],
26
+ frameworkConstantOffsets: ReadonlySet<number>
27
+ ): boolean =>
28
+ frameworkConstantOffsets.has(match.start) ||
29
+ hasIgnoreCommentOnLine(lines, offsetToLine(sourceCode, match.start));
30
+
31
+ const collectDraftVisibleDebtDiagnostics = (
32
+ sourceCode: string,
33
+ filePath: string,
34
+ ast: NonNullable<ReturnType<typeof parse>>
35
+ ): WardenDiagnostic[] => {
36
+ const frameworkConstantOffsets = collectFrameworkDraftPrefixConstantOffsets(
37
+ ast,
38
+ filePath
39
+ );
40
+ const lines = splitSourceLines(sourceCode);
41
+ const seen = new Set<string>();
42
+
43
+ return findStringLiterals(ast, (value) => isDraftId(value)).flatMap(
44
+ (match) => {
45
+ if (
46
+ isSuppressedMatch(match, sourceCode, lines, frameworkConstantOffsets)
47
+ ) {
48
+ return [];
49
+ }
50
+ const key = `${match.value}:${String(match.start)}`;
51
+ if (seen.has(key)) {
52
+ return [];
53
+ }
54
+ seen.add(key);
55
+ return [createDiagnostic(sourceCode, filePath, match)];
56
+ }
57
+ );
58
+ };
59
+
60
+ /**
61
+ * Warns when draft ids are still present so the debt stays visible during
62
+ * review even when the file is correctly marked.
63
+ *
64
+ * Severity is intentionally `warn`, not `error`. The hard rejection layer for
65
+ * draft state leaking into established outputs is `validateEstablishedTopo` at
66
+ * runtime — it blocks topo compile, surface projection, and lockfile writes.
67
+ * This rule surfaces the debt for human reviewers without duplicating that layer.
68
+ */
69
+ export const draftVisibleDebt: WardenRule = {
70
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
71
+ const ast = parse(filePath, sourceCode);
72
+ if (!ast) {
73
+ return [];
74
+ }
75
+
76
+ return collectDraftVisibleDebtDiagnostics(sourceCode, filePath, ast);
77
+ },
78
+ description:
79
+ 'Warn when draft ids remain in source so the debt stays visible during review.',
80
+ name: 'draft-visible-debt',
81
+ severity: 'warn',
82
+ };
@@ -0,0 +1,211 @@
1
+ import type {
2
+ ProjectAwareWardenRule,
3
+ ProjectContext,
4
+ WardenDiagnostic,
5
+ WardenExportedSymbolDefinition,
6
+ } from './types.js';
7
+
8
+ const RULE_NAME = 'duplicate-exported-symbol';
9
+
10
+ const ALLOWED_DUPLICATE_EXPORT_GROUPS: readonly {
11
+ readonly names: readonly string[];
12
+ readonly reason: string;
13
+ readonly workspaceNames: readonly string[];
14
+ }[] = [
15
+ {
16
+ names: ['surface'],
17
+ reason: 'peer surface packages expose the same conventional entry point',
18
+ workspaceNames: [
19
+ '@ontrails/commander',
20
+ '@ontrails/hono',
21
+ '@ontrails/http',
22
+ '@ontrails/library',
23
+ '@ontrails/mcp',
24
+ ],
25
+ },
26
+ {
27
+ names: ['createApp'],
28
+ reason: 'HTTP native and Hono bindings share app-construction vocabulary',
29
+ workspaceNames: ['@ontrails/hono', '@ontrails/http'],
30
+ },
31
+ {
32
+ names: ['CreateAppOptions'],
33
+ reason: 'HTTP native and Hono bindings share option vocabulary',
34
+ workspaceNames: ['@ontrails/hono', '@ontrails/http'],
35
+ },
36
+ {
37
+ names: ['SurfaceHttpResult'],
38
+ reason: 'HTTP native and Hono bindings share runtime handle vocabulary',
39
+ workspaceNames: ['@ontrails/hono', '@ontrails/http'],
40
+ },
41
+ {
42
+ names: ['AnyTrail', 'Field'],
43
+ reason: '@ontrails/cli mirrors core command-model types for CLI consumers',
44
+ workspaceNames: ['@ontrails/cli', '@ontrails/core'],
45
+ },
46
+ {
47
+ names: [
48
+ 'Logger',
49
+ 'LogFormatter',
50
+ 'LogLevel',
51
+ 'LogRecord',
52
+ 'LogSink',
53
+ 'ObserveCapabilities',
54
+ 'ObserveConfig',
55
+ 'ObserveInput',
56
+ ],
57
+ reason: '@ontrails/observe mirrors core observability contracts',
58
+ workspaceNames: ['@ontrails/core', '@ontrails/observe'],
59
+ },
60
+ {
61
+ names: [
62
+ 'DEFAULT_MEMORY_SINK_MAX_RECORDS',
63
+ 'MemorySinkOptions',
64
+ 'MemoryTraceSink',
65
+ 'createBoundedMemorySink',
66
+ 'createMemorySink',
67
+ ],
68
+ reason:
69
+ '@ontrails/tracing compatibility memory sink mirrors @ontrails/observe',
70
+ workspaceNames: ['@ontrails/observe', '@ontrails/tracing'],
71
+ },
72
+ {
73
+ names: [
74
+ 'ActivationTraceRecordName',
75
+ 'NOOP_SINK',
76
+ 'SignalTraceRecordName',
77
+ 'TRACE_CONTEXT_KEY',
78
+ 'TraceFn',
79
+ 'clearTraceSink',
80
+ 'createActivationTraceRecord',
81
+ 'createSignalTraceRecord',
82
+ 'createTraceRecord',
83
+ 'getTraceContext',
84
+ 'getTraceSink',
85
+ 'registerTraceSink',
86
+ 'traceContextFromRecord',
87
+ 'writeActivationTraceRecord',
88
+ 'writeSignalTraceRecord',
89
+ ],
90
+ reason:
91
+ '@ontrails/tracing mirrors core tracing contracts for compatibility',
92
+ workspaceNames: ['@ontrails/core', '@ontrails/tracing'],
93
+ },
94
+ {
95
+ names: ['TraceContext', 'TraceRecord', 'TraceSink'],
96
+ reason:
97
+ '@ontrails/observe and @ontrails/tracing mirror core trace contracts',
98
+ workspaceNames: [
99
+ '@ontrails/core',
100
+ '@ontrails/observe',
101
+ '@ontrails/tracing',
102
+ ],
103
+ },
104
+ {
105
+ names: ['AuthError', 'PermitError'],
106
+ reason:
107
+ '@ontrails/core owns the TrailsError subclass; @ontrails/permits owns the auth-adapter payload type',
108
+ workspaceNames: ['@ontrails/core', '@ontrails/permits'],
109
+ },
110
+ {
111
+ names: ['Result', 'Topo', 'TrailContextInit', 'TrailInput', 'TrailOutput'],
112
+ reason: '@ontrails/library mirrors core runtime types for library users',
113
+ workspaceNames: ['@ontrails/core', '@ontrails/library'],
114
+ },
115
+ ];
116
+
117
+ const definitionKey = (definition: WardenExportedSymbolDefinition): string =>
118
+ `${definition.workspaceName}:${definition.filePath}:${definition.line}:${definition.name}`;
119
+
120
+ const isAllowedDuplicateGroup = (
121
+ definitions: readonly WardenExportedSymbolDefinition[]
122
+ ): boolean => {
123
+ const [first] = definitions;
124
+ if (!first) {
125
+ return false;
126
+ }
127
+ const workspaceNames = new Set(
128
+ definitions.map((definition) => definition.workspaceName)
129
+ );
130
+ return ALLOWED_DUPLICATE_EXPORT_GROUPS.some(
131
+ (group) =>
132
+ group.names.includes(first.name) &&
133
+ [...workspaceNames].every((name) => group.workspaceNames.includes(name))
134
+ );
135
+ };
136
+
137
+ const duplicateGroupsForFile = (
138
+ context: ProjectContext,
139
+ filePath: string
140
+ ): readonly {
141
+ readonly current: WardenExportedSymbolDefinition;
142
+ readonly definitions: readonly WardenExportedSymbolDefinition[];
143
+ }[] => {
144
+ const groups: {
145
+ current: WardenExportedSymbolDefinition;
146
+ definitions: readonly WardenExportedSymbolDefinition[];
147
+ }[] = [];
148
+
149
+ for (const definitions of context.exportedSymbolDefinitionsByName?.values() ??
150
+ []) {
151
+ const workspaceNames = new Set(
152
+ definitions.map((definition) => definition.workspaceName)
153
+ );
154
+ if (workspaceNames.size < 2) {
155
+ continue;
156
+ }
157
+ if (isAllowedDuplicateGroup(definitions)) {
158
+ continue;
159
+ }
160
+
161
+ for (const definition of definitions) {
162
+ if (definition.filePath === filePath) {
163
+ groups.push({ current: definition, definitions });
164
+ }
165
+ }
166
+ }
167
+
168
+ return groups;
169
+ };
170
+
171
+ const formatOtherDefinitions = (
172
+ current: WardenExportedSymbolDefinition,
173
+ definitions: readonly WardenExportedSymbolDefinition[]
174
+ ): string =>
175
+ definitions
176
+ .filter(
177
+ (definition) => definitionKey(definition) !== definitionKey(current)
178
+ )
179
+ .map(
180
+ (definition) =>
181
+ `${definition.workspaceName} (${definition.filePath}:${definition.line})`
182
+ )
183
+ .join(', ');
184
+
185
+ export const duplicateExportedSymbol: ProjectAwareWardenRule = {
186
+ check(): readonly WardenDiagnostic[] {
187
+ return [];
188
+ },
189
+ checkWithContext(
190
+ _sourceCode: string,
191
+ filePath: string,
192
+ context: ProjectContext
193
+ ): readonly WardenDiagnostic[] {
194
+ return duplicateGroupsForFile(context, filePath).map(
195
+ ({ current, definitions }) => ({
196
+ filePath,
197
+ line: current.line,
198
+ message: `Exported symbol "${current.name}" is defined by ${current.workspaceName} and also by ${formatOtherDefinitions(
199
+ current,
200
+ definitions
201
+ )}. Keep one package as the owner, rename one side, or document a deliberate ownership mirror before exporting both symbols.`,
202
+ rule: RULE_NAME,
203
+ severity: 'warn',
204
+ })
205
+ );
206
+ },
207
+ description:
208
+ 'Warn when the same exported symbol is defined by multiple first-party packages.',
209
+ name: RULE_NAME,
210
+ severity: 'warn',
211
+ };
@@ -0,0 +1,137 @@
1
+ import { deriveTopoGraph } from '@ontrails/topography';
2
+ import type { TopoGraph, TopoGraphEntry } from '@ontrails/topography';
3
+
4
+ import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
5
+
6
+ const RULE_NAME = 'duplicate-public-contract';
7
+ const TOPO_FILE = '<topo>';
8
+ const INVERSE_OPERATION_PAIRS = new Map([
9
+ ['archive', 'restore'],
10
+ ['disable', 'enable'],
11
+ ['pause', 'resume'],
12
+ ['star', 'unstar'],
13
+ ]);
14
+
15
+ const resolveGraph = (
16
+ topo: Parameters<TopoAwareWardenRule['checkTopo']>[0],
17
+ graph: TopoGraph | undefined
18
+ ): TopoGraph => graph ?? deriveTopoGraph(topo);
19
+
20
+ const canonicalize = (value: unknown): unknown => {
21
+ if (Array.isArray(value)) {
22
+ return value.map(canonicalize);
23
+ }
24
+ if (value && typeof value === 'object') {
25
+ return Object.fromEntries(
26
+ Object.entries(value as Record<string, unknown>)
27
+ .filter(([, entryValue]) => entryValue !== undefined)
28
+ .toSorted(([left], [right]) => left.localeCompare(right))
29
+ .map(([key, entryValue]) => [key, canonicalize(entryValue)])
30
+ );
31
+ }
32
+ return value;
33
+ };
34
+
35
+ const contractFingerprint = (entry: TopoGraphEntry): string =>
36
+ JSON.stringify(
37
+ canonicalize({
38
+ composes: entry.composes,
39
+ detours: entry.detours,
40
+ dryRunCapable: entry.dryRunCapable,
41
+ entities: entry.entities,
42
+ fires: entry.fires,
43
+ idempotent: entry.idempotent,
44
+ input: entry.input,
45
+ intent: entry.intent,
46
+ meta: entry.meta,
47
+ on: entry.on,
48
+ output: entry.output,
49
+ permit: entry.permit,
50
+ resources: entry.resources,
51
+ })
52
+ );
53
+
54
+ const isCandidate = (entry: TopoGraphEntry): boolean =>
55
+ entry.kind === 'trail' &&
56
+ !entry.id.startsWith('warden.rule.') &&
57
+ entry.deprecated !== true &&
58
+ entry.meta?.['internal'] !== true &&
59
+ entry.input !== undefined &&
60
+ entry.output !== undefined &&
61
+ Boolean(entry.cli || entry.surfaces.length > 0);
62
+
63
+ const renderTrailIds = (trailIds: readonly string[]): string =>
64
+ trailIds
65
+ .toSorted((left, right) => left.localeCompare(right))
66
+ .map((trailId) => `"${trailId}"`)
67
+ .join(', ');
68
+
69
+ const splitOperation = (
70
+ trailId: string
71
+ ): { readonly scope: string; readonly operation: string } | undefined => {
72
+ const separatorIndex = trailId.lastIndexOf('.');
73
+
74
+ if (separatorIndex <= 0 || separatorIndex === trailId.length - 1) {
75
+ return undefined;
76
+ }
77
+
78
+ return {
79
+ operation: trailId.slice(separatorIndex + 1),
80
+ scope: trailId.slice(0, separatorIndex),
81
+ };
82
+ };
83
+
84
+ const areInverseOperations = (left: string, right: string): boolean =>
85
+ INVERSE_OPERATION_PAIRS.get(left) === right ||
86
+ INVERSE_OPERATION_PAIRS.get(right) === left;
87
+
88
+ const isAcceptedInverseOperationPair = (
89
+ trailIds: readonly string[]
90
+ ): boolean => {
91
+ if (trailIds.length !== 2) {
92
+ return false;
93
+ }
94
+
95
+ const [left, right] = trailIds.map(splitOperation);
96
+
97
+ return Boolean(
98
+ left &&
99
+ right &&
100
+ left.scope === right.scope &&
101
+ areInverseOperations(left.operation, right.operation)
102
+ );
103
+ };
104
+
105
+ const buildDiagnostic = (trailIds: readonly string[]): WardenDiagnostic => ({
106
+ filePath: TOPO_FILE,
107
+ line: 1,
108
+ message: `Likely duplicate public trail contracts ${renderTrailIds(trailIds)} share the same input, output, intent, permits, resources, entities, composes, signals, and detours. Keep one contract with aliases/input mappings, compose a distinct wrapper, or document why these public contracts are separate.`,
109
+ rule: RULE_NAME,
110
+ severity: 'warn',
111
+ });
112
+
113
+ export const duplicatePublicContract: TopoAwareWardenRule = {
114
+ checkTopo(topo, context) {
115
+ const graph = resolveGraph(topo, context?.graph);
116
+ const groups = new Map<string, string[]>();
117
+
118
+ for (const entry of graph.entries) {
119
+ if (!isCandidate(entry)) {
120
+ continue;
121
+ }
122
+ const key = contractFingerprint(entry);
123
+ groups.set(key, [...(groups.get(key) ?? []), entry.id]);
124
+ }
125
+
126
+ return [...groups.values()]
127
+ .filter(
128
+ (trailIds) =>
129
+ trailIds.length > 1 && !isAcceptedInverseOperationPair(trailIds)
130
+ )
131
+ .map(buildDiagnostic);
132
+ },
133
+ description:
134
+ 'Warn when public surface trails expose the same normalized contract facts.',
135
+ name: RULE_NAME,
136
+ severity: 'warn',
137
+ };