kibi-cli 2.31.1 → 2.35.1

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 (302) hide show
  1. package/dist/cli-register-proof.d.ts.map +1 -1
  2. package/dist/cli-register-proof.js +19 -7
  3. package/dist/cli-register-reporting.d.ts.map +1 -1
  4. package/dist/cli-register-reporting.js +2 -0
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +5 -1
  7. package/dist/commands/check-generated.d.ts +5 -1
  8. package/dist/commands/check-generated.d.ts.map +1 -1
  9. package/dist/commands/check-generated.js +24 -35
  10. package/dist/commands/check.d.ts +1 -0
  11. package/dist/commands/check.d.ts.map +1 -1
  12. package/dist/commands/check.js +14 -5
  13. package/dist/commands/discovery-table.d.ts.map +1 -1
  14. package/dist/commands/discovery-table.js +51 -4
  15. package/dist/commands/doctor.js +19 -4
  16. package/dist/commands/init-helpers.d.ts.map +1 -1
  17. package/dist/commands/init-helpers.js +8 -5
  18. package/dist/commands/migrate.d.ts.map +1 -1
  19. package/dist/commands/migrate.js +109 -5
  20. package/dist/commands/prove.d.ts +48 -1
  21. package/dist/commands/prove.d.ts.map +1 -1
  22. package/dist/commands/prove.js +293 -8
  23. package/dist/commands/report.d.ts +1 -0
  24. package/dist/commands/report.d.ts.map +1 -1
  25. package/dist/commands/report.js +70 -5
  26. package/dist/commands/signoff.d.ts.map +1 -1
  27. package/dist/commands/signoff.js +20 -1
  28. package/dist/commands/sync/manifest.d.ts +10 -1
  29. package/dist/commands/sync/manifest.d.ts.map +1 -1
  30. package/dist/commands/sync/manifest.js +106 -17
  31. package/dist/commands/sync/persistence.d.ts.map +1 -1
  32. package/dist/commands/sync/persistence.js +16 -5
  33. package/dist/commands/sync.d.ts.map +1 -1
  34. package/dist/commands/sync.js +19 -0
  35. package/dist/diagnostics.js +1 -1
  36. package/dist/engine.js +1 -1
  37. package/dist/extractors/markdown.d.ts +4 -2
  38. package/dist/extractors/markdown.d.ts.map +1 -1
  39. package/dist/extractors/markdown.js +17 -0
  40. package/dist/extractors/symbol-coordinates.js +2 -2
  41. package/dist/extractors/symbols-coordinator.d.ts +2 -4
  42. package/dist/extractors/symbols-coordinator.d.ts.map +1 -1
  43. package/dist/extractors/symbols-coordinator.js +1 -2
  44. package/dist/operations/bootstrap/candidate-helpers.d.ts +3 -0
  45. package/dist/operations/bootstrap/candidate-helpers.d.ts.map +1 -1
  46. package/dist/operations/bootstrap/candidate-helpers.js +4 -0
  47. package/dist/operations/migration/evidence-floor.d.ts +41 -0
  48. package/dist/operations/migration/evidence-floor.d.ts.map +1 -0
  49. package/dist/operations/migration/evidence-floor.js +230 -0
  50. package/dist/operations/migration/proof-contract-claims.d.ts +72 -0
  51. package/dist/operations/migration/proof-contract-claims.d.ts.map +1 -0
  52. package/dist/operations/migration/proof-contract-claims.js +215 -0
  53. package/dist/operations/migration/schema6.d.ts +6 -0
  54. package/dist/operations/migration/schema6.d.ts.map +1 -1
  55. package/dist/operations/migration/schema6.js +92 -0
  56. package/dist/operations/migration/symbol-coordinates-refresh.d.ts +3 -2
  57. package/dist/operations/migration/symbol-coordinates-refresh.d.ts.map +1 -1
  58. package/dist/operations/migration/symbol-coordinates-refresh.js +4 -3
  59. package/dist/operations/migration/untrack-symbol-coordinates.d.ts +23 -0
  60. package/dist/operations/migration/untrack-symbol-coordinates.d.ts.map +1 -0
  61. package/dist/operations/migration/untrack-symbol-coordinates.js +67 -0
  62. package/dist/operations/modeling/requirement-modeler.d.ts.map +1 -1
  63. package/dist/operations/modeling/requirement-modeler.js +48 -1
  64. package/dist/operations/modeling/requirement-types.d.ts +2 -1
  65. package/dist/operations/modeling/requirement-types.d.ts.map +1 -1
  66. package/dist/operations/modeling/requirement-utils.d.ts +1 -1
  67. package/dist/operations/modeling/requirement-utils.d.ts.map +1 -1
  68. package/dist/operations/modeling/requirement-utils.js +8 -1
  69. package/dist/operations/mutation/omitted-properties.d.ts +11 -0
  70. package/dist/operations/mutation/omitted-properties.d.ts.map +1 -0
  71. package/dist/operations/mutation/omitted-properties.js +76 -0
  72. package/dist/operations/mutation/serialization.d.ts.map +1 -1
  73. package/dist/operations/mutation/serialization.js +1 -0
  74. package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
  75. package/dist/operations/mutation/source-authoring.js +39 -20
  76. package/dist/operations/mutation/strict-fact.d.ts.map +1 -1
  77. package/dist/operations/mutation/strict-fact.js +6 -2
  78. package/dist/operations/mutation/symbol-granularity.d.ts.map +1 -1
  79. package/dist/operations/mutation/symbol-granularity.js +5 -3
  80. package/dist/operations/mutation/upsert.d.ts.map +1 -1
  81. package/dist/operations/mutation/upsert.js +7 -1
  82. package/dist/operations/mutation/validation.d.ts +2 -0
  83. package/dist/operations/mutation/validation.d.ts.map +1 -1
  84. package/dist/operations/mutation/validation.js +4 -0
  85. package/dist/operations/proof/claim-bindings.d.ts +41 -0
  86. package/dist/operations/proof/claim-bindings.d.ts.map +1 -0
  87. package/dist/operations/proof/claim-bindings.js +364 -0
  88. package/dist/operations/proof/code-certificates.d.ts +155 -0
  89. package/dist/operations/proof/code-certificates.d.ts.map +1 -0
  90. package/dist/operations/proof/code-certificates.js +894 -0
  91. package/dist/operations/proof/code-models.d.ts +96 -0
  92. package/dist/operations/proof/code-models.d.ts.map +1 -0
  93. package/dist/operations/proof/code-models.js +601 -0
  94. package/dist/operations/proof/code-runner.d.ts +40 -0
  95. package/dist/operations/proof/code-runner.d.ts.map +1 -0
  96. package/dist/operations/proof/code-runner.js +164 -0
  97. package/dist/operations/proof/code-scope.d.ts +14 -1
  98. package/dist/operations/proof/code-scope.d.ts.map +1 -1
  99. package/dist/operations/proof/code-scope.js +44 -2
  100. package/dist/operations/proof/compact-receipts.d.ts.map +1 -1
  101. package/dist/operations/proof/compact-receipts.js +4 -3
  102. package/dist/operations/proof/explain.d.ts +32 -0
  103. package/dist/operations/proof/explain.d.ts.map +1 -1
  104. package/dist/operations/proof/explain.js +91 -0
  105. package/dist/operations/proof/ingest-proof.d.ts +9 -1
  106. package/dist/operations/proof/ingest-proof.d.ts.map +1 -1
  107. package/dist/operations/proof/ingest-proof.js +42 -2
  108. package/dist/operations/proof/receipt-compaction.d.ts +2 -0
  109. package/dist/operations/proof/receipt-compaction.d.ts.map +1 -1
  110. package/dist/operations/proof/receipt-compaction.js +6 -2
  111. package/dist/operations/proof/regular-language.d.ts +89 -0
  112. package/dist/operations/proof/regular-language.d.ts.map +1 -0
  113. package/dist/operations/proof/regular-language.js +943 -0
  114. package/dist/operations/proof/source-code-readers.d.ts +12 -0
  115. package/dist/operations/proof/source-code-readers.d.ts.map +1 -0
  116. package/dist/operations/proof/source-code-readers.js +699 -0
  117. package/dist/operations/semantic-advisor/analyze-prose.d.ts.map +1 -1
  118. package/dist/operations/semantic-advisor/analyze-prose.js +2 -1
  119. package/dist/operations/semantic-advisor/shared.d.ts.map +1 -1
  120. package/dist/operations/semantic-advisor/shared.js +4 -0
  121. package/dist/operations/semantic-advisor/strict-count-rules.d.ts.map +1 -1
  122. package/dist/operations/semantic-advisor/strict-count-rules.js +8 -2
  123. package/dist/operations/semantic-advisor/strict-rules.d.ts +8 -0
  124. package/dist/operations/semantic-advisor/strict-rules.d.ts.map +1 -1
  125. package/dist/operations/semantic-advisor/strict-rules.js +234 -4
  126. package/dist/operations/semantic-advisor/types.d.ts +3 -2
  127. package/dist/operations/semantic-advisor/types.d.ts.map +1 -1
  128. package/dist/plugins/approved-source-analyzers.generated.d.ts +6 -6
  129. package/dist/plugins/approved-source-analyzers.generated.js +6 -6
  130. package/dist/prolog.d.ts.map +1 -1
  131. package/dist/prolog.js +13 -3
  132. package/dist/proof/command-test-report.d.ts +4 -3
  133. package/dist/proof/command-test-report.d.ts.map +1 -1
  134. package/dist/proof/command-test-report.js +4 -10
  135. package/dist/proof/evaluate.d.ts.map +1 -1
  136. package/dist/proof/evaluate.js +18 -2
  137. package/dist/proof/inspect.d.ts.map +1 -1
  138. package/dist/proof/inspect.js +58 -0
  139. package/dist/proof/producers/playwright-reporter.d.ts +3 -0
  140. package/dist/proof/producers/playwright-reporter.d.ts.map +1 -1
  141. package/dist/proof/producers/playwright-reporter.js +25 -2
  142. package/dist/proof/step-runner.d.ts +36 -0
  143. package/dist/proof/step-runner.d.ts.map +1 -0
  144. package/dist/proof/step-runner.js +234 -0
  145. package/dist/public/evidence-levels.d.ts +12 -0
  146. package/dist/public/evidence-levels.d.ts.map +1 -0
  147. package/dist/public/evidence-levels.js +19 -0
  148. package/dist/public/impact/coverage-depth-quality.d.ts +7 -0
  149. package/dist/public/impact/coverage-depth-quality.d.ts.map +1 -1
  150. package/dist/public/impact/coverage-depth-quality.js +59 -0
  151. package/dist/public/impact/full-kb-quality.d.ts.map +1 -1
  152. package/dist/public/impact/full-kb-quality.js +3 -1
  153. package/dist/public/operations/catalog.d.ts +315 -11
  154. package/dist/public/operations/catalog.d.ts.map +1 -1
  155. package/dist/public/operations/check-executor.d.ts +2 -0
  156. package/dist/public/operations/check-executor.d.ts.map +1 -1
  157. package/dist/public/operations/check-executor.js +6 -0
  158. package/dist/public/operations/evidence-floor.d.ts +48 -0
  159. package/dist/public/operations/evidence-floor.d.ts.map +1 -0
  160. package/dist/public/operations/evidence-floor.js +143 -0
  161. package/dist/public/operations/migration-plan.d.ts +19 -5
  162. package/dist/public/operations/migration-plan.d.ts.map +1 -1
  163. package/dist/public/operations/migration-plan.js +22 -5
  164. package/dist/public/operations/node-ports.d.ts.map +1 -1
  165. package/dist/public/operations/node-ports.js +4 -1
  166. package/dist/public/operations/proof-contract-claims.d.ts +26 -0
  167. package/dist/public/operations/proof-contract-claims.d.ts.map +1 -0
  168. package/dist/public/operations/proof-contract-claims.js +71 -0
  169. package/dist/public/operations/specs/check-rules.generated.d.ts +1 -1
  170. package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
  171. package/dist/public/operations/specs/check-rules.generated.js +3 -1
  172. package/dist/public/operations/specs/check.d.ts +1 -1
  173. package/dist/public/operations/specs/composite.d.ts +16 -3
  174. package/dist/public/operations/specs/composite.d.ts.map +1 -1
  175. package/dist/public/operations/specs/modeling.d.ts +16 -3
  176. package/dist/public/operations/specs/modeling.d.ts.map +1 -1
  177. package/dist/public/operations/specs/modeling.js +22 -3
  178. package/dist/public/operations/specs/mutation-schemas.d.ts +289 -7
  179. package/dist/public/operations/specs/mutation-schemas.d.ts.map +1 -1
  180. package/dist/public/operations/specs/mutation-schemas.js +28 -4
  181. package/dist/public/operations/specs/proof.d.ts +9 -0
  182. package/dist/public/operations/specs/proof.d.ts.map +1 -1
  183. package/dist/public/operations/specs/reporting.d.ts.map +1 -1
  184. package/dist/public/operations/specs/reporting.js +19 -10
  185. package/dist/public/operations/specs/upsert.d.ts +289 -7
  186. package/dist/public/operations/specs/upsert.d.ts.map +1 -1
  187. package/dist/public/proof-certificates.d.ts +163 -0
  188. package/dist/public/proof-certificates.d.ts.map +1 -0
  189. package/dist/public/proof-certificates.js +419 -0
  190. package/dist/public/proof-protocol.d.ts +259 -3
  191. package/dist/public/proof-protocol.d.ts.map +1 -1
  192. package/dist/public/proof-protocol.js +524 -3
  193. package/dist/public/proof-receipt.d.ts +90 -6
  194. package/dist/public/proof-receipt.d.ts.map +1 -1
  195. package/dist/public/proof-receipt.js +88 -7
  196. package/dist/public/proof-sources.d.ts +102 -0
  197. package/dist/public/proof-sources.d.ts.map +1 -0
  198. package/dist/public/proof-sources.js +1292 -0
  199. package/dist/public/schemas/entity.d.ts.map +1 -1
  200. package/dist/public/schemas/entity.js +39 -2
  201. package/dist/public/skills/kibi-bootstrap/SKILL.md +2 -2
  202. package/dist/public/skills/kibi-commit-review/SKILL.md +8 -8
  203. package/dist/public/skills/kibi-commit-review/resources/staged-gate.md +12 -7
  204. package/dist/public/skills/kibi-decisions/SKILL.md +13 -2
  205. package/dist/public/skills/kibi-decisions/resources/decision-records.md +9 -1
  206. package/dist/public/skills/kibi-freshness/SKILL.md +5 -1
  207. package/dist/public/skills/kibi-traceability/SKILL.md +12 -1
  208. package/dist/public/skills/kibi-usage/SKILL.md +8 -4
  209. package/dist/public/skills/kibi-usage/resources/proof.md +221 -20
  210. package/dist/public/skills/kibi-usage/resources/workflows.md +4 -4
  211. package/dist/report/html-report.css +83 -11
  212. package/dist/report/html-report.d.ts +8 -0
  213. package/dist/report/html-report.d.ts.map +1 -1
  214. package/dist/report/html-report.js +214 -22
  215. package/dist/report/report-view.d.ts +2 -2
  216. package/dist/report/report-view.d.ts.map +1 -1
  217. package/dist/report/report-view.js +8 -0
  218. package/dist/schemas/entity.schema.json +404 -5
  219. package/dist/search-answer.d.ts.map +1 -1
  220. package/dist/search-answer.js +1 -0
  221. package/dist/signoff/html.d.ts.map +1 -1
  222. package/dist/signoff/html.js +3 -0
  223. package/dist/signoff/markdown.d.ts.map +1 -1
  224. package/dist/signoff/markdown.js +3 -0
  225. package/dist/signoff/model.d.ts +20 -0
  226. package/dist/signoff/model.d.ts.map +1 -1
  227. package/dist/signoff/model.js +20 -1
  228. package/dist/traceability/evidence-model.d.ts +13 -12
  229. package/dist/traceability/evidence-model.d.ts.map +1 -1
  230. package/dist/traceability/evidence-model.js +1 -1
  231. package/dist/traceability/impact-knowledge.d.ts.map +1 -1
  232. package/dist/traceability/impact-knowledge.js +1 -0
  233. package/dist/traceability/staged-diagnostics.d.ts.map +1 -1
  234. package/dist/traceability/staged-diagnostics.js +24 -20
  235. package/dist/traceability/staged-impact-contract.d.ts +4 -4
  236. package/dist/traceability/staged-impact-contract.js +9 -9
  237. package/dist/traceability/staged-symbols-manifest.d.ts +11 -3
  238. package/dist/traceability/staged-symbols-manifest.d.ts.map +1 -1
  239. package/dist/traceability/staged-symbols-manifest.js +47 -140
  240. package/dist/traceability/temp-kb.d.ts.map +1 -1
  241. package/dist/traceability/temp-kb.js +1 -0
  242. package/dist/types/entities.d.ts +5 -2
  243. package/dist/types/entities.d.ts.map +1 -1
  244. package/dist/utils/merge-driver-setup.d.ts +8 -1
  245. package/dist/utils/merge-driver-setup.d.ts.map +1 -1
  246. package/dist/utils/merge-driver-setup.js +23 -2
  247. package/dist/utils/rule-registry.generated.d.ts +10 -0
  248. package/dist/utils/rule-registry.generated.d.ts.map +1 -1
  249. package/dist/utils/rule-registry.generated.js +12 -0
  250. package/dist/utils/schema-version.d.ts +1 -1
  251. package/dist/utils/schema-version.d.ts.map +1 -1
  252. package/dist/utils/schema-version.js +7 -1
  253. package/dist/utils/strict-modeling.d.ts +3 -2
  254. package/dist/utils/strict-modeling.d.ts.map +1 -1
  255. package/dist/utils/strict-modeling.js +37 -4
  256. package/dist/workspace-cwd.d.ts +15 -0
  257. package/dist/workspace-cwd.d.ts.map +1 -0
  258. package/dist/workspace-cwd.js +86 -0
  259. package/package.json +6 -2
  260. package/schema/entities.pl +6 -0
  261. package/schema/validation.pl +7 -2
  262. package/src/public/evidence-levels.ts +23 -0
  263. package/src/public/impact/coverage-depth-quality.ts +73 -0
  264. package/src/public/impact/full-kb-quality.ts +6 -1
  265. package/src/public/operations/check-executor.ts +13 -0
  266. package/src/public/operations/evidence-floor.ts +203 -0
  267. package/src/public/operations/migration-plan.ts +39 -6
  268. package/src/public/operations/node-ports.ts +3 -1
  269. package/src/public/operations/proof-contract-claims.ts +103 -0
  270. package/src/public/operations/specs/check-rules.generated.ts +3 -1
  271. package/src/public/operations/specs/modeling.ts +24 -5
  272. package/src/public/operations/specs/mutation-schemas.ts +30 -4
  273. package/src/public/operations/specs/reporting.ts +20 -10
  274. package/src/public/proof-certificates.ts +566 -0
  275. package/src/public/proof-protocol-types.d.ts +69 -1
  276. package/src/public/proof-protocol.ts +694 -3
  277. package/src/public/proof-receipt.ts +133 -7
  278. package/src/public/proof-sources.ts +1656 -0
  279. package/src/public/schemas/entity.ts +51 -3
  280. package/src/public/skills/kibi-bootstrap/SKILL.md +2 -2
  281. package/src/public/skills/kibi-commit-review/SKILL.md +8 -8
  282. package/src/public/skills/kibi-commit-review/resources/staged-gate.md +12 -7
  283. package/src/public/skills/kibi-decisions/SKILL.md +13 -2
  284. package/src/public/skills/kibi-decisions/resources/decision-records.md +9 -1
  285. package/src/public/skills/kibi-freshness/SKILL.md +5 -1
  286. package/src/public/skills/kibi-traceability/SKILL.md +12 -1
  287. package/src/public/skills/kibi-usage/SKILL.md +8 -4
  288. package/src/public/skills/kibi-usage/resources/proof.md +221 -20
  289. package/src/public/skills/kibi-usage/resources/workflows.md +4 -4
  290. package/src/schemas/entity.schema.json +404 -5
  291. package/dist/commands/proof-impact.d.ts +0 -7
  292. package/dist/commands/proof-impact.d.ts.map +0 -1
  293. package/dist/commands/proof-impact.js +0 -39
  294. package/dist/operations/proof/baseline-diff.d.ts +0 -41
  295. package/dist/operations/proof/baseline-diff.d.ts.map +0 -1
  296. package/dist/operations/proof/baseline-diff.js +0 -153
  297. package/dist/operations/proof/impact.d.ts +0 -51
  298. package/dist/operations/proof/impact.d.ts.map +0 -1
  299. package/dist/operations/proof/impact.js +0 -138
  300. package/dist/traceability/reviewed-coordinate-refresh.d.ts +0 -10
  301. package/dist/traceability/reviewed-coordinate-refresh.d.ts.map +0 -1
  302. package/dist/traceability/reviewed-coordinate-refresh.js +0 -49
@@ -0,0 +1,1292 @@
1
+ /*
2
+ Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
+ Copyright (C) 2026 Piotr Franczyk
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+ /**
19
+ * Project sources Kibi reads by itself for the `source` proof kind, and for
20
+ * source-backed rule domains, rule predicates and count value sources: a
21
+ * JSON or YAML file, the files a glob matches, an OpenAPI document, the
22
+ * references the code makes (environment variables, imports, calls, string
23
+ * arguments, thrown error codes, property values), a data schema (drizzle,
24
+ * Prisma or SQL migrations), a web app's route table (Hono, Express) and a
25
+ * validator object (zod and anything with safeParse).
26
+ *
27
+ * Every reader is exact or refuses: a member Kibi cannot read statically (a
28
+ * computed environment variable name, a dynamic import, an ALTER TABLE form
29
+ * it does not know, a router mounted on a pattern) fails the read with its
30
+ * location, so a set claim is never decided over a set Kibi only partly saw.
31
+ */
32
+ import { createHash } from "node:crypto";
33
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
34
+ import path from "node:path";
35
+ import fg from "fast-glob";
36
+ import { parse as parseYaml } from "yaml";
37
+ // implements REQ-kibi-source-proof-kinds
38
+ export const SOURCE_READERS = [
39
+ "json",
40
+ "yaml",
41
+ "glob",
42
+ "openapi",
43
+ "references",
44
+ "schema",
45
+ "routes",
46
+ "validator",
47
+ ];
48
+ const SELECTS = {
49
+ json: ["keys", "values", "items"],
50
+ yaml: ["keys", "values", "items"],
51
+ openapi: [
52
+ "operations",
53
+ "unsecured_operations",
54
+ "security_schemes",
55
+ "responses",
56
+ ],
57
+ schema: [
58
+ "tables",
59
+ "columns",
60
+ "required_columns",
61
+ "nullable_columns",
62
+ "foreign_keys",
63
+ ],
64
+ routes: ["routes", "middleware"],
65
+ validator: ["fields", "required_fields", "optional_fields"],
66
+ };
67
+ export const REFERENCE_EXTRACTS = [
68
+ "env",
69
+ "imports",
70
+ "calls",
71
+ "string_args",
72
+ "thrown",
73
+ "property",
74
+ ];
75
+ const KEYS = {
76
+ json: ["reader", "file", "path", "select"],
77
+ yaml: ["reader", "file", "path", "select"],
78
+ glob: ["reader", "pattern", "capture", "ignore"],
79
+ openapi: ["reader", "file", "select"],
80
+ references: ["reader", "files", "extract", "callee", "key", "ignore"],
81
+ schema: ["reader", "format", "file", "files", "exports", "table", "select"],
82
+ routes: [
83
+ "reader",
84
+ "file",
85
+ "export",
86
+ "framework",
87
+ "path_prefix",
88
+ "names_from",
89
+ "select",
90
+ ],
91
+ validator: ["reader", "file", "export", "select"],
92
+ };
93
+ function record(value) {
94
+ return value !== null && typeof value === "object" && !Array.isArray(value)
95
+ ? value
96
+ : null;
97
+ }
98
+ function nonEmpty(value) {
99
+ return typeof value === "string" && value.trim() !== "";
100
+ }
101
+ function stringList(value) {
102
+ return Array.isArray(value) && value.length > 0 && value.every(nonEmpty);
103
+ }
104
+ function relativeSafe(value) {
105
+ return !path.isAbsolute(value) && !value.split(/[\\/]/).includes("..");
106
+ }
107
+ /**
108
+ * Why a source spec is not one Kibi can read, or [] when it is. `label`
109
+ * names the field in the messages (proof_contract.source, ...).
110
+ */
111
+ // implements REQ-kibi-source-proof-kinds
112
+ export function sourceSpecErrors(value, label) {
113
+ const row = record(value);
114
+ if (!row || !SOURCE_READERS.includes(row.reader))
115
+ return [
116
+ `${label} must be an object whose reader is one of: ${SOURCE_READERS.join(", ")}`,
117
+ ];
118
+ const reader = row.reader;
119
+ const errors = [];
120
+ const unknown = Object.keys(row).filter((key) => !KEYS[reader].includes(key));
121
+ if (unknown.length > 0)
122
+ errors.push(`${label} (reader ${reader}) does not take ${unknown.join(", ")}; it takes ${KEYS[reader].join(", ")}`);
123
+ const file = (key, required) => {
124
+ const raw = row[key];
125
+ if (raw === undefined) {
126
+ if (required)
127
+ errors.push(`${label}.${key} is required for reader ${reader}`);
128
+ return;
129
+ }
130
+ if (!nonEmpty(raw) || !relativeSafe(raw))
131
+ errors.push(`${label}.${key} must be a repository-relative path inside the workspace`);
132
+ };
133
+ const globs = (key, required) => {
134
+ const raw = row[key];
135
+ if (raw === undefined) {
136
+ if (required)
137
+ errors.push(`${label}.${key} is required for reader ${reader}`);
138
+ return;
139
+ }
140
+ const list = typeof raw === "string" ? [raw] : raw;
141
+ if (!stringList(list) || !list.every(relativeSafe))
142
+ errors.push(`${label}.${key} must be a repository-relative glob or a list of them`);
143
+ };
144
+ const select = SELECTS[reader];
145
+ if (row.select !== undefined &&
146
+ (select === undefined || !select.includes(String(row.select))))
147
+ errors.push(select === undefined
148
+ ? `${label}.select does not apply to reader ${reader}`
149
+ : `${label}.select must be one of: ${select.join(", ")}`);
150
+ switch (reader) {
151
+ case "json":
152
+ case "yaml":
153
+ file("file", true);
154
+ if (row.path !== undefined && typeof row.path !== "string")
155
+ errors.push(`${label}.path must be a dotted path or a JSON pointer`);
156
+ break;
157
+ case "glob":
158
+ globs("pattern", true);
159
+ globs("ignore", false);
160
+ if (row.capture !== undefined && typeof row.capture !== "boolean")
161
+ errors.push(`${label}.capture must be true or false`);
162
+ break;
163
+ case "openapi":
164
+ file("file", true);
165
+ break;
166
+ case "references":
167
+ globs("files", true);
168
+ globs("ignore", false);
169
+ if (!REFERENCE_EXTRACTS.includes(row.extract))
170
+ errors.push(`${label}.extract must be one of: ${REFERENCE_EXTRACTS.join(", ")}`);
171
+ if (row.extract === "string_args" && !nonEmpty(row.callee))
172
+ errors.push(`${label}.callee is required for extract string_args: name the function whose first argument is read`);
173
+ if (row.callee !== undefined && !nonEmpty(row.callee))
174
+ errors.push(`${label}.callee must be a function name`);
175
+ if (row.extract === "property" && !nonEmpty(row.key))
176
+ errors.push(`${label}.key is required for extract property: name the property whose string values are read`);
177
+ if (row.key !== undefined && row.extract !== "property")
178
+ errors.push(`${label}.key applies only to extract property`);
179
+ break;
180
+ case "schema":
181
+ if (!["drizzle", "prisma", "sql"].includes(String(row.format)))
182
+ errors.push(`${label}.format must be drizzle, prisma or sql`);
183
+ if (row.format === "sql") {
184
+ globs("files", true);
185
+ if (row.file !== undefined)
186
+ errors.push(`${label}.file does not apply to format sql; use files`);
187
+ }
188
+ else {
189
+ file("file", true);
190
+ if (row.files !== undefined)
191
+ errors.push(`${label}.files applies only to format sql`);
192
+ }
193
+ if (row.exports !== undefined &&
194
+ (!stringList(row.exports) || row.format !== "drizzle"))
195
+ errors.push(`${label}.exports must be a list of export names, for format drizzle only`);
196
+ if (row.table !== undefined && !nonEmpty(row.table))
197
+ errors.push(`${label}.table must be a table name or a glob of table names`);
198
+ break;
199
+ case "routes":
200
+ file("file", true);
201
+ if (!nonEmpty(row.export))
202
+ errors.push(`${label}.export is required for reader routes: name the app (or a function returning it)`);
203
+ if (row.framework !== undefined &&
204
+ !["hono", "express"].includes(String(row.framework)))
205
+ errors.push(`${label}.framework must be hono or express`);
206
+ if (row.path_prefix !== undefined &&
207
+ (!nonEmpty(row.path_prefix) || !String(row.path_prefix).startsWith("/")))
208
+ errors.push(`${label}.path_prefix must be a path starting with /`);
209
+ if (row.names_from !== undefined &&
210
+ (!stringList(row.names_from) || !row.names_from.every(relativeSafe)))
211
+ errors.push(`${label}.names_from must list repository-relative module files whose exports name middleware`);
212
+ break;
213
+ case "validator":
214
+ file("file", true);
215
+ if (!nonEmpty(row.export))
216
+ errors.push(`${label}.export is required for reader validator: name the object schema`);
217
+ break;
218
+ }
219
+ return errors;
220
+ }
221
+ /** The spec with only the keys its reader takes, for contract hashing. */
222
+ // implements REQ-kibi-source-proof-kinds
223
+ export function normalizeSourceSpec(value) {
224
+ const row = record(value);
225
+ if (!row || !SOURCE_READERS.includes(row.reader))
226
+ return undefined;
227
+ const reader = row.reader;
228
+ const out = {};
229
+ for (const key of KEYS[reader])
230
+ if (row[key] !== undefined)
231
+ out[key] = row[key];
232
+ return out;
233
+ }
234
+ function globList(value) {
235
+ if (typeof value === "string")
236
+ return [value];
237
+ return Array.isArray(value) ? value.filter(nonEmpty) : [];
238
+ }
239
+ const DEFAULT_IGNORE = ["**/node_modules/**", "**/.git/**", "**/.kb/**"];
240
+ /** Workspace-relative files a glob list matches, sorted. */
241
+ // implements REQ-kibi-source-proof-kinds
242
+ export function matchWorkspaceFiles(workspaceRoot, patterns, ignore = []) {
243
+ return fg
244
+ .sync([...patterns], {
245
+ cwd: workspaceRoot,
246
+ ignore: [...DEFAULT_IGNORE, ...ignore],
247
+ dot: true,
248
+ onlyFiles: true,
249
+ followSymbolicLinks: false,
250
+ })
251
+ .map((file) => file.split(path.sep).join("/"))
252
+ .sort();
253
+ }
254
+ function readText(workspaceRoot, relative) {
255
+ const absolute = path.resolve(workspaceRoot, relative);
256
+ const root = path.resolve(workspaceRoot);
257
+ if (absolute !== root && !absolute.startsWith(root + path.sep))
258
+ return null;
259
+ if (!existsSync(absolute) || !statSync(absolute).isFile())
260
+ return null;
261
+ return readFileSync(absolute, "utf8");
262
+ }
263
+ function sha256(text) {
264
+ return createHash("sha256").update(text).digest("hex");
265
+ }
266
+ const MISSING_FILE_HASH = "missing";
267
+ /**
268
+ * What a read of `spec` depends on, so a receipt goes stale when any of it
269
+ * changes: each file the reader parses (its contents), the set of files a
270
+ * glob matches (their paths), and for the readers that evaluate a module
271
+ * (drizzle schemas, route tables, validators) every workspace module that
272
+ * module imports through relative or tsconfig path imports.
273
+ */
274
+ // implements REQ-kibi-source-proof-kinds
275
+ export function sourceDependencies(workspaceRoot, spec) {
276
+ const files = new Set();
277
+ const out = [];
278
+ const addFile = (relative) => files.add(relative.split(path.sep).join("/"));
279
+ switch (spec.reader) {
280
+ case "json":
281
+ case "yaml":
282
+ case "openapi":
283
+ case "validator":
284
+ addFile(String(spec.file));
285
+ break;
286
+ case "glob": {
287
+ const matched = matchWorkspaceFiles(workspaceRoot, globList(spec.pattern), globList(spec.ignore));
288
+ out.push({
289
+ id: `source-glob:${globList(spec.pattern).join(",")}`,
290
+ hash: sha256(matched.join("\n")),
291
+ });
292
+ break;
293
+ }
294
+ case "references":
295
+ for (const file of matchWorkspaceFiles(workspaceRoot, globList(spec.files), globList(spec.ignore)))
296
+ addFile(file);
297
+ out.push({
298
+ id: `source-glob:${globList(spec.files).join(",")}`,
299
+ hash: sha256(matchWorkspaceFiles(workspaceRoot, globList(spec.files), globList(spec.ignore)).join("\n")),
300
+ });
301
+ break;
302
+ case "schema":
303
+ if (spec.format === "sql") {
304
+ const matched = matchWorkspaceFiles(workspaceRoot, globList(spec.files));
305
+ for (const file of matched)
306
+ addFile(file);
307
+ out.push({
308
+ id: `source-glob:${globList(spec.files).join(",")}`,
309
+ hash: sha256(matched.join("\n")),
310
+ });
311
+ }
312
+ else
313
+ addFile(String(spec.file));
314
+ break;
315
+ case "routes":
316
+ addFile(String(spec.file));
317
+ for (const file of globList(spec.names_from))
318
+ addFile(file);
319
+ break;
320
+ }
321
+ if (spec.reader === "routes" ||
322
+ spec.reader === "validator" ||
323
+ (spec.reader === "schema" && spec.format === "drizzle"))
324
+ for (const file of [...files])
325
+ for (const imported of importClosure(workspaceRoot, file))
326
+ addFile(imported);
327
+ for (const file of [...files].sort()) {
328
+ const text = readText(workspaceRoot, file);
329
+ out.push({
330
+ id: `source-file:${file}`,
331
+ hash: text === null ? MISSING_FILE_HASH : sha256(text),
332
+ });
333
+ }
334
+ return out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
335
+ }
336
+ const IMPORT_SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s+|\brequire\s*\(\s*|\bexport\s+\*\s+from\s*)["']([^"']+)["']/g;
337
+ const MODULE_SUFFIXES = [
338
+ "",
339
+ ".ts",
340
+ ".tsx",
341
+ ".mts",
342
+ ".cts",
343
+ ".js",
344
+ ".jsx",
345
+ ".mjs",
346
+ ".cjs",
347
+ "/index.ts",
348
+ "/index.tsx",
349
+ "/index.js",
350
+ "/index.mjs",
351
+ ];
352
+ function readJsonc(file) {
353
+ try {
354
+ const text = readFileSync(file, "utf8")
355
+ .replace(/\/\*[\s\S]*?\*\//g, "")
356
+ .replace(/(^|[^:"])\/\/.*$/gm, "$1")
357
+ .replace(/,(\s*[}\]])/g, "$1");
358
+ return record(JSON.parse(text));
359
+ }
360
+ catch {
361
+ return null;
362
+ }
363
+ }
364
+ // The paths a tsconfig declares, following `extends` (a relative file, or a
365
+ // list of them) the way tsc does: the nearest declaration of paths wins, and
366
+ // its targets resolve against baseUrl, else against the file declaring them.
367
+ function tsconfigPaths(file, depth = 0) {
368
+ const config = readJsonc(file);
369
+ if (config === null || depth > 10)
370
+ return null;
371
+ const dir = path.dirname(file);
372
+ const options = record(config.compilerOptions);
373
+ const paths = record(options?.paths);
374
+ if (paths)
375
+ return {
376
+ base: path.resolve(dir, typeof options?.baseUrl === "string" ? options.baseUrl : "."),
377
+ paths: Object.entries(paths).map(([key, targets]) => [
378
+ key,
379
+ Array.isArray(targets) ? targets.map(String) : [],
380
+ ]),
381
+ };
382
+ const parents = (Array.isArray(config.extends) ? config.extends : [config.extends]).filter((parent) => typeof parent === "string");
383
+ for (const parent of parents.reverse()) {
384
+ if (!parent.startsWith("."))
385
+ continue;
386
+ const target = path.resolve(dir, parent);
387
+ const candidate = existsSync(target) ? target : `${target}.json`;
388
+ if (!existsSync(candidate))
389
+ continue;
390
+ const inherited = tsconfigPaths(candidate, depth + 1);
391
+ if (inherited === null)
392
+ continue;
393
+ // A baseUrl in the child re-roots inherited paths.
394
+ return typeof options?.baseUrl === "string"
395
+ ? { ...inherited, base: path.resolve(dir, options.baseUrl) }
396
+ : inherited;
397
+ }
398
+ return null;
399
+ }
400
+ function tsconfigAliases(workspaceRoot, fromFile) {
401
+ const root = path.resolve(workspaceRoot);
402
+ let dir = path.dirname(path.resolve(root, fromFile));
403
+ for (;;) {
404
+ const config = path.join(dir, "tsconfig.json");
405
+ if (existsSync(config))
406
+ return tsconfigPaths(config);
407
+ if (dir === root || path.dirname(dir) === dir)
408
+ return null;
409
+ dir = path.dirname(dir);
410
+ }
411
+ }
412
+ // A bare specifier that names a workspace package: node_modules/<name> is a
413
+ // link into the repository (pnpm, yarn and npm workspaces), so its source is
414
+ // part of the closure. A package installed from a registry is not.
415
+ function workspacePackageBases(root, fromFile, specifier) {
416
+ const parts = specifier.split("/");
417
+ const nameLength = specifier.startsWith("@") ? 2 : 1;
418
+ if (parts.length < nameLength || parts.some((part) => part === ""))
419
+ return [];
420
+ const name = parts.slice(0, nameLength).join("/");
421
+ const subpath = parts.slice(nameLength).join("/");
422
+ let dir = path.dirname(path.resolve(root, fromFile));
423
+ for (;;) {
424
+ const link = path.join(dir, "node_modules", name);
425
+ if (existsSync(link)) {
426
+ let real;
427
+ try {
428
+ real = realpathSync(link);
429
+ }
430
+ catch {
431
+ return [];
432
+ }
433
+ const relative = path.relative(root, real);
434
+ if (relative.startsWith("..") ||
435
+ path.isAbsolute(relative) ||
436
+ relative.split(path.sep).includes("node_modules"))
437
+ return [];
438
+ if (subpath !== "")
439
+ return [path.join(real, subpath), path.join(real, "src", subpath)];
440
+ const manifest = readJsonc(path.join(real, "package.json"));
441
+ const entries = [];
442
+ const collect = (value) => {
443
+ if (typeof value === "string")
444
+ entries.push(value);
445
+ else if (record(value))
446
+ for (const key of [
447
+ "source",
448
+ "import",
449
+ "module",
450
+ "default",
451
+ "require",
452
+ "node",
453
+ ])
454
+ collect(value[key]);
455
+ };
456
+ const exported = record(manifest?.exports)?.["."] ?? manifest?.exports;
457
+ collect(exported);
458
+ for (const key of ["source", "module", "main"])
459
+ collect(manifest?.[key]);
460
+ return [
461
+ ...entries
462
+ .filter((entry) => !entry.endsWith(".d.ts"))
463
+ .map((entry) => path.resolve(real, entry)),
464
+ path.join(real, "src", "index"),
465
+ path.join(real, "index"),
466
+ ];
467
+ }
468
+ if (path.resolve(dir) === root || path.dirname(dir) === dir)
469
+ return [];
470
+ dir = path.dirname(dir);
471
+ }
472
+ }
473
+ function resolveModule(workspaceRoot, fromFile, specifier, aliases) {
474
+ const root = path.resolve(workspaceRoot);
475
+ const bases = [];
476
+ if (specifier.startsWith("."))
477
+ bases.push(path.resolve(root, path.dirname(fromFile), specifier));
478
+ else if (aliases !== null)
479
+ for (const [key, targets] of aliases.paths) {
480
+ const star = key.indexOf("*");
481
+ const prefix = star === -1 ? key : key.slice(0, star);
482
+ const suffix = star === -1 ? "" : key.slice(star + 1);
483
+ if (star === -1
484
+ ? specifier !== key
485
+ : !specifier.startsWith(prefix) || !specifier.endsWith(suffix))
486
+ continue;
487
+ const middle = star === -1
488
+ ? ""
489
+ : specifier.slice(prefix.length, specifier.length - suffix.length);
490
+ for (const target of targets)
491
+ bases.push(path.resolve(aliases.base, target.replace("*", middle)));
492
+ }
493
+ if (bases.length === 0 &&
494
+ !specifier.startsWith(".") &&
495
+ !path.isAbsolute(specifier))
496
+ bases.push(...workspacePackageBases(root, fromFile, specifier));
497
+ for (const base of bases) {
498
+ const stripped = base.replace(/\.[cm]?js$/, "");
499
+ for (const candidate of [
500
+ base,
501
+ ...MODULE_SUFFIXES.map((suffix) => stripped + suffix),
502
+ ]) {
503
+ if (!existsSync(candidate) || !statSync(candidate).isFile())
504
+ continue;
505
+ let real = candidate;
506
+ try {
507
+ real = realpathSync(candidate);
508
+ }
509
+ catch {
510
+ // A dangling link resolves to nothing.
511
+ }
512
+ const relative = path.relative(root, real);
513
+ if (relative.startsWith("..") || path.isAbsolute(relative))
514
+ return null;
515
+ if (relative.split(path.sep).includes("node_modules"))
516
+ return null;
517
+ return relative.split(path.sep).join("/");
518
+ }
519
+ }
520
+ return null;
521
+ }
522
+ /**
523
+ * The workspace modules `file` imports, directly or not, through relative
524
+ * imports, the nearest tsconfig's `paths` aliases (following `extends`) and
525
+ * workspace packages linked into node_modules (packages installed from a
526
+ * registry are left out: a lockfile change stales nothing here).
527
+ */
528
+ // implements REQ-kibi-source-proof-kinds
529
+ export function importClosure(workspaceRoot, file) {
530
+ const seen = new Set();
531
+ const queue = [file.split(path.sep).join("/")];
532
+ while (queue.length > 0 && seen.size < 5000) {
533
+ const current = queue.pop();
534
+ if (seen.has(current))
535
+ continue;
536
+ seen.add(current);
537
+ const text = readText(workspaceRoot, current);
538
+ if (text === null)
539
+ continue;
540
+ const aliases = tsconfigAliases(workspaceRoot, current);
541
+ for (const match of text.matchAll(IMPORT_SPECIFIER)) {
542
+ const resolved = resolveModule(workspaceRoot, current, match[1] ?? "", aliases);
543
+ if (resolved !== null && !seen.has(resolved))
544
+ queue.push(resolved);
545
+ }
546
+ }
547
+ seen.delete(file.split(path.sep).join("/"));
548
+ return [...seen].sort();
549
+ }
550
+ function sortedUnique(values) {
551
+ return [...new Set(values)].sort();
552
+ }
553
+ function scalarText(value) {
554
+ if (typeof value === "string")
555
+ return value;
556
+ if (typeof value === "number" ||
557
+ typeof value === "boolean" ||
558
+ typeof value === "bigint")
559
+ return String(value);
560
+ return null;
561
+ }
562
+ function navigate(document, rawPath) {
563
+ const trimmed = rawPath.trim();
564
+ if (trimmed === "" || trimmed === "/")
565
+ return { ok: true, value: document };
566
+ const parts = trimmed.startsWith("/")
567
+ ? trimmed
568
+ .slice(1)
569
+ .split("/")
570
+ .map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"))
571
+ : trimmed.split(".");
572
+ let value = document;
573
+ for (const part of parts) {
574
+ if (Array.isArray(value) && /^\d+$/.test(part))
575
+ value = value[Number(part)];
576
+ else if (record(value) !== null &&
577
+ part in value)
578
+ value = value[part];
579
+ else
580
+ return {
581
+ ok: false,
582
+ reason: `the document has nothing at ${rawPath} (no ${part})`,
583
+ };
584
+ }
585
+ return { ok: true, value };
586
+ }
587
+ function parseDocument(workspaceRoot, file, yaml) {
588
+ const text = readText(workspaceRoot, file);
589
+ if (text === null)
590
+ return { ok: false, reason: `${file} does not exist in the workspace` };
591
+ try {
592
+ return {
593
+ ok: true,
594
+ value: yaml || /\.ya?ml$/i.test(file) ? parseYaml(text) : JSON.parse(text),
595
+ };
596
+ }
597
+ catch (error) {
598
+ return {
599
+ ok: false,
600
+ reason: `Kibi could not parse ${file}: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}`,
601
+ };
602
+ }
603
+ }
604
+ function readDocumentMembers(workspaceRoot, spec) {
605
+ const parsed = parseDocument(workspaceRoot, String(spec.file), spec.reader === "yaml");
606
+ if (!parsed.ok)
607
+ return parsed;
608
+ const at = navigate(parsed.value, typeof spec.path === "string" ? spec.path : "");
609
+ if (!at.ok)
610
+ return at;
611
+ const value = at.value;
612
+ const select = typeof spec.select === "string" ? spec.select : undefined;
613
+ let raw;
614
+ if (Array.isArray(value)) {
615
+ if (select === "keys")
616
+ return {
617
+ ok: false,
618
+ reason: `${spec.path ?? "the document"} is a list, which has no keys (use select items)`,
619
+ };
620
+ raw = value;
621
+ }
622
+ else if (record(value) !== null) {
623
+ const object = value;
624
+ if (select === "items")
625
+ return {
626
+ ok: false,
627
+ reason: `${spec.path ?? "the document"} is an object, not a list (use select keys or values)`,
628
+ };
629
+ raw = select === "values" ? Object.values(object) : Object.keys(object);
630
+ }
631
+ else if (scalarText(value) !== null)
632
+ raw = [value];
633
+ else
634
+ return { ok: false, reason: `${spec.path ?? "the document"} is empty` };
635
+ const members = [];
636
+ for (const item of raw) {
637
+ const text = scalarText(item);
638
+ if (text === null)
639
+ return {
640
+ ok: false,
641
+ reason: `${spec.path ?? "the document"} holds a value that is not a string, number or boolean, so it cannot be a member (select keys, or point path at the list)`,
642
+ };
643
+ members.push(text);
644
+ }
645
+ return { ok: true, model: { members: sortedUnique(members) } };
646
+ }
647
+ function globCaptureRegex(pattern) {
648
+ let out = "";
649
+ for (let index = 0; index < pattern.length; index++) {
650
+ const char = pattern[index];
651
+ if (char === "*" && pattern[index + 1] === "*") {
652
+ index++;
653
+ // "**/" is any number of directories, captured without the slash.
654
+ if (pattern[index + 1] === "/") {
655
+ out += "(?:(.*)/)?";
656
+ index++;
657
+ }
658
+ else
659
+ out += "(.*)";
660
+ }
661
+ else if (char === "*")
662
+ out += "([^/]*)";
663
+ else if (char === "?")
664
+ out += "([^/])";
665
+ else
666
+ out += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
667
+ }
668
+ return new RegExp(`^${out}$`);
669
+ }
670
+ function readGlob(workspaceRoot, spec) {
671
+ const patterns = globList(spec.pattern);
672
+ const matched = matchWorkspaceFiles(workspaceRoot, patterns, globList(spec.ignore));
673
+ // No match proves nothing: a mistyped pattern would pass every subset_of
674
+ // and excludes fact.
675
+ if (matched.length === 0)
676
+ return { ok: false, reason: `${patterns.join(", ")} matches no file` };
677
+ if (spec.capture !== true)
678
+ return { ok: true, model: { members: matched } };
679
+ const members = [];
680
+ for (const file of matched) {
681
+ const match = patterns
682
+ .map((pattern) => file.match(globCaptureRegex(pattern)))
683
+ .find((result) => result !== null);
684
+ if (!match)
685
+ return {
686
+ ok: false,
687
+ reason: `capture reads only *, ** and ? in the pattern; ${file} matched a pattern Kibi cannot capture from (braces or classes)`,
688
+ };
689
+ members.push(match
690
+ .slice(1)
691
+ .filter((part) => part !== undefined && part !== "")
692
+ .join("/"));
693
+ }
694
+ return { ok: true, model: { members: sortedUnique(members) } };
695
+ }
696
+ const HTTP_METHODS = [
697
+ "get",
698
+ "put",
699
+ "post",
700
+ "delete",
701
+ "options",
702
+ "head",
703
+ "patch",
704
+ "trace",
705
+ ];
706
+ function readOpenApi(workspaceRoot, spec) {
707
+ const parsed = parseDocument(workspaceRoot, String(spec.file), false);
708
+ if (!parsed.ok)
709
+ return parsed;
710
+ const document = record(parsed.value);
711
+ if (!document ||
712
+ (document.openapi === undefined && document.swagger === undefined))
713
+ return {
714
+ ok: false,
715
+ reason: `${spec.file} is not an OpenAPI or Swagger document`,
716
+ };
717
+ const select = typeof spec.select === "string" ? spec.select : "operations";
718
+ if (select === "security_schemes") {
719
+ const schemes = record(record(document.components)?.securitySchemes) ??
720
+ record(document.securityDefinitions) ??
721
+ {};
722
+ return { ok: true, model: { members: sortedUnique(Object.keys(schemes)) } };
723
+ }
724
+ const rootSecurity = document.security;
725
+ const members = [];
726
+ for (const [route, rawItem] of Object.entries(record(document.paths) ?? {})) {
727
+ const item = record(rawItem) ?? {};
728
+ for (const method of HTTP_METHODS) {
729
+ const operation = record(item[method]);
730
+ if (!operation)
731
+ continue;
732
+ const name = `${method.toUpperCase()} ${route}`;
733
+ if (select === "operations")
734
+ members.push(name);
735
+ else if (select === "responses")
736
+ for (const code of Object.keys(record(operation.responses) ?? {}))
737
+ members.push(`${name} ${code}`);
738
+ else {
739
+ const security = operation.security ?? rootSecurity;
740
+ const unsecured = !Array.isArray(security) ||
741
+ security.length === 0 ||
742
+ security.some((row) => record(row) !== null && Object.keys(row).length === 0);
743
+ if (unsecured)
744
+ members.push(name);
745
+ }
746
+ }
747
+ }
748
+ return { ok: true, model: { members: sortedUnique(members) } };
749
+ }
750
+ function identifierName(raw) {
751
+ const cleaned = raw.trim();
752
+ const parts = cleaned.match(/("[^"]+"|`[^`]+`|\[[^\]]+\]|[A-Za-z_][\w$]*)/g) ?? [cleaned];
753
+ return (parts[parts.length - 1] ?? cleaned).replace(/^["`[]|["`\]]$/g, "");
754
+ }
755
+ function splitTopLevel(body) {
756
+ const parts = [];
757
+ let depth = 0;
758
+ let current = "";
759
+ let quote = null;
760
+ for (const char of body) {
761
+ if (quote !== null) {
762
+ current += char;
763
+ if (char === quote)
764
+ quote = null;
765
+ continue;
766
+ }
767
+ if (char === "'" || char === '"' || char === "`")
768
+ quote = char;
769
+ if (char === "(")
770
+ depth++;
771
+ if (char === ")")
772
+ depth--;
773
+ if (char === "," && depth === 0) {
774
+ parts.push(current.trim());
775
+ current = "";
776
+ }
777
+ else
778
+ current += char;
779
+ }
780
+ if (current.trim() !== "")
781
+ parts.push(current.trim());
782
+ return parts;
783
+ }
784
+ function stripSqlComments(text) {
785
+ return text.replace(/--[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
786
+ }
787
+ // Statements split at top-level semicolons. Quoted text and dollar-quoted
788
+ // bodies ($$ ... $$, $fn$ ... $fn$) stay whole, so a function body's
789
+ // semicolons do not split it.
790
+ function sqlStatements(text) {
791
+ const statements = [];
792
+ let current = "";
793
+ let quote = null;
794
+ const source = stripSqlComments(text);
795
+ for (let index = 0; index < source.length; index++) {
796
+ const char = source[index];
797
+ if (quote !== null) {
798
+ if (quote.length > 1) {
799
+ if (source.startsWith(quote, index)) {
800
+ current += quote;
801
+ index += quote.length - 1;
802
+ quote = null;
803
+ }
804
+ else
805
+ current += char;
806
+ continue;
807
+ }
808
+ current += char;
809
+ if (char === quote)
810
+ quote = null;
811
+ continue;
812
+ }
813
+ if (char === "$") {
814
+ const tag = source.slice(index).match(/^\$[A-Za-z_]*\$/)?.[0];
815
+ if (tag !== undefined) {
816
+ quote = tag;
817
+ current += tag;
818
+ index += tag.length - 1;
819
+ continue;
820
+ }
821
+ }
822
+ if (char === "'" || char === '"' || char === "`")
823
+ quote = char;
824
+ if (char === ";") {
825
+ if (current.trim() !== "")
826
+ statements.push(current.trim());
827
+ current = "";
828
+ }
829
+ else
830
+ current += char;
831
+ }
832
+ if (current.trim() !== "")
833
+ statements.push(current.trim());
834
+ return statements;
835
+ }
836
+ // One name part, and a possibly schema-qualified name: users, public.users,
837
+ // "public"."users", [dbo].[users].
838
+ const SQL_NAME_PART = '(?:"[^"]+"|`[^`]+`|\\[[^\\]]+\\]|[\\w$]+)';
839
+ const SQL_NAME = String.raw `(${SQL_NAME_PART}(?:\s*\.\s*${SQL_NAME_PART})*)`;
840
+ const COLUMN_CONSTRAINT_START = /^(?:constraint|primary\s+key|foreign\s+key|unique|check|key|index|exclude)\b/i;
841
+ function foreignKeyText(table, columns, target, targetColumns, rest) {
842
+ const onDelete = rest.match(/\bon\s+delete\s+(cascade|restrict|set\s+null|set\s+default|no\s+action)/i);
843
+ const from = splitTopLevel(columns).map(identifierName).join(",");
844
+ const to = splitTopLevel(targetColumns).map(identifierName).join(",");
845
+ return `${table}.${from}->${identifierName(target)}.${to}${onDelete?.[1]
846
+ ? ` on delete ${onDelete[1].toLowerCase().replace(/\s+/g, " ")}`
847
+ : ""}`;
848
+ }
849
+ // A foreign key with the name a later DROP CONSTRAINT uses: the declared
850
+ // name, or PostgreSQL's default <table>_<columns>_fkey.
851
+ function addForeignKey(table, constraint, columns, target, targetColumns, rest) {
852
+ const text = foreignKeyText(table.name, columns, target, targetColumns, rest);
853
+ table.foreignKeys.push(text);
854
+ const name = constraint !== undefined
855
+ ? identifierName(constraint)
856
+ : `${table.name}_${splitTopLevel(columns).map(identifierName).join("_")}_fkey`;
857
+ table.constraints.set(name.toLowerCase(), text);
858
+ }
859
+ function sqlColumn(table, definition) {
860
+ const head = definition.match(new RegExp(`^${SQL_NAME_PART}`))?.[0] ?? "";
861
+ const name = identifierName(head);
862
+ const rest = definition.slice(head.length);
863
+ const notNull = /\bnot\s+null\b/i.test(rest) || /\bprimary\s+key\b/i.test(rest);
864
+ table.columns.set(name, { name, nullable: !notNull, optional: false });
865
+ const reference = rest.match(new RegExp(String.raw `\breferences\s+${SQL_NAME}\s*\(([^)]*)\)(.*)$`, "is"));
866
+ if (reference) {
867
+ const constraint = rest.match(new RegExp(String.raw `\bconstraint\s+(${SQL_NAME_PART})\s+references\b`, "i"))?.[1];
868
+ addForeignKey(table, constraint, name, reference[1] ?? "", reference[2] ?? "", reference[3] ?? "");
869
+ }
870
+ }
871
+ function setPrimaryKey(table, columns) {
872
+ for (const column of splitTopLevel(columns).map(identifierName)) {
873
+ const existing = table.columns.get(column);
874
+ if (existing)
875
+ existing.nullable = false;
876
+ }
877
+ }
878
+ // ALTER TABLE actions that change neither the columns, their nullability nor
879
+ // the foreign keys: row level security, triggers, ownership, storage,
880
+ // defaults, column types, statistics and comments.
881
+ const SCHEMA_NEUTRAL_ALTER = /^(?:(?:enable|disable|force|no\s+force)\s+row\s+level\s+security|(?:enable|disable)\s+(?:always\s+|replica\s+)?(?:trigger|rule)\b|replica\s+identity\b|owner\s+to\b|set\s+(?:schema|tablespace|logged|unlogged|without\s+(?:oids|cluster)|access\s+method)\b|set\s*\(|reset\s*\(|cluster\s+on\b|inherit\b|no\s+inherit\b|validate\s+constraint\b|rename\s+constraint\b|alter\s+constraint\b|alter\s+(?:column\s+)?\S+\s+(?:set\s+default|drop\s+default|(?:set\s+data\s+)?type|set\s+statistics|set\s+storage|set\s+compression|add\s+generated|set\s+generated|drop\s+identity|drop\s+expression|reset\s*\(|set\s*\(|options\s*\())/i;
882
+ function applyAlterAction(table, action) {
883
+ const add = action.match(/^add\s+(?:column\s+)?(?:if\s+not\s+exists\s+)?(.*)$/is);
884
+ if (add && !COLUMN_CONSTRAINT_START.test(add[1] ?? "")) {
885
+ sqlColumn(table, add[1] ?? "");
886
+ return null;
887
+ }
888
+ const dropColumn = action.match(new RegExp(String.raw `^drop\s+(?:column\s+)?(?:if\s+exists\s+)?(${SQL_NAME_PART})(?:\s+(?:cascade|restrict))?$`, "i"));
889
+ if (dropColumn && !/^drop\s+constraint\b/i.test(action)) {
890
+ const column = identifierName(dropColumn[1] ?? "");
891
+ table.columns.delete(column);
892
+ table.foreignKeys = table.foreignKeys.filter((fk) => !fk.startsWith(`${table.name}.${column}->`));
893
+ for (const [name, fk] of table.constraints)
894
+ if (fk.startsWith(`${table.name}.${column}->`))
895
+ table.constraints.delete(name);
896
+ return null;
897
+ }
898
+ const renameColumn = action.match(new RegExp(String.raw `^rename\s+(?:column\s+)?(${SQL_NAME_PART})\s+to\s+(${SQL_NAME_PART})$`, "i"));
899
+ if (renameColumn && !/^rename\s+constraint\b/i.test(action)) {
900
+ const from = identifierName(renameColumn[1] ?? "");
901
+ const to = identifierName(renameColumn[2] ?? "");
902
+ const column = table.columns.get(from);
903
+ if (column) {
904
+ table.columns.delete(from);
905
+ column.name = to;
906
+ table.columns.set(to, column);
907
+ }
908
+ const renamed = (fk) => fk.replace(`${table.name}.${from}->`, `${table.name}.${to}->`);
909
+ table.foreignKeys = table.foreignKeys.map(renamed);
910
+ for (const [name, fk] of table.constraints)
911
+ table.constraints.set(name, renamed(fk));
912
+ return null;
913
+ }
914
+ const nullability = action.match(new RegExp(String.raw `^alter\s+(?:column\s+)?(${SQL_NAME_PART})\s+(set|drop)\s+not\s+null$`, "i"));
915
+ if (nullability) {
916
+ const column = table.columns.get(identifierName(nullability[1] ?? ""));
917
+ if (column)
918
+ column.nullable = (nullability[2] ?? "").toLowerCase() === "drop";
919
+ return null;
920
+ }
921
+ const addFk = action.match(new RegExp(String.raw `^add\s+(?:constraint\s+(${SQL_NAME_PART})\s+)?foreign\s+key\s*\(([^)]*)\)\s*references\s+${SQL_NAME}\s*\(([^)]*)\)(.*)$`, "is"));
922
+ if (addFk) {
923
+ addForeignKey(table, addFk[1], addFk[2] ?? "", addFk[3] ?? "", addFk[4] ?? "", addFk[5] ?? "");
924
+ return null;
925
+ }
926
+ const addPrimary = action.match(new RegExp(String.raw `^add\s+(?:constraint\s+${SQL_NAME_PART}\s+)?primary\s+key\s*\(([^)]*)\)`, "i"));
927
+ if (addPrimary) {
928
+ setPrimaryKey(table, addPrimary[1] ?? "");
929
+ return null;
930
+ }
931
+ if (new RegExp(String.raw `^add\s+(?:constraint\s+${SQL_NAME_PART}\s+)?(?:unique|check|exclude)\b`, "i").test(action))
932
+ return null;
933
+ const dropConstraint = action.match(new RegExp(String.raw `^drop\s+constraint\s+(if\s+exists\s+)?(${SQL_NAME_PART})(?:\s+(?:cascade|restrict))?$`, "i"));
934
+ if (dropConstraint) {
935
+ const name = identifierName(dropConstraint[2] ?? "").toLowerCase();
936
+ const fk = table.constraints.get(name);
937
+ if (fk !== undefined) {
938
+ table.constraints.delete(name);
939
+ const index = table.foreignKeys.indexOf(fk);
940
+ if (index >= 0)
941
+ table.foreignKeys.splice(index, 1);
942
+ return null;
943
+ }
944
+ // With IF EXISTS a constraint the replay never declared is not there.
945
+ // Without it the database had it, so a foreign key Kibi did not read
946
+ // would leave the schema unknown.
947
+ if (dropConstraint[1] === undefined && /(?:_fkey|_fk)$/i.test(name))
948
+ return `drops constraint ${name}, a foreign key Kibi did not read`;
949
+ return null;
950
+ }
951
+ if (SCHEMA_NEUTRAL_ALTER.test(action))
952
+ return null;
953
+ return "cannot read this ALTER TABLE form";
954
+ }
955
+ function applySqlStatement(tables, statement, where) {
956
+ const normalized = statement.replace(/\s+/g, " ").trim();
957
+ const create = normalized.match(new RegExp(String.raw `^create\s+(?:(?:global|local)\s+)?(?:(?:temp(?:orary)?|unlogged)\s+)?table\s+(?:if\s+not\s+exists\s+)?${SQL_NAME}\s*\((.*)\)[^)]*$`, "is"));
958
+ if (create) {
959
+ const name = identifierName(create[1] ?? "");
960
+ const table = {
961
+ name,
962
+ columns: new Map(),
963
+ foreignKeys: [],
964
+ constraints: new Map(),
965
+ };
966
+ for (const part of splitTopLevel(create[2] ?? "")) {
967
+ if (COLUMN_CONSTRAINT_START.test(part)) {
968
+ const fk = part.match(new RegExp(String.raw `^(?:constraint\s+(${SQL_NAME_PART})\s+)?foreign\s+key\s*\(([^)]*)\)\s*references\s+${SQL_NAME}\s*\(([^)]*)\)(.*)$`, "is"));
969
+ if (fk)
970
+ addForeignKey(table, fk[1], fk[2] ?? "", fk[3] ?? "", fk[4] ?? "", fk[5] ?? "");
971
+ const primary = part.match(/primary\s+key\s*\(([^)]*)\)/i);
972
+ if (primary)
973
+ setPrimaryKey(table, primary[1] ?? "");
974
+ continue;
975
+ }
976
+ sqlColumn(table, part);
977
+ }
978
+ tables.set(name, table);
979
+ return null;
980
+ }
981
+ if (/^create\s+(?:(?:global|local)\s+)?(?:(?:temp(?:orary)?|unlogged)\s+)?table\b/i.test(normalized))
982
+ return `${where}: Kibi cannot read this CREATE TABLE form (${normalized.slice(0, 80)})`;
983
+ const drop = normalized.match(/^drop\s+table\s+(?:if\s+exists\s+)?(.*?)(?:\s+(?:cascade|restrict))?$/i);
984
+ if (drop) {
985
+ for (const name of splitTopLevel(drop[1] ?? ""))
986
+ tables.delete(identifierName(name));
987
+ return null;
988
+ }
989
+ const alter = normalized.match(new RegExp(String.raw `^alter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?${SQL_NAME}\s+(.*)$`, "i"));
990
+ if (!alter)
991
+ return null;
992
+ const tableName = identifierName(alter[1] ?? "");
993
+ const action = alter[2] ?? "";
994
+ const rename = action.match(new RegExp(String.raw `^rename\s+to\s+${SQL_NAME}$`, "i"));
995
+ const table = tables.get(tableName);
996
+ // A statement-wide neutral form needs no table: RLS on a table another
997
+ // tool created changes nothing Kibi reads.
998
+ const actions = splitTopLevel(action);
999
+ if (!table) {
1000
+ if (actions.every((part) => SCHEMA_NEUTRAL_ALTER.test(part)))
1001
+ return null;
1002
+ return `${where}: ALTER TABLE ${tableName} before Kibi read its CREATE TABLE`;
1003
+ }
1004
+ if (rename) {
1005
+ tables.delete(tableName);
1006
+ const renamedTo = identifierName(rename[1] ?? "");
1007
+ const prefix = (fk) => fk.replace(new RegExp(`^${tableName}\\.`), `${renamedTo}.`);
1008
+ table.name = renamedTo;
1009
+ table.foreignKeys = table.foreignKeys.map(prefix);
1010
+ for (const [name, fk] of table.constraints)
1011
+ table.constraints.set(name, prefix(fk));
1012
+ tables.set(table.name, table);
1013
+ return null;
1014
+ }
1015
+ for (const part of actions) {
1016
+ const problem = applyAlterAction(table, part);
1017
+ if (problem !== null)
1018
+ return `${where}: ALTER TABLE ${tableName} ${problem} (${normalized.slice(0, 80)}), so the schema after it is unknown`;
1019
+ }
1020
+ return null;
1021
+ }
1022
+ function readSqlSchema(workspaceRoot, spec) {
1023
+ const files = matchWorkspaceFiles(workspaceRoot, globList(spec.files));
1024
+ if (files.length === 0)
1025
+ return `${globList(spec.files).join(", ")} matches no file`;
1026
+ const tables = new Map();
1027
+ let created = 0;
1028
+ for (const file of files) {
1029
+ const text = readText(workspaceRoot, file) ?? "";
1030
+ const statements = sqlStatements(text.replace(/-->\s*statement-breakpoint/g, ";"));
1031
+ for (const statement of statements) {
1032
+ if (/^\s*create\s+(?:\S+\s+){0,2}table\b/i.test(statement))
1033
+ created++;
1034
+ const problem = applySqlStatement(tables, statement, file);
1035
+ if (problem !== null)
1036
+ return problem;
1037
+ }
1038
+ }
1039
+ // A replay that read no table proves nothing about the schema: a
1040
+ // mistyped pattern or a folder of TypeScript migrations would otherwise
1041
+ // pass every subset_of and excludes fact.
1042
+ if (created === 0)
1043
+ return `${globList(spec.files).join(", ")} holds no CREATE TABLE statement Kibi can read (${files.length} file${files.length === 1 ? "" : "s"})`;
1044
+ return [...tables.values()];
1045
+ }
1046
+ const PRISMA_SCALARS = new Set([
1047
+ "String",
1048
+ "Boolean",
1049
+ "Int",
1050
+ "BigInt",
1051
+ "Float",
1052
+ "Decimal",
1053
+ "DateTime",
1054
+ "Json",
1055
+ "Bytes",
1056
+ ]);
1057
+ function readPrismaSchema(workspaceRoot, spec) {
1058
+ const text = readText(workspaceRoot, String(spec.file));
1059
+ if (text === null)
1060
+ return `${spec.file} does not exist in the workspace`;
1061
+ const source = text.replace(/\/\/[^\n]*/g, "");
1062
+ const enums = new Set([...source.matchAll(/\benum\s+(\w+)\s*\{/g)].map((m) => m[1]));
1063
+ const types = new Set([...source.matchAll(/\b(?:type)\s+(\w+)\s*\{/g)].map((m) => m[1]));
1064
+ const tables = [];
1065
+ for (const model of source.matchAll(/\bmodel\s+(\w+)\s*\{([^}]*)\}/g)) {
1066
+ const body = model[2] ?? "";
1067
+ const mapped = body.match(/@@map\(\s*"([^"]+)"\s*\)/)?.[1];
1068
+ const name = mapped ?? model[1];
1069
+ const table = {
1070
+ name,
1071
+ columns: new Map(),
1072
+ foreignKeys: [],
1073
+ constraints: new Map(),
1074
+ };
1075
+ const fieldNames = new Map();
1076
+ for (const line of body.split("\n")) {
1077
+ const field = line.trim().match(/^(\w+)\s+(\w+)(\[\])?(\?)?(.*)$/);
1078
+ if (!field)
1079
+ continue;
1080
+ const [, fieldName = "", type = "", list, optional, attributes = ""] = field;
1081
+ const columnName = attributes.match(/@map\(\s*"([^"]+)"\s*\)/)?.[1] ?? fieldName;
1082
+ fieldNames.set(fieldName, columnName);
1083
+ if (PRISMA_SCALARS.has(type) ||
1084
+ enums.has(type) ||
1085
+ types.has(type) ||
1086
+ list === "[]") {
1087
+ if (list === "[]" && !PRISMA_SCALARS.has(type) && !enums.has(type))
1088
+ continue;
1089
+ table.columns.set(columnName, {
1090
+ name: columnName,
1091
+ nullable: optional === "?",
1092
+ optional: false,
1093
+ });
1094
+ }
1095
+ }
1096
+ for (const line of body.split("\n")) {
1097
+ const relation = line.match(/^\s*\w+\s+(\w+)\??\s+.*@relation\(([^)]*)\)/);
1098
+ if (!relation)
1099
+ continue;
1100
+ const fields = relation[2]?.match(/fields:\s*\[([^\]]*)\]/)?.[1];
1101
+ const references = relation[2]?.match(/references:\s*\[([^\]]*)\]/)?.[1];
1102
+ if (!fields || !references)
1103
+ continue;
1104
+ const target = relation[1];
1105
+ const targetModel = source.match(new RegExp(`\\bmodel\\s+${target}\\s*\\{([^}]*)\\}`));
1106
+ const targetName = targetModel?.[1]?.match(/@@map\(\s*"([^"]+)"\s*\)/)?.[1] ?? target;
1107
+ const onDelete = relation[2]?.match(/onDelete:\s*(\w+)/)?.[1];
1108
+ const columns = fields
1109
+ .split(",")
1110
+ .map((f) => fieldNames.get(f.trim()) ?? f.trim())
1111
+ .join(",");
1112
+ const targetColumns = references
1113
+ .split(",")
1114
+ .map((f) => f.trim())
1115
+ .join(",");
1116
+ table.foreignKeys.push(`${name}.${columns}->${targetName}.${targetColumns}${onDelete
1117
+ ? ` on delete ${onDelete.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase()}`
1118
+ : ""}`);
1119
+ }
1120
+ tables.push(table);
1121
+ }
1122
+ if (tables.length === 0)
1123
+ return `${spec.file} declares no model`;
1124
+ return tables;
1125
+ }
1126
+ function globMatcher(pattern) {
1127
+ if (pattern === undefined || pattern === "*")
1128
+ return () => true;
1129
+ const regex = new RegExp(`^${pattern
1130
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
1131
+ .replace(/\*/g, ".*")
1132
+ .replace(/\?/g, ".")}$`);
1133
+ return (name) => regex.test(name);
1134
+ }
1135
+ /** The entities (tables or validator objects) a spec's table filter selects. */
1136
+ export function selectedEntities(spec, model) {
1137
+ const matches = globMatcher(typeof spec.table === "string" ? spec.table : undefined);
1138
+ return (model.entities ?? []).filter((entity) => matches(entity.name));
1139
+ }
1140
+ function entityMembers(spec, entities) {
1141
+ const select = typeof spec.select === "string"
1142
+ ? spec.select
1143
+ : spec.reader === "validator"
1144
+ ? "fields"
1145
+ : "tables";
1146
+ const prefix = (entity, column) => spec.reader === "validator" ? column.name : `${entity.name}.${column.name}`;
1147
+ const members = [];
1148
+ for (const entity of entities) {
1149
+ if (select === "tables")
1150
+ members.push(entity.name);
1151
+ else if (select === "foreign_keys")
1152
+ members.push(...entity.foreign_keys);
1153
+ else
1154
+ for (const column of entity.columns) {
1155
+ const required = !column.nullable && !column.optional;
1156
+ if (select === "columns" ||
1157
+ select === "fields" ||
1158
+ ((select === "required_columns" || select === "required_fields") &&
1159
+ required) ||
1160
+ (select === "nullable_columns" && column.nullable) ||
1161
+ (select === "optional_fields" && !required))
1162
+ members.push(prefix(entity, column));
1163
+ }
1164
+ }
1165
+ return sortedUnique(members);
1166
+ }
1167
+ function finishEntities(spec, tables) {
1168
+ const entities = tables
1169
+ .map((table) => "columns" in table && table.columns instanceof Map
1170
+ ? {
1171
+ name: table.name,
1172
+ columns: [...table.columns.values()]
1173
+ .map((column) => ({ ...column }))
1174
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)),
1175
+ foreign_keys: sortedUnique(table.foreignKeys),
1176
+ }
1177
+ : table)
1178
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
1179
+ const model = { members: [], entities };
1180
+ const selected = selectedEntities(spec, model);
1181
+ if (typeof spec.table === "string" && selected.length === 0)
1182
+ return {
1183
+ ok: false,
1184
+ reason: `no ${spec.reader === "validator" ? "object" : "table"} matches ${spec.table} (${entities
1185
+ .map((e) => e.name)
1186
+ .slice(0, 10)
1187
+ .join(", ") || "none read"})`,
1188
+ };
1189
+ return {
1190
+ ok: true,
1191
+ model: { members: entityMembers(spec, selected), entities: selected },
1192
+ };
1193
+ }
1194
+ /** The routes a spec's path_prefix selects. */
1195
+ export function selectedRoutes(spec, routes) {
1196
+ const prefix = typeof spec.path_prefix === "string"
1197
+ ? spec.path_prefix.replace(/\/+$/, "")
1198
+ : "";
1199
+ return routes.filter((route) => prefix === "" ||
1200
+ route.path === prefix ||
1201
+ route.path.startsWith(`${prefix}/`));
1202
+ }
1203
+ /**
1204
+ * Read `spec` from the workspace. Readers that evaluate code (drizzle
1205
+ * schemas, route tables, validators) and the references reader go through
1206
+ * `evaluate`, which the CLI supplies; a read without it refuses those.
1207
+ */
1208
+ // implements REQ-kibi-source-proof-kinds
1209
+ export async function readSource(workspaceRoot, spec, evaluate) {
1210
+ const errors = sourceSpecErrors(spec, "source");
1211
+ if (errors.length > 0)
1212
+ return { ok: false, reason: errors.join("; ") };
1213
+ switch (spec.reader) {
1214
+ case "json":
1215
+ case "yaml":
1216
+ return readDocumentMembers(workspaceRoot, spec);
1217
+ case "glob":
1218
+ return readGlob(workspaceRoot, spec);
1219
+ case "openapi":
1220
+ return readOpenApi(workspaceRoot, spec);
1221
+ case "schema": {
1222
+ if (spec.format === "sql") {
1223
+ const tables = readSqlSchema(workspaceRoot, spec);
1224
+ return typeof tables === "string"
1225
+ ? { ok: false, reason: tables }
1226
+ : finishEntities(spec, tables);
1227
+ }
1228
+ if (spec.format === "prisma") {
1229
+ const tables = readPrismaSchema(workspaceRoot, spec);
1230
+ return typeof tables === "string"
1231
+ ? { ok: false, reason: tables }
1232
+ : finishEntities(spec, tables);
1233
+ }
1234
+ break;
1235
+ }
1236
+ default:
1237
+ break;
1238
+ }
1239
+ if (evaluate === undefined)
1240
+ return {
1241
+ ok: false,
1242
+ reason: `reader ${spec.reader} needs Kibi's code evaluator`,
1243
+ };
1244
+ const evaluated = await evaluate(workspaceRoot, spec);
1245
+ if (!evaluated.ok)
1246
+ return evaluated;
1247
+ if (spec.reader === "references")
1248
+ return {
1249
+ ok: true,
1250
+ model: { members: sortedUnique(evaluated.members ?? []) },
1251
+ };
1252
+ if (spec.reader === "routes") {
1253
+ const routes = selectedRoutes(spec, evaluated.routes ?? []).sort((a, b) => `${a.path} ${a.method}` < `${b.path} ${b.method}` ? -1 : 1);
1254
+ if (typeof spec.path_prefix === "string" && routes.length === 0)
1255
+ return {
1256
+ ok: false,
1257
+ reason: `no route of ${spec.export} is under ${spec.path_prefix}`,
1258
+ };
1259
+ const members = spec.select === "middleware"
1260
+ ? routes.flatMap((route) => route.middleware.map((name) => `${route.method} ${route.path} ${name}`))
1261
+ : routes.map((route) => `${route.method} ${route.path}`);
1262
+ return { ok: true, model: { members: sortedUnique(members), routes } };
1263
+ }
1264
+ return finishEntities(spec, evaluated.entities ?? []);
1265
+ }
1266
+ /** A short description of a spec, for messages. */
1267
+ export function describeSource(spec) {
1268
+ switch (spec.reader) {
1269
+ case "json":
1270
+ case "yaml":
1271
+ case "openapi":
1272
+ return `${spec.reader} ${spec.file}${spec.path ? ` at ${spec.path}` : ""}`;
1273
+ case "glob":
1274
+ return `files matching ${globList(spec.pattern).join(", ")}`;
1275
+ case "references":
1276
+ return `${spec.extract} references in ${globList(spec.files).join(", ")}`;
1277
+ case "schema":
1278
+ return `${spec.format} schema ${spec.file ?? globList(spec.files).join(", ")}${spec.table ? ` (tables ${spec.table})` : ""}`;
1279
+ case "routes":
1280
+ return `routes of ${spec.export} in ${spec.file}${spec.path_prefix ? ` under ${spec.path_prefix}` : ""}`;
1281
+ case "validator":
1282
+ return `validator ${spec.export} in ${spec.file}`;
1283
+ }
1284
+ }
1285
+ /** Unresolved references, as one rejection reason naming the first few. */
1286
+ export function unresolvedReason(what, unresolved) {
1287
+ const shown = unresolved
1288
+ .slice(0, 5)
1289
+ .map((row) => `${row.file}:${row.line} ${row.text}`)
1290
+ .join("; ");
1291
+ return `Kibi cannot read ${unresolved.length} ${what} statically, so the set is not known exactly: ${shown}${unresolved.length > 5 ? "; ..." : ""}`;
1292
+ }