@khoavu882/doflow 0.0.0-stage → 2.0.0-beta.3
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 +132 -2
- package/bin/doflow.js +44 -0
- package/core/.antigravity-plugin/hooks.json +39 -0
- package/core/.antigravity-plugin/mcp_config.json +18 -0
- package/core/.antigravity-plugin/plugin.json +31 -0
- package/core/.claude-plugin/marketplace.json +20 -0
- package/core/.claude-plugin/plugin.json +21 -0
- package/core/.codex-plugin/marketplace.json +20 -0
- package/core/.codex-plugin/plugin.json +35 -0
- package/core/.plugin/plugin.json +19 -0
- package/core/harnesses/antigravity/hooks/pre-implementation-gate.sh +9 -0
- package/core/harnesses/antigravity/hooks/stop-check.sh +10 -0
- package/core/harnesses/claude/hooks/lib.sh +304 -0
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +20 -0
- package/core/harnesses/claude/hooks/permission-audit.sh +47 -0
- package/core/harnesses/claude/hooks/post-compact.sh +74 -0
- package/core/harnesses/claude/hooks/post-edit-lint.sh +36 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +47 -0
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +20 -0
- package/core/harnesses/claude/hooks/pre-compact.sh +43 -0
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +21 -0
- package/core/harnesses/claude/hooks/session-end.sh +89 -0
- package/core/harnesses/claude/hooks/session-start.sh +5 -0
- package/core/harnesses/claude/hooks/skill-config-audit.sh +39 -0
- package/core/harnesses/claude/hooks/stop-check.sh +5 -0
- package/core/harnesses/claude/hooks/subagent-audit.sh +7 -0
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -0
- package/core/harnesses/claude/settings/keybindings.json +20 -0
- package/core/harnesses/claude/settings/settings.json +201 -0
- package/core/harnesses/claude/statusline/doflow-statusline.sh +49 -0
- package/core/harnesses/codex/agents/core-implementer.toml +7 -0
- package/core/harnesses/codex/agents/quality-guardian.toml +8 -0
- package/core/harnesses/codex/agents/research-writer.toml +8 -0
- package/core/harnesses/codex/agents/spec-analyst.toml +8 -0
- package/core/harnesses/codex/agents/system-architect.toml +9 -0
- package/core/harnesses/codex/config/config.toml +10 -0
- package/core/harnesses/codex/hooks/hooks.json +122 -0
- package/core/harnesses/codex/hooks/lib.sh +335 -0
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +18 -0
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +52 -0
- package/core/harnesses/codex/hooks/post-edit-lint.sh +6 -0
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +18 -0
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +46 -0
- package/core/harnesses/codex/hooks/pre-compact.sh +9 -0
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +19 -0
- package/core/harnesses/codex/hooks/session-end.impl.sh +92 -0
- package/core/harnesses/codex/hooks/session-end.sh +5 -0
- package/core/harnesses/codex/hooks/session-start.sh +5 -0
- package/core/harnesses/codex/hooks/stop-check.sh +8 -0
- package/core/harnesses/codex/hooks/subagent-audit.sh +8 -0
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +6 -0
- package/core/harnesses/gemini/hooks/hooks.json +77 -0
- package/core/harnesses/gemini/hooks/lib.sh +302 -0
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +21 -0
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +39 -0
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +21 -0
- package/core/harnesses/gemini/hooks/pre-compact.sh +38 -0
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +22 -0
- package/core/harnesses/gemini/hooks/session-end.impl.sh +89 -0
- package/core/harnesses/gemini/hooks/session-end.sh +5 -0
- package/core/harnesses/gemini/hooks/session-start.sh +5 -0
- package/core/harnesses/gemini/settings/adapter-defaults.json +4 -0
- package/core/harnesses/kiro/agents/doflow.json +58 -0
- package/core/harnesses/kiro/hooks/doflow.json +53 -0
- package/core/harnesses/kiro/hooks/lib.sh +324 -0
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +9 -0
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +9 -0
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +9 -0
- package/core/harnesses/kiro/hooks/session-start.sh +9 -0
- package/core/harnesses/kiro/hooks/stop-check.sh +43 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +39 -0
- package/core/harnesses/shared/hooks/adapters/antigravity.js +111 -0
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +19 -0
- package/core/harnesses/shared/hooks/adapters/gemini.js +114 -0
- package/core/harnesses/shared/hooks/hooks.json +47 -0
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +91 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +97 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +29 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +501 -0
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +21 -0
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +91 -0
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +328 -0
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +496 -0
- package/core/harnesses/shared/hooks/policies/prompt-nudge.jq +131 -0
- package/core/harnesses/shared/hooks/policies/session-context.sh +141 -0
- package/core/harnesses/shared/hooks/policies/stop-check.sh +264 -0
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +46 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +235 -0
- package/core/harnesses/shared/hooks/stream-hook-runner.js +590 -0
- package/core/harnesses/shared/locator/doflow-run +87 -0
- package/core/registry/advice.json +101 -0
- package/core/registry/assets.json +792 -0
- package/core/registry/capabilities.json +120 -0
- package/core/registry/contracts.json +189 -0
- package/core/registry/external-tools.json +191 -0
- package/core/registry/harnesses.json +2062 -0
- package/core/registry/lifecycle.json +231 -0
- package/core/registry/mcp.json +18 -0
- package/core/registry/models.json +79 -0
- package/core/registry/readiness-templates.json +118 -0
- package/core/registry/routes.json +47 -0
- package/core/registry/storage.json +1057 -0
- package/core/registry/verification.json +258 -0
- package/core/registry/workflows.json +790 -0
- package/core/shared/agent-specs/core-implementer.md +73 -0
- package/core/shared/agent-specs/quality-guardian.md +28 -0
- package/core/shared/agent-specs/research-writer.md +31 -0
- package/core/shared/agent-specs/spec-analyst.md +33 -0
- package/core/shared/agent-specs/system-architect.md +29 -0
- package/core/shared/guidance/DOFLOW_CORE.md +8 -0
- package/core/shared/guidance/FLAGS.md +25 -0
- package/core/shared/guidance/PRINCIPLES.md +61 -0
- package/core/shared/guidance/VERSION +1 -0
- package/core/shared/guidance/mcp/MCP_Context7.md +18 -0
- package/core/shared/guidance/mcp/MCP_Sequential.md +17 -0
- package/core/shared/guidance/modes/MODE_Brainstorming.md +46 -0
- package/core/shared/guidance/modes/MODE_DeepResearch.md +69 -0
- package/core/shared/guidance/modes/MODE_Introspection.md +33 -0
- package/core/shared/guidance/modes/MODE_Orchestration.md +57 -0
- package/core/shared/guidance/modes/MODE_Task_Management.md +74 -0
- package/core/shared/guidance/pointers/claude-gemini.md +2 -0
- package/core/shared/guidance/pointers/codex.md +6 -0
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +373 -0
- package/core/shared/guidance/references/ARTIFACT_VOICE.md +104 -0
- package/core/shared/guidance/references/CONSTITUTION_BASE.md +47 -0
- package/core/shared/guidance/references/DIAGRAM_GUIDE.md +161 -0
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +87 -0
- package/core/shared/guidance/references/EVIDENCE_LEDGER.md +93 -0
- package/core/shared/guidance/references/MODEL_SELECTION.md +74 -0
- package/core/shared/guidance/references/ORCHESTRATION.md +36 -0
- package/core/shared/guidance/references/RESEARCH_CONFIG.md +21 -0
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +81 -0
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +25 -0
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +23 -0
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +14 -0
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +46 -0
- package/core/shared/scripts/doflow/assets/mermaid-theme.json +46 -0
- package/core/shared/scripts/doflow/bash/do-exec-paths.sh +167 -0
- package/core/shared/scripts/doflow/bash/do-git-state.sh +546 -0
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +208 -0
- package/core/shared/scripts/doflow/bash/do-paths.sh +347 -0
- package/core/shared/scripts/doflow/bash/do-prereqs.sh +66 -0
- package/core/shared/scripts/doflow/bash/do-review-package.sh +127 -0
- package/core/shared/scripts/doflow/bash/do-task-brief.sh +698 -0
- package/core/shared/scripts/doflow/bash/render-audit.sh +224 -0
- package/core/shared/scripts/doflow/bash/render-diagrams.sh +177 -0
- package/core/shared/scripts/doflow/bash/render-puml.sh +223 -0
- package/core/shared/scripts/doflow/bash/sync-context.sh +66 -0
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +526 -0
- package/core/shared/scripts/doflow/bin/doflow-run +592 -0
- package/core/shared/skills/do/SKILL.md +128 -0
- package/core/shared/skills/do/references/estimation.md +13 -0
- package/core/shared/skills/do/references/pm_routing.md +25 -0
- package/core/shared/skills/do/references/tool_matrix.md +52 -0
- package/core/shared/skills/do-brainstorm/SKILL.md +252 -0
- package/core/shared/skills/do-code-review/README.md +49 -0
- package/core/shared/skills/do-code-review/SKILL.md +333 -0
- package/core/shared/skills/do-code-review/assets/sample_c_clean.c +71 -0
- package/core/shared/skills/do-code-review/assets/sample_c_smells.c +66 -0
- package/core/shared/skills/do-code-review/assets/sample_cpp_shift.cpp +19 -0
- package/core/shared/skills/do-code-review/assets/sample_csharp_clean.cs +100 -0
- package/core/shared/skills/do-code-review/assets/sample_csharp_smells.cs +78 -0
- package/core/shared/skills/do-code-review/assets/sample_java_clean.java +55 -0
- package/core/shared/skills/do-code-review/assets/sample_java_smells.java +56 -0
- package/core/shared/skills/do-code-review/assets/sample_json_clean.json +7 -0
- package/core/shared/skills/do-code-review/assets/sample_markdown_clean.md +30 -0
- package/core/shared/skills/do-code-review/assets/sample_markdown_smells.md +29 -0
- package/core/shared/skills/do-code-review/assets/sample_shell_heredoc.sh +38 -0
- package/core/shared/skills/do-code-review/assets/sample_shell_smells.sh +15 -0
- package/core/shared/skills/do-code-review/assets/sample_typescript_strings.ts +36 -0
- package/core/shared/skills/do-code-review/assets/sample_yaml_smells.yaml +6 -0
- package/core/shared/skills/do-code-review/content-types/config.md +83 -0
- package/core/shared/skills/do-code-review/content-types/markdown.md +132 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_c_clean_quality.json +79 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_c_smells_quality.json +169 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_cpp_shift_quality.json +49 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_clean_quality.json +149 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_smells_quality.json +157 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_java_clean_quality.json +67 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_java_smells_quality.json +97 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_json_clean_quality.json +33 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_clean_quality.json +1 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_smells_quality.json +16 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_shell_heredoc_quality.json +54 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_shell_smells_quality.json +91 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_typescript_strings_quality.json +60 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_yaml_smells_quality.json +46 -0
- package/core/shared/skills/do-code-review/languages/c.md +103 -0
- package/core/shared/skills/do-code-review/languages/cpp.md +103 -0
- package/core/shared/skills/do-code-review/languages/csharp.md +97 -0
- package/core/shared/skills/do-code-review/languages/dart.md +108 -0
- package/core/shared/skills/do-code-review/languages/go.md +92 -0
- package/core/shared/skills/do-code-review/languages/java.md +103 -0
- package/core/shared/skills/do-code-review/languages/kotlin.md +88 -0
- package/core/shared/skills/do-code-review/languages/php.md +109 -0
- package/core/shared/skills/do-code-review/languages/python.md +94 -0
- package/core/shared/skills/do-code-review/languages/ruby.md +105 -0
- package/core/shared/skills/do-code-review/languages/rust.md +99 -0
- package/core/shared/skills/do-code-review/languages/shell.md +90 -0
- package/core/shared/skills/do-code-review/languages/swift.md +88 -0
- package/core/shared/skills/do-code-review/languages/typescript.md +97 -0
- package/core/shared/skills/do-code-review/review-policy.json +47 -0
- package/core/shared/skills/do-code-review/rules/universal.md +49 -0
- package/core/shared/skills/do-code-review/scripts/code_quality_checker.py +1696 -0
- package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +532 -0
- package/core/shared/skills/do-code-review/scripts/pr_analyzer.py +620 -0
- package/core/shared/skills/do-code-review/scripts/review_report_generator.py +505 -0
- package/core/shared/skills/do-constitution/SKILL.md +72 -0
- package/core/shared/skills/do-design/SKILL.md +212 -0
- package/core/shared/skills/do-diagnose/SKILL.md +156 -0
- package/core/shared/skills/do-diagnose/references/code_audit.md +15 -0
- package/core/shared/skills/do-diagnose/references/refactoring.md +12 -0
- package/core/shared/skills/do-diagnose/references/root_cause.md +17 -0
- package/core/shared/skills/do-document/SKILL.md +81 -0
- package/core/shared/skills/do-document/references/api-reference.md +42 -0
- package/core/shared/skills/do-document/references/deep_research.md +18 -0
- package/core/shared/skills/do-document/references/embedded-research.md +11 -0
- package/core/shared/skills/do-document/references/implementation-flow.md +105 -0
- package/core/shared/skills/do-document/references/index_generator.md +9 -0
- package/core/shared/skills/do-document/references/user-guide.md +22 -0
- package/core/shared/skills/do-execute-plan/SKILL.md +261 -0
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +57 -0
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +92 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +155 -0
- package/core/shared/skills/do-execute-plan/references/run_position.md +63 -0
- package/core/shared/skills/do-execute-plan/references/scaffold.md +441 -0
- package/core/shared/skills/do-execute-plan/references/task_attempts.md +41 -0
- package/core/shared/skills/do-flow/SKILL.md +186 -0
- package/core/shared/skills/do-flow/references/task_classes.md +134 -0
- package/core/shared/skills/do-git/SKILL.md +115 -0
- package/core/shared/skills/do-git/references/hotfix-ritual.md +44 -0
- package/core/shared/skills/do-git/references/lifecycle-policy.md +54 -0
- package/core/shared/skills/do-git/references/release-ritual.md +64 -0
- package/core/shared/skills/do-implement/SKILL.md +222 -0
- package/core/shared/skills/do-plan/SKILL.md +219 -0
- package/core/shared/skills/do-test/SKILL.md +113 -0
- package/core/shared/templates/doflow/constitution-template.md +28 -0
- package/core/shared/templates/doflow/data-model-template.md +169 -0
- package/core/shared/templates/doflow/design-template.md +193 -0
- package/core/shared/templates/doflow/external-contract-template.md +35 -0
- package/core/shared/templates/doflow/intent-template.md +45 -0
- package/core/shared/templates/doflow/plan-template.md +151 -0
- package/core/shared/templates/doflow/question-log-template.md +51 -0
- package/core/shared/templates/doflow/requirement-template.md +126 -0
- package/core/shared/templates/doflow/specs-template.md +55 -0
- package/core/shared/templates/doflow/state-template.md +59 -0
- package/core/shared/workflows/do-flow-chain.md +17 -0
- package/package.json +46 -4
- package/src/adapters/antigravity/index.ts +631 -0
- package/src/adapters/claude/index.ts +548 -0
- package/src/adapters/claude/mcp.ts +94 -0
- package/src/adapters/codex/agents.ts +208 -0
- package/src/adapters/codex/config.ts +272 -0
- package/src/adapters/codex/hooks.ts +203 -0
- package/src/adapters/codex/index.ts +707 -0
- package/src/adapters/codex/mcp.ts +271 -0
- package/src/adapters/codex/native-projection.ts +34 -0
- package/src/adapters/copilot/index.ts +413 -0
- package/src/adapters/copy-tree.ts +620 -0
- package/src/adapters/gemini/hooks.ts +210 -0
- package/src/adapters/gemini/index.ts +390 -0
- package/src/adapters/hook-commands.ts +60 -0
- package/src/adapters/index.ts +106 -0
- package/src/adapters/kiro/index.ts +310 -0
- package/src/adapters/mcp-entries.ts +331 -0
- package/src/adapters/opencode/index.ts +466 -0
- package/src/adapters/pi/index.ts +279 -0
- package/src/adapters/pi/json-members.ts +185 -0
- package/src/adapters/pi/mcp.ts +373 -0
- package/src/adapters/resolve-source.ts +19 -0
- package/src/cli/commands/install.ts +218 -0
- package/src/cli/commands/list-backups.ts +21 -0
- package/src/cli/commands/reconcile.ts +120 -0
- package/src/cli/commands/remove.ts +82 -0
- package/src/cli/commands/rollback.ts +172 -0
- package/src/cli/commands/self-update.ts +33 -0
- package/src/cli/commands/status.ts +138 -0
- package/src/cli/commands/tools.ts +92 -0
- package/src/cli/commands/update.ts +162 -0
- package/src/cli/index.ts +568 -0
- package/src/cli/runtime-commands.ts +281 -0
- package/src/cli/shared.ts +218 -0
- package/src/helper/bash-helper.ts +34 -0
- package/src/helper/git.ts +24 -0
- package/src/helper/guardrails.ts +49 -0
- package/src/helper/harness-paths.ts +140 -0
- package/src/helper/marker-merge.ts +116 -0
- package/src/helper/prompt.ts +61 -0
- package/src/helper/repo-root.ts +29 -0
- package/src/helper/settings-merge.ts +151 -0
- package/src/helper/settings-scope.ts +33 -0
- package/src/helper/toml.ts +244 -0
- package/src/install/backup-set.ts +149 -0
- package/src/install/backup.ts +724 -0
- package/src/install/context.ts +58 -0
- package/src/install/manifest.ts +145 -0
- package/src/install/mcp.ts +254 -0
- package/src/install/paths.ts +84 -0
- package/src/install/targets.ts +90 -0
- package/src/install/tool-lifecycle.ts +151 -0
- package/src/lifecycle/bash-availability.ts +25 -0
- package/src/lifecycle/index.ts +812 -0
- package/src/lifecycle/mcp-index.ts +28 -0
- package/src/lifecycle/policies.ts +73 -0
- package/src/lifecycle/view.ts +175 -0
- package/src/registry/index.ts +611 -0
- package/src/release/sync-plugin-versions.ts +66 -0
- package/src/runtime/advice/cli.ts +283 -0
- package/src/runtime/advice/client.ts +324 -0
- package/src/runtime/advice/config.ts +189 -0
- package/src/runtime/advice/doctor.ts +169 -0
- package/src/runtime/advice/endpoint-guard.ts +134 -0
- package/src/runtime/advice/kinds.ts +154 -0
- package/src/runtime/advice/providers/ollama-systemone.ts +209 -0
- package/src/runtime/advice/record.ts +166 -0
- package/src/runtime/advice/registry.ts +170 -0
- package/src/runtime/advice/strength.ts +66 -0
- package/src/runtime/brief-tools.ts +342 -0
- package/src/runtime/c4-project.ts +371 -0
- package/src/runtime/capability-router.ts +484 -0
- package/src/runtime/checkouts.ts +170 -0
- package/src/runtime/claims.ts +468 -0
- package/src/runtime/cli-result.ts +55 -0
- package/src/runtime/cli.ts +678 -0
- package/src/runtime/command-detect.ts +516 -0
- package/src/runtime/context-pack.ts +492 -0
- package/src/runtime/decision-register.ts +493 -0
- package/src/runtime/evidence-ledger.ts +338 -0
- package/src/runtime/failure/capture.ts +272 -0
- package/src/runtime/failure/classifier.ts +68 -0
- package/src/runtime/failure/cli.ts +178 -0
- package/src/runtime/failure/home.ts +64 -0
- package/src/runtime/failure/store.ts +226 -0
- package/src/runtime/feature-resolve.ts +79 -0
- package/src/runtime/freshness.ts +177 -0
- package/src/runtime/health.ts +863 -0
- package/src/runtime/history-compactor.ts +237 -0
- package/src/runtime/implementation-gate.ts +321 -0
- package/src/runtime/inventory/coverage.ts +218 -0
- package/src/runtime/inventory/identity.ts +65 -0
- package/src/runtime/inventory/index.ts +705 -0
- package/src/runtime/inventory/precedence.ts +134 -0
- package/src/runtime/inventory/read-scopes.ts +131 -0
- package/src/runtime/inventory/siblings.ts +192 -0
- package/src/runtime/knowledge/chunker.ts +124 -0
- package/src/runtime/knowledge/hybrid.ts +149 -0
- package/src/runtime/knowledge/index-store.ts +145 -0
- package/src/runtime/knowledge/retrieval.ts +194 -0
- package/src/runtime/leak-scan.ts +185 -0
- package/src/runtime/lifecycle/cli.ts +276 -0
- package/src/runtime/lifecycle/event-store.ts +340 -0
- package/src/runtime/lifecycle/fold.ts +353 -0
- package/src/runtime/lifecycle/followup.ts +432 -0
- package/src/runtime/lifecycle/goal.ts +249 -0
- package/src/runtime/lifecycle/intent-writer.ts +86 -0
- package/src/runtime/lifecycle/overview.ts +292 -0
- package/src/runtime/lifecycle/release.ts +241 -0
- package/src/runtime/lifecycle/report-store.ts +221 -0
- package/src/runtime/lifecycle/retention.ts +212 -0
- package/src/runtime/lifecycle/root.ts +71 -0
- package/src/runtime/lifecycle/status.ts +353 -0
- package/src/runtime/lifecycle/store-upkeep.ts +393 -0
- package/src/runtime/locator-resolve.ts +158 -0
- package/src/runtime/mask.ts +210 -0
- package/src/runtime/mcp-consultations.ts +412 -0
- package/src/runtime/mcp-reach.ts +157 -0
- package/src/runtime/model-router.ts +153 -0
- package/src/runtime/outcome.ts +467 -0
- package/src/runtime/reach.ts +148 -0
- package/src/runtime/readiness-record.ts +150 -0
- package/src/runtime/readiness.ts +435 -0
- package/src/runtime/recovery.ts +282 -0
- package/src/runtime/research-request.ts +231 -0
- package/src/runtime/retrieval-plan.ts +578 -0
- package/src/runtime/scaffold/artifacts.ts +217 -0
- package/src/runtime/scaffold/fingerprint.ts +56 -0
- package/src/runtime/scaffold/generate.ts +911 -0
- package/src/runtime/scaffold/languages.ts +245 -0
- package/src/runtime/storage/cli.ts +64 -0
- package/src/runtime/storage/engine.ts +249 -0
- package/src/runtime/storage/registry.ts +130 -0
- package/src/runtime/storage/roots.ts +63 -0
- package/src/runtime/storage/rules.ts +282 -0
- package/src/runtime/storage/upkeep.ts +243 -0
- package/src/runtime/storage/work-status.ts +326 -0
- package/src/runtime/task-attempts.ts +415 -0
- package/src/runtime/task-classifier.ts +550 -0
- package/src/runtime/task-scope.ts +130 -0
- package/src/runtime/task-state.ts +176 -0
- package/src/runtime/trace/indicators.ts +235 -0
- package/src/runtime/trace/ledger.ts +626 -0
- package/src/runtime/trace/render.ts +76 -0
- package/src/runtime/trace/views.ts +459 -0
- package/src/runtime/verification/contract-runner.ts +341 -0
- package/src/runtime/verification/engine.ts +1461 -0
- package/src/runtime/verification/registry.ts +130 -0
- package/src/runtime/verification/scope-bound.ts +418 -0
- package/src/runtime/verification/test-selection.ts +172 -0
- package/src/runtime/workflow-engine.ts +588 -0
- package/src/runtime/workflow-orchestrator.ts +953 -0
- package/src/runtime/worktree.ts +340 -0
- package/src/state/index.ts +193 -0
- package/src/state/lockfile.ts +136 -0
- package/src/state/run-lock.ts +408 -0
- package/types/node-commonjs.d.ts +68 -0
- package/types/runtime.d.ts +91 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# sync-context.sh — idempotently write a marker-delimited block into an agent context
|
|
3
|
+
# file (CLAUDE.md / AGENTS.md / .github/copilot-instructions.md). Used by /do-constitution
|
|
4
|
+
# to propagate a pointer to the active constitution without rewriting the whole file.
|
|
5
|
+
#
|
|
6
|
+
# The block body is read from stdin; markers are added by this script. Re-running with new
|
|
7
|
+
# content REPLACES the previous block (idempotent — never duplicates). Deterministic so the
|
|
8
|
+
# model never does between-marker text surgery itself.
|
|
9
|
+
#
|
|
10
|
+
# Exit codes follow the dispatcher's uniform contract (design.md §4.2): 0 success, 1 the write
|
|
11
|
+
# did not land, 2 a usage error. This script used to exit 0 on a missing --file and on a failed
|
|
12
|
+
# write, which made every failure indistinguishable from success to its one caller
|
|
13
|
+
# (/do-constitution) — the skill is responsible for reporting whether the pointer landed, and it
|
|
14
|
+
# cannot report what it cannot observe.
|
|
15
|
+
#
|
|
16
|
+
# Usage: printf '%s' "<body>" | sync-context.sh --file <context-file>
|
|
17
|
+
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
START="<!-- DOFLOW START -->"
|
|
21
|
+
END="<!-- DOFLOW END -->"
|
|
22
|
+
|
|
23
|
+
file=""
|
|
24
|
+
while [ $# -gt 0 ]; do
|
|
25
|
+
case "$1" in
|
|
26
|
+
# `shift 2` fails when --file is the last argument, leaving $# unchanged and the loop
|
|
27
|
+
# spinning forever; consume the flag first so the value shift is always safe.
|
|
28
|
+
--file) shift; file="${1:-}"; [ $# -gt 0 ] && shift ;;
|
|
29
|
+
*) shift ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
[ -n "$file" ] || { echo "sync-context: --file required" >&2; exit 2; }
|
|
33
|
+
|
|
34
|
+
content="$(cat)"
|
|
35
|
+
block="$START
|
|
36
|
+
$content
|
|
37
|
+
$END"
|
|
38
|
+
|
|
39
|
+
if [ ! -f "$file" ]; then
|
|
40
|
+
printf '%s\n' "$block" > "$file" || { echo "sync-context: could not create $file" >&2; exit 1; }
|
|
41
|
+
echo "sync-context: created $file"
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
if grep -qF "$START" "$file" && grep -qF "$END" "$file"; then
|
|
46
|
+
# $block contains embedded newlines; passing it via awk -v triggers "newline in string" on
|
|
47
|
+
# BSD awk (macOS). Route it through ENVIRON instead — env-var access bypasses -v's
|
|
48
|
+
# argument-parsing grammar on both GNU and BSD awk.
|
|
49
|
+
export SYNC_CONTEXT_BLOCK="$block"
|
|
50
|
+
awk -v s="$START" -v e="$END" '
|
|
51
|
+
$0==s {
|
|
52
|
+
n = split(ENVIRON["SYNC_CONTEXT_BLOCK"], lines, "\n")
|
|
53
|
+
for (i = 1; i <= n; i++) print lines[i]
|
|
54
|
+
skip=1; next
|
|
55
|
+
}
|
|
56
|
+
$0==e {skip=0; next}
|
|
57
|
+
skip!=1 {print}
|
|
58
|
+
' "$file" > "$file.tmp" && mv "$file.tmp" "$file" ||
|
|
59
|
+
{ rm -f "$file.tmp"; echo "sync-context: could not update the block in $file" >&2; exit 1; }
|
|
60
|
+
echo "sync-context: updated block in $file"
|
|
61
|
+
else
|
|
62
|
+
printf '\n%s\n' "$block" >> "$file" ||
|
|
63
|
+
{ echo "sync-context: could not append the block to $file" >&2; exit 1; }
|
|
64
|
+
echo "sync-context: appended block to $file"
|
|
65
|
+
fi
|
|
66
|
+
exit 0
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# validate-artifacts.sh — advisory consistency checker for doflow chain artifacts.
|
|
3
|
+
#
|
|
4
|
+
# Checks that an artifact's index tables agree with the detail beside them. This is a CONSISTENCY
|
|
5
|
+
# checker, not a CONFORMANCE checker: it never asks whether an index had to exist, only whether one
|
|
6
|
+
# that does exist matches its detail. That is what lets artifacts written before this convention —
|
|
7
|
+
# which have no index tables at all — pass without any version marker or detection logic.
|
|
8
|
+
#
|
|
9
|
+
# Rules:
|
|
10
|
+
# parity index IDs and Detail IDs match, in both directions
|
|
11
|
+
# status every Status is "Live" or "Superseded -> <ref>"
|
|
12
|
+
# supersede an ID-shaped <ref> resolves to an ID present in the same artifact
|
|
13
|
+
# history a superseded ID has an entry in the History section (or in the archive file that
|
|
14
|
+
# the History section's "Earlier entries:" pointer names)
|
|
15
|
+
# stale a line outside History and HTML comments cites a superseded DEC-### from the
|
|
16
|
+
# feature's decision register without also naming a decision that replaced it. Inert
|
|
17
|
+
# when the feature has no decisions/register.json.
|
|
18
|
+
# unknown a line outside History, comments and code fences cites a DEC-### that is not in the
|
|
19
|
+
# feature's decision register. Inert without a register.
|
|
20
|
+
# rollup plan.md's phase rollup counts match its task checklist
|
|
21
|
+
#
|
|
22
|
+
# Grammar (see guidance/references/ARTIFACT_FORMAT.md) — deliberately prefix-agnostic, so
|
|
23
|
+
# FR/NFR/C/D/A and any future prefix work with no change here:
|
|
24
|
+
# indexed section a table whose first header cell is "ID", followed in the same "## " section
|
|
25
|
+
# by a "**Detail**" marker
|
|
26
|
+
# index ID first cell of each table body row
|
|
27
|
+
# Status the cell under the header column literally named "Status" (located by name,
|
|
28
|
+
# never by position, so a column added to the right cannot shift the check)
|
|
29
|
+
# detail entry either of two forms, both valid in the same artifact, neither deprecated:
|
|
30
|
+
# bullet form a line starting "- **<ID>", where <ID> is the leading token
|
|
31
|
+
# inside the bold; "- **FR-001:**" and
|
|
32
|
+
# "- **NFR-001 (qualifier):**" both parse
|
|
33
|
+
# heading form a line matching "#### <ID>: <text>", counted as a detail entry
|
|
34
|
+
# only when <ID> also appears in the first column of that
|
|
35
|
+
# section's index table. An ID-shaped heading with no matching
|
|
36
|
+
# index row is not a detail entry and is ignored, so an unrelated
|
|
37
|
+
# heading cannot invent a parity finding of its own
|
|
38
|
+
#
|
|
39
|
+
# Usage: validate-artifacts.sh [--json] [--slug=<slug>] [<path>...]
|
|
40
|
+
# No <path> → resolve the active feature via do-paths.sh and check every artifact present.
|
|
41
|
+
#
|
|
42
|
+
# Exit: 0 = clean, 1 = violations found (ADVISORY — no hook consumes this exit code),
|
|
43
|
+
# 0 + printed note = could not run (fail-open, matching do-prereqs.sh).
|
|
44
|
+
|
|
45
|
+
set -uo pipefail
|
|
46
|
+
|
|
47
|
+
emit_json=false
|
|
48
|
+
slug_override=""
|
|
49
|
+
targets=()
|
|
50
|
+
for a in "$@"; do
|
|
51
|
+
case "$a" in
|
|
52
|
+
--json) emit_json=true ;;
|
|
53
|
+
--slug=*) slug_override="${a#--slug=}" ;;
|
|
54
|
+
-*) ;; # unknown flag: ignore rather than fail the caller
|
|
55
|
+
*) targets+=("$a") ;;
|
|
56
|
+
esac
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
# Fail-open: always say why nothing was checked — an unrunnable checker must never be mistaken for
|
|
60
|
+
# a clean bill of health — but never obstruct the caller.
|
|
61
|
+
note() {
|
|
62
|
+
if [ "$emit_json" = true ]; then
|
|
63
|
+
printf '{"ok":true,"note":"%s","findings":[]}\n' "$1"
|
|
64
|
+
else
|
|
65
|
+
printf 'validate-artifacts: %s (nothing checked)\n' "$1"
|
|
66
|
+
fi
|
|
67
|
+
exit 0
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# The resolver refuses a slug that could name a path (IC-009). That refusal is the caller's mistake,
|
|
71
|
+
# not an unrunnable checker, so it is passed on as the refusal it is — exit 2 with the resolver's
|
|
72
|
+
# own error object — rather than folded into the fail-open notes above.
|
|
73
|
+
refuse_invalid_slug() {
|
|
74
|
+
[ "$(printf '%s' "$1" | jq -r '.error // empty' 2>/dev/null)" = "invalid-slug" ] || return 0
|
|
75
|
+
if [ "$emit_json" = true ]; then
|
|
76
|
+
printf '%s\n' "$1"
|
|
77
|
+
else
|
|
78
|
+
printf 'validate-artifacts: %s\n' "$(printf '%s' "$1" | jq -r '.message')" >&2
|
|
79
|
+
fi
|
|
80
|
+
exit 2
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# ── locate targets ────────────────────────────────────────────────────────────────────────────
|
|
84
|
+
if [ "${#targets[@]}" -eq 0 ]; then
|
|
85
|
+
command -v jq >/dev/null 2>&1 || note "jq-absent"
|
|
86
|
+
|
|
87
|
+
script_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
88
|
+
RESOLVER="$script_dir/do-paths.sh"
|
|
89
|
+
[ -f "$RESOLVER" ] || RESOLVER="${DOFLOW_CONFIG_DIR:+$DOFLOW_CONFIG_DIR/scripts/doflow/bash/do-paths.sh}"
|
|
90
|
+
[ -f "$RESOLVER" ] || note "resolver-absent"
|
|
91
|
+
|
|
92
|
+
resolver_args=(--json)
|
|
93
|
+
[ -n "$slug_override" ] && resolver_args+=("--slug=$slug_override")
|
|
94
|
+
rc=0; json=$(bash "$RESOLVER" "${resolver_args[@]}" 2>/dev/null) || rc=$?
|
|
95
|
+
[ "$rc" -eq 2 ] && refuse_invalid_slug "$json"
|
|
96
|
+
[ "$rc" -eq 0 ] || note "resolver-error"
|
|
97
|
+
|
|
98
|
+
root=$(printf '%s' "$json" | jq -r '.repo_root // empty')
|
|
99
|
+
[ -n "$(printf '%s' "$json" | jq -r '.feature_slug // empty')" ] || note "no-active-feature"
|
|
100
|
+
|
|
101
|
+
# data-model.md is a default target only for a feature that has a decision register, so a folder
|
|
102
|
+
# from before the register validates exactly as it always did (FR-016).
|
|
103
|
+
keys="requirement design specs plan"
|
|
104
|
+
[ "$(printf '%s' "$json" | jq -r '.has_decisions // false')" = "true" ] && keys="requirement design specs data_model plan"
|
|
105
|
+
for key in $keys; do
|
|
106
|
+
p=$(printf '%s' "$json" | jq -r ".$key // empty")
|
|
107
|
+
[ -n "$p" ] && [ -f "$root/$p" ] && targets+=("$root/$p")
|
|
108
|
+
done
|
|
109
|
+
[ "${#targets[@]}" -gt 0 ] || note "no-artifacts-present"
|
|
110
|
+
elif command -v jq >/dev/null 2>&1; then
|
|
111
|
+
# Explicit paths skip discovery, but the stale rule still needs the feature's register. Resolve
|
|
112
|
+
# quietly: any failure here only leaves the rule inert, never changes what was asked for.
|
|
113
|
+
script_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
114
|
+
RESOLVER="$script_dir/do-paths.sh"
|
|
115
|
+
[ -f "$RESOLVER" ] || RESOLVER="${DOFLOW_CONFIG_DIR:+$DOFLOW_CONFIG_DIR/scripts/doflow/bash/do-paths.sh}"
|
|
116
|
+
json=""
|
|
117
|
+
if [ -f "$RESOLVER" ]; then
|
|
118
|
+
resolver_args=(--json)
|
|
119
|
+
[ -n "$slug_override" ] && resolver_args+=("--slug=$slug_override")
|
|
120
|
+
rc=0; json=$(bash "$RESOLVER" "${resolver_args[@]}" 2>/dev/null) || rc=$?
|
|
121
|
+
[ "$rc" -eq 2 ] && refuse_invalid_slug "$json"
|
|
122
|
+
[ "$rc" -eq 0 ] || json=""
|
|
123
|
+
fi
|
|
124
|
+
root=$(printf '%s' "$json" | jq -r '.repo_root // empty' 2>/dev/null)
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# ── decision register (stale rule) ──────────────────────────────────────────────────────────────
|
|
128
|
+
# stale_map is "<id>:<successor>,<successor>;..." — every superseded decision with its chain, the
|
|
129
|
+
# live end last. Empty when there is no register, so the rule is inert; a register jq cannot read
|
|
130
|
+
# also leaves it empty (fail-open).
|
|
131
|
+
stale_map=""; known_ids=""; reg_ok=0; feat_abs=""
|
|
132
|
+
# The feature folder is canonicalised physically: the resolver reports a logical root in a non-git
|
|
133
|
+
# directory, while each file below is compared by its physical path. A symlink or /tmp vs
|
|
134
|
+
# /private/tmp on one side only would make the containment test silently fail.
|
|
135
|
+
if [ -n "${json:-}" ] && [ -n "$(printf '%s' "$json" | jq -r '.feature_dir // empty' 2>/dev/null)" ]; then
|
|
136
|
+
feat_abs=$(cd "$root/$(printf '%s' "$json" | jq -r '.feature_dir')" 2>/dev/null && pwd -P) || feat_abs=""
|
|
137
|
+
fi
|
|
138
|
+
if [ -n "$feat_abs" ] && [ "$(printf '%s' "$json" | jq -r '.has_decisions // false' 2>/dev/null)" = "true" ]; then
|
|
139
|
+
reg_file="$root/$(printf '%s' "$json" | jq -r '.decisions_register')"
|
|
140
|
+
# limit() bounds each chain at the decision count, so a hand-edited register whose supersededBy
|
|
141
|
+
# links form a cycle still terminates; a decision is never its own successor.
|
|
142
|
+
stale_map=$(jq -r '
|
|
143
|
+
.decisions as $d
|
|
144
|
+
| ($d | map({key: .id, value: .supersededBy}) | from_entries) as $next
|
|
145
|
+
| [ $d[] | select(.status == "superseded") | .id as $id
|
|
146
|
+
| {id: $id, chain: ([ limit($d | length; $id | recurse($next[.] // empty)) ] | .[1:] | map(select(. != $id)))} ]
|
|
147
|
+
| map(select(.chain | length > 0) | .id + ":" + (.chain | join(",")))
|
|
148
|
+
| join(";")' "$reg_file" 2>/dev/null) || stale_map=""
|
|
149
|
+
# Every id the register holds, as numbers ("1,2,3"): the unknown rule needs the whole set, and is
|
|
150
|
+
# active only when the register could be read.
|
|
151
|
+
if known_ids=$(jq -r '[.decisions[].id | ltrimstr("DEC-") | tonumber] | join(",")' "$reg_file" 2>/dev/null); then
|
|
152
|
+
reg_ok=1
|
|
153
|
+
else
|
|
154
|
+
known_ids=""
|
|
155
|
+
fi
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
# ── check each target ─────────────────────────────────────────────────────────────────────────
|
|
159
|
+
# Findings accumulate as: <file>\t<rule>\t<id>\t<message>
|
|
160
|
+
findings=""
|
|
161
|
+
for f in "${targets[@]}"; do
|
|
162
|
+
if [ ! -f "$f" ]; then
|
|
163
|
+
findings="${findings}${f}"$'\t'"io"$'\t'"-"$'\t'"file not found"$'\n'
|
|
164
|
+
continue
|
|
165
|
+
fi
|
|
166
|
+
# The register belongs to one feature: an explicit path outside that feature folder must not be
|
|
167
|
+
# checked against it.
|
|
168
|
+
file_stale_map=""; file_reg_ok=0; file_known_ids=""; hist_root=""
|
|
169
|
+
if [ -n "$feat_abs" ]; then
|
|
170
|
+
case "$(cd "$(dirname "$f")" 2>/dev/null && pwd -P)/" in
|
|
171
|
+
"$feat_abs"/*)
|
|
172
|
+
file_stale_map="$stale_map"; file_reg_ok="$reg_ok"; file_known_ids="$known_ids"
|
|
173
|
+
# The only place a History pointer may lead: the feature's own archive directory.
|
|
174
|
+
hist_root=$(cd "$feat_abs/decisions/history" 2>/dev/null && pwd -P) || hist_root=""
|
|
175
|
+
;;
|
|
176
|
+
esac
|
|
177
|
+
fi
|
|
178
|
+
# Paths reach awk through the environment: -v would process backslash escapes inside them.
|
|
179
|
+
out=$(DF_ART_DIR="$(dirname "$f")" DF_HIST_ROOT="$hist_root" awk -v is_plan="$([ "$(basename "$f")" = "plan.md" ] && echo 1 || echo 0)" \
|
|
180
|
+
-v stale_map="$file_stale_map" -v reg_ok="$file_reg_ok" -v known_ids="$file_known_ids" -v sq="'" '
|
|
181
|
+
# Inline markup is presentation, not value: "**Superseded → X**" and "`Live`" mean the same as
|
|
182
|
+
# their bare forms, so emphasis is stripped before any comparison.
|
|
183
|
+
function trim(s) { gsub(/[`*]/, "", s); gsub(/^[ \t]+|[ \t]+$/, "", s); return s }
|
|
184
|
+
function is_id(s) { return s ~ /^[A-Za-z]+-?[0-9]+$/ }
|
|
185
|
+
function finding(rule, id, msg) { print rule "\t" id "\t" msg }
|
|
186
|
+
|
|
187
|
+
# Blanks the parts of a line that sit inside an HTML comment, tracking comments that span
|
|
188
|
+
# lines, so a comment block is never read as artifact text. Mirrors the compactor: only a
|
|
189
|
+
# comment opening at the start of a line continues past it.
|
|
190
|
+
function strip_comments(s, out, p, rest) {
|
|
191
|
+
out = ""
|
|
192
|
+
while (s != "") {
|
|
193
|
+
if (in_comment) {
|
|
194
|
+
p = index(s, "-->")
|
|
195
|
+
if (!p) return out
|
|
196
|
+
s = substr(s, p + 3); in_comment = 0
|
|
197
|
+
} else {
|
|
198
|
+
p = index(s, "<!--")
|
|
199
|
+
if (!p) return out s
|
|
200
|
+
rest = substr(s, p + 4)
|
|
201
|
+
if (out == "" && substr(s, 1, p - 1) ~ /^[ \t]*$/) {
|
|
202
|
+
# A comment that starts the line may run on over later lines.
|
|
203
|
+
s = rest; in_comment = 1
|
|
204
|
+
} else if (index(rest, "-->")) {
|
|
205
|
+
# Mid-line, it counts only when it also closes on this line; a lone `<!--` quoted in
|
|
206
|
+
# prose is text and must not hide the rest of the file.
|
|
207
|
+
out = out substr(s, 1, p - 1); s = substr(rest, index(rest, "-->") + 3)
|
|
208
|
+
} else return out s
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return out
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# One `unknown` finding per DEC-### the register does not hold, then one `stale` finding per
|
|
215
|
+
# superseded DEC-### on the line, unless the same line also names a decision
|
|
216
|
+
# later in that decision chain (the line is then about the change, not a stale statement).
|
|
217
|
+
# awk has no \b, so a token counts only when no word character touches either end.
|
|
218
|
+
function check_stale(text, lineno, pos, s, l, tok, before, after, num, cn, i, j, order, cited, named, flagged, ok, word, ws, we) {
|
|
219
|
+
split("", order); split("", cited); split("", named); split("", flagged); cn = 0; pos = 1
|
|
220
|
+
while (pos <= length(text) && match(substr(text, pos), /DEC-[0-9]+/)) {
|
|
221
|
+
s = pos + RSTART - 1; l = RLENGTH
|
|
222
|
+
tok = substr(text, s, l)
|
|
223
|
+
before = (s > 1) ? substr(text, s - 1, 1) : ""
|
|
224
|
+
after = substr(text, s + l, 1)
|
|
225
|
+
pos = s + l
|
|
226
|
+
if (before ~ /[A-Za-z0-9_]/ || after ~ /[A-Za-z_]/) continue
|
|
227
|
+
# A token in a URL, a file path or a file name is not prose and cites nothing, so neither
|
|
228
|
+
# `stale` nor `unknown` reports it. Three shapes: its whitespace-delimited word holds "://"
|
|
229
|
+
# (https://x.test?id=DEC-095); a letter follows a "." (DEC-094.md, [n](DEC-097.md)); or a "/"
|
|
230
|
+
# precedes it, unless the segment before that slash is itself a DEC token (DEC-001/DEC-002 is
|
|
231
|
+
# prose and every half is checked). A sentence-final "DEC-099." is still a citation.
|
|
232
|
+
if (before == "/") {
|
|
233
|
+
word = substr(text, 1, s - 2); sub(/^.*[\/ \t]/, "", word); sub(/^[(\[{"`<*_]+/, "", word)
|
|
234
|
+
if (word !~ /^DEC-[0-9]+$/) continue
|
|
235
|
+
}
|
|
236
|
+
if (after == "." && substr(text, s + l + 1, 1) ~ /[A-Za-z]/) continue
|
|
237
|
+
ws = s; while (ws > 1 && substr(text, ws - 1, 1) !~ /[ \t]/) ws--
|
|
238
|
+
we = s + l; while (we <= length(text) && substr(text, we, 1) !~ /[ \t]/) we++
|
|
239
|
+
if (index(substr(text, ws, we - ws), "://")) continue
|
|
240
|
+
num = substr(tok, 5) + 0
|
|
241
|
+
named[num] = 1
|
|
242
|
+
if (reg_ok && !(num in known) && !(num in flagged)) {
|
|
243
|
+
flagged[num] = 1
|
|
244
|
+
finding("unknown", tok, "line " lineno " cites " tok ", which is not in the decision register")
|
|
245
|
+
}
|
|
246
|
+
if ((num in chain_end) && !(num in cited)) { cited[num] = tok; order[++cn] = num }
|
|
247
|
+
}
|
|
248
|
+
for (i = 1; i <= cn; i++) {
|
|
249
|
+
num = order[i]; ok = 0
|
|
250
|
+
for (j = 1; j <= chain_n[num]; j++) if (chain_m[num, j] in named) ok = 1
|
|
251
|
+
if (!ok) finding("stale", cited[num], "line " lineno " cites " cited[num] ", superseded by " chain_end[num])
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# The pointer line in History names an archive file (relative to this artifact). IDs that
|
|
256
|
+
# moved there still count as History entries for the history rule.
|
|
257
|
+
# True when the directory holding `path` resolves, physically, inside <feature>/decisions/history.
|
|
258
|
+
# A pointer is data from the artifact; it must not make the validator read arbitrary files.
|
|
259
|
+
function under_hist_root(path, d, cmd, real) {
|
|
260
|
+
if (hist_root == "" || index(path, sq)) return 0
|
|
261
|
+
d = path; sub(/\/[^\/]*$/, "", d)
|
|
262
|
+
if (d == "") d = "/"
|
|
263
|
+
cmd = "cd " sq d sq " 2>/dev/null && pwd -P"
|
|
264
|
+
real = ""
|
|
265
|
+
cmd | getline real
|
|
266
|
+
close(cmd)
|
|
267
|
+
return real != "" && index(real "/", hist_root "/") == 1
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function load_archive(line, target, path, l, c, first) {
|
|
271
|
+
if (!match(line, /\]\([^)]+\)/)) return
|
|
272
|
+
target = substr(line, RSTART + 2, RLENGTH - 3)
|
|
273
|
+
path = (target ~ /^\//) ? target : dir "/" target
|
|
274
|
+
if (!under_hist_root(path)) return
|
|
275
|
+
while ((getline l < path) > 0) {
|
|
276
|
+
if (l ~ /^[ \t]*\|/) {
|
|
277
|
+
if (l ~ /^[ \t]*\|[ :|-]*$/) continue
|
|
278
|
+
split(l, c, "|"); first = trim(c[2])
|
|
279
|
+
if (is_id(first)) hist[first] = 1
|
|
280
|
+
} else if (l ~ /^- \*\*[A-Za-z]+-?[0-9]+/) {
|
|
281
|
+
sub(/^- \*\*/, "", l)
|
|
282
|
+
if (match(l, /^[A-Za-z]+-?[0-9]+/)) hist[substr(l, 1, RLENGTH)] = 1
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
close(path)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
BEGIN {
|
|
289
|
+
dir = ENVIRON["DF_ART_DIR"]; hist_root = ENVIRON["DF_HIST_ROOT"]
|
|
290
|
+
sec = 0; in_table = 0; in_rollup = 0; status_col = 0; phase = ""; in_comment = 0; cur_hist = 0; fence_ch = ""; fence_len = 0
|
|
291
|
+
if (reg_ok) {
|
|
292
|
+
nk = split(known_ids, kn, ",")
|
|
293
|
+
for (k = 1; k <= nk; k++) if (kn[k] != "") known[kn[k] + 0] = 1
|
|
294
|
+
}
|
|
295
|
+
if (stale_map != "") {
|
|
296
|
+
nrec = split(stale_map, recs, ";")
|
|
297
|
+
for (r = 1; r <= nrec; r++) {
|
|
298
|
+
split(recs[r], kv, ":"); num = substr(kv[1], 5) + 0
|
|
299
|
+
nc = split(kv[2], ch, ",")
|
|
300
|
+
chain_end[num] = ch[nc]; chain_n[num] = nc
|
|
301
|
+
for (j = 1; j <= nc; j++) chain_m[num, j] = substr(ch[j], 5) + 0
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
# Runs on every line, before the structural rules below (which consume lines with "next").
|
|
307
|
+
# Fenced code is an example, not artifact text: it neither opens a section nor cites anything.
|
|
308
|
+
# Fence rules follow the compactor: a run of 3+ backticks or tildes, up to 3 spaces in, closed by
|
|
309
|
+
# a bare run of the same character at least as long.
|
|
310
|
+
{
|
|
311
|
+
in_fence = 0
|
|
312
|
+
t = $0; sub(/[ \t]+$/, "", t)
|
|
313
|
+
if (fence_ch != "") {
|
|
314
|
+
in_fence = 1
|
|
315
|
+
if (t ~ /^ ? ? ?(`+|~+)$/) {
|
|
316
|
+
sub(/^ +/, "", t)
|
|
317
|
+
if (substr(t, 1, 1) == fence_ch && length(t) >= fence_len) fence_ch = ""
|
|
318
|
+
}
|
|
319
|
+
} else if (t ~ /^ ? ? ?(```|~~~)/) {
|
|
320
|
+
in_fence = 1
|
|
321
|
+
sub(/^ +/, "", t); fence_ch = substr(t, 1, 1); fence_len = 0
|
|
322
|
+
while (substr(t, fence_len + 1, 1) == fence_ch) fence_len++
|
|
323
|
+
}
|
|
324
|
+
if (!in_fence) {
|
|
325
|
+
vis = strip_comments($0)
|
|
326
|
+
if ($0 ~ /^## /) cur_hist = ($0 ~ /[Hh]istory/) ? 1 : 0
|
|
327
|
+
else if (cur_hist && vis ~ /^Earlier entries: \[decisions\/history\/[a-z-]+\.md\]/) load_archive(vis)
|
|
328
|
+
if ((stale_map != "" || reg_ok) && !cur_hist && vis != "") check_stale(vis, NR)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
# With a register the fence state above gates every structural rule below: a heading, table row,
|
|
333
|
+
# detail entry or checklist line inside a fenced example is an example, so a quoted
|
|
334
|
+
# "## 9. History" or "### Phase A" cannot open a section or change a count. The fenced line still
|
|
335
|
+
# ends a table. Without a register the structural tracking is exactly what it always was.
|
|
336
|
+
reg_ok && in_fence { in_table = 0; in_rollup = 0; next }
|
|
337
|
+
|
|
338
|
+
# ── section boundaries ───────────────────────────────────────────────────────────────────
|
|
339
|
+
/^## / {
|
|
340
|
+
sec++
|
|
341
|
+
in_table = 0; in_rollup = 0; status_col = 0
|
|
342
|
+
is_hist[sec] = ($0 ~ /[Hh]istory/) ? 1 : 0
|
|
343
|
+
next
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# ── "### Phase X" headings drive the rollup task count; any other ### clears the phase, so
|
|
347
|
+
# checkboxes under "Completion criteria" are never counted as tasks ──────────────────────
|
|
348
|
+
# "### Task Summary" holds the rollup table and is not a phase; "Summary" is carved out
|
|
349
|
+
# explicitly so a heading written as "### Phase Summary" cannot be read as a phase named
|
|
350
|
+
# "Summary".
|
|
351
|
+
/^### / {
|
|
352
|
+
in_table = 0; in_rollup = 0
|
|
353
|
+
if ($0 ~ /^### Phase / && $3 != "Summary") { phase = $3; phase_seen[phase] = 1 } else { phase = "" }
|
|
354
|
+
next
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
# ── tables ───────────────────────────────────────────────────────────────────────────────
|
|
358
|
+
/^[ \t]*\|/ {
|
|
359
|
+
n = split($0, cell, "|")
|
|
360
|
+
if ($0 ~ /^[ \t]*\|[ :|-]*$/) next # separator row
|
|
361
|
+
|
|
362
|
+
if (!in_table && !in_rollup) {
|
|
363
|
+
first = trim(cell[2])
|
|
364
|
+
if (first == "ID") {
|
|
365
|
+
in_table = 1; has_index[sec] = 1; status_col = 0
|
|
366
|
+
for (i = 2; i <= n; i++) if (trim(cell[i]) == "Status") status_col = i
|
|
367
|
+
next
|
|
368
|
+
}
|
|
369
|
+
if (is_plan && first == "Phase") {
|
|
370
|
+
rollup_col = 0
|
|
371
|
+
for (i = 2; i <= n; i++) if (trim(cell[i]) == "Tasks") rollup_col = i
|
|
372
|
+
if (rollup_col) { in_rollup = 1; saw_rollup = 1 }
|
|
373
|
+
next
|
|
374
|
+
}
|
|
375
|
+
next
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (in_rollup) {
|
|
379
|
+
p = trim(cell[2]); c = trim(cell[rollup_col])
|
|
380
|
+
if (p != "" && c ~ /^[0-9]+$/) { rollup[p] = c + 0; rollup_seen[p] = 1 }
|
|
381
|
+
next
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
id = trim(cell[2])
|
|
385
|
+
if (id == "") next
|
|
386
|
+
idx[sec, id] = 1; ids[sec] = ids[sec] " " id; all[id] = 1
|
|
387
|
+
if (status_col) status[sec, id] = trim(cell[status_col])
|
|
388
|
+
if (is_hist[sec]) hist[id] = 1
|
|
389
|
+
next
|
|
390
|
+
}
|
|
391
|
+
{ in_table = 0; in_rollup = 0 } # any non-table line ends a table
|
|
392
|
+
|
|
393
|
+
/^\*\*Detail\*\*/ { has_detail[sec] = 1; next }
|
|
394
|
+
|
|
395
|
+
# The ID is the LEADING token inside the bold, not the whole of it. Real entries look like
|
|
396
|
+
# "- **FR-001:** ..." and "- **NFR-001 (No new hard gate — CRITICAL):** ..." — so match the ID
|
|
397
|
+
# prefix and ignore whatever qualifier follows it.
|
|
398
|
+
/^- \*\*[A-Za-z]+-?[0-9]+/ {
|
|
399
|
+
line = $0; sub(/^- \*\*/, "", line)
|
|
400
|
+
if (match(line, /^[A-Za-z]+-?[0-9]+/)) {
|
|
401
|
+
id = substr(line, 1, RLENGTH)
|
|
402
|
+
det[sec, id] = 1; dids[sec] = dids[sec] " " id; all[id] = 1
|
|
403
|
+
if (is_hist[sec]) hist[id] = 1
|
|
404
|
+
}
|
|
405
|
+
next
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
# The heading form of the same entry: "#### IC-002: <summary>". It counts only when the ID is
|
|
409
|
+
# already an index row of this section, which is decidable here because a section states its
|
|
410
|
+
# index table before its "**Detail**" marker, so idx is populated by the time a heading under
|
|
411
|
+
# that marker is read. Everything else that looks ID-shaped — "#### C4 Level 1: System Context",
|
|
412
|
+
# "#### Family: Checking" — matches no index row and is ignored outright: it neither satisfies a
|
|
413
|
+
# row nor reports an orphan of its own. Section tracking is untouched, since sec advances on
|
|
414
|
+
# "## " headings only.
|
|
415
|
+
/^#### [A-Za-z]+-?[0-9]+:/ {
|
|
416
|
+
line = $0; sub(/^#### /, "", line)
|
|
417
|
+
if (match(line, /^[A-Za-z]+-?[0-9]+/)) {
|
|
418
|
+
id = substr(line, 1, RLENGTH)
|
|
419
|
+
# Only an id the index already carries counts. Two statements the bullet rule needs are
|
|
420
|
+
# deliberately absent here: "all[id] = 1" is already set for every indexed id, and
|
|
421
|
+
# "hist[id] = 1" is redundant here in both shapes a History section can take. Under the
|
|
422
|
+
# documented Date-first table its ids never enter idx, so this branch is not reached at all;
|
|
423
|
+
# under an ID-first table the index rule above has already set hist[id] from the row. Either
|
|
424
|
+
# way the heading form is not the form to use in a History section -- see ARTIFACT_FORMAT.md
|
|
425
|
+
# section 1, which says History detail uses the bullet form.
|
|
426
|
+
if ((sec, id) in idx) { det[sec, id] = 1; dids[sec] = dids[sec] " " id }
|
|
427
|
+
}
|
|
428
|
+
next
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
# ── task checklist lines feed the rollup comparison ──────────────────────────────────────
|
|
432
|
+
is_plan && phase != "" && /^- \[[ xX]\] / { actual[phase]++ }
|
|
433
|
+
|
|
434
|
+
END {
|
|
435
|
+
for (s = 1; s <= sec; s++) {
|
|
436
|
+
if (!has_index[s] || !has_detail[s]) continue # not an indexed section — skip
|
|
437
|
+
|
|
438
|
+
ni = split(ids[s], a, " ")
|
|
439
|
+
for (i = 1; i <= ni; i++) {
|
|
440
|
+
id = a[i]; if (id == "") continue
|
|
441
|
+
if (!((s, id) in det)) finding("parity", id, "appears in the index but has no Detail entry")
|
|
442
|
+
|
|
443
|
+
st = status[s, id]
|
|
444
|
+
if (st == "" || st == "Live") continue
|
|
445
|
+
if (st ~ /^Superseded/) {
|
|
446
|
+
ref = st; sub(/^Superseded/, "", ref); gsub(/^[^A-Za-z0-9]+/, "", ref); ref = trim(ref)
|
|
447
|
+
sup[id] = 1
|
|
448
|
+
if (ref == "") finding("supersede", id, "is Superseded but names no replacement")
|
|
449
|
+
else if (is_id(ref) && !(ref in all)) finding("supersede", id, "is Superseded -> " ref ", which does not exist in this artifact")
|
|
450
|
+
} else {
|
|
451
|
+
finding("status", id, "Status \"" st "\" is not Live or Superseded -> <ref>")
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
nd = split(dids[s], b, " ")
|
|
455
|
+
for (i = 1; i <= nd; i++) {
|
|
456
|
+
id = b[i]; if (id == "") continue
|
|
457
|
+
if (!((s, id) in idx)) finding("parity", id, "appears in Detail but has no index row")
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
for (id in sup) if (!(id in hist)) finding("history", id, "is superseded but has no History entry")
|
|
462
|
+
|
|
463
|
+
# Consistency, not conformance: a plan with no rollup table at all has nothing to be
|
|
464
|
+
# inconsistent with, so the check is skipped rather than reported. This is the same rule that
|
|
465
|
+
# lets indexless sections pass, applied to the rollup.
|
|
466
|
+
if (is_plan && saw_rollup) {
|
|
467
|
+
for (p in rollup_seen) {
|
|
468
|
+
if (!(p in phase_seen)) { finding("rollup", "Phase " p, "is in the rollup but has no checklist section"); continue }
|
|
469
|
+
got = (p in actual) ? actual[p] : 0
|
|
470
|
+
if (got != rollup[p]) finding("rollup", "Phase " p, "rollup states " rollup[p] " tasks; the checklist has " got)
|
|
471
|
+
}
|
|
472
|
+
for (p in phase_seen) if (!(p in rollup_seen)) finding("rollup", "Phase " p, "has a checklist section but no rollup row")
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
' "$f" 2>/dev/null)
|
|
476
|
+
awk_status=$?
|
|
477
|
+
# A failed parse must never look like a clean file. Empty output is indistinguishable from "no
|
|
478
|
+
# violations", so the exit status is what separates "checked and clean" from "never checked" —
|
|
479
|
+
# for instance if this runs under an awk that rejects a construct used above.
|
|
480
|
+
if [ "$awk_status" -ne 0 ]; then
|
|
481
|
+
findings="${findings}${f}"$'\t'"io"$'\t'"-"$'\t'"could not be parsed (awk exit ${awk_status}) — this file was NOT checked"$'\n'
|
|
482
|
+
continue
|
|
483
|
+
fi
|
|
484
|
+
if [ -n "$out" ]; then
|
|
485
|
+
while IFS= read -r line; do
|
|
486
|
+
[ -n "$line" ] && findings="${findings}${f}"$'\t'"${line}"$'\n'
|
|
487
|
+
done <<< "$out"
|
|
488
|
+
fi
|
|
489
|
+
done
|
|
490
|
+
|
|
491
|
+
# ── report ────────────────────────────────────────────────────────────────────────────────────
|
|
492
|
+
if [ -z "$findings" ]; then
|
|
493
|
+
if [ "$emit_json" = true ]; then
|
|
494
|
+
printf '{"ok":true,"findings":[]}\n'
|
|
495
|
+
else
|
|
496
|
+
printf 'validate-artifacts: %d artifact(s) checked, no violations\n' "${#targets[@]}"
|
|
497
|
+
fi
|
|
498
|
+
exit 0
|
|
499
|
+
fi
|
|
500
|
+
|
|
501
|
+
if [ "$emit_json" = true ]; then
|
|
502
|
+
# Escape backslash then double quote, in that order — reversing it would re-escape the
|
|
503
|
+
# backslashes this step introduces. Findings legitimately contain quotes (a status message
|
|
504
|
+
# names the offending value), and a path may contain either.
|
|
505
|
+
json_escape() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'; }
|
|
506
|
+
printf '{"ok":false,"findings":['
|
|
507
|
+
first=1
|
|
508
|
+
while IFS=$'\t' read -r file rule id msg; do
|
|
509
|
+
[ -z "$file" ] && continue
|
|
510
|
+
[ $first -eq 0 ] && printf ','
|
|
511
|
+
printf '{"file":"%s","rule":"%s","id":"%s","message":"%s"}' \
|
|
512
|
+
"$(json_escape "$file")" "$rule" "$(json_escape "$id")" "$(json_escape "$msg")"
|
|
513
|
+
first=0
|
|
514
|
+
done <<< "$findings"
|
|
515
|
+
printf ']}\n'
|
|
516
|
+
else
|
|
517
|
+
last=""
|
|
518
|
+
while IFS=$'\t' read -r file rule id msg; do
|
|
519
|
+
[ -z "$file" ] && continue
|
|
520
|
+
if [ "$file" != "$last" ]; then printf '%s\n' "$file"; last="$file"; fi
|
|
521
|
+
printf ' %-10s %s %s\n' "$rule" "$id" "$msg"
|
|
522
|
+
done <<< "$findings"
|
|
523
|
+
printf '\n%d finding(s) — advisory; the chain continues regardless.\n' \
|
|
524
|
+
"$(printf '%s' "$findings" | grep -c .)"
|
|
525
|
+
fi
|
|
526
|
+
exit 1
|