@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.
Files changed (70) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +92 -55
  4. package/README.zh-CN.md +93 -56
  5. package/cli/commands/authority.mjs +80 -125
  6. package/cli/commands/clear.mjs +3 -2
  7. package/cli/commands/code.mjs +225 -0
  8. package/cli/commands/start.mjs +3 -2
  9. package/cli/constants.mjs +2 -1
  10. package/cli/dev/check-authority-surface.mjs +17 -8
  11. package/cli/dev/check-product-conformance.mjs +4 -32
  12. package/cli/dev/run-cli-smoke.mjs +6 -0
  13. package/cli/help.mjs +8 -3
  14. package/cli/index.mjs +3 -2
  15. package/cli/lib/authority/audit.mjs +262 -185
  16. package/cli/lib/authority/change-candidates.mjs +807 -0
  17. package/cli/lib/authority/closed-sets.mjs +7 -8
  18. package/cli/lib/authority/compile.mjs +6 -24
  19. package/cli/lib/authority/compiled-snapshot.mjs +143 -0
  20. package/cli/lib/authority/consumers.mjs +69 -19
  21. package/cli/lib/authority/diagnostics.mjs +10 -2
  22. package/cli/lib/authority/diff.mjs +77 -54
  23. package/cli/lib/authority/format.mjs +4 -32
  24. package/cli/lib/authority/git-snapshot.mjs +34 -379
  25. package/cli/lib/authority/graph.mjs +10 -10
  26. package/cli/lib/authority/impact.mjs +17 -144
  27. package/cli/lib/authority/review.mjs +33 -43
  28. package/cli/lib/authority/sarif.mjs +1 -8
  29. package/cli/lib/authority/source-yaml.mjs +0 -4
  30. package/cli/lib/authority/surface.mjs +19 -1
  31. package/cli/lib/authority/terms.mjs +10 -9
  32. package/cli/lib/authority/validate.mjs +6 -14
  33. package/cli/lib/code/authority-annotation-response.mjs +43 -0
  34. package/cli/lib/code/authority-annotation-scan.mjs +162 -0
  35. package/cli/lib/code/authority-annotations.mjs +293 -0
  36. package/cli/lib/code/context.mjs +710 -0
  37. package/cli/lib/code/response.mjs +165 -0
  38. package/cli/lib/doctor.mjs +3 -2
  39. package/cli/lib/entrypoints.mjs +13 -5
  40. package/cli/seeds/seed-policy.yaml +2 -2
  41. package/contracts/authority-source.schema.yaml +0 -6
  42. package/contracts/authority-verifier-bindings.schema.yaml +21 -10
  43. package/methodology/authority-authoring.yaml +68 -70
  44. package/methodology/core.yaml +10 -6
  45. package/package.json +5 -3
  46. package/spec/authority-audit.yaml +56 -52
  47. package/spec/authority-authoring.yaml +3 -16
  48. package/spec/authority-change-candidates.yaml +146 -0
  49. package/spec/authority-consumers.yaml +7 -0
  50. package/spec/authority-discovery.yaml +2 -2
  51. package/spec/authority-graph.yaml +1 -1
  52. package/spec/authority-impact.yaml +16 -39
  53. package/spec/authority-review.yaml +29 -47
  54. package/spec/authority-terms.yaml +5 -1
  55. package/spec/code-authority-annotations.yaml +42 -0
  56. package/spec/code-context.yaml +38 -0
  57. package/spec/product-scope.yaml +15 -9
  58. package/cli/commands/validate-ai-governance.mjs +0 -335
  59. package/cli/lib/authority/evidence-snapshot.mjs +0 -1
  60. package/cli/lib/authority/evidence.mjs +0 -1432
  61. package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +0 -377
  62. package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +0 -376
  63. package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +0 -155
  64. package/cli/lib/internal/governance/ai/check-high-risk-doc-metadata-core.mjs +0 -173
  65. package/cli/lib/internal/governance/config.mjs +0 -114
  66. package/cli/lib/internal/governance/shared/read-yaml-with-fragments.mjs +0 -49
  67. package/contracts/authority-evidence-bindings.schema.yaml +0 -53
  68. package/contracts/authority-evidence-probe-results.schema.yaml +0 -38
  69. package/contracts/authority-impact-dispositions.schema.yaml +0 -13
  70. 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/v1";
14
- const BINDING_FORMAT = "nimicoding.authority-verifier-bindings/v1";
15
- const DETECTOR = "minimum-independent-incoming-reference/v1";
16
- const FINGERPRINT_FORMAT = "nimicoding.authority-audit-fingerprint/v1";
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 !== 1 || contract?.contract?.id !== "nimicoding.authority-verifier-bindings.v1") {
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 the exact closed verifier binding without inferring project policy from authority prose",
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.required_bindings) || data.required_bindings.length === 0 || !Array.isArray(data.bindings)) {
137
- return { ok: false, diagnostics: [bindingDiagnostic(label, "verifier bindings require exact format, required_bindings, and bindings fields", parsedLocation(parsed, ""))] };
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 required = [];
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
- if (!exactObject(value, contract.fields.binding)) {
158
- return { ok: false, diagnostics: [bindingDiagnostic(label, "each binding requires exact id, detector, premise, targets, minimum, and policy fields", parsedLocation(parsed, pointer))] };
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.detector !== DETECTOR) {
166
- return { ok: false, diagnostics: [bindingDiagnostic(label, `unknown detector: ${String(value.detector)}`, parsedLocation(parsed, `${pointer}/detector`))] };
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: [], required, bindings };
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 observations, findings, or gaps are forbidden",
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 { premise };
294
+ return { ok: true };
272
295
  }
273
296
 
274
- function findingBase(binding, target, code, message, witness, primaryLocation, relatedLocations) {
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: DETECTOR,
301
+ detector: GRAPH_DETECTOR,
279
302
  binding: binding.id,
280
303
  premise: binding.premise,
281
304
  target: target.id,
282
- basis: "governance_bound",
305
+ basis: "configured",
283
306
  policy: binding.policy,
284
- fingerprint: fingerprint([code, DETECTOR, binding.id, binding.premise, target.id, `minimum=${binding.minimum}`]),
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 auditAuthorityPath(inputPath, bindingsPath, { maxUnits, maxEdges, maxBytes, bindingLabel = null }) {
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 = await compileAuthorityPath(inputPath);
302
- if (!compiled.ok) return refused(compiled, compiled.diagnostics);
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 absoluteInput = path.resolve(inputPath);
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, snapshot, compiled);
502
+ const validation = validateBindingAuthority(binding, authoritySnapshot, compiled);
311
503
  if (validation.ok === false) return validation;
312
504
  }
313
505
 
314
- const proofUnits = new Set();
315
- const proofEdges = new Set();
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
- const premise = snapshot.byId.get(binding.premise);
342
- proofUnits.add(binding.premise);
343
- const premiseEdges = snapshot.outgoing.get(binding.premise).filter((edge) => edge.type === "applies_to");
344
- for (const edge of premiseEdges) {
345
- proofEdges.add(edgeKey(edge));
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
- gaps.sort((left, right) => compareText(left.binding, right.binding));
449
- const traversal = { units: proofUnits.size, edges: proofEdges.size };
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
- policyStatus: blocking ? "blocked" : gaps.length > 0 ? "indeterminate" : "passed",
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: observations.length,
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 { AUDIT_FORMAT, BINDING_FORMAT, DETECTOR };
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 };