@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
package/cli/lib/topic.mjs CHANGED
@@ -1,93 +1,81 @@
1
- import{mkdir,readdir,rename,writeFile}from"node:fs/promises";import path from"node:path";import YAML from"yaml";import{buildDispatchPrompt,buildPostUpdateReviewDecision,buildPreImplementationDecision,loadAuthorityConvergencePolicy}from"./authority-convergence.mjs";import{loadTopicRuntimeContracts}from"./contracts.mjs";import{pathExists,readTextIfFile}from"./fs-helpers.mjs";import{parseYamlText}from"./yaml-helpers.mjs";import{findUniqueFreezableDraftPacket}from"./topic-draft-packets.mjs";const TOPIC_ROOT=path.join(".nimi","topics"),TOPIC_ID_PATTERN=/^\d{4}-\d{2}-\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*$/,TOPIC_SLUG_PATTERN=/^[a-z0-9]+(?:-[a-z0-9]+)*$/,WAVE_ID_PATTERN=/^wave-[a-z0-9]+(?:-[a-z0-9]+)*$/,DEFAULT_TOPIC_RUNTIME_AUTHORITY={topicStates:["proposal","ongoing","pending","closed"],minimalRequiredFields:["topic_id","state","created_at","last_transition_at","last_transition_reason"],enrichedRequiredFields:["title","mode","posture","design_policy","parallel_truth","layering","risk","applicability","entry_justification","execution_mode","selected_next_target","current_true_close_status","forbidden_shortcuts"],topicEnums:{mode:["greenfield","landed","superseding"],posture:["no_legacy_hard_cut","backward_compat"],designPolicy:["complete_contract_first","mvp_incremental"],parallelTruth:["forbidden","admitted"],layering:["ontology","time_phased"],risk:["high","low"],applicability:["authority_bearing","high_risk_refactor","multi_wave_iteration","complex_remediation"],executionMode:["inline_manager_worker","manager_worker_auditor"],trueCloseStatus:["not_started","pending","true_closed","revoked","superseded"]},waveStates:["candidate","preflight_draft","preflight_admitted","implementation_admitted","implementation_active","needs_revision","overflowed","continuation_packet_open","closed","retired","superseded"],packetRequiredFields:["packet_id","topic_id","wave_id","packet_kind","status","authority_owner","canonical_seams","forbidden_shortcuts","acceptance_invariants","negative_tests","reopen_conditions"],packetFreezeAllowedStatuses:["draft","preflight","candidate"],resultVerdicts:["PASS","NEEDS_REVISION","FAIL","OVERFLOW"],resultKinds:["preflight","implementation","audit","judgement"],resultVerifiedAtFormat:"iso8601_utc_timestamp",closeoutScopes:["wave","topic"],closureStates:["open","closed","blocked"],closeoutDispositions:["complete","partial","deferred"],remediationKinds:["a","b","continuation","execution_state_closure"],decisionDispositions:["retired","superseded","unchanged"],pendingNoteRequiredFields:["pending_note_id","topic_id","entered_from_state","reason","summary","status"],pendingNoteStatuses:["active","resumed","closed"],defaultForbiddenShortcuts:["mvp_subset_contract","legacy_alias","compat_shim","dual_read","dual_write","placeholder_success","happy_path_only_closure","time_phased_layering","app_local_shadow_truth","silent_owner_cut_reopen"],recommendedFiles:["README.md","design.md","preflight.md","waves.md","candidate-wave-plan.md","implementation-doctrine.md","admission-checklists.md","manager-session-protocol.md","manager-prompts.md"],closureDimensions:["authority","semantic","consumer","drift_resistance"],waveCloseoutEvidence:{requirePacketLineage:!0,requireResultLineage:!0},trueCloseAuditEvidence:{requireWaveCloseoutForClosedWaves:!0,requirePacketLineageForClosedWaves:!0,requireResultLineageForClosedWaves:!0},topicStepDecision:{stopClasses:["continue","require_human_confirmation","await_external_evidence","blocked","completed"],recommendedActions:["admit_wave","freeze_packet","dispatch_worker","dispatch_audit","record_result","open_remediation","continue_overflow","hold_topic","resume_topic","closeout_wave","closeout_topic","no_action"]},topicRunLedger:{eventKinds:["decision_emitted","wave_admitted","packet_frozen","worker_dispatched","audit_dispatched","result_recorded","human_gate_opened","human_gate_resolved","wave_closed","topic_closed","runner_blocked"],runStatuses:["running","awaiting_human_confirmation","awaiting_external_evidence","blocked","completed"],artifactRefKeys:["decision_ref","packet_ref","prompt_ref","worker_output_ref","audit_output_ref","result_ref","closeout_ref","evidence_ref"],retryPostures:["not_applicable","retry_allowed_same_command","retry_requires_new_packet","retry_forbidden_until_human_gate"]},ignoredTopicValidateSemantics:{status:"report_only",canonicalSuccess:!1}},topicRuntimeAuthorityCache=new Map,PENDING_ENTRY_BLOCKER_STATES=new Set(["preflight_admitted","implementation_admitted","implementation_active","needs_revision","overflowed","continuation_packet_open"]);function formatDate(date=new Date){const year=date.getFullYear(),month=String(date.getMonth()+1).padStart(2,"0"),day=String(date.getDate()).padStart(2,"0");return`${year}-${month}-${day}`}function toPortableRelativePath(filePath){return filePath.split(path.sep).join("/")}function toStringArray(value,fallback){if(!Array.isArray(value))return fallback;const normalized=value.filter(entry=>typeof entry=="string"&&entry.length>0).map(entry=>String(entry));return normalized.length>0?normalized:fallback}function normalizeBoolean(value,fallback){return typeof value=="boolean"?value:fallback}async function loadTopicRuntimeAuthority(projectRoot){const cached=topicRuntimeAuthorityCache.get(projectRoot);if(cached)return cached;const loaded=await loadTopicRuntimeContracts(projectRoot),topicSchema=loaded.topicSchema.data??{},waveSchema=loaded.waveSchema.data??{},packetSchema=loaded.packetSchema.data??{},resultSchema=loaded.resultSchema.data??{},closeoutSchema=loaded.closeoutSchema.data??{},remediationSchema=loaded.remediationSchema.data??{},decisionReviewSchema=loaded.decisionReviewSchema.data??{},pendingNoteSchema=loaded.pendingNoteSchema.data??{},topicStepDecisionSchema=loaded.topicStepDecisionSchema.data??{},topicRunLedgerSchema=loaded.topicRunLedgerSchema.data??{},forbiddenShortcutsCatalog=loaded.forbiddenShortcutsCatalog.data??{},lifecycleReport=loaded.lifecycleReport.data?.topic_lifecycle_report??{},fourClosurePolicy=loaded.fourClosurePolicy.data?.four_closure_policy??{},validationPolicy=loaded.validationPolicy.data?.topic_validation_policy??{},minimalRequiredFields=toStringArray(lifecycleReport.state_evidence?.required_fields,DEFAULT_TOPIC_RUNTIME_AUTHORITY.minimalRequiredFields),enrichedRequiredFields=toStringArray(topicSchema.required,DEFAULT_TOPIC_RUNTIME_AUTHORITY.enrichedRequiredFields).filter(field=>!minimalRequiredFields.includes(field)),authority={topicStates:toStringArray(topicSchema.state_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.topicStates),minimalRequiredFields,enrichedRequiredFields,topicEnums:{mode:toStringArray(topicSchema.mode_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.topicEnums.mode),posture:toStringArray(topicSchema.posture_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.topicEnums.posture),designPolicy:toStringArray(topicSchema.design_policy_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.topicEnums.designPolicy),parallelTruth:toStringArray(topicSchema.parallel_truth_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.topicEnums.parallelTruth),layering:toStringArray(topicSchema.layering_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.topicEnums.layering),risk:toStringArray(topicSchema.risk_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.topicEnums.risk),applicability:toStringArray(topicSchema.applicability_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.topicEnums.applicability),executionMode:toStringArray(topicSchema.execution_mode_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.topicEnums.executionMode),trueCloseStatus:toStringArray(topicSchema.true_close_status_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.topicEnums.trueCloseStatus)},waveStates:toStringArray(waveSchema.state_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.waveStates),packetRequiredFields:toStringArray(packetSchema.required,DEFAULT_TOPIC_RUNTIME_AUTHORITY.packetRequiredFields),packetFreezeAllowedStatuses:toStringArray(packetSchema.freeze_allowed_status_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.packetFreezeAllowedStatuses),resultVerdicts:toStringArray(resultSchema.verdict_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.resultVerdicts),resultKinds:toStringArray(resultSchema.result_kind_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.resultKinds),resultVerifiedAtFormat:typeof resultSchema.verified_at_format=="string"?resultSchema.verified_at_format:DEFAULT_TOPIC_RUNTIME_AUTHORITY.resultVerifiedAtFormat,closeoutScopes:toStringArray(closeoutSchema.scope_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.closeoutScopes),closureStates:toStringArray(closeoutSchema.closure_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.closureStates),closeoutDispositions:toStringArray(closeoutSchema.disposition_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.closeoutDispositions),remediationKinds:toStringArray(remediationSchema.kind_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.remediationKinds),decisionDispositions:toStringArray(decisionReviewSchema.disposition_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.decisionDispositions),pendingNoteRequiredFields:toStringArray(pendingNoteSchema.required,DEFAULT_TOPIC_RUNTIME_AUTHORITY.pendingNoteRequiredFields),pendingNoteStatuses:toStringArray(pendingNoteSchema.status_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.pendingNoteStatuses),defaultForbiddenShortcuts:Array.isArray(forbiddenShortcutsCatalog.entries)?forbiddenShortcutsCatalog.entries.map(entry=>typeof entry?.key=="string"?entry.key:null).filter(Boolean):DEFAULT_TOPIC_RUNTIME_AUTHORITY.defaultForbiddenShortcuts,recommendedFiles:toStringArray(lifecycleReport.recommended_files,DEFAULT_TOPIC_RUNTIME_AUTHORITY.recommendedFiles).filter(entry=>!entry.includes("*")),closureDimensions:toStringArray(fourClosurePolicy.closures,DEFAULT_TOPIC_RUNTIME_AUTHORITY.closureDimensions),waveCloseoutEvidence:{requirePacketLineage:normalizeBoolean(fourClosurePolicy.wave_closeout_evidence?.require_packet_lineage,DEFAULT_TOPIC_RUNTIME_AUTHORITY.waveCloseoutEvidence.requirePacketLineage),requireResultLineage:normalizeBoolean(fourClosurePolicy.wave_closeout_evidence?.require_result_lineage,DEFAULT_TOPIC_RUNTIME_AUTHORITY.waveCloseoutEvidence.requireResultLineage)},trueCloseAuditEvidence:{requireWaveCloseoutForClosedWaves:normalizeBoolean(fourClosurePolicy.true_close_audit_evidence?.require_wave_closeout_for_closed_waves,DEFAULT_TOPIC_RUNTIME_AUTHORITY.trueCloseAuditEvidence.requireWaveCloseoutForClosedWaves),requirePacketLineageForClosedWaves:normalizeBoolean(fourClosurePolicy.true_close_audit_evidence?.require_packet_lineage_for_closed_waves,DEFAULT_TOPIC_RUNTIME_AUTHORITY.trueCloseAuditEvidence.requirePacketLineageForClosedWaves),requireResultLineageForClosedWaves:normalizeBoolean(fourClosurePolicy.true_close_audit_evidence?.require_result_lineage_for_closed_waves,DEFAULT_TOPIC_RUNTIME_AUTHORITY.trueCloseAuditEvidence.requireResultLineageForClosedWaves)},topicStepDecision:{stopClasses:toStringArray(topicStepDecisionSchema.stop_class_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.topicStepDecision.stopClasses),recommendedActions:toStringArray(topicStepDecisionSchema.recommended_action_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.topicStepDecision.recommendedActions)},topicRunLedger:{eventKinds:toStringArray(topicRunLedgerSchema.event_kind_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.topicRunLedger.eventKinds),runStatuses:toStringArray(topicRunLedgerSchema.run_status_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.topicRunLedger.runStatuses),artifactRefKeys:toStringArray(topicRunLedgerSchema.artifact_ref_keys,DEFAULT_TOPIC_RUNTIME_AUTHORITY.topicRunLedger.artifactRefKeys),retryPostures:toStringArray(topicRunLedgerSchema.retry_posture_enum,DEFAULT_TOPIC_RUNTIME_AUTHORITY.topicRunLedger.retryPostures)},ignoredTopicValidateSemantics:{status:typeof validationPolicy.ignored_topic_validate_semantics?.status=="string"?validationPolicy.ignored_topic_validate_semantics.status:DEFAULT_TOPIC_RUNTIME_AUTHORITY.ignoredTopicValidateSemantics.status,canonicalSuccess:normalizeBoolean(validationPolicy.ignored_topic_validate_semantics?.canonical_success,DEFAULT_TOPIC_RUNTIME_AUTHORITY.ignoredTopicValidateSemantics.canonicalSuccess)}};return topicRuntimeAuthorityCache.set(projectRoot,authority),authority}function titleFromSlug(slug){return slug.split("-").map(part=>part.charAt(0).toUpperCase()+part.slice(1)).join(" ")}function deriveTopicId(slug,date=new Date){return TOPIC_ID_PATTERN.test(slug)?slug:`${formatDate(date)}-${slug}`}function getTopicRoot(projectRoot){return path.join(projectRoot,TOPIC_ROOT)}function getTopicStateRoot(projectRoot,state){return path.join(getTopicRoot(projectRoot),state)}function isTopicPathInput(value){return typeof value=="string"&&(value.includes("/")||value.startsWith("."))}function buildCreatePayload(options,authority){return{topic_id:options.topicId,state:"proposal",created_at:options.today,last_transition_at:options.today,last_transition_reason:"topic_created_via_nimicoding_topic_create",title:options.title,mode:options.mode,posture:options.posture,design_policy:options.designPolicy,parallel_truth:options.parallelTruth,layering:options.layering,risk:options.risk,applicability:options.applicability,entry_justification:options.justification,execution_mode:options.executionMode,selected_next_target:"topic_design_baseline",current_true_close_status:"not_started",forbidden_shortcuts:authority.defaultForbiddenShortcuts,waves:[]}}function buildReadme(topic){return`# ${topic.title}
2
- State: \`${topic.state}\`
3
- This topic was created by \`nimicoding topic create\`.
4
- ## Purpose
5
- TODO: explain why this work needs topic-level governance rather than the ordinary non-topic path.
6
- ## Entry Posture
7
- - mode: \`${topic.mode}\`
8
- - posture: \`${topic.posture}\`
9
- - design policy: \`${topic.design_policy}\`
10
- - applicability: \`${topic.applicability}\`
11
- - execution mode: \`${topic.execution_mode}\`
12
- ## Current Next Action
13
- - selected_next_target: \`${topic.selected_next_target}\`
14
- - TODO: freeze the first bounded wave target before admission
15
- `}function buildDesign(topicId){return`# Design
16
- Topic: \`${topicId}\`
17
- This file is the index for split design companions.
18
- - TODO: add subtopic design files as the topic grows
19
- - TODO: keep this file as an index rather than collapsing the whole topic into one document
20
- `}function buildSimpleCompanion(title,topicId,bullets){return`# ${title}
21
- Topic: \`${topicId}\`
22
- ${bullets.map(item=>`- ${item}`).join(`
23
- `)}
24
- `}async function writeTopicScaffold(topicDir,topic){const files=new Map([["topic.yaml",YAML.stringify(topic)],["README.md",buildReadme(topic)],["design.md",buildDesign(topic.topic_id)],["preflight.md",buildSimpleCompanion("Preflight",topic.topic_id,["TODO: record spec status, authority owner, work type, and parallel truth","TODO: freeze stop-line and closeout checks before admitted execution"])],["waves.md",buildSimpleCompanion("Waves",topic.topic_id,["TODO: define the program-level wave DAG","TODO: identify the selected next execution target"])],["candidate-wave-plan.md",buildSimpleCompanion("Candidate Wave Plan",topic.topic_id,["TODO: name the first bounded wave","TODO: explain why this is the next owner cut"])],["closeout.md",buildSimpleCompanion("Closeout",topic.topic_id,["TODO: record bounded closure verdicts as the topic exits active execution","TODO: distinguish wave closeout, pending hold, and topic closeout posture"])],["implementation-doctrine.md",buildSimpleCompanion("Implementation Doctrine",topic.topic_id,["TODO: freeze forbidden shortcuts specific to this topic","TODO: explain what would count as a false closure"])],["admission-checklists.md",buildSimpleCompanion("Admission Checklists",topic.topic_id,["TODO: define topic-local admission gates for the first wave","TODO: define stop phrases for non-admissible shortcuts"])],["manager-session-protocol.md",buildSimpleCompanion("Manager Session Protocol",topic.topic_id,["TODO: define manager / worker / auditor role separation for this topic","TODO: record how overflow and remediation will be judged"])],["manager-prompts.md",buildSimpleCompanion("Manager Prompts",topic.topic_id,["TODO: add packet-specific manager prompt baselines once the first wave is admitted"])]]);await mkdir(topicDir,{recursive:!1});for(const[fileName,contents]of files.entries())await writeFile(path.join(topicDir,fileName),contents,"utf8")}function validateTopicSlug(value){return TOPIC_SLUG_PATTERN.test(value)}function validateTopicId(value){return TOPIC_ID_PATTERN.test(value)}async function findTopicDirectory(projectRoot,input=null){const authority=await loadTopicRuntimeAuthority(projectRoot),topicStatePattern=authority.topicStates.join("|");if(!input){const current=process.cwd(),match=toPortableRelativePath(path.relative(projectRoot,current)).match(new RegExp(`^\\.nimi/topics/(${topicStatePattern})/(\\d{4}-\\d{2}-\\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*)`));return match?{ok:!0,topicDir:path.join(projectRoot,".nimi","topics",match[1],match[2]),topicId:match[2],state:match[1]}:{ok:!1,error:"No topic id or topic path was provided, and the current working directory is not inside a topic root."}}if(isTopicPathInput(input)){const topicDir=path.resolve(projectRoot,input),match=toPortableRelativePath(path.relative(projectRoot,topicDir)).match(new RegExp(`^\\.nimi/topics/(${topicStatePattern})/(\\d{4}-\\d{2}-\\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*)$`));return match?{ok:!0,topicDir,topicId:match[2],state:match[1]}:{ok:!1,error:`Topic path must resolve to .nimi/topics/<state>/<topic-id>: ${input}`}}const matches=[];for(const state of authority.topicStates){const candidate=path.join(getTopicStateRoot(projectRoot,state),input);(await pathExists(candidate))?.isDirectory()&&matches.push({state,topicDir:candidate,topicId:input})}return matches.length===1?{ok:!0,...matches[0]}:matches.length>1?{ok:!1,error:`Topic id resolves to multiple lifecycle roots and must be disambiguated by path: ${input}`}:{ok:!1,error:`Topic not found under ${TOPIC_ROOT}: ${input}`}}async function resolveTopicProjectRoot(startDir){let currentDir=path.resolve(startDir);for(;;){if((await pathExists(path.join(currentDir,".nimi")))?.isDirectory())return currentDir;const parentDir=path.dirname(currentDir);if(parentDir===currentDir)return path.resolve(startDir);currentDir=parentDir}}async function loadTopicReport(projectRoot,input=null){const resolved=await findTopicDirectory(projectRoot,input);if(!resolved.ok)return resolved;const topicYamlPath=path.join(resolved.topicDir,"topic.yaml"),topicYamlText=await readTextIfFile(topicYamlPath);if(topicYamlText===null)return{ok:!1,error:`Missing topic.yaml at ${toPortableRelativePath(path.relative(projectRoot,topicYamlPath))}`};const topic=parseYamlText(topicYamlText);return!topic||typeof topic!="object"?{ok:!1,error:`topic.yaml is not valid YAML at ${toPortableRelativePath(path.relative(projectRoot,topicYamlPath))}`}:{ok:!0,...resolved,topicYamlPath,topicYamlText,topic}}function getTopicWaves(topic){return Array.isArray(topic.waves)?topic.waves.map(entry=>({...entry})):[]}function findDeterministicNextWave(topic){const waves=getTopicWaves(topic),terminalIds=new Set(waves.filter(entry=>["closed","retired","superseded"].includes(entry.state)).map(entry=>entry.wave_id)),ready=waves.filter(entry=>!["closed","retired","superseded"].includes(entry.state)&&["candidate","preflight_draft","needs_revision"].includes(entry.state)&&(Array.isArray(entry.deps)?entry.deps:[]).every(dep=>terminalIds.has(dep)));return ready.length===1?ready[0]:null}async function writeTopicYaml(topicYamlPath,topic){await writeFile(topicYamlPath,YAML.stringify(topic),"utf8")}async function moveTopicDirectoryForState(projectRoot,currentDir,topicId,targetState){const targetDir=path.join(getTopicStateRoot(projectRoot,targetState),topicId);return currentDir===targetDir?{topicDir:currentDir,topicYamlPath:path.join(currentDir,"topic.yaml")}:(await mkdir(path.dirname(targetDir),{recursive:!0}),await rename(currentDir,targetDir),{topicDir:targetDir,topicYamlPath:path.join(targetDir,"topic.yaml")})}function topicHasEnrichedShape(topic,authority){return authority.enrichedRequiredFields.every(field=>{const value=topic[field];return field==="selected_next_target"?value===null||value==="topic_design_baseline"||typeof value=="string"&&value.length>0:value!=null&&value!==""&&(!Array.isArray(value)||value.length>0)})}function buildTopicNow(){return formatDate(new Date)}function isIsoUtcTimestamp(value){return typeof value=="string"&&/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(value)&&!Number.isNaN(Date.parse(value))}async function collectWaveArtifactEvidence(topicDir,waveId){const files=(await readdir(topicDir,{withFileTypes:!0})).filter(entry=>entry.isFile()).map(entry=>entry.name);return{packetRefs:files.filter(name=>name.startsWith("packet-")&&fileReferencesWave(name,waveId)),resultRefs:files.filter(name=>name.startsWith("result-")&&fileReferencesWave(name,waveId)),closeoutRefs:files.filter(name=>name.startsWith("closeout-")&&fileReferencesWave(name,waveId)),remediationRefs:files.filter(name=>name.includes("remediation")&&fileReferencesWave(name,waveId)),overflowRefs:files.filter(name=>name.includes("overflow-continuation")&&fileReferencesWave(name,waveId))}}async function loadPendingNote(topicDir){const notePath=path.join(topicDir,pendingNoteFilename()),noteText=await readTextIfFile(notePath);if(noteText===null)return{ok:!1,notePath,error:`Missing pending note artifact: ${pendingNoteFilename()}`};const note=readFrontmatterObject(noteText);return note?{ok:!0,notePath,note}:{ok:!1,notePath,error:"Pending note artifact frontmatter is invalid"}}function getPendingEntryBlockers(topic){return getTopicWaves(topic).filter(entry=>PENDING_ENTRY_BLOCKER_STATES.has(entry.state)).map(entry=>`${entry.wave_id}:${entry.state}`)}async function loadTopicValidationPolicy(projectRoot){const parsed=(await loadTopicRuntimeContracts(projectRoot)).validationPolicy.data,entries=Array.isArray(parsed?.topic_validation_policy?.ignore_for_default_validate)?parsed.topic_validation_policy.ignore_for_default_validate:[],ignoredTopicIds=new Map;for(const entry of entries)entry&&typeof entry.topic_id=="string"&&entry.topic_id.length>0&&ignoredTopicIds.set(entry.topic_id,{reason:typeof entry.reason=="string"?entry.reason:null,posture:typeof entry.posture=="string"?entry.posture:null});const semantics=parsed?.topic_validation_policy?.ignored_topic_validate_semantics??{};return{ignoredTopicIds,ignoredTopicValidateSemantics:{status:typeof semantics.status=="string"?semantics.status:DEFAULT_TOPIC_RUNTIME_AUTHORITY.ignoredTopicValidateSemantics.status,canonicalSuccess:typeof semantics.canonical_success=="boolean"?semantics.canonical_success:DEFAULT_TOPIC_RUNTIME_AUTHORITY.ignoredTopicValidateSemantics.canonicalSuccess}}}function extractLegacyWaveIdsFromName(fileName){return Array.from(fileName.matchAll(/wave-\d+[a-z]?(?=-|\.|$)/g),match=>match[0])}function fileReferencesWave(fileName,waveId){return fileName.includes(waveId)||extractLegacyWaveIdsFromName(fileName).includes(waveId)}function buildObservedLineage(entry){return entry.closeouts>0?"closed_lineage":entry.results>0?"result_lineage":entry.packets>0?"packet_lineage":entry.remediations>0||entry.exec_packs>0||entry.decision_reviews>0?"auxiliary_lineage":"declared_only"}function isRecognizedLifecycleArtifactName(fileName){return fileName.endsWith(".md")?fileName.startsWith("packet-")?/^packet-wave-\d+[a-z]?(?:-[a-z0-9]+)*\.md$/.test(fileName)||/^packet-true-close(?:-[a-z0-9]+)*\.md$/.test(fileName):fileName.startsWith("result-")?/^result-wave-\d+[a-z]?(?:-[a-z0-9]+)*\.md$/.test(fileName)||/^result-topic-true-close(?:-[a-z0-9]+)*\.md$/.test(fileName)||/^result-true-close(?:-[a-z0-9]+)*\.md$/.test(fileName):fileName.startsWith("closeout-")?/^closeout-wave-\d+[a-z]?(?:-[a-z0-9]+)*\.md$/.test(fileName)||/^closeout-topic(?:-[a-z0-9]+)*\.md$/.test(fileName)||/^closeout-true-close(?:-[a-z0-9]+)*\.md$/.test(fileName):fileName.startsWith("decision-review-")?/^decision-review-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/.test(fileName):fileName.startsWith("prompt-")?/^prompt-[a-z0-9-]+-(worker|audit)\.md$/.test(fileName):fileName.startsWith("overflow-continuation-")?/^overflow-continuation-wave-\d+[a-z]?(?:-[a-z0-9]+)*\.md$/.test(fileName):!0:!0}async function analyzeTopicArtifacts(topicDir,topic){const files=(await readdir(topicDir,{withFileTypes:!0})).filter(entry=>entry.isFile()).map(entry=>entry.name),packetFiles=files.filter(name=>name.startsWith("packet-")&&name.endsWith(".md")),resultFiles=files.filter(name=>name.startsWith("result-")&&name.endsWith(".md")),closeoutFiles=files.filter(name=>name.startsWith("closeout-")&&name.endsWith(".md")),decisionReviewFiles=files.filter(name=>name.startsWith("decision-review-")&&name.endsWith(".md")),remediationFiles=files.filter(name=>name.includes("remediation")&&name.endsWith(".md")),overflowFiles=files.filter(name=>name.includes("overflow-continuation")||name.includes("remediation-continuation")),execPackFiles=files.filter(name=>name.includes("exec-pack-")&&name.endsWith(".md")),trueCloseFiles=files.filter(name=>name==="topic-true-close-audit.md"||name.startsWith("result-topic-true-close")||name==="closeout-topic-true-close.md"),ambiguousLifecycleFiles=files.filter(name=>/^(packet|result|closeout|decision-review|prompt|overflow-continuation)-/.test(name)&&!isRecognizedLifecycleArtifactName(name)),packetWaveIds=new Set(packetFiles.flatMap(name=>extractLegacyWaveIdsFromName(name))),closeoutWaveIds=new Set(closeoutFiles.flatMap(name=>extractLegacyWaveIdsFromName(name))),topicWaveIds=new Set(getTopicWaves(topic).map(entry=>entry.wave_id)),knownWaveIds=new Set([...packetWaveIds,...closeoutWaveIds,...topicWaveIds]),resultWaveIds=resultFiles.flatMap(name=>extractLegacyWaveIdsFromName(name)),unresolvedResultWaveIds=Array.from(new Set(resultWaveIds.filter(waveId=>!knownWaveIds.has(waveId)))),closeoutWaveIdsArray=Array.from(closeoutWaveIds),activeWaveCloseoutConflicts=getTopicWaves(topic).filter(entry=>!["closed","retired","superseded"].includes(entry.state)&&closeoutFiles.some(name=>name.includes(entry.wave_id)||closeoutWaveIds.has(entry.wave_id))).map(entry=>`${entry.wave_id}:${entry.state}`),topicHasOpenBlockers=getTopicWaves(topic).some(entry=>!["closed","retired","superseded"].includes(entry.state))||typeof topic.selected_next_target=="string"&&topic.selected_next_target.length>0&&topic.selected_next_target!=="topic_design_baseline",prematureTrueClose=trueCloseFiles.length>0&&topicHasOpenBlockers,observedWaveIds=Array.from(new Set([...Array.from(topicWaveIds),...Array.from(packetWaveIds),...closeoutWaveIdsArray,...resultWaveIds,...files.flatMap(name=>extractLegacyWaveIdsFromName(name))])).sort(),legacyObservedWaves=observedWaveIds.map(waveId=>{const observed={wave_id:waveId,packets:packetFiles.filter(name=>fileReferencesWave(name,waveId)).length,results:resultFiles.filter(name=>fileReferencesWave(name,waveId)).length,closeouts:closeoutFiles.filter(name=>fileReferencesWave(name,waveId)).length,decision_reviews:decisionReviewFiles.filter(name=>fileReferencesWave(name,waveId)).length,remediations:remediationFiles.filter(name=>fileReferencesWave(name,waveId)).length,overflow_continuations:overflowFiles.filter(name=>fileReferencesWave(name,waveId)).length,exec_packs:execPackFiles.filter(name=>fileReferencesWave(name,waveId)).length,declared_in_topic_yaml:topicWaveIds.has(waveId)};return{...observed,observed_lineage:buildObservedLineage(observed)}});return{files,counts:{files:files.length,packets:packetFiles.length,results:resultFiles.length,closeouts:closeoutFiles.length,decision_reviews:decisionReviewFiles.length,remediations:remediationFiles.length,overflow_continuations:overflowFiles.length,exec_packs:execPackFiles.length,true_close_artifacts:trueCloseFiles.length},legacyWaveIds:observedWaveIds,legacyObservedWaves,featureFlags:{decision_review_lineage:decisionReviewFiles.length>0,remediation_lineage:remediationFiles.length>0,overflow_lineage:overflowFiles.length>0,true_close_lineage:trueCloseFiles.length>=2,exec_pack_lineage:execPackFiles.length>0},unresolvedResultWaveIds,closeoutWaveIds:closeoutWaveIdsArray,ambiguousLifecycleFiles,activeWaveCloseoutConflicts,prematureTrueClose}}function validateWaveId(value){return WAVE_ID_PATTERN.test(value)}function normalizeDeps(value){return Array.isArray(value)?value.map(entry=>String(entry)):[]}function validateGraphFromTopic(topic){const waves=getTopicWaves(topic),checks=[],warnings=[],waveIds=waves.map(entry=>entry.wave_id),uniqueWaveIds=new Set(waveIds);checks.push({id:"wave_ids_unique",ok:uniqueWaveIds.size===waveIds.length,reason:uniqueWaveIds.size===waveIds.length?"wave ids are unique":"duplicate wave ids exist in topic.yaml waves[]"});const invalidWaveIds=waveIds.filter(entry=>!validateWaveId(entry));checks.push({id:"wave_ids_valid",ok:invalidWaveIds.length===0,reason:invalidWaveIds.length===0?"wave ids use the canonical wave-<n>-slug shape":`invalid wave ids: ${invalidWaveIds.join(", ")}`});const missingDeps=[],selectedWaveIds=[],retiredSelected=[];for(const wave of waves){const deps=normalizeDeps(wave.deps);for(const dep of deps)uniqueWaveIds.has(dep)||missingDeps.push(`${wave.wave_id}->${dep}`);wave.selected===!0&&selectedWaveIds.push(wave.wave_id),wave.selected===!0&&["retired","superseded"].includes(wave.state)&&retiredSelected.push(wave.wave_id)}checks.push({id:"wave_dependencies_resolve",ok:missingDeps.length===0,reason:missingDeps.length===0?"all wave dependencies resolve inside the topic":`missing dependency refs: ${missingDeps.join(", ")}`}),checks.push({id:"selected_wave_unique",ok:selectedWaveIds.length<=1,reason:selectedWaveIds.length<=1?"selected wave is unique":`multiple selected waves exist: ${selectedWaveIds.join(", ")}`});const selectedMatchesTopicTarget=selectedWaveIds.length===0?topic.selected_next_target==="topic_design_baseline"||topic.selected_next_target===null:selectedWaveIds[0]===topic.selected_next_target;checks.push({id:"selected_wave_matches_topic_target",ok:selectedMatchesTopicTarget,reason:selectedMatchesTopicTarget?"selected wave matches topic.selected_next_target":`selected wave and topic.selected_next_target diverge (${selectedWaveIds[0]??"none"} vs ${topic.selected_next_target??"none"})`}),checks.push({id:"retired_or_superseded_not_selected",ok:retiredSelected.length===0,reason:retiredSelected.length===0?"retired or superseded waves are not selected":`retired/superseded waves remain selected: ${retiredSelected.join(", ")}`});const visiting=new Set,visited=new Set;let cycleRef=null;const waveMap=new Map(waves.map(wave=>[wave.wave_id,wave]));function dfs(waveId,trail=[]){if(cycleRef)return;if(visiting.has(waveId)){cycleRef=[...trail,waveId].join(" -> ");return}if(visited.has(waveId))return;visiting.add(waveId);const wave=waveMap.get(waveId);if(wave)for(const dep of normalizeDeps(wave.deps))dfs(dep,[...trail,waveId]);visiting.delete(waveId),visited.add(waveId)}for(const waveId of waveIds)dfs(waveId);return checks.push({id:"graph_acyclic",ok:cycleRef===null,reason:cycleRef===null?"wave graph is acyclic":`wave graph contains a cycle: ${cycleRef}`}),waves.length===0&&warnings.push("topic has no machine wave registry yet"),{ok:checks.every(entry=>entry.ok),checks,warnings,waves}}async function validateTopicGraph(projectRoot,input=null){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return{ok:!1,error:loaded.error,checks:[],warnings:[]};const rootValidation=await validateTopicRoot(projectRoot,input);if(!rootValidation.ok)return rootValidation;const authority=await loadTopicRuntimeAuthority(projectRoot);if(!topicHasEnrichedShape(loaded.topic,authority))return{...rootValidation,ok:!1,checks:[...rootValidation.checks,{id:"enriched_topic_required_for_wave_graph",ok:!1,reason:"wave graph commands require an enriched topic root"}],warnings:rootValidation.warnings};const graph=validateGraphFromTopic(loaded.topic);return{...rootValidation,ok:rootValidation.ok&&graph.ok,checks:[...rootValidation.checks,...graph.checks],warnings:[...rootValidation.warnings,...graph.warnings],waveCount:graph.waves.length}}async function validateWaveAdmission(projectRoot,input,waveId){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return{ok:!1,error:loaded.error,checks:[],warnings:[]};const graphReport=await validateTopicGraph(projectRoot,input),wave=getTopicWaves(loaded.topic).find(entry=>entry.wave_id===waveId)??null,checks=[...graphReport.checks??[]],warnings=[...graphReport.warnings??[]];if(checks.push({id:"wave_exists",ok:wave!==null,reason:wave?"wave exists in topic.yaml waves[]":`wave does not exist: ${waveId}`}),!wave)return{...graphReport,ok:!1,checks,warnings};const dispatchableState=!["retired","superseded","closed","overflowed"].includes(wave.state);checks.push({id:"wave_state_dispatchable",ok:dispatchableState,reason:dispatchableState?"wave state is eligible for admission":`wave state is not admissible: ${wave.state}`}),checks.push({id:"wave_selected",ok:wave.selected===!0,reason:wave.selected===!0?"wave is selected":"wave must be selected before admission"}),checks.push({id:"selected_target_matches_wave",ok:loaded.topic.selected_next_target===waveId,reason:loaded.topic.selected_next_target===waveId?"topic.selected_next_target matches the wave":`topic.selected_next_target does not match wave (${loaded.topic.selected_next_target??"none"} vs ${waveId})`});const waveMap=new Map(getTopicWaves(loaded.topic).map(entry=>[entry.wave_id,entry])),unmetDeps=normalizeDeps(wave.deps).filter(dep=>waveMap.get(dep)?.state!=="closed");checks.push({id:"upstream_dependencies_closed",ok:unmetDeps.length===0,reason:unmetDeps.length===0?"all upstream dependencies are closed":`upstream dependencies are not closed: ${unmetDeps.join(", ")}`});const waveStateAllowedForAdmit=["candidate","preflight_draft","needs_revision"].includes(wave.state);return checks.push({id:"wave_state_allows_preflight_admission",ok:waveStateAllowedForAdmit,reason:waveStateAllowedForAdmit?"wave state can move to preflight_admitted":`wave state cannot move to preflight_admitted from ${wave.state}`}),{...graphReport,ok:graphReport.ok&&checks.every(entry=>entry.ok),checks,warnings}}async function addWaveToTopic(projectRoot,input,wave){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const authority=await loadTopicRuntimeAuthority(projectRoot);if(!topicHasEnrichedShape(loaded.topic,authority))return{ok:!1,error:"Wave commands require an enriched topic root."};const waves=getTopicWaves(loaded.topic);if(waves.some(entry=>entry.wave_id===wave.wave_id))return{ok:!1,error:`Wave already exists: ${wave.wave_id}`};waves.push(wave);const graphPreview=validateGraphFromTopic({...loaded.topic,waves}),failedCheck=graphPreview.checks.find(entry=>!entry.ok);return failedCheck?{ok:!1,error:`Wave add refused: ${failedCheck.reason}`,checks:graphPreview.checks,warnings:graphPreview.warnings}:(loaded.topic.waves=waves,await writeTopicYaml(loaded.topicYamlPath,loaded.topic),{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),waveId:wave.wave_id,waveState:wave.state})}async function selectWaveInTopic(projectRoot,input,waveId){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const authority=await loadTopicRuntimeAuthority(projectRoot);if(!topicHasEnrichedShape(loaded.topic,authority))return{ok:!1,error:"Wave commands require an enriched topic root."};const waves=getTopicWaves(loaded.topic),wave=waves.find(entry=>entry.wave_id===waveId);if(!wave)return{ok:!1,error:`Wave not found: ${waveId}`};if(["retired","superseded","closed","overflowed"].includes(wave.state))return{ok:!1,error:`Wave select refused: ${waveId} is not selectable from state ${wave.state}`};for(const entry of waves)entry.selected=entry.wave_id===waveId;return loaded.topic.waves=waves,loaded.topic.selected_next_target=waveId,loaded.topic.last_transition_at=buildTopicNow(),loaded.topic.last_transition_reason=`selected_${waveId}_as_next_execution_target`,await writeTopicYaml(loaded.topicYamlPath,loaded.topic),{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),waveId,selectedNextTarget:loaded.topic.selected_next_target}}async function admitWaveInTopic(projectRoot,input,waveId){let validation=await validateWaveAdmission(projectRoot,input,waveId);if(!validation.ok){const loadedForSelection=await loadTopicReport(projectRoot,input),wavesForSelection=getTopicWaves(loadedForSelection.topic),waveForSelection=wavesForSelection.find(entry=>entry.wave_id===waveId),terminalIds=new Set(wavesForSelection.filter(entry=>["closed","retired","superseded"].includes(entry.state)).map(entry=>entry.wave_id)),depsClosed=waveForSelection&&(Array.isArray(waveForSelection.deps)?waveForSelection.deps:[]).every(dep=>terminalIds.has(dep)),canSelectForAdmission=loadedForSelection.ok&&(loadedForSelection.topic.selected_next_target===null||loadedForSelection.topic.selected_next_target==="topic_design_baseline")&&waveForSelection&&["candidate","preflight_draft","needs_revision"].includes(waveForSelection.state)&&depsClosed;if(canSelectForAdmission){const selected=await selectWaveInTopic(projectRoot,input,waveId);if(!selected.ok)return selected;validation=await validateWaveAdmission(projectRoot,input,waveId)}}if(!validation.ok)return validation;const loaded=await loadTopicReport(projectRoot,input),waves=getTopicWaves(loaded.topic),wave=waves.find(entry=>entry.wave_id===waveId);wave.state="preflight_admitted",loaded.topic.waves=waves;let nextState=loaded.topic.state;["proposal","pending"].includes(loaded.topic.state)&&(nextState="ongoing",loaded.topic.state=nextState),loaded.topic.last_transition_at=buildTopicNow(),loaded.topic.last_transition_reason=`wave_${waveId}_preflight_admitted`;const moved=await moveTopicDirectoryForState(projectRoot,loaded.topicDir,loaded.topicId,nextState);return await writeTopicYaml(moved.topicYamlPath,loaded.topic),{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,moved.topicDir)),waveId,waveState:wave.state,state:loaded.topic.state}}function parsePacketDraft(text){if(!text)return null;if(text.startsWith(`---
25
- `)){const closing=text.indexOf(`
26
- ---
27
- `,4);if(closing!==-1){const frontmatter=text.slice(4,closing);return parseYamlText(frontmatter)}}return parseYamlText(text)}function packetFilenameFromId(packetId){return`packet-${packetId}.md`}function resultFilename(waveId,slug,resultKind){return`result-${waveId}-${slug}-${resultKind}.md`}function decisionReviewFilename(slug){return`decision-review-${slug}.md`}function remediationFilename(waveId,kind,reason){return`packet-${waveId}-remediation-${kind}-${reason}.md`}function overflowContinuationFilename(waveId,continuationPacketId){return`overflow-continuation-${waveId}-${continuationPacketId}.md`}function waveCloseoutFilename(waveId){return`closeout-${waveId}.md`}function topicCloseoutFilename(){return"closeout-topic.md"}function topicTrueCloseAuditFilename(){return"topic-true-close-audit.md"}function topicTrueCloseJudgementFilename(){return"result-topic-true-close-audit.md"}function topicTrueCloseRecordFilename(){return"result-topic-true-close.md"}function pendingNoteFilename(){return"pending-note.md"}function pendingNoteMarkdown(note){return`---
28
- ${YAML.stringify(note).trimEnd()}
29
- ---
30
-
31
- # Pending Note
32
-
33
- Recorded by \`nimicoding topic hold\`.
34
- `}function packetMarkdown(packet){return`---
35
- ${YAML.stringify(packet).trimEnd()}
36
- ---
37
-
38
- # Packet ${packet.packet_id}
39
-
40
- Frozen by \`nimicoding topic packet freeze\`.
41
- `}async function freezePacketForTopic(projectRoot,input,draftPath){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const authority=await loadTopicRuntimeAuthority(projectRoot);if(!topicHasEnrichedShape(loaded.topic,authority))return{ok:!1,error:"Packet freeze requires an enriched topic root."};const draftText=await readTextIfFile(path.resolve(projectRoot,draftPath));if(draftText===null)return{ok:!1,error:`Draft packet not found: ${draftPath}`};const packet=parsePacketDraft(draftText);if(!packet||typeof packet!="object")return{ok:!1,error:`Draft packet is not valid YAML/frontmatter: ${draftPath}`};const missingFields=authority.packetRequiredFields.filter(field=>{const value=packet[field];return value==null||value===""||Array.isArray(value)&&value.length===0});if(missingFields.length>0)return{ok:!1,error:`Draft packet is missing required fields: ${missingFields.join(", ")}`};if(packet.topic_id!==loaded.topicId)return{ok:!1,error:`Draft packet topic_id does not match topic (${packet.topic_id} vs ${loaded.topicId})`};if(!getTopicWaves(loaded.topic).find(entry=>entry.wave_id===packet.wave_id))return{ok:!1,error:`Draft packet wave_id does not resolve inside the topic: ${packet.wave_id}`};if(!authority.packetFreezeAllowedStatuses.includes(packet.status))return{ok:!1,error:`Draft packet status is not freezeable: ${packet.status}`};packet.status="candidate";const packetPath=path.join(loaded.topicDir,packetFilenameFromId(packet.packet_id));return await writeFile(packetPath,packetMarkdown(packet),"utf8"),{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),packetId:packet.packet_id,packetRef:toPortableRelativePath(path.relative(projectRoot,packetPath)),waveId:packet.wave_id,status:packet.status}}async function loadTopicPacket(projectRoot,input,packetId){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const packetPath=path.join(loaded.topicDir,packetFilenameFromId(packetId)),packetText=await readTextIfFile(packetPath);if(packetText===null)return{ok:!1,error:`Packet not found: ${packetId}`};const packet=parsePacketDraft(packetText);return!packet||typeof packet!="object"?{ok:!1,error:`Packet is not valid YAML/frontmatter: ${packetId}`}:{ok:!0,...loaded,packetPath,packet}}async function listWavePackets(topicDir,waveId){const entries=await readdir(topicDir,{withFileTypes:!0}),packets=[];for(const entry of entries){if(!entry.isFile()||!entry.name.startsWith("packet-")||!entry.name.endsWith(".md"))continue;const packetPath=path.join(topicDir,entry.name),packetText=await readTextIfFile(packetPath),packet=readFrontmatterObject(packetText??"");packet?.wave_id===waveId&&packets.push({packet,packetPath,packetRefName:entry.name})}return packets.sort((left,right)=>left.packetRefName.localeCompare(right.packetRefName))}async function listWaveResults(topicDir,waveId){const entries=await readdir(topicDir,{withFileTypes:!0}),results=[];for(const entry of entries){if(!entry.isFile()||!entry.name.startsWith("result-")||!entry.name.endsWith(".md"))continue;const resultPath=path.join(topicDir,entry.name),resultText=await readTextIfFile(resultPath),result=readFrontmatterObject(resultText??"");result?.wave_id===waveId&&results.push({result,resultPath,resultRefName:entry.name})}return results.sort((left,right)=>left.resultRefName.localeCompare(right.resultRefName))}function buildTopicStepDecision(topic,wave,values){const waveId=wave?.wave_id??null;return{decision_id:`${topic.topic_id}:${waveId??"topic"}:${values.reasonCode}`,topic_id:topic.topic_id,wave_id:waveId,decision_kind:"topic_next_step",stop_class:values.stopClass,recommended_action:values.recommendedAction,reason_code:values.reasonCode,requires_human_confirmation:values.stopClass==="require_human_confirmation",recommended_decision:values.recommendedDecision,recommendation_rationale:values.recommendationRationale,expected_artifacts:values.expectedArtifacts??[],next_command_ref:values.nextCommandRef??null,blocking_checks:values.blockingChecks??[]}}function commandRef(parts){return["nimicoding","topic",...parts].join(" ")}async function buildDecisionForSelectedWave(projectRoot,loaded,graphReport,wave){const failedGraphChecks=(graphReport.checks??[]).filter(entry=>!entry.ok);if(!graphReport.ok)return buildTopicStepDecision(loaded.topic,wave,{stopClass:"blocked",recommendedAction:"no_action",reasonCode:"topic_graph_validation_failed",recommendedDecision:"fix_topic_graph_before_continuing",recommendationRationale:"The wave graph is the dispatch authority for topic execution and must validate before any next step is emitted.",blockingChecks:failedGraphChecks});if(["closed","retired","superseded"].includes(wave.state))return buildTopicStepDecision(loaded.topic,wave,{stopClass:"blocked",recommendedAction:"no_action",reasonCode:"selected_wave_is_terminal",recommendedDecision:"select_a_non_terminal_wave_or_close_the_topic",recommendationRationale:`Selected wave ${wave.wave_id} is ${wave.state} and cannot be dispatched.`});if(["candidate","preflight_draft","needs_revision"].includes(wave.state)){const admission=await validateWaveAdmission(projectRoot,loaded.topicId,wave.wave_id);return admission.ok?buildTopicStepDecision(loaded.topic,wave,{stopClass:"continue",recommendedAction:"admit_wave",reasonCode:"wave_admission_ready",recommendedDecision:"admit_wave",recommendationRationale:"Admission is mechanical.",nextCommandRef:commandRef(["wave","admit",loaded.topicId,wave.wave_id])}):buildTopicStepDecision(loaded.topic,wave,{stopClass:"blocked",recommendedAction:"admit_wave",reasonCode:"wave_admission_validation_failed",recommendedDecision:"repair_admission_blockers",recommendationRationale:"The selected wave cannot be admitted until its admission checks pass.",nextCommandRef:commandRef(["validate","admission",loaded.topicId,wave.wave_id,"--json"]),blockingChecks:(admission.checks??[]).filter(entry=>!entry.ok)})}if(["preflight_admitted","implementation_admitted","continuation_packet_open"].includes(wave.state))return buildTopicStepDecision(loaded.topic,wave,await buildPreImplementationDecision({projectRoot,loaded,wave,commandRef,listWavePackets,listWaveResults,findUniqueFreezableDraftPacket,loadTopicRuntimeAuthority}));if(wave.state==="implementation_active"){const waveResults=await listWaveResults(loaded.topicDir,wave.wave_id);if(waveResults.length===0)return buildTopicStepDecision(loaded.topic,wave,{stopClass:"await_external_evidence",recommendedAction:"record_result",reasonCode:"awaiting_worker_or_audit_result",recommendedDecision:"ingest_the_next_worker_or_audit_result_when_available",recommendationRationale:"The wave is active and has packet lineage, but no result has been recorded yet.",expectedArtifacts:["result-<wave-id>-<kind>.md"],nextCommandRef:commandRef(["result","record",loaded.topicId,"--kind","<kind>","--verdict","<verdict>","--from","<path>","--verified-at","<utc>"])});const postUpdateReviewDecision=buildPostUpdateReviewDecision({topicId:loaded.topicId,wave,packets:await listWavePackets(loaded.topicDir,wave.wave_id),results:waveResults,policy:await loadAuthorityConvergencePolicy(projectRoot),commandRef});return postUpdateReviewDecision?buildTopicStepDecision(loaded.topic,wave,postUpdateReviewDecision):buildTopicStepDecision(loaded.topic,wave,{stopClass:"continue",recommendedAction:"closeout_wave",reasonCode:"wave_has_result_lineage_ready_for_closeout",recommendedDecision:"closeout_wave",recommendationRationale:"Wave closeout is a deterministic phase transition once lineage-backed result evidence exists.",nextCommandRef:commandRef(["closeout","wave",loaded.topicId,wave.wave_id,"--authority","closed","--semantic","closed","--consumer","closed","--drift-resistance","closed","--disposition","complete"])})}return wave.state==="overflowed"?buildTopicStepDecision(loaded.topic,wave,{stopClass:"require_human_confirmation",recommendedAction:"continue_overflow",reasonCode:"overflow_requires_manager_judgement",recommendedDecision:"approve_continuation_only_if_the_same_owner_domain_rule_still_holds",recommendationRationale:"Overflow is not pass or rollback; continuation requires explicit manager judgement.",nextCommandRef:commandRef(["overflow","continue",loaded.topicId,"--packet","<continuation-packet-id>","--overflowed-packet","<packet-id>","--manager-judgement","<text>","--same-owner-domain"])}):buildTopicStepDecision(loaded.topic,wave,{stopClass:"blocked",recommendedAction:"no_action",reasonCode:"unsupported_wave_state",recommendedDecision:"repair_or_review_the_selected_wave_state",recommendationRationale:`No next-step rule is defined for wave state ${wave.state}.`})}async function decideTopicNextStep(projectRoot,input=null){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const rootValidation=await validateTopicRoot(projectRoot,input),selectedWave=getTopicWaves(loaded.topic).find(entry=>entry.wave_id===loaded.topic.selected_next_target)??null;if(!rootValidation.ok)return{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),decision:buildTopicStepDecision(loaded.topic,selectedWave,{stopClass:"blocked",recommendedAction:"no_action",reasonCode:"topic_root_validation_failed",recommendedDecision:"repair_topic_root_before_continuing",recommendationRationale:"The topic root must validate before a next execution step can be selected.",blockingChecks:(rootValidation.checks??[]).filter(entry=>!entry.ok)})};if(loaded.topic.state==="pending")return{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),decision:buildTopicStepDecision(loaded.topic,selectedWave,{stopClass:"await_external_evidence",recommendedAction:"resume_topic",reasonCode:"topic_pending_wait",recommendedDecision:"resume_only_when_the_pending_note_reopen_criteria_are_met",recommendationRationale:"Pending is an explicit wait state and must not be bypassed by the loop.",nextCommandRef:commandRef(["resume",loaded.topicId,"--criteria-met","<text>"])})};if(!loaded.topic.selected_next_target||loaded.topic.selected_next_target==="topic_design_baseline"){const allWavesTerminal=getTopicWaves(loaded.topic).filter(entry=>!["closed","retired","superseded"].includes(entry.state)).length===0&&getTopicWaves(loaded.topic).length>0,deterministicNextWave=allWavesTerminal?null:findDeterministicNextWave(loaded.topic);return{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),decision:buildTopicStepDecision(loaded.topic,deterministicNextWave,allWavesTerminal?{stopClass:"completed",recommendedAction:"closeout_topic",reasonCode:"all_waves_terminal",recommendedDecision:"run_topic_closeout_and_true_close_checks",recommendationRationale:"All waves are terminal, so the next step is topic-level closeout review.",nextCommandRef:commandRef(["closeout","topic",loaded.topicId,"--authority","closed","--semantic","closed","--consumer","closed","--drift-resistance","closed","--disposition","complete"])}:deterministicNextWave?{stopClass:"continue",recommendedAction:"admit_wave",reasonCode:"deterministic_next_wave_ready",recommendedDecision:"admit_wave",recommendationRationale:"Exactly one dependency-ready non-terminal wave exists, so selecting the next phase is mechanical.",nextCommandRef:commandRef(["wave","admit",loaded.topicId,deterministicNextWave.wave_id])}:{stopClass:"require_human_confirmation",recommendedAction:"admit_wave",reasonCode:"no_selected_next_target",recommendedDecision:"select_the_next_wave_or_hold_the_topic",recommendationRationale:"The loop cannot choose among possible next waves without manager judgement.",nextCommandRef:commandRef(["wave","select",loaded.topicId,"<wave-id>"])})}}if(!selectedWave)return{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),decision:buildTopicStepDecision(loaded.topic,null,{stopClass:"blocked",recommendedAction:"no_action",reasonCode:"selected_next_target_does_not_resolve",recommendedDecision:"repair_topic_selected_next_target",recommendationRationale:`selected_next_target does not resolve to a declared wave: ${loaded.topic.selected_next_target}`})};const graphReport=await validateTopicGraph(projectRoot,input);return{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),decision:await buildDecisionForSelectedWave(projectRoot,loaded,graphReport,selectedWave)}}const RUN_ID_PATTERN=/^[a-z0-9]+(?:-[a-z0-9]+)*$/;function validateRunId(value){return typeof value=="string"&&RUN_ID_PATTERN.test(value)}function runLedgerFilename(runId){return`run-ledger-${runId}.yaml`}function runEventFilename(runId,eventIndex,eventKind){return`run-event-${runId}-${String(eventIndex).padStart(4,"0")}-${eventKind}.yaml`}function stopClassToRunStatus(stopClass){return stopClass==="continue"?"running":stopClass==="require_human_confirmation"?"awaiting_human_confirmation":stopClass==="await_external_evidence"?"awaiting_external_evidence":stopClass==="blocked"?"blocked":stopClass==="completed"?"completed":"blocked"}function retryPostureForEvent(event){return event.stop_class==="require_human_confirmation"?"retry_forbidden_until_human_gate":event.stop_class==="blocked"?"retry_requires_new_packet":event.stop_class==="await_external_evidence"?"retry_allowed_same_command":"not_applicable"}function normalizeArtifactRefs(input){const refs={};for(const[key,value]of Object.entries(input??{}))typeof key=="string"&&key.length>0&&typeof value=="string"&&value.length>0&&(refs[key]=value);return refs}async function validatePortableRefExists(projectRoot,ref,label){return path.isAbsolute(ref)?`${label} must be project-relative: ${ref}`:(await pathExists(path.join(projectRoot,ref)))?.isFile()?null:`${label} does not resolve to a file: ${ref}`}async function loadTopicRunEvents(topicDir,runId){const entries=await readdir(topicDir,{withFileTypes:!0}),events=[];for(const entry of entries){if(!entry.isFile()||!entry.name.startsWith(`run-event-${runId}-`)||!entry.name.endsWith(".yaml"))continue;const eventPath=path.join(topicDir,entry.name),eventText=await readTextIfFile(eventPath),event=parseYamlText(eventText??"");!event||typeof event!="object"||event.run_id!==runId||events.push({event,eventRef:entry.name,eventPath})}return events.sort((left,right)=>{const leftIndex=Number(left.event.event_index??0),rightIndex=Number(right.event.event_index??0);return leftIndex-rightIndex||left.eventRef.localeCompare(right.eventRef)})}function latestArtifactRef(events,key){for(const entry of[...events].reverse()){const value=entry.event.artifact_refs?.[key];if(typeof value=="string"&&value.length>0)return value}return null}function buildCurrentHumanGate(events){const gateClosingEvents=new Set(["human_gate_resolved","wave_closed","topic_closed"]);for(const entry of[...events].reverse()){const event=entry.event;if(gateClosingEvents.has(event.event_kind))return null;if(event.stop_class==="require_human_confirmation"||event.event_kind==="human_gate_opened")return{event_ref:entry.eventRef,wave_id:event.wave_id??null,recommended_action:event.recommended_action,summary:event.summary,source_ref:event.source_ref}}return null}function buildTopicRunLedgerProjection(topic,runId,events,updatedAt){const latest=events.at(-1)??null,latestEvent=latest?.event??null;return{ledger_id:`${topic.topic_id}:${runId}`,topic_id:topic.topic_id,run_id:runId,kind:"topic-run-ledger",run_status:latestEvent?stopClassToRunStatus(latestEvent.stop_class):"running",event_count:events.length,event_refs:events.map(entry=>entry.eventRef),latest_event_ref:latest?.eventRef??null,current_wave_id:latestEvent?.wave_id??topic.selected_next_target??null,latest_decision_ref:latestArtifactRef(events,"decision_ref"),latest_packet_ref:latestArtifactRef(events,"packet_ref"),latest_prompt_ref:latestArtifactRef(events,"prompt_ref"),latest_result_ref:latestArtifactRef(events,"result_ref"),latest_closeout_ref:latestArtifactRef(events,"closeout_ref"),current_human_gate:buildCurrentHumanGate(events),retry_posture:latestEvent?retryPostureForEvent(latestEvent):"not_applicable",updated_at:updatedAt}}async function writeTopicRunLedger(projectRoot,loaded,runId,events,updatedAt){const ledger=buildTopicRunLedgerProjection(loaded.topic,runId,events,updatedAt),ledgerPath=path.join(loaded.topicDir,runLedgerFilename(runId));return await writeFile(ledgerPath,YAML.stringify(ledger),"utf8"),{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),runId,ledgerRef:toPortableRelativePath(path.relative(projectRoot,ledgerPath)),ledger}}async function initTopicRunLedger(projectRoot,input,runId,startedAt=new Date().toISOString()){if(!validateRunId(runId))return{ok:!1,error:`Topic run ledger refused: invalid run id ${runId}`};const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const ledgerPath=path.join(loaded.topicDir,runLedgerFilename(runId));if(await readTextIfFile(ledgerPath)!==null)return{ok:!1,error:`Topic run ledger already exists: ${runId}`};const report=await writeTopicRunLedger(projectRoot,loaded,runId,[],startedAt);return{...report,runStatus:report.ledger.run_status,eventCount:report.ledger.event_count}}async function recordTopicRunEvent(projectRoot,input,options){if(!validateRunId(options.runId))return{ok:!1,error:`Topic run event refused: invalid run id ${options.runId}`};const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const authority=await loadTopicRuntimeAuthority(projectRoot);if(!authority.topicRunLedger.eventKinds.includes(options.eventKind))return{ok:!1,error:`Topic run event refused: unsupported event kind ${options.eventKind}`};if(!authority.topicStepDecision.stopClasses.includes(options.stopClass))return{ok:!1,error:`Topic run event refused: unsupported stop class ${options.stopClass}`};if(!authority.topicStepDecision.recommendedActions.includes(options.recommendedAction))return{ok:!1,error:`Topic run event refused: unsupported recommended action ${options.recommendedAction}`};if(!isIsoUtcTimestamp(options.recordedAt))return{ok:!1,error:`Topic run event refused: --verified-at must be an ISO-8601 UTC timestamp: ${options.recordedAt}`};if(!options.sourceRef||!options.summary)return{ok:!1,error:"Topic run event refused: --source and --summary are required"};const sourceError=await validatePortableRefExists(projectRoot,options.sourceRef,"source_ref");if(sourceError)return{ok:!1,error:`Topic run event refused: ${sourceError}`};const ledgerPath=path.join(loaded.topicDir,runLedgerFilename(options.runId));if(await readTextIfFile(ledgerPath)===null)return{ok:!1,error:`Topic run event refused: run ledger does not exist: ${options.runId}`};const artifactRefs=normalizeArtifactRefs(options.artifactRefs),invalidArtifactKeys=Object.keys(artifactRefs).filter(key=>!authority.topicRunLedger.artifactRefKeys.includes(key));if(invalidArtifactKeys.length>0)return{ok:!1,error:`Topic run event refused: unsupported artifact ref keys: ${invalidArtifactKeys.join(", ")}`};for(const[key,ref]of Object.entries(artifactRefs)){const artifactError=await validatePortableRefExists(projectRoot,ref,key);if(artifactError)return{ok:!1,error:`Topic run event refused: ${artifactError}`}}const eventIndex=(await loadTopicRunEvents(loaded.topicDir,options.runId)).length+1,event={event_id:`${options.runId}:${String(eventIndex).padStart(4,"0")}:${options.eventKind}`,topic_id:loaded.topicId,run_id:options.runId,event_index:eventIndex,event_kind:options.eventKind,stop_class:options.stopClass,recommended_action:options.recommendedAction,wave_id:options.waveId??loaded.topic.selected_next_target??null,source_ref:options.sourceRef,summary:options.summary,recorded_at:options.recordedAt,artifact_refs:artifactRefs},eventPath=path.join(loaded.topicDir,runEventFilename(options.runId,eventIndex,options.eventKind));await writeFile(eventPath,YAML.stringify(event),"utf8");const updatedEvents=await loadTopicRunEvents(loaded.topicDir,options.runId),report=await writeTopicRunLedger(projectRoot,loaded,options.runId,updatedEvents,options.recordedAt);return{...report,eventId:event.event_id,eventRef:toPortableRelativePath(path.relative(projectRoot,eventPath)),runStatus:report.ledger.run_status,eventCount:report.ledger.event_count}}async function buildTopicRunLedger(projectRoot,input,runId,updatedAt=new Date().toISOString()){if(!validateRunId(runId))return{ok:!1,error:`Topic run ledger refused: invalid run id ${runId}`};const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const ledgerPath=path.join(loaded.topicDir,runLedgerFilename(runId));if(await readTextIfFile(ledgerPath)===null)return{ok:!1,error:`Topic run ledger not found: ${runId}`};const events=await loadTopicRunEvents(loaded.topicDir,runId),report=await writeTopicRunLedger(projectRoot,loaded,runId,events,updatedAt);return{...report,runStatus:report.ledger.run_status,eventCount:report.ledger.event_count}}async function readTopicRunLedger(projectRoot,input,runId){if(!validateRunId(runId))return{ok:!1,error:`Topic run ledger refused: invalid run id ${runId}`};const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const ledgerPath=path.join(loaded.topicDir,runLedgerFilename(runId)),ledgerText=await readTextIfFile(ledgerPath);if(ledgerText===null)return{ok:!1,error:`Topic run ledger not found: ${runId}`};const ledger=parseYamlText(ledgerText);return{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),runId,ledgerRef:toPortableRelativePath(path.relative(projectRoot,ledgerPath)),ledger,runStatus:ledger.run_status,eventCount:ledger.event_count}}function promptFilename(packetId,role){return`prompt-${packetId}-${role}.md`}async function dispatchTopicPacket(projectRoot,input,packetId,role){const loaded=await loadTopicPacket(projectRoot,input,packetId);if(!loaded.ok)return loaded;const wave=getTopicWaves(loaded.topic).find(entry=>entry.wave_id===loaded.packet.wave_id);if(!wave)return{ok:!1,error:`Packet wave_id does not resolve inside topic: ${loaded.packet.wave_id}`};if(["retired","superseded","closed"].includes(wave.state))return{ok:!1,error:`Wave is not dispatchable: ${wave.wave_id} (${wave.state})`};if(!["candidate","admitted","preflight","dispatched"].includes(loaded.packet.status))return{ok:!1,error:`Packet is not dispatchable from status ${loaded.packet.status}`};const promptPath=path.join(loaded.topicDir,promptFilename(packetId,role));return await writeFile(promptPath,buildDispatchPrompt(loaded.packet,loaded.topicId,role),"utf8"),loaded.packet.status="dispatched",await writeFile(loaded.packetPath,packetMarkdown(loaded.packet),"utf8"),role==="worker"&&["preflight_admitted","implementation_admitted","continuation_packet_open"].includes(wave.state)&&(wave.state="implementation_active",loaded.topic.waves=getTopicWaves(loaded.topic).map(entry=>entry.wave_id===wave.wave_id?wave:entry),loaded.topic.last_transition_at=buildTopicNow(),loaded.topic.last_transition_reason=`packet_${packetId}_worker_dispatched`,await writeTopicYaml(loaded.topicYamlPath,loaded.topic)),{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),packetId,packetRef:toPortableRelativePath(path.relative(projectRoot,loaded.packetPath)),promptRef:toPortableRelativePath(path.relative(projectRoot,promptPath)),waveId:wave.wave_id,waveState:wave.state,role}}function resultMarkdown(result,sourceText){return`---
42
- ${YAML.stringify(result).trimEnd()}
43
- ---
44
-
45
- # Result ${result.result_id}
46
-
47
- ${sourceText??""}`.trimEnd()+`
48
- `}async function recordTopicResult(projectRoot,input,resultKind,verdict,fromPath,verifiedAt){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const authority=await loadTopicRuntimeAuthority(projectRoot),sourcePath=path.resolve(projectRoot,fromPath),sourceText=await readTextIfFile(sourcePath);if(sourceText===null)return{ok:!1,error:`Result source not found: ${fromPath}`};if(!authority.resultKinds.includes(resultKind))return{ok:!1,error:`Unsupported result kind: ${resultKind}`};if(!authority.resultVerdicts.includes(verdict))return{ok:!1,error:`Unsupported result verdict: ${verdict}`};if(authority.resultVerifiedAtFormat==="iso8601_utc_timestamp"&&!isIsoUtcTimestamp(verifiedAt))return{ok:!1,error:`Result verified_at must be an ISO-8601 UTC timestamp: ${verifiedAt}`};const waveId=loaded.topic.selected_next_target,wave=getTopicWaves(loaded.topic).find(entry=>entry.wave_id===waveId)??null;if(!wave)return{ok:!1,error:"Result recording requires a selected wave in topic.selected_next_target"};if((await collectWaveArtifactEvidence(loaded.topicDir,wave.wave_id)).packetRefs.length===0)return{ok:!1,error:`Result recording requires at least one packet lineage for ${wave.wave_id}`};const resultId=`${wave.wave_id}-${resultKind}`,result={result_id:resultId,topic_id:loaded.topicId,wave_id:wave.wave_id,result_kind:resultKind,verdict,verified_at:verifiedAt,source_ref:toPortableRelativePath(path.relative(projectRoot,sourcePath))},resultPath=path.join(loaded.topicDir,resultFilename(wave.wave_id,wave.slug,resultKind));return await writeFile(resultPath,resultMarkdown(result,sourceText),"utf8"),verdict==="OVERFLOW"?wave.state="overflowed":verdict==="NEEDS_REVISION"||verdict==="FAIL"?wave.state="needs_revision":verdict==="PASS"&&wave.state==="preflight_admitted"&&(wave.state="implementation_admitted"),loaded.topic.waves=getTopicWaves(loaded.topic).map(entry=>entry.wave_id===wave.wave_id?wave:entry),loaded.topic.last_transition_at=buildTopicNow(),loaded.topic.last_transition_reason=`recorded_${resultKind}_${verdict}_for_${wave.wave_id}`,await writeTopicYaml(loaded.topicYamlPath,loaded.topic),{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),resultId,resultRef:toPortableRelativePath(path.relative(projectRoot,resultPath)),waveId:wave.wave_id,waveState:wave.state,verdict,resultKind}}function remediationMarkdown(remediation){return`---
49
- ${YAML.stringify(remediation).trimEnd()}
50
- ---
51
-
52
- # Remediation ${remediation.remediation_id}
53
-
54
- Opened by \`nimicoding topic remediation open\`.
55
- `}async function openTopicRemediation(projectRoot,input,options){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const authority=await loadTopicRuntimeAuthority(projectRoot);if(!topicHasEnrichedShape(loaded.topic,authority))return{ok:!1,error:"Remediation commands require an enriched topic root."};if(!authority.remediationKinds.includes(options.kind))return{ok:!1,error:`Unsupported remediation kind: ${options.kind}`};const waveId=loaded.topic.selected_next_target,wave=getTopicWaves(loaded.topic).find(entry=>entry.wave_id===waveId)??null;if(!wave)return{ok:!1,error:"Remediation open requires a selected wave in topic.selected_next_target"};if(["retired","superseded","closed"].includes(wave.state))return{ok:!1,error:`Wave is not remediation-eligible: ${wave.wave_id} (${wave.state})`};if(options.kind==="continuation"&&wave.state!=="overflowed")return{ok:!1,error:`Continuation remediation requires an overflowed wave, found ${wave.state}`};if(options.kind==="continuation"&&!options.overflowedPacketId)return{ok:!1,error:"Continuation remediation requires --overflowed-packet lineage"};if(options.overflowedPacketId){const overflowedPacket=await loadTopicPacket(projectRoot,input,options.overflowedPacketId);if(!overflowedPacket.ok)return{ok:!1,error:`Overflowed packet lineage could not be loaded: ${options.overflowedPacketId}`};if(overflowedPacket.packet.wave_id!==wave.wave_id)return{ok:!1,error:`Overflowed packet does not belong to the selected wave (${overflowedPacket.packet.wave_id} vs ${wave.wave_id})`}}const remediationId=`${wave.wave_id}-remediation-${options.kind}-${options.reason}`,remediation={remediation_id:remediationId,topic_id:loaded.topicId,wave_id:wave.wave_id,kind:options.kind,reason:options.reason};options.overflowedPacketId&&(remediation.overflowed_packet_id=options.overflowedPacketId);const remediationPath=path.join(loaded.topicDir,remediationFilename(wave.wave_id,options.kind,options.reason));return await writeFile(remediationPath,remediationMarkdown(remediation),"utf8"),options.kind!=="continuation"&&wave.state!=="needs_revision"&&(wave.state="needs_revision",loaded.topic.waves=getTopicWaves(loaded.topic).map(entry=>entry.wave_id===wave.wave_id?wave:entry)),loaded.topic.last_transition_at=buildTopicNow(),loaded.topic.last_transition_reason=`opened_remediation_${options.kind}_${options.reason}_for_${wave.wave_id}`,await writeTopicYaml(loaded.topicYamlPath,loaded.topic),{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),remediationId,remediationRef:toPortableRelativePath(path.relative(projectRoot,remediationPath)),waveId:wave.wave_id,waveState:wave.state,kind:options.kind,reason:options.reason}}function overflowContinuationMarkdown(continuation){return`---
56
- ${YAML.stringify(continuation).trimEnd()}
57
- ---
58
-
59
- # Overflow Continuation ${continuation.continuation_packet_id}
60
-
61
- Recorded by \`nimicoding topic overflow continue\`.
62
- `}async function continueTopicOverflow(projectRoot,input,options){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const authority=await loadTopicRuntimeAuthority(projectRoot);if(!topicHasEnrichedShape(loaded.topic,authority))return{ok:!1,error:"Overflow continuation requires an enriched topic root."};const waveId=loaded.topic.selected_next_target,wave=getTopicWaves(loaded.topic).find(entry=>entry.wave_id===waveId)??null;if(!wave)return{ok:!1,error:"Overflow continuation requires a selected wave in topic.selected_next_target"};if(wave.state!=="overflowed")return{ok:!1,error:`Overflow continuation requires an overflowed wave, found ${wave.state}`};if(options.sameOwnerDomain!==!0)return{ok:!1,error:"Overflow continuation requires explicit same-owner-domain acknowledgement"};const overflowedPacket=await loadTopicPacket(projectRoot,input,options.overflowedPacketId);if(!overflowedPacket.ok)return{ok:!1,error:`Overflowed packet lineage could not be loaded: ${options.overflowedPacketId}`};if(overflowedPacket.packet.wave_id!==wave.wave_id)return{ok:!1,error:`Overflowed packet does not belong to the selected wave (${overflowedPacket.packet.wave_id} vs ${wave.wave_id})`};const continuationPacket=await loadTopicPacket(projectRoot,input,options.continuationPacketId);if(!continuationPacket.ok)return{ok:!1,error:`Continuation packet could not be loaded: ${options.continuationPacketId}`};if(continuationPacket.packet.wave_id!==wave.wave_id)return{ok:!1,error:`Continuation packet does not belong to the selected wave (${continuationPacket.packet.wave_id} vs ${wave.wave_id})`};const continuation={topic_id:loaded.topicId,wave_id:wave.wave_id,overflowed_packet_id:options.overflowedPacketId,manager_judgement:options.managerJudgement,continuation_packet_id:options.continuationPacketId,same_owner_domain:!0},continuationPath=path.join(loaded.topicDir,overflowContinuationFilename(wave.wave_id,options.continuationPacketId));return await writeFile(continuationPath,overflowContinuationMarkdown(continuation),"utf8"),wave.state="continuation_packet_open",loaded.topic.waves=getTopicWaves(loaded.topic).map(entry=>entry.wave_id===wave.wave_id?wave:entry),loaded.topic.last_transition_at=buildTopicNow(),loaded.topic.last_transition_reason=`continued_overflow_for_${wave.wave_id}_via_${options.continuationPacketId}`,await writeTopicYaml(loaded.topicYamlPath,loaded.topic),{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),waveId:wave.wave_id,waveState:wave.state,overflowedPacketId:options.overflowedPacketId,continuationPacketId:options.continuationPacketId,continuationRef:toPortableRelativePath(path.relative(projectRoot,continuationPath))}}async function createDecisionReview(projectRoot,input,slug,options){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const authority=await loadTopicRuntimeAuthority(projectRoot);if(!validateTopicSlug(slug))return{ok:!1,error:`Decision review slug must be lowercase kebab-case: ${slug}`};if(!authority.decisionDispositions.includes(options.disposition))return{ok:!1,error:`Unsupported decision disposition: ${options.disposition}`};const review={decision_review_id:slug,topic_id:loaded.topicId,date:options.date,decision:options.decision,replaced_scope:options.replacedScope,active_replacement_scope:options.activeReplacementScope,disposition:options.disposition};if(options.targetWaveId&&!getTopicWaves(loaded.topic).find(entry=>entry.wave_id===options.targetWaveId))return{ok:!1,error:`Decision review target wave does not exist: ${options.targetWaveId}`};if(options.activeReplacementScope!=="topic_design_baseline"&&options.activeReplacementScope!==null&&!getTopicWaves(loaded.topic).some(entry=>entry.wave_id===options.activeReplacementScope))return{ok:!1,error:`Decision review active replacement scope must be machine-identifiable: ${options.activeReplacementScope}`};const reviewPath=path.join(loaded.topicDir,decisionReviewFilename(slug));if(await writeFile(reviewPath,`---
63
- ${YAML.stringify(review).trimEnd()}
64
- ---
65
-
66
- # Decision Review ${slug}
67
- `,"utf8"),options.targetWaveId){const waves=getTopicWaves(loaded.topic).map(entry=>entry.wave_id!==options.targetWaveId?entry:options.disposition==="retired"?{...entry,state:"retired",selected:!1}:options.disposition==="superseded"?{...entry,state:"superseded",selected:!1}:entry);loaded.topic.waves=waves,loaded.topic.selected_next_target===options.targetWaveId&&(loaded.topic.selected_next_target=options.activeReplacementScope),loaded.topic.last_transition_at=buildTopicNow(),loaded.topic.last_transition_reason=`decision_review_${slug}`,await writeTopicYaml(loaded.topicYamlPath,loaded.topic)}return{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),decisionReviewId:slug,decisionReviewRef:toPortableRelativePath(path.relative(projectRoot,reviewPath)),disposition:options.disposition,targetWaveId:options.targetWaveId??null}}async function holdTopicInPending(projectRoot,input,options){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const authority=await loadTopicRuntimeAuthority(projectRoot);if(!topicHasEnrichedShape(loaded.topic,authority))return{ok:!1,error:"Topic hold requires an enriched topic root."};if(loaded.topic.state!=="ongoing")return{ok:!1,error:`Topic hold requires ongoing state, found ${loaded.topic.state}`};const blockers=getPendingEntryBlockers(loaded.topic);if(blockers.length>0)return{ok:!1,error:`Topic hold requires no active implementation wave, found ${blockers.join(", ")}`};if(!options.reopenCriteria&&!options.closeTrigger)return{ok:!1,error:"Topic hold requires explicit reopen criteria or close trigger."};const pendingNote={pending_note_id:`pending-${loaded.topicId}`,topic_id:loaded.topicId,entered_from_state:loaded.topic.state,reason:options.reason,summary:options.summary,status:"active"};options.reopenCriteria&&(pendingNote.reopen_criteria=options.reopenCriteria),options.closeTrigger&&(pendingNote.close_trigger=options.closeTrigger);const notePath=path.join(loaded.topicDir,pendingNoteFilename());await writeFile(notePath,pendingNoteMarkdown(pendingNote),"utf8"),loaded.topic.state="pending",loaded.topic.last_transition_at=buildTopicNow(),loaded.topic.last_transition_reason=`entered_pending_${options.reason}`;const moved=await moveTopicDirectoryForState(projectRoot,loaded.topicDir,loaded.topicId,"pending");return await writeTopicYaml(moved.topicYamlPath,loaded.topic),{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,moved.topicDir)),state:loaded.topic.state,pendingNoteRef:toPortableRelativePath(path.relative(projectRoot,path.join(moved.topicDir,pendingNoteFilename()))),reason:options.reason}}async function resumePendingTopic(projectRoot,input,options){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const authority=await loadTopicRuntimeAuthority(projectRoot);if(!topicHasEnrichedShape(loaded.topic,authority))return{ok:!1,error:"Topic resume requires an enriched topic root."};if(loaded.topic.state!=="pending")return{ok:!1,error:`Topic resume requires pending state, found ${loaded.topic.state}`};const pendingNoteLoaded=await loadPendingNote(loaded.topicDir);if(!pendingNoteLoaded.ok)return{ok:!1,error:pendingNoteLoaded.error};const pendingNote=pendingNoteLoaded.note;if(!pendingNote.reopen_criteria)return{ok:!1,error:"Topic resume requires pending note reopen criteria."};const selectedWave=getTopicWaves(loaded.topic).find(entry=>entry.selected===!0)??null;if(!(typeof loaded.topic.selected_next_target=="string"&&loaded.topic.selected_next_target!=="topic_design_baseline"&&selectedWave!==null&&selectedWave.wave_id===loaded.topic.selected_next_target))return{ok:!1,error:"Topic resume requires exactly one selected next execution target before reopening."};pendingNote.status="resumed",pendingNote.last_resumed_at=buildTopicNow(),pendingNote.last_resume_reason=options.criteriaMet,await writeFile(pendingNoteLoaded.notePath,pendingNoteMarkdown(pendingNote),"utf8"),loaded.topic.state="ongoing",loaded.topic.last_transition_at=buildTopicNow(),loaded.topic.last_transition_reason="resumed_from_pending_after_reopen_criteria_met";const moved=await moveTopicDirectoryForState(projectRoot,loaded.topicDir,loaded.topicId,"ongoing");return await writeTopicYaml(moved.topicYamlPath,loaded.topic),{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,moved.topicDir)),state:loaded.topic.state,pendingNoteRef:toPortableRelativePath(path.relative(projectRoot,path.join(moved.topicDir,pendingNoteFilename()))),criteriaMet:options.criteriaMet}}function closeoutMarkdown(closeout,title){return`---
68
- ${YAML.stringify(closeout).trimEnd()}
69
- ---
70
-
71
- # ${title}
72
-
73
- Recorded by \`nimicoding topic closeout\`.
74
- `}function trueCloseAuditMarkdown(audit,judgementText){return`---
75
- ${YAML.stringify(audit).trimEnd()}
76
- ---
77
-
78
- # Topic True-Close Audit
79
-
80
- ${judgementText}
81
- `}function trueCloseRecordMarkdown(record){return`---
82
- ${YAML.stringify(record).trimEnd()}
83
- ---
84
-
85
- # Topic True-Close
86
-
87
- Recorded by \`nimicoding topic closeout topic\`.
88
- `}function readFrontmatterObject(text){const parsed=parsePacketDraft(text);return parsed&&typeof parsed=="object"?parsed:null}async function buildWaveClosureChecks(projectRoot,topicDir,topic,wave,closeout){const authority=await loadTopicRuntimeAuthority(projectRoot),evidence=await collectWaveArtifactEvidence(topicDir,wave.wave_id),checks=[];checks.push({id:"closeout_scope_wave",ok:closeout.scope==="wave"&&authority.closeoutScopes.includes(closeout.scope),reason:closeout.scope==="wave"?"closeout scope is wave":`closeout scope must be wave, found ${closeout.scope??"missing"}`}),checks.push({id:"closeout_topic_matches",ok:closeout.topic_id===topic.topic_id,reason:closeout.topic_id===topic.topic_id?"closeout topic_id matches the topic":`closeout topic_id does not match topic (${closeout.topic_id??"missing"} vs ${topic.topic_id})`});const closurePairs=authority.closureDimensions.map(dimension=>[`${dimension}_closure`,closeout[`${dimension}_closure`]]);for(const[field,value]of closurePairs)checks.push({id:`${field}_explicit_closed`,ok:value==="closed"&&authority.closureStates.includes(value),reason:value==="closed"?`${field} is explicitly closed`:`${field} must be closed for wave closeout, found ${value??"missing"}`});checks.push({id:"closeout_disposition_complete",ok:closeout.disposition==="complete"&&authority.closeoutDispositions.includes(closeout.disposition),reason:closeout.disposition==="complete"?"closeout disposition is complete":`closeout disposition must be complete for wave closeout, found ${closeout.disposition??"missing"}`});const activeBlockers=["needs_revision","overflowed","continuation_packet_open"].includes(wave.state);checks.push({id:"wave_has_no_active_blockers",ok:!activeBlockers,reason:activeBlockers?`wave remains in an active blocker state: ${wave.state}`:"wave has no active blocker state"});const closeableState=["implementation_active","closed"].includes(wave.state);return checks.push({id:"wave_state_closeable",ok:closeableState,reason:closeableState?"wave state remains eligible for closeout":`wave closeout requires implementation_active or closed, found ${wave.state}`}),authority.waveCloseoutEvidence.requirePacketLineage&&checks.push({id:"wave_packet_lineage_exists",ok:evidence.packetRefs.length>0,reason:evidence.packetRefs.length>0?"wave closeout has packet lineage evidence":`wave closeout requires packet lineage evidence for ${wave.wave_id}`}),authority.waveCloseoutEvidence.requireResultLineage&&checks.push({id:"wave_result_lineage_exists",ok:evidence.resultRefs.length>0,reason:evidence.resultRefs.length>0?"wave closeout has result lineage evidence":`wave closeout requires result lineage evidence for ${wave.wave_id}`}),checks}async function validateWaveClosure(projectRoot,input,waveId){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return{ok:!1,error:loaded.error,checks:[],warnings:[]};const rootValidation=await validateTopicRoot(projectRoot,input),wave=getTopicWaves(loaded.topic).find(entry=>entry.wave_id===waveId)??null,checks=[...rootValidation.checks??[]],warnings=[...rootValidation.warnings??[]];if(checks.push({id:"wave_exists",ok:wave!==null,reason:wave?"wave exists in topic.yaml waves[]":`wave does not exist: ${waveId}`}),!wave)return{...rootValidation,ok:!1,checks,warnings};const closeoutPath=path.join(loaded.topicDir,waveCloseoutFilename(waveId)),closeoutText=await readTextIfFile(closeoutPath);if(checks.push({id:"wave_closeout_artifact_exists",ok:closeoutText!==null,reason:closeoutText!==null?"wave closeout artifact exists":`missing wave closeout artifact: ${waveCloseoutFilename(waveId)}`}),closeoutText===null)return{...rootValidation,ok:!1,checks,warnings,waveId};const closeout=readFrontmatterObject(closeoutText);return checks.push({id:"wave_closeout_frontmatter_valid",ok:closeout!==null,reason:closeout!==null?"wave closeout frontmatter is valid":"wave closeout artifact frontmatter is invalid"}),closeout===null?{...rootValidation,ok:!1,checks,warnings,waveId}:(checks.push(...await buildWaveClosureChecks(projectRoot,loaded.topicDir,loaded.topic,wave,closeout)),{...rootValidation,ok:rootValidation.ok&&checks.every(entry=>entry.ok),checks,warnings,waveId,closeoutRef:toPortableRelativePath(path.relative(projectRoot,closeoutPath))})}async function closeoutWaveInTopic(projectRoot,input,waveId,options){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const authority=await loadTopicRuntimeAuthority(projectRoot);if(!topicHasEnrichedShape(loaded.topic,authority))return{ok:!1,error:"Wave closeout requires an enriched topic root."};const wave=getTopicWaves(loaded.topic).find(entry=>entry.wave_id===waveId)??null;if(!wave)return{ok:!1,error:`Wave not found: ${waveId}`};const closeout={closeout_id:waveId,topic_id:loaded.topicId,scope:"wave",authority_closure:options.authorityClosure,semantic_closure:options.semanticClosure,consumer_closure:options.consumerClosure,drift_resistance_closure:options.driftResistanceClosure,disposition:options.disposition},checks=await buildWaveClosureChecks(projectRoot,loaded.topicDir,loaded.topic,wave,closeout);if(!checks.every(entry=>entry.ok))return{ok:!1,error:`Wave closeout refused: ${checks.find(entry=>!entry.ok)?.reason??"closure validation failed"}`,checks,warnings:[],topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),waveId};const closeoutPath=path.join(loaded.topicDir,waveCloseoutFilename(waveId));return await writeFile(closeoutPath,closeoutMarkdown(closeout,`Wave Closeout ${waveId}`),"utf8"),wave.state="closed",wave.selected=!1,loaded.topic.waves=getTopicWaves(loaded.topic).map(entry=>entry.wave_id===waveId?wave:entry),loaded.topic.selected_next_target===waveId&&(loaded.topic.selected_next_target=null),loaded.topic.last_transition_at=buildTopicNow(),loaded.topic.last_transition_reason=`closed_${waveId}`,await writeTopicYaml(loaded.topicYamlPath,loaded.topic),{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),waveId,waveState:wave.state,closeoutRef:toPortableRelativePath(path.relative(projectRoot,closeoutPath))}}async function buildTrueCloseAuditChecks(projectRoot,topicDir,topic){const authority=await loadTopicRuntimeAuthority(projectRoot),waves=getTopicWaves(topic),checks=[],nonTerminalWaves=waves.filter(entry=>!["closed","retired","superseded"].includes(entry.state)).map(entry=>`${entry.wave_id}:${entry.state}`);checks.push({id:"all_waves_terminal",ok:nonTerminalWaves.length===0,reason:nonTerminalWaves.length===0?"all waves are closed, retired, or superseded":`non-terminal waves remain: ${nonTerminalWaves.join(", ")}`});const selectedActive=waves.filter(entry=>entry.selected===!0).map(entry=>entry.wave_id);checks.push({id:"no_selected_wave_remains",ok:selectedActive.length===0,reason:selectedActive.length===0?"no selected wave remains active":`selected waves remain: ${selectedActive.join(", ")}`}),checks.push({id:"selected_target_cleared",ok:topic.selected_next_target===null||topic.selected_next_target==="topic_design_baseline",reason:topic.selected_next_target===null||topic.selected_next_target==="topic_design_baseline"?"selected_next_target is cleared for topic closeout":`selected_next_target remains active: ${topic.selected_next_target}`});for(const wave of waves.filter(entry=>entry.state==="closed")){const evidence=await collectWaveArtifactEvidence(topicDir,wave.wave_id);authority.trueCloseAuditEvidence.requireWaveCloseoutForClosedWaves&&checks.push({id:`wave_closeout_exists_${wave.wave_id}`,ok:evidence.closeoutRefs.length>0,reason:evidence.closeoutRefs.length>0?`${wave.wave_id} has closeout evidence`:`${wave.wave_id} is closed but has no wave closeout evidence`}),authority.trueCloseAuditEvidence.requirePacketLineageForClosedWaves&&checks.push({id:`wave_packet_lineage_exists_${wave.wave_id}`,ok:evidence.packetRefs.length>0,reason:evidence.packetRefs.length>0?`${wave.wave_id} has packet lineage evidence`:`${wave.wave_id} is closed but has no packet lineage evidence`}),authority.trueCloseAuditEvidence.requireResultLineageForClosedWaves&&checks.push({id:`wave_result_lineage_exists_${wave.wave_id}`,ok:evidence.resultRefs.length>0,reason:evidence.resultRefs.length>0?`${wave.wave_id} has result lineage evidence`:`${wave.wave_id} is closed but has no result lineage evidence`})}return checks}async function runTopicTrueCloseAudit(projectRoot,input,judgementText){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const authority=await loadTopicRuntimeAuthority(projectRoot);if(!topicHasEnrichedShape(loaded.topic,authority))return{ok:!1,error:"True-close audit requires an enriched topic root."};const checks=await buildTrueCloseAuditChecks(projectRoot,loaded.topicDir,loaded.topic),passed=checks.every(entry=>entry.ok),auditPath=path.join(loaded.topicDir,topicTrueCloseAuditFilename()),judgementPath=path.join(loaded.topicDir,topicTrueCloseJudgementFilename()),audit={topic_id:loaded.topicId,status:passed?"passed":"pending",checks};return await writeFile(auditPath,trueCloseAuditMarkdown(audit,judgementText),"utf8"),await writeFile(judgementPath,`---
89
- ${YAML.stringify({topic_id:loaded.topicId,status:passed?"passed":"pending",judgement:judgementText}).trimEnd()}
90
- ---
91
-
92
- # Topic True-Close Audit Result
93
- `,"utf8"),loaded.topic.last_transition_at=buildTopicNow(),loaded.topic.last_transition_reason="ran_topic_true_close_audit",passed&&(loaded.topic.current_true_close_status="pending"),await writeTopicYaml(loaded.topicYamlPath,loaded.topic),{ok:passed,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir)),status:passed?"passed":"pending",auditRef:toPortableRelativePath(path.relative(projectRoot,auditPath)),judgementRef:toPortableRelativePath(path.relative(projectRoot,judgementPath)),checks,warnings:[]}}async function closeoutTopicInTopic(projectRoot,input,options){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return loaded;const authority=await loadTopicRuntimeAuthority(projectRoot);if(!topicHasEnrichedShape(loaded.topic,authority))return{ok:!1,error:"Topic closeout requires an enriched topic root."};let pendingNoteLoaded=null;if(loaded.topic.state==="pending"){if(pendingNoteLoaded=await loadPendingNote(loaded.topicDir),!pendingNoteLoaded.ok)return{ok:!1,error:`Topic closeout from pending requires a pending note artifact: ${pendingNoteLoaded.error}`};if(typeof pendingNoteLoaded.note.close_trigger!="string"||pendingNoteLoaded.note.close_trigger.length===0)return{ok:!1,error:"Topic closeout from pending requires an explicit close trigger in pending-note.md."}}const auditPath=path.join(loaded.topicDir,topicTrueCloseAuditFilename()),judgementPath=path.join(loaded.topicDir,topicTrueCloseJudgementFilename()),auditText=await readTextIfFile(auditPath),judgementText=await readTextIfFile(judgementPath);if(auditText===null||judgementText===null)return{ok:!1,error:"Topic closeout requires a recorded true-close audit and judgement."};const audit=readFrontmatterObject(auditText);if(!audit||audit.status!=="passed")return{ok:!1,error:"Topic closeout requires a passed true-close audit."};const auditChecks=await buildTrueCloseAuditChecks(projectRoot,loaded.topicDir,loaded.topic);if(!auditChecks.every(entry=>entry.ok))return{ok:!1,error:`Topic closeout refused: ${auditChecks.find(entry=>!entry.ok)?.reason??"true-close checks failed"}`,checks:auditChecks,warnings:[],topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,loaded.topicDir))};const closeout={closeout_id:loaded.topicId,topic_id:loaded.topicId,scope:"topic",authority_closure:options.authorityClosure,semantic_closure:options.semanticClosure,consumer_closure:options.consumerClosure,drift_resistance_closure:options.driftResistanceClosure,disposition:options.disposition};if([closeout.authority_closure,closeout.semantic_closure,closeout.consumer_closure,closeout.drift_resistance_closure].some(entry=>entry!=="closed")||closeout.disposition!=="complete")return{ok:!1,error:"Topic closeout requires all four closures to be closed and disposition=complete."};const closeoutPath=path.join(loaded.topicDir,topicCloseoutFilename());await writeFile(closeoutPath,closeoutMarkdown(closeout,"Topic Closeout"),"utf8");const trueCloseRecordPath=path.join(loaded.topicDir,topicTrueCloseRecordFilename());await writeFile(trueCloseRecordPath,trueCloseRecordMarkdown({topic_id:loaded.topicId,status:"passed",audit_ref:toPortableRelativePath(path.relative(projectRoot,auditPath)),judgement_ref:toPortableRelativePath(path.relative(projectRoot,judgementPath))}),"utf8"),loaded.topic.state="closed",loaded.topic.current_true_close_status="true_closed",loaded.topic.last_transition_at=buildTopicNow(),loaded.topic.last_transition_reason="closed_topic_after_true_close";const moved=await moveTopicDirectoryForState(projectRoot,loaded.topicDir,loaded.topicId,"closed");return await writeTopicYaml(moved.topicYamlPath,loaded.topic),pendingNoteLoaded?.ok&&(pendingNoteLoaded.note.status="closed",pendingNoteLoaded.note.closed_at=buildTopicNow(),await writeFile(path.join(moved.topicDir,pendingNoteFilename()),pendingNoteMarkdown(pendingNoteLoaded.note),"utf8")),{ok:!0,topicId:loaded.topicId,topicRef:toPortableRelativePath(path.relative(projectRoot,moved.topicDir)),state:loaded.topic.state,closeoutRef:toPortableRelativePath(path.relative(projectRoot,path.join(moved.topicDir,topicCloseoutFilename()))),trueCloseRef:toPortableRelativePath(path.relative(projectRoot,path.join(moved.topicDir,topicTrueCloseRecordFilename()))),currentTrueCloseStatus:loaded.topic.current_true_close_status}}async function validateTopicRoot(projectRoot,input=null){const loaded=await loadTopicReport(projectRoot,input);if(!loaded.ok)return{ok:!1,error:loaded.error,checks:[],warnings:[]};const{topicDir,topicId,state,topic}=loaded,authority=await loadTopicRuntimeAuthority(projectRoot),validationPolicy=await loadTopicValidationPolicy(projectRoot),ignoredByPolicy=validationPolicy.ignoredTopicIds.get(topicId)??null,checks=[],warnings=[],relativeTopicDir=toPortableRelativePath(path.relative(projectRoot,topicDir)),artifactAnalysis=await analyzeTopicArtifacts(topicDir,topic),pendingNoteLoaded=await loadPendingNote(topicDir),topicIdMatchesFolder=topic.topic_id===topicId;checks.push({id:"topic_id_matches_folder",ok:topicIdMatchesFolder,reason:topicIdMatchesFolder?"topic.yaml topic_id matches the topic folder":`topic.yaml topic_id does not match folder name (${topic.topic_id??"missing"} vs ${topicId})`});const stateMatchesRoot=topic.state===state;checks.push({id:"state_matches_root",ok:stateMatchesRoot,reason:stateMatchesRoot?"topic.yaml state matches the lifecycle root":`topic.yaml state does not match lifecycle root (${topic.state??"missing"} vs ${state})`});const missingMinimalFields=authority.minimalRequiredFields.filter(field=>{const value=topic[field];return value==null||value===""});checks.push({id:"minimal_state_evidence",ok:missingMinimalFields.length===0,reason:missingMinimalFields.length===0?"topic.yaml contains the required lifecycle evidence fields":`topic.yaml is missing required lifecycle evidence fields: ${missingMinimalFields.join(", ")}`});const topicIdFormatValid=validateTopicId(topicId);checks.push({id:"topic_id_format",ok:topicIdFormatValid,reason:topicIdFormatValid?"topic id remains date-first and sortable":`topic id is not date-first and sortable: ${topicId}`});const missingRecommendedFiles=[];for(const fileName of authority.recommendedFiles)(await pathExists(path.join(topicDir,fileName)))?.isFile()||missingRecommendedFiles.push(fileName);missingRecommendedFiles.length>0&&warnings.push(`recommended topic companion files are missing: ${missingRecommendedFiles.join(", ")}`);const missingEnrichedFields=authority.enrichedRequiredFields.filter(field=>{const value=topic[field];return field==="selected_next_target"?!(value===null||value==="topic_design_baseline"||typeof value=="string"&&value.length>0):value==null||value===""||Array.isArray(value)&&value.length===0}),enumViolations=[];if(topic.mode!==void 0&&!authority.topicEnums.mode.includes(topic.mode)&&enumViolations.push(`mode=${topic.mode}`),topic.posture!==void 0&&!authority.topicEnums.posture.includes(topic.posture)&&enumViolations.push(`posture=${topic.posture}`),topic.design_policy!==void 0&&!authority.topicEnums.designPolicy.includes(topic.design_policy)&&enumViolations.push(`design_policy=${topic.design_policy}`),topic.parallel_truth!==void 0&&!authority.topicEnums.parallelTruth.includes(topic.parallel_truth)&&enumViolations.push(`parallel_truth=${topic.parallel_truth}`),topic.layering!==void 0&&!authority.topicEnums.layering.includes(topic.layering)&&enumViolations.push(`layering=${topic.layering}`),topic.risk!==void 0&&!authority.topicEnums.risk.includes(topic.risk)&&enumViolations.push(`risk=${topic.risk}`),topic.applicability!==void 0&&!authority.topicEnums.applicability.includes(topic.applicability)&&enumViolations.push(`applicability=${topic.applicability}`),topic.execution_mode!==void 0&&!authority.topicEnums.executionMode.includes(topic.execution_mode)&&enumViolations.push(`execution_mode=${topic.execution_mode}`),topic.current_true_close_status!==void 0&&!authority.topicEnums.trueCloseStatus.includes(topic.current_true_close_status)&&enumViolations.push(`current_true_close_status=${topic.current_true_close_status}`),missingEnrichedFields.length>0&&warnings.push(`topic.yaml is using the legacy minimal shape and is missing enriched fields: ${missingEnrichedFields.join(", ")}`),enumViolations.length>0&&warnings.push(`topic.yaml contains values outside the current enriched enums: ${enumViolations.join(", ")}`),checks.push({id:"result_wave_lineage_resolves",ok:artifactAnalysis.unresolvedResultWaveIds.length===0,reason:artifactAnalysis.unresolvedResultWaveIds.length===0?"result artifacts resolve to known wave lineage":`result artifacts reference unknown wave lineage: ${artifactAnalysis.unresolvedResultWaveIds.join(", ")}`}),topic.state==="pending"){const pendingNote=pendingNoteLoaded.ok?pendingNoteLoaded.note:null;if(checks.push({id:"pending_note_exists",ok:pendingNoteLoaded.ok,reason:pendingNoteLoaded.ok?"pending note artifact exists":pendingNoteLoaded.error}),pendingNote){const pendingNoteMissingFields=authority.pendingNoteRequiredFields.filter(field=>{const value=pendingNote[field];return value==null||value===""});checks.push({id:"pending_note_required_fields",ok:pendingNoteMissingFields.length===0,reason:pendingNoteMissingFields.length===0?"pending note contains required fields":`pending note is missing required fields: ${pendingNoteMissingFields.join(", ")}`}),checks.push({id:"pending_note_topic_matches",ok:pendingNote.topic_id===topicId,reason:pendingNote.topic_id===topicId?"pending note topic_id matches the topic":`pending note topic_id does not match topic (${pendingNote.topic_id??"missing"} vs ${topicId})`}),checks.push({id:"pending_note_status_active",ok:pendingNote.status==="active"&&authority.pendingNoteStatuses.includes(pendingNote.status),reason:pendingNote.status==="active"?"pending note remains active while topic is pending":`pending note status must be active while pending, found ${pendingNote.status??"missing"}`}),checks.push({id:"pending_note_reopen_or_close_defined",ok:typeof pendingNote.reopen_criteria=="string"||typeof pendingNote.close_trigger=="string",reason:typeof pendingNote.reopen_criteria=="string"||typeof pendingNote.close_trigger=="string"?"pending note declares reopen criteria or close trigger":"pending note must declare reopen criteria or close trigger"})}const pendingBlockers=getPendingEntryBlockers(topic);checks.push({id:"pending_has_no_active_implementation_wave",ok:pendingBlockers.length===0,reason:pendingBlockers.length===0?"pending topic has no active implementation wave":`pending topic still has active implementation waves: ${pendingBlockers.join(", ")}`})}ignoredByPolicy?(warnings.push(`topic is ignored by default strict validate policy: ${ignoredByPolicy.reason??topicId}`),checks.push({id:"strict_validate_policy_ignored",ok:!0,reason:`strict topic rails skipped by policy (${ignoredByPolicy.posture??"ignored"})`})):(checks.push({id:"artifact_naming_unambiguous",ok:artifactAnalysis.ambiguousLifecycleFiles.length===0,reason:artifactAnalysis.ambiguousLifecycleFiles.length===0?"lifecycle artifact naming remains unambiguous":`ambiguous lifecycle artifact names: ${artifactAnalysis.ambiguousLifecycleFiles.join(", ")}`}),checks.push({id:"no_active_wave_closeout_conflict",ok:artifactAnalysis.activeWaveCloseoutConflicts.length===0,reason:artifactAnalysis.activeWaveCloseoutConflicts.length===0?"no closeout artifact claims closure for an active wave":`closeout artifacts exist for non-terminal waves: ${artifactAnalysis.activeWaveCloseoutConflicts.join(", ")}`}),checks.push({id:"true_close_not_premature",ok:!artifactAnalysis.prematureTrueClose,reason:artifactAnalysis.prematureTrueClose?"true-close artifacts exist while open blockers remain":"true-close artifacts do not coexist with known open blockers"}));const ok=checks.every(entry=>entry.ok),schemaMode=missingEnrichedFields.length===0&&enumViolations.length===0?"enriched":"legacy_minimal",migrationPosture=schemaMode==="legacy_minimal"&&artifactAnalysis.counts.files>0?"explicit_legacy_reconstruction_required":"not_required",validationDisposition=ignoredByPolicy?validationPolicy.ignoredTopicValidateSemantics.status:"strict",canonicalValidated=ignoredByPolicy?validationPolicy.ignoredTopicValidateSemantics.canonicalSuccess:ok;return{ok,topicId,topicDir,topicRef:relativeTopicDir,state,schemaMode,selectedNextTarget:typeof topic.selected_next_target=="string"?topic.selected_next_target:null,currentTrueCloseStatus:typeof topic.current_true_close_status=="string"?topic.current_true_close_status:null,title:typeof topic.title=="string"?topic.title:null,pendingNoteStatus:pendingNoteLoaded.ok&&typeof pendingNoteLoaded.note.status=="string"?pendingNoteLoaded.note.status:null,missingEnrichedFields,artifactSummary:artifactAnalysis.counts,legacyWaveIds:artifactAnalysis.legacyWaveIds,legacyObservedWaves:artifactAnalysis.legacyObservedWaves,featureFlags:artifactAnalysis.featureFlags,migrationPosture,validationDisposition,canonicalValidated,ignoredByPolicy:ignoredByPolicy!==null,ignorePolicyReason:ignoredByPolicy?.reason??null,ignorePolicyPosture:ignoredByPolicy?.posture??null,checks,warnings}}async function createTopic(projectRoot,options){const topicId=deriveTopicId(options.slug,options.now??new Date),today=formatDate(options.now??new Date),topicDir=path.join(getTopicStateRoot(projectRoot,"proposal"),topicId);if(await pathExists(topicDir))return{ok:!1,error:`Topic already exists: ${toPortableRelativePath(path.relative(projectRoot,topicDir))}`};await mkdir(getTopicStateRoot(projectRoot,"proposal"),{recursive:!0});const authority=await loadTopicRuntimeAuthority(projectRoot),topic=buildCreatePayload({topicId,today,title:options.title,mode:options.mode,posture:options.posture,designPolicy:options.designPolicy,parallelTruth:options.parallelTruth,layering:options.layering,risk:options.risk,applicability:options.applicability,justification:options.justification,executionMode:options.executionMode},authority);return await writeTopicScaffold(topicDir,topic),{ok:!0,topicId,topicDir,topicRef:toPortableRelativePath(path.relative(projectRoot,topicDir)),state:"proposal",title:topic.title}}function deriveCreateDefaults(options){const mode=options.mode??"greenfield",posture=options.posture??(mode==="landed"?"backward_compat":"no_legacy_hard_cut");return{mode,posture,designPolicy:options.designPolicy??"complete_contract_first",parallelTruth:options.parallelTruth??"forbidden",layering:options.layering??"ontology",risk:options.risk??"high",applicability:options.applicability??"authority_bearing",executionMode:options.executionMode??"manager_worker_auditor"}}export{addWaveToTopic,admitWaveInTopic,buildTopicRunLedger,closeoutTopicInTopic,closeoutWaveInTopic,continueTopicOverflow,createDecisionReview,createTopic,decideTopicNextStep,deriveCreateDefaults,dispatchTopicPacket,findTopicDirectory,freezePacketForTopic,holdTopicInPending,initTopicRunLedger,loadTopicPacket,loadTopicReport,loadTopicRuntimeAuthority,openTopicRemediation,readTopicRunLedger,recordTopicResult,recordTopicRunEvent,resolveTopicProjectRoot,resumePendingTopic,runTopicTrueCloseAudit,selectWaveInTopic,validateTopicGraph,validateTopicId,validateTopicRoot,validateTopicSlug,validateWaveAdmission,validateWaveClosure,validateWaveId};
1
+ import { mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { pathExists } from "./fs-helpers.mjs";
5
+ import { formatDate, loadTopicRuntimeAuthority, toPortableRelativePath } from "./topic-common.mjs";
6
+ import {
7
+ buildCreatePayload,
8
+ deriveTopicId,
9
+ getTopicStateRoot,
10
+ titleFromSlug,
11
+ writeTopicScaffold,
12
+ } from "./topic-scaffold.mjs";
13
+
14
+ export * from "./topic-common.mjs";
15
+ export * from "./topic-scaffold.mjs";
16
+ export * from "./topic-waves.mjs";
17
+ export * from "./topic-artifacts.mjs";
18
+ export * from "./topic-decisions.mjs";
19
+ export * from "./topic-ledger.mjs";
20
+ export * from "./topic-execution.mjs";
21
+ export * from "./topic-closeout.mjs";
22
+ export * from "./topic-root-validation.mjs";
23
+
24
+ export async function createTopic(projectRoot, options) {
25
+ const now = options.now ?? new Date();
26
+ const topicId = deriveTopicId(options.slug, now);
27
+ const today = formatDate(now);
28
+ const topicDir = path.join(getTopicStateRoot(projectRoot, "proposal"), topicId);
29
+
30
+ if (await pathExists(topicDir)) {
31
+ return {
32
+ ok: false,
33
+ error: `Topic already exists: ${toPortableRelativePath(path.relative(projectRoot, topicDir))}`,
34
+ };
35
+ }
36
+
37
+ await mkdir(getTopicStateRoot(projectRoot, "proposal"), { recursive: true });
38
+ const authority = await loadTopicRuntimeAuthority(projectRoot);
39
+ const topic = buildCreatePayload(
40
+ {
41
+ topicId,
42
+ today,
43
+ title: options.title,
44
+ mode: options.mode,
45
+ posture: options.posture,
46
+ designPolicy: options.designPolicy,
47
+ parallelTruth: options.parallelTruth,
48
+ layering: options.layering,
49
+ risk: options.risk,
50
+ applicability: options.applicability,
51
+ justification: options.justification,
52
+ executionMode: options.executionMode,
53
+ },
54
+ authority,
55
+ );
56
+
57
+ await writeTopicScaffold(topicDir, topic);
58
+ return {
59
+ ok: true,
60
+ topicId,
61
+ topicDir,
62
+ topicRef: toPortableRelativePath(path.relative(projectRoot, topicDir)),
63
+ state: "proposal",
64
+ title: topic.title,
65
+ };
66
+ }
67
+
68
+ export function deriveCreateDefaults(options) {
69
+ const mode = options.mode ?? "greenfield";
70
+ const posture = options.posture ?? (mode === "landed" ? "backward_compat" : "no_legacy_hard_cut");
71
+ return {
72
+ mode,
73
+ posture,
74
+ designPolicy: options.designPolicy ?? "complete_contract_first",
75
+ parallelTruth: options.parallelTruth ?? "forbidden",
76
+ layering: options.layering ?? "ontology",
77
+ risk: options.risk ?? "high",
78
+ applicability: options.applicability ?? "authority_bearing",
79
+ executionMode: options.executionMode ?? "manager_worker_auditor",
80
+ };
81
+ }
@@ -11,8 +11,13 @@ export function isIsoUtcTimestamp(value) {
11
11
  return false;
12
12
  }
13
13
 
14
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value)) {
15
+ return false;
16
+ }
17
+
14
18
  const parsed = new Date(value);
15
- return !Number.isNaN(parsed.getTime()) && parsed.toISOString() === value;
19
+ const canonicalValue = value.includes(".") ? value : value.replace("Z", ".000Z");
20
+ return !Number.isNaN(parsed.getTime()) && parsed.toISOString() === canonicalValue;
16
21
  }
17
22
 
18
23
  export function toStringArray(value) {
@@ -2,46 +2,122 @@ import { readFile, readdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
+ import YAML from "yaml";
6
+
5
7
  const PACKAGE_ROOT = fileURLToPath(new URL("../..", import.meta.url));
6
- const HOST_BOOTSTRAP_EXCLUDED_PATHS = new Set([
7
- "methodology/spec-target-truth-profile.yaml",
8
- ]);
9
- const SOURCE_PROJECTIONS = [
10
- { sourceDir: "config", outputDir: ".nimi/config" },
11
- { sourceDir: "contracts", outputDir: ".nimi/contracts" },
12
- { sourceDir: "methodology", outputDir: ".nimi/methodology" },
13
- { sourceDir: "spec", outputDir: ".nimi/spec" },
14
- ];
8
+ const POLICY_PATH = fileURLToPath(new URL("./seed-policy.yaml", import.meta.url));
9
+
10
+ const SUPPORTED_OWNERSHIP = new Set(["package_canonical", "host_state_seed", "host_profile_override"]);
11
+
12
+ let cachedPolicy = null;
15
13
 
16
14
  function toPortableRelativePath(filePath) {
17
15
  return filePath.split(path.sep).join("/");
18
16
  }
19
17
 
20
- async function collectProjectedFiles(rootPath, currentPath, outputDir, seedMap) {
21
- const entries = await readdir(currentPath, { withFileTypes: true });
18
+ async function loadPolicy() {
19
+ if (cachedPolicy) {
20
+ return cachedPolicy;
21
+ }
22
+
23
+ const text = await readFile(POLICY_PATH, "utf8");
24
+ const parsed = YAML.parse(text);
22
25
 
23
- for (const entry of entries) {
26
+ if (!parsed || typeof parsed !== "object") {
27
+ throw new Error(`seed policy at ${POLICY_PATH} is not a valid YAML object`);
28
+ }
29
+
30
+ if (!Array.isArray(parsed.projections) || parsed.projections.length === 0) {
31
+ throw new Error("seed policy is missing required `projections` list");
32
+ }
33
+
34
+ const projections = parsed.projections.map((projection) => {
35
+ if (!projection || typeof projection.source_dir !== "string" || typeof projection.output_dir !== "string") {
36
+ throw new Error("seed policy projection entries must declare source_dir and output_dir strings");
37
+ }
38
+ return { sourceDir: projection.source_dir, outputDir: projection.output_dir };
39
+ });
40
+
41
+ const defaultOwnership = parsed.default_ownership ?? "package_canonical";
42
+ if (!SUPPORTED_OWNERSHIP.has(defaultOwnership)) {
43
+ throw new Error(`seed policy declares unsupported default_ownership: ${defaultOwnership}`);
44
+ }
45
+
46
+ const excludedRaw = Array.isArray(parsed.excluded_projection) ? parsed.excluded_projection : [];
47
+ const excludedSourceRelativePaths = new Set(excludedRaw.map((entry) => String(entry)));
48
+
49
+ const overrideEntries = Array.isArray(parsed.ownership_overrides) ? parsed.ownership_overrides : [];
50
+ const ownershipByOutputPath = new Map();
51
+ for (const entry of overrideEntries) {
52
+ if (!entry || typeof entry.path !== "string" || typeof entry.ownership !== "string") {
53
+ throw new Error("seed policy ownership_overrides entries must declare path and ownership strings");
54
+ }
55
+ if (!SUPPORTED_OWNERSHIP.has(entry.ownership)) {
56
+ throw new Error(`seed policy ownership override at ${entry.path} declares unsupported ownership: ${entry.ownership}`);
57
+ }
58
+ ownershipByOutputPath.set(entry.path, entry.ownership);
59
+ }
60
+
61
+ cachedPolicy = {
62
+ policyId: parsed.policy_id ?? null,
63
+ projections,
64
+ defaultOwnership,
65
+ excludedSourceRelativePaths,
66
+ ownershipByOutputPath,
67
+ };
68
+ return cachedPolicy;
69
+ }
70
+
71
+ async function collectProjectedEntries(policy, projection, rootPath, currentPath, entries) {
72
+ const directoryEntries = await readdir(currentPath, { withFileTypes: true });
73
+ for (const entry of directoryEntries) {
24
74
  const absolutePath = path.join(currentPath, entry.name);
25
75
  if (entry.isDirectory()) {
26
- await collectProjectedFiles(rootPath, absolutePath, outputDir, seedMap);
76
+ await collectProjectedEntries(policy, projection, rootPath, absolutePath, entries);
27
77
  continue;
28
78
  }
29
79
 
30
- const relativePath = toPortableRelativePath(path.relative(rootPath, absolutePath));
31
- if (HOST_BOOTSTRAP_EXCLUDED_PATHS.has(`${path.basename(rootPath)}/${relativePath}`)) {
80
+ const relativeFromSourceRoot = toPortableRelativePath(path.relative(rootPath, absolutePath));
81
+ const sourceRelativePath = `${projection.sourceDir}/${relativeFromSourceRoot}`;
82
+ if (policy.excludedSourceRelativePaths.has(sourceRelativePath)) {
32
83
  continue;
33
84
  }
34
- seedMap.set(`${outputDir}/${relativePath}`, await readFile(absolutePath, "utf8"));
85
+
86
+ const outputRelativePath = `${projection.outputDir}/${relativeFromSourceRoot}`;
87
+ const ownership = policy.ownershipByOutputPath.get(outputRelativePath) ?? policy.defaultOwnership;
88
+ const content = await readFile(absolutePath, "utf8");
89
+ entries.push({
90
+ outputRelativePath,
91
+ sourceRelativePath,
92
+ sourceAbsolutePath: absolutePath,
93
+ content,
94
+ ownership,
95
+ });
35
96
  }
36
97
  }
37
98
 
38
- export async function createBootstrapSeedFileMap() {
39
- const seedMap = new Map();
99
+ export async function loadSeedPolicy() {
100
+ return loadPolicy();
101
+ }
102
+
103
+ export async function getBootstrapSeedEntries() {
104
+ const policy = await loadPolicy();
105
+ const entries = [];
40
106
 
41
- for (const projection of SOURCE_PROJECTIONS) {
107
+ for (const projection of policy.projections) {
42
108
  const sourceRoot = path.join(PACKAGE_ROOT, projection.sourceDir);
43
- await collectProjectedFiles(sourceRoot, sourceRoot, projection.outputDir, seedMap);
109
+ await collectProjectedEntries(policy, projection, sourceRoot, sourceRoot, entries);
44
110
  }
45
111
 
112
+ entries.sort((a, b) => a.outputRelativePath.localeCompare(b.outputRelativePath));
113
+ return entries;
114
+ }
115
+
116
+ export async function createBootstrapSeedFileMap() {
117
+ const entries = await getBootstrapSeedEntries();
118
+ const seedMap = new Map();
119
+ for (const entry of entries) {
120
+ seedMap.set(entry.outputRelativePath, entry.content);
121
+ }
46
122
  return seedMap;
47
123
  }