wicked-garden 12.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/archetypes.json +166 -0
- package/.claude-plugin/components.json +75 -0
- package/.claude-plugin/marketplace.json +15 -0
- package/.claude-plugin/plugin.json +174 -0
- package/.claude-plugin/specialist.json +48 -0
- package/.claude-plugin/stack-registry.json +96 -0
- package/AGENTS.md +13 -0
- package/CHANGELOG.md +712 -0
- package/CONTRIBUTING.md +99 -0
- package/ETHOS.md +83 -0
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/WICKED_GARDEN_BUS_EVENTS.md +149 -0
- package/agents/agentic/architect.md +449 -0
- package/agents/agentic/performance-analyst.md +702 -0
- package/agents/agentic/safety-reviewer.md +662 -0
- package/agents/crew/implementer.md +163 -0
- package/agents/crew/researcher.md +94 -0
- package/agents/crew/reviewer.md +184 -0
- package/agents/data/data-engineer.md +227 -0
- package/agents/engineering/api-documentarian.md +440 -0
- package/agents/engineering/migration-engineer.md +301 -0
- package/agents/engineering/solution-architect.md +208 -0
- package/agents/jam/brainstorm-facilitator.md +297 -0
- package/agents/jam/council.md +322 -0
- package/agents/jam/quick-facilitator.md +85 -0
- package/agents/persona/persona-agent.md +121 -0
- package/agents/platform/compliance-officer.md +266 -0
- package/agents/platform/privacy-expert.md +496 -0
- package/agents/platform/security-engineer.md +287 -0
- package/agents/product/a11y-expert.md +323 -0
- package/agents/product/requirements-analyst.md +238 -0
- package/agents/product/ui-reviewer.md +180 -0
- package/agents/product/ux-designer.md +199 -0
- package/agents/product/value-strategist.md +269 -0
- package/agents/qe/semantic-reviewer.md +78 -0
- package/commands/agentic/audit.md +24 -0
- package/commands/agentic/design.md +24 -0
- package/commands/agentic/frameworks.md +25 -0
- package/commands/agentic/review.md +27 -0
- package/commands/archetype/build.md +12 -0
- package/commands/archetype/decide.md +12 -0
- package/commands/archetype/explore.md +12 -0
- package/commands/archetype/incident.md +12 -0
- package/commands/archetype/migrate.md +12 -0
- package/commands/archetype/modernize.md +12 -0
- package/commands/archetype/review.md +14 -0
- package/commands/archetype/ship.md +12 -0
- package/commands/archetype/specify.md +12 -0
- package/commands/archetype/triage.md +12 -0
- package/commands/compile.md +37 -0
- package/commands/data/analyze.md +18 -0
- package/commands/data/data.md +21 -0
- package/commands/data/ml.md +18 -0
- package/commands/data/ontology.md +22 -0
- package/commands/data/pipeline.md +17 -0
- package/commands/deliberate.md +181 -0
- package/commands/engineering/add-field.md +77 -0
- package/commands/engineering/apply.md +58 -0
- package/commands/engineering/arch.md +26 -0
- package/commands/engineering/debug.md +27 -0
- package/commands/engineering/docs.md +27 -0
- package/commands/engineering/new-generator.md +257 -0
- package/commands/engineering/patch-plan.md +59 -0
- package/commands/engineering/plan.md +29 -0
- package/commands/engineering/remove.md +48 -0
- package/commands/engineering/rename.md +44 -0
- package/commands/engineering/review.md +32 -0
- package/commands/help.md +128 -0
- package/commands/install.md +118 -0
- package/commands/intent.md +54 -0
- package/commands/jam/brainstorm.md +37 -0
- package/commands/jam/council.md +49 -0
- package/commands/jam/quick.md +21 -0
- package/commands/jam/revisit.md +79 -0
- package/commands/persona/as.md +136 -0
- package/commands/persona/define.md +24 -0
- package/commands/persona/list.md +118 -0
- package/commands/platform/actions.md +20 -0
- package/commands/platform/assert.md +18 -0
- package/commands/platform/audit.md +23 -0
- package/commands/platform/compliance.md +20 -0
- package/commands/platform/gh.md +22 -0
- package/commands/platform/health.md +22 -0
- package/commands/platform/incident.md +23 -0
- package/commands/platform/infra.md +24 -0
- package/commands/platform/peer-health.md +103 -0
- package/commands/platform/plugin-health.md +58 -0
- package/commands/platform/security.md +38 -0
- package/commands/platform/toolchain.md +96 -0
- package/commands/platform/traces.md +20 -0
- package/commands/product/a11y.md +19 -0
- package/commands/product/acceptance.md +19 -0
- package/commands/product/align.md +20 -0
- package/commands/product/analyze.md +23 -0
- package/commands/product/elicit.md +19 -0
- package/commands/product/listen.md +24 -0
- package/commands/product/mockup.md +20 -0
- package/commands/product/screenshot.md +19 -0
- package/commands/product/strategy.md +40 -0
- package/commands/product/synthesize.md +23 -0
- package/commands/product/ux-review.md +42 -0
- package/commands/product/ux.md +20 -0
- package/commands/product/visual-direction.md +34 -0
- package/commands/prove.md +47 -0
- package/commands/report-issue.md +243 -0
- package/commands/reset.md +144 -0
- package/commands/search/blast-radius.md +58 -0
- package/commands/search/hotspots.md +55 -0
- package/commands/search/index.md +38 -0
- package/commands/search/lineage.md +47 -0
- package/commands/search/service-map.md +79 -0
- package/commands/setup.md +363 -0
- package/commands/smaht/briefing.md +200 -0
- package/commands/smaht/events-import.md +95 -0
- package/commands/smaht/state.md +19 -0
- package/commands/where-am-i.md +58 -0
- package/docs/README.md +32 -0
- package/docs/adr/0001-code-relationship-graph-engine.md +116 -0
- package/docs/adr/0002-major-cleanup-keep-cut.md +91 -0
- package/docs/adr/0003-peers-gate-required-layers-optin.md +41 -0
- package/docs/adr/0004-code-graph-moves-to-wicked-brain.md +47 -0
- package/docs/brain-chunk-format.md +135 -0
- package/docs/compiler.md +95 -0
- package/docs/daemon-api.md +70 -0
- package/docs/domains.md +197 -0
- package/docs/getting-started.md +208 -0
- package/docs/plans/2026-06-08-wicked-loom-conduct.md +1337 -0
- package/docs/plans/2026-06-08-wicked-loom-contract.md +638 -0
- package/docs/plans/2026-06-08-wicked-loom-cutover.md +1300 -0
- package/docs/plans/2026-06-08-wicked-loom-expand-compose.md +843 -0
- package/docs/plans/2026-06-08-wicked-loom-flow-cutover.md +403 -0
- package/docs/plans/2026-06-10-codegraph-to-brain-phase1a-graph-core.md +838 -0
- package/docs/plans/2026-06-10-codegraph-to-brain-phase1b-injected-edges.md +44 -0
- package/docs/required-peers.md +60 -0
- package/docs/specs/2026-06-08-wicked-loom-extraction.md +225 -0
- package/docs/specs/2026-06-10-codegraph-to-brain-migration.md +168 -0
- package/docs/v11/archetypes.md +231 -0
- package/hooks/hooks.json +275 -0
- package/hooks/scripts/bootstrap.py +1797 -0
- package/hooks/scripts/invoke.py +62 -0
- package/hooks/scripts/notification.py +166 -0
- package/hooks/scripts/permission_request.py +223 -0
- package/hooks/scripts/post_tool.py +1374 -0
- package/hooks/scripts/pre_compact.py +272 -0
- package/hooks/scripts/pre_tool.py +1278 -0
- package/hooks/scripts/prompt_submit.py +1109 -0
- package/hooks/scripts/session_end.py +95 -0
- package/hooks/scripts/stop.py +624 -0
- package/hooks/scripts/subagent_lifecycle.py +464 -0
- package/hooks/scripts/subscribers/on_gate_decided.py +84 -0
- package/hooks/scripts/task_completed.py +322 -0
- package/hooks/scripts/worktree_guard.py +148 -0
- package/hooks/subscriptions/on_gate_decided.json +9 -0
- package/install.mjs +128 -0
- package/package.json +41 -0
- package/pyproject.toml +59 -0
- package/schemas/README.md +32 -0
- package/schemas/assert_contracts.json +37 -0
- package/schemas/health_probe.json +46 -0
- package/schemas/plugin_status.json +37 -0
- package/scripts/_adapters/__init__.py +166 -0
- package/scripts/_adapters/jira_adapter.py +18 -0
- package/scripts/_adapters/linear_adapter.py +18 -0
- package/scripts/_adapters/miro_adapter.py +18 -0
- package/scripts/_adapters/notion_adapter.py +18 -0
- package/scripts/_agents.py +357 -0
- package/scripts/_brain_ingest/__init__.py +2 -0
- package/scripts/_brain_ingest/session_fact_extractor.py +341 -0
- package/scripts/_brain_port.py +163 -0
- package/scripts/_bus.py +1081 -0
- package/scripts/_bus_catalog_gen.py +93 -0
- package/scripts/_bus_consumers.json +46 -0
- package/scripts/_bus_emit.py +43 -0
- package/scripts/_capability_registry.py +334 -0
- package/scripts/_capability_resolver.py +322 -0
- package/scripts/_domain_store.py +559 -0
- package/scripts/_event_log_reader.py +154 -0
- package/scripts/_event_schema.py +305 -0
- package/scripts/_event_store.py +397 -0
- package/scripts/_heavy_cadence.py +324 -0
- package/scripts/_integration_resolver.py +321 -0
- package/scripts/_logger.py +151 -0
- package/scripts/_loom.py +482 -0
- package/scripts/_paths.py +133 -0
- package/scripts/_python.sh +22 -0
- package/scripts/_run.py +137 -0
- package/scripts/_session.py +502 -0
- package/scripts/_validate_registry.py +1042 -0
- package/scripts/_wicked_testing_probe.py +277 -0
- package/scripts/agentic/analyze_agents.py +391 -0
- package/scripts/agentic/detect_framework.py +420 -0
- package/scripts/agentic/issue_taxonomy.py +357 -0
- package/scripts/agentic/pattern_scorer.py +434 -0
- package/scripts/classify/persist.py +169 -0
- package/scripts/compiler/compile.py +552 -0
- package/scripts/compiler/phase0/VERDICT.md +60 -0
- package/scripts/compiler/phase0/detect.py +292 -0
- package/scripts/compiler/phase0/emit.py +228 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.bindings.json +30 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/.wg/evidence/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/.wg/evidence/honest-tests.txt +2 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/command_iq.bindings.json +45 -0
- package/scripts/compiler/phase0/evidence/command_iq.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/.projects/evidence-domain/phase-1-clarify/evidence/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/.projects/evidence-domain/phase-1-clarify/evidence/honest-tests.txt +3 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/memos.bindings.json +28 -0
- package/scripts/compiler/phase0/evidence/memos.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/memos.stage/.wg/evidence/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/memos.stage/.wg/evidence/honest-tests.txt +2 -0
- package/scripts/compiler/phase0/evidence/memos.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/memos.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/memos.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/memos.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/verdict.json +104 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.bindings.json +29 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/coverage/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/coverage/honest-tests.txt +3 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/wire-vault-transcript.txt +60 -0
- package/scripts/compiler/phase0/run_probe.py +234 -0
- package/scripts/compiler/phase0/wire_vault.py +310 -0
- package/scripts/crew/__init__.py +0 -0
- package/scripts/crew/_task_audit_writer.py +243 -0
- package/scripts/crew/_task_reader.py +389 -0
- package/scripts/crew/archetypes_v11.py +348 -0
- package/scripts/crew/detectors/__init__.py +12 -0
- package/scripts/crew/flow_compiler.py +121 -0
- package/scripts/crew/phase_manager.py +703 -0
- package/scripts/crew/scope_delta.py +264 -0
- package/scripts/crew/specialist_resolver.py +218 -0
- package/scripts/crew/stack_registry.py +189 -0
- package/scripts/data/data_profiler.py +271 -0
- package/scripts/data/ontology_recommender.py +231 -0
- package/scripts/data/schema_validator.py +267 -0
- package/scripts/delivery/drift.py +739 -0
- package/scripts/delivery/telemetry.py +614 -0
- package/scripts/engineering/patch/codegraph_db.py +125 -0
- package/scripts/engineering/patch/generators/__init__.py +56 -0
- package/scripts/engineering/patch/generators/base.py +373 -0
- package/scripts/engineering/patch/generators/csharp_generator.py +494 -0
- package/scripts/engineering/patch/generators/go_generator.py +430 -0
- package/scripts/engineering/patch/generators/java_generator.py +928 -0
- package/scripts/engineering/patch/generators/jsp_generator.py +454 -0
- package/scripts/engineering/patch/generators/kotlin_generator.py +561 -0
- package/scripts/engineering/patch/generators/perl_generator.py +510 -0
- package/scripts/engineering/patch/generators/php_generator.py +644 -0
- package/scripts/engineering/patch/generators/propagation_engine.py +929 -0
- package/scripts/engineering/patch/generators/python_generator.py +487 -0
- package/scripts/engineering/patch/generators/ruby_generator.py +463 -0
- package/scripts/engineering/patch/generators/rust_generator.py +400 -0
- package/scripts/engineering/patch/generators/sql_generator.py +594 -0
- package/scripts/engineering/patch/generators/typescript_generator.py +439 -0
- package/scripts/engineering/patch/patch.py +800 -0
- package/scripts/engineering/patch/safety.py +429 -0
- package/scripts/jam/__init__.py +0 -0
- package/scripts/jam/_bus_consumers.py +271 -0
- package/scripts/jam/agentic_cli_registry.py +703 -0
- package/scripts/jam/consensus.py +737 -0
- package/scripts/jam/detect_clis.py +409 -0
- package/scripts/jam/jam.py +283 -0
- package/scripts/jam/save_transcript.py +171 -0
- package/scripts/loom/__init__.py +22 -0
- package/scripts/loom/cli.py +170 -0
- package/scripts/loom/compose.py +180 -0
- package/scripts/loom/gate.py +120 -0
- package/scripts/loom/manifest.py +140 -0
- package/scripts/loom/resolve.py +59 -0
- package/scripts/persona/__init__.py +0 -0
- package/scripts/persona/registry.py +688 -0
- package/scripts/platform/guard_pipeline.py +1214 -0
- package/scripts/platform/guard_profiles.py +216 -0
- package/scripts/platform/observability/assert_contracts.py +434 -0
- package/scripts/platform/observability/health_probe.py +915 -0
- package/scripts/platform/observability/ops_log_viewer.py +209 -0
- package/scripts/platform/prereq_doctor.py +567 -0
- package/scripts/qe/conditions_manifest.py +260 -0
- package/scripts/qe/content_sanitizer.py +160 -0
- package/scripts/qe/coverage_tracker.py +535 -0
- package/scripts/qe/discover_scenarios.py +243 -0
- package/scripts/qe/evidence_tracker.py +232 -0
- package/scripts/qe/prove.py +196 -0
- package/scripts/qe/registry_lookup.py +80 -0
- package/scripts/qe/registry_store.py +176 -0
- package/scripts/qe/semantic_review.py +1005 -0
- package/scripts/qe/vault_gate.py +382 -0
- package/scripts/qe/verdict_audit.py +175 -0
- package/scripts/qe/verdict_schema.py +335 -0
- package/scripts/reset.py +220 -0
- package/scripts/resolve_path.py +22 -0
- package/scripts/sentinel/invariants.py +402 -0
- package/scripts/sentinel/pre_push.py +73 -0
- package/scripts/setup/__init__.py +1 -0
- package/scripts/setup/detect_state.py +114 -0
- package/scripts/setup/migrate_legacy.py +67 -0
- package/scripts/setup/migrate_v6_projects.py +232 -0
- package/scripts/setup/onboarding.py +114 -0
- package/scripts/smaht/_bus_consumers.py +98 -0
- package/scripts/smaht/adapters/__init__.py +83 -0
- package/scripts/smaht/adapters/brain_adapter.py +254 -0
- package/scripts/smaht/adapters/context7_adapter.py +438 -0
- package/scripts/smaht/adapters/delegation_adapter.py +104 -0
- package/scripts/smaht/adapters/domain_adapter.py +206 -0
- package/scripts/smaht/adapters/events_adapter.py +231 -0
- package/scripts/smaht/adapters/tools_adapter.py +49 -0
- package/scripts/smaht/cheatsheet_store.py +236 -0
- package/scripts/smaht/context_package.py +386 -0
- package/scripts/smaht/knowledge_graph.py +395 -0
- package/scripts/smaht/propose_skills.py +856 -0
- package/scripts/smaht/test_cheatsheet_store.py +428 -0
- package/scripts/statusline.py +151 -0
- package/scripts/where_am_i.py +236 -0
- package/skills/agentic/agentic-patterns/SKILL.md +91 -0
- package/skills/agentic/agentic-patterns/refs/anti-patterns-design.md +283 -0
- package/skills/agentic/agentic-patterns/refs/anti-patterns-operational.md +216 -0
- package/skills/agentic/agentic-patterns/refs/pattern-catalog.md +329 -0
- package/skills/agentic/context-engineering/SKILL.md +142 -0
- package/skills/agentic/context-engineering/refs/caching-and-optimization.md +251 -0
- package/skills/agentic/context-engineering/refs/compression-techniques.md +171 -0
- package/skills/agentic/context-engineering/refs/cost-calculation-budget.md +250 -0
- package/skills/agentic/context-engineering/refs/cost-optimization-reporting.md +300 -0
- package/skills/agentic/context-engineering/refs/selective-loading.md +179 -0
- package/skills/agentic/frameworks/SKILL.md +191 -0
- package/skills/agentic/frameworks/refs/framework-profiles-1.md +209 -0
- package/skills/agentic/frameworks/refs/framework-profiles-2.md +214 -0
- package/skills/agentic/frameworks/refs/migration-patterns-paths.md +267 -0
- package/skills/agentic/frameworks/refs/migration-patterns-testing.md +208 -0
- package/skills/agentic/refs/audit.md +134 -0
- package/skills/agentic/refs/design.md +137 -0
- package/skills/agentic/refs/frameworks.md +102 -0
- package/skills/agentic/refs/review.md +160 -0
- package/skills/agentic/review-methodology/SKILL.md +186 -0
- package/skills/agentic/review-methodology/refs/deliverable-templates.md +246 -0
- package/skills/agentic/review-methodology/refs/issue-taxonomy-quality-testing.md +277 -0
- package/skills/agentic/review-methodology/refs/issue-taxonomy-reliability-safety.md +253 -0
- package/skills/agentic/trust-and-safety/SKILL.md +177 -0
- package/skills/agentic/trust-and-safety/refs/guardrails-actions.md +246 -0
- package/skills/agentic/trust-and-safety/refs/guardrails-input-output.md +238 -0
- package/skills/agentic/trust-and-safety/refs/guardrails-resources.md +209 -0
- package/skills/agentic/trust-and-safety/refs/safety-checklist-advanced.md +209 -0
- package/skills/agentic/trust-and-safety/refs/safety-checklist-core.md +215 -0
- package/skills/archetype/SKILL.md +104 -0
- package/skills/archetype/refs/build.md +142 -0
- package/skills/archetype/refs/decide.md +117 -0
- package/skills/archetype/refs/explore.md +66 -0
- package/skills/archetype/refs/incident.md +149 -0
- package/skills/archetype/refs/migrate.md +170 -0
- package/skills/archetype/refs/modernize.md +187 -0
- package/skills/archetype/refs/review.md +145 -0
- package/skills/archetype/refs/ship.md +117 -0
- package/skills/archetype/refs/specify.md +108 -0
- package/skills/archetype/refs/triage.md +62 -0
- package/skills/classify/SKILL.md +157 -0
- package/skills/data/analysis/SKILL.md +125 -0
- package/skills/data/analysis/refs/templates.md +170 -0
- package/skills/data/data/SKILL.md +118 -0
- package/skills/data/data/refs/examples.md +235 -0
- package/skills/data/ml/SKILL.md +125 -0
- package/skills/data/ml/refs/templates.md +172 -0
- package/skills/data/pipeline/SKILL.md +136 -0
- package/skills/data/pipeline/refs/templates.md +175 -0
- package/skills/data/refs/analyze.md +123 -0
- package/skills/data/refs/data.md +95 -0
- package/skills/data/refs/ml.md +118 -0
- package/skills/data/refs/pipeline.md +92 -0
- package/skills/deliberate/SKILL.md +125 -0
- package/skills/deliberate/refs/architecture-lens.md +37 -0
- package/skills/deliberate/refs/assumptions-lens.md +43 -0
- package/skills/deliberate/refs/code-lens.md +38 -0
- package/skills/deliberate/refs/content-lens.md +37 -0
- package/skills/deliberate/refs/data-lens.md +43 -0
- package/skills/deliberate/refs/design-lens.md +37 -0
- package/skills/deliberate/refs/opportunity-patterns.md +107 -0
- package/skills/deliberate/refs/ops-lens.md +43 -0
- package/skills/deliberate/refs/product-lens.md +46 -0
- package/skills/deliberate/refs/rethink-framework.md +77 -0
- package/skills/deliberate/refs/security-lens.md +47 -0
- package/skills/deliberate/refs/testing-lens.md +45 -0
- package/skills/deliberate/refs/timing-lens.md +42 -0
- package/skills/engineering/architecture/SKILL.md +177 -0
- package/skills/engineering/architecture/refs/adr-template.md +268 -0
- package/skills/engineering/architecture/refs/architecture-template-deployment.md +176 -0
- package/skills/engineering/architecture/refs/architecture-template-design.md +300 -0
- package/skills/engineering/architecture/refs/examples-ecommerce-analytics.md +299 -0
- package/skills/engineering/architecture/refs/examples-saas-trading.md +291 -0
- package/skills/engineering/architecture/refs/patterns-advanced.md +238 -0
- package/skills/engineering/architecture/refs/patterns-structural.md +303 -0
- package/skills/engineering/backend/SKILL.md +102 -0
- package/skills/engineering/backend/refs/checklists.md +146 -0
- package/skills/engineering/backend/refs/patterns.md +181 -0
- package/skills/engineering/debugging/SKILL.md +99 -0
- package/skills/engineering/debugging/refs/patterns.md +248 -0
- package/skills/engineering/debugging/refs/process.md +203 -0
- package/skills/engineering/docs/SKILL.md +97 -0
- package/skills/engineering/engineering/SKILL.md +88 -0
- package/skills/engineering/engineering/refs/checklists.md +91 -0
- package/skills/engineering/engineering/refs/templates.md +71 -0
- package/skills/engineering/frontend/SKILL.md +92 -0
- package/skills/engineering/frontend/refs/checklists.md +96 -0
- package/skills/engineering/frontend/refs/patterns.md +167 -0
- package/skills/engineering/integration/SKILL.md +183 -0
- package/skills/engineering/integration/refs/error-handling-best-practices.md +139 -0
- package/skills/engineering/integration/refs/error-handling-error-classes-typescript.md +212 -0
- package/skills/engineering/integration/refs/error-handling-error-response-format.md +301 -0
- package/skills/engineering/integration/refs/error-handling-retry-strategies.md +190 -0
- package/skills/engineering/integration/refs/event-schemas-best-practices.md +225 -0
- package/skills/engineering/integration/refs/event-schemas-event-naming-conventions.md +303 -0
- package/skills/engineering/integration/refs/event-schemas-event-structure.md +288 -0
- package/skills/engineering/integration/refs/graphql-best-practices.md +138 -0
- package/skills/engineering/integration/refs/graphql-error-handling.md +198 -0
- package/skills/engineering/integration/refs/graphql-graphql-basics.md +197 -0
- package/skills/engineering/integration/refs/graphql-query-examples.md +226 -0
- package/skills/engineering/integration/refs/graphql-schema-design-patterns.md +280 -0
- package/skills/engineering/integration/refs/grpc-grpc-overview.md +246 -0
- package/skills/engineering/integration/refs/grpc-implementation-examples.md +269 -0
- package/skills/engineering/integration/refs/grpc-metadata-headers.md +304 -0
- package/skills/engineering/integration/refs/grpc-testing.md +49 -0
- package/skills/engineering/integration/refs/openapi-template.yaml +682 -0
- package/skills/engineering/integration/refs/resilience-best-practices.md +109 -0
- package/skills/engineering/integration/refs/resilience-circuit-breaker-pattern.md +302 -0
- package/skills/engineering/integration/refs/resilience-rate-limiting.md +245 -0
- package/skills/engineering/integration/refs/resilience-timeout-pattern.md +212 -0
- package/skills/engineering/integration/refs/rest-api-fundamentals.md +299 -0
- package/skills/engineering/integration/refs/rest-api-responses-security.md +302 -0
- package/skills/engineering/integration/refs/versioning-breaking-changes.md +237 -0
- package/skills/engineering/integration/refs/versioning-migration-strategies.md +257 -0
- package/skills/engineering/integration/refs/versioning-versioning-strategies.md +296 -0
- package/skills/engineering/large-scale-migration/SKILL.md +65 -0
- package/skills/engineering/patch/SKILL.md +184 -0
- package/skills/engineering/refs/arch.md +126 -0
- package/skills/engineering/refs/debug.md +142 -0
- package/skills/engineering/refs/docs.md +190 -0
- package/skills/engineering/refs/plan.md +109 -0
- package/skills/engineering/refs/review.md +138 -0
- package/skills/engineering/system-design/SKILL.md +154 -0
- package/skills/engineering/system-design/refs/anti-patterns-big-ball-of-mud.md +254 -0
- package/skills/engineering/system-design/refs/anti-patterns-circular-dependencies.md +241 -0
- package/skills/engineering/system-design/refs/anti-patterns-feature-envy.md +168 -0
- package/skills/engineering/system-design/refs/anti-patterns-god-object.md +247 -0
- package/skills/engineering/system-design/refs/component-template-lifecycle.md +210 -0
- package/skills/engineering/system-design/refs/component-template-maintenance.md +219 -0
- package/skills/engineering/system-design/refs/component-template-structure.md +276 -0
- package/skills/engineering/system-design/refs/dependencies-best-practices.md +260 -0
- package/skills/engineering/system-design/refs/dependencies-dependency-principles.md +295 -0
- package/skills/engineering/system-design/refs/dependencies-dependency-tracking.md +261 -0
- package/skills/engineering/system-design/refs/interface-template-lifecycle.md +211 -0
- package/skills/engineering/system-design/refs/interface-template-maintenance.md +212 -0
- package/skills/engineering/system-design/refs/interface-template-structure.md +269 -0
- package/skills/engineering/system-design/refs/patterns-dependency-injection.md +151 -0
- package/skills/engineering/system-design/refs/patterns-facade-pattern.md +257 -0
- package/skills/engineering/system-design/refs/patterns-layered-pattern.md +204 -0
- package/skills/engineering/system-design/refs/patterns-plugin-architecture.md +229 -0
- package/skills/engineering/unit-test-quality/SKILL.md +194 -0
- package/skills/ground/SKILL.md +130 -0
- package/skills/integration-discovery/SKILL.md +168 -0
- package/skills/integration-discovery/refs/capabilities.md +282 -0
- package/skills/integration-discovery/refs/cli-detection.md +295 -0
- package/skills/integration-discovery/refs/discovery-sources.md +233 -0
- package/skills/integration-discovery/refs/task-patterns.md +206 -0
- package/skills/jam/SKILL.md +45 -0
- package/skills/jam/refs/facilitation-patterns.md +125 -0
- package/skills/jam/refs/quick.md +64 -0
- package/skills/jam/refs/synthesis-patterns.md +114 -0
- package/skills/multi-model/SKILL.md +168 -0
- package/skills/multi-model/refs/aichat.md +119 -0
- package/skills/multi-model/refs/aider.md +100 -0
- package/skills/multi-model/refs/auditability.md +238 -0
- package/skills/multi-model/refs/codex.md +169 -0
- package/skills/multi-model/refs/context.md +306 -0
- package/skills/multi-model/refs/copilot.md +161 -0
- package/skills/multi-model/refs/examples.md +224 -0
- package/skills/multi-model/refs/gemini.md +185 -0
- package/skills/multi-model/refs/goose.md +112 -0
- package/skills/multi-model/refs/llm.md +124 -0
- package/skills/multi-model/refs/opencode.md +196 -0
- package/skills/multi-model/refs/orchestration.md +188 -0
- package/skills/multi-model/refs/pi.md +208 -0
- package/skills/persona/SKILL.md +144 -0
- package/skills/persona/refs/define.md +133 -0
- package/skills/platform/actions/refs/actions.md +114 -0
- package/skills/platform/audit/SKILL.md +174 -0
- package/skills/platform/audit/refs/audit.md +145 -0
- package/skills/platform/audit/refs/checklists-evidence-operations.md +144 -0
- package/skills/platform/audit/refs/checklists-gdpr-pci-evidence.md +213 -0
- package/skills/platform/audit/refs/checklists-soc2-hipaa.md +120 -0
- package/skills/platform/audit/refs/frameworks-gdpr-pci.md +225 -0
- package/skills/platform/audit/refs/frameworks-soc2-hipaa.md +228 -0
- package/skills/platform/compliance/SKILL.md +169 -0
- package/skills/platform/compliance/refs/checklists.md +303 -0
- package/skills/platform/compliance/refs/compliance.md +117 -0
- package/skills/platform/compliance/refs/frameworks.md +282 -0
- package/skills/platform/errors/SKILL.md +183 -0
- package/skills/platform/errors/refs/patterns.md +271 -0
- package/skills/platform/errors/refs/severity.md +264 -0
- package/skills/platform/gate-benchmark-rebaseline/SKILL.md +108 -0
- package/skills/platform/gh/refs/gh.md +105 -0
- package/skills/platform/gh-cli/SKILL.md +120 -0
- package/skills/platform/gh-cli/refs/automation.md +63 -0
- package/skills/platform/gh-cli/refs/gh-ops.md +103 -0
- package/skills/platform/gh-cli/refs/patterns.md +132 -0
- package/skills/platform/gh-cli/scripts/gh_ops.py +454 -0
- package/skills/platform/github-actions/SKILL.md +159 -0
- package/skills/platform/github-actions/refs/security.md +150 -0
- package/skills/platform/github-actions/refs/templates.md +230 -0
- package/skills/platform/github-actions/refs/troubleshooting.md +174 -0
- package/skills/platform/github-actions/scripts/workflow_gen.py +215 -0
- package/skills/platform/gitlab-ci/SKILL.md +140 -0
- package/skills/platform/gitlab-ci/refs/templates.md +223 -0
- package/skills/platform/gitlab-ci/refs/troubleshooting.md +213 -0
- package/skills/platform/glab-cli/SKILL.md +133 -0
- package/skills/platform/glab-cli/refs/ci-debugging.md +95 -0
- package/skills/platform/glab-cli/refs/glab-ops.md +78 -0
- package/skills/platform/glab-cli/refs/patterns.md +131 -0
- package/skills/platform/glab-cli/scripts/glab_ops.py +313 -0
- package/skills/platform/health/SKILL.md +177 -0
- package/skills/platform/health/refs/health.md +72 -0
- package/skills/platform/health/refs/sources.md +302 -0
- package/skills/platform/incident/refs/incident.md +136 -0
- package/skills/platform/infra/refs/infra.md +104 -0
- package/skills/platform/observability/SKILL.md +128 -0
- package/skills/platform/observability/refs/toolchain-discovery.md +243 -0
- package/skills/platform/prereq-doctor/SKILL.md +57 -0
- package/skills/platform/prereq-doctor/refs/tool-registry.md +58 -0
- package/skills/platform/traces/refs/traces.md +101 -0
- package/skills/product/acceptance-criteria/SKILL.md +193 -0
- package/skills/product/acceptance-criteria/refs/ac-anti-patterns.md +291 -0
- package/skills/product/acceptance-criteria/refs/ac-examples.md +277 -0
- package/skills/product/accessibility/SKILL.md +197 -0
- package/skills/product/accessibility/refs/aria-patterns-basics.md +250 -0
- package/skills/product/accessibility/refs/aria-patterns-dynamic.md +187 -0
- package/skills/product/accessibility/refs/aria-patterns-interactive.md +246 -0
- package/skills/product/accessibility/refs/audit-report-findings.md +248 -0
- package/skills/product/accessibility/refs/audit-report-results.md +276 -0
- package/skills/product/accessibility/refs/common-violations-html-css.md +270 -0
- package/skills/product/accessibility/refs/common-violations-interactive.md +303 -0
- package/skills/product/accessibility/refs/keyboard-testing-basics.md +222 -0
- package/skills/product/accessibility/refs/keyboard-testing-patterns.md +173 -0
- package/skills/product/accessibility/refs/screen-reader-testing-advanced.md +294 -0
- package/skills/product/accessibility/refs/screen-reader-testing-commands.md +255 -0
- package/skills/product/accessibility/refs/wcag-checklist.md +262 -0
- package/skills/product/analyze/SKILL.md +164 -0
- package/skills/product/analyze/refs/algorithms.md +238 -0
- package/skills/product/analyze/refs/sentiment-patterns.md +180 -0
- package/skills/product/imagery/SKILL.md +97 -0
- package/skills/product/imagery/alter/SKILL.md +105 -0
- package/skills/product/imagery/alter/refs/editing-reference.md +146 -0
- package/skills/product/imagery/create/SKILL.md +109 -0
- package/skills/product/imagery/create/refs/prompt-engineering.md +112 -0
- package/skills/product/imagery/create/refs/provider-reference.md +219 -0
- package/skills/product/imagery/review/SKILL.md +101 -0
- package/skills/product/imagery/review/refs/analysis-lenses.md +136 -0
- package/skills/product/imagery/review/refs/quality-gates.md +153 -0
- package/skills/product/imagery/scripts/analyze.sh +23 -0
- package/skills/product/imagery/scripts/edit.sh +14 -0
- package/skills/product/imagery/scripts/generate.sh +13 -0
- package/skills/product/imagery/scripts/provider.py +787 -0
- package/skills/product/listen/SKILL.md +175 -0
- package/skills/product/listen/refs/channels.md +220 -0
- package/skills/product/mockup/SKILL.md +106 -0
- package/skills/product/mockup/refs/templates.md +95 -0
- package/skills/product/refs/a11y.md +109 -0
- package/skills/product/refs/acceptance.md +79 -0
- package/skills/product/refs/align.md +67 -0
- package/skills/product/refs/analyze.md +77 -0
- package/skills/product/refs/elicit.md +67 -0
- package/skills/product/refs/listen.md +58 -0
- package/skills/product/refs/mockup.md +77 -0
- package/skills/product/refs/screenshot.md +59 -0
- package/skills/product/refs/strategy.md +77 -0
- package/skills/product/refs/synthesize.md +54 -0
- package/skills/product/refs/ux-review.md +68 -0
- package/skills/product/refs/ux.md +101 -0
- package/skills/product/requirements-analysis/SKILL.md +136 -0
- package/skills/product/requirements-analysis/refs/requirements-example-auth.md +264 -0
- package/skills/product/requirements-analysis/refs/requirements-example-export-integration.md +88 -0
- package/skills/product/requirements-analysis/refs/requirements-output-format-template.md +247 -0
- package/skills/product/requirements-analysis/refs/user-story-guide.md +296 -0
- package/skills/product/requirements-graph/SKILL.md +155 -0
- package/skills/product/requirements-graph/refs/examples.md +223 -0
- package/skills/product/requirements-graph/refs/schema.md +162 -0
- package/skills/product/requirements-migrate/SKILL.md +90 -0
- package/skills/product/requirements-navigate/SKILL.md +141 -0
- package/skills/product/requirements-navigate/refs/meta-templates.md +141 -0
- package/skills/product/screenshot/SKILL.md +147 -0
- package/skills/product/strategy/SKILL.md +198 -0
- package/skills/product/strategy/refs/frameworks.md +295 -0
- package/skills/product/strategy/refs/templates-competitive-market.md +299 -0
- package/skills/product/strategy/refs/templates-roi-value.md +207 -0
- package/skills/product/synthesize/SKILL.md +116 -0
- package/skills/product/synthesize/refs/journey-mapping.md +212 -0
- package/skills/product/synthesize/refs/prioritization.md +193 -0
- package/skills/product/ux-review/SKILL.md +164 -0
- package/skills/product/visual-direction/SKILL.md +137 -0
- package/skills/product/visual-review/SKILL.md +131 -0
- package/skills/runtime-exec/SKILL.md +138 -0
- package/skills/search/codebase-narrator/SKILL.md +131 -0
- package/skills/search/codebase-narrator/refs/output-template.md +100 -0
- package/skills/smaht/SKILL.md +72 -0
- package/skills/smaht/discovery/SKILL.md +65 -0
- package/skills/smaht/intent/SKILL.md +89 -0
- package/skills/smaht/propose-skills/SKILL.md +142 -0
- package/skills/smaht/refs/state.md +76 -0
- package/skills/swarm/SKILL.md +132 -0
- package/skills/swarm/refs/fan-out.md +104 -0
- package/skills/swarm/refs/independent-verification.md +86 -0
- package/skills/swarm/refs/receipts-and-evidence.md +69 -0
- package/skills/swarm/refs/recon-synthesis.md +87 -0
- package/skills/swarm/refs/ship-discipline.md +90 -0
- package/skills/wickedizer/SKILL.md +147 -0
- package/skills/wickedizer/refs/artifact-types.md +238 -0
- package/skills/wickedizer/refs/complexity.md +278 -0
- package/skills/wickedizer/refs/de-ai-pass.md +140 -0
- package/skills/wickedizer/refs/failure-modes.md +249 -0
- package/skills/wickedizer/refs/patterns.md +282 -0
- package/skills/wickedizer/refs/review.md +221 -0
- package/skills/wickedizer/refs/structure.md +282 -0
- package/skills/wickedizer/refs/voice-profile.md +125 -0
- package/skills/workflow/SKILL.md +132 -0
- package/skills/workflow/refs/archetype-adjustments.md +205 -0
- package/skills/workflow/refs/evidence.md +58 -0
- package/skills/workflow/refs/integration.md +130 -0
- package/skills/workflow/refs/risk-dimension-signals.md +260 -0
- package/skills/workflow/refs/scoring-rubric.md +49 -0
- package/skills/workflow/refs/signal-keywords.md +176 -0
- package/skills/workflow/refs/specialist-routing-rules.md +159 -0
- package/skills/worktrees/SKILL.md +87 -0
- package/skills/worktrees/refs/recipes.md +96 -0
|
@@ -0,0 +1,1005 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Spec-to-code TRACEABILITY heuristic (issue #444) — NOT a semantic judgment.
|
|
3
|
+
|
|
4
|
+
Autonomous post-implementation pass that extracts numbered acceptance criteria
|
|
5
|
+
(AC-*) and functional requirements (FR-*/REQ-*) from clarify-phase specs
|
|
6
|
+
(``acceptance-criteria.md``, ``objective.md``) and checks whether each one is
|
|
7
|
+
*referenced* in the build-phase implementation + test corpus.
|
|
8
|
+
|
|
9
|
+
Emits a structured Gap Report (JSON) per spec item with ``status`` in
|
|
10
|
+
``{aligned, divergent, missing}``, confidence score, evidence, and reason.
|
|
11
|
+
|
|
12
|
+
**Important — what this does NOT do.** It matches the AC *identifier* (e.g.
|
|
13
|
+
``AC-1``) and surrounding keywords, not the AC's *meaning*. An untagged-but-
|
|
14
|
+
correct implementation reads as MISSING (the code below `subtract` fully
|
|
15
|
+
implements "AC-2: subtract" but, lacking an ``AC-2`` reference, is reported
|
|
16
|
+
missing); a tagged-but-wrong one can read ALIGNED. So this is a traceability
|
|
17
|
+
pre-filter — "is each AC referenced?" — that complements
|
|
18
|
+
``verification_protocol.py`` check #6.
|
|
19
|
+
|
|
20
|
+
The genuinely *semantic* question — "does the code correctly implement what the
|
|
21
|
+
AC described?" — is answered by the ``agents/qe/semantic-reviewer`` agent (LLM):
|
|
22
|
+
it reads the code, judges each AC by **meaning** (behavior, not id occurrence),
|
|
23
|
+
and records its verdict as an independent vault **attestation**. A gate declared
|
|
24
|
+
with ``require_attestation`` then enforces it (UNATTESTED/REJECT until the
|
|
25
|
+
independent reviewer attests pass). This script is that agent's cheap pre-filter
|
|
26
|
+
— do not gate on its verdict alone.
|
|
27
|
+
|
|
28
|
+
CLI::
|
|
29
|
+
|
|
30
|
+
semantic_review.py review \\
|
|
31
|
+
--project-dir <dir> [--ac-file <path>] [--objective-file <path>] \\
|
|
32
|
+
[--impl-dir <dir>] [--test-dir <dir>] [--output <json-path>]
|
|
33
|
+
|
|
34
|
+
semantic_review.py constraints \\
|
|
35
|
+
--adr-dir <dir> [--output <json-path>]
|
|
36
|
+
|
|
37
|
+
Python API::
|
|
38
|
+
|
|
39
|
+
from semantic_review import (
|
|
40
|
+
extract_spec_items, generate_gap_report, extract_adr_constraints,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
Design notes
|
|
44
|
+
------------
|
|
45
|
+
- **Stdlib-only**. No external deps. Cross-platform.
|
|
46
|
+
- **Heuristic-based** — LLM-free. Uses keyword overlap + reference counting
|
|
47
|
+
and a curated list of ``constraint signals`` (numeric limits, scoping
|
|
48
|
+
phrases like "per session", modal verbs).
|
|
49
|
+
- **Conservative** on missing — only reports MISSING when the AC identifier
|
|
50
|
+
literal is absent from both the implementation corpus and the test corpus.
|
|
51
|
+
- **Conservative** on aligned — requires the AC identifier in BOTH corpora
|
|
52
|
+
AND a minimum keyword overlap with the code context around the reference.
|
|
53
|
+
- **Divergent** — everything else where the AC is referenced but signals
|
|
54
|
+
don't line up. Confidence score reports how sure we are.
|
|
55
|
+
|
|
56
|
+
The agent consuming this output (``agents/qe/semantic-reviewer.md``) can
|
|
57
|
+
promote MISSING/DIVERGENT findings to gate conditions when complexity >= 3.
|
|
58
|
+
"""
|
|
59
|
+
from __future__ import annotations
|
|
60
|
+
|
|
61
|
+
import argparse
|
|
62
|
+
import json
|
|
63
|
+
import re
|
|
64
|
+
import sys
|
|
65
|
+
from dataclasses import asdict, dataclass, field
|
|
66
|
+
from pathlib import Path
|
|
67
|
+
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Constants
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
# Spec-id extraction — accept AC-*, FR-*, REQ-* (with optional domain infix,
|
|
75
|
+
# e.g. REQ-auth-3). Matches existing patterns used by verification_protocol.py
|
|
76
|
+
# and traceability_generator.
|
|
77
|
+
_SPEC_ID_RE = re.compile(
|
|
78
|
+
r"\b(?:AC|FR|REQ|NFR|US)-[A-Za-z]*-?\d+\b",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Table-row AC extraction (from verification_protocol._AC_TABLE_ROW_RE)
|
|
82
|
+
_AC_TABLE_ROW_RE = re.compile(
|
|
83
|
+
r"\|\s*(?P<id>AC-\d+|[A-Z]{1,4}-\d+|\d+)\s*\|"
|
|
84
|
+
r"\s*(?P<desc>[^|]+)\s*\|"
|
|
85
|
+
r"\s*(?P<test>[^|]*)\s*\|",
|
|
86
|
+
re.IGNORECASE,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# List-style AC extraction
|
|
90
|
+
_AC_LIST_RE = re.compile(
|
|
91
|
+
r"[-*]\s+(?:\*\*)?(?P<id>AC-\d+|[A-Z]{1,4}-\d+)(?:\*\*)?\s*[:\-]\s*(?P<desc>.+)",
|
|
92
|
+
re.IGNORECASE,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Inline AC extraction (e.g. "AC1: Given ... When ... Then ...")
|
|
96
|
+
_AC_INLINE_RE = re.compile(
|
|
97
|
+
r"(?P<id>AC\d+|[A-Z]{1,4}-\d+|AC-\d+)\s*[:\-]\s*(?P<desc>Given[^\n]+)",
|
|
98
|
+
re.IGNORECASE,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Constraint signals — numeric quantifiers, scoping qualifiers, and modal
|
|
102
|
+
# verbs that frequently appear in ACs and must be honored verbatim.
|
|
103
|
+
_NUMERIC_CONSTRAINT_RE = re.compile(
|
|
104
|
+
r"\b(\d+)\s*(ms|s|seconds?|min(?:utes?)?|hours?|days?|%|requests?|"
|
|
105
|
+
r"attempts?|tries|sessions?|users?|bytes?|kb|mb|gb|tokens?|items?)\b",
|
|
106
|
+
re.IGNORECASE,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
_SCOPE_PHRASE_RE = re.compile(
|
|
110
|
+
r"\bper\s+(session|request|user|tenant|account|minute|hour|day|page|call|token|device|ip)\b",
|
|
111
|
+
re.IGNORECASE,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
_MODAL_DIRECTIVE_RE = re.compile(
|
|
115
|
+
r"\b(MUST NOT|MUST|SHALL NOT|SHALL|SHOULD NOT|SHOULD|REQUIRED|FORBIDDEN)\b",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Word extraction for overlap scoring — alphanumeric tokens of length 3+,
|
|
119
|
+
# lowercase normalised, minus stopwords.
|
|
120
|
+
_WORD_RE = re.compile(r"[A-Za-z][A-Za-z0-9]{2,}")
|
|
121
|
+
|
|
122
|
+
_STOPWORDS = frozenset({
|
|
123
|
+
"the", "and", "for", "when", "then", "given", "that", "with", "this",
|
|
124
|
+
"has", "have", "will", "shall", "should", "must", "not", "are", "was",
|
|
125
|
+
"were", "from", "user", "users", "system", "code", "test", "case",
|
|
126
|
+
"example", "into", "than", "less", "more", "which", "while", "its",
|
|
127
|
+
"any", "all", "one", "two", "three", "four", "five", "also", "their",
|
|
128
|
+
"been", "being", "upon", "other", "some", "such", "each", "them",
|
|
129
|
+
"they", "these", "those", "there", "here", "above", "below",
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
# Context window around AC references (chars on either side) for overlap
|
|
133
|
+
# scoring.
|
|
134
|
+
_CONTEXT_WINDOW = 400
|
|
135
|
+
|
|
136
|
+
# Alignment thresholds.
|
|
137
|
+
_ALIGNED_OVERLAP_THRESHOLD = 0.35 # >= this ratio of AC keywords seen in context
|
|
138
|
+
_DIVERGENT_MIN_OVERLAP = 0.10 # below this & referenced => divergent
|
|
139
|
+
|
|
140
|
+
_DEFAULT_IMPL_EXTENSIONS = (
|
|
141
|
+
".py", ".js", ".jsx", ".ts", ".tsx", ".go", ".rs", ".java", ".kt",
|
|
142
|
+
".swift", ".rb", ".c", ".h", ".cpp", ".hpp", ".md", ".json", ".yaml",
|
|
143
|
+
".yml", ".sh",
|
|
144
|
+
)
|
|
145
|
+
_DEFAULT_TEST_DIR_NAMES = ("tests", "test", "__tests__", "spec", "specs")
|
|
146
|
+
|
|
147
|
+
# Complexity threshold at/above which semantic alignment is MANDATORY.
|
|
148
|
+
SEMANTIC_ALIGNMENT_COMPLEXITY_THRESHOLD = 3
|
|
149
|
+
|
|
150
|
+
# Gate-result verdicts
|
|
151
|
+
_VERDICT_APPROVE = "APPROVE"
|
|
152
|
+
_VERDICT_CONDITIONAL = "CONDITIONAL"
|
|
153
|
+
_VERDICT_REJECT = "REJECT"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# Data classes
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class SpecItem:
|
|
162
|
+
"""A single extracted acceptance criterion / functional requirement."""
|
|
163
|
+
id: str
|
|
164
|
+
description: str
|
|
165
|
+
source_file: str
|
|
166
|
+
priority: Optional[str] = None # P0/P1/P2 when detected
|
|
167
|
+
|
|
168
|
+
def keywords(self) -> List[str]:
|
|
169
|
+
"""Return deduped, lowercased, stopword-filtered keywords."""
|
|
170
|
+
return _keywords(self.description)
|
|
171
|
+
|
|
172
|
+
def constraints(self) -> List[str]:
|
|
173
|
+
"""Return concrete constraint literals from the description."""
|
|
174
|
+
return _extract_constraint_literals(self.description)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class Finding:
|
|
179
|
+
"""One Gap Report finding for a single spec item."""
|
|
180
|
+
id: str
|
|
181
|
+
description: str
|
|
182
|
+
source_file: str
|
|
183
|
+
status: str # aligned | divergent | missing
|
|
184
|
+
confidence: float # 0.0 - 1.0
|
|
185
|
+
evidence: List[str] # file paths / snippets supporting the verdict
|
|
186
|
+
reason: str # human-readable why
|
|
187
|
+
expected_constraints: List[str] = field(default_factory=list)
|
|
188
|
+
matched_constraints: List[str] = field(default_factory=list)
|
|
189
|
+
unmatched_constraints: List[str] = field(default_factory=list)
|
|
190
|
+
in_impl: bool = False
|
|
191
|
+
in_tests: bool = False
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclass
|
|
195
|
+
class GapReport:
|
|
196
|
+
"""Full Gap Report payload."""
|
|
197
|
+
project: str
|
|
198
|
+
complexity: int
|
|
199
|
+
total: int
|
|
200
|
+
aligned: int
|
|
201
|
+
divergent: int
|
|
202
|
+
missing: int
|
|
203
|
+
verdict: str # APPROVE | CONDITIONAL | REJECT
|
|
204
|
+
score: float # aligned / total
|
|
205
|
+
findings: List[Finding]
|
|
206
|
+
summary: str
|
|
207
|
+
# Emit schema_version so downstream consumers can evolve safely.
|
|
208
|
+
schema_version: str = "1.0.0"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# Helpers
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
def _read_text(path: Path) -> str:
|
|
216
|
+
"""Read file as UTF-8 text, replacing bad bytes. Returns "" on error."""
|
|
217
|
+
try:
|
|
218
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
219
|
+
except (OSError, ValueError):
|
|
220
|
+
return ""
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _keywords(text: str) -> List[str]:
|
|
224
|
+
"""Return deduped, lowercased, stopword-filtered tokens from text."""
|
|
225
|
+
seen: set = set()
|
|
226
|
+
out: List[str] = []
|
|
227
|
+
for tok in _WORD_RE.findall(text):
|
|
228
|
+
low = tok.lower()
|
|
229
|
+
if low in _STOPWORDS:
|
|
230
|
+
continue
|
|
231
|
+
if low not in seen:
|
|
232
|
+
seen.add(low)
|
|
233
|
+
out.append(low)
|
|
234
|
+
return out
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _extract_constraint_literals(text: str) -> List[str]:
|
|
238
|
+
"""Extract concrete numeric + scope constraints from a description.
|
|
239
|
+
|
|
240
|
+
Example: "3 failed attempts per session" -> ["3 attempts", "per session"].
|
|
241
|
+
Returned literals are normalised-lowercase and deduped preserving order.
|
|
242
|
+
"""
|
|
243
|
+
found: List[str] = []
|
|
244
|
+
seen: set = set()
|
|
245
|
+
|
|
246
|
+
for match in _NUMERIC_CONSTRAINT_RE.finditer(text):
|
|
247
|
+
literal = f"{match.group(1)} {match.group(2).lower()}"
|
|
248
|
+
if literal not in seen:
|
|
249
|
+
seen.add(literal)
|
|
250
|
+
found.append(literal)
|
|
251
|
+
|
|
252
|
+
for match in _SCOPE_PHRASE_RE.finditer(text):
|
|
253
|
+
literal = f"per {match.group(1).lower()}"
|
|
254
|
+
if literal not in seen:
|
|
255
|
+
seen.add(literal)
|
|
256
|
+
found.append(literal)
|
|
257
|
+
|
|
258
|
+
return found
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _iter_source_files(
|
|
262
|
+
root: Path,
|
|
263
|
+
extensions: Sequence[str] = _DEFAULT_IMPL_EXTENSIONS,
|
|
264
|
+
exclude_paths: Optional[Sequence[Path]] = None,
|
|
265
|
+
) -> Iterable[Path]:
|
|
266
|
+
"""Yield source files under root with matching extensions.
|
|
267
|
+
|
|
268
|
+
Skips common build / vcs / dep dirs so we don't index node_modules.
|
|
269
|
+
Also skips any path inside the explicit ``exclude_paths`` set (used to
|
|
270
|
+
prevent the spec files themselves from being scanned as implementation,
|
|
271
|
+
which otherwise self-matches every AC).
|
|
272
|
+
"""
|
|
273
|
+
if not root.is_dir():
|
|
274
|
+
return
|
|
275
|
+
skip_dirs = {
|
|
276
|
+
".git", "node_modules", "__pycache__", ".venv", "venv", "dist",
|
|
277
|
+
"build", ".pytest_cache", ".mypy_cache", "target", ".next",
|
|
278
|
+
"coverage", ".claude",
|
|
279
|
+
}
|
|
280
|
+
# The ``phases/`` tree of a crew project holds spec artefacts — never
|
|
281
|
+
# scan those as implementation. Using a ``parts`` check so we catch
|
|
282
|
+
# phases/ at any depth relative to ``root``.
|
|
283
|
+
skip_top_segments = {"phases"}
|
|
284
|
+
resolved_excludes = {Path(p).resolve() for p in (exclude_paths or [])}
|
|
285
|
+
|
|
286
|
+
for path in root.rglob("*"):
|
|
287
|
+
if not path.is_file():
|
|
288
|
+
continue
|
|
289
|
+
rel_parts = path.relative_to(root).parts[:-1]
|
|
290
|
+
if any(part in skip_dirs for part in rel_parts):
|
|
291
|
+
continue
|
|
292
|
+
if rel_parts and rel_parts[0] in skip_top_segments:
|
|
293
|
+
continue
|
|
294
|
+
if path.resolve() in resolved_excludes:
|
|
295
|
+
continue
|
|
296
|
+
if path.suffix.lower() in extensions:
|
|
297
|
+
yield path
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
# Spec extraction
|
|
302
|
+
# ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
def extract_spec_items(
|
|
305
|
+
ac_file: Optional[Path] = None,
|
|
306
|
+
objective_file: Optional[Path] = None,
|
|
307
|
+
extra_sources: Optional[Sequence[Path]] = None,
|
|
308
|
+
) -> List[SpecItem]:
|
|
309
|
+
"""Extract numbered spec items (AC / FR / REQ) from given files.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
ac_file: Path to ``acceptance-criteria.md`` (preferred primary source).
|
|
313
|
+
objective_file: Path to ``objective.md`` (secondary source; FR lines).
|
|
314
|
+
extra_sources: Additional markdown files to scan (e.g. ADRs).
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Deduped list of SpecItem ordered by first appearance.
|
|
318
|
+
"""
|
|
319
|
+
sources: List[Path] = []
|
|
320
|
+
if ac_file and ac_file.exists():
|
|
321
|
+
sources.append(ac_file)
|
|
322
|
+
if objective_file and objective_file.exists():
|
|
323
|
+
sources.append(objective_file)
|
|
324
|
+
if extra_sources:
|
|
325
|
+
for src in extra_sources:
|
|
326
|
+
if src and src.exists():
|
|
327
|
+
sources.append(src)
|
|
328
|
+
|
|
329
|
+
items: List[SpecItem] = []
|
|
330
|
+
seen_ids: set = set()
|
|
331
|
+
|
|
332
|
+
for src in sources:
|
|
333
|
+
text = _read_text(src)
|
|
334
|
+
if not text:
|
|
335
|
+
continue
|
|
336
|
+
for item in _extract_from_text(text, str(src)):
|
|
337
|
+
# Normalise id casing — AC-1 and ac-1 should dedupe.
|
|
338
|
+
key = item.id.upper().replace(" ", "")
|
|
339
|
+
if key in seen_ids:
|
|
340
|
+
# Enrich: merge description if we had a shorter one before.
|
|
341
|
+
continue
|
|
342
|
+
seen_ids.add(key)
|
|
343
|
+
items.append(SpecItem(
|
|
344
|
+
id=item.id.upper().replace(" ", ""),
|
|
345
|
+
description=item.description,
|
|
346
|
+
source_file=item.source_file,
|
|
347
|
+
priority=item.priority,
|
|
348
|
+
))
|
|
349
|
+
|
|
350
|
+
return items
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _extract_from_text(text: str, source_file: str) -> List[SpecItem]:
|
|
354
|
+
"""Pull AC/FR items out of a single markdown body."""
|
|
355
|
+
out: List[SpecItem] = []
|
|
356
|
+
seen: set = set()
|
|
357
|
+
|
|
358
|
+
# Strategy 1 — table rows
|
|
359
|
+
for match in _AC_TABLE_ROW_RE.finditer(text):
|
|
360
|
+
ac_id = match.group("id").strip()
|
|
361
|
+
desc = match.group("desc").strip()
|
|
362
|
+
if "criterion" in desc.lower() or "description" in desc.lower():
|
|
363
|
+
continue # header row
|
|
364
|
+
if ac_id and ac_id not in seen:
|
|
365
|
+
seen.add(ac_id)
|
|
366
|
+
out.append(SpecItem(
|
|
367
|
+
id=ac_id,
|
|
368
|
+
description=desc,
|
|
369
|
+
source_file=source_file,
|
|
370
|
+
priority=_detect_priority(desc),
|
|
371
|
+
))
|
|
372
|
+
|
|
373
|
+
# Strategy 2 — bulleted list "- AC-1: Given ..., When ..., Then ..."
|
|
374
|
+
for match in _AC_LIST_RE.finditer(text):
|
|
375
|
+
ac_id = match.group("id").strip()
|
|
376
|
+
desc = match.group("desc").strip()
|
|
377
|
+
if ac_id and ac_id not in seen:
|
|
378
|
+
seen.add(ac_id)
|
|
379
|
+
out.append(SpecItem(
|
|
380
|
+
id=ac_id,
|
|
381
|
+
description=desc,
|
|
382
|
+
source_file=source_file,
|
|
383
|
+
priority=_detect_priority(desc),
|
|
384
|
+
))
|
|
385
|
+
|
|
386
|
+
# Strategy 3 — inline "AC1: Given ... When ... Then ..."
|
|
387
|
+
for match in _AC_INLINE_RE.finditer(text):
|
|
388
|
+
ac_id = match.group("id").strip()
|
|
389
|
+
desc = match.group("desc").strip()
|
|
390
|
+
if ac_id and ac_id not in seen:
|
|
391
|
+
seen.add(ac_id)
|
|
392
|
+
out.append(SpecItem(
|
|
393
|
+
id=ac_id,
|
|
394
|
+
description=desc,
|
|
395
|
+
source_file=source_file,
|
|
396
|
+
priority=_detect_priority(desc),
|
|
397
|
+
))
|
|
398
|
+
|
|
399
|
+
# Strategy 4 — bare id mentions as last resort, pair with surrounding line
|
|
400
|
+
if not out:
|
|
401
|
+
for match in _SPEC_ID_RE.finditer(text):
|
|
402
|
+
ac_id = match.group(0)
|
|
403
|
+
if ac_id in seen:
|
|
404
|
+
continue
|
|
405
|
+
# Capture up to 240 chars of context after the id as description.
|
|
406
|
+
start = match.end()
|
|
407
|
+
tail = text[start:start + 240].strip()
|
|
408
|
+
if not tail:
|
|
409
|
+
continue
|
|
410
|
+
# Cut at next newline-newline (end of paragraph) if present.
|
|
411
|
+
end_idx = tail.find("\n\n")
|
|
412
|
+
if end_idx >= 0:
|
|
413
|
+
tail = tail[:end_idx].strip()
|
|
414
|
+
seen.add(ac_id)
|
|
415
|
+
out.append(SpecItem(
|
|
416
|
+
id=ac_id,
|
|
417
|
+
description=tail[:240],
|
|
418
|
+
source_file=source_file,
|
|
419
|
+
priority=_detect_priority(tail),
|
|
420
|
+
))
|
|
421
|
+
|
|
422
|
+
return out
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
_PRIORITY_RE = re.compile(r"\bP([012])\b")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _detect_priority(text: str) -> Optional[str]:
|
|
429
|
+
match = _PRIORITY_RE.search(text)
|
|
430
|
+
return f"P{match.group(1)}" if match else None
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# ---------------------------------------------------------------------------
|
|
434
|
+
# Alignment scoring
|
|
435
|
+
# ---------------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
def _find_references(corpus: Dict[str, str], spec_id: str) -> List[Tuple[str, str]]:
|
|
438
|
+
"""Return list of (file_path, context_snippet) for occurrences of spec_id.
|
|
439
|
+
|
|
440
|
+
Uses case-insensitive literal search. Returns up to 20 hits per id to
|
|
441
|
+
keep reports bounded.
|
|
442
|
+
"""
|
|
443
|
+
needle = spec_id.upper()
|
|
444
|
+
needle_lower = spec_id.lower()
|
|
445
|
+
results: List[Tuple[str, str]] = []
|
|
446
|
+
for file_path, text in corpus.items():
|
|
447
|
+
if not text:
|
|
448
|
+
continue
|
|
449
|
+
# Quick reject — case-insensitive substring check.
|
|
450
|
+
lower = text.lower()
|
|
451
|
+
if needle_lower not in lower:
|
|
452
|
+
continue
|
|
453
|
+
# Collect each occurrence's context window.
|
|
454
|
+
idx = 0
|
|
455
|
+
while True:
|
|
456
|
+
pos = lower.find(needle_lower, idx)
|
|
457
|
+
if pos < 0:
|
|
458
|
+
break
|
|
459
|
+
start = max(0, pos - _CONTEXT_WINDOW)
|
|
460
|
+
end = min(len(text), pos + len(needle) + _CONTEXT_WINDOW)
|
|
461
|
+
snippet = text[start:end]
|
|
462
|
+
results.append((file_path, snippet))
|
|
463
|
+
idx = pos + len(needle)
|
|
464
|
+
if len(results) >= 20:
|
|
465
|
+
return results
|
|
466
|
+
return results
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _keyword_overlap(spec_keywords: Sequence[str], context: str) -> float:
|
|
470
|
+
"""Ratio of spec keywords that appear in context text. Range 0.0-1.0."""
|
|
471
|
+
if not spec_keywords:
|
|
472
|
+
return 1.0
|
|
473
|
+
context_lower = context.lower()
|
|
474
|
+
hits = sum(1 for kw in spec_keywords if kw in context_lower)
|
|
475
|
+
return hits / len(spec_keywords)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _best_overlap(
|
|
479
|
+
spec_keywords: Sequence[str],
|
|
480
|
+
refs: Sequence[Tuple[str, str]],
|
|
481
|
+
) -> float:
|
|
482
|
+
"""Return highest keyword-overlap ratio across all references."""
|
|
483
|
+
if not refs:
|
|
484
|
+
return 0.0
|
|
485
|
+
return max(_keyword_overlap(spec_keywords, snippet) for _, snippet in refs)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _constraints_matched(
|
|
489
|
+
constraints: Sequence[str],
|
|
490
|
+
context: str,
|
|
491
|
+
) -> Tuple[List[str], List[str]]:
|
|
492
|
+
"""Return (matched, unmatched) constraint literals against context."""
|
|
493
|
+
matched: List[str] = []
|
|
494
|
+
unmatched: List[str] = []
|
|
495
|
+
lower = context.lower()
|
|
496
|
+
for literal in constraints:
|
|
497
|
+
if literal.lower() in lower:
|
|
498
|
+
matched.append(literal)
|
|
499
|
+
else:
|
|
500
|
+
unmatched.append(literal)
|
|
501
|
+
return matched, unmatched
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _aggregate_constraints(
|
|
505
|
+
constraints: Sequence[str],
|
|
506
|
+
refs: Sequence[Tuple[str, str]],
|
|
507
|
+
) -> Tuple[List[str], List[str]]:
|
|
508
|
+
"""Aggregate constraint matching across ALL reference contexts.
|
|
509
|
+
|
|
510
|
+
A constraint is counted as matched if it appears in ANY reference snippet.
|
|
511
|
+
"""
|
|
512
|
+
if not constraints:
|
|
513
|
+
return [], []
|
|
514
|
+
combined = "\n".join(snippet for _, snippet in refs)
|
|
515
|
+
return _constraints_matched(constraints, combined)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _classify(
|
|
519
|
+
item: SpecItem,
|
|
520
|
+
impl_refs: Sequence[Tuple[str, str]],
|
|
521
|
+
test_refs: Sequence[Tuple[str, str]],
|
|
522
|
+
) -> Finding:
|
|
523
|
+
"""Produce a Finding for a single spec item given impl + test references."""
|
|
524
|
+
in_impl = bool(impl_refs)
|
|
525
|
+
in_tests = bool(test_refs)
|
|
526
|
+
|
|
527
|
+
spec_keywords = item.keywords()
|
|
528
|
+
all_refs = list(impl_refs) + list(test_refs)
|
|
529
|
+
|
|
530
|
+
overlap = _best_overlap(spec_keywords, all_refs)
|
|
531
|
+
matched_c, unmatched_c = _aggregate_constraints(item.constraints(), all_refs)
|
|
532
|
+
|
|
533
|
+
evidence = [fp for fp, _ in all_refs[:8]]
|
|
534
|
+
|
|
535
|
+
# MISSING — literal id absent from both corpora.
|
|
536
|
+
if not in_impl and not in_tests:
|
|
537
|
+
return Finding(
|
|
538
|
+
id=item.id,
|
|
539
|
+
description=item.description,
|
|
540
|
+
source_file=item.source_file,
|
|
541
|
+
status="missing",
|
|
542
|
+
confidence=0.9,
|
|
543
|
+
evidence=[],
|
|
544
|
+
reason=(
|
|
545
|
+
f"Spec item {item.id} has no occurrence in implementation "
|
|
546
|
+
f"corpus or test corpus. Expected at least a comment or "
|
|
547
|
+
f"test-name reference."
|
|
548
|
+
),
|
|
549
|
+
expected_constraints=item.constraints(),
|
|
550
|
+
matched_constraints=[],
|
|
551
|
+
unmatched_constraints=list(item.constraints()),
|
|
552
|
+
in_impl=False,
|
|
553
|
+
in_tests=False,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
# DIVERGENT path 1 — referenced in one corpus only.
|
|
557
|
+
partial = (in_impl and not in_tests) or (in_tests and not in_impl)
|
|
558
|
+
|
|
559
|
+
# DIVERGENT path 2 — has numeric/scope constraints that aren't matched.
|
|
560
|
+
has_unmatched_constraints = bool(unmatched_c) and bool(item.constraints())
|
|
561
|
+
|
|
562
|
+
# DIVERGENT path 3 — keyword overlap below threshold
|
|
563
|
+
low_overlap = overlap < _ALIGNED_OVERLAP_THRESHOLD
|
|
564
|
+
|
|
565
|
+
if partial or has_unmatched_constraints or low_overlap:
|
|
566
|
+
# Build specific reason.
|
|
567
|
+
reasons: List[str] = []
|
|
568
|
+
if partial:
|
|
569
|
+
if in_impl and not in_tests:
|
|
570
|
+
reasons.append(
|
|
571
|
+
f"{item.id} referenced in implementation but no matching "
|
|
572
|
+
f"test asserts it"
|
|
573
|
+
)
|
|
574
|
+
else:
|
|
575
|
+
reasons.append(
|
|
576
|
+
f"{item.id} referenced in tests but not in implementation"
|
|
577
|
+
)
|
|
578
|
+
if has_unmatched_constraints:
|
|
579
|
+
reasons.append(
|
|
580
|
+
f"constraint(s) {unmatched_c} from spec not found near "
|
|
581
|
+
f"{item.id} references"
|
|
582
|
+
)
|
|
583
|
+
if low_overlap and not (partial or has_unmatched_constraints):
|
|
584
|
+
reasons.append(
|
|
585
|
+
f"implementation context near {item.id} overlaps only "
|
|
586
|
+
f"{overlap:.0%} with spec keywords — likely divergent intent"
|
|
587
|
+
)
|
|
588
|
+
# Confidence: high when we have concrete constraint gaps, medium
|
|
589
|
+
# when only overlap is low.
|
|
590
|
+
confidence = 0.85 if has_unmatched_constraints else (
|
|
591
|
+
0.7 if partial else 0.6
|
|
592
|
+
)
|
|
593
|
+
return Finding(
|
|
594
|
+
id=item.id,
|
|
595
|
+
description=item.description,
|
|
596
|
+
source_file=item.source_file,
|
|
597
|
+
status="divergent",
|
|
598
|
+
confidence=confidence,
|
|
599
|
+
evidence=evidence,
|
|
600
|
+
reason="; ".join(reasons),
|
|
601
|
+
expected_constraints=item.constraints(),
|
|
602
|
+
matched_constraints=matched_c,
|
|
603
|
+
unmatched_constraints=unmatched_c,
|
|
604
|
+
in_impl=in_impl,
|
|
605
|
+
in_tests=in_tests,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
# ALIGNED — in both corpora, overlap >= threshold, all constraints matched.
|
|
609
|
+
return Finding(
|
|
610
|
+
id=item.id,
|
|
611
|
+
description=item.description,
|
|
612
|
+
source_file=item.source_file,
|
|
613
|
+
status="aligned",
|
|
614
|
+
confidence=min(0.95, 0.5 + overlap * 0.5),
|
|
615
|
+
evidence=evidence,
|
|
616
|
+
reason=(
|
|
617
|
+
f"{item.id} referenced in implementation ({sum(1 for _ in impl_refs)} "
|
|
618
|
+
f"hit(s)) and tests ({sum(1 for _ in test_refs)} hit(s)); "
|
|
619
|
+
f"keyword overlap {overlap:.0%} >= "
|
|
620
|
+
f"{_ALIGNED_OVERLAP_THRESHOLD:.0%}"
|
|
621
|
+
),
|
|
622
|
+
expected_constraints=item.constraints(),
|
|
623
|
+
matched_constraints=matched_c,
|
|
624
|
+
unmatched_constraints=[],
|
|
625
|
+
in_impl=True,
|
|
626
|
+
in_tests=True,
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
# ---------------------------------------------------------------------------
|
|
631
|
+
# Report assembly
|
|
632
|
+
# ---------------------------------------------------------------------------
|
|
633
|
+
|
|
634
|
+
def _build_corpus(
|
|
635
|
+
directory: Path,
|
|
636
|
+
extensions: Sequence[str] = _DEFAULT_IMPL_EXTENSIONS,
|
|
637
|
+
file_limit: int = 5000,
|
|
638
|
+
exclude_paths: Optional[Sequence[Path]] = None,
|
|
639
|
+
) -> Dict[str, str]:
|
|
640
|
+
"""Build a {path: text} corpus for a directory, bounded by file_limit.
|
|
641
|
+
|
|
642
|
+
``exclude_paths`` lets callers keep the spec files (``acceptance-criteria.md``,
|
|
643
|
+
``objective.md``) out of the implementation scan — otherwise every AC
|
|
644
|
+
self-matches and every project looks 100% aligned.
|
|
645
|
+
"""
|
|
646
|
+
corpus: Dict[str, str] = {}
|
|
647
|
+
if not directory.is_dir():
|
|
648
|
+
return corpus
|
|
649
|
+
count = 0
|
|
650
|
+
for path in _iter_source_files(directory, extensions, exclude_paths=exclude_paths):
|
|
651
|
+
text = _read_text(path)
|
|
652
|
+
if not text:
|
|
653
|
+
continue
|
|
654
|
+
corpus[str(path)] = text
|
|
655
|
+
count += 1
|
|
656
|
+
if count >= file_limit:
|
|
657
|
+
break
|
|
658
|
+
return corpus
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _build_corpus_from_files(files: Sequence[Path]) -> Dict[str, str]:
|
|
662
|
+
"""Build a {path: text} corpus from an explicit file list."""
|
|
663
|
+
corpus: Dict[str, str] = {}
|
|
664
|
+
for path in files:
|
|
665
|
+
if not path.exists() or not path.is_file():
|
|
666
|
+
continue
|
|
667
|
+
text = _read_text(path)
|
|
668
|
+
if text:
|
|
669
|
+
corpus[str(path)] = text
|
|
670
|
+
return corpus
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _split_impl_and_tests(corpus: Dict[str, str]) -> Tuple[Dict[str, str], Dict[str, str]]:
|
|
674
|
+
"""Partition an arbitrary corpus into (impl_corpus, test_corpus).
|
|
675
|
+
|
|
676
|
+
A file is considered a test file when any ancestor directory matches
|
|
677
|
+
a known test-dir name (``tests``, ``test``, ``spec``, etc.) or when
|
|
678
|
+
the filename starts with ``test_`` or ends with ``_test.py`` /
|
|
679
|
+
``.test.ts`` / ``.spec.ts``.
|
|
680
|
+
"""
|
|
681
|
+
impl: Dict[str, str] = {}
|
|
682
|
+
tests: Dict[str, str] = {}
|
|
683
|
+
|
|
684
|
+
for path_str, text in corpus.items():
|
|
685
|
+
p = Path(path_str)
|
|
686
|
+
is_test = False
|
|
687
|
+
for part in p.parts:
|
|
688
|
+
if part.lower() in _DEFAULT_TEST_DIR_NAMES:
|
|
689
|
+
is_test = True
|
|
690
|
+
break
|
|
691
|
+
if not is_test:
|
|
692
|
+
name = p.name.lower()
|
|
693
|
+
if (name.startswith("test_") or name.endswith("_test.py") or
|
|
694
|
+
name.endswith(".test.ts") or name.endswith(".test.tsx") or
|
|
695
|
+
name.endswith(".test.js") or name.endswith(".spec.ts") or
|
|
696
|
+
name.endswith(".spec.js")):
|
|
697
|
+
is_test = True
|
|
698
|
+
if is_test:
|
|
699
|
+
tests[path_str] = text
|
|
700
|
+
else:
|
|
701
|
+
impl[path_str] = text
|
|
702
|
+
|
|
703
|
+
return impl, tests
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def generate_gap_report(
|
|
707
|
+
spec_items: Sequence[SpecItem],
|
|
708
|
+
impl_corpus: Dict[str, str],
|
|
709
|
+
test_corpus: Dict[str, str],
|
|
710
|
+
project: str = "unknown",
|
|
711
|
+
complexity: int = 0,
|
|
712
|
+
) -> GapReport:
|
|
713
|
+
"""Produce a complete Gap Report from spec items + impl/test corpora.
|
|
714
|
+
|
|
715
|
+
Args:
|
|
716
|
+
spec_items: Extracted AC/FR items from clarify-phase specs.
|
|
717
|
+
impl_corpus: {file_path: text} for non-test source files.
|
|
718
|
+
test_corpus: {file_path: text} for test files.
|
|
719
|
+
project: Project identifier (for the report header).
|
|
720
|
+
complexity: Project complexity score (0-7).
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
GapReport dataclass — ``asdict`` it for JSON output.
|
|
724
|
+
"""
|
|
725
|
+
findings: List[Finding] = []
|
|
726
|
+
|
|
727
|
+
for item in spec_items:
|
|
728
|
+
impl_refs = _find_references(impl_corpus, item.id)
|
|
729
|
+
test_refs = _find_references(test_corpus, item.id)
|
|
730
|
+
findings.append(_classify(item, impl_refs, test_refs))
|
|
731
|
+
|
|
732
|
+
total = len(findings)
|
|
733
|
+
aligned = sum(1 for f in findings if f.status == "aligned")
|
|
734
|
+
divergent = sum(1 for f in findings if f.status == "divergent")
|
|
735
|
+
missing = sum(1 for f in findings if f.status == "missing")
|
|
736
|
+
|
|
737
|
+
# Score = aligned ratio (1.0 when no specs — vacuously aligned).
|
|
738
|
+
score = (aligned / total) if total > 0 else 1.0
|
|
739
|
+
|
|
740
|
+
# Verdict: APPROVE only when all aligned; REJECT when anything missing;
|
|
741
|
+
# CONDITIONAL when only divergent findings.
|
|
742
|
+
if total == 0:
|
|
743
|
+
verdict = _VERDICT_APPROVE
|
|
744
|
+
summary = "No numbered spec items found — advisory skip."
|
|
745
|
+
elif missing > 0:
|
|
746
|
+
verdict = _VERDICT_REJECT
|
|
747
|
+
summary = (
|
|
748
|
+
f"{missing} spec item(s) missing from implementation. "
|
|
749
|
+
f"Hard reject — implement referenced AC before approval."
|
|
750
|
+
)
|
|
751
|
+
elif divergent > 0:
|
|
752
|
+
verdict = _VERDICT_CONDITIONAL
|
|
753
|
+
summary = (
|
|
754
|
+
f"{divergent} spec item(s) diverge from stated intent. "
|
|
755
|
+
f"Fix divergences or explicitly document the deviation."
|
|
756
|
+
)
|
|
757
|
+
else:
|
|
758
|
+
verdict = _VERDICT_APPROVE
|
|
759
|
+
summary = f"All {aligned} spec items aligned with implementation."
|
|
760
|
+
|
|
761
|
+
return GapReport(
|
|
762
|
+
project=project,
|
|
763
|
+
complexity=complexity,
|
|
764
|
+
total=total,
|
|
765
|
+
aligned=aligned,
|
|
766
|
+
divergent=divergent,
|
|
767
|
+
missing=missing,
|
|
768
|
+
verdict=verdict,
|
|
769
|
+
score=round(score, 3),
|
|
770
|
+
findings=findings,
|
|
771
|
+
summary=summary,
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
# ---------------------------------------------------------------------------
|
|
776
|
+
# ADR constraint extraction (conservative MVP)
|
|
777
|
+
# ---------------------------------------------------------------------------
|
|
778
|
+
|
|
779
|
+
def extract_adr_constraints(adr_dir: Path) -> List[Dict[str, str]]:
|
|
780
|
+
"""Extract MUST/SHALL directives from ADR markdown files.
|
|
781
|
+
|
|
782
|
+
Conservative regex-only approach: pulls every line containing a modal
|
|
783
|
+
directive (MUST / MUST NOT / SHALL / SHALL NOT / SHOULD NOT) and returns
|
|
784
|
+
``{source, directive, statement}``. A follow-up iteration could run these
|
|
785
|
+
statements against the implementation corpus and flag violations.
|
|
786
|
+
|
|
787
|
+
Returns:
|
|
788
|
+
List of dicts; empty if dir missing or no directives.
|
|
789
|
+
|
|
790
|
+
Limitations:
|
|
791
|
+
- Line-granular only (no multi-line parsing).
|
|
792
|
+
- No negation / scope reasoning — downstream code must still decide
|
|
793
|
+
whether a match is a violation or a legitimate exception.
|
|
794
|
+
- Treats every modal-directive hit as equally important.
|
|
795
|
+
"""
|
|
796
|
+
out: List[Dict[str, str]] = []
|
|
797
|
+
if not adr_dir.is_dir():
|
|
798
|
+
return out
|
|
799
|
+
|
|
800
|
+
for md in sorted(adr_dir.rglob("*.md")):
|
|
801
|
+
text = _read_text(md)
|
|
802
|
+
if not text:
|
|
803
|
+
continue
|
|
804
|
+
for line_no, line in enumerate(text.splitlines(), start=1):
|
|
805
|
+
match = _MODAL_DIRECTIVE_RE.search(line)
|
|
806
|
+
if not match:
|
|
807
|
+
continue
|
|
808
|
+
# Skip section headers and front-matter noise.
|
|
809
|
+
stripped = line.strip()
|
|
810
|
+
if stripped.startswith("#") or stripped.startswith("---"):
|
|
811
|
+
continue
|
|
812
|
+
out.append({
|
|
813
|
+
"source": str(md),
|
|
814
|
+
"line": str(line_no),
|
|
815
|
+
"directive": match.group(1),
|
|
816
|
+
"statement": stripped[:240],
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
return out
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
# ---------------------------------------------------------------------------
|
|
823
|
+
# Orchestration — project-dir convention
|
|
824
|
+
# ---------------------------------------------------------------------------
|
|
825
|
+
|
|
826
|
+
def review_project(
|
|
827
|
+
project_dir: Path,
|
|
828
|
+
project_name: str = "",
|
|
829
|
+
complexity: int = 0,
|
|
830
|
+
ac_file: Optional[Path] = None,
|
|
831
|
+
objective_file: Optional[Path] = None,
|
|
832
|
+
impl_dir: Optional[Path] = None,
|
|
833
|
+
test_dir: Optional[Path] = None,
|
|
834
|
+
) -> GapReport:
|
|
835
|
+
"""Run a full semantic review over a project directory.
|
|
836
|
+
|
|
837
|
+
Expected layout (overrideable via args)::
|
|
838
|
+
|
|
839
|
+
<project_dir>/
|
|
840
|
+
phases/
|
|
841
|
+
clarify/
|
|
842
|
+
acceptance-criteria.md <-- ac_file default
|
|
843
|
+
objective.md <-- objective_file default
|
|
844
|
+
<impl sources> <-- impl_dir default = project_dir
|
|
845
|
+
tests/ <-- test_dir default
|
|
846
|
+
|
|
847
|
+
Args:
|
|
848
|
+
project_dir: Root dir containing phases/ and source.
|
|
849
|
+
project_name: Name for the report header.
|
|
850
|
+
complexity: Project complexity 0-7.
|
|
851
|
+
ac_file: Override path to acceptance-criteria.md.
|
|
852
|
+
objective_file: Override path to objective.md.
|
|
853
|
+
impl_dir: Override implementation-scan root. Defaults to project_dir.
|
|
854
|
+
test_dir: Override test-scan root. When None, impl_dir is scanned and
|
|
855
|
+
files are partitioned by filename + dir heuristic.
|
|
856
|
+
|
|
857
|
+
Returns:
|
|
858
|
+
GapReport.
|
|
859
|
+
"""
|
|
860
|
+
ac_path = ac_file or (project_dir / "phases" / "clarify" / "acceptance-criteria.md")
|
|
861
|
+
obj_path = objective_file or (project_dir / "phases" / "clarify" / "objective.md")
|
|
862
|
+
|
|
863
|
+
spec_items = extract_spec_items(ac_file=ac_path, objective_file=obj_path)
|
|
864
|
+
|
|
865
|
+
impl_root = impl_dir or project_dir
|
|
866
|
+
|
|
867
|
+
# Never scan the spec files themselves — otherwise every AC
|
|
868
|
+
# self-matches and overlap is trivially 100%.
|
|
869
|
+
spec_excludes: List[Path] = []
|
|
870
|
+
if ac_path and ac_path.exists():
|
|
871
|
+
spec_excludes.append(ac_path)
|
|
872
|
+
if obj_path and obj_path.exists():
|
|
873
|
+
spec_excludes.append(obj_path)
|
|
874
|
+
|
|
875
|
+
if test_dir:
|
|
876
|
+
impl_corpus = _build_corpus(impl_root, exclude_paths=spec_excludes)
|
|
877
|
+
test_corpus = _build_corpus(test_dir, exclude_paths=spec_excludes)
|
|
878
|
+
# Avoid double-counting: strip test files from impl_corpus.
|
|
879
|
+
test_paths = set(test_corpus.keys())
|
|
880
|
+
impl_corpus = {p: t for p, t in impl_corpus.items() if p not in test_paths}
|
|
881
|
+
else:
|
|
882
|
+
full_corpus = _build_corpus(impl_root, exclude_paths=spec_excludes)
|
|
883
|
+
impl_corpus, test_corpus = _split_impl_and_tests(full_corpus)
|
|
884
|
+
|
|
885
|
+
project = project_name or project_dir.name
|
|
886
|
+
|
|
887
|
+
return generate_gap_report(
|
|
888
|
+
spec_items=spec_items,
|
|
889
|
+
impl_corpus=impl_corpus,
|
|
890
|
+
test_corpus=test_corpus,
|
|
891
|
+
project=project,
|
|
892
|
+
complexity=complexity,
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
# ---------------------------------------------------------------------------
|
|
897
|
+
# JSON serialisation
|
|
898
|
+
# ---------------------------------------------------------------------------
|
|
899
|
+
|
|
900
|
+
def report_to_dict(report: GapReport) -> Dict[str, Any]:
|
|
901
|
+
"""Convert a GapReport (incl. nested Findings) to a JSON-safe dict."""
|
|
902
|
+
return asdict(report)
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def run_semantic_review(project_dir: Path, complexity: int = 3) -> Dict[str, Any]:
|
|
906
|
+
"""Public entry point used by ``scripts/platform/guard_pipeline.py``.
|
|
907
|
+
|
|
908
|
+
Thin wrapper that runs ``review_project`` on a directory and returns the
|
|
909
|
+
JSON-safe dict ``GapReport`` representation. Returns the dict shape the
|
|
910
|
+
guard pipeline expects — keys: ``verdict``, ``findings`` (list of dicts
|
|
911
|
+
with ``message`` / ``rule_id`` / ``file`` / ``line``), ``summary``.
|
|
912
|
+
|
|
913
|
+
Fail-open responsibility lives in the caller — if the project layout
|
|
914
|
+
has no clarify-phase spec files (the common case for the wicked-garden
|
|
915
|
+
repo itself), ``review_project`` returns a vacuously-APPROVE report
|
|
916
|
+
with zero findings, which is exactly what the guard pipeline needs to
|
|
917
|
+
silently no-op.
|
|
918
|
+
"""
|
|
919
|
+
report = review_project(project_dir=project_dir, complexity=complexity)
|
|
920
|
+
return report_to_dict(report)
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
# ---------------------------------------------------------------------------
|
|
924
|
+
# CLI
|
|
925
|
+
# ---------------------------------------------------------------------------
|
|
926
|
+
|
|
927
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
928
|
+
parser = argparse.ArgumentParser(
|
|
929
|
+
prog="semantic_review.py",
|
|
930
|
+
description="Semantic reviewer — spec-to-code alignment (issue #444).",
|
|
931
|
+
)
|
|
932
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
933
|
+
|
|
934
|
+
p_review = sub.add_parser("review", help="Run full semantic review")
|
|
935
|
+
p_review.add_argument("--project-dir", required=True)
|
|
936
|
+
p_review.add_argument("--project-name", default="")
|
|
937
|
+
p_review.add_argument("--complexity", type=int, default=0)
|
|
938
|
+
p_review.add_argument("--ac-file", default=None)
|
|
939
|
+
p_review.add_argument("--objective-file", default=None)
|
|
940
|
+
p_review.add_argument("--impl-dir", default=None)
|
|
941
|
+
p_review.add_argument("--test-dir", default=None)
|
|
942
|
+
p_review.add_argument("--output", default=None, help="Write JSON report to this path")
|
|
943
|
+
|
|
944
|
+
p_constraints = sub.add_parser("constraints", help="Extract ADR constraints (MVP)")
|
|
945
|
+
p_constraints.add_argument("--adr-dir", required=True)
|
|
946
|
+
p_constraints.add_argument("--output", default=None)
|
|
947
|
+
|
|
948
|
+
return parser
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def _cli_review(args: argparse.Namespace) -> int:
|
|
952
|
+
project_dir = Path(args.project_dir).resolve()
|
|
953
|
+
if not project_dir.is_dir():
|
|
954
|
+
print(f"error: project-dir not found: {project_dir}", file=sys.stderr)
|
|
955
|
+
return 2
|
|
956
|
+
|
|
957
|
+
report = review_project(
|
|
958
|
+
project_dir=project_dir,
|
|
959
|
+
project_name=args.project_name,
|
|
960
|
+
complexity=args.complexity,
|
|
961
|
+
ac_file=Path(args.ac_file).resolve() if args.ac_file else None,
|
|
962
|
+
objective_file=Path(args.objective_file).resolve() if args.objective_file else None,
|
|
963
|
+
impl_dir=Path(args.impl_dir).resolve() if args.impl_dir else None,
|
|
964
|
+
test_dir=Path(args.test_dir).resolve() if args.test_dir else None,
|
|
965
|
+
)
|
|
966
|
+
payload = report_to_dict(report)
|
|
967
|
+
data = json.dumps(payload, indent=2)
|
|
968
|
+
if args.output:
|
|
969
|
+
out_path = Path(args.output).resolve()
|
|
970
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
971
|
+
out_path.write_text(data)
|
|
972
|
+
else:
|
|
973
|
+
sys.stdout.write(data + "\n")
|
|
974
|
+
# Non-zero exit only when verdict is REJECT — CONDITIONAL is advisory.
|
|
975
|
+
if report.verdict == _VERDICT_REJECT:
|
|
976
|
+
return 1
|
|
977
|
+
return 0
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
def _cli_constraints(args: argparse.Namespace) -> int:
|
|
981
|
+
adr_dir = Path(args.adr_dir).resolve()
|
|
982
|
+
constraints = extract_adr_constraints(adr_dir)
|
|
983
|
+
data = json.dumps({"constraints": constraints, "count": len(constraints)}, indent=2)
|
|
984
|
+
if args.output:
|
|
985
|
+
out_path = Path(args.output).resolve()
|
|
986
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
987
|
+
out_path.write_text(data)
|
|
988
|
+
else:
|
|
989
|
+
sys.stdout.write(data + "\n")
|
|
990
|
+
return 0
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
994
|
+
parser = _build_parser()
|
|
995
|
+
args = parser.parse_args(argv)
|
|
996
|
+
if args.command == "review":
|
|
997
|
+
return _cli_review(args)
|
|
998
|
+
if args.command == "constraints":
|
|
999
|
+
return _cli_constraints(args)
|
|
1000
|
+
parser.error(f"unknown command: {args.command}")
|
|
1001
|
+
return 2
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
if __name__ == "__main__":
|
|
1005
|
+
raise SystemExit(main())
|