karajan-code 4.70.3 → 5.0.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/README.md +2 -2
- package/package.json +6 -14
- package/packages/hu-board/package.json +1 -1
- package/packages/hu-board/public/app.js +4 -18
- package/packages/hu-board/public/index.html +5 -2
- package/packages/hu-board/public/styles.css +26 -0
- package/packages/hu-board/public/utils/api.js +0 -74
- package/packages/hu-board/public/utils/board-tour.js +62 -0
- package/packages/hu-board/public/utils/board-view.js +49 -144
- package/packages/hu-board/public/utils/command-launcher.js +8 -70
- package/packages/hu-board/public/utils/formatters.js +10 -0
- package/packages/hu-board/public/utils/governance-view.js +13 -1
- package/packages/hu-board/public/utils/hu-actions.js +17 -68
- package/packages/hu-board/public/utils/init-listeners.js +7 -8
- package/packages/hu-board/public/utils/log-panel.js +3 -16
- package/packages/hu-board/public/utils/maggle-mode.js +2 -12
- package/packages/hu-board/public/utils/modals.js +7 -0
- package/packages/hu-board/public/utils/project-picker-view.js +1 -1
- package/packages/hu-board/public/utils/server-push.js +3 -22
- package/packages/hu-board/public/utils/shared-board-notice.js +33 -0
- package/packages/hu-board/public/utils/story-detail-view.js +5 -19
- package/packages/hu-board/src/cleanup-zombies.js +0 -1
- package/packages/hu-board/src/command-runner.js +36 -25
- package/packages/hu-board/src/db-connection.js +282 -0
- package/packages/hu-board/src/db-paths.js +113 -0
- package/packages/hu-board/src/db-queries.js +188 -0
- package/packages/hu-board/src/db-tombstones.js +51 -0
- package/packages/hu-board/src/db.js +28 -619
- package/packages/hu-board/src/ephemeral-cleaner.js +2 -3
- package/packages/hu-board/src/hu-zombie-reaper.js +5 -1
- package/packages/hu-board/src/plan-mutations.js +3 -153
- package/packages/hu-board/src/plans-service.js +55 -0
- package/packages/hu-board/src/routes/api/commands.js +122 -0
- package/packages/hu-board/src/routes/api/plans.js +55 -0
- package/packages/hu-board/src/routes/api/projects.js +267 -0
- package/packages/hu-board/src/routes/api/prompts.js +135 -0
- package/packages/hu-board/src/routes/api/sessions.js +95 -0
- package/packages/hu-board/src/routes/api/stories.js +183 -0
- package/packages/hu-board/src/routes/api/system.js +241 -0
- package/packages/hu-board/src/routes/api/tombstones.js +38 -0
- package/packages/hu-board/src/routes/api.js +24 -1371
- package/packages/hu-board/src/routes/governance.js +23 -1
- package/packages/hu-board/src/routes/lanes.js +40 -0
- package/packages/hu-board/src/routes/rag.js +4 -3
- package/packages/hu-board/src/server.js +20 -34
- package/packages/hu-board/src/sync-files.js +318 -0
- package/packages/hu-board/src/sync-project.js +99 -0
- package/packages/hu-board/src/sync.js +22 -566
- package/packages/hu-board/src/zombie-reaper.js +1 -1
- package/scripts/dual-publish.mjs +19 -3
- package/scripts/postinstall.js +15 -2
- package/scripts/verify-pack-mcp.mjs +11 -7
- package/scripts/verify-pack.mjs +4 -4
- package/src/audit/audit-history.js +3 -2
- package/src/audit/dead-exports.js +68 -59
- package/src/audit/deterministic-summary.js +33 -13
- package/src/audit/npm-signatures.js +41 -0
- package/src/audit/sbom.js +35 -0
- package/src/checks/agent-token.js +112 -0
- package/src/checks/ai-surface.js +9 -7
- package/src/checks/config-files.js +8 -7
- package/src/checks/dir-setup.js +34 -1
- package/src/checks/native-build.js +21 -39
- package/src/checks/runner.js +128 -142
- package/src/cli/advanced-commands.js +6 -6
- package/src/cli/register-meta.js +42 -82
- package/src/cli/register-pipeline.js +8 -140
- package/src/cli/register-plan.js +9 -100
- package/src/cli/retired-commands.js +49 -0
- package/src/cli.js +18 -5
- package/src/commands/agent-run.js +40 -26
- package/src/commands/audit.js +127 -124
- package/src/commands/board.js +5 -9
- package/src/commands/check.js +29 -16
- package/src/commands/doctor.js +2 -0
- package/src/commands/env.js +176 -148
- package/src/commands/go.js +76 -55
- package/src/commands/harden.js +105 -86
- package/src/commands/hu-start.js +52 -0
- package/src/commands/hu.js +288 -221
- package/src/commands/identity.js +111 -78
- package/src/commands/init-atril.js +47 -0
- package/src/commands/init.js +11 -40
- package/src/commands/install-tools.js +70 -85
- package/src/commands/main-guard.js +78 -0
- package/src/commands/policy.js +226 -184
- package/src/commands/qmd.js +27 -21
- package/src/commands/rag.js +75 -48
- package/src/commands/review-gate.js +422 -339
- package/src/commands/start.js +4 -5
- package/src/commands/steward.js +96 -47
- package/src/commands/tournament.js +46 -37
- package/src/commands/worktree.js +94 -25
- package/src/config/loader.js +34 -17
- package/src/config/overrides.js +2 -1
- package/src/config/secrets.js +36 -0
- package/src/config.js +1 -0
- package/src/environment/adr.js +16 -2
- package/src/environment/board-access.js +11 -9
- package/src/git/karajan-label.js +20 -8
- package/src/guards/main-guard.js +54 -0
- package/src/harden/branch-protection.js +36 -3
- package/src/harden/hook-templates.js +16 -1
- package/src/harden/sentinel/posttooluse.mjs +18 -6
- package/src/harden/sentinel/pretooluse-sentinel.mjs +81 -35
- package/src/harden/sentinel/sentinel-lib.mjs +149 -23
- package/src/harden/sentinel/sentinel-shell.mjs +64 -0
- package/src/harden/sentinel/sessionstart.mjs +3 -2
- package/src/harden/sentinel/stop.mjs +5 -4
- package/src/harden/workflow-engine.js +12 -1
- package/src/harden/workflow-templates.js +1 -1
- package/src/hu/board-lookup.js +29 -0
- package/src/hu/gh-identity.js +23 -0
- package/src/hu/github-board.js +4 -2
- package/src/hu/lane-prune.js +15 -7
- package/src/hu/lane-setup.js +65 -0
- package/src/hu/lanes.js +66 -0
- package/src/hu/team-board-mirror.js +3 -2
- package/src/infrastructure/command-runner.js +1 -1
- package/src/infrastructure/file-system-service.js +1 -1
- package/src/mcp/handlers/direct-handlers.js +2 -292
- package/src/mcp/handlers/hu-handlers.js +44 -55
- package/src/mcp/handlers/management-handlers.js +2 -42
- package/src/mcp/server-handlers.js +16 -22
- package/src/mcp/tools.js +5 -200
- package/src/plan/lane-plans.js +54 -0
- package/src/plan/plan-store.js +7 -4
- package/src/policy/add.js +26 -12
- package/src/policy/adr-number-check.js +31 -0
- package/src/prompts/audit.js +299 -339
- package/src/prompts/card-context.js +10 -5
- package/src/prompts/reviewer.js +5 -2
- package/src/rag/auto-update.js +56 -43
- package/src/rag/bootstrap-index.js +28 -54
- package/src/rag/migrate.js +1 -1
- package/src/rag/onnx-fallback.js +2 -2
- package/src/rag/pid-lease.js +70 -0
- package/src/rag/project-store.js +2 -12
- package/src/review/card-first.js +57 -46
- package/src/review/dual-review.js +5 -4
- package/src/review/gate-gitignore.js +23 -4
- package/src/review/gate-hook.js +47 -0
- package/src/review/one-shot-review.js +51 -37
- package/src/review/publish-pr.js +18 -3
- package/src/review/repo-docs.js +73 -0
- package/src/review/repo-stack.js +59 -0
- package/src/review/reviewer-fallback.js +34 -3
- package/src/review/seal-commit.js +29 -0
- package/src/review/sonar-pregate.js +59 -44
- package/src/review/third-party-review.js +90 -16
- package/src/review/third-party-size.js +43 -0
- package/src/review/third-party-skills.js +54 -0
- package/src/review/verify-pr.js +2 -26
- package/src/roles/audit-role.js +106 -125
- package/src/skills/atril-skills.js +5 -4
- package/src/sonar/scanner.js +116 -101
- package/src/steward/invariants.js +5 -4
- package/src/steward/phantom-sweep.js +66 -0
- package/src/tournament/judge.js +60 -53
- package/src/utils/agent-home.js +33 -0
- package/src/utils/gh-account.js +37 -0
- package/src/utils/git.js +0 -13
- package/src/utils/home-migration.js +7 -1
- package/src/utils/main-checkout.js +53 -0
- package/src/utils/qmd-collection.js +3 -1
- package/src/utils/welcome.js +3 -3
- package/templates/{skills → atril/commands}/kj-architect.md +5 -0
- package/templates/{skills → atril/commands}/kj-audit.md +5 -0
- package/templates/{skills → atril/commands}/kj-board.md +5 -0
- package/templates/{skills → atril/commands}/kj-code.md +5 -0
- package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
- package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
- package/templates/{skills → atril/commands}/kj-discover.md +5 -0
- package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
- package/templates/{skills → atril/commands}/kj-review.md +5 -0
- package/templates/{skills → atril/commands}/kj-security.md +5 -0
- package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
- package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
- package/templates/{skills → atril/commands}/kj-test.md +5 -0
- package/packages/hu-board/public/utils/run-launcher.js +0 -169
- package/packages/hu-board/src/run-tracker.js +0 -121
- package/src/activity-log.js +0 -59
- package/src/agents/host-agent.js +0 -38
- package/src/agents/model-registry.js +0 -142
- package/src/audit/plan-adherence.js +0 -224
- package/src/autonomy/arbiter-role.js +0 -71
- package/src/autonomy/arbiter.js +0 -22
- package/src/autonomy/decider.js +0 -18
- package/src/autonomy/decision-resolver.js +0 -58
- package/src/autonomy/outcome-report.js +0 -55
- package/src/autonomy/stage-ask.js +0 -30
- package/src/brain/decision-tracker.js +0 -125
- package/src/brain/decisor.js +0 -243
- package/src/brain/solomon-consult.js +0 -171
- package/src/brain/standby-scheduler.js +0 -8
- package/src/budget/comparison.js +0 -96
- package/src/canvas/parse-md.js +0 -238
- package/src/canvas/schema.js +0 -101
- package/src/canvas/to-flat.js +0 -85
- package/src/canvas/validate.js +0 -163
- package/src/ci/dispatch.js +0 -99
- package/src/ci/index.js +0 -3
- package/src/ci/pr-diff.js +0 -26
- package/src/cli/register-standby.js +0 -26
- package/src/commands/architect.js +0 -111
- package/src/commands/autorun.js +0 -49
- package/src/commands/researcher.js +0 -54
- package/src/commands/resume.js +0 -72
- package/src/commands/review.js +0 -89
- package/src/commands/run.js +0 -185
- package/src/commands/standby.js +0 -117
- package/src/commands/status.js +0 -36
- package/src/commands/triage.js +0 -78
- package/src/commands/undo.js +0 -62
- package/src/domains/domain-loader.js +0 -245
- package/src/domains/domain-registry.js +0 -140
- package/src/domains/domain-synthesizer.js +0 -129
- package/src/git/automation.js +0 -433
- package/src/git/coder-commit-guard.js +0 -40
- package/src/git/hu-automation.js +0 -243
- package/src/git/hu-snapshot.js +0 -10
- package/src/git/symlink-targets.js +0 -57
- package/src/golden/asserter.js +0 -92
- package/src/golden/loader.js +0 -52
- package/src/golden/runner.js +0 -92
- package/src/golden/schema.js +0 -41
- package/src/guards/duplicate-members.js +0 -86
- package/src/guards/intent-guard.js +0 -161
- package/src/guards/perf-guard.js +0 -126
- package/src/guards/policy-guard.js +0 -90
- package/src/guards/policy-resolver.js +0 -85
- package/src/guards/secret-redactor.js +0 -71
- package/src/hu/acceptance-runner.js +0 -138
- package/src/hu/lazy-planner.js +0 -109
- package/src/hu/parallel-executor.js +0 -94
- package/src/hu/routing-metrics.js +0 -17
- package/src/hu/split-validator.js +0 -143
- package/src/hu/splitting-detector.js +0 -158
- package/src/hu/splitting-generator.js +0 -175
- package/src/hu/store.js +0 -394
- package/src/infrastructure/mocks.js +0 -182
- package/src/mcp/handlers/run-handler.js +0 -318
- package/src/mcp/sovereignty-guard.js +0 -113
- package/src/mcp/tool-arg-normalizers.js +0 -16
- package/src/mutate/reviewer-signal.js +0 -76
- package/src/orchestrator/agent-fallback.js +0 -81
- package/src/orchestrator/brain-coordinator.js +0 -186
- package/src/orchestrator/ci-integration.js +0 -139
- package/src/orchestrator/config-init.js +0 -426
- package/src/orchestrator/direct-actions.js +0 -217
- package/src/orchestrator/drivers/error-recovery.js +0 -287
- package/src/orchestrator/drivers/init-context.js +0 -238
- package/src/orchestrator/drivers/iteration-loop.js +0 -382
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
- package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
- package/src/orchestrator/drivers/post-loop.js +0 -404
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
- package/src/orchestrator/drivers/pre-loop.js +0 -524
- package/src/orchestrator/drivers/run-hu-batch.js +0 -468
- package/src/orchestrator/feedback-enrichment.js +0 -140
- package/src/orchestrator/feedback-queue.js +0 -151
- package/src/orchestrator/flow-control.js +0 -170
- package/src/orchestrator/flow-runner.js +0 -365
- package/src/orchestrator/fs-leak-detector.js +0 -228
- package/src/orchestrator/hu-scheduler.js +0 -114
- package/src/orchestrator/hu-sub-pipeline.js +0 -642
- package/src/orchestrator/iteration-gate.js +0 -73
- package/src/orchestrator/iteration-stages.js +0 -9
- package/src/orchestrator/parallel-limiter.js +0 -74
- package/src/orchestrator/pipeline-context.js +0 -72
- package/src/orchestrator/post-loop-stages.js +0 -468
- package/src/orchestrator/pre-loop-stages.js +0 -11
- package/src/orchestrator/preflight-checks.js +0 -487
- package/src/orchestrator/repair/repair-detector.js +0 -117
- package/src/orchestrator/repair/repair-runner.js +0 -142
- package/src/orchestrator/reviewer-fallback.js +0 -45
- package/src/orchestrator/role-output-compressor.js +0 -189
- package/src/orchestrator/session-journal.js +0 -308
- package/src/orchestrator/solomon-escalation.js +0 -331
- package/src/orchestrator/solomon-rules.js +0 -212
- package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
- package/src/orchestrator/stages/architect-stage.js +0 -172
- package/src/orchestrator/stages/coder-stage.js +0 -506
- package/src/orchestrator/stages/domain-curator-stage.js +0 -82
- package/src/orchestrator/stages/format-stage.js +0 -61
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
- package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
- package/src/orchestrator/stages/perf-stage.js +0 -140
- package/src/orchestrator/stages/planner-stage.js +0 -85
- package/src/orchestrator/stages/rag-context-stage.js +0 -80
- package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
- package/src/orchestrator/stages/research-stage.js +0 -69
- package/src/orchestrator/stages/reviewer-stage.js +0 -431
- package/src/orchestrator/stages/sonar-stage.js +0 -439
- package/src/orchestrator/stages/stage-classes.js +0 -84
- package/src/orchestrator/stages/stage-executor.js +0 -155
- package/src/orchestrator/stages/suite-green-stage.js +0 -62
- package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
- package/src/orchestrator/stages/tool-judge-stage.js +0 -53
- package/src/orchestrator/stages/triage-stage.js +0 -172
- package/src/orchestrator/standby.js +0 -71
- package/src/orchestrator/verification-gate.js +0 -183
- package/src/orchestrator.js +0 -22
- package/src/plan/adr-generator.js +0 -170
- package/src/plan/adr-loader.js +0 -60
- package/src/plan/plan-executor.js +0 -100
- package/src/plan/plan-validation.js +0 -4
- package/src/prompts/acceptance-coverage.js +0 -32
- package/src/prompts/arbiter.js +0 -38
- package/src/prompts/architect.js +0 -103
- package/src/prompts/hu-reviewer.js +0 -244
- package/src/prompts/tool-correctness-judge.js +0 -31
- package/src/prompts/triage.js +0 -57
- package/src/rag/embedder.js +0 -2
- package/src/rag/embedders/_cloud-base.js +0 -2
- package/src/rag/embedders/cohere.js +0 -2
- package/src/rag/embedders/mistral.js +0 -2
- package/src/rag/embedders/onnx.js +0 -2
- package/src/rag/embedders/openai.js +0 -2
- package/src/rag/embedders/voyage.js +0 -2
- package/src/rag/rerank.js +0 -4
- package/src/rag/where-parser.js +0 -4
- package/src/repeat-detector.js +0 -88
- package/src/review/diff-generator.js +0 -112
- package/src/review/git-stash-helper.js +0 -104
- package/src/review/schema.js +0 -31
- package/src/review/scope-filter.js +0 -152
- package/src/review/snapshot-diff.js +0 -122
- package/src/review/tdd-discipline.js +0 -90
- package/src/review/tdd-policy.js +0 -69
- package/src/roles/acceptance-coverage-role.js +0 -78
- package/src/roles/architect-role.js +0 -77
- package/src/roles/coder-role.js +0 -113
- package/src/roles/commiter-role.js +0 -113
- package/src/roles/domain-curator-role.js +0 -134
- package/src/roles/hu-reviewer-role.js +0 -64
- package/src/roles/impeccable-role.js +0 -85
- package/src/roles/index.js +0 -18
- package/src/roles/karajan-brain-role.js +0 -134
- package/src/roles/perf-role.js +0 -122
- package/src/roles/planner-role.js +0 -151
- package/src/roles/refactorer-role.js +0 -30
- package/src/roles/repairer-role.js +0 -181
- package/src/roles/researcher-role.js +0 -70
- package/src/roles/reviewer-role.js +0 -115
- package/src/roles/security-role.js +0 -73
- package/src/roles/solomon-role.js +0 -271
- package/src/roles/sonar-role.js +0 -72
- package/src/roles/tester-role.js +0 -235
- package/src/roles/tool-correctness-judge-role.js +0 -78
- package/src/roles/triage-role.js +0 -99
- package/src/session/journal/decisions-writer.js +0 -200
- package/src/session/journal/iteration-logger.js +0 -194
- package/src/session/journal/summary-writer.js +0 -320
- package/src/session/journal/tree-writer.js +0 -164
- package/src/session/journal/write-all.js +0 -89
- package/src/session-cleanup.js +0 -116
- package/src/skills/semantic-detector.js +0 -167
- package/src/skills/skill-detector.js +0 -385
- package/src/sonar/cloud-scanner.js +0 -78
- package/src/tracker-bootstrap.js +0 -44
- package/src/types/index.js +0 -38
- package/src/types/policy.js +0 -43
- package/src/types/solomon.js +0 -87
- package/src/utils/async-lock.js +0 -31
- package/src/utils/budget.js +0 -229
- package/src/utils/cwd-confirm.js +0 -39
- package/src/utils/display/event-handlers.js +0 -403
- package/src/utils/display/formatters.js +0 -132
- package/src/utils/display/header.js +0 -47
- package/src/utils/display/session-summary.js +0 -118
- package/src/utils/display/solomon.js +0 -42
- package/src/utils/pricing.js +0 -29
- package/src/utils/quota-simulator.js +0 -56
- package/src/utils/role-assigner.js +0 -137
- package/src/utils/rtk-wrapper.js +0 -87
- package/src/utils/run-registry.js +0 -9
- package/src/utils/status-dashboard.js +0 -254
- package/src/utils/tool-call-extractor.js +0 -56
- package/src/webperf/devtools-detect.js +0 -112
- package/src/webperf/webperf-role-integration.js +0 -66
- package/templates/skills/kj-run.md +0 -73
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
// Surgical stash helper for the TDD discipline gate (KJC-TSK-0398 PR2).
|
|
2
|
-
// Stashes only the source files we want to hide for the "red" run and
|
|
3
|
-
// restores them afterwards. Tracked files use `git stash push -- <paths>`;
|
|
4
|
-
// untracked files (not yet known to git) are moved aside to a private
|
|
5
|
-
// `.karajan/tmp/stash-<id>/` tree because `git stash` would ignore them.
|
|
6
|
-
|
|
7
|
-
import { mkdir, rename, rm } from "node:fs/promises";
|
|
8
|
-
import { existsSync } from "node:fs";
|
|
9
|
-
import { dirname, join, resolve } from "node:path";
|
|
10
|
-
import { runCommand } from "../utils/process.js";
|
|
11
|
-
|
|
12
|
-
function defaultRunner(cmd, args, opts) {
|
|
13
|
-
return runCommand(cmd, args, opts);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async function isTracked(runner, projectDir, file) {
|
|
17
|
-
const res = await runner("git", ["ls-files", "--error-unmatch", "--", file], { cwd: projectDir });
|
|
18
|
-
return res.exitCode === 0;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function stashRefForToken(runner, projectDir, marker) {
|
|
22
|
-
const res = await runner("git", ["stash", "list"], { cwd: projectDir });
|
|
23
|
-
if (res.exitCode !== 0) return null;
|
|
24
|
-
for (const line of String(res.stdout || "").split("\n")) {
|
|
25
|
-
if (line.includes(marker)) {
|
|
26
|
-
const m = line.match(/^(stash@\{\d+\})/);
|
|
27
|
-
if (m) return m[1];
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Move `files` aside so a test run sees them as missing. Returns a token to
|
|
34
|
-
// pass to `restoreFiles()`. Throws on failure with the partial token attached.
|
|
35
|
-
export async function stashFilesAside(files, { projectDir = process.cwd(), runner = defaultRunner, tmpRoot } = {}) {
|
|
36
|
-
if (!Array.isArray(files) || files.length === 0) {
|
|
37
|
-
return { trackedStashRef: null, untrackedMoves: [], marker: null };
|
|
38
|
-
}
|
|
39
|
-
const tracked = [];
|
|
40
|
-
const untracked = [];
|
|
41
|
-
for (const f of files) {
|
|
42
|
-
if (await isTracked(runner, projectDir, f)) tracked.push(f);
|
|
43
|
-
else if (existsSync(resolve(projectDir, f))) untracked.push(f);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const marker = `kj-tdd-discipline-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
47
|
-
const stashRoot = tmpRoot || join(projectDir, ".karajan", "tmp", marker);
|
|
48
|
-
const untrackedMoves = [];
|
|
49
|
-
let trackedStashRef = null;
|
|
50
|
-
|
|
51
|
-
if (tracked.length > 0) {
|
|
52
|
-
const args = ["stash", "push", "--keep-index", "-m", marker, "--", ...tracked];
|
|
53
|
-
const res = await runner("git", args, { cwd: projectDir });
|
|
54
|
-
if (res.exitCode !== 0) {
|
|
55
|
-
throw new Error(`git stash push failed: ${res.stderr || res.stdout}`);
|
|
56
|
-
}
|
|
57
|
-
trackedStashRef = await stashRefForToken(runner, projectDir, marker);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
for (const f of untracked) {
|
|
61
|
-
const src = resolve(projectDir, f);
|
|
62
|
-
const dst = join(stashRoot, f);
|
|
63
|
-
await mkdir(dirname(dst), { recursive: true });
|
|
64
|
-
await rename(src, dst);
|
|
65
|
-
untrackedMoves.push({ src, dst });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return { trackedStashRef, untrackedMoves, marker, stashRoot };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Reverse a `stashFilesAside` token. Idempotent: a restored token becomes a no-op.
|
|
72
|
-
export async function restoreFiles(token, { projectDir = process.cwd(), runner = defaultRunner } = {}) {
|
|
73
|
-
if (!token || (token.trackedStashRef == null && token.untrackedMoves.length === 0)) return;
|
|
74
|
-
|
|
75
|
-
for (const { src, dst } of token.untrackedMoves) {
|
|
76
|
-
if (existsSync(dst)) {
|
|
77
|
-
await mkdir(dirname(src), { recursive: true });
|
|
78
|
-
await rename(dst, src);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
if (token.stashRoot && existsSync(token.stashRoot)) {
|
|
82
|
-
await rm(token.stashRoot, { recursive: true, force: true });
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (token.trackedStashRef) {
|
|
86
|
-
const res = await runner("git", ["stash", "pop", token.trackedStashRef], { cwd: projectDir });
|
|
87
|
-
if (res.exitCode !== 0) {
|
|
88
|
-
throw new Error(`git stash pop failed: ${res.stderr || res.stdout}`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
token.trackedStashRef = null;
|
|
92
|
-
token.untrackedMoves = [];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Run `body` with `files` stashed aside, always restoring them — even when
|
|
96
|
-
// `body` throws. The TDD discipline stage wraps its "red" run with this.
|
|
97
|
-
export async function withStashedFiles(files, body, opts = {}) {
|
|
98
|
-
const token = await stashFilesAside(files, opts);
|
|
99
|
-
try {
|
|
100
|
-
return await body(token);
|
|
101
|
-
} finally {
|
|
102
|
-
await restoreFiles(token, opts).catch(() => { /* swallow: surface body error */ });
|
|
103
|
-
}
|
|
104
|
-
}
|
package/src/review/schema.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
export function validateReviewResult(reviewResult) {
|
|
2
|
-
if (!reviewResult || typeof reviewResult !== "object") {
|
|
3
|
-
throw new TypeError("Reviewer output must be a JSON object");
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
if (typeof reviewResult.approved !== "boolean") {
|
|
7
|
-
throw new TypeError("Reviewer output missing boolean field: approved");
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
if (!Array.isArray(reviewResult.blocking_issues)) {
|
|
11
|
-
throw new TypeError("Reviewer output missing array field: blocking_issues");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
if (!Array.isArray(reviewResult.non_blocking_suggestions)) {
|
|
15
|
-
reviewResult.non_blocking_suggestions = [];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (typeof reviewResult.summary !== "string") {
|
|
19
|
-
reviewResult.summary = "";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (typeof reviewResult.confidence !== "number") {
|
|
23
|
-
reviewResult.confidence = 0.5;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (reviewResult.approved && reviewResult.blocking_issues.length > 0) {
|
|
27
|
-
throw new Error("Invalid reviewer output: approved=true with blocking issues");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return reviewResult;
|
|
31
|
-
}
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scope filter — auto-defers reviewer blocking issues that reference
|
|
3
|
-
* files NOT present in the diff. This prevents reviewer scope drift
|
|
4
|
-
* (flagging missing features, unchanged code, future tasks) from
|
|
5
|
-
* stalling the pipeline.
|
|
6
|
-
*
|
|
7
|
-
* Deferred issues are NOT forgotten — they are tracked in the session
|
|
8
|
-
* as technical debt that should be resolved in future iterations or
|
|
9
|
-
* follow-up tasks. The coder and planner receive context about what
|
|
10
|
-
* was deferred and why.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Extract the set of changed file paths from a unified diff string.
|
|
15
|
-
*/
|
|
16
|
-
export function extractDiffFiles(diff) {
|
|
17
|
-
const files = new Set();
|
|
18
|
-
for (const line of (diff || "").split("\n")) {
|
|
19
|
-
// Match "+++ b/path" lines in unified diff
|
|
20
|
-
const m = /^\+\+\+ b\/(.+)/.exec(line);
|
|
21
|
-
if (m) files.add(m[1]);
|
|
22
|
-
}
|
|
23
|
-
return files;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Determine whether a blocking issue is within scope of the diff.
|
|
28
|
-
*
|
|
29
|
-
* An issue is considered IN scope when:
|
|
30
|
-
* - It has no `file` field (general concern about the diff)
|
|
31
|
-
* - Its `file` matches one of the changed files (exact or suffix match)
|
|
32
|
-
* - It references a pattern present in the diff content itself
|
|
33
|
-
*
|
|
34
|
-
* An issue is OUT of scope when:
|
|
35
|
-
* - It explicitly references a file NOT in the diff
|
|
36
|
-
*/
|
|
37
|
-
export function isIssueInScope(issue, diffFiles, diffContent) {
|
|
38
|
-
const file = (issue.file || "").trim();
|
|
39
|
-
|
|
40
|
-
// No file specified — the reviewer is commenting on the diff generally
|
|
41
|
-
if (!file) return true;
|
|
42
|
-
|
|
43
|
-
// Direct match
|
|
44
|
-
if (diffFiles.has(file)) return true;
|
|
45
|
-
|
|
46
|
-
// Suffix match (reviewer might use full path vs relative)
|
|
47
|
-
for (const df of diffFiles) {
|
|
48
|
-
if (df.endsWith(file) || file.endsWith(df)) return true;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Check if the file path appears anywhere in the diff content
|
|
52
|
-
// (covers cases where the file is referenced in imports/requires)
|
|
53
|
-
if (diffContent?.includes(file)) return true;
|
|
54
|
-
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Filter a review result, demoting out-of-scope blocking issues to
|
|
60
|
-
* non-blocking suggestions.
|
|
61
|
-
*
|
|
62
|
-
* Returns { review, demoted, deferred, allDemoted } where:
|
|
63
|
-
* - review: the filtered review (may flip approved to true)
|
|
64
|
-
* - demoted: array of original issues that were demoted
|
|
65
|
-
* - deferred: structured deferred issues with metadata for session tracking
|
|
66
|
-
* - allDemoted: true if ALL blocking issues were out of scope
|
|
67
|
-
*/
|
|
68
|
-
export function filterReviewScope(review, diff) {
|
|
69
|
-
if (!review || review.approved) {
|
|
70
|
-
return { review, demoted: [], deferred: [], allDemoted: false };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const diffFiles = extractDiffFiles(diff);
|
|
74
|
-
|
|
75
|
-
// If we can't parse diff files, don't filter (safety)
|
|
76
|
-
if (diffFiles.size === 0) {
|
|
77
|
-
return { review, demoted: [], deferred: [], allDemoted: false };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const inScope = [];
|
|
81
|
-
const demoted = [];
|
|
82
|
-
|
|
83
|
-
for (const issue of review.blocking_issues || []) {
|
|
84
|
-
if (isIssueInScope(issue, diffFiles, diff)) {
|
|
85
|
-
inScope.push(issue);
|
|
86
|
-
} else {
|
|
87
|
-
demoted.push(issue);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (demoted.length === 0) {
|
|
92
|
-
return { review, demoted: [], deferred: [], allDemoted: false };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const demotedSuggestions = demoted.map(
|
|
96
|
-
(issue) => `[auto-demoted] ${issue.file || "unknown"}: ${issue.description || issue.id || "no description"}`
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
const filtered = {
|
|
100
|
-
...review,
|
|
101
|
-
blocking_issues: inScope,
|
|
102
|
-
non_blocking_suggestions: [
|
|
103
|
-
...(review.non_blocking_suggestions || []),
|
|
104
|
-
...demotedSuggestions
|
|
105
|
-
]
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
// If no in-scope blocking issues remain, auto-approve
|
|
109
|
-
const allDemoted = inScope.length === 0;
|
|
110
|
-
if (allDemoted) {
|
|
111
|
-
filtered.approved = true;
|
|
112
|
-
filtered.summary = `${review.summary || ""} [Auto-approved: ${demoted.length} out-of-scope issue(s) demoted to suggestions]`.trim();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Build structured deferred issues for session tracking
|
|
116
|
-
const deferred = demoted.map((issue) => ({
|
|
117
|
-
id: issue.id || null,
|
|
118
|
-
file: issue.file || null,
|
|
119
|
-
severity: issue.severity || "medium",
|
|
120
|
-
description: issue.description || "no description",
|
|
121
|
-
suggested_fix: issue.suggested_fix || null,
|
|
122
|
-
deferred_at: new Date().toISOString(),
|
|
123
|
-
reason: "out_of_scope"
|
|
124
|
-
}));
|
|
125
|
-
|
|
126
|
-
return { review: filtered, demoted, deferred, allDemoted };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Build a human-readable summary of deferred issues for injection
|
|
131
|
-
* into coder/planner prompts so they are aware of the tech debt.
|
|
132
|
-
*/
|
|
133
|
-
export function buildDeferredContext(deferredIssues) {
|
|
134
|
-
if (!deferredIssues?.length) return "";
|
|
135
|
-
|
|
136
|
-
const lines = [
|
|
137
|
-
"## Deferred reviewer concerns (technical debt)",
|
|
138
|
-
"The following issues were flagged by the reviewer but deferred because they are outside the current diff scope.",
|
|
139
|
-
"You do NOT need to fix them now, but be aware of them:",
|
|
140
|
-
""
|
|
141
|
-
];
|
|
142
|
-
|
|
143
|
-
for (const issue of deferredIssues) {
|
|
144
|
-
const file = issue.file ? `\`${issue.file}\`` : "general";
|
|
145
|
-
const fix = issue.suggested_fix ? ` — Suggestion: ${issue.suggested_fix}` : "";
|
|
146
|
-
lines.push(`- [${issue.severity}] ${file}: ${issue.description}${fix}`);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
lines.push("", "If your current changes naturally address any of these, great. Otherwise, they will be tracked for future resolution.");
|
|
150
|
-
|
|
151
|
-
return lines.join("\n");
|
|
152
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Git-free diff generation using filesystem snapshots.
|
|
3
|
-
*
|
|
4
|
-
* Takes a snapshot of project files (path → content hash), then compares
|
|
5
|
-
* a later snapshot to generate a unified diff. Works on any OS, no git needed.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readdir, readFile, stat } from "node:fs/promises";
|
|
9
|
-
import { join, relative } from "node:path";
|
|
10
|
-
import { createHash } from "node:crypto";
|
|
11
|
-
|
|
12
|
-
const DEFAULT_IGNORE = new Set([
|
|
13
|
-
"node_modules", ".git", ".kj", ".karajan", "dist", "build",
|
|
14
|
-
".next", ".nuxt", ".svelte-kit", "__pycache__", ".venv",
|
|
15
|
-
"coverage", ".nyc_output", ".reviews"
|
|
16
|
-
]);
|
|
17
|
-
|
|
18
|
-
const MAX_FILE_SIZE = 512 * 1024; // 512KB — skip large files
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Scan a directory recursively and return a Map of relative path → content hash.
|
|
22
|
-
* @param {string} rootDir
|
|
23
|
-
* @returns {Promise<Map<string, string>>} path → sha256 hash
|
|
24
|
-
*/
|
|
25
|
-
export async function takeSnapshot(rootDir) {
|
|
26
|
-
const snapshot = new Map();
|
|
27
|
-
await scanDir(rootDir, rootDir, snapshot);
|
|
28
|
-
return snapshot;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function scanDir(dir, rootDir, snapshot) {
|
|
32
|
-
let entries;
|
|
33
|
-
try {
|
|
34
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
35
|
-
} catch { return; }
|
|
36
|
-
|
|
37
|
-
for (const entry of entries) {
|
|
38
|
-
if (DEFAULT_IGNORE.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
39
|
-
|
|
40
|
-
const fullPath = join(dir, entry.name);
|
|
41
|
-
if (entry.isDirectory()) {
|
|
42
|
-
await scanDir(fullPath, rootDir, snapshot);
|
|
43
|
-
} else if (entry.isFile()) {
|
|
44
|
-
try {
|
|
45
|
-
const s = await stat(fullPath);
|
|
46
|
-
if (s.size > MAX_FILE_SIZE) continue;
|
|
47
|
-
const content = await readFile(fullPath);
|
|
48
|
-
const hash = createHash("sha256").update(content).digest("hex");
|
|
49
|
-
snapshot.set(relative(rootDir, fullPath), hash);
|
|
50
|
-
} catch { /* skip unreadable files */ }
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Compare two snapshots and generate a unified diff string.
|
|
57
|
-
* @param {Map<string, string>} before — snapshot before coder
|
|
58
|
-
* @param {Map<string, string>|null} after — snapshot after coder (null = read from disk)
|
|
59
|
-
* @param {string} rootDir — project root (for reading file contents)
|
|
60
|
-
* @returns {Promise<string>} unified diff text
|
|
61
|
-
*/
|
|
62
|
-
export async function generateSnapshotDiff(before, after, rootDir) {
|
|
63
|
-
if (!after) {
|
|
64
|
-
after = await takeSnapshot(rootDir);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const lines = [];
|
|
68
|
-
|
|
69
|
-
// New files (in after but not before)
|
|
70
|
-
for (const [path, _hash] of after) {
|
|
71
|
-
if (!before.has(path)) {
|
|
72
|
-
const content = await safeRead(join(rootDir, path));
|
|
73
|
-
if (content !== null) {
|
|
74
|
-
lines.push(`diff --snapshot a/${path} b/${path}`);
|
|
75
|
-
lines.push("new file");
|
|
76
|
-
lines.push(`--- /dev/null`);
|
|
77
|
-
lines.push(`+++ b/${path}`);
|
|
78
|
-
for (const line of content.split("\n")) {
|
|
79
|
-
lines.push(`+${line}`);
|
|
80
|
-
}
|
|
81
|
-
lines.push("");
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Modified files (in both, different hash)
|
|
87
|
-
for (const [path, hash] of after) {
|
|
88
|
-
if (before.has(path) && before.get(path) !== hash) {
|
|
89
|
-
const content = await safeRead(join(rootDir, path));
|
|
90
|
-
if (content !== null) {
|
|
91
|
-
lines.push(`diff --snapshot a/${path} b/${path}`);
|
|
92
|
-
lines.push("modified file");
|
|
93
|
-
lines.push(`--- a/${path}`);
|
|
94
|
-
lines.push(`+++ b/${path}`);
|
|
95
|
-
// Show full new content (we don't have the old content)
|
|
96
|
-
for (const line of content.split("\n")) {
|
|
97
|
-
lines.push(`+${line}`);
|
|
98
|
-
}
|
|
99
|
-
lines.push("");
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Deleted files (in before but not after)
|
|
105
|
-
for (const [path] of before) {
|
|
106
|
-
if (!after.has(path)) {
|
|
107
|
-
lines.push(`diff --snapshot a/${path} b/${path}`);
|
|
108
|
-
lines.push("deleted file");
|
|
109
|
-
lines.push(`--- a/${path}`);
|
|
110
|
-
lines.push(`+++ /dev/null`);
|
|
111
|
-
lines.push("");
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return lines.join("\n");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function safeRead(filePath) {
|
|
119
|
-
try {
|
|
120
|
-
return await readFile(filePath, "utf-8");
|
|
121
|
-
} catch { return null; }
|
|
122
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
// TDD discipline gate (KJC-TSK-0398 PR1).
|
|
2
|
-
// PR1 ships the pure decision logic with injected runTests/stashFiles
|
|
3
|
-
// helpers. PR2 ships the real surgical stash. PR3 wires the gate into
|
|
4
|
-
// the pipeline behind `development.require_red_then_green`.
|
|
5
|
-
|
|
6
|
-
const PARSERS = {
|
|
7
|
-
vitest: parseVitestFailures,
|
|
8
|
-
jest: parseVitestFailures,
|
|
9
|
-
pytest: parsePytestFailures,
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
function parseVitestFailures(output) {
|
|
13
|
-
const failures = new Set();
|
|
14
|
-
for (const line of String(output || "").split("\n")) {
|
|
15
|
-
const m = line.match(/^\s*(?:FAIL|×|✖)\s+(\S+)/);
|
|
16
|
-
if (m) failures.add(m[1].trim());
|
|
17
|
-
}
|
|
18
|
-
return failures;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function parsePytestFailures(output) {
|
|
22
|
-
const failures = new Set();
|
|
23
|
-
for (const line of String(output || "").split("\n")) {
|
|
24
|
-
const m = line.match(/^FAILED\s+(\S+)/);
|
|
25
|
-
if (m) failures.add(m[1].trim());
|
|
26
|
-
}
|
|
27
|
-
return failures;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function anyNewTestIn(failures, testFiles) {
|
|
31
|
-
return [...failures].some((f) => testFiles.some((t) => f.includes(t) || t.includes(f)));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function verifyTddDiscipline({
|
|
35
|
-
projectDir,
|
|
36
|
-
sourceFiles = [],
|
|
37
|
-
testFiles = [],
|
|
38
|
-
framework = null,
|
|
39
|
-
runTests,
|
|
40
|
-
stashFiles,
|
|
41
|
-
restoreFiles,
|
|
42
|
-
} = {}) {
|
|
43
|
-
if (sourceFiles.length === 0) {
|
|
44
|
-
return { ok: true, reason: "no_source_changes", sourceFiles, testFiles };
|
|
45
|
-
}
|
|
46
|
-
if (testFiles.length === 0) {
|
|
47
|
-
return { ok: true, reason: "no_test_changes", sourceFiles, testFiles };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const parser = framework ? PARSERS[framework] : null;
|
|
51
|
-
if (!parser) {
|
|
52
|
-
return {
|
|
53
|
-
ok: true,
|
|
54
|
-
reason: "framework_unsupported",
|
|
55
|
-
sourceFiles,
|
|
56
|
-
testFiles,
|
|
57
|
-
warning: `tdd-discipline: framework "${framework || "unknown"}" has no failure parser; gate skipped`,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
let stashToken = null;
|
|
62
|
-
try {
|
|
63
|
-
stashToken = await stashFiles(sourceFiles);
|
|
64
|
-
const before = await runTests(projectDir);
|
|
65
|
-
if (!anyNewTestIn(parser(before.output), testFiles)) {
|
|
66
|
-
return {
|
|
67
|
-
ok: false,
|
|
68
|
-
reason: "tests_pass_without_impl",
|
|
69
|
-
sourceFiles,
|
|
70
|
-
testFiles,
|
|
71
|
-
message:
|
|
72
|
-
"TDD discipline violation: new tests pass without the implementation. Tests must fail first (red), then pass (green).",
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
} finally {
|
|
76
|
-
if (stashToken) await restoreFiles(stashToken);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const after = await runTests(projectDir);
|
|
80
|
-
if (anyNewTestIn(parser(after.output), testFiles)) {
|
|
81
|
-
return {
|
|
82
|
-
ok: false,
|
|
83
|
-
reason: "tests_fail_with_impl",
|
|
84
|
-
sourceFiles,
|
|
85
|
-
testFiles,
|
|
86
|
-
message: "TDD discipline violation: new tests still fail with the implementation present.",
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
return { ok: true, reason: "red_then_green_verified", sourceFiles, testFiles };
|
|
90
|
-
}
|
package/src/review/tdd-policy.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
function extractChangedFiles(diff) {
|
|
2
|
-
const files = new Set();
|
|
3
|
-
const lines = String(diff || "").split("\n");
|
|
4
|
-
for (const line of lines) {
|
|
5
|
-
// Support both git diff and snapshot diff formats
|
|
6
|
-
if (!line.startsWith("diff --git ") && !line.startsWith("diff --snapshot ")) continue;
|
|
7
|
-
const parts = line.split(" ");
|
|
8
|
-
const b = parts[3] || "";
|
|
9
|
-
if (b.startsWith("b/")) files.add(b.slice(2));
|
|
10
|
-
}
|
|
11
|
-
return [...files];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function isTestFile(file, patterns = []) {
|
|
15
|
-
const normalized = `/${file}`;
|
|
16
|
-
return patterns.some((pattern) => normalized.includes(pattern));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function isSourceFile(file, extensions = []) {
|
|
20
|
-
return extensions.some((ext) => file.endsWith(ext));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const SKIP_TDD_TASK_TYPES = new Set(["doc", "infra", "no-code"]);
|
|
24
|
-
|
|
25
|
-
const DEFAULT_TEST_PATTERNS = ["/tests/", "/__tests__/", ".test.", ".spec.", "/src/test/", "/test/", "Test.java", "Test.kt", "test_", "_test.py", "_test.go", "Test.cs", "_spec.rb", "Test.php", "_test.dart", "Tests.swift"];
|
|
26
|
-
const DEFAULT_SOURCE_EXTENSIONS = [".js", ".jsx", ".ts", ".tsx", ".py", ".go", ".java", ".kt", ".rb", ".php", ".cs", ".rs", ".swift", ".dart"];
|
|
27
|
-
|
|
28
|
-
export function evaluateTddPolicy(diff, developmentConfig = {}, untrackedFiles = [], taskType = null) {
|
|
29
|
-
if (taskType && SKIP_TDD_TASK_TYPES.has(taskType)) {
|
|
30
|
-
return { ok: true, reason: "tdd_not_applicable_for_task_type", sourceFiles: [], testFiles: [] };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const requireTestChanges = developmentConfig.require_test_changes !== false;
|
|
34
|
-
const patterns = developmentConfig.test_file_patterns || DEFAULT_TEST_PATTERNS;
|
|
35
|
-
const extensions = developmentConfig.source_file_extensions || DEFAULT_SOURCE_EXTENSIONS;
|
|
36
|
-
|
|
37
|
-
const diffFiles = extractChangedFiles(diff);
|
|
38
|
-
const extra = Array.isArray(untrackedFiles) ? untrackedFiles : [];
|
|
39
|
-
const files = [...new Set([...diffFiles, ...extra])];
|
|
40
|
-
const sourceFiles = files.filter((f) => isSourceFile(f, extensions) && !isTestFile(f, patterns));
|
|
41
|
-
const testFiles = files.filter((f) => isTestFile(f, patterns));
|
|
42
|
-
|
|
43
|
-
if (!requireTestChanges) {
|
|
44
|
-
return {
|
|
45
|
-
ok: true,
|
|
46
|
-
reason: "test_changes_not_required",
|
|
47
|
-
sourceFiles,
|
|
48
|
-
testFiles
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (sourceFiles.length === 0) {
|
|
53
|
-
const reason = files.length === 0 ? "no_diff_available" : "no_source_changes";
|
|
54
|
-
return { ok: true, reason, sourceFiles, testFiles };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (testFiles.length === 0) {
|
|
58
|
-
return {
|
|
59
|
-
ok: false,
|
|
60
|
-
reason: "source_changes_without_tests",
|
|
61
|
-
sourceFiles,
|
|
62
|
-
testFiles,
|
|
63
|
-
message:
|
|
64
|
-
"TDD policy violation: source code changed without test changes. Add/adjust tests first, then implementation."
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return { ok: true, reason: "tests_present", sourceFiles, testFiles };
|
|
69
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
// KJC-TSK-0987 (issue #1894): AcceptanceCoverageRole, the judge that reads each
|
|
2
|
-
// acceptance criterion of the card against the diff before the run is approved.
|
|
3
|
-
// Judged by a different AI than the coder (the reviewer's provider by default).
|
|
4
|
-
// Role + helpers ship here; the stage wires it (same PR), the loops next.
|
|
5
|
-
|
|
6
|
-
import { AgentRole } from "./agent-role.js";
|
|
7
|
-
import { buildAcceptanceCoveragePrompt } from "../prompts/acceptance-coverage.js";
|
|
8
|
-
import { extractFirstJson } from "../utils/json-extract.js";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* The card's acceptance criteria as plain sentences, one per criterion.
|
|
12
|
-
* Structured Given/When/Then first; else the free text, one per line or bullet.
|
|
13
|
-
* @param {object|null} card
|
|
14
|
-
* @returns {string[]}
|
|
15
|
-
*/
|
|
16
|
-
export function criteriaOfCard(card) {
|
|
17
|
-
const structured = Array.isArray(card?.acceptanceCriteriaStructured) ? card.acceptanceCriteriaStructured : [];
|
|
18
|
-
const fromStructured = structured
|
|
19
|
-
.map((ac) => (ac?.given && ac?.when && ac?.then ? `Given ${ac.given}, when ${ac.when}, then ${ac.then}` : String(ac?.raw || "").trim()))
|
|
20
|
-
.filter(Boolean);
|
|
21
|
-
if (fromStructured.length > 0) return fromStructured;
|
|
22
|
-
return String(card?.acceptanceCriteria || "")
|
|
23
|
-
.split("\n")
|
|
24
|
-
.map((line) => line.replace(/^\s*(?:[-*•]|\d+[.)])\s*/, "").trim())
|
|
25
|
-
.filter(Boolean);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const normalizeVerdicts = (parsed, criteria) => criteria.map((text, i) => {
|
|
29
|
-
const v = Array.isArray(parsed?.criteria) ? parsed.criteria.find((c) => Number(c?.index) === i + 1) : null;
|
|
30
|
-
return {
|
|
31
|
-
index: i + 1,
|
|
32
|
-
text,
|
|
33
|
-
// No entry for a criterion is no evidence for it: not covered, never assumed.
|
|
34
|
-
covered: v ? Boolean(v.covered) : false,
|
|
35
|
-
evidence: typeof v?.evidence === "string" ? v.evidence.trim() : "",
|
|
36
|
-
};
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
export class AcceptanceCoverageRole extends AgentRole {
|
|
40
|
-
constructor(opts) {
|
|
41
|
-
super({ ...opts, name: "acceptance-coverage" });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// A different AI than the coder: the reviewer's, unless the role has its own.
|
|
45
|
-
resolveProvider() {
|
|
46
|
-
return this.config?.roles?.["acceptance-coverage"]?.provider
|
|
47
|
-
|| this.config?.roles?.reviewer?.provider
|
|
48
|
-
|| this.config?.reviewer
|
|
49
|
-
|| "codex";
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
extractInput(input) {
|
|
53
|
-
return { ...super.extractInput(input), criteria: Array.isArray(input?.criteria) ? input.criteria : [], diff: String(input?.diff || "") };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async buildPrompt({ task, criteria, diff }) {
|
|
57
|
-
this._criteria = criteria;
|
|
58
|
-
return { prompt: buildAcceptanceCoveragePrompt({ criteria, diff, task, instructions: this.instructions }) };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
parseOutput(raw) { return extractFirstJson(raw); }
|
|
62
|
-
|
|
63
|
-
// A verdict with no `criteria` array is not a verdict: the reviewer's failure.
|
|
64
|
-
isSuccessful(parsed) { return Array.isArray(parsed?.criteria); }
|
|
65
|
-
|
|
66
|
-
buildSuccessResult(parsed, provider) {
|
|
67
|
-
const criteria = normalizeVerdicts(parsed, this._criteria || []);
|
|
68
|
-
const uncovered = criteria.filter((c) => !c.covered);
|
|
69
|
-
return { criteria, uncovered, provider, error: Array.isArray(parsed?.criteria) ? undefined : "no criteria array in the verdict" };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
buildSummary(parsed) {
|
|
73
|
-
if (!Array.isArray(parsed?.criteria)) return "acceptance-coverage: unreadable verdict (no criteria array)";
|
|
74
|
-
const total = (this._criteria || []).length;
|
|
75
|
-
const uncovered = normalizeVerdicts(parsed, this._criteria || []).filter((c) => !c.covered).length;
|
|
76
|
-
return uncovered === 0 ? `acceptance-coverage: ${total}/${total} criteria covered` : `acceptance-coverage: ${uncovered} of ${total} criteria NOT covered`;
|
|
77
|
-
}
|
|
78
|
-
}
|