@nimiplatform/nimi-coding 0.4.0 → 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 (47) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +47 -35
  3. package/README.zh-CN.md +48 -36
  4. package/cli/commands/authority.mjs +80 -125
  5. package/cli/constants.mjs +1 -1
  6. package/cli/dev/check-authority-surface.mjs +17 -8
  7. package/cli/dev/check-product-conformance.mjs +4 -32
  8. package/cli/lib/authority/audit.mjs +262 -185
  9. package/cli/lib/authority/change-candidates.mjs +807 -0
  10. package/cli/lib/authority/closed-sets.mjs +7 -8
  11. package/cli/lib/authority/compile.mjs +6 -24
  12. package/cli/lib/authority/compiled-snapshot.mjs +143 -0
  13. package/cli/lib/authority/consumers.mjs +69 -19
  14. package/cli/lib/authority/diagnostics.mjs +7 -1
  15. package/cli/lib/authority/diff.mjs +77 -54
  16. package/cli/lib/authority/format.mjs +4 -32
  17. package/cli/lib/authority/git-snapshot.mjs +34 -379
  18. package/cli/lib/authority/graph.mjs +10 -10
  19. package/cli/lib/authority/impact.mjs +17 -144
  20. package/cli/lib/authority/review.mjs +33 -43
  21. package/cli/lib/authority/sarif.mjs +1 -8
  22. package/cli/lib/authority/source-yaml.mjs +0 -4
  23. package/cli/lib/authority/surface.mjs +19 -1
  24. package/cli/lib/authority/terms.mjs +10 -9
  25. package/cli/lib/authority/validate.mjs +6 -14
  26. package/cli/lib/entrypoints.mjs +2 -1
  27. package/contracts/authority-source.schema.yaml +0 -6
  28. package/contracts/authority-verifier-bindings.schema.yaml +21 -10
  29. package/methodology/authority-authoring.yaml +9 -26
  30. package/methodology/core.yaml +5 -3
  31. package/package.json +1 -1
  32. package/spec/authority-audit.yaml +56 -52
  33. package/spec/authority-authoring.yaml +3 -16
  34. package/spec/authority-change-candidates.yaml +146 -0
  35. package/spec/authority-consumers.yaml +7 -0
  36. package/spec/authority-discovery.yaml +2 -2
  37. package/spec/authority-graph.yaml +1 -1
  38. package/spec/authority-impact.yaml +16 -39
  39. package/spec/authority-review.yaml +29 -47
  40. package/spec/authority-terms.yaml +5 -1
  41. package/spec/product-scope.yaml +8 -7
  42. package/cli/lib/authority/evidence-snapshot.mjs +0 -1
  43. package/cli/lib/authority/evidence.mjs +0 -1432
  44. package/contracts/authority-evidence-bindings.schema.yaml +0 -53
  45. package/contracts/authority-evidence-probe-results.schema.yaml +0 -38
  46. package/contracts/authority-impact-dispositions.schema.yaml +0 -13
  47. package/spec/authority-evidence.yaml +0 -200
@@ -1,21 +1,11 @@
1
1
  import { Buffer } from "node:buffer";
2
- import { lstat, readFile, stat } from "node:fs/promises";
3
- import path from "node:path";
4
- import YAML from "yaml";
5
2
 
6
- import { compareText, makeDiagnostic, portablePath, sortDiagnostics } from "./diagnostics.mjs";
3
+ import { compareText, makeDiagnostic } from "./diagnostics.mjs";
7
4
  import { diffAuthorityPaths } from "./diff.mjs";
8
5
 
9
- const contractUrl = new URL("../../../contracts/authority-impact-dispositions.schema.yaml", import.meta.url);
10
- const IMPACT_FORMAT = "nimicoding.authority-impact/v1";
6
+ const IMPACT_FORMAT = "nimicoding.authority-impact/v2";
11
7
  const IMPACT_RELATIONS = new Set(["applies_to", "supersedes"]);
12
8
 
13
- async function rootFor(inputPath) {
14
- const absolute = path.resolve(inputPath);
15
- const info = await stat(absolute);
16
- return info.isDirectory() ? absolute : path.dirname(absolute);
17
- }
18
-
19
9
  function impactBudgetDiagnostic(requiredBytes, maxBytes) {
20
10
  return makeDiagnostic({
21
11
  code: "AUTH_IMPACT_BUDGET",
@@ -44,84 +34,11 @@ function overBudget(diffResult, impact, maxBytes) {
44
34
  payloadBytes,
45
35
  diff: null,
46
36
  impact: null,
37
+ partial: false,
47
38
  },
48
39
  };
49
40
  }
50
41
 
51
- function invalidDisposition(file, reason) {
52
- return makeDiagnostic({
53
- code: "AUTH_IMPACT_DISPOSITION_INVALID",
54
- file: path.basename(file),
55
- range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
56
- pointer: "",
57
- reason,
58
- repair: "declare each exact impacted rule consumer and test disposition with addressed status and explicit evidence",
59
- });
60
- }
61
-
62
- function exactObject(value, fields) {
63
- return value !== null && typeof value === "object" && !Array.isArray(value)
64
- && Object.keys(value).every((key) => fields.includes(key));
65
- }
66
-
67
- async function loadDispositions(file, label = file) {
68
- const absolute = path.resolve(file);
69
- let bytes;
70
- try {
71
- const info = await lstat(absolute);
72
- if (info.isSymbolicLink() || !info.isFile()) {
73
- return { ok: false, diagnostics: [invalidDisposition(label, "dispositions must be one regular non-symlink YAML file")] };
74
- }
75
- bytes = await readFile(absolute);
76
- } catch (error) {
77
- return { ok: false, diagnostics: [invalidDisposition(label, `disposition file is not readable: ${error.message}`)] };
78
- }
79
- const contractText = await readFile(contractUrl, "utf8");
80
- let text;
81
- try {
82
- text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
83
- } catch {
84
- return { ok: false, diagnostics: [invalidDisposition(label, "disposition bytes are not valid UTF-8")] };
85
- }
86
- const document = YAML.parseDocument(text, { uniqueKeys: true, version: "1.2" });
87
- if (document.errors.length > 0) return { ok: false, diagnostics: [invalidDisposition(label, document.errors[0].message)] };
88
- let data;
89
- try {
90
- data = document.toJS({ maxAliasCount: 0 });
91
- } catch (error) {
92
- return { ok: false, diagnostics: [invalidDisposition(label, error.message)] };
93
- }
94
- const contract = YAML.parse(contractText);
95
- if (!exactObject(data, contract.fields.top) || data.format !== contract.format || !Array.isArray(data.rules)) {
96
- return { ok: false, diagnostics: [invalidDisposition(label, "dispositions require the exact format and rules list")] };
97
- }
98
- const entries = new Map();
99
- const ruleIds = new Set();
100
- const identifier = new RegExp(contract.identifier_pattern);
101
- for (const rule of data.rules) {
102
- if (!exactObject(rule, contract.fields.rule) || !identifier.test(rule.id) || !Array.isArray(rule.consumers) || !exactObject(rule.test, contract.fields.test)) {
103
- return { ok: false, diagnostics: [invalidDisposition(label, "each rule requires exact id, consumers, and test fields")] };
104
- }
105
- if (ruleIds.has(rule.id)) return { ok: false, diagnostics: [invalidDisposition(label, `duplicate rule disposition: ${rule.id}`)] };
106
- ruleIds.add(rule.id);
107
- for (const consumer of rule.consumers) {
108
- if (!exactObject(consumer, contract.fields.consumer) || !identifier.test(consumer.scope) || consumer.status !== contract.status || typeof consumer.evidence !== "string" || consumer.evidence.trim() !== consumer.evidence || consumer.evidence.length === 0) {
109
- return { ok: false, diagnostics: [invalidDisposition(label, `invalid consumer disposition for ${rule.id}`)] };
110
- }
111
- const key = `${rule.id}\0consumer\0${consumer.scope}`;
112
- if (entries.has(key)) return { ok: false, diagnostics: [invalidDisposition(label, `duplicate disposition for ${rule.id} consumer ${consumer.scope}`)] };
113
- entries.set(key, { ruleId: rule.id, type: "consumer", target: consumer.scope, status: consumer.status, evidence: consumer.evidence });
114
- }
115
- if (rule.test.status !== contract.status || typeof rule.test.evidence !== "string" || rule.test.evidence.trim() !== rule.test.evidence || rule.test.evidence.length === 0) {
116
- return { ok: false, diagnostics: [invalidDisposition(label, `invalid test disposition for ${rule.id}`)] };
117
- }
118
- const testKey = `${rule.id}\0test\0${rule.id}`;
119
- if (entries.has(testKey)) return { ok: false, diagnostics: [invalidDisposition(label, `duplicate rule disposition: ${rule.id}`)] };
120
- entries.set(testKey, { ruleId: rule.id, type: "test", target: rule.id, status: rule.test.status, evidence: rule.test.evidence });
121
- }
122
- return { ok: true, diagnostics: [], entries };
123
- }
124
-
125
42
  function allUnits(diffResult) {
126
43
  const units = new Map();
127
44
  for (const compiled of [diffResult.before, diffResult.after]) {
@@ -162,7 +79,7 @@ function impactedIds(diffResult) {
162
79
  return [...selected].sort(compareText);
163
80
  }
164
81
 
165
- function requiredObligations(diffResult, impacted) {
82
+ function declaredObligations(diffResult, impacted) {
166
83
  const before = new Map(diffResult.before.ir.units.map((unit) => [unit.id, unit]));
167
84
  const after = new Map(diffResult.after.ir.units.map((unit) => [unit.id, unit]));
168
85
  const obligations = [];
@@ -176,77 +93,33 @@ function requiredObligations(diffResult, impacted) {
176
93
  return obligations;
177
94
  }
178
95
 
179
- async function unitLocation(diffResult, id) {
180
- for (const [compiled, inputPath] of [[diffResult.after, diffResult.afterPath], [diffResult.before, diffResult.beforePath]]) {
181
- const index = compiled.ir.units.findIndex((unit) => unit.id === id);
182
- if (index < 0) continue;
183
- const mapped = compiled.sourceMap.fields[`/units/${index}/id`];
184
- return { ...mapped, file: portablePath(mapped.file, await rootFor(inputPath)) };
185
- }
186
- return { file: ".", range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } }, sourcePointer: "/id" };
187
- }
188
-
189
- export async function impactAuthorityPaths(beforePath, afterPath, dispositionsPath, { maxBytes = null, dispositionLabel = null } = {}) {
190
- const diffResult = await diffAuthorityPaths(beforePath, afterPath);
191
- if (!diffResult.ok) return { ...diffResult, impact: null };
192
- diffResult.beforePath = beforePath;
193
- diffResult.afterPath = afterPath;
194
- const parsed = await loadDispositions(dispositionsPath, dispositionLabel ?? dispositionsPath);
195
- if (!parsed.ok) {
196
- const budget = overBudget(diffResult, null, maxBytes);
197
- if (budget.failure) return budget.failure;
198
- return {
199
- ok: false,
200
- diagnostics: parsed.diagnostics,
201
- fileCount: diffResult.fileCount,
202
- unitCount: 0,
203
- payloadBytes: budget.payloadBytes,
204
- diff: diffResult.diff,
205
- impact: null,
206
- };
207
- }
96
+ export function impactAuthorityDiff(diffResult, { maxBytes = null } = {}) {
97
+ if (!diffResult.ok) return { ...diffResult, impact: null, partial: false };
208
98
  const impacted = impactedIds(diffResult);
209
99
  const units = allUnits(diffResult);
210
- const required = requiredObligations(diffResult, impacted);
211
- const requiredKeys = new Set(required.map((item) => `${item.ruleId}\0${item.type}\0${item.target}`));
212
- const diagnostics = [];
213
- for (const obligation of required) {
214
- const key = `${obligation.ruleId}\0${obligation.type}\0${obligation.target}`;
215
- if (parsed.entries.has(key)) continue;
216
- const mapped = await unitLocation(diffResult, obligation.ruleId);
217
- diagnostics.push(makeDiagnostic({
218
- code: "AUTH_IMPACT_UNDISPOSED",
219
- file: mapped.file,
220
- range: mapped.range,
221
- pointer: mapped.sourcePointer,
222
- reason: `${obligation.type} obligation is not disposed for ${obligation.ruleId}: ${obligation.target}`,
223
- repair: "add an addressed disposition with explicit evidence; do not infer completion",
224
- }));
225
- }
226
- for (const [key, disposition] of parsed.entries) {
227
- if (requiredKeys.has(key)) continue;
228
- diagnostics.push(invalidDisposition(dispositionLabel ?? dispositionsPath, `disposition does not match a required impact obligation: ${disposition.ruleId} ${disposition.type} ${disposition.target}`));
229
- }
230
- const disposed = required.map((obligation) => ({
231
- ...obligation,
232
- disposition: parsed.entries.get(`${obligation.ruleId}\0${obligation.type}\0${obligation.target}`) ?? null,
233
- }));
234
100
  const impact = {
235
101
  format: IMPACT_FORMAT,
102
+ analysisStatus: "completed",
236
103
  changedUnits: [...new Set(impactingChanges(diffResult).map((change) => change.unitId))].sort(compareText),
237
104
  impactedUnits: impacted.map((id) => ({ id, kind: units.get(id)?.kind ?? null })),
238
- obligations: disposed,
239
- complete: diagnostics.length === 0,
105
+ obligations: declaredObligations(diffResult, impacted),
240
106
  };
241
107
  const budget = overBudget(diffResult, impact, maxBytes);
242
108
  if (budget.failure) return budget.failure;
243
109
  return {
244
- ok: diagnostics.length === 0,
245
- diagnostics: sortDiagnostics(diagnostics),
110
+ ok: true,
111
+ diagnostics: [],
246
112
  fileCount: diffResult.fileCount,
247
113
  unitCount: impacted.length,
248
114
  payloadBytes: budget.payloadBytes,
249
115
  diff: diffResult.diff,
250
116
  impact,
117
+ partial: false,
251
118
  };
252
119
  }
120
+
121
+ export async function impactAuthorityPaths(beforePath, afterPath, options = {}) {
122
+ return impactAuthorityDiff(await diffAuthorityPaths(beforePath, afterPath), options);
123
+ }
124
+
125
+ export { IMPACT_FORMAT };
@@ -1,11 +1,12 @@
1
1
  import { Buffer } from "node:buffer";
2
- import { mkdir, writeFile } from "node:fs/promises";
2
+ import { mkdir, realpath, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
5
- import { auditAuthorityPath } from "./audit.mjs";
5
+ import { auditAuthoritySnapshot } from "./audit.mjs";
6
6
  import { compileAuthorityPath } from "./compile.mjs";
7
- import { makeDiagnostic, portablePath, sortDiagnostics } from "./diagnostics.mjs";
8
- import { diffAuthorityPaths } from "./diff.mjs";
7
+ import { createCompiledAuthoritySnapshot } from "./compiled-snapshot.mjs";
8
+ import { isPathWithin, makeDiagnostic, portablePath, sortDiagnostics } from "./diagnostics.mjs";
9
+ import { diffAuthoritySnapshots } from "./diff.mjs";
9
10
  import { AuthorityInputError } from "./format.mjs";
10
11
  import {
11
12
  AuthorityReviewRefusal,
@@ -13,9 +14,9 @@ import {
13
14
  captureStableRegularFile,
14
15
  withGitAuthoritySnapshots,
15
16
  } from "./git-snapshot.mjs";
16
- import { impactAuthorityPaths } from "./impact.mjs";
17
+ import { impactAuthorityDiff } from "./impact.mjs";
17
18
 
18
- const REVIEW_FORMAT = "nimicoding.authority-review/v1";
19
+ const REVIEW_FORMAT = "nimicoding.authority-review/v2";
19
20
 
20
21
  function validBudget(value) {
21
22
  return Number.isSafeInteger(value) && value > 0;
@@ -39,8 +40,8 @@ function reviewBudgetDiagnostic(requiredBytes, maxBytes) {
39
40
  file: ".",
40
41
  range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
41
42
  pointer: "",
42
- reason: `complete authority review requires ${requiredBytes} UTF-8 bytes but max-bytes is ${maxBytes}`,
43
- repair: "increase the explicit review byte budget; partial change review is forbidden",
43
+ reason: `complete authority review analysis requires ${requiredBytes} UTF-8 bytes but max-bytes is ${maxBytes}`,
44
+ repair: "increase the explicit review byte budget; partial change analysis is forbidden",
44
45
  });
45
46
  }
46
47
 
@@ -88,11 +89,12 @@ async function compileSnapshot(root, side) {
88
89
  }
89
90
  }
90
91
 
91
- function inputLabel(file, repository, fallback) {
92
- const absolute = path.resolve(process.cwd(), file);
92
+ async function inputLabel(file, repository, fallback) {
93
+ const requested = path.resolve(process.cwd(), file);
94
+ const absolute = await realpath(requested).catch(() => requested);
93
95
  const relative = path.relative(repository, absolute);
94
- if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) return relative.split(path.sep).join(path.posix.sep);
95
- return path.basename(absolute) || fallback;
96
+ if (relative && isPathWithin(repository, absolute)) return relative.split(path.sep).join(path.posix.sep);
97
+ return path.basename(requested) || fallback;
96
98
  }
97
99
 
98
100
  async function captureReviewInput(file, temporaryRoot, name, label) {
@@ -113,23 +115,13 @@ async function captureReviewInput(file, temporaryRoot, name, label) {
113
115
  return target;
114
116
  }
115
117
 
116
- function invalidDisposition(result) {
117
- return result.diagnostics.some((diagnostic) => diagnostic.code === "AUTH_IMPACT_DISPOSITION_INVALID");
118
- }
119
-
120
118
  export function canonicalReviewBytes(review) {
121
119
  return Buffer.byteLength(JSON.stringify(review), "utf8");
122
120
  }
123
121
 
124
- function component(operationStatus, complete) {
125
- return { operationStatus, complete };
126
- }
127
-
128
- async function composeReview(snapshot, bindingsPath, dispositionsPath, budgets) {
129
- const bindingsLabel = inputLabel(bindingsPath, snapshot.repository, "bindings.yaml");
130
- const dispositionsLabel = inputLabel(dispositionsPath, snapshot.repository, "dispositions.yaml");
122
+ async function composeReview(snapshot, bindingsPath, budgets) {
123
+ const bindingsLabel = await inputLabel(bindingsPath, snapshot.repository, "bindings.yaml");
131
124
  const capturedBindings = await captureReviewInput(bindingsPath, snapshot.temporaryRoot, "bindings", bindingsLabel);
132
- const capturedDispositions = await captureReviewInput(dispositionsPath, snapshot.temporaryRoot, "dispositions", dispositionsLabel);
133
125
  const [baseCompiled, worktreeCompiled] = await Promise.all([
134
126
  compileSnapshot(snapshot.base.root, "base"),
135
127
  compileSnapshot(snapshot.worktree.root, "worktree"),
@@ -140,26 +132,23 @@ async function composeReview(snapshot, bindingsPath, dispositionsPath, budgets)
140
132
  throw new Error("captured authority file counts do not match complete compiler admission");
141
133
  }
142
134
 
143
- const [diffResult, impactResult, auditResult] = await Promise.all([
144
- diffAuthorityPaths(snapshot.base.root, snapshot.worktree.root, { maxBytes: budgets.maxBytes }),
145
- impactAuthorityPaths(snapshot.base.root, snapshot.worktree.root, capturedDispositions, { maxBytes: budgets.maxBytes, dispositionLabel: dispositionsLabel }),
146
- auditAuthorityPath(snapshot.worktree.root, capturedBindings, { ...budgets, bindingLabel: bindingsLabel }),
147
- ]);
135
+ const baseSnapshot = createCompiledAuthoritySnapshot(baseCompiled.result, snapshot.base.root);
136
+ const worktreeSnapshot = createCompiledAuthoritySnapshot(worktreeCompiled.result, snapshot.worktree.root);
137
+ const diffResult = diffAuthoritySnapshots(baseSnapshot, worktreeSnapshot, { maxBytes: budgets.maxBytes });
148
138
  if (!diffResult.ok || diffResult.diff === null) return refused(diffResult.diagnostics);
149
- if (impactResult.impact === null || invalidDisposition(impactResult)) return refused(impactResult.diagnostics);
139
+ const impactResult = impactAuthorityDiff(diffResult, { maxBytes: budgets.maxBytes });
140
+ if (!impactResult.ok || impactResult.impact === null) return refused(impactResult.diagnostics);
141
+ const auditResult = await auditAuthoritySnapshot(worktreeSnapshot, capturedBindings, { ...budgets, bindingLabel: bindingsLabel });
150
142
  if (auditResult.audit === null) return refused(auditResult.diagnostics);
151
143
  if (JSON.stringify(diffResult.diff) !== JSON.stringify(impactResult.diff)) throw new Error("diff and impact oracles returned different semantic diffs for immutable snapshots");
152
144
 
153
- const complete = impactResult.impact.complete && auditResult.audit.complete;
154
- const policyStatus = auditResult.audit.policyStatus === "blocked"
155
- ? "blocked"
156
- : complete ? "passed" : "indeterminate";
157
145
  const review = {
158
146
  format: REVIEW_FORMAT,
159
147
  operationStatus: "completed",
160
- policyStatus,
161
- complete,
148
+ analysisComplete: true,
162
149
  partial: false,
150
+ businessSemanticStatus: "not_evaluated",
151
+ implementationConformanceStatus: "not_evaluated",
163
152
  snapshots: {
164
153
  base: {
165
154
  commitOid: snapshot.baseOid,
@@ -180,10 +169,11 @@ async function composeReview(snapshot, bindingsPath, dispositionsPath, budgets)
180
169
  },
181
170
  },
182
171
  components: {
183
- snapshots: component("completed", true),
184
- diff: component("completed", true),
185
- impact: component("completed", impactResult.impact.complete),
186
- audit: component(auditResult.audit.operationStatus, auditResult.audit.complete),
172
+ snapshotCapture: { status: "captured" },
173
+ corpusAdmission: { status: "admitted" },
174
+ semanticDiff: { status: "computed" },
175
+ declaredImpact: { status: "computed" },
176
+ configuredAudit: { status: "evaluated", blockingStatus: auditResult.audit.blockingStatus },
187
177
  },
188
178
  diff: diffResult.diff,
189
179
  impact: impactResult.impact,
@@ -194,7 +184,7 @@ async function composeReview(snapshot, bindingsPath, dispositionsPath, budgets)
194
184
  if (reviewBytes > budgets.maxBytes) return refused([reviewBudgetDiagnostic(reviewBytes, budgets.maxBytes)], reviewBytes);
195
185
  return {
196
186
  ok: true,
197
- diagnostics: sortDiagnostics(impactResult.diagnostics),
187
+ diagnostics: [],
198
188
  fileCount: snapshot.base.fileCount + snapshot.worktree.fileCount,
199
189
  unitCount: worktreeCompiled.result.unitCount,
200
190
  reviewBytes,
@@ -203,7 +193,7 @@ async function composeReview(snapshot, bindingsPath, dispositionsPath, budgets)
203
193
  };
204
194
  }
205
195
 
206
- export async function reviewAuthorityRepository(repositoryPath, baseRef, bindingsPath, dispositionsPath, budgets, options = {}) {
196
+ export async function reviewAuthorityRepository(repositoryPath, baseRef, bindingsPath, budgets, options = {}) {
207
197
  if (![budgets?.maxUnits, budgets?.maxEdges, budgets?.maxBytes].every(validBudget)) {
208
198
  return refused([reviewBudgetDiagnostic(1, budgets?.maxBytes ?? 0)]);
209
199
  }
@@ -212,7 +202,7 @@ export async function reviewAuthorityRepository(repositoryPath, baseRef, binding
212
202
  repositoryPath,
213
203
  baseRef,
214
204
  hooks: options.snapshotHooks ?? null,
215
- }, (snapshot) => composeReview(snapshot, bindingsPath, dispositionsPath, budgets));
205
+ }, (snapshot) => composeReview(snapshot, bindingsPath, budgets));
216
206
  } catch (error) {
217
207
  if (error instanceof AuthorityReviewRefusal) return refused([authorityReviewRefusalDiagnostic(error)]);
218
208
  throw error;
@@ -56,8 +56,6 @@ function relatedLocations(values = []) {
56
56
  }
57
57
 
58
58
  function itemLevel(kind, item) {
59
- if (kind === "observation") return "note";
60
- if (kind === "gap") return "error";
61
59
  return item.policy === "blocking" ? "error" : "warning";
62
60
  }
63
61
 
@@ -124,15 +122,12 @@ function invocationProperties(result) {
124
122
  if (!result.audit) {
125
123
  return {
126
124
  operationStatus: "refused",
127
- policyStatus: "indeterminate",
128
- complete: false,
129
125
  };
130
126
  }
131
127
  return {
132
128
  format: result.audit.format,
133
129
  operationStatus: result.audit.operationStatus,
134
- policyStatus: result.audit.policyStatus,
135
- complete: result.audit.complete,
130
+ blockingStatus: result.audit.blockingStatus,
136
131
  counts: copy(result.audit.counts),
137
132
  budgets: copy(result.audit.budgets),
138
133
  };
@@ -146,9 +141,7 @@ export function authorityAuditResultToSarif(result, { toolVersion }) {
146
141
 
147
142
  const results = result.audit
148
143
  ? [
149
- ...(result.audit.observations ?? []).map((item) => auditItemResult("observation", item)),
150
144
  ...(result.audit.findings ?? []).map((item) => auditItemResult("finding", item)),
151
- ...(result.audit.gaps ?? []).map((item) => auditItemResult("gap", item)),
152
145
  ]
153
146
  : (result.diagnostics ?? []).map(diagnosticResult);
154
147
  const rules = rulesFor(results);
@@ -12,9 +12,6 @@ export async function loadAuthorityContract() {
12
12
  if (!contractPromise) {
13
13
  contractPromise = readFile(CONTRACT_PATH, "utf8").then((text) => {
14
14
  const value = YAML.parse(text);
15
- // v2 is the current authoring contract; v1 remains readable as a
16
- // migration input, which the contract itself records rather than the
17
- // loader inferring it.
18
15
  if (value?.version !== 1 || value?.contract?.id !== "nimicoding.authority-source.v2") {
19
16
  throw new Error("installed authority source contract is invalid");
20
17
  }
@@ -193,7 +190,6 @@ export function parseRestrictedYaml(text, absolutePath, options = {}) {
193
190
  };
194
191
  }
195
192
 
196
- export const AUTHORITY_FORMAT_V1 = "nimicoding.authority/v1";
197
193
  export const AUTHORITY_FORMAT_V2 = "nimicoding.authority/v2";
198
194
  export const YAML_CONTAINER_KEY_ORDER = ["format", "owner", "scope", "units"];
199
195
  export const YAML_UNIT_KEY_ORDER = [
@@ -9,12 +9,21 @@
9
9
  // do not. Each contract states which it is rather than the name implying it.
10
10
  export const REPOSITORY_BOUND_COMMANDS = ["anchors", "consumers", "terms", "closed-sets"];
11
11
 
12
+ // audit reads one complete authority input plus one explicit bindings file. It
13
+ // is corpus-bound, not repository-bound, but its public parser/help/contract
14
+ // surface is enforced by the same consistency gate.
15
+ export const CORPUS_BOUND_COMMANDS = ["audit"];
16
+ export const ENFORCED_AUTHORITY_COMMANDS = [
17
+ ...REPOSITORY_BOUND_COMMANDS,
18
+ ...CORPUS_BOUND_COMMANDS,
19
+ ];
20
+
12
21
  // fmt is not repository-bound, but its flags and the formats it admits are
13
22
  // exactly what the authoring contract promises, so it is compared too.
14
23
  export const AUTHORING_SURFACE = {
15
24
  fmt: {
16
25
  contract: "nimicoding.authority-authoring.v2",
17
- booleanFlags: ["json", "check", "upgrade"],
26
+ booleanFlags: ["json", "check"],
18
27
  valueFlags: [],
19
28
  integerFlags: [],
20
29
  repeatableFlags: [],
@@ -22,6 +31,15 @@ export const AUTHORING_SURFACE = {
22
31
  };
23
32
 
24
33
  export const AUTHORITY_SURFACE = {
34
+ audit: {
35
+ contract: "nimicoding.authority-audit.v2",
36
+ reportFormat: "nimicoding.authority-audit/v2",
37
+ corpus: "authority-input",
38
+ booleanFlags: ["json", "sarif"],
39
+ valueFlags: ["bindings"],
40
+ integerFlags: ["max-units", "max-edges", "max-bytes"],
41
+ repeatableFlags: [],
42
+ },
25
43
  anchors: {
26
44
  contract: "nimicoding.authority-anchors.v1",
27
45
  reportFormat: "nimicoding.authority-anchors/v1",
@@ -15,12 +15,13 @@ import { classifyTrackedTextFiles, trackedTextInventory } from "./tracked-text.m
15
15
  // is decided by content, not by this list.
16
16
  const CODE_EXTENSION_SET = new Set(CODE_EXTENSIONS);
17
17
  const TERMS_FORMAT = "nimicoding.authority-terms/v1";
18
- const TERM_CLASS_ORDER = ["snake_case", "camelCase", "UPPER_SNAKE", "PascalCase", "dotted"];
18
+ const TERM_CLASS_ORDER = ["snake_case", "camelCase", "UPPER_SNAKE", "ALL_CAPS", "PascalCase", "dotted"];
19
19
  const TERM_EXPRESSIONS = [
20
20
  ["snake_case", /[a-z][a-z0-9]*(?:_[a-z0-9]+)+/gu],
21
21
  ["camelCase", /[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)+/gu],
22
22
  ["UPPER_SNAKE", /[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+/gu],
23
- ["PascalCase", /[A-Z][a-z0-9]+(?:[A-Z][a-z0-9]*)+/gu],
23
+ ["ALL_CAPS", /[A-Z]{2,}/gu],
24
+ ["PascalCase", /(?:[A-Z][a-z0-9]+(?:[A-Z][a-z0-9]*)+|[A-Z]{2,}[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*)/gu],
24
25
  ["dotted", /[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+/gu],
25
26
  ];
26
27
  const AUTHORITY_FIELDS = ["title", "meaning", "statement", "condition", "failure"];
@@ -33,7 +34,8 @@ export const AUTHORITY_TERM_GRAMMAR_HELP = [
33
34
  " snake_case: [a-z][a-z0-9]*(_[a-z0-9]+)+",
34
35
  " camelCase: [a-z][a-z0-9]*([A-Z][a-z0-9]*)+",
35
36
  " UPPER_SNAKE: [A-Z][A-Z0-9]*(_[A-Z0-9]+)+",
36
- " PascalCase: [A-Z][a-z0-9]+([A-Z][a-z0-9]*)+",
37
+ " ALL_CAPS: [A-Z]{2,} (lexical shape only; no acronym inference)",
38
+ " PascalCase: [A-Z][a-z0-9]+([A-Z][a-z0-9]*)+ or [A-Z]{2,}[a-z][a-z0-9]*([A-Z][a-z0-9]*)*",
37
39
  " dotted: [A-Za-z0-9_-]+(\\.[A-Za-z0-9_-]+)+, excluding final segments that are code extensions.",
38
40
  "Matches require ASCII identifier boundaries and are excluded from longer path-like tokens containing '/'.",
39
41
  ].join("\n");
@@ -85,11 +87,6 @@ function inputDiagnostic(code, reason, file = ".") {
85
87
  );
86
88
  }
87
89
 
88
- function isWithin(root, candidate) {
89
- const relative = path.relative(root, candidate);
90
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
91
- }
92
-
93
90
  function portableRelative(repository, candidate) {
94
91
  return path.relative(repository, candidate).split(path.sep).join(path.posix.sep);
95
92
  }
@@ -371,10 +368,14 @@ export async function termsAuthorityRepository(repositoryPath, specPath, { term
371
368
  let codeReferences = [];
372
369
  let codeInventory = null;
373
370
  if (code) {
371
+ const gitlinks = new Set(inventory.gitlinks);
374
372
  const classified = await classifyTrackedTextFiles(
375
373
  inventory.repository,
376
374
  inventory.files,
377
- (file) => corpusFile(file, corpus) ? "authority-corpus" : null,
375
+ (file) => {
376
+ if (gitlinks.has(file)) return "gitlink";
377
+ return corpusFile(file, corpus) ? "authority-corpus" : null;
378
+ },
378
379
  );
379
380
  if (classified.error !== undefined) {
380
381
  return refused([inputDiagnostic("AUTH_TERMS_CODE_INPUT_INVALID", classified.error)], { fileCount: compiled.fileCount });
@@ -1,5 +1,5 @@
1
1
  import { compareText, makeDiagnostic, relatedLocation, REPAIRS, sortDiagnostics, sourcePointer } from "./diagnostics.mjs";
2
- import { AUTHORITY_FORMAT_V1, AUTHORITY_FORMAT_V2 } from "./source-yaml.mjs";
2
+ import { AUTHORITY_FORMAT_V2 } from "./source-yaml.mjs";
3
3
 
4
4
  function isObject(value) {
5
5
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -49,12 +49,7 @@ function structuralDiagnostics(source, contract) {
49
49
  }
50
50
  }
51
51
  for (const [field, admitted] of [
52
- // The authoring format plus whatever the contract records as a readable
53
- // migration input, so fmt --upgrade and history diffs keep working without
54
- // the loader inventing a compatibility list of its own.
55
- ...(source.profile === "markdown"
56
- ? [["format", [contract.format, ...(contract.migration_inputs?.format ? [contract.migration_inputs.format] : [])]]]
57
- : []),
52
+ ...(source.profile === "markdown" ? [["format", [contract.format]]] : []),
58
53
  ["kind", contract.kinds],
59
54
  ["lifecycle", contract.lifecycles],
60
55
  ]) {
@@ -90,19 +85,16 @@ function documentStructuralDiagnostics(document, contract) {
90
85
  const diagnostics = [];
91
86
  const data = document.data;
92
87
  if (!isObject(data)) return diagnostics;
93
- const v2 = data.format === AUTHORITY_FORMAT_V2;
94
88
  // Container field admission comes from the contract, so changing what a v2
95
89
  // container may carry is a contract edit that the source-contract gate sees,
96
90
  // not a constant edited here alone.
97
- const allowed = new Set(v2 ? contract.fields.yaml_document : contract.fields.yaml_document_migration_input);
91
+ const allowed = new Set(contract.fields.yaml_document);
98
92
  for (const key of Object.keys(data)) {
99
- if (!allowed.has(key)) add(diagnostics, document, "AUTH_UNKNOWN_FIELD", `/${key}`, `unknown authority document field: ${key}`, v2
100
- ? "remove the field; v2 YAML authority documents contain only format, optional owner, optional scope, and units"
101
- : "remove the field; v1 YAML authority documents contain only format and units");
93
+ if (!allowed.has(key)) add(diagnostics, document, "AUTH_UNKNOWN_FIELD", `/${key}`, `unknown authority document field: ${key}`, "remove the field; v2 YAML authority documents contain only format, optional owner, optional scope, and units");
102
94
  }
103
95
  if (!Object.hasOwn(data, "format")) add(diagnostics, document, "AUTH_FIELD_REQUIRED", "/format", "format is required before the formatter can select the YAML container profile", REPAIRS.required, { missing: true });
104
- else if (![contract.format, contract.migration_inputs?.format].includes(data.format)) add(diagnostics, document, "AUTH_FIELD_INVALID", "/format", `format must be one of: ${contract.format}, ${contract.migration_inputs?.format}`, REPAIRS.invalid);
105
- if (v2) {
96
+ else if (data.format !== contract.format) add(diagnostics, document, "AUTH_FIELD_INVALID", "/format", `format must be: ${contract.format}`, REPAIRS.invalid);
97
+ if (data.format === AUTHORITY_FORMAT_V2) {
106
98
  const identifier = new RegExp(contract.identifier_pattern);
107
99
  if (Object.hasOwn(data, "owner") && (typeof data.owner !== "string" || !identifier.test(data.owner))) {
108
100
  add(diagnostics, document, "AUTH_FIELD_INVALID", "/owner", "container owner must be a dot-separated lower-case identifier", REPAIRS.invalid);
@@ -22,10 +22,11 @@ function authorityAuthoringLines() {
22
22
  "- For canonical authority authoring, read only `.nimi/methodology/authority-authoring.yaml`, the affected authority files or bounded task context, and CLI diagnostics.",
23
23
  "- Use `nimicoding authority context <path> <id> --max-units <n> --max-bytes <n> --json` only for the complete declared outgoing interpretation closure; it is not complete task context, and failure never permits guessed or partial context.",
24
24
  "- Use `nimicoding authority diff` and `authority impact` with explicit `--max-bytes`; impact reports declared review obligations and does not prove implementation, consumers, or tests are synchronized.",
25
+ "- Use `nimicoding authority change-candidates` only with explicit channels and budgets; its complete union is recall input, never conflict, retirement, absence, authority, or conformance judgment.",
25
26
  "- Under `.nimi/spec/**`, author only closed multi-unit `*.authority.yaml` containers or single-unit `*.authority.md`; historical document formats are unsupported and never inferred.",
26
27
  "- Run `nimicoding authority fmt` on each changed file, then `nimicoding authority check` on the complete authority input set.",
27
28
  "- Never bypass a failure with inferred or fallback semantics; choose repair values only from product/task authority.",
28
- "- Keep derived and verification evidence under `.nimi/local/**`; it is never product authority.",
29
+ "- Keep derived and local verification output under `.nimi/local/**`; it is never product authority.",
29
30
  ];
30
31
  }
31
32
 
@@ -4,11 +4,6 @@ contract:
4
4
  owner: nimi-coding
5
5
  visibility: package_internal
6
6
  format: nimicoding.authority/v2
7
- migration_inputs:
8
- format: nimicoding.authority/v1
9
- admitted_by: [fmt --upgrade, diff, compile, check, impact]
10
- rationale: history_snapshots_and_migration_inputs_are_read_not_authored
11
- compatibility_product_surface: none_v1_is_not_a_current_authoring_format
12
7
  file_suffixes: [.authority.yaml, .authority.md]
13
8
  yaml_document:
14
9
  allowed_fields: [format, owner, scope, units]
@@ -40,7 +35,6 @@ text_values:
40
35
  line_break_code_points: [U+000A, U+000B, U+000C, U+000D, U+0085, U+2028, U+2029]
41
36
  fields:
42
37
  yaml_document: [format, owner, scope, units]
43
- yaml_document_migration_input: [format, units]
44
38
  unit_common: [id, kind, owner, lifecycle, title, relations]
45
39
  active_definition_required: [meaning]
46
40
  active_rule_required: [modality, scope, statement, condition, failure]
@@ -1,20 +1,31 @@
1
- version: 1
1
+ version: 2
2
2
  contract:
3
- id: nimicoding.authority-verifier-bindings.v1
3
+ id: nimicoding.authority-verifier-bindings.v2
4
4
  owner: nimi-coding
5
5
  visibility: package_internal
6
- format: nimicoding.authority-verifier-bindings/v1
7
- detector: minimum-independent-incoming-reference/v1
6
+ format: nimicoding.authority-verifier-bindings/v2
7
+ detectors:
8
+ - minimum-independent-incoming-reference/v1
9
+ - exact-lexical-invariant/v1
8
10
  identifier_pattern: '^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z](?:[a-z0-9-]*[a-z0-9])?)+$'
11
+ token_pattern: '^[\p{L}\p{N}](?:[\p{L}\p{N}_.-]*[\p{L}\p{N}])?$'
9
12
  policies: [advisory, blocking]
13
+ lexical_matches: [token, phrase]
14
+ lexical_expectations: [present, absent]
10
15
  fields:
11
- top: [format, required_bindings, bindings]
12
- binding: [id, detector, premise, targets, minimum, policy]
16
+ top: [format, bindings]
17
+ graph_binding: [id, detector, premise, targets, minimum, policy]
18
+ lexical_binding: [id, detector, term, match, expectation, policy]
13
19
  constraints:
14
- required_bindings: non_empty_unique_exact_binding_IDs
20
+ bindings: non_empty_closed_detector_binding_list
15
21
  binding_ID: unique_exact_dotted_lowercase_identifier
16
- premise: exact_active_rule_ID
17
- targets: non_empty_unique_exact_active_definition_IDs
18
- minimum: positive_safe_integer
22
+ graph_premise: exact_active_rule_ID
23
+ graph_targets: non_empty_unique_exact_active_definition_IDs
24
+ graph_minimum: positive_safe_integer
25
+ lexical_term: non_empty_trimmed_single_line_exact_text
26
+ lexical_token: token_pattern_when_match_is_token
27
+ lexical_phrase: exact_case_sensitive_code_point_substring
28
+ lexical_corpus: active_authority_only
29
+ lexical_fields: [meaning, statement, condition, failure]
19
30
  policy: advisory_or_blocking
20
31
  extra_fields: forbidden