kibi-cli 2.2.0 → 2.3.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-operation-loader.d.ts.map +1 -1
- package/dist/cli-operation-loader.js +3 -0
- package/dist/cli-operation-metadata.d.ts +4 -0
- package/dist/cli-operation-metadata.d.ts.map +1 -1
- package/dist/cli-operation-metadata.js +5 -0
- package/dist/cli-register-reporting.d.ts.map +1 -1
- package/dist/cli-register-reporting.js +4 -0
- package/dist/commands/check-diff.d.ts +5 -0
- package/dist/commands/check-diff.d.ts.map +1 -0
- package/dist/commands/check-diff.js +62 -0
- package/dist/commands/check-generated.d.ts.map +1 -1
- package/dist/commands/check-generated.js +41 -18
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +210 -73
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +63 -29
- package/dist/commands/github-init.d.ts.map +1 -1
- package/dist/commands/github-init.js +9 -2
- package/dist/commands/sync/manifest.d.ts.map +1 -1
- package/dist/commands/sync/manifest.js +3 -1
- package/dist/engine.d.ts +15 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +252 -52
- package/dist/extractors/symbols-coordinator.d.ts +7 -0
- package/dist/extractors/symbols-coordinator.d.ts.map +1 -1
- package/dist/extractors/symbols-coordinator.js +75 -39
- package/dist/operations/modeling/predicate-inference-1.js +1 -1
- package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
- package/dist/operations/mutation/source-authoring.js +175 -53
- package/dist/operations/mutation/upsert.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.js +5 -7
- package/dist/operations/planning/apply-plan.d.ts.map +1 -1
- package/dist/operations/planning/apply-plan.js +5 -9
- package/dist/operations/proof/ingest-proof.d.ts.map +1 -1
- package/dist/operations/proof/ingest-proof.js +1 -0
- package/dist/operations/semantic-advisor/analysis-receipt.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/analysis-receipt.js +7 -1
- package/dist/operations/semantic-advisor/analyze-prose.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/analyze-prose.js +26 -4
- package/dist/operations/semantic-advisor/clauses.d.ts +1 -0
- package/dist/operations/semantic-advisor/clauses.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/clauses.js +7 -1
- package/dist/performance-trace.d.ts +17 -0
- package/dist/performance-trace.d.ts.map +1 -0
- package/dist/performance-trace.js +134 -0
- package/dist/plugins/approved-source-analyzers.d.ts +13 -0
- package/dist/plugins/approved-source-analyzers.d.ts.map +1 -0
- package/dist/plugins/approved-source-analyzers.generated.d.ts +71 -0
- package/dist/plugins/approved-source-analyzers.generated.d.ts.map +1 -0
- package/dist/plugins/approved-source-analyzers.generated.js +77 -0
- package/dist/plugins/approved-source-analyzers.js +3 -0
- package/dist/plugins/maintenance-source-analysis.d.ts +14 -0
- package/dist/plugins/maintenance-source-analysis.d.ts.map +1 -0
- package/dist/plugins/maintenance-source-analysis.js +191 -0
- package/dist/plugins/registry.d.ts +2 -1
- package/dist/plugins/registry.d.ts.map +1 -1
- package/dist/plugins/registry.js +6 -1
- package/dist/plugins/source-analysis-service.d.ts +36 -1
- package/dist/plugins/source-analysis-service.d.ts.map +1 -1
- package/dist/plugins/source-analysis-service.js +184 -27
- package/dist/plugins/source-change-analysis.d.ts +15 -0
- package/dist/plugins/source-change-analysis.d.ts.map +1 -0
- package/dist/plugins/source-change-analysis.js +54 -0
- package/dist/plugins/source-classification.d.ts +41 -0
- package/dist/plugins/source-classification.d.ts.map +1 -0
- package/dist/plugins/source-classification.js +490 -0
- package/dist/prolog/swipl-resolver.d.ts +88 -0
- package/dist/prolog/swipl-resolver.d.ts.map +1 -0
- package/dist/prolog/swipl-resolver.js +446 -0
- package/dist/prolog.d.ts +15 -1
- package/dist/prolog.d.ts.map +1 -1
- package/dist/prolog.js +67 -6
- package/dist/public/impact/analyzer.d.ts.map +1 -1
- package/dist/public/impact/analyzer.js +56 -5
- package/dist/public/impact/diagnostics.d.ts.map +1 -1
- package/dist/public/impact/diagnostics.js +3 -1
- package/dist/public/impact/manifest.d.ts.map +1 -1
- package/dist/public/impact/manifest.js +15 -1
- package/dist/public/impact/source-changes.d.ts.map +1 -1
- package/dist/public/impact/source-changes.js +35 -27
- package/dist/public/impact/types.d.ts +2 -0
- package/dist/public/impact/types.d.ts.map +1 -1
- package/dist/public/operations/catalog.d.ts +44 -0
- package/dist/public/operations/catalog.d.ts.map +1 -1
- package/dist/public/operations/catalog.js +2 -0
- package/dist/public/operations/contracts.d.ts.map +1 -1
- package/dist/public/operations/contracts.js +309 -8
- package/dist/public/operations/impact-review-preparation.d.ts +14 -0
- package/dist/public/operations/impact-review-preparation.d.ts.map +1 -0
- package/dist/public/operations/impact-review-preparation.js +379 -0
- package/dist/public/operations/index.d.ts +2 -0
- package/dist/public/operations/index.d.ts.map +1 -1
- package/dist/public/operations/index.js +2 -0
- package/dist/public/operations/node-ports.d.ts.map +1 -1
- package/dist/public/operations/node-ports.js +1 -35
- package/dist/public/operations/proof-receipt-projection.d.ts +5 -0
- package/dist/public/operations/proof-receipt-projection.d.ts.map +1 -0
- package/dist/public/operations/proof-receipt-projection.js +36 -0
- package/dist/public/operations/specs/impact-review-preparation.d.ts +47 -0
- package/dist/public/operations/specs/impact-review-preparation.d.ts.map +1 -0
- package/dist/public/operations/specs/impact-review-preparation.js +43 -0
- package/dist/public/operations/types.d.ts +1 -1
- package/dist/public/operations/types.d.ts.map +1 -1
- package/dist/public/skills/kibi-usage/resources/operation-access.md +1 -0
- package/dist/traceability/analysis-gate.d.ts +39 -0
- package/dist/traceability/analysis-gate.d.ts.map +1 -0
- package/dist/traceability/analysis-gate.js +51 -0
- package/dist/traceability/git-change-snapshot.d.ts +27 -0
- package/dist/traceability/git-change-snapshot.d.ts.map +1 -0
- package/dist/traceability/git-change-snapshot.js +411 -0
- package/dist/traceability/git-staged.d.ts +5 -1
- package/dist/traceability/git-staged.d.ts.map +1 -1
- package/dist/traceability/git-staged.js +16 -11
- package/dist/traceability/impact-evaluator.d.ts +54 -0
- package/dist/traceability/impact-evaluator.d.ts.map +1 -0
- package/dist/traceability/impact-evaluator.js +270 -0
- package/dist/traceability/impact-git-tree.d.ts +12 -0
- package/dist/traceability/impact-git-tree.d.ts.map +1 -0
- package/dist/traceability/impact-git-tree.js +46 -0
- package/dist/traceability/impact-knowledge.d.ts +16 -0
- package/dist/traceability/impact-knowledge.d.ts.map +1 -0
- package/dist/traceability/impact-knowledge.js +165 -0
- package/dist/traceability/impact-policy.d.ts +15 -0
- package/dist/traceability/impact-policy.d.ts.map +1 -0
- package/dist/traceability/impact-policy.js +48 -0
- package/dist/traceability/impact-policy.v1.schema.json +48 -0
- package/dist/traceability/impact-pr-diff.d.ts +5 -0
- package/dist/traceability/impact-pr-diff.d.ts.map +1 -0
- package/dist/traceability/impact-pr-diff.js +83 -0
- package/dist/traceability/impact-record-validation.d.ts +9 -0
- package/dist/traceability/impact-record-validation.d.ts.map +1 -0
- package/dist/traceability/impact-record-validation.js +113 -0
- package/dist/traceability/impact-review.d.ts +148 -0
- package/dist/traceability/impact-review.d.ts.map +1 -0
- package/dist/traceability/impact-review.js +494 -0
- package/dist/traceability/impact-review.schema.json +365 -0
- package/dist/traceability/impact-side-evidence.d.ts +13 -0
- package/dist/traceability/impact-side-evidence.d.ts.map +1 -0
- package/dist/traceability/impact-side-evidence.js +240 -0
- package/dist/traceability/reviewed-coordinate-refresh.d.ts +10 -0
- package/dist/traceability/reviewed-coordinate-refresh.d.ts.map +1 -0
- package/dist/traceability/reviewed-coordinate-refresh.js +49 -0
- package/dist/traceability/snapshot-knowledge.d.ts +4 -0
- package/dist/traceability/snapshot-knowledge.d.ts.map +1 -0
- package/dist/traceability/snapshot-knowledge.js +98 -0
- package/dist/traceability/staged-file-coverage.d.ts +23 -0
- package/dist/traceability/staged-file-coverage.d.ts.map +1 -1
- package/dist/traceability/staged-impact-contract.d.ts.map +1 -1
- package/dist/traceability/staged-impact-contract.js +2 -16
- package/dist/traceability/staged-symbols-manifest.d.ts +4 -0
- package/dist/traceability/staged-symbols-manifest.d.ts.map +1 -1
- package/dist/traceability/staged-symbols-manifest.js +17 -6
- package/dist/traceability/symbol-extract.d.ts +3 -5
- package/dist/traceability/symbol-extract.d.ts.map +1 -1
- package/dist/traceability/symbol-extract.js +49 -34
- package/dist/traceability/temp-kb.d.ts +1 -1
- package/dist/traceability/temp-kb.d.ts.map +1 -1
- package/dist/traceability/temp-kb.js +49 -13
- package/dist/traceability/trusted-pr-event.d.ts +13 -0
- package/dist/traceability/trusted-pr-event.d.ts.map +1 -0
- package/dist/traceability/trusted-pr-event.js +105 -0
- package/dist/utils/canonical-path.d.ts +3 -0
- package/dist/utils/canonical-path.d.ts.map +1 -0
- package/dist/utils/canonical-path.js +33 -0
- package/package.json +14 -5
- package/src/public/impact/analyzer.ts +88 -5
- package/src/public/impact/diagnostics.ts +7 -5
- package/src/public/impact/manifest.ts +14 -1
- package/src/public/impact/source-changes.ts +39 -34
- package/src/public/impact/types.ts +5 -0
- package/src/public/operations/catalog.ts +2 -0
- package/src/public/operations/contracts.ts +385 -9
- package/src/public/operations/impact-review-preparation.ts +548 -0
- package/src/public/operations/index.ts +2 -0
- package/src/public/operations/node-ports.ts +1 -39
- package/src/public/operations/proof-receipt-projection.ts +39 -0
- package/src/public/operations/specs/impact-review-preparation.ts +46 -0
- package/src/public/operations/types.ts +1 -0
- package/src/public/skills/kibi-usage/resources/operation-access.md +1 -0
- package/src/traceability/impact-policy.v1.schema.json +48 -0
- package/src/traceability/impact-review.schema.json +365 -0
- package/templates/github/kibi-badge.yml +0 -4
- package/templates/github/kibi-report.yml +0 -4
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export const IMPACT_POLICY_PATH = ".kibi/impact-policy.json";
|
|
3
|
+
export const IMPACT_REVIEW_PATH = ".kibi/impact-review.json";
|
|
4
|
+
export const IMPACT_POLICY_VERSION = "kibi.impact-policy.v1";
|
|
5
|
+
export const IMPACT_REVIEW_VERSION = "kibi.impact-review.v1";
|
|
6
|
+
export const IMPACT_EVALUATOR_VERSION = "kibi.impact-evaluator.v1";
|
|
7
|
+
const SHA256_RE = /^sha256:[0-9a-f]{64}$/;
|
|
8
|
+
const GIT_OID_RE = /^[0-9a-f]{40,64}$/;
|
|
9
|
+
export function canonicalJson(value) {
|
|
10
|
+
const active = new Set();
|
|
11
|
+
const encode = (item) => {
|
|
12
|
+
if (item === null || typeof item === "string" || typeof item === "boolean")
|
|
13
|
+
return JSON.stringify(item);
|
|
14
|
+
if (typeof item === "number") {
|
|
15
|
+
if (!Number.isFinite(item))
|
|
16
|
+
throw new Error("Impact evidence contains a non-finite number");
|
|
17
|
+
return JSON.stringify(item);
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(item)) {
|
|
20
|
+
if (active.has(item))
|
|
21
|
+
throw new Error("Impact evidence contains a cycle");
|
|
22
|
+
active.add(item);
|
|
23
|
+
const result = `[${item.map(encode).join(",")}]`;
|
|
24
|
+
active.delete(item);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
if (typeof item === "object" && item !== null) {
|
|
28
|
+
if (active.has(item))
|
|
29
|
+
throw new Error("Impact evidence contains a cycle");
|
|
30
|
+
active.add(item);
|
|
31
|
+
const record = item;
|
|
32
|
+
const keys = Object.keys(record).sort();
|
|
33
|
+
const result = `{${keys.map((key) => `${JSON.stringify(key)}:${encode(record[key])}`).join(",")}}`;
|
|
34
|
+
active.delete(item);
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
throw new Error("Impact evidence is not JSON-safe");
|
|
38
|
+
};
|
|
39
|
+
return encode(value);
|
|
40
|
+
}
|
|
41
|
+
export function fingerprint(value) {
|
|
42
|
+
return `sha256:${createHash("sha256").update(canonicalJson(value), "utf8").digest("hex")}`;
|
|
43
|
+
}
|
|
44
|
+
export function fingerprintBytes(value) {
|
|
45
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
46
|
+
}
|
|
47
|
+
function isObject(value) {
|
|
48
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49
|
+
}
|
|
50
|
+
function exactKeys(value, keys, label) {
|
|
51
|
+
const actual = Object.keys(value).sort();
|
|
52
|
+
const expected = [...keys].sort();
|
|
53
|
+
if (canonicalJson(actual) !== canonicalJson(expected))
|
|
54
|
+
throw new Error(`${label} has unknown or missing fields`);
|
|
55
|
+
}
|
|
56
|
+
function exactKeysWithOptional(value, required, optional, label) {
|
|
57
|
+
const actual = Object.keys(value);
|
|
58
|
+
if (required.some((key) => !Object.hasOwn(value, key)) ||
|
|
59
|
+
actual.some((key) => !required.includes(key) && !optional.includes(key)))
|
|
60
|
+
throw new Error(`${label} has unknown or missing fields`);
|
|
61
|
+
}
|
|
62
|
+
export function parseImpactPolicy(value) {
|
|
63
|
+
if (!isObject(value))
|
|
64
|
+
throw new Error("Impact policy must be an object");
|
|
65
|
+
exactKeys(value, [
|
|
66
|
+
"contractVersion",
|
|
67
|
+
"id",
|
|
68
|
+
"version",
|
|
69
|
+
"allowUnsupportedReview",
|
|
70
|
+
"allowedPartial",
|
|
71
|
+
"notApplicablePaths",
|
|
72
|
+
], "Impact policy");
|
|
73
|
+
if (value.contractVersion !== IMPACT_POLICY_VERSION)
|
|
74
|
+
throw new Error("Unknown impact policy version");
|
|
75
|
+
if (typeof value.id !== "string" ||
|
|
76
|
+
!value.id.trim() ||
|
|
77
|
+
typeof value.version !== "string" ||
|
|
78
|
+
!value.version.trim())
|
|
79
|
+
throw new Error("Impact policy id and version are required");
|
|
80
|
+
if (typeof value.allowUnsupportedReview !== "boolean")
|
|
81
|
+
throw new Error("Impact policy allowUnsupportedReview must be boolean");
|
|
82
|
+
if (!Array.isArray(value.allowedPartial) ||
|
|
83
|
+
!Array.isArray(value.notApplicablePaths))
|
|
84
|
+
throw new Error("Impact policy lists are required");
|
|
85
|
+
const partial = [];
|
|
86
|
+
const partialKeys = new Set();
|
|
87
|
+
for (const [index, row] of value.allowedPartial.entries()) {
|
|
88
|
+
if (!isObject(row))
|
|
89
|
+
throw new Error(`allowedPartial[${index}] must be an object`);
|
|
90
|
+
exactKeys(row, [
|
|
91
|
+
"providerId",
|
|
92
|
+
"providerFingerprint",
|
|
93
|
+
"diagnosticCode",
|
|
94
|
+
"limitationClass",
|
|
95
|
+
], `allowedPartial[${index}]`);
|
|
96
|
+
if ([row.providerId, row.diagnosticCode, row.limitationClass].some((part) => typeof part !== "string" || !part.trim()) ||
|
|
97
|
+
typeof row.providerFingerprint !== "string" ||
|
|
98
|
+
!SHA256_RE.test(row.providerFingerprint))
|
|
99
|
+
throw new Error(`allowedPartial[${index}] is invalid`);
|
|
100
|
+
const entry = row;
|
|
101
|
+
const key = canonicalJson({
|
|
102
|
+
providerId: entry.providerId,
|
|
103
|
+
providerFingerprint: entry.providerFingerprint,
|
|
104
|
+
diagnosticCode: entry.diagnosticCode,
|
|
105
|
+
});
|
|
106
|
+
if (partialKeys.has(key))
|
|
107
|
+
throw new Error("Impact policy contains conflicting or duplicate partial allowance selectors");
|
|
108
|
+
partialKeys.add(key);
|
|
109
|
+
partial.push(entry);
|
|
110
|
+
}
|
|
111
|
+
const paths = [];
|
|
112
|
+
const seenPaths = new Set();
|
|
113
|
+
for (const path of value.notApplicablePaths) {
|
|
114
|
+
if (typeof path !== "string" ||
|
|
115
|
+
!path.trim() ||
|
|
116
|
+
path.startsWith("/") ||
|
|
117
|
+
path.includes("\\") ||
|
|
118
|
+
path
|
|
119
|
+
.split("/")
|
|
120
|
+
.some((segment) => !segment || segment === "." || segment === ".."))
|
|
121
|
+
throw new Error("Impact policy has an invalid notApplicablePaths entry");
|
|
122
|
+
if (seenPaths.has(path))
|
|
123
|
+
throw new Error(`Impact policy duplicates not-applicable path: ${path}`);
|
|
124
|
+
seenPaths.add(path);
|
|
125
|
+
paths.push(path);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
contractVersion: IMPACT_POLICY_VERSION,
|
|
129
|
+
id: value.id,
|
|
130
|
+
version: value.version,
|
|
131
|
+
allowUnsupportedReview: value.allowUnsupportedReview,
|
|
132
|
+
allowedPartial: partial,
|
|
133
|
+
notApplicablePaths: paths,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export function parseImpactReviewRecord(value) {
|
|
137
|
+
if (!isObject(value))
|
|
138
|
+
throw new Error("Impact review record must be an object");
|
|
139
|
+
if (value.contractVersion !== IMPACT_REVIEW_VERSION)
|
|
140
|
+
throw new Error("Unknown impact review version");
|
|
141
|
+
const top = [
|
|
142
|
+
"contractVersion",
|
|
143
|
+
"policy",
|
|
144
|
+
"evaluator",
|
|
145
|
+
"scope",
|
|
146
|
+
"reviewer",
|
|
147
|
+
"reviewedAt",
|
|
148
|
+
"files",
|
|
149
|
+
];
|
|
150
|
+
exactKeys(value, top, "Impact review record");
|
|
151
|
+
if (!isObject(value.policy) ||
|
|
152
|
+
!isObject(value.evaluator) ||
|
|
153
|
+
!isObject(value.scope) ||
|
|
154
|
+
!isObject(value.reviewer) ||
|
|
155
|
+
!Array.isArray(value.files))
|
|
156
|
+
throw new Error("Impact review record has invalid fields");
|
|
157
|
+
exactKeys(value.policy, ["id", "version", "fingerprint"], "Record policy");
|
|
158
|
+
exactKeys(value.evaluator, ["contractVersion", "fingerprint"], "Record evaluator");
|
|
159
|
+
exactKeys(value.scope, ["fingerprint", "knowledgeFingerprint", "providerSetFingerprint"], "Record scope");
|
|
160
|
+
exactKeys(value.reviewer, ["id", "source"], "Record reviewer");
|
|
161
|
+
const hashes = [
|
|
162
|
+
value.policy.fingerprint,
|
|
163
|
+
value.evaluator.fingerprint,
|
|
164
|
+
value.scope.fingerprint,
|
|
165
|
+
value.scope.knowledgeFingerprint,
|
|
166
|
+
value.scope.providerSetFingerprint,
|
|
167
|
+
];
|
|
168
|
+
if (hashes.some((hash) => typeof hash !== "string" || !SHA256_RE.test(hash)))
|
|
169
|
+
throw new Error("Impact review record has an invalid fingerprint");
|
|
170
|
+
if (typeof value.policy.id !== "string" ||
|
|
171
|
+
typeof value.policy.version !== "string" ||
|
|
172
|
+
value.evaluator.contractVersion !== IMPACT_EVALUATOR_VERSION ||
|
|
173
|
+
typeof value.reviewer.id !== "string" ||
|
|
174
|
+
!value.reviewer.id.trim() ||
|
|
175
|
+
value.reviewer.source !== "self-claimed-local")
|
|
176
|
+
throw new Error("Impact review identity is invalid");
|
|
177
|
+
if (typeof value.reviewedAt !== "string" ||
|
|
178
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value.reviewedAt) ||
|
|
179
|
+
!Number.isFinite(Date.parse(value.reviewedAt)))
|
|
180
|
+
throw new Error("Impact review timestamp is invalid");
|
|
181
|
+
const paths = new Set();
|
|
182
|
+
for (const [index, row] of value.files.entries()) {
|
|
183
|
+
if (!isObject(row))
|
|
184
|
+
throw new Error(`files[${index}] must be an object`);
|
|
185
|
+
const required = [
|
|
186
|
+
"path",
|
|
187
|
+
"status",
|
|
188
|
+
"analysisDepth",
|
|
189
|
+
"disposition",
|
|
190
|
+
"oldHunkRanges",
|
|
191
|
+
"newHunkRanges",
|
|
192
|
+
"before",
|
|
193
|
+
"after",
|
|
194
|
+
"analysisReviews",
|
|
195
|
+
"decision",
|
|
196
|
+
];
|
|
197
|
+
for (const key of [
|
|
198
|
+
"oldPath",
|
|
199
|
+
"copyFromPath",
|
|
200
|
+
"skipReason",
|
|
201
|
+
"gitMode",
|
|
202
|
+
"previousMode",
|
|
203
|
+
])
|
|
204
|
+
if (key in row)
|
|
205
|
+
required.push(key);
|
|
206
|
+
exactKeys(row, required, `files[${index}]`);
|
|
207
|
+
if (typeof row.path !== "string" ||
|
|
208
|
+
!row.path ||
|
|
209
|
+
row.path === IMPACT_REVIEW_PATH ||
|
|
210
|
+
row.path.startsWith("/") ||
|
|
211
|
+
row.path.includes("\\") ||
|
|
212
|
+
row.path
|
|
213
|
+
.split("/")
|
|
214
|
+
.some((segment) => !segment || segment === "." || segment === ".."))
|
|
215
|
+
throw new Error(`files[${index}] has an invalid path`);
|
|
216
|
+
if (paths.has(row.path))
|
|
217
|
+
throw new Error(`Duplicate impact review path: ${row.path}`);
|
|
218
|
+
paths.add(row.path);
|
|
219
|
+
const validStatuses = ["A", "M", "R", "C", "T", "D"];
|
|
220
|
+
if (typeof row.status !== "string" || !validStatuses.includes(row.status))
|
|
221
|
+
throw new Error(`files[${index}] has an invalid status`);
|
|
222
|
+
if (row.oldPath !== undefined &&
|
|
223
|
+
(typeof row.oldPath !== "string" || !row.oldPath))
|
|
224
|
+
throw new Error(`files[${index}] has an invalid oldPath`);
|
|
225
|
+
if (row.copyFromPath !== undefined &&
|
|
226
|
+
(typeof row.copyFromPath !== "string" || !row.copyFromPath))
|
|
227
|
+
throw new Error(`files[${index}] has an invalid copyFromPath`);
|
|
228
|
+
if (!["symbol", "metadata", "file", "none"].includes(String(row.analysisDepth)) ||
|
|
229
|
+
!["checked", "advisory", "skipped"].includes(String(row.disposition)))
|
|
230
|
+
throw new Error(`files[${index}] has invalid analysis disposition`);
|
|
231
|
+
if (row.skipReason !== undefined &&
|
|
232
|
+
!["binary", "unsupported_encoding", "symlink", "submodule"].includes(String(row.skipReason)))
|
|
233
|
+
throw new Error(`files[${index}] has invalid skipReason`);
|
|
234
|
+
if ((row.gitMode !== undefined && typeof row.gitMode !== "string") ||
|
|
235
|
+
(row.previousMode !== undefined && typeof row.previousMode !== "string"))
|
|
236
|
+
throw new Error(`files[${index}] has invalid mode metadata`);
|
|
237
|
+
if (!Array.isArray(row.oldHunkRanges) ||
|
|
238
|
+
!Array.isArray(row.newHunkRanges) ||
|
|
239
|
+
!Array.isArray(row.analysisReviews) ||
|
|
240
|
+
!isObject(row.decision))
|
|
241
|
+
throw new Error(`files[${index}] is malformed`);
|
|
242
|
+
for (const hunk of [...row.oldHunkRanges, ...row.newHunkRanges])
|
|
243
|
+
if (!isObject(hunk) ||
|
|
244
|
+
Object.keys(hunk).sort().join(",") !== "end,start" ||
|
|
245
|
+
!Number.isSafeInteger(hunk.start) ||
|
|
246
|
+
!Number.isSafeInteger(hunk.end) ||
|
|
247
|
+
hunk.start < 0 ||
|
|
248
|
+
hunk.end < hunk.start)
|
|
249
|
+
throw new Error(`files[${index}] has an invalid hunk range`);
|
|
250
|
+
if (row.before !== null)
|
|
251
|
+
validateSideEvidence(row.before, `files[${index}].before`);
|
|
252
|
+
if (row.after !== null)
|
|
253
|
+
validateSideEvidence(row.after, `files[${index}].after`);
|
|
254
|
+
for (const side of [row.before, row.after])
|
|
255
|
+
if (isObject(side) &&
|
|
256
|
+
Object.hasOwn(side, "contentProjection") &&
|
|
257
|
+
(!row.path.startsWith(".kb/") || !row.path.endsWith(".md")))
|
|
258
|
+
throw new Error(`files[${index}] uses a KB markdown projection outside a KB markdown path`);
|
|
259
|
+
if (row.before === null && row.after === null)
|
|
260
|
+
throw new Error(`files[${index}] has no Git side`);
|
|
261
|
+
validateDecision(row.decision);
|
|
262
|
+
for (const review of row.analysisReviews)
|
|
263
|
+
validateAnalysisReview(review);
|
|
264
|
+
}
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
function validateDecision(value) {
|
|
268
|
+
switch (value.kind) {
|
|
269
|
+
case "impact": {
|
|
270
|
+
exactKeys(value, ["kind", "requirementIds", "knowledge"], "Impact decision");
|
|
271
|
+
if (!Array.isArray(value.requirementIds) ||
|
|
272
|
+
value.requirementIds.length === 0 ||
|
|
273
|
+
value.requirementIds.some((id) => typeof id !== "string" || !id.trim()) ||
|
|
274
|
+
!isObject(value.knowledge))
|
|
275
|
+
throw new Error("Impact decision requires requirement IDs and knowledge evidence");
|
|
276
|
+
if (value.knowledge.state === "still_current") {
|
|
277
|
+
exactKeys(value.knowledge, ["state", "rationale"], "still_current evidence");
|
|
278
|
+
if (typeof value.knowledge.rationale !== "string" ||
|
|
279
|
+
!value.knowledge.rationale.trim())
|
|
280
|
+
throw new Error("still_current requires rationale");
|
|
281
|
+
}
|
|
282
|
+
else if (value.knowledge.state === "updated") {
|
|
283
|
+
exactKeysWithOptional(value.knowledge, ["state", "entities"], ["stillCurrent"], "updated evidence");
|
|
284
|
+
if (!Array.isArray(value.knowledge.entities) ||
|
|
285
|
+
value.knowledge.entities.length === 0)
|
|
286
|
+
throw new Error("updated evidence requires entity fingerprints");
|
|
287
|
+
if (value.knowledge.stillCurrent !== undefined) {
|
|
288
|
+
if (!Array.isArray(value.knowledge.stillCurrent))
|
|
289
|
+
throw new Error("stillCurrent must be an array");
|
|
290
|
+
const acknowledgments = new Set();
|
|
291
|
+
for (const item of value.knowledge.stillCurrent) {
|
|
292
|
+
if (!isObject(item))
|
|
293
|
+
throw new Error("stillCurrent requirement acknowledgment is invalid");
|
|
294
|
+
exactKeys(item, ["requirementId", "rationale"], "stillCurrent requirement acknowledgment");
|
|
295
|
+
if (typeof item.requirementId !== "string" ||
|
|
296
|
+
!item.requirementId.trim() ||
|
|
297
|
+
typeof item.rationale !== "string" ||
|
|
298
|
+
!item.rationale.trim())
|
|
299
|
+
throw new Error("stillCurrent requirement acknowledgment requires an ID and rationale");
|
|
300
|
+
const canonicalItem = canonicalJson(item);
|
|
301
|
+
if (acknowledgments.has(canonicalItem))
|
|
302
|
+
throw new Error(`stillCurrent duplicates acknowledgment ${item.requirementId}`);
|
|
303
|
+
acknowledgments.add(canonicalItem);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const entityIds = new Set();
|
|
307
|
+
for (const entity of value.knowledge.entities) {
|
|
308
|
+
if (!isObject(entity))
|
|
309
|
+
throw new Error("updated entity evidence is invalid");
|
|
310
|
+
exactKeys(entity, [
|
|
311
|
+
"entityId",
|
|
312
|
+
"artifactPath",
|
|
313
|
+
"beforeFingerprint",
|
|
314
|
+
"afterFingerprint",
|
|
315
|
+
], "updated entity evidence");
|
|
316
|
+
if (typeof entity.entityId !== "string" ||
|
|
317
|
+
typeof entity.artifactPath !== "string" ||
|
|
318
|
+
entity.artifactPath.startsWith("/") ||
|
|
319
|
+
entity.artifactPath.includes("\\") ||
|
|
320
|
+
entity.artifactPath
|
|
321
|
+
.split("/")
|
|
322
|
+
.some((segment) => !segment || segment === "." || segment === "..") ||
|
|
323
|
+
(entity.beforeFingerprint !== null &&
|
|
324
|
+
(typeof entity.beforeFingerprint !== "string" ||
|
|
325
|
+
!SHA256_RE.test(entity.beforeFingerprint))) ||
|
|
326
|
+
typeof entity.afterFingerprint !== "string" ||
|
|
327
|
+
!SHA256_RE.test(entity.afterFingerprint))
|
|
328
|
+
throw new Error("updated entity fingerprint is invalid");
|
|
329
|
+
if (entityIds.has(entity.entityId))
|
|
330
|
+
throw new Error(`updated evidence duplicates entity ${entity.entityId}`);
|
|
331
|
+
entityIds.add(entity.entityId);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else
|
|
335
|
+
throw new Error("Unknown impact knowledge state");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
case "no_impact":
|
|
339
|
+
exactKeys(value, ["kind", "reason", "rationale"], "no_impact decision");
|
|
340
|
+
if (typeof value.reason !== "string" ||
|
|
341
|
+
!value.reason.trim() ||
|
|
342
|
+
typeof value.rationale !== "string" ||
|
|
343
|
+
!value.rationale.trim())
|
|
344
|
+
throw new Error("no_impact requires a reason and rationale");
|
|
345
|
+
return;
|
|
346
|
+
case "not_applicable":
|
|
347
|
+
exactKeys(value, ["kind", "policyReason", "rationale"], "not_applicable decision");
|
|
348
|
+
if (typeof value.policyReason !== "string" ||
|
|
349
|
+
!value.policyReason.trim() ||
|
|
350
|
+
typeof value.rationale !== "string" ||
|
|
351
|
+
!value.rationale.trim())
|
|
352
|
+
throw new Error("not_applicable requires policy reason and rationale");
|
|
353
|
+
return;
|
|
354
|
+
default:
|
|
355
|
+
throw new Error("Unknown impact decision kind");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function validateAnalysisReview(value) {
|
|
359
|
+
if (!isObject(value))
|
|
360
|
+
throw new Error("Analysis review must be an object");
|
|
361
|
+
if (value.kind === "unsupported_review") {
|
|
362
|
+
exactKeys(value, ["kind", "side", "wholeFile", "rationale"], "unsupported review");
|
|
363
|
+
if ((value.side !== "before" && value.side !== "after") ||
|
|
364
|
+
value.wholeFile !== true ||
|
|
365
|
+
typeof value.rationale !== "string" ||
|
|
366
|
+
!value.rationale.trim())
|
|
367
|
+
throw new Error("Unsupported review is invalid");
|
|
368
|
+
}
|
|
369
|
+
else if (value.kind === "partial_review") {
|
|
370
|
+
exactKeys(value, ["kind", "side", "limitationClass", "ranges", "rationale"], "partial review");
|
|
371
|
+
if ((value.side !== "before" && value.side !== "after") ||
|
|
372
|
+
typeof value.limitationClass !== "string" ||
|
|
373
|
+
!value.limitationClass.trim() ||
|
|
374
|
+
!Array.isArray(value.ranges) ||
|
|
375
|
+
value.ranges.length === 0 ||
|
|
376
|
+
typeof value.rationale !== "string" ||
|
|
377
|
+
!value.rationale.trim())
|
|
378
|
+
throw new Error("Partial review is invalid");
|
|
379
|
+
for (const range of value.ranges) {
|
|
380
|
+
if (!isObject(range))
|
|
381
|
+
throw new Error("Partial review range is invalid");
|
|
382
|
+
exactKeys(range, ["startLine", "startColumn", "endLine", "endColumn", "reason"], "partial review range");
|
|
383
|
+
if (!isObject(range) ||
|
|
384
|
+
!Number.isSafeInteger(range.startLine) ||
|
|
385
|
+
range.startLine < 1 ||
|
|
386
|
+
!Number.isSafeInteger(range.startColumn) ||
|
|
387
|
+
range.startColumn < 0 ||
|
|
388
|
+
!Number.isSafeInteger(range.endLine) ||
|
|
389
|
+
range.endLine < 1 ||
|
|
390
|
+
!Number.isSafeInteger(range.endColumn) ||
|
|
391
|
+
range.endColumn < 0 ||
|
|
392
|
+
typeof range.reason !== "string" ||
|
|
393
|
+
!range.reason.trim())
|
|
394
|
+
throw new Error("Partial review range is invalid");
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else
|
|
398
|
+
throw new Error("Unknown analysis review kind");
|
|
399
|
+
}
|
|
400
|
+
function validateSideEvidence(value, label) {
|
|
401
|
+
if (!isObject(value))
|
|
402
|
+
throw new Error(`${label} must be an object`);
|
|
403
|
+
exactKeysWithOptional(value, ["mode", "objectId", "byteFingerprint", "kind", "analysis"], ["contentProjection"], label);
|
|
404
|
+
const hasProjection = Object.hasOwn(value, "contentProjection");
|
|
405
|
+
const projected = hasProjection && value.contentProjection === "proof_receipts_stripped";
|
|
406
|
+
const modeKinds = {
|
|
407
|
+
"100644": "regular",
|
|
408
|
+
"100755": "regular",
|
|
409
|
+
"120000": "symlink",
|
|
410
|
+
"160000": "gitlink",
|
|
411
|
+
};
|
|
412
|
+
if (typeof value.mode !== "string" ||
|
|
413
|
+
modeKinds[value.mode] !== value.kind ||
|
|
414
|
+
(projected
|
|
415
|
+
? value.objectId !== null
|
|
416
|
+
: hasProjection ||
|
|
417
|
+
typeof value.objectId !== "string" ||
|
|
418
|
+
!GIT_OID_RE.test(value.objectId)) ||
|
|
419
|
+
typeof value.byteFingerprint !== "string" ||
|
|
420
|
+
!SHA256_RE.test(value.byteFingerprint) ||
|
|
421
|
+
(value.analysis !== null && !isObject(value.analysis)))
|
|
422
|
+
throw new Error(`${label} has invalid Git-side evidence`);
|
|
423
|
+
if (value.analysis === null)
|
|
424
|
+
return;
|
|
425
|
+
const analysis = value.analysis;
|
|
426
|
+
exactKeys(analysis, [
|
|
427
|
+
"contractVersion",
|
|
428
|
+
"status",
|
|
429
|
+
"language",
|
|
430
|
+
"sourceFile",
|
|
431
|
+
"providerId",
|
|
432
|
+
"providerStamp",
|
|
433
|
+
"providerFingerprint",
|
|
434
|
+
"inputFingerprint",
|
|
435
|
+
"resultFingerprint",
|
|
436
|
+
"diagnosticCodes",
|
|
437
|
+
"uncoveredRanges",
|
|
438
|
+
], `${label}.analysis`);
|
|
439
|
+
if (analysis.contractVersion !== "kibi.symbol-extractor.v2" ||
|
|
440
|
+
!["ok", "partial", "unsupported", "failed"].includes(String(analysis.status)) ||
|
|
441
|
+
typeof analysis.language !== "string" ||
|
|
442
|
+
!analysis.language.trim() ||
|
|
443
|
+
typeof analysis.sourceFile !== "string" ||
|
|
444
|
+
!analysis.sourceFile.trim() ||
|
|
445
|
+
(analysis.providerId !== null &&
|
|
446
|
+
(typeof analysis.providerId !== "string" ||
|
|
447
|
+
!analysis.providerId.trim())) ||
|
|
448
|
+
!Array.isArray(analysis.diagnosticCodes) ||
|
|
449
|
+
analysis.diagnosticCodes.some((code) => typeof code !== "string" || !code.trim()) ||
|
|
450
|
+
!Array.isArray(analysis.uncoveredRanges))
|
|
451
|
+
throw new Error(`${label}.analysis has invalid source analysis fields`);
|
|
452
|
+
try {
|
|
453
|
+
canonicalJson(analysis.providerStamp);
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
throw new Error(`${label}.analysis.providerStamp is not JSON-safe`);
|
|
457
|
+
}
|
|
458
|
+
if ((analysis.providerFingerprint !== null &&
|
|
459
|
+
(typeof analysis.providerFingerprint !== "string" ||
|
|
460
|
+
!SHA256_RE.test(analysis.providerFingerprint))) ||
|
|
461
|
+
typeof analysis.inputFingerprint !== "string" ||
|
|
462
|
+
!SHA256_RE.test(analysis.inputFingerprint) ||
|
|
463
|
+
typeof analysis.resultFingerprint !== "string" ||
|
|
464
|
+
!SHA256_RE.test(analysis.resultFingerprint))
|
|
465
|
+
throw new Error(`${label}.analysis has invalid fingerprints`);
|
|
466
|
+
for (const [index, range] of analysis.uncoveredRanges.entries()) {
|
|
467
|
+
if (!isObject(range))
|
|
468
|
+
throw new Error(`${label}.analysis.uncoveredRanges[${index}] is invalid`);
|
|
469
|
+
exactKeys(range, ["startLine", "startColumn", "endLine", "endColumn", "reason"], `${label}.analysis.uncoveredRanges[${index}]`);
|
|
470
|
+
if (!Number.isSafeInteger(range.startLine) ||
|
|
471
|
+
range.startLine < 1 ||
|
|
472
|
+
!Number.isSafeInteger(range.startColumn) ||
|
|
473
|
+
range.startColumn < 0 ||
|
|
474
|
+
!Number.isSafeInteger(range.endLine) ||
|
|
475
|
+
range.endLine < range.startLine ||
|
|
476
|
+
!Number.isSafeInteger(range.endColumn) ||
|
|
477
|
+
range.endColumn < 0 ||
|
|
478
|
+
typeof range.reason !== "string" ||
|
|
479
|
+
!range.reason.trim() ||
|
|
480
|
+
(range.endLine === range.startLine &&
|
|
481
|
+
range.endColumn < range.startColumn))
|
|
482
|
+
throw new Error(`${label}.analysis.uncoveredRanges[${index}] is invalid`);
|
|
483
|
+
}
|
|
484
|
+
if ((analysis.status === "partial" && analysis.uncoveredRanges.length === 0) ||
|
|
485
|
+
(analysis.status === "ok" && analysis.uncoveredRanges.length > 0))
|
|
486
|
+
throw new Error(`${label}.analysis status/range mismatch`);
|
|
487
|
+
}
|
|
488
|
+
export function assertSha256(value, name) {
|
|
489
|
+
if (!SHA256_RE.test(value))
|
|
490
|
+
throw new Error(`${name} must be a sha256 fingerprint`);
|
|
491
|
+
}
|
|
492
|
+
export function isGitObjectId(value) {
|
|
493
|
+
return GIT_OID_RE.test(value);
|
|
494
|
+
}
|