@nimiplatform/nimi-coding 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/CODE_OF_CONDUCT.md +28 -0
- package/CONTRIBUTING.md +45 -0
- package/README.md +371 -344
- package/README.zh-CN.md +307 -0
- package/SECURITY.md +26 -0
- package/adapters/oh-my-codex/README.md +8 -9
- package/cli/commands/audit-sweep.mjs +10 -10
- package/cli/commands/classify-spec-tree.mjs +5 -0
- package/cli/commands/closeout.mjs +3 -0
- package/cli/commands/generate-spec-derived-docs.mjs +20 -0
- package/cli/commands/generate-spec-migration-plan.mjs +30 -0
- package/cli/commands/start.mjs +5 -1
- package/cli/commands/surface-validator-command.mjs +49 -0
- package/cli/commands/sweep-design.mjs +295 -0
- package/cli/commands/sweep.mjs +22 -0
- package/cli/commands/sync.mjs +132 -0
- package/cli/commands/topic-formatters.mjs +8 -8
- package/cli/commands/validate-ai-governance.mjs +167 -46
- package/cli/commands/validate-domain-admission.mjs +5 -0
- package/cli/commands/validate-guidance-bodies.mjs +5 -0
- package/cli/commands/validate-placement.mjs +5 -0
- package/cli/commands/validate-projection-edges.mjs +5 -0
- package/cli/commands/validate-spec-audit.mjs +5 -1
- package/cli/commands/validate-table-family.mjs +5 -0
- package/cli/commands/validate-tracked-output-admission.mjs +5 -0
- package/cli/constants.mjs +5 -49
- package/cli/help.mjs +33 -11
- package/cli/index.mjs +20 -2
- package/cli/lib/audit-sweep-runtime/admissions.mjs +38 -29
- package/cli/lib/audit-sweep-runtime/audit-validity.mjs +8 -0
- package/cli/lib/audit-sweep-runtime/chunks.mjs +11 -11
- package/cli/lib/audit-sweep-runtime/closeout.mjs +8 -8
- package/cli/lib/audit-sweep-runtime/codex-auditor-evidence.mjs +3 -3
- package/cli/lib/audit-sweep-runtime/codex-auditor.mjs +10 -10
- package/cli/lib/audit-sweep-runtime/common.mjs +7 -7
- package/cli/lib/audit-sweep-runtime/format.mjs +3 -3
- package/cli/lib/audit-sweep-runtime/ingest.mjs +8 -8
- package/cli/lib/audit-sweep-runtime/inventory-spec-chunks.mjs +24 -27
- package/cli/lib/audit-sweep-runtime/inventory.mjs +58 -18
- package/cli/lib/audit-sweep-runtime/ledger.mjs +1 -1
- package/cli/lib/audit-sweep-runtime/p0p1-profile.mjs +2 -2
- package/cli/lib/audit-sweep-runtime/remediation.mjs +6 -6
- package/cli/lib/audit-sweep-runtime/rerun.mjs +6 -6
- package/cli/lib/audit-sweep-runtime/status.mjs +1 -1
- package/cli/lib/audit-sweep-runtime/validators.mjs +2 -2
- package/cli/lib/authority-convergence.mjs +397 -2
- package/cli/lib/blueprint-audit.mjs +5 -5
- package/cli/lib/closeout.mjs +126 -3
- package/cli/lib/contracts.mjs +21 -17
- package/cli/lib/handoff.mjs +29 -11
- package/cli/lib/high-risk-admission.mjs +60 -11
- package/cli/lib/high-risk-decision.mjs +31 -2
- package/cli/lib/high-risk-ingest.mjs +5 -1
- package/cli/lib/high-risk-review.mjs +5 -1
- package/cli/lib/internal/contracts-parse.mjs +195 -24
- package/cli/lib/internal/contracts-validators.mjs +3 -2
- package/cli/lib/internal/doctor-bootstrap-surface.mjs +82 -35
- package/cli/lib/internal/doctor-delegated-surface.mjs +1 -1
- package/cli/lib/internal/doctor-finalize.mjs +12 -8
- package/cli/lib/internal/doctor-inspectors.mjs +34 -1
- package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +74 -12
- package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +24 -6
- package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +18 -23
- package/cli/lib/internal/surface-taxonomy-validators.mjs +931 -0
- package/cli/lib/internal/validators-spec.mjs +229 -20
- package/cli/lib/sweep-design-runtime/common.mjs +246 -0
- package/cli/lib/sweep-design-runtime/engine.mjs +733 -0
- package/cli/lib/sweep-design-runtime/fix-topic.mjs +414 -0
- package/cli/lib/sweep-design-runtime/lifecycle.mjs +54 -0
- package/cli/lib/sweep-design-runtime/results.mjs +324 -0
- package/cli/lib/sweep-design.mjs +8 -0
- package/cli/lib/sync.mjs +143 -0
- package/cli/lib/topic-artifacts.mjs +186 -0
- package/cli/lib/topic-authority-coverage.mjs +73 -0
- package/cli/lib/topic-closeout.mjs +560 -0
- package/cli/lib/topic-common.mjs +404 -0
- package/cli/lib/topic-decisions.mjs +332 -0
- package/cli/lib/topic-draft-packets.mjs +126 -7
- package/cli/lib/topic-execution.mjs +515 -0
- package/cli/lib/topic-goal.mjs +112 -33
- package/cli/lib/topic-ledger.mjs +281 -0
- package/cli/lib/topic-lifecycle-artifacts.mjs +173 -0
- package/cli/lib/topic-root-validation.mjs +288 -0
- package/cli/lib/topic-runner-commands.mjs +174 -0
- package/cli/lib/topic-runner-deferral.mjs +532 -0
- package/cli/lib/topic-runner-stale-gates.mjs +114 -0
- package/cli/lib/topic-runner-validation.mjs +138 -0
- package/cli/lib/topic-runner.mjs +109 -154
- package/cli/lib/topic-scaffold.mjs +252 -0
- package/cli/lib/topic-waves.mjs +403 -0
- package/cli/lib/topic.mjs +81 -93
- package/cli/lib/value-helpers.mjs +6 -1
- package/cli/seeds/bootstrap.mjs +96 -20
- package/cli/seeds/seed-policy.yaml +67 -0
- package/config/bootstrap.yaml +1 -1
- package/config/skill-manifest.yaml +4 -2
- package/config/spec-generation-inputs.yaml +41 -19
- package/contracts/audit-remediation-map.schema.yaml +1 -0
- package/contracts/audit-sweep-result.yaml +4 -0
- package/contracts/domain-admission.schema.yaml +56 -0
- package/contracts/migration-inventory.schema.yaml +80 -0
- package/contracts/negative-fixtures.yaml +91 -0
- package/contracts/placement-contract.schema.yaml +163 -0
- package/contracts/projection-edge.schema.yaml +130 -0
- package/contracts/shared-enums.yaml +68 -0
- package/contracts/spec-generation-audit.schema.yaml +19 -4
- package/contracts/spec-generation-inputs.schema.yaml +130 -29
- package/contracts/spec-reconstruction-result.yaml +9 -5
- package/contracts/surface-taxonomy.schema.yaml +201 -0
- package/contracts/sweep-design-result.yaml +349 -0
- package/contracts/table-family.schema.yaml +121 -0
- package/contracts/topic-goal.schema.yaml +10 -1
- package/contracts/tracked-output-admission.schema.yaml +70 -0
- package/contracts/workflow-consumer.schema.yaml +112 -0
- package/methodology/audit-sweep-p0p1-recall.yaml +1 -1
- package/methodology/spec-reconstruction.yaml +53 -30
- package/package.json +19 -4
- package/spec/_meta/command-gating-matrix.yaml +33 -0
- package/spec/_meta/generate-drift-migration-checklist.yaml +44 -62
- package/spec/_meta/governance-routing-cutover-checklist.yaml +3 -3
- package/spec/_meta/phase2-impacted-surface-matrix.yaml +14 -14
- package/spec/_meta/spec-authority-cutover-readiness.yaml +3 -5
- package/spec/_meta/spec-tree-model.yaml +104 -36
- package/spec/bootstrap-state.yaml +36 -36
- package/spec/product-scope.yaml +13 -10
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
function stringArray(value) {
|
|
2
|
+
if (typeof value === "string" && value.trim()) return [value.trim()];
|
|
3
|
+
if (!Array.isArray(value)) return [];
|
|
4
|
+
return value
|
|
5
|
+
.filter((entry) => typeof entry === "string")
|
|
6
|
+
.map((entry) => entry.trim())
|
|
7
|
+
.filter(Boolean);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isAuthorityRef(value) {
|
|
11
|
+
const ref = String(value ?? "").trim();
|
|
12
|
+
if (!ref) return false;
|
|
13
|
+
return (
|
|
14
|
+
ref.startsWith(".nimi/spec/")
|
|
15
|
+
|| ref.startsWith("apps/") && ref.includes("/spec/")
|
|
16
|
+
|| ref.startsWith("package://@nimiplatform/nimi-coding/spec/")
|
|
17
|
+
|| ref.startsWith("package://@nimiplatform/nimi-coding/contracts/")
|
|
18
|
+
|| ref.startsWith("package://@nimiplatform/nimi-coding/methodology/")
|
|
19
|
+
|| ref.startsWith("package://@nimiplatform/nimi-coding/config/")
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function unique(values) {
|
|
24
|
+
return [...new Set(values.filter(Boolean))];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sourceSweepDesignAuthorityRefs(source) {
|
|
28
|
+
if (!source || typeof source !== "object") return [];
|
|
29
|
+
return unique([
|
|
30
|
+
...stringArray(source.authority_owner),
|
|
31
|
+
...stringArray(source.authority_refs),
|
|
32
|
+
...stringArray(source.authority_refs_considered),
|
|
33
|
+
...stringArray(source.merged_root_cause_keys).filter(isAuthorityRef),
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function sweepDesignWaveAuthorityRefs(wave) {
|
|
38
|
+
if (!wave || typeof wave !== "object") return [];
|
|
39
|
+
return sourceSweepDesignAuthorityRefs(wave);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function packetAuthorityCoverage(packet, wave) {
|
|
43
|
+
const required = sourceSweepDesignAuthorityRefs(wave?.source_sweep_design);
|
|
44
|
+
if (required.length === 0) {
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
requiredAuthorityRefs: [],
|
|
48
|
+
missingAuthorityOwnerRefs: [],
|
|
49
|
+
missingCanonicalSeamRefs: [],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const owners = new Set(stringArray(packet?.authority_owner));
|
|
53
|
+
const seams = new Set(stringArray(packet?.canonical_seams));
|
|
54
|
+
const missingAuthorityOwnerRefs = required.filter((ref) => !owners.has(ref));
|
|
55
|
+
const missingCanonicalSeamRefs = required.filter((ref) => !seams.has(ref));
|
|
56
|
+
return {
|
|
57
|
+
ok: missingAuthorityOwnerRefs.length === 0 && missingCanonicalSeamRefs.length === 0,
|
|
58
|
+
requiredAuthorityRefs: required,
|
|
59
|
+
missingAuthorityOwnerRefs,
|
|
60
|
+
missingCanonicalSeamRefs,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function packetAuthorityCoverageError(coverage) {
|
|
65
|
+
const parts = [];
|
|
66
|
+
if (coverage.missingAuthorityOwnerRefs?.length) {
|
|
67
|
+
parts.push(`authority_owner missing ${coverage.missingAuthorityOwnerRefs.join(", ")}`);
|
|
68
|
+
}
|
|
69
|
+
if (coverage.missingCanonicalSeamRefs?.length) {
|
|
70
|
+
parts.push(`canonical_seams missing ${coverage.missingCanonicalSeamRefs.join(", ")}`);
|
|
71
|
+
}
|
|
72
|
+
return `Draft packet authority coverage is incomplete: ${parts.join("; ")}`;
|
|
73
|
+
}
|
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import { readdir, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
|
|
5
|
+
import { readTextIfFile } from "./fs-helpers.mjs";
|
|
6
|
+
import { loadTopicRuntimeAuthority, toPortableRelativePath } from "./topic-common.mjs";
|
|
7
|
+
import {
|
|
8
|
+
buildTopicNow,
|
|
9
|
+
getTopicWaves,
|
|
10
|
+
loadTopicReport,
|
|
11
|
+
moveTopicDirectoryForState,
|
|
12
|
+
topicHasEnrichedShape,
|
|
13
|
+
writeTopicYaml,
|
|
14
|
+
} from "./topic-scaffold.mjs";
|
|
15
|
+
import {
|
|
16
|
+
listWavePackets,
|
|
17
|
+
listWaveResults,
|
|
18
|
+
pendingNoteFilename,
|
|
19
|
+
pendingNoteMarkdown,
|
|
20
|
+
readFrontmatterObject,
|
|
21
|
+
topicCloseoutFilename,
|
|
22
|
+
topicTrueCloseAuditFilename,
|
|
23
|
+
topicTrueCloseJudgementFilename,
|
|
24
|
+
topicTrueCloseRecordFilename,
|
|
25
|
+
waveCloseoutFilename,
|
|
26
|
+
} from "./topic-artifacts.mjs";
|
|
27
|
+
import { collectWaveArtifactEvidence, loadPendingNote, validateWaveId } from "./topic-waves.mjs";
|
|
28
|
+
|
|
29
|
+
export function closeoutMarkdown(closeout, title) {
|
|
30
|
+
return `---
|
|
31
|
+
${YAML.stringify(closeout).trimEnd()}
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
# ${title}
|
|
35
|
+
|
|
36
|
+
Recorded by \`nimicoding topic closeout\`.
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
export function trueCloseAuditMarkdown(audit, judgementText) {
|
|
40
|
+
return `---
|
|
41
|
+
${YAML.stringify(audit).trimEnd()}
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
# Topic True-Close Audit
|
|
45
|
+
|
|
46
|
+
${judgementText}
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
export function trueCloseRecordMarkdown(record) {
|
|
50
|
+
return `---
|
|
51
|
+
${YAML.stringify(record).trimEnd()}
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
# Topic True-Close
|
|
55
|
+
|
|
56
|
+
Recorded by \`nimicoding topic closeout topic\`.
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
async function collectActiveDeferredBlockers(topicDir) {
|
|
60
|
+
const entries = await readdir(topicDir, { withFileTypes: true }),
|
|
61
|
+
blockers = [];
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (!entry.isFile() || !entry.name.startsWith("deferred-blocker-") || !entry.name.endsWith(".md"))
|
|
64
|
+
continue;
|
|
65
|
+
const blockerPath = path.join(topicDir, entry.name),
|
|
66
|
+
blockerText = await readTextIfFile(blockerPath),
|
|
67
|
+
blocker = readFrontmatterObject(blockerText ?? "");
|
|
68
|
+
if (!blocker || !["resolved", "closed"].includes(blocker.status)) {
|
|
69
|
+
blockers.push(entry.name);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return blockers.sort();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function packetRequiresPlacementCloseoutEvidence(packet) {
|
|
76
|
+
const searchableFields = [
|
|
77
|
+
...(Array.isArray(packet?.authority_owner) ? packet.authority_owner : []),
|
|
78
|
+
...(Array.isArray(packet?.canonical_seams) ? packet.canonical_seams : []),
|
|
79
|
+
...(Array.isArray(packet?.acceptance_invariants) ? packet.acceptance_invariants : []),
|
|
80
|
+
...(Array.isArray(packet?.negative_tests) ? packet.negative_tests : []),
|
|
81
|
+
...(Array.isArray(packet?.reopen_conditions) ? packet.reopen_conditions : []),
|
|
82
|
+
].join("\n");
|
|
83
|
+
return /placement|surface[-_ ]taxonomy|surface[-_ ]class|classify-spec-tree|validate-placement|closeout_drift_resistance_requires_placement_report/i.test(
|
|
84
|
+
searchableFields,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function waveHasPlacementReportEvidence(projectRoot, topicDir, waveId) {
|
|
89
|
+
const packets = await listWavePackets(topicDir, waveId);
|
|
90
|
+
if (!packets.some(({ packet }) => packetRequiresPlacementCloseoutEvidence(packet))) {
|
|
91
|
+
return { required: false, found: false };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const results = await listWaveResults(topicDir, waveId);
|
|
95
|
+
const evidenceTexts = [];
|
|
96
|
+
for (const { result, resultPath } of results) {
|
|
97
|
+
const resultText = await readTextIfFile(resultPath);
|
|
98
|
+
evidenceTexts.push(resultText ?? "");
|
|
99
|
+
if (typeof result?.source_ref === "string" && result.source_ref.length > 0) {
|
|
100
|
+
const sourcePath = path.resolve(projectRoot, result.source_ref);
|
|
101
|
+
const sourceText = await readTextIfFile(sourcePath);
|
|
102
|
+
if (sourceText !== null) {
|
|
103
|
+
evidenceTexts.push(sourceText);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const hasReport = evidenceTexts.some((text) =>
|
|
109
|
+
/nimicoding\.surface-validator-result\.v1|nimicoding\.spec-migration-plan\.v1|classify-spec-tree|generate-spec-migration-plan|validate-placement|validate-table-family|validate-projection-edges|validate-guidance-bodies|validate-domain-admission|validate-tracked-output-admission/i.test(
|
|
110
|
+
text,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
return { required: true, found: hasReport };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function buildWaveClosureChecks(projectRoot, topicDir, topic, wave, closeout) {
|
|
117
|
+
const authority = await loadTopicRuntimeAuthority(projectRoot),
|
|
118
|
+
evidence = await collectWaveArtifactEvidence(topicDir, wave.wave_id),
|
|
119
|
+
placementEvidence = closeout.drift_resistance_closure === "closed"
|
|
120
|
+
? await waveHasPlacementReportEvidence(projectRoot, topicDir, wave.wave_id)
|
|
121
|
+
: { required: false, found: false },
|
|
122
|
+
checks = [];
|
|
123
|
+
(checks.push({
|
|
124
|
+
id: "closeout_scope_wave",
|
|
125
|
+
ok: closeout.scope === "wave" && authority.closeoutScopes.includes(closeout.scope),
|
|
126
|
+
reason:
|
|
127
|
+
closeout.scope === "wave"
|
|
128
|
+
? "closeout scope is wave"
|
|
129
|
+
: `closeout scope must be wave, found ${closeout.scope ?? "missing"}`,
|
|
130
|
+
}),
|
|
131
|
+
checks.push({
|
|
132
|
+
id: "closeout_topic_matches",
|
|
133
|
+
ok: closeout.topic_id === topic.topic_id,
|
|
134
|
+
reason:
|
|
135
|
+
closeout.topic_id === topic.topic_id
|
|
136
|
+
? "closeout topic_id matches the topic"
|
|
137
|
+
: `closeout topic_id does not match topic (${closeout.topic_id ?? "missing"} vs ${topic.topic_id})`,
|
|
138
|
+
}));
|
|
139
|
+
const closurePairs = authority.closureDimensions.map((dimension) => [
|
|
140
|
+
`${dimension}_closure`,
|
|
141
|
+
closeout[`${dimension}_closure`],
|
|
142
|
+
]);
|
|
143
|
+
for (const [field, value] of closurePairs)
|
|
144
|
+
checks.push({
|
|
145
|
+
id: `${field}_explicit_closed`,
|
|
146
|
+
ok: value === "closed" && authority.closureStates.includes(value),
|
|
147
|
+
reason:
|
|
148
|
+
value === "closed"
|
|
149
|
+
? `${field} is explicitly closed`
|
|
150
|
+
: `${field} must be closed for wave closeout, found ${value ?? "missing"}`,
|
|
151
|
+
});
|
|
152
|
+
checks.push({
|
|
153
|
+
id: "closeout_disposition_complete",
|
|
154
|
+
ok:
|
|
155
|
+
closeout.disposition === "complete" &&
|
|
156
|
+
authority.closeoutDispositions.includes(closeout.disposition),
|
|
157
|
+
reason:
|
|
158
|
+
closeout.disposition === "complete"
|
|
159
|
+
? "closeout disposition is complete"
|
|
160
|
+
: `closeout disposition must be complete for wave closeout, found ${closeout.disposition ?? "missing"}`,
|
|
161
|
+
});
|
|
162
|
+
if (placementEvidence.required) {
|
|
163
|
+
checks.push({
|
|
164
|
+
id: "drift_resistance_has_placement_report",
|
|
165
|
+
ok: placementEvidence.found,
|
|
166
|
+
reason: placementEvidence.found
|
|
167
|
+
? "drift-resistance closure has recorded placement validation evidence"
|
|
168
|
+
: "drift-resistance closure requires recorded placement validation evidence for this packet",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const activeBlockers = ["needs_revision", "overflowed", "continuation_packet_open"].includes(
|
|
172
|
+
wave.state,
|
|
173
|
+
);
|
|
174
|
+
checks.push({
|
|
175
|
+
id: "wave_has_no_active_blockers",
|
|
176
|
+
ok: !activeBlockers,
|
|
177
|
+
reason: activeBlockers
|
|
178
|
+
? `wave remains in an active blocker state: ${wave.state}`
|
|
179
|
+
: "wave has no active blocker state",
|
|
180
|
+
});
|
|
181
|
+
const closeableState = ["implementation_active", "preflight_admitted", "closed"].includes(
|
|
182
|
+
wave.state,
|
|
183
|
+
);
|
|
184
|
+
return (
|
|
185
|
+
checks.push({
|
|
186
|
+
id: "wave_state_closeable",
|
|
187
|
+
ok: closeableState,
|
|
188
|
+
reason: closeableState
|
|
189
|
+
? "wave state remains eligible for closeout"
|
|
190
|
+
: `wave closeout requires implementation_active, preflight_admitted, or closed, found ${wave.state}`,
|
|
191
|
+
}),
|
|
192
|
+
authority.waveCloseoutEvidence.requirePacketLineage &&
|
|
193
|
+
checks.push({
|
|
194
|
+
id: "wave_packet_lineage_exists",
|
|
195
|
+
ok: evidence.packetRefs.length > 0,
|
|
196
|
+
reason:
|
|
197
|
+
evidence.packetRefs.length > 0
|
|
198
|
+
? "wave closeout has packet lineage evidence"
|
|
199
|
+
: `wave closeout requires packet lineage evidence for ${wave.wave_id}`,
|
|
200
|
+
}),
|
|
201
|
+
authority.waveCloseoutEvidence.requireResultLineage &&
|
|
202
|
+
checks.push({
|
|
203
|
+
id: "wave_result_lineage_exists",
|
|
204
|
+
ok: evidence.resultRefs.length > 0,
|
|
205
|
+
reason:
|
|
206
|
+
evidence.resultRefs.length > 0
|
|
207
|
+
? "wave closeout has result lineage evidence"
|
|
208
|
+
: `wave closeout requires result lineage evidence for ${wave.wave_id}`,
|
|
209
|
+
}),
|
|
210
|
+
checks
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
export async function validateWaveClosure(projectRoot, input, waveId) {
|
|
214
|
+
const loaded = await loadTopicReport(projectRoot, input);
|
|
215
|
+
if (!loaded.ok) return { ok: false, error: loaded.error, checks: [], warnings: [] };
|
|
216
|
+
const { validateTopicRoot } = await import("./topic-root-validation.mjs");
|
|
217
|
+
const rootValidation = await validateTopicRoot(projectRoot, input),
|
|
218
|
+
wave = getTopicWaves(loaded.topic).find((entry) => entry.wave_id === waveId) ?? null,
|
|
219
|
+
checks = [...(rootValidation.checks ?? [])],
|
|
220
|
+
warnings = [...(rootValidation.warnings ?? [])];
|
|
221
|
+
if (
|
|
222
|
+
(checks.push({
|
|
223
|
+
id: "wave_exists",
|
|
224
|
+
ok: wave !== null,
|
|
225
|
+
reason: wave ? "wave exists in topic.yaml waves[]" : `wave does not exist: ${waveId}`,
|
|
226
|
+
}),
|
|
227
|
+
!wave)
|
|
228
|
+
)
|
|
229
|
+
return { ...rootValidation, ok: false, checks, warnings };
|
|
230
|
+
const closeoutPath = path.join(loaded.topicDir, waveCloseoutFilename(waveId)),
|
|
231
|
+
closeoutText = await readTextIfFile(closeoutPath);
|
|
232
|
+
if (
|
|
233
|
+
(checks.push({
|
|
234
|
+
id: "wave_closeout_artifact_exists",
|
|
235
|
+
ok: closeoutText !== null,
|
|
236
|
+
reason:
|
|
237
|
+
closeoutText !== null
|
|
238
|
+
? "wave closeout artifact exists"
|
|
239
|
+
: `missing wave closeout artifact: ${waveCloseoutFilename(waveId)}`,
|
|
240
|
+
}),
|
|
241
|
+
closeoutText === null)
|
|
242
|
+
)
|
|
243
|
+
return { ...rootValidation, ok: false, checks, warnings, waveId };
|
|
244
|
+
const closeout = readFrontmatterObject(closeoutText);
|
|
245
|
+
return (
|
|
246
|
+
checks.push({
|
|
247
|
+
id: "wave_closeout_frontmatter_valid",
|
|
248
|
+
ok: closeout !== null,
|
|
249
|
+
reason:
|
|
250
|
+
closeout !== null
|
|
251
|
+
? "wave closeout frontmatter is valid"
|
|
252
|
+
: "wave closeout artifact frontmatter is invalid",
|
|
253
|
+
}),
|
|
254
|
+
closeout === null
|
|
255
|
+
? { ...rootValidation, ok: false, checks, warnings, waveId }
|
|
256
|
+
: (checks.push(
|
|
257
|
+
...(await buildWaveClosureChecks(
|
|
258
|
+
projectRoot,
|
|
259
|
+
loaded.topicDir,
|
|
260
|
+
loaded.topic,
|
|
261
|
+
wave,
|
|
262
|
+
closeout,
|
|
263
|
+
)),
|
|
264
|
+
),
|
|
265
|
+
{
|
|
266
|
+
...rootValidation,
|
|
267
|
+
ok: rootValidation.ok && checks.every((entry) => entry.ok),
|
|
268
|
+
checks,
|
|
269
|
+
warnings,
|
|
270
|
+
waveId,
|
|
271
|
+
closeoutRef: toPortableRelativePath(path.relative(projectRoot, closeoutPath)),
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
export async function closeoutWaveInTopic(projectRoot, input, waveId, options) {
|
|
276
|
+
const loaded = await loadTopicReport(projectRoot, input);
|
|
277
|
+
if (!loaded.ok) return loaded;
|
|
278
|
+
const authority = await loadTopicRuntimeAuthority(projectRoot);
|
|
279
|
+
if (!topicHasEnrichedShape(loaded.topic, authority))
|
|
280
|
+
return { ok: false, error: "Wave closeout requires an enriched topic root." };
|
|
281
|
+
const wave = getTopicWaves(loaded.topic).find((entry) => entry.wave_id === waveId) ?? null;
|
|
282
|
+
if (!wave) return { ok: false, error: `Wave not found: ${waveId}` };
|
|
283
|
+
const closeout = {
|
|
284
|
+
closeout_id: waveId,
|
|
285
|
+
topic_id: loaded.topicId,
|
|
286
|
+
scope: "wave",
|
|
287
|
+
authority_closure: options.authorityClosure,
|
|
288
|
+
semantic_closure: options.semanticClosure,
|
|
289
|
+
consumer_closure: options.consumerClosure,
|
|
290
|
+
drift_resistance_closure: options.driftResistanceClosure,
|
|
291
|
+
disposition: options.disposition,
|
|
292
|
+
},
|
|
293
|
+
checks = await buildWaveClosureChecks(
|
|
294
|
+
projectRoot,
|
|
295
|
+
loaded.topicDir,
|
|
296
|
+
loaded.topic,
|
|
297
|
+
wave,
|
|
298
|
+
closeout,
|
|
299
|
+
);
|
|
300
|
+
if (!checks.every((entry) => entry.ok))
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
error: `Wave closeout refused: ${checks.find((entry) => !entry.ok)?.reason ?? "closure validation failed"}`,
|
|
304
|
+
checks,
|
|
305
|
+
warnings: [],
|
|
306
|
+
topicId: loaded.topicId,
|
|
307
|
+
topicRef: toPortableRelativePath(path.relative(projectRoot, loaded.topicDir)),
|
|
308
|
+
waveId,
|
|
309
|
+
};
|
|
310
|
+
const closeoutPath = path.join(loaded.topicDir, waveCloseoutFilename(waveId));
|
|
311
|
+
return (
|
|
312
|
+
await writeFile(closeoutPath, closeoutMarkdown(closeout, `Wave Closeout ${waveId}`), "utf8"),
|
|
313
|
+
(wave.state = "closed"),
|
|
314
|
+
(wave.selected = false),
|
|
315
|
+
(loaded.topic.waves = getTopicWaves(loaded.topic).map((entry) =>
|
|
316
|
+
entry.wave_id === waveId ? wave : entry,
|
|
317
|
+
)),
|
|
318
|
+
loaded.topic.selected_next_target === waveId && (loaded.topic.selected_next_target = null),
|
|
319
|
+
(loaded.topic.last_transition_at = buildTopicNow()),
|
|
320
|
+
(loaded.topic.last_transition_reason = `closed_${waveId}`),
|
|
321
|
+
await writeTopicYaml(loaded.topicYamlPath, loaded.topic),
|
|
322
|
+
{
|
|
323
|
+
ok: true,
|
|
324
|
+
topicId: loaded.topicId,
|
|
325
|
+
topicRef: toPortableRelativePath(path.relative(projectRoot, loaded.topicDir)),
|
|
326
|
+
waveId,
|
|
327
|
+
waveState: wave.state,
|
|
328
|
+
closeoutRef: toPortableRelativePath(path.relative(projectRoot, closeoutPath)),
|
|
329
|
+
}
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
export async function buildTrueCloseAuditChecks(projectRoot, topicDir, topic) {
|
|
333
|
+
const authority = await loadTopicRuntimeAuthority(projectRoot),
|
|
334
|
+
waves = getTopicWaves(topic),
|
|
335
|
+
checks = [],
|
|
336
|
+
activeDeferredBlockers = await collectActiveDeferredBlockers(topicDir),
|
|
337
|
+
nonTerminalWaves = waves
|
|
338
|
+
.filter((entry) => !["closed", "retired", "superseded"].includes(entry.state))
|
|
339
|
+
.map((entry) => `${entry.wave_id}:${entry.state}`);
|
|
340
|
+
checks.push({
|
|
341
|
+
id: "all_waves_terminal",
|
|
342
|
+
ok: nonTerminalWaves.length === 0,
|
|
343
|
+
reason:
|
|
344
|
+
nonTerminalWaves.length === 0
|
|
345
|
+
? "all waves are closed, retired, or superseded"
|
|
346
|
+
: `non-terminal waves remain: ${nonTerminalWaves.join(", ")}`,
|
|
347
|
+
});
|
|
348
|
+
checks.push({
|
|
349
|
+
id: "no_active_deferred_blockers",
|
|
350
|
+
ok: activeDeferredBlockers.length === 0,
|
|
351
|
+
reason:
|
|
352
|
+
activeDeferredBlockers.length === 0
|
|
353
|
+
? "no active deferred blocker artifacts remain"
|
|
354
|
+
: `active deferred blocker artifacts remain: ${activeDeferredBlockers.join(", ")}`,
|
|
355
|
+
});
|
|
356
|
+
const selectedActive = waves
|
|
357
|
+
.filter((entry) => entry.selected === true)
|
|
358
|
+
.map((entry) => entry.wave_id);
|
|
359
|
+
(checks.push({
|
|
360
|
+
id: "no_selected_wave_remains",
|
|
361
|
+
ok: selectedActive.length === 0,
|
|
362
|
+
reason:
|
|
363
|
+
selectedActive.length === 0
|
|
364
|
+
? "no selected wave remains active"
|
|
365
|
+
: `selected waves remain: ${selectedActive.join(", ")}`,
|
|
366
|
+
}),
|
|
367
|
+
checks.push({
|
|
368
|
+
id: "selected_target_cleared",
|
|
369
|
+
ok:
|
|
370
|
+
topic.selected_next_target === null ||
|
|
371
|
+
topic.selected_next_target === "topic_design_baseline",
|
|
372
|
+
reason:
|
|
373
|
+
topic.selected_next_target === null ||
|
|
374
|
+
topic.selected_next_target === "topic_design_baseline"
|
|
375
|
+
? "selected_next_target is cleared for topic closeout"
|
|
376
|
+
: `selected_next_target remains active: ${topic.selected_next_target}`,
|
|
377
|
+
}));
|
|
378
|
+
for (const wave of waves.filter((entry) => entry.state === "closed")) {
|
|
379
|
+
const evidence = await collectWaveArtifactEvidence(topicDir, wave.wave_id);
|
|
380
|
+
(authority.trueCloseAuditEvidence.requireWaveCloseoutForClosedWaves &&
|
|
381
|
+
checks.push({
|
|
382
|
+
id: `wave_closeout_exists_${wave.wave_id}`,
|
|
383
|
+
ok: evidence.closeoutRefs.length > 0,
|
|
384
|
+
reason:
|
|
385
|
+
evidence.closeoutRefs.length > 0
|
|
386
|
+
? `${wave.wave_id} has closeout evidence`
|
|
387
|
+
: `${wave.wave_id} is closed but has no wave closeout evidence`,
|
|
388
|
+
}),
|
|
389
|
+
authority.trueCloseAuditEvidence.requirePacketLineageForClosedWaves &&
|
|
390
|
+
checks.push({
|
|
391
|
+
id: `wave_packet_lineage_exists_${wave.wave_id}`,
|
|
392
|
+
ok: evidence.packetRefs.length > 0,
|
|
393
|
+
reason:
|
|
394
|
+
evidence.packetRefs.length > 0
|
|
395
|
+
? `${wave.wave_id} has packet lineage evidence`
|
|
396
|
+
: `${wave.wave_id} is closed but has no packet lineage evidence`,
|
|
397
|
+
}),
|
|
398
|
+
authority.trueCloseAuditEvidence.requireResultLineageForClosedWaves &&
|
|
399
|
+
checks.push({
|
|
400
|
+
id: `wave_result_lineage_exists_${wave.wave_id}`,
|
|
401
|
+
ok: evidence.resultRefs.length > 0,
|
|
402
|
+
reason:
|
|
403
|
+
evidence.resultRefs.length > 0
|
|
404
|
+
? `${wave.wave_id} has result lineage evidence`
|
|
405
|
+
: `${wave.wave_id} is closed but has no result lineage evidence`,
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
return checks;
|
|
409
|
+
}
|
|
410
|
+
export async function runTopicTrueCloseAudit(projectRoot, input, judgementText) {
|
|
411
|
+
const loaded = await loadTopicReport(projectRoot, input);
|
|
412
|
+
if (!loaded.ok) return loaded;
|
|
413
|
+
const authority = await loadTopicRuntimeAuthority(projectRoot);
|
|
414
|
+
if (!topicHasEnrichedShape(loaded.topic, authority))
|
|
415
|
+
return { ok: false, error: "True-close audit requires an enriched topic root." };
|
|
416
|
+
const checks = await buildTrueCloseAuditChecks(projectRoot, loaded.topicDir, loaded.topic),
|
|
417
|
+
passed = checks.every((entry) => entry.ok),
|
|
418
|
+
auditPath = path.join(loaded.topicDir, topicTrueCloseAuditFilename()),
|
|
419
|
+
judgementPath = path.join(loaded.topicDir, topicTrueCloseJudgementFilename()),
|
|
420
|
+
audit = { topic_id: loaded.topicId, status: passed ? "passed" : "pending", checks };
|
|
421
|
+
return (
|
|
422
|
+
await writeFile(auditPath, trueCloseAuditMarkdown(audit, judgementText), "utf8"),
|
|
423
|
+
await writeFile(
|
|
424
|
+
judgementPath,
|
|
425
|
+
`---
|
|
426
|
+
${YAML.stringify({ topic_id: loaded.topicId, status: passed ? "passed" : "pending", judgement: judgementText }).trimEnd()}
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
# Topic True-Close Audit Result
|
|
430
|
+
`,
|
|
431
|
+
"utf8",
|
|
432
|
+
),
|
|
433
|
+
(loaded.topic.last_transition_at = buildTopicNow()),
|
|
434
|
+
(loaded.topic.last_transition_reason = "ran_topic_true_close_audit"),
|
|
435
|
+
passed && (loaded.topic.current_true_close_status = "pending"),
|
|
436
|
+
await writeTopicYaml(loaded.topicYamlPath, loaded.topic),
|
|
437
|
+
{
|
|
438
|
+
ok: passed,
|
|
439
|
+
topicId: loaded.topicId,
|
|
440
|
+
topicRef: toPortableRelativePath(path.relative(projectRoot, loaded.topicDir)),
|
|
441
|
+
status: passed ? "passed" : "pending",
|
|
442
|
+
auditRef: toPortableRelativePath(path.relative(projectRoot, auditPath)),
|
|
443
|
+
judgementRef: toPortableRelativePath(path.relative(projectRoot, judgementPath)),
|
|
444
|
+
checks,
|
|
445
|
+
warnings: [],
|
|
446
|
+
}
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
export async function closeoutTopicInTopic(projectRoot, input, options) {
|
|
450
|
+
const loaded = await loadTopicReport(projectRoot, input);
|
|
451
|
+
if (!loaded.ok) return loaded;
|
|
452
|
+
const authority = await loadTopicRuntimeAuthority(projectRoot);
|
|
453
|
+
if (!topicHasEnrichedShape(loaded.topic, authority))
|
|
454
|
+
return { ok: false, error: "Topic closeout requires an enriched topic root." };
|
|
455
|
+
let pendingNoteLoaded = null;
|
|
456
|
+
if (loaded.topic.state === "pending") {
|
|
457
|
+
if (((pendingNoteLoaded = await loadPendingNote(loaded.topicDir)), !pendingNoteLoaded.ok))
|
|
458
|
+
return {
|
|
459
|
+
ok: false,
|
|
460
|
+
error: `Topic closeout from pending requires a pending note artifact: ${pendingNoteLoaded.error}`,
|
|
461
|
+
};
|
|
462
|
+
if (
|
|
463
|
+
typeof pendingNoteLoaded.note.close_trigger != "string" ||
|
|
464
|
+
pendingNoteLoaded.note.close_trigger.length === 0
|
|
465
|
+
)
|
|
466
|
+
return {
|
|
467
|
+
ok: false,
|
|
468
|
+
error: "Topic closeout from pending requires an explicit close trigger in pending-note.md.",
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const auditPath = path.join(loaded.topicDir, topicTrueCloseAuditFilename()),
|
|
472
|
+
judgementPath = path.join(loaded.topicDir, topicTrueCloseJudgementFilename()),
|
|
473
|
+
auditText = await readTextIfFile(auditPath),
|
|
474
|
+
judgementText = await readTextIfFile(judgementPath);
|
|
475
|
+
if (auditText === null || judgementText === null)
|
|
476
|
+
return { ok: false, error: "Topic closeout requires a recorded true-close audit and judgement." };
|
|
477
|
+
const audit = readFrontmatterObject(auditText);
|
|
478
|
+
if (!audit || audit.status !== "passed")
|
|
479
|
+
return { ok: false, error: "Topic closeout requires a passed true-close audit." };
|
|
480
|
+
const auditChecks = await buildTrueCloseAuditChecks(projectRoot, loaded.topicDir, loaded.topic);
|
|
481
|
+
if (!auditChecks.every((entry) => entry.ok))
|
|
482
|
+
return {
|
|
483
|
+
ok: false,
|
|
484
|
+
error: `Topic closeout refused: ${auditChecks.find((entry) => !entry.ok)?.reason ?? "true-close checks failed"}`,
|
|
485
|
+
checks: auditChecks,
|
|
486
|
+
warnings: [],
|
|
487
|
+
topicId: loaded.topicId,
|
|
488
|
+
topicRef: toPortableRelativePath(path.relative(projectRoot, loaded.topicDir)),
|
|
489
|
+
};
|
|
490
|
+
const closeout = {
|
|
491
|
+
closeout_id: loaded.topicId,
|
|
492
|
+
topic_id: loaded.topicId,
|
|
493
|
+
scope: "topic",
|
|
494
|
+
authority_closure: options.authorityClosure,
|
|
495
|
+
semantic_closure: options.semanticClosure,
|
|
496
|
+
consumer_closure: options.consumerClosure,
|
|
497
|
+
drift_resistance_closure: options.driftResistanceClosure,
|
|
498
|
+
disposition: options.disposition,
|
|
499
|
+
};
|
|
500
|
+
if (
|
|
501
|
+
[
|
|
502
|
+
closeout.authority_closure,
|
|
503
|
+
closeout.semantic_closure,
|
|
504
|
+
closeout.consumer_closure,
|
|
505
|
+
closeout.drift_resistance_closure,
|
|
506
|
+
].some((entry) => entry !== "closed") ||
|
|
507
|
+
closeout.disposition !== "complete"
|
|
508
|
+
)
|
|
509
|
+
return {
|
|
510
|
+
ok: false,
|
|
511
|
+
error: "Topic closeout requires all four closures to be closed and disposition=complete.",
|
|
512
|
+
};
|
|
513
|
+
const closeoutPath = path.join(loaded.topicDir, topicCloseoutFilename());
|
|
514
|
+
await writeFile(closeoutPath, closeoutMarkdown(closeout, "Topic Closeout"), "utf8");
|
|
515
|
+
const trueCloseRecordPath = path.join(loaded.topicDir, topicTrueCloseRecordFilename());
|
|
516
|
+
(await writeFile(
|
|
517
|
+
trueCloseRecordPath,
|
|
518
|
+
trueCloseRecordMarkdown({
|
|
519
|
+
topic_id: loaded.topicId,
|
|
520
|
+
status: "passed",
|
|
521
|
+
audit_ref: toPortableRelativePath(path.relative(projectRoot, auditPath)),
|
|
522
|
+
judgement_ref: toPortableRelativePath(path.relative(projectRoot, judgementPath)),
|
|
523
|
+
}),
|
|
524
|
+
"utf8",
|
|
525
|
+
),
|
|
526
|
+
(loaded.topic.state = "closed"),
|
|
527
|
+
(loaded.topic.current_true_close_status = "true_closed"),
|
|
528
|
+
(loaded.topic.last_transition_at = buildTopicNow()),
|
|
529
|
+
(loaded.topic.last_transition_reason = "closed_topic_after_true_close"));
|
|
530
|
+
const moved = await moveTopicDirectoryForState(
|
|
531
|
+
projectRoot,
|
|
532
|
+
loaded.topicDir,
|
|
533
|
+
loaded.topicId,
|
|
534
|
+
"closed",
|
|
535
|
+
);
|
|
536
|
+
return (
|
|
537
|
+
await writeTopicYaml(moved.topicYamlPath, loaded.topic),
|
|
538
|
+
pendingNoteLoaded?.ok &&
|
|
539
|
+
((pendingNoteLoaded.note.status = "closed"),
|
|
540
|
+
(pendingNoteLoaded.note.closed_at = buildTopicNow()),
|
|
541
|
+
await writeFile(
|
|
542
|
+
path.join(moved.topicDir, pendingNoteFilename()),
|
|
543
|
+
pendingNoteMarkdown(pendingNoteLoaded.note),
|
|
544
|
+
"utf8",
|
|
545
|
+
)),
|
|
546
|
+
{
|
|
547
|
+
ok: true,
|
|
548
|
+
topicId: loaded.topicId,
|
|
549
|
+
topicRef: toPortableRelativePath(path.relative(projectRoot, moved.topicDir)),
|
|
550
|
+
state: loaded.topic.state,
|
|
551
|
+
closeoutRef: toPortableRelativePath(
|
|
552
|
+
path.relative(projectRoot, path.join(moved.topicDir, topicCloseoutFilename())),
|
|
553
|
+
),
|
|
554
|
+
trueCloseRef: toPortableRelativePath(
|
|
555
|
+
path.relative(projectRoot, path.join(moved.topicDir, topicTrueCloseRecordFilename())),
|
|
556
|
+
),
|
|
557
|
+
currentTrueCloseStatus: loaded.topic.current_true_close_status,
|
|
558
|
+
}
|
|
559
|
+
);
|
|
560
|
+
}
|