@nimiplatform/nimi-coding 0.3.1 → 0.5.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 (115) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/CONTRIBUTING.md +3 -3
  3. package/README.md +297 -41
  4. package/README.zh-CN.md +306 -31
  5. package/bin/nimicoding.mjs +1 -1
  6. package/cli/commands/authority.mjs +804 -0
  7. package/cli/commands/clear.mjs +27 -7
  8. package/cli/commands/doctor.mjs +10 -1
  9. package/cli/commands/start.mjs +23 -27
  10. package/cli/commands/sync.mjs +21 -101
  11. package/cli/constants.mjs +7 -31
  12. package/cli/dev/check-authority-surface.mjs +182 -0
  13. package/cli/dev/check-product-conformance.mjs +889 -0
  14. package/cli/help.mjs +10 -17
  15. package/cli/index.mjs +6 -47
  16. package/cli/lib/authority/anchors.mjs +491 -0
  17. package/cli/lib/authority/audit.mjs +567 -0
  18. package/cli/lib/authority/change-candidates.mjs +807 -0
  19. package/cli/lib/authority/closed-sets.mjs +645 -0
  20. package/cli/lib/authority/compile.mjs +92 -0
  21. package/cli/lib/authority/compiled-snapshot.mjs +143 -0
  22. package/cli/lib/authority/consumers.mjs +520 -0
  23. package/cli/lib/authority/diagnostics.mjs +93 -0
  24. package/cli/lib/authority/diff.mjs +310 -0
  25. package/cli/lib/authority/discover.mjs +447 -0
  26. package/cli/lib/authority/format.mjs +169 -0
  27. package/cli/lib/authority/git-snapshot.mjs +720 -0
  28. package/cli/lib/authority/graph.mjs +351 -0
  29. package/cli/lib/authority/impact.mjs +125 -0
  30. package/cli/lib/authority/lexicon.mjs +55 -0
  31. package/cli/lib/authority/query.mjs +162 -0
  32. package/cli/lib/authority/repository-corpus.mjs +37 -0
  33. package/cli/lib/authority/repository-inventory.mjs +34 -0
  34. package/cli/lib/authority/review.mjs +212 -0
  35. package/cli/lib/authority/sarif.mjs +174 -0
  36. package/cli/lib/authority/scope-bindings.mjs +225 -0
  37. package/cli/lib/authority/source-map.mjs +105 -0
  38. package/cli/lib/authority/source-markdown.mjs +167 -0
  39. package/cli/lib/authority/source-yaml.mjs +308 -0
  40. package/cli/lib/authority/surface.mjs +97 -0
  41. package/cli/lib/authority/terms.mjs +422 -0
  42. package/cli/lib/authority/tracked-text.mjs +70 -0
  43. package/cli/lib/authority/validate.mjs +300 -0
  44. package/cli/lib/bootstrap.mjs +55 -71
  45. package/cli/lib/doctor.mjs +40 -123
  46. package/cli/lib/entrypoints.mjs +104 -105
  47. package/cli/lib/fs-helpers.mjs +139 -15
  48. package/cli/lib/internal/governance/config.mjs +0 -36
  49. package/cli/lib/shared.mjs +8 -21
  50. package/cli/lib/sync.mjs +81 -92
  51. package/cli/lib/value-helpers.mjs +0 -26
  52. package/cli/lib/yaml-helpers.mjs +1 -124
  53. package/cli/seeds/bootstrap.mjs +31 -87
  54. package/cli/seeds/seed-policy.yaml +9 -63
  55. package/contracts/authority-scope-bindings.schema.yaml +15 -0
  56. package/contracts/authority-source.schema.yaml +58 -0
  57. package/contracts/authority-verifier-bindings.schema.yaml +31 -0
  58. package/methodology/authority-authoring.yaml +127 -0
  59. package/methodology/core.yaml +34 -27
  60. package/package.json +10 -6
  61. package/spec/authority-anchors.yaml +99 -0
  62. package/spec/authority-audit.yaml +89 -0
  63. package/spec/authority-authoring.yaml +137 -0
  64. package/spec/authority-change-candidates.yaml +146 -0
  65. package/spec/authority-closed-sets.yaml +65 -0
  66. package/spec/authority-consumers.yaml +77 -0
  67. package/spec/authority-context.yaml +50 -0
  68. package/spec/authority-discovery.yaml +119 -0
  69. package/spec/authority-graph.yaml +59 -0
  70. package/spec/authority-impact.yaml +61 -0
  71. package/spec/authority-review.yaml +71 -0
  72. package/spec/authority-terms.yaml +63 -0
  73. package/spec/product-scope.yaml +37 -23
  74. package/cli/commands/blueprint-audit.mjs +0 -91
  75. package/cli/commands/classify-spec-tree.mjs +0 -5
  76. package/cli/commands/generate-spec-derived-docs.mjs +0 -151
  77. package/cli/commands/generate-spec-migration-plan.mjs +0 -30
  78. package/cli/commands/surface-validator-command.mjs +0 -49
  79. package/cli/commands/validate-domain-admission.mjs +0 -5
  80. package/cli/commands/validate-guidance-bodies.mjs +0 -5
  81. package/cli/commands/validate-placement.mjs +0 -5
  82. package/cli/commands/validate-projection-edges.mjs +0 -5
  83. package/cli/commands/validate-spec-audit.mjs +0 -27
  84. package/cli/commands/validate-spec-governance.mjs +0 -144
  85. package/cli/commands/validate-spec-tree.mjs +0 -27
  86. package/cli/commands/validate-table-family.mjs +0 -5
  87. package/cli/commands/validate-tracked-output-admission.mjs +0 -5
  88. package/cli/lib/blueprint-audit.mjs +0 -363
  89. package/cli/lib/contracts.mjs +0 -21
  90. package/cli/lib/internal/contracts-loaders.mjs +0 -47
  91. package/cli/lib/internal/contracts-parse.mjs +0 -192
  92. package/cli/lib/internal/governance/runner.mjs +0 -35
  93. package/cli/lib/internal/surface-taxonomy-validators.mjs +0 -1147
  94. package/cli/lib/internal/validators-shared.mjs +0 -12
  95. package/cli/lib/internal/validators-spec-helpers.mjs +0 -52
  96. package/cli/lib/internal/validators-spec.mjs +0 -380
  97. package/cli/lib/validators.mjs +0 -30
  98. package/config/bootstrap.yaml +0 -6
  99. package/config/spec-generation-inputs.yaml +0 -41
  100. package/contracts/domain-admission.schema.yaml +0 -56
  101. package/contracts/migration-inventory.schema.yaml +0 -15
  102. package/contracts/negative-fixtures.yaml +0 -26
  103. package/contracts/placement-contract.schema.yaml +0 -127
  104. package/contracts/projection-edge.schema.yaml +0 -46
  105. package/contracts/shared-enums.yaml +0 -63
  106. package/contracts/spec-generation-audit.schema.yaml +0 -46
  107. package/contracts/spec-generation-inputs.schema.yaml +0 -43
  108. package/contracts/spec-layout.schema.yaml +0 -32
  109. package/contracts/surface-taxonomy.schema.yaml +0 -139
  110. package/contracts/table-family.schema.yaml +0 -133
  111. package/contracts/tracked-output-admission.schema.yaml +0 -17
  112. package/methodology/four-closure-policy.yaml +0 -20
  113. package/methodology/role-separation-policy.yaml +0 -22
  114. package/methodology/spec-reconstruction.yaml +0 -69
  115. package/spec/_meta/spec-tree-model.yaml +0 -44
@@ -0,0 +1,567 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { createHash } from "node:crypto";
3
+ import { lstat, readFile, stat } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ import YAML, { isMap, isScalar, isSeq } from "yaml";
8
+
9
+ import { compileAuthorityPath } from "./compile.mjs";
10
+ import { createCompiledAuthoritySnapshot } from "./compiled-snapshot.mjs";
11
+ import { compareText, createLocator, makeDiagnostic, portablePath } from "./diagnostics.mjs";
12
+
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"];
19
+ const contractPath = fileURLToPath(new URL("../../../contracts/authority-verifier-bindings.schema.yaml", import.meta.url));
20
+
21
+ let contractPromise = null;
22
+
23
+ function exactObject(value, fields) {
24
+ return value !== null && typeof value === "object" && !Array.isArray(value)
25
+ && Object.keys(value).length === fields.length
26
+ && Object.keys(value).every((key) => fields.includes(key));
27
+ }
28
+
29
+ function pointerToken(value) {
30
+ return String(value).replaceAll("~", "~0").replaceAll("/", "~1");
31
+ }
32
+
33
+ function nodeRange(node, locator) {
34
+ const range = node?.range;
35
+ if (!Array.isArray(range)) return locator.range(0);
36
+ return locator.range(range[0], range[1] ?? range[0]);
37
+ }
38
+
39
+ function collectLocations(node, locator, pointer, locations) {
40
+ if (!node) return;
41
+ locations.set(pointer, nodeRange(node, locator));
42
+ if (isMap(node)) {
43
+ for (const pair of node.items) {
44
+ const key = isScalar(pair.key) ? String(pair.key.value) : "";
45
+ const childPointer = `${pointer}/${pointerToken(key)}`;
46
+ locations.set(childPointer, nodeRange(pair.value ?? pair.key, locator));
47
+ collectLocations(pair.value, locator, childPointer, locations);
48
+ }
49
+ } else if (isSeq(node)) {
50
+ node.items.forEach((item, index) => collectLocations(item, locator, `${pointer}/${index}`, locations));
51
+ }
52
+ }
53
+
54
+ async function loadContract() {
55
+ if (!contractPromise) contractPromise = readFile(contractPath, "utf8").then((text) => {
56
+ const contract = YAML.parse(text);
57
+ if (contract?.version !== 2 || contract?.contract?.id !== "nimicoding.authority-verifier-bindings.v2") {
58
+ throw new Error("installed authority verifier binding contract is invalid");
59
+ }
60
+ return contract;
61
+ });
62
+ return contractPromise;
63
+ }
64
+
65
+ function bindingDiagnostic(file, reason, location = null) {
66
+ return makeDiagnostic({
67
+ code: "AUTH_AUDIT_BINDING_INVALID",
68
+ file,
69
+ range: location?.range ?? { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
70
+ pointer: location?.sourcePointer ?? "",
71
+ reason,
72
+ repair: "declare one exact closed built-in verifier binding; project policy is never inferred from authority prose or change history",
73
+ });
74
+ }
75
+
76
+ function parsedLocation(parsed, pointer) {
77
+ return {
78
+ file: parsed.file,
79
+ range: parsed.locations.get(pointer) ?? parsed.locations.get("") ?? { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
80
+ sourcePointer: pointer,
81
+ };
82
+ }
83
+
84
+ function syntaxLocation(error, file, locator) {
85
+ const positions = Array.isArray(error?.pos) ? error.pos : [0, 0];
86
+ return {
87
+ file,
88
+ range: locator.range(positions[0] ?? 0, positions[1] ?? positions[0] ?? 0),
89
+ sourcePointer: "",
90
+ };
91
+ }
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
+
100
+ async function loadBindings(file, labelOverride = null) {
101
+ const absolute = path.resolve(file);
102
+ const label = labelOverride ?? portablePath(absolute);
103
+ let bytes;
104
+ try {
105
+ const info = await lstat(absolute);
106
+ if (info.isSymbolicLink() || !info.isFile()) {
107
+ return { ok: false, diagnostics: [bindingDiagnostic(label, "verifier bindings must be one regular non-symlink YAML file")] };
108
+ }
109
+ bytes = await readFile(absolute);
110
+ } catch (error) {
111
+ return { ok: false, diagnostics: [bindingDiagnostic(label, `verifier binding file is not readable: ${error.message}`)] };
112
+ }
113
+ let text;
114
+ try {
115
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
116
+ } catch {
117
+ return { ok: false, diagnostics: [bindingDiagnostic(label, "verifier binding bytes are not valid UTF-8")] };
118
+ }
119
+ const locator = createLocator(text);
120
+ const documents = YAML.parseAllDocuments(text, {
121
+ keepSourceTokens: true,
122
+ prettyErrors: false,
123
+ strict: true,
124
+ uniqueKeys: true,
125
+ version: "1.2",
126
+ });
127
+ if (documents.length !== 1 || documents[0].errors.length > 0 || !isMap(documents[0].contents)) {
128
+ const error = documents[0]?.errors?.[0];
129
+ const location = syntaxLocation(error, label, locator);
130
+ const reason = documents.length !== 1
131
+ ? "verifier bindings must contain exactly one YAML document"
132
+ : error ? `invalid verifier binding YAML: ${error.message.split(" at line")[0]}` : "verifier bindings require one top-level mapping";
133
+ return { ok: false, diagnostics: [bindingDiagnostic(label, reason, location)] };
134
+ }
135
+ let data;
136
+ try {
137
+ data = documents[0].toJS({ maxAliasCount: 0 });
138
+ } catch (error) {
139
+ return { ok: false, diagnostics: [bindingDiagnostic(label, `invalid verifier binding YAML: ${error.message}`)] };
140
+ }
141
+ const locations = new Map();
142
+ collectLocations(documents[0].contents, locator, "", locations);
143
+ const parsed = { file: label, locations, data };
144
+ const contract = await loadContract();
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, ""))] };
147
+ }
148
+ const identifier = new RegExp(contract.identifier_pattern);
149
+ const token = new RegExp(contract.token_pattern, "u");
150
+ const bindings = [];
151
+ const bindingIds = new Set();
152
+ for (let index = 0; index < data.bindings.length; index += 1) {
153
+ const value = data.bindings[index];
154
+ const pointer = `/bindings/${index}`;
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))] };
163
+ }
164
+ if (typeof value.id !== "string" || !identifier.test(value.id)) {
165
+ return { ok: false, diagnostics: [bindingDiagnostic(label, "binding id must be an exact dotted lowercase identifier", parsedLocation(parsed, `${pointer}/id`))] };
166
+ }
167
+ if (bindingIds.has(value.id)) return { ok: false, diagnostics: [bindingDiagnostic(label, `duplicate binding ID: ${value.id}`, parsedLocation(parsed, `${pointer}/id`))] };
168
+ bindingIds.add(value.id);
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;
192
+ }
193
+
194
+ if (typeof value.premise !== "string" || !identifier.test(value.premise)) {
195
+ return { ok: false, diagnostics: [bindingDiagnostic(label, "binding premise must be one exact authority ID", parsedLocation(parsed, `${pointer}/premise`))] };
196
+ }
197
+ if (!Array.isArray(value.targets) || value.targets.length === 0) {
198
+ return { ok: false, diagnostics: [bindingDiagnostic(label, "binding targets must be a non-empty exact ID list", parsedLocation(parsed, `${pointer}/targets`))] };
199
+ }
200
+ const targets = [];
201
+ const targetIds = new Set();
202
+ for (let targetIndex = 0; targetIndex < value.targets.length; targetIndex += 1) {
203
+ const target = value.targets[targetIndex];
204
+ const targetPointer = `${pointer}/targets/${targetIndex}`;
205
+ if (typeof target !== "string" || !identifier.test(target)) {
206
+ return { ok: false, diagnostics: [bindingDiagnostic(label, "binding targets must contain only exact authority IDs", parsedLocation(parsed, targetPointer))] };
207
+ }
208
+ if (targetIds.has(target)) return { ok: false, diagnostics: [bindingDiagnostic(label, `duplicate binding target: ${target}`, parsedLocation(parsed, targetPointer))] };
209
+ targetIds.add(target);
210
+ targets.push({ id: target, location: parsedLocation(parsed, targetPointer) });
211
+ }
212
+ if (!Number.isSafeInteger(value.minimum) || value.minimum <= 0) {
213
+ return { ok: false, diagnostics: [bindingDiagnostic(label, "binding minimum must be a positive safe integer", parsedLocation(parsed, `${pointer}/minimum`))] };
214
+ }
215
+ bindings.push({
216
+ ...value,
217
+ location: parsedLocation(parsed, `${pointer}/id`),
218
+ premiseLocation: parsedLocation(parsed, `${pointer}/premise`),
219
+ targetEntries: targets,
220
+ });
221
+ }
222
+ bindings.sort((left, right) => compareText(left.id, right.id));
223
+ return { ok: true, diagnostics: [], bindings };
224
+ }
225
+
226
+ function refused(compiled, diagnostics) {
227
+ return {
228
+ ok: false,
229
+ diagnostics,
230
+ fileCount: compiled?.fileCount ?? 0,
231
+ unitCount: 0,
232
+ auditBytes: 0,
233
+ audit: null,
234
+ partial: false,
235
+ };
236
+ }
237
+
238
+ function fingerprint(parts) {
239
+ const digest = createHash("sha256").update([FINGERPRINT_FORMAT, ...parts].join("\0")).digest("hex");
240
+ return `sha256:${digest}`;
241
+ }
242
+
243
+ function relatedLocation(role, location) {
244
+ return { role, location };
245
+ }
246
+
247
+ function compareRelatedLocation(left, right) {
248
+ return compareText(left.role, right.role)
249
+ || compareText(left.location.file, right.location.file)
250
+ || left.location.range.start.line - right.location.range.start.line
251
+ || left.location.range.start.column - right.location.range.start.column
252
+ || compareText(left.location.sourcePointer, right.location.sourcePointer);
253
+ }
254
+
255
+ function edgeKey(edge) {
256
+ return `${edge.source}\0${edge.type}\0${edge.target}`;
257
+ }
258
+
259
+ function auditBudgetDiagnostic(kind, required, admitted, location = null) {
260
+ return makeDiagnostic({
261
+ code: "AUTH_AUDIT_BUDGET",
262
+ file: location?.file ?? ".",
263
+ range: location?.range ?? { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
264
+ pointer: location?.sourcePointer ?? "",
265
+ reason: `complete authority audit requires ${required} ${kind} but the explicit budget is ${admitted}`,
266
+ repair: "increase the explicit audit budget; partial findings are forbidden",
267
+ });
268
+ }
269
+
270
+ function validBudget(value) {
271
+ return Number.isSafeInteger(value) && value > 0;
272
+ }
273
+
274
+ function validateBindingAuthority(binding, snapshot, compiled) {
275
+ if (binding.detector !== GRAPH_DETECTOR) return { ok: true };
276
+ const premise = snapshot.byId.get(binding.premise);
277
+ if (!premise || premise.unit.kind !== "rule" || premise.unit.lifecycle !== "active") {
278
+ return refused(compiled, [bindingDiagnostic(
279
+ binding.premiseLocation.file,
280
+ `binding premise must resolve to one exact active rule: ${binding.premise}`,
281
+ binding.premiseLocation,
282
+ )]);
283
+ }
284
+ for (const target of binding.targetEntries) {
285
+ const resolved = snapshot.byId.get(target.id);
286
+ if (!resolved || resolved.unit.kind !== "definition" || resolved.unit.lifecycle !== "active") {
287
+ return refused(compiled, [bindingDiagnostic(
288
+ target.location.file,
289
+ `binding target must resolve to one exact active definition: ${target.id}`,
290
+ target.location,
291
+ )]);
292
+ }
293
+ }
294
+ return { ok: true };
295
+ }
296
+
297
+ function graphFinding(binding, target, code, message, witness, primaryLocation, relatedLocations) {
298
+ const authorityRefs = [...new Set([binding.premise, target.id, ...(witness.incomingRefs ?? [])])].sort(compareText);
299
+ return {
300
+ code,
301
+ detector: GRAPH_DETECTOR,
302
+ binding: binding.id,
303
+ premise: binding.premise,
304
+ target: target.id,
305
+ basis: "configured",
306
+ policy: binding.policy,
307
+ fingerprint: fingerprint([code, GRAPH_DETECTOR, binding.id, binding.premise, target.id, `minimum=${binding.minimum}`]),
308
+ message,
309
+ authorityRefs,
310
+ primaryLocation,
311
+ relatedLocations: [...relatedLocations].sort(compareRelatedLocation),
312
+ witness,
313
+ };
314
+ }
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
+
486
+ export function canonicalAuditBytes(audit) {
487
+ return Buffer.byteLength(JSON.stringify(audit), "utf8");
488
+ }
489
+
490
+ export async function auditAuthoritySnapshot(authoritySnapshot, bindingsPath, { maxUnits, maxEdges, maxBytes, bindingLabel = null }) {
491
+ if (![maxUnits, maxEdges, maxBytes].every(validBudget)) {
492
+ return refused(null, [auditBudgetDiagnostic("positive safe-integer budget values", "all", "invalid")]);
493
+ }
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
+ }
498
+ const parsedBindings = await loadBindings(bindingsPath, bindingLabel);
499
+ if (!parsedBindings.ok) return refused(compiled, parsedBindings.diagnostics);
500
+ const basePath = authoritySnapshot.inputRoot;
501
+ for (const binding of parsedBindings.bindings) {
502
+ const validation = validateBindingAuthority(binding, authoritySnapshot, compiled);
503
+ if (validation.ok === false) return validation;
504
+ }
505
+
506
+ const traversedUnits = new Set();
507
+ const traversedEdges = new Set();
508
+ const findings = [];
509
+ for (const binding of parsedBindings.bindings) {
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);
515
+ }
516
+ }
517
+
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;
521
+ if (traversal.units > maxUnits) return refused(compiled, [auditBudgetDiagnostic("units", traversal.units, maxUnits, firstLocation)]);
522
+ if (traversal.edges > maxEdges) return refused(compiled, [auditBudgetDiagnostic("edges", traversal.edges, maxEdges, firstLocation)]);
523
+ const blocking = findings.some((finding) => finding.policy === "blocking");
524
+ const audit = {
525
+ format: AUDIT_FORMAT,
526
+ operationStatus: "completed",
527
+ blockingStatus: blocking ? "blocked" : "clear",
528
+ findings,
529
+ counts: {
530
+ bindings: {
531
+ configured: parsedBindings.bindings.length,
532
+ evaluated: parsedBindings.bindings.length,
533
+ },
534
+ returned: {
535
+ findings: findings.length,
536
+ },
537
+ traversal,
538
+ },
539
+ budgets: { maxUnits, maxEdges, maxBytes },
540
+ };
541
+ const auditBytes = canonicalAuditBytes(audit);
542
+ if (auditBytes > maxBytes) return refused(compiled, [auditBudgetDiagnostic("UTF-8 bytes", auditBytes, maxBytes, firstLocation)]);
543
+ return {
544
+ ok: true,
545
+ diagnostics: [],
546
+ fileCount: compiled.fileCount,
547
+ unitCount: compiled.unitCount,
548
+ auditBytes,
549
+ audit,
550
+ partial: false,
551
+ };
552
+ }
553
+
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 };