@nimiplatform/nimi-coding 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/CONTRIBUTING.md +3 -3
  3. package/README.md +285 -41
  4. package/README.zh-CN.md +294 -31
  5. package/bin/nimicoding.mjs +1 -1
  6. package/cli/commands/authority.mjs +849 -0
  7. package/cli/commands/clear.mjs +27 -7
  8. package/cli/commands/doctor.mjs +10 -1
  9. package/cli/commands/start.mjs +23 -27
  10. package/cli/commands/sync.mjs +21 -101
  11. package/cli/constants.mjs +7 -31
  12. package/cli/dev/check-authority-surface.mjs +173 -0
  13. package/cli/dev/check-product-conformance.mjs +917 -0
  14. package/cli/help.mjs +10 -17
  15. package/cli/index.mjs +6 -47
  16. package/cli/lib/authority/anchors.mjs +491 -0
  17. package/cli/lib/authority/audit.mjs +490 -0
  18. package/cli/lib/authority/closed-sets.mjs +646 -0
  19. package/cli/lib/authority/compile.mjs +110 -0
  20. package/cli/lib/authority/consumers.mjs +470 -0
  21. package/cli/lib/authority/diagnostics.mjs +87 -0
  22. package/cli/lib/authority/diff.mjs +287 -0
  23. package/cli/lib/authority/discover.mjs +447 -0
  24. package/cli/lib/authority/evidence-snapshot.mjs +1 -0
  25. package/cli/lib/authority/evidence.mjs +1432 -0
  26. package/cli/lib/authority/format.mjs +197 -0
  27. package/cli/lib/authority/git-snapshot.mjs +1065 -0
  28. package/cli/lib/authority/graph.mjs +351 -0
  29. package/cli/lib/authority/impact.mjs +252 -0
  30. package/cli/lib/authority/lexicon.mjs +55 -0
  31. package/cli/lib/authority/query.mjs +162 -0
  32. package/cli/lib/authority/repository-corpus.mjs +37 -0
  33. package/cli/lib/authority/repository-inventory.mjs +34 -0
  34. package/cli/lib/authority/review.mjs +222 -0
  35. package/cli/lib/authority/sarif.mjs +181 -0
  36. package/cli/lib/authority/scope-bindings.mjs +225 -0
  37. package/cli/lib/authority/source-map.mjs +105 -0
  38. package/cli/lib/authority/source-markdown.mjs +167 -0
  39. package/cli/lib/authority/source-yaml.mjs +312 -0
  40. package/cli/lib/authority/surface.mjs +79 -0
  41. package/cli/lib/authority/terms.mjs +421 -0
  42. package/cli/lib/authority/tracked-text.mjs +70 -0
  43. package/cli/lib/authority/validate.mjs +308 -0
  44. package/cli/lib/bootstrap.mjs +55 -71
  45. package/cli/lib/doctor.mjs +40 -123
  46. package/cli/lib/entrypoints.mjs +103 -105
  47. package/cli/lib/fs-helpers.mjs +139 -15
  48. package/cli/lib/internal/governance/config.mjs +0 -36
  49. package/cli/lib/shared.mjs +8 -21
  50. package/cli/lib/sync.mjs +81 -92
  51. package/cli/lib/value-helpers.mjs +0 -26
  52. package/cli/lib/yaml-helpers.mjs +1 -124
  53. package/cli/seeds/bootstrap.mjs +31 -87
  54. package/cli/seeds/seed-policy.yaml +9 -63
  55. package/contracts/authority-evidence-bindings.schema.yaml +53 -0
  56. package/contracts/authority-evidence-probe-results.schema.yaml +38 -0
  57. package/contracts/authority-impact-dispositions.schema.yaml +13 -0
  58. package/contracts/authority-scope-bindings.schema.yaml +15 -0
  59. package/contracts/authority-source.schema.yaml +64 -0
  60. package/contracts/authority-verifier-bindings.schema.yaml +20 -0
  61. package/methodology/authority-authoring.yaml +144 -0
  62. package/methodology/core.yaml +31 -26
  63. package/package.json +10 -6
  64. package/spec/authority-anchors.yaml +99 -0
  65. package/spec/authority-audit.yaml +85 -0
  66. package/spec/authority-authoring.yaml +150 -0
  67. package/spec/authority-closed-sets.yaml +65 -0
  68. package/spec/authority-consumers.yaml +70 -0
  69. package/spec/authority-context.yaml +50 -0
  70. package/spec/authority-discovery.yaml +119 -0
  71. package/spec/authority-evidence.yaml +200 -0
  72. package/spec/authority-graph.yaml +59 -0
  73. package/spec/authority-impact.yaml +84 -0
  74. package/spec/authority-review.yaml +89 -0
  75. package/spec/authority-terms.yaml +59 -0
  76. package/spec/product-scope.yaml +36 -23
  77. package/cli/commands/blueprint-audit.mjs +0 -91
  78. package/cli/commands/classify-spec-tree.mjs +0 -5
  79. package/cli/commands/generate-spec-derived-docs.mjs +0 -151
  80. package/cli/commands/generate-spec-migration-plan.mjs +0 -30
  81. package/cli/commands/surface-validator-command.mjs +0 -49
  82. package/cli/commands/validate-domain-admission.mjs +0 -5
  83. package/cli/commands/validate-guidance-bodies.mjs +0 -5
  84. package/cli/commands/validate-placement.mjs +0 -5
  85. package/cli/commands/validate-projection-edges.mjs +0 -5
  86. package/cli/commands/validate-spec-audit.mjs +0 -27
  87. package/cli/commands/validate-spec-governance.mjs +0 -124
  88. package/cli/commands/validate-spec-tree.mjs +0 -27
  89. package/cli/commands/validate-table-family.mjs +0 -5
  90. package/cli/commands/validate-tracked-output-admission.mjs +0 -5
  91. package/cli/lib/blueprint-audit.mjs +0 -363
  92. package/cli/lib/contracts.mjs +0 -21
  93. package/cli/lib/internal/contracts-loaders.mjs +0 -47
  94. package/cli/lib/internal/contracts-parse.mjs +0 -192
  95. package/cli/lib/internal/governance/runner.mjs +0 -35
  96. package/cli/lib/internal/surface-taxonomy-validators.mjs +0 -1147
  97. package/cli/lib/internal/validators-shared.mjs +0 -12
  98. package/cli/lib/internal/validators-spec-helpers.mjs +0 -52
  99. package/cli/lib/internal/validators-spec.mjs +0 -380
  100. package/cli/lib/validators.mjs +0 -30
  101. package/config/bootstrap.yaml +0 -6
  102. package/config/spec-generation-inputs.yaml +0 -41
  103. package/contracts/domain-admission.schema.yaml +0 -56
  104. package/contracts/migration-inventory.schema.yaml +0 -15
  105. package/contracts/negative-fixtures.yaml +0 -26
  106. package/contracts/placement-contract.schema.yaml +0 -127
  107. package/contracts/projection-edge.schema.yaml +0 -46
  108. package/contracts/shared-enums.yaml +0 -63
  109. package/contracts/spec-generation-audit.schema.yaml +0 -46
  110. package/contracts/spec-generation-inputs.schema.yaml +0 -43
  111. package/contracts/spec-layout.schema.yaml +0 -32
  112. package/contracts/surface-taxonomy.schema.yaml +0 -139
  113. package/contracts/table-family.schema.yaml +0 -133
  114. package/contracts/tracked-output-admission.schema.yaml +0 -17
  115. package/methodology/four-closure-policy.yaml +0 -20
  116. package/methodology/role-separation-policy.yaml +0 -22
  117. package/methodology/spec-reconstruction.yaml +0 -69
  118. package/spec/_meta/spec-tree-model.yaml +0 -44
@@ -1,1147 +0,0 @@
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.v2";
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
- specLayout: "contracts/spec-layout.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_DEFINITION_PATTERN = /^\s*(?:[-*]\s*)?(?:rule_id|rule id)\s*[:|]/im;
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
- const hostSpecLayout = await readHostYamlAt(projectRoot, ".nimi/config/spec-layout.yaml");
69
- contracts.hostSpecLayout = parseHostSpecLayout(
70
- hostSpecLayout,
71
- contracts.specLayout,
72
- contracts.tableFamily,
73
- );
74
- return contracts;
75
- }
76
-
77
- function isPortableRef(ref) {
78
- return typeof ref === "string"
79
- && ref.length > 0
80
- && !path.posix.isAbsolute(ref)
81
- && !ref.startsWith("./")
82
- && !ref.split("/").includes("..")
83
- && path.posix.normalize(ref) === ref;
84
- }
85
-
86
- function isRefWithin(ref, root) {
87
- return ref === root || ref.startsWith(`${root}/`);
88
- }
89
-
90
- function hasExactFields(value, requiredFields) {
91
- if (!isPlainObject(value)) {
92
- return false;
93
- }
94
- const actual = Object.keys(value).sort();
95
- const expected = [...requiredFields].sort();
96
- return actual.length === expected.length
97
- && actual.every((field, index) => field === expected[index]);
98
- }
99
-
100
- function isLiteralPortableRef(ref) {
101
- return isPortableRef(ref) && !/[\*{}]/.test(ref);
102
- }
103
-
104
- function isGeneratedProjectionRoot(ref, canonicalRoot) {
105
- return ref === `${canonicalRoot}/generated`
106
- || ref.startsWith(`${canonicalRoot}/generated/`)
107
- || /^\.nimi\/spec\/[^/]+\/kernel\/generated(?:\/.*)?$/.test(ref);
108
- }
109
-
110
- function parseHostSpecLayout(hostConfig, schemaContract, tableFamilyContract) {
111
- if (!hostConfig) {
112
- return {
113
- ok: true,
114
- present: false,
115
- errors: [],
116
- canonicalRoot: ".nimi/spec",
117
- hostInstructionPaths: [],
118
- trackedDerivedProjections: [],
119
- tableFamilyExtensions: [],
120
- };
121
- }
122
-
123
- const errors = [];
124
- const parsed = hostConfig.data;
125
- const layout = parsed?.spec_layout;
126
- const requiredDocumentFields = Array.isArray(schemaContract.data?.required_top_level_fields)
127
- ? schemaContract.data.required_top_level_fields.map(String)
128
- : [];
129
- const requiredLayoutFields = Array.isArray(schemaContract.data?.layout_required_fields)
130
- ? schemaContract.data.layout_required_fields.map(String)
131
- : [];
132
- const requiredProjectionFields = Array.isArray(schemaContract.data?.tracked_derived_projection_required_fields)
133
- ? schemaContract.data.tracked_derived_projection_required_fields.map(String)
134
- : [];
135
- const requiredFamilyFields = Array.isArray(schemaContract.data?.table_family_extension_required_fields)
136
- ? schemaContract.data.table_family_extension_required_fields.map(String)
137
- : [];
138
- const packageFamilyIds = new Set(
139
- (tableFamilyContract.data?.table_families ?? []).map((entry) => String(entry.table_family)),
140
- );
141
-
142
- if (parsed?.version !== 1 || parsed?.contract_ref !== ".nimi/contracts/spec-layout.schema.yaml" || !isPlainObject(layout)) {
143
- errors.push("invalid_spec_layout_config: .nimi/config/spec-layout.yaml");
144
- }
145
- if (!hasExactFields(parsed, requiredDocumentFields)) {
146
- errors.push("invalid_spec_layout_document_fields: .nimi/config/spec-layout.yaml");
147
- }
148
- if (!hasExactFields(layout, requiredLayoutFields)) {
149
- errors.push("invalid_spec_layout_fields: .nimi/config/spec-layout.yaml");
150
- }
151
- for (const field of requiredLayoutFields) {
152
- if (!(field in (layout ?? {}))) {
153
- errors.push(`missing_spec_layout_field: .nimi/config/spec-layout.yaml: ${field}`);
154
- }
155
- }
156
-
157
- const canonicalRoot = typeof layout?.canonical_root === "string" ? layout.canonical_root : ".nimi/spec";
158
- if (!isPortableRef(canonicalRoot) || canonicalRoot !== ".nimi/spec") {
159
- errors.push(`invalid_spec_layout_canonical_root: ${canonicalRoot}`);
160
- }
161
-
162
- if (!Array.isArray(layout?.host_instruction_paths)) {
163
- errors.push("invalid_host_instruction_paths: .nimi/config/spec-layout.yaml");
164
- }
165
- const hostInstructionPaths = Array.isArray(layout?.host_instruction_paths)
166
- ? layout.host_instruction_paths.map(String)
167
- : [];
168
- const seenInstructionPaths = new Set();
169
- for (const ref of hostInstructionPaths) {
170
- if (!isPortableRef(ref)
171
- || !ref.startsWith(`${canonicalRoot}/`)
172
- || ref === `${canonicalRoot}/INDEX.md`
173
- || ref.includes("/kernel/")
174
- || !ref.endsWith(".md")
175
- || /[\*{}]/.test(path.posix.basename(ref))) {
176
- errors.push(`invalid_host_instruction_path: ${ref}`);
177
- }
178
- if (seenInstructionPaths.has(ref)) {
179
- errors.push(`duplicate_host_instruction_path: ${ref}`);
180
- }
181
- seenInstructionPaths.add(ref);
182
- }
183
-
184
- if (!Array.isArray(layout?.tracked_derived_projections)) {
185
- errors.push("invalid_tracked_derived_projections: .nimi/config/spec-layout.yaml");
186
- }
187
- const trackedDerivedProjections = Array.isArray(layout?.tracked_derived_projections)
188
- ? layout.tracked_derived_projections
189
- : [];
190
- const seenProjectionIds = new Set();
191
- const seenProjectionRoots = new Set();
192
- for (const projection of trackedDerivedProjections) {
193
- if (!isPlainObject(projection)) {
194
- errors.push("invalid_tracked_derived_projection: non-object entry");
195
- continue;
196
- }
197
- if (!hasExactFields(projection, requiredProjectionFields)) {
198
- errors.push(`invalid_tracked_derived_projection_fields: ${String(projection.projection_id ?? "unknown")}`);
199
- }
200
- for (const field of requiredProjectionFields) {
201
- if (!(field in projection)) {
202
- errors.push(`missing_tracked_derived_projection_field: ${String(projection.projection_id ?? "unknown")}: ${field}`);
203
- }
204
- }
205
- const projectionId = String(projection.projection_id ?? "");
206
- const root = String(projection.root ?? "");
207
- const sourceRoots = Array.isArray(projection.source_roots) ? projection.source_roots.map(String) : [];
208
- if (!projectionId || seenProjectionIds.has(projectionId)) {
209
- errors.push(`invalid_or_duplicate_projection_id: ${projectionId || "empty"}`);
210
- }
211
- const overlapsExistingRoot = [...seenProjectionRoots].some((existingRoot) => (
212
- isRefWithin(root, existingRoot) || isRefWithin(existingRoot, root)
213
- ));
214
- if (!isLiteralPortableRef(root)
215
- || !isGeneratedProjectionRoot(root, canonicalRoot)
216
- || overlapsExistingRoot) {
217
- errors.push(`invalid_or_duplicate_projection_root: ${root || "empty"}`);
218
- }
219
- if (sourceRoots.length === 0 || sourceRoots.some((ref) => (
220
- !isLiteralPortableRef(ref)
221
- || !ref.startsWith(`${canonicalRoot}/`)
222
- || isRefWithin(ref, root)
223
- ))) {
224
- errors.push(`invalid_projection_source_roots: ${projectionId || "unknown"}`);
225
- }
226
- for (const field of ["generate_command", "drift_check_command", "required_marker"]) {
227
- if (typeof projection[field] !== "string" || projection[field].trim().length === 0) {
228
- errors.push(`invalid_tracked_derived_projection_field: ${projectionId || "unknown"}: ${field}`);
229
- }
230
- }
231
- seenProjectionIds.add(projectionId);
232
- seenProjectionRoots.add(root);
233
- }
234
-
235
- if (!Array.isArray(layout?.table_family_extensions)) {
236
- errors.push("invalid_table_family_extensions: .nimi/config/spec-layout.yaml");
237
- }
238
- const tableFamilyExtensions = Array.isArray(layout?.table_family_extensions)
239
- ? layout.table_family_extensions
240
- : [];
241
- const seenFamilies = new Set();
242
- for (const family of tableFamilyExtensions) {
243
- if (!isPlainObject(family)) {
244
- errors.push("invalid_table_family_extension: non-object entry");
245
- continue;
246
- }
247
- if (!hasExactFields(family, requiredFamilyFields)) {
248
- errors.push(`invalid_table_family_extension_fields: ${String(family.table_family ?? "unknown")}`);
249
- }
250
- for (const field of requiredFamilyFields) {
251
- if (!(field in family)) {
252
- errors.push(`missing_table_family_extension_field: ${String(family.table_family ?? "unknown")}: ${field}`);
253
- }
254
- }
255
- const familyId = String(family.table_family ?? "");
256
- if (!/^[a-z][a-z0-9_]*$/.test(familyId) || seenFamilies.has(familyId)) {
257
- errors.push(`invalid_or_duplicate_table_family_extension: ${familyId || "empty"}`);
258
- }
259
- if (packageFamilyIds.has(familyId)) {
260
- errors.push(`table_family_extension_shadows_package_family: ${familyId}`);
261
- }
262
- if (family.authority_class !== "product_authority_table"
263
- || !Array.isArray(family.required_fields)
264
- || family.required_fields.length === 0
265
- || family.required_fields.some((field) => typeof field !== "string" || field.length === 0)
266
- || !family.required_fields.includes("table_family")
267
- || !family.required_fields.includes("owner")
268
- || new Set(family.required_fields).size !== family.required_fields.length
269
- || !Array.isArray(family.forbidden_fields)
270
- || family.forbidden_fields.some((field) => typeof field !== "string" || field.length === 0)
271
- || new Set(family.forbidden_fields).size !== family.forbidden_fields.length
272
- || family.required_fields.some((field) => family.forbidden_fields.includes(field))) {
273
- errors.push(`invalid_table_family_extension_shape: ${familyId || "unknown"}`);
274
- }
275
- seenFamilies.add(familyId);
276
- }
277
-
278
- return {
279
- ok: errors.length === 0,
280
- present: true,
281
- errors,
282
- canonicalRoot,
283
- hostInstructionPaths,
284
- trackedDerivedProjections,
285
- tableFamilyExtensions,
286
- };
287
- }
288
-
289
- async function collectFilesUnder(rootPath) {
290
- const exists = await pathExists(rootPath);
291
- if (!exists || !exists.isDirectory()) {
292
- return [];
293
- }
294
-
295
- async function walk(currentPath) {
296
- const entries = await readdir(currentPath, { withFileTypes: true });
297
- const files = [];
298
- for (const entry of entries) {
299
- const childPath = path.join(currentPath, entry.name);
300
- if (entry.isDirectory()) {
301
- files.push(...await walk(childPath));
302
- } else if (entry.isFile()) {
303
- files.push(childPath);
304
- }
305
- }
306
- return files.sort();
307
- }
308
-
309
- return walk(rootPath);
310
- }
311
-
312
- async function collectCandidateFiles(projectRoot, rootRef = ".nimi/spec") {
313
- const roots = [
314
- rootRef,
315
- ".nimi/contracts",
316
- ".nimi/methodology",
317
- ".nimi/config",
318
- ".nimi/derived",
319
- ".nimi/state",
320
- ".nimi/audit",
321
- ];
322
- const uniqueRoots = [...new Set(roots)];
323
- const files = [];
324
- for (const root of uniqueRoots) {
325
- files.push(...await collectFilesUnder(path.resolve(projectRoot, root)));
326
- }
327
- return [...new Set(files)].sort();
328
- }
329
-
330
- function pathMatchesGlob(candidate, pattern) {
331
- const tokens = [];
332
- let source = pattern
333
- .replaceAll("{spec,methodology,contracts,config}", "__BRACE_PACKAGE_ROOTS__")
334
- .replaceAll("{config,contracts,methodology,spec}", "__BRACE_PACKAGE_ROOTS__")
335
- .replaceAll("<domain>", "__DOMAIN__")
336
- .replaceAll("**", "__DOUBLE_STAR__")
337
- .replaceAll("*", "__STAR__");
338
-
339
- source = source.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
340
- source = source
341
- .replaceAll("__BRACE_PACKAGE_ROOTS__", "(?:spec|methodology|contracts|config)")
342
- .replaceAll("__DOMAIN__", "[^/]+")
343
- .replaceAll("__DOUBLE_STAR__", ".*")
344
- .replaceAll("__STAR__", "[^/]*");
345
- tokens.push(new RegExp(`^${source}$`));
346
- return tokens.some((regex) => regex.test(candidate));
347
- }
348
-
349
- function basenameNoExt(ref) {
350
- return path.posix.basename(ref).replace(/\.(yaml|yml|md)$/i, "");
351
- }
352
-
353
- function firstPathSegment(ref) {
354
- return ref.split("/")[0] ?? "";
355
- }
356
-
357
- function domainForSpecRef(ref) {
358
- const parts = ref.split("/");
359
- if (parts[0] !== ".nimi" || parts[1] !== "spec" || parts.length < 4) {
360
- return null;
361
- }
362
- if (parts[2] === "_meta" || parts[2] === "generated") {
363
- return null;
364
- }
365
- return parts[2] ?? null;
366
- }
367
-
368
- function walkObjectKeys(value, callback) {
369
- if (Array.isArray(value)) {
370
- for (const entry of value) {
371
- walkObjectKeys(entry, callback);
372
- }
373
- return;
374
- }
375
- if (!isPlainObject(value)) {
376
- return;
377
- }
378
- for (const [key, entry] of Object.entries(value)) {
379
- callback(String(key), entry);
380
- walkObjectKeys(entry, callback);
381
- }
382
- }
383
-
384
- function containsAnyKey(value, keys) {
385
- const wanted = new Set(keys);
386
- let found = false;
387
- walkObjectKeys(value, (key) => {
388
- if (wanted.has(key)) {
389
- found = true;
390
- }
391
- });
392
- return found;
393
- }
394
-
395
- function yamlForText(text) {
396
- return parseYamlText(text);
397
- }
398
-
399
- function isYamlRef(ref) {
400
- return /\.(yaml|yml)$/i.test(ref);
401
- }
402
-
403
- function isMarkdownRef(ref) {
404
- return /\.md$/i.test(ref);
405
- }
406
-
407
- function tableFamilyContractMap(contracts) {
408
- const families = Array.isArray(contracts.tableFamily.data?.table_families)
409
- ? contracts.tableFamily.data.table_families
410
- : [];
411
- const extensions = contracts.hostSpecLayout?.ok
412
- ? contracts.hostSpecLayout.tableFamilyExtensions
413
- : [];
414
- return new Map([...families, ...extensions].map((entry) => [String(entry.table_family), entry]));
415
- }
416
-
417
- function domainAdmissionMap(contracts) {
418
- const admissions = Array.isArray(contracts.domainAdmission.data?.domain_admissions)
419
- ? contracts.domainAdmission.data.domain_admissions
420
- : [];
421
- return new Map(admissions.map((entry) => [String(entry.domain_id), entry]));
422
- }
423
-
424
- function trackedAdmissionRoots(contracts) {
425
- const admissions = Array.isArray(contracts.trackedOutputAdmission.data?.admissions)
426
- ? contracts.trackedOutputAdmission.data.admissions
427
- : [];
428
- return admissions.map((entry) => String(entry.root)).filter(Boolean);
429
- }
430
-
431
- function isTrackedNonProductRef(ref) {
432
- return ref.startsWith(".nimi/derived/")
433
- || ref.startsWith(".nimi/state/")
434
- || ref.startsWith(".nimi/audit/");
435
- }
436
-
437
- function isAdmittedTrackedOutput(ref, admittedRoots) {
438
- return admittedRoots.some((root) => pathMatchesGlob(ref, root));
439
- }
440
-
441
- function projectionForRef(ref, contracts) {
442
- if (!contracts.hostSpecLayout?.ok) {
443
- return null;
444
- }
445
- return contracts.hostSpecLayout.trackedDerivedProjections.find((entry) => isRefWithin(ref, String(entry.root))) ?? null;
446
- }
447
-
448
- function isHostInstructionRef(ref, contracts) {
449
- return contracts.hostSpecLayout?.ok
450
- && contracts.hostSpecLayout.hostInstructionPaths.some((pattern) => pathMatchesGlob(ref, pattern));
451
- }
452
-
453
- function classifyRef(ref, text, parsedYaml, contracts) {
454
- if (ref.startsWith(".nimi/spec/future/")) {
455
- return "unclassified";
456
- }
457
- if (isHostInstructionRef(ref, contracts)) {
458
- return "host_instruction";
459
- }
460
- if (projectionForRef(ref, contracts)) {
461
- return "tracked_derived_projection";
462
- }
463
- if (ref.startsWith(".nimi/spec/generated/") || ref.includes("/kernel/generated/")) {
464
- return "derived_view";
465
- }
466
- if (ref === ".nimi/spec/_meta/spec-generation-audit.yaml" || ref.startsWith(".nimi/spec/_meta/spec-generation-audit/")) {
467
- return "spec_generation_state";
468
- }
469
- if (ref.startsWith(".nimi/spec/_meta/")) {
470
- if (ref.endsWith("-admission-anchor.yaml")) {
471
- return "host_projection_anchor";
472
- }
473
- return "methodology_authority";
474
- }
475
- if (ref === ".nimi/spec/product-scope.yaml") {
476
- return "methodology_authority";
477
- }
478
- if (ref.startsWith(".nimi/spec/") && ref.includes("/kernel/tables/") && isYamlRef(ref)) {
479
- if (isPlainObject(parsedYaml) && parsedYaml.table_family === "support_registry") {
480
- return "support_registry";
481
- }
482
- if (isPlainObject(parsedYaml) && typeof parsedYaml.table_family === "string") {
483
- return "product_authority_table";
484
- }
485
- const stateKeys = ["done", "covered", "coverage_status", "audit_date", "evidence_report", "current", "proposed", "backlog_status", "migration_status", "mapping_status", "run_id", "ledger_ref"];
486
- if (containsAnyKey(parsedYaml, stateKeys) || /rule-evidence|backlog|migration/i.test(basenameNoExt(ref))) {
487
- return "audit_evidence_state";
488
- }
489
- return "product_authority_table";
490
- }
491
- if (ref.startsWith(".nimi/spec/") && isYamlRef(ref) && basenameNoExt(ref) === "spec-exempt-modules") {
492
- return "support_registry";
493
- }
494
- if (ref.startsWith(".nimi/spec/") && ref.includes("/kernel/") && isMarkdownRef(ref)) {
495
- return "product_authority";
496
- }
497
- if (ref === ".nimi/spec/INDEX.md" || (ref.startsWith(".nimi/spec/") && isMarkdownRef(ref))) {
498
- return "thin_guidance";
499
- }
500
- if (ref.startsWith(".nimi/local/derived/")) {
501
- return "derived_view";
502
- }
503
- if (ref.startsWith(".nimi/local/state/spec-generation/")) {
504
- return "spec_generation_state";
505
- }
506
- if (ref.startsWith(".nimi/local/audit/")) {
507
- return "audit_evidence_state";
508
- }
509
- if (ref.startsWith(".nimi/local/") || ref.startsWith(".local/")) {
510
- return "operational_local_artifact";
511
- }
512
- if (ref.startsWith(".nimi/contracts/") || ref.startsWith(".nimi/methodology/")) {
513
- return "nimicoding_managed_projection";
514
- }
515
- if (ref.startsWith(".nimi/config/")) {
516
- return ref === ".nimi/config/host-overlay.yaml" ? "host_projection_anchor" : "nimicoding_managed_projection";
517
- }
518
- if (ref.startsWith("package://@nimiplatform/nimi-coding/")) {
519
- return "methodology_authority";
520
- }
521
- if (isTrackedNonProductRef(ref)) {
522
- if (ref.startsWith(".nimi/derived/")) return "derived_view";
523
- if (ref.startsWith(".nimi/state/")) return "spec_generation_state";
524
- if (ref.startsWith(".nimi/audit/")) return "audit_evidence_state";
525
- }
526
- return "unclassified";
527
- }
528
-
529
- function dispositionFor(ref, surfaceClass, errors) {
530
- if (errors.length > 0 && surfaceClass === "unclassified") {
531
- return "block";
532
- }
533
- if (surfaceClass === "methodology_authority") {
534
- return "move_package";
535
- }
536
- if (surfaceClass === "nimicoding_managed_projection") {
537
- return "keep";
538
- }
539
- if (surfaceClass === "derived_view") {
540
- return "delete";
541
- }
542
- if (surfaceClass === "host_instruction" || surfaceClass === "tracked_derived_projection") {
543
- return errors.length > 0 ? "block" : "keep";
544
- }
545
- if (["spec_generation_state", "audit_evidence_state", "operational_local_artifact"].includes(surfaceClass)) {
546
- return "move_local";
547
- }
548
- if (errors.length > 0) {
549
- return "block";
550
- }
551
- return "keep";
552
- }
553
-
554
- function targetRootFor(surfaceClass, ref) {
555
- if (surfaceClass === "methodology_authority") {
556
- return "nimi-coding";
557
- }
558
- if (surfaceClass === "nimicoding_managed_projection") {
559
- return ".nimi";
560
- }
561
- if (surfaceClass === "derived_view") {
562
- return "stdout_view";
563
- }
564
- if (surfaceClass === "host_instruction" || surfaceClass === "tracked_derived_projection") {
565
- return ".nimi/spec";
566
- }
567
- if (surfaceClass === "spec_generation_state" || surfaceClass === "operational_local_artifact") {
568
- return ".nimi/local/state";
569
- }
570
- if (surfaceClass === "audit_evidence_state") {
571
- return ".nimi/local/audit";
572
- }
573
- if (surfaceClass === "unclassified") {
574
- return null;
575
- }
576
- if (ref.startsWith(".nimi/spec/")) {
577
- return ".nimi/spec";
578
- }
579
- return firstPathSegment(ref);
580
- }
581
-
582
- function ownerFor(surfaceClass, ref) {
583
- if (surfaceClass === "methodology_authority" || surfaceClass === "nimicoding_managed_projection") {
584
- return "nimi-coding";
585
- }
586
- if (surfaceClass === "derived_view") {
587
- return "generator";
588
- }
589
- if (surfaceClass === "tracked_derived_projection") {
590
- return "generator";
591
- }
592
- if (surfaceClass === "host_instruction") {
593
- return "product_host";
594
- }
595
- if (surfaceClass === "spec_generation_state") {
596
- return "spec_generator";
597
- }
598
- if (surfaceClass === "audit_evidence_state") {
599
- return "verification_process";
600
- }
601
- if (surfaceClass === "operational_local_artifact") {
602
- return "external_tooling";
603
- }
604
- return domainForSpecRef(ref) ?? "product_domain";
605
- }
606
-
607
- function confirmationFor(errors) {
608
- if (errors.some((error) => error.includes("unadmitted_domain_retained_as_spec_input"))) {
609
- return "product_semantic_fork";
610
- }
611
- if (errors.some((error) => error.includes("missing_domain_admission"))) {
612
- return "owner_ambiguity";
613
- }
614
- if (errors.some((error) => error.includes("package_methodology_under_host_spec") || error.includes("package_body_promoted_to_product_authority"))) {
615
- return "package_boundary_ambiguity";
616
- }
617
- return "none";
618
- }
619
-
620
- function ambiguityFor(requiredConfirmation, surfaceClass) {
621
- if (requiredConfirmation === "none") {
622
- return {
623
- posture: "none",
624
- reason: null,
625
- candidate_owners: [],
626
- };
627
- }
628
- return {
629
- posture: "blocks_migration",
630
- reason: requiredConfirmation,
631
- candidate_owners: surfaceClass === "unclassified" ? [] : [ownerFor(surfaceClass, "")],
632
- };
633
- }
634
-
635
- function validationCommandsFor(entryClass) {
636
- const commands = {
637
- product_authority_table: ["pnpm exec nimicoding validate-table-family --profile nimi --root .nimi/spec"],
638
- support_registry: ["pnpm exec nimicoding validate-table-family --profile nimi --root .nimi/spec"],
639
- thin_guidance: ["pnpm exec nimicoding validate-guidance-bodies --profile nimi --root .nimi/spec"],
640
- host_instruction: ["pnpm exec nimicoding validate-placement --profile nimi --root .nimi/spec"],
641
- tracked_derived_projection: ["pnpm exec nimicoding validate-placement --profile nimi --root .nimi/spec"],
642
- host_projection_anchor: ["pnpm exec nimicoding validate-projection-edges --profile nimi --root .nimi/spec"],
643
- nimicoding_managed_projection: ["pnpm exec nimicoding validate-placement --profile nimi --root .nimi/spec"],
644
- };
645
- return commands[entryClass] ?? ["pnpm exec nimicoding validate-placement --profile nimi --root .nimi/spec"];
646
- }
647
-
648
- function addError(errors, code, ref, detail) {
649
- errors.push(detail ? `${code}: ${ref}: ${detail}` : `${code}: ${ref}`);
650
- }
651
-
652
- function validateRefPlacement(ref, surfaceClass, parsedYaml, text, contracts) {
653
- const errors = [];
654
-
655
- if (surfaceClass === "unclassified") {
656
- addError(errors, "unclassified_file", ref);
657
- }
658
- if (surfaceClass === "derived_view" && ref.startsWith(".nimi/spec/")) {
659
- addError(errors, "derived_view_under_product_authority_root", ref);
660
- }
661
- if (surfaceClass === "tracked_derived_projection") {
662
- const projection = projectionForRef(ref, contracts);
663
- if (!projection) {
664
- addError(errors, "tracked_derived_projection_without_layout_admission", ref);
665
- } else if (!text.includes(String(projection.required_marker))) {
666
- addError(errors, "tracked_derived_projection_missing_generated_marker", ref, String(projection.required_marker));
667
- }
668
- }
669
- if (surfaceClass === "host_instruction" && PRODUCT_RULE_DEFINITION_PATTERN.test(text)) {
670
- addError(errors, "host_instruction_defines_product_rule", ref);
671
- }
672
- if (surfaceClass === "derived_view" && ref.startsWith(".nimi/local/derived/")) {
673
- addError(errors, "derived_view_written_to_local_derived", ref);
674
- }
675
- if (surfaceClass === "spec_generation_state" && ref.startsWith(".nimi/spec/")) {
676
- addError(errors, "spec_generation_state_under_spec", ref);
677
- }
678
- if (surfaceClass === "audit_evidence_state" && ref.startsWith(".nimi/spec/")) {
679
- addError(errors, "audit_evidence_state_under_spec", ref);
680
- }
681
- if (surfaceClass === "methodology_authority" && ref.startsWith(".nimi/spec/")) {
682
- addError(errors, "package_methodology_under_host_spec", ref);
683
- }
684
- if (isTrackedNonProductRef(ref) && !isAdmittedTrackedOutput(ref, trackedAdmissionRoots(contracts))) {
685
- addError(errors, "tracked_non_product_without_admission", ref);
686
- }
687
- if (surfaceClass === "thin_guidance" && isMarkdownRef(ref)) {
688
- if (TEXT_RULE_BODY_PATTERN.test(text)) {
689
- addError(errors, "guidance_defines_rule_body", ref);
690
- }
691
- if (PRODUCT_RULE_DEFINITION_PATTERN.test(text)) {
692
- addError(errors, "guidance_defines_product_rule", ref);
693
- }
694
- }
695
- if (surfaceClass === "product_authority" && isMarkdownRef(ref) && GENERATED_REF_PATTERN.test(text)) {
696
- addError(errors, "derived_view_referenced_as_authority", ref);
697
- }
698
- return errors;
699
- }
700
-
701
- function validateTableRef(ref, parsedYaml, contracts) {
702
- const errors = [];
703
- if (!ref.startsWith(".nimi/spec/") || !ref.includes("/kernel/tables/") || !isYamlRef(ref)) {
704
- return errors;
705
- }
706
-
707
- if (!isPlainObject(parsedYaml)) {
708
- addError(errors, "invalid_yaml_table", ref);
709
- return errors;
710
- }
711
-
712
- const family = typeof parsedYaml.table_family === "string" ? parsedYaml.table_family : null;
713
- const familyMap = tableFamilyContractMap(contracts);
714
- if (!family) {
715
- addError(errors, "missing_table_family", ref);
716
- } else if (!familyMap.has(family)) {
717
- addError(errors, "unknown_table_family", ref, family);
718
- } else {
719
- const familyContract = familyMap.get(family);
720
- for (const field of familyContract.required_fields ?? []) {
721
- if (!(field in parsedYaml)) {
722
- addError(errors, "missing_table_family_required_field", ref, String(field));
723
- }
724
- }
725
- if (containsAnyKey(parsedYaml, familyContract.forbidden_fields ?? [])) {
726
- addError(errors, "table_contains_family_forbidden_field", ref, family);
727
- }
728
- }
729
-
730
- const forbiddenFields = contracts.tableFamily.data?.forbidden_fields_by_authority_class?.product_authority_table ?? [];
731
- if (containsAnyKey(parsedYaml, forbiddenFields)) {
732
- addError(errors, "table_contains_forbidden_state_or_audit_field", ref);
733
- }
734
-
735
- return errors;
736
- }
737
-
738
- function validateDomainRef(ref, contracts) {
739
- const errors = [];
740
- const domain = domainForSpecRef(ref);
741
- if (!domain) {
742
- return errors;
743
- }
744
- const admissions = domainAdmissionMap(contracts);
745
- const admission = admissions.get(domain);
746
- if (!admission) {
747
- addError(errors, "missing_domain_admission", ref, domain);
748
- return errors;
749
- }
750
- if (admission.authority_class === "excluded_from_spec") {
751
- addError(errors, "excluded_domain_retained_under_spec", ref, domain);
752
- }
753
- if (admission.authority_class === "migration_input_only") {
754
- addError(errors, "unadmitted_domain_retained_as_spec_input", ref, domain);
755
- }
756
- return errors;
757
- }
758
-
759
- async function buildInventory(projectRoot, options = {}) {
760
- const contracts = await loadSurfaceContracts(projectRoot);
761
- const rootRef = options.rootRef ?? ".nimi/spec";
762
- const absoluteFiles = await collectCandidateFiles(projectRoot, rootRef);
763
- const entries = [];
764
- const layoutErrors = [...(contracts.hostSpecLayout?.errors ?? [])];
765
- const errors = [...layoutErrors];
766
-
767
- for (const absolutePath of absoluteFiles) {
768
- const ref = relativeRef(projectRoot, absolutePath);
769
- const text = await readFile(absolutePath, "utf8");
770
- const parsedYaml = isYamlRef(ref) ? yamlForText(text) : null;
771
- const surfaceClass = classifyRef(ref, text, parsedYaml, contracts);
772
- const validationErrors = {
773
- placement: validateRefPlacement(ref, surfaceClass, parsedYaml, text, contracts),
774
- table_family: validateTableRef(ref, parsedYaml, contracts),
775
- domain_admission: validateDomainRef(ref, contracts),
776
- };
777
- const entryErrors = Object.values(validationErrors).flat();
778
- errors.push(...entryErrors);
779
- const requiredConfirmation = confirmationFor(entryErrors);
780
- entries.push({
781
- source_path: ref,
782
- current_inferred_class: surfaceClass,
783
- target_class: surfaceClass,
784
- disposition: dispositionFor(ref, surfaceClass, entryErrors),
785
- target_root: targetRootFor(surfaceClass, ref),
786
- owner: ownerFor(surfaceClass, ref),
787
- required_confirmation: requiredConfirmation,
788
- ambiguity: ambiguityFor(requiredConfirmation, surfaceClass),
789
- evidence: entryErrors.length > 0 ? entryErrors : [`classified_as:${surfaceClass}`],
790
- validation_commands: validationCommandsFor(surfaceClass),
791
- validation_errors: validationErrors,
792
- errors: entryErrors,
793
- });
794
- }
795
-
796
- return {
797
- contracts,
798
- entries,
799
- layoutErrors,
800
- errors,
801
- };
802
- }
803
-
804
- function summarize(entries) {
805
- const byClass = {};
806
- const byDisposition = {};
807
- for (const entry of entries) {
808
- byClass[entry.current_inferred_class] = (byClass[entry.current_inferred_class] ?? 0) + 1;
809
- byDisposition[entry.disposition] = (byDisposition[entry.disposition] ?? 0) + 1;
810
- }
811
- return {
812
- total_files: entries.length,
813
- by_surface_class: byClass,
814
- by_disposition: byDisposition,
815
- blocking_entries: entries.filter((entry) => entry.disposition === "block").length,
816
- };
817
- }
818
-
819
- function reportFor(validator, ok, errors, warnings, entries, extra = {}) {
820
- return {
821
- contract: SURFACE_RESULT_CONTRACT,
822
- validator,
823
- ok,
824
- errors,
825
- warnings,
826
- summary: summarize(entries),
827
- ...extra,
828
- };
829
- }
830
-
831
- function countBy(entries, field) {
832
- const counts = {};
833
- for (const entry of entries) {
834
- const value = String(entry[field] ?? "unknown");
835
- counts[value] = (counts[value] ?? 0) + 1;
836
- }
837
- return counts;
838
- }
839
-
840
- function groupInventoryByDisposition(entries) {
841
- const groups = {};
842
- for (const entry of entries) {
843
- const disposition = String(entry.disposition);
844
- const group = groups[disposition] ?? [];
845
- group.push(entry.source_path);
846
- groups[disposition] = group;
847
- }
848
- return Object.fromEntries(Object.entries(groups).map(([key, value]) => [key, value.sort()]));
849
- }
850
-
851
- function buildMigrationGroups(entries) {
852
- const groupKinds = [
853
- {
854
- group_id: "move-package-methodology-copied-under-spec",
855
- dispositions: ["move_package"],
856
- requires_confirmation: false,
857
- },
858
- {
859
- group_id: "move-local-derived-and-audit-state",
860
- dispositions: ["move_local"],
861
- requires_confirmation: false,
862
- },
863
- {
864
- group_id: "rewrite-product-authority-tables-and-guidance",
865
- dispositions: ["rewrite"],
866
- requires_confirmation: true,
867
- },
868
- {
869
- group_id: "resolve-blocked-semantic-forks",
870
- dispositions: ["block"],
871
- requires_confirmation: true,
872
- },
873
- ];
874
- return groupKinds
875
- .map((group) => {
876
- const matching = entries.filter((entry) => group.dispositions.includes(entry.disposition));
877
- return {
878
- ...group,
879
- entry_count: matching.length,
880
- source_paths: matching.map((entry) => entry.source_path).sort(),
881
- };
882
- })
883
- .filter((group) => group.entry_count > 0);
884
- }
885
-
886
- function enumValidation(entries, inventory) {
887
- const targetClasses = new Set(inventory.target_class_enum);
888
- const dispositions = new Set(inventory.disposition_enum);
889
- return {
890
- unknown_target_classes: entries
891
- .filter((entry) => !targetClasses.has(entry.target_class))
892
- .map((entry) => entry.source_path),
893
- unknown_dispositions: entries
894
- .filter((entry) => !dispositions.has(entry.disposition))
895
- .map((entry) => entry.source_path),
896
- };
897
- }
898
-
899
- function migrationPlanForInventory(rootRef, inventory) {
900
- const entries = inventory.inventory;
901
- const enumStatus = enumValidation(entries, inventory);
902
- const requiredConfirmationEntries = entries.filter((entry) => entry.required_confirmation !== "none");
903
- return {
904
- contract: "nimicoding.spec-migration-plan.v2",
905
- ok: enumStatus.unknown_target_classes.length === 0 && enumStatus.unknown_dispositions.length === 0,
906
- version: inventory.version,
907
- root: rootRef,
908
- inventory: entries,
909
- disposition_enum: inventory.disposition_enum,
910
- target_class_enum: inventory.target_class_enum,
911
- semantic_constraints: [
912
- ...inventory.semantic_constraints,
913
- "migration_plan_must_not_modify_files",
914
- "required_confirmation_entries_must_be_preserved",
915
- "local_only_plan_artifact_required",
916
- ],
917
- summary: {
918
- total_files: entries.length,
919
- by_surface_class: countBy(entries, "current_inferred_class"),
920
- by_disposition: countBy(entries, "disposition"),
921
- by_required_confirmation: countBy(entries, "required_confirmation"),
922
- required_confirmation_count: requiredConfirmationEntries.length,
923
- blocking_entries: entries.filter((entry) => entry.disposition === "block").length,
924
- },
925
- enum_validation: enumStatus,
926
- required_confirmations: requiredConfirmationEntries.map((entry) => ({
927
- source_path: entry.source_path,
928
- required_confirmation: entry.required_confirmation,
929
- ambiguity: entry.ambiguity,
930
- evidence: entry.evidence,
931
- recommendation: entry.required_confirmation === "product_semantic_fork"
932
- ? "stop_for_user_decision_before_migration"
933
- : "resolve_owner_or_package_boundary_before_migration",
934
- })),
935
- groups: groupInventoryByDisposition(entries),
936
- migration_groups: buildMigrationGroups(entries),
937
- mutation_policy: {
938
- mutates_source_tree: false,
939
- allowed_output_roots: [".nimi/local/state/spec-surface"],
940
- forbidden_output_roots: [".nimi/spec", ".nimi/contracts", ".nimi/methodology", ".nimi/config"],
941
- },
942
- };
943
- }
944
-
945
- export async function classifySpecSurface(projectRoot, options = {}) {
946
- const { entries, errors } = await buildInventory(projectRoot, options);
947
- return reportFor("classify-spec-tree", errors.length === 0, errors, [], entries, {
948
- inventory: {
949
- version: 2,
950
- inventory: entries,
951
- disposition_enum: ["keep", "move_package", "move_local", "split", "rewrite", "delete", "block"],
952
- target_class_enum: [
953
- "product_authority",
954
- "product_authority_table",
955
- "thin_guidance",
956
- "host_instruction",
957
- "tracked_derived_projection",
958
- "derived_view",
959
- "spec_generation_state",
960
- "audit_evidence_state",
961
- "operational_local_artifact",
962
- "methodology_authority",
963
- "nimicoding_managed_projection",
964
- "host_projection_anchor",
965
- "support_registry",
966
- ],
967
- semantic_constraints: [
968
- "inventory_must_not_modify_files",
969
- "future_under_spec_must_fail_classification",
970
- "nimicoding_managed_projection_must_not_be_promoted_to_product_authority",
971
- ],
972
- },
973
- });
974
- }
975
-
976
- export async function generateSpecMigrationPlan(projectRoot, options = {}) {
977
- const classification = await classifySpecSurface(projectRoot, options);
978
- return migrationPlanForInventory(options.rootRef ?? ".nimi/spec", classification.inventory);
979
- }
980
-
981
- export async function buildSpecSurfaceInventory(projectRoot, options = {}) {
982
- const { entries, errors } = await buildInventory(projectRoot, options);
983
- return {
984
- contract: SURFACE_RESULT_CONTRACT,
985
- entries,
986
- errors,
987
- summary: summarize(entries),
988
- };
989
- }
990
-
991
- export function isProductAuthoritySurfaceClass(surfaceClass) {
992
- return [
993
- "product_authority",
994
- "product_authority_table",
995
- "thin_guidance",
996
- "host_projection_anchor",
997
- "support_registry",
998
- ].includes(surfaceClass);
999
- }
1000
-
1001
- export async function validatePlacement(projectRoot, options = {}) {
1002
- const { entries, layoutErrors } = await buildInventory(projectRoot, options);
1003
- const errors = [
1004
- ...layoutErrors,
1005
- ...entries.flatMap((entry) => entry.validation_errors?.placement ?? []),
1006
- ];
1007
- return reportFor("validate-placement", errors.length === 0, errors, [], entries);
1008
- }
1009
-
1010
- export async function validateTableFamily(projectRoot, options = {}) {
1011
- const { contracts, entries } = await buildInventory(projectRoot, options);
1012
- const errors = [];
1013
- for (const entry of entries.filter((item) => item.source_path.startsWith(".nimi/spec/")
1014
- && item.source_path.includes("/kernel/tables/")
1015
- && isYamlRef(item.source_path)
1016
- && ["product_authority_table", "support_registry"].includes(item.current_inferred_class))) {
1017
- const text = await readFile(path.join(projectRoot, entry.source_path), "utf8");
1018
- errors.push(...validateTableRef(entry.source_path, yamlForText(text), contracts));
1019
- }
1020
- return reportFor("validate-table-family", errors.length === 0, errors, [], entries);
1021
- }
1022
-
1023
- export async function validateProjectionEdges(projectRoot, options = {}) {
1024
- const { entries, contracts } = await buildInventory(projectRoot, options);
1025
- const errors = [];
1026
- const edgeTargets = new Set((contracts.projectionEdge.data?.projection_edges ?? []).map((entry) => String(entry.target_ref)));
1027
- const anchorRef = ".nimi/spec/_meta/nimi-coding-admission-anchor.yaml";
1028
- const overlayRef = ".nimi/config/host-overlay.yaml";
1029
- if (entries.some((entry) => entry.source_path === anchorRef) && !edgeTargets.has(anchorRef)) {
1030
- addError(errors, "missing_projection_edge_for_anchor", anchorRef);
1031
- }
1032
- if (entries.some((entry) => entry.source_path === overlayRef) && !edgeTargets.has(overlayRef)) {
1033
- addError(errors, "missing_projection_edge_for_overlay", overlayRef);
1034
- }
1035
- for (const entry of entries) {
1036
- if (entry.current_inferred_class === "methodology_authority" && entry.source_path.startsWith(".nimi/spec/")) {
1037
- addError(errors, "package_body_promoted_to_product_authority", entry.source_path);
1038
- }
1039
- if (entry.current_inferred_class === "product_authority") {
1040
- const text = await readFile(path.join(projectRoot, entry.source_path), "utf8");
1041
- if (GENERATED_REF_PATTERN.test(text)) {
1042
- addError(errors, "derived_view_referenced_as_authority", entry.source_path);
1043
- }
1044
- }
1045
- }
1046
- return reportFor("validate-projection-edges", errors.length === 0, errors, [], entries);
1047
- }
1048
-
1049
- export async function validateGuidanceBodies(projectRoot, options = {}) {
1050
- const { entries } = await buildInventory(projectRoot, options);
1051
- const errors = [];
1052
- for (const entry of entries.filter((item) => item.current_inferred_class === "thin_guidance")) {
1053
- const text = await readFile(path.join(projectRoot, entry.source_path), "utf8");
1054
- if (TEXT_RULE_BODY_PATTERN.test(text)) {
1055
- addError(errors, "guidance_defines_rule_body", entry.source_path);
1056
- }
1057
- if (PRODUCT_RULE_DEFINITION_PATTERN.test(text)) {
1058
- addError(errors, "guidance_defines_product_rule", entry.source_path);
1059
- }
1060
- }
1061
- return reportFor("validate-guidance-bodies", errors.length === 0, errors, [], entries);
1062
- }
1063
-
1064
- export async function validateDomainAdmission(projectRoot, options = {}) {
1065
- const { contracts, entries } = await buildInventory(projectRoot, options);
1066
- const errors = [];
1067
- for (const entry of entries.filter((item) => item.source_path.startsWith(".nimi/spec/"))) {
1068
- errors.push(...validateDomainRef(entry.source_path, contracts));
1069
- }
1070
- return reportFor("validate-domain-admission", errors.length === 0, errors, [], entries);
1071
- }
1072
-
1073
- export async function validateTrackedOutputAdmission(projectRoot, options = {}) {
1074
- const { contracts, entries } = await buildInventory(projectRoot, options);
1075
- const admittedRoots = trackedAdmissionRoots(contracts);
1076
- const errors = [];
1077
- for (const entry of entries.filter((item) => isTrackedNonProductRef(item.source_path))) {
1078
- if (!isAdmittedTrackedOutput(entry.source_path, admittedRoots)) {
1079
- addError(errors, "tracked_non_product_without_admission", entry.source_path);
1080
- }
1081
- }
1082
- return reportFor("validate-tracked-output-admission", errors.length === 0, errors, [], entries);
1083
- }
1084
-
1085
- export function parseSurfaceValidatorOptions(args) {
1086
- const options = {
1087
- profile: "default",
1088
- rootRef: ".nimi/spec",
1089
- emit: null,
1090
- check: false,
1091
- json: false,
1092
- };
1093
-
1094
- for (let index = 0; index < args.length; index += 1) {
1095
- const arg = args[index];
1096
- if (arg === "--profile") {
1097
- options.profile = args[index + 1] ?? "";
1098
- index += 1;
1099
- } else if (arg === "--root") {
1100
- options.rootRef = args[index + 1] ?? "";
1101
- index += 1;
1102
- } else if (arg === "--emit") {
1103
- options.emit = args[index + 1] ?? "";
1104
- index += 1;
1105
- } else if (arg === "--check") {
1106
- options.check = true;
1107
- } else if (arg === "--json") {
1108
- options.json = true;
1109
- } else if (arg === "--strict") {
1110
- options.strict = true;
1111
- } else {
1112
- return { ok: false, error: `unknown option: ${arg}` };
1113
- }
1114
- }
1115
-
1116
- if (!options.rootRef) {
1117
- return { ok: false, error: "--root must not be empty" };
1118
- }
1119
- return { ok: true, options };
1120
- }
1121
-
1122
- export async function writeInventoryIfRequested(report, emitRef, projectRoot) {
1123
- if (!emitRef) {
1124
- return;
1125
- }
1126
- const absolutePath = path.resolve(projectRoot, emitRef);
1127
- await mkdir(path.dirname(absolutePath), { recursive: true });
1128
- await writeFile(absolutePath, `${JSON.stringify(report.inventory, null, 2)}\n`, "utf8");
1129
- }
1130
-
1131
- export async function writeMigrationPlanIfRequested(report, emitRef, projectRoot) {
1132
- if (!emitRef) {
1133
- return;
1134
- }
1135
- const allowedRoot = path.resolve(projectRoot, ".nimi/local/state/spec-surface");
1136
- const absolutePath = path.resolve(projectRoot, emitRef);
1137
- const relativeToAllowedRoot = path.relative(allowedRoot, absolutePath);
1138
- if (
1139
- relativeToAllowedRoot === "" ||
1140
- relativeToAllowedRoot.startsWith("..") ||
1141
- path.isAbsolute(relativeToAllowedRoot)
1142
- ) {
1143
- throw new Error("--emit must target .nimi/local/state/spec-surface/**");
1144
- }
1145
- await mkdir(path.dirname(absolutePath), { recursive: true });
1146
- await writeFile(absolutePath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
1147
- }