@nimiplatform/nimi-coding 0.4.0 → 0.6.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/CHANGELOG.md +16 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +92 -55
- package/README.zh-CN.md +93 -56
- package/cli/commands/authority.mjs +80 -125
- package/cli/commands/clear.mjs +3 -2
- package/cli/commands/code.mjs +225 -0
- package/cli/commands/start.mjs +3 -2
- package/cli/constants.mjs +2 -1
- package/cli/dev/check-authority-surface.mjs +17 -8
- package/cli/dev/check-product-conformance.mjs +4 -32
- package/cli/dev/run-cli-smoke.mjs +6 -0
- package/cli/help.mjs +8 -3
- package/cli/index.mjs +3 -2
- package/cli/lib/authority/audit.mjs +262 -185
- package/cli/lib/authority/change-candidates.mjs +807 -0
- package/cli/lib/authority/closed-sets.mjs +7 -8
- package/cli/lib/authority/compile.mjs +6 -24
- package/cli/lib/authority/compiled-snapshot.mjs +143 -0
- package/cli/lib/authority/consumers.mjs +69 -19
- package/cli/lib/authority/diagnostics.mjs +10 -2
- package/cli/lib/authority/diff.mjs +77 -54
- package/cli/lib/authority/format.mjs +4 -32
- package/cli/lib/authority/git-snapshot.mjs +34 -379
- package/cli/lib/authority/graph.mjs +10 -10
- package/cli/lib/authority/impact.mjs +17 -144
- package/cli/lib/authority/review.mjs +33 -43
- package/cli/lib/authority/sarif.mjs +1 -8
- package/cli/lib/authority/source-yaml.mjs +0 -4
- package/cli/lib/authority/surface.mjs +19 -1
- package/cli/lib/authority/terms.mjs +10 -9
- package/cli/lib/authority/validate.mjs +6 -14
- package/cli/lib/code/authority-annotation-response.mjs +43 -0
- package/cli/lib/code/authority-annotation-scan.mjs +162 -0
- package/cli/lib/code/authority-annotations.mjs +293 -0
- package/cli/lib/code/context.mjs +710 -0
- package/cli/lib/code/response.mjs +165 -0
- package/cli/lib/doctor.mjs +3 -2
- package/cli/lib/entrypoints.mjs +13 -5
- package/cli/seeds/seed-policy.yaml +2 -2
- package/contracts/authority-source.schema.yaml +0 -6
- package/contracts/authority-verifier-bindings.schema.yaml +21 -10
- package/methodology/authority-authoring.yaml +68 -70
- package/methodology/core.yaml +10 -6
- package/package.json +5 -3
- package/spec/authority-audit.yaml +56 -52
- package/spec/authority-authoring.yaml +3 -16
- package/spec/authority-change-candidates.yaml +146 -0
- package/spec/authority-consumers.yaml +7 -0
- package/spec/authority-discovery.yaml +2 -2
- package/spec/authority-graph.yaml +1 -1
- package/spec/authority-impact.yaml +16 -39
- package/spec/authority-review.yaml +29 -47
- package/spec/authority-terms.yaml +5 -1
- package/spec/code-authority-annotations.yaml +42 -0
- package/spec/code-context.yaml +38 -0
- package/spec/product-scope.yaml +15 -9
- package/cli/commands/validate-ai-governance.mjs +0 -335
- package/cli/lib/authority/evidence-snapshot.mjs +0 -1
- package/cli/lib/authority/evidence.mjs +0 -1432
- package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +0 -377
- package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +0 -376
- package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +0 -155
- package/cli/lib/internal/governance/ai/check-high-risk-doc-metadata-core.mjs +0 -173
- package/cli/lib/internal/governance/config.mjs +0 -114
- package/cli/lib/internal/governance/shared/read-yaml-with-fragments.mjs +0 -49
- package/contracts/authority-evidence-bindings.schema.yaml +0 -53
- package/contracts/authority-evidence-probe-results.schema.yaml +0 -38
- package/contracts/authority-impact-dispositions.schema.yaml +0 -13
- package/spec/authority-evidence.yaml +0 -200
|
@@ -7,13 +7,15 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import YAML, { isMap, isScalar, isSeq } from "yaml";
|
|
8
8
|
|
|
9
9
|
import { compileAuthorityPath } from "./compile.mjs";
|
|
10
|
+
import { createCompiledAuthoritySnapshot } from "./compiled-snapshot.mjs";
|
|
10
11
|
import { compareText, createLocator, makeDiagnostic, portablePath } from "./diagnostics.mjs";
|
|
11
|
-
import { buildAuthorityGraphSnapshot } from "./graph.mjs";
|
|
12
12
|
|
|
13
|
-
const AUDIT_FORMAT = "nimicoding.authority-audit/
|
|
14
|
-
const BINDING_FORMAT = "nimicoding.authority-verifier-bindings/
|
|
15
|
-
const
|
|
16
|
-
const
|
|
13
|
+
const AUDIT_FORMAT = "nimicoding.authority-audit/v2";
|
|
14
|
+
const BINDING_FORMAT = "nimicoding.authority-verifier-bindings/v2";
|
|
15
|
+
const GRAPH_DETECTOR = "minimum-independent-incoming-reference/v1";
|
|
16
|
+
const LEXICAL_DETECTOR = "exact-lexical-invariant/v1";
|
|
17
|
+
const FINGERPRINT_FORMAT = "nimicoding.authority-audit-fingerprint/v2";
|
|
18
|
+
const NORMATIVE_FIELDS = ["meaning", "statement", "condition", "failure"];
|
|
17
19
|
const contractPath = fileURLToPath(new URL("../../../contracts/authority-verifier-bindings.schema.yaml", import.meta.url));
|
|
18
20
|
|
|
19
21
|
let contractPromise = null;
|
|
@@ -52,7 +54,7 @@ function collectLocations(node, locator, pointer, locations) {
|
|
|
52
54
|
async function loadContract() {
|
|
53
55
|
if (!contractPromise) contractPromise = readFile(contractPath, "utf8").then((text) => {
|
|
54
56
|
const contract = YAML.parse(text);
|
|
55
|
-
if (contract?.version !==
|
|
57
|
+
if (contract?.version !== 2 || contract?.contract?.id !== "nimicoding.authority-verifier-bindings.v2") {
|
|
56
58
|
throw new Error("installed authority verifier binding contract is invalid");
|
|
57
59
|
}
|
|
58
60
|
return contract;
|
|
@@ -67,7 +69,7 @@ function bindingDiagnostic(file, reason, location = null) {
|
|
|
67
69
|
range: location?.range ?? { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
|
|
68
70
|
pointer: location?.sourcePointer ?? "",
|
|
69
71
|
reason,
|
|
70
|
-
repair: "declare
|
|
72
|
+
repair: "declare one exact closed built-in verifier binding; project policy is never inferred from authority prose or change history",
|
|
71
73
|
});
|
|
72
74
|
}
|
|
73
75
|
|
|
@@ -88,6 +90,13 @@ function syntaxLocation(error, file, locator) {
|
|
|
88
90
|
};
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
function validExactTerm(value) {
|
|
94
|
+
return typeof value === "string"
|
|
95
|
+
&& value.length > 0
|
|
96
|
+
&& value.trim() === value
|
|
97
|
+
&& !/[\0\r\n\v\f\u0085\u2028\u2029]/u.test(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
91
100
|
async function loadBindings(file, labelOverride = null) {
|
|
92
101
|
const absolute = path.resolve(file);
|
|
93
102
|
const label = labelOverride ?? portablePath(absolute);
|
|
@@ -133,38 +142,55 @@ async function loadBindings(file, labelOverride = null) {
|
|
|
133
142
|
collectLocations(documents[0].contents, locator, "", locations);
|
|
134
143
|
const parsed = { file: label, locations, data };
|
|
135
144
|
const contract = await loadContract();
|
|
136
|
-
if (!exactObject(data, contract.fields.top) || data.format !== BINDING_FORMAT || !Array.isArray(data.
|
|
137
|
-
return { ok: false, diagnostics: [bindingDiagnostic(label, "verifier bindings require exact format
|
|
145
|
+
if (!exactObject(data, contract.fields.top) || data.format !== BINDING_FORMAT || !Array.isArray(data.bindings) || data.bindings.length === 0) {
|
|
146
|
+
return { ok: false, diagnostics: [bindingDiagnostic(label, "verifier bindings require exact format and one non-empty bindings list", parsedLocation(parsed, ""))] };
|
|
138
147
|
}
|
|
139
148
|
const identifier = new RegExp(contract.identifier_pattern);
|
|
140
|
-
const
|
|
141
|
-
const requiredIds = new Set();
|
|
142
|
-
for (let index = 0; index < data.required_bindings.length; index += 1) {
|
|
143
|
-
const id = data.required_bindings[index];
|
|
144
|
-
const pointer = `/required_bindings/${index}`;
|
|
145
|
-
if (typeof id !== "string" || !identifier.test(id)) {
|
|
146
|
-
return { ok: false, diagnostics: [bindingDiagnostic(label, "required binding IDs must be exact dotted lowercase identifiers", parsedLocation(parsed, pointer))] };
|
|
147
|
-
}
|
|
148
|
-
if (requiredIds.has(id)) return { ok: false, diagnostics: [bindingDiagnostic(label, `duplicate required binding ID: ${id}`, parsedLocation(parsed, pointer))] };
|
|
149
|
-
requiredIds.add(id);
|
|
150
|
-
required.push({ id, location: parsedLocation(parsed, pointer) });
|
|
151
|
-
}
|
|
149
|
+
const token = new RegExp(contract.token_pattern, "u");
|
|
152
150
|
const bindings = [];
|
|
153
151
|
const bindingIds = new Set();
|
|
154
152
|
for (let index = 0; index < data.bindings.length; index += 1) {
|
|
155
153
|
const value = data.bindings[index];
|
|
156
154
|
const pointer = `/bindings/${index}`;
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
const fields = value?.detector === GRAPH_DETECTOR
|
|
156
|
+
? contract.fields.graph_binding
|
|
157
|
+
: value?.detector === LEXICAL_DETECTOR ? contract.fields.lexical_binding : null;
|
|
158
|
+
if (fields === null) {
|
|
159
|
+
return { ok: false, diagnostics: [bindingDiagnostic(label, `unknown detector: ${String(value?.detector)}`, parsedLocation(parsed, `${pointer}/detector`))] };
|
|
160
|
+
}
|
|
161
|
+
if (!exactObject(value, fields)) {
|
|
162
|
+
return { ok: false, diagnostics: [bindingDiagnostic(label, `binding fields do not match detector ${value.detector}`, parsedLocation(parsed, pointer))] };
|
|
159
163
|
}
|
|
160
164
|
if (typeof value.id !== "string" || !identifier.test(value.id)) {
|
|
161
165
|
return { ok: false, diagnostics: [bindingDiagnostic(label, "binding id must be an exact dotted lowercase identifier", parsedLocation(parsed, `${pointer}/id`))] };
|
|
162
166
|
}
|
|
163
167
|
if (bindingIds.has(value.id)) return { ok: false, diagnostics: [bindingDiagnostic(label, `duplicate binding ID: ${value.id}`, parsedLocation(parsed, `${pointer}/id`))] };
|
|
164
168
|
bindingIds.add(value.id);
|
|
165
|
-
if (value.
|
|
166
|
-
return { ok: false, diagnostics: [bindingDiagnostic(label,
|
|
169
|
+
if (!contract.policies.includes(value.policy)) {
|
|
170
|
+
return { ok: false, diagnostics: [bindingDiagnostic(label, "binding policy must be advisory or blocking", parsedLocation(parsed, `${pointer}/policy`))] };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (value.detector === LEXICAL_DETECTOR) {
|
|
174
|
+
if (!validExactTerm(value.term)) {
|
|
175
|
+
return { ok: false, diagnostics: [bindingDiagnostic(label, "lexical term must be non-empty trimmed single-line exact text", parsedLocation(parsed, `${pointer}/term`))] };
|
|
176
|
+
}
|
|
177
|
+
if (!contract.lexical_matches.includes(value.match)) {
|
|
178
|
+
return { ok: false, diagnostics: [bindingDiagnostic(label, "lexical match must be token or phrase", parsedLocation(parsed, `${pointer}/match`))] };
|
|
179
|
+
}
|
|
180
|
+
if (value.match === "token" && !token.test(value.term)) {
|
|
181
|
+
return { ok: false, diagnostics: [bindingDiagnostic(label, "token matching requires one exact lexical token", parsedLocation(parsed, `${pointer}/term`))] };
|
|
182
|
+
}
|
|
183
|
+
if (!contract.lexical_expectations.includes(value.expectation)) {
|
|
184
|
+
return { ok: false, diagnostics: [bindingDiagnostic(label, "lexical expectation must be present or absent", parsedLocation(parsed, `${pointer}/expectation`))] };
|
|
185
|
+
}
|
|
186
|
+
bindings.push({
|
|
187
|
+
...value,
|
|
188
|
+
location: parsedLocation(parsed, `${pointer}/id`),
|
|
189
|
+
termLocation: parsedLocation(parsed, `${pointer}/term`),
|
|
190
|
+
});
|
|
191
|
+
continue;
|
|
167
192
|
}
|
|
193
|
+
|
|
168
194
|
if (typeof value.premise !== "string" || !identifier.test(value.premise)) {
|
|
169
195
|
return { ok: false, diagnostics: [bindingDiagnostic(label, "binding premise must be one exact authority ID", parsedLocation(parsed, `${pointer}/premise`))] };
|
|
170
196
|
}
|
|
@@ -186,9 +212,6 @@ async function loadBindings(file, labelOverride = null) {
|
|
|
186
212
|
if (!Number.isSafeInteger(value.minimum) || value.minimum <= 0) {
|
|
187
213
|
return { ok: false, diagnostics: [bindingDiagnostic(label, "binding minimum must be a positive safe integer", parsedLocation(parsed, `${pointer}/minimum`))] };
|
|
188
214
|
}
|
|
189
|
-
if (!contract.policies.includes(value.policy)) {
|
|
190
|
-
return { ok: false, diagnostics: [bindingDiagnostic(label, "binding policy must be advisory or blocking", parsedLocation(parsed, `${pointer}/policy`))] };
|
|
191
|
-
}
|
|
192
215
|
bindings.push({
|
|
193
216
|
...value,
|
|
194
217
|
location: parsedLocation(parsed, `${pointer}/id`),
|
|
@@ -196,9 +219,8 @@ async function loadBindings(file, labelOverride = null) {
|
|
|
196
219
|
targetEntries: targets,
|
|
197
220
|
});
|
|
198
221
|
}
|
|
199
|
-
required.sort((left, right) => compareText(left.id, right.id));
|
|
200
222
|
bindings.sort((left, right) => compareText(left.id, right.id));
|
|
201
|
-
return { ok: true, diagnostics: [],
|
|
223
|
+
return { ok: true, diagnostics: [], bindings };
|
|
202
224
|
}
|
|
203
225
|
|
|
204
226
|
function refused(compiled, diagnostics) {
|
|
@@ -241,7 +263,7 @@ function auditBudgetDiagnostic(kind, required, admitted, location = null) {
|
|
|
241
263
|
range: location?.range ?? { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
|
|
242
264
|
pointer: location?.sourcePointer ?? "",
|
|
243
265
|
reason: `complete authority audit requires ${required} ${kind} but the explicit budget is ${admitted}`,
|
|
244
|
-
repair: "increase the explicit audit budget; partial
|
|
266
|
+
repair: "increase the explicit audit budget; partial findings are forbidden",
|
|
245
267
|
});
|
|
246
268
|
}
|
|
247
269
|
|
|
@@ -250,6 +272,7 @@ function validBudget(value) {
|
|
|
250
272
|
}
|
|
251
273
|
|
|
252
274
|
function validateBindingAuthority(binding, snapshot, compiled) {
|
|
275
|
+
if (binding.detector !== GRAPH_DETECTOR) return { ok: true };
|
|
253
276
|
const premise = snapshot.byId.get(binding.premise);
|
|
254
277
|
if (!premise || premise.unit.kind !== "rule" || premise.unit.lifecycle !== "active") {
|
|
255
278
|
return refused(compiled, [bindingDiagnostic(
|
|
@@ -268,20 +291,20 @@ function validateBindingAuthority(binding, snapshot, compiled) {
|
|
|
268
291
|
)]);
|
|
269
292
|
}
|
|
270
293
|
}
|
|
271
|
-
return {
|
|
294
|
+
return { ok: true };
|
|
272
295
|
}
|
|
273
296
|
|
|
274
|
-
function
|
|
297
|
+
function graphFinding(binding, target, code, message, witness, primaryLocation, relatedLocations) {
|
|
275
298
|
const authorityRefs = [...new Set([binding.premise, target.id, ...(witness.incomingRefs ?? [])])].sort(compareText);
|
|
276
299
|
return {
|
|
277
300
|
code,
|
|
278
|
-
detector:
|
|
301
|
+
detector: GRAPH_DETECTOR,
|
|
279
302
|
binding: binding.id,
|
|
280
303
|
premise: binding.premise,
|
|
281
304
|
target: target.id,
|
|
282
|
-
basis: "
|
|
305
|
+
basis: "configured",
|
|
283
306
|
policy: binding.policy,
|
|
284
|
-
fingerprint: fingerprint([code,
|
|
307
|
+
fingerprint: fingerprint([code, GRAPH_DETECTOR, binding.id, binding.premise, target.id, `minimum=${binding.minimum}`]),
|
|
285
308
|
message,
|
|
286
309
|
authorityRefs,
|
|
287
310
|
primaryLocation,
|
|
@@ -290,185 +313,226 @@ function findingBase(binding, target, code, message, witness, primaryLocation, r
|
|
|
290
313
|
};
|
|
291
314
|
}
|
|
292
315
|
|
|
316
|
+
function escapeRegExp(value) {
|
|
317
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function countExactMatches(text, term, match) {
|
|
321
|
+
if (match === "token") {
|
|
322
|
+
const expression = new RegExp(`(?<![\\p{L}\\p{N}_.-])${escapeRegExp(term)}(?![\\p{L}\\p{N}_.-])`, "gu");
|
|
323
|
+
return [...text.matchAll(expression)].length;
|
|
324
|
+
}
|
|
325
|
+
let count = 0;
|
|
326
|
+
let offset = 0;
|
|
327
|
+
while (offset <= text.length - term.length) {
|
|
328
|
+
const found = text.indexOf(term, offset);
|
|
329
|
+
if (found < 0) break;
|
|
330
|
+
count += 1;
|
|
331
|
+
offset = found + term.length;
|
|
332
|
+
}
|
|
333
|
+
return count;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function portableMappedLocation(compiled, pointer, basePath) {
|
|
337
|
+
const mapped = compiled.sourceMap.fields[pointer];
|
|
338
|
+
return { file: portablePath(mapped.file, basePath), range: mapped.range, sourcePointer: mapped.sourcePointer };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function unitIdentityLocation(snapshot, entry) {
|
|
342
|
+
return portableMappedLocation(
|
|
343
|
+
snapshot.compiled,
|
|
344
|
+
`/units/${entry.unitIndex}/id`,
|
|
345
|
+
snapshot.inputRoot,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function relationTargetLocation(snapshot, edge) {
|
|
350
|
+
return portableMappedLocation(
|
|
351
|
+
snapshot.compiled,
|
|
352
|
+
`/units/${edge.sourceUnitIndex}/relations/${edge.relationIndex}/target`,
|
|
353
|
+
snapshot.inputRoot,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function lexicalOccurrences(binding, compiled, basePath, traversedUnits) {
|
|
358
|
+
const occurrences = [];
|
|
359
|
+
compiled.ir.units.forEach((unit, unitIndex) => {
|
|
360
|
+
if (unit.lifecycle !== "active") return;
|
|
361
|
+
traversedUnits.add(unit.id);
|
|
362
|
+
for (const field of NORMATIVE_FIELDS) {
|
|
363
|
+
const text = unit.semantic?.[field];
|
|
364
|
+
if (typeof text !== "string") continue;
|
|
365
|
+
const count = countExactMatches(text, binding.term, binding.match);
|
|
366
|
+
if (count === 0) continue;
|
|
367
|
+
occurrences.push({
|
|
368
|
+
unitId: unit.id,
|
|
369
|
+
field,
|
|
370
|
+
count,
|
|
371
|
+
location: portableMappedLocation(compiled, `/units/${unitIndex}/semantic/${field}`, basePath),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
return occurrences;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function evaluateLexicalBinding(binding, compiled, basePath, traversedUnits) {
|
|
379
|
+
const occurrences = lexicalOccurrences(binding, compiled, basePath, traversedUnits);
|
|
380
|
+
const count = occurrences.reduce((total, entry) => total + entry.count, 0);
|
|
381
|
+
const satisfied = binding.expectation === "present" ? count > 0 : count === 0;
|
|
382
|
+
const authorityRefs = [...new Set(occurrences.map((entry) => entry.unitId))].sort(compareText);
|
|
383
|
+
const witness = {
|
|
384
|
+
type: "exact-lexical-invariant",
|
|
385
|
+
term: binding.term,
|
|
386
|
+
match: binding.match,
|
|
387
|
+
expectation: binding.expectation,
|
|
388
|
+
satisfied,
|
|
389
|
+
count,
|
|
390
|
+
occurrences,
|
|
391
|
+
};
|
|
392
|
+
const relatedLocations = [
|
|
393
|
+
relatedLocation("binding-term", binding.termLocation),
|
|
394
|
+
...occurrences.map((entry) => relatedLocation("normative-field", entry.location)),
|
|
395
|
+
].sort(compareRelatedLocation);
|
|
396
|
+
if (satisfied) return null;
|
|
397
|
+
const code = "AUTH_AUDIT_EXACT_LEXICAL_INVARIANT_VIOLATION";
|
|
398
|
+
const primaryLocation = occurrences[0]?.location ?? binding.termLocation;
|
|
399
|
+
return {
|
|
400
|
+
code,
|
|
401
|
+
detector: LEXICAL_DETECTOR,
|
|
402
|
+
binding: binding.id,
|
|
403
|
+
premise: null,
|
|
404
|
+
basis: "configured",
|
|
405
|
+
policy: binding.policy,
|
|
406
|
+
fingerprint: fingerprint([code, LEXICAL_DETECTOR, binding.id, binding.match, binding.expectation, binding.term]),
|
|
407
|
+
message: `expected exact ${binding.match} ${JSON.stringify(binding.term)} to be ${binding.expectation} in active normative authority fields; observed ${count}`,
|
|
408
|
+
authorityRefs,
|
|
409
|
+
primaryLocation,
|
|
410
|
+
relatedLocations,
|
|
411
|
+
witness,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function evaluateGraphBinding(binding, snapshot, traversedUnits, traversedEdges) {
|
|
416
|
+
const premise = snapshot.byId.get(binding.premise);
|
|
417
|
+
traversedUnits.add(binding.premise);
|
|
418
|
+
const premiseEdges = snapshot.outgoing.get(binding.premise).filter((edge) => edge.type === "applies_to");
|
|
419
|
+
for (const edge of premiseEdges) {
|
|
420
|
+
traversedEdges.add(edgeKey(edge));
|
|
421
|
+
traversedUnits.add(edge.source);
|
|
422
|
+
traversedUnits.add(edge.target);
|
|
423
|
+
}
|
|
424
|
+
const findings = [];
|
|
425
|
+
for (const target of [...binding.targetEntries].sort((left, right) => compareText(left.id, right.id))) {
|
|
426
|
+
const targetNode = snapshot.byId.get(target.id);
|
|
427
|
+
traversedUnits.add(target.id);
|
|
428
|
+
const attachment = premiseEdges.find((edge) => edge.target === target.id) ?? null;
|
|
429
|
+
if (attachment) traversedEdges.add(edgeKey(attachment));
|
|
430
|
+
const incomingCandidates = snapshot.incoming.get(target.id).filter((edge) => edge.type === "applies_to");
|
|
431
|
+
for (const edge of incomingCandidates) {
|
|
432
|
+
traversedEdges.add(edgeKey(edge));
|
|
433
|
+
traversedUnits.add(edge.source);
|
|
434
|
+
traversedUnits.add(edge.target);
|
|
435
|
+
}
|
|
436
|
+
const incoming = incomingCandidates
|
|
437
|
+
.filter((edge) => edge.source !== binding.premise)
|
|
438
|
+
.filter((edge) => {
|
|
439
|
+
const source = snapshot.byId.get(edge.source).unit;
|
|
440
|
+
return source.kind === "rule" && source.lifecycle === "active";
|
|
441
|
+
});
|
|
442
|
+
if (!attachment) {
|
|
443
|
+
findings.push(graphFinding(
|
|
444
|
+
binding,
|
|
445
|
+
target,
|
|
446
|
+
"AUTH_AUDIT_PREMISE_TARGET_UNATTACHED",
|
|
447
|
+
`premise ${binding.premise} does not directly attach required target ${target.id}`,
|
|
448
|
+
{ type: "premise-direct-attachment", relation: "applies_to", attached: false },
|
|
449
|
+
unitIdentityLocation(snapshot, targetNode),
|
|
450
|
+
[
|
|
451
|
+
relatedLocation("binding", binding.location),
|
|
452
|
+
relatedLocation("binding-target", target.location),
|
|
453
|
+
relatedLocation("premise", unitIdentityLocation(snapshot, premise)),
|
|
454
|
+
],
|
|
455
|
+
));
|
|
456
|
+
}
|
|
457
|
+
const observed = new Set(incoming.map((edge) => edge.source)).size;
|
|
458
|
+
if (observed < binding.minimum) {
|
|
459
|
+
const relatedLocations = [
|
|
460
|
+
relatedLocation("binding", binding.location),
|
|
461
|
+
relatedLocation("binding-target", target.location),
|
|
462
|
+
relatedLocation("premise", unitIdentityLocation(snapshot, premise)),
|
|
463
|
+
];
|
|
464
|
+
if (attachment) relatedLocations.push(relatedLocation("premise-attachment", relationTargetLocation(snapshot, attachment)));
|
|
465
|
+
for (const edge of incoming) relatedLocations.push(relatedLocation("incoming-reference", relationTargetLocation(snapshot, edge)));
|
|
466
|
+
findings.push(graphFinding(
|
|
467
|
+
binding,
|
|
468
|
+
target,
|
|
469
|
+
"AUTH_AUDIT_MINIMUM_INDEPENDENT_INCOMING_REFERENCE",
|
|
470
|
+
`target ${target.id} has ${observed} independent active-rule applies_to references; binding requires ${binding.minimum}`,
|
|
471
|
+
{
|
|
472
|
+
type: "minimum-independent-incoming-reference",
|
|
473
|
+
relation: "applies_to",
|
|
474
|
+
minimum: binding.minimum,
|
|
475
|
+
observed,
|
|
476
|
+
incomingRefs: [...new Set(incoming.map((edge) => edge.source))].sort(compareText),
|
|
477
|
+
},
|
|
478
|
+
unitIdentityLocation(snapshot, targetNode),
|
|
479
|
+
relatedLocations,
|
|
480
|
+
));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return findings;
|
|
484
|
+
}
|
|
485
|
+
|
|
293
486
|
export function canonicalAuditBytes(audit) {
|
|
294
487
|
return Buffer.byteLength(JSON.stringify(audit), "utf8");
|
|
295
488
|
}
|
|
296
489
|
|
|
297
|
-
export async function
|
|
490
|
+
export async function auditAuthoritySnapshot(authoritySnapshot, bindingsPath, { maxUnits, maxEdges, maxBytes, bindingLabel = null }) {
|
|
298
491
|
if (![maxUnits, maxEdges, maxBytes].every(validBudget)) {
|
|
299
492
|
return refused(null, [auditBudgetDiagnostic("positive safe-integer budget values", "all", "invalid")]);
|
|
300
493
|
}
|
|
301
|
-
const compiled =
|
|
302
|
-
if (
|
|
494
|
+
const compiled = authoritySnapshot?.compiled;
|
|
495
|
+
if (compiled?.ok !== true || typeof authoritySnapshot?.inputRoot !== "string") {
|
|
496
|
+
throw new TypeError("authority audit snapshot requires one successful immutable compiler snapshot");
|
|
497
|
+
}
|
|
303
498
|
const parsedBindings = await loadBindings(bindingsPath, bindingLabel);
|
|
304
499
|
if (!parsedBindings.ok) return refused(compiled, parsedBindings.diagnostics);
|
|
305
|
-
const
|
|
306
|
-
const inputInfo = await stat(absoluteInput);
|
|
307
|
-
const basePath = inputInfo.isDirectory() ? absoluteInput : path.dirname(absoluteInput);
|
|
308
|
-
const snapshot = buildAuthorityGraphSnapshot(compiled, basePath, ["applies_to"]);
|
|
500
|
+
const basePath = authoritySnapshot.inputRoot;
|
|
309
501
|
for (const binding of parsedBindings.bindings) {
|
|
310
|
-
const validation = validateBindingAuthority(binding,
|
|
502
|
+
const validation = validateBindingAuthority(binding, authoritySnapshot, compiled);
|
|
311
503
|
if (validation.ok === false) return validation;
|
|
312
504
|
}
|
|
313
505
|
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
const observations = [];
|
|
506
|
+
const traversedUnits = new Set();
|
|
507
|
+
const traversedEdges = new Set();
|
|
317
508
|
const findings = [];
|
|
318
|
-
const gaps = [];
|
|
319
|
-
const presentBindings = new Set(parsedBindings.bindings.map((binding) => binding.id));
|
|
320
|
-
|
|
321
|
-
for (const required of parsedBindings.required) {
|
|
322
|
-
if (presentBindings.has(required.id)) continue;
|
|
323
|
-
const code = "AUTH_AUDIT_REQUIRED_BINDING_MISSING";
|
|
324
|
-
gaps.push({
|
|
325
|
-
code,
|
|
326
|
-
detector: null,
|
|
327
|
-
binding: required.id,
|
|
328
|
-
premise: null,
|
|
329
|
-
basis: "required_coverage",
|
|
330
|
-
policy: "required",
|
|
331
|
-
fingerprint: fingerprint([code, required.id]),
|
|
332
|
-
message: `required verifier binding is missing: ${required.id}`,
|
|
333
|
-
authorityRefs: [],
|
|
334
|
-
primaryLocation: required.location,
|
|
335
|
-
relatedLocations: [],
|
|
336
|
-
witness: { type: "required-binding", required: true, present: false },
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
509
|
for (const binding of parsedBindings.bindings) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
proofUnits.add(edge.source);
|
|
347
|
-
proofUnits.add(edge.target);
|
|
510
|
+
if (binding.detector === GRAPH_DETECTOR) {
|
|
511
|
+
findings.push(...evaluateGraphBinding(binding, authoritySnapshot, traversedUnits, traversedEdges));
|
|
512
|
+
} else {
|
|
513
|
+
const finding = evaluateLexicalBinding(binding, compiled, basePath, traversedUnits);
|
|
514
|
+
if (finding) findings.push(finding);
|
|
348
515
|
}
|
|
349
|
-
const targetObservations = [];
|
|
350
|
-
const observationRelated = [relatedLocation("premise", premise.node.location)];
|
|
351
|
-
for (const target of [...binding.targetEntries].sort((left, right) => compareText(left.id, right.id))) {
|
|
352
|
-
const targetNode = snapshot.byId.get(target.id);
|
|
353
|
-
proofUnits.add(target.id);
|
|
354
|
-
const attachment = premiseEdges.find((edge) => edge.target === target.id) ?? null;
|
|
355
|
-
if (attachment) proofEdges.add(edgeKey(attachment));
|
|
356
|
-
const incomingCandidates = snapshot.incoming.get(target.id).filter((edge) => edge.type === "applies_to");
|
|
357
|
-
for (const edge of incomingCandidates) {
|
|
358
|
-
proofEdges.add(edgeKey(edge));
|
|
359
|
-
proofUnits.add(edge.source);
|
|
360
|
-
proofUnits.add(edge.target);
|
|
361
|
-
}
|
|
362
|
-
const incoming = incomingCandidates
|
|
363
|
-
.filter((edge) => edge.source !== binding.premise)
|
|
364
|
-
.filter((edge) => {
|
|
365
|
-
const source = snapshot.byId.get(edge.source).unit;
|
|
366
|
-
return source.kind === "rule" && source.lifecycle === "active";
|
|
367
|
-
});
|
|
368
|
-
targetObservations.push({
|
|
369
|
-
id: target.id,
|
|
370
|
-
attached: attachment !== null,
|
|
371
|
-
observed: new Set(incoming.map((edge) => edge.source)).size,
|
|
372
|
-
incomingRefs: [...new Set(incoming.map((edge) => edge.source))].sort(compareText),
|
|
373
|
-
});
|
|
374
|
-
observationRelated.push(relatedLocation("binding-target", target.location));
|
|
375
|
-
observationRelated.push(relatedLocation("target", targetNode.node.location));
|
|
376
|
-
if (attachment) observationRelated.push(relatedLocation("premise-attachment", attachment.relationLocation));
|
|
377
|
-
for (const edge of incoming) observationRelated.push(relatedLocation("incoming-reference", edge.relationLocation));
|
|
378
|
-
const commonRelated = [
|
|
379
|
-
relatedLocation("binding", binding.location),
|
|
380
|
-
relatedLocation("binding-target", target.location),
|
|
381
|
-
relatedLocation("premise", premise.node.location),
|
|
382
|
-
];
|
|
383
|
-
if (!attachment) {
|
|
384
|
-
findings.push(findingBase(
|
|
385
|
-
binding,
|
|
386
|
-
target,
|
|
387
|
-
"AUTH_AUDIT_PREMISE_TARGET_UNATTACHED",
|
|
388
|
-
`premise ${binding.premise} does not directly attach required target ${target.id}`,
|
|
389
|
-
{ type: "premise-direct-attachment", relation: "applies_to", attached: false },
|
|
390
|
-
targetNode.node.location,
|
|
391
|
-
commonRelated,
|
|
392
|
-
));
|
|
393
|
-
} else {
|
|
394
|
-
commonRelated.push(relatedLocation("premise-attachment", attachment.relationLocation));
|
|
395
|
-
}
|
|
396
|
-
const observed = new Set(incoming.map((edge) => edge.source)).size;
|
|
397
|
-
if (observed < binding.minimum) {
|
|
398
|
-
for (const edge of incoming) commonRelated.push(relatedLocation("incoming-reference", edge.relationLocation));
|
|
399
|
-
findings.push(findingBase(
|
|
400
|
-
binding,
|
|
401
|
-
target,
|
|
402
|
-
"AUTH_AUDIT_MINIMUM_INDEPENDENT_INCOMING_REFERENCE",
|
|
403
|
-
`target ${target.id} has ${observed} independent active-rule applies_to references; binding requires ${binding.minimum}`,
|
|
404
|
-
{
|
|
405
|
-
type: "minimum-independent-incoming-reference",
|
|
406
|
-
relation: "applies_to",
|
|
407
|
-
minimum: binding.minimum,
|
|
408
|
-
observed,
|
|
409
|
-
incomingRefs: [...new Set(incoming.map((edge) => edge.source))].sort(compareText),
|
|
410
|
-
},
|
|
411
|
-
targetNode.node.location,
|
|
412
|
-
commonRelated,
|
|
413
|
-
));
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
observations.push({
|
|
417
|
-
code: "AUTH_AUDIT_INDEPENDENT_INCOMING_REFERENCE_OBSERVATION",
|
|
418
|
-
detector: DETECTOR,
|
|
419
|
-
binding: binding.id,
|
|
420
|
-
premise: binding.premise,
|
|
421
|
-
basis: "governance_bound",
|
|
422
|
-
policy: binding.policy,
|
|
423
|
-
fingerprint: fingerprint([
|
|
424
|
-
"AUTH_AUDIT_INDEPENDENT_INCOMING_REFERENCE_OBSERVATION",
|
|
425
|
-
DETECTOR,
|
|
426
|
-
binding.id,
|
|
427
|
-
binding.premise,
|
|
428
|
-
`targets=${targetObservations.map((target) => target.id).join(",")}`,
|
|
429
|
-
`minimum=${binding.minimum}`,
|
|
430
|
-
]),
|
|
431
|
-
message: `evaluated ${targetObservations.length} exact targets for binding ${binding.id}`,
|
|
432
|
-
authorityRefs: [...new Set([
|
|
433
|
-
binding.premise,
|
|
434
|
-
...targetObservations.flatMap((target) => [target.id, ...target.incomingRefs]),
|
|
435
|
-
])].sort(compareText),
|
|
436
|
-
primaryLocation: binding.location,
|
|
437
|
-
relatedLocations: observationRelated.sort(compareRelatedLocation),
|
|
438
|
-
witness: {
|
|
439
|
-
type: "minimum-independent-incoming-reference",
|
|
440
|
-
relation: "applies_to",
|
|
441
|
-
minimum: binding.minimum,
|
|
442
|
-
targets: targetObservations,
|
|
443
|
-
},
|
|
444
|
-
});
|
|
445
516
|
}
|
|
446
517
|
|
|
447
|
-
findings.sort((left, right) => compareText(left.binding, right.binding) || compareText(left.target, right.target) || compareText(left.code, right.code));
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
const firstLocation = parsedBindings.bindings[0]?.location ?? parsedBindings.required[0]?.location ?? null;
|
|
518
|
+
findings.sort((left, right) => compareText(left.binding, right.binding) || compareText(left.target ?? "", right.target ?? "") || compareText(left.code, right.code));
|
|
519
|
+
const traversal = { units: traversedUnits.size, edges: traversedEdges.size };
|
|
520
|
+
const firstLocation = parsedBindings.bindings[0]?.location ?? null;
|
|
451
521
|
if (traversal.units > maxUnits) return refused(compiled, [auditBudgetDiagnostic("units", traversal.units, maxUnits, firstLocation)]);
|
|
452
522
|
if (traversal.edges > maxEdges) return refused(compiled, [auditBudgetDiagnostic("edges", traversal.edges, maxEdges, firstLocation)]);
|
|
453
523
|
const blocking = findings.some((finding) => finding.policy === "blocking");
|
|
454
524
|
const audit = {
|
|
455
525
|
format: AUDIT_FORMAT,
|
|
456
526
|
operationStatus: "completed",
|
|
457
|
-
|
|
458
|
-
complete: gaps.length === 0,
|
|
459
|
-
observations,
|
|
527
|
+
blockingStatus: blocking ? "blocked" : "clear",
|
|
460
528
|
findings,
|
|
461
|
-
gaps,
|
|
462
529
|
counts: {
|
|
463
530
|
bindings: {
|
|
464
|
-
required: parsedBindings.required.length,
|
|
465
531
|
configured: parsedBindings.bindings.length,
|
|
466
|
-
evaluated:
|
|
532
|
+
evaluated: parsedBindings.bindings.length,
|
|
467
533
|
},
|
|
468
534
|
returned: {
|
|
469
|
-
observations: observations.length,
|
|
470
535
|
findings: findings.length,
|
|
471
|
-
gaps: gaps.length,
|
|
472
536
|
},
|
|
473
537
|
traversal,
|
|
474
538
|
},
|
|
@@ -487,4 +551,17 @@ export async function auditAuthorityPath(inputPath, bindingsPath, { maxUnits, ma
|
|
|
487
551
|
};
|
|
488
552
|
}
|
|
489
553
|
|
|
490
|
-
export
|
|
554
|
+
export async function auditAuthorityPath(inputPath, bindingsPath, options) {
|
|
555
|
+
const { maxUnits, maxEdges, maxBytes } = options;
|
|
556
|
+
if (![maxUnits, maxEdges, maxBytes].every(validBudget)) {
|
|
557
|
+
return refused(null, [auditBudgetDiagnostic("positive safe-integer budget values", "all", "invalid")]);
|
|
558
|
+
}
|
|
559
|
+
const compiled = await compileAuthorityPath(inputPath);
|
|
560
|
+
if (!compiled.ok) return refused(compiled, compiled.diagnostics);
|
|
561
|
+
const absoluteInput = path.resolve(inputPath);
|
|
562
|
+
const inputInfo = await stat(absoluteInput);
|
|
563
|
+
const basePath = inputInfo.isDirectory() ? absoluteInput : path.dirname(absoluteInput);
|
|
564
|
+
return auditAuthoritySnapshot(createCompiledAuthoritySnapshot(compiled, basePath), bindingsPath, options);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export { AUDIT_FORMAT, BINDING_FORMAT, GRAPH_DETECTOR, LEXICAL_DETECTOR };
|