@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,1461 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Registry-driven verification layer (plan task C.2, design C7): `VerificationEngine` compiles a
|
|
7
|
+
* contract **before** implementation and a report **after**, scaling required tiers to risk,
|
|
8
|
+
* detecting commands from project manifests, and wiring the C8 recovery classifier into the
|
|
9
|
+
* failure path with its bounded retry.
|
|
10
|
+
*
|
|
11
|
+
* Built on two modules split out alongside it:
|
|
12
|
+
* - `verification-contract-runner.js` — `VerificationContractRunner`, the deterministic
|
|
13
|
+
* check-execution primitive this engine compiles tiers of checks down to.
|
|
14
|
+
* - `verification-registry.js` — `loadVerificationRegistry`, which reads and validates
|
|
15
|
+
* `core/registry/verification.json` into the shape `compileContract` reads tiers from.
|
|
16
|
+
*
|
|
17
|
+
* Three defects in the runner this engine sits on (a check with no command reporting PASS, a
|
|
18
|
+
* timed-out check losing its output, an empty contract reporting PASS over zero evidence — see
|
|
19
|
+
* `verification-contract-runner.js`) are the reason this layer looks the way it does. Their shared
|
|
20
|
+
* shape is a gate answering confidently about something it never evaluated, so the engine keeps
|
|
21
|
+
* "not evaluated" as a first-class outcome — UNRESOLVED — that can never become PASS.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
const nodeFs = require('node:fs');
|
|
26
|
+
const { spawnSync } = require('node:child_process');
|
|
27
|
+
const { detectCommands, applyTargetPattern, TARGET_PATTERN_TOKEN } = require('../command-detect.ts');
|
|
28
|
+
const { selectChangedTests } = require('./test-selection.ts');
|
|
29
|
+
const { RecoveryManager } = require('../recovery.ts');
|
|
30
|
+
const { finishRuntime, usageError } = require('../cli-result.ts');
|
|
31
|
+
const { REPO_ROOT } = require('../../helper/repo-root.ts');
|
|
32
|
+
const {
|
|
33
|
+
resolveScopeBound, resolveIntegrationBase, resolveExplicitBaseline, parseDeclaredScope, scopeReasonText, boundSourcesText,
|
|
34
|
+
} = require('./scope-bound.ts');
|
|
35
|
+
const nodeCrypto = require('node:crypto');
|
|
36
|
+
const { readReadinessRecord, RECORD_VERSION } = require('../readiness-record.ts');
|
|
37
|
+
const { verifyReadinessCheck, featureSlugFor } = require('../implementation-gate.ts');
|
|
38
|
+
const { assertSafeTaskId } = require('../evidence-ledger.ts');
|
|
39
|
+
const { upkeepAfterVerify } = require('../storage/upkeep.ts');
|
|
40
|
+
const {
|
|
41
|
+
VerificationContractRunner,
|
|
42
|
+
FATAL_CHECK_MARKERS,
|
|
43
|
+
TIMEOUT_EXIT_CODE,
|
|
44
|
+
MAX_STREAM_CHARS,
|
|
45
|
+
} = require('./contract-runner.ts');
|
|
46
|
+
const {
|
|
47
|
+
loadVerificationRegistry,
|
|
48
|
+
TIER_RESOLUTIONS,
|
|
49
|
+
TIER_STATUSES,
|
|
50
|
+
RECOVERY_OUTCOMES,
|
|
51
|
+
REGISTRY_FILENAME,
|
|
52
|
+
} = require('./registry.ts');
|
|
53
|
+
|
|
54
|
+
// `handleVerifyCommand` (moved from bin/doflow.js) used bin/doflow.js's own REPO_ROOT — that file's
|
|
55
|
+
// SCRIPT_DIR is bin/, so its repo root is path.dirname(SCRIPT_DIR). This module takes the same root
|
|
56
|
+
// from src/helper/repo-root.js rather than counting its own depth.
|
|
57
|
+
|
|
58
|
+
/** `PASS` and `FAIL` are the Python's vocabulary. `INCONCLUSIVE` is added for the empty-contract
|
|
59
|
+
* case only — see `evaluateContract`. */
|
|
60
|
+
const VERIFICATION_STATUSES = Object.freeze(['PASS', 'FAIL', 'INCONCLUSIVE']);
|
|
61
|
+
|
|
62
|
+
/** The targeted tier is subsumed by the broad one when both would run the same command (FR-001). */
|
|
63
|
+
const TARGETED_TIER_ID = 'targeted-tests';
|
|
64
|
+
const BROAD_TIER_ID = 'broad-tests';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {*} value
|
|
68
|
+
* @returns {Array<string>}
|
|
69
|
+
*/
|
|
70
|
+
/** A stderr excerpt for a message. (Referenced by the observation errors below without ever being
|
|
71
|
+
* defined, so a non-zero `git status` with no spawn error threw a ReferenceError instead of reporting.) */
|
|
72
|
+
function truncate(text: any, max: number = 200): string {
|
|
73
|
+
const flat = String(text || '').trim();
|
|
74
|
+
return flat.length > max ? `${flat.slice(0, max)}…` : flat;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toStringArray(value: any): string[] {
|
|
78
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === 'string' && v !== '') : [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parses `git status --porcelain` into the set of paths it touched.
|
|
83
|
+
* @param {string} stdout
|
|
84
|
+
* @returns {Array<string>}
|
|
85
|
+
*/
|
|
86
|
+
function parsePorcelain(stdout: any): string[] {
|
|
87
|
+
const files: string[] = [];
|
|
88
|
+
for (const line of String(stdout || '').split('\n')) {
|
|
89
|
+
if (line.trim() === '') continue;
|
|
90
|
+
const body = line.slice(3);
|
|
91
|
+
// A rename is reported as `old -> new`; the new path is the one that exists now.
|
|
92
|
+
const arrow = body.indexOf(' -> ');
|
|
93
|
+
files.push(arrow === -1 ? body.trim() : body.slice(arrow + 4).trim());
|
|
94
|
+
}
|
|
95
|
+
return files.filter(Boolean);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class VerificationEngine {
|
|
99
|
+
cwd: string;
|
|
100
|
+
fsImpl: any;
|
|
101
|
+
exec: any;
|
|
102
|
+
clock: any;
|
|
103
|
+
registry: any;
|
|
104
|
+
policy: any;
|
|
105
|
+
runner: any;
|
|
106
|
+
recoveryOverride: any;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {Object} [options]
|
|
110
|
+
* @param {string} [options.cwd] directory checks run in
|
|
111
|
+
* @param {string} [options.repoRoot] where `core/registry/` lives
|
|
112
|
+
* @param {Object} [options.registry] pre-parsed registry (test seam)
|
|
113
|
+
* @param {Object} [options.fsImpl] injection seam for manifest and registry reads
|
|
114
|
+
* @param {Function} [options.exec] injection seam for shell execution; `spawnSync` contract
|
|
115
|
+
* @param {Function} [options.clock] injection seam returning epoch milliseconds
|
|
116
|
+
* @param {RecoveryManager} [options.recovery] overrides the per-risk-level manager
|
|
117
|
+
*/
|
|
118
|
+
constructor(options: any = {}) {
|
|
119
|
+
this.cwd = options.cwd || process.cwd();
|
|
120
|
+
this.fsImpl = options.fsImpl || nodeFs;
|
|
121
|
+
this.exec = options.exec || spawnSync;
|
|
122
|
+
this.clock = options.clock || Date.now;
|
|
123
|
+
this.registry = loadVerificationRegistry({
|
|
124
|
+
repoRoot: options.repoRoot,
|
|
125
|
+
registryDir: options.registryDir,
|
|
126
|
+
registryPath: options.registryPath,
|
|
127
|
+
registry: options.registry,
|
|
128
|
+
fsImpl: this.fsImpl,
|
|
129
|
+
});
|
|
130
|
+
this.policy = this.registry.policy || {};
|
|
131
|
+
this.runner = new VerificationContractRunner({ cwd: this.cwd, exec: this.exec });
|
|
132
|
+
this.recoveryOverride = options.recovery || null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** @returns {string} ISO timestamp from the injected clock */
|
|
136
|
+
timestamp(): string {
|
|
137
|
+
return new Date(this.clock()).toISOString();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} riskLevel
|
|
142
|
+
* @returns {Object} the risk-level definition
|
|
143
|
+
*/
|
|
144
|
+
riskDefinition(riskLevel: string): any {
|
|
145
|
+
const def = this.registry.riskLevels[riskLevel];
|
|
146
|
+
if (!def) {
|
|
147
|
+
// Never coerce to the default: a caller that mistyped CRITICAL would silently get MEDIUM's
|
|
148
|
+
// lighter tier set on the change that most needed the heavier one.
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Unknown risk level '${riskLevel}'. Valid: ${Object.keys(this.registry.riskLevels).join(', ')}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return def;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Compiles the verification contract. Called **before** implementation: this is the artifact that
|
|
158
|
+
* states how success will be established (FR-009), and `runContract` later reports against it.
|
|
159
|
+
*
|
|
160
|
+
* @param {Object} input
|
|
161
|
+
* @param {string} input.taskId
|
|
162
|
+
* @param {string} [input.riskLevel] defaults to `policy.defaultRiskLevel`
|
|
163
|
+
* @param {Object} [input.detection] a `detectCommands` result; detected here when absent
|
|
164
|
+
* @param {string} [input.projectRoot] used for detection when `detection` is not supplied
|
|
165
|
+
* @param {string} [input.planPath] a feature `plan.md` whose override block wins over detection
|
|
166
|
+
* @param {string} [input.planText] override text supplied directly
|
|
167
|
+
* @param {string} [input.targetPattern] scopes the targeted-tests tier; without one, the tier is
|
|
168
|
+
* scoped to the feature's changed test files (see test-selection.js) where the toolchain allows
|
|
169
|
+
* @param {Array<string>} [input.changedFiles] files the selection treats as changed, in place of the
|
|
170
|
+
* change-scope observation
|
|
171
|
+
* @param {Array<{name: string, command: string, timeoutMs?: number}>} [input.structuralChecks]
|
|
172
|
+
* @param {Array<{id: string, description?: string, command?: string}>} [input.requirements]
|
|
173
|
+
* @param {{maxFiles?: number, allowedPaths?: Array<string>, source?: string, sources?: Array<Object>,
|
|
174
|
+
* baseline?: 'integration', unresolvedReason?: string, searched?: Array<Object>}} [input.scope]
|
|
175
|
+
* `baseline: 'integration'` measures the change from the merge base with the integration ref
|
|
176
|
+
* (a bound derived from a plan, scope-bound.js); without it only the working tree is observed.
|
|
177
|
+
* `sources` names where the bound came from; with no bound, `unresolvedReason` replaces the
|
|
178
|
+
* tier's generic reason and `searched` lists the places looked.
|
|
179
|
+
* @returns {Object} the contract
|
|
180
|
+
*/
|
|
181
|
+
compileContract(input: any = {}): any {
|
|
182
|
+
// A contract with no task is the readiness.ts `taskId || 'default'` defect in a new place: a
|
|
183
|
+
// confident verdict filed against the wrong subject. Refuse instead of inventing an id.
|
|
184
|
+
if (typeof input.taskId !== 'string' || input.taskId.trim() === '') {
|
|
185
|
+
throw new Error('compileContract requires a non-empty taskId');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const riskLevel = input.riskLevel || this.policy.defaultRiskLevel;
|
|
189
|
+
const risk = this.riskDefinition(riskLevel);
|
|
190
|
+
const requiredTiers = new Set(risk.requiredTiers);
|
|
191
|
+
const advisoryTiers = new Set(risk.advisoryTiers || []);
|
|
192
|
+
|
|
193
|
+
const detection = input.detection || detectCommands({
|
|
194
|
+
projectRoot: input.projectRoot || this.cwd,
|
|
195
|
+
planPath: input.planPath,
|
|
196
|
+
planText: input.planText,
|
|
197
|
+
fsImpl: this.fsImpl,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
let structuralChecks = Array.isArray(input.structuralChecks) ? input.structuralChecks : [];
|
|
201
|
+
let requirements = Array.isArray(input.requirements) ? input.requirements : [];
|
|
202
|
+
const scope = input.scope && typeof input.scope === 'object' ? input.scope : {};
|
|
203
|
+
const phase = input.phase === 'postimplementation' ? 'postimplementation' : 'unknown';
|
|
204
|
+
const declaredNone = new Set<string>(detection.declaredNone || []);
|
|
205
|
+
const absent = new Set<string>(detection.absent || []);
|
|
206
|
+
let provenance = input.provenance && typeof input.provenance === 'object' ? { ...input.provenance } : null;
|
|
207
|
+
|
|
208
|
+
if (input.verificationInput !== undefined && input.verificationInput !== null) {
|
|
209
|
+
const val = validateVerificationInput({ cwd: this.cwd, verificationInput: input.verificationInput });
|
|
210
|
+
if (!val.ok) throw new Error(val.message);
|
|
211
|
+
structuralChecks = val.structuralChecks;
|
|
212
|
+
requirements = val.requirements;
|
|
213
|
+
provenance = { ...(provenance || {}), ...val.provenance };
|
|
214
|
+
}
|
|
215
|
+
if (scope.baseline === 'explicit' && scope.baselineSha) {
|
|
216
|
+
provenance = { ...(provenance || {}), baselineSha: scope.baselineSha };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const tiers = this.registry.tiers.map((tier: any) => {
|
|
220
|
+
const required = requiredTiers.has(tier.id);
|
|
221
|
+
const advisory = advisoryTiers.has(tier.id);
|
|
222
|
+
const base = {
|
|
223
|
+
id: tier.id,
|
|
224
|
+
name: tier.name,
|
|
225
|
+
kind: tier.kind,
|
|
226
|
+
order: tier.order,
|
|
227
|
+
required,
|
|
228
|
+
advisory,
|
|
229
|
+
fatal: Boolean(tier.fatal),
|
|
230
|
+
modelBased: Boolean(tier.modelBased),
|
|
231
|
+
failureClass: tier.failureClass || null,
|
|
232
|
+
checks: [],
|
|
233
|
+
resolution: 'SKIPPED',
|
|
234
|
+
reason: `Not required at risk level ${riskLevel}.`,
|
|
235
|
+
};
|
|
236
|
+
if (!required && !advisory) return base;
|
|
237
|
+
|
|
238
|
+
if (tier.source === 'detected-command') {
|
|
239
|
+
const selectorContext = { changedFiles: input.changedFiles, scope, root: detection.projectRoot || input.projectRoot || this.cwd };
|
|
240
|
+
return Object.assign(base, this.resolveCommandTier(tier, detection, declaredNone, absent, input.targetPattern, selectorContext));
|
|
241
|
+
}
|
|
242
|
+
if (tier.source === 'caller-supplied' && tier.id === 'structural-invariants') {
|
|
243
|
+
return Object.assign(base, this.resolveStructuralTier(tier, structuralChecks));
|
|
244
|
+
}
|
|
245
|
+
if (tier.source === 'caller-supplied' && tier.id === 'requirement-satisfaction') {
|
|
246
|
+
return Object.assign(base, this.resolveRequirementTier(tier, requirements));
|
|
247
|
+
}
|
|
248
|
+
if (tier.source === 'scope-observation') {
|
|
249
|
+
return Object.assign(base, this.resolveScopeTier(tier, scope));
|
|
250
|
+
}
|
|
251
|
+
if (tier.source === 'model') {
|
|
252
|
+
return Object.assign(base, {
|
|
253
|
+
resolution: 'RESOLVED',
|
|
254
|
+
reason: 'Advisory model review; supplied to runContract, never establishes PASS.',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
// A tier whose source this engine does not implement is unevaluated, not satisfied.
|
|
258
|
+
return Object.assign(base, {
|
|
259
|
+
resolution: 'UNRESOLVED',
|
|
260
|
+
reason: `Tier source '${tier.source}' has no evaluator in this engine.`,
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Subsumption is a second pass because it depends on how a *later* tier resolved: parse is
|
|
265
|
+
// covered by build only if build actually has a command.
|
|
266
|
+
const byId = new Map<string, any>(tiers.map((t: any) => [t.id, t]));
|
|
267
|
+
for (const tier of this.registry.tiers) {
|
|
268
|
+
const resolved: any = byId.get(tier.id);
|
|
269
|
+
if (!resolved || resolved.resolution !== 'UNRESOLVED') continue;
|
|
270
|
+
const cover = toStringArray(tier.subsumedBy).find((id: string) => { const e: any = byId.get(id); return e && e.resolution === 'RESOLVED'; });
|
|
271
|
+
if (cover) {
|
|
272
|
+
resolved.resolution = 'SUBSUMED';
|
|
273
|
+
resolved.subsumedBy = cover;
|
|
274
|
+
resolved.reason = `No dedicated command; covered by the '${cover}' tier, which runs the same source through a stricter step.`;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// A targeted tier that would run the very command a required broad tier runs is that tier's
|
|
279
|
+
// duplicate, not a cheaper check: it is reported SUBSUMED and never executed (FR-001).
|
|
280
|
+
const targeted: any = byId.get(TARGETED_TIER_ID);
|
|
281
|
+
const broad: any = byId.get(BROAD_TIER_ID);
|
|
282
|
+
if (targeted && broad && targeted.resolution === 'RESOLVED' && broad.required && broad.resolution === 'RESOLVED') {
|
|
283
|
+
const broadCommands = new Set<string>(broad.checks.map((c: any) => c.command as string));
|
|
284
|
+
if (targeted.checks.length > 0 && targeted.checks.every((c: any) => broadCommands.has(c.command))) {
|
|
285
|
+
targeted.resolution = 'SUBSUMED';
|
|
286
|
+
targeted.subsumedBy = BROAD_TIER_ID;
|
|
287
|
+
targeted.checks = [];
|
|
288
|
+
targeted.reason = `Would run the same command as the required '${BROAD_TIER_ID}' tier, which runs it once.`;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const unresolvedRequiredTiers = tiers
|
|
293
|
+
.filter((t: any) => t.required && t.resolution === 'UNRESOLVED')
|
|
294
|
+
.map((t: any) => ({ tier: t.id, reason: t.reason }));
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
version: this.registry.version,
|
|
298
|
+
taskId: input.taskId,
|
|
299
|
+
riskLevel,
|
|
300
|
+
compiledAt: this.timestamp(),
|
|
301
|
+
// Compilation does not know when it runs relative to implementation, so it never asserts it ran
|
|
302
|
+
// before: `unknown` is null, and only an explicit `postimplementation` phase records false (IC-022).
|
|
303
|
+
phase,
|
|
304
|
+
compiledBeforeImplementation: phase === 'postimplementation' ? false : null,
|
|
305
|
+
...(provenance && typeof provenance === 'object' ? { provenance } : {}),
|
|
306
|
+
cwd: this.cwd,
|
|
307
|
+
maxRecoveryIterations: risk.maxRecoveryIterations,
|
|
308
|
+
detection: {
|
|
309
|
+
usable: detection.usable,
|
|
310
|
+
manifestFound: detection.manifestFound,
|
|
311
|
+
manifests: detection.manifests,
|
|
312
|
+
absent: detection.absent,
|
|
313
|
+
declaredNone: detection.declaredNone,
|
|
314
|
+
errors: detection.errors,
|
|
315
|
+
},
|
|
316
|
+
scope: {
|
|
317
|
+
maxFiles: Number.isFinite(scope.maxFiles) ? scope.maxFiles : null,
|
|
318
|
+
allowedPaths: toStringArray(scope.allowedPaths),
|
|
319
|
+
...(scope.baseline === 'explicit' ? { baseline: { kind: 'pinned', ref: scope.baselineRef, sha: scope.baselineSha } } : {}),
|
|
320
|
+
...(Array.isArray(scope.sources) ? { sources: scope.sources } : {}),
|
|
321
|
+
...(Array.isArray(scope.searched) ? { searched: scope.searched } : {}),
|
|
322
|
+
},
|
|
323
|
+
tiers,
|
|
324
|
+
unresolvedRequiredTiers,
|
|
325
|
+
runnableCheckCount: tiers.reduce((n: number, t: any) => n + t.checks.length, 0),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Resolves a tier served by a detected command. The whole point of the three-way distinction in
|
|
331
|
+
* `command-detect` lands here.
|
|
332
|
+
* @private
|
|
333
|
+
*/
|
|
334
|
+
resolveCommandTier(tier: any, detection: any, declaredNone: Set<string>, absent: Set<string>, targetPattern: any, selectorContext: any = null): any {
|
|
335
|
+
if (!detection.usable) {
|
|
336
|
+
return {
|
|
337
|
+
resolution: 'UNRESOLVED',
|
|
338
|
+
reason: `Command detection is unusable: ${detection.errors.join('; ')}`,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const roles = toStringArray(tier.commandRoles);
|
|
343
|
+
const selectAll = tier.commandRoleSelection === 'all';
|
|
344
|
+
const checks: any[] = [];
|
|
345
|
+
const notes: string[] = [];
|
|
346
|
+
let selection: any = null;
|
|
347
|
+
|
|
348
|
+
for (const role of roles) {
|
|
349
|
+
const found = detection.commands[role];
|
|
350
|
+
if (!found) continue;
|
|
351
|
+
let pattern = targetPattern;
|
|
352
|
+
if (role === 'testTargeted' && found.command.includes(TARGET_PATTERN_TOKEN)) {
|
|
353
|
+
if (typeof targetPattern === 'string' && targetPattern.trim() !== '') {
|
|
354
|
+
selection = { basis: 'explicit-pattern', selectedCount: null, totalCount: null, fallback: null };
|
|
355
|
+
} else {
|
|
356
|
+
// No caller-supplied target: scope to the files this feature changed, where the toolchain
|
|
357
|
+
// has a test-file convention. The changed files are the ones the change-scope tier reads.
|
|
358
|
+
const ctx = selectorContext || {};
|
|
359
|
+
const scope = ctx.scope || {};
|
|
360
|
+
const observation = this.observeForSelection(ctx.changedFiles, {
|
|
361
|
+
baseline: scope.baseline, baselineRef: scope.baselineRef, baselineSha: scope.baselineSha,
|
|
362
|
+
});
|
|
363
|
+
const selected = selectChangedTests({
|
|
364
|
+
found, observation, root: ctx.root || this.cwd, fsImpl: this.fsImpl,
|
|
365
|
+
});
|
|
366
|
+
if (selected) {
|
|
367
|
+
pattern = selected.pattern;
|
|
368
|
+
selection = selected.selection;
|
|
369
|
+
} else {
|
|
370
|
+
selection = { basis: 'none', selectedCount: null, totalCount: null, fallback: 'not-scopable' };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const { command, substituted } = applyTargetPattern(found.command, pattern);
|
|
375
|
+
if (command === null) {
|
|
376
|
+
// The command is a template and no target was supplied. Fall through to the next role —
|
|
377
|
+
// for targeted-tests that is the broad test command, which the report records.
|
|
378
|
+
notes.push(selection && selection.fallback
|
|
379
|
+
? `role '${role}' has no selection (${selection.fallback})`
|
|
380
|
+
: `role '${role}' needs a target pattern and none was supplied`);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
checks.push({
|
|
384
|
+
name: `${tier.id}:${role}`,
|
|
385
|
+
tier: tier.id,
|
|
386
|
+
role,
|
|
387
|
+
command,
|
|
388
|
+
timeoutMs: tier.timeoutMs || undefined,
|
|
389
|
+
source: found.source,
|
|
390
|
+
derivation: found.derivation,
|
|
391
|
+
targetScoped: substituted,
|
|
392
|
+
});
|
|
393
|
+
if (!selectAll) break;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (checks.length > 0) {
|
|
397
|
+
const fellBack = !selectAll && checks[0].role !== roles[0];
|
|
398
|
+
return {
|
|
399
|
+
resolution: 'RESOLVED',
|
|
400
|
+
reason: fellBack
|
|
401
|
+
? `Using the '${checks[0].role}' command; ${notes.join('; ')}.`
|
|
402
|
+
: null,
|
|
403
|
+
checks,
|
|
404
|
+
...(selection ? { selection } : {}),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Explicit human declaration outranks everything: the plan said this project has no such
|
|
409
|
+
// command, which is a statement, not a detection miss.
|
|
410
|
+
if (roles.every((role: string) => declaredNone.has(role))) {
|
|
411
|
+
return {
|
|
412
|
+
resolution: 'NOT_APPLICABLE',
|
|
413
|
+
reason: `plan.md declares this project has no ${roles.join('/')} command.`,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// A manifest was read and does not declare the role. For build or lint that is real evidence
|
|
418
|
+
// the project has no such step; for a test command it is not, and `absenceIsEvidence: false`
|
|
419
|
+
// in the registry is what keeps that case UNRESOLVED and off PASS.
|
|
420
|
+
if (tier.absenceIsEvidence && detection.manifestFound && roles.every((role: string) => absent.has(role) || declaredNone.has(role))) {
|
|
421
|
+
return {
|
|
422
|
+
resolution: 'NOT_APPLICABLE',
|
|
423
|
+
reason: `${detection.manifests.join(', ')} declares no ${roles.join('/')} step.`,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const why = detection.manifestFound
|
|
428
|
+
? `no ${roles.join('/')} command could be detected from ${detection.manifests.join(', ')}`
|
|
429
|
+
: `no project manifest was found under ${detection.projectRoot || this.cwd}`;
|
|
430
|
+
return {
|
|
431
|
+
resolution: 'UNRESOLVED',
|
|
432
|
+
reason: `${why}. Declare it in plan.md's \`${this.policy.planOverrideBlock}\` block (use null to state the project has none).`,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** @private */
|
|
437
|
+
resolveStructuralTier(tier: any, structuralChecks: any[]): any {
|
|
438
|
+
const checks = structuralChecks
|
|
439
|
+
.filter((c) => c && typeof c.command === 'string' && c.command.trim() !== '')
|
|
440
|
+
.map((c, i) => ({
|
|
441
|
+
name: `${tier.id}:${c.name || `invariant_${i + 1}`}`,
|
|
442
|
+
tier: tier.id,
|
|
443
|
+
role: 'structural',
|
|
444
|
+
command: c.command,
|
|
445
|
+
timeoutMs: c.timeoutMs || tier.timeoutMs || undefined,
|
|
446
|
+
source: 'caller',
|
|
447
|
+
derivation: 'declared',
|
|
448
|
+
}));
|
|
449
|
+
if (checks.length === 0) {
|
|
450
|
+
return {
|
|
451
|
+
resolution: 'UNRESOLVED',
|
|
452
|
+
reason: 'No structural invariant checks were supplied, and this risk level requires structural evidence.',
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
return { resolution: 'RESOLVED', checks, reason: null };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** @private */
|
|
459
|
+
resolveRequirementTier(tier: any, requirements: any[]): any {
|
|
460
|
+
const checks: any[] = [];
|
|
461
|
+
const unevaluated: any[] = [];
|
|
462
|
+
for (const req of requirements) {
|
|
463
|
+
if (!req || typeof req.id !== 'string' || req.id === '') continue;
|
|
464
|
+
if (typeof req.command === 'string' && req.command.trim() !== '') {
|
|
465
|
+
checks.push({
|
|
466
|
+
name: `${tier.id}:${req.id}`,
|
|
467
|
+
tier: tier.id,
|
|
468
|
+
role: 'requirement',
|
|
469
|
+
requirementId: req.id,
|
|
470
|
+
description: req.description || null,
|
|
471
|
+
command: req.command,
|
|
472
|
+
timeoutMs: req.timeoutMs || tier.timeoutMs || undefined,
|
|
473
|
+
source: 'caller',
|
|
474
|
+
derivation: 'declared',
|
|
475
|
+
});
|
|
476
|
+
} else {
|
|
477
|
+
// An asserted requirement is not a verified one. It is listed so the report can name what
|
|
478
|
+
// is unproven, rather than counted as satisfied.
|
|
479
|
+
unevaluated.push({ id: req.id, description: req.description || null });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (checks.length === 0 && unevaluated.length === 0) {
|
|
483
|
+
return {
|
|
484
|
+
resolution: 'UNRESOLVED',
|
|
485
|
+
reason: 'No requirement checks were supplied, and this risk level requires requirement evidence.',
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
resolution: 'RESOLVED',
|
|
490
|
+
checks,
|
|
491
|
+
unevaluatedRequirements: unevaluated,
|
|
492
|
+
reason: unevaluated.length > 0
|
|
493
|
+
? `${unevaluated.length} requirement(s) have no executable check and cannot be reported satisfied.`
|
|
494
|
+
: null,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** @private */
|
|
499
|
+
resolveScopeTier(tier: any, scope: any): any {
|
|
500
|
+
const maxFiles = Number.isFinite(scope.maxFiles) ? scope.maxFiles : null;
|
|
501
|
+
const allowedPaths = toStringArray(scope.allowedPaths);
|
|
502
|
+
if (maxFiles === null && allowedPaths.length === 0) {
|
|
503
|
+
return {
|
|
504
|
+
resolution: 'UNRESOLVED',
|
|
505
|
+
reason: typeof scope.unresolvedReason === 'string' && scope.unresolvedReason !== ''
|
|
506
|
+
? scope.unresolvedReason
|
|
507
|
+
: 'No change-scope bound was declared before implementation, so the actual scope has nothing to be compared against.',
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
resolution: 'RESOLVED',
|
|
512
|
+
bound: {
|
|
513
|
+
maxFiles,
|
|
514
|
+
allowedPaths,
|
|
515
|
+
...(typeof scope.source === 'string' ? { source: scope.source } : {}),
|
|
516
|
+
...(Array.isArray(scope.sources) ? { sources: scope.sources } : {}),
|
|
517
|
+
...(scope.baseline === 'integration' ? { baseline: 'integration' } : {}),
|
|
518
|
+
...(scope.baseline === 'explicit' ? { baseline: 'explicit', baselineRef: scope.baselineRef, baselineSha: scope.baselineSha } : {}),
|
|
519
|
+
},
|
|
520
|
+
reason: null,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Observes which files the change actually touched.
|
|
526
|
+
* @private
|
|
527
|
+
* @param {Array<string>} [changedFiles] caller-supplied files, which win over any observation
|
|
528
|
+
* @param {Object} [bound] a bound with `baseline: 'integration'` adds the files committed since the
|
|
529
|
+
* merge base with the integration ref to the working tree's
|
|
530
|
+
* @returns {{ files: Array<string>|null, reason: string|null, baseline?: Object }}
|
|
531
|
+
*/
|
|
532
|
+
observeChangedFiles(changedFiles: any, bound: any = null): any {
|
|
533
|
+
// An explicit baseline observes git itself: a caller's file list cannot stand in for what changed.
|
|
534
|
+
if (bound && bound.baseline === 'explicit') return this.observeSince(bound.baselineSha, { kind: 'pinned', ref: bound.baselineRef, sha: bound.baselineSha });
|
|
535
|
+
if (Array.isArray(changedFiles)) return { files: changedFiles.filter((f: any) => typeof f === 'string'), reason: null };
|
|
536
|
+
|
|
537
|
+
if (bound && bound.baseline === 'integration') return this.observeFromBase();
|
|
538
|
+
|
|
539
|
+
let res: any;
|
|
540
|
+
try {
|
|
541
|
+
res = this.exec('git status --porcelain', {
|
|
542
|
+
shell: true,
|
|
543
|
+
cwd: this.cwd,
|
|
544
|
+
encoding: 'utf8',
|
|
545
|
+
timeout: 30000,
|
|
546
|
+
});
|
|
547
|
+
} catch (error: any) {
|
|
548
|
+
return { files: null, reason: `Could not observe the changed files: ${error.message}` };
|
|
549
|
+
}
|
|
550
|
+
if (!res || res.error || res.status !== 0) {
|
|
551
|
+
const detail = (res && res.error && res.error.message) || (res && truncate(res.stderr)) || 'git returned a non-zero status';
|
|
552
|
+
return { files: null, reason: `Could not observe the changed files: ${detail}` };
|
|
553
|
+
}
|
|
554
|
+
return { files: parsePorcelain(res.stdout), reason: null };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* The changed files the test selector reads. Same sources as `observeChangedFiles`, except that
|
|
559
|
+
* with no baseline and no caller list the working tree is read NUL-separated, unquoted and with
|
|
560
|
+
* untracked directories expanded, so a non-ASCII name or a new directory of tests is not dropped.
|
|
561
|
+
* @private
|
|
562
|
+
*/
|
|
563
|
+
observeForSelection(changedFiles: any, bound: any = null): any {
|
|
564
|
+
const hasBaseline = bound && (bound.baseline === 'explicit' || bound.baseline === 'integration');
|
|
565
|
+
if (!Array.isArray(changedFiles) && !hasBaseline) return this.observeSince(null, null);
|
|
566
|
+
return this.observeChangedFiles(changedFiles, bound);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* The bounded observation (IC-004): files committed since the merge base with the integration ref,
|
|
571
|
+
* plus the working tree. Paths are read NUL-separated and unquoted, so a name with a space or
|
|
572
|
+
* non-ASCII characters is the name the plan wrote, and renames are not collapsed, so a moved file
|
|
573
|
+
* reports both where it was and where it went.
|
|
574
|
+
* @private
|
|
575
|
+
*/
|
|
576
|
+
observeFromBase(): any {
|
|
577
|
+
const base = resolveIntegrationBase({ cwd: this.cwd, exec: this.exec });
|
|
578
|
+
if (base.reason) {
|
|
579
|
+
const working = this.observeSince(null, null);
|
|
580
|
+
if (working.files === null) return working;
|
|
581
|
+
return { files: working.files, reason: null, baseline: { kind: 'working-tree', note: base.note || `${base.reason}; only the working tree was compared` } };
|
|
582
|
+
}
|
|
583
|
+
return this.observeSince(base.mergeBase, { kind: 'merge-base', ref: base.ref, mergeBase: base.mergeBase });
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Files changed since `commit` (null: the working tree alone): committed changes to HEAD, staged,
|
|
588
|
+
* unstaged and untracked paths. NUL-delimited, unquoted, with renames split so both names count; only
|
|
589
|
+
* DoFlow's own `.doflow/` state is excluded.
|
|
590
|
+
* @private
|
|
591
|
+
*/
|
|
592
|
+
observeSince(commit: string | null, baseline: any): any {
|
|
593
|
+
const git = (args: string[]) => {
|
|
594
|
+
let res: any;
|
|
595
|
+
try {
|
|
596
|
+
res = this.exec('git', ['-c', 'core.quotePath=false', ...args], { cwd: this.cwd, encoding: 'utf8', timeout: 30000 });
|
|
597
|
+
} catch (error: any) {
|
|
598
|
+
return { reason: `Could not observe the changed files: ${error.message}` };
|
|
599
|
+
}
|
|
600
|
+
if (!res || res.error || res.status !== 0) {
|
|
601
|
+
const detail = (res && res.error && res.error.message) || (res && truncate(res.stderr)) || 'git returned a non-zero status';
|
|
602
|
+
return { reason: `Could not observe the changed files: ${detail}` };
|
|
603
|
+
}
|
|
604
|
+
return { out: String(res.stdout || '') };
|
|
605
|
+
};
|
|
606
|
+
const status = git(['status', '--porcelain', '-z', '--no-renames', '--untracked-files=all']);
|
|
607
|
+
if (status.reason) return { files: null, reason: status.reason };
|
|
608
|
+
// `-z` porcelain entries are `XY <path>` separated by NUL.
|
|
609
|
+
// `.doflow/` is DoFlow's own state (records, journals): writing it is not changing the project,
|
|
610
|
+
// and a repo that does not ignore it would otherwise fail its own scope after any record.
|
|
611
|
+
const counts = (f: string) => f !== '.doflow' && !f.startsWith('.doflow/');
|
|
612
|
+
const working = status.out!.split('\0').filter(Boolean).map((entry: string) => entry.slice(3)).filter(Boolean).filter(counts);
|
|
613
|
+
if (commit === null) return { files: working, reason: null };
|
|
614
|
+
|
|
615
|
+
const diff = git(['diff', '--name-only', '--no-renames', '-z', `${commit}..HEAD`]);
|
|
616
|
+
if (diff.reason) return { files: null, reason: diff.reason };
|
|
617
|
+
const committed = diff.out!.split('\0').filter(Boolean).filter(counts);
|
|
618
|
+
return { files: [...new Set([...committed, ...working])], reason: null, baseline };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Runs a compiled contract and produces the report. Called **after** implementation.
|
|
623
|
+
*
|
|
624
|
+
* @param {Object} contract from `compileContract`
|
|
625
|
+
* @param {Object} [runtimeInput]
|
|
626
|
+
* @param {Array<string>} [runtimeInput.changedFiles] overrides the git observation
|
|
627
|
+
* @param {{status: string, findings?: Array<string>, reviewer?: string}} [runtimeInput.modelReview]
|
|
628
|
+
* @returns {Object} the report
|
|
629
|
+
*/
|
|
630
|
+
runContract(contract: any, runtimeInput: any = {}): any {
|
|
631
|
+
if (!contract || !Array.isArray(contract.tiers)) {
|
|
632
|
+
throw new Error('runContract expects a contract produced by compileContract');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const startedAt = this.timestamp();
|
|
636
|
+
const reportTiers: any[] = [];
|
|
637
|
+
const failedChecks: string[] = [];
|
|
638
|
+
let fatalTier: string | null = null;
|
|
639
|
+
let checksRun = 0;
|
|
640
|
+
let checksPassed = 0;
|
|
641
|
+
// One evaluation pass, one cache: two tiers declaring the same command (targeted and broad
|
|
642
|
+
// tests are often both `npm test`) run it once and share the result. A recovery retry calls
|
|
643
|
+
// runContract again and gets a fresh cache, so retries always observe live behaviour.
|
|
644
|
+
const dedupeCache = new Map<string, any>();
|
|
645
|
+
|
|
646
|
+
for (const tier of contract.tiers) {
|
|
647
|
+
const entry: any = {
|
|
648
|
+
id: tier.id,
|
|
649
|
+
name: tier.name,
|
|
650
|
+
kind: tier.kind,
|
|
651
|
+
required: tier.required,
|
|
652
|
+
advisory: tier.advisory,
|
|
653
|
+
modelBased: tier.modelBased,
|
|
654
|
+
failureClass: tier.failureClass,
|
|
655
|
+
status: tier.resolution === 'RESOLVED' ? 'NOT_RUN' : tier.resolution,
|
|
656
|
+
reason: tier.reason,
|
|
657
|
+
checks: [],
|
|
658
|
+
...(tier.selection ? { selection: tier.selection } : {}),
|
|
659
|
+
...(tier.subsumedBy ? { subsumedBy: tier.subsumedBy } : {}),
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
if (tier.resolution !== 'RESOLVED') {
|
|
663
|
+
reportTiers.push(entry);
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (fatalTier) {
|
|
668
|
+
entry.status = 'NOT_RUN';
|
|
669
|
+
entry.reason = `Not run: the fatal '${fatalTier}' tier failed, so any result here would describe that failure rather than this tier.`;
|
|
670
|
+
reportTiers.push(entry);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (tier.modelBased) {
|
|
675
|
+
Object.assign(entry, this.evaluateModelTier(tier, runtimeInput.modelReview));
|
|
676
|
+
reportTiers.push(entry);
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (tier.kind === 'scope') {
|
|
681
|
+
Object.assign(entry, this.evaluateScopeTier(tier, runtimeInput.changedFiles));
|
|
682
|
+
reportTiers.push(entry);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
for (const check of tier.checks) {
|
|
687
|
+
const result = this.runner.runCheck(check.name, check.command, check.timeoutMs, dedupeCache);
|
|
688
|
+
result.tier = tier.id;
|
|
689
|
+
result.role = check.role;
|
|
690
|
+
if (check.requirementId) result.requirementId = check.requirementId;
|
|
691
|
+
entry.checks.push(result);
|
|
692
|
+
checksRun += 1;
|
|
693
|
+
if (result.status === 'PASS') checksPassed += 1;
|
|
694
|
+
else failedChecks.push(result.name);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const anyFailed = entry.checks.some((c: any) => c.status === 'FAIL');
|
|
698
|
+
if (anyFailed) {
|
|
699
|
+
entry.status = 'FAIL';
|
|
700
|
+
if (tier.fatal) fatalTier = tier.id;
|
|
701
|
+
} else if (Array.isArray(tier.unevaluatedRequirements) && tier.unevaluatedRequirements.length > 0) {
|
|
702
|
+
// Every executable requirement check passed, but some requirements had none. Reporting the
|
|
703
|
+
// tier as PASS would claim satisfaction for requirements nothing evaluated.
|
|
704
|
+
entry.status = 'UNRESOLVED';
|
|
705
|
+
entry.unevaluatedRequirements = tier.unevaluatedRequirements;
|
|
706
|
+
} else if (entry.checks.length === 0) {
|
|
707
|
+
entry.status = 'UNRESOLVED';
|
|
708
|
+
entry.reason = 'Tier resolved but contributed no runnable check.';
|
|
709
|
+
} else {
|
|
710
|
+
entry.status = 'PASS';
|
|
711
|
+
}
|
|
712
|
+
reportTiers.push(entry);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const requiredTiers = reportTiers.filter((t) => t.required);
|
|
716
|
+
const requiredFailed = requiredTiers.filter((t) => t.status === 'FAIL');
|
|
717
|
+
const requiredUnverified = requiredTiers.filter((t) => t.status === 'UNRESOLVED' || t.status === 'NOT_RUN');
|
|
718
|
+
const evaluated = reportTiers.filter((t) => t.status === 'PASS' || t.status === 'FAIL');
|
|
719
|
+
const modelTier = reportTiers.find((t) => t.modelBased);
|
|
720
|
+
|
|
721
|
+
let status: string;
|
|
722
|
+
let reason: string;
|
|
723
|
+
if (evaluated.length === 0) {
|
|
724
|
+
// The empty-contract defect, generalized: zero evaluated tiers is a verdict over zero
|
|
725
|
+
// evidence whether the contract was empty or every tier turned out unresolvable.
|
|
726
|
+
status = 'INCONCLUSIVE';
|
|
727
|
+
reason = contract.tiers.every((t: any) => t.resolution === 'SKIPPED')
|
|
728
|
+
? 'Contract declared no checks; nothing was verified'
|
|
729
|
+
: 'No tier produced a result; nothing was verified';
|
|
730
|
+
} else if (requiredFailed.length > 0) {
|
|
731
|
+
status = 'FAIL';
|
|
732
|
+
reason = `Required tier(s) failed: ${requiredFailed.map((t) => t.id).join(', ')}.`;
|
|
733
|
+
} else if (modelTier && modelTier.status === 'FAIL') {
|
|
734
|
+
// A model review can lower the verdict but never raise it — checked after the deterministic
|
|
735
|
+
// tiers so it is never the thing that established correctness.
|
|
736
|
+
status = 'FAIL';
|
|
737
|
+
reason = 'Model-based review reported a failure.';
|
|
738
|
+
} else if (requiredUnverified.length > 0) {
|
|
739
|
+
status = 'INCONCLUSIVE';
|
|
740
|
+
reason = `Required tier(s) were never evaluated: ${requiredUnverified.map((t) => t.id).join(', ')}.`;
|
|
741
|
+
} else {
|
|
742
|
+
status = 'PASS';
|
|
743
|
+
reason = 'Every required tier passed.';
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const requirementTier = reportTiers.find((t) => t.kind === 'requirement');
|
|
747
|
+
const scopeTier = reportTiers.find((t) => t.kind === 'scope');
|
|
748
|
+
|
|
749
|
+
return {
|
|
750
|
+
version: contract.version,
|
|
751
|
+
taskId: contract.taskId,
|
|
752
|
+
riskLevel: contract.riskLevel,
|
|
753
|
+
status,
|
|
754
|
+
reason,
|
|
755
|
+
startedAt,
|
|
756
|
+
finishedAt: this.timestamp(),
|
|
757
|
+
tiers: reportTiers,
|
|
758
|
+
failedChecks,
|
|
759
|
+
counts: {
|
|
760
|
+
checksRun,
|
|
761
|
+
checksPassed,
|
|
762
|
+
checksFailed: checksRun - checksPassed,
|
|
763
|
+
tiersRequired: requiredTiers.length,
|
|
764
|
+
tiersPassed: requiredTiers.filter((t) => t.status === 'PASS').length,
|
|
765
|
+
tiersFailed: requiredFailed.length,
|
|
766
|
+
tiersUnverified: requiredUnverified.length,
|
|
767
|
+
tiersNotApplicable: requiredTiers.filter((t) => t.status === 'NOT_APPLICABLE' || t.status === 'SUBSUMED').length,
|
|
768
|
+
},
|
|
769
|
+
requirementSatisfaction: requirementTier
|
|
770
|
+
? {
|
|
771
|
+
status: requirementTier.status,
|
|
772
|
+
satisfied: requirementTier.checks.filter((c: any) => c.status === 'PASS').map((c: any) => c.requirementId || c.name),
|
|
773
|
+
failed: requirementTier.checks.filter((c: any) => c.status === 'FAIL').map((c: any) => c.requirementId || c.name),
|
|
774
|
+
unevaluated: (requirementTier.unevaluatedRequirements || []).map((r: any) => r.id),
|
|
775
|
+
}
|
|
776
|
+
: null,
|
|
777
|
+
scope: scopeTier ? scopeTier.scope || { status: scopeTier.status, reason: scopeTier.reason } : null,
|
|
778
|
+
modelReview: modelTier
|
|
779
|
+
? { status: modelTier.status, primaryProof: false, findings: modelTier.findings || [] }
|
|
780
|
+
: null,
|
|
781
|
+
unresolved: reportTiers
|
|
782
|
+
.filter((t) => t.status === 'UNRESOLVED' || t.status === 'NOT_RUN')
|
|
783
|
+
.map((t) => ({ tier: t.id, required: t.required, reason: t.reason })),
|
|
784
|
+
// A PASS reached with several tiers declared inapplicable is still a PASS, but a reader who
|
|
785
|
+
// cannot see which tiers never ran will over-read it. Listed next to the verdict, not buried.
|
|
786
|
+
notApplicable: reportTiers
|
|
787
|
+
.filter((t) => t.status === 'NOT_APPLICABLE' || t.status === 'SUBSUMED')
|
|
788
|
+
.map((t) => ({ tier: t.id, status: t.status, reason: t.reason })),
|
|
789
|
+
// The contract's own chronology and provenance travel with the verdict, so a report cannot read
|
|
790
|
+
// as stronger evidence than the contract it was run against (IC-022).
|
|
791
|
+
contract: {
|
|
792
|
+
compiledAt: contract.compiledAt || null,
|
|
793
|
+
phase: contract.phase || 'unknown',
|
|
794
|
+
compiledBeforeImplementation: contract.compiledBeforeImplementation === undefined ? null : contract.compiledBeforeImplementation,
|
|
795
|
+
...(contract.provenance ? { provenance: contract.provenance } : {}),
|
|
796
|
+
},
|
|
797
|
+
detection: contract.detection,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/** @private */
|
|
802
|
+
evaluateModelTier(tier: any, modelReview: any): any {
|
|
803
|
+
if (!modelReview || typeof modelReview.status !== 'string') {
|
|
804
|
+
return {
|
|
805
|
+
status: 'UNRESOLVED',
|
|
806
|
+
reason: 'No model review was supplied for a risk level that requires one to be recorded.',
|
|
807
|
+
findings: [],
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
const supplied = modelReview.status.toUpperCase();
|
|
811
|
+
if (supplied === 'FAIL') {
|
|
812
|
+
return { status: 'FAIL', reason: 'Model review reported findings.', findings: toStringArray(modelReview.findings) };
|
|
813
|
+
}
|
|
814
|
+
if (supplied === 'PASS') {
|
|
815
|
+
// Recorded as PASS for the tier, but the overall verdict already required every deterministic
|
|
816
|
+
// tier to pass before reaching here — this never becomes the proof of anything on its own.
|
|
817
|
+
return { status: 'PASS', reason: 'Model review recorded no findings; advisory only.', findings: [] };
|
|
818
|
+
}
|
|
819
|
+
return {
|
|
820
|
+
status: 'UNRESOLVED',
|
|
821
|
+
reason: `Model review status '${modelReview.status}' is neither PASS nor FAIL.`,
|
|
822
|
+
findings: toStringArray(modelReview.findings),
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/** @private */
|
|
827
|
+
evaluateScopeTier(tier: any, changedFiles: any): any {
|
|
828
|
+
const bound = tier.bound || { maxFiles: null, allowedPaths: [] };
|
|
829
|
+
const { files, reason, baseline } = this.observeChangedFiles(changedFiles, bound);
|
|
830
|
+
if (files === null) {
|
|
831
|
+
return {
|
|
832
|
+
status: 'UNRESOLVED',
|
|
833
|
+
reason,
|
|
834
|
+
scope: { bound, actual: null, withinBound: null, reason },
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const violations: string[] = [];
|
|
839
|
+
if (Number.isFinite(bound.maxFiles) && files.length > bound.maxFiles) {
|
|
840
|
+
violations.push(`${files.length} files changed, bound was ${bound.maxFiles}`);
|
|
841
|
+
}
|
|
842
|
+
if (bound.allowedPaths.length > 0) {
|
|
843
|
+
const outside = files.filter((f: string) => !bound.allowedPaths.some((p: string) => f === p || f.startsWith(p.endsWith('/') ? p : `${p}/`)));
|
|
844
|
+
if (outside.length > 0) violations.push(`outside the declared paths: ${outside.join(', ')}`);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const withinBound = violations.length === 0;
|
|
848
|
+
// A bound measured from the working tree alone (no integration ref) says so: committed work
|
|
849
|
+
// is invisible to it, which is a limit of the answer, not a clean result.
|
|
850
|
+
const note = baseline && baseline.note ? baseline.note : null;
|
|
851
|
+
const verdictReason = withinBound
|
|
852
|
+
? note
|
|
853
|
+
: `Change exceeded its declared scope — ${violations.join('; ')}.${note ? ` (${note})` : ''}`;
|
|
854
|
+
return {
|
|
855
|
+
status: withinBound ? 'PASS' : 'FAIL',
|
|
856
|
+
reason: verdictReason,
|
|
857
|
+
scope: { bound, actual: { fileCount: files.length, files }, withinBound, violations, ...(baseline ? { baseline } : {}) },
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Declares completion. FR-009: a task is complete only when its required checks pass, or when the
|
|
863
|
+
* user explicitly accepts what is still failing.
|
|
864
|
+
*
|
|
865
|
+
* @param {Object} report
|
|
866
|
+
* @param {Object} [acceptance]
|
|
867
|
+
* @param {boolean} [acceptance.userAccepted]
|
|
868
|
+
* @param {string} [acceptance.acceptedBy] who accepted; an unattributed acceptance is refused
|
|
869
|
+
* @param {string} [acceptance.note]
|
|
870
|
+
* @returns {{ complete: boolean, basis: string, reason: string, acceptedFailures?: Array<Object> }}
|
|
871
|
+
*/
|
|
872
|
+
declareCompletion(report: any, acceptance: any = {}): any {
|
|
873
|
+
if (!report || typeof report.status !== 'string') {
|
|
874
|
+
throw new Error('declareCompletion expects a report produced by runContract');
|
|
875
|
+
}
|
|
876
|
+
if (report.status === 'PASS') {
|
|
877
|
+
return { complete: true, basis: 'required-checks-passed', reason: report.reason };
|
|
878
|
+
}
|
|
879
|
+
if (acceptance.userAccepted !== true) {
|
|
880
|
+
return {
|
|
881
|
+
complete: false,
|
|
882
|
+
basis: 'not-accepted',
|
|
883
|
+
reason: `Verification is ${report.status}: ${report.reason} Completion needs the required checks to pass or an explicit user acceptance.`,
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
const requiresAccepter = (this.policy.completion || {}).overrideRequiresAccepter !== false;
|
|
887
|
+
if (requiresAccepter && (typeof acceptance.acceptedBy !== 'string' || acceptance.acceptedBy.trim() === '')) {
|
|
888
|
+
// "The user explicitly accepts" needs a user. An anonymous flag is indistinguishable from the
|
|
889
|
+
// caller setting it for itself.
|
|
890
|
+
return {
|
|
891
|
+
complete: false,
|
|
892
|
+
basis: 'acceptance-unattributed',
|
|
893
|
+
reason: 'Accepting a failing verification requires naming who accepted it.',
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
return {
|
|
897
|
+
complete: true,
|
|
898
|
+
basis: 'user-accepted-failures',
|
|
899
|
+
reason: acceptance.note || `Accepted by ${acceptance.acceptedBy} while ${report.status}.`,
|
|
900
|
+
acceptedFailures: (report.tiers || [])
|
|
901
|
+
.filter((t: any) => t.required && t.status !== 'PASS' && t.status !== 'NOT_APPLICABLE' && t.status !== 'SUBSUMED' && t.status !== 'SKIPPED')
|
|
902
|
+
.map((t: any) => ({ tier: t.id, status: t.status, reason: t.reason })),
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* A stable fingerprint of what failed. Two identical fingerprints across attempts mean the retry
|
|
908
|
+
* changed nothing observable, which FR-010 forbids as a response.
|
|
909
|
+
* @param {Object} report
|
|
910
|
+
* @returns {string}
|
|
911
|
+
*/
|
|
912
|
+
failureSignature(report: any): string {
|
|
913
|
+
const failed = [...(report.failedChecks || [])].sort().join('|');
|
|
914
|
+
const unresolved = (report.unresolved || []).map((u: any) => u.tier).sort().join('|');
|
|
915
|
+
return `${report.status}::${failed}::${unresolved}`;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Classifies a failing report and returns the targeted action for that class (C8), bounded by the
|
|
920
|
+
* risk level's retry limit.
|
|
921
|
+
*
|
|
922
|
+
* @param {Object} report
|
|
923
|
+
* @param {Object} [options]
|
|
924
|
+
* @param {number} [options.iteration=0] retries already spent
|
|
925
|
+
* @param {string} [options.lastAgent]
|
|
926
|
+
* @param {number} [options.maxIterations] defaults to the report's risk level
|
|
927
|
+
* @returns {Object|null} null when there is nothing to recover from
|
|
928
|
+
*/
|
|
929
|
+
planRecovery(report: any, options: any = {}): any {
|
|
930
|
+
if (!report || report.status === 'PASS') return null;
|
|
931
|
+
|
|
932
|
+
const maxIterations = Number.isInteger(options.maxIterations)
|
|
933
|
+
? options.maxIterations
|
|
934
|
+
: this.riskDefinition(report.riskLevel).maxRecoveryIterations;
|
|
935
|
+
const manager = this.recoveryOverride || new RecoveryManager({ maxIterations });
|
|
936
|
+
|
|
937
|
+
const failingTier = (report.tiers || []).find((t: any) => t.required && t.status === 'FAIL')
|
|
938
|
+
|| (report.tiers || []).find((t: any) => t.status === 'FAIL');
|
|
939
|
+
const unverifiedTier = (report.tiers || []).find((t: any) => t.required && (t.status === 'UNRESOLVED' || t.status === 'NOT_RUN'));
|
|
940
|
+
|
|
941
|
+
let failureClass: string;
|
|
942
|
+
let classificationSource: string;
|
|
943
|
+
let errorMessage: string;
|
|
944
|
+
let hint: string | null = null;
|
|
945
|
+
|
|
946
|
+
if (failingTier) {
|
|
947
|
+
const firstFailure = (failingTier.checks || []).find((c: any) => c.status === 'FAIL');
|
|
948
|
+
errorMessage = [failingTier.reason, firstFailure && firstFailure.error, firstFailure && firstFailure.stderr]
|
|
949
|
+
.filter(Boolean)
|
|
950
|
+
.join(' ');
|
|
951
|
+
// A tier's declared class is structural evidence about what kind of check failed; the keyword
|
|
952
|
+
// classifier reads prose. recovery.js already prefers structure over prose between check
|
|
953
|
+
// names and error text, and this applies the same precedence one level up. The classifier
|
|
954
|
+
// still runs and is reported, so a disagreement stays visible rather than being hidden.
|
|
955
|
+
const classified = manager.classifyFailure(errorMessage, report.failedChecks);
|
|
956
|
+
failureClass = failingTier.failureClass || classified;
|
|
957
|
+
classificationSource = failingTier.failureClass ? 'tier-declaration' : 'classifier';
|
|
958
|
+
if (failingTier.failureClass && failingTier.failureClass !== classified) {
|
|
959
|
+
hint = `Keyword classifier suggested ${classified}; the '${failingTier.id}' tier declares ${failingTier.failureClass}.`;
|
|
960
|
+
}
|
|
961
|
+
} else if (unverifiedTier) {
|
|
962
|
+
errorMessage = unverifiedTier.reason || 'A required verification tier was never evaluated.';
|
|
963
|
+
failureClass = this.policy.unresolvedDetectionFailureClass || manager.classifyFailure(errorMessage, []);
|
|
964
|
+
classificationSource = 'unresolved-tier';
|
|
965
|
+
hint = `Nothing verified the '${unverifiedTier.id}' tier. Declare its command in plan.md's \`${this.policy.planOverrideBlock}\` block, or set it to null to state the project has none.`;
|
|
966
|
+
} else {
|
|
967
|
+
errorMessage = report.reason || 'Verification did not pass.';
|
|
968
|
+
failureClass = manager.classifyFailure(errorMessage, report.failedChecks);
|
|
969
|
+
classificationSource = 'classifier';
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const plan = manager.planRecovery(failureClass, options.iteration || 0, options.lastAgent);
|
|
973
|
+
return {
|
|
974
|
+
...plan,
|
|
975
|
+
classificationSource,
|
|
976
|
+
maxIterations,
|
|
977
|
+
errorMessage,
|
|
978
|
+
hint,
|
|
979
|
+
reportStatus: report.status,
|
|
980
|
+
failureSignature: this.failureSignature(report),
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Runs a contract and, on failure, drives the bounded recovery loop.
|
|
986
|
+
*
|
|
987
|
+
* `remediate` must report that it changed something. FR-010 forbids re-running unchanged
|
|
988
|
+
* behaviour, so a remediation that returns `changed !== true` stops the loop instead of producing
|
|
989
|
+
* a second identical attempt, and an attempt whose failure fingerprint repeats stops it too — a
|
|
990
|
+
* change that altered nothing observable is the same thing wearing a different hat.
|
|
991
|
+
*
|
|
992
|
+
* @param {Object} contract
|
|
993
|
+
* @param {Object} [options]
|
|
994
|
+
* @param {Function} [options.remediate] `(plan, report, iteration) => {changed: boolean, contract?}`
|
|
995
|
+
* @param {string} [options.lastAgent]
|
|
996
|
+
* @param {Object} [options.runtimeInput] forwarded to each `runContract`
|
|
997
|
+
* @returns {{ outcome: string, report: Object, plan: Object|null, attempts: Array<Object>, reason: string }}
|
|
998
|
+
*/
|
|
999
|
+
runWithRecovery(contract: any, options: any = {}): any {
|
|
1000
|
+
let current = contract;
|
|
1001
|
+
let lastAgent = options.lastAgent;
|
|
1002
|
+
let previousSignature: string | null = null;
|
|
1003
|
+
let iteration = 0;
|
|
1004
|
+
const attempts: any[] = [];
|
|
1005
|
+
|
|
1006
|
+
for (;;) {
|
|
1007
|
+
const report = this.runContract(current, options.runtimeInput || {});
|
|
1008
|
+
const signature = this.failureSignature(report);
|
|
1009
|
+
attempts.push({ iteration, status: report.status, signature, failedChecks: report.failedChecks });
|
|
1010
|
+
|
|
1011
|
+
if (report.status === 'PASS') {
|
|
1012
|
+
return { outcome: 'PASS', report, plan: null, attempts, reason: report.reason };
|
|
1013
|
+
}
|
|
1014
|
+
if (signature === previousSignature) {
|
|
1015
|
+
return {
|
|
1016
|
+
outcome: 'NO_PROGRESS',
|
|
1017
|
+
report,
|
|
1018
|
+
plan: null,
|
|
1019
|
+
attempts,
|
|
1020
|
+
reason: 'The previous recovery reported a change but produced an identical failure; retrying again would repeat unchanged behaviour.',
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
previousSignature = signature;
|
|
1024
|
+
|
|
1025
|
+
const plan = this.planRecovery(report, { iteration, lastAgent });
|
|
1026
|
+
if (!plan.canRetry) {
|
|
1027
|
+
return { outcome: 'ABORTED', report, plan, attempts, reason: plan.reason };
|
|
1028
|
+
}
|
|
1029
|
+
if (typeof options.remediate !== 'function') {
|
|
1030
|
+
return {
|
|
1031
|
+
outcome: 'NO_REMEDIATION',
|
|
1032
|
+
report,
|
|
1033
|
+
plan,
|
|
1034
|
+
attempts,
|
|
1035
|
+
reason: `Recovery action ${plan.action} is available but no remediation callback was supplied.`,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const outcome = options.remediate(plan, report, iteration);
|
|
1040
|
+
if (!outcome || outcome.changed !== true) {
|
|
1041
|
+
return {
|
|
1042
|
+
outcome: 'NO_CHANGE',
|
|
1043
|
+
report,
|
|
1044
|
+
plan,
|
|
1045
|
+
attempts,
|
|
1046
|
+
reason: `Remediation for ${plan.failureClass} reported no change; re-running unchanged behaviour is not a permitted response.`,
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
if (outcome.contract) current = outcome.contract;
|
|
1050
|
+
lastAgent = plan.agent;
|
|
1051
|
+
iteration += 1;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Convenience: compile, run, and report in one call. The two halves stay separately callable
|
|
1057
|
+
* because FR-009 requires the contract to exist *before* implementation, which this shape cannot
|
|
1058
|
+
* express on its own.
|
|
1059
|
+
* @param {Object} input `compileContract` input
|
|
1060
|
+
* @param {Object} [runtimeInput] `runContract` input
|
|
1061
|
+
*/
|
|
1062
|
+
verify(input: any, runtimeInput: any): any {
|
|
1063
|
+
const contract = this.compileContract(input);
|
|
1064
|
+
return { contract, report: this.runContract(contract, runtimeInput) };
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Handles `doflow verify` — compile the verification contract (FR-009) and, by default, run it and
|
|
1070
|
+
* report against it.
|
|
1071
|
+
*
|
|
1072
|
+
* `--action contract` stops after compiling, which is the before-implementation half: it states
|
|
1073
|
+
* how success will be established without establishing anything. The default runs the checks and
|
|
1074
|
+
* emits the report. The contract is recompiled rather than read back from a state file, so the two
|
|
1075
|
+
* halves cannot describe different contracts.
|
|
1076
|
+
*
|
|
1077
|
+
* @param {Object} options
|
|
1078
|
+
* @param {string} options.taskId
|
|
1079
|
+
* @param {'report'|'contract'|'status'} [options.action='report']
|
|
1080
|
+
* @param {string} [options.risk] risk level; the registry's default when omitted
|
|
1081
|
+
* @param {string} [options.planPath] a `plan.md` whose command override beats detection
|
|
1082
|
+
* @param {boolean} [options.json=false]
|
|
1083
|
+
* @param {string} [options.projectRoot]
|
|
1084
|
+
* @param {string} [options.slug] the feature whose plan bounds the change; the branch's when omitted
|
|
1085
|
+
* @param {string} [options.scope] a declared scope (`<path>[,<path>...]`), unioned with any plan bound
|
|
1086
|
+
* @returns {number} exit code
|
|
1087
|
+
*/
|
|
1088
|
+
/**
|
|
1089
|
+
* Validates a verification input manifest file (IC-022).
|
|
1090
|
+
*
|
|
1091
|
+
* @param {Object} options
|
|
1092
|
+
* @param {string} options.cwd
|
|
1093
|
+
* @param {string} options.verificationInput
|
|
1094
|
+
* @param {Object} [options.fs]
|
|
1095
|
+
* @param {Object} [options.crypto]
|
|
1096
|
+
* @returns {{ ok: true, structuralChecks: Array<Object>, requirements: Array<Object>, provenance: Object } | { ok: false, message: string }}
|
|
1097
|
+
*/
|
|
1098
|
+
function validateVerificationInput({ cwd, verificationInput, fs = nodeFs, crypto = nodeCrypto }: {
|
|
1099
|
+
cwd: string;
|
|
1100
|
+
verificationInput: any;
|
|
1101
|
+
fs?: any;
|
|
1102
|
+
crypto?: any;
|
|
1103
|
+
}): any {
|
|
1104
|
+
if (typeof verificationInput !== 'string' || verificationInput.trim() === '') {
|
|
1105
|
+
return { ok: false, message: '--verification-input requires a file path.' };
|
|
1106
|
+
}
|
|
1107
|
+
const inputAbs = path.isAbsolute(verificationInput) ? verificationInput : path.resolve(cwd, verificationInput);
|
|
1108
|
+
let rawBytes: Buffer;
|
|
1109
|
+
try {
|
|
1110
|
+
rawBytes = fs.readFileSync(inputAbs);
|
|
1111
|
+
} catch (err: any) {
|
|
1112
|
+
return { ok: false, message: `--verification-input '${verificationInput}' could not be read: ${err.message}` };
|
|
1113
|
+
}
|
|
1114
|
+
const manifestDigest = 'sha256:' + crypto.createHash('sha256').update(rawBytes).digest('hex');
|
|
1115
|
+
let manifest: any;
|
|
1116
|
+
try {
|
|
1117
|
+
manifest = JSON.parse(rawBytes.toString('utf8'));
|
|
1118
|
+
} catch (err: any) {
|
|
1119
|
+
return { ok: false, message: `--verification-input '${verificationInput}' is not valid JSON: ${err.message}` };
|
|
1120
|
+
}
|
|
1121
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
1122
|
+
return { ok: false, message: `--verification-input '${verificationInput}' must be a JSON object.` };
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const ALLOWED_KEYS = new Set(['structuralChecks', 'requirements', 'originalContractPath', 'originalContractDigest']);
|
|
1126
|
+
for (const key of Object.keys(manifest)) {
|
|
1127
|
+
if (!ALLOWED_KEYS.has(key)) {
|
|
1128
|
+
return { ok: false, message: `--verification-input '${verificationInput}' contains unknown field '${key}'.` };
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const hasOrigPath = manifest.originalContractPath !== undefined;
|
|
1133
|
+
const hasOrigDigest = manifest.originalContractDigest !== undefined;
|
|
1134
|
+
if ((hasOrigPath && !hasOrigDigest) || (!hasOrigPath && hasOrigDigest)) {
|
|
1135
|
+
return { ok: false, message: `--verification-input '${verificationInput}' must specify both originalContractPath and originalContractDigest together.` };
|
|
1136
|
+
}
|
|
1137
|
+
if (hasOrigPath && hasOrigDigest) {
|
|
1138
|
+
if (typeof manifest.originalContractPath !== 'string' || manifest.originalContractPath.trim() === '') {
|
|
1139
|
+
return { ok: false, message: '--verification-input originalContractPath must be a non-empty string.' };
|
|
1140
|
+
}
|
|
1141
|
+
if (typeof manifest.originalContractDigest !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(manifest.originalContractDigest)) {
|
|
1142
|
+
return { ok: false, message: '--verification-input originalContractDigest must be in format sha256:<64 hex chars>.' };
|
|
1143
|
+
}
|
|
1144
|
+
const origAbs = path.isAbsolute(manifest.originalContractPath) ? manifest.originalContractPath : path.resolve(cwd, manifest.originalContractPath);
|
|
1145
|
+
let origBytes: Buffer;
|
|
1146
|
+
try {
|
|
1147
|
+
origBytes = fs.readFileSync(origAbs);
|
|
1148
|
+
} catch (err: any) {
|
|
1149
|
+
return { ok: false, message: `--verification-input originalContractPath '${manifest.originalContractPath}' could not be read: ${err.message}` };
|
|
1150
|
+
}
|
|
1151
|
+
const actualOrigDigest = 'sha256:' + crypto.createHash('sha256').update(origBytes).digest('hex');
|
|
1152
|
+
if (actualOrigDigest !== manifest.originalContractDigest) {
|
|
1153
|
+
return { ok: false, message: `--verification-input originalContractDigest mismatch for '${manifest.originalContractPath}': expected ${manifest.originalContractDigest}, got ${actualOrigDigest}.` };
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
let structuralChecks: any[] = [];
|
|
1158
|
+
if (manifest.structuralChecks !== undefined) {
|
|
1159
|
+
if (!Array.isArray(manifest.structuralChecks)) {
|
|
1160
|
+
return { ok: false, message: '--verification-input structuralChecks must be an array.' };
|
|
1161
|
+
}
|
|
1162
|
+
const seenNames = new Set<string>();
|
|
1163
|
+
const ALLOWED_STRUCT_KEYS = new Set(['name', 'command', 'timeoutMs']);
|
|
1164
|
+
for (let i = 0; i < manifest.structuralChecks.length; i++) {
|
|
1165
|
+
const entry = manifest.structuralChecks[i];
|
|
1166
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
1167
|
+
return { ok: false, message: `--verification-input structuralChecks[${i}] must be an object.` };
|
|
1168
|
+
}
|
|
1169
|
+
for (const k of Object.keys(entry)) {
|
|
1170
|
+
if (!ALLOWED_STRUCT_KEYS.has(k)) {
|
|
1171
|
+
return { ok: false, message: `--verification-input structuralChecks[${i}] contains unknown field '${k}'.` };
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
if (typeof entry.name !== 'string' || entry.name.trim() === '') {
|
|
1175
|
+
return { ok: false, message: `--verification-input structuralChecks[${i}].name must be a non-empty string.` };
|
|
1176
|
+
}
|
|
1177
|
+
if (seenNames.has(entry.name)) {
|
|
1178
|
+
return { ok: false, message: `--verification-input duplicate structural check name '${entry.name}'.` };
|
|
1179
|
+
}
|
|
1180
|
+
seenNames.add(entry.name);
|
|
1181
|
+
if (typeof entry.command !== 'string' || entry.command.trim() === '') {
|
|
1182
|
+
return { ok: false, message: `--verification-input structuralChecks[${i}].command must be a non-empty string.` };
|
|
1183
|
+
}
|
|
1184
|
+
if (entry.timeoutMs !== undefined && (!Number.isInteger(entry.timeoutMs) || entry.timeoutMs <= 0)) {
|
|
1185
|
+
return { ok: false, message: `--verification-input structuralChecks[${i}].timeoutMs must be a positive integer.` };
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
structuralChecks = manifest.structuralChecks;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
let requirements: any[] = [];
|
|
1192
|
+
if (manifest.requirements !== undefined) {
|
|
1193
|
+
if (!Array.isArray(manifest.requirements)) {
|
|
1194
|
+
return { ok: false, message: '--verification-input requirements must be an array.' };
|
|
1195
|
+
}
|
|
1196
|
+
const seenIds = new Set<string>();
|
|
1197
|
+
const ALLOWED_REQ_KEYS = new Set(['id', 'description', 'command', 'timeoutMs']);
|
|
1198
|
+
for (let i = 0; i < manifest.requirements.length; i++) {
|
|
1199
|
+
const entry = manifest.requirements[i];
|
|
1200
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
1201
|
+
return { ok: false, message: `--verification-input requirements[${i}] must be an object.` };
|
|
1202
|
+
}
|
|
1203
|
+
for (const k of Object.keys(entry)) {
|
|
1204
|
+
if (!ALLOWED_REQ_KEYS.has(k)) {
|
|
1205
|
+
return { ok: false, message: `--verification-input requirements[${i}] contains unknown field '${k}'.` };
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
if (typeof entry.id !== 'string' || entry.id.trim() === '') {
|
|
1209
|
+
return { ok: false, message: `--verification-input requirements[${i}].id must be a non-empty string.` };
|
|
1210
|
+
}
|
|
1211
|
+
if (seenIds.has(entry.id)) {
|
|
1212
|
+
return { ok: false, message: `--verification-input duplicate requirement id '${entry.id}'.` };
|
|
1213
|
+
}
|
|
1214
|
+
seenIds.add(entry.id);
|
|
1215
|
+
if (entry.description !== undefined && typeof entry.description !== 'string') {
|
|
1216
|
+
return { ok: false, message: `--verification-input requirements[${i}].description must be a string.` };
|
|
1217
|
+
}
|
|
1218
|
+
if (entry.command !== undefined && (typeof entry.command !== 'string' || entry.command.trim() === '')) {
|
|
1219
|
+
return { ok: false, message: `--verification-input requirements[${i}].command must be a non-empty string when provided.` };
|
|
1220
|
+
}
|
|
1221
|
+
if (entry.timeoutMs !== undefined && (!Number.isInteger(entry.timeoutMs) || entry.timeoutMs <= 0)) {
|
|
1222
|
+
return { ok: false, message: `--verification-input requirements[${i}].timeoutMs must be a positive integer.` };
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
requirements = manifest.requirements;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const provenance = {
|
|
1229
|
+
manifestPath: verificationInput,
|
|
1230
|
+
manifestDigest,
|
|
1231
|
+
...(hasOrigPath ? {
|
|
1232
|
+
originalContractPath: manifest.originalContractPath,
|
|
1233
|
+
originalContractDigest: manifest.originalContractDigest,
|
|
1234
|
+
} : {}),
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
return { ok: true, structuralChecks, requirements, provenance };
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Handles `doflow verify` — compile the verification contract (FR-009) and, by default, run it and
|
|
1242
|
+
* report against it.
|
|
1243
|
+
*
|
|
1244
|
+
* `--action contract` stops after compiling, which is the before-implementation half: it states
|
|
1245
|
+
* how success will be established without establishing anything. The default runs the checks and
|
|
1246
|
+
* emits the report. The contract is recompiled rather than read back from a state file, so the two
|
|
1247
|
+
* halves cannot describe different contracts.
|
|
1248
|
+
*
|
|
1249
|
+
* @param {Object} options
|
|
1250
|
+
* @param {string} options.taskId
|
|
1251
|
+
* @param {'report'|'contract'|'status'} [options.action='report']
|
|
1252
|
+
* @param {string} [options.risk] risk level; the registry's default when omitted
|
|
1253
|
+
* @param {string} [options.planPath] a `plan.md` whose command override beats detection
|
|
1254
|
+
* @param {boolean} [options.json=false]
|
|
1255
|
+
* @param {string} [options.projectRoot]
|
|
1256
|
+
* @param {string} [options.slug] the feature whose plan bounds the change; the branch's when omitted
|
|
1257
|
+
* @param {string} [options.scope] a declared scope (`<path>[,<path>...]`), unioned with any plan bound
|
|
1258
|
+
* @param {string} [options.verificationInput] path to input manifest with structuralChecks and requirements (IC-022)
|
|
1259
|
+
* @param {string} [options.verificationPhase] explicit compilation phase (e.g. postimplementation) (IC-022)
|
|
1260
|
+
* @param {string} [options.baselineRef] commit or ref to measure change against (IC-022)
|
|
1261
|
+
* @returns {number} exit code
|
|
1262
|
+
*/
|
|
1263
|
+
function handleVerifyCommand({ taskId, action = 'report', risk, planPath, json = false, projectRoot, slug = null, scope = null, verificationInput = null, verificationPhase = null, baselineRef = null }: {
|
|
1264
|
+
taskId?: any;
|
|
1265
|
+
action?: string;
|
|
1266
|
+
risk?: any;
|
|
1267
|
+
planPath?: any;
|
|
1268
|
+
json?: boolean;
|
|
1269
|
+
projectRoot?: any;
|
|
1270
|
+
slug?: string | null;
|
|
1271
|
+
scope?: any;
|
|
1272
|
+
verificationInput?: string | null;
|
|
1273
|
+
verificationPhase?: string | null;
|
|
1274
|
+
baselineRef?: string | null;
|
|
1275
|
+
} = {}): number {
|
|
1276
|
+
// The task id names state files here, so it obeys the rule every other task verb applies.
|
|
1277
|
+
try {
|
|
1278
|
+
assertSafeTaskId(taskId);
|
|
1279
|
+
} catch (error: any) {
|
|
1280
|
+
return usageError('verify', error.message, json);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (verificationPhase !== null && verificationPhase !== undefined) {
|
|
1284
|
+
if (verificationPhase !== 'postimplementation' && verificationPhase !== 'unknown') {
|
|
1285
|
+
return usageError('verify', `unknown --verification-phase '${verificationPhase}'. Valid: postimplementation`, json);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
let declared: any = null;
|
|
1290
|
+
if (scope !== null && scope !== undefined) {
|
|
1291
|
+
const parsed = parseDeclaredScope(typeof scope === 'string' ? scope : '');
|
|
1292
|
+
if (parsed.reason) {
|
|
1293
|
+
return usageError('verify', `--scope ${parsed.reason}. Write repository-relative paths separated by commas, a trailing / for a directory, for example --scope src/a.js,src/lib/. Nothing was changed.`, json);
|
|
1294
|
+
}
|
|
1295
|
+
declared = { paths: parsed.paths, origin: 'verify-flag' };
|
|
1296
|
+
}
|
|
1297
|
+
const cwd = projectRoot || process.cwd();
|
|
1298
|
+
|
|
1299
|
+
let structuralChecks: any[] = [];
|
|
1300
|
+
let requirements: any[] = [];
|
|
1301
|
+
let inputProvenance: any = null;
|
|
1302
|
+
if (verificationInput !== null && verificationInput !== undefined) {
|
|
1303
|
+
const val = validateVerificationInput({ cwd, verificationInput });
|
|
1304
|
+
if (!val.ok) {
|
|
1305
|
+
return usageError('verify', val.message, json);
|
|
1306
|
+
}
|
|
1307
|
+
structuralChecks = val.structuralChecks;
|
|
1308
|
+
requirements = val.requirements;
|
|
1309
|
+
inputProvenance = val.provenance;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// The feature the task belongs to, for the readiness record's namespace and for a run keyed by
|
|
1313
|
+
// the feature slug: `--slug`, else the task id's own folder, else the branch's.
|
|
1314
|
+
const featureSlug = featureSlugFor({ projectRoot: cwd, taskId, slug });
|
|
1315
|
+
|
|
1316
|
+
// A scope is declared in one place. The readiness record carries the one stated before the
|
|
1317
|
+
// work; a flag that says something else is refused rather than silently preferred. Records in two
|
|
1318
|
+
// other checkouts declare nothing: the contract says so and still answers.
|
|
1319
|
+
const recorded = readReadinessRecord({ stateRoot: cwd, taskId, slug: featureSlug });
|
|
1320
|
+
const recordNote = recorded.status === 'ambiguous'
|
|
1321
|
+
? `readiness record: not used; task '${taskId}' has records in more than one other checkout (${recorded.candidates.join(', ')})`
|
|
1322
|
+
: null;
|
|
1323
|
+
// Only a record this runtime can read declares a scope; one it cannot read is reported by the
|
|
1324
|
+
// readiness check, and refusing --scope against it would send the caller to a readiness run that
|
|
1325
|
+
// cannot replace it.
|
|
1326
|
+
const usable = recorded.status === 'found' && recorded.record && recorded.record.version === RECORD_VERSION;
|
|
1327
|
+
const recordedScope = usable && Array.isArray(recorded.record.declaredScope) && recorded.record.declaredScope.length > 0
|
|
1328
|
+
? recorded.record.declaredScope : null;
|
|
1329
|
+
if (recordedScope) {
|
|
1330
|
+
const recordRel = recorded.origin === 'current' ? path.relative(cwd, recorded.file) : recorded.file;
|
|
1331
|
+
if (declared && !sameSet(declared.paths, recordedScope)) {
|
|
1332
|
+
return usageError('verify', `--scope ${String(scope).trim()} differs from the scope declared in the readiness record ${recordRel} (${recordedScope.join(',')}). A scope is declared in one place: re-run doflow-run readiness --task-class ${recorded.record.taskClass} --task-id ${taskId} --scope ${String(scope).trim()}, or drop --scope. Nothing was changed.`, json);
|
|
1333
|
+
}
|
|
1334
|
+
if (!declared) declared = { paths: [...recordedScope], origin: 'readiness-record', record: recordRel };
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
let engine: any;
|
|
1338
|
+
let contract: any;
|
|
1339
|
+
try {
|
|
1340
|
+
engine = new VerificationEngine({ cwd, repoRoot: REPO_ROOT });
|
|
1341
|
+
// The bound comes from the feature's plan, this checkout's or the main checkout's, and from a
|
|
1342
|
+
// declared scope; with neither the change-scope tier is UNRESOLVED and says where it looked.
|
|
1343
|
+
const resolved = resolveScopeBound({ projectRoot: cwd, slug, planPath, declared, baselineRef });
|
|
1344
|
+
if (recordNote) resolved.searched.push({ place: 'readiness record', result: recordNote.slice('readiness record: '.length) });
|
|
1345
|
+
const scopeInput = resolved.bound
|
|
1346
|
+
? { ...resolved.bound, searched: resolved.searched }
|
|
1347
|
+
: { unresolvedReason: resolved.unresolvedReason || scopeReasonText({ reason: resolved.reason, taskId, planSource: resolved.planSource }), searched: resolved.searched };
|
|
1348
|
+
contract = engine.compileContract({
|
|
1349
|
+
taskId,
|
|
1350
|
+
riskLevel: risk,
|
|
1351
|
+
projectRoot: cwd,
|
|
1352
|
+
planPath,
|
|
1353
|
+
scope: scopeInput,
|
|
1354
|
+
phase: verificationPhase,
|
|
1355
|
+
structuralChecks,
|
|
1356
|
+
requirements,
|
|
1357
|
+
provenance: inputProvenance,
|
|
1358
|
+
});
|
|
1359
|
+
} catch (error: any) {
|
|
1360
|
+
return usageError('verify', error.message, json, error);
|
|
1361
|
+
}
|
|
1362
|
+
// The bound's sources print under the change-scope tier's line; no other line changes.
|
|
1363
|
+
const boundLines = new Map(contract.tiers
|
|
1364
|
+
.filter((t: any) => t.bound && Array.isArray(t.bound.sources) && t.bound.sources.length > 0)
|
|
1365
|
+
.map((t: any) => [t.id, ` bound: ${boundSourcesText(t.bound.sources)}`]));
|
|
1366
|
+
if (recordNote) {
|
|
1367
|
+
const scopeTier = contract.tiers.find((t: any) => t.kind === 'scope');
|
|
1368
|
+
if (scopeTier) boundLines.set(scopeTier.id, [boundLines.get(scopeTier.id), ` ${recordNote}`].filter(Boolean).join('\n'));
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if (action === 'contract') {
|
|
1372
|
+
if (json) console.log(JSON.stringify(contract, null, 2));
|
|
1373
|
+
else {
|
|
1374
|
+
console.log(`\nDoFlow Verification Contract [${contract.taskId}] — risk ${contract.riskLevel}:`);
|
|
1375
|
+
console.log('═'.repeat(78));
|
|
1376
|
+
for (const tier of contract.tiers) {
|
|
1377
|
+
console.log(` ${tier.id.padEnd(22)} ${tier.resolution.padEnd(12)} ${tier.required ? 'required' : 'advisory'}`);
|
|
1378
|
+
if (boundLines.has(tier.id)) console.log(boundLines.get(tier.id));
|
|
1379
|
+
if (tier.reason) console.log(` ${tier.reason}`);
|
|
1380
|
+
if (tier.selection) console.log(` ${selectionText(tier.selection)}`);
|
|
1381
|
+
}
|
|
1382
|
+
console.log('═'.repeat(78) + '\n');
|
|
1383
|
+
}
|
|
1384
|
+
// A contract is a plan, not a verdict — compiling one always answers, even when a tier could
|
|
1385
|
+
// not be resolved, because the unresolved tier is itself part of what the contract states.
|
|
1386
|
+
return finishRuntime(0);
|
|
1387
|
+
}
|
|
1388
|
+
if (action !== 'report' && action !== 'status') {
|
|
1389
|
+
return usageError('verify', `unknown --action '${action}'. Valid: report (default), contract`, json);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
const report = engine.runContract(contract);
|
|
1393
|
+
// A task whose run still has its gated stage pending is not verified while no READY readiness
|
|
1394
|
+
// record exists: the checks passing says nothing about whether the work should have started.
|
|
1395
|
+
try {
|
|
1396
|
+
report.readiness = verifyReadinessCheck({ stateRoot: cwd, taskId, slug: featureSlug });
|
|
1397
|
+
} catch (error: any) {
|
|
1398
|
+
return usageError('verify', error.message, json, error);
|
|
1399
|
+
}
|
|
1400
|
+
const readinessHeld = report.readiness.applies && !report.readiness.ok;
|
|
1401
|
+
if (readinessHeld && report.status === 'PASS') {
|
|
1402
|
+
report.status = 'INCONCLUSIVE';
|
|
1403
|
+
report.reason = report.readiness.message;
|
|
1404
|
+
}
|
|
1405
|
+
if (json) console.log(JSON.stringify(report, null, 2));
|
|
1406
|
+
else {
|
|
1407
|
+
console.log(`\nDoFlow Verification Report [${report.taskId}] — ${report.status}:`);
|
|
1408
|
+
console.log('═'.repeat(78));
|
|
1409
|
+
console.log(report.reason);
|
|
1410
|
+
if (readinessHeld) console.log(`readiness: ${report.readiness.message}`);
|
|
1411
|
+
console.log('─'.repeat(78));
|
|
1412
|
+
for (const tier of report.tiers) {
|
|
1413
|
+
console.log(` ${tier.id.padEnd(22)} ${tier.status.padEnd(16)} ${tier.required ? 'required' : 'advisory'}`);
|
|
1414
|
+
if (boundLines.has(tier.id)) console.log(boundLines.get(tier.id));
|
|
1415
|
+
if (tier.reason) console.log(` ${tier.reason}`);
|
|
1416
|
+
if (tier.selection) console.log(` ${selectionText(tier.selection)}`);
|
|
1417
|
+
}
|
|
1418
|
+
console.log('─'.repeat(78));
|
|
1419
|
+
console.log(`Checks: ${report.counts.checksPassed}/${report.counts.checksRun} passed`);
|
|
1420
|
+
if (report.unresolved.length) {
|
|
1421
|
+
console.log('Never evaluated (not a pass):');
|
|
1422
|
+
for (const u of report.unresolved) console.log(` ${u.tier}${u.required ? ' [required]' : ''} — ${u.reason}`);
|
|
1423
|
+
}
|
|
1424
|
+
console.log('═'.repeat(78) + '\n');
|
|
1425
|
+
}
|
|
1426
|
+
// PASS is the only status that answers "verified". FAIL and INCONCLUSIVE are both findings, and
|
|
1427
|
+
// collapsing INCONCLUSIVE into success would report a verdict over zero evidence as a pass.
|
|
1428
|
+
upkeepAfterVerify({ projectRoot: cwd });
|
|
1429
|
+
return finishRuntime(report.status === 'PASS' ? 0 : 1);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/** One line stating what the targeted tier selected, or why it ran the broad command instead. */
|
|
1433
|
+
function selectionText(selection: any): string {
|
|
1434
|
+
const of = selection.totalCount === null ? '' : ` of ${selection.totalCount}`;
|
|
1435
|
+
if (selection.fallback) return `selection: ${selection.basis}, fell back to the broad command (${selection.fallback})`;
|
|
1436
|
+
if (selection.selectedCount === null) return `selection: ${selection.basis}`;
|
|
1437
|
+
return `selection: ${selection.basis}, ${selection.selectedCount}${of} test files`;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
/** Whether two path lists name the same set. */
|
|
1441
|
+
function sameSet(a: string[], b: string[]): boolean {
|
|
1442
|
+
const left = new Set(a);
|
|
1443
|
+
const right = new Set(b);
|
|
1444
|
+
return left.size === right.size && [...left].every((p) => right.has(p));
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
module.exports = {
|
|
1448
|
+
VerificationContractRunner,
|
|
1449
|
+
VerificationEngine,
|
|
1450
|
+
loadVerificationRegistry,
|
|
1451
|
+
VERIFICATION_STATUSES,
|
|
1452
|
+
TIER_RESOLUTIONS,
|
|
1453
|
+
TIER_STATUSES,
|
|
1454
|
+
RECOVERY_OUTCOMES,
|
|
1455
|
+
FATAL_CHECK_MARKERS,
|
|
1456
|
+
TIMEOUT_EXIT_CODE,
|
|
1457
|
+
MAX_STREAM_CHARS,
|
|
1458
|
+
REGISTRY_FILENAME,
|
|
1459
|
+
handleVerifyCommand,
|
|
1460
|
+
validateVerificationInput,
|
|
1461
|
+
};
|