kibi-cli 2.31.0 → 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.
- package/dist/cli-register-proof.d.ts.map +1 -1
- package/dist/cli-register-proof.js +19 -7
- package/dist/cli-register-reporting.d.ts.map +1 -1
- package/dist/cli-register-reporting.js +2 -0
- package/dist/commands/check.d.ts +1 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +14 -5
- package/dist/commands/discovery-table.d.ts.map +1 -1
- package/dist/commands/discovery-table.js +51 -4
- package/dist/commands/doctor.js +19 -4
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +89 -3
- package/dist/commands/prove.d.ts +48 -1
- package/dist/commands/prove.d.ts.map +1 -1
- package/dist/commands/prove.js +293 -8
- package/dist/commands/report.d.ts +1 -0
- package/dist/commands/report.d.ts.map +1 -1
- package/dist/commands/report.js +70 -5
- package/dist/commands/signoff.d.ts.map +1 -1
- package/dist/commands/signoff.js +21 -2
- package/dist/commands/sync/manifest.d.ts.map +1 -1
- package/dist/commands/sync/manifest.js +20 -4
- package/dist/commands/sync/persistence.d.ts.map +1 -1
- package/dist/commands/sync/persistence.js +16 -5
- package/dist/diagnostics.js +1 -1
- package/dist/engine.js +1 -1
- package/dist/extractors/markdown.d.ts +3 -1
- package/dist/extractors/markdown.d.ts.map +1 -1
- package/dist/extractors/markdown.js +17 -0
- package/dist/operations/bootstrap/candidate-helpers.d.ts +3 -0
- package/dist/operations/bootstrap/candidate-helpers.d.ts.map +1 -1
- package/dist/operations/bootstrap/candidate-helpers.js +4 -0
- package/dist/operations/migration/evidence-floor.d.ts +41 -0
- package/dist/operations/migration/evidence-floor.d.ts.map +1 -0
- package/dist/operations/migration/evidence-floor.js +230 -0
- package/dist/operations/migration/proof-contract-claims.d.ts +59 -0
- package/dist/operations/migration/proof-contract-claims.d.ts.map +1 -0
- package/dist/operations/migration/proof-contract-claims.js +182 -0
- package/dist/operations/migration/schema6.d.ts +6 -0
- package/dist/operations/migration/schema6.d.ts.map +1 -1
- package/dist/operations/migration/schema6.js +94 -0
- package/dist/operations/mutation/omitted-properties.d.ts +11 -0
- package/dist/operations/mutation/omitted-properties.d.ts.map +1 -0
- package/dist/operations/mutation/omitted-properties.js +76 -0
- package/dist/operations/mutation/serialization.d.ts.map +1 -1
- package/dist/operations/mutation/serialization.js +1 -0
- package/dist/operations/mutation/strict-fact.d.ts.map +1 -1
- package/dist/operations/mutation/strict-fact.js +6 -2
- package/dist/operations/mutation/symbol-granularity.d.ts.map +1 -1
- package/dist/operations/mutation/symbol-granularity.js +5 -3
- package/dist/operations/mutation/upsert.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.js +7 -1
- package/dist/operations/mutation/validation.d.ts +2 -0
- package/dist/operations/mutation/validation.d.ts.map +1 -1
- package/dist/operations/mutation/validation.js +4 -0
- package/dist/operations/proof/claim-bindings.d.ts +23 -0
- package/dist/operations/proof/claim-bindings.d.ts.map +1 -0
- package/dist/operations/proof/claim-bindings.js +211 -0
- package/dist/operations/proof/code-certificates.d.ts +135 -0
- package/dist/operations/proof/code-certificates.d.ts.map +1 -0
- package/dist/operations/proof/code-certificates.js +785 -0
- package/dist/operations/proof/code-scope.d.ts +11 -1
- package/dist/operations/proof/code-scope.d.ts.map +1 -1
- package/dist/operations/proof/code-scope.js +20 -2
- package/dist/operations/proof/compact-receipts.d.ts.map +1 -1
- package/dist/operations/proof/compact-receipts.js +4 -3
- package/dist/operations/proof/explain.d.ts +32 -0
- package/dist/operations/proof/explain.d.ts.map +1 -1
- package/dist/operations/proof/explain.js +91 -0
- package/dist/operations/proof/ingest-proof.d.ts +9 -1
- package/dist/operations/proof/ingest-proof.d.ts.map +1 -1
- package/dist/operations/proof/ingest-proof.js +41 -2
- package/dist/operations/proof/receipt-compaction.d.ts +2 -0
- package/dist/operations/proof/receipt-compaction.d.ts.map +1 -1
- package/dist/operations/proof/receipt-compaction.js +6 -2
- package/dist/operations/semantic-advisor/shared.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/shared.js +4 -0
- package/dist/operations/semantic-advisor/strict-rules.js +3 -3
- package/dist/operations/semantic-advisor/types.d.ts +2 -1
- package/dist/operations/semantic-advisor/types.d.ts.map +1 -1
- package/dist/plugins/approved-source-analyzers.generated.d.ts +6 -6
- package/dist/plugins/approved-source-analyzers.generated.js +6 -6
- package/dist/proof/command-test-report.d.ts +4 -3
- package/dist/proof/command-test-report.d.ts.map +1 -1
- package/dist/proof/command-test-report.js +4 -10
- package/dist/proof/evaluate.d.ts.map +1 -1
- package/dist/proof/evaluate.js +18 -2
- package/dist/proof/inspect.d.ts.map +1 -1
- package/dist/proof/inspect.js +58 -0
- package/dist/proof/producers/playwright-reporter.d.ts +3 -0
- package/dist/proof/producers/playwright-reporter.d.ts.map +1 -1
- package/dist/proof/producers/playwright-reporter.js +25 -2
- package/dist/proof/step-runner.d.ts +36 -0
- package/dist/proof/step-runner.d.ts.map +1 -0
- package/dist/proof/step-runner.js +234 -0
- package/dist/public/evidence-levels.d.ts +12 -0
- package/dist/public/evidence-levels.d.ts.map +1 -0
- package/dist/public/evidence-levels.js +19 -0
- package/dist/public/impact/coverage-depth-quality.d.ts +7 -0
- package/dist/public/impact/coverage-depth-quality.d.ts.map +1 -1
- package/dist/public/impact/coverage-depth-quality.js +59 -0
- package/dist/public/impact/full-kb-quality.d.ts.map +1 -1
- package/dist/public/impact/full-kb-quality.js +3 -1
- package/dist/public/operations/catalog.d.ts +167 -6
- package/dist/public/operations/catalog.d.ts.map +1 -1
- package/dist/public/operations/check-executor.d.ts +2 -0
- package/dist/public/operations/check-executor.d.ts.map +1 -1
- package/dist/public/operations/check-executor.js +6 -0
- package/dist/public/operations/evidence-floor.d.ts +48 -0
- package/dist/public/operations/evidence-floor.d.ts.map +1 -0
- package/dist/public/operations/evidence-floor.js +143 -0
- package/dist/public/operations/migration-plan.d.ts +19 -5
- package/dist/public/operations/migration-plan.d.ts.map +1 -1
- package/dist/public/operations/migration-plan.js +22 -5
- package/dist/public/operations/node-ports.d.ts.map +1 -1
- package/dist/public/operations/node-ports.js +4 -1
- package/dist/public/operations/proof-contract-claims.d.ts +26 -0
- package/dist/public/operations/proof-contract-claims.d.ts.map +1 -0
- package/dist/public/operations/proof-contract-claims.js +71 -0
- package/dist/public/operations/specs/check-rules.generated.d.ts +1 -1
- package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
- package/dist/public/operations/specs/check-rules.generated.js +3 -1
- package/dist/public/operations/specs/check.d.ts +1 -1
- package/dist/public/operations/specs/mutation-schemas.d.ts +157 -5
- package/dist/public/operations/specs/mutation-schemas.d.ts.map +1 -1
- package/dist/public/operations/specs/mutation-schemas.js +15 -2
- package/dist/public/operations/specs/proof.d.ts +9 -0
- package/dist/public/operations/specs/proof.d.ts.map +1 -1
- package/dist/public/operations/specs/reporting.d.ts.map +1 -1
- package/dist/public/operations/specs/reporting.js +19 -10
- package/dist/public/operations/specs/upsert.d.ts +157 -5
- package/dist/public/operations/specs/upsert.d.ts.map +1 -1
- package/dist/public/proof-certificates.d.ts +89 -0
- package/dist/public/proof-certificates.d.ts.map +1 -0
- package/dist/public/proof-certificates.js +207 -0
- package/dist/public/proof-protocol.d.ts +126 -3
- package/dist/public/proof-protocol.d.ts.map +1 -1
- package/dist/public/proof-protocol.js +239 -3
- package/dist/public/proof-receipt.d.ts +89 -6
- package/dist/public/proof-receipt.d.ts.map +1 -1
- package/dist/public/proof-receipt.js +83 -7
- package/dist/public/schemas/entity.d.ts.map +1 -1
- package/dist/public/schemas/entity.js +18 -1
- package/dist/public/skills/kibi-bootstrap/SKILL.md +2 -2
- package/dist/public/skills/kibi-commit-review/SKILL.md +4 -3
- package/dist/public/skills/kibi-commit-review/resources/staged-gate.md +6 -1
- package/dist/public/skills/kibi-decisions/SKILL.md +10 -2
- package/dist/public/skills/kibi-decisions/resources/decision-records.md +6 -1
- package/dist/public/skills/kibi-decisions/resources/signoff-views.md +3 -2
- package/dist/public/skills/kibi-freshness/SKILL.md +5 -1
- package/dist/public/skills/kibi-traceability/SKILL.md +11 -1
- package/dist/public/skills/kibi-usage/SKILL.md +6 -4
- package/dist/public/skills/kibi-usage/resources/proof.md +124 -19
- package/dist/public/skills/kibi-usage/resources/workflows.md +4 -4
- package/dist/report/html-report.css +38 -1
- package/dist/report/html-report.d.ts +8 -0
- package/dist/report/html-report.d.ts.map +1 -1
- package/dist/report/html-report.js +141 -6
- package/dist/report/report-view.d.ts +1 -1
- package/dist/report/report-view.d.ts.map +1 -1
- package/dist/report/report-view.js +7 -0
- package/dist/schemas/entity.schema.json +212 -4
- package/dist/search-answer.d.ts.map +1 -1
- package/dist/search-answer.js +1 -0
- package/dist/signoff/html.d.ts.map +1 -1
- package/dist/signoff/html.js +3 -0
- package/dist/signoff/markdown.d.ts.map +1 -1
- package/dist/signoff/markdown.js +3 -0
- package/dist/signoff/model.d.ts +20 -0
- package/dist/signoff/model.d.ts.map +1 -1
- package/dist/signoff/model.js +20 -1
- package/dist/traceability/impact-knowledge.d.ts.map +1 -1
- package/dist/traceability/impact-knowledge.js +1 -0
- package/dist/traceability/temp-kb.d.ts.map +1 -1
- package/dist/traceability/temp-kb.js +1 -0
- package/dist/types/entities.d.ts +4 -1
- package/dist/types/entities.d.ts.map +1 -1
- package/dist/utils/merge-driver-setup.d.ts +8 -1
- package/dist/utils/merge-driver-setup.d.ts.map +1 -1
- package/dist/utils/merge-driver-setup.js +23 -2
- package/dist/utils/rule-registry.generated.d.ts +10 -0
- package/dist/utils/rule-registry.generated.d.ts.map +1 -1
- package/dist/utils/rule-registry.generated.js +12 -0
- package/dist/utils/schema-version.d.ts +1 -1
- package/dist/utils/schema-version.d.ts.map +1 -1
- package/dist/utils/schema-version.js +5 -1
- package/package.json +6 -2
- package/schema/entities.pl +6 -0
- package/schema/validation.pl +4 -1
- package/src/public/evidence-levels.ts +23 -0
- package/src/public/impact/coverage-depth-quality.ts +73 -0
- package/src/public/impact/full-kb-quality.ts +6 -1
- package/src/public/operations/check-executor.ts +13 -0
- package/src/public/operations/evidence-floor.ts +203 -0
- package/src/public/operations/migration-plan.ts +39 -6
- package/src/public/operations/node-ports.ts +3 -1
- package/src/public/operations/proof-contract-claims.ts +103 -0
- package/src/public/operations/specs/check-rules.generated.ts +3 -1
- package/src/public/operations/specs/mutation-schemas.ts +17 -2
- package/src/public/operations/specs/reporting.ts +20 -10
- package/src/public/proof-certificates.ts +271 -0
- package/src/public/proof-protocol-types.d.ts +17 -1
- package/src/public/proof-protocol.ts +318 -3
- package/src/public/proof-receipt.ts +127 -7
- package/src/public/schemas/entity.ts +19 -1
- package/src/public/skills/kibi-bootstrap/SKILL.md +2 -2
- package/src/public/skills/kibi-commit-review/SKILL.md +4 -3
- package/src/public/skills/kibi-commit-review/resources/staged-gate.md +6 -1
- package/src/public/skills/kibi-decisions/SKILL.md +10 -2
- package/src/public/skills/kibi-decisions/resources/decision-records.md +6 -1
- package/src/public/skills/kibi-decisions/resources/signoff-views.md +3 -2
- package/src/public/skills/kibi-freshness/SKILL.md +5 -1
- package/src/public/skills/kibi-traceability/SKILL.md +11 -1
- package/src/public/skills/kibi-usage/SKILL.md +6 -4
- package/src/public/skills/kibi-usage/resources/proof.md +124 -19
- package/src/public/skills/kibi-usage/resources/workflows.md +4 -4
- package/src/schemas/entity.schema.json +212 -4
- package/dist/commands/proof-impact.d.ts +0 -7
- package/dist/commands/proof-impact.d.ts.map +0 -1
- package/dist/commands/proof-impact.js +0 -39
- package/dist/operations/proof/baseline-diff.d.ts +0 -41
- package/dist/operations/proof/baseline-diff.d.ts.map +0 -1
- package/dist/operations/proof/baseline-diff.js +0 -153
- package/dist/operations/proof/impact.d.ts +0 -51
- package/dist/operations/proof/impact.d.ts.map +0 -1
- 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
|
+
}
|