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

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 +835 -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,310 @@
1
+ /** Warden-private store/factory pattern helpers. */
2
+
3
+ import {
4
+ extractBindingName,
5
+ findConfigProperty,
6
+ getPropertyName,
7
+ identifierName,
8
+ walk,
9
+ } from '@ontrails/source';
10
+ import type { AstNode } from '@ontrails/source';
11
+
12
+ export interface StoreTableDefinition {
13
+ /** Table name declared inside store({ ... }). */
14
+ readonly name: string;
15
+ /**
16
+ * Local binding name of the enclosing `store(...)` declaration, if the
17
+ * `store(...)` call is bound to a `const`/`let`/`var` (e.g. `db` in
18
+ * `const db = store({ ... })`). Null for anonymous stores.
19
+ */
20
+ readonly storeBinding: string | null;
21
+ /**
22
+ * Stable composite key for this table in the form `${storeBinding}:${name}`,
23
+ * falling back to the bare `name` when the store is anonymous. Use this for
24
+ * compose-rule / compose-file keying so two stores with the same table name
25
+ * never collide.
26
+ */
27
+ readonly key: string;
28
+ /** Start offset of the table property declaration. */
29
+ readonly start: number;
30
+ /** Whether the authored table opts into version tracking. */
31
+ readonly versioned: boolean;
32
+ }
33
+
34
+ /**
35
+ * Build a composite key for a store table: `${storeBinding}:${tableName}`,
36
+ * falling back to the bare `tableName` when the enclosing store has no local
37
+ * binding. Centralized so rule keying stays stable.
38
+ *
39
+ * @remarks
40
+ * The key is intentionally file-local (no module path prefix). Compose-file
41
+ * aggregation in `ProjectContext` merges keys from all files, so two files
42
+ * with `const db = store({ notes: ... })` both produce `db:notes` — this is
43
+ * the desired behavior because the warden checks for *pattern completeness*
44
+ * across the project and matching keys signals that the same logical table
45
+ * is covered. If two genuinely different tables share a binding and name,
46
+ * that is a code-level naming collision the developer should resolve.
47
+ */
48
+ export const makeStoreTableKey = (
49
+ storeBinding: string | null,
50
+ tableName: string
51
+ ): string => (storeBinding ? `${storeBinding}:${tableName}` : tableName);
52
+
53
+ const isBooleanLiteral = (node: AstNode | undefined): boolean =>
54
+ Boolean(
55
+ node &&
56
+ ((node.type === 'BooleanLiteral' &&
57
+ (node as unknown as { value?: unknown }).value === true) ||
58
+ (node.type === 'Literal' &&
59
+ (node as unknown as { value?: unknown }).value === true))
60
+ );
61
+
62
+ /**
63
+ * Check if a node is a `CallExpression` to the identifier `name`.
64
+ *
65
+ * e.g. `isNamedCall(node, 'store')` matches `store({...})` but not
66
+ * `someObj.store()` or `storeAlt()`.
67
+ */
68
+ export const isNamedCall = (node: AstNode | undefined, name: string): boolean =>
69
+ !!node &&
70
+ node.type === 'CallExpression' &&
71
+ identifierName((node as unknown as { callee?: AstNode }).callee) === name;
72
+
73
+ /**
74
+ * Narrow a member-expression node (`a.b` or `a['b']`) to its `object` /
75
+ * `property` pair, returning `null` for anything else.
76
+ */
77
+ export const getMemberExpression = (
78
+ node: AstNode | undefined
79
+ ): { readonly object?: AstNode; readonly property?: AstNode } | null => {
80
+ if (
81
+ !node ||
82
+ (node.type !== 'MemberExpression' && node.type !== 'StaticMemberExpression')
83
+ ) {
84
+ return null;
85
+ }
86
+
87
+ return node as unknown as {
88
+ readonly object?: AstNode;
89
+ readonly property?: AstNode;
90
+ };
91
+ };
92
+
93
+ /**
94
+ * Resolve a `<store>.tables.<name>` member expression to its store binding
95
+ * and table name.
96
+ *
97
+ * Returns `null` for anything that isn't a two-level member access ending in
98
+ * `.tables.<name>`. The store binding is the identifier of the object owning
99
+ * `.tables` — typically the local binding from `const db = store(...)`.
100
+ */
101
+ export const extractStoreTableFromMember = (
102
+ node: AstNode | undefined
103
+ ): {
104
+ readonly storeBinding: string | null;
105
+ readonly tableName: string;
106
+ } | null => {
107
+ const member = getMemberExpression(node);
108
+ const tableName = member ? getPropertyName(member.property) : null;
109
+ const tablesMember = member ? getMemberExpression(member.object) : null;
110
+ if (!tableName || !tablesMember) {
111
+ return null;
112
+ }
113
+
114
+ if (getPropertyName(tablesMember.property) !== 'tables') {
115
+ return null;
116
+ }
117
+
118
+ const storeBinding = identifierName(tablesMember.object) ?? null;
119
+ return { storeBinding, tableName };
120
+ };
121
+
122
+ /**
123
+ * Collect `const foo = <store>.tables.<name>` bindings from a parsed file,
124
+ * keyed by the local binding name. Values are the composite table key
125
+ * (`${storeBinding}:${tableName}`) so callers can dedupe across stores that
126
+ * share a table name.
127
+ */
128
+ export const collectNamedStoreTableIds = (
129
+ ast: AstNode
130
+ ): ReadonlyMap<string, string> => {
131
+ const ids = new Map<string, string>();
132
+
133
+ walk(ast, (node) => {
134
+ if (node.type !== 'VariableDeclarator') {
135
+ return;
136
+ }
137
+
138
+ const { id, init } = node as unknown as {
139
+ readonly id?: AstNode;
140
+ readonly init?: AstNode;
141
+ };
142
+ const name = extractBindingName(id);
143
+ const table = extractStoreTableFromMember(init);
144
+ if (name && table) {
145
+ ids.set(name, makeStoreTableKey(table.storeBinding, table.tableName));
146
+ }
147
+ });
148
+
149
+ return ids;
150
+ };
151
+
152
+ /**
153
+ * Resolve an argument node to a composite store-table key
154
+ * (`${storeBinding}:${tableName}` or bare `tableName` when anonymous).
155
+ *
156
+ * Handles the two authoring patterns:
157
+ * - direct member access: `db.tables.notes`
158
+ * - identifier reference: `const notesTable = db.tables.notes; crud(notesTable, …)`
159
+ */
160
+ export const deriveStoreTableId = (
161
+ node: AstNode | undefined,
162
+ namedStoreTableIds: ReadonlyMap<string, string>
163
+ ): string | null => {
164
+ if (!node) {
165
+ return null;
166
+ }
167
+
168
+ if (node.type === 'Identifier') {
169
+ const name = identifierName(node);
170
+ return name ? (namedStoreTableIds.get(name) ?? null) : null;
171
+ }
172
+
173
+ const member = extractStoreTableFromMember(node);
174
+ return member
175
+ ? makeStoreTableKey(member.storeBinding, member.tableName)
176
+ : null;
177
+ };
178
+
179
+ const extractStoreTableDefinitions = (
180
+ node: AstNode,
181
+ storeBinding: string | null
182
+ ): readonly StoreTableDefinition[] => {
183
+ if (!isNamedCall(node, 'store')) {
184
+ return [];
185
+ }
186
+
187
+ const [tablesArg] = ((node as unknown as { arguments?: readonly AstNode[] })
188
+ .arguments ?? []) as readonly AstNode[];
189
+ if (!tablesArg || tablesArg.type !== 'ObjectExpression') {
190
+ return [];
191
+ }
192
+
193
+ const properties = tablesArg['properties'] as readonly AstNode[] | undefined;
194
+ if (!properties) {
195
+ return [];
196
+ }
197
+
198
+ return properties.flatMap((property) => {
199
+ if (property.type !== 'Property') {
200
+ return [];
201
+ }
202
+
203
+ const name = getPropertyName(property.key);
204
+ const value = property.value as AstNode | undefined;
205
+ if (!name || value?.type !== 'ObjectExpression') {
206
+ return [];
207
+ }
208
+
209
+ const versionedProp = findConfigProperty(value, 'versioned');
210
+ return [
211
+ {
212
+ key: makeStoreTableKey(storeBinding, name),
213
+ name,
214
+ start: property.start,
215
+ storeBinding,
216
+ versioned: isBooleanLiteral(
217
+ versionedProp?.value as AstNode | undefined
218
+ ),
219
+ },
220
+ ];
221
+ });
222
+ };
223
+
224
+ export const findStoreTableDefinitions = (
225
+ ast: AstNode
226
+ ): readonly StoreTableDefinition[] => {
227
+ const definitions: StoreTableDefinition[] = [];
228
+ const seenStoreCalls = new WeakSet<AstNode>();
229
+
230
+ // First pass: bound stores (walk VariableDeclarators so we know the binding).
231
+ walk(ast, (node) => {
232
+ if (node.type !== 'VariableDeclarator') {
233
+ return;
234
+ }
235
+
236
+ const { id, init } = node as unknown as {
237
+ readonly id?: AstNode;
238
+ readonly init?: AstNode;
239
+ };
240
+ if (!init || !isNamedCall(init, 'store')) {
241
+ return;
242
+ }
243
+
244
+ seenStoreCalls.add(init);
245
+ const storeBinding = extractBindingName(id);
246
+ definitions.push(...extractStoreTableDefinitions(init, storeBinding));
247
+ });
248
+
249
+ // Second pass: anonymous `store({...})` calls not bound to a variable
250
+ // (e.g. an inline default export). Use the bare table name as the key.
251
+ walk(ast, (node) => {
252
+ if (!isNamedCall(node, 'store') || seenStoreCalls.has(node)) {
253
+ return;
254
+ }
255
+ definitions.push(...extractStoreTableDefinitions(node, null));
256
+ });
257
+
258
+ return definitions;
259
+ };
260
+
261
+ export const collectCrudTableIds = (ast: AstNode): ReadonlySet<string> => {
262
+ const ids = new Set<string>();
263
+ const namedStoreTableIds = collectNamedStoreTableIds(ast);
264
+
265
+ walk(ast, (node) => {
266
+ if (!isNamedCall(node, 'crud')) {
267
+ return;
268
+ }
269
+
270
+ const [tableArg] = ((node as unknown as { arguments?: readonly AstNode[] })
271
+ .arguments ?? []) as readonly AstNode[];
272
+ const tableId = deriveStoreTableId(tableArg, namedStoreTableIds);
273
+ if (tableId) {
274
+ ids.add(tableId);
275
+ }
276
+ });
277
+
278
+ return ids;
279
+ };
280
+
281
+ export const collectReconcileTableIds = (ast: AstNode): ReadonlySet<string> => {
282
+ const ids = new Set<string>();
283
+ const namedStoreTableIds = collectNamedStoreTableIds(ast);
284
+
285
+ walk(ast, (node) => {
286
+ if (!isNamedCall(node, 'reconcile')) {
287
+ return;
288
+ }
289
+
290
+ const [configArg] = ((
291
+ node as unknown as {
292
+ arguments?: readonly AstNode[];
293
+ }
294
+ ).arguments ?? []) as readonly AstNode[];
295
+ if (!configArg || configArg.type !== 'ObjectExpression') {
296
+ return;
297
+ }
298
+
299
+ const tableProp = findConfigProperty(configArg, 'table');
300
+ const tableId = deriveStoreTableId(
301
+ tableProp?.value as AstNode | undefined,
302
+ namedStoreTableIds
303
+ );
304
+ if (tableId) {
305
+ ids.add(tableId);
306
+ }
307
+ });
308
+
309
+ return ids;
310
+ };
@@ -18,7 +18,7 @@ export interface ObjectProperty extends ParsedEntry {
18
18
 
19
19
  export interface TrailLikeSpec {
20
20
  readonly id: string;
21
- readonly kind: 'event' | 'trail';
21
+ readonly kind: 'signal' | 'trail';
22
22
  readonly line: number;
23
23
  readonly properties: ReadonlyMap<string, ObjectProperty>;
24
24
  readonly specText: string;
@@ -31,7 +31,11 @@ export interface SchemaFieldInfo {
31
31
  readonly required: boolean;
32
32
  }
33
33
 
34
- const TRAIL_LIKE_PATTERN = /\b(trail|event)\s*\(/g;
34
+ // Match `trail(...)` / `signal(...)` declaration sites, not method calls.
35
+ // The negative lookbehind excludes `foo.signal(...)`, `foo?.signal(...)`, and
36
+ // similar member-expression call sites (including optional chaining with
37
+ // whitespace) where `signal` / `trail` is a property name, not a factory.
38
+ const TRAIL_LIKE_PATTERN = /(?<![.?])\b(trail|signal)\s*\(/g;
35
39
 
36
40
  const PROPERTY_PATTERN =
37
41
  /^(?:readonly\s+)?(?:(["'`])([^"'`]+)\1|([A-Za-z_$][\w$]*))\s*:\s*([\s\S]+)$/;
@@ -214,7 +218,7 @@ const resolveSpecId = (
214
218
 
215
219
  const buildTrailLikeSpec = (
216
220
  sourceCode: string,
217
- kind: 'event' | 'trail',
221
+ kind: 'signal' | 'trail',
218
222
  specArg: SplitEntry,
219
223
  specStart: number,
220
224
  id: string,
@@ -276,7 +280,7 @@ const resolveTrailLikeSpec = (
276
280
 
277
281
  const parseTrailLikeMatch = (
278
282
  sourceCode: string,
279
- kind: 'event' | 'trail',
283
+ kind: 'signal' | 'trail',
280
284
  callStart: number
281
285
  ): TrailLikeSpec | null => {
282
286
  const resolved = resolveTrailLikeSpec(sourceCode, callStart);
@@ -352,7 +356,7 @@ export const findTrailLikeSpecs = (
352
356
  continue;
353
357
  }
354
358
 
355
- const kind = match[1] === 'event' ? 'event' : 'trail';
359
+ const kind = match[1] === 'signal' ? 'signal' : 'trail';
356
360
  const spec = parseTrailLikeMatch(sourceCode, kind, callStart);
357
361
  if (spec !== null) {
358
362
  specs.push(spec);