@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
@@ -1,3 +1,6 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+
1
4
  import { loadTopicRuntimeContracts } from "./contracts.mjs";
2
5
 
3
6
  function stringList(value) {
@@ -81,8 +84,307 @@ export function hasFreshPassingPostUpdateReview(results, implementationResult, p
81
84
  ));
82
85
  }
83
86
 
84
- export function buildPostUpdateReviewDecision({ topicId, wave, packets, results, policy, commandRef }) {
85
- const specUpdatingPacket = packets.find((entry) => needsPostUpdateReview(entry.packet, policy));
87
+ function hasPlaceholder(value) {
88
+ return /<[^>]+>/u.test(String(value ?? ""));
89
+ }
90
+
91
+ function concreteRef(value) {
92
+ return typeof value === "string"
93
+ && value.length > 0
94
+ && !hasPlaceholder(value)
95
+ && !path.isAbsolute(value)
96
+ && !value.includes("..");
97
+ }
98
+
99
+ function refsAreConcrete(values) {
100
+ return stringList(values).length > 0 && stringList(values).every(concreteRef);
101
+ }
102
+
103
+ async function readJsonRef(projectRoot, ref) {
104
+ if (!concreteRef(ref)) return null;
105
+ try {
106
+ return JSON.parse(await readFile(path.join(projectRoot, ref), "utf8"));
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ function extractTopicValidationEvidenceRefs(sourceText, waveId) {
113
+ const refs = new Set();
114
+ const pattern = new RegExp(`\\.nimi/topics/[^\\s)\\]'"<>]+/evidence-validation-${waveId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}[^\\s)\\]'"<>]+\\.json`, "gu");
115
+ for (const match of sourceText.matchAll(pattern)) {
116
+ refs.add(match[0]);
117
+ }
118
+ return [...refs];
119
+ }
120
+
121
+ function extractPacketIdsFromSource(sourceText) {
122
+ const ids = new Set();
123
+ const patterns = [
124
+ /^\s*packet_id:\s*`?([a-z0-9]+(?:-[a-z0-9]+)*)`?\s*$/gimu,
125
+ /\bpacket_id\s*[:=]\s*`?([a-z0-9]+(?:-[a-z0-9]+)*)`?/gimu,
126
+ /\bpacket\s+id\s*[:=]\s*`?([a-z0-9]+(?:-[a-z0-9]+)*)`?/gimu,
127
+ ];
128
+ for (const pattern of patterns) {
129
+ for (const match of sourceText.matchAll(pattern)) {
130
+ ids.add(match[1]);
131
+ }
132
+ }
133
+ return [...ids];
134
+ }
135
+
136
+ function declaresPostUpdateAmbiguity(sourceText) {
137
+ const ambiguityPattern = /\b(authority|scope|gate|product|semantic)\s+(ambiguity|ambiguous|fork|blocked|blocker|change required)\b/iu;
138
+ const normalized = sourceText.replace(/\s+/gu, " ");
139
+ for (const match of normalized.matchAll(new RegExp(ambiguityPattern.source, "giu"))) {
140
+ const preceding = normalized.slice(0, match.index).toLowerCase();
141
+ if (/\b(no|none|without)\b[^.!?\n]{0,120}$/u.test(preceding)) {
142
+ continue;
143
+ }
144
+ return true;
145
+ }
146
+ return false;
147
+ }
148
+
149
+ function sourceContainsNegatedTerms(sourceText, terms) {
150
+ const normalized = sourceText.replace(/\s+/gu, " ").toLowerCase();
151
+ return terms.every((term) => {
152
+ const escaped = term.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&").replace(/\\ /gu, "\\s+");
153
+ return new RegExp(`\\b(no|not|without)\\b[^.!?]{0,160}\\b${escaped}\\b`, "iu").test(normalized)
154
+ || new RegExp(`\\b${escaped}\\b[^.!?]{0,80}\\b(not\\s+mutated|not\\s+introduced)\\b`, "iu").test(normalized);
155
+ });
156
+ }
157
+
158
+ function hasRequiredPostUpdateNegativeDeclarations(sourceText) {
159
+ return sourceContainsNegatedTerms(sourceText, [
160
+ "source audit findings",
161
+ "source sweep-design artifacts",
162
+ "pseudo-success",
163
+ "fallback success",
164
+ "compatibility shim",
165
+ "dual-read",
166
+ "dual-write",
167
+ ]);
168
+ }
169
+
170
+ async function latestWorkerPromptPacketId(topicDir, packetEntries) {
171
+ const packetIds = new Set(packetEntries.map((entry) => entry.packet?.packet_id).filter(Boolean));
172
+ if (!topicDir || packetIds.size === 0) return null;
173
+ let entries = [];
174
+ try {
175
+ entries = await readdir(topicDir, { withFileTypes: true });
176
+ } catch {
177
+ return null;
178
+ }
179
+ const prompts = [];
180
+ for (const entry of entries) {
181
+ if (!entry.isFile() || !entry.name.startsWith("prompt-") || !entry.name.endsWith("-worker.md")) continue;
182
+ const packetId = entry.name.slice("prompt-".length, -"-worker.md".length);
183
+ if (!packetIds.has(packetId)) continue;
184
+ const promptPath = path.join(topicDir, entry.name);
185
+ try {
186
+ const promptStat = await stat(promptPath);
187
+ prompts.push({ packetId, mtimeMs: promptStat.mtimeMs, promptRefName: entry.name });
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+ if (prompts.length === 0) return null;
193
+ prompts.sort((left, right) => right.mtimeMs - left.mtimeMs || left.promptRefName.localeCompare(right.promptRefName));
194
+ if (prompts.length > 1 && prompts[0].mtimeMs === prompts[1].mtimeMs) return { ambiguous: true };
195
+ return prompts[0];
196
+ }
197
+
198
+ async function workerPromptExists(topicDir, packetId) {
199
+ if (!topicDir || !packetId) return false;
200
+ try {
201
+ const promptStat = await stat(path.join(topicDir, `prompt-${packetId}-worker.md`));
202
+ return promptStat.isFile();
203
+ } catch {
204
+ return false;
205
+ }
206
+ }
207
+
208
+ function passResultSourceRefSet(projectRoot, results, waveId) {
209
+ const refs = new Set();
210
+ for (const entry of results) {
211
+ if (entry.result?.verdict !== "PASS") continue;
212
+ if (entry.result?.wave_id && entry.result.wave_id !== waveId) continue;
213
+ const sourceRef = entry.result?.source_ref;
214
+ if (!concreteRef(sourceRef)) continue;
215
+ refs.add(path.resolve(projectRoot, sourceRef));
216
+ }
217
+ return refs;
218
+ }
219
+
220
+ async function hasWaveRemediationArtifact(projectRoot, topicDir, waveId, results) {
221
+ try {
222
+ const passSourceRefs = passResultSourceRefSet(projectRoot, results, waveId);
223
+ const entries = await readdir(topicDir, { withFileTypes: true });
224
+ for (const entry of entries) {
225
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
226
+ if (entry.name.startsWith(`packet-${waveId}-remediation-`)) return true;
227
+ if (!entry.name.startsWith(`source-${waveId}-`) || !/remediat/iu.test(entry.name)) continue;
228
+ const sourcePath = path.join(topicDir, entry.name);
229
+ if (!passSourceRefs.has(path.resolve(sourcePath))) continue;
230
+ const sourceText = await readFile(sourcePath, "utf8");
231
+ if (
232
+ /\bverdict:\s*PASS\b/iu.test(sourceText)
233
+ && /\blocal_packet_authority_scope_remediation_only:\s*true\b/iu.test(sourceText)
234
+ && /\bproduct_semantic_ambiguity:\s*false\b/iu.test(sourceText)
235
+ && /\bsource_audit_findings_mutated:\s*false\b/iu.test(sourceText)
236
+ && /\bsource_sweep_design_artifacts_mutated:\s*false\b/iu.test(sourceText)
237
+ ) {
238
+ return true;
239
+ }
240
+ }
241
+ return false;
242
+ } catch {
243
+ return false;
244
+ }
245
+ }
246
+
247
+ async function selectPostUpdateProofPacket({ topicDir, wave, specUpdatingPackets, sourceText }) {
248
+ if (specUpdatingPackets.length === 0) {
249
+ return { ok: false, reason: "post-update proof requires a spec-updating packet" };
250
+ }
251
+
252
+ const packetIds = extractPacketIdsFromSource(sourceText);
253
+ const matchingSourcePackets = packetIds
254
+ .map((packetId) => specUpdatingPackets.find((entry) => entry.packet?.packet_id === packetId) ?? null)
255
+ .filter(Boolean);
256
+ const uniqueMatchingSourcePackets = [...new Map(matchingSourcePackets.map((entry) => [entry.packet.packet_id, entry])).values()];
257
+ if (uniqueMatchingSourcePackets.length > 1) {
258
+ return { ok: false, reason: "implementation source names multiple post-update packets" };
259
+ }
260
+ if (packetIds.length > 0 && uniqueMatchingSourcePackets.length === 0) {
261
+ return { ok: false, reason: "implementation source packet_id does not match a current post-update packet" };
262
+ }
263
+ if (uniqueMatchingSourcePackets.length === 1) {
264
+ return { ok: true, entry: uniqueMatchingSourcePackets[0], selectionSource: "implementation_source" };
265
+ }
266
+
267
+ const promptLineage = await latestWorkerPromptPacketId(topicDir, specUpdatingPackets);
268
+ if (promptLineage?.ambiguous) {
269
+ return { ok: false, reason: "latest worker prompt lineage is ambiguous" };
270
+ }
271
+ if (promptLineage?.packetId) {
272
+ const promptPacket = specUpdatingPackets.find((entry) => entry.packet?.packet_id === promptLineage.packetId);
273
+ if (promptPacket) return { ok: true, entry: promptPacket, selectionSource: "worker_prompt" };
274
+ }
275
+
276
+ if (specUpdatingPackets.length === 1) {
277
+ return { ok: true, entry: specUpdatingPackets[0], selectionSource: "single_packet" };
278
+ }
279
+
280
+ return { ok: false, reason: "post-update packet lineage is ambiguous" };
281
+ }
282
+
283
+ async function mechanicalPostUpdateJudgementProof({ projectRoot, topicDir, wave, specUpdatingPackets, results, implementationResult }) {
284
+ if (!projectRoot || !topicDir) {
285
+ return { ok: false, reason: "mechanical proof requires project and topic roots" };
286
+ }
287
+ const implementationVerifiedAt = verifiedAtMs(implementationResult);
288
+ if (!Number.isFinite(implementationVerifiedAt)) {
289
+ return { ok: false, reason: "implementation result verified_at is not concrete" };
290
+ }
291
+ const requiredKinds = new Map([
292
+ ["audit", "audit result must pass"],
293
+ ["preflight", "preflight result must pass"],
294
+ ["implementation", "implementation result must pass"],
295
+ ]);
296
+ for (const [kind, reason] of requiredKinds) {
297
+ const result = latestResultOfKind(results, kind);
298
+ if (result?.result?.verdict !== "PASS") {
299
+ return { ok: false, reason };
300
+ }
301
+ }
302
+ const laterBlockingResult = results.find((entry) => {
303
+ const verifiedAt = verifiedAtMs(entry);
304
+ return Number.isFinite(verifiedAt)
305
+ && verifiedAt >= implementationVerifiedAt
306
+ && entry.result?.verdict !== "PASS";
307
+ });
308
+ if (laterBlockingResult) {
309
+ return { ok: false, reason: "a later non-PASS result exists" };
310
+ }
311
+ const sourceRef = implementationResult?.result?.source_ref;
312
+ if (!concreteRef(sourceRef)) {
313
+ return { ok: false, reason: "implementation source ref is missing or non-concrete" };
314
+ }
315
+ let sourceText = "";
316
+ try {
317
+ sourceText = await readFile(path.join(projectRoot, sourceRef), "utf8");
318
+ } catch {
319
+ return { ok: false, reason: "implementation source ref is not readable" };
320
+ }
321
+ const selectedPacket = await selectPostUpdateProofPacket({
322
+ topicDir,
323
+ wave,
324
+ specUpdatingPackets,
325
+ sourceText,
326
+ });
327
+ if (!selectedPacket.ok) {
328
+ return { ok: false, reason: selectedPacket.reason };
329
+ }
330
+ const packet = selectedPacket.entry.packet;
331
+ if (packet.status !== "dispatched") {
332
+ return { ok: false, reason: "post-update proof packet is not the current dispatched packet" };
333
+ }
334
+ if (!refsAreConcrete(packet.authority_owner) || !refsAreConcrete(packet.canonical_seams)) {
335
+ return { ok: false, reason: "packet authority refs are missing or non-concrete" };
336
+ }
337
+ const promptLineage = await latestWorkerPromptPacketId(topicDir, specUpdatingPackets);
338
+ if (selectedPacket.selectionSource === "implementation_source") {
339
+ if (!await workerPromptExists(topicDir, packet.packet_id)) {
340
+ return { ok: false, reason: "implementation result packet_id does not have worker prompt lineage" };
341
+ }
342
+ } else if (promptLineage?.ambiguous) {
343
+ return { ok: false, reason: "latest worker prompt lineage is ambiguous" };
344
+ }
345
+ if (promptLineage?.ambiguous) {
346
+ return { ok: false, reason: "latest worker prompt lineage is ambiguous" };
347
+ }
348
+ if (promptLineage?.packetId && promptLineage.packetId !== packet.packet_id) {
349
+ return { ok: false, reason: "implementation result packet_id is not the latest worker prompt lineage" };
350
+ }
351
+ const isMultiAuthority = stringList(packet.authority_owner).length > 1;
352
+ if (isMultiAuthority) {
353
+ const hasExplicitPacketLineage = selectedPacket.selectionSource === "implementation_source"
354
+ && extractPacketIdsFromSource(sourceText).includes(packet.packet_id);
355
+ if (!hasExplicitPacketLineage) {
356
+ return { ok: false, reason: "multi-authority post-update proof requires implementation-source packet lineage" };
357
+ }
358
+ if (!await hasWaveRemediationArtifact(projectRoot, topicDir, wave.wave_id, results) || !/\bremediat(?:e|ed|ion)\b/iu.test(sourceText)) {
359
+ return { ok: false, reason: "multi-authority post-update proof requires explicit remediation lineage" };
360
+ }
361
+ }
362
+ const evidenceRefs = extractTopicValidationEvidenceRefs(sourceText, wave.wave_id);
363
+ if (evidenceRefs.length === 0) {
364
+ return { ok: false, reason: "implementation source does not cite topic-local validation evidence" };
365
+ }
366
+ for (const ref of evidenceRefs) {
367
+ if (!ref.startsWith(`${path.relative(projectRoot, topicDir).split(path.sep).join("/")}/`)) {
368
+ return { ok: false, reason: `validation evidence is outside the topic root: ${ref}` };
369
+ }
370
+ const evidence = await readJsonRef(projectRoot, ref);
371
+ if (!evidence || evidence.status !== "pass" || evidence.exit_code !== 0) {
372
+ return { ok: false, reason: `validation evidence is not a clean pass: ${ref}` };
373
+ }
374
+ }
375
+ if (declaresPostUpdateAmbiguity(sourceText)) {
376
+ return { ok: false, reason: "implementation source declares authority/scope/gate/product/semantic ambiguity" };
377
+ }
378
+ if (!hasRequiredPostUpdateNegativeDeclarations(sourceText)) {
379
+ return { ok: false, reason: "implementation source does not declare required mutation and shortcut negative checks" };
380
+ }
381
+ return { ok: true, evidenceRefs, sourceRef, packetId: packet.packet_id };
382
+ }
383
+
384
+ export async function buildPostUpdateReviewDecision({ projectRoot, topicDir, topicId, wave, packets, results, policy, commandRef }) {
385
+ const wavePackets = packets.filter((entry) => entry.packet?.wave_id === wave.wave_id);
386
+ const specUpdatingPackets = wavePackets.filter((entry) => needsPostUpdateReview(entry.packet, policy));
387
+ const specUpdatingPacket = specUpdatingPackets[0] ?? null;
86
388
  const implementationResult = latestResultOfKind(results, "implementation");
87
389
  if (
88
390
  !specUpdatingPacket
@@ -92,6 +394,37 @@ export function buildPostUpdateReviewDecision({ topicId, wave, packets, results,
92
394
  return null;
93
395
  }
94
396
  const reviewPolicy = policy.postUpdateReview ?? {};
397
+ const mechanicalProof = await mechanicalPostUpdateJudgementProof({
398
+ projectRoot,
399
+ topicDir,
400
+ wave,
401
+ specUpdatingPackets,
402
+ results,
403
+ implementationResult,
404
+ });
405
+ if (mechanicalProof.ok) {
406
+ return {
407
+ stopClass: "continue",
408
+ recommendedAction: "record_result",
409
+ reasonCode: "mechanical_post_update_judgement_pass",
410
+ recommendedDecision: "record_mechanical_post_update_judgement_pass",
411
+ recommendationRationale: "Post-update evidence proves the implementation stayed inside packet authority and all cited validation checks passed.",
412
+ expectedArtifacts: [`result-${wave.wave_id}-${reviewPolicy.requiredResultKind}.md`],
413
+ nextCommandRef: commandRef([
414
+ "result",
415
+ "record",
416
+ topicId,
417
+ "--kind",
418
+ reviewPolicy.requiredResultKind,
419
+ "--verdict",
420
+ reviewPolicy.passVerdict,
421
+ "--from",
422
+ mechanicalProof.sourceRef,
423
+ "--verified-at",
424
+ implementationResult.result.verified_at,
425
+ ]),
426
+ };
427
+ }
95
428
  return {
96
429
  stopClass: "require_human_confirmation",
97
430
  recommendedAction: "record_result",
@@ -112,6 +445,11 @@ export function buildPostUpdateReviewDecision({ topicId, wave, packets, results,
112
445
  "--verified-at",
113
446
  "<utc>",
114
447
  ]),
448
+ blockingChecks: [{
449
+ ok: false,
450
+ code: "mechanical_post_update_judgement_not_proven",
451
+ message: mechanicalProof.reason,
452
+ }],
115
453
  };
116
454
  }
117
455
 
@@ -139,6 +477,40 @@ export function buildAuthorityConvergenceDecision({ topicId, wave, packet, audit
139
477
  };
140
478
  }
141
479
  if (auditResult?.result?.verdict === policy.passVerdict) {
480
+ if (packet.packet_kind === "preflight") {
481
+ return {
482
+ stopClass: "require_human_confirmation",
483
+ recommendedAction: "freeze_packet",
484
+ reasonCode: "preflight_authority_audit_passed_requires_implementation_packet",
485
+ recommendedDecision: "create_or_select_an_implementation_ready_packet_before_worker_dispatch",
486
+ recommendationRationale: "The authority convergence audit passed for a preflight packet, but preflight evidence is not implementation admission.",
487
+ expectedArtifacts: ["packet-<implementation-ready-packet-id>.md"],
488
+ nextCommandRef: commandRef(["packet", "freeze", topicId, "--from", "<implementation-ready-draft-packet>"]),
489
+ };
490
+ }
491
+ if (wave.state === "preflight_admitted") {
492
+ return {
493
+ stopClass: "continue",
494
+ recommendedAction: "record_result",
495
+ reasonCode: "implementation_admission_result_required",
496
+ recommendedDecision: "record_preflight_pass_before_worker_dispatch",
497
+ recommendationRationale: "The authority convergence audit passed, but the selected wave must explicitly enter implementation admission before worker dispatch.",
498
+ expectedArtifacts: [`result-${wave.wave_id}-preflight.md`],
499
+ nextCommandRef: commandRef([
500
+ "result",
501
+ "record",
502
+ topicId,
503
+ "--kind",
504
+ "preflight",
505
+ "--verdict",
506
+ policy.passVerdict,
507
+ "--from",
508
+ auditResult.result.source_ref ?? "<authority-convergence-audit-source>",
509
+ "--verified-at",
510
+ auditResult.result.verified_at ?? "<utc>",
511
+ ]),
512
+ };
513
+ }
142
514
  return {
143
515
  stopClass: "continue",
144
516
  recommendedAction: "dispatch_worker",
@@ -247,6 +619,29 @@ export async function buildPreImplementationDecision({
247
619
  commandRef,
248
620
  });
249
621
  }
622
+ if (wave.state === "preflight_admitted") {
623
+ return {
624
+ stopClass: "require_human_confirmation",
625
+ recommendedAction: "record_result",
626
+ reasonCode: "implementation_admission_result_required",
627
+ recommendedDecision: "record_preflight_pass_before_worker_dispatch",
628
+ recommendationRationale: "A dispatchable implementation packet exists, but worker dispatch requires explicit implementation admission evidence.",
629
+ expectedArtifacts: [`result-${wave.wave_id}-preflight.md`],
630
+ nextCommandRef: commandRef([
631
+ "result",
632
+ "record",
633
+ loaded.topicId,
634
+ "--kind",
635
+ "preflight",
636
+ "--verdict",
637
+ "PASS",
638
+ "--from",
639
+ "<implementation-readiness-evidence>",
640
+ "--verified-at",
641
+ "<utc>",
642
+ ]),
643
+ };
644
+ }
250
645
  const decision = dispatchWorkerDecision(loaded.topicId, dispatchable.packet);
251
646
  decision.nextCommandRef = commandRef(["worker", "dispatch", loaded.topicId, "--packet", dispatchable.packet.packet_id]);
252
647
  return decision;
@@ -222,7 +222,11 @@ export async function buildBlueprintAuditPayload(projectRoot, options = {}) {
222
222
 
223
223
  const kernelMarkdown = compareFileSets(blueprintFiles.kernelMarkdown, canonicalFiles.kernelMarkdown);
224
224
  const kernelTables = compareFileSets(blueprintFiles.kernelTables, canonicalFiles.kernelTables);
225
- const kernelGenerated = compareFileSets(blueprintFiles.kernelGenerated, canonicalFiles.kernelGenerated);
225
+ const kernelGenerated = {
226
+ present: [],
227
+ missing: [],
228
+ extra: [],
229
+ };
226
230
  const domainGuides = compareFileSets(blueprintFiles.domainGuides, canonicalFiles.domainGuides);
227
231
  const ruleIdPreservation = await compareRuleIds(
228
232
  blueprintAbsoluteRoot,
@@ -234,7 +238,6 @@ export async function buildBlueprintAuditPayload(projectRoot, options = {}) {
234
238
  const ok = missingDomains.length === 0
235
239
  && kernelMarkdown.missing.length === 0
236
240
  && kernelTables.missing.length === 0
237
- && kernelGenerated.missing.length === 0
238
241
  && domainGuides.missing.length === 0
239
242
  && ruleIdPreservation.missingRuleIds.length === 0
240
243
  && ruleIdPreservation.parseErrors.length === 0
@@ -245,9 +248,6 @@ export async function buildBlueprintAuditPayload(projectRoot, options = {}) {
245
248
  if (missingDomains.length > 0 || kernelMarkdown.missing.length > 0 || kernelTables.missing.length > 0 || !indexPresent) {
246
249
  nextSteps.push("Copy the missing blueprint structure into `/.nimi/spec/**` before attempting authority cutover.");
247
250
  }
248
- if (kernelGenerated.missing.length > 0) {
249
- nextSteps.push("Regenerate derived kernel docs after canonical blueprint content is built out.");
250
- }
251
251
  if (domainGuides.missing.length > 0) {
252
252
  nextSteps.push("Thin and map domain guides only after kernel coverage is in place.");
253
253
  }
@@ -42,10 +42,11 @@ function translateCloseoutReason(reason) {
42
42
  ["Non-completed outcomes may be projected as local-only closeout artifacts", "非 completed 的 outcome 可以仅投影为本地 closeout 产物"],
43
43
  ["Completed closeout is not allowed in the current lifecycle state", "当前生命周期状态不允许完成该 closeout"],
44
44
  ["Completed closeout requires declared canonical tree files to be valid", "完成 closeout 需要声明的 canonical tree 文件有效"],
45
- ["Completed closeout requires a valid `.nimi/spec/_meta/spec-generation-audit.yaml` artifact", "完成 closeout 需要一个有效的 `.nimi/spec/_meta/spec-generation-audit.yaml` 产物"],
45
+ ["Completed closeout requires a valid `.nimi/local/state/spec-generation/spec-generation-audit.yaml` artifact", "完成 closeout 需要一个有效的 `.nimi/local/state/spec-generation/spec-generation-audit.yaml` 产物"],
46
46
  ["Completed doc_spec_audit closeout must compare against `.nimi/spec`", "完成 doc_spec_audit closeout 时必须对 `.nimi/spec` 进行比较"],
47
47
  ["Completed high_risk_execution closeout requires canonical admissions truth to remain `.nimi/spec/high-risk-admissions.yaml`", "完成 high_risk_execution closeout 需要 canonical admissions truth 继续落在 `.nimi/spec/high-risk-admissions.yaml`"],
48
48
  ["Completed closeout is consistent with the current canonical tree state", "completed closeout 与当前 canonical tree 状态一致"],
49
+ ["Imported spec_reconstruction summary must match active spec-generation audit coverage", "导入的 spec_reconstruction 摘要必须与当前 spec-generation audit 覆盖情况一致"],
49
50
  ]);
50
51
 
51
52
  if (translations.has(reason)) {
@@ -250,7 +251,8 @@ async function synthesizeSpecReconstructionSummary(projectRoot, doctorResult, ve
250
251
 
251
252
  return {
252
253
  generated_paths: generatedPaths,
253
- audit_ref: ".nimi/spec/_meta/spec-generation-audit.yaml",
254
+ audit_ref: ".nimi/local/state/spec-generation/spec-generation-audit.yaml",
255
+ placement_report_ref: ".nimi/local/state/spec-surface/current-inventory.json",
254
256
  coverage_summary: {
255
257
  complete_files: Math.max(generatedPaths.length - partialFiles - placeholderFiles, 0),
256
258
  partial_files: partialFiles,
@@ -266,6 +268,44 @@ async function synthesizeSpecReconstructionSummary(projectRoot, doctorResult, ve
266
268
  };
267
269
  }
268
270
 
271
+ async function validateSpecReconstructionSummaryAgainstAudit(projectRoot, summary, doctorResult) {
272
+ if (!summary) {
273
+ return { ok: true };
274
+ }
275
+
276
+ const generatedPaths = await collectSpecPaths(path.join(projectRoot, ".nimi", "spec"), ".nimi/spec");
277
+ const auditSummary = doctorResult.specGenerationAudit?.summary ?? {};
278
+ const unresolvedFileCount = Number.isInteger(auditSummary.unresolvedFiles) ? auditSummary.unresolvedFiles : 0;
279
+ const inferredFileCount = Number.isInteger(auditSummary.inferredFiles) ? auditSummary.inferredFiles : 0;
280
+ const placeholderFiles = Number.isInteger(auditSummary.placeholderFiles) ? auditSummary.placeholderFiles : 0;
281
+ const partialFiles = Number.isInteger(auditSummary.partialFiles) ? auditSummary.partialFiles : unresolvedFileCount;
282
+ const shouldBePartial = partialFiles > 0 || unresolvedFileCount > 0 || inferredFileCount > 0;
283
+ const expectedStatus = doctorResult.specGenerationAudit?.ok && !shouldBePartial ? "reconstructed" : "partial";
284
+ const expectedCompleteUpperBound = Math.max(generatedPaths.length - partialFiles - placeholderFiles, 0);
285
+
286
+ const coverageSummary = isPlainObject(summary.coverage_summary) ? summary.coverage_summary : {};
287
+ const summaryPartialFiles = Number.isInteger(coverageSummary.partial_files) ? coverageSummary.partial_files : null;
288
+ const summaryPlaceholderFiles = Number.isInteger(coverageSummary.placeholder_files) ? coverageSummary.placeholder_files : null;
289
+ const summaryCompleteFiles = Number.isInteger(coverageSummary.complete_files) ? coverageSummary.complete_files : null;
290
+ const summaryUnresolvedCount = Number.isInteger(summary.unresolved_file_count) ? summary.unresolved_file_count : null;
291
+ const summaryInferredCount = Number.isInteger(summary.inferred_file_count) ? summary.inferred_file_count : null;
292
+
293
+ const matchesAudit =
294
+ summary.status === expectedStatus &&
295
+ summaryPartialFiles === partialFiles &&
296
+ summaryPlaceholderFiles === placeholderFiles &&
297
+ summaryUnresolvedCount === unresolvedFileCount &&
298
+ summaryInferredCount === inferredFileCount &&
299
+ (summaryCompleteFiles === null || summaryCompleteFiles <= expectedCompleteUpperBound);
300
+
301
+ return matchesAudit
302
+ ? { ok: true }
303
+ : {
304
+ ok: false,
305
+ reason: "Imported spec_reconstruction summary must match active spec-generation audit coverage",
306
+ };
307
+ }
308
+
269
309
  function evaluateCloseoutReadiness(skillId, outcome, doctorResult, summary) {
270
310
  if (outcome !== "completed") {
271
311
  if (!doctorResult.ok || !doctorResult.handoffReadiness.ok) {
@@ -280,6 +320,41 @@ function evaluateCloseoutReadiness(skillId, outcome, doctorResult, summary) {
280
320
  };
281
321
  }
282
322
 
323
+ const usesV2SurfaceModel = doctorResult.specGenerationInputs?.mode === "class_filtered";
324
+ if (usesV2SurfaceModel && (doctorResult.commandGating?.entries ?? []).length === 0) {
325
+ if (!doctorResult.ok || !doctorResult.handoffReadiness.ok) {
326
+ return {
327
+ ok: false,
328
+ reason: "Bootstrap or handoff validation is failing; repair doctor errors before projecting closeout results",
329
+ };
330
+ }
331
+ if (doctorResult.canonicalTree?.requiredFilesValid !== true) {
332
+ return {
333
+ ok: false,
334
+ reason: "Completed closeout requires declared canonical tree files to be valid",
335
+ };
336
+ }
337
+ if (doctorResult.specGenerationAudit?.ok !== true) {
338
+ return {
339
+ ok: false,
340
+ reason: "Completed closeout requires a valid `.nimi/local/state/spec-generation/spec-generation-audit.yaml` artifact",
341
+ };
342
+ }
343
+ if (skillId === "doc_spec_audit") {
344
+ const comparedPaths = Array.isArray(summary?.compared_paths) ? summary.compared_paths : [];
345
+ if (!comparedPaths.includes(".nimi/spec")) {
346
+ return {
347
+ ok: false,
348
+ reason: "Completed doc_spec_audit closeout must compare against `.nimi/spec`",
349
+ };
350
+ }
351
+ }
352
+ return {
353
+ ok: true,
354
+ reason: "Completed closeout is consistent with the current canonical tree state",
355
+ };
356
+ }
357
+
283
358
  const rule = (doctorResult.commandGating?.entries ?? []).find((entry) => entry.command === "closeout" && entry.skill === skillId) ?? null;
284
359
  if (!rule?.completedRequires) {
285
360
  return {
@@ -306,7 +381,7 @@ function evaluateCloseoutReadiness(skillId, outcome, doctorResult, summary) {
306
381
  if (rule.completedRequires.spec_generation_audit_valid === true && doctorResult.specGenerationAudit?.ok !== true) {
307
382
  return {
308
383
  ok: false,
309
- reason: "Completed closeout requires a valid `.nimi/spec/_meta/spec-generation-audit.yaml` artifact",
384
+ reason: "Completed closeout requires a valid `.nimi/local/state/spec-generation/spec-generation-audit.yaml` artifact",
310
385
  };
311
386
  }
312
387
 
@@ -520,6 +595,7 @@ export async function buildCloseoutPayload(projectRoot, options) {
520
595
  ok: false,
521
596
  exitCode: 2,
522
597
  inputError: true,
598
+ readiness: null,
523
599
  error: `${localize(
524
600
  `nimicoding closeout refused: ${summaryValidation.reason}.`,
525
601
  `nimicoding closeout 已拒绝:${translateCloseoutReason(summaryValidation.reason)}。`,
@@ -537,6 +613,7 @@ export async function buildCloseoutPayload(projectRoot, options) {
537
613
  ok: false,
538
614
  exitCode: 2,
539
615
  inputError: true,
616
+ readiness: null,
540
617
  error: `${localize(
541
618
  `nimicoding closeout refused: ${statusConsistency.reason}.`,
542
619
  `nimicoding closeout 已拒绝:${translateCloseoutReason(statusConsistency.reason)}。`,
@@ -544,6 +621,52 @@ export async function buildCloseoutPayload(projectRoot, options) {
544
621
  };
545
622
  }
546
623
 
624
+ if (options.skill === "spec_reconstruction" && options.outcome === "completed") {
625
+ // Canonical tree must be intact before audit-coverage comparison; a
626
+ // missing domain kernel file is a readiness fail with its own reason.
627
+ if (doctorResult.canonicalTree?.requiredFilesValid !== true) {
628
+ const reason = "Completed closeout requires declared canonical tree files to be valid";
629
+ return {
630
+ ok: false,
631
+ exitCode: 1,
632
+ readiness: { ok: false, reason },
633
+ error: `${localize(
634
+ `nimicoding closeout refused: ${reason}.`,
635
+ `nimicoding closeout 已拒绝:${translateCloseoutReason(reason)}。`,
636
+ )}\n`,
637
+ };
638
+ }
639
+ if (doctorResult.specGenerationAudit?.ok !== true) {
640
+ const reason = "Completed closeout requires a valid `.nimi/local/state/spec-generation/spec-generation-audit.yaml` artifact";
641
+ return {
642
+ ok: false,
643
+ exitCode: 1,
644
+ readiness: { ok: false, reason },
645
+ error: `${localize(
646
+ `nimicoding closeout refused: ${reason}.`,
647
+ `nimicoding closeout 已拒绝:${translateCloseoutReason(reason)}。`,
648
+ )}\n`,
649
+ };
650
+ }
651
+ const auditConsistency = await validateSpecReconstructionSummaryAgainstAudit(
652
+ projectRoot,
653
+ effectiveSummary,
654
+ doctorResult,
655
+ );
656
+ if (!auditConsistency.ok) {
657
+ return {
658
+ ok: false,
659
+ exitCode: 2,
660
+ inputError: true,
661
+ readiness: null,
662
+ error: `${localize(
663
+ `nimicoding closeout refused: ${auditConsistency.reason}.`,
664
+ `nimicoding closeout 已拒绝:${translateCloseoutReason(auditConsistency.reason)}。`,
665
+ )}\n`,
666
+ };
667
+ }
668
+ }
669
+
547
670
  const readiness = evaluateCloseoutReadiness(options.skill, options.outcome, doctorResult, effectiveSummary);
548
671
  const localArtifactPath = path.join(projectRoot, ".nimi", "local", "handoff-results", `${options.skill}.json`);
549
672
  const payload = {