@nimiplatform/nimi-coding 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +92 -55
- package/README.zh-CN.md +93 -56
- package/cli/commands/authority.mjs +80 -125
- package/cli/commands/clear.mjs +3 -2
- package/cli/commands/code.mjs +225 -0
- package/cli/commands/start.mjs +3 -2
- package/cli/constants.mjs +2 -1
- package/cli/dev/check-authority-surface.mjs +17 -8
- package/cli/dev/check-product-conformance.mjs +4 -32
- package/cli/dev/run-cli-smoke.mjs +6 -0
- package/cli/help.mjs +8 -3
- package/cli/index.mjs +3 -2
- package/cli/lib/authority/audit.mjs +262 -185
- package/cli/lib/authority/change-candidates.mjs +807 -0
- package/cli/lib/authority/closed-sets.mjs +7 -8
- package/cli/lib/authority/compile.mjs +6 -24
- package/cli/lib/authority/compiled-snapshot.mjs +143 -0
- package/cli/lib/authority/consumers.mjs +69 -19
- package/cli/lib/authority/diagnostics.mjs +10 -2
- package/cli/lib/authority/diff.mjs +77 -54
- package/cli/lib/authority/format.mjs +4 -32
- package/cli/lib/authority/git-snapshot.mjs +34 -379
- package/cli/lib/authority/graph.mjs +10 -10
- package/cli/lib/authority/impact.mjs +17 -144
- package/cli/lib/authority/review.mjs +33 -43
- package/cli/lib/authority/sarif.mjs +1 -8
- package/cli/lib/authority/source-yaml.mjs +0 -4
- package/cli/lib/authority/surface.mjs +19 -1
- package/cli/lib/authority/terms.mjs +10 -9
- package/cli/lib/authority/validate.mjs +6 -14
- package/cli/lib/code/authority-annotation-response.mjs +43 -0
- package/cli/lib/code/authority-annotation-scan.mjs +162 -0
- package/cli/lib/code/authority-annotations.mjs +293 -0
- package/cli/lib/code/context.mjs +710 -0
- package/cli/lib/code/response.mjs +165 -0
- package/cli/lib/doctor.mjs +3 -2
- package/cli/lib/entrypoints.mjs +13 -5
- package/cli/seeds/seed-policy.yaml +2 -2
- package/contracts/authority-source.schema.yaml +0 -6
- package/contracts/authority-verifier-bindings.schema.yaml +21 -10
- package/methodology/authority-authoring.yaml +68 -70
- package/methodology/core.yaml +10 -6
- package/package.json +5 -3
- package/spec/authority-audit.yaml +56 -52
- package/spec/authority-authoring.yaml +3 -16
- package/spec/authority-change-candidates.yaml +146 -0
- package/spec/authority-consumers.yaml +7 -0
- package/spec/authority-discovery.yaml +2 -2
- package/spec/authority-graph.yaml +1 -1
- package/spec/authority-impact.yaml +16 -39
- package/spec/authority-review.yaml +29 -47
- package/spec/authority-terms.yaml +5 -1
- package/spec/code-authority-annotations.yaml +42 -0
- package/spec/code-context.yaml +38 -0
- package/spec/product-scope.yaml +15 -9
- package/cli/commands/validate-ai-governance.mjs +0 -335
- package/cli/lib/authority/evidence-snapshot.mjs +0 -1
- package/cli/lib/authority/evidence.mjs +0 -1432
- package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +0 -377
- package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +0 -376
- package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +0 -155
- package/cli/lib/internal/governance/ai/check-high-risk-doc-metadata-core.mjs +0 -173
- package/cli/lib/internal/governance/config.mjs +0 -114
- package/cli/lib/internal/governance/shared/read-yaml-with-fragments.mjs +0 -49
- package/contracts/authority-evidence-bindings.schema.yaml +0 -53
- package/contracts/authority-evidence-probe-results.schema.yaml +0 -38
- package/contracts/authority-impact-dispositions.schema.yaml +0 -13
- package/spec/authority-evidence.yaml +0 -200
|
@@ -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
|
|
3
|
+
import { compareText, makeDiagnostic } from "./diagnostics.mjs";
|
|
7
4
|
import { diffAuthorityPaths } from "./diff.mjs";
|
|
8
5
|
|
|
9
|
-
const
|
|
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
|
|
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
|
-
|
|
180
|
-
|
|
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:
|
|
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:
|
|
245
|
-
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 {
|
|
5
|
+
import { auditAuthoritySnapshot } from "./audit.mjs";
|
|
6
6
|
import { compileAuthorityPath } from "./compile.mjs";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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 {
|
|
17
|
+
import { impactAuthorityDiff } from "./impact.mjs";
|
|
17
18
|
|
|
18
|
-
const REVIEW_FORMAT = "nimicoding.authority-review/
|
|
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
|
|
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
|
|
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 &&
|
|
95
|
-
return path.basename(
|
|
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
|
|
125
|
-
|
|
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
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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"
|
|
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
|
-
["
|
|
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
|
-
"
|
|
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) =>
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
105
|
-
if (
|
|
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);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
|
|
3
|
+
export const CODE_AUTHORITY_FORMAT = "nimicoding.code-authority-response/v1";
|
|
4
|
+
export const MIN_CODE_AUTHORITY_BYTES = 512;
|
|
5
|
+
|
|
6
|
+
const MAX_ERROR_MESSAGE_BYTES = 160;
|
|
7
|
+
|
|
8
|
+
export function refusedCodeAuthorityResponse(code, message) {
|
|
9
|
+
if (Buffer.byteLength(message, "utf8") > MAX_ERROR_MESSAGE_BYTES) {
|
|
10
|
+
throw new Error("code authority error message exceeds its fixed protocol bound");
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
format: CODE_AUTHORITY_FORMAT,
|
|
14
|
+
status: "refused",
|
|
15
|
+
complete: false,
|
|
16
|
+
operation: null,
|
|
17
|
+
scope: null,
|
|
18
|
+
result: null,
|
|
19
|
+
findings: null,
|
|
20
|
+
counts: null,
|
|
21
|
+
usage: null,
|
|
22
|
+
error: { code, message },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function completedCodeAuthorityResponse(value) {
|
|
27
|
+
return {
|
|
28
|
+
format: CODE_AUTHORITY_FORMAT,
|
|
29
|
+
status: value.operation === "lifecycle-audit" && value.findings.length > 0 ? "findings" : "completed",
|
|
30
|
+
complete: true,
|
|
31
|
+
...value,
|
|
32
|
+
error: null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function serializeCodeAuthorityResponse(response) {
|
|
37
|
+
return `${JSON.stringify(response)}\n`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function renderCodeAuthorityResponse(response, maxBytes) {
|
|
41
|
+
const text = serializeCodeAuthorityResponse(response);
|
|
42
|
+
return Buffer.byteLength(text, "utf8") <= maxBytes ? text : null;
|
|
43
|
+
}
|