@nathapp/nax 0.18.1
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/.gitlab-ci.yml +96 -0
- package/BRIEF.md +140 -0
- package/CHANGELOG.md +60 -0
- package/CLAUDE.md +159 -0
- package/README.md +373 -0
- package/US-007-IMPLEMENTATION.md +139 -0
- package/bin/nax.ts +930 -0
- package/biome.json +14 -0
- package/bun.lock +168 -0
- package/bunfig.toml +11 -0
- package/docs/20260216-fix-plan-context-review.md +56 -0
- package/docs/20260216-relentless-vs-ngent-comparison.md +208 -0
- package/docs/20260216-v02-plan.md +136 -0
- package/docs/20260216-v02-review.md +685 -0
- package/docs/20260217-dogfood-findings.md +56 -0
- package/docs/20260217-p2-plus-plan.md +117 -0
- package/docs/20260217-partial-fixes-plan.md +62 -0
- package/docs/20260217-plan-analyze-spec.md +117 -0
- package/docs/20260217-post-impl-review.md +1137 -0
- package/docs/20260217-quick-wins-plan.md +66 -0
- package/docs/20260217-split-runner-plan.md +75 -0
- package/docs/20260217-v03-impl-plan.md +80 -0
- package/docs/20260217-v03-post-impl-review.md +589 -0
- package/docs/20260217-v04-impl-plan.md +86 -0
- package/docs/20260217-v05-post-impl-review.md +850 -0
- package/docs/20260217-v06-post-impl-review.md +817 -0
- package/docs/20260218-adr003-port-plan.md +151 -0
- package/docs/20260218-review-adr003-verification.md +175 -0
- package/docs/20260219-fix-plan-bug16-19.md +79 -0
- package/docs/20260219-fix-plan-bug20-22.md +114 -0
- package/docs/20260219-plan-llm-routing.md +116 -0
- package/docs/20260219-review-bug20-22-fixes.md +135 -0
- package/docs/20260219-routing-baseline-keyword.md +63 -0
- package/docs/20260220-plan-structured-logging-p1.md +80 -0
- package/docs/20260220-plan-structured-logging-p2.md +37 -0
- package/docs/20260220-review-llm-routing.md +180 -0
- package/docs/20260220-review-post-fix-llm-routing.md +70 -0
- package/docs/20260221-fix-plan-relevantfiles-split.md +101 -0
- package/docs/20260221-fix-plan-routing-mode.md +125 -0
- package/docs/20260221-review-v0.9-implementation.md +379 -0
- package/docs/20260222-fix-plan-v091-routing-isolation.md +197 -0
- package/docs/20260223-fix-plan-prompt-audit.md +62 -0
- package/docs/20260224-nax-roadmap-phases.md +189 -0
- package/docs/20260225-phase2-llm-service-layer.md +401 -0
- package/docs/20260225-review-v0.10.1.md +187 -0
- package/docs/20260303-v010-implementation-plan.md +165 -0
- package/docs/CLAUDE.md.bak +191 -0
- package/docs/ROADMAP.md +165 -0
- package/docs/SPEC-rectification.md +0 -0
- package/docs/SPEC.md +324 -0
- package/docs/US-001-plugin-loading-verification.md +152 -0
- package/docs/architecture-analysis.md +1076 -0
- package/docs/bugs/BUG-21-escalation-null-attempts.md +48 -0
- package/docs/bugs-from-dogfood-run-c.md +243 -0
- package/docs/code-review-20260228.md +612 -0
- package/docs/code-review-v0.15.0.md +629 -0
- package/docs/hook-lifecycle-test-plan.md +149 -0
- package/docs/releases/v0.11.0-and-earlier.md +20 -0
- package/docs/releases/v0.12.0.md +15 -0
- package/docs/releases/v0.13.0.md +14 -0
- package/docs/releases/v0.14.0.md +20 -0
- package/docs/releases/v0.14.1.md +36 -0
- package/docs/releases/v0.14.2.md +51 -0
- package/docs/releases/v0.14.3.md +174 -0
- package/docs/releases/v0.14.4.md +94 -0
- package/docs/releases/v0.15.0.md +502 -0
- package/docs/releases/v0.15.1.md +170 -0
- package/docs/releases/v0.15.3.md +193 -0
- package/docs/specs/status-file-v0.10.1.md +812 -0
- package/docs/v0.10-global-config.md +206 -0
- package/docs/v0.10-plugin-system.md +415 -0
- package/docs/v0.10-prompt-optimizer.md +234 -0
- package/docs/v0.3-spec.md +244 -0
- package/docs/v0.4-spec.md +140 -0
- package/docs/v0.5-spec.md +237 -0
- package/docs/v0.6-spec.md +371 -0
- package/docs/v0.7-spec.md +177 -0
- package/docs/v0.8-llm-routing.md +206 -0
- package/docs/v0.8-structured-logging.md +132 -0
- package/docs/v0.9.3-prompt-audit.md +112 -0
- package/examples/plugins/console-reporter/index.test.ts +207 -0
- package/examples/plugins/console-reporter/index.ts +110 -0
- package/nax/config.json +147 -0
- package/nax/features/bugfix-v0171/prd.json +52 -0
- package/nax/features/config-management/prd.json +108 -0
- package/nax/features/config-management/progress.txt +5 -0
- package/nax/features/diagnose/acceptance.test.ts +412 -0
- package/nax/features/diagnose/prd.json +41 -0
- package/nax/features/orchestration-fixes/prd.json +89 -0
- package/nax/features/orchestration-fixes/progress.txt +1 -0
- package/nax/features/plugin-integration/US-007-VERIFICATION.md +259 -0
- package/nax/features/plugin-integration/prd.json +208 -0
- package/nax/features/plugin-integration/progress.txt +5 -0
- package/nax/features/precheck/prd.json +205 -0
- package/nax/features/precheck/progress.txt +15 -0
- package/nax/features/structured-logging/prd.json +199 -0
- package/nax/features/unlock/prd.json +36 -0
- package/package.json +47 -0
- package/src/acceptance/fix-generator.ts +348 -0
- package/src/acceptance/generator.ts +282 -0
- package/src/acceptance/index.ts +30 -0
- package/src/acceptance/types.ts +79 -0
- package/src/agents/claude-decompose.ts +169 -0
- package/src/agents/claude-plan.ts +139 -0
- package/src/agents/claude.ts +324 -0
- package/src/agents/cost.ts +268 -0
- package/src/agents/index.ts +13 -0
- package/src/agents/registry.ts +48 -0
- package/src/agents/types-extended.ts +133 -0
- package/src/agents/types.ts +113 -0
- package/src/agents/validation.ts +69 -0
- package/src/analyze/classifier.ts +305 -0
- package/src/analyze/index.ts +16 -0
- package/src/analyze/scanner.ts +175 -0
- package/src/analyze/types.ts +51 -0
- package/src/cli/accept.ts +108 -0
- package/src/cli/analyze-parser.ts +284 -0
- package/src/cli/analyze.ts +207 -0
- package/src/cli/config.ts +561 -0
- package/src/cli/constitution.ts +109 -0
- package/src/cli/diagnose-analysis.ts +159 -0
- package/src/cli/diagnose-formatter.ts +87 -0
- package/src/cli/diagnose.ts +203 -0
- package/src/cli/generate.ts +127 -0
- package/src/cli/index.ts +37 -0
- package/src/cli/init.ts +188 -0
- package/src/cli/interact.ts +295 -0
- package/src/cli/plan.ts +198 -0
- package/src/cli/plugins.ts +111 -0
- package/src/cli/prompts.ts +295 -0
- package/src/cli/runs.ts +174 -0
- package/src/cli/status-cost.ts +151 -0
- package/src/cli/status-features.ts +338 -0
- package/src/cli/status.ts +13 -0
- package/src/commands/common.ts +171 -0
- package/src/commands/diagnose.ts +17 -0
- package/src/commands/index.ts +8 -0
- package/src/commands/logs.ts +384 -0
- package/src/commands/precheck.ts +86 -0
- package/src/commands/unlock.ts +96 -0
- package/src/config/defaults.ts +160 -0
- package/src/config/index.ts +22 -0
- package/src/config/loader.ts +121 -0
- package/src/config/merger.ts +147 -0
- package/src/config/path-security.ts +121 -0
- package/src/config/paths.ts +27 -0
- package/src/config/schema.ts +56 -0
- package/src/config/schemas.ts +286 -0
- package/src/config/types.ts +423 -0
- package/src/config/validate.ts +103 -0
- package/src/constitution/generator.ts +191 -0
- package/src/constitution/generators/aider.ts +41 -0
- package/src/constitution/generators/claude.ts +35 -0
- package/src/constitution/generators/cursor.ts +36 -0
- package/src/constitution/generators/opencode.ts +38 -0
- package/src/constitution/generators/types.ts +33 -0
- package/src/constitution/generators/windsurf.ts +36 -0
- package/src/constitution/index.ts +10 -0
- package/src/constitution/loader.ts +133 -0
- package/src/constitution/types.ts +31 -0
- package/src/context/auto-detect.ts +227 -0
- package/src/context/builder.ts +246 -0
- package/src/context/elements.ts +83 -0
- package/src/context/formatter.ts +107 -0
- package/src/context/generator.ts +129 -0
- package/src/context/generators/aider.ts +34 -0
- package/src/context/generators/claude.ts +28 -0
- package/src/context/generators/cursor.ts +28 -0
- package/src/context/generators/opencode.ts +30 -0
- package/src/context/generators/windsurf.ts +28 -0
- package/src/context/greenfield.ts +114 -0
- package/src/context/index.ts +33 -0
- package/src/context/injector.ts +279 -0
- package/src/context/test-scanner.ts +370 -0
- package/src/context/types.ts +98 -0
- package/src/errors.ts +67 -0
- package/src/execution/batching.ts +157 -0
- package/src/execution/crash-recovery.ts +373 -0
- package/src/execution/escalation/escalation.ts +44 -0
- package/src/execution/escalation/index.ts +13 -0
- package/src/execution/escalation/tier-escalation.ts +295 -0
- package/src/execution/escalation/tier-outcome.ts +158 -0
- package/src/execution/helpers.ts +38 -0
- package/src/execution/index.ts +45 -0
- package/src/execution/lifecycle/acceptance-loop.ts +272 -0
- package/src/execution/lifecycle/headless-formatter.ts +85 -0
- package/src/execution/lifecycle/index.ts +12 -0
- package/src/execution/lifecycle/parallel-lifecycle.ts +101 -0
- package/src/execution/lifecycle/precheck-runner.ts +140 -0
- package/src/execution/lifecycle/run-cleanup.ts +81 -0
- package/src/execution/lifecycle/run-completion.ts +129 -0
- package/src/execution/lifecycle/run-initialization.ts +141 -0
- package/src/execution/lifecycle/run-lifecycle.ts +312 -0
- package/src/execution/lifecycle/run-setup.ts +204 -0
- package/src/execution/lifecycle/story-hooks.ts +38 -0
- package/src/execution/lifecycle/story-size-prompts.ts +123 -0
- package/src/execution/lock.ts +115 -0
- package/src/execution/parallel-executor.ts +216 -0
- package/src/execution/parallel.ts +400 -0
- package/src/execution/pid-registry.ts +280 -0
- package/src/execution/pipeline-result-handler.ts +388 -0
- package/src/execution/post-verify-rectification.ts +188 -0
- package/src/execution/post-verify.ts +274 -0
- package/src/execution/progress.ts +25 -0
- package/src/execution/prompts.ts +127 -0
- package/src/execution/queue-handler.ts +109 -0
- package/src/execution/rectification.ts +13 -0
- package/src/execution/runner.ts +377 -0
- package/src/execution/sequential-executor.ts +388 -0
- package/src/execution/status-file.ts +264 -0
- package/src/execution/status-writer.ts +139 -0
- package/src/execution/story-context.ts +229 -0
- package/src/execution/test-output-parser.ts +14 -0
- package/src/execution/verification.ts +72 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/runner.ts +286 -0
- package/src/hooks/types.ts +67 -0
- package/src/interaction/chain.ts +154 -0
- package/src/interaction/index.ts +60 -0
- package/src/interaction/init.ts +83 -0
- package/src/interaction/plugins/auto.ts +217 -0
- package/src/interaction/plugins/cli.ts +300 -0
- package/src/interaction/plugins/telegram.ts +384 -0
- package/src/interaction/plugins/webhook.ts +258 -0
- package/src/interaction/state.ts +171 -0
- package/src/interaction/triggers.ts +229 -0
- package/src/interaction/types.ts +163 -0
- package/src/logger/formatters.ts +84 -0
- package/src/logger/index.ts +16 -0
- package/src/logger/logger.ts +298 -0
- package/src/logger/types.ts +48 -0
- package/src/logging/formatter.ts +355 -0
- package/src/logging/index.ts +22 -0
- package/src/logging/types.ts +93 -0
- package/src/metrics/aggregator.ts +190 -0
- package/src/metrics/index.ts +14 -0
- package/src/metrics/tracker.ts +200 -0
- package/src/metrics/types.ts +109 -0
- package/src/optimizer/index.ts +62 -0
- package/src/optimizer/noop.optimizer.ts +24 -0
- package/src/optimizer/rule-based.optimizer.ts +248 -0
- package/src/optimizer/types.ts +53 -0
- package/src/pipeline/events.ts +130 -0
- package/src/pipeline/index.ts +19 -0
- package/src/pipeline/runner.ts +161 -0
- package/src/pipeline/stages/acceptance.ts +197 -0
- package/src/pipeline/stages/completion.ts +99 -0
- package/src/pipeline/stages/constitution.ts +63 -0
- package/src/pipeline/stages/context.ts +117 -0
- package/src/pipeline/stages/execution.ts +194 -0
- package/src/pipeline/stages/index.ts +62 -0
- package/src/pipeline/stages/optimizer.ts +74 -0
- package/src/pipeline/stages/prompt.ts +57 -0
- package/src/pipeline/stages/queue-check.ts +103 -0
- package/src/pipeline/stages/review.ts +181 -0
- package/src/pipeline/stages/routing.ts +81 -0
- package/src/pipeline/stages/verify.ts +100 -0
- package/src/pipeline/types.ts +167 -0
- package/src/plugins/index.ts +31 -0
- package/src/plugins/loader.ts +287 -0
- package/src/plugins/registry.ts +168 -0
- package/src/plugins/types.ts +327 -0
- package/src/plugins/validator.ts +352 -0
- package/src/prd/index.ts +172 -0
- package/src/prd/types.ts +202 -0
- package/src/precheck/checks-blockers.ts +391 -0
- package/src/precheck/checks-warnings.ts +142 -0
- package/src/precheck/checks.ts +30 -0
- package/src/precheck/index.ts +247 -0
- package/src/precheck/story-size-gate.ts +144 -0
- package/src/precheck/types.ts +31 -0
- package/src/queue/index.ts +2 -0
- package/src/queue/manager.ts +254 -0
- package/src/queue/types.ts +54 -0
- package/src/review/index.ts +8 -0
- package/src/review/runner.ts +172 -0
- package/src/review/types.ts +66 -0
- package/src/routing/builder.ts +81 -0
- package/src/routing/chain.ts +74 -0
- package/src/routing/index.ts +16 -0
- package/src/routing/loader.ts +58 -0
- package/src/routing/router.ts +303 -0
- package/src/routing/strategies/adaptive.ts +215 -0
- package/src/routing/strategies/index.ts +8 -0
- package/src/routing/strategies/keyword.ts +163 -0
- package/src/routing/strategies/llm-prompts.ts +209 -0
- package/src/routing/strategies/llm.ts +235 -0
- package/src/routing/strategies/manual.ts +50 -0
- package/src/routing/strategy.ts +99 -0
- package/src/tdd/cleanup.ts +111 -0
- package/src/tdd/index.ts +23 -0
- package/src/tdd/isolation.ts +123 -0
- package/src/tdd/orchestrator.ts +383 -0
- package/src/tdd/prompts.ts +270 -0
- package/src/tdd/rectification-gate.ts +183 -0
- package/src/tdd/session-runner.ts +179 -0
- package/src/tdd/types.ts +81 -0
- package/src/tdd/verdict.ts +271 -0
- package/src/tui/App.tsx +265 -0
- package/src/tui/components/AgentPanel.tsx +75 -0
- package/src/tui/components/CostOverlay.tsx +118 -0
- package/src/tui/components/HelpOverlay.tsx +107 -0
- package/src/tui/components/StatusBar.tsx +63 -0
- package/src/tui/components/StoriesPanel.tsx +177 -0
- package/src/tui/hooks/useKeyboard.ts +142 -0
- package/src/tui/hooks/useLayout.ts +137 -0
- package/src/tui/hooks/usePipelineEvents.ts +183 -0
- package/src/tui/hooks/usePty.ts +194 -0
- package/src/tui/index.tsx +38 -0
- package/src/tui/types.ts +76 -0
- package/src/utils/git.ts +83 -0
- package/src/utils/queue-writer.ts +54 -0
- package/src/verification/executor.ts +235 -0
- package/src/verification/gate.ts +207 -0
- package/src/verification/index.ts +12 -0
- package/src/verification/parser.ts +230 -0
- package/src/verification/rectification.ts +108 -0
- package/src/verification/types.ts +113 -0
- package/src/worktree/dispatcher.ts +65 -0
- package/src/worktree/index.ts +2 -0
- package/src/worktree/manager.ts +187 -0
- package/src/worktree/merge.ts +301 -0
- package/src/worktree/types.ts +4 -0
- package/test/TEST_COVERAGE_US001.md +217 -0
- package/test/TEST_COVERAGE_US003.md +84 -0
- package/test/TEST_COVERAGE_US005.md +86 -0
- package/test/US-002-orchestrator.test.ts +246 -0
- package/test/acceptance/cm-003-default-view.test.ts +194 -0
- package/test/execution/pid-registry.test.ts +240 -0
- package/test/execution/post-verify.test.ts +224 -0
- package/test/helpers/timeout.ts +42 -0
- package/test/integration/US-002-TEST-SUMMARY.md +107 -0
- package/test/integration/US-003-TEST-SUMMARY.md +149 -0
- package/test/integration/US-004-TEST-SUMMARY.md +106 -0
- package/test/integration/US-005-TEST-SUMMARY.md +138 -0
- package/test/integration/US-007-TEST-SUMMARY.md +100 -0
- package/test/integration/agent-validation.test.ts +439 -0
- package/test/integration/analyze-integration.test.ts +261 -0
- package/test/integration/analyze-scanner.test.ts +131 -0
- package/test/integration/cli-config-default-edge-cases.test.ts +222 -0
- package/test/integration/cli-config-default-view.test.ts +229 -0
- package/test/integration/cli-config-diff.test.ts +460 -0
- package/test/integration/cli-config.test.ts +736 -0
- package/test/integration/cli-diagnose.test.ts +592 -0
- package/test/integration/cli-logs.test.ts +314 -0
- package/test/integration/cli-plugins.test.ts +678 -0
- package/test/integration/cli-precheck.test.ts +371 -0
- package/test/integration/cli-run-headless.test.ts +173 -0
- package/test/integration/cli.test.ts +75 -0
- package/test/integration/config/merger.test.ts +465 -0
- package/test/integration/config/paths.test.ts +51 -0
- package/test/integration/config-loader.test.ts +265 -0
- package/test/integration/config.test.ts +444 -0
- package/test/integration/context-integration.test.ts +702 -0
- package/test/integration/context-provider-injection.test.ts +506 -0
- package/test/integration/context-verification-integration.test.ts +295 -0
- package/test/integration/e2e.test.ts +896 -0
- package/test/integration/execution.test.ts +625 -0
- package/test/integration/helpers.test.ts +295 -0
- package/test/integration/hooks.test.ts +361 -0
- package/test/integration/interaction-chain-pipeline.test.ts +464 -0
- package/test/integration/isolation.test.ts +143 -0
- package/test/integration/logger.test.ts +461 -0
- package/test/integration/parallel.test.ts +250 -0
- package/test/integration/path-security.test.ts +173 -0
- package/test/integration/pipeline-acceptance.test.ts +302 -0
- package/test/integration/pipeline-events.test.ts +475 -0
- package/test/integration/pipeline.test.ts +658 -0
- package/test/integration/plan.test.ts +157 -0
- package/test/integration/plugin-routing.test.ts +921 -0
- package/test/integration/plugins/config-integration.test.ts +172 -0
- package/test/integration/plugins/config-resolution.test.ts +522 -0
- package/test/integration/plugins/loader.test.ts +641 -0
- package/test/integration/plugins/registry.test.ts +746 -0
- package/test/integration/plugins/validator.test.ts +563 -0
- package/test/integration/prd-pause.test.ts +205 -0
- package/test/integration/prd-resolvers.test.ts +185 -0
- package/test/integration/precheck-integration.test.ts +468 -0
- package/test/integration/precheck.test.ts +805 -0
- package/test/integration/progress.test.ts +34 -0
- package/test/integration/rectification-flow.test.ts +512 -0
- package/test/integration/reporter-lifecycle.test.ts +860 -0
- package/test/integration/review-config-commands.test.ts +319 -0
- package/test/integration/review-config-schema.test.ts +116 -0
- package/test/integration/review-plugin-integration.test.ts +722 -0
- package/test/integration/review.test.ts +149 -0
- package/test/integration/routing-stage-bug-021.test.ts +274 -0
- package/test/integration/routing-stage-greenfield.test.ts +286 -0
- package/test/integration/runner-config-plugins.test.ts +461 -0
- package/test/integration/runner-fixes.test.ts +399 -0
- package/test/integration/runner-plugin-integration.test.ts +543 -0
- package/test/integration/runner.test.ts +1679 -0
- package/test/integration/s5-greenfield-fallback.test.ts +297 -0
- package/test/integration/status-file-integration.test.ts +325 -0
- package/test/integration/status-file.test.ts +379 -0
- package/test/integration/status-writer.test.ts +345 -0
- package/test/integration/story-id-in-events.test.ts +273 -0
- package/test/integration/tdd-cleanup.test.ts +246 -0
- package/test/integration/tdd-orchestrator.test.ts +1762 -0
- package/test/integration/test-scanner.test.ts +403 -0
- package/test/integration/verification-asset-check.test.ts +142 -0
- package/test/integration/verify-stage.test.ts +275 -0
- package/test/integration/worktree/manager.test.ts +218 -0
- package/test/integration/worktree/merge.test.ts +341 -0
- package/test/manual/logging-formatter-demo.ts +158 -0
- package/test/ui/tui-agent-panel.test.tsx +99 -0
- package/test/ui/tui-controls.test.ts +334 -0
- package/test/ui/tui-cost-and-pty.test.ts +189 -0
- package/test/ui/tui-layout.test.ts +378 -0
- package/test/ui/tui-pty-integration.test.tsx +159 -0
- package/test/ui/tui-stories.test.ts +332 -0
- package/test/unit/acceptance.test.ts +186 -0
- package/test/unit/agent-stderr-capture.test.ts +146 -0
- package/test/unit/analyze-classifier.test.ts +215 -0
- package/test/unit/analyze.test.ts +224 -0
- package/test/unit/auto-detect.test.ts +249 -0
- package/test/unit/cli-status.test.ts +417 -0
- package/test/unit/commands/common.test.ts +320 -0
- package/test/unit/commands/logs.test.ts +416 -0
- package/test/unit/commands/unlock.test.ts +319 -0
- package/test/unit/constitution-generators.test.ts +160 -0
- package/test/unit/constitution.test.ts +209 -0
- package/test/unit/context.test.ts +1722 -0
- package/test/unit/cost.test.ts +231 -0
- package/test/unit/crash-recovery.test.ts +308 -0
- package/test/unit/escalation.test.ts +126 -0
- package/test/unit/execution-logging-stderr.test.ts +156 -0
- package/test/unit/execution-stage.test.ts +122 -0
- package/test/unit/fix-generator.test.ts +275 -0
- package/test/unit/formatters.test.ts +469 -0
- package/test/unit/greenfield.test.ts +179 -0
- package/test/unit/helpers.test.ts +317 -0
- package/test/unit/interaction/human-review-trigger.test.ts +164 -0
- package/test/unit/interaction-network-failures.test.ts +389 -0
- package/test/unit/interaction-plugins.test.ts +164 -0
- package/test/unit/isolation.test.ts +134 -0
- package/test/unit/logging/formatter.test.ts +455 -0
- package/test/unit/merge.test.ts +268 -0
- package/test/unit/metrics.test.ts +276 -0
- package/test/unit/optimizer/noop.optimizer.test.ts +125 -0
- package/test/unit/optimizer/rule-based.optimizer.test.ts +358 -0
- package/test/unit/prd-auto-default.test.ts +290 -0
- package/test/unit/prd-failure-category.test.ts +176 -0
- package/test/unit/prd-get-next-story.test.ts +186 -0
- package/test/unit/precheck-checks.test.ts +840 -0
- package/test/unit/precheck-story-size-gate.test.ts +287 -0
- package/test/unit/precheck-types.test.ts +142 -0
- package/test/unit/prompts.test.ts +475 -0
- package/test/unit/queue.test.ts +237 -0
- package/test/unit/rectification.test.ts +284 -0
- package/test/unit/registry.test.ts +287 -0
- package/test/unit/routing.test.ts +937 -0
- package/test/unit/run-lifecycle.test.ts +140 -0
- package/test/unit/storyid-events.test.ts +224 -0
- package/test/unit/tdd-verdict.test.ts +492 -0
- package/test/unit/test-output-parser.test.ts +377 -0
- package/test/unit/verdict.test.ts +324 -0
- package/test/unit/worktree-manager.test.ts +158 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Metadata Auto-Injector (v0.16.1)
|
|
3
|
+
*
|
|
4
|
+
* Detects project language/stack and injects metadata into agent configs.
|
|
5
|
+
* Supports: Node.js/Bun (package.json), Go (go.mod), Rust (Cargo.toml),
|
|
6
|
+
* Python (pyproject.toml / requirements.txt), PHP (composer.json),
|
|
7
|
+
* Ruby (Gemfile), Java/Kotlin (pom.xml / build.gradle).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import type { NaxConfig } from "../config";
|
|
13
|
+
import type { ProjectMetadata } from "./types";
|
|
14
|
+
|
|
15
|
+
/** Notable Node.js dependency keywords */
|
|
16
|
+
const NOTABLE_NODE_DEPS = [
|
|
17
|
+
"@nestjs",
|
|
18
|
+
"express",
|
|
19
|
+
"fastify",
|
|
20
|
+
"koa",
|
|
21
|
+
"hono",
|
|
22
|
+
"next",
|
|
23
|
+
"nuxt",
|
|
24
|
+
"react",
|
|
25
|
+
"vue",
|
|
26
|
+
"svelte",
|
|
27
|
+
"solid",
|
|
28
|
+
"prisma",
|
|
29
|
+
"typeorm",
|
|
30
|
+
"mongoose",
|
|
31
|
+
"drizzle",
|
|
32
|
+
"sequelize",
|
|
33
|
+
"jest",
|
|
34
|
+
"vitest",
|
|
35
|
+
"mocha",
|
|
36
|
+
"bun",
|
|
37
|
+
"zod",
|
|
38
|
+
"typescript",
|
|
39
|
+
"graphql",
|
|
40
|
+
"trpc",
|
|
41
|
+
"bull",
|
|
42
|
+
"ioredis",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// ─── Language detectors ──────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/** Node.js / Bun: read package.json */
|
|
48
|
+
async function detectNode(workdir: string): Promise<{ name?: string; lang: string; dependencies: string[] } | null> {
|
|
49
|
+
const pkgPath = join(workdir, "package.json");
|
|
50
|
+
if (!existsSync(pkgPath)) return null;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const file = Bun.file(pkgPath);
|
|
54
|
+
const pkg = await file.json();
|
|
55
|
+
const allDeps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
56
|
+
const notable = [
|
|
57
|
+
...new Set(
|
|
58
|
+
Object.keys(allDeps).filter((dep) =>
|
|
59
|
+
NOTABLE_NODE_DEPS.some((kw) => dep === kw || dep.startsWith(`${kw}/`) || dep.includes(kw)),
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
].slice(0, 10);
|
|
63
|
+
const lang = pkg.devDependencies?.typescript || pkg.dependencies?.typescript ? "TypeScript" : "JavaScript";
|
|
64
|
+
return { name: pkg.name, lang, dependencies: notable };
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Go: read go.mod for module name + direct dependencies */
|
|
71
|
+
function detectGo(workdir: string): { name?: string; lang: string; dependencies: string[] } | null {
|
|
72
|
+
const goMod = join(workdir, "go.mod");
|
|
73
|
+
if (!existsSync(goMod)) return null;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const content = readFileSync(goMod, "utf8");
|
|
77
|
+
const moduleMatch = content.match(/^module\s+(\S+)/m);
|
|
78
|
+
const name = moduleMatch?.[1];
|
|
79
|
+
|
|
80
|
+
// Extract require block entries (direct deps, not indirect)
|
|
81
|
+
const requires: string[] = [];
|
|
82
|
+
const requireBlock = content.match(/require\s*\(([^)]+)\)/s)?.[1] ?? "";
|
|
83
|
+
for (const line of requireBlock.split("\n")) {
|
|
84
|
+
const trimmed = line.trim();
|
|
85
|
+
if (trimmed && !trimmed.startsWith("//") && !trimmed.includes("// indirect")) {
|
|
86
|
+
const dep = trimmed.split(/\s+/)[0];
|
|
87
|
+
if (dep) requires.push(dep.split("/").slice(-1)[0]); // last segment only
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { name, lang: "Go", dependencies: requires.slice(0, 10) };
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Rust: read Cargo.toml for package name + dependencies */
|
|
98
|
+
function detectRust(workdir: string): { name?: string; lang: string; dependencies: string[] } | null {
|
|
99
|
+
const cargoPath = join(workdir, "Cargo.toml");
|
|
100
|
+
if (!existsSync(cargoPath)) return null;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const content = readFileSync(cargoPath, "utf8");
|
|
104
|
+
const nameMatch = content.match(/^\[package\][^[]*name\s*=\s*"([^"]+)"/ms);
|
|
105
|
+
const name = nameMatch?.[1];
|
|
106
|
+
|
|
107
|
+
// Extract [dependencies] section keys
|
|
108
|
+
const depsSection = content.match(/^\[dependencies\]([^[]*)/ms)?.[1] ?? "";
|
|
109
|
+
const deps = depsSection
|
|
110
|
+
.split("\n")
|
|
111
|
+
.map((l) => l.split("=")[0].trim())
|
|
112
|
+
.filter((l) => l && !l.startsWith("#"))
|
|
113
|
+
.slice(0, 10);
|
|
114
|
+
|
|
115
|
+
return { name, lang: "Rust", dependencies: deps };
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Python: read pyproject.toml or requirements.txt */
|
|
122
|
+
function detectPython(workdir: string): { name?: string; lang: string; dependencies: string[] } | null {
|
|
123
|
+
const pyproject = join(workdir, "pyproject.toml");
|
|
124
|
+
const requirements = join(workdir, "requirements.txt");
|
|
125
|
+
|
|
126
|
+
if (!existsSync(pyproject) && !existsSync(requirements)) return null;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
if (existsSync(pyproject)) {
|
|
130
|
+
const content = readFileSync(pyproject, "utf8");
|
|
131
|
+
const nameMatch = content.match(/^\s*name\s*=\s*"([^"]+)"/m);
|
|
132
|
+
const depsSection = content.match(/^\[project\][^[]*dependencies\s*=\s*\[([^\]]*)\]/ms)?.[1] ?? "";
|
|
133
|
+
const deps = depsSection
|
|
134
|
+
.split(",")
|
|
135
|
+
.map((d) => d.trim().replace(/["'\s>=<!^~].*/g, ""))
|
|
136
|
+
.filter(Boolean)
|
|
137
|
+
.slice(0, 10);
|
|
138
|
+
return { name: nameMatch?.[1], lang: "Python", dependencies: deps };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Fallback: requirements.txt
|
|
142
|
+
const lines = readFileSync(requirements, "utf8")
|
|
143
|
+
.split("\n")
|
|
144
|
+
.map((l) => l.split(/[>=<!]/)[0].trim())
|
|
145
|
+
.filter((l) => l && !l.startsWith("#"))
|
|
146
|
+
.slice(0, 10);
|
|
147
|
+
return { lang: "Python", dependencies: lines };
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** PHP: read composer.json */
|
|
154
|
+
async function detectPhp(workdir: string): Promise<{ name?: string; lang: string; dependencies: string[] } | null> {
|
|
155
|
+
const composerPath = join(workdir, "composer.json");
|
|
156
|
+
if (!existsSync(composerPath)) return null;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const file = Bun.file(composerPath);
|
|
160
|
+
const composer = await file.json();
|
|
161
|
+
const deps = Object.keys({ ...(composer.require ?? {}), ...(composer["require-dev"] ?? {}) })
|
|
162
|
+
.filter((d) => d !== "php")
|
|
163
|
+
.map((d) => d.split("/").pop() ?? d)
|
|
164
|
+
.slice(0, 10);
|
|
165
|
+
return { name: composer.name, lang: "PHP", dependencies: deps };
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Ruby: read Gemfile */
|
|
172
|
+
function detectRuby(workdir: string): { name?: string; lang: string; dependencies: string[] } | null {
|
|
173
|
+
const gemfile = join(workdir, "Gemfile");
|
|
174
|
+
if (!existsSync(gemfile)) return null;
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const content = readFileSync(gemfile, "utf8");
|
|
178
|
+
const gems = [...content.matchAll(/^\s*gem\s+['"]([^'"]+)['"]/gm)].map((m) => m[1]).slice(0, 10);
|
|
179
|
+
return { lang: "Ruby", dependencies: gems };
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Java/Kotlin: detect from pom.xml or build.gradle */
|
|
186
|
+
function detectJvm(workdir: string): { name?: string; lang: string; dependencies: string[] } | null {
|
|
187
|
+
const pom = join(workdir, "pom.xml");
|
|
188
|
+
const gradle = join(workdir, "build.gradle");
|
|
189
|
+
const gradleKts = join(workdir, "build.gradle.kts");
|
|
190
|
+
|
|
191
|
+
if (!existsSync(pom) && !existsSync(gradle) && !existsSync(gradleKts)) return null;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
if (existsSync(pom)) {
|
|
195
|
+
const content = readFileSync(pom, "utf8");
|
|
196
|
+
const nameMatch = content.match(/<artifactId>([^<]+)<\/artifactId>/);
|
|
197
|
+
const deps = [...content.matchAll(/<artifactId>([^<]+)<\/artifactId>/g)]
|
|
198
|
+
.map((m) => m[1])
|
|
199
|
+
.filter((d) => d !== nameMatch?.[1])
|
|
200
|
+
.slice(0, 10);
|
|
201
|
+
const lang = existsSync(join(workdir, "src/main/kotlin")) ? "Kotlin" : "Java";
|
|
202
|
+
return { name: nameMatch?.[1], lang, dependencies: deps };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const gradleFile = existsSync(gradleKts) ? gradleKts : gradle;
|
|
206
|
+
const content = readFileSync(gradleFile, "utf8");
|
|
207
|
+
const lang = gradleFile.endsWith(".kts") ? "Kotlin" : "Java";
|
|
208
|
+
const deps = [...content.matchAll(/implementation[^'"]*['"]([^:'"]+:[^:'"]+)[^'"]*['"]/g)]
|
|
209
|
+
.map((m) => m[1].split(":").pop() ?? m[1])
|
|
210
|
+
.slice(0, 10);
|
|
211
|
+
return { lang, dependencies: deps };
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Detect project language and build metadata.
|
|
221
|
+
* Runs all detectors; first match wins (Node checked last to avoid false positives in polyglot repos).
|
|
222
|
+
*/
|
|
223
|
+
export async function buildProjectMetadata(workdir: string, config: NaxConfig): Promise<ProjectMetadata> {
|
|
224
|
+
// Priority: Go > Rust > Python > PHP > Ruby > JVM > Node
|
|
225
|
+
const detected =
|
|
226
|
+
detectGo(workdir) ??
|
|
227
|
+
detectRust(workdir) ??
|
|
228
|
+
detectPython(workdir) ??
|
|
229
|
+
(await detectPhp(workdir)) ??
|
|
230
|
+
detectRuby(workdir) ??
|
|
231
|
+
detectJvm(workdir) ??
|
|
232
|
+
(await detectNode(workdir));
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
name: detected?.name,
|
|
236
|
+
language: detected?.lang,
|
|
237
|
+
dependencies: detected?.dependencies ?? [],
|
|
238
|
+
testCommand: config.execution?.testCommand ?? undefined,
|
|
239
|
+
lintCommand: config.execution?.lintCommand ?? undefined,
|
|
240
|
+
typecheckCommand: config.execution?.typecheckCommand ?? undefined,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Format metadata as a markdown section for injection into agent configs.
|
|
246
|
+
*/
|
|
247
|
+
export function formatMetadataSection(metadata: ProjectMetadata): string {
|
|
248
|
+
const lines: string[] = ["## Project Metadata", "", "> Auto-injected by `nax generate`", ""];
|
|
249
|
+
|
|
250
|
+
if (metadata.name) {
|
|
251
|
+
lines.push(`**Project:** \`${metadata.name}\``);
|
|
252
|
+
lines.push("");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (metadata.language) {
|
|
256
|
+
lines.push(`**Language:** ${metadata.language}`);
|
|
257
|
+
lines.push("");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (metadata.dependencies.length > 0) {
|
|
261
|
+
lines.push(`**Key dependencies:** ${metadata.dependencies.join(", ")}`);
|
|
262
|
+
lines.push("");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const commands: string[] = [];
|
|
266
|
+
if (metadata.testCommand) commands.push(`test: \`${metadata.testCommand}\``);
|
|
267
|
+
if (metadata.lintCommand) commands.push(`lint: \`${metadata.lintCommand}\``);
|
|
268
|
+
if (metadata.typecheckCommand) commands.push(`typecheck: \`${metadata.typecheckCommand}\``);
|
|
269
|
+
|
|
270
|
+
if (commands.length > 0) {
|
|
271
|
+
lines.push(`**Commands:** ${commands.join(" | ")}`);
|
|
272
|
+
lines.push("");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
lines.push("---");
|
|
276
|
+
lines.push("");
|
|
277
|
+
|
|
278
|
+
return lines.join("\n");
|
|
279
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test File Scanner (v0.7)
|
|
3
|
+
*
|
|
4
|
+
* Scans test directories and extracts describe/test block names
|
|
5
|
+
* to generate a coverage summary for prompt injection.
|
|
6
|
+
* Prevents test duplication across isolated story sessions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { Glob } from "bun";
|
|
11
|
+
import { getLogger } from "../logger";
|
|
12
|
+
import { estimateTokens } from "./builder";
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Types
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
/** Detail level for test summary */
|
|
19
|
+
export type TestSummaryDetail = "names-only" | "names-and-counts" | "describe-blocks";
|
|
20
|
+
|
|
21
|
+
/** Options for scanning test files */
|
|
22
|
+
export interface TestScanOptions {
|
|
23
|
+
/** Working directory (base for testDir) */
|
|
24
|
+
workdir: string;
|
|
25
|
+
/** Test directory relative to workdir (default: auto-detect) */
|
|
26
|
+
testDir?: string;
|
|
27
|
+
/** Glob pattern for test files (default: "**\/*.test.{ts,js,tsx,jsx}") */
|
|
28
|
+
testPattern?: string;
|
|
29
|
+
/** Max tokens for the summary (default: 500) */
|
|
30
|
+
maxTokens?: number;
|
|
31
|
+
/** Summary detail level (default: "names-and-counts") */
|
|
32
|
+
detail?: TestSummaryDetail;
|
|
33
|
+
/** Context files to scope test coverage to (default: undefined = scan all) */
|
|
34
|
+
contextFiles?: string[];
|
|
35
|
+
/** Enable scoping to context files (default: true) */
|
|
36
|
+
scopeToStory?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A single describe block extracted from a test file */
|
|
40
|
+
export interface DescribeBlock {
|
|
41
|
+
name: string;
|
|
42
|
+
tests: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Parsed test file info */
|
|
46
|
+
export interface TestFileInfo {
|
|
47
|
+
/** Relative path from workdir */
|
|
48
|
+
relativePath: string;
|
|
49
|
+
/** Total test count (it/test calls) */
|
|
50
|
+
testCount: number;
|
|
51
|
+
/** Top-level describe blocks with their test names */
|
|
52
|
+
describes: DescribeBlock[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Scan result */
|
|
56
|
+
export interface TestScanResult {
|
|
57
|
+
files: TestFileInfo[];
|
|
58
|
+
totalTests: number;
|
|
59
|
+
summary: string;
|
|
60
|
+
tokens: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Regex Extraction
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract describe and test block names from test file source.
|
|
69
|
+
*
|
|
70
|
+
* Uses regex to find:
|
|
71
|
+
* - `describe("name", ...)` / `describe('name', ...)`
|
|
72
|
+
* - `test("name", ...)` / `it("name", ...)`
|
|
73
|
+
*
|
|
74
|
+
* Only extracts top-level describes (not nested). All test/it calls
|
|
75
|
+
* are associated with the most recent describe block.
|
|
76
|
+
*/
|
|
77
|
+
export function extractTestStructure(source: string): { describes: DescribeBlock[]; testCount: number } {
|
|
78
|
+
const describes: DescribeBlock[] = [];
|
|
79
|
+
let currentDescribe: DescribeBlock | null = null;
|
|
80
|
+
let testCount = 0;
|
|
81
|
+
|
|
82
|
+
// Match describe/test/it calls with string arguments (single or double quotes, backticks)
|
|
83
|
+
// Match describe/test/it calls anywhere (not just line-start) to handle single-line files
|
|
84
|
+
const linePattern = /(?:^|\s|;|\{)(describe|test|it)\s*\(\s*(['"`])(.*?)\2/gm;
|
|
85
|
+
|
|
86
|
+
let match: RegExpExecArray | null = linePattern.exec(source);
|
|
87
|
+
while (match !== null) {
|
|
88
|
+
const keyword = match[1];
|
|
89
|
+
const name = match[3];
|
|
90
|
+
|
|
91
|
+
if (keyword === "describe") {
|
|
92
|
+
currentDescribe = { name, tests: [] };
|
|
93
|
+
describes.push(currentDescribe);
|
|
94
|
+
} else {
|
|
95
|
+
// test or it
|
|
96
|
+
testCount++;
|
|
97
|
+
if (currentDescribe) {
|
|
98
|
+
currentDescribe.tests.push(name);
|
|
99
|
+
} else {
|
|
100
|
+
// Top-level test without describe
|
|
101
|
+
if (describes.length === 0 || describes[describes.length - 1].name !== "(top-level)") {
|
|
102
|
+
describes.push({ name: "(top-level)", tests: [] });
|
|
103
|
+
}
|
|
104
|
+
describes[describes.length - 1].tests.push(name);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
match = linePattern.exec(source);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { describes, testCount };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// File Scanning
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
/** Common test directory names to auto-detect */
|
|
118
|
+
const COMMON_TEST_DIRS = ["test", "tests", "__tests__", "src/__tests__", "spec"];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Derive test file patterns from source file paths.
|
|
122
|
+
*
|
|
123
|
+
* Maps source files to their likely test file counterparts:
|
|
124
|
+
* - src/foo.ts → test/foo.test.ts, test/foo.spec.ts
|
|
125
|
+
* - src/bar/baz.service.ts → test/bar/baz.service.test.ts, test/baz.service.test.ts
|
|
126
|
+
*
|
|
127
|
+
* @param contextFiles - Array of source file paths (relative to workdir)
|
|
128
|
+
* @returns Array of test file path patterns (basename patterns for matching)
|
|
129
|
+
*/
|
|
130
|
+
export function deriveTestPatterns(contextFiles: string[]): string[] {
|
|
131
|
+
const patterns = new Set<string>();
|
|
132
|
+
|
|
133
|
+
for (const filePath of contextFiles) {
|
|
134
|
+
const basename = path.basename(filePath);
|
|
135
|
+
const basenameNoExt = basename.replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
136
|
+
|
|
137
|
+
// Pattern 1: exact basename match with .test/.spec extension
|
|
138
|
+
// e.g., foo.ts → foo.test.ts, foo.spec.ts
|
|
139
|
+
patterns.add(`${basenameNoExt}.test.ts`);
|
|
140
|
+
patterns.add(`${basenameNoExt}.test.js`);
|
|
141
|
+
patterns.add(`${basenameNoExt}.test.tsx`);
|
|
142
|
+
patterns.add(`${basenameNoExt}.test.jsx`);
|
|
143
|
+
patterns.add(`${basenameNoExt}.spec.ts`);
|
|
144
|
+
patterns.add(`${basenameNoExt}.spec.js`);
|
|
145
|
+
patterns.add(`${basenameNoExt}.spec.tsx`);
|
|
146
|
+
patterns.add(`${basenameNoExt}.spec.jsx`);
|
|
147
|
+
|
|
148
|
+
// Pattern 2: if basename contains .service/.controller/etc, also match without it
|
|
149
|
+
// e.g., foo.service.ts → foo.test.ts
|
|
150
|
+
const simpleBasename = basenameNoExt.replace(
|
|
151
|
+
/\.(service|controller|resolver|module|guard|middleware|util|helper)$/,
|
|
152
|
+
"",
|
|
153
|
+
);
|
|
154
|
+
if (simpleBasename !== basenameNoExt) {
|
|
155
|
+
patterns.add(`${simpleBasename}.test.ts`);
|
|
156
|
+
patterns.add(`${simpleBasename}.test.js`);
|
|
157
|
+
patterns.add(`${simpleBasename}.spec.ts`);
|
|
158
|
+
patterns.add(`${simpleBasename}.spec.js`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return Array.from(patterns);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Auto-detect test directory by checking common locations.
|
|
167
|
+
*/
|
|
168
|
+
async function detectTestDir(workdir: string): Promise<string | null> {
|
|
169
|
+
for (const dir of COMMON_TEST_DIRS) {
|
|
170
|
+
const fullPath = path.join(workdir, dir);
|
|
171
|
+
const file = Bun.file(path.join(fullPath, ".")); // Check directory exists
|
|
172
|
+
try {
|
|
173
|
+
const dirStat = await Bun.file(fullPath).exists();
|
|
174
|
+
// Bun.file().exists() returns false for directories, use different check
|
|
175
|
+
const proc = Bun.spawn(["test", "-d", fullPath], { stdout: "pipe", stderr: "pipe" });
|
|
176
|
+
const exitCode = await proc.exited;
|
|
177
|
+
if (exitCode === 0) return dir;
|
|
178
|
+
} catch {}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Scan test files and extract structure.
|
|
185
|
+
*
|
|
186
|
+
* @param options - Scan options
|
|
187
|
+
* @returns Array of parsed test file info
|
|
188
|
+
*/
|
|
189
|
+
export async function scanTestFiles(options: TestScanOptions): Promise<TestFileInfo[]> {
|
|
190
|
+
const { workdir, testPattern = "**/*.test.{ts,js,tsx,jsx}", contextFiles, scopeToStory = true } = options;
|
|
191
|
+
let testDir = options.testDir;
|
|
192
|
+
|
|
193
|
+
// Auto-detect test directory if not specified
|
|
194
|
+
if (!testDir) {
|
|
195
|
+
testDir = (await detectTestDir(workdir)) || "test";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const scanDir = path.join(workdir, testDir);
|
|
199
|
+
|
|
200
|
+
// Check directory exists
|
|
201
|
+
const dirCheck = Bun.spawn(["test", "-d", scanDir], { stdout: "pipe", stderr: "pipe" });
|
|
202
|
+
if ((await dirCheck.exited) !== 0) {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Derive test patterns from context files if scoping is enabled
|
|
207
|
+
let allowedBasenames: Set<string> | null = null;
|
|
208
|
+
if (scopeToStory && contextFiles && contextFiles.length > 0) {
|
|
209
|
+
const patterns = deriveTestPatterns(contextFiles);
|
|
210
|
+
allowedBasenames = new Set(patterns);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const glob = new Glob(testPattern);
|
|
214
|
+
const files: TestFileInfo[] = [];
|
|
215
|
+
|
|
216
|
+
for await (const filePath of glob.scan({ cwd: scanDir, absolute: false })) {
|
|
217
|
+
// Filter by derived patterns if scoping is enabled
|
|
218
|
+
if (allowedBasenames !== null) {
|
|
219
|
+
const basename = path.basename(filePath);
|
|
220
|
+
if (!allowedBasenames.has(basename)) {
|
|
221
|
+
continue; // Skip test files not matching context files
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const fullPath = path.join(scanDir, filePath);
|
|
226
|
+
try {
|
|
227
|
+
const source = await Bun.file(fullPath).text();
|
|
228
|
+
const { describes, testCount } = extractTestStructure(source);
|
|
229
|
+
|
|
230
|
+
if (testCount > 0 || describes.length > 0) {
|
|
231
|
+
files.push({
|
|
232
|
+
relativePath: path.join(testDir, filePath),
|
|
233
|
+
testCount,
|
|
234
|
+
describes,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
} catch {}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Sort by path for stable output
|
|
241
|
+
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
242
|
+
|
|
243
|
+
return files;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// Summary Formatting
|
|
248
|
+
// ============================================================================
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Format test files as a markdown summary at the specified detail level.
|
|
252
|
+
*/
|
|
253
|
+
export function formatTestSummary(files: TestFileInfo[], detail: TestSummaryDetail): string {
|
|
254
|
+
if (files.length === 0) {
|
|
255
|
+
return "";
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const lines: string[] = [];
|
|
259
|
+
const totalTests = files.reduce((sum, f) => sum + f.testCount, 0);
|
|
260
|
+
|
|
261
|
+
lines.push(`## Existing Test Coverage (${totalTests} tests across ${files.length} files)`);
|
|
262
|
+
lines.push("");
|
|
263
|
+
lines.push("The following tests already exist. DO NOT duplicate this coverage.");
|
|
264
|
+
lines.push("Focus only on testing NEW behavior introduced by this story.");
|
|
265
|
+
lines.push("");
|
|
266
|
+
|
|
267
|
+
for (const file of files) {
|
|
268
|
+
switch (detail) {
|
|
269
|
+
case "names-only":
|
|
270
|
+
lines.push(`- **${file.relativePath}** (${file.testCount} tests)`);
|
|
271
|
+
break;
|
|
272
|
+
|
|
273
|
+
case "names-and-counts":
|
|
274
|
+
lines.push(`### ${file.relativePath} (${file.testCount} tests)`);
|
|
275
|
+
for (const desc of file.describes) {
|
|
276
|
+
lines.push(`- ${desc.name} (${desc.tests.length} tests)`);
|
|
277
|
+
}
|
|
278
|
+
lines.push("");
|
|
279
|
+
break;
|
|
280
|
+
|
|
281
|
+
case "describe-blocks":
|
|
282
|
+
lines.push(`### ${file.relativePath} (${file.testCount} tests)`);
|
|
283
|
+
for (const desc of file.describes) {
|
|
284
|
+
lines.push(`- **${desc.name}** (${desc.tests.length} tests)`);
|
|
285
|
+
for (const test of desc.tests) {
|
|
286
|
+
lines.push(` - ${test}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
lines.push("");
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return lines.join("\n");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Truncate summary to fit within token budget.
|
|
299
|
+
*
|
|
300
|
+
* Strategy: progressively reduce detail level, then truncate files.
|
|
301
|
+
*/
|
|
302
|
+
export function truncateToTokenBudget(
|
|
303
|
+
files: TestFileInfo[],
|
|
304
|
+
maxTokens: number,
|
|
305
|
+
preferredDetail: TestSummaryDetail,
|
|
306
|
+
): { summary: string; detail: TestSummaryDetail; truncated: boolean } {
|
|
307
|
+
// Try preferred detail level first
|
|
308
|
+
const detailLevels: TestSummaryDetail[] = ["describe-blocks", "names-and-counts", "names-only"];
|
|
309
|
+
const startIndex = detailLevels.indexOf(preferredDetail);
|
|
310
|
+
|
|
311
|
+
for (let i = startIndex; i < detailLevels.length; i++) {
|
|
312
|
+
const detail = detailLevels[i];
|
|
313
|
+
const summary = formatTestSummary(files, detail);
|
|
314
|
+
const tokens = estimateTokens(summary);
|
|
315
|
+
|
|
316
|
+
if (tokens <= maxTokens) {
|
|
317
|
+
return { summary, detail, truncated: i !== startIndex };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Even names-only exceeds budget — truncate files
|
|
322
|
+
let truncatedFiles = [...files];
|
|
323
|
+
while (truncatedFiles.length > 1) {
|
|
324
|
+
truncatedFiles = truncatedFiles.slice(0, truncatedFiles.length - 1);
|
|
325
|
+
const summary = `${formatTestSummary(truncatedFiles, "names-only")}\n... and ${files.length - truncatedFiles.length} more test files`;
|
|
326
|
+
if (estimateTokens(summary) <= maxTokens) {
|
|
327
|
+
return { summary, detail: "names-only", truncated: true };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Last resort: just file count
|
|
332
|
+
const fallback = `## Existing Test Coverage\n\n${files.length} test files with ${files.reduce((s, f) => s + f.testCount, 0)} total tests exist. Review test/ directory before adding new tests.`;
|
|
333
|
+
return { summary: fallback, detail: "names-only", truncated: true };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ============================================================================
|
|
337
|
+
// Main Entry Point
|
|
338
|
+
// ============================================================================
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Scan test files and generate a token-budgeted summary.
|
|
342
|
+
*
|
|
343
|
+
* @param options - Scan and formatting options
|
|
344
|
+
* @returns Scan result with summary, or empty result if no tests found
|
|
345
|
+
*/
|
|
346
|
+
export async function generateTestCoverageSummary(options: TestScanOptions): Promise<TestScanResult> {
|
|
347
|
+
const { maxTokens = 500, detail = "names-and-counts", contextFiles, scopeToStory = true } = options;
|
|
348
|
+
|
|
349
|
+
// Log warning if scoping is enabled but no context files provided
|
|
350
|
+
if (scopeToStory && (!contextFiles || contextFiles.length === 0)) {
|
|
351
|
+
try {
|
|
352
|
+
const logger = getLogger();
|
|
353
|
+
logger.warn("context", "scopeToStory=true but no contextFiles provided — falling back to full scan");
|
|
354
|
+
} catch {
|
|
355
|
+
// Logger not initialized (e.g., in tests) — silently skip
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const files = await scanTestFiles(options);
|
|
360
|
+
|
|
361
|
+
if (files.length === 0) {
|
|
362
|
+
return { files: [], totalTests: 0, summary: "", tokens: 0 };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const totalTests = files.reduce((sum, f) => sum + f.testCount, 0);
|
|
366
|
+
const { summary } = truncateToTokenBudget(files, maxTokens, detail);
|
|
367
|
+
const tokens = estimateTokens(summary);
|
|
368
|
+
|
|
369
|
+
return { files, totalTests, summary, tokens };
|
|
370
|
+
}
|