@nimiplatform/nimi-coding 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +348 -0
- package/adapters/README.md +25 -0
- package/adapters/claude/README.md +89 -0
- package/adapters/claude/profile.yaml +70 -0
- package/adapters/codex/README.md +53 -0
- package/adapters/codex/profile.yaml +78 -0
- package/adapters/oh-my-codex/README.md +185 -0
- package/adapters/oh-my-codex/profile.yaml +46 -0
- package/bin/nimicoding.mjs +6 -0
- package/cli/commands/admit-high-risk-decision.mjs +108 -0
- package/cli/commands/audit-sweep.mjs +341 -0
- package/cli/commands/blueprint-audit.mjs +91 -0
- package/cli/commands/clear.mjs +168 -0
- package/cli/commands/closeout.mjs +183 -0
- package/cli/commands/decide-high-risk-execution.mjs +124 -0
- package/cli/commands/doctor.mjs +53 -0
- package/cli/commands/generate-spec-derived-docs.mjs +131 -0
- package/cli/commands/handoff.mjs +123 -0
- package/cli/commands/ingest-high-risk-execution.mjs +95 -0
- package/cli/commands/review-high-risk-execution.mjs +95 -0
- package/cli/commands/start.mjs +717 -0
- package/cli/commands/topic-formatters.mjs +382 -0
- package/cli/commands/topic-goal.mjs +33 -0
- package/cli/commands/topic-options-shared.mjs +27 -0
- package/cli/commands/topic-options-workflow.mjs +767 -0
- package/cli/commands/topic-options.mjs +626 -0
- package/cli/commands/topic-runner.mjs +169 -0
- package/cli/commands/topic.mjs +795 -0
- package/cli/commands/validate-acceptance.mjs +5 -0
- package/cli/commands/validate-ai-governance.mjs +214 -0
- package/cli/commands/validate-execution-packet.mjs +5 -0
- package/cli/commands/validate-orchestration-state.mjs +5 -0
- package/cli/commands/validate-prompt.mjs +5 -0
- package/cli/commands/validate-spec-audit.mjs +27 -0
- package/cli/commands/validate-spec-governance.mjs +124 -0
- package/cli/commands/validate-spec-tree.mjs +27 -0
- package/cli/commands/validate-worker-output.mjs +5 -0
- package/cli/constants.mjs +489 -0
- package/cli/help.mjs +134 -0
- package/cli/index.mjs +103 -0
- package/cli/lib/adapter-profiles.mjs +403 -0
- package/cli/lib/audit-execution.mjs +52 -0
- package/cli/lib/audit-sweep-runtime/admissions.mjs +381 -0
- package/cli/lib/audit-sweep-runtime/audit-validity.mjs +333 -0
- package/cli/lib/audit-sweep-runtime/chunks.mjs +697 -0
- package/cli/lib/audit-sweep-runtime/closeout.mjs +144 -0
- package/cli/lib/audit-sweep-runtime/codex-auditor-evidence.mjs +639 -0
- package/cli/lib/audit-sweep-runtime/codex-auditor.mjs +515 -0
- package/cli/lib/audit-sweep-runtime/common.mjs +329 -0
- package/cli/lib/audit-sweep-runtime/coverage-quality.mjs +172 -0
- package/cli/lib/audit-sweep-runtime/evidence-assignment.mjs +152 -0
- package/cli/lib/audit-sweep-runtime/format.mjs +57 -0
- package/cli/lib/audit-sweep-runtime/ingest.mjs +486 -0
- package/cli/lib/audit-sweep-runtime/inventory-spec-chunks.mjs +198 -0
- package/cli/lib/audit-sweep-runtime/inventory.mjs +728 -0
- package/cli/lib/audit-sweep-runtime/ledger.mjs +315 -0
- package/cli/lib/audit-sweep-runtime/p0p1-profile.mjs +101 -0
- package/cli/lib/audit-sweep-runtime/remediation.mjs +349 -0
- package/cli/lib/audit-sweep-runtime/rerun.mjs +129 -0
- package/cli/lib/audit-sweep-runtime/risk-budget.mjs +300 -0
- package/cli/lib/audit-sweep-runtime/status.mjs +62 -0
- package/cli/lib/audit-sweep-runtime/validators-ledger.mjs +215 -0
- package/cli/lib/audit-sweep-runtime/validators.mjs +758 -0
- package/cli/lib/audit-sweep.mjs +18 -0
- package/cli/lib/authority-convergence.mjs +309 -0
- package/cli/lib/blueprint-audit.mjs +370 -0
- package/cli/lib/bootstrap.mjs +228 -0
- package/cli/lib/closeout.mjs +623 -0
- package/cli/lib/codex-sdk-runner.mjs +76 -0
- package/cli/lib/contracts.mjs +180 -0
- package/cli/lib/doctor.mjs +18 -0
- package/cli/lib/entrypoints.mjs +274 -0
- package/cli/lib/external-execution.mjs +101 -0
- package/cli/lib/fs-helpers.mjs +33 -0
- package/cli/lib/handoff.mjs +785 -0
- package/cli/lib/high-risk-admission.mjs +442 -0
- package/cli/lib/high-risk-decision.mjs +324 -0
- package/cli/lib/high-risk-ingest.mjs +317 -0
- package/cli/lib/high-risk-review.mjs +263 -0
- package/cli/lib/internal/contracts-loaders.mjs +132 -0
- package/cli/lib/internal/contracts-parse-high-risk.mjs +131 -0
- package/cli/lib/internal/contracts-parse.mjs +457 -0
- package/cli/lib/internal/contracts-validators.mjs +398 -0
- package/cli/lib/internal/doctor-bootstrap-surface.mjs +359 -0
- package/cli/lib/internal/doctor-delegated-surface.mjs +256 -0
- package/cli/lib/internal/doctor-finalize.mjs +385 -0
- package/cli/lib/internal/doctor-format.mjs +286 -0
- package/cli/lib/internal/doctor-inspectors.mjs +294 -0
- package/cli/lib/internal/doctor-state.mjs +205 -0
- package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +315 -0
- package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +358 -0
- package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +155 -0
- package/cli/lib/internal/governance/ai/check-high-risk-doc-metadata-core.mjs +173 -0
- package/cli/lib/internal/governance/config.mjs +150 -0
- package/cli/lib/internal/governance/runner.mjs +35 -0
- package/cli/lib/internal/governance/shared/read-yaml-with-fragments.mjs +49 -0
- package/cli/lib/internal/validators-artifacts.mjs +515 -0
- package/cli/lib/internal/validators-shared.mjs +28 -0
- package/cli/lib/internal/validators-spec-helpers.mjs +186 -0
- package/cli/lib/internal/validators-spec.mjs +410 -0
- package/cli/lib/shared.mjs +83 -0
- package/cli/lib/topic-draft-packets.mjs +48 -0
- package/cli/lib/topic-goal.mjs +361 -0
- package/cli/lib/topic-runner.mjs +772 -0
- package/cli/lib/topic.mjs +93 -0
- package/cli/lib/ui.mjs +178 -0
- package/cli/lib/validators.mjs +78 -0
- package/cli/lib/value-helpers.mjs +24 -0
- package/cli/lib/yaml-helpers.mjs +133 -0
- package/cli/nimicoding.mjs +1 -0
- package/cli/seeds/bootstrap.mjs +47 -0
- package/config/audit-execution-artifacts.yaml +20 -0
- package/config/bootstrap.yaml +6 -0
- package/config/external-execution-artifacts.yaml +16 -0
- package/config/host-adapter.yaml +30 -0
- package/config/host-profile.yaml +29 -0
- package/config/installer-evidence.yaml +31 -0
- package/config/skill-installer.yaml +23 -0
- package/config/skill-manifest.yaml +46 -0
- package/config/skills.yaml +30 -0
- package/config/spec-generation-inputs.yaml +25 -0
- package/contracts/acceptance.schema.yaml +16 -0
- package/contracts/admission-checklist.schema.yaml +15 -0
- package/contracts/audit-chunk.schema.yaml +110 -0
- package/contracts/audit-closeout.schema.yaml +51 -0
- package/contracts/audit-finding.schema.yaml +61 -0
- package/contracts/audit-ledger.schema.yaml +138 -0
- package/contracts/audit-plan.schema.yaml +123 -0
- package/contracts/audit-remediation-map.schema.yaml +51 -0
- package/contracts/audit-rerun.schema.yaml +31 -0
- package/contracts/audit-sweep-result.yaml +49 -0
- package/contracts/authority-convergence-audit.schema.yaml +19 -0
- package/contracts/closeout.schema.yaml +25 -0
- package/contracts/decision-review.schema.yaml +16 -0
- package/contracts/doc-spec-audit-result.yaml +19 -0
- package/contracts/execution-packet.schema.yaml +49 -0
- package/contracts/external-host-compatibility.yaml +22 -0
- package/contracts/forbidden-shortcuts.catalog.yaml +23 -0
- package/contracts/high-risk-admission.schema.yaml +23 -0
- package/contracts/high-risk-execution-result.yaml +20 -0
- package/contracts/orchestration-state.schema.yaml +41 -0
- package/contracts/overflow-continuation.schema.yaml +12 -0
- package/contracts/packet.schema.yaml +30 -0
- package/contracts/pending-note.schema.yaml +17 -0
- package/contracts/prompt.schema.yaml +12 -0
- package/contracts/remediation.schema.yaml +16 -0
- package/contracts/result.schema.yaml +24 -0
- package/contracts/spec-generation-audit.schema.yaml +31 -0
- package/contracts/spec-generation-inputs.schema.yaml +39 -0
- package/contracts/spec-reconstruction-result.yaml +37 -0
- package/contracts/topic-goal.schema.yaml +78 -0
- package/contracts/topic-run-ledger.schema.yaml +72 -0
- package/contracts/topic-step-decision.schema.yaml +45 -0
- package/contracts/topic.schema.yaml +65 -0
- package/contracts/true-close.schema.yaml +15 -0
- package/contracts/wave.schema.yaml +29 -0
- package/contracts/worker-output.schema.yaml +15 -0
- package/methodology/audit-sweep-p0p1-recall.yaml +45 -0
- package/methodology/authority-convergence-policy.yaml +42 -0
- package/methodology/core.yaml +25 -0
- package/methodology/four-closure-policy.yaml +28 -0
- package/methodology/overflow-continuation-policy.yaml +14 -0
- package/methodology/role-separation-policy.yaml +28 -0
- package/methodology/skill-exchange-projection.yaml +114 -0
- package/methodology/skill-handoff.yaml +34 -0
- package/methodology/skill-installer-result.yaml +27 -0
- package/methodology/skill-installer-summary-projection.yaml +181 -0
- package/methodology/skill-runtime.yaml +23 -0
- package/methodology/spec-reconstruction.yaml +63 -0
- package/methodology/spec-target-truth-profile.yaml +53 -0
- package/methodology/topic-lifecycle-report.yaml +144 -0
- package/methodology/topic-lifecycle.yaml +37 -0
- package/methodology/topic-naming-ontology.yaml +21 -0
- package/methodology/topic-ontology.yaml +38 -0
- package/methodology/topic-validation-policy.yaml +9 -0
- package/methodology/wave-dag-policy.yaml +14 -0
- package/package.json +50 -0
- package/spec/_meta/command-gating-matrix.yaml +110 -0
- package/spec/_meta/generate-drift-migration-checklist.yaml +155 -0
- package/spec/_meta/governance-routing-cutover-checklist.yaml +35 -0
- package/spec/_meta/phase2-impacted-surface-matrix.yaml +44 -0
- package/spec/_meta/spec-authority-cutover-readiness.yaml +104 -0
- package/spec/_meta/spec-tree-model.yaml +72 -0
- package/spec/bootstrap-state.yaml +99 -0
- package/spec/product-scope.yaml +56 -0
|
@@ -0,0 +1,93 @@
|
|
|
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};
|
package/cli/lib/ui.mjs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
|
|
3
|
+
const SUPPORTED_LOCALES = new Set(["en", "zh"]);
|
|
4
|
+
|
|
5
|
+
let currentLocale = "en";
|
|
6
|
+
let currentColorEnabled = false;
|
|
7
|
+
let currentLocalePinned = false;
|
|
8
|
+
|
|
9
|
+
function detectLocale() {
|
|
10
|
+
const envLocale = process.env.NIMICODING_LANG ?? process.env.LANG ?? "";
|
|
11
|
+
return envLocale.toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function detectColorEnabled() {
|
|
15
|
+
if (process.env.NO_COLOR) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return Boolean(process.stdout.isTTY);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function configureCliUi(options = {}) {
|
|
27
|
+
currentLocale = SUPPORTED_LOCALES.has(options.locale) ? options.locale : detectLocale();
|
|
28
|
+
currentColorEnabled = typeof options.colorEnabled === "boolean"
|
|
29
|
+
? options.colorEnabled
|
|
30
|
+
: detectColorEnabled();
|
|
31
|
+
currentLocalePinned = typeof options.locale === "string" && SUPPORTED_LOCALES.has(options.locale);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getCliLocale() {
|
|
35
|
+
return currentLocale;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getCliColorEnabled() {
|
|
39
|
+
return currentColorEnabled;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isCliLocalePinned() {
|
|
43
|
+
return currentLocalePinned;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function localize(en, zh) {
|
|
47
|
+
return currentLocale === "zh" ? zh : en;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function parseGlobalUiOptions(args) {
|
|
51
|
+
const remainingArgs = [];
|
|
52
|
+
let locale = null;
|
|
53
|
+
let colorEnabled;
|
|
54
|
+
|
|
55
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
56
|
+
const arg = args[index];
|
|
57
|
+
|
|
58
|
+
if (arg === "--lang") {
|
|
59
|
+
const next = args[index + 1];
|
|
60
|
+
if (!next || next.startsWith("--")) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
error: `${localize(
|
|
64
|
+
"nimicoding refused: --lang requires `en` or `zh`.",
|
|
65
|
+
"nimicoding 已拒绝:`--lang` 需要 `en` 或 `zh`。",
|
|
66
|
+
)}\n`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (!SUPPORTED_LOCALES.has(next)) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
error: `${localize(
|
|
73
|
+
`nimicoding refused: unsupported --lang value ${next}. Use \`en\` or \`zh\`.`,
|
|
74
|
+
`nimicoding 已拒绝:不支持的 --lang 值 ${next}。请使用 \`en\` 或 \`zh\`。`,
|
|
75
|
+
)}\n`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
locale = next;
|
|
79
|
+
index += 1;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (arg.startsWith("--lang=")) {
|
|
84
|
+
const value = arg.slice("--lang=".length);
|
|
85
|
+
if (!SUPPORTED_LOCALES.has(value)) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
error: `${localize(
|
|
89
|
+
`nimicoding refused: unsupported --lang value ${value}. Use \`en\` or \`zh\`.`,
|
|
90
|
+
`nimicoding 已拒绝:不支持的 --lang 值 ${value}。请使用 \`en\` 或 \`zh\`。`,
|
|
91
|
+
)}\n`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
locale = value;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (arg === "--color") {
|
|
99
|
+
colorEnabled = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (arg === "--no-color") {
|
|
104
|
+
colorEnabled = false;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
remainingArgs.push(arg);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
ok: true,
|
|
113
|
+
args: remainingArgs,
|
|
114
|
+
locale,
|
|
115
|
+
colorEnabled,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const ANSI = {
|
|
120
|
+
reset: "\u001B[0m",
|
|
121
|
+
bold: "\u001B[1m",
|
|
122
|
+
dim: "\u001B[2m",
|
|
123
|
+
red: "\u001B[31m",
|
|
124
|
+
green: "\u001B[32m",
|
|
125
|
+
yellow: "\u001B[33m",
|
|
126
|
+
blue: "\u001B[34m",
|
|
127
|
+
magenta: "\u001B[35m",
|
|
128
|
+
cyan: "\u001B[36m",
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
function applyAnsi(codes, text) {
|
|
132
|
+
if (!currentColorEnabled) {
|
|
133
|
+
return text;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return `${codes.join("")}${text}${ANSI.reset}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function styleHeading(text) {
|
|
140
|
+
return applyAnsi([ANSI.bold, ANSI.cyan], text);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function styleLabel(text) {
|
|
144
|
+
return applyAnsi([ANSI.bold], text);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function styleMuted(text) {
|
|
148
|
+
return applyAnsi([ANSI.dim], text);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function styleCommand(text) {
|
|
152
|
+
return applyAnsi([ANSI.magenta], text);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function styleSuccess(text) {
|
|
156
|
+
return applyAnsi([ANSI.green], text);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function styleWarning(text) {
|
|
160
|
+
return applyAnsi([ANSI.yellow], text);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function styleError(text) {
|
|
164
|
+
return applyAnsi([ANSI.red], text);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function styleStatus(status) {
|
|
168
|
+
if (status === "ok" || status === "complete" || status === "ready") {
|
|
169
|
+
return styleSuccess(status);
|
|
170
|
+
}
|
|
171
|
+
if (status.includes("missing") || status.includes("required") || status === "needs_attention") {
|
|
172
|
+
return styleWarning(status);
|
|
173
|
+
}
|
|
174
|
+
if (status === "fail" || status === "error") {
|
|
175
|
+
return styleError(status);
|
|
176
|
+
}
|
|
177
|
+
return status;
|
|
178
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
validateAcceptance as validateAcceptanceInternal,
|
|
5
|
+
validateExecutionPacket as validateExecutionPacketInternal,
|
|
6
|
+
validateOrchestrationState as validateOrchestrationStateInternal,
|
|
7
|
+
validatePrompt as validatePromptInternal,
|
|
8
|
+
validateWorkerOutput as validateWorkerOutputInternal,
|
|
9
|
+
} from "./internal/validators-artifacts.mjs";
|
|
10
|
+
import {
|
|
11
|
+
normalizeArgv,
|
|
12
|
+
VALIDATOR_CLI_RESULT_CONTRACT,
|
|
13
|
+
VALIDATOR_NATIVE_REFUSAL_CODES,
|
|
14
|
+
} from "./internal/validators-shared.mjs";
|
|
15
|
+
import {
|
|
16
|
+
validateSpecAudit as validateSpecAuditInternal,
|
|
17
|
+
validateSpecTree as validateSpecTreeInternal,
|
|
18
|
+
} from "./internal/validators-spec.mjs";
|
|
19
|
+
|
|
20
|
+
export { VALIDATOR_NATIVE_REFUSAL_CODES };
|
|
21
|
+
|
|
22
|
+
export function validateExecutionPacket(filePath) {
|
|
23
|
+
return validateExecutionPacketInternal(filePath);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function validateOrchestrationState(filePath) {
|
|
27
|
+
return validateOrchestrationStateInternal(filePath);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function validatePrompt(filePath) {
|
|
31
|
+
return validatePromptInternal(filePath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function validateWorkerOutput(filePath) {
|
|
35
|
+
return validateWorkerOutputInternal(filePath);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateAcceptance(filePath) {
|
|
39
|
+
return validateAcceptanceInternal(filePath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateSpecTree(rootPath, options = {}) {
|
|
43
|
+
return validateSpecTreeInternal(rootPath, options);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validateSpecAudit(auditPath, options = {}) {
|
|
47
|
+
return validateSpecAuditInternal(auditPath, options);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildValidatorCliReport(validator, filePath, report) {
|
|
51
|
+
return {
|
|
52
|
+
contract: VALIDATOR_CLI_RESULT_CONTRACT,
|
|
53
|
+
validator,
|
|
54
|
+
target_ref: filePath,
|
|
55
|
+
ok: Boolean(report.ok),
|
|
56
|
+
refusal: report.refusal || null,
|
|
57
|
+
errors: report.errors || [],
|
|
58
|
+
warnings: report.warnings || [],
|
|
59
|
+
...(report.summary ? { summary: report.summary } : {}),
|
|
60
|
+
...(report.signal ? { signal: report.signal } : {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function runValidatorCommand(args, validator, validate) {
|
|
65
|
+
const normalizedArgv = normalizeArgv(args);
|
|
66
|
+
const [filePath, ...rest] = normalizedArgv;
|
|
67
|
+
|
|
68
|
+
if (!filePath || rest.length > 0) {
|
|
69
|
+
process.stderr.write(`nimicoding ${validator} refused: expected exactly one path argument.\n`);
|
|
70
|
+
return 2;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const targetPath = path.resolve(process.cwd(), filePath);
|
|
74
|
+
const report = await validate(targetPath);
|
|
75
|
+
const cliReport = buildValidatorCliReport(validator, targetPath, report);
|
|
76
|
+
process.stdout.write(`${JSON.stringify(cliReport, null, 2)}\n`);
|
|
77
|
+
return report.ok ? 0 : 1;
|
|
78
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function arraysEqual(left, right) {
|
|
2
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isPlainObject(value) {
|
|
6
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isIsoUtcTimestamp(value) {
|
|
10
|
+
if (typeof value !== "string") {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const parsed = new Date(value);
|
|
15
|
+
return !Number.isNaN(parsed.getTime()) && parsed.toISOString() === value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function toStringArray(value) {
|
|
19
|
+
if (!Array.isArray(value)) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return value.map((entry) => String(entry));
|
|
24
|
+
}
|