@nimiplatform/nimi-coding 0.3.1 → 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 +23 -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 -144
- 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,917 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Exercises behavioural contract leaves against the CLI artifact. Structural
|
|
3
|
+
// agreement alone cannot prove that a declared exit policy or budget is real.
|
|
4
|
+
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
|
|
12
|
+
import YAML from "yaml";
|
|
13
|
+
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
16
|
+
|
|
17
|
+
const PRODUCT_CONTRACTS = {
|
|
18
|
+
anchors: "authority-anchors.yaml",
|
|
19
|
+
consumers: "authority-consumers.yaml",
|
|
20
|
+
terms: "authority-terms.yaml",
|
|
21
|
+
"closed-sets": "authority-closed-sets.yaml",
|
|
22
|
+
};
|
|
23
|
+
const STATUS_NAMES = ["verified", "contradicted", "not_machine_checkable", "unmapped"];
|
|
24
|
+
const NOT_MACHINE_CHECKABLE = new Map();
|
|
25
|
+
const EMPTY_ARRAY_SENTINEL = Object.freeze({ collection: "array", empty: true });
|
|
26
|
+
const EMPTY_OBJECT_SENTINEL = Object.freeze({ collection: "object", empty: true });
|
|
27
|
+
|
|
28
|
+
async function execute(file, args, options = {}) {
|
|
29
|
+
try {
|
|
30
|
+
const result = await execFileAsync(file, args, { maxBuffer: 16 * 1024 * 1024, ...options });
|
|
31
|
+
return { code: 0, stdout: result.stdout, stderr: result.stderr };
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return {
|
|
34
|
+
code: typeof error.code === "number" ? error.code : error.code ?? 1,
|
|
35
|
+
stdout: error.stdout ?? "",
|
|
36
|
+
stderr: error.stderr ?? error.message ?? "",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function npmCommand(args) {
|
|
42
|
+
if (process.platform !== "win32") return { file: "npm", args };
|
|
43
|
+
return {
|
|
44
|
+
file: process.env.ComSpec ?? "cmd.exe",
|
|
45
|
+
args: ["/d", "/s", "/c", "npm", ...args],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function nestedNpmEnvironment() {
|
|
50
|
+
// An outer `npm pack --dry-run` exports its mode to prepack hooks. The inner
|
|
51
|
+
// artifact build must create and extract a tarball rather than inherit that
|
|
52
|
+
// simulation and report an artifact it never wrote.
|
|
53
|
+
const environment = { ...process.env };
|
|
54
|
+
for (const key of Object.keys(environment)) {
|
|
55
|
+
if (["npm_config_dry_run", "npm_config_pack_destination"].includes(key.toLowerCase())) delete environment[key];
|
|
56
|
+
}
|
|
57
|
+
return environment;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function runCli(cliPath, cwd, args) {
|
|
61
|
+
return execute(process.execPath, [cliPath, ...args], {
|
|
62
|
+
cwd,
|
|
63
|
+
env: { ...process.env, NIMICODING_LANG: "en", NO_COLOR: "1" },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function runGit(root, args) {
|
|
68
|
+
return execute("git", args, { cwd: root, env: process.env });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function walkLeaves(value, prefix, visit) {
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
if (value.length === 0) visit(prefix, EMPTY_ARRAY_SENTINEL);
|
|
74
|
+
else value.forEach((entry, index) => walkLeaves(entry, `${prefix}[${index}]`, visit));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (value !== null && typeof value === "object") {
|
|
78
|
+
const entries = Object.entries(value);
|
|
79
|
+
if (entries.length === 0) visit(prefix, EMPTY_OBJECT_SENTINEL);
|
|
80
|
+
else for (const [key, entry] of entries) {
|
|
81
|
+
walkLeaves(entry, prefix.length === 0 ? key : `${prefix}.${key}`, visit);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
visit(prefix, value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function valueAt(root, dottedPath) {
|
|
89
|
+
const parts = [...dottedPath.matchAll(/(?:^|\.)([^.[\]]+)|\[(\d+)\]/gu)]
|
|
90
|
+
.map((match) => match[1] ?? Number(match[2]));
|
|
91
|
+
let value = root;
|
|
92
|
+
for (const part of parts) value = value?.[part];
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseJson(output) {
|
|
97
|
+
try {
|
|
98
|
+
return output.stdout.length > 0 ? JSON.parse(output.stdout) : null;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function createRepositoryFixture() {
|
|
105
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "nimicoding-product-conformance-fixture-"));
|
|
106
|
+
const spec = path.join(root, ".nimi", "spec");
|
|
107
|
+
const source = path.join(spec, "fixture.authority.yaml");
|
|
108
|
+
const scopeBindings = path.join(root, "scope-bindings.yaml");
|
|
109
|
+
const externalOne = path.join(root, "external-one");
|
|
110
|
+
const externalTwo = path.join(root, "external-two");
|
|
111
|
+
await mkdir(spec, { recursive: true });
|
|
112
|
+
await mkdir(path.join(root, "src"), { recursive: true });
|
|
113
|
+
await mkdir(path.join(root, "proto"), { recursive: true });
|
|
114
|
+
await mkdir(path.join(root, "config"), { recursive: true });
|
|
115
|
+
await mkdir(externalOne);
|
|
116
|
+
await mkdir(externalTwo);
|
|
117
|
+
|
|
118
|
+
const container = {
|
|
119
|
+
format: "nimicoding.authority/v2",
|
|
120
|
+
owner: "team.fixture",
|
|
121
|
+
units: [
|
|
122
|
+
{
|
|
123
|
+
id: "definition.fixture",
|
|
124
|
+
kind: "definition",
|
|
125
|
+
title: "Fixture concept",
|
|
126
|
+
meaning: "The standaloneTerm and secondTerm are canonical. Run pnpm install. The path token src/pathCamelCase is excluded. artifact=src/live.ts.old",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "definition.vocabulary",
|
|
130
|
+
kind: "definition",
|
|
131
|
+
title: "Fixture vocabulary",
|
|
132
|
+
meaning: "Supported kinds are IMAGE, VIDEO, AUDIO.",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: "rule.fixture",
|
|
136
|
+
kind: "rule",
|
|
137
|
+
title: "Fixture rule",
|
|
138
|
+
modality: "must",
|
|
139
|
+
scope: ["api.fixture"],
|
|
140
|
+
statement: "Use standaloneTerm exactly.",
|
|
141
|
+
condition: "Always.",
|
|
142
|
+
failure: "Reject invalid input.",
|
|
143
|
+
relations: [{ type: "applies_to", target: "definition.fixture" }],
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
await writeFile(source, YAML.stringify(container, { indent: 2, lineWidth: 0 }), "utf8");
|
|
148
|
+
await writeFile(scopeBindings, YAML.stringify({
|
|
149
|
+
format: "nimicoding.scope-bindings/v1",
|
|
150
|
+
scopes: [{ scope: "api.fixture", bindings: [{ kind: "path_glob", value: "src/*.ts" }] }],
|
|
151
|
+
}, { indent: 2, lineWidth: 0 }), "utf8");
|
|
152
|
+
await writeFile(path.join(root, "src", "live.ts"), "export const standaloneTerm = true; export const secondTerm = true;\n", "utf8");
|
|
153
|
+
await writeFile(
|
|
154
|
+
path.join(root, "src", "consumer.ts"),
|
|
155
|
+
[
|
|
156
|
+
"export const firstAuthority = '.nimi/spec/missing.authority.yaml';",
|
|
157
|
+
"export const secondAuthority = '.nimi/spec/other-missing.authority.yaml';",
|
|
158
|
+
"",
|
|
159
|
+
].join("\n"),
|
|
160
|
+
"utf8",
|
|
161
|
+
);
|
|
162
|
+
await writeFile(path.join(root, "proto", "kinds.proto"), [
|
|
163
|
+
"syntax = \"proto3\";",
|
|
164
|
+
"enum AssetKind {",
|
|
165
|
+
" ASSET_KIND_IMAGE = 0;",
|
|
166
|
+
" ASSET_KIND_VIDEO = 1;",
|
|
167
|
+
" ASSET_KIND_EMBEDDING = 2;",
|
|
168
|
+
"}",
|
|
169
|
+
"",
|
|
170
|
+
].join("\n"), "utf8");
|
|
171
|
+
await writeFile(path.join(root, "config", "kinds.yaml"), "values: [IMAGE, VIDEO, EMBEDDING]\n", "utf8");
|
|
172
|
+
await writeFile(path.join(root, "package.json"), `${JSON.stringify({
|
|
173
|
+
name: "product-conformance-fixture",
|
|
174
|
+
private: true,
|
|
175
|
+
scripts: { build: "node src/live.ts" },
|
|
176
|
+
}, null, 2)}\n`, "utf8");
|
|
177
|
+
|
|
178
|
+
const initialized = await runGit(root, ["init", "-q", "-b", "main"]);
|
|
179
|
+
if (initialized.code !== 0) throw new Error(`could not initialize fixture Git repository: ${initialized.stderr}`);
|
|
180
|
+
const added = await runGit(root, ["add", "--", "."]);
|
|
181
|
+
if (added.code !== 0) throw new Error(`could not populate fixture Git index: ${added.stderr}`);
|
|
182
|
+
return { root, spec, source, scopeBindings, externalOne, externalTwo };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function requiredArgs(command, fixture, overrides = {}) {
|
|
186
|
+
const budgets = {
|
|
187
|
+
maxUnits: overrides.maxUnits ?? 100,
|
|
188
|
+
maxAnchors: overrides.maxAnchors ?? 100,
|
|
189
|
+
maxFiles: overrides.maxFiles ?? 100,
|
|
190
|
+
maxRefs: overrides.maxRefs ?? 100,
|
|
191
|
+
maxTerms: overrides.maxTerms ?? 1000,
|
|
192
|
+
maxSets: overrides.maxSets ?? 100,
|
|
193
|
+
maxBytes: overrides.maxBytes ?? 1000000,
|
|
194
|
+
};
|
|
195
|
+
const common = ["authority", command, fixture.root, "--spec", fixture.spec];
|
|
196
|
+
if (command === "anchors") return [...common, "--max-units", String(budgets.maxUnits), "--max-anchors", String(budgets.maxAnchors), "--max-bytes", String(budgets.maxBytes), "--json"];
|
|
197
|
+
if (command === "consumers") return [...common, "--max-files", String(budgets.maxFiles), "--max-refs", String(budgets.maxRefs), "--max-bytes", String(budgets.maxBytes), "--json"];
|
|
198
|
+
if (command === "terms") return [...common, "--max-units", String(budgets.maxUnits), "--max-terms", String(budgets.maxTerms), "--max-bytes", String(budgets.maxBytes), "--json"];
|
|
199
|
+
return [...common, "--max-units", String(budgets.maxUnits), "--max-sets", String(budgets.maxSets), "--max-bytes", String(budgets.maxBytes), "--json"];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function allFlagArgs(command, fixture, contract) {
|
|
203
|
+
const args = ["authority", command, fixture.root];
|
|
204
|
+
const valueFor = {
|
|
205
|
+
spec: fixture.spec,
|
|
206
|
+
"scope-bindings": fixture.scopeBindings,
|
|
207
|
+
term: "standaloneTerm",
|
|
208
|
+
};
|
|
209
|
+
const integerValue = { "max-bytes": "1000000", "max-terms": "1000" };
|
|
210
|
+
for (const flag of contract.surface?.value_flags ?? []) args.push(`--${flag}`, valueFor[flag] ?? "fixture-value");
|
|
211
|
+
for (const flag of contract.surface?.integer_flags ?? []) args.push(`--${flag}`, integerValue[flag] ?? "100");
|
|
212
|
+
for (const flag of contract.surface?.repeatable_flags ?? []) {
|
|
213
|
+
if (flag === "external-root") {
|
|
214
|
+
args.push("--external-root", `peer=${fixture.externalOne}`, "--external-root", `other=${fixture.externalTwo}`);
|
|
215
|
+
} else args.push(`--${flag}`, "fixture-one", `--${flag}`, "fixture-two");
|
|
216
|
+
}
|
|
217
|
+
for (const flag of contract.surface?.boolean_flags ?? []) args.push(`--${flag}`);
|
|
218
|
+
return args;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function replaceFlagValue(args, flag, value) {
|
|
222
|
+
const result = [...args];
|
|
223
|
+
const index = result.indexOf(flag);
|
|
224
|
+
if (index === -1) throw new Error(`fixture invocation has no ${flag}`);
|
|
225
|
+
result[index + 1] = String(value);
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function reportFormat(command, report) {
|
|
230
|
+
if (command === "anchors") return report?.format;
|
|
231
|
+
if (command === "consumers") return report?.consumers?.format;
|
|
232
|
+
if (command === "terms") return report?.terms?.format;
|
|
233
|
+
return report?.closed_sets?.format;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function noPartialPayload(command, report) {
|
|
237
|
+
if (report?.complete !== false || report?.partial !== false) return false;
|
|
238
|
+
if (command === "anchors") {
|
|
239
|
+
return report?.summary?.units === 0 && report?.summary?.anchorsChecked === 0;
|
|
240
|
+
}
|
|
241
|
+
if (command === "consumers") return report?.consumers === null;
|
|
242
|
+
if (command === "terms") return report?.terms === null;
|
|
243
|
+
return report?.closed_sets === null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function hasFinding(command, report) {
|
|
247
|
+
if (command === "anchors") return (report?.diagnostics?.length ?? 0) > 0;
|
|
248
|
+
if (command === "consumers") return (report?.consumers?.specConsumption?.counts?.canonicalDead ?? 0) > 0;
|
|
249
|
+
if (command === "terms") return (report?.terms?.index?.length ?? 0) > 0;
|
|
250
|
+
return (report?.summary?.pairsWithDeltas ?? 0) > 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function addAnchorFinding(fixture) {
|
|
254
|
+
const container = YAML.parse(await readFile(fixture.source, "utf8"));
|
|
255
|
+
const definition = container.units.find((unit) => unit.id === "definition.fixture");
|
|
256
|
+
definition.meaning += " Run pnpm missing-script and pnpm another-missing-script";
|
|
257
|
+
await writeFile(fixture.source, YAML.stringify(container, { indent: 2, lineWidth: 0 }), "utf8");
|
|
258
|
+
const added = await runGit(fixture.root, ["add", "--", "."]);
|
|
259
|
+
if (added.code !== 0) throw new Error(`could not update fixture Git index: ${added.stderr}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function addTrackedInventoryFile(fixture) {
|
|
263
|
+
await writeFile(path.join(fixture.root, "inventory-added.txt"), "tracked inventory change\n", "utf8");
|
|
264
|
+
const added = await runGit(fixture.root, ["add", "--", "inventory-added.txt"]);
|
|
265
|
+
if (added.code !== 0) throw new Error(`could not extend fixture Git inventory: ${added.stderr}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function packedCli(root) {
|
|
269
|
+
const temporaryRoot = await mkdtemp(path.join(os.tmpdir(), "nimicoding-product-conformance-pack-"));
|
|
270
|
+
const packDirectory = path.join(temporaryRoot, "pack");
|
|
271
|
+
const consumer = path.join(temporaryRoot, "consumer");
|
|
272
|
+
await mkdir(packDirectory);
|
|
273
|
+
await mkdir(consumer);
|
|
274
|
+
await writeFile(path.join(consumer, "package.json"), '{"name":"product-conformance-packed-consumer","private":true}\n', "utf8");
|
|
275
|
+
const environment = nestedNpmEnvironment();
|
|
276
|
+
const pack = npmCommand(["pack", "--ignore-scripts", "--pack-destination", packDirectory]);
|
|
277
|
+
const packed = await execute(pack.file, pack.args, { cwd: root, env: environment });
|
|
278
|
+
if (packed.code !== 0) throw new Error(`npm pack failed: ${packed.stderr || packed.stdout}`);
|
|
279
|
+
const tarballName = packed.stdout.trim().split(/\r?\n/u).at(-1);
|
|
280
|
+
const install = npmCommand(["install", "--ignore-scripts", "--no-audit", "--no-fund", "--no-package-lock", path.join(packDirectory, tarballName)]);
|
|
281
|
+
const installed = await execute(install.file, install.args, { cwd: consumer, env: environment });
|
|
282
|
+
if (installed.code !== 0) throw new Error(`packed package extraction failed: ${installed.stderr || installed.stdout}`);
|
|
283
|
+
const installedPackageRoot = path.join(consumer, "node_modules", "@nimiplatform", "nimi-coding");
|
|
284
|
+
return {
|
|
285
|
+
cliPath: path.join(installedPackageRoot, "bin", "nimicoding.mjs"),
|
|
286
|
+
contractDirectory: path.join(installedPackageRoot, "spec"),
|
|
287
|
+
packageRoot: installedPackageRoot,
|
|
288
|
+
cleanup: () => rm(temporaryRoot, { recursive: true, force: true }),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function installBuiltinLeafPath(contract) {
|
|
293
|
+
const index = contract?.lexical_grammar?.class_B?.builtin_subcommands_excluded?.indexOf("install") ?? -1;
|
|
294
|
+
if (index === -1) throw new Error("anchors contract does not classify pnpm install as a builtin");
|
|
295
|
+
return `lexical_grammar.class_B.builtin_subcommands_excluded[${index}]`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function createClassification(contracts) {
|
|
299
|
+
const leaves = new Map();
|
|
300
|
+
const add = (contract, leafPath, value) => {
|
|
301
|
+
const key = `${contract}:${leafPath}`;
|
|
302
|
+
if (!leaves.has(key)) leaves.set(key, { contract, path: leafPath, value, status: "unmapped", details: [] });
|
|
303
|
+
};
|
|
304
|
+
for (const [command, contract] of Object.entries(contracts)) {
|
|
305
|
+
for (const section of ["surface", "budgets", "result"]) {
|
|
306
|
+
walkLeaves(contract[section], section, (leafPath, value) => add(command, leafPath, value));
|
|
307
|
+
}
|
|
308
|
+
for (const key of ["boolean_flags", "value_flags", "integer_flags", "repeatable_flags"]) {
|
|
309
|
+
const leafPath = `surface.${key}`;
|
|
310
|
+
if (!Object.hasOwn(contract.surface ?? {}, key) || !Array.isArray(contract.surface[key])) {
|
|
311
|
+
add(command, leafPath, contract.surface?.[key]);
|
|
312
|
+
const leaf = leaves.get(`${command}:${leafPath}`);
|
|
313
|
+
leaf.status = "contradicted";
|
|
314
|
+
leaf.details.push("required surface collection is missing or is not an array");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
add(command, "command", contract.command);
|
|
318
|
+
}
|
|
319
|
+
for (const [command, paths] of Object.entries({
|
|
320
|
+
anchors: [
|
|
321
|
+
installBuiltinLeafPath(contracts.anchors),
|
|
322
|
+
"lexical_grammar.class_B.resolution",
|
|
323
|
+
"lexical_grammar.class_C.shape",
|
|
324
|
+
],
|
|
325
|
+
terms: ["inputs.term.required_for_code", "lexical_grammar.excluded"],
|
|
326
|
+
})) {
|
|
327
|
+
for (const leafPath of paths) add(command, leafPath, valueAt(contracts[command], leafPath));
|
|
328
|
+
}
|
|
329
|
+
for (const [key, reason] of NOT_MACHINE_CHECKABLE) {
|
|
330
|
+
const leaf = leaves.get(key);
|
|
331
|
+
if (!leaf) throw new Error(`not-machine-checkable declaration names no leaf: ${key}`);
|
|
332
|
+
leaf.status = "not_machine_checkable";
|
|
333
|
+
leaf.details.push(reason);
|
|
334
|
+
}
|
|
335
|
+
return leaves;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function addAuthoringClassification(leaves, authoring) {
|
|
339
|
+
const values = authoring?.commands?.check?.require_format?.admitted_values;
|
|
340
|
+
if (!Array.isArray(values) || values.length === 0) throw new Error("authoring contract declares no admitted check formats");
|
|
341
|
+
values.forEach((value, index) => leaves.set(`authoring:commands.check.require_format.admitted_values[${index}]`, {
|
|
342
|
+
contract: "authoring",
|
|
343
|
+
path: `commands.check.require_format.admitted_values[${index}]`,
|
|
344
|
+
value,
|
|
345
|
+
status: "unmapped",
|
|
346
|
+
details: [],
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function classify(leaves, contract, leafPath, matches, detail) {
|
|
351
|
+
const leaf = leaves.get(`${contract}:${leafPath}`);
|
|
352
|
+
if (!leaf) throw new Error(`behavioural check names no classified leaf: ${contract}:${leafPath}`);
|
|
353
|
+
leaf.details.push(detail);
|
|
354
|
+
if (!matches) leaf.status = "contradicted";
|
|
355
|
+
else if (leaf.status === "unmapped") leaf.status = "verified";
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function flagPaths(contract) {
|
|
359
|
+
const paths = [];
|
|
360
|
+
for (const key of ["boolean_flags", "value_flags", "integer_flags", "repeatable_flags"]) {
|
|
361
|
+
const values = contract.surface?.[key];
|
|
362
|
+
if (Array.isArray(values) && values.length === 0) paths.push(`surface.${key}`);
|
|
363
|
+
else (values ?? []).forEach((_, index) => paths.push(`surface.${key}[${index}]`));
|
|
364
|
+
}
|
|
365
|
+
return paths;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function commandSequence(command) {
|
|
369
|
+
const sequence = [];
|
|
370
|
+
for (let raw of String(command).trim().split(/\s+/u).filter(Boolean)) {
|
|
371
|
+
while (raw.startsWith("[")) {
|
|
372
|
+
sequence.push("optional:start");
|
|
373
|
+
raw = raw.slice(1);
|
|
374
|
+
}
|
|
375
|
+
const repeatable = raw.endsWith("...");
|
|
376
|
+
if (repeatable) raw = raw.slice(0, -3);
|
|
377
|
+
let optionalEnds = 0;
|
|
378
|
+
while (raw.endsWith("]")) {
|
|
379
|
+
optionalEnds += 1;
|
|
380
|
+
raw = raw.slice(0, -1);
|
|
381
|
+
}
|
|
382
|
+
for (const token of raw.match(/<[^>]+>|--[a-z][a-z0-9-]*|[^<]+/gu) ?? []) {
|
|
383
|
+
if (/^<[^>]+>$/u.test(token)) sequence.push(`metavariable:${token.slice(1, -1)}`);
|
|
384
|
+
else if (/^--/u.test(token)) sequence.push(`flag:${token.slice(2)}`);
|
|
385
|
+
else sequence.push(`token:${token}`);
|
|
386
|
+
}
|
|
387
|
+
for (let index = 0; index < optionalEnds; index += 1) sequence.push("optional:end");
|
|
388
|
+
if (repeatable) sequence.push("repeatable");
|
|
389
|
+
}
|
|
390
|
+
return sequence;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function usageCommand(stderr, command) {
|
|
394
|
+
return String(stderr).split(/\r?\n/u).find((line) => line.startsWith(`nimicoding authority ${command} `)) ?? "";
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function exerciseParserSurface(cliPath, fixture, contracts, leaves) {
|
|
398
|
+
const validReports = {};
|
|
399
|
+
for (const [command, contract] of Object.entries(contracts)) {
|
|
400
|
+
const validArgs = allFlagArgs(command, fixture, contract);
|
|
401
|
+
const valid = await runCli(cliPath, fixture.root, validArgs);
|
|
402
|
+
const report = parseJson(valid);
|
|
403
|
+
validReports[command] = { output: valid, report, args: validArgs };
|
|
404
|
+
|
|
405
|
+
const unknown = await runCli(cliPath, fixture.root, [...requiredArgs(command, fixture), "--unknown-product-conformance-flag"]);
|
|
406
|
+
const repeat = await runCli(cliPath, fixture.root, [...requiredArgs(command, fixture), "--json"]);
|
|
407
|
+
const parserMatches = valid.code === 0
|
|
408
|
+
&& unknown.code === 2 && unknown.stdout === ""
|
|
409
|
+
&& repeat.code === 2 && repeat.stdout === "";
|
|
410
|
+
const usage = usageCommand(unknown.stderr, command);
|
|
411
|
+
const commandMatches = parserMatches
|
|
412
|
+
&& JSON.stringify(commandSequence(contract.command)) === JSON.stringify(commandSequence(usage));
|
|
413
|
+
classify(
|
|
414
|
+
leaves,
|
|
415
|
+
command,
|
|
416
|
+
"command",
|
|
417
|
+
commandMatches,
|
|
418
|
+
`valid=${valid.code}; unknown=${unknown.code}; repeated=${repeat.code}; usage=${JSON.stringify(usage)}`,
|
|
419
|
+
);
|
|
420
|
+
for (const leafPath of flagPaths(contract)) {
|
|
421
|
+
classify(leaves, command, leafPath, parserMatches, `closed parser invocation: valid=${valid.code}; unknown=${unknown.code}; repeated=${repeat.code}`);
|
|
422
|
+
}
|
|
423
|
+
classify(
|
|
424
|
+
leaves,
|
|
425
|
+
command,
|
|
426
|
+
"surface.report_format",
|
|
427
|
+
valid.code === 0 && reportFormat(command, report) === contract.surface.report_format,
|
|
428
|
+
`JSON declared ${JSON.stringify(reportFormat(command, report))}`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
return validReports;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function exerciseRequiredCombinations(cliPath, fixture, contracts, authoring, leaves) {
|
|
435
|
+
const codeWithoutTerm = await runCli(cliPath, fixture.root, [
|
|
436
|
+
...requiredArgs("terms", fixture).filter((entry) => entry !== "--json"),
|
|
437
|
+
"--code", "--json",
|
|
438
|
+
]);
|
|
439
|
+
const refusedCode = codeWithoutTerm.code === 2 && codeWithoutTerm.stdout === "";
|
|
440
|
+
classify(leaves, "terms", "inputs.term.required_for_code", refusedCode, `--code without --term exited ${codeWithoutTerm.code}`);
|
|
441
|
+
for (const leafPath of ["surface.boolean_flags[1]", "surface.value_flags[1]"]) {
|
|
442
|
+
classify(leaves, "terms", leafPath, refusedCode, `required --code/--term combination exited ${codeWithoutTerm.code}`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const admitted = authoring.commands.check.require_format.admitted_values;
|
|
446
|
+
const invalidFormat = await runCli(cliPath, fixture.root, [
|
|
447
|
+
"authority", "check", fixture.spec, "--require-format", "nimicoding.authority/invalid", "--json",
|
|
448
|
+
]);
|
|
449
|
+
const formatRefusal = invalidFormat.code === 2
|
|
450
|
+
&& invalidFormat.stdout === ""
|
|
451
|
+
&& admitted.every((format) => invalidFormat.stderr.includes(format));
|
|
452
|
+
admitted.forEach((_, index) => classify(
|
|
453
|
+
leaves,
|
|
454
|
+
"authoring",
|
|
455
|
+
`commands.check.require_format.admitted_values[${index}]`,
|
|
456
|
+
formatRefusal,
|
|
457
|
+
`invalid --require-format exited ${invalidFormat.code} and named admitted formats`,
|
|
458
|
+
));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function independentFixtureOracle(fixture) {
|
|
462
|
+
const bytes = async (relative) => Buffer.byteLength(await readFile(path.join(fixture.root, relative)));
|
|
463
|
+
const authorityBytes = await bytes(".nimi/spec/fixture.authority.yaml");
|
|
464
|
+
const nonCorpus = [
|
|
465
|
+
"config/kinds.yaml",
|
|
466
|
+
"package.json",
|
|
467
|
+
"proto/kinds.proto",
|
|
468
|
+
"scope-bindings.yaml",
|
|
469
|
+
"src/consumer.ts",
|
|
470
|
+
"src/live.ts",
|
|
471
|
+
];
|
|
472
|
+
const nonCorpusBytes = (await Promise.all(nonCorpus.map(bytes))).reduce((total, value) => total + value, 0);
|
|
473
|
+
const implementationBytes = (await Promise.all([
|
|
474
|
+
"config/kinds.yaml",
|
|
475
|
+
"proto/kinds.proto",
|
|
476
|
+
"scope-bindings.yaml",
|
|
477
|
+
].map(bytes))).reduce((total, value) => total + value, 0);
|
|
478
|
+
return {
|
|
479
|
+
units: 3,
|
|
480
|
+
files: 6,
|
|
481
|
+
refs: 2,
|
|
482
|
+
terms: 2,
|
|
483
|
+
sets: 2,
|
|
484
|
+
consumersInputBytes: nonCorpusBytes,
|
|
485
|
+
termsInputBytes: authorityBytes + nonCorpusBytes,
|
|
486
|
+
closedSetsInputBytes: authorityBytes + implementationBytes,
|
|
487
|
+
fixedBytesMatch: authorityBytes === 706
|
|
488
|
+
&& nonCorpusBytes === 602
|
|
489
|
+
&& authorityBytes + nonCorpusBytes === 1308
|
|
490
|
+
&& authorityBytes + implementationBytes === 984,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function budgetBoundary(cliPath, fixture, validArgs, flag, admittedValue) {
|
|
495
|
+
const admitted = await runCli(cliPath, fixture.root, replaceFlagValue(validArgs, flag, admittedValue));
|
|
496
|
+
const refused = await runCli(cliPath, fixture.root, replaceFlagValue(validArgs, flag, admittedValue - 1));
|
|
497
|
+
return {
|
|
498
|
+
admitted,
|
|
499
|
+
admittedReport: parseJson(admitted),
|
|
500
|
+
refused,
|
|
501
|
+
refusedReport: parseJson(refused),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function exerciseCompleteAndBudgets(cliPath, fixture, contracts, leaves, validReports) {
|
|
506
|
+
const oracle = await independentFixtureOracle(fixture);
|
|
507
|
+
const budgetCases = {
|
|
508
|
+
anchors: [["budgets.max_units", "--max-units", oracle.units]],
|
|
509
|
+
consumers: [
|
|
510
|
+
["budgets.max_files", "--max-files", oracle.files],
|
|
511
|
+
["budgets.max_refs", "--max-refs", oracle.refs],
|
|
512
|
+
["budgets.max_bytes", "--max-bytes", oracle.consumersInputBytes],
|
|
513
|
+
],
|
|
514
|
+
terms: [
|
|
515
|
+
["budgets.max_units", "--max-units", oracle.units],
|
|
516
|
+
["budgets.max_terms", "--max-terms", oracle.terms],
|
|
517
|
+
["budgets.max_bytes", "--max-bytes", oracle.termsInputBytes],
|
|
518
|
+
],
|
|
519
|
+
"closed-sets": [
|
|
520
|
+
["budgets.max_units", "--max-units", oracle.units],
|
|
521
|
+
["budgets.max_sets", "--max-sets", oracle.sets],
|
|
522
|
+
["budgets.max_bytes", "--max-bytes", oracle.closedSetsInputBytes],
|
|
523
|
+
],
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
for (const command of Object.keys(contracts)) {
|
|
527
|
+
const valid = validReports[command];
|
|
528
|
+
const validComplete = valid.output.code === 0 && valid.report?.complete === true && valid.report?.partial === false;
|
|
529
|
+
if (command === "anchors") {
|
|
530
|
+
classify(leaves, command, "budgets.truncation_or_partial_result", validComplete, `valid report complete=${valid.report?.complete}; partial=${valid.report?.partial}`);
|
|
531
|
+
} else {
|
|
532
|
+
classify(leaves, command, "result.complete", validComplete, `valid report complete=${valid.report?.complete}`);
|
|
533
|
+
classify(leaves, command, "result.partial", validComplete, `valid report partial=${valid.report?.partial}`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
for (const [budgetPath, flag, expected] of budgetCases[command]) {
|
|
537
|
+
const boundary = await budgetBoundary(cliPath, fixture, valid.args, flag, expected);
|
|
538
|
+
const boundaryMatches = oracle.fixedBytesMatch
|
|
539
|
+
&& boundary.admitted.code === 0
|
|
540
|
+
&& boundary.refused.code === 1
|
|
541
|
+
&& noPartialPayload(command, boundary.refusedReport);
|
|
542
|
+
classify(
|
|
543
|
+
leaves,
|
|
544
|
+
command,
|
|
545
|
+
budgetPath,
|
|
546
|
+
boundaryMatches,
|
|
547
|
+
`independent fixture N=${expected}; admitted=${boundary.admitted.code}; N-1=${boundary.refused.code}`,
|
|
548
|
+
);
|
|
549
|
+
const exceededPath = command === "anchors" ? "budgets.overflow" : "budgets.exceeded";
|
|
550
|
+
classify(leaves, command, exceededPath, boundaryMatches, `${flag} independent N/N-1 boundary`);
|
|
551
|
+
if (command === "anchors") {
|
|
552
|
+
classify(leaves, command, "budgets.N_and_N_minus_1", boundaryMatches, `${flag} independent symmetric boundary`);
|
|
553
|
+
classify(leaves, command, "budgets.truncation_or_partial_result", boundaryMatches, `${flag} refusal contained no partial product`);
|
|
554
|
+
} else {
|
|
555
|
+
classify(leaves, command, "result.complete", boundaryMatches, `${flag} refusal complete=${boundary.refusedReport?.complete}`);
|
|
556
|
+
classify(leaves, command, "result.partial", boundaryMatches, `${flag} refusal partial=${boundary.refusedReport?.partial}`);
|
|
557
|
+
}
|
|
558
|
+
if (budgetPath === "budgets.max_bytes" && command !== "consumers") {
|
|
559
|
+
classify(
|
|
560
|
+
leaves,
|
|
561
|
+
command,
|
|
562
|
+
"result.byte_counts.inputBytes",
|
|
563
|
+
boundaryMatches && boundary.admittedReport?.input_bytes === expected,
|
|
564
|
+
`independent decoded input bytes=${expected}; reported=${boundary.admittedReport?.input_bytes}`,
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function trackedTextInventoryMatches(inventory) {
|
|
572
|
+
return Number.isSafeInteger(inventory?.trackedFiles)
|
|
573
|
+
&& Number.isSafeInteger(inventory?.scannedFiles)
|
|
574
|
+
&& inventory?.excludedByReason !== null
|
|
575
|
+
&& typeof inventory?.excludedByReason === "object"
|
|
576
|
+
&& Array.isArray(inventory?.excluded)
|
|
577
|
+
&& inventory.trackedFiles === inventory.scannedFiles + inventory.excluded.length;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function exerciseRepositoryInventory(cliPath, fixture, contracts, leaves, validReports) {
|
|
581
|
+
const commands = Object.keys(contracts);
|
|
582
|
+
const before = Object.fromEntries(commands.map((command) => [command, validReports[command].report?.trackedPathSetDigest]));
|
|
583
|
+
const sameBefore = new Set(Object.values(before)).size === 1;
|
|
584
|
+
const hexBefore = Object.values(before).every((digest) => /^[0-9a-f]{64}$/u.test(digest ?? ""));
|
|
585
|
+
|
|
586
|
+
const consumerInventory = validReports.consumers.report?.consumers?.fileInventory;
|
|
587
|
+
classify(
|
|
588
|
+
leaves,
|
|
589
|
+
"consumers",
|
|
590
|
+
"result.file_inventory",
|
|
591
|
+
contracts.consumers.result.file_inventory === "tracked_text_inventory_v1" && trackedTextInventoryMatches(consumerInventory),
|
|
592
|
+
`tracked=${consumerInventory?.trackedFiles}; scanned=${consumerInventory?.scannedFiles}; excluded=${consumerInventory?.excluded?.length}`,
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
await addTrackedInventoryFile(fixture);
|
|
596
|
+
const after = {};
|
|
597
|
+
for (const command of commands) {
|
|
598
|
+
const output = await runCli(cliPath, fixture.root, validReports[command].args);
|
|
599
|
+
const report = parseJson(output);
|
|
600
|
+
after[command] = report?.trackedPathSetDigest;
|
|
601
|
+
}
|
|
602
|
+
const sameAfter = new Set(Object.values(after)).size === 1;
|
|
603
|
+
const changedTogether = commands.every((command) => before[command] !== after[command]);
|
|
604
|
+
for (const command of commands) {
|
|
605
|
+
classify(
|
|
606
|
+
leaves,
|
|
607
|
+
command,
|
|
608
|
+
"result.trackedPathSetDigest",
|
|
609
|
+
hexBefore && sameBefore && sameAfter && changedTogether,
|
|
610
|
+
`before=${before[command]}; after=${after[command]}; same-before=${sameBefore}; same-after=${sameAfter}; changed-together=${changedTogether}`,
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function exerciseGrammar(cliPath, fixture, leaves, validReports) {
|
|
616
|
+
const anchors = validReports.anchors.report;
|
|
617
|
+
const rightBoundary = validReports.anchors.output.code === 0
|
|
618
|
+
&& anchors?.summary?.anchorsChecked === 0
|
|
619
|
+
&& (anchors?.diagnostics?.length ?? -1) === 0;
|
|
620
|
+
classify(leaves, "anchors", "lexical_grammar.class_C.shape", rightBoundary, `src/live.ts.old yielded ${anchors?.summary?.anchorsChecked} anchors`);
|
|
621
|
+
|
|
622
|
+
const termsOutput = await runCli(cliPath, fixture.root, requiredArgs("terms", fixture));
|
|
623
|
+
const terms = parseJson(termsOutput);
|
|
624
|
+
const indexed = terms?.terms?.index?.map((entry) => entry.term) ?? [];
|
|
625
|
+
const pathPartition = termsOutput.code === 0 && indexed.includes("standaloneTerm") && !indexed.includes("pathCamelCase");
|
|
626
|
+
classify(leaves, "terms", "lexical_grammar.excluded", pathPartition, `pathCamelCase indexed=${indexed.includes("pathCamelCase")}`);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async function findingsInvocation(cliPath, fixture, command) {
|
|
630
|
+
const output = await runCli(cliPath, fixture.root, requiredArgs(command, fixture));
|
|
631
|
+
return { output, report: parseJson(output) };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function classifyFinding(leaves, contracts, command, execution, label) {
|
|
635
|
+
const paths = ["surface.findings_change_exit_code"];
|
|
636
|
+
if (valueAt(contracts[command], "result.findings_change_exit_code") !== undefined) paths.push("result.findings_change_exit_code");
|
|
637
|
+
for (const leafPath of paths) {
|
|
638
|
+
const expectedExit = valueAt(contracts[command], leafPath) === true ? 1 : 0;
|
|
639
|
+
const matches = execution.output.code === expectedExit && hasFinding(command, execution.report);
|
|
640
|
+
classify(
|
|
641
|
+
leaves,
|
|
642
|
+
command,
|
|
643
|
+
leafPath,
|
|
644
|
+
matches,
|
|
645
|
+
`${label} finding exited ${execution.output.code}; expected ${expectedExit}; finding=${hasFinding(command, execution.report)}`,
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async function exerciseFindings(cliPath, fixture, contracts, leaves, label) {
|
|
651
|
+
const results = {};
|
|
652
|
+
for (const command of Object.keys(contracts)) {
|
|
653
|
+
results[command] = await findingsInvocation(cliPath, fixture, command);
|
|
654
|
+
classifyFinding(leaves, contracts, command, results[command], label);
|
|
655
|
+
}
|
|
656
|
+
return results;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function classifyAnchorGrammar(leaves, contract, execution) {
|
|
660
|
+
const diagnostics = execution.report?.diagnostics ?? [];
|
|
661
|
+
const anchors = diagnostics.map((diagnostic) => diagnostic.anchor);
|
|
662
|
+
const partition = execution.output.code === 1
|
|
663
|
+
&& execution.report?.summary?.anchorsChecked === 2
|
|
664
|
+
&& anchors.includes("pnpm missing-script")
|
|
665
|
+
&& !anchors.includes("pnpm install");
|
|
666
|
+
classify(leaves, "anchors", installBuiltinLeafPath(contract), partition, `pnpm install excluded=${!anchors.includes("pnpm install")}`);
|
|
667
|
+
classify(leaves, "anchors", "lexical_grammar.class_B.resolution", partition, `pnpm missing-script diagnosed=${anchors.includes("pnpm missing-script")}`);
|
|
668
|
+
classify(leaves, "anchors", "result.unresolved_anchor_or_glob", partition, `dead script report exited ${execution.output.code}`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function classifyExpected(leaves, contract, leafPath, expectedValue, behaviourMatches, detail) {
|
|
672
|
+
const leaf = leaves.get(`${contract}:${leafPath}`);
|
|
673
|
+
const valueMatches = leaf !== undefined && JSON.stringify(leaf.value) === JSON.stringify(expectedValue);
|
|
674
|
+
classify(leaves, contract, leafPath, valueMatches && behaviourMatches, `${detail}; value=${JSON.stringify(leaf?.value)}`);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async function exerciseCoreClosure(cliPath, fixture, contracts, leaves, validReports, findings) {
|
|
678
|
+
const baselineMatches = {
|
|
679
|
+
anchors: validReports.anchors.report?.summary?.units === 3
|
|
680
|
+
&& validReports.anchors.report?.summary?.anchorsChecked === 0
|
|
681
|
+
&& validReports.anchors.report?.summary?.diagnostics === 0,
|
|
682
|
+
consumers: validReports.consumers.report?.summary?.filesScanned === 6
|
|
683
|
+
&& validReports.consumers.report?.summary?.references === 2,
|
|
684
|
+
terms: validReports.terms.report?.summary?.units === 3
|
|
685
|
+
&& validReports.terms.report?.summary?.distinctTerms === 2,
|
|
686
|
+
"closed-sets": validReports["closed-sets"].report?.summary?.sets?.total === 2,
|
|
687
|
+
};
|
|
688
|
+
for (const command of Object.keys(contracts)) {
|
|
689
|
+
classifyExpected(
|
|
690
|
+
leaves,
|
|
691
|
+
command,
|
|
692
|
+
"surface.corpus_root",
|
|
693
|
+
"repository_authority_corpus_only",
|
|
694
|
+
validReports[command].output.code === 0 && baselineMatches[command],
|
|
695
|
+
"canonical fixture corpus was admitted with fixed counts",
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const anchorReport = findings.anchors.report;
|
|
700
|
+
const anchorArgs = requiredArgs("anchors", fixture);
|
|
701
|
+
const anchorCountBoundary = await budgetBoundary(cliPath, fixture, anchorArgs, "--max-anchors", 2);
|
|
702
|
+
const anchorCountMatches = anchorCountBoundary.admitted.code === 1
|
|
703
|
+
&& anchorCountBoundary.admittedReport?.complete === true
|
|
704
|
+
&& anchorCountBoundary.admittedReport?.summary?.anchorsChecked === 2
|
|
705
|
+
&& anchorCountBoundary.refused.code === 1
|
|
706
|
+
&& noPartialPayload("anchors", anchorCountBoundary.refusedReport);
|
|
707
|
+
classifyExpected(leaves, "anchors", "budgets.max_anchors", "complete_extracted_anchor_occurrence_count", anchorCountMatches, "fixed anchor count N=2/N-1=1");
|
|
708
|
+
classifyExpected(leaves, "anchors", "budgets.N_and_N_minus_1", "exact_symmetric_boundary", anchorCountMatches, "fixed anchor count symmetric boundary");
|
|
709
|
+
classifyExpected(leaves, "anchors", "budgets.overflow", "refused_exit_1_with_required_and_admitted_values", anchorCountMatches, "anchor count overflow refused without payload");
|
|
710
|
+
classifyExpected(leaves, "anchors", "budgets.truncation_or_partial_result", "forbidden", anchorCountMatches, "anchor count refusal was not partial");
|
|
711
|
+
|
|
712
|
+
const stdoutBytes = Buffer.byteLength(findings.anchors.output.stdout, "utf8");
|
|
713
|
+
const anchorByteBoundary = await budgetBoundary(cliPath, fixture, anchorArgs, "--max-bytes", stdoutBytes);
|
|
714
|
+
const anchorByteMatches = anchorByteBoundary.admitted.code === 1
|
|
715
|
+
&& Buffer.byteLength(anchorByteBoundary.admitted.stdout, "utf8") === stdoutBytes
|
|
716
|
+
&& anchorByteBoundary.admittedReport?.summary?.units === 3
|
|
717
|
+
&& anchorByteBoundary.admittedReport?.summary?.anchorsChecked === 2
|
|
718
|
+
&& anchorByteBoundary.admittedReport?.summary?.diagnostics === 2
|
|
719
|
+
&& anchorByteBoundary.refused.code === 1
|
|
720
|
+
&& noPartialPayload("anchors", anchorByteBoundary.refusedReport);
|
|
721
|
+
classifyExpected(leaves, "anchors", "budgets.max_bytes", "UTF-8_size_of_compact_format_complete_summary_and_diagnostics_product", anchorByteMatches, `actual stdout UTF-8 bytes N=${stdoutBytes}/N-1`);
|
|
722
|
+
classifyExpected(leaves, "anchors", "budgets.N_and_N_minus_1", "exact_symmetric_boundary", anchorByteMatches, "actual stdout byte symmetric boundary");
|
|
723
|
+
|
|
724
|
+
const diagnosticFields = ["unitId", "field", "anchor"];
|
|
725
|
+
const diagnostics = anchorReport?.diagnostics ?? [];
|
|
726
|
+
const expectedEndColumn = 207;
|
|
727
|
+
const locationsMatch = diagnostics.length === 2 && diagnostics.every((diagnostic) => (
|
|
728
|
+
diagnostic.unitId === "definition.fixture"
|
|
729
|
+
&& diagnostic.field === "meaning"
|
|
730
|
+
&& diagnostic.pointer === "/units/0/meaning"
|
|
731
|
+
&& diagnostic.range?.start?.line === 7
|
|
732
|
+
&& diagnostic.range?.start?.column === 14
|
|
733
|
+
&& diagnostic.range?.end?.line === 7
|
|
734
|
+
&& diagnostic.range?.end?.column === expectedEndColumn
|
|
735
|
+
));
|
|
736
|
+
classifyExpected(leaves, "anchors", "result.diagnostic_location", "exact_unit_ID_field_SourceMap_range_and_source_pointer", locationsMatch, "fixed YAML SourceMap anchor locations");
|
|
737
|
+
for (const [index, field] of diagnosticFields.entries()) {
|
|
738
|
+
classifyExpected(leaves, "anchors", `result.anchor_diagnostic_fields[${index}]`, field, diagnostics.every((entry) => Object.hasOwn(entry, field)), `diagnostics expose ${field}`);
|
|
739
|
+
}
|
|
740
|
+
for (const [index, field] of ["units", "anchorsChecked", "diagnostics"].entries()) {
|
|
741
|
+
classifyExpected(leaves, "anchors", `result.summary_fields[${index}]`, field, Object.hasOwn(anchorReport?.summary ?? {}, field), `summary exposes ${field}`);
|
|
742
|
+
}
|
|
743
|
+
const anchorOrder = diagnostics.map((entry) => entry.anchor);
|
|
744
|
+
const orderingMatches = JSON.stringify(anchorOrder) === JSON.stringify(["pnpm another-missing-script", "pnpm missing-script"]);
|
|
745
|
+
for (const [index, value] of ["unit_ID", "anchor_text", "field", "class"].entries()) {
|
|
746
|
+
classifyExpected(leaves, "anchors", `result.ordering.anchors[${index}]`, value, orderingMatches, `fixed diagnostics order=${JSON.stringify(anchorOrder)}`);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const originalBindings = await readFile(fixture.scopeBindings, "utf8");
|
|
750
|
+
let scopeExecution;
|
|
751
|
+
try {
|
|
752
|
+
const bindings = YAML.parse(originalBindings);
|
|
753
|
+
bindings.scopes[0].bindings[0].value = "missing/**/*.ts";
|
|
754
|
+
await writeFile(fixture.scopeBindings, YAML.stringify(bindings, { indent: 2, lineWidth: 0 }), "utf8");
|
|
755
|
+
const scopeArgs = [...anchorArgs];
|
|
756
|
+
scopeArgs.splice(-1, 0, "--scope-bindings", fixture.scopeBindings);
|
|
757
|
+
scopeExecution = await runCli(cliPath, fixture.root, scopeArgs);
|
|
758
|
+
} finally {
|
|
759
|
+
await writeFile(fixture.scopeBindings, originalBindings, "utf8");
|
|
760
|
+
}
|
|
761
|
+
const scopeDiagnostics = (parseJson(scopeExecution)?.diagnostics ?? []).filter((entry) => entry.scope !== undefined);
|
|
762
|
+
const scopeMatches = scopeDiagnostics.length === 1
|
|
763
|
+
&& scopeDiagnostics[0].scope === "api.fixture"
|
|
764
|
+
&& scopeDiagnostics[0].glob === "missing/**/*.ts";
|
|
765
|
+
for (const [index, field] of ["scope", "glob"].entries()) {
|
|
766
|
+
classifyExpected(leaves, "anchors", `result.scope_diagnostic_fields[${index}]`, field, scopeMatches && Object.hasOwn(scopeDiagnostics[0], field), `fixed scope diagnostic exposes ${field}`);
|
|
767
|
+
classifyExpected(leaves, "anchors", `result.ordering.scope_globs[${index}]`, field, scopeMatches, "one fixed scope/glob row is deterministically ordered");
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const anchorHuman = await runCli(cliPath, fixture.root, anchorArgs.filter((entry) => entry !== "--json"));
|
|
771
|
+
classifyExpected(
|
|
772
|
+
leaves,
|
|
773
|
+
"anchors",
|
|
774
|
+
"result.human_and_json",
|
|
775
|
+
"required",
|
|
776
|
+
anchorHuman.code === 1 && anchorHuman.stdout.includes("nimicoding authority anchors:") && anchorReport?.format === contracts.anchors.surface.report_format,
|
|
777
|
+
"fixed finding rendered in human and JSON modes",
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
const consumerCounts = validReports.consumers.report?.consumers?.specConsumption?.counts ?? {};
|
|
781
|
+
const countNames = ["total", "canonical", "canonicalDead", "externalAuthority", "externalDead", "externalUnresolved", "unresolved"];
|
|
782
|
+
for (const [index, field] of countNames.entries()) {
|
|
783
|
+
classifyExpected(leaves, "consumers", `result.counts[${index}]`, field, Object.hasOwn(consumerCounts, field) && consumerCounts.total === 2, `fixed reference count field ${field}`);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const termsIndex = await runCli(cliPath, fixture.root, requiredArgs("terms", fixture));
|
|
787
|
+
const termsIndexReport = parseJson(termsIndex);
|
|
788
|
+
const termsDetail = validReports.terms.report?.terms;
|
|
789
|
+
const modeMatches = termsIndexReport?.terms?.mode === "index" && termsDetail?.mode === "detail";
|
|
790
|
+
for (const [index, mode] of ["index", "detail"].entries()) {
|
|
791
|
+
classifyExpected(leaves, "terms", `result.modes[${index}]`, mode, modeMatches, "fixed index and detail invocations");
|
|
792
|
+
}
|
|
793
|
+
for (const [index, field] of ["term", "classes", "unitCount", "units", "code", "codeReferences", "codeInventory"].entries()) {
|
|
794
|
+
classifyExpected(leaves, "terms", `result.detail_fields[${index}]`, field, Object.hasOwn(termsDetail ?? {}, field), `detail report exposes ${field}`);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
classifyExpected(leaves, "terms", "result.byte_counts.scannedBytes", "selected_normative_authority_fields_plus_optional_decoded_code_inputs", validReports.terms.report?.scanned_bytes === 879, `fixed selected bytes=${validReports.terms.report?.scanned_bytes}`);
|
|
798
|
+
classifyExpected(leaves, "closed-sets", "result.byte_counts.scannedBytes", "normative_authority_fields_plus_decoded_proto_and_YAML_extractor_inputs", validReports["closed-sets"].report?.scanned_bytes === 510, `fixed selected bytes=${validReports["closed-sets"].report?.scanned_bytes}`);
|
|
799
|
+
|
|
800
|
+
for (const command of ["consumers", "terms", "closed-sets"]) {
|
|
801
|
+
const first = await runCli(cliPath, fixture.root, validReports[command].args);
|
|
802
|
+
const second = await runCli(cliPath, fixture.root, validReports[command].args);
|
|
803
|
+
classifyExpected(
|
|
804
|
+
leaves,
|
|
805
|
+
command,
|
|
806
|
+
"result.determinism",
|
|
807
|
+
"identical_inputs_produce_byte_identical_output",
|
|
808
|
+
first.code === second.code && Buffer.from(first.stdout).equals(Buffer.from(second.stdout)) && Buffer.from(first.stderr).equals(Buffer.from(second.stderr)),
|
|
809
|
+
"two identical invocations were byte-identical",
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function summarize(leaves) {
|
|
815
|
+
const contractNames = [...new Set([...leaves.values()].map((leaf) => leaf.contract))];
|
|
816
|
+
const contracts = contractNames.map((contract) => {
|
|
817
|
+
const selected = [...leaves.values()].filter((leaf) => leaf.contract === contract);
|
|
818
|
+
const counts = Object.fromEntries(STATUS_NAMES.map((status) => [status, selected.filter((leaf) => leaf.status === status).length]));
|
|
819
|
+
return { contract, counts, leaves: selected };
|
|
820
|
+
});
|
|
821
|
+
const core = [...leaves.values()].filter((leaf) => (
|
|
822
|
+
leaf.path === "command"
|
|
823
|
+
|| leaf.path.startsWith("surface.")
|
|
824
|
+
|| leaf.path.startsWith("budgets.")
|
|
825
|
+
|| leaf.path.startsWith("result.")
|
|
826
|
+
));
|
|
827
|
+
const contradictions = [...leaves.values()].filter((leaf) => leaf.status === "contradicted");
|
|
828
|
+
const unmapped = [...leaves.values()].filter((leaf) => leaf.status === "unmapped");
|
|
829
|
+
return {
|
|
830
|
+
contracts,
|
|
831
|
+
contradictions,
|
|
832
|
+
unmapped,
|
|
833
|
+
coverageComplete: unmapped.length === 0,
|
|
834
|
+
enforcedComplete: core.every((leaf) => leaf.status === "verified"),
|
|
835
|
+
coreFailures: core.filter((leaf) => leaf.status !== "verified"),
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
async function runConformanceLane({ contractDirectory, cliPath, lane }) {
|
|
840
|
+
const contracts = {};
|
|
841
|
+
for (const [command, file] of Object.entries(PRODUCT_CONTRACTS)) {
|
|
842
|
+
contracts[command] = YAML.parse(await readFile(path.join(contractDirectory, file), "utf8"));
|
|
843
|
+
}
|
|
844
|
+
const authoring = YAML.parse(await readFile(path.join(contractDirectory, "authority-authoring.yaml"), "utf8"));
|
|
845
|
+
const leaves = createClassification(contracts);
|
|
846
|
+
addAuthoringClassification(leaves, authoring);
|
|
847
|
+
const fixture = await createRepositoryFixture();
|
|
848
|
+
try {
|
|
849
|
+
const validReports = await exerciseParserSurface(cliPath, fixture, contracts, leaves);
|
|
850
|
+
await exerciseRequiredCombinations(cliPath, fixture, contracts, authoring, leaves);
|
|
851
|
+
await exerciseCompleteAndBudgets(cliPath, fixture, contracts, leaves, validReports);
|
|
852
|
+
await exerciseGrammar(cliPath, fixture, leaves, validReports);
|
|
853
|
+
await addAnchorFinding(fixture);
|
|
854
|
+
const findings = await exerciseFindings(cliPath, fixture, contracts, leaves, lane);
|
|
855
|
+
classifyAnchorGrammar(leaves, contracts.anchors, findings.anchors);
|
|
856
|
+
await exerciseCoreClosure(cliPath, fixture, contracts, leaves, validReports, findings);
|
|
857
|
+
await exerciseRepositoryInventory(cliPath, fixture, contracts, leaves, validReports);
|
|
858
|
+
|
|
859
|
+
return { ...summarize(leaves), lane };
|
|
860
|
+
} finally {
|
|
861
|
+
await rm(fixture.root, { recursive: true, force: true });
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
export async function checkProductConformanceLeaves({
|
|
866
|
+
contractDirectory = path.join(packageRoot, "spec"),
|
|
867
|
+
cliPath = path.join(packageRoot, "bin", "nimicoding.mjs"),
|
|
868
|
+
} = {}) {
|
|
869
|
+
return runConformanceLane({ contractDirectory, cliPath, lane: "worktree" });
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
export async function checkPackedProductConformance({ packRoot = packageRoot } = {}) {
|
|
873
|
+
const packed = await packedCli(packRoot);
|
|
874
|
+
try {
|
|
875
|
+
return await runConformanceLane({
|
|
876
|
+
contractDirectory: packed.contractDirectory,
|
|
877
|
+
cliPath: packed.cliPath,
|
|
878
|
+
lane: "packed",
|
|
879
|
+
});
|
|
880
|
+
} finally {
|
|
881
|
+
await packed.cleanup();
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
export function formatProductConformance(result) {
|
|
886
|
+
const lines = [
|
|
887
|
+
`product-conformance: lane=${result.lane}; coverageComplete=${result.coverageComplete}; enforcedComplete=${result.enforcedComplete}`,
|
|
888
|
+
...result.contracts.map(({ contract, counts }) => (
|
|
889
|
+
`product-conformance: ${contract} {verified: ${counts.verified}, contradicted: ${counts.contradicted}, not_machine_checkable: ${counts.not_machine_checkable}, unmapped: ${counts.unmapped}}`
|
|
890
|
+
)),
|
|
891
|
+
];
|
|
892
|
+
for (const leaf of result.contradictions) {
|
|
893
|
+
lines.push(`contradicted: ${leaf.contract} ${leaf.path} (${leaf.details.at(-1)})`);
|
|
894
|
+
}
|
|
895
|
+
lines.push("not_machine_checkable:");
|
|
896
|
+
for (const { leaves } of result.contracts) {
|
|
897
|
+
for (const leaf of leaves.filter((entry) => entry.status === "not_machine_checkable")) {
|
|
898
|
+
lines.push(` ${leaf.contract} ${leaf.path} (${leaf.details.at(-1)})`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
lines.push("unmapped:");
|
|
902
|
+
for (const leaf of result.unmapped) lines.push(` ${leaf.contract} ${leaf.path} = ${JSON.stringify(leaf.value)}`);
|
|
903
|
+
return `${lines.join("\n")}\n`;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("check-product-conformance.mjs")) {
|
|
907
|
+
try {
|
|
908
|
+
const result = process.argv.includes("--packed")
|
|
909
|
+
? await checkPackedProductConformance()
|
|
910
|
+
: await checkProductConformanceLeaves();
|
|
911
|
+
process.stdout.write(formatProductConformance(result));
|
|
912
|
+
if (!result.enforcedComplete) process.exitCode = 1;
|
|
913
|
+
} catch (error) {
|
|
914
|
+
process.stderr.write(`product-conformance: could not execute gate: ${error.message}\n`);
|
|
915
|
+
process.exitCode = 1;
|
|
916
|
+
}
|
|
917
|
+
}
|