@nimiplatform/nimi-coding 0.1.0 → 0.2.1
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 +19 -0
- package/CODE_OF_CONDUCT.md +28 -0
- package/CONTRIBUTING.md +45 -0
- package/README.md +371 -344
- package/README.zh-CN.md +307 -0
- package/SECURITY.md +26 -0
- package/adapters/oh-my-codex/README.md +8 -9
- package/cli/commands/audit-sweep.mjs +10 -10
- package/cli/commands/classify-spec-tree.mjs +5 -0
- package/cli/commands/closeout.mjs +3 -0
- package/cli/commands/generate-spec-derived-docs.mjs +20 -0
- package/cli/commands/generate-spec-migration-plan.mjs +30 -0
- package/cli/commands/start.mjs +5 -1
- package/cli/commands/surface-validator-command.mjs +49 -0
- package/cli/commands/sweep-design.mjs +295 -0
- package/cli/commands/sweep.mjs +22 -0
- package/cli/commands/sync.mjs +132 -0
- package/cli/commands/topic-formatters.mjs +8 -8
- package/cli/commands/validate-ai-governance.mjs +167 -46
- package/cli/commands/validate-domain-admission.mjs +5 -0
- package/cli/commands/validate-guidance-bodies.mjs +5 -0
- package/cli/commands/validate-placement.mjs +5 -0
- package/cli/commands/validate-projection-edges.mjs +5 -0
- package/cli/commands/validate-spec-audit.mjs +5 -1
- package/cli/commands/validate-table-family.mjs +5 -0
- package/cli/commands/validate-tracked-output-admission.mjs +5 -0
- package/cli/constants.mjs +5 -49
- package/cli/help.mjs +33 -11
- package/cli/index.mjs +20 -2
- package/cli/lib/audit-sweep-runtime/admissions.mjs +38 -29
- package/cli/lib/audit-sweep-runtime/audit-validity.mjs +8 -0
- package/cli/lib/audit-sweep-runtime/chunks.mjs +11 -11
- package/cli/lib/audit-sweep-runtime/closeout.mjs +8 -8
- package/cli/lib/audit-sweep-runtime/codex-auditor-evidence.mjs +3 -3
- package/cli/lib/audit-sweep-runtime/codex-auditor.mjs +10 -10
- package/cli/lib/audit-sweep-runtime/common.mjs +7 -7
- package/cli/lib/audit-sweep-runtime/format.mjs +3 -3
- package/cli/lib/audit-sweep-runtime/ingest.mjs +8 -8
- package/cli/lib/audit-sweep-runtime/inventory-spec-chunks.mjs +24 -27
- package/cli/lib/audit-sweep-runtime/inventory.mjs +58 -18
- package/cli/lib/audit-sweep-runtime/ledger.mjs +1 -1
- package/cli/lib/audit-sweep-runtime/p0p1-profile.mjs +2 -2
- package/cli/lib/audit-sweep-runtime/remediation.mjs +6 -6
- package/cli/lib/audit-sweep-runtime/rerun.mjs +6 -6
- package/cli/lib/audit-sweep-runtime/status.mjs +1 -1
- package/cli/lib/audit-sweep-runtime/validators.mjs +2 -2
- package/cli/lib/authority-convergence.mjs +397 -2
- package/cli/lib/blueprint-audit.mjs +5 -5
- package/cli/lib/closeout.mjs +126 -3
- package/cli/lib/contracts.mjs +21 -17
- package/cli/lib/handoff.mjs +29 -11
- package/cli/lib/high-risk-admission.mjs +60 -11
- package/cli/lib/high-risk-decision.mjs +31 -2
- package/cli/lib/high-risk-ingest.mjs +5 -1
- package/cli/lib/high-risk-review.mjs +5 -1
- package/cli/lib/internal/contracts-parse.mjs +195 -24
- package/cli/lib/internal/contracts-validators.mjs +3 -2
- package/cli/lib/internal/doctor-bootstrap-surface.mjs +82 -35
- package/cli/lib/internal/doctor-delegated-surface.mjs +1 -1
- package/cli/lib/internal/doctor-finalize.mjs +12 -8
- package/cli/lib/internal/doctor-inspectors.mjs +34 -1
- package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +74 -12
- package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +24 -6
- package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +18 -23
- package/cli/lib/internal/surface-taxonomy-validators.mjs +931 -0
- package/cli/lib/internal/validators-spec.mjs +229 -20
- package/cli/lib/sweep-design-runtime/common.mjs +246 -0
- package/cli/lib/sweep-design-runtime/engine.mjs +733 -0
- package/cli/lib/sweep-design-runtime/fix-topic.mjs +414 -0
- package/cli/lib/sweep-design-runtime/lifecycle.mjs +54 -0
- package/cli/lib/sweep-design-runtime/results.mjs +324 -0
- package/cli/lib/sweep-design.mjs +8 -0
- package/cli/lib/sync.mjs +143 -0
- package/cli/lib/topic-artifacts.mjs +186 -0
- package/cli/lib/topic-authority-coverage.mjs +73 -0
- package/cli/lib/topic-closeout.mjs +560 -0
- package/cli/lib/topic-common.mjs +404 -0
- package/cli/lib/topic-decisions.mjs +332 -0
- package/cli/lib/topic-draft-packets.mjs +126 -7
- package/cli/lib/topic-execution.mjs +515 -0
- package/cli/lib/topic-goal.mjs +112 -33
- package/cli/lib/topic-ledger.mjs +281 -0
- package/cli/lib/topic-lifecycle-artifacts.mjs +173 -0
- package/cli/lib/topic-root-validation.mjs +288 -0
- package/cli/lib/topic-runner-commands.mjs +174 -0
- package/cli/lib/topic-runner-deferral.mjs +532 -0
- package/cli/lib/topic-runner-stale-gates.mjs +114 -0
- package/cli/lib/topic-runner-validation.mjs +138 -0
- package/cli/lib/topic-runner.mjs +109 -154
- package/cli/lib/topic-scaffold.mjs +252 -0
- package/cli/lib/topic-waves.mjs +403 -0
- package/cli/lib/topic.mjs +81 -93
- package/cli/lib/value-helpers.mjs +6 -1
- package/cli/seeds/bootstrap.mjs +96 -20
- package/cli/seeds/seed-policy.yaml +67 -0
- package/config/bootstrap.yaml +1 -1
- package/config/skill-manifest.yaml +4 -2
- package/config/spec-generation-inputs.yaml +41 -19
- package/contracts/audit-remediation-map.schema.yaml +1 -0
- package/contracts/audit-sweep-result.yaml +4 -0
- package/contracts/domain-admission.schema.yaml +56 -0
- package/contracts/migration-inventory.schema.yaml +80 -0
- package/contracts/negative-fixtures.yaml +91 -0
- package/contracts/placement-contract.schema.yaml +163 -0
- package/contracts/projection-edge.schema.yaml +130 -0
- package/contracts/shared-enums.yaml +68 -0
- package/contracts/spec-generation-audit.schema.yaml +19 -4
- package/contracts/spec-generation-inputs.schema.yaml +130 -29
- package/contracts/spec-reconstruction-result.yaml +9 -5
- package/contracts/surface-taxonomy.schema.yaml +201 -0
- package/contracts/sweep-design-result.yaml +349 -0
- package/contracts/table-family.schema.yaml +121 -0
- package/contracts/topic-goal.schema.yaml +10 -1
- package/contracts/tracked-output-admission.schema.yaml +70 -0
- package/contracts/workflow-consumer.schema.yaml +112 -0
- package/methodology/audit-sweep-p0p1-recall.yaml +1 -1
- package/methodology/spec-reconstruction.yaml +53 -30
- package/package.json +19 -4
- package/spec/_meta/command-gating-matrix.yaml +33 -0
- package/spec/_meta/generate-drift-migration-checklist.yaml +44 -62
- package/spec/_meta/governance-routing-cutover-checklist.yaml +3 -3
- package/spec/_meta/phase2-impacted-surface-matrix.yaml +14 -14
- package/spec/_meta/spec-authority-cutover-readiness.yaml +3 -5
- package/spec/_meta/spec-tree-model.yaml +104 -36
- package/spec/bootstrap-state.yaml +36 -36
- package/spec/product-scope.yaml +13 -10
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
import { pathExists } from "../fs-helpers.mjs";
|
|
6
|
+
import { parseYamlText } from "../yaml-helpers.mjs";
|
|
7
|
+
import { isPlainObject } from "../value-helpers.mjs";
|
|
8
|
+
|
|
9
|
+
const SURFACE_RESULT_CONTRACT = "nimicoding.surface-validator-result.v1";
|
|
10
|
+
|
|
11
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
12
|
+
|
|
13
|
+
const CONTRACT_REFS = {
|
|
14
|
+
sharedEnums: "contracts/shared-enums.yaml",
|
|
15
|
+
surfaceTaxonomy: "contracts/surface-taxonomy.schema.yaml",
|
|
16
|
+
placement: "contracts/placement-contract.schema.yaml",
|
|
17
|
+
projectionEdge: "contracts/projection-edge.schema.yaml",
|
|
18
|
+
tableFamily: "contracts/table-family.schema.yaml",
|
|
19
|
+
domainAdmission: "contracts/domain-admission.schema.yaml",
|
|
20
|
+
trackedOutputAdmission: "contracts/tracked-output-admission.schema.yaml",
|
|
21
|
+
highRiskAdmission: "contracts/high-risk-admission.schema.yaml",
|
|
22
|
+
negativeFixtures: "contracts/negative-fixtures.yaml",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const TEXT_RULE_BODY_PATTERN = /\bMUST(?:\s+NOT)?\b|\bmust\s+not\b|必须|不得|fail(?:s|ed)?\s+closed/i;
|
|
26
|
+
const PRODUCT_RULE_ID_PATTERN = /\b[A-Z][A-Z0-9]*-[A-Z0-9]+-[A-Z0-9-]+\b/;
|
|
27
|
+
const GENERATED_REF_PATTERN = /\.nimi\/spec\/[^)\s]+\/kernel\/generated\/|\.nimi\/spec\/generated\/|kernel\/generated\//;
|
|
28
|
+
|
|
29
|
+
function toPosix(value) {
|
|
30
|
+
return value.split(path.sep).join(path.posix.sep);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function relativeRef(projectRoot, absolutePath) {
|
|
34
|
+
return toPosix(path.relative(projectRoot, absolutePath));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function readYamlAt(relativePath) {
|
|
38
|
+
const absolutePath = path.join(PACKAGE_ROOT, relativePath);
|
|
39
|
+
const text = await readFile(absolutePath, "utf8");
|
|
40
|
+
return {
|
|
41
|
+
path: path.relative(PACKAGE_ROOT, absolutePath),
|
|
42
|
+
text,
|
|
43
|
+
data: parseYamlText(text),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readHostYamlAt(projectRoot, relativePath) {
|
|
48
|
+
const absolutePath = path.join(projectRoot, relativePath);
|
|
49
|
+
const exists = await pathExists(absolutePath);
|
|
50
|
+
if (!exists?.isFile()) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const text = await readFile(absolutePath, "utf8");
|
|
54
|
+
return {
|
|
55
|
+
path: relativePath,
|
|
56
|
+
text,
|
|
57
|
+
data: parseYamlText(text),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function loadSurfaceContracts(projectRoot) {
|
|
62
|
+
const entries = await Promise.all(Object.entries(CONTRACT_REFS).map(async ([key, ref]) => [key, await readYamlAt(ref)]));
|
|
63
|
+
const contracts = Object.fromEntries(entries);
|
|
64
|
+
const hostDomainAdmission = await readHostYamlAt(projectRoot, ".nimi/contracts/domain-admission.schema.yaml");
|
|
65
|
+
if (hostDomainAdmission) {
|
|
66
|
+
contracts.domainAdmission = hostDomainAdmission;
|
|
67
|
+
}
|
|
68
|
+
return contracts;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function collectFilesUnder(rootPath) {
|
|
72
|
+
const exists = await pathExists(rootPath);
|
|
73
|
+
if (!exists || !exists.isDirectory()) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function walk(currentPath) {
|
|
78
|
+
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
79
|
+
const files = [];
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
const childPath = path.join(currentPath, entry.name);
|
|
82
|
+
if (entry.isDirectory()) {
|
|
83
|
+
files.push(...await walk(childPath));
|
|
84
|
+
} else if (entry.isFile()) {
|
|
85
|
+
files.push(childPath);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return files.sort();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return walk(rootPath);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function collectCandidateFiles(projectRoot, rootRef = ".nimi/spec") {
|
|
95
|
+
const roots = [
|
|
96
|
+
rootRef,
|
|
97
|
+
".nimi/contracts",
|
|
98
|
+
".nimi/methodology",
|
|
99
|
+
".nimi/config",
|
|
100
|
+
".nimi/derived",
|
|
101
|
+
".nimi/state",
|
|
102
|
+
".nimi/audit",
|
|
103
|
+
".nimi/roadmap",
|
|
104
|
+
];
|
|
105
|
+
const uniqueRoots = [...new Set(roots)];
|
|
106
|
+
const files = [];
|
|
107
|
+
for (const root of uniqueRoots) {
|
|
108
|
+
files.push(...await collectFilesUnder(path.resolve(projectRoot, root)));
|
|
109
|
+
}
|
|
110
|
+
return [...new Set(files)].sort();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function pathMatchesGlob(candidate, pattern) {
|
|
114
|
+
const tokens = [];
|
|
115
|
+
let source = pattern
|
|
116
|
+
.replaceAll("{spec,methodology,contracts,config}", "__BRACE_PACKAGE_ROOTS__")
|
|
117
|
+
.replaceAll("{config,contracts,methodology,spec}", "__BRACE_PACKAGE_ROOTS__")
|
|
118
|
+
.replaceAll("<domain>", "__DOMAIN__")
|
|
119
|
+
.replaceAll("**", "__DOUBLE_STAR__")
|
|
120
|
+
.replaceAll("*", "__STAR__");
|
|
121
|
+
|
|
122
|
+
source = source.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
123
|
+
source = source
|
|
124
|
+
.replaceAll("__BRACE_PACKAGE_ROOTS__", "(?:spec|methodology|contracts|config)")
|
|
125
|
+
.replaceAll("__DOMAIN__", "[^/]+")
|
|
126
|
+
.replaceAll("__DOUBLE_STAR__", ".*")
|
|
127
|
+
.replaceAll("__STAR__", "[^/]*");
|
|
128
|
+
tokens.push(new RegExp(`^${source}$`));
|
|
129
|
+
return tokens.some((regex) => regex.test(candidate));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function basenameNoExt(ref) {
|
|
133
|
+
return path.posix.basename(ref).replace(/\.(yaml|yml|md)$/i, "");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function firstPathSegment(ref) {
|
|
137
|
+
return ref.split("/")[0] ?? "";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function domainForSpecRef(ref) {
|
|
141
|
+
const parts = ref.split("/");
|
|
142
|
+
if (parts[0] !== ".nimi" || parts[1] !== "spec" || parts.length < 4) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
if (parts[2] === "_meta" || parts[2] === "generated") {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return parts[2] ?? null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function walkObjectKeys(value, callback) {
|
|
152
|
+
if (Array.isArray(value)) {
|
|
153
|
+
for (const entry of value) {
|
|
154
|
+
walkObjectKeys(entry, callback);
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (!isPlainObject(value)) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
162
|
+
callback(String(key), entry);
|
|
163
|
+
walkObjectKeys(entry, callback);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function containsAnyKey(value, keys) {
|
|
168
|
+
const wanted = new Set(keys);
|
|
169
|
+
let found = false;
|
|
170
|
+
walkObjectKeys(value, (key) => {
|
|
171
|
+
if (wanted.has(key)) {
|
|
172
|
+
found = true;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return found;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function yamlForText(text) {
|
|
179
|
+
return parseYamlText(text);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isYamlRef(ref) {
|
|
183
|
+
return /\.(yaml|yml)$/i.test(ref);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isMarkdownRef(ref) {
|
|
187
|
+
return /\.md$/i.test(ref);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function tableFamilyContractMap(contracts) {
|
|
191
|
+
const families = Array.isArray(contracts.tableFamily.data?.table_families)
|
|
192
|
+
? contracts.tableFamily.data.table_families
|
|
193
|
+
: [];
|
|
194
|
+
return new Map(families.map((entry) => [String(entry.table_family), entry]));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function domainAdmissionMap(contracts) {
|
|
198
|
+
const admissions = Array.isArray(contracts.domainAdmission.data?.domain_admissions)
|
|
199
|
+
? contracts.domainAdmission.data.domain_admissions
|
|
200
|
+
: [];
|
|
201
|
+
return new Map(admissions.map((entry) => [String(entry.domain_id), entry]));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function trackedAdmissionRoots(contracts) {
|
|
205
|
+
const admissions = Array.isArray(contracts.trackedOutputAdmission.data?.admissions)
|
|
206
|
+
? contracts.trackedOutputAdmission.data.admissions
|
|
207
|
+
: [];
|
|
208
|
+
return admissions.map((entry) => String(entry.root)).filter(Boolean);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isTrackedNonProductRef(ref) {
|
|
212
|
+
return ref.startsWith(".nimi/derived/")
|
|
213
|
+
|| ref.startsWith(".nimi/state/")
|
|
214
|
+
|| ref.startsWith(".nimi/audit/")
|
|
215
|
+
|| ref.startsWith(".nimi/roadmap/");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isAdmittedTrackedOutput(ref, admittedRoots) {
|
|
219
|
+
return admittedRoots.some((root) => pathMatchesGlob(ref, root));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function classifyRef(ref, text, parsedYaml) {
|
|
223
|
+
if (ref.startsWith(".nimi/spec/future/")) {
|
|
224
|
+
return "candidate_roadmap";
|
|
225
|
+
}
|
|
226
|
+
if (ref.startsWith(".nimi/spec/generated/") || ref.includes("/kernel/generated/")) {
|
|
227
|
+
return "derived_view";
|
|
228
|
+
}
|
|
229
|
+
if (ref === ".nimi/spec/_meta/spec-generation-audit.yaml" || ref.startsWith(".nimi/spec/_meta/spec-generation-audit/")) {
|
|
230
|
+
return "spec_generation_state";
|
|
231
|
+
}
|
|
232
|
+
if (ref.startsWith(".nimi/spec/_meta/")) {
|
|
233
|
+
if (ref.endsWith("-admission-anchor.yaml")) {
|
|
234
|
+
return "host_projection_anchor";
|
|
235
|
+
}
|
|
236
|
+
if (/cutover|checklist|matrix|readiness|migration/i.test(ref)) {
|
|
237
|
+
return "lifecycle_progress_state";
|
|
238
|
+
}
|
|
239
|
+
return "methodology_authority";
|
|
240
|
+
}
|
|
241
|
+
if (ref === ".nimi/spec/bootstrap-state.yaml") {
|
|
242
|
+
return "lifecycle_progress_state";
|
|
243
|
+
}
|
|
244
|
+
if (ref === ".nimi/spec/high-risk-admissions.yaml") {
|
|
245
|
+
return "product_admission_registry";
|
|
246
|
+
}
|
|
247
|
+
if (ref === ".nimi/spec/product-scope.yaml") {
|
|
248
|
+
return "methodology_authority";
|
|
249
|
+
}
|
|
250
|
+
if (ref.startsWith(".nimi/spec/") && ref.includes("/kernel/tables/") && isYamlRef(ref)) {
|
|
251
|
+
if (isPlainObject(parsedYaml) && parsedYaml.table_family === "support_registry") {
|
|
252
|
+
return "support_registry";
|
|
253
|
+
}
|
|
254
|
+
if (isPlainObject(parsedYaml) && typeof parsedYaml.table_family === "string") {
|
|
255
|
+
return "product_authority_table";
|
|
256
|
+
}
|
|
257
|
+
const stateKeys = ["done", "covered", "coverage_status", "audit_date", "evidence_report", "current", "proposed", "backlog_status", "migration_status", "mapping_status", "run_id", "ledger_ref"];
|
|
258
|
+
if (containsAnyKey(parsedYaml, stateKeys) || /rule-evidence|backlog|migration/i.test(basenameNoExt(ref))) {
|
|
259
|
+
return "audit_evidence_state";
|
|
260
|
+
}
|
|
261
|
+
return "product_authority_table";
|
|
262
|
+
}
|
|
263
|
+
if (ref.startsWith(".nimi/spec/") && isYamlRef(ref) && basenameNoExt(ref) === "spec-exempt-modules") {
|
|
264
|
+
return "support_registry";
|
|
265
|
+
}
|
|
266
|
+
if (ref.startsWith(".nimi/spec/") && ref.includes("/kernel/") && isMarkdownRef(ref)) {
|
|
267
|
+
return "product_authority";
|
|
268
|
+
}
|
|
269
|
+
if (ref === ".nimi/spec/INDEX.md" || (ref.startsWith(".nimi/spec/") && isMarkdownRef(ref))) {
|
|
270
|
+
return "thin_guidance";
|
|
271
|
+
}
|
|
272
|
+
if (ref.startsWith(".nimi/local/derived/")) {
|
|
273
|
+
return "derived_view";
|
|
274
|
+
}
|
|
275
|
+
if (ref.startsWith(".nimi/local/state/spec-generation/")) {
|
|
276
|
+
return "spec_generation_state";
|
|
277
|
+
}
|
|
278
|
+
if (ref.startsWith(".nimi/local/audit/")) {
|
|
279
|
+
return "audit_evidence_state";
|
|
280
|
+
}
|
|
281
|
+
if (ref.startsWith(".nimi/local/") || ref.startsWith(".local/")) {
|
|
282
|
+
return "operational_local_artifact";
|
|
283
|
+
}
|
|
284
|
+
if (ref.startsWith(".nimi/topics/")) {
|
|
285
|
+
return "lifecycle_progress_state";
|
|
286
|
+
}
|
|
287
|
+
if (ref.startsWith(".nimi/contracts/") || ref.startsWith(".nimi/methodology/")) {
|
|
288
|
+
return "nimicoding_managed_projection";
|
|
289
|
+
}
|
|
290
|
+
if (ref.startsWith(".nimi/config/")) {
|
|
291
|
+
return ref === ".nimi/config/host-overlay.yaml" ? "host_projection_anchor" : "nimicoding_managed_projection";
|
|
292
|
+
}
|
|
293
|
+
if (ref.startsWith("package://@nimiplatform/nimi-coding/")) {
|
|
294
|
+
return "methodology_authority";
|
|
295
|
+
}
|
|
296
|
+
if (isTrackedNonProductRef(ref)) {
|
|
297
|
+
if (ref.startsWith(".nimi/derived/")) return "derived_view";
|
|
298
|
+
if (ref.startsWith(".nimi/state/")) return "spec_generation_state";
|
|
299
|
+
if (ref.startsWith(".nimi/audit/")) return "audit_evidence_state";
|
|
300
|
+
return "candidate_roadmap";
|
|
301
|
+
}
|
|
302
|
+
return "unclassified";
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function dispositionFor(ref, surfaceClass, errors) {
|
|
306
|
+
if (errors.length > 0 && surfaceClass === "unclassified") {
|
|
307
|
+
return "block";
|
|
308
|
+
}
|
|
309
|
+
if (surfaceClass === "methodology_authority") {
|
|
310
|
+
return "move_package";
|
|
311
|
+
}
|
|
312
|
+
if (surfaceClass === "nimicoding_managed_projection") {
|
|
313
|
+
return "keep";
|
|
314
|
+
}
|
|
315
|
+
if (surfaceClass === "derived_view") {
|
|
316
|
+
return "delete";
|
|
317
|
+
}
|
|
318
|
+
if (["spec_generation_state", "audit_evidence_state", "candidate_roadmap", "lifecycle_progress_state", "operational_local_artifact"].includes(surfaceClass)) {
|
|
319
|
+
return "move_local";
|
|
320
|
+
}
|
|
321
|
+
if (errors.length > 0) {
|
|
322
|
+
return "block";
|
|
323
|
+
}
|
|
324
|
+
return "keep";
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function targetRootFor(surfaceClass, ref) {
|
|
328
|
+
if (surfaceClass === "methodology_authority") {
|
|
329
|
+
return "nimi-coding";
|
|
330
|
+
}
|
|
331
|
+
if (surfaceClass === "nimicoding_managed_projection") {
|
|
332
|
+
return ".nimi";
|
|
333
|
+
}
|
|
334
|
+
if (surfaceClass === "derived_view") {
|
|
335
|
+
return "stdout_view";
|
|
336
|
+
}
|
|
337
|
+
if (surfaceClass === "spec_generation_state" || surfaceClass === "lifecycle_progress_state" || surfaceClass === "operational_local_artifact") {
|
|
338
|
+
return ".nimi/local/state";
|
|
339
|
+
}
|
|
340
|
+
if (surfaceClass === "audit_evidence_state") {
|
|
341
|
+
return ".nimi/local/audit";
|
|
342
|
+
}
|
|
343
|
+
if (surfaceClass === "candidate_roadmap") {
|
|
344
|
+
return ".nimi/topics";
|
|
345
|
+
}
|
|
346
|
+
if (surfaceClass === "unclassified") {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
if (ref.startsWith(".nimi/spec/")) {
|
|
350
|
+
return ".nimi/spec";
|
|
351
|
+
}
|
|
352
|
+
return firstPathSegment(ref);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function ownerFor(surfaceClass, ref) {
|
|
356
|
+
if (surfaceClass === "methodology_authority" || surfaceClass === "nimicoding_managed_projection") {
|
|
357
|
+
return "nimi-coding";
|
|
358
|
+
}
|
|
359
|
+
if (surfaceClass === "derived_view") {
|
|
360
|
+
return "generator";
|
|
361
|
+
}
|
|
362
|
+
if (surfaceClass === "spec_generation_state") {
|
|
363
|
+
return "spec_generator";
|
|
364
|
+
}
|
|
365
|
+
if (surfaceClass === "audit_evidence_state") {
|
|
366
|
+
return "audit_workflow";
|
|
367
|
+
}
|
|
368
|
+
if (surfaceClass === "candidate_roadmap") {
|
|
369
|
+
return "planning_owner";
|
|
370
|
+
}
|
|
371
|
+
if (surfaceClass === "lifecycle_progress_state") {
|
|
372
|
+
return "topic_or_execution_workflow";
|
|
373
|
+
}
|
|
374
|
+
return domainForSpecRef(ref) ?? "product_domain";
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function confirmationFor(errors) {
|
|
378
|
+
if (errors.some((error) => error.includes("unadmitted_domain_retained_as_spec_input"))) {
|
|
379
|
+
return "product_semantic_fork";
|
|
380
|
+
}
|
|
381
|
+
if (errors.some((error) => error.includes("missing_domain_admission"))) {
|
|
382
|
+
return "owner_ambiguity";
|
|
383
|
+
}
|
|
384
|
+
if (errors.some((error) => error.includes("package_methodology_under_host_spec") || error.includes("package_body_promoted_to_product_authority"))) {
|
|
385
|
+
return "package_boundary_ambiguity";
|
|
386
|
+
}
|
|
387
|
+
return "none";
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function ambiguityFor(requiredConfirmation, surfaceClass) {
|
|
391
|
+
if (requiredConfirmation === "none") {
|
|
392
|
+
return {
|
|
393
|
+
posture: "none",
|
|
394
|
+
reason: null,
|
|
395
|
+
candidate_owners: [],
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
posture: "blocks_migration",
|
|
400
|
+
reason: requiredConfirmation,
|
|
401
|
+
candidate_owners: surfaceClass === "unclassified" ? [] : [ownerFor(surfaceClass, "")],
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function validationCommandsFor(entryClass) {
|
|
406
|
+
const commands = {
|
|
407
|
+
product_admission_registry: ["pnpm exec nimicoding validate-placement --profile nimi --root .nimi/spec"],
|
|
408
|
+
product_authority_table: ["pnpm exec nimicoding validate-table-family --profile nimi --root .nimi/spec"],
|
|
409
|
+
support_registry: ["pnpm exec nimicoding validate-table-family --profile nimi --root .nimi/spec"],
|
|
410
|
+
thin_guidance: ["pnpm exec nimicoding validate-guidance-bodies --profile nimi --root .nimi/spec"],
|
|
411
|
+
host_projection_anchor: ["pnpm exec nimicoding validate-projection-edges --profile nimi --root .nimi/spec"],
|
|
412
|
+
nimicoding_managed_projection: ["pnpm exec nimicoding validate-placement --profile nimi --root .nimi/spec"],
|
|
413
|
+
candidate_roadmap: ["pnpm exec nimicoding validate-domain-admission --profile nimi --root .nimi/spec"],
|
|
414
|
+
};
|
|
415
|
+
return commands[entryClass] ?? ["pnpm exec nimicoding validate-placement --profile nimi --root .nimi/spec"];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function addError(errors, code, ref, detail) {
|
|
419
|
+
errors.push(detail ? `${code}: ${ref}: ${detail}` : `${code}: ${ref}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function validateRefPlacement(ref, surfaceClass, parsedYaml, text, contracts) {
|
|
423
|
+
const errors = [];
|
|
424
|
+
|
|
425
|
+
if (surfaceClass === "unclassified") {
|
|
426
|
+
addError(errors, "unclassified_file", ref);
|
|
427
|
+
}
|
|
428
|
+
if (surfaceClass === "derived_view" && ref.startsWith(".nimi/spec/")) {
|
|
429
|
+
addError(errors, "derived_view_under_product_authority_root", ref);
|
|
430
|
+
}
|
|
431
|
+
if (surfaceClass === "derived_view" && ref.startsWith(".nimi/local/derived/")) {
|
|
432
|
+
addError(errors, "derived_view_written_to_local_derived", ref);
|
|
433
|
+
}
|
|
434
|
+
if (surfaceClass === "spec_generation_state" && ref.startsWith(".nimi/spec/")) {
|
|
435
|
+
addError(errors, "spec_generation_state_under_spec", ref);
|
|
436
|
+
}
|
|
437
|
+
if (surfaceClass === "audit_evidence_state" && ref.startsWith(".nimi/spec/")) {
|
|
438
|
+
addError(errors, "audit_evidence_state_under_spec", ref);
|
|
439
|
+
}
|
|
440
|
+
if (surfaceClass === "lifecycle_progress_state" && ref.startsWith(".nimi/spec/")) {
|
|
441
|
+
addError(errors, "lifecycle_progress_state_under_spec", ref);
|
|
442
|
+
}
|
|
443
|
+
if (surfaceClass === "methodology_authority" && ref.startsWith(".nimi/spec/")) {
|
|
444
|
+
addError(errors, "package_methodology_under_host_spec", ref);
|
|
445
|
+
}
|
|
446
|
+
if (surfaceClass === "candidate_roadmap" && ref.startsWith(".nimi/spec/")) {
|
|
447
|
+
addError(errors, "candidate_roadmap_under_spec", ref);
|
|
448
|
+
}
|
|
449
|
+
if (isTrackedNonProductRef(ref) && !isAdmittedTrackedOutput(ref, trackedAdmissionRoots(contracts))) {
|
|
450
|
+
addError(errors, "tracked_non_product_without_admission", ref);
|
|
451
|
+
}
|
|
452
|
+
if (surfaceClass === "thin_guidance" && isMarkdownRef(ref)) {
|
|
453
|
+
if (TEXT_RULE_BODY_PATTERN.test(text)) {
|
|
454
|
+
addError(errors, "guidance_defines_rule_body", ref);
|
|
455
|
+
}
|
|
456
|
+
if (PRODUCT_RULE_ID_PATTERN.test(text) && !ref.endsWith("INDEX.md")) {
|
|
457
|
+
addError(errors, "guidance_defines_or_restates_rule_id", ref);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (surfaceClass === "product_authority" && isMarkdownRef(ref) && GENERATED_REF_PATTERN.test(text)) {
|
|
461
|
+
addError(errors, "derived_view_referenced_as_authority", ref);
|
|
462
|
+
}
|
|
463
|
+
if (surfaceClass === "product_admission_registry") {
|
|
464
|
+
errors.push(...validateProductAdmissionRegistryRef(ref, parsedYaml, text, contracts));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return errors;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function validateProductAdmissionRegistryRef(ref, parsedYaml, text, contracts) {
|
|
471
|
+
const errors = [];
|
|
472
|
+
if (ref !== ".nimi/spec/high-risk-admissions.yaml") {
|
|
473
|
+
addError(errors, "product_admission_registry_outside_allowed_root", ref);
|
|
474
|
+
return errors;
|
|
475
|
+
}
|
|
476
|
+
if (!isPlainObject(parsedYaml)) {
|
|
477
|
+
addError(errors, "product_admission_registry_invalid_yaml", ref);
|
|
478
|
+
return errors;
|
|
479
|
+
}
|
|
480
|
+
const requiredKeys = Array.isArray(contracts.highRiskAdmission.data?.top_level_required_keys)
|
|
481
|
+
? contracts.highRiskAdmission.data.top_level_required_keys
|
|
482
|
+
: [];
|
|
483
|
+
for (const key of requiredKeys) {
|
|
484
|
+
if (!(key in parsedYaml)) {
|
|
485
|
+
addError(errors, "product_admission_registry_missing_required_key", ref, key);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (/package_name:\s*"?@nimiplatform\/nimi-coding"?|spec_tree_model:|canonical_spec_root:/i.test(text)) {
|
|
489
|
+
addError(errors, "product_admission_registry_contains_package_methodology_body", ref);
|
|
490
|
+
}
|
|
491
|
+
return errors;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function validateTableRef(ref, parsedYaml, contracts) {
|
|
495
|
+
const errors = [];
|
|
496
|
+
if (!ref.startsWith(".nimi/spec/") || !ref.includes("/kernel/tables/") || !isYamlRef(ref)) {
|
|
497
|
+
return errors;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (!isPlainObject(parsedYaml)) {
|
|
501
|
+
addError(errors, "invalid_yaml_table", ref);
|
|
502
|
+
return errors;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const family = typeof parsedYaml.table_family === "string" ? parsedYaml.table_family : null;
|
|
506
|
+
const familyMap = tableFamilyContractMap(contracts);
|
|
507
|
+
if (!family) {
|
|
508
|
+
addError(errors, "missing_table_family", ref);
|
|
509
|
+
} else if (!familyMap.has(family)) {
|
|
510
|
+
addError(errors, "unknown_table_family", ref, family);
|
|
511
|
+
} else {
|
|
512
|
+
const familyContract = familyMap.get(family);
|
|
513
|
+
for (const field of familyContract.required_fields ?? []) {
|
|
514
|
+
if (!(field in parsedYaml)) {
|
|
515
|
+
addError(errors, "missing_table_family_required_field", ref, String(field));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const forbiddenFields = contracts.tableFamily.data?.forbidden_fields_by_authority_class?.product_authority_table ?? [];
|
|
521
|
+
if (containsAnyKey(parsedYaml, forbiddenFields)) {
|
|
522
|
+
addError(errors, "table_contains_forbidden_state_or_audit_field", ref);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return errors;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function validateDomainRef(ref, contracts) {
|
|
529
|
+
const errors = [];
|
|
530
|
+
const domain = domainForSpecRef(ref);
|
|
531
|
+
if (!domain) {
|
|
532
|
+
return errors;
|
|
533
|
+
}
|
|
534
|
+
const admissions = domainAdmissionMap(contracts);
|
|
535
|
+
const admission = admissions.get(domain);
|
|
536
|
+
if (!admission) {
|
|
537
|
+
addError(errors, "missing_domain_admission", ref, domain);
|
|
538
|
+
return errors;
|
|
539
|
+
}
|
|
540
|
+
if (admission.authority_class === "excluded_from_spec") {
|
|
541
|
+
addError(errors, "excluded_domain_retained_under_spec", ref, domain);
|
|
542
|
+
}
|
|
543
|
+
if (admission.authority_class === "migration_input_only") {
|
|
544
|
+
addError(errors, "unadmitted_domain_retained_as_spec_input", ref, domain);
|
|
545
|
+
}
|
|
546
|
+
return errors;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function buildInventory(projectRoot, options = {}) {
|
|
550
|
+
const contracts = await loadSurfaceContracts(projectRoot);
|
|
551
|
+
const rootRef = options.rootRef ?? ".nimi/spec";
|
|
552
|
+
const absoluteFiles = await collectCandidateFiles(projectRoot, rootRef);
|
|
553
|
+
const entries = [];
|
|
554
|
+
const errors = [];
|
|
555
|
+
|
|
556
|
+
for (const absolutePath of absoluteFiles) {
|
|
557
|
+
const ref = relativeRef(projectRoot, absolutePath);
|
|
558
|
+
const text = await readFile(absolutePath, "utf8");
|
|
559
|
+
const parsedYaml = isYamlRef(ref) ? yamlForText(text) : null;
|
|
560
|
+
const surfaceClass = classifyRef(ref, text, parsedYaml);
|
|
561
|
+
const entryErrors = [
|
|
562
|
+
...validateRefPlacement(ref, surfaceClass, parsedYaml, text, contracts),
|
|
563
|
+
...validateTableRef(ref, parsedYaml, contracts),
|
|
564
|
+
...validateDomainRef(ref, contracts),
|
|
565
|
+
];
|
|
566
|
+
errors.push(...entryErrors);
|
|
567
|
+
const requiredConfirmation = confirmationFor(entryErrors);
|
|
568
|
+
entries.push({
|
|
569
|
+
source_path: ref,
|
|
570
|
+
current_inferred_class: surfaceClass,
|
|
571
|
+
target_class: surfaceClass,
|
|
572
|
+
disposition: dispositionFor(ref, surfaceClass, entryErrors),
|
|
573
|
+
target_root: targetRootFor(surfaceClass, ref),
|
|
574
|
+
owner: ownerFor(surfaceClass, ref),
|
|
575
|
+
required_confirmation: requiredConfirmation,
|
|
576
|
+
ambiguity: ambiguityFor(requiredConfirmation, surfaceClass),
|
|
577
|
+
evidence: entryErrors.length > 0 ? entryErrors : [`classified_as:${surfaceClass}`],
|
|
578
|
+
validation_commands: validationCommandsFor(surfaceClass),
|
|
579
|
+
errors: entryErrors,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
contracts,
|
|
585
|
+
entries,
|
|
586
|
+
errors,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function summarize(entries) {
|
|
591
|
+
const byClass = {};
|
|
592
|
+
const byDisposition = {};
|
|
593
|
+
for (const entry of entries) {
|
|
594
|
+
byClass[entry.current_inferred_class] = (byClass[entry.current_inferred_class] ?? 0) + 1;
|
|
595
|
+
byDisposition[entry.disposition] = (byDisposition[entry.disposition] ?? 0) + 1;
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
total_files: entries.length,
|
|
599
|
+
by_surface_class: byClass,
|
|
600
|
+
by_disposition: byDisposition,
|
|
601
|
+
blocking_entries: entries.filter((entry) => entry.disposition === "block").length,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function reportFor(validator, ok, errors, warnings, entries, extra = {}) {
|
|
606
|
+
return {
|
|
607
|
+
contract: SURFACE_RESULT_CONTRACT,
|
|
608
|
+
validator,
|
|
609
|
+
ok,
|
|
610
|
+
errors,
|
|
611
|
+
warnings,
|
|
612
|
+
summary: summarize(entries),
|
|
613
|
+
...extra,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function countBy(entries, field) {
|
|
618
|
+
const counts = {};
|
|
619
|
+
for (const entry of entries) {
|
|
620
|
+
const value = String(entry[field] ?? "unknown");
|
|
621
|
+
counts[value] = (counts[value] ?? 0) + 1;
|
|
622
|
+
}
|
|
623
|
+
return counts;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function groupInventoryByDisposition(entries) {
|
|
627
|
+
const groups = {};
|
|
628
|
+
for (const entry of entries) {
|
|
629
|
+
const disposition = String(entry.disposition);
|
|
630
|
+
const group = groups[disposition] ?? [];
|
|
631
|
+
group.push(entry.source_path);
|
|
632
|
+
groups[disposition] = group;
|
|
633
|
+
}
|
|
634
|
+
return Object.fromEntries(Object.entries(groups).map(([key, value]) => [key, value.sort()]));
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function buildMigrationExecutionPackets(entries) {
|
|
638
|
+
const packetKinds = [
|
|
639
|
+
{
|
|
640
|
+
packet_id: "move-package-methodology-copied-under-spec",
|
|
641
|
+
dispositions: ["move_package"],
|
|
642
|
+
requires_confirmation: false,
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
packet_id: "move-local-derived-audit-state-and-lifecycle",
|
|
646
|
+
dispositions: ["move_local"],
|
|
647
|
+
requires_confirmation: false,
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
packet_id: "rewrite-product-authority-tables-and-guidance",
|
|
651
|
+
dispositions: ["rewrite"],
|
|
652
|
+
requires_confirmation: true,
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
packet_id: "resolve-blocked-semantic-forks",
|
|
656
|
+
dispositions: ["block"],
|
|
657
|
+
requires_confirmation: true,
|
|
658
|
+
},
|
|
659
|
+
];
|
|
660
|
+
return packetKinds
|
|
661
|
+
.map((packet) => {
|
|
662
|
+
const matching = entries.filter((entry) => packet.dispositions.includes(entry.disposition));
|
|
663
|
+
return {
|
|
664
|
+
...packet,
|
|
665
|
+
entry_count: matching.length,
|
|
666
|
+
source_paths: matching.map((entry) => entry.source_path).sort(),
|
|
667
|
+
};
|
|
668
|
+
})
|
|
669
|
+
.filter((packet) => packet.entry_count > 0);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function enumValidation(entries, inventory) {
|
|
673
|
+
const targetClasses = new Set(inventory.target_class_enum);
|
|
674
|
+
const dispositions = new Set(inventory.disposition_enum);
|
|
675
|
+
return {
|
|
676
|
+
unknown_target_classes: entries
|
|
677
|
+
.filter((entry) => !targetClasses.has(entry.target_class))
|
|
678
|
+
.map((entry) => entry.source_path),
|
|
679
|
+
unknown_dispositions: entries
|
|
680
|
+
.filter((entry) => !dispositions.has(entry.disposition))
|
|
681
|
+
.map((entry) => entry.source_path),
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function migrationPlanForInventory(rootRef, inventory) {
|
|
686
|
+
const entries = inventory.inventory;
|
|
687
|
+
const enumStatus = enumValidation(entries, inventory);
|
|
688
|
+
const requiredConfirmationEntries = entries.filter((entry) => entry.required_confirmation !== "none");
|
|
689
|
+
return {
|
|
690
|
+
contract: "nimicoding.spec-migration-plan.v1",
|
|
691
|
+
ok: enumStatus.unknown_target_classes.length === 0 && enumStatus.unknown_dispositions.length === 0,
|
|
692
|
+
version: inventory.version,
|
|
693
|
+
root: rootRef,
|
|
694
|
+
inventory: entries,
|
|
695
|
+
disposition_enum: inventory.disposition_enum,
|
|
696
|
+
target_class_enum: inventory.target_class_enum,
|
|
697
|
+
semantic_constraints: [
|
|
698
|
+
...inventory.semantic_constraints,
|
|
699
|
+
"migration_plan_must_not_modify_files",
|
|
700
|
+
"required_confirmation_entries_must_be_preserved",
|
|
701
|
+
"local_only_plan_artifact_required",
|
|
702
|
+
],
|
|
703
|
+
summary: {
|
|
704
|
+
total_files: entries.length,
|
|
705
|
+
by_surface_class: countBy(entries, "current_inferred_class"),
|
|
706
|
+
by_disposition: countBy(entries, "disposition"),
|
|
707
|
+
by_required_confirmation: countBy(entries, "required_confirmation"),
|
|
708
|
+
required_confirmation_count: requiredConfirmationEntries.length,
|
|
709
|
+
blocking_entries: entries.filter((entry) => entry.disposition === "block").length,
|
|
710
|
+
},
|
|
711
|
+
enum_validation: enumStatus,
|
|
712
|
+
required_confirmations: requiredConfirmationEntries.map((entry) => ({
|
|
713
|
+
source_path: entry.source_path,
|
|
714
|
+
required_confirmation: entry.required_confirmation,
|
|
715
|
+
ambiguity: entry.ambiguity,
|
|
716
|
+
evidence: entry.evidence,
|
|
717
|
+
recommendation: entry.required_confirmation === "product_semantic_fork"
|
|
718
|
+
? "stop_for_user_decision_before_migration"
|
|
719
|
+
: "resolve_owner_or_package_boundary_before_migration",
|
|
720
|
+
})),
|
|
721
|
+
groups: groupInventoryByDisposition(entries),
|
|
722
|
+
execution_packets: buildMigrationExecutionPackets(entries),
|
|
723
|
+
mutation_policy: {
|
|
724
|
+
mutates_source_tree: false,
|
|
725
|
+
allowed_output_roots: [".nimi/local/state/spec-surface"],
|
|
726
|
+
forbidden_output_roots: [".nimi/spec", ".nimi/contracts", ".nimi/methodology", ".nimi/config"],
|
|
727
|
+
},
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export async function classifySpecSurface(projectRoot, options = {}) {
|
|
732
|
+
const { entries, errors } = await buildInventory(projectRoot, options);
|
|
733
|
+
return reportFor("classify-spec-tree", errors.length === 0, errors, [], entries, {
|
|
734
|
+
inventory: {
|
|
735
|
+
version: 1,
|
|
736
|
+
inventory: entries,
|
|
737
|
+
disposition_enum: ["keep", "move_package", "move_local", "split", "rewrite", "delete", "block"],
|
|
738
|
+
target_class_enum: [
|
|
739
|
+
"product_authority",
|
|
740
|
+
"product_authority_table",
|
|
741
|
+
"product_admission_registry",
|
|
742
|
+
"thin_guidance",
|
|
743
|
+
"derived_view",
|
|
744
|
+
"spec_generation_state",
|
|
745
|
+
"audit_evidence_state",
|
|
746
|
+
"operational_local_artifact",
|
|
747
|
+
"methodology_authority",
|
|
748
|
+
"nimicoding_managed_projection",
|
|
749
|
+
"host_projection_anchor",
|
|
750
|
+
"candidate_roadmap",
|
|
751
|
+
"support_registry",
|
|
752
|
+
"lifecycle_progress_state",
|
|
753
|
+
],
|
|
754
|
+
semantic_constraints: [
|
|
755
|
+
"inventory_must_not_modify_files",
|
|
756
|
+
"future_under_spec_must_not_have_keep_disposition",
|
|
757
|
+
"nimicoding_managed_projection_must_not_be_promoted_to_product_authority",
|
|
758
|
+
],
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
export async function generateSpecMigrationPlan(projectRoot, options = {}) {
|
|
764
|
+
const classification = await classifySpecSurface(projectRoot, options);
|
|
765
|
+
return migrationPlanForInventory(options.rootRef ?? ".nimi/spec", classification.inventory);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
export async function buildSpecSurfaceInventory(projectRoot, options = {}) {
|
|
769
|
+
const { entries, errors } = await buildInventory(projectRoot, options);
|
|
770
|
+
return {
|
|
771
|
+
contract: SURFACE_RESULT_CONTRACT,
|
|
772
|
+
entries,
|
|
773
|
+
errors,
|
|
774
|
+
summary: summarize(entries),
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
export function isProductAuthoritySurfaceClass(surfaceClass) {
|
|
779
|
+
return [
|
|
780
|
+
"product_authority",
|
|
781
|
+
"product_authority_table",
|
|
782
|
+
"product_admission_registry",
|
|
783
|
+
"thin_guidance",
|
|
784
|
+
"host_projection_anchor",
|
|
785
|
+
"support_registry",
|
|
786
|
+
].includes(surfaceClass);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
export async function validatePlacement(projectRoot, options = {}) {
|
|
790
|
+
const { entries, errors } = await buildInventory(projectRoot, options);
|
|
791
|
+
return reportFor("validate-placement", errors.length === 0, errors, [], entries);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
export async function validateTableFamily(projectRoot, options = {}) {
|
|
795
|
+
const { contracts, entries } = await buildInventory(projectRoot, options);
|
|
796
|
+
const errors = [];
|
|
797
|
+
for (const entry of entries.filter((item) => item.source_path.startsWith(".nimi/spec/")
|
|
798
|
+
&& item.source_path.includes("/kernel/tables/")
|
|
799
|
+
&& isYamlRef(item.source_path)
|
|
800
|
+
&& ["product_authority_table", "support_registry"].includes(item.current_inferred_class))) {
|
|
801
|
+
const text = await readFile(path.join(projectRoot, entry.source_path), "utf8");
|
|
802
|
+
errors.push(...validateTableRef(entry.source_path, yamlForText(text), contracts));
|
|
803
|
+
}
|
|
804
|
+
return reportFor("validate-table-family", errors.length === 0, errors, [], entries);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export async function validateProjectionEdges(projectRoot, options = {}) {
|
|
808
|
+
const { entries, contracts } = await buildInventory(projectRoot, options);
|
|
809
|
+
const errors = [];
|
|
810
|
+
const edgeTargets = new Set((contracts.projectionEdge.data?.projection_edges ?? []).map((entry) => String(entry.target_ref)));
|
|
811
|
+
const anchorRef = ".nimi/spec/_meta/nimi-coding-admission-anchor.yaml";
|
|
812
|
+
const overlayRef = ".nimi/config/host-overlay.yaml";
|
|
813
|
+
if (entries.some((entry) => entry.source_path === anchorRef) && !edgeTargets.has(anchorRef)) {
|
|
814
|
+
addError(errors, "missing_projection_edge_for_anchor", anchorRef);
|
|
815
|
+
}
|
|
816
|
+
if (entries.some((entry) => entry.source_path === overlayRef) && !edgeTargets.has(overlayRef)) {
|
|
817
|
+
addError(errors, "missing_projection_edge_for_overlay", overlayRef);
|
|
818
|
+
}
|
|
819
|
+
for (const entry of entries) {
|
|
820
|
+
if (entry.current_inferred_class === "methodology_authority" && entry.source_path.startsWith(".nimi/spec/")) {
|
|
821
|
+
addError(errors, "package_body_promoted_to_product_authority", entry.source_path);
|
|
822
|
+
}
|
|
823
|
+
if (entry.current_inferred_class === "product_authority") {
|
|
824
|
+
const text = await readFile(path.join(projectRoot, entry.source_path), "utf8");
|
|
825
|
+
if (GENERATED_REF_PATTERN.test(text)) {
|
|
826
|
+
addError(errors, "derived_view_referenced_as_authority", entry.source_path);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return reportFor("validate-projection-edges", errors.length === 0, errors, [], entries);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
export async function validateGuidanceBodies(projectRoot, options = {}) {
|
|
834
|
+
const { entries } = await buildInventory(projectRoot, options);
|
|
835
|
+
const errors = [];
|
|
836
|
+
for (const entry of entries.filter((item) => item.current_inferred_class === "thin_guidance")) {
|
|
837
|
+
const text = await readFile(path.join(projectRoot, entry.source_path), "utf8");
|
|
838
|
+
if (TEXT_RULE_BODY_PATTERN.test(text)) {
|
|
839
|
+
addError(errors, "guidance_defines_rule_body", entry.source_path);
|
|
840
|
+
}
|
|
841
|
+
if (PRODUCT_RULE_ID_PATTERN.test(text) && !entry.source_path.endsWith("INDEX.md")) {
|
|
842
|
+
addError(errors, "guidance_defines_or_restates_rule_id", entry.source_path);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return reportFor("validate-guidance-bodies", errors.length === 0, errors, [], entries);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
export async function validateDomainAdmission(projectRoot, options = {}) {
|
|
849
|
+
const { contracts, entries } = await buildInventory(projectRoot, options);
|
|
850
|
+
const errors = [];
|
|
851
|
+
for (const entry of entries.filter((item) => item.source_path.startsWith(".nimi/spec/"))) {
|
|
852
|
+
errors.push(...validateDomainRef(entry.source_path, contracts));
|
|
853
|
+
}
|
|
854
|
+
return reportFor("validate-domain-admission", errors.length === 0, errors, [], entries);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
export async function validateTrackedOutputAdmission(projectRoot, options = {}) {
|
|
858
|
+
const { contracts, entries } = await buildInventory(projectRoot, options);
|
|
859
|
+
const admittedRoots = trackedAdmissionRoots(contracts);
|
|
860
|
+
const errors = [];
|
|
861
|
+
for (const entry of entries.filter((item) => isTrackedNonProductRef(item.source_path))) {
|
|
862
|
+
if (!isAdmittedTrackedOutput(entry.source_path, admittedRoots)) {
|
|
863
|
+
addError(errors, "tracked_non_product_without_admission", entry.source_path);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return reportFor("validate-tracked-output-admission", errors.length === 0, errors, [], entries);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
export function parseSurfaceValidatorOptions(args) {
|
|
870
|
+
const options = {
|
|
871
|
+
profile: "default",
|
|
872
|
+
rootRef: ".nimi/spec",
|
|
873
|
+
emit: null,
|
|
874
|
+
check: false,
|
|
875
|
+
json: false,
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
879
|
+
const arg = args[index];
|
|
880
|
+
if (arg === "--profile") {
|
|
881
|
+
options.profile = args[index + 1] ?? "";
|
|
882
|
+
index += 1;
|
|
883
|
+
} else if (arg === "--root") {
|
|
884
|
+
options.rootRef = args[index + 1] ?? "";
|
|
885
|
+
index += 1;
|
|
886
|
+
} else if (arg === "--emit") {
|
|
887
|
+
options.emit = args[index + 1] ?? "";
|
|
888
|
+
index += 1;
|
|
889
|
+
} else if (arg === "--check") {
|
|
890
|
+
options.check = true;
|
|
891
|
+
} else if (arg === "--json") {
|
|
892
|
+
options.json = true;
|
|
893
|
+
} else if (arg === "--strict") {
|
|
894
|
+
options.strict = true;
|
|
895
|
+
} else {
|
|
896
|
+
return { ok: false, error: `unknown option: ${arg}` };
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (!options.rootRef) {
|
|
901
|
+
return { ok: false, error: "--root must not be empty" };
|
|
902
|
+
}
|
|
903
|
+
return { ok: true, options };
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
export async function writeInventoryIfRequested(report, emitRef, projectRoot) {
|
|
907
|
+
if (!emitRef) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
const absolutePath = path.resolve(projectRoot, emitRef);
|
|
911
|
+
await mkdir(path.dirname(absolutePath), { recursive: true });
|
|
912
|
+
await writeFile(absolutePath, `${JSON.stringify(report.inventory, null, 2)}\n`, "utf8");
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
export async function writeMigrationPlanIfRequested(report, emitRef, projectRoot) {
|
|
916
|
+
if (!emitRef) {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const allowedRoot = path.resolve(projectRoot, ".nimi/local/state/spec-surface");
|
|
920
|
+
const absolutePath = path.resolve(projectRoot, emitRef);
|
|
921
|
+
const relativeToAllowedRoot = path.relative(allowedRoot, absolutePath);
|
|
922
|
+
if (
|
|
923
|
+
relativeToAllowedRoot === "" ||
|
|
924
|
+
relativeToAllowedRoot.startsWith("..") ||
|
|
925
|
+
path.isAbsolute(relativeToAllowedRoot)
|
|
926
|
+
) {
|
|
927
|
+
throw new Error("--emit must target .nimi/local/state/spec-surface/**");
|
|
928
|
+
}
|
|
929
|
+
await mkdir(path.dirname(absolutePath), { recursive: true });
|
|
930
|
+
await writeFile(absolutePath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
931
|
+
}
|