@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.
Files changed (126) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/CODE_OF_CONDUCT.md +28 -0
  3. package/CONTRIBUTING.md +45 -0
  4. package/README.md +371 -344
  5. package/README.zh-CN.md +307 -0
  6. package/SECURITY.md +26 -0
  7. package/adapters/oh-my-codex/README.md +8 -9
  8. package/cli/commands/audit-sweep.mjs +10 -10
  9. package/cli/commands/classify-spec-tree.mjs +5 -0
  10. package/cli/commands/closeout.mjs +3 -0
  11. package/cli/commands/generate-spec-derived-docs.mjs +20 -0
  12. package/cli/commands/generate-spec-migration-plan.mjs +30 -0
  13. package/cli/commands/start.mjs +5 -1
  14. package/cli/commands/surface-validator-command.mjs +49 -0
  15. package/cli/commands/sweep-design.mjs +295 -0
  16. package/cli/commands/sweep.mjs +22 -0
  17. package/cli/commands/sync.mjs +132 -0
  18. package/cli/commands/topic-formatters.mjs +8 -8
  19. package/cli/commands/validate-ai-governance.mjs +167 -46
  20. package/cli/commands/validate-domain-admission.mjs +5 -0
  21. package/cli/commands/validate-guidance-bodies.mjs +5 -0
  22. package/cli/commands/validate-placement.mjs +5 -0
  23. package/cli/commands/validate-projection-edges.mjs +5 -0
  24. package/cli/commands/validate-spec-audit.mjs +5 -1
  25. package/cli/commands/validate-table-family.mjs +5 -0
  26. package/cli/commands/validate-tracked-output-admission.mjs +5 -0
  27. package/cli/constants.mjs +5 -49
  28. package/cli/help.mjs +33 -11
  29. package/cli/index.mjs +20 -2
  30. package/cli/lib/audit-sweep-runtime/admissions.mjs +38 -29
  31. package/cli/lib/audit-sweep-runtime/audit-validity.mjs +8 -0
  32. package/cli/lib/audit-sweep-runtime/chunks.mjs +11 -11
  33. package/cli/lib/audit-sweep-runtime/closeout.mjs +8 -8
  34. package/cli/lib/audit-sweep-runtime/codex-auditor-evidence.mjs +3 -3
  35. package/cli/lib/audit-sweep-runtime/codex-auditor.mjs +10 -10
  36. package/cli/lib/audit-sweep-runtime/common.mjs +7 -7
  37. package/cli/lib/audit-sweep-runtime/format.mjs +3 -3
  38. package/cli/lib/audit-sweep-runtime/ingest.mjs +8 -8
  39. package/cli/lib/audit-sweep-runtime/inventory-spec-chunks.mjs +24 -27
  40. package/cli/lib/audit-sweep-runtime/inventory.mjs +58 -18
  41. package/cli/lib/audit-sweep-runtime/ledger.mjs +1 -1
  42. package/cli/lib/audit-sweep-runtime/p0p1-profile.mjs +2 -2
  43. package/cli/lib/audit-sweep-runtime/remediation.mjs +6 -6
  44. package/cli/lib/audit-sweep-runtime/rerun.mjs +6 -6
  45. package/cli/lib/audit-sweep-runtime/status.mjs +1 -1
  46. package/cli/lib/audit-sweep-runtime/validators.mjs +2 -2
  47. package/cli/lib/authority-convergence.mjs +397 -2
  48. package/cli/lib/blueprint-audit.mjs +5 -5
  49. package/cli/lib/closeout.mjs +126 -3
  50. package/cli/lib/contracts.mjs +21 -17
  51. package/cli/lib/handoff.mjs +29 -11
  52. package/cli/lib/high-risk-admission.mjs +60 -11
  53. package/cli/lib/high-risk-decision.mjs +31 -2
  54. package/cli/lib/high-risk-ingest.mjs +5 -1
  55. package/cli/lib/high-risk-review.mjs +5 -1
  56. package/cli/lib/internal/contracts-parse.mjs +195 -24
  57. package/cli/lib/internal/contracts-validators.mjs +3 -2
  58. package/cli/lib/internal/doctor-bootstrap-surface.mjs +82 -35
  59. package/cli/lib/internal/doctor-delegated-surface.mjs +1 -1
  60. package/cli/lib/internal/doctor-finalize.mjs +12 -8
  61. package/cli/lib/internal/doctor-inspectors.mjs +34 -1
  62. package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +74 -12
  63. package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +24 -6
  64. package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +18 -23
  65. package/cli/lib/internal/surface-taxonomy-validators.mjs +931 -0
  66. package/cli/lib/internal/validators-spec.mjs +229 -20
  67. package/cli/lib/sweep-design-runtime/common.mjs +246 -0
  68. package/cli/lib/sweep-design-runtime/engine.mjs +733 -0
  69. package/cli/lib/sweep-design-runtime/fix-topic.mjs +414 -0
  70. package/cli/lib/sweep-design-runtime/lifecycle.mjs +54 -0
  71. package/cli/lib/sweep-design-runtime/results.mjs +324 -0
  72. package/cli/lib/sweep-design.mjs +8 -0
  73. package/cli/lib/sync.mjs +143 -0
  74. package/cli/lib/topic-artifacts.mjs +186 -0
  75. package/cli/lib/topic-authority-coverage.mjs +73 -0
  76. package/cli/lib/topic-closeout.mjs +560 -0
  77. package/cli/lib/topic-common.mjs +404 -0
  78. package/cli/lib/topic-decisions.mjs +332 -0
  79. package/cli/lib/topic-draft-packets.mjs +126 -7
  80. package/cli/lib/topic-execution.mjs +515 -0
  81. package/cli/lib/topic-goal.mjs +112 -33
  82. package/cli/lib/topic-ledger.mjs +281 -0
  83. package/cli/lib/topic-lifecycle-artifacts.mjs +173 -0
  84. package/cli/lib/topic-root-validation.mjs +288 -0
  85. package/cli/lib/topic-runner-commands.mjs +174 -0
  86. package/cli/lib/topic-runner-deferral.mjs +532 -0
  87. package/cli/lib/topic-runner-stale-gates.mjs +114 -0
  88. package/cli/lib/topic-runner-validation.mjs +138 -0
  89. package/cli/lib/topic-runner.mjs +109 -154
  90. package/cli/lib/topic-scaffold.mjs +252 -0
  91. package/cli/lib/topic-waves.mjs +403 -0
  92. package/cli/lib/topic.mjs +81 -93
  93. package/cli/lib/value-helpers.mjs +6 -1
  94. package/cli/seeds/bootstrap.mjs +96 -20
  95. package/cli/seeds/seed-policy.yaml +67 -0
  96. package/config/bootstrap.yaml +1 -1
  97. package/config/skill-manifest.yaml +4 -2
  98. package/config/spec-generation-inputs.yaml +41 -19
  99. package/contracts/audit-remediation-map.schema.yaml +1 -0
  100. package/contracts/audit-sweep-result.yaml +4 -0
  101. package/contracts/domain-admission.schema.yaml +56 -0
  102. package/contracts/migration-inventory.schema.yaml +80 -0
  103. package/contracts/negative-fixtures.yaml +91 -0
  104. package/contracts/placement-contract.schema.yaml +163 -0
  105. package/contracts/projection-edge.schema.yaml +130 -0
  106. package/contracts/shared-enums.yaml +68 -0
  107. package/contracts/spec-generation-audit.schema.yaml +19 -4
  108. package/contracts/spec-generation-inputs.schema.yaml +130 -29
  109. package/contracts/spec-reconstruction-result.yaml +9 -5
  110. package/contracts/surface-taxonomy.schema.yaml +201 -0
  111. package/contracts/sweep-design-result.yaml +349 -0
  112. package/contracts/table-family.schema.yaml +121 -0
  113. package/contracts/topic-goal.schema.yaml +10 -1
  114. package/contracts/tracked-output-admission.schema.yaml +70 -0
  115. package/contracts/workflow-consumer.schema.yaml +112 -0
  116. package/methodology/audit-sweep-p0p1-recall.yaml +1 -1
  117. package/methodology/spec-reconstruction.yaml +53 -30
  118. package/package.json +19 -4
  119. package/spec/_meta/command-gating-matrix.yaml +33 -0
  120. package/spec/_meta/generate-drift-migration-checklist.yaml +44 -62
  121. package/spec/_meta/governance-routing-cutover-checklist.yaml +3 -3
  122. package/spec/_meta/phase2-impacted-surface-matrix.yaml +14 -14
  123. package/spec/_meta/spec-authority-cutover-readiness.yaml +3 -5
  124. package/spec/_meta/spec-tree-model.yaml +104 -36
  125. package/spec/bootstrap-state.yaml +36 -36
  126. package/spec/product-scope.yaml +13 -10
@@ -19,6 +19,11 @@ import {
19
19
  makeValidatorRefusal,
20
20
  VALIDATOR_NATIVE_REFUSAL_CODES,
21
21
  } from "./validators-shared.mjs";
22
+ import {
23
+ validateDomainAdmission,
24
+ validatePlacement,
25
+ validateTableFamily,
26
+ } from "./surface-taxonomy-validators.mjs";
22
27
  import {
23
28
  classifyAuditCoveredFiles,
24
29
  classifySpecTreeFiles,
@@ -27,13 +32,136 @@ import {
27
32
  isSourceRefWithinDeclaredRoots,
28
33
  } from "./validators-spec-helpers.mjs";
29
34
 
35
+ async function loadV2RequiredFiles(projectRoot) {
36
+ const text = await readTextIfFile(path.join(projectRoot, ".nimi", "methodology", "spec-reconstruction.yaml"));
37
+ const parsed = parseYamlText(text);
38
+ return Array.isArray(parsed?.reconstruction?.target_tree_shape?.minimal_required_outputs)
39
+ ? parsed.reconstruction.target_tree_shape.minimal_required_outputs.map(String)
40
+ : [];
41
+ }
42
+
43
+ function isV2SpecGenerationInputs(specGenerationInputs) {
44
+ return specGenerationInputs.ok && specGenerationInputs.mode === "class_filtered";
45
+ }
46
+
47
+ async function validateSpecTreeV2(rootPath, projectRoot, specGenerationInputs) {
48
+ const errors = [];
49
+ const warnings = [];
50
+ const expectedRoot = path.resolve(projectRoot, specGenerationInputs.canonicalTargetRoot ?? ".nimi/spec");
51
+ const targetRoot = path.resolve(rootPath);
52
+
53
+ if (targetRoot !== expectedRoot) {
54
+ errors.push(`spec tree root mismatch: expected ${expectedRoot} but received ${targetRoot}`);
55
+ }
56
+
57
+ const files = await collectTreeFiles(targetRoot);
58
+ if (files.length === 0) {
59
+ return {
60
+ ok: false,
61
+ errors: errors.length > 0 ? errors : [`missing spec tree root: ${targetRoot}`],
62
+ warnings,
63
+ refusal: makeValidatorRefusal(
64
+ VALIDATOR_NATIVE_REFUSAL_CODES.SPEC_TREE_MISSING,
65
+ "spec tree root is missing or empty",
66
+ ),
67
+ };
68
+ }
69
+
70
+ const requiredFiles = await loadV2RequiredFiles(projectRoot);
71
+ const canonicalRoot = specGenerationInputs.canonicalTargetRoot ?? ".nimi/spec";
72
+ const missingRequired = requiredFiles
73
+ .map((entry) => path.posix.relative(canonicalRoot, entry))
74
+ .filter((entry) => !files.includes(entry));
75
+ if (missingRequired.length > 0) {
76
+ errors.push(`missing required canonical files: ${missingRequired.join(", ")}`);
77
+ }
78
+
79
+ const rootRef = path.relative(projectRoot, targetRoot).split(path.sep).join(path.posix.sep) || ".";
80
+ const placement = await validatePlacement(projectRoot, { rootRef });
81
+ const domainAdmission = await validateDomainAdmission(projectRoot, { rootRef });
82
+ const tableFamily = await validateTableFamily(projectRoot, { rootRef });
83
+ errors.push(...(placement.errors ?? []), ...(domainAdmission.errors ?? []), ...(tableFamily.errors ?? []));
84
+
85
+ return {
86
+ ok: errors.length === 0,
87
+ errors,
88
+ warnings,
89
+ refusal: errors.length === 0
90
+ ? null
91
+ : makeValidatorRefusal(
92
+ VALIDATOR_NATIVE_REFUSAL_CODES.SPEC_TREE_INVALID,
93
+ `spec tree is invalid: ${path.basename(targetRoot)}`,
94
+ ),
95
+ summary: {
96
+ profile: "surface_taxonomy_v1",
97
+ canonicalRoot,
98
+ totalFiles: files.length,
99
+ requiredFiles: requiredFiles.length,
100
+ missingRequired,
101
+ classifiedFiles: placement.summary?.total ?? files.length,
102
+ unexpectedFiles: [],
103
+ conflictingFiles: [],
104
+ },
105
+ };
106
+ }
107
+
108
+ function toPortableProjectPath(projectRoot, absolutePath) {
109
+ return path.relative(projectRoot, absolutePath).split(path.sep).join(path.posix.sep);
110
+ }
111
+
112
+ function pathStartsWithRoot(candidate, root) {
113
+ return candidate === root || candidate.startsWith(`${root}/`);
114
+ }
115
+
116
+ function generatedOutputNormativeRootOverlaps(specTreeModel) {
117
+ const normativeRoots = specTreeModel.domains.map((domain) => domain.normativeRoot).filter(Boolean);
118
+ const overlaps = [];
119
+ for (const pipeline of specTreeModel.generatedPipelines) {
120
+ for (const outputRoot of pipeline.outputRoots) {
121
+ for (const normativeRoot of normativeRoots) {
122
+ if (pathStartsWithRoot(outputRoot, normativeRoot) || pathStartsWithRoot(normativeRoot, outputRoot)) {
123
+ overlaps.push({
124
+ pipelineId: pipeline.id,
125
+ outputRoot,
126
+ normativeRoot,
127
+ });
128
+ }
129
+ }
130
+ }
131
+ }
132
+ return overlaps;
133
+ }
134
+
135
+ function normalizeAuditFileClass(entry) {
136
+ const explicitSurfaceClass = typeof entry.surface_class === "string" ? entry.surface_class : null;
137
+ if (explicitSurfaceClass) {
138
+ return explicitSurfaceClass;
139
+ }
140
+
141
+ switch (String(entry.file_class ?? "")) {
142
+ case "kernel_markdown":
143
+ return "product_authority";
144
+ case "kernel_tables":
145
+ return "product_authority_table";
146
+ case "domain_guides":
147
+ case "kernel_generated":
148
+ return "thin_guidance";
149
+ default:
150
+ return String(entry.file_class ?? "");
151
+ }
152
+ }
153
+
30
154
  export async function validateSpecTree(rootPath, options = {}) {
31
155
  const projectRoot = options.projectRoot ?? process.cwd();
32
156
  const specTreeModel = await loadSpecTreeModelContract(projectRoot);
157
+ const specGenerationInputs = await loadSpecGenerationInputsConfig(projectRoot);
33
158
  const errors = [];
34
159
  const warnings = [];
35
160
 
36
161
  if (!specTreeModel.ok) {
162
+ if (isV2SpecGenerationInputs(specGenerationInputs)) {
163
+ return validateSpecTreeV2(rootPath, projectRoot, specGenerationInputs);
164
+ }
37
165
  return {
38
166
  ok: false,
39
167
  errors: [`invalid spec tree model contract: ${specTreeModel.path}`],
@@ -74,6 +202,13 @@ export async function validateSpecTree(rootPath, options = {}) {
74
202
  errors.push(`missing required canonical files: ${missingRequired.join(", ")}`);
75
203
  }
76
204
 
205
+ const generatedOutputOverlaps = generatedOutputNormativeRootOverlaps(specTreeModel);
206
+ if (generatedOutputOverlaps.length > 0) {
207
+ errors.push(
208
+ `generated output roots overlap normative roots: ${generatedOutputOverlaps.map((entry) => `${entry.pipelineId}:${entry.outputRoot}->${entry.normativeRoot}`).join(", ")}`,
209
+ );
210
+ }
211
+
77
212
  for (const domain of specTreeModel.domains) {
78
213
  const domainRoot = path.posix.relative(specTreeModel.canonicalRoot, domain.root);
79
214
  const normativeRoot = path.posix.relative(specTreeModel.canonicalRoot, domain.normativeRoot);
@@ -132,10 +267,11 @@ export async function validateSpecAudit(auditPath, options = {}) {
132
267
  const specGenerationInputs = await loadSpecGenerationInputsConfig(projectRoot);
133
268
  const blueprintReference = await loadBlueprintReference(projectRoot);
134
269
  const auditContract = await loadSpecGenerationAuditContract(projectRoot);
270
+ const usesV2SurfaceModel = isV2SpecGenerationInputs(specGenerationInputs);
135
271
  const errors = [];
136
272
  const warnings = [];
137
273
 
138
- if (!specTreeModel.ok) {
274
+ if (!specTreeModel.ok && !usesV2SurfaceModel) {
139
275
  return {
140
276
  ok: false,
141
277
  errors: [`invalid spec tree model contract: ${specTreeModel.path}`],
@@ -199,8 +335,8 @@ export async function validateSpecAudit(auditPath, options = {}) {
199
335
  };
200
336
  }
201
337
 
202
- if (parsed.version !== 1) {
203
- errors.push("spec generation audit version must be 1");
338
+ if (![1, 2].includes(parsed.version)) {
339
+ errors.push("spec generation audit version must be 1 or 2");
204
340
  }
205
341
 
206
342
  if (String(parsed.contract_ref ?? "") !== SPEC_GENERATION_AUDIT_CONTRACT_REF) {
@@ -224,20 +360,27 @@ export async function validateSpecAudit(auditPath, options = {}) {
224
360
  : null,
225
361
  };
226
362
 
227
- if (String(audit.generation_mode ?? "") !== "mixed") {
363
+ if (usesV2SurfaceModel) {
364
+ if (!["mixed", "class_filtered"].includes(String(audit.generation_mode ?? ""))) {
365
+ errors.push("spec generation audit generation_mode must be `mixed` or `class_filtered`");
366
+ }
367
+ } else if (String(audit.generation_mode ?? "") !== "mixed") {
228
368
  errors.push("spec generation audit generation_mode must be `mixed`");
229
369
  }
230
- if (String(audit.canonical_target_root ?? "") !== specTreeModel.canonicalRoot) {
231
- errors.push(`spec generation audit canonical_target_root must be ${specTreeModel.canonicalRoot}`);
370
+
371
+ const canonicalRoot = specTreeModel.ok ? specTreeModel.canonicalRoot : specGenerationInputs.canonicalTargetRoot ?? ".nimi/spec";
372
+ const declaredProfile = specTreeModel.ok ? specTreeModel.profile : "surface_taxonomy_v1";
373
+ if (String(audit.canonical_target_root ?? "") !== canonicalRoot) {
374
+ errors.push(`spec generation audit canonical_target_root must be ${canonicalRoot}`);
232
375
  }
233
- if (String(audit.declared_profile ?? "") !== specTreeModel.profile) {
234
- errors.push(`spec generation audit declared_profile must be ${specTreeModel.profile}`);
376
+ if (String(audit.declared_profile ?? "") !== declaredProfile) {
377
+ errors.push(`spec generation audit declared_profile must be ${declaredProfile}`);
235
378
  }
236
379
  if (!isDeclaredInputsCompatibleWithConfig(declaredInputs, specGenerationInputs, blueprintReference)) {
237
380
  errors.push("spec generation audit input_roots must stay within the declared generation inputs and optional benchmark root");
238
381
  }
239
382
 
240
- const canonicalRootPath = path.resolve(projectRoot, specTreeModel.canonicalRoot);
383
+ const canonicalRootPath = path.resolve(projectRoot, canonicalRoot);
241
384
  const treeFiles = await collectTreeFiles(canonicalRootPath);
242
385
  if (treeFiles.length === 0) {
243
386
  return {
@@ -251,7 +394,14 @@ export async function validateSpecAudit(auditPath, options = {}) {
251
394
  };
252
395
  }
253
396
 
254
- const { auditedFiles, classifications } = classifyAuditCoveredFiles(treeFiles, specTreeModel);
397
+ const { auditedFiles, classifications } = specTreeModel.ok
398
+ ? classifyAuditCoveredFiles(treeFiles, specTreeModel)
399
+ : {
400
+ auditedFiles: treeFiles
401
+ .filter((entry) => !entry.startsWith("_meta/"))
402
+ .map((entry) => ({ path: entry, category: "surface_taxonomy_v1" })),
403
+ classifications: { unexpected: [], conflicts: [] },
404
+ };
255
405
  if (classifications.unexpected.length > 0) {
256
406
  errors.push(`spec tree contains unexpected files outside declared spec classes: ${classifications.unexpected.join(", ")}`);
257
407
  }
@@ -263,27 +413,82 @@ export async function validateSpecAudit(auditPath, options = {}) {
263
413
  if (!Array.isArray(audit.files)) {
264
414
  errors.push("spec generation audit files must be an array");
265
415
  }
416
+ const fileEntryRefs = audit.file_entry_refs === undefined
417
+ ? []
418
+ : Array.isArray(audit.file_entry_refs)
419
+ ? audit.file_entry_refs.map(String)
420
+ : null;
421
+ if (fileEntryRefs === null) {
422
+ errors.push("spec generation audit file_entry_refs must be an array when present");
423
+ }
424
+
425
+ const referencedFileEntries = [];
426
+ for (const entryRef of fileEntryRefs ?? []) {
427
+ if (entryRef.length === 0 || path.isAbsolute(entryRef)) {
428
+ errors.push(`spec generation audit file_entry_refs must be non-empty repository-relative paths: ${entryRef}`);
429
+ continue;
430
+ }
431
+ const absoluteEntryRef = path.resolve(projectRoot, entryRef);
432
+ const relativeEntryRef = path.relative(projectRoot, absoluteEntryRef).split(path.sep).join(path.posix.sep);
433
+ const expectedPrefix = usesV2SurfaceModel
434
+ ? ".nimi/local/state/spec-generation/spec-generation-audit/"
435
+ : `${specTreeModel.canonicalRoot}/_meta/spec-generation-audit/`;
436
+ if (relativeEntryRef !== entryRef || !relativeEntryRef.startsWith(expectedPrefix)) {
437
+ errors.push(`spec generation audit file_entry_ref must stay under ${expectedPrefix}: ${entryRef}`);
438
+ continue;
439
+ }
440
+ const entryText = await readTextIfFile(absoluteEntryRef);
441
+ if (entryText === null) {
442
+ errors.push(`spec generation audit file_entry_ref is missing: ${entryRef}`);
443
+ continue;
444
+ }
445
+ const entryParsed = parseYamlText(entryText);
446
+ const entryPayload = entryParsed?.spec_generation_audit_file_entries;
447
+ if (!isPlainObject(entryParsed) || !isPlainObject(entryPayload)) {
448
+ errors.push(`spec generation audit file_entry_ref is not a valid entry shard: ${entryRef}`);
449
+ continue;
450
+ }
451
+ const expectedShardVersion = usesV2SurfaceModel ? 2 : 1;
452
+ if (entryParsed.version !== expectedShardVersion) {
453
+ errors.push(`spec generation audit file_entry_ref version must be ${expectedShardVersion}: ${entryRef}`);
454
+ }
455
+ if (String(entryParsed.contract_ref ?? "") !== SPEC_GENERATION_AUDIT_CONTRACT_REF) {
456
+ errors.push(`spec generation audit file_entry_ref contract_ref must be ${SPEC_GENERATION_AUDIT_CONTRACT_REF}: ${entryRef}`);
457
+ }
458
+ if (String(entryPayload.parent_ref ?? "") !== toPortableProjectPath(projectRoot, absoluteAuditPath)) {
459
+ errors.push(`spec generation audit file_entry_ref parent_ref must point to ${toPortableProjectPath(projectRoot, absoluteAuditPath)}: ${entryRef}`);
460
+ }
461
+ if (!Array.isArray(entryPayload.files)) {
462
+ errors.push(`spec generation audit file_entry_ref files must be an array: ${entryRef}`);
463
+ continue;
464
+ }
465
+ referencedFileEntries.push(...entryPayload.files);
466
+ }
467
+ const allFileEntries = [...fileEntries, ...referencedFileEntries];
266
468
 
267
469
  const auditEntryByRelativePath = new Map();
268
- for (const entry of fileEntries) {
470
+ for (const entry of allFileEntries) {
269
471
  if (!isPlainObject(entry)) {
270
472
  errors.push("spec generation audit file entries must be mappings");
271
473
  continue;
272
474
  }
273
475
 
274
- const missingEntryFields = SPEC_GENERATION_AUDIT_FILE_REQUIRED_FIELDS.filter((field) => !(field in entry));
476
+ const requiredEntryFields = auditContract.requiredFileEntryFields?.length > 0
477
+ ? auditContract.requiredFileEntryFields
478
+ : SPEC_GENERATION_AUDIT_FILE_REQUIRED_FIELDS;
479
+ const missingEntryFields = requiredEntryFields.filter((field) => !(field in entry));
275
480
  if (missingEntryFields.length > 0) {
276
481
  errors.push(`spec generation audit file entry is missing required fields: ${missingEntryFields.join(", ")}`);
277
482
  continue;
278
483
  }
279
484
 
280
485
  const canonicalPath = String(entry.canonical_path ?? "");
281
- if (!canonicalPath.startsWith(`${specTreeModel.canonicalRoot}/`) && canonicalPath !== `${specTreeModel.canonicalRoot}/INDEX.md`) {
282
- errors.push(`spec generation audit canonical_path must stay under ${specTreeModel.canonicalRoot}: ${canonicalPath}`);
486
+ if (!canonicalPath.startsWith(`${canonicalRoot}/`) && canonicalPath !== `${canonicalRoot}/INDEX.md`) {
487
+ errors.push(`spec generation audit canonical_path must stay under ${canonicalRoot}: ${canonicalPath}`);
283
488
  continue;
284
489
  }
285
490
 
286
- const relativePath = path.posix.relative(specTreeModel.canonicalRoot, canonicalPath);
491
+ const relativePath = path.posix.relative(canonicalRoot, canonicalPath);
287
492
  if (relativePath.startsWith("_meta/")) {
288
493
  errors.push(`spec generation audit must not record _meta files as generated canonical files: ${canonicalPath}`);
289
494
  continue;
@@ -331,8 +536,11 @@ export async function validateSpecAudit(auditPath, options = {}) {
331
536
  }
332
537
 
333
538
  const missingAuditEntries = [];
334
- const requiredFiles = (specTreeModel.requiredFilesByProfile[specTreeModel.profile] ?? [])
335
- .map((entry) => path.posix.relative(specTreeModel.canonicalRoot, entry))
539
+ const requiredFileRefs = specTreeModel.ok
540
+ ? (specTreeModel.requiredFilesByProfile[specTreeModel.profile] ?? [])
541
+ : await loadV2RequiredFiles(projectRoot);
542
+ const requiredFiles = requiredFileRefs
543
+ .map((entry) => path.posix.relative(canonicalRoot, entry))
336
544
  .filter((entry) => !entry.startsWith("_meta/"));
337
545
 
338
546
  for (const classifiedFile of auditedFiles) {
@@ -342,7 +550,8 @@ export async function validateSpecAudit(auditPath, options = {}) {
342
550
  continue;
343
551
  }
344
552
 
345
- if (auditEntry.file_class !== classifiedFile.classId && !(classifiedFile.path === "INDEX.md" && auditEntry.file_class === "index")) {
553
+ const auditFileClass = normalizeAuditFileClass(auditEntry);
554
+ if (classifiedFile.classId && auditFileClass !== classifiedFile.classId && !(classifiedFile.path === "INDEX.md" && auditFileClass === "index")) {
346
555
  errors.push(`spec generation audit file_class does not match canonical tree classification for ${classifiedFile.path}: expected ${classifiedFile.classId}`);
347
556
  }
348
557
  }
@@ -395,8 +604,8 @@ export async function validateSpecAudit(auditPath, options = {}) {
395
604
  `spec generation audit is invalid: ${path.basename(absoluteAuditPath)}`,
396
605
  ),
397
606
  summary: {
398
- canonicalRoot: specTreeModel.canonicalRoot,
399
- declaredProfile: specTreeModel.profile,
607
+ canonicalRoot,
608
+ declaredProfile,
400
609
  auditedFiles: auditEntryByRelativePath.size,
401
610
  requiredAuditedFiles: requiredFiles.length,
402
611
  missingAuditEntries,
@@ -0,0 +1,246 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import YAML from "yaml";
6
+
7
+ import { pathExists } from "../fs-helpers.mjs";
8
+ import { isPlainObject } from "../value-helpers.mjs";
9
+
10
+ export const DESIGN_ROOT = ".nimi/local/sweep-design";
11
+ export const DESIGN_STATES = new Set([
12
+ "raw",
13
+ "confirmed",
14
+ "duplicate",
15
+ "superseded",
16
+ "false_positive",
17
+ "needs_more_audit",
18
+ "needs_user_decision",
19
+ "needs_authority_alignment",
20
+ "needs_design",
21
+ "ready_for_implementation_wave",
22
+ "blocked",
23
+ ]);
24
+ export const TERMINAL_STATES = new Set(["duplicate", "superseded", "false_positive"]);
25
+ export const TRANSIENT_STATES = new Set(["raw", "confirmed", "needs_design"]);
26
+ export const FINAL_OUTCOME_STATES = new Set([
27
+ "duplicate",
28
+ "superseded",
29
+ "false_positive",
30
+ "needs_more_audit",
31
+ "needs_user_decision",
32
+ "needs_authority_alignment",
33
+ "ready_for_implementation_wave",
34
+ "blocked",
35
+ ]);
36
+ export const AUDITOR_FAMILIES = new Set([
37
+ "anthropic_claude",
38
+ "openai_gpt",
39
+ "openai_codex",
40
+ "google_gemini",
41
+ "xai_grok",
42
+ "meta_llama",
43
+ "mistral",
44
+ "other",
45
+ ]);
46
+ export const AUDITOR_MODES = new Set(["focused", "all", "degraded"]);
47
+ export const AUDITOR_RESULT_ORIGINS = new Set(["llm_session", "external_llm_session", "synthetic_trial"]);
48
+ export const LLM_AUDITOR_RESULT_ORIGINS = new Set(["llm_session", "external_llm_session"]);
49
+ export const PRIOR_DESIGN_STATE_MARKERS = new Set([
50
+ "empty",
51
+ "present",
52
+ "partial",
53
+ "superseded_by_later_audit",
54
+ "evidence_gap",
55
+ ]);
56
+ export const REVISION_TYPES = new Set([
57
+ "finding_state_revision",
58
+ "duplicate_judgement",
59
+ "superseded_judgement",
60
+ "cluster_create",
61
+ "cluster_merge",
62
+ "cluster_split",
63
+ "cluster_retire",
64
+ "cluster_reopen",
65
+ "finding_move",
66
+ "wave_create",
67
+ "wave_merge",
68
+ "wave_split",
69
+ "wave_retract",
70
+ "wave_demote",
71
+ "wave_block",
72
+ "wave_implementation_ready",
73
+ "wave_dependency_rewrite",
74
+ "wave_validation_or_closeout_strengthening",
75
+ "decision_packet_create",
76
+ "extra_audit_request_create",
77
+ "extra_audit_request_close",
78
+ "human_decision_request_create",
79
+ "human_decision_request_resolve",
80
+ "final_state_projection_update",
81
+ "user_decision_queue_rewrite",
82
+ ]);
83
+
84
+ export function toPosix(filePath) {
85
+ return filePath.split(path.sep).join(path.posix.sep);
86
+ }
87
+
88
+ export function relPath(projectRoot, absolutePath) {
89
+ return toPosix(path.relative(projectRoot, absolutePath));
90
+ }
91
+
92
+ export function safeDesignId(value) {
93
+ if (typeof value !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{2,140}$/.test(value)) {
94
+ return null;
95
+ }
96
+ return value;
97
+ }
98
+
99
+ export function deriveRunId(sweepId) {
100
+ return `sweep-design-${String(sweepId).replace(/[^a-zA-Z0-9._-]+/g, "-")}`;
101
+ }
102
+
103
+ export function designRef(runId, ...parts) {
104
+ return path.posix.join(DESIGN_ROOT, runId, ...parts);
105
+ }
106
+
107
+ export function artifactPath(projectRoot, ref) {
108
+ return path.join(projectRoot, ...ref.split("/"));
109
+ }
110
+
111
+ export function inputError(error) {
112
+ return { ok: false, inputError: true, exitCode: 2, error };
113
+ }
114
+
115
+ export async function loadYamlPath(filePath) {
116
+ try {
117
+ const text = await readFile(filePath, "utf8");
118
+ return YAML.parse(text);
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ export function sha256Text(text) {
125
+ return createHash("sha256").update(text).digest("hex");
126
+ }
127
+
128
+ export function stableStringify(value) {
129
+ if (Array.isArray(value)) {
130
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
131
+ }
132
+ if (value && typeof value === "object") {
133
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
134
+ }
135
+ return JSON.stringify(value);
136
+ }
137
+
138
+ export function sha256Object(value) {
139
+ return sha256Text(stableStringify(value));
140
+ }
141
+
142
+ export async function loadYamlRef(projectRoot, ref) {
143
+ return loadYamlPath(artifactPath(projectRoot, ref));
144
+ }
145
+
146
+ export async function writeYamlRef(projectRoot, ref, value) {
147
+ const destination = artifactPath(projectRoot, ref);
148
+ await mkdir(path.dirname(destination), { recursive: true });
149
+ await writeFile(destination, YAML.stringify(value, { aliasDuplicateObjects: false }), "utf8");
150
+ return ref;
151
+ }
152
+
153
+ export async function assertDesignArtifact(projectRoot, ref, kind, label) {
154
+ const value = await loadYamlRef(projectRoot, ref);
155
+ if (!isPlainObject(value) || value.kind !== kind) {
156
+ return inputError(`nimicoding sweep design refused: ${label} is missing or malformed.\n`);
157
+ }
158
+ return { ok: true, value, ref };
159
+ }
160
+
161
+ export function auditFindingsRef(sweepId) {
162
+ return `.nimi/local/audit/evidence/${sweepId}/findings.yaml`;
163
+ }
164
+
165
+ export async function loadAuditFindings(projectRoot, sweepId) {
166
+ const ref = auditFindingsRef(sweepId);
167
+ const sourcePath = artifactPath(projectRoot, ref);
168
+ const info = await pathExists(sourcePath);
169
+ if (!info || !info.isFile()) {
170
+ return inputError(`nimicoding sweep design refused: audit findings not found for ${sweepId}.\n`);
171
+ }
172
+ const sourceText = await readFile(sourcePath, "utf8");
173
+ const store = YAML.parse(sourceText);
174
+ if (!isPlainObject(store) || store.kind !== "audit-findings" || store.sweep_id !== sweepId || !Array.isArray(store.findings)) {
175
+ return inputError(`nimicoding sweep design refused: audit findings are malformed for ${sweepId}.\n`);
176
+ }
177
+ return { ok: true, ref, store, sourceSha256: sha256Text(sourceText) };
178
+ }
179
+
180
+ export function findingOwnerDomain(finding) {
181
+ if (typeof finding.owner_domain === "string" && finding.owner_domain.length > 0) {
182
+ return finding.owner_domain;
183
+ }
184
+ const file = finding.location?.file;
185
+ if (typeof file === "string" && file.includes("/")) {
186
+ return file.split("/")[0];
187
+ }
188
+ return "root";
189
+ }
190
+
191
+ export function findingAuthorityRef(finding) {
192
+ return finding.root_cause?.authority_ref ?? finding.authority_ref ?? null;
193
+ }
194
+
195
+ export function findingRepairTarget(finding) {
196
+ return finding.root_cause?.repair_target ?? finding.location?.file ?? null;
197
+ }
198
+
199
+ export function findingCodeRefs(finding) {
200
+ return [
201
+ finding.location?.file,
202
+ ...(Array.isArray(finding.implementation_refs) ? finding.implementation_refs : []),
203
+ ].filter((value, index, array) => typeof value === "string" && value.length > 0 && array.indexOf(value) === index);
204
+ }
205
+
206
+ export function normalizeFindingForDesign(finding, sourceFindingsRef) {
207
+ return {
208
+ finding_id: finding.id,
209
+ source_audit_sweep_id: finding.sweep_id,
210
+ source_chunk_id: finding.chunk_id ?? null,
211
+ source_finding_ref: `${sourceFindingsRef}#${finding.id}`,
212
+ fingerprint: finding.fingerprint ?? null,
213
+ severity: finding.severity ?? null,
214
+ category: finding.category ?? null,
215
+ actionability: finding.actionability ?? null,
216
+ confidence: finding.confidence ?? null,
217
+ title: finding.title ?? null,
218
+ owner_domain: findingOwnerDomain(finding),
219
+ authority_ref: findingAuthorityRef(finding),
220
+ evidence_refs: Array.isArray(finding.implementation_refs) ? finding.implementation_refs : [],
221
+ repair_target: findingRepairTarget(finding),
222
+ location: finding.location ?? null,
223
+ root_cause_key: finding.root_cause?.key ?? null,
224
+ contract_seam: finding.root_cause?.contract_seam ?? null,
225
+ impact: finding.impact ?? null,
226
+ };
227
+ }
228
+
229
+ export function requireRunId(options) {
230
+ const runId = safeDesignId(options.runId);
231
+ if (!runId) {
232
+ return inputError("nimicoding sweep design refused: --run-id is required.\n");
233
+ }
234
+ return { ok: true, runId };
235
+ }
236
+
237
+ export function nowIso() {
238
+ return new Date().toISOString();
239
+ }
240
+
241
+ export function slug(value) {
242
+ return String(value ?? "item")
243
+ .toLowerCase()
244
+ .replace(/[^a-z0-9]+/g, "-")
245
+ .replace(/^-+|-+$/g, "") || "item";
246
+ }