@khoavu882/doflow 0.0.0-stage → 2.0.0-beta.4
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 +1061 -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 +48 -4
- package/src/adapters/antigravity/index.js +631 -0
- package/src/adapters/claude/index.js +548 -0
- package/src/adapters/claude/mcp.js +94 -0
- package/src/adapters/codex/agents.js +208 -0
- package/src/adapters/codex/config.js +272 -0
- package/src/adapters/codex/hooks.js +203 -0
- package/src/adapters/codex/index.js +707 -0
- package/src/adapters/codex/mcp.js +271 -0
- package/src/adapters/codex/native-projection.js +34 -0
- package/src/adapters/copilot/index.js +413 -0
- package/src/adapters/copy-tree.js +620 -0
- package/src/adapters/gemini/hooks.js +210 -0
- package/src/adapters/gemini/index.js +390 -0
- package/src/adapters/hook-commands.js +60 -0
- package/src/adapters/index.js +106 -0
- package/src/adapters/kiro/index.js +310 -0
- package/src/adapters/mcp-entries.js +331 -0
- package/src/adapters/opencode/index.js +466 -0
- package/src/adapters/pi/index.js +279 -0
- package/src/adapters/pi/json-members.js +185 -0
- package/src/adapters/pi/mcp.js +373 -0
- package/src/adapters/resolve-source.js +19 -0
- package/src/cli/commands/install.js +218 -0
- package/src/cli/commands/list-backups.js +21 -0
- package/src/cli/commands/reconcile.js +120 -0
- package/src/cli/commands/remove.js +82 -0
- package/src/cli/commands/rollback.js +172 -0
- package/src/cli/commands/self-update.js +33 -0
- package/src/cli/commands/status.js +138 -0
- package/src/cli/commands/tools.js +92 -0
- package/src/cli/commands/update.js +162 -0
- package/src/cli/index.js +568 -0
- package/src/cli/runtime-commands.js +281 -0
- package/src/cli/shared.js +218 -0
- package/src/helper/bash-helper.js +34 -0
- package/src/helper/git.js +24 -0
- package/src/helper/guardrails.js +49 -0
- package/src/helper/harness-paths.js +140 -0
- package/src/helper/marker-merge.js +116 -0
- package/src/helper/prompt.js +61 -0
- package/src/helper/repo-root.js +29 -0
- package/src/helper/settings-merge.js +151 -0
- package/src/helper/settings-scope.js +33 -0
- package/src/helper/toml.js +244 -0
- package/src/install/backup-set.js +149 -0
- package/src/install/backup.js +724 -0
- package/src/install/context.js +58 -0
- package/src/install/manifest.js +145 -0
- package/src/install/mcp.js +254 -0
- package/src/install/paths.js +84 -0
- package/src/install/targets.js +90 -0
- package/src/install/tool-lifecycle.js +151 -0
- package/src/lifecycle/bash-availability.js +25 -0
- package/src/lifecycle/index.js +812 -0
- package/src/lifecycle/mcp-index.js +28 -0
- package/src/lifecycle/policies.js +73 -0
- package/src/lifecycle/view.js +175 -0
- package/src/registry/index.js +611 -0
- package/src/release/build-package.js +91 -0
- package/src/release/changelog-section.js +70 -0
- package/src/release/sync-plugin-versions.js +66 -0
- package/src/runtime/advice/cli.js +283 -0
- package/src/runtime/advice/client.js +324 -0
- package/src/runtime/advice/config.js +189 -0
- package/src/runtime/advice/doctor.js +169 -0
- package/src/runtime/advice/endpoint-guard.js +134 -0
- package/src/runtime/advice/kinds.js +154 -0
- package/src/runtime/advice/providers/ollama-systemone.js +209 -0
- package/src/runtime/advice/record.js +166 -0
- package/src/runtime/advice/registry.js +170 -0
- package/src/runtime/advice/strength.js +66 -0
- package/src/runtime/brief-tools.js +342 -0
- package/src/runtime/c4-project.js +371 -0
- package/src/runtime/capability-router.js +484 -0
- package/src/runtime/checkouts.js +170 -0
- package/src/runtime/claims.js +468 -0
- package/src/runtime/cli-result.js +55 -0
- package/src/runtime/cli.js +678 -0
- package/src/runtime/command-detect.js +516 -0
- package/src/runtime/context-pack.js +492 -0
- package/src/runtime/decision-register.js +493 -0
- package/src/runtime/evidence-ledger.js +338 -0
- package/src/runtime/failure/capture.js +272 -0
- package/src/runtime/failure/classifier.js +68 -0
- package/src/runtime/failure/cli.js +178 -0
- package/src/runtime/failure/home.js +64 -0
- package/src/runtime/failure/store.js +226 -0
- package/src/runtime/feature-resolve.js +79 -0
- package/src/runtime/freshness.js +177 -0
- package/src/runtime/health.js +863 -0
- package/src/runtime/history-compactor.js +237 -0
- package/src/runtime/implementation-gate.js +321 -0
- package/src/runtime/inventory/coverage.js +218 -0
- package/src/runtime/inventory/identity.js +65 -0
- package/src/runtime/inventory/index.js +705 -0
- package/src/runtime/inventory/precedence.js +134 -0
- package/src/runtime/inventory/read-scopes.js +131 -0
- package/src/runtime/inventory/siblings.js +192 -0
- package/src/runtime/knowledge/chunker.js +124 -0
- package/src/runtime/knowledge/hybrid.js +149 -0
- package/src/runtime/knowledge/index-store.js +145 -0
- package/src/runtime/knowledge/retrieval.js +194 -0
- package/src/runtime/leak-scan.js +185 -0
- package/src/runtime/lifecycle/cli.js +276 -0
- package/src/runtime/lifecycle/event-store.js +340 -0
- package/src/runtime/lifecycle/fold.js +353 -0
- package/src/runtime/lifecycle/followup.js +432 -0
- package/src/runtime/lifecycle/goal.js +249 -0
- package/src/runtime/lifecycle/intent-writer.js +86 -0
- package/src/runtime/lifecycle/overview.js +292 -0
- package/src/runtime/lifecycle/release.js +241 -0
- package/src/runtime/lifecycle/report-store.js +221 -0
- package/src/runtime/lifecycle/retention.js +212 -0
- package/src/runtime/lifecycle/root.js +71 -0
- package/src/runtime/lifecycle/status.js +353 -0
- package/src/runtime/lifecycle/store-upkeep.js +393 -0
- package/src/runtime/locator-resolve.js +158 -0
- package/src/runtime/mask.js +210 -0
- package/src/runtime/mcp-consultations.js +412 -0
- package/src/runtime/mcp-reach.js +157 -0
- package/src/runtime/model-router.js +153 -0
- package/src/runtime/outcome.js +467 -0
- package/src/runtime/reach.js +148 -0
- package/src/runtime/readiness-record.js +150 -0
- package/src/runtime/readiness.js +435 -0
- package/src/runtime/recovery.js +282 -0
- package/src/runtime/research-request.js +231 -0
- package/src/runtime/retrieval-plan.js +578 -0
- package/src/runtime/scaffold/artifacts.js +217 -0
- package/src/runtime/scaffold/fingerprint.js +56 -0
- package/src/runtime/scaffold/generate.js +911 -0
- package/src/runtime/scaffold/languages.js +245 -0
- package/src/runtime/storage/cli.js +64 -0
- package/src/runtime/storage/engine.js +249 -0
- package/src/runtime/storage/registry.js +130 -0
- package/src/runtime/storage/roots.js +63 -0
- package/src/runtime/storage/rules.js +282 -0
- package/src/runtime/storage/upkeep.js +243 -0
- package/src/runtime/storage/work-status.js +326 -0
- package/src/runtime/task-attempts.js +415 -0
- package/src/runtime/task-classifier.js +550 -0
- package/src/runtime/task-scope.js +130 -0
- package/src/runtime/task-state.js +176 -0
- package/src/runtime/trace/indicators.js +235 -0
- package/src/runtime/trace/ledger.js +626 -0
- package/src/runtime/trace/render.js +76 -0
- package/src/runtime/trace/views.js +459 -0
- package/src/runtime/verification/contract-runner.js +341 -0
- package/src/runtime/verification/engine.js +1461 -0
- package/src/runtime/verification/registry.js +130 -0
- package/src/runtime/verification/scope-bound.js +418 -0
- package/src/runtime/verification/test-selection.js +172 -0
- package/src/runtime/workflow-engine.js +588 -0
- package/src/runtime/workflow-orchestrator.js +953 -0
- package/src/runtime/worktree.js +340 -0
- package/src/state/index.js +193 -0
- package/src/state/lockfile.js +136 -0
- package/src/state/run-lock.js +408 -0
- package/types/node-commonjs.d.ts +68 -0
- package/types/runtime.d.ts +91 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Doc Quality Checker
|
|
4
|
+
|
|
5
|
+
Analyzes markdown/prose content (SKILL.md files, agent specs, guidance-tree
|
|
6
|
+
references) for structural health issues: skill-creator's progressive-disclosure
|
|
7
|
+
length guideline, presence of an explicit Boundaries (Will / Will Not) section,
|
|
8
|
+
triggering-description strength, dangling references/modes/ cross-references, and
|
|
9
|
+
staleness markers.
|
|
10
|
+
|
|
11
|
+
This is deliberately NOT a code-smell checker: markdown/prose has no "language" in
|
|
12
|
+
the sense `code_quality_checker.py`'s LANGUAGE_EXTENSIONS assumes, so there is no
|
|
13
|
+
complexity scoring, no SOLID-violation detection, and no aggregate quality score —
|
|
14
|
+
per this repo's Professional Honesty principle, inventing a single quality number
|
|
15
|
+
for prose would claim more precision than a pattern match can support. Every
|
|
16
|
+
finding here is deterministic and cite-the-match: it names the exact line, phrase,
|
|
17
|
+
or reference that triggered it.
|
|
18
|
+
|
|
19
|
+
See `core/shared/skills/do-code-review/content-types/markdown.md` for the full
|
|
20
|
+
rationale behind each check.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
python doc_quality_checker.py /path/to/SKILL.md
|
|
24
|
+
python doc_quality_checker.py /path/to/directory
|
|
25
|
+
python doc_quality_checker.py . --json
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import re
|
|
32
|
+
import sys
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Dict, List, Optional, Tuple
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Structural thresholds, named the same way code_quality_checker.py's THRESHOLDS dict is.
|
|
38
|
+
THRESHOLDS = {
|
|
39
|
+
"skill_md_max_lines": 500,
|
|
40
|
+
"description_min_words": 40,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def read_file_content(filepath: Path) -> str:
|
|
45
|
+
"""Read file content safely."""
|
|
46
|
+
try:
|
|
47
|
+
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
|
|
48
|
+
return f.read()
|
|
49
|
+
except Exception:
|
|
50
|
+
return ""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def display_path(path: Path) -> str:
|
|
54
|
+
"""The form a path takes in emitted output: relative to the working directory.
|
|
55
|
+
|
|
56
|
+
Mirrors code_quality_checker.py's own `display_path` exactly, so findings from both
|
|
57
|
+
checkers can be merged by review_report_generator.py without reconciling two path
|
|
58
|
+
conventions. os.path.relpath rather than Path.relative_to: the latter raises ValueError
|
|
59
|
+
for a target outside the working directory, which is a supported input here.
|
|
60
|
+
"""
|
|
61
|
+
return os.path.relpath(str(path), os.getcwd())
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_skill_md(path: Path) -> bool:
|
|
65
|
+
"""True for any `.../skills/<skill-name>/SKILL.md` — matches the dispatch pattern
|
|
66
|
+
`*/skills/*/SKILL.md`, regardless of what precedes `skills/` in the path."""
|
|
67
|
+
return path.name == "SKILL.md" and path.parent.parent.name == "skills"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_TOP_LEVEL_FRONTMATTER_KEY_RE = re.compile(r"^([A-Za-z][A-Za-z0-9_-]*)\s*:")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def frontmatter_keys(lines: List[str]) -> List[str]:
|
|
74
|
+
"""Top-level YAML frontmatter keys only — values are irrelevant here, and parsing them
|
|
75
|
+
would take on a YAML dependency this repo deliberately does not have (mirrors
|
|
76
|
+
test/guards/_shared.js's frontmatterKeys(), same reasoning)."""
|
|
77
|
+
if not lines or lines[0].strip() != "---":
|
|
78
|
+
return []
|
|
79
|
+
keys = []
|
|
80
|
+
for line in lines[1:]:
|
|
81
|
+
if line.strip() == "---":
|
|
82
|
+
break
|
|
83
|
+
if line[:1].isspace() or not line.strip():
|
|
84
|
+
continue # nested value, not a top-level key
|
|
85
|
+
m = _TOP_LEVEL_FRONTMATTER_KEY_RE.match(line)
|
|
86
|
+
if m:
|
|
87
|
+
keys.append(m.group(1))
|
|
88
|
+
return keys
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def is_agent_spec(path: Path, lines: Optional[List[str]] = None) -> bool:
|
|
92
|
+
"""True for a specialist agent-spec file. `agent-specs` is this repo's own SOURCE-TREE
|
|
93
|
+
directory name — every harness projects it to a differently-named directory on install
|
|
94
|
+
(`agents/` for claude/codex/gemini/copilot/kiro; never `agent-specs/`), so a real installed
|
|
95
|
+
copy would silently never match a directory-name-only check. Since DoFlow is designed to be
|
|
96
|
+
installed anywhere, this checks the installed-shape name too, but path name alone is too
|
|
97
|
+
generic (an unrelated project can have its own unrelated `agents/` folder) — content shape
|
|
98
|
+
is the tie-breaker: an agent-spec's frontmatter has `model:` and either `tools:` (the legacy
|
|
99
|
+
shape) or `effort:` (the current shared shape), but never `argument-hint:` (that key belongs
|
|
100
|
+
to a skill's own invocation contract, never an agent's). Its `tools:` field is optional in
|
|
101
|
+
the current shape; omitting it lets the host provide the agent's available tools."""
|
|
102
|
+
if path.suffix.lower() != ".md" or path.parent.name not in ("agent-specs", "agents"):
|
|
103
|
+
return False
|
|
104
|
+
if lines is None:
|
|
105
|
+
lines = read_file_content(path).splitlines()
|
|
106
|
+
keys = set(frontmatter_keys(lines))
|
|
107
|
+
return (
|
|
108
|
+
"model" in keys
|
|
109
|
+
and ("tools" in keys or "effort" in keys)
|
|
110
|
+
and "argument-hint" not in keys
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def find_guidance_dir(start: Path) -> Optional[Path]:
|
|
115
|
+
"""Walk up from `start` (a file or directory) looking for an ancestor that contains
|
|
116
|
+
`core/shared/guidance`. Self-contained — does not depend on the caller's cwd, so this
|
|
117
|
+
check works standalone on a single file passed from anywhere on disk."""
|
|
118
|
+
for parent in [start if start.is_dir() else start.parent, *start.parents]:
|
|
119
|
+
candidate = parent / "core" / "shared" / "guidance"
|
|
120
|
+
if candidate.is_dir():
|
|
121
|
+
return candidate
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def make_finding(path: Path, line: int, rule: str, severity: str, message: str) -> Dict:
|
|
126
|
+
"""One finding, shaped `{file, line, rule, message}` at minimum (plus `severity`, the
|
|
127
|
+
same triage field code_quality_checker.py's `smells` already carry) — the schema
|
|
128
|
+
`review_report_generator.py` can consume from either checker without a new aggregation
|
|
129
|
+
path. `line` is 0 for a file-level finding with no single matching line (an absence,
|
|
130
|
+
like a missing section) — the same "no specific line" sentinel
|
|
131
|
+
review_report_generator.py's own `issue.get("line", 0)` already treats as the default.
|
|
132
|
+
"""
|
|
133
|
+
return {
|
|
134
|
+
"file": display_path(path),
|
|
135
|
+
"line": line,
|
|
136
|
+
"rule": rule,
|
|
137
|
+
"severity": severity,
|
|
138
|
+
"message": message,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# --------------------------------------------------------------------------------------
|
|
143
|
+
# Check 1: SKILL.md length vs. skill-creator's progressive-disclosure guideline
|
|
144
|
+
# --------------------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def check_length(path: Path, lines: List[str]) -> List[Dict]:
|
|
147
|
+
if not is_skill_md(path):
|
|
148
|
+
return []
|
|
149
|
+
total = len(lines)
|
|
150
|
+
# A file ending in a single trailing newline splits into one extra empty element —
|
|
151
|
+
# that is not a line of content, so it should not count toward the guideline.
|
|
152
|
+
if total and lines[-1] == "":
|
|
153
|
+
total -= 1
|
|
154
|
+
limit = THRESHOLDS["skill_md_max_lines"]
|
|
155
|
+
if total <= limit:
|
|
156
|
+
return []
|
|
157
|
+
return [make_finding(
|
|
158
|
+
path, total, "skill_md_length", "medium",
|
|
159
|
+
f"SKILL.md has {total} lines, exceeding skill-creator's ~{limit}-line "
|
|
160
|
+
"progressive-disclosure guideline — split overflow into a references/ file so this "
|
|
161
|
+
"stays a dispatch/overview surface."
|
|
162
|
+
)]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# --------------------------------------------------------------------------------------
|
|
166
|
+
# Check 2: Boundaries (Will / Will Not) section, for SKILL.md and agent-spec files
|
|
167
|
+
# --------------------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
_BOUNDARIES_HEADING_RE = re.compile(r'^#{1,6}\s*Boundaries\b', re.IGNORECASE)
|
|
170
|
+
_HEADING_RE = re.compile(r'^#{1,6}\s')
|
|
171
|
+
_WILL_RE = re.compile(r'\*\*Will:?\*\*', re.IGNORECASE)
|
|
172
|
+
_WILL_NOT_RE = re.compile(r'\*\*Will Not:?\*\*', re.IGNORECASE)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _find_boundaries_section(lines: List[str]) -> Tuple[Optional[int], str]:
|
|
176
|
+
"""Return (0-based heading line index or None, section text from the heading to the
|
|
177
|
+
next heading or EOF). section text is "" when no heading is found."""
|
|
178
|
+
heading_idx = None
|
|
179
|
+
for i, line in enumerate(lines):
|
|
180
|
+
if _BOUNDARIES_HEADING_RE.match(line.strip()):
|
|
181
|
+
heading_idx = i
|
|
182
|
+
break
|
|
183
|
+
if heading_idx is None:
|
|
184
|
+
return None, ""
|
|
185
|
+
end_idx = len(lines)
|
|
186
|
+
for j in range(heading_idx + 1, len(lines)):
|
|
187
|
+
if _HEADING_RE.match(lines[j]):
|
|
188
|
+
end_idx = j
|
|
189
|
+
break
|
|
190
|
+
return heading_idx, "\n".join(lines[heading_idx:end_idx])
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def check_boundaries(path: Path, lines: List[str]) -> List[Dict]:
|
|
194
|
+
if not (is_skill_md(path) or is_agent_spec(path, lines)):
|
|
195
|
+
return []
|
|
196
|
+
|
|
197
|
+
heading_idx, section_text = _find_boundaries_section(lines)
|
|
198
|
+
|
|
199
|
+
if heading_idx is not None:
|
|
200
|
+
if _WILL_RE.search(section_text) and _WILL_NOT_RE.search(section_text):
|
|
201
|
+
return []
|
|
202
|
+
return [make_finding(
|
|
203
|
+
path, heading_idx + 1, "missing_boundaries", "medium",
|
|
204
|
+
"Has a '## Boundaries' heading but no explicit '**Will:**' / '**Will Not:**' "
|
|
205
|
+
"framing within it — scope is headed but not actually stated."
|
|
206
|
+
)]
|
|
207
|
+
|
|
208
|
+
# No heading at all — an equivalent Will/Will-Not framing elsewhere in the file still
|
|
209
|
+
# counts, per this check's own "or equivalent framing" allowance.
|
|
210
|
+
full_text = "\n".join(lines)
|
|
211
|
+
if _WILL_RE.search(full_text) and _WILL_NOT_RE.search(full_text):
|
|
212
|
+
return []
|
|
213
|
+
return [make_finding(
|
|
214
|
+
path, 0, "missing_boundaries", "medium",
|
|
215
|
+
"No '## Boundaries' heading or equivalent '**Will:**' / '**Will Not:**' framing "
|
|
216
|
+
"found — this file's scope is not explicit."
|
|
217
|
+
)]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# --------------------------------------------------------------------------------------
|
|
221
|
+
# Check 3: triggering description strength, SKILL.md frontmatter only
|
|
222
|
+
# --------------------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
# Same shape as G8's own `argument-hint` extraction regex (test/guards/_shared.js), applied
|
|
225
|
+
# to `description:` instead — an optional wrapping quote, non-greedy body, trailing quote.
|
|
226
|
+
_FRONTMATTER_DESCRIPTION_RE = re.compile(r'^description:\s*"?(.*?)"?\s*$')
|
|
227
|
+
|
|
228
|
+
_TRIGGER_SIGNAL_RE = re.compile(
|
|
229
|
+
r"\buse\s+(when|whenever|if)\b|\bfor\s+example\b|\be\.g\.|['\"][^'\"]{6,}['\"]",
|
|
230
|
+
re.IGNORECASE,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _parse_frontmatter_description(lines: List[str]) -> Optional[Dict]:
|
|
235
|
+
"""Return {"description": str, "line": 1-based line number} if the file opens with a
|
|
236
|
+
YAML frontmatter block (`---` ... `---`) containing a `description:` field, else None."""
|
|
237
|
+
if not lines or lines[0].strip() != "---":
|
|
238
|
+
return None
|
|
239
|
+
closing = None
|
|
240
|
+
for i in range(1, len(lines)):
|
|
241
|
+
if lines[i].strip() == "---":
|
|
242
|
+
closing = i
|
|
243
|
+
break
|
|
244
|
+
if closing is None:
|
|
245
|
+
return None
|
|
246
|
+
for i in range(1, closing):
|
|
247
|
+
m = _FRONTMATTER_DESCRIPTION_RE.match(lines[i])
|
|
248
|
+
if m:
|
|
249
|
+
return {"description": m.group(1), "line": i + 1}
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def check_description(path: Path, lines: List[str]) -> List[Dict]:
|
|
254
|
+
if not is_skill_md(path):
|
|
255
|
+
return []
|
|
256
|
+
fm = _parse_frontmatter_description(lines)
|
|
257
|
+
if fm is None:
|
|
258
|
+
return []
|
|
259
|
+
|
|
260
|
+
desc = fm["description"]
|
|
261
|
+
word_count = len(desc.split())
|
|
262
|
+
problems = []
|
|
263
|
+
|
|
264
|
+
min_words = THRESHOLDS["description_min_words"]
|
|
265
|
+
if word_count < min_words:
|
|
266
|
+
problems.append(f"only {word_count} words (skill-creator's guidance wants roughly {min_words}+)")
|
|
267
|
+
if not _TRIGGER_SIGNAL_RE.search(desc):
|
|
268
|
+
problems.append('no concrete trigger phrase or example (e.g. "Use when...", a quoted user phrase)')
|
|
269
|
+
|
|
270
|
+
if not problems:
|
|
271
|
+
return []
|
|
272
|
+
return [make_finding(
|
|
273
|
+
path, fm["line"], "weak_trigger_description", "low",
|
|
274
|
+
"Description is not sufficiently 'pushy' per skill-creator's guidance: "
|
|
275
|
+
+ "; ".join(problems) + "."
|
|
276
|
+
)]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# --------------------------------------------------------------------------------------
|
|
280
|
+
# Check 4: dangling references/X.md or modes/X.md cross-references
|
|
281
|
+
# --------------------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
# Same match shape as G3's own dangling-reference regex (test/guards/consumers.test.js).
|
|
284
|
+
_REFERENCE_MENTION_RE = re.compile(r'`?((?:modes|references)/[A-Za-z0-9_.-]+\.md)`?')
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _unresolved_reference_message(ref: str, bases: List[Path]) -> Optional[str]:
|
|
288
|
+
"""None if `ref` resolves under any candidate base; otherwise the human-readable
|
|
289
|
+
"does not resolve" message citing every base that was tried."""
|
|
290
|
+
if any((base / ref).is_file() for base in bases):
|
|
291
|
+
return None
|
|
292
|
+
base_desc = " or ".join(display_path(b) for b in bases)
|
|
293
|
+
return f"'{ref}' does not resolve to a real file relative to {base_desc}."
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def check_dangling_references(path: Path, lines: List[str]) -> List[Dict]:
|
|
297
|
+
findings: List[Dict] = []
|
|
298
|
+
guidance_dir = find_guidance_dir(path)
|
|
299
|
+
|
|
300
|
+
# Two candidate resolution roots: the file's own directory (a skill's or an agent
|
|
301
|
+
# spec's own references/modes subdirectory) and the shared guidance tree (what most
|
|
302
|
+
# SKILL.md `references/X.md` mentions actually point at). A reference is only dangling
|
|
303
|
+
# if it resolves under NEITHER — checking both, rather than picking one based on which
|
|
304
|
+
# tree the file lives in, avoids a false "dangling" result for a guidance-tree file that
|
|
305
|
+
# also happens to keep a mention relative to its own subdirectory, and vice versa.
|
|
306
|
+
bases = [path.parent]
|
|
307
|
+
if guidance_dir is not None and guidance_dir != path.parent:
|
|
308
|
+
bases.append(guidance_dir)
|
|
309
|
+
|
|
310
|
+
in_fence = False
|
|
311
|
+
for i, line in enumerate(lines):
|
|
312
|
+
stripped = line.strip()
|
|
313
|
+
if stripped.startswith("```") or stripped.startswith("~~~"):
|
|
314
|
+
in_fence = not in_fence
|
|
315
|
+
continue
|
|
316
|
+
if in_fence:
|
|
317
|
+
continue
|
|
318
|
+
for m in _REFERENCE_MENTION_RE.finditer(line):
|
|
319
|
+
message = _unresolved_reference_message(m.group(1), bases)
|
|
320
|
+
if message:
|
|
321
|
+
findings.append(make_finding(path, i + 1, "dangling_reference", "high", message))
|
|
322
|
+
return findings
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# --------------------------------------------------------------------------------------
|
|
326
|
+
# Check 5: staleness markers (deprecated/legacy/no longer/outdated/obsolete)
|
|
327
|
+
# --------------------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
_STALE_KEYWORD_RE = re.compile(r"\b(deprecated|legacy|no longer|outdated|obsolete)\b", re.IGNORECASE)
|
|
330
|
+
|
|
331
|
+
# Any of these appearing in the same paragraph is a strong signal the paragraph is
|
|
332
|
+
# *describing a detection rule or an instruction about reviewed code* — e.g. "flag TODO/
|
|
333
|
+
# deprecated comments" — not asserting that this file's own content is stale. This feature's
|
|
334
|
+
# own design-phase investigation already found a naive keyword grep over-triggers on exactly
|
|
335
|
+
# this pattern; excluding whole paragraphs on this signal is deliberately conservative.
|
|
336
|
+
_SIGNAL_WORDS_RE = re.compile(
|
|
337
|
+
r"\b(flags?|flagged|flagging|detects?|detecting|pattern|patterns|listing|quotes?|"
|
|
338
|
+
r"quoting|prune|pruned|remove|removed|identify|consult|consider|searches?)\b",
|
|
339
|
+
re.IGNORECASE,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# A paragraph opening with a conditional/temporal connector ("When an item is superseded,
|
|
343
|
+
# ...") is describing a general mechanism, not asserting the current file's state.
|
|
344
|
+
_CONDITIONAL_START_RE = re.compile(r"^[^a-zA-Z]{0,10}(when|if|once|after|before)\b", re.IGNORECASE)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _iter_paragraphs(lines: List[str]):
|
|
348
|
+
"""Yield (0-based start line index, paragraph lines) for each blank-line-delimited
|
|
349
|
+
block of prose, skipping fenced code blocks entirely."""
|
|
350
|
+
in_fence = False
|
|
351
|
+
start = None
|
|
352
|
+
block: List[str] = []
|
|
353
|
+
for i, line in enumerate(lines):
|
|
354
|
+
stripped = line.strip()
|
|
355
|
+
if stripped.startswith("```") or stripped.startswith("~~~"):
|
|
356
|
+
if block:
|
|
357
|
+
yield start, block
|
|
358
|
+
block, start = [], None
|
|
359
|
+
in_fence = not in_fence
|
|
360
|
+
continue
|
|
361
|
+
if in_fence:
|
|
362
|
+
continue
|
|
363
|
+
if not stripped:
|
|
364
|
+
if block:
|
|
365
|
+
yield start, block
|
|
366
|
+
block, start = [], None
|
|
367
|
+
continue
|
|
368
|
+
if start is None:
|
|
369
|
+
start = i
|
|
370
|
+
block.append(line)
|
|
371
|
+
if block:
|
|
372
|
+
yield start, block
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _offset_to_line(start_idx: int, para_lines: List[str], offset: int) -> int:
|
|
376
|
+
"""Map a character offset in `" ".join(para_lines)` back to a 1-based source line."""
|
|
377
|
+
consumed = 0
|
|
378
|
+
for j, l in enumerate(para_lines):
|
|
379
|
+
seg_len = len(l) + (1 if j < len(para_lines) - 1 else 0) # +1 for the joining space
|
|
380
|
+
if offset < consumed + seg_len:
|
|
381
|
+
return start_idx + j + 1
|
|
382
|
+
consumed += seg_len
|
|
383
|
+
return start_idx + len(para_lines)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def check_staleness(path: Path, lines: List[str]) -> List[Dict]:
|
|
387
|
+
findings: List[Dict] = []
|
|
388
|
+
for start_idx, para_lines in _iter_paragraphs(lines):
|
|
389
|
+
para_text = " ".join(para_lines)
|
|
390
|
+
|
|
391
|
+
if _CONDITIONAL_START_RE.match(para_text.strip()):
|
|
392
|
+
continue
|
|
393
|
+
# A paragraph carrying inline code (backtick-wrapped identifiers) is virtually
|
|
394
|
+
# always technical prose naming actual fields/functions/paths, not a claim that
|
|
395
|
+
# this document's own content is stale — every real example this check was
|
|
396
|
+
# calibrated against (e.g. "the legacy `overlaps`/`serialize` fields") is this shape.
|
|
397
|
+
if "`" in para_text:
|
|
398
|
+
continue
|
|
399
|
+
if _SIGNAL_WORDS_RE.search(para_text):
|
|
400
|
+
continue
|
|
401
|
+
|
|
402
|
+
for m in _STALE_KEYWORD_RE.finditer(para_text):
|
|
403
|
+
start, end = m.span()
|
|
404
|
+
before = para_text[start - 1] if start > 0 else ""
|
|
405
|
+
after = para_text[end] if end < len(para_text) else ""
|
|
406
|
+
if before in "\"'/" or after in "\"'/":
|
|
407
|
+
# The keyword itself is quoted or slash-enumerated ("deprecated / legacy /
|
|
408
|
+
# outdated") — the word is the referent, not a claim about this file.
|
|
409
|
+
continue
|
|
410
|
+
line_no = _offset_to_line(start_idx, para_lines, start)
|
|
411
|
+
findings.append(make_finding(
|
|
412
|
+
path, line_no, "staleness_marker", "low",
|
|
413
|
+
f"Possible staleness marker '{m.group(0)}' — confirm this prose actually "
|
|
414
|
+
"asserts the surrounding content is outdated, not a rule/instruction "
|
|
415
|
+
"quoting the word as a detection target."
|
|
416
|
+
))
|
|
417
|
+
return findings
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# --------------------------------------------------------------------------------------
|
|
421
|
+
# File / directory orchestration
|
|
422
|
+
# --------------------------------------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
def analyze_file(filepath: Path) -> List[Dict]:
|
|
425
|
+
"""Analyze a single markdown file. Returns [] for a non-.md file or an unreadable one —
|
|
426
|
+
the same quiet-skip behavior code_quality_checker.py's directory walk relies on."""
|
|
427
|
+
if filepath.suffix.lower() != ".md":
|
|
428
|
+
return []
|
|
429
|
+
content = read_file_content(filepath)
|
|
430
|
+
if not content:
|
|
431
|
+
return []
|
|
432
|
+
lines = content.split("\n")
|
|
433
|
+
|
|
434
|
+
findings: List[Dict] = []
|
|
435
|
+
findings.extend(check_length(filepath, lines))
|
|
436
|
+
findings.extend(check_boundaries(filepath, lines))
|
|
437
|
+
findings.extend(check_description(filepath, lines))
|
|
438
|
+
findings.extend(check_dangling_references(filepath, lines))
|
|
439
|
+
findings.extend(check_staleness(filepath, lines))
|
|
440
|
+
return findings
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def analyze_directory(dir_path: Path, recursive: bool = True) -> List[Dict]:
|
|
444
|
+
"""Analyze every .md file under a directory."""
|
|
445
|
+
findings: List[Dict] = []
|
|
446
|
+
pattern = "**/*.md" if recursive else "*.md"
|
|
447
|
+
for filepath in sorted(dir_path.glob(pattern)):
|
|
448
|
+
if "node_modules" in str(filepath) or ".git" in str(filepath):
|
|
449
|
+
continue
|
|
450
|
+
findings.extend(analyze_file(filepath))
|
|
451
|
+
return findings
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def print_report(findings: List[Dict], target: Path) -> None:
|
|
455
|
+
"""Print human-readable analysis report."""
|
|
456
|
+
print("=" * 60)
|
|
457
|
+
print("DOC QUALITY REPORT")
|
|
458
|
+
print("=" * 60)
|
|
459
|
+
print(f"\nTarget: {display_path(target)}")
|
|
460
|
+
print(f"Findings: {len(findings)}")
|
|
461
|
+
|
|
462
|
+
if not findings:
|
|
463
|
+
print("\nNo findings.")
|
|
464
|
+
print("\n" + "=" * 60)
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
by_file: Dict[str, List[Dict]] = {}
|
|
468
|
+
for finding in findings:
|
|
469
|
+
by_file.setdefault(finding["file"], []).append(finding)
|
|
470
|
+
|
|
471
|
+
for file, file_findings in by_file.items():
|
|
472
|
+
print(f"\n{file}")
|
|
473
|
+
for finding in file_findings:
|
|
474
|
+
print(f" [{finding['severity'].upper()}] line {finding['line']} "
|
|
475
|
+
f"({finding['rule']}): {finding['message']}")
|
|
476
|
+
|
|
477
|
+
print("\n" + "=" * 60)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def main():
|
|
481
|
+
parser = argparse.ArgumentParser(
|
|
482
|
+
description="Analyze markdown/prose content quality (structural + staleness checks)"
|
|
483
|
+
)
|
|
484
|
+
parser.add_argument(
|
|
485
|
+
"path",
|
|
486
|
+
help="File or directory to analyze"
|
|
487
|
+
)
|
|
488
|
+
parser.add_argument(
|
|
489
|
+
"--recursive", "-r",
|
|
490
|
+
action="store_true",
|
|
491
|
+
default=True,
|
|
492
|
+
help="Recursively analyze directories for .md files (default: true)"
|
|
493
|
+
)
|
|
494
|
+
parser.add_argument(
|
|
495
|
+
"--json",
|
|
496
|
+
action="store_true",
|
|
497
|
+
help="Output in JSON format"
|
|
498
|
+
)
|
|
499
|
+
parser.add_argument(
|
|
500
|
+
"--output", "-o",
|
|
501
|
+
help="Write output to file"
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
args = parser.parse_args()
|
|
505
|
+
|
|
506
|
+
target = Path(args.path).resolve()
|
|
507
|
+
|
|
508
|
+
if not target.exists():
|
|
509
|
+
print(f"Error: Path does not exist: {target}", file=sys.stderr)
|
|
510
|
+
sys.exit(1)
|
|
511
|
+
|
|
512
|
+
if target.is_file():
|
|
513
|
+
findings = analyze_file(target)
|
|
514
|
+
else:
|
|
515
|
+
findings = analyze_directory(target, args.recursive)
|
|
516
|
+
if not findings and not any(target.glob("**/*.md" if args.recursive else "*.md")):
|
|
517
|
+
print(f"Warning: no .md files found under {display_path(target)}", file=sys.stderr)
|
|
518
|
+
|
|
519
|
+
if args.json:
|
|
520
|
+
output = json.dumps(findings, indent=2, default=str)
|
|
521
|
+
if args.output:
|
|
522
|
+
with open(args.output, "w") as f:
|
|
523
|
+
f.write(output)
|
|
524
|
+
print(f"Results written to {args.output}")
|
|
525
|
+
else:
|
|
526
|
+
print(output)
|
|
527
|
+
else:
|
|
528
|
+
print_report(findings, target)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
if __name__ == "__main__":
|
|
532
|
+
main()
|