@khoavu882/doflow 2.0.0-beta.4 → 2.0.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/README.md +12 -11
- package/bin/doflow.js +21 -11
- package/core/.antigravity-plugin/plugin.json +1 -3
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/lib.sh +43 -24
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
- package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
- package/core/harnesses/claude/hooks/post-compact.sh +35 -32
- package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
- package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
- package/core/harnesses/claude/hooks/session-end.sh +23 -10
- package/core/harnesses/claude/hooks/session-start.sh +5 -3
- package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
- package/core/harnesses/claude/hooks/stop-check.sh +5 -3
- package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
- package/core/harnesses/claude/settings/settings.json +18 -0
- package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
- package/core/harnesses/codex/agents/core-implementer.toml +1 -0
- package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
- package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
- package/core/harnesses/codex/agents/research-writer.toml +1 -1
- package/core/harnesses/codex/agents/reviewer.toml +10 -0
- package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
- package/core/harnesses/codex/agents/system-architect.toml +1 -1
- package/core/harnesses/codex/hooks/lib.sh +44 -25
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
- package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
- package/core/harnesses/codex/hooks/session-start.sh +5 -3
- package/core/harnesses/codex/hooks/stop-check.sh +6 -5
- package/core/harnesses/codex/hooks/subagent-audit.sh +18 -6
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
- package/core/harnesses/copilot/hooks/doflow-lib.sh +93 -0
- package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow.json +57 -0
- package/core/harnesses/kiro/agents/doflow.json +1 -1
- package/core/harnesses/kiro/hooks/lib.sh +26 -6
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +21 -5
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +21 -5
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +21 -5
- package/core/harnesses/kiro/hooks/session-start.sh +12 -5
- package/core/harnesses/kiro/hooks/stop-check.sh +22 -30
- package/core/harnesses/pi/extensions/doflow-guard.js +167 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -12
- package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
- package/core/harnesses/shared/hooks/bridge/bridge.sh +277 -0
- package/core/harnesses/shared/hooks/bridge/envelope.jq +129 -0
- package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +70 -0
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +51 -16
- package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
- package/core/harnesses/shared/hooks/policies/contract.sh +78 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +35 -9
- package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
- package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
- package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
- package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +65 -40
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +48 -35
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +284 -33
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +180 -110
- package/core/harnesses/shared/hooks/policies/session-context.sh +66 -7
- package/core/harnesses/shared/hooks/policies/stop-check.sh +61 -23
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +39 -6
- package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +97 -159
- package/core/harnesses/shared/hooks/stream-hook-runner.js +114 -187
- package/core/harnesses/shared/locator/doflow-run +2 -3
- package/core/registry/advice.json +3 -3
- package/core/registry/assets.json +72 -80
- package/core/registry/capabilities.json +12 -0
- package/core/registry/contracts.json +60 -33
- package/core/registry/contribution-points.json +32 -0
- package/core/registry/gates.json +32 -0
- package/core/registry/guidance-caps.json +40 -0
- package/core/registry/harnesses.json +1542 -277
- package/core/registry/lifecycle.json +2837 -37
- package/core/registry/mcp.json +5 -1
- package/core/registry/models.json +7 -1
- package/core/registry/package-templates/README.md +19 -0
- package/core/registry/package-templates/agent/files/agent.md +9 -0
- package/core/registry/package-templates/agent/template.json +4 -0
- package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
- package/core/registry/package-templates/capability-provider/template.json +11 -0
- package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
- package/core/registry/package-templates/debate-preset/template.json +4 -0
- package/core/registry/package-templates/review-preset/files/preset.json +30 -0
- package/core/registry/package-templates/review-preset/template.json +4 -0
- package/core/registry/package-templates/skill/files/SKILL.md +8 -0
- package/core/registry/package-templates/skill/template.json +4 -0
- package/core/registry/package-templates/worker/files/worker.json +92 -0
- package/core/registry/package-templates/worker/template.json +11 -0
- package/core/registry/packages/doflow/doflow-package.json +15 -0
- package/core/registry/packages/doflow/presets/debate-3.json +26 -0
- package/core/registry/protocols.json +61 -0
- package/core/registry/routes.json +17 -0
- package/core/registry/rules.json +431 -0
- package/core/registry/settings.json +276 -0
- package/core/registry/storage.json +701 -30
- package/core/registry/telemetry.json +236 -0
- package/core/registry/transcripts.json +142 -0
- package/core/registry/verification.json +76 -1
- package/core/registry/workflows.json +55 -0
- package/core/shared/agent-specs/core-implementer.md +3 -0
- package/core/shared/agent-specs/delivery-lead.md +68 -0
- package/core/shared/agent-specs/quality-guardian.md +5 -1
- package/core/shared/agent-specs/research-writer.md +10 -3
- package/core/shared/agent-specs/reviewer.md +53 -0
- package/core/shared/agent-specs/spec-analyst.md +5 -6
- package/core/shared/agent-specs/system-architect.md +5 -1
- package/core/shared/guidance/DOFLOW_CORE.md +1 -5
- package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
- package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
- package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
- package/core/shared/guidance/pointers/codex.md +1 -4
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
- package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
- package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
- package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
- package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
- package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
- package/core/shared/guidance/references/ORCHESTRATION.md +1 -2
- package/core/shared/guidance/rules/RULES.md +45 -0
- package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
- package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
- package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
- package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
- package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
- package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +48 -4
- package/core/shared/scripts/doflow/bin/doflow-run +154 -35
- package/core/shared/skills/do/SKILL.md +12 -10
- package/core/shared/skills/do/references/advice_calls.md +26 -0
- package/core/shared/skills/do/references/pm_routing.md +1 -2
- package/core/shared/skills/do/references/program_mode.md +31 -0
- package/core/shared/skills/do-brainstorm/SKILL.md +20 -74
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +35 -0
- package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
- package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
- package/core/shared/skills/do-code-review/SKILL.md +41 -144
- package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
- package/core/shared/skills/do-code-review/references/tools.md +90 -0
- package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
- package/core/shared/skills/do-design/SKILL.md +24 -23
- package/core/shared/skills/do-diagnose/SKILL.md +14 -4
- package/core/shared/skills/do-document/SKILL.md +11 -1
- package/core/shared/skills/do-document/references/deep_research.md +1 -2
- package/core/shared/skills/do-document/references/embedded-research.md +18 -6
- package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +17 -59
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +28 -1
- package/core/shared/skills/do-execute-plan/references/run_position.md +13 -1
- package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
- package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
- package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
- package/core/shared/skills/do-flow/SKILL.md +8 -9
- package/core/shared/skills/do-flow/references/task_classes.md +2 -2
- package/core/shared/skills/do-git/SKILL.md +4 -0
- package/core/shared/skills/do-implement/SKILL.md +7 -9
- package/core/shared/skills/do-plan/SKILL.md +29 -19
- package/core/shared/skills/do-test/SKILL.md +7 -4
- package/core/shared/templates/doflow/constitution-template.md +9 -3
- package/core/shared/templates/doflow/data-model-template.md +1 -1
- package/core/shared/templates/doflow/design-template.md +113 -16
- package/core/shared/templates/doflow/plan-template.md +11 -10
- package/core/shared/templates/doflow/question-log-template.md +14 -2
- package/core/shared/templates/doflow/requirement-template.md +1 -1
- package/core/worker-evidence/antigravity/checks.json +14 -0
- package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
- package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
- package/core/worker-evidence/codex/checks.json +38 -0
- package/package.json +10 -6
- package/src/adapters/antigravity/index.js +226 -113
- package/src/adapters/claude/index.js +39 -5
- package/src/adapters/codex/agents.js +25 -15
- package/src/adapters/codex/hooks.js +40 -5
- package/src/adapters/codex/index.js +100 -22
- package/src/adapters/codex/native-projection.js +6 -2
- package/src/adapters/copilot/index.js +58 -23
- package/src/adapters/copy-tree.js +294 -48
- package/src/adapters/guidance-manifest.js +71 -0
- package/src/adapters/guidance-pointer.js +19 -0
- package/src/adapters/hook-commands.js +6 -10
- package/src/adapters/index.js +1 -0
- package/src/adapters/kiro/index.js +38 -13
- package/src/adapters/opencode/index.js +8 -8
- package/src/adapters/pi/hooks.js +120 -0
- package/src/adapters/pi/index.js +48 -16
- package/src/adapters/resolve-source.js +26 -2
- package/src/cli/commands/install.js +3 -2
- package/src/cli/commands/reconcile.js +53 -9
- package/src/cli/commands/remove.js +2 -2
- package/src/cli/commands/rollback.js +6 -3
- package/src/cli/commands/self-update.js +18 -9
- package/src/cli/commands/status.js +21 -2
- package/src/cli/commands/ui.js +99 -0
- package/src/cli/commands/update.js +3 -2
- package/src/cli/index.js +338 -33
- package/src/cli/runtime-commands.js +243 -52
- package/src/cli/shared.js +0 -2
- package/src/helper/git.js +1 -1
- package/src/helper/guardrails.js +1 -1
- package/src/helper/marker-merge.js +31 -13
- package/src/helper/package-ids.js +37 -0
- package/src/helper/semver.js +94 -0
- package/src/install/backup.js +105 -15
- package/src/install/context.js +1 -1
- package/src/install/targets.js +31 -16
- package/src/lifecycle/coverage.js +74 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/hook-tier.js +160 -0
- package/src/lifecycle/index.js +434 -86
- package/src/lifecycle/retired-harnesses.js +318 -0
- package/src/lifecycle/view.js +35 -15
- package/src/registry/index.js +686 -32
- package/src/registry/worker-ids.js +45 -0
- package/src/release/build-package.js +0 -3
- package/src/release/docs-blocks.js +492 -0
- package/src/release/preflight.js +425 -0
- package/src/release/rehearse.js +401 -0
- package/src/release/release-workflow.js +733 -0
- package/src/release/sync-plugin-versions.js +43 -7
- package/src/runtime/advice/bench-gate.js +25 -0
- package/src/runtime/advice/cli.js +59 -16
- package/src/runtime/advice/client.js +32 -9
- package/src/runtime/advice/config.js +98 -133
- package/src/runtime/advice/doctor.js +1 -1
- package/src/runtime/advice/elevation.js +97 -0
- package/src/runtime/advice/endpoint-guard.js +9 -3
- package/src/runtime/advice/mode.js +37 -0
- package/src/runtime/advice/record.js +18 -5
- package/src/runtime/advice/registry.js +4 -1
- package/src/runtime/advice/shadow-config.js +229 -0
- package/src/runtime/advice/shadow-dispatcher.js +276 -0
- package/src/runtime/brief-rules.js +81 -0
- package/src/runtime/brief-tools.js +111 -34
- package/src/runtime/candidate.js +514 -0
- package/src/runtime/capabilities/executor.js +450 -0
- package/src/runtime/capabilities/loopback.js +41 -0
- package/src/runtime/capabilities/sanitizer.js +88 -0
- package/src/runtime/capability-router.js +135 -3
- package/src/runtime/checkouts.js +73 -11
- package/src/runtime/claims.js +27 -12
- package/src/runtime/cli-result.js +11 -4
- package/src/runtime/cli.js +221 -277
- package/src/runtime/config-home.js +42 -0
- package/src/runtime/context/arbitrate.js +767 -0
- package/src/runtime/context/budget.js +203 -0
- package/src/runtime/context/compose.js +1367 -0
- package/src/runtime/context/delivery-store.js +100 -0
- package/src/runtime/context/emission-rules.js +118 -0
- package/src/runtime/context/identity.js +227 -0
- package/src/runtime/context/mandatory.js +594 -0
- package/src/runtime/context/pointer.js +142 -0
- package/src/runtime/context/primary-command.js +117 -0
- package/src/runtime/context/primary.js +344 -0
- package/src/runtime/context/summary-store.js +655 -0
- package/src/runtime/context-pack.js +1106 -245
- package/src/runtime/decision-register.js +13 -4
- package/src/runtime/dispatch/admission.js +392 -0
- package/src/runtime/dispatch/attempt-end.js +274 -0
- package/src/runtime/dispatch/command.js +628 -0
- package/src/runtime/dispatch/controls.js +904 -0
- package/src/runtime/dispatch/envelope.js +276 -0
- package/src/runtime/dispatch/grants.js +310 -0
- package/src/runtime/dispatch/integrate.js +617 -0
- package/src/runtime/dispatch/loop.js +1835 -0
- package/src/runtime/dispatch/store.js +352 -0
- package/src/runtime/dispatch/verify-run.js +201 -0
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/evidence-ledger.js +41 -7
- package/src/runtime/failure/capture.js +3 -1
- package/src/runtime/failure/cli.js +25 -9
- package/src/runtime/failure/home.js +57 -30
- package/src/runtime/failure/store.js +7 -2
- package/src/runtime/feature-pointer.js +174 -0
- package/src/runtime/feature-resolve.js +13 -2
- package/src/runtime/fleet/command.js +77 -0
- package/src/runtime/fleet/projects.js +178 -0
- package/src/runtime/fleet/tree.js +383 -0
- package/src/runtime/freshness.js +77 -10
- package/src/runtime/git-memo.js +46 -0
- package/src/runtime/guidance/ablation.js +427 -0
- package/src/runtime/health.js +565 -17
- package/src/runtime/history-compactor.js +13 -3
- package/src/runtime/implementation-gate.js +277 -22
- package/src/runtime/inventory/coverage.js +1 -1
- package/src/runtime/inventory/index.js +1 -1
- package/src/runtime/inventory/precedence.js +2 -2
- package/src/runtime/inventory/siblings.js +6 -4
- package/src/runtime/knowledge/code-corpus.js +277 -0
- package/src/runtime/knowledge/code-lexical.js +130 -0
- package/src/runtime/knowledge/code-retrieval.js +307 -0
- package/src/runtime/knowledge/code-validation.js +106 -0
- package/src/runtime/knowledge/gp3-gate.js +74 -0
- package/src/runtime/knowledge/graph-adapter.js +288 -0
- package/src/runtime/knowledge/graph-fallback.js +394 -0
- package/src/runtime/knowledge/hybrid.js +136 -8
- package/src/runtime/knowledge/retrieval.js +34 -1
- package/src/runtime/knowledge/rtk-compress.js +77 -0
- package/src/runtime/knowledge/semble-bridge.py +106 -0
- package/src/runtime/knowledge/semble-lifecycle.js +114 -0
- package/src/runtime/knowledge/semble-provider.js +365 -0
- package/src/runtime/lifecycle/cli.js +16 -6
- package/src/runtime/lifecycle/event-store.js +8 -6
- package/src/runtime/lifecycle/followup.js +6 -1
- package/src/runtime/lifecycle/overview.js +24 -1
- package/src/runtime/lifecycle/report-store.js +8 -10
- package/src/runtime/lifecycle/retention.js +95 -31
- package/src/runtime/lifecycle/root.js +9 -1
- package/src/runtime/lifecycle/status.js +34 -17
- package/src/runtime/lifecycle/store-upkeep.js +56 -7
- package/src/runtime/mask.js +134 -7
- package/src/runtime/mcp-consultations.js +40 -6
- package/src/runtime/model-router.js +79 -7
- package/src/runtime/models/catalogue.js +191 -0
- package/src/runtime/models/command.js +153 -0
- package/src/runtime/off-words.js +12 -0
- package/src/runtime/outcome.js +3 -3
- package/src/runtime/packages/command.js +1009 -0
- package/src/runtime/packages/contributions.js +116 -0
- package/src/runtime/packages/digest.js +132 -0
- package/src/runtime/packages/fetch.js +182 -0
- package/src/runtime/packages/grants.js +192 -0
- package/src/runtime/packages/index-file.js +134 -0
- package/src/runtime/packages/instruction.js +90 -0
- package/src/runtime/packages/manifest.js +331 -0
- package/src/runtime/packages/points.js +477 -0
- package/src/runtime/packages/programs.js +216 -0
- package/src/runtime/packages/scaffold.js +277 -0
- package/src/runtime/packages/source.js +90 -0
- package/src/runtime/packages/state.js +164 -0
- package/src/runtime/packages/store.js +449 -0
- package/src/runtime/packages/tar.js +106 -0
- package/src/runtime/packages/view.js +406 -0
- package/src/runtime/plan-check.js +59 -0
- package/src/runtime/plan-content.js +324 -0
- package/src/runtime/policy-init.js +118 -0
- package/src/runtime/profiles/doctor.js +96 -0
- package/src/runtime/profiles/resolve.js +244 -0
- package/src/runtime/profiles/rollback.js +254 -0
- package/src/runtime/profiles/sampler.js +41 -0
- package/src/runtime/readiness-record.js +16 -11
- package/src/runtime/readiness.js +127 -16
- package/src/runtime/research-request.js +50 -25
- package/src/runtime/retrieval-plan.js +402 -73
- package/src/runtime/retrieval-record.js +293 -0
- package/src/runtime/review/command.js +246 -0
- package/src/runtime/review/dominance.js +68 -0
- package/src/runtime/review/findings.js +434 -0
- package/src/runtime/review/identity.js +255 -0
- package/src/runtime/review/protocol-command.js +109 -0
- package/src/runtime/review/protocols.js +405 -0
- package/src/runtime/review/resolve-protocol.js +302 -0
- package/src/runtime/review/start.js +530 -0
- package/src/runtime/roadmap/facts.js +479 -0
- package/src/runtime/roadmap/gates.js +270 -0
- package/src/runtime/roadmap/loop.js +1636 -0
- package/src/runtime/roadmap/merge.js +381 -0
- package/src/runtime/roadmap/rows.js +1113 -0
- package/src/runtime/roadmap/seed.js +951 -0
- package/src/runtime/rules-report.js +336 -0
- package/src/runtime/scaffold/artifacts.js +63 -9
- package/src/runtime/scaffold/generate.js +21 -9
- package/src/runtime/settings/autopilot.js +106 -0
- package/src/runtime/settings/cli.js +280 -0
- package/src/runtime/settings/config-write.js +314 -0
- package/src/runtime/settings/files.js +271 -0
- package/src/runtime/settings/legacy.js +154 -0
- package/src/runtime/settings/lexical.js +56 -0
- package/src/runtime/settings/prepared-run.js +269 -0
- package/src/runtime/settings/project-layers.js +118 -0
- package/src/runtime/settings/refs.js +288 -0
- package/src/runtime/settings/registry.js +553 -0
- package/src/runtime/settings/resolve.js +713 -0
- package/src/runtime/settings/shared.js +37 -0
- package/src/runtime/settings/worker-billing.js +107 -0
- package/src/runtime/settings/write.js +269 -0
- package/src/runtime/slot-command.js +78 -0
- package/src/runtime/slots.js +671 -0
- package/src/runtime/source-version.js +290 -0
- package/src/runtime/state-root.js +217 -0
- package/src/runtime/storage/cli.js +12 -5
- package/src/runtime/storage/engine.js +9 -2
- package/src/runtime/storage/registry.js +14 -1
- package/src/runtime/storage/roots.js +66 -16
- package/src/runtime/storage/rules.js +82 -10
- package/src/runtime/storage/upkeep.js +10 -2
- package/src/runtime/storage/work-status.js +245 -26
- package/src/runtime/task-attempts.js +589 -69
- package/src/runtime/task-classifier.js +47 -11
- package/src/runtime/task-graph.js +373 -0
- package/src/runtime/task-run.js +778 -0
- package/src/runtime/task-scope.js +202 -56
- package/src/runtime/task-sources.js +147 -0
- package/src/runtime/task-state.js +440 -65
- package/src/runtime/team/admission.js +301 -0
- package/src/runtime/team/envelope.js +324 -0
- package/src/runtime/team/review-guard.js +156 -0
- package/src/runtime/trace/analyze.js +427 -0
- package/src/runtime/trace/baseline.js +770 -0
- package/src/runtime/trace/ledger.js +63 -140
- package/src/runtime/trace/otlp-export.js +568 -0
- package/src/runtime/trace/otlp-map.js +481 -0
- package/src/runtime/trace/spans.js +745 -0
- package/src/runtime/trace/telemetry.js +225 -0
- package/src/runtime/trace/views.js +1 -0
- package/src/runtime/transcripts/command.js +98 -0
- package/src/runtime/transcripts/cursor.js +326 -0
- package/src/runtime/transcripts/read.js +126 -0
- package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
- package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
- package/src/runtime/transcripts/readers/copilot-events.js +90 -0
- package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
- package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
- package/src/runtime/transcripts/registry.js +161 -0
- package/src/runtime/transcripts/stats.js +161 -0
- package/src/runtime/verification/check-key.js +628 -0
- package/src/runtime/verification/contract-runner.js +546 -11
- package/src/runtime/verification/engine.js +499 -9
- package/src/runtime/verification/impact.js +617 -0
- package/src/runtime/verification/inflight.js +205 -0
- package/src/runtime/verification/registry.js +176 -0
- package/src/runtime/verification/reuse.js +184 -0
- package/src/runtime/verification/telemetry-emit.js +254 -0
- package/src/runtime/verification/test-selection.js +67 -7
- package/src/runtime/worker/active.js +90 -0
- package/src/runtime/worker/cancel.js +222 -0
- package/src/runtime/worker/catalogue.js +122 -0
- package/src/runtime/worker/command.js +677 -0
- package/src/runtime/worker/evidence.js +288 -0
- package/src/runtime/worker/launch.js +288 -0
- package/src/runtime/worker/models.js +137 -0
- package/src/runtime/worker/parsers.js +218 -0
- package/src/runtime/worker/probes/index.js +706 -0
- package/src/runtime/worker/process-tree.js +415 -0
- package/src/runtime/worker/quota.js +43 -0
- package/src/runtime/worker/readiness.js +713 -0
- package/src/runtime/worker/run-store.js +166 -0
- package/src/runtime/worker/supervisor.js +541 -0
- package/src/runtime/worker/worktree-check.js +252 -0
- package/src/runtime/workflow-engine.js +5 -1
- package/src/runtime/workflow-orchestrator.js +578 -63
- package/src/runtime/worktree.js +149 -22
- package/src/state/index.js +1 -1
- package/src/state/lockfile.js +64 -10
- package/src/state/run-lock.js +1 -1
- package/types/node-commonjs.d.ts +106 -7
- package/core/.antigravity-plugin/hooks.json +0 -39
- package/core/harnesses/gemini/hooks/hooks.json +0 -77
- package/core/harnesses/gemini/hooks/lib.sh +0 -302
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -21
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -21
- package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -22
- package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
- package/core/harnesses/gemini/hooks/session-end.sh +0 -5
- package/core/harnesses/gemini/hooks/session-start.sh +0 -5
- package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
- package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
- package/core/harnesses/shared/hooks/hooks.json +0 -47
- package/core/shared/guidance/FLAGS.md +0 -25
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
- package/src/adapters/gemini/hooks.js +0 -210
- package/src/adapters/gemini/index.js +0 -390
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Roadmap rows: pure data model, seed parser, status derivation, pick table,
|
|
5
|
+
* markdown table render, and integrity check (feature 108, IC-001, IC-002, IC-003; FR-001, FR-002, FR-003).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
|
|
11
|
+
function safeText(value ) {
|
|
12
|
+
let text ;
|
|
13
|
+
if (typeof value === 'string') {
|
|
14
|
+
try {
|
|
15
|
+
text = JSON.stringify(value).slice(1, -1)
|
|
16
|
+
.replace(/\u2028/g, '\\u2028')
|
|
17
|
+
.replace(/\u2029/g, '\\u2029');
|
|
18
|
+
} catch {
|
|
19
|
+
text = '';
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
try {
|
|
23
|
+
const encoded = JSON.stringify(value);
|
|
24
|
+
text = encoded === undefined ? 'undefined' : encoded;
|
|
25
|
+
} catch {
|
|
26
|
+
text = '[unserializable]';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
const ID_PATTERN = /^[0-9]{3}([a-z]|-[A-Z])?$/;
|
|
128
|
+
const FORBIDDEN_KEYS = new Set(['status', 'phase', 'evidence']);
|
|
129
|
+
|
|
130
|
+
const SEED_RULES = [
|
|
131
|
+
'seed-invalid-json',
|
|
132
|
+
'seed-invalid',
|
|
133
|
+
'seed-unknown-key',
|
|
134
|
+
'seed-typed-status',
|
|
135
|
+
'slug-id-mismatch',
|
|
136
|
+
'duplicate-id',
|
|
137
|
+
'class-unknown',
|
|
138
|
+
'needs-unresolved',
|
|
139
|
+
'needs-cycle',
|
|
140
|
+
'id-exists',
|
|
141
|
+
'row-not-found',
|
|
142
|
+
'row-started',
|
|
143
|
+
'row-not-proposed',
|
|
144
|
+
] ;
|
|
145
|
+
|
|
146
|
+
const TOP_KEYS = ['schema', 'layout', 'rows'];
|
|
147
|
+
const ROW_KEYS = [
|
|
148
|
+
'id',
|
|
149
|
+
'slug',
|
|
150
|
+
'title',
|
|
151
|
+
'class',
|
|
152
|
+
'needs',
|
|
153
|
+
'writeSet',
|
|
154
|
+
'requires',
|
|
155
|
+
'hold',
|
|
156
|
+
'proposed',
|
|
157
|
+
'verifyArgv',
|
|
158
|
+
'mergeCommit',
|
|
159
|
+
'intent',
|
|
160
|
+
'acceptance',
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
let _cachedRegistryClasses = null;
|
|
164
|
+
|
|
165
|
+
function registryClasses() {
|
|
166
|
+
if (_cachedRegistryClasses === null) {
|
|
167
|
+
const registryPath = path.join(__dirname, '../../../core/registry/workflows.json');
|
|
168
|
+
const doc = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
169
|
+
_cachedRegistryClasses = Object.keys(doc.classes || {});
|
|
170
|
+
}
|
|
171
|
+
return _cachedRegistryClasses;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
class SeedParseError extends Error {
|
|
175
|
+
code ;
|
|
176
|
+
rowId ;
|
|
177
|
+
constructor(code , message , rowId ) {
|
|
178
|
+
super(message);
|
|
179
|
+
this.name = 'SeedParseError';
|
|
180
|
+
this.code = code;
|
|
181
|
+
this.rowId = rowId;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Validates a roadmap seed document (store or draft) without throwing.
|
|
194
|
+
* Returns every finding in stable IC-006 order.
|
|
195
|
+
*/
|
|
196
|
+
function seedFindings(
|
|
197
|
+
rawDoc ,
|
|
198
|
+
opts
|
|
199
|
+
) {
|
|
200
|
+
let doc ;
|
|
201
|
+
if (typeof rawDoc === 'string') {
|
|
202
|
+
try {
|
|
203
|
+
doc = JSON.parse(rawDoc);
|
|
204
|
+
} catch (err ) {
|
|
205
|
+
return [
|
|
206
|
+
{
|
|
207
|
+
rule: 'seed-invalid-json',
|
|
208
|
+
message: `Seed is not valid JSON: ${safeText(err.message)}`,
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
doc = rawDoc;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
217
|
+
return [
|
|
218
|
+
{
|
|
219
|
+
rule: 'seed-invalid',
|
|
220
|
+
message: 'Seed must be an object',
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const isDraft = Boolean(opts?.draft);
|
|
226
|
+
const items = [];
|
|
227
|
+
let seq = 0;
|
|
228
|
+
|
|
229
|
+
const add = (
|
|
230
|
+
rowPos ,
|
|
231
|
+
rule ,
|
|
232
|
+
message ,
|
|
233
|
+
rowId ,
|
|
234
|
+
rowIds
|
|
235
|
+
) => {
|
|
236
|
+
const finding = { rule, message };
|
|
237
|
+
if (rowId !== undefined) finding.rowId = rowId;
|
|
238
|
+
if (rowIds !== undefined) finding.rowIds = rowIds;
|
|
239
|
+
items.push({
|
|
240
|
+
rowPos,
|
|
241
|
+
ruleIdx: SEED_RULES.indexOf(rule),
|
|
242
|
+
seq: ++seq,
|
|
243
|
+
finding,
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// Document level checks (rowPos = -1)
|
|
248
|
+
// Top-level key status/phase/evidence -> seed-typed-status
|
|
249
|
+
for (const k of Object.keys(doc)) {
|
|
250
|
+
if (FORBIDDEN_KEYS.has(k)) {
|
|
251
|
+
add(-1, 'seed-typed-status', `Top-level seed declares forbidden key '${safeText(k)}'`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Any other key not in TOP_KEYS -> seed-unknown-key
|
|
256
|
+
// In draft mode TOP_KEYS is only schema and rows, so layout is seed-unknown-key
|
|
257
|
+
const allowedTop = isDraft ? new Set(['schema', 'rows']) : new Set(TOP_KEYS);
|
|
258
|
+
for (const k of Object.keys(doc)) {
|
|
259
|
+
if (!FORBIDDEN_KEYS.has(k) && !allowedTop.has(k)) {
|
|
260
|
+
add(-1, 'seed-unknown-key', `Top-level seed declares unknown key '${safeText(k)}'`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// In draft mode additionally schema must be exactly 1
|
|
265
|
+
if (isDraft && doc.schema !== 1) {
|
|
266
|
+
add(-1, 'seed-invalid', `Draft 'schema' must be 1, got ${safeText(doc.schema)}`);
|
|
267
|
+
} else if (!isDraft && doc.schema !== 1) {
|
|
268
|
+
add(-1, 'seed-invalid', "Seed 'schema' must be 1");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// rows not an array -> seed-invalid ("rows must be an array")
|
|
272
|
+
if (!Array.isArray(doc.rows)) {
|
|
273
|
+
add(-1, 'seed-invalid', "Seed 'rows' must be an array");
|
|
274
|
+
items.sort((a, b) => a.rowPos - b.rowPos || a.ruleIdx - b.ruleIdx || a.seq - b.seq);
|
|
275
|
+
return items.map((x) => x.finding);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// In draft mode additionally rows non-empty
|
|
279
|
+
if (isDraft && doc.rows.length === 0) {
|
|
280
|
+
add(-1, 'seed-invalid', "Draft 'rows' must not be empty");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Row level checks
|
|
284
|
+
const rowKeySet = new Set(ROW_KEYS);
|
|
285
|
+
const validClasses = opts?.classes ?? registryClasses();
|
|
286
|
+
const validClassSet = new Set(validClasses);
|
|
287
|
+
|
|
288
|
+
const seenIds = new Set ();
|
|
289
|
+
const allRowIds = new Set ();
|
|
290
|
+
for (const r of doc.rows) {
|
|
291
|
+
if (r && typeof r === 'object' && typeof r.id === 'string') {
|
|
292
|
+
allRowIds.add(r.id);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
for (let idx = 0; idx < doc.rows.length; idx++) {
|
|
297
|
+
const row = doc.rows[idx];
|
|
298
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) {
|
|
299
|
+
add(idx, 'seed-invalid', 'Each row must be an object');
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const rowId = typeof row.id === 'string' ? row.id : undefined;
|
|
304
|
+
|
|
305
|
+
// 1. seed-invalid:
|
|
306
|
+
// title: must be a non-empty single-line string (no \r \n U+2028 U+2029) of at most 120 characters (count with Array.from)
|
|
307
|
+
if (
|
|
308
|
+
typeof row.title !== 'string' ||
|
|
309
|
+
row.title.trim().length === 0 ||
|
|
310
|
+
Array.from(row.title).length > 120 ||
|
|
311
|
+
/[\r\n\u2028\u2029]/.test(row.title)
|
|
312
|
+
) {
|
|
313
|
+
add(
|
|
314
|
+
idx,
|
|
315
|
+
'seed-invalid',
|
|
316
|
+
`Row '${safeText(rowId ?? 'unknown')}' title must be a non-empty single-line string of at most 120 characters`,
|
|
317
|
+
rowId
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// needs: if present must be an array of strings (the key may be absent)
|
|
322
|
+
if (
|
|
323
|
+
row.needs !== undefined &&
|
|
324
|
+
(!Array.isArray(row.needs) || !row.needs.every((n ) => typeof n === 'string'))
|
|
325
|
+
) {
|
|
326
|
+
add(
|
|
327
|
+
idx,
|
|
328
|
+
'seed-invalid',
|
|
329
|
+
`Row '${safeText(rowId ?? 'unknown')}' needs must be an array of strings`,
|
|
330
|
+
rowId
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// writeSet: if present, an array of 1 to 32 canonical repo-relative paths.
|
|
335
|
+
if (row.writeSet !== undefined && validWriteSet(row.writeSet) === null) {
|
|
336
|
+
add(
|
|
337
|
+
idx,
|
|
338
|
+
'seed-invalid',
|
|
339
|
+
`Row '${safeText(rowId ?? 'unknown')}' writeSet must be 1 to 32 repo-relative paths with no leading slash, drive letter, backslash, empty, '.' or '..' segment`,
|
|
340
|
+
rowId
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// requires: if present an array of non-empty strings
|
|
345
|
+
if (
|
|
346
|
+
row.requires !== undefined &&
|
|
347
|
+
(!Array.isArray(row.requires) ||
|
|
348
|
+
!row.requires.every((s ) => typeof s === 'string' && s.trim().length > 0))
|
|
349
|
+
) {
|
|
350
|
+
add(
|
|
351
|
+
idx,
|
|
352
|
+
'seed-invalid',
|
|
353
|
+
`Row '${safeText(rowId ?? 'unknown')}' requires must be an array of non-empty strings`,
|
|
354
|
+
rowId
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// hold: as today (non-empty single-line string at most 120 chars)
|
|
359
|
+
if (
|
|
360
|
+
row.hold !== undefined &&
|
|
361
|
+
(typeof row.hold !== 'string' ||
|
|
362
|
+
row.hold.trim().length === 0 ||
|
|
363
|
+
Array.from(row.hold).length > 120 ||
|
|
364
|
+
/[\r\n\u2028\u2029]/.test(row.hold))
|
|
365
|
+
) {
|
|
366
|
+
add(
|
|
367
|
+
idx,
|
|
368
|
+
'seed-invalid',
|
|
369
|
+
`Row '${safeText(rowId ?? 'unknown')}' hold must be a non-empty single-line string of at most 120 characters`,
|
|
370
|
+
rowId
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// proposed (maint-thin): only `true`; absent means ready. Only seed apply/amend writes it, so a draft may not
|
|
375
|
+
// declare it at all (a draft cannot pre-mark a row, and no model can launder a promotion).
|
|
376
|
+
if (isDraft && 'proposed' in row) {
|
|
377
|
+
add(
|
|
378
|
+
idx,
|
|
379
|
+
'seed-invalid',
|
|
380
|
+
`Draft row '${safeText(rowId ?? 'unknown')}' must not declare proposed; seed apply marks it`,
|
|
381
|
+
rowId
|
|
382
|
+
);
|
|
383
|
+
} else if (row.proposed !== undefined && row.proposed !== true) {
|
|
384
|
+
add(
|
|
385
|
+
idx,
|
|
386
|
+
'seed-invalid',
|
|
387
|
+
`Row '${safeText(rowId ?? 'unknown')}' proposed must be true when present`,
|
|
388
|
+
rowId
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// verifyArgv: as today (1 to 8 non-empty strings)
|
|
393
|
+
if (
|
|
394
|
+
row.verifyArgv !== undefined &&
|
|
395
|
+
(!Array.isArray(row.verifyArgv) ||
|
|
396
|
+
row.verifyArgv.length === 0 ||
|
|
397
|
+
row.verifyArgv.length > 8 ||
|
|
398
|
+
!row.verifyArgv.every((arg ) => typeof arg === 'string' && arg.trim().length > 0))
|
|
399
|
+
) {
|
|
400
|
+
add(
|
|
401
|
+
idx,
|
|
402
|
+
'seed-invalid',
|
|
403
|
+
`Row '${safeText(rowId ?? 'unknown')}' verifyArgv must be 1 to 8 non-empty strings`,
|
|
404
|
+
rowId
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// mergeCommit: if present a 40-hex lowercase string (store mode only)
|
|
409
|
+
if (!isDraft && row.mergeCommit !== undefined) {
|
|
410
|
+
if (typeof row.mergeCommit !== 'string' || !/^[0-9a-f]{40}$/.test(row.mergeCommit)) {
|
|
411
|
+
add(
|
|
412
|
+
idx,
|
|
413
|
+
'seed-invalid',
|
|
414
|
+
`Row '${safeText(rowId ?? 'unknown')}' mergeCommit must be a 40-hex lowercase string`,
|
|
415
|
+
rowId
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// intent: if present a non-empty single-line string at most 200 characters
|
|
421
|
+
if (
|
|
422
|
+
row.intent !== undefined &&
|
|
423
|
+
(typeof row.intent !== 'string' ||
|
|
424
|
+
row.intent.trim().length === 0 ||
|
|
425
|
+
Array.from(row.intent).length > 200 ||
|
|
426
|
+
/[\r\n\u2028\u2029]/.test(row.intent))
|
|
427
|
+
) {
|
|
428
|
+
add(
|
|
429
|
+
idx,
|
|
430
|
+
'seed-invalid',
|
|
431
|
+
`Row '${safeText(rowId ?? 'unknown')}' intent must be a non-empty single-line string of at most 200 characters`,
|
|
432
|
+
rowId
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// acceptance: if present an array of 1 to 8 non-empty single-line strings, each at most 200 characters
|
|
437
|
+
if (
|
|
438
|
+
row.acceptance !== undefined &&
|
|
439
|
+
(!Array.isArray(row.acceptance) ||
|
|
440
|
+
row.acceptance.length < 1 ||
|
|
441
|
+
row.acceptance.length > 8 ||
|
|
442
|
+
!row.acceptance.every(
|
|
443
|
+
(s ) =>
|
|
444
|
+
typeof s === 'string' &&
|
|
445
|
+
s.trim().length > 0 &&
|
|
446
|
+
Array.from(s).length <= 200 &&
|
|
447
|
+
!/[\r\n\u2028\u2029]/.test(s)
|
|
448
|
+
))
|
|
449
|
+
) {
|
|
450
|
+
add(
|
|
451
|
+
idx,
|
|
452
|
+
'seed-invalid',
|
|
453
|
+
`Row '${safeText(rowId ?? 'unknown')}' acceptance must be an array of 1 to 8 non-empty single-line strings of at most 200 characters`,
|
|
454
|
+
rowId
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// 2. seed-unknown-key: a key not in ROW_KEYS (excluding forbidden keys and draft mergeCommit)
|
|
459
|
+
for (const k of Object.keys(row)) {
|
|
460
|
+
if (!FORBIDDEN_KEYS.has(k) && !(isDraft && k === 'mergeCommit') && !rowKeySet.has(k)) {
|
|
461
|
+
add(idx, 'seed-unknown-key', `Row '${safeText(rowId ?? 'unknown')}' declares unknown key '${safeText(k)}'`, rowId);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// 3. seed-typed-status: key status/phase/evidence; in draft mode also a mergeCommit key
|
|
466
|
+
for (const k of Object.keys(row)) {
|
|
467
|
+
if (FORBIDDEN_KEYS.has(k)) {
|
|
468
|
+
add(idx, 'seed-typed-status', `Row '${safeText(rowId ?? 'unknown')}' declares forbidden key '${safeText(k)}'`, rowId);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (isDraft && 'mergeCommit' in row) {
|
|
472
|
+
add(idx, 'seed-typed-status', `Draft row '${safeText(rowId ?? 'unknown')}' declares forbidden key 'mergeCommit'`, rowId);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// 4. slug-id-mismatch: id not matching pattern or slug not starting with id-
|
|
476
|
+
if (typeof row.id !== 'string' || !ID_PATTERN.test(row.id)) {
|
|
477
|
+
add(
|
|
478
|
+
idx,
|
|
479
|
+
'slug-id-mismatch',
|
|
480
|
+
`Row id '${safeText(row.id)}' does not match pattern ^[0-9]{3}([a-z]|-[A-Z])?$`,
|
|
481
|
+
rowId
|
|
482
|
+
);
|
|
483
|
+
} else if (typeof row.slug !== 'string' || !row.slug.startsWith(`${row.id}-`)) {
|
|
484
|
+
add(
|
|
485
|
+
idx,
|
|
486
|
+
'slug-id-mismatch',
|
|
487
|
+
`Row slug '${safeText(row.slug)}' must start with '${safeText(row.id)}-'`,
|
|
488
|
+
rowId
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// 5. duplicate-id: reported on each repeated occurrence of an already-seen id (the later row)
|
|
493
|
+
if (typeof row.id === 'string' && ID_PATTERN.test(row.id)) {
|
|
494
|
+
if (seenIds.has(row.id)) {
|
|
495
|
+
add(idx, 'duplicate-id', `Duplicate row id: ${safeText(row.id)}`, rowId);
|
|
496
|
+
} else {
|
|
497
|
+
seenIds.add(row.id);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 6. class-unknown: class not a string in opts.classes ?? registryClasses(); message lists the valid set
|
|
502
|
+
if (typeof row.class !== 'string' || !validClassSet.has(row.class)) {
|
|
503
|
+
add(
|
|
504
|
+
idx,
|
|
505
|
+
'class-unknown',
|
|
506
|
+
`Row '${safeText(rowId ?? 'unknown')}' class '${safeText(row.class)}' is unknown; valid classes: ${validClasses.join(', ')}`,
|
|
507
|
+
rowId
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// 7. needs-unresolved (skipped in draft mode)
|
|
512
|
+
if (!isDraft && Array.isArray(row.needs)) {
|
|
513
|
+
for (const need of row.needs) {
|
|
514
|
+
if (typeof need === 'string' && !allRowIds.has(need)) {
|
|
515
|
+
add(
|
|
516
|
+
idx,
|
|
517
|
+
'needs-unresolved',
|
|
518
|
+
`Row '${safeText(rowId ?? 'unknown')}' declares unresolved need '${safeText(need)}'`,
|
|
519
|
+
rowId
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 8. needs-cycle (skipped in draft mode)
|
|
527
|
+
if (!isDraft) {
|
|
528
|
+
const firstRowById = new Map ();
|
|
529
|
+
const firstRowList = [];
|
|
530
|
+
|
|
531
|
+
for (let idx = 0; idx < doc.rows.length; idx++) {
|
|
532
|
+
const r = doc.rows[idx];
|
|
533
|
+
if (r && typeof r === 'object' && typeof r.id === 'string') {
|
|
534
|
+
if (!firstRowById.has(r.id)) {
|
|
535
|
+
const entry = { row: r, docPos: idx };
|
|
536
|
+
firstRowById.set(r.id, entry);
|
|
537
|
+
firstRowList.push(entry);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const WHITE = 0;
|
|
543
|
+
const GRAY = 1;
|
|
544
|
+
const BLACK = 2;
|
|
545
|
+
const color = new Map ();
|
|
546
|
+
const callStack = [];
|
|
547
|
+
const seenCycleKeys = new Set ();
|
|
548
|
+
|
|
549
|
+
const dfs = (u ) => {
|
|
550
|
+
color.set(u, GRAY);
|
|
551
|
+
callStack.push(u);
|
|
552
|
+
|
|
553
|
+
const entry = firstRowById.get(u);
|
|
554
|
+
const uNeeds = entry && Array.isArray(entry.row.needs) ? entry.row.needs : [];
|
|
555
|
+
|
|
556
|
+
for (const v of uNeeds) {
|
|
557
|
+
if (typeof v !== 'string' || !firstRowById.has(v)) continue;
|
|
558
|
+
const vColor = color.get(v) ?? WHITE;
|
|
559
|
+
if (vColor === GRAY) {
|
|
560
|
+
// Cycle found
|
|
561
|
+
const cycleStartIdx = callStack.indexOf(v);
|
|
562
|
+
const cyclePath = callStack.slice(cycleStartIdx);
|
|
563
|
+
|
|
564
|
+
let minPos = Infinity;
|
|
565
|
+
let minIdx = 0;
|
|
566
|
+
for (let i = 0; i < cyclePath.length; i++) {
|
|
567
|
+
const pos = firstRowById.get(cyclePath[i]) .docPos;
|
|
568
|
+
if (pos < minPos) {
|
|
569
|
+
minPos = pos;
|
|
570
|
+
minIdx = i;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const canonicalPath = cyclePath.slice(minIdx).concat(cyclePath.slice(0, minIdx));
|
|
574
|
+
const cycleKey = canonicalPath.join('->');
|
|
575
|
+
if (!seenCycleKeys.has(cycleKey)) {
|
|
576
|
+
seenCycleKeys.add(cycleKey);
|
|
577
|
+
const msg = `needs cycle ${canonicalPath.map(safeText).join(' -> ')} -> ${safeText(canonicalPath[0])}`;
|
|
578
|
+
add(minPos, 'needs-cycle', msg, undefined, canonicalPath);
|
|
579
|
+
}
|
|
580
|
+
} else if (vColor === WHITE) {
|
|
581
|
+
dfs(v);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
callStack.pop();
|
|
586
|
+
color.set(u, BLACK);
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
for (const entry of firstRowList) {
|
|
590
|
+
if ((color.get(entry.row.id) ?? WHITE) === WHITE) {
|
|
591
|
+
dfs(entry.row.id);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Sort findings: document-level (-1) first, then by rowPos, then by ruleIdx, then seq
|
|
597
|
+
items.sort((a, b) => a.rowPos - b.rowPos || a.ruleIdx - b.ruleIdx || a.seq - b.seq);
|
|
598
|
+
return items.map((x) => x.finding);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Validates and parses rows.json seed data.
|
|
603
|
+
* Throws SeedParseError for the first finding if any finding exists.
|
|
604
|
+
*/
|
|
605
|
+
function parseSeed(raw ) {
|
|
606
|
+
const findings = seedFindings(raw, {});
|
|
607
|
+
if (findings.length > 0) {
|
|
608
|
+
const first = findings[0];
|
|
609
|
+
throw new SeedParseError(first.rule, first.message, first.rowId ?? first.rowIds?.[0]);
|
|
610
|
+
}
|
|
611
|
+
const doc = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
612
|
+
return doc ;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function normalizePath(p ) {
|
|
616
|
+
return p.replace(/^\.\//, '').replace(/^\//, '');
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function pathsOverlap(p1 , p2 ) {
|
|
620
|
+
const n1 = normalizePath(p1);
|
|
621
|
+
const n2 = normalizePath(p2);
|
|
622
|
+
if (n1 === n2) return true;
|
|
623
|
+
if (n1.endsWith('/')) {
|
|
624
|
+
if (n2.startsWith(n1)) return true;
|
|
625
|
+
} else {
|
|
626
|
+
if (n2.startsWith(`${n1}/`)) return true;
|
|
627
|
+
}
|
|
628
|
+
if (n2.endsWith('/')) {
|
|
629
|
+
if (n1.startsWith(n2)) return true;
|
|
630
|
+
} else {
|
|
631
|
+
if (n1.startsWith(`${n2}/`)) return true;
|
|
632
|
+
}
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function validWriteSet(value ) {
|
|
637
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 32) return null;
|
|
638
|
+
const canonical = (entry ) => {
|
|
639
|
+
if (typeof entry !== 'string' || entry.trim().length === 0 || entry.startsWith('/') || /^[A-Za-z]:/.test(entry) || entry.includes('\\')) {
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
const withoutDirectoryMarker = entry.endsWith('/') ? entry.slice(0, -1) : entry;
|
|
643
|
+
return withoutDirectoryMarker.split('/').every((segment) => segment !== '' && segment !== '.' && segment !== '..');
|
|
644
|
+
};
|
|
645
|
+
return value.every(canonical) ? value : null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function writeSetsOverlap(ws1 , ws2 ) {
|
|
649
|
+
if (!ws1 || !ws2 || ws1.length === 0 || ws2.length === 0) return false;
|
|
650
|
+
for (const p1 of ws1) {
|
|
651
|
+
for (const p2 of ws2) {
|
|
652
|
+
if (pathsOverlap(p1, p2)) return true;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Derives the status of each row from git facts only (merges and `feat/<slug>` branches); the run
|
|
660
|
+
* record's holds and pauses are machine state, which `status` shows separately (C11, C19).
|
|
661
|
+
* Trailed merges in git develop history take precedence; subjects are NEVER evidence.
|
|
662
|
+
*/
|
|
663
|
+
function deriveStatus(
|
|
664
|
+
rows ,
|
|
665
|
+
gitFacts = {}
|
|
666
|
+
) {
|
|
667
|
+
const statuses = new Map ();
|
|
668
|
+
const rowStatus = {};
|
|
669
|
+
|
|
670
|
+
// Group 2-parent first-parent merges on develop by DoFlow-Row trailer value
|
|
671
|
+
const trailerMergesByRow = new Map ();
|
|
672
|
+
for (const m of gitFacts.merges || []) {
|
|
673
|
+
if (m.parents && m.parents.length === 2 && m.rowTrailer) {
|
|
674
|
+
const rowId = m.rowTrailer.trim();
|
|
675
|
+
if (!trailerMergesByRow.has(rowId)) trailerMergesByRow.set(rowId, []);
|
|
676
|
+
trailerMergesByRow.get(rowId) .push(m);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
for (const row of rows) {
|
|
681
|
+
let status = 'not-started';
|
|
682
|
+
let evidence = null;
|
|
683
|
+
let holdReason = null;
|
|
684
|
+
|
|
685
|
+
const trailerMerges = trailerMergesByRow.get(row.id) || [];
|
|
686
|
+
if (trailerMerges.length > 0) {
|
|
687
|
+
status = 'merged';
|
|
688
|
+
const m = trailerMerges[0];
|
|
689
|
+
evidence = {
|
|
690
|
+
kind: 'trailer',
|
|
691
|
+
sha: m.sha,
|
|
692
|
+
shortSha: m.sha.slice(0, 7),
|
|
693
|
+
};
|
|
694
|
+
} else if (row.mergeCommit) {
|
|
695
|
+
// Must be a 2-parent first-parent merge in git develop history
|
|
696
|
+
const matchingMerge = (gitFacts.merges || []).find(
|
|
697
|
+
(m) => m.sha === row.mergeCommit && m.parents && m.parents.length === 2
|
|
698
|
+
);
|
|
699
|
+
if (matchingMerge) {
|
|
700
|
+
status = 'merged';
|
|
701
|
+
evidence = {
|
|
702
|
+
kind: 'seeded',
|
|
703
|
+
sha: row.mergeCommit,
|
|
704
|
+
shortSha: row.mergeCommit.slice(0, 7),
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (status !== 'merged') {
|
|
710
|
+
const branchName = `feat/${row.slug}`;
|
|
711
|
+
const branchExists =
|
|
712
|
+
(gitFacts.branches || []).includes(branchName) ||
|
|
713
|
+
(gitFacts.branches || []).includes(`refs/heads/${branchName}`);
|
|
714
|
+
|
|
715
|
+
if (branchExists) status = 'in-flight';
|
|
716
|
+
// maint-thin: an unstarted row a seed write proposed waits for a person's promote.
|
|
717
|
+
else if (row.proposed === true) status = 'proposed';
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// A seed-level hold is reported for unmerged rows.
|
|
721
|
+
if (status !== 'merged' && row.hold) holdReason = row.hold;
|
|
722
|
+
|
|
723
|
+
const info = {
|
|
724
|
+
rowId: row.id,
|
|
725
|
+
status,
|
|
726
|
+
evidence,
|
|
727
|
+
holdReason,
|
|
728
|
+
};
|
|
729
|
+
statuses.set(row.id, info);
|
|
730
|
+
rowStatus[row.id] = status;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return { statuses, rowStatus };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* S1 Pick table: chooses the next ready row in seed order.
|
|
738
|
+
* - Ready only when all needs are merged
|
|
739
|
+
* - Write set disjoint from all in-flight rows
|
|
740
|
+
* - WIP bound (default 2)
|
|
741
|
+
* - Row without a write set runs alone
|
|
742
|
+
* - Non-feature class is class-unsupported
|
|
743
|
+
* - Yields one reason per skipped row
|
|
744
|
+
*/
|
|
745
|
+
function pick(
|
|
746
|
+
rows ,
|
|
747
|
+
statuses ,
|
|
748
|
+
options = {}
|
|
749
|
+
) {
|
|
750
|
+
const wip = options.wip ?? 2;
|
|
751
|
+
const getStatus = (id ) => {
|
|
752
|
+
if (statuses instanceof Map) {
|
|
753
|
+
return statuses.get(id)?.status || 'not-started';
|
|
754
|
+
}
|
|
755
|
+
const val = statuses[id];
|
|
756
|
+
if (typeof val === 'string') return val;
|
|
757
|
+
return val?.status || 'not-started';
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
const inFlightRows = rows.filter((r) => getStatus(r.id) === 'in-flight');
|
|
761
|
+
const inFlightCount = inFlightRows.length;
|
|
762
|
+
const exclusiveInFlight = inFlightRows.some((r) => !r.writeSet || r.writeSet.length === 0);
|
|
763
|
+
|
|
764
|
+
const skips = {};
|
|
765
|
+
let picked = null;
|
|
766
|
+
|
|
767
|
+
for (const row of rows) {
|
|
768
|
+
if (row.hold) {
|
|
769
|
+
skips[row.id] = 'row-held';
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const status = getStatus(row.id);
|
|
774
|
+
if (status === 'merged' || status === 'in-flight') {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// maint-thin: autopilot never picks a proposed row; a person promotes it first.
|
|
779
|
+
if (row.proposed === true) {
|
|
780
|
+
skips[row.id] = 'row-proposed';
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Class check
|
|
785
|
+
if (row.class !== 'feature') {
|
|
786
|
+
skips[row.id] = 'class-unsupported';
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Dependencies check
|
|
791
|
+
const hasUnmergedNeed = (row.needs || []).some((needId) => getStatus(needId) !== 'merged');
|
|
792
|
+
if (hasUnmergedNeed) {
|
|
793
|
+
skips[row.id] = 'need-unmerged';
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Exclusive in-flight check (another row running alone)
|
|
798
|
+
if (exclusiveInFlight) {
|
|
799
|
+
skips[row.id] = 'exclusive-row-in-flight';
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Candidate has no write set and requires running alone
|
|
804
|
+
const hasNoWriteSet = !row.writeSet || row.writeSet.length === 0;
|
|
805
|
+
if (hasNoWriteSet && inFlightCount > 0) {
|
|
806
|
+
skips[row.id] = 'needs-exclusive';
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Write set collision check
|
|
811
|
+
const overlapping = inFlightRows.find((ifr) => writeSetsOverlap(row.writeSet, ifr.writeSet));
|
|
812
|
+
if (overlapping) {
|
|
813
|
+
skips[row.id] = `write-set-overlap:${overlapping.id}`;
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// WIP cap check
|
|
818
|
+
if (inFlightCount >= wip) {
|
|
819
|
+
skips[row.id] = 'wip-full';
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Row is ready!
|
|
824
|
+
if (!picked) {
|
|
825
|
+
picked = row;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return { picked, skips };
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Renders byte-stable ROADMAP.md markdown table.
|
|
834
|
+
* Columns: Row, Title, Class, Needs, Status, Evidence, Next.
|
|
835
|
+
*/
|
|
836
|
+
function render(
|
|
837
|
+
rows ,
|
|
838
|
+
statuses
|
|
839
|
+
) {
|
|
840
|
+
const getInfo = (id ) => {
|
|
841
|
+
if (statuses instanceof Map) {
|
|
842
|
+
return statuses.get(id) || { rowId: id, status: 'not-started', evidence: null };
|
|
843
|
+
}
|
|
844
|
+
return statuses[id] || { rowId: id, status: 'not-started', evidence: null };
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
const pickRes = pick(rows, statuses);
|
|
848
|
+
|
|
849
|
+
const lines = [
|
|
850
|
+
'# Roadmap',
|
|
851
|
+
'',
|
|
852
|
+
'| Row | Title | Class | Needs | Status | Evidence | Next |',
|
|
853
|
+
'|---|---|---|---|---|---|---|',
|
|
854
|
+
];
|
|
855
|
+
|
|
856
|
+
for (const row of rows) {
|
|
857
|
+
const info = getInfo(row.id);
|
|
858
|
+
const needsStr = row.needs && row.needs.length > 0 ? row.needs.join(', ') : '-';
|
|
859
|
+
const statusStr = info.status;
|
|
860
|
+
|
|
861
|
+
let evidenceStr = '-';
|
|
862
|
+
if (info.evidence) {
|
|
863
|
+
evidenceStr = `${info.evidence.shortSha} (${info.evidence.kind})`;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
let nextStr = '-';
|
|
867
|
+
if (info.status === 'merged') {
|
|
868
|
+
nextStr = '-';
|
|
869
|
+
} else if (row.hold) {
|
|
870
|
+
nextStr = `held: ${row.hold}`;
|
|
871
|
+
} else if (info.status === 'in-flight') {
|
|
872
|
+
nextStr = info.holdReason || '-';
|
|
873
|
+
} else {
|
|
874
|
+
// not-started
|
|
875
|
+
if (pickRes.picked && pickRes.picked.id === row.id) {
|
|
876
|
+
nextStr = 'pick';
|
|
877
|
+
} else if (pickRes.skips[row.id]) {
|
|
878
|
+
nextStr = pickRes.skips[row.id];
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
lines.push(`| ${row.id} | ${row.title} | ${row.class} | ${needsStr} | ${statusStr} | ${evidenceStr} | ${nextStr} |`);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
lines.push('');
|
|
886
|
+
return lines.join('\n');
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Writes rendered table to disk using atomic temp+rename only when bytes differ.
|
|
891
|
+
*/
|
|
892
|
+
function renderRoadmapFile({
|
|
893
|
+
roadmapPath,
|
|
894
|
+
content,
|
|
895
|
+
fsImpl = fs,
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
) {
|
|
901
|
+
let existing = null;
|
|
902
|
+
try {
|
|
903
|
+
existing = fsImpl.readFileSync(roadmapPath, 'utf8');
|
|
904
|
+
} catch {
|
|
905
|
+
existing = null;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const bytes = Buffer.byteLength(content, 'utf8');
|
|
909
|
+
if (existing === content) {
|
|
910
|
+
return { written: false, bytes };
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const dir = path.dirname(roadmapPath);
|
|
914
|
+
fsImpl.mkdirSync(dir, { recursive: true });
|
|
915
|
+
const tmpPath = path.join(dir, `.tmp-roadmap-${process.pid}-${Date.now()}`);
|
|
916
|
+
try {
|
|
917
|
+
fsImpl.writeFileSync(tmpPath, content, 'utf8');
|
|
918
|
+
fsImpl.renameSync(tmpPath, roadmapPath);
|
|
919
|
+
} catch (error) {
|
|
920
|
+
try { fsImpl.unlinkSync(tmpPath); } catch { /* ignore temp cleanup failure */ }
|
|
921
|
+
throw error;
|
|
922
|
+
}
|
|
923
|
+
return { written: true, bytes };
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Performs IC-003 integrity check on seed, git facts, and rendered table:
|
|
928
|
+
* 1. render-drift
|
|
929
|
+
* 2. duplicate-merge
|
|
930
|
+
* 3. trailer-unknown-row
|
|
931
|
+
* 4. commit-claimed-twice
|
|
932
|
+
* 5. trailered-and-seeded
|
|
933
|
+
* 6. pointer-not-merge
|
|
934
|
+
* 7. needs-unresolved
|
|
935
|
+
* 8. duplicate-id
|
|
936
|
+
* 9. slug-id-mismatch
|
|
937
|
+
* Plus: seed-typed-status
|
|
938
|
+
*/
|
|
939
|
+
function check({
|
|
940
|
+
rows = [],
|
|
941
|
+
gitFacts = {},
|
|
942
|
+
rendered = null,
|
|
943
|
+
seedRaw = null,
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
) {
|
|
950
|
+
const findings = [];
|
|
951
|
+
const offendingRowIdsSet = new Set ();
|
|
952
|
+
|
|
953
|
+
const addFinding = (finding ) => {
|
|
954
|
+
findings.push(finding);
|
|
955
|
+
if (finding.rowId) offendingRowIdsSet.add(finding.rowId);
|
|
956
|
+
if (finding.rowIds) {
|
|
957
|
+
for (const id of finding.rowIds) offendingRowIdsSet.add(id);
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
// 1. Validate seed with seedFindings
|
|
962
|
+
const seedInput =
|
|
963
|
+
seedRaw !== undefined && seedRaw !== null
|
|
964
|
+
? seedRaw
|
|
965
|
+
: {
|
|
966
|
+
schema: 1,
|
|
967
|
+
rows: rows.map((r) =>
|
|
968
|
+
r && typeof r === 'object'
|
|
969
|
+
? { ...r, title: r.title ?? r.slug ?? r.id ?? 'row' }
|
|
970
|
+
: r
|
|
971
|
+
),
|
|
972
|
+
};
|
|
973
|
+
const seedFindingList = seedFindings(seedInput, {});
|
|
974
|
+
for (const f of seedFindingList) {
|
|
975
|
+
addFinding(f);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// 2. Compute effectiveRows and rowIdSet for git checks
|
|
979
|
+
let rawObj = null;
|
|
980
|
+
if (seedRaw !== undefined && seedRaw !== null) {
|
|
981
|
+
if (typeof seedRaw === 'string') {
|
|
982
|
+
try {
|
|
983
|
+
rawObj = JSON.parse(seedRaw);
|
|
984
|
+
} catch {
|
|
985
|
+
rawObj = null;
|
|
986
|
+
}
|
|
987
|
+
} else {
|
|
988
|
+
rawObj = seedRaw;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const fallbackRawRows = (rawObj && Array.isArray(rawObj.rows)) ? rawObj.rows : [];
|
|
993
|
+
const effectiveRows = rows.length > 0 ? rows : fallbackRawRows.filter((r ) => r && typeof r === 'object' && r.id);
|
|
994
|
+
const rowIdSet = new Set (effectiveRows.map((r ) => r.id));
|
|
995
|
+
|
|
996
|
+
// 3. Git trailer checks
|
|
997
|
+
const trailerMerges = (gitFacts.merges || []).filter(
|
|
998
|
+
(m) => m.parents && m.parents.length === 2 && Boolean(m.rowTrailer)
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
const trailerCountByRow = new Map ();
|
|
1002
|
+
for (const m of trailerMerges) {
|
|
1003
|
+
const rowId = m.rowTrailer .trim();
|
|
1004
|
+
if (!trailerCountByRow.has(rowId)) trailerCountByRow.set(rowId, []);
|
|
1005
|
+
trailerCountByRow.get(rowId) .push(m);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
for (const [rowId, list] of trailerCountByRow.entries()) {
|
|
1009
|
+
if (!rowIdSet.has(rowId)) {
|
|
1010
|
+
addFinding({
|
|
1011
|
+
rule: 'trailer-unknown-row',
|
|
1012
|
+
message: `Merge trailer references unknown row '${rowId}'`,
|
|
1013
|
+
rowId,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
if (list.length > 1) {
|
|
1017
|
+
addFinding({
|
|
1018
|
+
rule: 'duplicate-merge',
|
|
1019
|
+
message: `Row '${rowId}' has multiple merge commits (${list.map((x) => x.sha).join(', ')})`,
|
|
1020
|
+
rowId,
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// 4. Seeded pointer checks & trailered-and-seeded
|
|
1026
|
+
for (const r of effectiveRows) {
|
|
1027
|
+
if (r.mergeCommit) {
|
|
1028
|
+
// Check if also trailered
|
|
1029
|
+
if (trailerCountByRow.has(r.id)) {
|
|
1030
|
+
addFinding({
|
|
1031
|
+
rule: 'trailered-and-seeded',
|
|
1032
|
+
message: `Row '${r.id}' has both mergeCommit and git trailer`,
|
|
1033
|
+
rowId: r.id,
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Check if mergeCommit is a 2-parent first-parent merge
|
|
1038
|
+
const isTwoParentMerge = (gitFacts.merges || []).some(
|
|
1039
|
+
(m) => m.sha === r.mergeCommit && m.parents && m.parents.length === 2
|
|
1040
|
+
);
|
|
1041
|
+
if (!isTwoParentMerge) {
|
|
1042
|
+
addFinding({
|
|
1043
|
+
rule: 'pointer-not-merge',
|
|
1044
|
+
message: `Row '${r.id}' seeded mergeCommit '${r.mergeCommit}' is not a first-parent two-parent merge`,
|
|
1045
|
+
rowId: r.id,
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// 5. Commit claimed twice
|
|
1052
|
+
const commitClaimants = new Map ();
|
|
1053
|
+
for (const r of effectiveRows) {
|
|
1054
|
+
if (r.mergeCommit) {
|
|
1055
|
+
if (!commitClaimants.has(r.mergeCommit)) commitClaimants.set(r.mergeCommit, new Set());
|
|
1056
|
+
commitClaimants.get(r.mergeCommit) .add(r.id);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
for (const m of trailerMerges) {
|
|
1060
|
+
const rowId = m.rowTrailer .trim();
|
|
1061
|
+
if (!commitClaimants.has(m.sha)) commitClaimants.set(m.sha, new Set());
|
|
1062
|
+
commitClaimants.get(m.sha) .add(rowId);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
for (const [sha, claimants] of commitClaimants.entries()) {
|
|
1066
|
+
if (claimants.size > 1) {
|
|
1067
|
+
addFinding({
|
|
1068
|
+
rule: 'commit-claimed-twice',
|
|
1069
|
+
message: `Commit '${sha}' is claimed by multiple rows: ${Array.from(claimants).join(', ')}`,
|
|
1070
|
+
rowIds: Array.from(claimants),
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// 6. Render drift check (only when parsed rows are valid without seed syntax errors)
|
|
1076
|
+
const storeSeedRules = new Set(SEED_RULES.slice(0, 9));
|
|
1077
|
+
const hasSeedErrors = findings.some((f) => storeSeedRules.has(f.rule ));
|
|
1078
|
+
if (rendered !== undefined && rendered !== null && rows.length > 0 && !hasSeedErrors) {
|
|
1079
|
+
const { statuses } = deriveStatus(rows, gitFacts);
|
|
1080
|
+
const expected = render(rows, statuses);
|
|
1081
|
+
if (rendered !== expected) {
|
|
1082
|
+
addFinding({
|
|
1083
|
+
rule: 'render-drift',
|
|
1084
|
+
message: 'Rendered ROADMAP.md differs from derived status table',
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const offendingRowIds = Array.from(offendingRowIdsSet);
|
|
1090
|
+
return {
|
|
1091
|
+
ok: findings.length === 0,
|
|
1092
|
+
findings,
|
|
1093
|
+
offendingRowIds,
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
module.exports = {
|
|
1098
|
+
parseSeed,
|
|
1099
|
+
safeText,
|
|
1100
|
+
validWriteSet,
|
|
1101
|
+
pathsOverlap,
|
|
1102
|
+
writeSetsOverlap,
|
|
1103
|
+
deriveStatus,
|
|
1104
|
+
pick,
|
|
1105
|
+
render,
|
|
1106
|
+
renderRoadmapFile,
|
|
1107
|
+
check,
|
|
1108
|
+
seedFindings,
|
|
1109
|
+
registryClasses,
|
|
1110
|
+
SEED_RULES,
|
|
1111
|
+
ROW_KEYS,
|
|
1112
|
+
TOP_KEYS,
|
|
1113
|
+
};
|