@starci/hfs 4.0.9 → 4.1.0

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 (247) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/LICENSE +21 -0
  3. package/emit/operations.mjs +1 -1
  4. package/emit/type-schema.mjs +2 -1
  5. package/lint/run.mjs +82 -8
  6. package/package.json +3 -2
  7. package/report/order.mjs +2 -0
  8. package/runtime/config.example.yaml +187 -0
  9. package/runtime/engine/by-code-unit.mjs +12 -0
  10. package/runtime/engine/config.mjs +247 -273
  11. package/runtime/engine/invalid-config.mjs +5 -5
  12. package/runtime/engine/model-config.mjs +90 -0
  13. package/runtime/engine/orca-config.mjs +3 -1
  14. package/runtime/engine/removed-vocabulary.mjs +28 -0
  15. package/runtime/engine/resources-config.mjs +52 -0
  16. package/runtime/engine/runtime-root.mjs +17 -0
  17. package/runtime/engine/secrets.mjs +134 -0
  18. package/runtime/engine/sonar-config.mjs +21 -0
  19. package/runtime/engine/temp-root.mjs +32 -0
  20. package/runtime/knowledge/hfs/canon-pins.yaml +18 -18
  21. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  22. package/runtime/knowledge/hfs/rules.yaml +102 -69
  23. package/runtime/knowledge/hfs/slots.yaml +11 -14
  24. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  25. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  26. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  27. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  28. package/runtime/modules/kernel/failure-codes.yaml +1 -1
  29. package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
  30. package/runtime/modules/models/registry.yaml +25 -397
  31. package/runtime/modules/models/runtimes.yaml +92 -262
  32. package/runtime/modules/models/tiers.yaml +65 -0
  33. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  34. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  35. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  36. package/runtime/scripts/api/fs/lib.mjs +6 -0
  37. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  38. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  39. package/runtime/scripts/api/git/lib.mjs +36 -1
  40. package/runtime/scripts/api/process/resolve-real-tool.mjs +52 -0
  41. package/runtime/scripts/api/process/run-program.mjs +8 -0
  42. package/runtime/scripts/api/sops/decrypt.mjs +9 -16
  43. package/runtime/scripts/api/sops/lib.mjs +309 -11
  44. package/runtime/scripts/api/sops/seal.mjs +8 -4
  45. package/runtime/scripts/hfs/allows.mjs +3 -3
  46. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  47. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  48. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  49. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  50. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  51. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  52. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  53. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  54. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  55. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  56. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  57. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  58. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  59. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  60. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  61. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  62. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  63. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  64. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  65. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  66. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  67. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  68. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  69. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  70. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  71. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +3 -2
  72. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  73. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  74. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  75. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  76. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  77. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  78. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  79. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  80. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  81. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  82. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  83. package/runtime/scripts/hfs/architecture/index.mjs +158 -102
  84. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  85. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  86. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  87. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  88. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  89. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  90. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  91. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  92. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  93. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  94. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  95. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  96. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  97. package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
  98. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  99. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  100. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  101. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  102. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  103. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  104. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  105. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  106. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  107. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  108. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  109. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  110. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  111. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  112. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  113. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  114. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  115. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  116. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  117. package/runtime/scripts/hfs/architecture.mjs +3 -4
  118. package/runtime/scripts/hfs/check.mjs +61 -70
  119. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  120. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  121. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  122. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  123. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  124. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  125. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  126. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  127. package/runtime/scripts/hfs/repo-identity.mjs +5 -2
  128. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  129. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  130. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  131. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  132. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  133. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  134. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  135. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  136. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  137. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  138. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  139. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  140. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  141. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  142. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  143. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  144. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  145. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  146. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  147. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  148. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  149. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  150. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  151. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  152. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  153. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  154. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  155. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  156. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  157. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  158. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  159. package/runtime/scripts/hfs/secret.mjs +90 -45
  160. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  161. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  162. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  163. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  164. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  165. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  166. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  167. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  168. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  169. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  170. package/runtime/scripts/hfs/slots.mjs +72 -693
  171. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  172. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  173. package/runtime/scripts/hfs/tree.mjs +19 -14
  174. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  175. package/runtime/scripts/hfs/view.mjs +1 -1
  176. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  177. package/runtime/scripts/lib/event-contract.mjs +17 -13
  178. package/runtime/scripts/lib/fs-kind.mjs +14 -4
  179. package/runtime/scripts/lib/git.mjs +2 -2
  180. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  181. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  182. package/runtime/scripts/lib/i18n.mjs +3 -2
  183. package/runtime/scripts/lib/in-order.mjs +71 -0
  184. package/runtime/scripts/lib/language.mjs +3 -3
  185. package/runtime/scripts/lib/list.mjs +8 -1
  186. package/runtime/scripts/lib/mutation-fence.mjs +15 -0
  187. package/runtime/scripts/lib/path-key.mjs +42 -9
  188. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  189. package/runtime/scripts/lib/regex.mjs +1 -1
  190. package/runtime/scripts/lib/same-text.mjs +1 -1
  191. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  192. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  193. package/runtime/scripts/lib/sops-envelope.mjs +103 -2
  194. package/runtime/scripts/lib/stack-services.mjs +1 -2
  195. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  196. package/runtime/scripts/lib/walk.mjs +6 -1
  197. package/scaffold/add-cli-lite.mjs +2 -1
  198. package/scaffold/add-table.mjs +2 -2
  199. package/scaffold/add.mjs +3 -2
  200. package/scaffold/app.mjs +4 -2
  201. package/scaffold/edition-gate.mjs +25 -25
  202. package/scaffold/lite-exports.mjs +2 -1
  203. package/scaffold/service.mjs +5 -4
  204. package/sync/hygiene.mjs +15 -9
  205. package/sync/index.mjs +7 -3
  206. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  207. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  208. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  209. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  210. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  211. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  212. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  213. package/templates/app/starciwork.gitignore +1 -1
  214. package/templates/be/image/api/Dockerfile +1 -1
  215. package/templates/be/image/cli/Dockerfile +1 -1
  216. package/templates/be/image/worker/Dockerfile +1 -1
  217. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  218. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  219. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  220. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  221. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  222. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  223. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  224. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  225. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  226. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  227. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  228. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  229. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  230. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  231. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  232. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  233. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  234. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  235. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  236. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  237. package/templates/fe/image/next/Dockerfile +2 -1
  238. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  239. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  240. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  241. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  242. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  243. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  244. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  245. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  246. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  247. package/upgrade/index.mjs +9 -2
@@ -31,64 +31,98 @@ const literalStrings = (ts, node) => {
31
31
  return null;
32
32
  };
33
33
 
34
- export function entitiesOf(kit, graph) {
34
+ function isTypeormDecorator(kit, checker, node, ...names) {
35
+ const call = kit.ts.isCallExpression(node.expression) ? node.expression.expression : node.expression;
36
+ const binding = kit.importBinding(checker, call);
37
+ return binding?.module === TYPEORM && names.includes(binding.name);
38
+ }
39
+
40
+ function entityTableInfo(kit, checker, statement) {
41
+ const { ts } = kit;
42
+ const entity = kit.decorators(statement).find(decorator => isTypeormDecorator(kit, checker, decorator, 'Entity'));
43
+ if (!entity) return null;
44
+ const argument = ts.isCallExpression(entity.expression) ? entity.expression.arguments[0] : null;
45
+ let table = argument && ts.isStringLiteralLike(argument) ? argument.text : null;
46
+ if (table === null && argument && ts.isObjectLiteralExpression(argument)) {
47
+ const property = kit.propertyOf(argument, 'name');
48
+ if (property && ts.isStringLiteralLike(kit.valueOfProperty(property))) table = kit.valueOfProperty(property).text;
49
+ }
50
+ return { table };
51
+ }
52
+
53
+ function mappedColumnName(kit, member, property) {
54
+ const { ts } = kit;
55
+ let column = property;
56
+ for (const decorator of kit.decorators(member)) {
57
+ const args = ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];
58
+ for (const item of args) if (ts.isObjectLiteralExpression(item)) {
59
+ const named = kit.propertyOf(item, 'name');
60
+ if (named && ts.isStringLiteralLike(kit.valueOfProperty(named))) column = kit.valueOfProperty(named).text;
61
+ }
62
+ }
63
+ return column;
64
+ }
65
+
66
+ function addUniqueColumnSet(kit, checker, decorator, column, primary, sets) {
35
67
  const { ts } = kit;
68
+ const args = ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];
69
+ const uniqueOption = args.some(item => ts.isObjectLiteralExpression(item) && kit.propertyOf(item, 'unique')
70
+ && kit.valueOfProperty(kit.propertyOf(item, 'unique')).kind === ts.SyntaxKind.TrueKeyword);
71
+ if (isTypeormDecorator(kit, checker, decorator, 'PrimaryColumn', 'PrimaryGeneratedColumn')) primary.push(column.toLowerCase());
72
+ else if (isTypeormDecorator(kit, checker, decorator, 'Column', 'Index') && uniqueOption) sets.push([column.toLowerCase()]);
73
+ }
74
+
75
+ function entityColumnSets(kit, checker, statement) {
76
+ const { ts } = kit;
77
+ const dbName = new Map();
78
+ const primary = [];
79
+ const sets = [];
80
+ for (const member of statement.members) {
81
+ if (!ts.isPropertyDeclaration(member) || !member.name || !ts.isIdentifier(member.name)) continue;
82
+ const property = member.name.text;
83
+ const column = mappedColumnName(kit, member, property);
84
+ dbName.set(property, column.toLowerCase());
85
+ for (const decorator of kit.decorators(member)) addUniqueColumnSet(kit, checker, decorator, column, primary, sets);
86
+ }
87
+ if (primary.length) sets.push(primary);
88
+ return { dbName, sets };
89
+ }
90
+
91
+ function addEntityUniqueSets(kit, checker, statement, dbName, sets) {
92
+ const { ts } = kit;
93
+ for (const decorator of kit.decorators(statement)) {
94
+ const args = ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];
95
+ const list = args.find(item => ts.isArrayLiteralExpression(item) && literalStrings(ts, item));
96
+ const columns = list ? literalStrings(ts, list) : null;
97
+ const unique = isTypeormDecorator(kit, checker, decorator, 'Unique') || (isTypeormDecorator(kit, checker, decorator, 'Index') && args.some(item => ts.isObjectLiteralExpression(item) && kit.propertyOf(item, 'unique')
98
+ && kit.valueOfProperty(kit.propertyOf(item, 'unique')).kind === ts.SyntaxKind.TrueKeyword));
99
+ if (unique && columns) sets.push(columns.map(column => (dbName.get(column) ?? column).toLowerCase()));
100
+ }
101
+ }
102
+
103
+ /** Records the entities one `.entity.ts` file declares into `tables`; returns how many of its entities have no readable table name. */
104
+ function addFileEntities(kit, file, tables) {
105
+ const checker = kit.checkerOf(file.sourceFile);
106
+ let unreadable = 0;
107
+ for (const statement of file.sourceFile.statements) {
108
+ if (!kit.ts.isClassDeclaration(statement)) continue;
109
+ const entity = entityTableInfo(kit, checker, statement);
110
+ if (!entity) continue;
111
+ if (entity.table === null) { unreadable += 1; continue; }
112
+ const { dbName, sets } = entityColumnSets(kit, checker, statement);
113
+ addEntityUniqueSets(kit, checker, statement, dbName, sets);
114
+ const key = entity.table.toLowerCase();
115
+ if (!tables.has(key)) tables.set(key, { table: entity.table, owner: file.owner.root, rel: file.rel, uniqueSets: sets });
116
+ }
117
+ return unreadable;
118
+ }
119
+
120
+ export function entitiesOf(kit, graph) {
36
121
  const tables = new Map();
37
122
  let unreadable = 0;
38
123
  for (const file of graph.files.values()) {
39
124
  if (file.slot !== 'be.persistence' || !path.posix.basename(file.rel).endsWith('.entity.ts') || !file.owner) continue;
40
- const checker = kit.checkerOf(file.sourceFile);
41
- const typeorm = (node, ...names) => {
42
- const call = ts.isCallExpression(node.expression) ? node.expression.expression : node.expression;
43
- const binding = kit.importBinding(checker, call);
44
- return binding?.module === TYPEORM && names.includes(binding.name);
45
- };
46
- for (const statement of file.sourceFile.statements) {
47
- if (!ts.isClassDeclaration(statement)) continue;
48
- const entity = kit.decorators(statement).find(decorator => typeorm(decorator, 'Entity'));
49
- if (!entity) continue;
50
- const argument = ts.isCallExpression(entity.expression) ? entity.expression.arguments[0] : null;
51
- let table = argument && ts.isStringLiteralLike(argument) ? argument.text : null;
52
- if (table === null && argument && ts.isObjectLiteralExpression(argument)) {
53
- const property = kit.propertyOf(argument, 'name');
54
- if (property && ts.isStringLiteralLike(kit.valueOfProperty(property))) table = kit.valueOfProperty(property).text;
55
- }
56
- if (table === null) { unreadable += 1; continue; }
57
- const dbName = new Map();
58
- const primary = [];
59
- const sets = [];
60
- for (const member of statement.members) {
61
- if (!ts.isPropertyDeclaration(member) || !member.name || !ts.isIdentifier(member.name)) continue;
62
- const property = member.name.text;
63
- let column = property;
64
- for (const decorator of kit.decorators(member)) {
65
- const args = ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];
66
- for (const item of args) if (ts.isObjectLiteralExpression(item)) {
67
- const named = kit.propertyOf(item, 'name');
68
- if (named && ts.isStringLiteralLike(kit.valueOfProperty(named))) column = kit.valueOfProperty(named).text;
69
- }
70
- }
71
- dbName.set(property, column.toLowerCase());
72
- for (const decorator of kit.decorators(member)) {
73
- const args = ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];
74
- const uniqueOption = args.some(item => ts.isObjectLiteralExpression(item) && kit.propertyOf(item, 'unique')
75
- && kit.valueOfProperty(kit.propertyOf(item, 'unique')).kind === ts.SyntaxKind.TrueKeyword);
76
- if (typeorm(decorator, 'PrimaryColumn', 'PrimaryGeneratedColumn')) primary.push(column.toLowerCase());
77
- else if (typeorm(decorator, 'Column', 'Index') && uniqueOption) sets.push([column.toLowerCase()]);
78
- }
79
- }
80
- if (primary.length) sets.push(primary);
81
- for (const decorator of kit.decorators(statement)) {
82
- const args = ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];
83
- const list = args.find(item => ts.isArrayLiteralExpression(item) && literalStrings(ts, item));
84
- const columns = list ? literalStrings(ts, list) : null;
85
- const unique = typeorm(decorator, 'Unique') || (typeorm(decorator, 'Index') && args.some(item => ts.isObjectLiteralExpression(item) && kit.propertyOf(item, 'unique')
86
- && kit.valueOfProperty(kit.propertyOf(item, 'unique')).kind === ts.SyntaxKind.TrueKeyword));
87
- if (unique && columns) sets.push(columns.map(column => (dbName.get(column) ?? column).toLowerCase()));
88
- }
89
- const key = table.toLowerCase();
90
- if (!tables.has(key)) tables.set(key, { table, owner: file.owner.root, rel: file.rel, uniqueSets: sets });
91
- }
125
+ unreadable += addFileEntities(kit, file, tables);
92
126
  }
93
127
  return { tables, unreadable };
94
128
  }
@@ -103,11 +137,93 @@ function supabaseDeclarations(graph, appRoot, ts) {
103
137
  if (file.slot !== 'be.persistence' || !file.owner) continue;
104
138
  const key = capabilityTable(file.owner.root);
105
139
  const declaration = tables.get(key);
106
- if (declaration && declaration.owner === null) tables.set(key, { ...declaration, owner: file.owner.root, rel: file.rel });
140
+ if (declaration?.owner === null) tables.set(key, { ...declaration, owner: file.owner.root, rel: file.rel });
107
141
  }
108
142
  return tables;
109
143
  }
110
144
 
145
+ function reportSqlWrite({ tables, supabase, file, ownTable, report }, write) {
146
+ const declaration = tables.get(write.table.toLowerCase());
147
+ if (!declaration) {
148
+ report(supabase
149
+ ? `SQL writes table ${write.table}, which Database['public']['Tables'] does not declare; regenerate supabase/types/database.types.ts from the migrations.`
150
+ : `SQL writes table ${write.table}, which no @Entity declares; declare the entity in the owning capability's persistence/entities.`, { table: write.table });
151
+ } else if (supabase) {
152
+ if (write.table.toLowerCase() === ownTable) return;
153
+ const ownerNote = declaration.owner ? `, owned by ${declaration.owner}` : '';
154
+ report(`SQL in ${file.owner.root} writes table ${write.table}${ownerNote}; under Supabase schema authority a capability writes only its own table (${ownTable}), named for that capability.`, { table: write.table, ...(declaration.owner ? { tableOwner: declaration.owner } : {}) });
155
+ } else if (declaration.owner !== file.owner.root) {
156
+ report(`SQL in ${file.owner.root} writes table ${write.table}, owned by ${declaration.owner}; a capability writes only the tables of its own entities, call the owner's public API instead.`, { table: write.table, tableOwner: declaration.owner });
157
+ }
158
+ }
159
+
160
+ function reportSqlWrites(input) {
161
+ for (const write of input.result.writes) reportSqlWrite(input, write);
162
+ }
163
+
164
+ function reportSqlRead(tables, supabase, file, model, resolver, report, read) {
165
+ const declaration = tables.get(read.table.toLowerCase());
166
+ if (!declaration) {
167
+ report(supabase
168
+ ? `SQL reads table ${read.table}, which Database['public']['Tables'] does not declare; regenerate supabase/types/database.types.ts from the migrations.`
169
+ : `SQL reads table ${read.table}, which no @Entity declares; declare the entity in the owning capability's persistence/entities.`, { table: read.table });
170
+ return;
171
+ }
172
+ if (!declaration.owner || declaration.owner === file.owner.root) return;
173
+ const ownContext = model.contextOfCapability(file.owner.root);
174
+ const tableContext = model.contextOfCapability(declaration.owner);
175
+ if (ownContext && tableContext && ownContext !== tableContext) {
176
+ report(`SQL in ${file.owner.root} (context ${ownContext}) reads table ${read.table}, owned by ${declaration.owner} (context ${tableContext}); a context reads and writes only its own context's tables, so a cross-context JOIN or subquery is refused. Keep a local copy fed by the other context's events (a reactor), or read a projection.`, { table: read.table, tableOwner: declaration.owner, context: ownContext, tableContext });
177
+ return;
178
+ }
179
+ const verdict = resolver.importAllowed(file.rel, `${declaration.owner}/index.ts`);
180
+ if (!verdict.allowed) report(`SQL in ${file.owner.root} reads table ${read.table}, owned by ${declaration.owner}, which ${file.owner.root} may not import (${verdict.reason}); read it through the owner's public API.`, { table: read.table, tableOwner: declaration.owner, reason: verdict.reason });
181
+ }
182
+
183
+ function reportSqlReads({ result, tables, supabase, file, model, resolver, report }) {
184
+ for (const read of result.reads) reportSqlRead(tables, supabase, file, model, resolver, report, read);
185
+ }
186
+
187
+ function reportSqlSelects({ result, tables, report }) {
188
+ for (const select of result.selects) {
189
+ if (select.bounded || (select.table && !tables.has(select.table.toLowerCase()))) continue;
190
+ const tableNote = select.table ? `on ${select.table} ` : '';
191
+ report(`SELECT ${tableNote}has no LIMIT and does not constrain a primary key or unique column with =; bound it with LIMIT (PAGE_SIZE_MAX, LIST_ROWS_MAX or BATCH_ROWS from platform/database), filter by key, or select only aggregates.`, { table: select.table ?? undefined });
192
+ }
193
+ }
194
+
195
+ function inspectSqlTag(input, state, file, checker, node) {
196
+ const { kit, ts, tables, supabase, model, resolver } = input;
197
+ if (!ts.isTaggedTemplateExpression(node)) return true;
198
+ const home = kit.declarationsOf(checker, node.tag).map(kit.ownerOfDeclaration).find(Boolean);
199
+ if (home?.tier !== 'platform' || home?.name !== 'database') { state.foreignTags += 1; return true; }
200
+ state.templates += 1;
201
+ const template = node.template;
202
+ const text = ts.isNoSubstitutionTemplateLiteral(template) ? template.text
203
+ : template.head.text + template.templateSpans.map(span => HOLE + span.literal.text).join('');
204
+ const seen = new Set();
205
+ const report = (message, extra = {}) => {
206
+ const key = `${message}`;
207
+ if (seen.has(key)) return;
208
+ seen.add(key);
209
+ state.violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
210
+ };
211
+ const result = analyzeSql(text, { uniqueSetsOf: table => tables.get(table.toLowerCase())?.uniqueSets ?? null });
212
+ state.dynamic += result.dynamic;
213
+ const ownTable = capabilityTable(file.owner.root);
214
+ reportSqlWrites({ result, tables, supabase, file, ownTable, report });
215
+ reportSqlReads({ result, tables, supabase, file, model, resolver, report });
216
+ reportSqlSelects({ result, tables, report });
217
+ return true;
218
+ }
219
+
220
+ function inspectSqlFile(input, state, file) {
221
+ const { kit } = input;
222
+ if (file.slot !== 'be.persistence' || !path.posix.basename(file.rel).endsWith('.sql.ts') || !file.owner) return;
223
+ const checker = kit.checkerOf(file.sourceFile);
224
+ kit.walk(file.sourceFile, node => inspectSqlTag(input, state, file, checker, node));
225
+ }
226
+
111
227
  export function checkSqlOwner(input) {
112
228
  const { config, graph } = input;
113
229
  const kit = machineKit(input);
@@ -116,68 +232,9 @@ export function checkSqlOwner(input) {
116
232
  const entityDeclarations = supabase ? { tables: new Map(), unreadable: 0 } : entitiesOf(kit, graph);
117
233
  const tables = supabase ? supabaseDeclarations(graph, config.packageRoot, ts) : entityDeclarations.tables;
118
234
  const model = contextModelOf(kit, graph);
119
- const violations = [];
120
- let templates = 0;
121
- let dynamic = 0;
122
- let foreignTags = 0;
123
- for (const file of graph.files.values()) {
124
- if (file.slot !== 'be.persistence' || !path.posix.basename(file.rel).endsWith('.sql.ts') || !file.owner) continue;
125
- const checker = kit.checkerOf(file.sourceFile);
126
- kit.walk(file.sourceFile, node => {
127
- if (!ts.isTaggedTemplateExpression(node)) return true;
128
- const home = kit.declarationsOf(checker, node.tag).map(kit.ownerOfDeclaration).find(Boolean);
129
- if (!home || home.tier !== 'platform' || home.name !== 'database') { foreignTags += 1; return true; }
130
- templates += 1;
131
- const template = node.template;
132
- const text = ts.isNoSubstitutionTemplateLiteral(template) ? template.text
133
- : template.head.text + template.templateSpans.map(span => HOLE + span.literal.text).join('');
134
- const seen = new Set();
135
- const report = (message, extra = {}) => {
136
- const key = `${message}`;
137
- if (seen.has(key)) return;
138
- seen.add(key);
139
- violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
140
- };
141
- const result = analyzeSql(text, { uniqueSetsOf: table => tables.get(table.toLowerCase())?.uniqueSets ?? null });
142
- dynamic += result.dynamic;
143
- const ownTable = capabilityTable(file.owner.root);
144
- for (const write of result.writes) {
145
- const declaration = tables.get(write.table.toLowerCase());
146
- if (!declaration) {
147
- report(supabase
148
- ? `SQL writes table ${write.table}, which Database['public']['Tables'] does not declare; regenerate supabase/types/database.types.ts from the migrations.`
149
- : `SQL writes table ${write.table}, which no @Entity declares; declare the entity in the owning capability's persistence/entities.`, { table: write.table });
150
- } else if (supabase && write.table.toLowerCase() !== ownTable) {
151
- report(`SQL in ${file.owner.root} writes table ${write.table}${declaration.owner ? `, owned by ${declaration.owner}` : ''}; under Supabase schema authority a capability writes only its own table (${ownTable}), named for that capability.`, { table: write.table, ...(declaration.owner ? { tableOwner: declaration.owner } : {}) });
152
- } else if (!supabase && declaration.owner !== file.owner.root) {
153
- report(`SQL in ${file.owner.root} writes table ${write.table}, owned by ${declaration.owner}; a capability writes only the tables of its own entities, call the owner's public API instead.`, { table: write.table, tableOwner: declaration.owner });
154
- }
155
- }
156
- for (const read of result.reads) {
157
- const declaration = tables.get(read.table.toLowerCase());
158
- if (!declaration) {
159
- report(supabase
160
- ? `SQL reads table ${read.table}, which Database['public']['Tables'] does not declare; regenerate supabase/types/database.types.ts from the migrations.`
161
- : `SQL reads table ${read.table}, which no @Entity declares; declare the entity in the owning capability's persistence/entities.`, { table: read.table });
162
- continue;
163
- }
164
- if (!declaration.owner || declaration.owner === file.owner.root) continue;
165
- const ownContext = model.contextOfCapability(file.owner.root);
166
- const tableContext = model.contextOfCapability(declaration.owner);
167
- if (ownContext && tableContext && ownContext !== tableContext) {
168
- report(`SQL in ${file.owner.root} (context ${ownContext}) reads table ${read.table}, owned by ${declaration.owner} (context ${tableContext}); a context reads and writes only its own context's tables, so a cross-context JOIN or subquery is refused. Keep a local copy fed by the other context's events (a reactor), or read a projection.`, { table: read.table, tableOwner: declaration.owner, context: ownContext, tableContext });
169
- continue;
170
- }
171
- const verdict = resolver.importAllowed(file.rel, `${declaration.owner}/index.ts`);
172
- if (!verdict.allowed) report(`SQL in ${file.owner.root} reads table ${read.table}, owned by ${declaration.owner}, which ${file.owner.root} may not import (${verdict.reason}); read it through the owner's public API.`, { table: read.table, tableOwner: declaration.owner, reason: verdict.reason });
173
- }
174
- for (const select of result.selects) {
175
- if (select.bounded || (select.table && !tables.has(select.table.toLowerCase()))) continue;
176
- report(`SELECT ${select.table ? `on ${select.table} ` : ''}has no LIMIT and does not constrain a primary key or unique column with =; bound it with LIMIT (PAGE_SIZE_MAX, LIST_ROWS_MAX or BATCH_ROWS from platform/database), filter by key, or select only aggregates.`, { table: select.table ?? undefined });
177
- }
178
- return true;
179
- });
180
- }
181
- return { violations, coverage: { status: 'checked', entities: entityDeclarations.tables.size, unreadableEntities: entityDeclarations.unreadable,
182
- ...(supabase ? { supabaseTables: tables.size } : {}), templates, dynamicIdentifiers: dynamic, foreignTags } };
235
+ const state = { violations: [], templates: 0, dynamic: 0, foreignTags: 0 };
236
+ const inspection = { kit, ts, tables, supabase, model, resolver };
237
+ for (const file of graph.files.values()) inspectSqlFile(inspection, state, file);
238
+ return { violations: state.violations, coverage: { status: 'checked', entities: entityDeclarations.tables.size, unreadableEntities: entityDeclarations.unreadable,
239
+ ...(supabase ? { supabaseTables: tables.size } : {}), templates: state.templates, dynamicIdentifiers: state.dynamic, foreignTags: state.foreignTags } };
183
240
  }
@@ -15,28 +15,47 @@ export const SQL_RETURNING_RULE_IDS = ['BE_SQL_RETURNING_SHAPE'];
15
15
 
16
16
  const RULE = 'BE_SQL_RETURNING_SHAPE';
17
17
 
18
+ /** Ends the statement in progress, recording its leading verb when it carried RETURNING. */
19
+ function closeStatement(state) {
20
+ if (state.verb && state.returning) state.found.push(state.verb);
21
+ state.verb = null;
22
+ state.returning = false;
23
+ }
24
+
25
+ /** Folds one token into the statement state: parenthesis depth, statement ends and the top-level words. */
26
+ function readToken(state, token) {
27
+ if (token.t === 'punct') {
28
+ if (token.v === '(') state.depth += 1;
29
+ else if (token.v === ')') state.depth = Math.max(0, state.depth - 1);
30
+ else if (token.v === ';' && state.depth === 0) closeStatement(state);
31
+ return;
32
+ }
33
+ if (state.depth !== 0 || token.t !== 'word') return;
34
+ if (state.verb === null) state.verb = token.up;
35
+ else if (token.up === 'RETURNING') state.returning = true;
36
+ }
37
+
18
38
  /** The leading verbs of the top-level statements of `text` that RETURN from an UPDATE or DELETE without a wrapping SELECT. */
19
39
  function unwrappedReturning(text) {
20
- const found = [];
21
- let depth = 0;
22
- let verb = null;
23
- let returning = false;
24
- const close = () => { if (verb && returning) found.push(verb); verb = null; returning = false; };
25
- for (const token of tokenizeSql(text)) {
26
- if (token.t === 'punct') {
27
- if (token.v === '(') depth += 1;
28
- else if (token.v === ')') depth = Math.max(0, depth - 1);
29
- else if (token.v === ';' && depth === 0) close();
30
- continue;
31
- }
32
- if (depth !== 0 || token.t !== 'word') continue;
33
- if (verb === null) verb = token.up;
34
- else if (token.up === 'RETURNING') returning = true;
35
- }
36
- close();
37
- return found.filter(verb => verb === 'UPDATE' || verb === 'DELETE');
40
+ const state = { found: [], depth: 0, verb: null, returning: false };
41
+ for (const token of tokenizeSql(text)) readToken(state, token);
42
+ closeStatement(state);
43
+ return state.found.filter(verb => verb === 'UPDATE' || verb === 'DELETE');
44
+ }
45
+
46
+ /** The SQL text of a tagged template, each substitution replaced by a HOLE marker. */
47
+ function templateText(ts, template) {
48
+ return ts.isNoSubstitutionTemplateLiteral(template) ? template.text
49
+ : template.head.text + template.templateSpans.map(span => HOLE + span.literal.text).join('');
38
50
  }
39
51
 
52
+ const isDatabaseTag = (kit, checker, node) => {
53
+ const home = kit.declarationsOf(checker, node.tag).map(kit.ownerOfDeclaration).find(Boolean);
54
+ return home?.tier === 'platform' && home?.name === 'database';
55
+ };
56
+
57
+ const returningViolation = (kit, file, node, verb) => ({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message: `${verb} ... RETURNING in ${file.rel} is returned by EntityManager.query as [rows, affectedCount], not as rows, so a caller reading rows reads the wrong value. Wrap it: WITH changed AS (${verb} ... RETURNING ...) SELECT ... FROM changed.`, verb });
58
+
40
59
  export function checkSqlReturning(input) {
41
60
  const { graph } = input;
42
61
  const kit = machineKit(input);
@@ -47,17 +66,9 @@ export function checkSqlReturning(input) {
47
66
  if (file.slot !== 'be.persistence' || !path.posix.basename(file.rel).endsWith('.sql.ts') || !file.owner) continue;
48
67
  const checker = kit.checkerOf(file.sourceFile);
49
68
  kit.walk(file.sourceFile, node => {
50
- if (!ts.isTaggedTemplateExpression(node)) return true;
51
- const home = kit.declarationsOf(checker, node.tag).map(kit.ownerOfDeclaration).find(Boolean);
52
- if (!home || home.tier !== 'platform' || home.name !== 'database') return true;
69
+ if (!ts.isTaggedTemplateExpression(node) || !isDatabaseTag(kit, checker, node)) return;
53
70
  templates += 1;
54
- const template = node.template;
55
- const text = ts.isNoSubstitutionTemplateLiteral(template) ? template.text
56
- : template.head.text + template.templateSpans.map(span => HOLE + span.literal.text).join('');
57
- for (const verb of unwrappedReturning(text)) {
58
- violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message: `${verb} ... RETURNING in ${file.rel} is returned by EntityManager.query as [rows, affectedCount], not as rows, so a caller reading rows reads the wrong value. Wrap it: WITH changed AS (${verb} ... RETURNING ...) SELECT ... FROM changed.`, verb });
59
- }
60
- return true;
71
+ for (const verb of unwrappedReturning(templateText(ts, node.template))) violations.push(returningViolation(kit, file, node, verb));
61
72
  });
62
73
  }
63
74
  return { violations, coverage: { status: 'checked', templates } };