@nimiplatform/nimi-coding 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/CODE_OF_CONDUCT.md +28 -0
- package/CONTRIBUTING.md +45 -0
- package/README.md +371 -344
- package/README.zh-CN.md +307 -0
- package/SECURITY.md +26 -0
- package/adapters/oh-my-codex/README.md +8 -9
- package/cli/commands/audit-sweep.mjs +10 -10
- package/cli/commands/classify-spec-tree.mjs +5 -0
- package/cli/commands/closeout.mjs +3 -0
- package/cli/commands/generate-spec-derived-docs.mjs +20 -0
- package/cli/commands/generate-spec-migration-plan.mjs +30 -0
- package/cli/commands/start.mjs +5 -1
- package/cli/commands/surface-validator-command.mjs +49 -0
- package/cli/commands/sweep-design.mjs +295 -0
- package/cli/commands/sweep.mjs +22 -0
- package/cli/commands/sync.mjs +132 -0
- package/cli/commands/topic-formatters.mjs +8 -8
- package/cli/commands/validate-ai-governance.mjs +167 -46
- package/cli/commands/validate-domain-admission.mjs +5 -0
- package/cli/commands/validate-guidance-bodies.mjs +5 -0
- package/cli/commands/validate-placement.mjs +5 -0
- package/cli/commands/validate-projection-edges.mjs +5 -0
- package/cli/commands/validate-spec-audit.mjs +5 -1
- package/cli/commands/validate-table-family.mjs +5 -0
- package/cli/commands/validate-tracked-output-admission.mjs +5 -0
- package/cli/constants.mjs +5 -49
- package/cli/help.mjs +33 -11
- package/cli/index.mjs +20 -2
- package/cli/lib/audit-sweep-runtime/admissions.mjs +38 -29
- package/cli/lib/audit-sweep-runtime/audit-validity.mjs +8 -0
- package/cli/lib/audit-sweep-runtime/chunks.mjs +11 -11
- package/cli/lib/audit-sweep-runtime/closeout.mjs +8 -8
- package/cli/lib/audit-sweep-runtime/codex-auditor-evidence.mjs +3 -3
- package/cli/lib/audit-sweep-runtime/codex-auditor.mjs +10 -10
- package/cli/lib/audit-sweep-runtime/common.mjs +7 -7
- package/cli/lib/audit-sweep-runtime/format.mjs +3 -3
- package/cli/lib/audit-sweep-runtime/ingest.mjs +8 -8
- package/cli/lib/audit-sweep-runtime/inventory-spec-chunks.mjs +24 -27
- package/cli/lib/audit-sweep-runtime/inventory.mjs +58 -18
- package/cli/lib/audit-sweep-runtime/ledger.mjs +1 -1
- package/cli/lib/audit-sweep-runtime/p0p1-profile.mjs +2 -2
- package/cli/lib/audit-sweep-runtime/remediation.mjs +6 -6
- package/cli/lib/audit-sweep-runtime/rerun.mjs +6 -6
- package/cli/lib/audit-sweep-runtime/status.mjs +1 -1
- package/cli/lib/audit-sweep-runtime/validators.mjs +2 -2
- package/cli/lib/authority-convergence.mjs +397 -2
- package/cli/lib/blueprint-audit.mjs +5 -5
- package/cli/lib/closeout.mjs +126 -3
- package/cli/lib/contracts.mjs +21 -17
- package/cli/lib/handoff.mjs +29 -11
- package/cli/lib/high-risk-admission.mjs +60 -11
- package/cli/lib/high-risk-decision.mjs +31 -2
- package/cli/lib/high-risk-ingest.mjs +5 -1
- package/cli/lib/high-risk-review.mjs +5 -1
- package/cli/lib/internal/contracts-parse.mjs +195 -24
- package/cli/lib/internal/contracts-validators.mjs +3 -2
- package/cli/lib/internal/doctor-bootstrap-surface.mjs +82 -35
- package/cli/lib/internal/doctor-delegated-surface.mjs +1 -1
- package/cli/lib/internal/doctor-finalize.mjs +12 -8
- package/cli/lib/internal/doctor-inspectors.mjs +34 -1
- package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +74 -12
- package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +24 -6
- package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +18 -23
- package/cli/lib/internal/surface-taxonomy-validators.mjs +931 -0
- package/cli/lib/internal/validators-spec.mjs +229 -20
- package/cli/lib/sweep-design-runtime/common.mjs +246 -0
- package/cli/lib/sweep-design-runtime/engine.mjs +733 -0
- package/cli/lib/sweep-design-runtime/fix-topic.mjs +414 -0
- package/cli/lib/sweep-design-runtime/lifecycle.mjs +54 -0
- package/cli/lib/sweep-design-runtime/results.mjs +324 -0
- package/cli/lib/sweep-design.mjs +8 -0
- package/cli/lib/sync.mjs +143 -0
- package/cli/lib/topic-artifacts.mjs +186 -0
- package/cli/lib/topic-authority-coverage.mjs +73 -0
- package/cli/lib/topic-closeout.mjs +560 -0
- package/cli/lib/topic-common.mjs +404 -0
- package/cli/lib/topic-decisions.mjs +332 -0
- package/cli/lib/topic-draft-packets.mjs +126 -7
- package/cli/lib/topic-execution.mjs +515 -0
- package/cli/lib/topic-goal.mjs +112 -33
- package/cli/lib/topic-ledger.mjs +281 -0
- package/cli/lib/topic-lifecycle-artifacts.mjs +173 -0
- package/cli/lib/topic-root-validation.mjs +288 -0
- package/cli/lib/topic-runner-commands.mjs +174 -0
- package/cli/lib/topic-runner-deferral.mjs +532 -0
- package/cli/lib/topic-runner-stale-gates.mjs +114 -0
- package/cli/lib/topic-runner-validation.mjs +138 -0
- package/cli/lib/topic-runner.mjs +109 -154
- package/cli/lib/topic-scaffold.mjs +252 -0
- package/cli/lib/topic-waves.mjs +403 -0
- package/cli/lib/topic.mjs +81 -93
- package/cli/lib/value-helpers.mjs +6 -1
- package/cli/seeds/bootstrap.mjs +96 -20
- package/cli/seeds/seed-policy.yaml +67 -0
- package/config/bootstrap.yaml +1 -1
- package/config/skill-manifest.yaml +4 -2
- package/config/spec-generation-inputs.yaml +41 -19
- package/contracts/audit-remediation-map.schema.yaml +1 -0
- package/contracts/audit-sweep-result.yaml +4 -0
- package/contracts/domain-admission.schema.yaml +56 -0
- package/contracts/migration-inventory.schema.yaml +80 -0
- package/contracts/negative-fixtures.yaml +91 -0
- package/contracts/placement-contract.schema.yaml +163 -0
- package/contracts/projection-edge.schema.yaml +130 -0
- package/contracts/shared-enums.yaml +68 -0
- package/contracts/spec-generation-audit.schema.yaml +19 -4
- package/contracts/spec-generation-inputs.schema.yaml +130 -29
- package/contracts/spec-reconstruction-result.yaml +9 -5
- package/contracts/surface-taxonomy.schema.yaml +201 -0
- package/contracts/sweep-design-result.yaml +349 -0
- package/contracts/table-family.schema.yaml +121 -0
- package/contracts/topic-goal.schema.yaml +10 -1
- package/contracts/tracked-output-admission.schema.yaml +70 -0
- package/contracts/workflow-consumer.schema.yaml +112 -0
- package/methodology/audit-sweep-p0p1-recall.yaml +1 -1
- package/methodology/spec-reconstruction.yaml +53 -30
- package/package.json +19 -4
- package/spec/_meta/command-gating-matrix.yaml +33 -0
- package/spec/_meta/generate-drift-migration-checklist.yaml +44 -62
- package/spec/_meta/governance-routing-cutover-checklist.yaml +3 -3
- package/spec/_meta/phase2-impacted-surface-matrix.yaml +14 -14
- package/spec/_meta/spec-authority-cutover-readiness.yaml +3 -5
- package/spec/_meta/spec-tree-model.yaml +104 -36
- package/spec/bootstrap-state.yaml +36 -36
- package/spec/product-scope.yaml +13 -10
|
@@ -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
|
-
|
|
85
|
-
|
|
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 =
|
|
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
|
}
|
package/cli/lib/closeout.mjs
CHANGED
|
@@ -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/
|
|
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/
|
|
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/
|
|
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 = {
|