@ontrails/warden 1.0.0-beta.2 → 1.0.0-beta.22

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 (249) hide show
  1. package/CHANGELOG.md +508 -6
  2. package/README.md +77 -26
  3. package/bin/warden.ts +50 -0
  4. package/package.json +27 -5
  5. package/src/adapter-check.ts +136 -0
  6. package/src/ast.ts +28 -0
  7. package/src/cli.ts +1374 -103
  8. package/src/command.ts +953 -0
  9. package/src/config.ts +184 -0
  10. package/src/draft.ts +22 -0
  11. package/src/drift.ts +106 -22
  12. package/src/fix.ts +120 -0
  13. package/src/formatters.ts +79 -9
  14. package/src/guide.ts +245 -0
  15. package/src/index.ts +206 -14
  16. package/src/project-context.ts +163 -0
  17. package/src/resolve.ts +530 -0
  18. package/src/rules/activation-orphan.ts +97 -0
  19. package/src/rules/ast.ts +3176 -85
  20. package/src/rules/circular-refs.ts +154 -0
  21. package/src/rules/composes-declarations.ts +704 -0
  22. package/src/rules/context-no-surface-types.ts +68 -8
  23. package/src/rules/contour-exists.ts +251 -0
  24. package/src/rules/contour-ids.ts +15 -0
  25. package/src/rules/dead-internal-trail.ts +154 -0
  26. package/src/rules/draft-file-marking.ts +160 -0
  27. package/src/rules/draft-visible-debt.ts +87 -0
  28. package/src/rules/error-mapping-completeness.ts +288 -0
  29. package/src/rules/example-valid.ts +401 -0
  30. package/src/rules/fires-declarations.ts +758 -0
  31. package/src/rules/implementation-returns-result.ts +1265 -95
  32. package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
  33. package/src/rules/incomplete-crud.ts +580 -0
  34. package/src/rules/index.ts +219 -18
  35. package/src/rules/intent-propagation.ts +127 -0
  36. package/src/rules/layer-field-name-drift.ts +96 -0
  37. package/src/rules/metadata.ts +654 -0
  38. package/src/rules/missing-reconcile.ts +98 -0
  39. package/src/rules/missing-visibility.ts +110 -0
  40. package/src/rules/no-destructured-compose.ts +192 -0
  41. package/src/rules/no-dev-permit-in-source.ts +99 -0
  42. package/src/rules/no-direct-implementation-call.ts +7 -7
  43. package/src/rules/no-legacy-layer-imports.ts +211 -0
  44. package/src/rules/no-native-error-result.ts +111 -0
  45. package/src/rules/no-redundant-result-error-wrap.ts +331 -0
  46. package/src/rules/no-retired-cross-vocabulary.ts +194 -0
  47. package/src/rules/no-sync-result-assumption.ts +1134 -99
  48. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  49. package/src/rules/no-throw-in-implementation.ts +10 -9
  50. package/src/rules/no-top-level-surface.ts +389 -0
  51. package/src/rules/on-references-exist.ts +194 -0
  52. package/src/rules/orphaned-signal.ts +150 -0
  53. package/src/rules/owner-projection-parity.ts +146 -0
  54. package/src/rules/permit-governance.ts +25 -0
  55. package/src/rules/public-export-example-coverage.ts +553 -0
  56. package/src/rules/public-internal-deep-imports.ts +517 -0
  57. package/src/rules/public-output-schema.ts +29 -0
  58. package/src/rules/public-union-output-discriminants.ts +150 -0
  59. package/src/rules/read-intent-fires.ts +187 -0
  60. package/src/rules/reference-exists.ts +98 -0
  61. package/src/rules/registry-names.ts +145 -0
  62. package/src/rules/resolved-import-boundary.ts +146 -0
  63. package/src/rules/resource-declarations.ts +704 -0
  64. package/src/rules/resource-exists.ts +179 -0
  65. package/src/rules/resource-id-grammar.ts +65 -0
  66. package/src/rules/resource-mock-coverage.ts +115 -0
  67. package/src/rules/scan.ts +38 -25
  68. package/src/rules/scheduled-destroy-intent.ts +44 -0
  69. package/src/rules/signal-graph-coaching.ts +191 -0
  70. package/src/rules/specs.ts +9 -5
  71. package/src/rules/static-resource-accessor-preference.ts +657 -0
  72. package/src/rules/surface-facet-coherence.ts +370 -0
  73. package/src/rules/trail-versioning-source.ts +1094 -0
  74. package/src/rules/trail-versioning-topo.ts +172 -0
  75. package/src/rules/types.ts +270 -6
  76. package/src/rules/unmaterialized-activation-source.ts +84 -0
  77. package/src/rules/unreachable-detour-shadowing.ts +344 -0
  78. package/src/rules/valid-describe-refs.ts +160 -32
  79. package/src/rules/valid-detour-contract.ts +78 -0
  80. package/src/rules/warden-export-symmetry.ts +533 -0
  81. package/src/rules/warden-rules-use-ast.ts +996 -0
  82. package/src/rules/webhook-route-collision.ts +243 -0
  83. package/src/trails/activation-orphan.trail.ts +84 -0
  84. package/src/trails/circular-refs.trail.ts +29 -0
  85. package/src/trails/composes-declarations.trail.ts +22 -0
  86. package/src/trails/context-no-surface-types.trail.ts +21 -0
  87. package/src/trails/contour-exists.trail.ts +21 -0
  88. package/src/trails/dead-internal-trail.trail.ts +26 -0
  89. package/src/trails/deprecation-without-guidance.trail.ts +21 -0
  90. package/src/trails/draft-file-marking.trail.ts +16 -0
  91. package/src/trails/draft-visible-debt.trail.ts +16 -0
  92. package/src/trails/error-mapping-completeness.trail.ts +29 -0
  93. package/src/trails/example-valid.trail.ts +25 -0
  94. package/src/trails/fires-declarations.trail.ts +23 -0
  95. package/src/trails/fork-without-preserved-blaze.trail.ts +31 -0
  96. package/src/trails/implementation-returns-result.trail.ts +20 -0
  97. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  98. package/src/trails/incomplete-crud.trail.ts +39 -0
  99. package/src/trails/index.ts +78 -0
  100. package/src/trails/intent-propagation.trail.ts +30 -0
  101. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  102. package/src/trails/marker-schema-unsupported.trail.ts +23 -0
  103. package/src/trails/missing-reconcile.trail.ts +33 -0
  104. package/src/trails/missing-visibility.trail.ts +22 -0
  105. package/src/trails/no-destructured-compose.trail.ts +44 -0
  106. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  107. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  108. package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
  109. package/src/trails/no-native-error-result.trail.ts +18 -0
  110. package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
  111. package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
  112. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  113. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  114. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  115. package/src/trails/no-top-level-surface.trail.ts +43 -0
  116. package/src/trails/on-references-exist.trail.ts +21 -0
  117. package/src/trails/orphaned-signal.trail.ts +36 -0
  118. package/src/trails/owner-projection-parity.trail.ts +26 -0
  119. package/src/trails/pending-force.trail.ts +21 -0
  120. package/src/trails/permit-governance.trail.ts +51 -0
  121. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  122. package/src/trails/public-export-example-coverage.trail.ts +16 -0
  123. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  124. package/src/trails/public-output-schema.trail.ts +55 -0
  125. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  126. package/src/trails/read-intent-fires.trail.ts +20 -0
  127. package/src/trails/reference-exists.trail.ts +25 -0
  128. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  129. package/src/trails/resource-declarations.trail.ts +25 -0
  130. package/src/trails/resource-exists.trail.ts +27 -0
  131. package/src/trails/resource-id-grammar.trail.ts +39 -0
  132. package/src/trails/resource-mock-coverage.trail.ts +40 -0
  133. package/src/trails/run.ts +162 -0
  134. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  135. package/src/trails/schema.ts +194 -0
  136. package/src/trails/signal-graph-coaching.trail.ts +77 -0
  137. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  138. package/src/trails/surface-facet-coherence.trail.ts +25 -0
  139. package/src/trails/topo.ts +6 -0
  140. package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
  141. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  142. package/src/trails/valid-describe-refs.trail.ts +18 -0
  143. package/src/trails/valid-detour-contract.trail.ts +71 -0
  144. package/src/trails/version-gap.trail.ts +35 -0
  145. package/src/trails/version-pinned-compose.trail.ts +23 -0
  146. package/src/trails/version-without-examples.trail.ts +38 -0
  147. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  148. package/src/trails/warden-rules-use-ast.trail.ts +45 -0
  149. package/src/trails/webhook-route-collision.trail.ts +50 -0
  150. package/src/trails/wrap-rule.ts +213 -0
  151. package/src/workspaces.ts +238 -0
  152. package/.turbo/turbo-build.log +0 -1
  153. package/.turbo/turbo-lint.log +0 -3
  154. package/.turbo/turbo-typecheck.log +0 -1
  155. package/dist/cli.d.ts +0 -46
  156. package/dist/cli.d.ts.map +0 -1
  157. package/dist/cli.js +0 -221
  158. package/dist/cli.js.map +0 -1
  159. package/dist/drift.d.ts +0 -26
  160. package/dist/drift.d.ts.map +0 -1
  161. package/dist/drift.js +0 -27
  162. package/dist/drift.js.map +0 -1
  163. package/dist/formatters.d.ts +0 -29
  164. package/dist/formatters.d.ts.map +0 -1
  165. package/dist/formatters.js +0 -87
  166. package/dist/formatters.js.map +0 -1
  167. package/dist/index.d.ts +0 -26
  168. package/dist/index.d.ts.map +0 -1
  169. package/dist/index.js +0 -26
  170. package/dist/index.js.map +0 -1
  171. package/dist/rules/ast.d.ts +0 -41
  172. package/dist/rules/ast.d.ts.map +0 -1
  173. package/dist/rules/ast.js +0 -163
  174. package/dist/rules/ast.js.map +0 -1
  175. package/dist/rules/context-no-surface-types.d.ts +0 -12
  176. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  177. package/dist/rules/context-no-surface-types.js +0 -96
  178. package/dist/rules/context-no-surface-types.js.map +0 -1
  179. package/dist/rules/implementation-returns-result.d.ts +0 -13
  180. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  181. package/dist/rules/implementation-returns-result.js +0 -231
  182. package/dist/rules/implementation-returns-result.js.map +0 -1
  183. package/dist/rules/index.d.ts +0 -22
  184. package/dist/rules/index.d.ts.map +0 -1
  185. package/dist/rules/index.js +0 -41
  186. package/dist/rules/index.js.map +0 -1
  187. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  188. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  189. package/dist/rules/no-direct-impl-in-route.js +0 -46
  190. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  191. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  192. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  193. package/dist/rules/no-direct-implementation-call.js +0 -39
  194. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  195. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  196. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  197. package/dist/rules/no-sync-result-assumption.js +0 -98
  198. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  199. package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
  200. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  201. package/dist/rules/no-throw-in-detour-target.js +0 -87
  202. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  203. package/dist/rules/no-throw-in-implementation.d.ts +0 -9
  204. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  205. package/dist/rules/no-throw-in-implementation.js +0 -34
  206. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  207. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  208. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  209. package/dist/rules/prefer-schema-inference.js +0 -86
  210. package/dist/rules/prefer-schema-inference.js.map +0 -1
  211. package/dist/rules/scan.d.ts +0 -8
  212. package/dist/rules/scan.d.ts.map +0 -1
  213. package/dist/rules/scan.js +0 -32
  214. package/dist/rules/scan.js.map +0 -1
  215. package/dist/rules/specs.d.ts +0 -29
  216. package/dist/rules/specs.d.ts.map +0 -1
  217. package/dist/rules/specs.js +0 -192
  218. package/dist/rules/specs.js.map +0 -1
  219. package/dist/rules/structure.d.ts +0 -13
  220. package/dist/rules/structure.d.ts.map +0 -1
  221. package/dist/rules/structure.js +0 -142
  222. package/dist/rules/structure.js.map +0 -1
  223. package/dist/rules/types.d.ts +0 -52
  224. package/dist/rules/types.d.ts.map +0 -1
  225. package/dist/rules/types.js +0 -2
  226. package/dist/rules/types.js.map +0 -1
  227. package/dist/rules/valid-describe-refs.d.ts +0 -7
  228. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  229. package/dist/rules/valid-describe-refs.js +0 -51
  230. package/dist/rules/valid-describe-refs.js.map +0 -1
  231. package/dist/rules/valid-detour-refs.d.ts +0 -6
  232. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  233. package/dist/rules/valid-detour-refs.js +0 -116
  234. package/dist/rules/valid-detour-refs.js.map +0 -1
  235. package/src/__tests__/cli.test.ts +0 -198
  236. package/src/__tests__/drift.test.ts +0 -74
  237. package/src/__tests__/formatters.test.ts +0 -157
  238. package/src/__tests__/implementation-returns-result.test.ts +0 -75
  239. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  240. package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
  241. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  242. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  243. package/src/__tests__/rules.test.ts +0 -188
  244. package/src/__tests__/valid-describe-refs.test.ts +0 -60
  245. package/src/rules/no-direct-impl-in-route.ts +0 -77
  246. package/src/rules/no-throw-in-detour-target.ts +0 -150
  247. package/src/rules/valid-detour-refs.ts +0 -187
  248. package/tsconfig.json +0 -9
  249. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,98 @@
1
+ import {
2
+ collectCrudTableIds,
3
+ collectReconcileTableIds,
4
+ findStoreTableDefinitions,
5
+ offsetToLine,
6
+ parse,
7
+ } from './ast.js';
8
+ import type { AstNode } from './ast.js';
9
+ import { isTestFile } from './scan.js';
10
+ import type {
11
+ ProjectAwareWardenRule,
12
+ ProjectContext,
13
+ WardenDiagnostic,
14
+ } from './types.js';
15
+
16
+ const buildMissingReconcileDiagnostic = (
17
+ tableId: string,
18
+ filePath: string,
19
+ line: number
20
+ ): WardenDiagnostic => ({
21
+ filePath,
22
+ line,
23
+ message: `Versioned store table "${tableId}" is used with CRUD factories but has no reconcile trail. Add reconcile(...) to complete the versioned store pattern.`,
24
+ rule: 'missing-reconcile',
25
+ severity: 'warn',
26
+ });
27
+
28
+ const checkMissingReconcile = (
29
+ ast: AstNode | null,
30
+ sourceCode: string,
31
+ filePath: string,
32
+ crudTableIds: ReadonlySet<string>,
33
+ reconcileTableIds: ReadonlySet<string>
34
+ ): readonly WardenDiagnostic[] => {
35
+ if (isTestFile(filePath) || !ast) {
36
+ return [];
37
+ }
38
+
39
+ const diagnostics: WardenDiagnostic[] = [];
40
+
41
+ for (const definition of findStoreTableDefinitions(ast)) {
42
+ if (
43
+ !definition.versioned ||
44
+ !crudTableIds.has(definition.key) ||
45
+ reconcileTableIds.has(definition.key)
46
+ ) {
47
+ continue;
48
+ }
49
+
50
+ diagnostics.push(
51
+ buildMissingReconcileDiagnostic(
52
+ definition.name,
53
+ filePath,
54
+ offsetToLine(sourceCode, definition.start)
55
+ )
56
+ );
57
+ }
58
+
59
+ return diagnostics;
60
+ };
61
+
62
+ export const missingReconcile: ProjectAwareWardenRule = {
63
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
64
+ const ast = parse(filePath, sourceCode);
65
+ return checkMissingReconcile(
66
+ ast,
67
+ sourceCode,
68
+ filePath,
69
+ ast ? collectCrudTableIds(ast) : new Set<string>(),
70
+ ast ? collectReconcileTableIds(ast) : new Set<string>()
71
+ );
72
+ },
73
+ checkWithContext(
74
+ sourceCode: string,
75
+ filePath: string,
76
+ context: ProjectContext
77
+ ): readonly WardenDiagnostic[] {
78
+ const ast = parse(filePath, sourceCode);
79
+ const localCrudTableIds = ast
80
+ ? collectCrudTableIds(ast)
81
+ : new Set<string>();
82
+ const localReconcileTableIds = ast
83
+ ? collectReconcileTableIds(ast)
84
+ : new Set<string>();
85
+
86
+ return checkMissingReconcile(
87
+ ast,
88
+ sourceCode,
89
+ filePath,
90
+ context.crudTableIds ?? localCrudTableIds,
91
+ context.reconcileTableIds ?? localReconcileTableIds
92
+ );
93
+ },
94
+ description:
95
+ 'Warn when a versioned store table participates in CRUD factory generation without a matching reconcile trail.',
96
+ name: 'missing-reconcile',
97
+ severity: 'warn',
98
+ };
@@ -0,0 +1,110 @@
1
+ import { collectComposeTargetTrailIds, parse } from './ast.js';
2
+ import { isTestFile } from './scan.js';
3
+ import {
4
+ findTrailLikeSpecs,
5
+ parseStringLiteral,
6
+ parseZodObjectShape,
7
+ } from './specs.js';
8
+ import type { TrailLikeSpec } from './specs.js';
9
+ import type {
10
+ ProjectAwareWardenRule,
11
+ ProjectContext,
12
+ WardenDiagnostic,
13
+ } from './types.js';
14
+
15
+ /** Check legacy `meta: { internal: true }` convention (mirrors runtime effectiveVisibility). */
16
+ const hasLegacyMetaInternal = (spec: TrailLikeSpec): boolean => {
17
+ const meta = spec.properties.get('meta')?.value ?? '';
18
+ return /(?:^|[{,])\s*internal\s*:\s*true/.test(meta);
19
+ };
20
+
21
+ const trailVisibility = (spec: TrailLikeSpec): 'internal' | 'public' => {
22
+ if (
23
+ parseStringLiteral(spec.properties.get('visibility')?.value ?? '') ===
24
+ 'internal'
25
+ ) {
26
+ return 'internal';
27
+ }
28
+ return hasLegacyMetaInternal(spec) ? 'internal' : 'public';
29
+ };
30
+
31
+ const hasRequiredComposeInput = (spec: TrailLikeSpec): boolean => {
32
+ const composeInput = spec.properties.get('composeInput');
33
+ if (!composeInput) {
34
+ return false;
35
+ }
36
+
37
+ const fields = parseZodObjectShape(composeInput.value);
38
+ return [...fields.values()].some((field) => field.required);
39
+ };
40
+
41
+ const buildMissingVisibilityDiagnostic = (
42
+ trailId: string,
43
+ filePath: string,
44
+ line: number
45
+ ): WardenDiagnostic => ({
46
+ filePath,
47
+ line,
48
+ message: `Trail "${trailId}" is composed elsewhere and declares required composeInput fields, but it is still public. Consider visibility: 'internal' so surfaces do not expose a trail that only works through ctx.compose().`,
49
+ rule: 'missing-visibility',
50
+ severity: 'warn',
51
+ });
52
+
53
+ const checkMissingVisibility = (
54
+ sourceCode: string,
55
+ filePath: string,
56
+ composedTrailIds: ReadonlySet<string>
57
+ ): readonly WardenDiagnostic[] => {
58
+ if (isTestFile(filePath)) {
59
+ return [];
60
+ }
61
+
62
+ const diagnostics: WardenDiagnostic[] = [];
63
+
64
+ for (const spec of findTrailLikeSpecs(sourceCode)) {
65
+ if (
66
+ spec.kind !== 'trail' ||
67
+ trailVisibility(spec) === 'internal' ||
68
+ !composedTrailIds.has(spec.id) ||
69
+ !hasRequiredComposeInput(spec)
70
+ ) {
71
+ continue;
72
+ }
73
+
74
+ diagnostics.push(
75
+ buildMissingVisibilityDiagnostic(spec.id, filePath, spec.line)
76
+ );
77
+ }
78
+
79
+ return diagnostics;
80
+ };
81
+
82
+ export const missingVisibility: ProjectAwareWardenRule = {
83
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
84
+ const ast = parse(filePath, sourceCode);
85
+ return checkMissingVisibility(
86
+ sourceCode,
87
+ filePath,
88
+ ast ? collectComposeTargetTrailIds(ast, sourceCode) : new Set<string>()
89
+ );
90
+ },
91
+ checkWithContext(
92
+ sourceCode: string,
93
+ filePath: string,
94
+ context: ProjectContext
95
+ ): readonly WardenDiagnostic[] {
96
+ const ast = parse(filePath, sourceCode);
97
+ const localComposeTargetTrailIds = ast
98
+ ? collectComposeTargetTrailIds(ast, sourceCode)
99
+ : new Set<string>();
100
+ return checkMissingVisibility(
101
+ sourceCode,
102
+ filePath,
103
+ context.composeTargetTrailIds ?? localComposeTargetTrailIds
104
+ );
105
+ },
106
+ description:
107
+ 'Coach when a composed trail still looks composition-only because it declares required composeInput but remains public.',
108
+ name: 'missing-visibility',
109
+ severity: 'warn',
110
+ };
@@ -0,0 +1,192 @@
1
+ import {
2
+ findBlazeBodies,
3
+ findTrailDefinitions,
4
+ getStringValue,
5
+ identifierName,
6
+ isShadowed,
7
+ isStringLiteral,
8
+ offsetToLine,
9
+ parse,
10
+ walkWithScopes,
11
+ } from './ast.js';
12
+ import { isFrameworkInternalFile, isTestFile } from './scan.js';
13
+ import type { AstNode } from './ast.js';
14
+ import type { WardenDiagnostic, WardenRule } from './types.js';
15
+
16
+ const RULE_NAME = 'no-destructured-compose';
17
+
18
+ const diagnosticMessage = (trailId: string): string =>
19
+ `Trail "${trailId}" destructures compose from the blaze context. Use ctx.compose(...) directly so composition stays visible and Warden can recognize composed Result values.`;
20
+
21
+ const propertyKeyName = (property: AstNode): string | null => {
22
+ if ((property as unknown as { computed?: boolean }).computed === true) {
23
+ return null;
24
+ }
25
+
26
+ const key = property.key as AstNode | undefined;
27
+ if (!key) {
28
+ return null;
29
+ }
30
+
31
+ return (
32
+ identifierName(key) ?? (isStringLiteral(key) ? getStringValue(key) : null)
33
+ );
34
+ };
35
+
36
+ const findComposeBinding = (pattern: AstNode | undefined): AstNode | null => {
37
+ if (pattern?.type !== 'ObjectPattern') {
38
+ return null;
39
+ }
40
+
41
+ const properties =
42
+ (pattern as unknown as { properties?: readonly AstNode[] }).properties ??
43
+ [];
44
+
45
+ for (const property of properties) {
46
+ if (
47
+ property.type === 'Property' &&
48
+ propertyKeyName(property) === 'compose'
49
+ ) {
50
+ return property;
51
+ }
52
+ }
53
+
54
+ return null;
55
+ };
56
+
57
+ const blazeParams = (blaze: AstNode): readonly AstNode[] =>
58
+ (blaze as unknown as { params?: readonly AstNode[] }).params ?? [];
59
+
60
+ const destructuredComposeFromVariableDeclarator = (
61
+ node: AstNode,
62
+ contextName: string
63
+ ): AstNode | null => {
64
+ if (node.type !== 'VariableDeclarator') {
65
+ return null;
66
+ }
67
+
68
+ const { id, init } = node as unknown as {
69
+ readonly id?: AstNode;
70
+ readonly init?: AstNode;
71
+ };
72
+
73
+ if (identifierName(init) !== contextName) {
74
+ return null;
75
+ }
76
+
77
+ return findComposeBinding(id);
78
+ };
79
+
80
+ const destructuredComposeFromAssignment = (
81
+ node: AstNode,
82
+ contextName: string
83
+ ): AstNode | null => {
84
+ if (node.type !== 'AssignmentExpression') {
85
+ return null;
86
+ }
87
+
88
+ const { left, operator, right } = node as unknown as {
89
+ readonly left?: AstNode;
90
+ readonly operator?: string;
91
+ readonly right?: AstNode;
92
+ };
93
+
94
+ if (operator !== '=' || identifierName(right) !== contextName) {
95
+ return null;
96
+ }
97
+
98
+ return findComposeBinding(left);
99
+ };
100
+
101
+ const checkBodyDestructuring = (
102
+ sourceCode: string,
103
+ filePath: string,
104
+ trailId: string,
105
+ blaze: AstNode,
106
+ contextName: string
107
+ ): WardenDiagnostic[] => {
108
+ const diagnostics: WardenDiagnostic[] = [];
109
+
110
+ walkWithScopes(
111
+ blaze,
112
+ (node, scopes) => {
113
+ if (isShadowed(contextName, scopes)) {
114
+ return;
115
+ }
116
+
117
+ const composeBinding =
118
+ destructuredComposeFromVariableDeclarator(node, contextName) ??
119
+ destructuredComposeFromAssignment(node, contextName);
120
+ if (!composeBinding) {
121
+ return;
122
+ }
123
+
124
+ diagnostics.push({
125
+ filePath,
126
+ line: offsetToLine(sourceCode, composeBinding.start),
127
+ message: diagnosticMessage(trailId),
128
+ rule: RULE_NAME,
129
+ severity: 'warn',
130
+ });
131
+ },
132
+ { stopAtNestedFunctions: true }
133
+ );
134
+
135
+ return diagnostics;
136
+ };
137
+
138
+ export const noDestructuredCompose: WardenRule = {
139
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
140
+ if (isTestFile(filePath) || isFrameworkInternalFile(filePath)) {
141
+ return [];
142
+ }
143
+
144
+ const ast = parse(filePath, sourceCode);
145
+ if (!ast) {
146
+ return [];
147
+ }
148
+
149
+ const diagnostics: WardenDiagnostic[] = [];
150
+
151
+ for (const definition of findTrailDefinitions(ast)) {
152
+ if (definition.kind !== 'trail') {
153
+ continue;
154
+ }
155
+
156
+ for (const blaze of findBlazeBodies(definition.config)) {
157
+ const params = blazeParams(blaze);
158
+ const [, contextParam] = params;
159
+ const paramComposeBinding = findComposeBinding(contextParam);
160
+
161
+ if (paramComposeBinding) {
162
+ diagnostics.push({
163
+ filePath,
164
+ line: offsetToLine(sourceCode, paramComposeBinding.start),
165
+ message: diagnosticMessage(definition.id),
166
+ rule: RULE_NAME,
167
+ severity: 'warn',
168
+ });
169
+ }
170
+
171
+ const contextName = identifierName(contextParam);
172
+ if (contextName) {
173
+ diagnostics.push(
174
+ ...checkBodyDestructuring(
175
+ sourceCode,
176
+ filePath,
177
+ definition.id,
178
+ blaze,
179
+ contextName
180
+ )
181
+ );
182
+ }
183
+ }
184
+ }
185
+
186
+ return diagnostics;
187
+ },
188
+ description:
189
+ 'Coach trail blazes to compose with ctx.compose(...) directly instead of destructuring compose from the context.',
190
+ name: RULE_NAME,
191
+ severity: 'warn',
192
+ };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Flags occurrences of the `--dev-permit` CLI flag string in source code.
3
+ *
4
+ * `--dev-permit` is a local-development ergonomic that injects a synthetic
5
+ * full-access permit into the CLI execution pipeline (see TRL-410). It must
6
+ * never appear in committed scripts, CI configs, or library source — its
7
+ * presence in checked-in code defeats permit governance.
8
+ *
9
+ * The rule scans source text for the literal token `--dev-permit` (string
10
+ * literal, comment, or code). It is intentionally text-based rather than
11
+ * AST-based: the failure modes the rule targets (a developer pasting a CLI
12
+ * invocation into a script or doc, or wiring it into a `package.json`-like
13
+ * manifest) appear in places the AST cannot reason about.
14
+ *
15
+ * Allow-list: the CLI flag/build modules and the Warden rule surfaces that
16
+ * define this rule's own metadata are exempt. The Warden runner invokes this
17
+ * rule across TypeScript source plus committed script/config surfaces such as
18
+ * shell scripts, CI YAML, and `package.json`. Unlike most Warden source rules,
19
+ * test TypeScript files are still scanned for this literal because checked-in
20
+ * tests can become copied scripts or examples.
21
+ */
22
+ import { resolve, sep } from 'node:path';
23
+
24
+ import type { WardenDiagnostic, WardenRule } from './types.js';
25
+
26
+ const RULE_NAME = 'no-dev-permit-in-source';
27
+
28
+ /** Literal CLI flag string the rule searches for. */
29
+ const DEV_PERMIT_LITERAL = '--dev-permit';
30
+
31
+ /**
32
+ * Path suffixes (in POSIX form) for source files that legitimately contain
33
+ * the literal `--dev-permit` string. Other files are flagged.
34
+ *
35
+ * Each entry is matched against the normalized (forward-slash) absolute
36
+ * path with a trailing-segment match, so the rule stays correct regardless
37
+ * of the consumer's repository root.
38
+ */
39
+ const ALLOWED_PATH_SUFFIXES: readonly string[] = [
40
+ // The CLI flag preset module authors `--dev-permit` as the canonical name.
41
+ '/packages/cli/src/flags.ts',
42
+ // The build module spells the kebab-case form when reading the parsed flag.
43
+ '/packages/cli/src/build.ts',
44
+ // The rule's own implementation file references the literal it searches for.
45
+ '/packages/warden/src/rules/no-dev-permit-in-source.ts',
46
+ // Rule metadata and trail wrapper document the same literal for users.
47
+ '/packages/warden/src/rules/metadata.ts',
48
+ '/packages/warden/src/trails/no-dev-permit-in-source.trail.ts',
49
+ ];
50
+
51
+ const normalizePath = (filePath: string): string =>
52
+ resolve(filePath).split(sep).join('/');
53
+
54
+ const isAllowedFile = (filePath: string): boolean => {
55
+ const normalized = normalizePath(filePath);
56
+ return ALLOWED_PATH_SUFFIXES.some((suffix) => normalized.endsWith(suffix));
57
+ };
58
+
59
+ const findLineOfFirstMatch = (sourceCode: string): number => {
60
+ const idx = sourceCode.indexOf(DEV_PERMIT_LITERAL);
61
+ if (idx === -1) {
62
+ return 1;
63
+ }
64
+ let line = 1;
65
+ for (let i = 0; i < idx; i += 1) {
66
+ if (sourceCode.codePointAt(i) === 10) {
67
+ line += 1;
68
+ }
69
+ }
70
+ return line;
71
+ };
72
+
73
+ /**
74
+ * Flags occurrences of the `--dev-permit` flag string in committed source.
75
+ */
76
+ export const noDevPermitInSource: WardenRule = {
77
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
78
+ if (isAllowedFile(filePath)) {
79
+ return [];
80
+ }
81
+ if (!sourceCode.includes(DEV_PERMIT_LITERAL)) {
82
+ return [];
83
+ }
84
+ return [
85
+ {
86
+ filePath,
87
+ line: findLineOfFirstMatch(sourceCode),
88
+ message:
89
+ '`--dev-permit` is a local-development flag and must not appear in committed source. Use `--token` or `--permit` for CI and scripted invocations.',
90
+ rule: RULE_NAME,
91
+ severity: 'error',
92
+ },
93
+ ];
94
+ },
95
+ description:
96
+ 'Disallow the `--dev-permit` CLI flag string in committed source code.',
97
+ name: RULE_NAME,
98
+ severity: 'error',
99
+ };
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Flags direct `.implementation()` calls in application code.
2
+ * Flags direct `.blaze()` calls in application code.
3
3
  *
4
- * Uses AST parsing to find `.implementation()` call expressions,
4
+ * Uses AST parsing to find `.blaze()` call expressions,
5
5
  * ignoring occurrences in strings and comments.
6
6
  */
7
7
 
8
- import { isImplementationCall, offsetToLine, parse, walk } from './ast.js';
8
+ import { isBlazeCall, offsetToLine, parse, walk } from './ast.js';
9
9
  import { isFrameworkInternalFile, isTestFile } from './scan.js';
10
10
  import type { WardenDiagnostic, WardenRule } from './types.js';
11
11
 
12
12
  /**
13
- * Flags direct `.implementation()` calls in application code.
13
+ * Flags direct `.blaze()` calls in application code.
14
14
  */
15
15
  export const noDirectImplementationCall: WardenRule = {
16
16
  check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
@@ -26,12 +26,12 @@ export const noDirectImplementationCall: WardenRule = {
26
26
  const diagnostics: WardenDiagnostic[] = [];
27
27
 
28
28
  walk(ast, (node) => {
29
- if (isImplementationCall(node)) {
29
+ if (isBlazeCall(node)) {
30
30
  diagnostics.push({
31
31
  filePath,
32
32
  line: offsetToLine(sourceCode, node.start),
33
33
  message:
34
- 'Use ctx.follow("trailId", input) instead of direct .implementation() calls. Direct implementation access bypasses validation, tracing, and layers.',
34
+ 'Use ctx.compose("trailId", input) instead of direct .blaze() calls. Direct implementation access bypasses validation, tracing, and layers.',
35
35
  rule: 'no-direct-implementation-call',
36
36
  severity: 'warn',
37
37
  });
@@ -41,7 +41,7 @@ export const noDirectImplementationCall: WardenRule = {
41
41
  return diagnostics;
42
42
  },
43
43
  description:
44
- 'Disallow direct .implementation() calls in application code. Use ctx.follow() instead.',
44
+ 'Disallow direct .blaze() calls in application code. Use ctx.compose() instead.',
45
45
  name: 'no-direct-implementation-call',
46
46
  severity: 'warn',
47
47
  };