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,894 @@
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
+ import { spawnSync } from "node:child_process";
19
+ import { createHash } from "node:crypto";
20
+ import { existsSync, readFileSync } from "node:fs";
21
+ import { createRequire } from "node:module";
22
+ import path from "node:path";
23
+ import { loadEntities } from "../../public/operations/discovery-entities.js";
24
+ import { runOperationJsonQuery, toPrologAtom, } from "../../public/operations/prolog-json.js";
25
+ import { canonicalCertificateJson, certificateDigest, } from "../../public/proof-certificates.js";
26
+ import { CODE_CERTIFICATE_VERSION, isCertificateProofKind, } from "../../public/proof-protocol.js";
27
+ import { describeSource, normalizeSourceSpec, readSource, } from "../../public/proof-sources.js";
28
+ import { runCodeEvaluator, TS_RESOLVE_HOOKS, } from "./code-runner.js";
29
+ import { evaluateSourceCode } from "./source-code-readers.js";
30
+ import { machineCertificateProblem, patternCertificateProblem, ruleCertificateProblem, } from "./code-models.js";
31
+ function record(value) {
32
+ return value !== null && typeof value === "object" && !Array.isArray(value)
33
+ ? value
34
+ : null;
35
+ }
36
+ function text(value) {
37
+ return typeof value === "string" ? value : "";
38
+ }
39
+ export async function loadSymbol(context, symbolId) {
40
+ if (context.prolog === undefined)
41
+ return null;
42
+ const [row] = await loadEntities(context.prolog, {
43
+ id: symbolId,
44
+ type: "symbol",
45
+ });
46
+ if (!row)
47
+ return null;
48
+ const sourceFile = text(row.sourceFile) || text(row.source_file);
49
+ return { id: symbolId, title: text(row.title), sourceFile };
50
+ }
51
+ export function readWorkspaceFile(workspaceRoot, relative) {
52
+ const absolute = path.resolve(workspaceRoot, relative);
53
+ if (!absolute.startsWith(path.resolve(workspaceRoot) + path.sep))
54
+ return null;
55
+ return existsSync(absolute) ? readFileSync(absolute, "utf8") : null;
56
+ }
57
+ const IMPORT_SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s+|\brequire\s*\(\s*)["']([^"']+)["']/g;
58
+ /**
59
+ * Whether `source` (at `sourcePath`) imports `targetPath` and names
60
+ * `identifier`: the domain is read from the code, not hand-listed. A suite
61
+ * that tests compiled output imports the domain from the TypeScript
62
+ * project's `outDir`; given the workspace root, such an import counts when it
63
+ * maps back to the domain's source file under `rootDir`.
64
+ */
65
+ // implements REQ-kibi-code-proof-kinds
66
+ export function importsDomain(source, sourcePath, targetPath, identifier, workspaceRoot) {
67
+ if (identifier === "" || !new RegExp(`\\b${escapeRegExp(identifier)}\\b`).test(source))
68
+ return false;
69
+ const target = stripExtension(path.normalize(targetPath));
70
+ if (/\.py$/i.test(sourcePath))
71
+ return pythonImportsModule(source, sourcePath, targetPath);
72
+ for (const match of source.matchAll(IMPORT_SPECIFIER)) {
73
+ const specifier = match[1] ?? "";
74
+ if (!specifier.startsWith("."))
75
+ continue;
76
+ const joined = path.normalize(path.join(path.dirname(sourcePath), specifier));
77
+ if (stripExtension(joined) === target)
78
+ return true;
79
+ if (workspaceRoot === undefined)
80
+ continue;
81
+ const mapped = compiledToSourcePath(workspaceRoot, joined);
82
+ if (mapped !== null && stripExtension(mapped) === target)
83
+ return true;
84
+ }
85
+ return false;
86
+ }
87
+ const PYTHON_FROM_IMPORT = /^\s*from\s+(\.*)([\w.]*)\s+import\b/gm;
88
+ const PYTHON_IMPORT = /^\s*import\s+([\w.]+(?:\s*,\s*[\w.]+)*)/gm;
89
+ /**
90
+ * Whether a Python proof imports the module at `targetPath`: a relative
91
+ * `from .mod import X` resolved from the proof's package, or an absolute
92
+ * `from pkg.mod import X` / `import pkg.mod` whose dotted path names the
93
+ * file's trailing path segments (src layouts and packages alike).
94
+ */
95
+ function pythonImportsModule(source, sourcePath, targetPath) {
96
+ const target = path
97
+ .normalize(targetPath)
98
+ .replace(/\.pyi?$/i, "")
99
+ .replace(/[\\/]__init__$/, "");
100
+ const matchesDotted = (dotted) => {
101
+ if (dotted === "")
102
+ return false;
103
+ const tail = dotted.split(".").join(path.sep);
104
+ return target === tail || target.endsWith(`${path.sep}${tail}`);
105
+ };
106
+ for (const match of source.matchAll(PYTHON_FROM_IMPORT)) {
107
+ const dots = match[1] ?? "";
108
+ const module = match[2] ?? "";
109
+ if (dots === "") {
110
+ if (matchesDotted(module))
111
+ return true;
112
+ continue;
113
+ }
114
+ let base = path.dirname(path.normalize(sourcePath));
115
+ for (let level = 1; level < dots.length; level++)
116
+ base = path.dirname(base);
117
+ const resolved = path.join(base, ...module.split(".").filter(Boolean));
118
+ if (resolved === target)
119
+ return true;
120
+ }
121
+ for (const match of source.matchAll(PYTHON_IMPORT))
122
+ for (const dotted of (match[1] ?? "").split(","))
123
+ if (matchesDotted(dotted.trim()))
124
+ return true;
125
+ return false;
126
+ }
127
+ /** Strip `//` and block comments and trailing commas: tsconfig is JSONC. */
128
+ function parseJsonc(text) {
129
+ let out = "";
130
+ let inString = false;
131
+ for (let index = 0; index < text.length; index++) {
132
+ const char = text[index];
133
+ const next = text[index + 1];
134
+ if (inString) {
135
+ out += char;
136
+ if (char === "\\") {
137
+ out += next ?? "";
138
+ index++;
139
+ }
140
+ else if (char === '"')
141
+ inString = false;
142
+ continue;
143
+ }
144
+ if (char === '"') {
145
+ inString = true;
146
+ out += char;
147
+ }
148
+ else if (char === "/" && next === "/") {
149
+ while (index < text.length && text[index] !== "\n")
150
+ index++;
151
+ out += "\n";
152
+ }
153
+ else if (char === "/" && next === "*") {
154
+ index += 2;
155
+ while (index < text.length && !(text[index] === "*" && text[index + 1] === "/"))
156
+ index++;
157
+ index++;
158
+ }
159
+ else
160
+ out += char;
161
+ }
162
+ return JSON.parse(out.replace(/,(\s*[}\]])/g, "$1"));
163
+ }
164
+ /**
165
+ * Map a workspace-relative path inside a TypeScript project's `outDir` to the
166
+ * matching path under its `rootDir` (the tsconfig's directory when unset),
167
+ * using the nearest tsconfig.json above the path. Null when no tsconfig
168
+ * declares an `outDir` that contains it.
169
+ */
170
+ // implements REQ-kibi-code-proof-kinds
171
+ export function compiledToSourcePath(workspaceRoot, relativePath) {
172
+ const absolute = path.resolve(workspaceRoot, relativePath);
173
+ let dir = path.dirname(absolute);
174
+ const root = path.resolve(workspaceRoot);
175
+ for (;;) {
176
+ const config = path.join(dir, "tsconfig.json");
177
+ if (existsSync(config)) {
178
+ try {
179
+ const options = record(record(parseJsonc(readFileSync(config, "utf8")))?.compilerOptions);
180
+ const outDir = text(options?.outDir);
181
+ if (outDir !== "") {
182
+ const out = path.resolve(dir, outDir);
183
+ const rel = path.relative(out, absolute);
184
+ if (rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel)) {
185
+ const rootDir = path.resolve(dir, text(options?.rootDir) || ".");
186
+ return path.relative(root, path.join(rootDir, rel));
187
+ }
188
+ }
189
+ }
190
+ catch {
191
+ // An unreadable tsconfig maps nothing.
192
+ }
193
+ }
194
+ if (dir === root || path.dirname(dir) === dir)
195
+ return null;
196
+ dir = path.dirname(dir);
197
+ }
198
+ }
199
+ function stripExtension(file) {
200
+ return file.replace(/\.(?:d\.ts|[cm]?[jt]sx?)$/, "");
201
+ }
202
+ function escapeRegExp(value) {
203
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
204
+ }
205
+ const DOMAIN_EVALUATOR = `
206
+ const [file, name] = process.argv.slice(-2);
207
+ const { pathToFileURL } = await import("node:url");
208
+ if (typeof globalThis.Bun === "undefined") {
209
+ const { register } = await import("node:module");
210
+ register?.("data:text/javascript," + encodeURIComponent(${JSON.stringify(TS_RESOLVE_HOOKS)}));
211
+ }
212
+ const value = (await import(pathToFileURL(file).href))[name];
213
+ let items = null;
214
+ if (Array.isArray(value)) items = value;
215
+ else if (value instanceof Set) items = [...value];
216
+ else if (value instanceof Map) items = [...value.keys()];
217
+ else if (value !== null && typeof value === "object") items = Object.keys(value);
218
+ const primitive = (item) => ["string", "number", "boolean", "bigint"].includes(typeof item);
219
+ const scalar = typeof value === "number" || typeof value === "bigint" ? Number(value) : null;
220
+ console.log(JSON.stringify(scalar !== null
221
+ ? { scalar }
222
+ : items === null
223
+ ? { error: \`\${name} is not an array, set, map or object\` }
224
+ : { count: items.length, members: items.every(primitive) ? items.map(String) : null }));
225
+ `;
226
+ // A Python domain: a list, tuple, set, frozenset, dict or Enum class.
227
+ const PYTHON_DOMAIN_EVALUATOR = `
228
+ import enum, importlib, json, os, sys
229
+ file, name = sys.argv[-2], sys.argv[-1]
230
+ # Import the module under its package name, from the directory above its
231
+ # topmost package, so absolute and relative imports inside it resolve.
232
+ directory, module_name = os.path.split(os.path.splitext(file)[0])
233
+ parts = [] if module_name == "__init__" else [module_name]
234
+ while os.path.exists(os.path.join(directory, "__init__.py")):
235
+ directory, package = os.path.split(directory)
236
+ parts.insert(0, package)
237
+ sys.path.insert(0, directory)
238
+ module = importlib.import_module(".".join(parts))
239
+ value = getattr(module, name, None)
240
+ if isinstance(value, type) and issubclass(value, enum.Enum):
241
+ items = [member.value for member in value]
242
+ elif isinstance(value, dict):
243
+ items = list(value.keys())
244
+ elif isinstance(value, (list, tuple, set, frozenset)):
245
+ items = list(value)
246
+ else:
247
+ items = None
248
+ def primitive(item):
249
+ return isinstance(item, (str, int, float, bool))
250
+ def text(item):
251
+ if isinstance(item, bool):
252
+ return "true" if item else "false"
253
+ return str(item)
254
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
255
+ print(json.dumps({"scalar": value}))
256
+ elif items is None:
257
+ print(json.dumps({"error": f"{name} is not a list, tuple, set, dict or Enum"}))
258
+ else:
259
+ print(json.dumps({"count": len(items), "members": [text(i) for i in items] if all(primitive(i) for i in items) else None}))
260
+ `;
261
+ /**
262
+ * The domain as the code defines it: Kibi imports the domain_source file
263
+ * itself and reads the named export's members, so a certificate cannot cover
264
+ * a hand-listed subset. A TypeScript or JavaScript domain loads with bun when
265
+ * it is on PATH, else with the workspace's tsx, else with node's type
266
+ * stripping (which retries extensionless relative imports as TypeScript
267
+ * files); a Python domain (`.py`) loads with the first interpreter
268
+ * pythonDomainInterpreters finds.
269
+ */
270
+ // implements REQ-kibi-code-proof-kinds
271
+ export function evaluateDomain(workspaceRoot, sourceFile, exportName, options = {}) {
272
+ const evaluated = evaluateExport(workspaceRoot, sourceFile, exportName, options);
273
+ if (!evaluated.ok)
274
+ return evaluated;
275
+ if (evaluated.scalar !== undefined)
276
+ return {
277
+ ok: false,
278
+ reason: `${exportName} is a number, not a list, set, map or object`,
279
+ };
280
+ return { ok: true, count: evaluated.count, members: evaluated.members };
281
+ }
282
+ /**
283
+ * The number a value_source export holds, read the same way as a domain.
284
+ */
285
+ // implements REQ-kibi-claim-bound-proofs
286
+ export function evaluateScalar(workspaceRoot, sourceFile, exportName) {
287
+ const evaluated = evaluateExport(workspaceRoot, sourceFile, exportName);
288
+ if (!evaluated.ok)
289
+ return evaluated;
290
+ if (evaluated.scalar === undefined || !Number.isFinite(evaluated.scalar))
291
+ return { ok: false, reason: `${exportName} in ${sourceFile} is not a finite number` };
292
+ return { ok: true, value: evaluated.scalar };
293
+ }
294
+ function evaluateExport(workspaceRoot, sourceFile, exportName, options = {}) {
295
+ const absolute = path.resolve(workspaceRoot, sourceFile);
296
+ const run = runCodeEvaluator(workspaceRoot, sourceFile, { js: DOMAIN_EVALUATOR, python: PYTHON_DOMAIN_EVALUATOR }, [absolute, exportName], (parsed) => {
297
+ const row = parsed;
298
+ return (row.error !== undefined ||
299
+ typeof row.scalar === "number" ||
300
+ typeof row.count === "number");
301
+ }, options);
302
+ if (!run.ok)
303
+ return {
304
+ ok: false,
305
+ reason: `Kibi could not load ${exportName} from ${sourceFile}${run.failure ? `: ${run.failure}` : ""}${run.python ? " (a Python domain loads with KIBI_PYTHON, the active or project virtualenv (.venv or venv), then python3 on PATH)" : " (without bun or the workspace's tsx, node's type stripping loads the domain; a type imported without import type stops it)"}`,
306
+ };
307
+ const parsed = run.value;
308
+ if (parsed.error !== undefined)
309
+ return { ok: false, reason: parsed.error };
310
+ if (typeof parsed.scalar === "number")
311
+ return { ok: true, count: 0, members: null, scalar: parsed.scalar };
312
+ return {
313
+ ok: true,
314
+ count: parsed.count,
315
+ members: parsed.members ?? null,
316
+ };
317
+ }
318
+ /**
319
+ * Whether an exhaustive certificate covers the domain Kibi evaluated: the
320
+ * same members when they are primitive values, else the same count.
321
+ */
322
+ // implements REQ-kibi-code-proof-kinds
323
+ export function domainCoverageProblem(exportName, evaluated, certified) {
324
+ if (evaluated.members !== null) {
325
+ const expected = new Set(evaluated.members);
326
+ const covered = new Set(certified);
327
+ const missing = [...expected].filter((member) => !covered.has(member));
328
+ const extra = [...covered].filter((member) => !expected.has(member));
329
+ if (missing.length === 0 && extra.length === 0)
330
+ return null;
331
+ return `the certificate does not cover ${exportName} as the code defines it (${[
332
+ missing.length > 0 ? `missing ${missing.slice(0, 5).join(", ")}` : "",
333
+ extra.length > 0 ? `not in the domain ${extra.slice(0, 5).join(", ")}` : "",
334
+ ]
335
+ .filter(Boolean)
336
+ .join("; ")})`;
337
+ }
338
+ return certified.length === evaluated.count
339
+ ? null
340
+ : `the certificate covers ${certified.length} members but ${exportName} has ${evaluated.count}`;
341
+ }
342
+ /**
343
+ * The TypeScript compiler API for a witness: the package nearest the witness
344
+ * first, then the workspace root. TypeScript 7 ships without the JS compiler
345
+ * API, so a 7.x install is skipped for an older one further up, and named in
346
+ * the reason when none is left.
347
+ */
348
+ function loadTypeScript(workspaceRoot, absoluteWitness) {
349
+ const noApi = [];
350
+ const bases = [
351
+ path.join(path.dirname(absoluteWitness), "noop.js"),
352
+ path.join(workspaceRoot, "package.json"),
353
+ ];
354
+ for (const base of bases) {
355
+ let loaded;
356
+ try {
357
+ loaded = createRequire(base)("typescript");
358
+ }
359
+ catch {
360
+ continue;
361
+ }
362
+ if (typeof loaded.createProgram === "function")
363
+ return { ok: true, ts: loaded };
364
+ noApi.push(String(loaded.version ?? "unknown"));
365
+ }
366
+ return {
367
+ ok: false,
368
+ reason: noApi.length > 0
369
+ ? `the workspace's TypeScript ${noApi[0]} has no compiler API to compile the witness (TypeScript 7 ships without it); add typescript@6 as a dev dependency for typecheck proofs`
370
+ : "Kibi could not load TypeScript from the workspace to compile the witness (install typescript)",
371
+ };
372
+ }
373
+ /** Options that decide how the witness's imports resolve, from the nearest tsconfig. */
374
+ const RESOLUTION_OPTIONS = [
375
+ "module",
376
+ "moduleResolution",
377
+ "baseUrl",
378
+ "paths",
379
+ "rootDirs",
380
+ "customConditions",
381
+ "resolvePackageJsonExports",
382
+ "resolvePackageJsonImports",
383
+ "allowImportingTsExtensions",
384
+ "resolveJsonModule",
385
+ "jsx",
386
+ "jsxImportSource",
387
+ "lib",
388
+ "target",
389
+ "esModuleInterop",
390
+ "allowSyntheticDefaultImports",
391
+ "verbatimModuleSyntax",
392
+ ];
393
+ /**
394
+ * The nearest tsconfig's resolution options, and the declaration files it
395
+ * includes: global typings such as a `reset.d.ts` that the project's types
396
+ * rely on, which a witness importing those types needs too.
397
+ */
398
+ function projectCompileSettings(ts, workspaceRoot, absoluteWitness) {
399
+ const none = { options: {}, declarations: [] };
400
+ const configPath = ts.findConfigFile(path.dirname(absoluteWitness), ts.sys.fileExists, "tsconfig.json");
401
+ if (configPath === undefined ||
402
+ path.relative(workspaceRoot, configPath).startsWith(".."))
403
+ return none;
404
+ const read = ts.readConfigFile(configPath, ts.sys.readFile);
405
+ if (read.error !== undefined)
406
+ return none;
407
+ const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, path.dirname(configPath), undefined, configPath);
408
+ const options = {};
409
+ for (const key of RESOLUTION_OPTIONS)
410
+ if (parsed.options[key] !== undefined)
411
+ options[key] = parsed.options[key];
412
+ return {
413
+ options: options,
414
+ declarations: parsed.fileNames.filter((file) => file.endsWith(".d.ts")),
415
+ };
416
+ }
417
+ /**
418
+ * Whether a witness can stop compiling when a domain member goes unhandled:
419
+ * it uses the never type (a switch ending in a never assignment) or a
420
+ * satisfies expression (a map over the domain). Read from the syntax tree,
421
+ * so the word never in a comment or a string does not count.
422
+ */
423
+ function hasExhaustivenessCheck(ts, witness) {
424
+ const visit = (node) => node.kind === ts.SyntaxKind.NeverKeyword ||
425
+ node.kind === ts.SyntaxKind.SatisfiesExpression ||
426
+ (ts.forEachChild(node, (child) => (visit(child) ? true : undefined)) ??
427
+ false);
428
+ return visit(witness);
429
+ }
430
+ /**
431
+ * Kibi compiles a typecheck witness itself, with the workspace's TypeScript.
432
+ * Imports resolve the way the project's own tsconfig says (module,
433
+ * moduleResolution, paths, ...) and its declaration files are in the
434
+ * program, but the checking options stay fixed and
435
+ * strict, so a lax project tsconfig cannot make the never check vacuous. It
436
+ * accepts the witness only with no errors in it.
437
+ */
438
+ // implements REQ-kibi-code-proof-kinds
439
+ export function compileWitness(workspaceRoot, witnessPath, options = {}) {
440
+ const absolute = path.resolve(workspaceRoot, witnessPath);
441
+ const loaded = loadTypeScript(workspaceRoot, absolute);
442
+ if (!loaded.ok)
443
+ return loaded;
444
+ const { ts } = loaded;
445
+ const { options: project, declarations } = projectCompileSettings(ts, workspaceRoot, absolute);
446
+ // module and moduleResolution must agree, so take both or neither.
447
+ const moduleOptions = project.module !== undefined || project.moduleResolution !== undefined
448
+ ? {}
449
+ : {
450
+ module: ts.ModuleKind.NodeNext,
451
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
452
+ };
453
+ const program = ts.createProgram([absolute, ...declarations], {
454
+ target: ts.ScriptTarget.ES2022,
455
+ ...moduleOptions,
456
+ ...project,
457
+ strict: true,
458
+ noEmit: true,
459
+ skipLibCheck: true,
460
+ allowJs: true,
461
+ checkJs: true,
462
+ types: [],
463
+ });
464
+ const witness = program.getSourceFile(absolute);
465
+ if (witness === undefined)
466
+ return { ok: false, reason: `Kibi could not read the witness ${witnessPath}` };
467
+ if (options.requireExhaustiveness === true && !hasExhaustivenessCheck(ts, witness))
468
+ return {
469
+ ok: false,
470
+ reason: `the witness ${witnessPath} has no exhaustiveness check (a never type or a satisfies map), so a new domain member would still compile`,
471
+ };
472
+ const errors = [
473
+ ...program.getSyntacticDiagnostics(witness),
474
+ ...program.getSemanticDiagnostics(witness),
475
+ ].filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
476
+ if (errors.length > 0)
477
+ return {
478
+ ok: false,
479
+ reason: `TypeScript ${ts.version} rejects the witness ${witnessPath}: ${ts.flattenDiagnosticMessageText(errors[0]?.messageText ?? "", " ")}`,
480
+ // Zero-based witness lines with an error, so a generated witness can
481
+ // say which of its checks failed.
482
+ errorLines: [
483
+ ...new Set(errors
484
+ .filter((error) => error.file === witness && error.start !== undefined)
485
+ .map((error) => witness.getLineAndCharacterOfPosition(error.start ?? 0).line)),
486
+ ],
487
+ };
488
+ return { ok: true, version: ts.version };
489
+ }
490
+ /**
491
+ * The constraints the contract's constraint_source facts state on one
492
+ * numeric property, or why they cannot ground a certificate.
493
+ */
494
+ async function factConstraints(context, factIds) {
495
+ if (context.prolog === undefined)
496
+ return { ok: false, reason: "no Prolog runtime is attached to read the facts" };
497
+ const constraints = [];
498
+ const subjects = new Set();
499
+ const valueTypes = new Set();
500
+ for (const id of factIds) {
501
+ const [fact] = await loadEntities(context.prolog, { id, type: "fact" });
502
+ if (!fact)
503
+ return { ok: false, reason: `constraint_source fact ${id} does not exist` };
504
+ if (text(fact.status) !== "active")
505
+ return { ok: false, reason: `constraint_source fact ${id} is not active` };
506
+ const polarity = text(fact.polarity);
507
+ if (text(fact.fact_kind) !== "property_value" || (polarity !== "" && polarity !== "require"))
508
+ return {
509
+ ok: false,
510
+ reason: `constraint_source fact ${id} is not a required property_value fact`,
511
+ };
512
+ const valueType = text(fact.value_type);
513
+ const value = valueType === "int" ? fact.value_int : valueType === "number" ? fact.value_number : undefined;
514
+ const operator = text(fact.operator);
515
+ if (typeof value !== "number" || !["eq", "neq", "lt", "lte", "gt", "gte"].includes(operator))
516
+ return {
517
+ ok: false,
518
+ reason: `constraint_source fact ${id} has no numeric operator and value`,
519
+ };
520
+ subjects.add(`${text(fact.subject_key)}\0${text(fact.property_key)}`);
521
+ valueTypes.add(valueType);
522
+ constraints.push({ operator, value });
523
+ }
524
+ if (subjects.size !== 1)
525
+ return {
526
+ ok: false,
527
+ reason: "the constraint_source facts are about more than one subject and property",
528
+ };
529
+ const [subject = ""] = [...subjects];
530
+ return {
531
+ ok: true,
532
+ property: subject.split("\0")[1] ?? "",
533
+ valueType: valueTypes.has("number") ? "real" : "int",
534
+ constraints,
535
+ };
536
+ }
537
+ function constraintKey(row) {
538
+ return `${text(row.operator)}:${String(row.value)}`;
539
+ }
540
+ const SMT_OPERATORS = {
541
+ eq: "=",
542
+ lt: "<",
543
+ lte: "<=",
544
+ gt: ">",
545
+ gte: ">=",
546
+ };
547
+ function smtNumber(value, valueType) {
548
+ const magnitude = valueType === "real" && Number.isInteger(value)
549
+ ? `${Math.abs(value)}.0`
550
+ : String(Math.abs(value));
551
+ return value < 0 ? `(- ${magnitude})` : magnitude;
552
+ }
553
+ /** The SMT-LIB assertion a fact constraint must appear as in the query. */
554
+ export function smtAssertion(property, valueType, row) {
555
+ const value = smtNumber(row.value, valueType);
556
+ return row.operator === "neq"
557
+ ? `(assert (distinct ${property} ${value}))`
558
+ : `(assert (${SMT_OPERATORS[row.operator]} ${property} ${value}))`;
559
+ }
560
+ /**
561
+ * The values a contract's value_source exports hold, read from the code the
562
+ * same way as a domain (a count measure is the collection's size), or why
563
+ * one cannot be read.
564
+ */
565
+ // implements REQ-kibi-claim-bound-proofs
566
+ export async function valueSourceValues(context, contract) {
567
+ const values = [];
568
+ for (const source of contract.value_source ?? []) {
569
+ if ("source" in source) {
570
+ // A source count reads the members Kibi reads from the project.
571
+ const read = await readSource(context.workspaceRoot, source.source, evaluateSourceCode);
572
+ if (!read.ok)
573
+ return {
574
+ ok: false,
575
+ reason: `Kibi could not read the value_source ${describeSource(source.source)}: ${read.reason}`,
576
+ };
577
+ values.push({ symbol_id: describeSource(source.source), value: read.model.members.length });
578
+ continue;
579
+ }
580
+ const symbol = await loadSymbol(context, source.symbol_id);
581
+ if (!symbol || symbol.sourceFile === "")
582
+ return {
583
+ ok: false,
584
+ reason: `the value_source symbol ${source.symbol_id} has no source file`,
585
+ };
586
+ const exportName = source.export ?? symbol.title;
587
+ if (source.measure === "count") {
588
+ // A count claim reads the collection itself, not a number kept
589
+ // beside it for the proof.
590
+ const domain = evaluateDomain(context.workspaceRoot, symbol.sourceFile, exportName);
591
+ if (!domain.ok)
592
+ return domain;
593
+ values.push({ symbol_id: source.symbol_id, value: domain.count });
594
+ continue;
595
+ }
596
+ const evaluated = evaluateScalar(context.workspaceRoot, symbol.sourceFile, exportName);
597
+ if (!evaluated.ok)
598
+ return evaluated;
599
+ values.push({ symbol_id: source.symbol_id, value: evaluated.value });
600
+ }
601
+ return { ok: true, values };
602
+ }
603
+ /**
604
+ * arithmetic and smt certificates must rest on the knowledge base and the
605
+ * code: the arithmetic assumptions are exactly the constraint_source facts'
606
+ * constraints plus one `eq` per value_source constant, on a variable named
607
+ * after the fact property, and an smt query declares that constant and
608
+ * asserts each of them.
609
+ */
610
+ async function constraintSourceProblem(context, contract, certificate) {
611
+ const factIds = contract.constraint_source?.fact_ids ?? [];
612
+ if (factIds.length === 0 && (contract.value_source ?? []).length === 0)
613
+ return `the contract names no constraint_source facts or value_source constants, so the ${contract.kind} certificate proves numbers the test chose`;
614
+ const facts = factIds.length === 0
615
+ ? null
616
+ : await factConstraints(context, factIds);
617
+ if (facts !== null && !facts.ok)
618
+ return facts.reason;
619
+ const sources = await valueSourceValues(context, contract);
620
+ if (!sources.ok)
621
+ return sources.reason;
622
+ const property = facts !== null && facts.ok ? facts.property : text(certificate.variable);
623
+ const valueType = facts !== null && facts.ok
624
+ ? facts.valueType
625
+ : text(certificate.value_type) === "real" ||
626
+ sources.values.some((row) => !Number.isInteger(row.value))
627
+ ? "real"
628
+ : "int";
629
+ const constraints = [
630
+ ...(facts !== null && facts.ok ? facts.constraints : []),
631
+ ...sources.values.map((row) => ({ operator: "eq", value: row.value })),
632
+ ];
633
+ if (contract.kind === "arithmetic") {
634
+ if (text(certificate.variable) !== property)
635
+ return `the certificate's variable ${text(certificate.variable) || "(none)"} is not the facts' property ${property}`;
636
+ if (text(certificate.value_type) !== valueType)
637
+ return `the certificate's value_type ${text(certificate.value_type)} is not the facts' ${valueType}`;
638
+ const assumptions = Array.isArray(certificate.assumptions)
639
+ ? certificate.assumptions.map((row) => constraintKey(record(row) ?? {}))
640
+ : [];
641
+ const expected = constraints.map(constraintKey);
642
+ const same = assumptions.length === expected.length &&
643
+ [...assumptions].sort().join("|") === [...expected].sort().join("|");
644
+ if (!same)
645
+ return `the certificate's assumptions are not exactly the constraint_source facts' constraints and value_source values (${expected.join(", ")})`;
646
+ return null;
647
+ }
648
+ const query = text(certificate.query).replace(/\s+/g, " ");
649
+ const sort = valueType === "int" ? "Int" : "Real";
650
+ if (property === "")
651
+ return "the smt contract names no constraint_source facts, so Kibi cannot tell which constant the query is about";
652
+ if (!query.includes(`(declare-const ${property} ${sort})`))
653
+ return `the query does not declare (declare-const ${property} ${sort}) for the facts' property`;
654
+ for (const row of constraints) {
655
+ const assertion = smtAssertion(property, valueType, row);
656
+ if (!query.includes(assertion))
657
+ return `the query does not assert the constraint_source or value_source constraint ${assertion}`;
658
+ }
659
+ return null;
660
+ }
661
+ /** The solver re-run behind an smt certificate; unsat is the only proof. */
662
+ // implements REQ-kibi-code-proof-kinds
663
+ export function rerunSmtQuery(solver, query) {
664
+ const argv = solver === "z3"
665
+ ? ["-smt2", "-in"]
666
+ : solver === "cvc5"
667
+ ? ["--lang", "smt2"]
668
+ : null;
669
+ if (argv === null)
670
+ return { ok: false, reason: `solver ${solver} is not z3 or cvc5` };
671
+ const run = spawnSync(solver, argv, {
672
+ input: query,
673
+ encoding: "utf8",
674
+ timeout: 60_000,
675
+ });
676
+ if (run.error)
677
+ return {
678
+ ok: false,
679
+ reason: `Kibi could not re-run ${solver} on the query: ${run.error.message}`,
680
+ };
681
+ const answer = (run.stdout ?? "").trim().split(/\s+/)[0] ?? "";
682
+ return answer === "unsat"
683
+ ? { ok: true }
684
+ : {
685
+ ok: false,
686
+ reason: `re-running ${solver} on the query answered ${answer || "nothing"}, not unsat`,
687
+ };
688
+ }
689
+ async function workspaceProblem(context, contract, certificate) {
690
+ const kind = contract.kind;
691
+ if (kind === "exhaustive" || kind === "typecheck") {
692
+ const domainId = contract.domain_source?.symbol_id ?? "";
693
+ const named = text(record(certificate.domain)?.symbol_id);
694
+ if (named !== domainId)
695
+ return `the certificate's domain ${named || "(none)"} is not the contract's domain_source ${domainId}`;
696
+ const domain = await loadSymbol(context, domainId);
697
+ if (!domain || domain.sourceFile === "")
698
+ return `the domain_source symbol ${domainId} has no source file`;
699
+ const readers = kind === "typecheck"
700
+ ? [contract.witness?.path ?? ""]
701
+ : (await Promise.all(contract.required_proofs.map((proof) => loadSymbol(context, proof.symbol_id)))).map((symbol) => symbol?.sourceFile ?? "");
702
+ // The export Kibi reads: domain_source.export, else the symbol's title.
703
+ const exportName = contract.domain_source?.export ?? domain.title;
704
+ const importing = readers.some((reader) => {
705
+ if (reader === "")
706
+ return false;
707
+ const source = readWorkspaceFile(context.workspaceRoot, reader);
708
+ return (source !== null &&
709
+ importsDomain(source, reader, domain.sourceFile, exportName, context.workspaceRoot));
710
+ });
711
+ if (!importing)
712
+ return `the proof does not import ${exportName || domainId} from ${domain.sourceFile}, so its domain may be hand-listed${contract.domain_source?.export === undefined ? ` (Kibi reads the export named by the symbol's title; set domain_source.export when the export has another name)` : ""}`;
713
+ if (kind === "exhaustive") {
714
+ const evaluated = evaluateDomain(context.workspaceRoot, domain.sourceFile, exportName);
715
+ if (!evaluated.ok)
716
+ return evaluated.reason;
717
+ const members = record(certificate.domain)?.members;
718
+ const problem = domainCoverageProblem(exportName, evaluated, Array.isArray(members) ? members.map(String) : []);
719
+ if (problem !== null)
720
+ return problem;
721
+ }
722
+ if (kind === "typecheck") {
723
+ const witness = record(certificate.witness);
724
+ const witnessPath = contract.witness?.path ?? "";
725
+ if (text(witness?.path) !== witnessPath)
726
+ return `the certificate's witness is not the contract's witness ${witnessPath}`;
727
+ const content = readWorkspaceFile(context.workspaceRoot, witnessPath);
728
+ if (content === null)
729
+ return `the witness ${witnessPath} does not exist`;
730
+ const sha = createHash("sha256").update(content).digest("hex");
731
+ if (text(witness?.sha256) !== sha)
732
+ return `the witness sha256 does not match ${witnessPath} in the workspace`;
733
+ // With proves, Kibi decides each claim with its own witness (set
734
+ // equality, required fields), so the author's witness only has to
735
+ // compile. Without proves, the receipt's proved rests on this witness
736
+ // alone, so it must be able to fail when a domain member goes unhandled.
737
+ const compiled = compileWitness(context.workspaceRoot, witnessPath, {
738
+ requireExhaustiveness: (contract.proves ?? []).length === 0,
739
+ });
740
+ if (!compiled.ok)
741
+ return compiled.reason;
742
+ }
743
+ }
744
+ if (kind === "arithmetic" || kind === "smt") {
745
+ const problem = await constraintSourceProblem(context, contract, certificate);
746
+ if (problem !== null)
747
+ return problem;
748
+ }
749
+ if (kind === "smt") {
750
+ const solver = text(record(certificate.solver)?.name);
751
+ const rerun = rerunSmtQuery(solver, text(certificate.query));
752
+ if (!rerun.ok)
753
+ return rerun.reason;
754
+ }
755
+ if (kind === "rule")
756
+ return ruleCertificateProblem(context, contract, certificate);
757
+ if (kind === "state_machine")
758
+ return machineCertificateProblem(context, contract, certificate);
759
+ if (kind === "regex")
760
+ return patternCertificateProblem(context, contract, certificate);
761
+ return null;
762
+ }
763
+ /**
764
+ * The certificate of a source proof: the contract's source and what Kibi
765
+ * reads from it. A certificate the run left must name the same source, and
766
+ * a model it carries must be what Kibi reads; either way the stored model is
767
+ * Kibi's own read, so the proof never rests on members the test listed.
768
+ */
769
+ // implements REQ-kibi-source-proof-kinds
770
+ export async function completeSourceCertificate(context, testId, contract, left) {
771
+ const spec = contract.source;
772
+ if (spec === undefined)
773
+ return { ok: false, reason: "the source contract names no source" };
774
+ const row = record(left);
775
+ if (left !== undefined && left !== null && row === null)
776
+ return { ok: false, reason: "the run left a source certificate that is not a JSON object" };
777
+ if (row !== null) {
778
+ if (text(row.kind) !== "source")
779
+ return { ok: false, reason: `the certificate kind ${text(row.kind) || "(none)"} is not the contract kind source` };
780
+ if (text(row.test_id) !== testId)
781
+ return { ok: false, reason: `the certificate is for ${text(row.test_id)}` };
782
+ if (row.source !== undefined &&
783
+ canonicalCertificateJson(normalizeSourceSpec(row.source) ?? row.source) !==
784
+ canonicalCertificateJson(spec))
785
+ return { ok: false, reason: `the certificate reads ${JSON.stringify(row.source)}, not the contract's source` };
786
+ }
787
+ const read = await readSource(context.workspaceRoot, spec, evaluateSourceCode);
788
+ if (!read.ok)
789
+ return { ok: false, reason: `Kibi could not read ${describeSource(spec)}: ${read.reason}` };
790
+ const model = read.model;
791
+ if (row?.model !== undefined && canonicalCertificateJson(row.model) !== canonicalCertificateJson(model))
792
+ return {
793
+ ok: false,
794
+ reason: `the certificate's model is not what Kibi reads from ${describeSource(spec)} (${model.members.slice(0, 8).join(", ") || "no members"})`,
795
+ };
796
+ return {
797
+ ok: true,
798
+ certificate: {
799
+ version: CODE_CERTIFICATE_VERSION,
800
+ kind: "source",
801
+ test_id: testId,
802
+ source: spec,
803
+ model,
804
+ },
805
+ };
806
+ }
807
+ const WITNESS_NOT_ACCEPTED = "the type checker did not accept the witness";
808
+ /**
809
+ * A typecheck certificate whose own outcome is failed says only that: add
810
+ * what Kibi's compile of the same witness reports, so the author sees the
811
+ * first error, or learns that the test compiled it differently.
812
+ */
813
+ export function withWitnessDiagnosis(context, contract, reason) {
814
+ const witnessPath = contract.witness?.path ?? "";
815
+ if (reason !== WITNESS_NOT_ACCEPTED || witnessPath === "")
816
+ return reason;
817
+ const compiled = compileWitness(context.workspaceRoot, witnessPath);
818
+ return compiled.ok
819
+ ? `${reason}, though Kibi's own compile of ${witnessPath} with TypeScript ${compiled.version} accepts it (the test compiled it with other settings)`
820
+ : `${reason}: ${compiled.reason}`;
821
+ }
822
+ export { pythonDomainInterpreters, runCodeEvaluator, TS_RESOLVE_HOOKS, } from "./code-runner.js";
823
+ /**
824
+ * Re-check the certificate a certificate-kind run left for one test. A
825
+ * runner kind (e2e, example, property) supports tested evidence and needs
826
+ * no certificate; a certificate kind supports proved evidence only when the
827
+ * certificate re-checks: its structure and arithmetic in code_certificates.pl,
828
+ * and here that the proof read its domain from the code, that the witness is
829
+ * the workspace file, that the solver still answers unsat, and that a rule
830
+ * table, state machine or pattern is what Kibi itself reads from the code.
831
+ */
832
+ // implements REQ-kibi-code-proof-kinds
833
+ export async function checkCodeCertificate(context, input) {
834
+ const kind = input.contract.kind ?? "e2e";
835
+ if (!isCertificateProofKind(kind))
836
+ return { kind, evidence_level: "tested" };
837
+ let left = input.certificate;
838
+ if (kind === "source" && input.outcome === "passed") {
839
+ // Kibi reads the source itself, so the certificate it stores is its own
840
+ // read, whatever the run left (or nothing).
841
+ const completed = await completeSourceCertificate(context, input.testId, input.contract, input.certificate);
842
+ if (!completed.ok) {
843
+ const leftRecord = record(left);
844
+ return {
845
+ kind,
846
+ evidence_level: "tested",
847
+ ...(leftRecord
848
+ ? { certificate: leftRecord, certificate_digest: certificateDigest(leftRecord) }
849
+ : {}),
850
+ certificate_rejection: completed.reason,
851
+ };
852
+ }
853
+ left = completed.certificate;
854
+ }
855
+ const certificate = record(left);
856
+ if (!certificate)
857
+ return {
858
+ kind,
859
+ evidence_level: "tested",
860
+ certificate_rejection: `the run left no ${CODE_CERTIFICATE_VERSION} for ${input.testId}`,
861
+ };
862
+ const digest = certificateDigest(certificate);
863
+ const rejected = (reason) => ({
864
+ kind,
865
+ evidence_level: "tested",
866
+ certificate,
867
+ certificate_digest: digest,
868
+ certificate_rejection: reason,
869
+ });
870
+ if (text(certificate.kind) !== kind)
871
+ return rejected(`the certificate kind ${text(certificate.kind) || "(none)"} is not the contract kind ${kind}`);
872
+ if (text(certificate.test_id) !== input.testId)
873
+ return rejected(`the certificate is for ${text(certificate.test_id)}`);
874
+ if (input.outcome !== "passed")
875
+ return rejected("the proof run did not pass");
876
+ if (context.prolog === undefined)
877
+ return rejected("no Prolog runtime is attached to re-check the certificate");
878
+ const checked = await runOperationJsonQuery(context.prolog, "code_certificates.pl", `code_certificates:code_certificate_check_json(${toPrologAtom(JSON.stringify(certificate))}, JsonString)`, "Code certificate check");
879
+ if (checked.accepted !== true)
880
+ return rejected(withWitnessDiagnosis(context, input.contract, checked.reason ?? "the certificate did not re-check"));
881
+ // Every later re-check hashes the stored certificate in Prolog, so a
882
+ // certificate whose two canonical forms differ would silently lose proved.
883
+ if (checked.digest !== digest)
884
+ return rejected("the certificate does not hash the same in the CLI and in code_certificates.pl (write non-integer numbers as strings)");
885
+ const problem = await workspaceProblem(context, input.contract, certificate);
886
+ if (problem !== null)
887
+ return rejected(problem);
888
+ return {
889
+ kind,
890
+ evidence_level: "proved",
891
+ certificate,
892
+ certificate_digest: digest,
893
+ };
894
+ }