kibi-cli 2.31.1 → 2.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) 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/commands/check.d.ts +1 -0
  6. package/dist/commands/check.d.ts.map +1 -1
  7. package/dist/commands/check.js +14 -5
  8. package/dist/commands/discovery-table.d.ts.map +1 -1
  9. package/dist/commands/discovery-table.js +51 -4
  10. package/dist/commands/doctor.js +19 -4
  11. package/dist/commands/migrate.d.ts.map +1 -1
  12. package/dist/commands/migrate.js +89 -3
  13. package/dist/commands/prove.d.ts +48 -1
  14. package/dist/commands/prove.d.ts.map +1 -1
  15. package/dist/commands/prove.js +293 -8
  16. package/dist/commands/report.d.ts +1 -0
  17. package/dist/commands/report.d.ts.map +1 -1
  18. package/dist/commands/report.js +70 -5
  19. package/dist/commands/signoff.d.ts.map +1 -1
  20. package/dist/commands/signoff.js +20 -1
  21. package/dist/commands/sync/manifest.d.ts.map +1 -1
  22. package/dist/commands/sync/manifest.js +20 -4
  23. package/dist/commands/sync/persistence.d.ts.map +1 -1
  24. package/dist/commands/sync/persistence.js +16 -5
  25. package/dist/diagnostics.js +1 -1
  26. package/dist/engine.js +1 -1
  27. package/dist/extractors/markdown.d.ts +3 -1
  28. package/dist/extractors/markdown.d.ts.map +1 -1
  29. package/dist/extractors/markdown.js +17 -0
  30. package/dist/operations/bootstrap/candidate-helpers.d.ts +3 -0
  31. package/dist/operations/bootstrap/candidate-helpers.d.ts.map +1 -1
  32. package/dist/operations/bootstrap/candidate-helpers.js +4 -0
  33. package/dist/operations/migration/evidence-floor.d.ts +41 -0
  34. package/dist/operations/migration/evidence-floor.d.ts.map +1 -0
  35. package/dist/operations/migration/evidence-floor.js +230 -0
  36. package/dist/operations/migration/proof-contract-claims.d.ts +59 -0
  37. package/dist/operations/migration/proof-contract-claims.d.ts.map +1 -0
  38. package/dist/operations/migration/proof-contract-claims.js +182 -0
  39. package/dist/operations/migration/schema6.d.ts +6 -0
  40. package/dist/operations/migration/schema6.d.ts.map +1 -1
  41. package/dist/operations/migration/schema6.js +94 -0
  42. package/dist/operations/mutation/omitted-properties.d.ts +11 -0
  43. package/dist/operations/mutation/omitted-properties.d.ts.map +1 -0
  44. package/dist/operations/mutation/omitted-properties.js +76 -0
  45. package/dist/operations/mutation/serialization.d.ts.map +1 -1
  46. package/dist/operations/mutation/serialization.js +1 -0
  47. package/dist/operations/mutation/strict-fact.d.ts.map +1 -1
  48. package/dist/operations/mutation/strict-fact.js +6 -2
  49. package/dist/operations/mutation/symbol-granularity.d.ts.map +1 -1
  50. package/dist/operations/mutation/symbol-granularity.js +5 -3
  51. package/dist/operations/mutation/upsert.d.ts.map +1 -1
  52. package/dist/operations/mutation/upsert.js +7 -1
  53. package/dist/operations/mutation/validation.d.ts +2 -0
  54. package/dist/operations/mutation/validation.d.ts.map +1 -1
  55. package/dist/operations/mutation/validation.js +4 -0
  56. package/dist/operations/proof/claim-bindings.d.ts +23 -0
  57. package/dist/operations/proof/claim-bindings.d.ts.map +1 -0
  58. package/dist/operations/proof/claim-bindings.js +211 -0
  59. package/dist/operations/proof/code-certificates.d.ts +135 -0
  60. package/dist/operations/proof/code-certificates.d.ts.map +1 -0
  61. package/dist/operations/proof/code-certificates.js +785 -0
  62. package/dist/operations/proof/code-scope.d.ts +11 -1
  63. package/dist/operations/proof/code-scope.d.ts.map +1 -1
  64. package/dist/operations/proof/code-scope.js +20 -2
  65. package/dist/operations/proof/compact-receipts.d.ts.map +1 -1
  66. package/dist/operations/proof/compact-receipts.js +4 -3
  67. package/dist/operations/proof/explain.d.ts +32 -0
  68. package/dist/operations/proof/explain.d.ts.map +1 -1
  69. package/dist/operations/proof/explain.js +91 -0
  70. package/dist/operations/proof/ingest-proof.d.ts +9 -1
  71. package/dist/operations/proof/ingest-proof.d.ts.map +1 -1
  72. package/dist/operations/proof/ingest-proof.js +41 -2
  73. package/dist/operations/proof/receipt-compaction.d.ts +2 -0
  74. package/dist/operations/proof/receipt-compaction.d.ts.map +1 -1
  75. package/dist/operations/proof/receipt-compaction.js +6 -2
  76. package/dist/operations/semantic-advisor/shared.d.ts.map +1 -1
  77. package/dist/operations/semantic-advisor/shared.js +4 -0
  78. package/dist/operations/semantic-advisor/strict-rules.js +3 -3
  79. package/dist/operations/semantic-advisor/types.d.ts +2 -1
  80. package/dist/operations/semantic-advisor/types.d.ts.map +1 -1
  81. package/dist/plugins/approved-source-analyzers.generated.d.ts +6 -6
  82. package/dist/plugins/approved-source-analyzers.generated.js +6 -6
  83. package/dist/proof/command-test-report.d.ts +4 -3
  84. package/dist/proof/command-test-report.d.ts.map +1 -1
  85. package/dist/proof/command-test-report.js +4 -10
  86. package/dist/proof/evaluate.d.ts.map +1 -1
  87. package/dist/proof/evaluate.js +18 -2
  88. package/dist/proof/inspect.d.ts.map +1 -1
  89. package/dist/proof/inspect.js +58 -0
  90. package/dist/proof/producers/playwright-reporter.d.ts +3 -0
  91. package/dist/proof/producers/playwright-reporter.d.ts.map +1 -1
  92. package/dist/proof/producers/playwright-reporter.js +25 -2
  93. package/dist/proof/step-runner.d.ts +36 -0
  94. package/dist/proof/step-runner.d.ts.map +1 -0
  95. package/dist/proof/step-runner.js +234 -0
  96. package/dist/public/evidence-levels.d.ts +12 -0
  97. package/dist/public/evidence-levels.d.ts.map +1 -0
  98. package/dist/public/evidence-levels.js +19 -0
  99. package/dist/public/impact/coverage-depth-quality.d.ts +7 -0
  100. package/dist/public/impact/coverage-depth-quality.d.ts.map +1 -1
  101. package/dist/public/impact/coverage-depth-quality.js +59 -0
  102. package/dist/public/impact/full-kb-quality.d.ts.map +1 -1
  103. package/dist/public/impact/full-kb-quality.js +3 -1
  104. package/dist/public/operations/catalog.d.ts +167 -6
  105. package/dist/public/operations/catalog.d.ts.map +1 -1
  106. package/dist/public/operations/check-executor.d.ts +2 -0
  107. package/dist/public/operations/check-executor.d.ts.map +1 -1
  108. package/dist/public/operations/check-executor.js +6 -0
  109. package/dist/public/operations/evidence-floor.d.ts +48 -0
  110. package/dist/public/operations/evidence-floor.d.ts.map +1 -0
  111. package/dist/public/operations/evidence-floor.js +143 -0
  112. package/dist/public/operations/migration-plan.d.ts +19 -5
  113. package/dist/public/operations/migration-plan.d.ts.map +1 -1
  114. package/dist/public/operations/migration-plan.js +22 -5
  115. package/dist/public/operations/node-ports.d.ts.map +1 -1
  116. package/dist/public/operations/node-ports.js +4 -1
  117. package/dist/public/operations/proof-contract-claims.d.ts +26 -0
  118. package/dist/public/operations/proof-contract-claims.d.ts.map +1 -0
  119. package/dist/public/operations/proof-contract-claims.js +71 -0
  120. package/dist/public/operations/specs/check-rules.generated.d.ts +1 -1
  121. package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
  122. package/dist/public/operations/specs/check-rules.generated.js +3 -1
  123. package/dist/public/operations/specs/check.d.ts +1 -1
  124. package/dist/public/operations/specs/mutation-schemas.d.ts +157 -5
  125. package/dist/public/operations/specs/mutation-schemas.d.ts.map +1 -1
  126. package/dist/public/operations/specs/mutation-schemas.js +15 -2
  127. package/dist/public/operations/specs/proof.d.ts +9 -0
  128. package/dist/public/operations/specs/proof.d.ts.map +1 -1
  129. package/dist/public/operations/specs/reporting.d.ts.map +1 -1
  130. package/dist/public/operations/specs/reporting.js +19 -10
  131. package/dist/public/operations/specs/upsert.d.ts +157 -5
  132. package/dist/public/operations/specs/upsert.d.ts.map +1 -1
  133. package/dist/public/proof-certificates.d.ts +89 -0
  134. package/dist/public/proof-certificates.d.ts.map +1 -0
  135. package/dist/public/proof-certificates.js +207 -0
  136. package/dist/public/proof-protocol.d.ts +126 -3
  137. package/dist/public/proof-protocol.d.ts.map +1 -1
  138. package/dist/public/proof-protocol.js +239 -3
  139. package/dist/public/proof-receipt.d.ts +89 -6
  140. package/dist/public/proof-receipt.d.ts.map +1 -1
  141. package/dist/public/proof-receipt.js +83 -7
  142. package/dist/public/schemas/entity.d.ts.map +1 -1
  143. package/dist/public/schemas/entity.js +18 -1
  144. package/dist/public/skills/kibi-bootstrap/SKILL.md +2 -2
  145. package/dist/public/skills/kibi-commit-review/SKILL.md +1 -1
  146. package/dist/public/skills/kibi-commit-review/resources/staged-gate.md +6 -1
  147. package/dist/public/skills/kibi-decisions/SKILL.md +10 -2
  148. package/dist/public/skills/kibi-decisions/resources/decision-records.md +6 -1
  149. package/dist/public/skills/kibi-freshness/SKILL.md +5 -1
  150. package/dist/public/skills/kibi-traceability/SKILL.md +11 -1
  151. package/dist/public/skills/kibi-usage/SKILL.md +6 -4
  152. package/dist/public/skills/kibi-usage/resources/proof.md +124 -19
  153. package/dist/public/skills/kibi-usage/resources/workflows.md +4 -4
  154. package/dist/report/html-report.css +38 -1
  155. package/dist/report/html-report.d.ts +8 -0
  156. package/dist/report/html-report.d.ts.map +1 -1
  157. package/dist/report/html-report.js +141 -6
  158. package/dist/report/report-view.d.ts +1 -1
  159. package/dist/report/report-view.d.ts.map +1 -1
  160. package/dist/report/report-view.js +7 -0
  161. package/dist/schemas/entity.schema.json +212 -4
  162. package/dist/search-answer.d.ts.map +1 -1
  163. package/dist/search-answer.js +1 -0
  164. package/dist/signoff/html.d.ts.map +1 -1
  165. package/dist/signoff/html.js +3 -0
  166. package/dist/signoff/markdown.d.ts.map +1 -1
  167. package/dist/signoff/markdown.js +3 -0
  168. package/dist/signoff/model.d.ts +20 -0
  169. package/dist/signoff/model.d.ts.map +1 -1
  170. package/dist/signoff/model.js +20 -1
  171. package/dist/traceability/impact-knowledge.d.ts.map +1 -1
  172. package/dist/traceability/impact-knowledge.js +1 -0
  173. package/dist/traceability/temp-kb.d.ts.map +1 -1
  174. package/dist/traceability/temp-kb.js +1 -0
  175. package/dist/types/entities.d.ts +4 -1
  176. package/dist/types/entities.d.ts.map +1 -1
  177. package/dist/utils/merge-driver-setup.d.ts +8 -1
  178. package/dist/utils/merge-driver-setup.d.ts.map +1 -1
  179. package/dist/utils/merge-driver-setup.js +23 -2
  180. package/dist/utils/rule-registry.generated.d.ts +10 -0
  181. package/dist/utils/rule-registry.generated.d.ts.map +1 -1
  182. package/dist/utils/rule-registry.generated.js +12 -0
  183. package/dist/utils/schema-version.d.ts +1 -1
  184. package/dist/utils/schema-version.d.ts.map +1 -1
  185. package/dist/utils/schema-version.js +5 -1
  186. package/package.json +6 -2
  187. package/schema/entities.pl +6 -0
  188. package/schema/validation.pl +4 -1
  189. package/src/public/evidence-levels.ts +23 -0
  190. package/src/public/impact/coverage-depth-quality.ts +73 -0
  191. package/src/public/impact/full-kb-quality.ts +6 -1
  192. package/src/public/operations/check-executor.ts +13 -0
  193. package/src/public/operations/evidence-floor.ts +203 -0
  194. package/src/public/operations/migration-plan.ts +39 -6
  195. package/src/public/operations/node-ports.ts +3 -1
  196. package/src/public/operations/proof-contract-claims.ts +103 -0
  197. package/src/public/operations/specs/check-rules.generated.ts +3 -1
  198. package/src/public/operations/specs/mutation-schemas.ts +17 -2
  199. package/src/public/operations/specs/reporting.ts +20 -10
  200. package/src/public/proof-certificates.ts +271 -0
  201. package/src/public/proof-protocol-types.d.ts +17 -1
  202. package/src/public/proof-protocol.ts +318 -3
  203. package/src/public/proof-receipt.ts +127 -7
  204. package/src/public/schemas/entity.ts +19 -1
  205. package/src/public/skills/kibi-bootstrap/SKILL.md +2 -2
  206. package/src/public/skills/kibi-commit-review/SKILL.md +1 -1
  207. package/src/public/skills/kibi-commit-review/resources/staged-gate.md +6 -1
  208. package/src/public/skills/kibi-decisions/SKILL.md +10 -2
  209. package/src/public/skills/kibi-decisions/resources/decision-records.md +6 -1
  210. package/src/public/skills/kibi-freshness/SKILL.md +5 -1
  211. package/src/public/skills/kibi-traceability/SKILL.md +11 -1
  212. package/src/public/skills/kibi-usage/SKILL.md +6 -4
  213. package/src/public/skills/kibi-usage/resources/proof.md +124 -19
  214. package/src/public/skills/kibi-usage/resources/workflows.md +4 -4
  215. package/src/schemas/entity.schema.json +212 -4
  216. package/dist/commands/proof-impact.d.ts +0 -7
  217. package/dist/commands/proof-impact.d.ts.map +0 -1
  218. package/dist/commands/proof-impact.js +0 -39
  219. package/dist/operations/proof/baseline-diff.d.ts +0 -41
  220. package/dist/operations/proof/baseline-diff.d.ts.map +0 -1
  221. package/dist/operations/proof/baseline-diff.js +0 -153
  222. package/dist/operations/proof/impact.d.ts +0 -51
  223. package/dist/operations/proof/impact.d.ts.map +0 -1
  224. package/dist/operations/proof/impact.js +0 -138
@@ -0,0 +1,785 @@
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 { certificateDigest } from "../../public/proof-certificates.js";
26
+ import { CODE_CERTIFICATE_VERSION, isCertificateProofKind, } from "../../public/proof-protocol.js";
27
+ function record(value) {
28
+ return value !== null && typeof value === "object" && !Array.isArray(value)
29
+ ? value
30
+ : null;
31
+ }
32
+ function text(value) {
33
+ return typeof value === "string" ? value : "";
34
+ }
35
+ export async function loadSymbol(context, symbolId) {
36
+ if (context.prolog === undefined)
37
+ return null;
38
+ const [row] = await loadEntities(context.prolog, {
39
+ id: symbolId,
40
+ type: "symbol",
41
+ });
42
+ if (!row)
43
+ return null;
44
+ const sourceFile = text(row.sourceFile) || text(row.source_file);
45
+ return { id: symbolId, title: text(row.title), sourceFile };
46
+ }
47
+ export function readWorkspaceFile(workspaceRoot, relative) {
48
+ const absolute = path.resolve(workspaceRoot, relative);
49
+ if (!absolute.startsWith(path.resolve(workspaceRoot) + path.sep))
50
+ return null;
51
+ return existsSync(absolute) ? readFileSync(absolute, "utf8") : null;
52
+ }
53
+ const IMPORT_SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s+|\brequire\s*\(\s*)["']([^"']+)["']/g;
54
+ /**
55
+ * Whether `source` (at `sourcePath`) imports `targetPath` and names
56
+ * `identifier`: the domain is read from the code, not hand-listed. A suite
57
+ * that tests compiled output imports the domain from the TypeScript
58
+ * project's `outDir`; given the workspace root, such an import counts when it
59
+ * maps back to the domain's source file under `rootDir`.
60
+ */
61
+ // implements REQ-kibi-code-proof-kinds
62
+ export function importsDomain(source, sourcePath, targetPath, identifier, workspaceRoot) {
63
+ if (identifier === "" || !new RegExp(`\\b${escapeRegExp(identifier)}\\b`).test(source))
64
+ return false;
65
+ const target = stripExtension(path.normalize(targetPath));
66
+ if (/\.py$/i.test(sourcePath))
67
+ return pythonImportsModule(source, sourcePath, targetPath);
68
+ for (const match of source.matchAll(IMPORT_SPECIFIER)) {
69
+ const specifier = match[1] ?? "";
70
+ if (!specifier.startsWith("."))
71
+ continue;
72
+ const joined = path.normalize(path.join(path.dirname(sourcePath), specifier));
73
+ if (stripExtension(joined) === target)
74
+ return true;
75
+ if (workspaceRoot === undefined)
76
+ continue;
77
+ const mapped = compiledToSourcePath(workspaceRoot, joined);
78
+ if (mapped !== null && stripExtension(mapped) === target)
79
+ return true;
80
+ }
81
+ return false;
82
+ }
83
+ const PYTHON_FROM_IMPORT = /^\s*from\s+(\.*)([\w.]*)\s+import\b/gm;
84
+ const PYTHON_IMPORT = /^\s*import\s+([\w.]+(?:\s*,\s*[\w.]+)*)/gm;
85
+ /**
86
+ * Whether a Python proof imports the module at `targetPath`: a relative
87
+ * `from .mod import X` resolved from the proof's package, or an absolute
88
+ * `from pkg.mod import X` / `import pkg.mod` whose dotted path names the
89
+ * file's trailing path segments (src layouts and packages alike).
90
+ */
91
+ function pythonImportsModule(source, sourcePath, targetPath) {
92
+ const target = path
93
+ .normalize(targetPath)
94
+ .replace(/\.pyi?$/i, "")
95
+ .replace(/[\\/]__init__$/, "");
96
+ const matchesDotted = (dotted) => {
97
+ if (dotted === "")
98
+ return false;
99
+ const tail = dotted.split(".").join(path.sep);
100
+ return target === tail || target.endsWith(`${path.sep}${tail}`);
101
+ };
102
+ for (const match of source.matchAll(PYTHON_FROM_IMPORT)) {
103
+ const dots = match[1] ?? "";
104
+ const module = match[2] ?? "";
105
+ if (dots === "") {
106
+ if (matchesDotted(module))
107
+ return true;
108
+ continue;
109
+ }
110
+ let base = path.dirname(path.normalize(sourcePath));
111
+ for (let level = 1; level < dots.length; level++)
112
+ base = path.dirname(base);
113
+ const resolved = path.join(base, ...module.split(".").filter(Boolean));
114
+ if (resolved === target)
115
+ return true;
116
+ }
117
+ for (const match of source.matchAll(PYTHON_IMPORT))
118
+ for (const dotted of (match[1] ?? "").split(","))
119
+ if (matchesDotted(dotted.trim()))
120
+ return true;
121
+ return false;
122
+ }
123
+ /** Strip `//` and block comments and trailing commas: tsconfig is JSONC. */
124
+ function parseJsonc(text) {
125
+ let out = "";
126
+ let inString = false;
127
+ for (let index = 0; index < text.length; index++) {
128
+ const char = text[index];
129
+ const next = text[index + 1];
130
+ if (inString) {
131
+ out += char;
132
+ if (char === "\\") {
133
+ out += next ?? "";
134
+ index++;
135
+ }
136
+ else if (char === '"')
137
+ inString = false;
138
+ continue;
139
+ }
140
+ if (char === '"') {
141
+ inString = true;
142
+ out += char;
143
+ }
144
+ else if (char === "/" && next === "/") {
145
+ while (index < text.length && text[index] !== "\n")
146
+ index++;
147
+ out += "\n";
148
+ }
149
+ else if (char === "/" && next === "*") {
150
+ index += 2;
151
+ while (index < text.length && !(text[index] === "*" && text[index + 1] === "/"))
152
+ index++;
153
+ index++;
154
+ }
155
+ else
156
+ out += char;
157
+ }
158
+ return JSON.parse(out.replace(/,(\s*[}\]])/g, "$1"));
159
+ }
160
+ /**
161
+ * Map a workspace-relative path inside a TypeScript project's `outDir` to the
162
+ * matching path under its `rootDir` (the tsconfig's directory when unset),
163
+ * using the nearest tsconfig.json above the path. Null when no tsconfig
164
+ * declares an `outDir` that contains it.
165
+ */
166
+ // implements REQ-kibi-code-proof-kinds
167
+ export function compiledToSourcePath(workspaceRoot, relativePath) {
168
+ const absolute = path.resolve(workspaceRoot, relativePath);
169
+ let dir = path.dirname(absolute);
170
+ const root = path.resolve(workspaceRoot);
171
+ for (;;) {
172
+ const config = path.join(dir, "tsconfig.json");
173
+ if (existsSync(config)) {
174
+ try {
175
+ const options = record(record(parseJsonc(readFileSync(config, "utf8")))?.compilerOptions);
176
+ const outDir = text(options?.outDir);
177
+ if (outDir !== "") {
178
+ const out = path.resolve(dir, outDir);
179
+ const rel = path.relative(out, absolute);
180
+ if (rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel)) {
181
+ const rootDir = path.resolve(dir, text(options?.rootDir) || ".");
182
+ return path.relative(root, path.join(rootDir, rel));
183
+ }
184
+ }
185
+ }
186
+ catch {
187
+ // An unreadable tsconfig maps nothing.
188
+ }
189
+ }
190
+ if (dir === root || path.dirname(dir) === dir)
191
+ return null;
192
+ dir = path.dirname(dir);
193
+ }
194
+ }
195
+ function stripExtension(file) {
196
+ return file.replace(/\.(?:d\.ts|[cm]?[jt]sx?)$/, "");
197
+ }
198
+ function escapeRegExp(value) {
199
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
200
+ }
201
+ // Node's type stripping resolves only exact specifiers. TypeScript apps
202
+ // import relative modules without an extension, or with `.js` for a `.ts`
203
+ // file; this resolve hook retries those as the TypeScript files they name.
204
+ const TS_RESOLVE_HOOKS = `
205
+ const SUFFIXES = [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", "/index.ts", "/index.tsx", "/index.js"];
206
+ export async function resolve(specifier, context, next) {
207
+ try {
208
+ return await next(specifier, context);
209
+ } catch (error) {
210
+ if (error?.code !== "ERR_MODULE_NOT_FOUND" || !/^\\.\\.?\\//.test(specifier)) throw error;
211
+ const base = specifier.replace(/\\.[cm]?js$/, "");
212
+ for (const suffix of SUFFIXES) {
213
+ try {
214
+ return await next(base + suffix, context);
215
+ } catch {}
216
+ }
217
+ throw error;
218
+ }
219
+ }
220
+ `;
221
+ const DOMAIN_EVALUATOR = `
222
+ const [file, name] = process.argv.slice(-2);
223
+ const { pathToFileURL } = await import("node:url");
224
+ if (typeof globalThis.Bun === "undefined") {
225
+ const { register } = await import("node:module");
226
+ register?.("data:text/javascript," + encodeURIComponent(${JSON.stringify(TS_RESOLVE_HOOKS)}));
227
+ }
228
+ const value = (await import(pathToFileURL(file).href))[name];
229
+ let items = null;
230
+ if (Array.isArray(value)) items = value;
231
+ else if (value instanceof Set) items = [...value];
232
+ else if (value instanceof Map) items = [...value.keys()];
233
+ else if (value !== null && typeof value === "object") items = Object.keys(value);
234
+ const primitive = (item) => ["string", "number", "boolean", "bigint"].includes(typeof item);
235
+ const scalar = typeof value === "number" || typeof value === "bigint" ? Number(value) : null;
236
+ console.log(JSON.stringify(scalar !== null
237
+ ? { scalar }
238
+ : items === null
239
+ ? { error: \`\${name} is not an array, set, map or object\` }
240
+ : { count: items.length, members: items.every(primitive) ? items.map(String) : null }));
241
+ `;
242
+ // A Python domain: a list, tuple, set, frozenset, dict or Enum class.
243
+ const PYTHON_DOMAIN_EVALUATOR = `
244
+ import enum, importlib, json, os, sys
245
+ file, name = sys.argv[-2], sys.argv[-1]
246
+ # Import the module under its package name, from the directory above its
247
+ # topmost package, so absolute and relative imports inside it resolve.
248
+ directory, module_name = os.path.split(os.path.splitext(file)[0])
249
+ parts = [] if module_name == "__init__" else [module_name]
250
+ while os.path.exists(os.path.join(directory, "__init__.py")):
251
+ directory, package = os.path.split(directory)
252
+ parts.insert(0, package)
253
+ sys.path.insert(0, directory)
254
+ module = importlib.import_module(".".join(parts))
255
+ value = getattr(module, name, None)
256
+ if isinstance(value, type) and issubclass(value, enum.Enum):
257
+ items = [member.value for member in value]
258
+ elif isinstance(value, dict):
259
+ items = list(value.keys())
260
+ elif isinstance(value, (list, tuple, set, frozenset)):
261
+ items = list(value)
262
+ else:
263
+ items = None
264
+ def primitive(item):
265
+ return isinstance(item, (str, int, float, bool))
266
+ def text(item):
267
+ if isinstance(item, bool):
268
+ return "true" if item else "false"
269
+ return str(item)
270
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
271
+ print(json.dumps({"scalar": value}))
272
+ elif items is None:
273
+ print(json.dumps({"error": f"{name} is not a list, tuple, set, dict or Enum"}))
274
+ else:
275
+ print(json.dumps({"count": len(items), "members": [text(i) for i in items] if all(primitive(i) for i in items) else None}))
276
+ `;
277
+ function workspaceBin(workspaceRoot, name) {
278
+ let dir = path.resolve(workspaceRoot);
279
+ for (;;) {
280
+ const candidate = path.join(dir, "node_modules", ".bin", name);
281
+ if (existsSync(candidate))
282
+ return candidate;
283
+ if (path.dirname(dir) === dir)
284
+ return null;
285
+ dir = path.dirname(dir);
286
+ }
287
+ }
288
+ /**
289
+ * The Python interpreters a domain loads with, most specific first: the
290
+ * KIBI_PYTHON override, the active virtualenv, a project virtualenv (`.venv`
291
+ * or `venv`) between the domain file and the workspace root, then python3
292
+ * and python on PATH. The system interpreter usually cannot import the
293
+ * project's dependencies, so a project environment comes before it.
294
+ */
295
+ // implements REQ-kibi-code-proof-kinds
296
+ export function pythonDomainInterpreters(workspaceRoot, sourceFile, env = process.env) {
297
+ const interpreters = [];
298
+ const add = (candidate) => {
299
+ if (candidate && !interpreters.includes(candidate))
300
+ interpreters.push(candidate);
301
+ };
302
+ add(env.KIBI_PYTHON?.trim() || undefined);
303
+ const venvPython = (venv) => [
304
+ path.join(venv, "bin", "python"),
305
+ path.join(venv, "Scripts", "python.exe"),
306
+ ].find((candidate) => existsSync(candidate));
307
+ if (env.VIRTUAL_ENV)
308
+ add(venvPython(env.VIRTUAL_ENV));
309
+ const root = path.resolve(workspaceRoot);
310
+ let dir = path.dirname(path.resolve(root, sourceFile));
311
+ for (;;) {
312
+ for (const name of [".venv", "venv"])
313
+ add(venvPython(path.join(dir, name)));
314
+ if (dir === root || path.dirname(dir) === dir)
315
+ break;
316
+ if (!dir.startsWith(`${root}${path.sep}`))
317
+ break;
318
+ dir = path.dirname(dir);
319
+ }
320
+ add("python3");
321
+ add("python");
322
+ return interpreters;
323
+ }
324
+ /** The line of a runner's stderr that names the failure. */
325
+ function failureLine(stderr, python) {
326
+ const lines = stderr
327
+ .trim()
328
+ .split("\n")
329
+ .map((line) => line.trim())
330
+ .filter((line) => line !== "");
331
+ // A Python traceback names the exception on its last line.
332
+ return (python ? lines.at(-1) : lines[0]) ?? "";
333
+ }
334
+ /**
335
+ * The domain as the code defines it: Kibi imports the domain_source file
336
+ * itself and reads the named export's members, so a certificate cannot cover
337
+ * a hand-listed subset. A TypeScript or JavaScript domain loads with bun when
338
+ * it is on PATH, else with the workspace's tsx, else with node's type
339
+ * stripping (which retries extensionless relative imports as TypeScript
340
+ * files); a Python domain (`.py`) loads with the first interpreter
341
+ * pythonDomainInterpreters finds.
342
+ */
343
+ // implements REQ-kibi-code-proof-kinds
344
+ export function evaluateDomain(workspaceRoot, sourceFile, exportName, options = {}) {
345
+ const evaluated = evaluateExport(workspaceRoot, sourceFile, exportName, options);
346
+ if (!evaluated.ok)
347
+ return evaluated;
348
+ if (evaluated.scalar !== undefined)
349
+ return {
350
+ ok: false,
351
+ reason: `${exportName} is a number, not a list, set, map or object`,
352
+ };
353
+ return { ok: true, count: evaluated.count, members: evaluated.members };
354
+ }
355
+ /**
356
+ * The number a value_source export holds, read the same way as a domain.
357
+ */
358
+ // implements REQ-kibi-claim-bound-proofs
359
+ export function evaluateScalar(workspaceRoot, sourceFile, exportName) {
360
+ const evaluated = evaluateExport(workspaceRoot, sourceFile, exportName);
361
+ if (!evaluated.ok)
362
+ return evaluated;
363
+ if (evaluated.scalar === undefined || !Number.isFinite(evaluated.scalar))
364
+ return { ok: false, reason: `${exportName} in ${sourceFile} is not a finite number` };
365
+ return { ok: true, value: evaluated.scalar };
366
+ }
367
+ function evaluateExport(workspaceRoot, sourceFile, exportName, options = {}) {
368
+ const absolute = path.resolve(workspaceRoot, sourceFile);
369
+ const python = /\.py$/i.test(sourceFile);
370
+ const tsx = python ? null : workspaceBin(workspaceRoot, "tsx");
371
+ const runners = python
372
+ ? pythonDomainInterpreters(workspaceRoot, sourceFile).map((interpreter) => [
373
+ interpreter,
374
+ ["-c", PYTHON_DOMAIN_EVALUATOR],
375
+ ])
376
+ : (options.runners ?? ["bun", "tsx", "node"]).flatMap((runner) => {
377
+ if (runner === "bun")
378
+ return [["bun", ["-e", DOMAIN_EVALUATOR]]];
379
+ if (runner === "tsx")
380
+ return tsx === null
381
+ ? []
382
+ : [[tsx, ["--eval", DOMAIN_EVALUATOR, "--"]]];
383
+ return [
384
+ [
385
+ process.execPath,
386
+ [
387
+ "--experimental-strip-types",
388
+ "--no-warnings",
389
+ "--input-type=module",
390
+ "-e",
391
+ DOMAIN_EVALUATOR,
392
+ ],
393
+ ],
394
+ ];
395
+ });
396
+ let failure = "";
397
+ for (const [command, argv] of runners) {
398
+ const run = spawnSync(command, [...argv, absolute, exportName], {
399
+ cwd: workspaceRoot,
400
+ encoding: "utf8",
401
+ timeout: 60_000,
402
+ });
403
+ if (run.error)
404
+ continue;
405
+ const line = (run.stdout ?? "").trim().split("\n").pop() ?? "";
406
+ try {
407
+ const parsed = JSON.parse(line);
408
+ if (parsed.error !== undefined)
409
+ return { ok: false, reason: parsed.error };
410
+ if (typeof parsed.scalar === "number")
411
+ return { ok: true, count: 0, members: null, scalar: parsed.scalar };
412
+ if (typeof parsed.count === "number")
413
+ return { ok: true, count: parsed.count, members: parsed.members ?? null };
414
+ }
415
+ catch {
416
+ // Fall through to the next runner.
417
+ }
418
+ failure = failureLine(run.stderr ?? "", python);
419
+ }
420
+ return {
421
+ ok: false,
422
+ reason: `Kibi could not load ${exportName} from ${sourceFile}${failure ? `: ${failure}` : ""}${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)"}`,
423
+ };
424
+ }
425
+ /**
426
+ * Whether an exhaustive certificate covers the domain Kibi evaluated: the
427
+ * same members when they are primitive values, else the same count.
428
+ */
429
+ // implements REQ-kibi-code-proof-kinds
430
+ export function domainCoverageProblem(exportName, evaluated, certified) {
431
+ if (evaluated.members !== null) {
432
+ const expected = new Set(evaluated.members);
433
+ const covered = new Set(certified);
434
+ const missing = [...expected].filter((member) => !covered.has(member));
435
+ const extra = [...covered].filter((member) => !expected.has(member));
436
+ if (missing.length === 0 && extra.length === 0)
437
+ return null;
438
+ return `the certificate does not cover ${exportName} as the code defines it (${[
439
+ missing.length > 0 ? `missing ${missing.slice(0, 5).join(", ")}` : "",
440
+ extra.length > 0 ? `not in the domain ${extra.slice(0, 5).join(", ")}` : "",
441
+ ]
442
+ .filter(Boolean)
443
+ .join("; ")})`;
444
+ }
445
+ return certified.length === evaluated.count
446
+ ? null
447
+ : `the certificate covers ${certified.length} members but ${exportName} has ${evaluated.count}`;
448
+ }
449
+ /**
450
+ * Kibi compiles a typecheck witness itself, with the workspace's TypeScript
451
+ * and fixed strict options (a lax project tsconfig could make the never
452
+ * check vacuous), and accepts it only with no errors in the witness.
453
+ */
454
+ // implements REQ-kibi-code-proof-kinds
455
+ export function compileWitness(workspaceRoot, witnessPath) {
456
+ let ts;
457
+ try {
458
+ ts = createRequire(path.join(workspaceRoot, "package.json"))("typescript");
459
+ }
460
+ catch {
461
+ return {
462
+ ok: false,
463
+ reason: "Kibi could not load TypeScript from the workspace to compile the witness (install typescript)",
464
+ };
465
+ }
466
+ const absolute = path.resolve(workspaceRoot, witnessPath);
467
+ const program = ts.createProgram([absolute], {
468
+ strict: true,
469
+ noEmit: true,
470
+ skipLibCheck: true,
471
+ allowJs: true,
472
+ checkJs: true,
473
+ target: ts.ScriptTarget.ES2022,
474
+ module: ts.ModuleKind.NodeNext,
475
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
476
+ types: [],
477
+ });
478
+ const witness = program.getSourceFile(absolute);
479
+ if (witness === undefined)
480
+ return { ok: false, reason: `Kibi could not read the witness ${witnessPath}` };
481
+ const errors = [
482
+ ...program.getSyntacticDiagnostics(witness),
483
+ ...program.getSemanticDiagnostics(witness),
484
+ ].filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
485
+ if (errors.length > 0)
486
+ return {
487
+ ok: false,
488
+ reason: `TypeScript ${ts.version} rejects the witness ${witnessPath}: ${ts.flattenDiagnosticMessageText(errors[0]?.messageText ?? "", " ")}`,
489
+ };
490
+ return { ok: true, version: ts.version };
491
+ }
492
+ /**
493
+ * The constraints the contract's constraint_source facts state on one
494
+ * numeric property, or why they cannot ground a certificate.
495
+ */
496
+ async function factConstraints(context, factIds) {
497
+ if (context.prolog === undefined)
498
+ return { ok: false, reason: "no Prolog runtime is attached to read the facts" };
499
+ const constraints = [];
500
+ const subjects = new Set();
501
+ const valueTypes = new Set();
502
+ for (const id of factIds) {
503
+ const [fact] = await loadEntities(context.prolog, { id, type: "fact" });
504
+ if (!fact)
505
+ return { ok: false, reason: `constraint_source fact ${id} does not exist` };
506
+ if (text(fact.status) !== "active")
507
+ return { ok: false, reason: `constraint_source fact ${id} is not active` };
508
+ const polarity = text(fact.polarity);
509
+ if (text(fact.fact_kind) !== "property_value" || (polarity !== "" && polarity !== "require"))
510
+ return {
511
+ ok: false,
512
+ reason: `constraint_source fact ${id} is not a required property_value fact`,
513
+ };
514
+ const valueType = text(fact.value_type);
515
+ const value = valueType === "int" ? fact.value_int : valueType === "number" ? fact.value_number : undefined;
516
+ const operator = text(fact.operator);
517
+ if (typeof value !== "number" || !["eq", "neq", "lt", "lte", "gt", "gte"].includes(operator))
518
+ return {
519
+ ok: false,
520
+ reason: `constraint_source fact ${id} has no numeric operator and value`,
521
+ };
522
+ subjects.add(`${text(fact.subject_key)}\0${text(fact.property_key)}`);
523
+ valueTypes.add(valueType);
524
+ constraints.push({ operator, value });
525
+ }
526
+ if (subjects.size !== 1)
527
+ return {
528
+ ok: false,
529
+ reason: "the constraint_source facts are about more than one subject and property",
530
+ };
531
+ const [subject = ""] = [...subjects];
532
+ return {
533
+ ok: true,
534
+ property: subject.split("\0")[1] ?? "",
535
+ valueType: valueTypes.has("number") ? "real" : "int",
536
+ constraints,
537
+ };
538
+ }
539
+ function constraintKey(row) {
540
+ return `${text(row.operator)}:${String(row.value)}`;
541
+ }
542
+ const SMT_OPERATORS = {
543
+ eq: "=",
544
+ lt: "<",
545
+ lte: "<=",
546
+ gt: ">",
547
+ gte: ">=",
548
+ };
549
+ function smtNumber(value, valueType) {
550
+ const magnitude = valueType === "real" && Number.isInteger(value)
551
+ ? `${Math.abs(value)}.0`
552
+ : String(Math.abs(value));
553
+ return value < 0 ? `(- ${magnitude})` : magnitude;
554
+ }
555
+ /** The SMT-LIB assertion a fact constraint must appear as in the query. */
556
+ export function smtAssertion(property, valueType, row) {
557
+ const value = smtNumber(row.value, valueType);
558
+ return row.operator === "neq"
559
+ ? `(assert (distinct ${property} ${value}))`
560
+ : `(assert (${SMT_OPERATORS[row.operator]} ${property} ${value}))`;
561
+ }
562
+ /**
563
+ * The values a contract's value_source exports hold, read from the code the
564
+ * same way as a domain, or why one cannot be read.
565
+ */
566
+ // implements REQ-kibi-claim-bound-proofs
567
+ export async function valueSourceValues(context, contract) {
568
+ const values = [];
569
+ for (const source of contract.value_source ?? []) {
570
+ const symbol = await loadSymbol(context, source.symbol_id);
571
+ if (!symbol || symbol.sourceFile === "")
572
+ return {
573
+ ok: false,
574
+ reason: `the value_source symbol ${source.symbol_id} has no source file`,
575
+ };
576
+ const evaluated = evaluateScalar(context.workspaceRoot, symbol.sourceFile, source.export ?? symbol.title);
577
+ if (!evaluated.ok)
578
+ return evaluated;
579
+ values.push({ symbol_id: source.symbol_id, value: evaluated.value });
580
+ }
581
+ return { ok: true, values };
582
+ }
583
+ /**
584
+ * arithmetic and smt certificates must rest on the knowledge base and the
585
+ * code: the arithmetic assumptions are exactly the constraint_source facts'
586
+ * constraints plus one `eq` per value_source constant, on a variable named
587
+ * after the fact property, and an smt query declares that constant and
588
+ * asserts each of them.
589
+ */
590
+ async function constraintSourceProblem(context, contract, certificate) {
591
+ const factIds = contract.constraint_source?.fact_ids ?? [];
592
+ if (factIds.length === 0 && (contract.value_source ?? []).length === 0)
593
+ return `the contract names no constraint_source facts or value_source constants, so the ${contract.kind} certificate proves numbers the test chose`;
594
+ const facts = factIds.length === 0
595
+ ? null
596
+ : await factConstraints(context, factIds);
597
+ if (facts !== null && !facts.ok)
598
+ return facts.reason;
599
+ const sources = await valueSourceValues(context, contract);
600
+ if (!sources.ok)
601
+ return sources.reason;
602
+ const property = facts !== null && facts.ok ? facts.property : text(certificate.variable);
603
+ const valueType = facts !== null && facts.ok
604
+ ? facts.valueType
605
+ : text(certificate.value_type) === "real" ||
606
+ sources.values.some((row) => !Number.isInteger(row.value))
607
+ ? "real"
608
+ : "int";
609
+ const constraints = [
610
+ ...(facts !== null && facts.ok ? facts.constraints : []),
611
+ ...sources.values.map((row) => ({ operator: "eq", value: row.value })),
612
+ ];
613
+ if (contract.kind === "arithmetic") {
614
+ if (text(certificate.variable) !== property)
615
+ return `the certificate's variable ${text(certificate.variable) || "(none)"} is not the facts' property ${property}`;
616
+ if (text(certificate.value_type) !== valueType)
617
+ return `the certificate's value_type ${text(certificate.value_type)} is not the facts' ${valueType}`;
618
+ const assumptions = Array.isArray(certificate.assumptions)
619
+ ? certificate.assumptions.map((row) => constraintKey(record(row) ?? {}))
620
+ : [];
621
+ const expected = constraints.map(constraintKey);
622
+ const same = assumptions.length === expected.length &&
623
+ [...assumptions].sort().join("|") === [...expected].sort().join("|");
624
+ if (!same)
625
+ return `the certificate's assumptions are not exactly the constraint_source facts' constraints and value_source values (${expected.join(", ")})`;
626
+ return null;
627
+ }
628
+ const query = text(certificate.query).replace(/\s+/g, " ");
629
+ const sort = valueType === "int" ? "Int" : "Real";
630
+ if (property === "")
631
+ return "the smt contract names no constraint_source facts, so Kibi cannot tell which constant the query is about";
632
+ if (!query.includes(`(declare-const ${property} ${sort})`))
633
+ return `the query does not declare (declare-const ${property} ${sort}) for the facts' property`;
634
+ for (const row of constraints) {
635
+ const assertion = smtAssertion(property, valueType, row);
636
+ if (!query.includes(assertion))
637
+ return `the query does not assert the constraint_source or value_source constraint ${assertion}`;
638
+ }
639
+ return null;
640
+ }
641
+ /** The solver re-run behind an smt certificate; unsat is the only proof. */
642
+ // implements REQ-kibi-code-proof-kinds
643
+ export function rerunSmtQuery(solver, query) {
644
+ const argv = solver === "z3"
645
+ ? ["-smt2", "-in"]
646
+ : solver === "cvc5"
647
+ ? ["--lang", "smt2"]
648
+ : null;
649
+ if (argv === null)
650
+ return { ok: false, reason: `solver ${solver} is not z3 or cvc5` };
651
+ const run = spawnSync(solver, argv, {
652
+ input: query,
653
+ encoding: "utf8",
654
+ timeout: 60_000,
655
+ });
656
+ if (run.error)
657
+ return {
658
+ ok: false,
659
+ reason: `Kibi could not re-run ${solver} on the query: ${run.error.message}`,
660
+ };
661
+ const answer = (run.stdout ?? "").trim().split(/\s+/)[0] ?? "";
662
+ return answer === "unsat"
663
+ ? { ok: true }
664
+ : {
665
+ ok: false,
666
+ reason: `re-running ${solver} on the query answered ${answer || "nothing"}, not unsat`,
667
+ };
668
+ }
669
+ async function workspaceProblem(context, contract, certificate) {
670
+ const kind = contract.kind;
671
+ if (kind === "exhaustive" || kind === "typecheck") {
672
+ const domainId = contract.domain_source?.symbol_id ?? "";
673
+ const named = text(record(certificate.domain)?.symbol_id);
674
+ if (named !== domainId)
675
+ return `the certificate's domain ${named || "(none)"} is not the contract's domain_source ${domainId}`;
676
+ const domain = await loadSymbol(context, domainId);
677
+ if (!domain || domain.sourceFile === "")
678
+ return `the domain_source symbol ${domainId} has no source file`;
679
+ const readers = kind === "typecheck"
680
+ ? [contract.witness?.path ?? ""]
681
+ : (await Promise.all(contract.required_proofs.map((proof) => loadSymbol(context, proof.symbol_id)))).map((symbol) => symbol?.sourceFile ?? "");
682
+ // The export Kibi reads: domain_source.export, else the symbol's title.
683
+ const exportName = contract.domain_source?.export ?? domain.title;
684
+ const importing = readers.some((reader) => {
685
+ if (reader === "")
686
+ return false;
687
+ const source = readWorkspaceFile(context.workspaceRoot, reader);
688
+ return (source !== null &&
689
+ importsDomain(source, reader, domain.sourceFile, exportName, context.workspaceRoot));
690
+ });
691
+ if (!importing)
692
+ 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)` : ""}`;
693
+ if (kind === "exhaustive") {
694
+ const evaluated = evaluateDomain(context.workspaceRoot, domain.sourceFile, exportName);
695
+ if (!evaluated.ok)
696
+ return evaluated.reason;
697
+ const members = record(certificate.domain)?.members;
698
+ const problem = domainCoverageProblem(exportName, evaluated, Array.isArray(members) ? members.map(String) : []);
699
+ if (problem !== null)
700
+ return problem;
701
+ }
702
+ if (kind === "typecheck") {
703
+ const witness = record(certificate.witness);
704
+ const witnessPath = contract.witness?.path ?? "";
705
+ if (text(witness?.path) !== witnessPath)
706
+ return `the certificate's witness is not the contract's witness ${witnessPath}`;
707
+ const content = readWorkspaceFile(context.workspaceRoot, witnessPath);
708
+ if (content === null)
709
+ return `the witness ${witnessPath} does not exist`;
710
+ const sha = createHash("sha256").update(content).digest("hex");
711
+ if (text(witness?.sha256) !== sha)
712
+ return `the witness sha256 does not match ${witnessPath} in the workspace`;
713
+ if (!/\bnever\b/.test(content))
714
+ return `the witness ${witnessPath} has no never check, so a new domain member would still compile`;
715
+ const compiled = compileWitness(context.workspaceRoot, witnessPath);
716
+ if (!compiled.ok)
717
+ return compiled.reason;
718
+ }
719
+ }
720
+ if (kind === "arithmetic" || kind === "smt") {
721
+ const problem = await constraintSourceProblem(context, contract, certificate);
722
+ if (problem !== null)
723
+ return problem;
724
+ }
725
+ if (kind === "smt") {
726
+ const solver = text(record(certificate.solver)?.name);
727
+ const rerun = rerunSmtQuery(solver, text(certificate.query));
728
+ if (!rerun.ok)
729
+ return rerun.reason;
730
+ }
731
+ return null;
732
+ }
733
+ /**
734
+ * Re-check the certificate a certificate-kind run left for one test. A
735
+ * runner kind (e2e, example, property) supports tested evidence and needs
736
+ * no certificate; a certificate kind supports proved evidence only when the
737
+ * certificate re-checks: its structure and arithmetic in code_certificates.pl,
738
+ * and here that the proof read its domain from the code, that the witness is
739
+ * the workspace file, and that the solver still answers unsat.
740
+ */
741
+ // implements REQ-kibi-code-proof-kinds
742
+ export async function checkCodeCertificate(context, input) {
743
+ const kind = input.contract.kind ?? "e2e";
744
+ if (!isCertificateProofKind(kind))
745
+ return { kind, evidence_level: "tested" };
746
+ const certificate = record(input.certificate);
747
+ if (!certificate)
748
+ return {
749
+ kind,
750
+ evidence_level: "tested",
751
+ certificate_rejection: `the run left no ${CODE_CERTIFICATE_VERSION} for ${input.testId}`,
752
+ };
753
+ const digest = certificateDigest(certificate);
754
+ const rejected = (reason) => ({
755
+ kind,
756
+ evidence_level: "tested",
757
+ certificate,
758
+ certificate_digest: digest,
759
+ certificate_rejection: reason,
760
+ });
761
+ if (text(certificate.kind) !== kind)
762
+ return rejected(`the certificate kind ${text(certificate.kind) || "(none)"} is not the contract kind ${kind}`);
763
+ if (text(certificate.test_id) !== input.testId)
764
+ return rejected(`the certificate is for ${text(certificate.test_id)}`);
765
+ if (input.outcome !== "passed")
766
+ return rejected("the proof run did not pass");
767
+ if (context.prolog === undefined)
768
+ return rejected("no Prolog runtime is attached to re-check the certificate");
769
+ const checked = await runOperationJsonQuery(context.prolog, "code_certificates.pl", `code_certificates:code_certificate_check_json(${toPrologAtom(JSON.stringify(certificate))}, JsonString)`, "Code certificate check");
770
+ if (checked.accepted !== true)
771
+ return rejected(checked.reason ?? "the certificate did not re-check");
772
+ // Every later re-check hashes the stored certificate in Prolog, so a
773
+ // certificate whose two canonical forms differ would silently lose proved.
774
+ if (checked.digest !== digest)
775
+ return rejected("the certificate does not hash the same in the CLI and in code_certificates.pl (write non-integer numbers as strings)");
776
+ const problem = await workspaceProblem(context, input.contract, certificate);
777
+ if (problem !== null)
778
+ return rejected(problem);
779
+ return {
780
+ kind,
781
+ evidence_level: "proved",
782
+ certificate,
783
+ certificate_digest: digest,
784
+ };
785
+ }