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