@nimiplatform/nimi-coding 0.3.0 → 0.4.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 +27 -0
- package/CONTRIBUTING.md +3 -3
- package/README.md +285 -41
- package/README.zh-CN.md +294 -31
- package/bin/nimicoding.mjs +1 -1
- package/cli/commands/authority.mjs +849 -0
- package/cli/commands/clear.mjs +27 -7
- package/cli/commands/doctor.mjs +10 -1
- package/cli/commands/start.mjs +23 -27
- package/cli/commands/sync.mjs +21 -101
- package/cli/constants.mjs +7 -31
- package/cli/dev/check-authority-surface.mjs +173 -0
- package/cli/dev/check-product-conformance.mjs +917 -0
- package/cli/help.mjs +10 -17
- package/cli/index.mjs +6 -47
- package/cli/lib/authority/anchors.mjs +491 -0
- package/cli/lib/authority/audit.mjs +490 -0
- package/cli/lib/authority/closed-sets.mjs +646 -0
- package/cli/lib/authority/compile.mjs +110 -0
- package/cli/lib/authority/consumers.mjs +470 -0
- package/cli/lib/authority/diagnostics.mjs +87 -0
- package/cli/lib/authority/diff.mjs +287 -0
- package/cli/lib/authority/discover.mjs +447 -0
- package/cli/lib/authority/evidence-snapshot.mjs +1 -0
- package/cli/lib/authority/evidence.mjs +1432 -0
- package/cli/lib/authority/format.mjs +197 -0
- package/cli/lib/authority/git-snapshot.mjs +1065 -0
- package/cli/lib/authority/graph.mjs +351 -0
- package/cli/lib/authority/impact.mjs +252 -0
- package/cli/lib/authority/lexicon.mjs +55 -0
- package/cli/lib/authority/query.mjs +162 -0
- package/cli/lib/authority/repository-corpus.mjs +37 -0
- package/cli/lib/authority/repository-inventory.mjs +34 -0
- package/cli/lib/authority/review.mjs +222 -0
- package/cli/lib/authority/sarif.mjs +181 -0
- package/cli/lib/authority/scope-bindings.mjs +225 -0
- package/cli/lib/authority/source-map.mjs +105 -0
- package/cli/lib/authority/source-markdown.mjs +167 -0
- package/cli/lib/authority/source-yaml.mjs +312 -0
- package/cli/lib/authority/surface.mjs +79 -0
- package/cli/lib/authority/terms.mjs +421 -0
- package/cli/lib/authority/tracked-text.mjs +70 -0
- package/cli/lib/authority/validate.mjs +308 -0
- package/cli/lib/bootstrap.mjs +55 -71
- package/cli/lib/doctor.mjs +40 -123
- package/cli/lib/entrypoints.mjs +103 -105
- package/cli/lib/fs-helpers.mjs +139 -15
- package/cli/lib/internal/governance/config.mjs +0 -36
- package/cli/lib/shared.mjs +8 -21
- package/cli/lib/sync.mjs +81 -92
- package/cli/lib/value-helpers.mjs +0 -26
- package/cli/lib/yaml-helpers.mjs +1 -124
- package/cli/seeds/bootstrap.mjs +31 -87
- package/cli/seeds/seed-policy.yaml +9 -63
- package/contracts/authority-evidence-bindings.schema.yaml +53 -0
- package/contracts/authority-evidence-probe-results.schema.yaml +38 -0
- package/contracts/authority-impact-dispositions.schema.yaml +13 -0
- package/contracts/authority-scope-bindings.schema.yaml +15 -0
- package/contracts/authority-source.schema.yaml +64 -0
- package/contracts/authority-verifier-bindings.schema.yaml +20 -0
- package/methodology/authority-authoring.yaml +144 -0
- package/methodology/core.yaml +31 -26
- package/package.json +10 -6
- package/spec/authority-anchors.yaml +99 -0
- package/spec/authority-audit.yaml +85 -0
- package/spec/authority-authoring.yaml +150 -0
- package/spec/authority-closed-sets.yaml +65 -0
- package/spec/authority-consumers.yaml +70 -0
- package/spec/authority-context.yaml +50 -0
- package/spec/authority-discovery.yaml +119 -0
- package/spec/authority-evidence.yaml +200 -0
- package/spec/authority-graph.yaml +59 -0
- package/spec/authority-impact.yaml +84 -0
- package/spec/authority-review.yaml +89 -0
- package/spec/authority-terms.yaml +59 -0
- package/spec/product-scope.yaml +36 -23
- package/cli/commands/blueprint-audit.mjs +0 -91
- package/cli/commands/classify-spec-tree.mjs +0 -5
- package/cli/commands/generate-spec-derived-docs.mjs +0 -151
- package/cli/commands/generate-spec-migration-plan.mjs +0 -30
- package/cli/commands/surface-validator-command.mjs +0 -49
- package/cli/commands/validate-domain-admission.mjs +0 -5
- package/cli/commands/validate-guidance-bodies.mjs +0 -5
- package/cli/commands/validate-placement.mjs +0 -5
- package/cli/commands/validate-projection-edges.mjs +0 -5
- package/cli/commands/validate-spec-audit.mjs +0 -27
- package/cli/commands/validate-spec-governance.mjs +0 -124
- package/cli/commands/validate-spec-tree.mjs +0 -27
- package/cli/commands/validate-table-family.mjs +0 -5
- package/cli/commands/validate-tracked-output-admission.mjs +0 -5
- package/cli/lib/blueprint-audit.mjs +0 -363
- package/cli/lib/contracts.mjs +0 -21
- package/cli/lib/internal/contracts-loaders.mjs +0 -47
- package/cli/lib/internal/contracts-parse.mjs +0 -192
- package/cli/lib/internal/governance/runner.mjs +0 -35
- package/cli/lib/internal/surface-taxonomy-validators.mjs +0 -1147
- package/cli/lib/internal/validators-shared.mjs +0 -12
- package/cli/lib/internal/validators-spec-helpers.mjs +0 -52
- package/cli/lib/internal/validators-spec.mjs +0 -380
- package/cli/lib/validators.mjs +0 -30
- package/config/bootstrap.yaml +0 -6
- package/config/spec-generation-inputs.yaml +0 -41
- package/contracts/domain-admission.schema.yaml +0 -56
- package/contracts/migration-inventory.schema.yaml +0 -15
- package/contracts/negative-fixtures.yaml +0 -26
- package/contracts/placement-contract.schema.yaml +0 -127
- package/contracts/projection-edge.schema.yaml +0 -46
- package/contracts/shared-enums.yaml +0 -63
- package/contracts/spec-generation-audit.schema.yaml +0 -46
- package/contracts/spec-generation-inputs.schema.yaml +0 -43
- package/contracts/spec-layout.schema.yaml +0 -32
- package/contracts/surface-taxonomy.schema.yaml +0 -139
- package/contracts/table-family.schema.yaml +0 -133
- package/contracts/tracked-output-admission.schema.yaml +0 -17
- package/methodology/four-closure-policy.yaml +0 -20
- package/methodology/role-separation-policy.yaml +0 -22
- package/methodology/spec-reconstruction.yaml +0 -69
- package/spec/_meta/spec-tree-model.yaml +0 -44
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { compileAuthorityPath } from "./compile.mjs";
|
|
6
|
+
import { compareText, makeDiagnostic, portablePath } from "./diagnostics.mjs";
|
|
7
|
+
|
|
8
|
+
const PACKET_FORMAT = "nimicoding.authority-context/v1";
|
|
9
|
+
const CLOSURE_CATEGORIES = ["applies_to", "supersedes"];
|
|
10
|
+
|
|
11
|
+
async function inputLocation(inputPath) {
|
|
12
|
+
const absolute = path.resolve(inputPath);
|
|
13
|
+
const info = await stat(absolute);
|
|
14
|
+
return {
|
|
15
|
+
basePath: info.isDirectory() ? absolute : path.dirname(absolute),
|
|
16
|
+
label: info.isDirectory() ? "." : path.basename(absolute),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function queryDiagnostic(location, code, id, reason, repair, mapped = null) {
|
|
21
|
+
return makeDiagnostic({
|
|
22
|
+
code,
|
|
23
|
+
file: mapped ? portablePath(mapped.file, location.basePath) : location.label,
|
|
24
|
+
range: mapped?.range ?? { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
|
|
25
|
+
pointer: "/id",
|
|
26
|
+
reason: `${reason}: ${id}`,
|
|
27
|
+
repair,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function mappedId(compiled, unitIndex) {
|
|
32
|
+
return compiled.sourceMap.fields[`/units/${unitIndex}/id`] ?? null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildPacket(compiled, selectedIds, rootId, closure, basePath) {
|
|
36
|
+
const sourceIndices = compiled.ir.units
|
|
37
|
+
.map((unit, index) => ({ unit, index }))
|
|
38
|
+
.filter(({ unit }) => selectedIds.has(unit.id))
|
|
39
|
+
.sort((left, right) => compareText(left.unit.id, right.unit.id));
|
|
40
|
+
const fields = {};
|
|
41
|
+
sourceIndices.forEach(({ index: sourceIndex }, packetIndex) => {
|
|
42
|
+
const prefix = `/units/${sourceIndex}`;
|
|
43
|
+
for (const [pointer, mapped] of Object.entries(compiled.sourceMap.fields)) {
|
|
44
|
+
if (pointer !== prefix && !pointer.startsWith(`${prefix}/`)) continue;
|
|
45
|
+
const suffix = pointer.slice(prefix.length);
|
|
46
|
+
fields[`/units/${packetIndex}${suffix}`] = {
|
|
47
|
+
...mapped,
|
|
48
|
+
file: portablePath(mapped.file, basePath),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
format: PACKET_FORMAT,
|
|
54
|
+
root: rootId,
|
|
55
|
+
closure,
|
|
56
|
+
units: sourceIndices.map(({ unit }) => unit),
|
|
57
|
+
sourceMap: { fields },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function canonicalPacketBytes(packet) {
|
|
62
|
+
return Buffer.byteLength(JSON.stringify(packet), "utf8");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function failedResult(compiled, diagnostics = compiled.diagnostics) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
diagnostics,
|
|
69
|
+
fileCount: compiled.fileCount,
|
|
70
|
+
unitCount: 0,
|
|
71
|
+
packetBytes: 0,
|
|
72
|
+
packet: null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function boundedPacketResult(compiled, packet, maxBytes, rootIndex, location) {
|
|
77
|
+
const packetBytes = canonicalPacketBytes(packet);
|
|
78
|
+
if (packetBytes > maxBytes) return failedResult(compiled, [queryDiagnostic(
|
|
79
|
+
location,
|
|
80
|
+
"AUTH_CONTEXT_BUDGET",
|
|
81
|
+
packet.root,
|
|
82
|
+
`complete canonical packet requires ${packetBytes} UTF-8 bytes but max-bytes is ${maxBytes}`,
|
|
83
|
+
"increase the explicit byte budget or choose a narrower exact root; partial context is forbidden",
|
|
84
|
+
mappedId(compiled, rootIndex),
|
|
85
|
+
)]);
|
|
86
|
+
return {
|
|
87
|
+
ok: true,
|
|
88
|
+
diagnostics: [],
|
|
89
|
+
fileCount: compiled.fileCount,
|
|
90
|
+
unitCount: packet.units.length,
|
|
91
|
+
packetBytes,
|
|
92
|
+
packet,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function queryAuthorityPath(inputPath, id, { maxBytes }) {
|
|
97
|
+
const compiled = await compileAuthorityPath(inputPath);
|
|
98
|
+
if (!compiled.ok) return failedResult(compiled);
|
|
99
|
+
const location = await inputLocation(inputPath);
|
|
100
|
+
const unitIndex = compiled.ir.units.findIndex((unit) => unit.id === id);
|
|
101
|
+
if (unitIndex < 0) return failedResult(compiled, [queryDiagnostic(
|
|
102
|
+
location,
|
|
103
|
+
"AUTH_QUERY_NOT_FOUND",
|
|
104
|
+
id,
|
|
105
|
+
"authority identity does not resolve",
|
|
106
|
+
"use an exact admitted authority ID; do not infer a near match",
|
|
107
|
+
)]);
|
|
108
|
+
const packet = buildPacket(compiled, new Set([id]), id, {
|
|
109
|
+
direction: "exact",
|
|
110
|
+
relationCategories: [],
|
|
111
|
+
maxBytes,
|
|
112
|
+
complete: true,
|
|
113
|
+
}, location.basePath);
|
|
114
|
+
return boundedPacketResult(compiled, packet, maxBytes, unitIndex, location);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function contextAuthorityPath(inputPath, id, { maxUnits, maxBytes }) {
|
|
118
|
+
const compiled = await compileAuthorityPath(inputPath);
|
|
119
|
+
if (!compiled.ok) return failedResult(compiled);
|
|
120
|
+
const location = await inputLocation(inputPath);
|
|
121
|
+
const byId = new Map(compiled.ir.units.map((unit, index) => [unit.id, { unit, index }]));
|
|
122
|
+
const root = byId.get(id);
|
|
123
|
+
if (!root) return failedResult(compiled, [queryDiagnostic(
|
|
124
|
+
location,
|
|
125
|
+
"AUTH_QUERY_NOT_FOUND",
|
|
126
|
+
id,
|
|
127
|
+
"context root does not resolve",
|
|
128
|
+
"use an exact admitted authority ID; do not infer a near match",
|
|
129
|
+
)]);
|
|
130
|
+
|
|
131
|
+
const selected = new Set();
|
|
132
|
+
const pending = [id];
|
|
133
|
+
while (pending.length > 0) {
|
|
134
|
+
const currentId = pending.shift();
|
|
135
|
+
if (selected.has(currentId)) continue;
|
|
136
|
+
selected.add(currentId);
|
|
137
|
+
const current = byId.get(currentId).unit;
|
|
138
|
+
const targets = current.relations
|
|
139
|
+
.filter((relation) => CLOSURE_CATEGORIES.includes(relation.type))
|
|
140
|
+
.map((relation) => relation.target)
|
|
141
|
+
.sort(compareText);
|
|
142
|
+
for (const target of targets) if (!selected.has(target)) pending.push(target);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (selected.size > maxUnits) return failedResult(compiled, [queryDiagnostic(
|
|
146
|
+
location,
|
|
147
|
+
"AUTH_CONTEXT_BUDGET",
|
|
148
|
+
id,
|
|
149
|
+
`complete context requires ${selected.size} units but max-units is ${maxUnits}`,
|
|
150
|
+
"increase the explicit unit budget or choose a narrower exact root; partial context is forbidden",
|
|
151
|
+
mappedId(compiled, root.index),
|
|
152
|
+
)]);
|
|
153
|
+
|
|
154
|
+
const packet = buildPacket(compiled, selected, id, {
|
|
155
|
+
direction: "outgoing_recursive",
|
|
156
|
+
relationCategories: CLOSURE_CATEGORIES,
|
|
157
|
+
maxUnits,
|
|
158
|
+
maxBytes,
|
|
159
|
+
complete: true,
|
|
160
|
+
}, location.basePath);
|
|
161
|
+
return boundedPacketResult(compiled, packet, maxBytes, root.index, location);
|
|
162
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
// The repository-bound report products -- anchors, consumers, terms and
|
|
5
|
+
// closed-sets -- read a repository and the authority that governs it, then
|
|
6
|
+
// report one against the other. Their guard used to be "somewhere inside this
|
|
7
|
+
// Git repository", which let a fixture directory stand in as the canonical
|
|
8
|
+
// corpus and still produce a report marked complete. The corpus for these
|
|
9
|
+
// products is one exact location.
|
|
10
|
+
//
|
|
11
|
+
// This does not apply to fmt, check, diff, compile or impact: those accept an
|
|
12
|
+
// arbitrary path on purpose, because they read history snapshots and migration
|
|
13
|
+
// inputs that are not the working corpus.
|
|
14
|
+
export const REPOSITORY_CORPUS_SEGMENTS = [".nimi", "spec"];
|
|
15
|
+
export const REPOSITORY_CORPUS_RELATIVE = REPOSITORY_CORPUS_SEGMENTS.join("/");
|
|
16
|
+
|
|
17
|
+
export async function resolveRepositoryCorpus(repositoryPath, specPath, label) {
|
|
18
|
+
const repository = await realpath(path.resolve(repositoryPath));
|
|
19
|
+
const absolute = path.resolve(specPath);
|
|
20
|
+
let metadata;
|
|
21
|
+
try {
|
|
22
|
+
metadata = await lstat(absolute);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new Error(`${label} corpus is not readable: ${error.message}`);
|
|
25
|
+
}
|
|
26
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
27
|
+
throw new Error(`${label} corpus must be one regular non-symlink directory`);
|
|
28
|
+
}
|
|
29
|
+
const resolved = await realpath(absolute);
|
|
30
|
+
const expected = path.join(repository, ...REPOSITORY_CORPUS_SEGMENTS);
|
|
31
|
+
if (resolved !== expected) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`${label} corpus must be the repository authority corpus at ${REPOSITORY_CORPUS_RELATIVE}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return { path: resolved, directory: true, relative: REPOSITORY_CORPUS_RELATIVE };
|
|
37
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { compareText } from "./diagnostics.mjs";
|
|
4
|
+
import { listGitTrackedFiles } from "./git-snapshot.mjs";
|
|
5
|
+
|
|
6
|
+
const REPOSITORY_INVENTORY_FORMAT = "nimicoding.repository-inventory/v1";
|
|
7
|
+
|
|
8
|
+
function trackedPathSetDigest(files) {
|
|
9
|
+
const ordered = [...new Set(files)].sort(compareText);
|
|
10
|
+
// The SHA-256 preimage is exactly the UTF-8 encoding of this compact JSON:
|
|
11
|
+
// {"format":"nimicoding.repository-inventory/v1","files":[<ordered paths>]}
|
|
12
|
+
// where paths are the unique Git-decoded tracked paths sorted by compareText.
|
|
13
|
+
// The repository root is intentionally absent: it locates the inventory but
|
|
14
|
+
// is not part of the state two reports compare.
|
|
15
|
+
//
|
|
16
|
+
// This digest covers the tracked path SET and nothing else - not blob
|
|
17
|
+
// contents, not the commit, not worktree cleanliness. Two commits whose
|
|
18
|
+
// files were all rewritten but none renamed produce the same digest. The
|
|
19
|
+
// name says so on purpose: a field called inventoryDigest would be read as
|
|
20
|
+
// "these reports describe the same repository state", which this value does
|
|
21
|
+
// not establish. Content identity is a separate, additive field.
|
|
22
|
+
const preimage = JSON.stringify({ format: REPOSITORY_INVENTORY_FORMAT, files: ordered });
|
|
23
|
+
return createHash("sha256").update(preimage, "utf8").digest("hex");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function repositoryInventory(repositoryPath) {
|
|
27
|
+
const inventory = await listGitTrackedFiles(repositoryPath);
|
|
28
|
+
return {
|
|
29
|
+
...inventory,
|
|
30
|
+
trackedPathSetDigest: trackedPathSetDigest(inventory.files),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { REPOSITORY_INVENTORY_FORMAT, trackedPathSetDigest };
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { auditAuthorityPath } from "./audit.mjs";
|
|
6
|
+
import { compileAuthorityPath } from "./compile.mjs";
|
|
7
|
+
import { makeDiagnostic, portablePath, sortDiagnostics } from "./diagnostics.mjs";
|
|
8
|
+
import { diffAuthorityPaths } from "./diff.mjs";
|
|
9
|
+
import { AuthorityInputError } from "./format.mjs";
|
|
10
|
+
import {
|
|
11
|
+
AuthorityReviewRefusal,
|
|
12
|
+
authorityReviewRefusalDiagnostic,
|
|
13
|
+
captureStableRegularFile,
|
|
14
|
+
withGitAuthoritySnapshots,
|
|
15
|
+
} from "./git-snapshot.mjs";
|
|
16
|
+
import { impactAuthorityPaths } from "./impact.mjs";
|
|
17
|
+
|
|
18
|
+
const REVIEW_FORMAT = "nimicoding.authority-review/v1";
|
|
19
|
+
|
|
20
|
+
function validBudget(value) {
|
|
21
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function refused(diagnostics, reviewBytes = 0) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
diagnostics: sortDiagnostics(diagnostics),
|
|
28
|
+
fileCount: 0,
|
|
29
|
+
unitCount: 0,
|
|
30
|
+
reviewBytes,
|
|
31
|
+
review: null,
|
|
32
|
+
partial: false,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function reviewBudgetDiagnostic(requiredBytes, maxBytes) {
|
|
37
|
+
return makeDiagnostic({
|
|
38
|
+
code: "AUTH_REVIEW_BUDGET",
|
|
39
|
+
file: ".",
|
|
40
|
+
range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
|
|
41
|
+
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",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizePath(file, root, side) {
|
|
48
|
+
const relative = portablePath(file, root);
|
|
49
|
+
const safe = path.isAbsolute(relative) ? path.basename(relative) : relative;
|
|
50
|
+
return path.posix.join(side, safe || ".");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeCompilerDiagnostics(diagnostics, root, side) {
|
|
54
|
+
return diagnostics.map((diagnostic) => ({
|
|
55
|
+
...diagnostic,
|
|
56
|
+
path: normalizePath(diagnostic.path, root, side),
|
|
57
|
+
related: diagnostic.related.map((related) => ({
|
|
58
|
+
...related,
|
|
59
|
+
path: normalizePath(related.path, root, side),
|
|
60
|
+
})),
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function corpusInputDiagnostic(error, root, side) {
|
|
65
|
+
const portableRoot = root.split(path.sep).join(path.posix.sep);
|
|
66
|
+
const reason = error.message
|
|
67
|
+
.split(root).join(side)
|
|
68
|
+
.split(portableRoot).join(side)
|
|
69
|
+
.split(path.sep).join(path.posix.sep);
|
|
70
|
+
return makeDiagnostic({
|
|
71
|
+
code: "AUTH_REVIEW_CORPUS_INVALID",
|
|
72
|
+
file: side,
|
|
73
|
+
range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
|
|
74
|
+
pointer: "",
|
|
75
|
+
reason,
|
|
76
|
+
repair: "provide one complete canonical authority corpus; unsupported or partial inputs are refused",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function compileSnapshot(root, side) {
|
|
81
|
+
try {
|
|
82
|
+
const result = await compileAuthorityPath(root);
|
|
83
|
+
if (result.ok) return { result, diagnostics: [] };
|
|
84
|
+
return { result: null, diagnostics: normalizeCompilerDiagnostics(result.diagnostics, root, side) };
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof AuthorityInputError) return { result: null, diagnostics: [corpusInputDiagnostic(error, root, side)] };
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function inputLabel(file, repository, fallback) {
|
|
92
|
+
const absolute = path.resolve(process.cwd(), file);
|
|
93
|
+
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
|
+
}
|
|
97
|
+
|
|
98
|
+
async function captureReviewInput(file, temporaryRoot, name, label) {
|
|
99
|
+
let bytes;
|
|
100
|
+
try {
|
|
101
|
+
bytes = await captureStableRegularFile(file, label);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error instanceof AuthorityReviewRefusal) {
|
|
104
|
+
if (error.code === "AUTH_REVIEW_CAPTURE_CHANGED") throw error;
|
|
105
|
+
throw new AuthorityReviewRefusal("AUTH_REVIEW_INPUT_INVALID", `${name} must be one readable regular non-symlink file`, label);
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
const directory = path.join(temporaryRoot, "inputs");
|
|
110
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
111
|
+
const target = path.join(directory, `${name}.yaml`);
|
|
112
|
+
await writeFile(target, bytes, { flag: "wx", mode: 0o600 });
|
|
113
|
+
return target;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function invalidDisposition(result) {
|
|
117
|
+
return result.diagnostics.some((diagnostic) => diagnostic.code === "AUTH_IMPACT_DISPOSITION_INVALID");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function canonicalReviewBytes(review) {
|
|
121
|
+
return Buffer.byteLength(JSON.stringify(review), "utf8");
|
|
122
|
+
}
|
|
123
|
+
|
|
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");
|
|
131
|
+
const capturedBindings = await captureReviewInput(bindingsPath, snapshot.temporaryRoot, "bindings", bindingsLabel);
|
|
132
|
+
const capturedDispositions = await captureReviewInput(dispositionsPath, snapshot.temporaryRoot, "dispositions", dispositionsLabel);
|
|
133
|
+
const [baseCompiled, worktreeCompiled] = await Promise.all([
|
|
134
|
+
compileSnapshot(snapshot.base.root, "base"),
|
|
135
|
+
compileSnapshot(snapshot.worktree.root, "worktree"),
|
|
136
|
+
]);
|
|
137
|
+
const compilerDiagnostics = [...baseCompiled.diagnostics, ...worktreeCompiled.diagnostics];
|
|
138
|
+
if (compilerDiagnostics.length > 0) return refused(compilerDiagnostics);
|
|
139
|
+
if (baseCompiled.result.fileCount !== snapshot.base.fileCount || worktreeCompiled.result.fileCount !== snapshot.worktree.fileCount) {
|
|
140
|
+
throw new Error("captured authority file counts do not match complete compiler admission");
|
|
141
|
+
}
|
|
142
|
+
|
|
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
|
+
]);
|
|
148
|
+
if (!diffResult.ok || diffResult.diff === null) return refused(diffResult.diagnostics);
|
|
149
|
+
if (impactResult.impact === null || invalidDisposition(impactResult)) return refused(impactResult.diagnostics);
|
|
150
|
+
if (auditResult.audit === null) return refused(auditResult.diagnostics);
|
|
151
|
+
if (JSON.stringify(diffResult.diff) !== JSON.stringify(impactResult.diff)) throw new Error("diff and impact oracles returned different semantic diffs for immutable snapshots");
|
|
152
|
+
|
|
153
|
+
const complete = impactResult.impact.complete && auditResult.audit.complete;
|
|
154
|
+
const policyStatus = auditResult.audit.policyStatus === "blocked"
|
|
155
|
+
? "blocked"
|
|
156
|
+
: complete ? "passed" : "indeterminate";
|
|
157
|
+
const review = {
|
|
158
|
+
format: REVIEW_FORMAT,
|
|
159
|
+
operationStatus: "completed",
|
|
160
|
+
policyStatus,
|
|
161
|
+
complete,
|
|
162
|
+
partial: false,
|
|
163
|
+
snapshots: {
|
|
164
|
+
base: {
|
|
165
|
+
commitOid: snapshot.baseOid,
|
|
166
|
+
contentIdentity: snapshot.base.contentIdentity,
|
|
167
|
+
counts: {
|
|
168
|
+
files: snapshot.base.fileCount,
|
|
169
|
+
units: baseCompiled.result.unitCount,
|
|
170
|
+
bytes: snapshot.base.byteCount,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
worktree: {
|
|
174
|
+
contentIdentity: snapshot.worktree.contentIdentity,
|
|
175
|
+
counts: {
|
|
176
|
+
files: snapshot.worktree.fileCount,
|
|
177
|
+
units: worktreeCompiled.result.unitCount,
|
|
178
|
+
bytes: snapshot.worktree.byteCount,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
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),
|
|
187
|
+
},
|
|
188
|
+
diff: diffResult.diff,
|
|
189
|
+
impact: impactResult.impact,
|
|
190
|
+
audit: auditResult.audit,
|
|
191
|
+
budgets,
|
|
192
|
+
};
|
|
193
|
+
const reviewBytes = canonicalReviewBytes(review);
|
|
194
|
+
if (reviewBytes > budgets.maxBytes) return refused([reviewBudgetDiagnostic(reviewBytes, budgets.maxBytes)], reviewBytes);
|
|
195
|
+
return {
|
|
196
|
+
ok: true,
|
|
197
|
+
diagnostics: sortDiagnostics(impactResult.diagnostics),
|
|
198
|
+
fileCount: snapshot.base.fileCount + snapshot.worktree.fileCount,
|
|
199
|
+
unitCount: worktreeCompiled.result.unitCount,
|
|
200
|
+
reviewBytes,
|
|
201
|
+
review,
|
|
202
|
+
partial: false,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function reviewAuthorityRepository(repositoryPath, baseRef, bindingsPath, dispositionsPath, budgets, options = {}) {
|
|
207
|
+
if (![budgets?.maxUnits, budgets?.maxEdges, budgets?.maxBytes].every(validBudget)) {
|
|
208
|
+
return refused([reviewBudgetDiagnostic(1, budgets?.maxBytes ?? 0)]);
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
return await withGitAuthoritySnapshots({
|
|
212
|
+
repositoryPath,
|
|
213
|
+
baseRef,
|
|
214
|
+
hooks: options.snapshotHooks ?? null,
|
|
215
|
+
}, (snapshot) => composeReview(snapshot, bindingsPath, dispositionsPath, budgets));
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error instanceof AuthorityReviewRefusal) return refused([authorityReviewRefusalDiagnostic(error)]);
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export { REVIEW_FORMAT };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
const SARIF_SCHEMA =
|
|
2
|
+
"https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json";
|
|
3
|
+
const SARIF_VERSION = "2.1.0";
|
|
4
|
+
const TOOL_NAME = "@nimiplatform/nimi-coding";
|
|
5
|
+
const FINGERPRINT_KEY = "nimicoding.semanticFingerprint/v1";
|
|
6
|
+
|
|
7
|
+
function copy(value) {
|
|
8
|
+
return value === undefined ? undefined : structuredClone(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function portableUri(file) {
|
|
12
|
+
return String(file)
|
|
13
|
+
.replaceAll("\\", "/")
|
|
14
|
+
.split("/")
|
|
15
|
+
.map((segment) => encodeURIComponent(segment))
|
|
16
|
+
.join("/");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function physicalLocation(location) {
|
|
20
|
+
const file = location?.file ?? location?.path;
|
|
21
|
+
if (typeof file !== "string" || !location?.range) return null;
|
|
22
|
+
const value = {
|
|
23
|
+
artifactLocation: { uri: portableUri(file) },
|
|
24
|
+
region: {
|
|
25
|
+
startLine: location.range.start.line,
|
|
26
|
+
startColumn: location.range.start.column,
|
|
27
|
+
endLine: location.range.end.line,
|
|
28
|
+
endColumn: location.range.end.column,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
const sourcePointer = location.sourcePointer ?? location.pointer;
|
|
32
|
+
if (typeof sourcePointer === "string") value.properties = { sourcePointer };
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function primaryLocation(location) {
|
|
37
|
+
const physical = physicalLocation(location);
|
|
38
|
+
return physical ? [{ physicalLocation: physical }] : [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function relatedLocations(values = []) {
|
|
42
|
+
return values.flatMap((entry, index) => {
|
|
43
|
+
const physical = physicalLocation(entry.location ?? entry);
|
|
44
|
+
if (!physical) return [];
|
|
45
|
+
const role = entry.role ?? entry.location?.role;
|
|
46
|
+
const related = {
|
|
47
|
+
id: index + 1,
|
|
48
|
+
physicalLocation: physical,
|
|
49
|
+
};
|
|
50
|
+
if (typeof role === "string") {
|
|
51
|
+
related.message = { text: role };
|
|
52
|
+
related.properties = { role };
|
|
53
|
+
}
|
|
54
|
+
return [related];
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function itemLevel(kind, item) {
|
|
59
|
+
if (kind === "observation") return "note";
|
|
60
|
+
if (kind === "gap") return "error";
|
|
61
|
+
return item.policy === "blocking" ? "error" : "warning";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function auditItemResult(kind, item) {
|
|
65
|
+
const result = {
|
|
66
|
+
ruleId: item.code,
|
|
67
|
+
level: itemLevel(kind, item),
|
|
68
|
+
message: { text: item.message },
|
|
69
|
+
locations: primaryLocation(item.primaryLocation),
|
|
70
|
+
relatedLocations: relatedLocations(item.relatedLocations),
|
|
71
|
+
partialFingerprints: { [FINGERPRINT_KEY]: item.fingerprint },
|
|
72
|
+
properties: {
|
|
73
|
+
kind,
|
|
74
|
+
code: item.code,
|
|
75
|
+
fingerprint: item.fingerprint,
|
|
76
|
+
detector: item.detector,
|
|
77
|
+
binding: item.binding,
|
|
78
|
+
premise: item.premise,
|
|
79
|
+
basis: item.basis,
|
|
80
|
+
policy: item.policy,
|
|
81
|
+
authorityRefs: copy(item.authorityRefs),
|
|
82
|
+
witness: copy(item.witness),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
if (typeof item.target === "string") result.properties.target = item.target;
|
|
86
|
+
if (result.locations.length === 0) delete result.locations;
|
|
87
|
+
if (result.relatedLocations.length === 0) delete result.relatedLocations;
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function diagnosticResult(diagnostic) {
|
|
92
|
+
const result = {
|
|
93
|
+
ruleId: diagnostic.code,
|
|
94
|
+
level: "error",
|
|
95
|
+
message: { text: diagnostic.reason },
|
|
96
|
+
locations: primaryLocation({
|
|
97
|
+
path: diagnostic.path,
|
|
98
|
+
range: diagnostic.range,
|
|
99
|
+
pointer: diagnostic.pointer,
|
|
100
|
+
}),
|
|
101
|
+
relatedLocations: relatedLocations(diagnostic.related),
|
|
102
|
+
properties: {
|
|
103
|
+
kind: "diagnostic",
|
|
104
|
+
code: diagnostic.code,
|
|
105
|
+
severity: diagnostic.severity,
|
|
106
|
+
pointer: diagnostic.pointer,
|
|
107
|
+
repair: diagnostic.repair,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
if (result.locations.length === 0) delete result.locations;
|
|
111
|
+
if (result.relatedLocations.length === 0) delete result.relatedLocations;
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function rulesFor(results) {
|
|
116
|
+
const codes = [...new Set(results.map((result) => result.ruleId))].sort();
|
|
117
|
+
return codes.map((code) => ({
|
|
118
|
+
id: code,
|
|
119
|
+
shortDescription: { text: code },
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function invocationProperties(result) {
|
|
124
|
+
if (!result.audit) {
|
|
125
|
+
return {
|
|
126
|
+
operationStatus: "refused",
|
|
127
|
+
policyStatus: "indeterminate",
|
|
128
|
+
complete: false,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
format: result.audit.format,
|
|
133
|
+
operationStatus: result.audit.operationStatus,
|
|
134
|
+
policyStatus: result.audit.policyStatus,
|
|
135
|
+
complete: result.audit.complete,
|
|
136
|
+
counts: copy(result.audit.counts),
|
|
137
|
+
budgets: copy(result.audit.budgets),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function authorityAuditResultToSarif(result, { toolVersion }) {
|
|
142
|
+
if (!result || typeof result !== "object")
|
|
143
|
+
throw new TypeError("authority audit result must be an object");
|
|
144
|
+
if (typeof toolVersion !== "string" || toolVersion.length === 0)
|
|
145
|
+
throw new TypeError("toolVersion must be a non-empty string");
|
|
146
|
+
|
|
147
|
+
const results = result.audit
|
|
148
|
+
? [
|
|
149
|
+
...(result.audit.observations ?? []).map((item) => auditItemResult("observation", item)),
|
|
150
|
+
...(result.audit.findings ?? []).map((item) => auditItemResult("finding", item)),
|
|
151
|
+
...(result.audit.gaps ?? []).map((item) => auditItemResult("gap", item)),
|
|
152
|
+
]
|
|
153
|
+
: (result.diagnostics ?? []).map(diagnosticResult);
|
|
154
|
+
const rules = rulesFor(results);
|
|
155
|
+
const ruleIndexes = new Map(rules.map((rule, index) => [rule.id, index]));
|
|
156
|
+
for (const entry of results) entry.ruleIndex = ruleIndexes.get(entry.ruleId);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
$schema: SARIF_SCHEMA,
|
|
160
|
+
version: SARIF_VERSION,
|
|
161
|
+
runs: [
|
|
162
|
+
{
|
|
163
|
+
tool: {
|
|
164
|
+
driver: {
|
|
165
|
+
name: TOOL_NAME,
|
|
166
|
+
version: toolVersion,
|
|
167
|
+
rules,
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
columnKind: "unicodeCodePoints",
|
|
171
|
+
invocations: [
|
|
172
|
+
{
|
|
173
|
+
executionSuccessful: result.ok === true,
|
|
174
|
+
properties: invocationProperties(result),
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
results,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
}
|