karajan-code 4.70.4 → 5.0.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/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/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 -143
- package/src/cli/register-plan.js +9 -100
- package/src/cli/register-roles-skills.js +1 -1
- 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 -150
- package/src/commands/go.js +71 -53
- package/src/commands/harden.js +90 -81
- 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 +121 -55
- package/src/commands/review-gate.js +421 -341
- package/src/commands/skills.js +4 -1
- 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 +107 -26
- package/src/config/defaults.js +1 -0
- package/src/config/loader.js +34 -17
- package/src/config/role-resolver.js +1 -0
- package/src/config/schema.js +1 -0
- package/src/config/secrets.js +36 -0
- package/src/config.js +1 -0
- package/src/environment/adr.js +16 -2
- package/src/git/karajan-label.js +20 -8
- package/src/guards/main-guard.js +54 -0
- package/src/harden/hook-templates.js +16 -1
- package/src/harden/sentinel/posttooluse.mjs +18 -6
- package/src/harden/sentinel/pretooluse-sentinel.mjs +64 -36
- package/src/harden/sentinel/sentinel-lib.mjs +129 -26
- package/src/harden/sentinel/sentinel-shell.mjs +5 -16
- package/src/harden/sentinel/sessionstart.mjs +3 -2
- package/src/harden/sentinel/stop.mjs +5 -4
- 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 +83 -13
- 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 +1 -35
- package/src/mcp/handlers/management-handlers.js +2 -42
- package/src/mcp/handlers/rag-handler.js +9 -1
- package/src/mcp/server-handlers.js +16 -22
- package/src/mcp/tools.js +1 -196
- 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 +71 -44
- package/src/rag/bootstrap-index.js +46 -54
- package/src/rag/indexer.js +9 -4
- package/src/rag/migrate.js +1 -1
- package/src/rag/onnx-fallback.js +2 -2
- package/src/rag/pid-lease.js +181 -0
- package/src/rag/project-store.js +2 -12
- package/src/review/card-first.js +57 -46
- package/src/review/gate-gitignore.js +23 -4
- package/src/review/gate-hook.js +12 -4
- package/src/review/one-shot-review.js +45 -35
- package/src/review/publish-pr.js +14 -2
- package/src/review/repo-docs.js +73 -0
- package/src/review/repo-stack.js +59 -0
- package/src/review/reviewer-fallback.js +19 -4
- 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 +1 -24
- package/src/roles/audit-role.js +106 -125
- package/src/skills/atril-cli.js +19 -1
- package/src/skills/atril-skills.js +5 -4
- package/src/skills/skills-search.js +72 -0
- package/src/skills/translate.js +62 -0
- 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/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
package/src/sonar/scanner.js
CHANGED
|
@@ -85,7 +85,9 @@ export function buildScannerOpts(projectKey, scanner = {}, where = {}) {
|
|
|
85
85
|
if (scanner.scm_disabled || where.linkedWorktree) opts.push("-Dsonar.scm.disabled=true");
|
|
86
86
|
// KJC-TSK-0838: verbose is what makes the scanner name every indexed file.
|
|
87
87
|
if (scanner.verbose) opts.push("-Dsonar.verbose=true");
|
|
88
|
+
if (scanner.project_name) opts.push(`-Dsonar.projectName=${scanner.project_name}`);
|
|
88
89
|
if (scanner.sources) opts.push(`-Dsonar.sources=${scanner.sources}`);
|
|
90
|
+
if (scanner.tests) opts.push(`-Dsonar.tests=${scanner.tests}`);
|
|
89
91
|
if (scanner.exclusions) opts.push(`-Dsonar.exclusions=${scanner.exclusions}`);
|
|
90
92
|
if (scanner.test_inclusions) opts.push(`-Dsonar.test.inclusions=${scanner.test_inclusions}`);
|
|
91
93
|
if (scanner.coverage_exclusions) opts.push(`-Dsonar.coverage.exclusions=${scanner.coverage_exclusions}`);
|
|
@@ -107,14 +109,15 @@ export function buildScannerOpts(projectKey, scanner = {}, where = {}) {
|
|
|
107
109
|
return opts.join(" ");
|
|
108
110
|
}
|
|
109
111
|
|
|
110
|
-
|
|
112
|
+
// The folders are checked in the scanned directory (baseDir), not in process.cwd().
|
|
113
|
+
function normalizeScannerConfig(scanner = {}, baseDir = process.cwd()) {
|
|
111
114
|
const out = { ...scanner };
|
|
112
115
|
if (typeof out.sources === "string" && out.sources.trim()) {
|
|
113
116
|
const existing = out.sources
|
|
114
117
|
.split(",")
|
|
115
118
|
.map((s) => s.trim())
|
|
116
119
|
.filter(Boolean)
|
|
117
|
-
.filter((entry) => fs.existsSync(entry));
|
|
120
|
+
.filter((entry) => fs.existsSync(path.resolve(baseDir, entry)));
|
|
118
121
|
|
|
119
122
|
if (existing.length > 0) {
|
|
120
123
|
out.sources = existing.join(",");
|
|
@@ -238,7 +241,7 @@ export async function resolveSonarTokenWithFallback(config, apiHost) {
|
|
|
238
241
|
export function respectRepoProperties(scanner = {}, props = {}) {
|
|
239
242
|
if (!props.existed) return scanner;
|
|
240
243
|
const kept = { ...scanner };
|
|
241
|
-
for (const k of ["sources", "exclusions", "test_inclusions", "coverage_exclusions", "javascript_lcov_report_paths"]) delete kept[k];
|
|
244
|
+
for (const k of ["project_name", "sources", "tests", "exclusions", "test_inclusions", "coverage_exclusions", "javascript_lcov_report_paths"]) delete kept[k];
|
|
242
245
|
return kept;
|
|
243
246
|
}
|
|
244
247
|
|
|
@@ -271,100 +274,145 @@ export async function ensureSonarProjectProperties(cwd = process.cwd()) {
|
|
|
271
274
|
const wanted = SOURCE_DIRS.map((d) => `${d}/`).join(", ");
|
|
272
275
|
return { existed: false, declaredKey: null, written: false, nothingToScan: `no ${wanted} folder to analyse: write a sonar-project.properties declaring sonar.sources when the code arrives` };
|
|
273
276
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
...
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const note = `sonar: wrote sonar-project.properties (${layout.join(", ")}): review it and commit it`;
|
|
286
|
-
return { existed: false, declaredKey: null, written: true, note };
|
|
277
|
+
// KJC-BUG-0445 (#2854): nothing is written in the tree (a lane kept the file
|
|
278
|
+
// untracked); the layout reaches the scanner as -D opts, under kj's config.
|
|
279
|
+
const layout = {
|
|
280
|
+
project_name: pkg.name || path.basename(cwd),
|
|
281
|
+
sources: sources.join(","),
|
|
282
|
+
...(tests.length ? { tests: tests.join(",") } : {}),
|
|
283
|
+
exclusions: "**/node_modules/**,**/dist/**,**/build/**,**/coverage/**",
|
|
284
|
+
};
|
|
285
|
+
const declared = [`sonar.sources=${layout.sources}`, ...(layout.tests ? [`sonar.tests=${layout.tests}`] : [])];
|
|
286
|
+
const note = `sonar: no sonar-project.properties, kj scanned with ${declared.join(", ")}: commit one to own the layout`;
|
|
287
|
+
return { existed: false, declaredKey: null, written: false, layout, note };
|
|
287
288
|
}
|
|
288
289
|
}
|
|
289
290
|
|
|
290
|
-
|
|
291
|
-
const scanCwd = cwd || process.cwd();
|
|
292
|
-
// KJC-TSK-0838 step 3 (monorepo): a nested package scans from its own
|
|
293
|
-
// directory under the key its properties declare — so the proof of
|
|
294
|
-
// coverage is about that package, not the root's `sonar.sources`.
|
|
295
|
-
const repoProps = await ensureSonarProjectProperties(scanCwd);
|
|
296
|
-
if (repoProps.nothingToScan) {
|
|
297
|
-
return { ok: false, skipped: true, projectKey: null, stdout: "", stderr: `sonar: ${repoProps.nothingToScan}`, exitCode: 0 };
|
|
298
|
-
}
|
|
299
|
-
let effectiveProjectKey;
|
|
300
|
-
if (cwd && !projectKey && repoProps.declaredKey) {
|
|
301
|
-
effectiveProjectKey = repoProps.declaredKey;
|
|
302
|
-
} else {
|
|
303
|
-
try {
|
|
304
|
-
effectiveProjectKey = await resolveSonarProjectKey(config, { projectKey });
|
|
305
|
-
} catch (error) {
|
|
306
|
-
return {
|
|
307
|
-
ok: false,
|
|
308
|
-
projectKey: null,
|
|
309
|
-
stdout: "",
|
|
310
|
-
stderr: error?.message || String(error),
|
|
311
|
-
exitCode: 1
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
const sonarConfig = config?.sonarqube || {};
|
|
291
|
+
function scanSettings(sonarConfig) {
|
|
316
292
|
const rawHost = sonarConfig.host || "http://localhost:9000";
|
|
317
|
-
const isExternalSonar = sonarConfig.external === true;
|
|
318
|
-
const scannerTimeout = Number(sonarConfig?.timeouts?.scanner_ms) > 0
|
|
319
|
-
? Number(sonarConfig.timeouts.scanner_ms)
|
|
320
|
-
: 15 * 60 * 1000;
|
|
321
|
-
const sonarNetwork = sonarConfig.network || "karajan_sonar_net";
|
|
322
|
-
const apiHost = resolveSonarHost(rawHost);
|
|
323
293
|
const isLocalHost = /localhost|127\.0\.0\.1/.test(rawHost);
|
|
324
|
-
|
|
294
|
+
return {
|
|
295
|
+
rawHost,
|
|
296
|
+
isExternalSonar: sonarConfig.external === true,
|
|
297
|
+
scannerTimeout: Number(sonarConfig?.timeouts?.scanner_ms) > 0 ? Number(sonarConfig.timeouts.scanner_ms) : 15 * 60 * 1000,
|
|
298
|
+
sonarNetwork: sonarConfig.network || "karajan_sonar_net",
|
|
299
|
+
apiHost: resolveSonarHost(rawHost),
|
|
300
|
+
isLocalHost,
|
|
301
|
+
host: isLocalHost ? rawHost.replaceAll(/localhost|127\.0\.0\.1/g, "host.docker.internal") : rawHost,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const repoPropertiesNote = (repoProps, sonarConfig) => (repoProps.existed && sonarConfig.scanner?.sources
|
|
306
|
+
? "sonar: the repo's sonar-project.properties wins over sonarqube.scanner.sources — kj passed only its ignore rules"
|
|
307
|
+
: null);
|
|
325
308
|
|
|
309
|
+
// Server up, token resolved (and exported for the scanner), coverage generated:
|
|
310
|
+
// the first step that fails is the scan's result.
|
|
311
|
+
async function prepareServer(config, rawHost, apiHost) {
|
|
326
312
|
const start = await sonarUp(rawHost);
|
|
327
313
|
if (start.exitCode !== 0) {
|
|
328
|
-
return {
|
|
329
|
-
ok: false,
|
|
330
|
-
stdout: start.stdout || "",
|
|
331
|
-
stderr: start.stderr || "Failed to start SonarQube service",
|
|
332
|
-
exitCode: start.exitCode
|
|
333
|
-
};
|
|
314
|
+
return { failure: { ok: false, stdout: start.stdout || "", stderr: start.stderr || "Failed to start SonarQube service", exitCode: start.exitCode } };
|
|
334
315
|
}
|
|
335
|
-
// KJC-BUG-0156: kj's -Dsonar.projectKey stays (scan and query must use the
|
|
336
|
-
// SAME key), but the repo's properties own the LAYOUT from here on.
|
|
337
316
|
const token = await resolveSonarTokenWithFallback(config, apiHost);
|
|
338
317
|
if (!token) {
|
|
339
|
-
return {
|
|
318
|
+
return { failure: {
|
|
340
319
|
ok: false,
|
|
341
320
|
stdout: "",
|
|
342
321
|
stderr:
|
|
343
322
|
"Unable to resolve Sonar token. Fix: (1) run 'kj init' to configure it, (2) set KJ_SONAR_TOKEN env var, or (3) add sonarqube.token to ~/.karajan/kj.config.yml.",
|
|
344
323
|
exitCode: 1
|
|
345
|
-
};
|
|
324
|
+
} };
|
|
346
325
|
}
|
|
347
326
|
process.env.KJ_SONAR_TOKEN = token;
|
|
348
327
|
const coverage = await maybeRunCoverage(config);
|
|
349
328
|
if (!coverage.ok) {
|
|
350
|
-
return {
|
|
329
|
+
return { failure: {
|
|
351
330
|
ok: false,
|
|
352
331
|
stdout: coverage.stdout || "",
|
|
353
332
|
stderr: coverage.stderr || "Failed to generate coverage report for SonarQube",
|
|
354
333
|
exitCode: coverage.exitCode || 1
|
|
334
|
+
} };
|
|
335
|
+
}
|
|
336
|
+
return { token, coverage };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// A nested package (explicit cwd, no explicit key) scans under the key its properties declare.
|
|
340
|
+
function scanProjectKey(config, projectKey, cwd, repoProps) {
|
|
341
|
+
if (cwd && !projectKey && repoProps.declaredKey) return repoProps.declaredKey;
|
|
342
|
+
return resolveSonarProjectKey(config, { projectKey });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function scannerCommand(pick, { scanCwd, isLocalHost, isExternalSonar, sonarNetwork }) {
|
|
346
|
+
if (pick.type === "native") return { cmd: pick.bin, args: [] };
|
|
347
|
+
return {
|
|
348
|
+
cmd: "docker",
|
|
349
|
+
args: [
|
|
350
|
+
"run",
|
|
351
|
+
"--rm",
|
|
352
|
+
"-v",
|
|
353
|
+
`${scanCwd}:/usr/src`,
|
|
354
|
+
...(isLocalHost ? ["--add-host", "host.docker.internal:host-gateway"] : []),
|
|
355
|
+
...(isLocalHost || isExternalSonar ? [] : ["--network", sonarNetwork]),
|
|
356
|
+
"-e", "SONAR_HOST_URL",
|
|
357
|
+
"-e", "SONAR_TOKEN",
|
|
358
|
+
"-e", "SONAR_SCANNER_OPTS",
|
|
359
|
+
"sonarsource/sonar-scanner-cli"
|
|
360
|
+
]
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// KJC-BUG-0263: the scan is done when the server has processed it; whoever reads
|
|
365
|
+
// the issues next would otherwise get the previous analysis. Said out loud when
|
|
366
|
+
// the log names no task to wait for.
|
|
367
|
+
async function serverAnalysis(result, apiConfig) {
|
|
368
|
+
const taskId = /api\/ce\/task\?id=([\w-]+)/.exec(String(result.stdout || ""))?.[1];
|
|
369
|
+
if (!taskId) {
|
|
370
|
+
return { ok: true, analysisNote: "sonar: the scanner log names no analysis task, so kj could not wait for the server — issues may belong to the previous analysis" };
|
|
371
|
+
}
|
|
372
|
+
const done = await waitForAnalysis(apiConfig, taskId);
|
|
373
|
+
if (done.ok) return { ok: true, analysisNote: null };
|
|
374
|
+
return { ok: false, analysisNote: `sonar: the server did not finish analysis ${taskId} (${done.status}${done.timedOut ? ", timed out" : ""}) — its issues would be the previous analysis` };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function runSonarScan(config, projectKey = null, { verbose = false, cwd = null } = {}) {
|
|
378
|
+
const scanCwd = cwd || process.cwd();
|
|
379
|
+
// KJC-TSK-0838 step 3 (monorepo): a nested package scans from its own
|
|
380
|
+
// directory under the key its properties declare — so the proof of
|
|
381
|
+
// coverage is about that package, not the root's `sonar.sources`.
|
|
382
|
+
const repoProps = await ensureSonarProjectProperties(scanCwd);
|
|
383
|
+
if (repoProps.nothingToScan) {
|
|
384
|
+
return { ok: false, skipped: true, projectKey: null, stdout: "", stderr: `sonar: ${repoProps.nothingToScan}`, exitCode: 0 };
|
|
385
|
+
}
|
|
386
|
+
let effectiveProjectKey;
|
|
387
|
+
try {
|
|
388
|
+
effectiveProjectKey = await scanProjectKey(config, projectKey, cwd, repoProps);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
return {
|
|
391
|
+
ok: false,
|
|
392
|
+
projectKey: null,
|
|
393
|
+
stdout: "",
|
|
394
|
+
stderr: error?.message || String(error),
|
|
395
|
+
exitCode: 1
|
|
355
396
|
};
|
|
356
397
|
}
|
|
398
|
+
const sonarConfig = config?.sonarqube || {};
|
|
399
|
+
const { rawHost, isExternalSonar, scannerTimeout, sonarNetwork, apiHost, isLocalHost, host } = scanSettings(sonarConfig);
|
|
400
|
+
|
|
401
|
+
const prepared = await prepareServer(config, rawHost, apiHost);
|
|
402
|
+
if (prepared.failure) return prepared.failure;
|
|
403
|
+
const { token, coverage } = prepared;
|
|
404
|
+
// KJC-BUG-0156: kj's -Dsonar.projectKey stays (scan and query must use the
|
|
405
|
+
// SAME key), but the repo's properties own the LAYOUT from here on.
|
|
357
406
|
const scannerConfig = respectRepoProperties(
|
|
358
407
|
normalizeScannerConfig({
|
|
408
|
+
...repoProps.layout,
|
|
359
409
|
...sonarConfig.scanner,
|
|
360
410
|
...coverage.scannerPatch,
|
|
361
411
|
...(verbose ? { verbose: true } : {})
|
|
362
|
-
}),
|
|
412
|
+
}, scanCwd),
|
|
363
413
|
repoProps
|
|
364
414
|
);
|
|
365
|
-
const note = repoProps
|
|
366
|
-
? "sonar: the repo's sonar-project.properties wins over sonarqube.scanner.sources — kj passed only its ignore rules"
|
|
367
|
-
: null;
|
|
415
|
+
const note = repoPropertiesNote(repoProps, sonarConfig);
|
|
368
416
|
|
|
369
417
|
const pick = await pickSonarScanner(sonarConfig.scanner);
|
|
370
418
|
// KJC-BUG-0225: en un carril, el plugin SCM del scanner (JGit) no sabe abrir
|
|
@@ -379,44 +427,11 @@ export async function runSonarScan(config, projectKey = null, { verbose = false,
|
|
|
379
427
|
SONAR_SCANNER_OPTS: buildScannerOpts(effectiveProjectKey, scannerConfig, { linkedWorktree })
|
|
380
428
|
};
|
|
381
429
|
|
|
382
|
-
|
|
383
|
-
if (pick.type === "native") {
|
|
384
|
-
cmd = pick.bin;
|
|
385
|
-
args = [];
|
|
386
|
-
} else {
|
|
387
|
-
cmd = "docker";
|
|
388
|
-
args = [
|
|
389
|
-
"run",
|
|
390
|
-
"--rm",
|
|
391
|
-
"-v",
|
|
392
|
-
`${scanCwd}:/usr/src`,
|
|
393
|
-
...(isLocalHost ? ["--add-host", "host.docker.internal:host-gateway"] : []),
|
|
394
|
-
...(isLocalHost || isExternalSonar ? [] : ["--network", sonarNetwork]),
|
|
395
|
-
"-e", "SONAR_HOST_URL",
|
|
396
|
-
"-e", "SONAR_TOKEN",
|
|
397
|
-
"-e", "SONAR_SCANNER_OPTS",
|
|
398
|
-
"sonarsource/sonar-scanner-cli"
|
|
399
|
-
];
|
|
400
|
-
}
|
|
401
|
-
|
|
430
|
+
const { cmd, args } = scannerCommand(pick, { scanCwd, isLocalHost, isExternalSonar, sonarNetwork });
|
|
402
431
|
const result = await runCommand(cmd, args, { timeout: scannerTimeout, env, cwd: scanCwd });
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
let ok = result.exitCode === 0;
|
|
407
|
-
let analysisNote = null;
|
|
408
|
-
if (ok) {
|
|
409
|
-
const taskId = /api\/ce\/task\?id=([\w-]+)/.exec(String(result.stdout || ""))?.[1];
|
|
410
|
-
if (taskId) {
|
|
411
|
-
const done = await waitForAnalysis({ ...config, sonarqube: { ...sonarConfig, host: apiHost } }, taskId);
|
|
412
|
-
if (!done.ok) {
|
|
413
|
-
ok = false;
|
|
414
|
-
analysisNote = `sonar: the server did not finish analysis ${taskId} (${done.status}${done.timedOut ? ", timed out" : ""}) — its issues would be the previous analysis`;
|
|
415
|
-
}
|
|
416
|
-
} else {
|
|
417
|
-
analysisNote = "sonar: the scanner log names no analysis task, so kj could not wait for the server — issues may belong to the previous analysis";
|
|
418
|
-
}
|
|
419
|
-
}
|
|
432
|
+
const { ok, analysisNote } = result.exitCode === 0
|
|
433
|
+
? await serverAnalysis(result, { ...config, sonarqube: { ...sonarConfig, host: apiHost } })
|
|
434
|
+
: { ok: false, analysisNote: null };
|
|
420
435
|
return {
|
|
421
436
|
ok,
|
|
422
437
|
projectKey: effectiveProjectKey,
|
|
@@ -154,11 +154,12 @@ export function evaluateCoverageConfig({ projectDir }) {
|
|
|
154
154
|
}
|
|
155
155
|
return { verdict: VERDICTS.NOT_OBSERVABLE, evidence: "no coverage threshold configured anywhere — nothing measures the level", remedy: "instrument: configure a coverage threshold (any value the team stands behind — no 80% is demanded)" };
|
|
156
156
|
}
|
|
157
|
-
/** AC3 — phantom coverage: the
|
|
158
|
-
*
|
|
159
|
-
* No output → not observable: never ok by absence
|
|
160
|
-
export function evaluatePhantomCoverage({ phantoms } = {}) {
|
|
157
|
+
/** AC3 — phantom coverage: the sweep (KJC-TSK-1343) runs the two detectors
|
|
158
|
+
* (KJC-TSK-0800) over the test↔source pairs and hands their output in.
|
|
159
|
+
* No output → not observable, with the sweep's reason: never ok by absence. */
|
|
160
|
+
export function evaluatePhantomCoverage({ phantoms, reason } = {}) {
|
|
161
161
|
if (!Array.isArray(phantoms)) {
|
|
162
|
+
if (reason) return { verdict: VERDICTS.NOT_OBSERVABLE, evidence: reason, remedy: "the detectors read Lit components in a git repository; until the project has them this invariant cannot be observed" };
|
|
162
163
|
return { verdict: VERDICTS.NOT_OBSERVABLE, evidence: "the phantom detectors were not run", remedy: "instrument: run the detectors (steward/phantom-coverage) over the suite's test↔source pairs and feed their output" };
|
|
163
164
|
}
|
|
164
165
|
if (phantoms.length > 0) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phantom sweep (KJC-TSK-1343) — finds the test↔source pairs the phantom
|
|
3
|
+
* detectors (KJC-TSK-0800) need and runs them. Only Lit components sit inside
|
|
4
|
+
* the reachability perimeter, so only they can make a test a phantom; anywhere
|
|
5
|
+
* else the answer is NOT OBSERVABLE with its reason, never a clean bill.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { analyzeMemberReachability } from "../audit/member-reachability.js";
|
|
10
|
+
import { detectPhantomUnit, detectPhantomE2E } from "./phantom-coverage.js";
|
|
11
|
+
import { listProjectFiles } from "../rag/indexable.js";
|
|
12
|
+
|
|
13
|
+
const CODE = /\.(m?js|jsx|ts|tsx|mts)$/;
|
|
14
|
+
const TEST = /\.(test|spec)\.(m?js|jsx|ts|tsx|mts)$/;
|
|
15
|
+
const RELATIVE_IMPORT = /(?:from\s*|import\s*\(\s*)["'](\.{1,2}\/[^"']+)["']/g;
|
|
16
|
+
const SUFFIXES = ["", ".js", ".mjs", ".ts", ".tsx", ".jsx", "/index.js", "/index.ts"];
|
|
17
|
+
|
|
18
|
+
/** The candidate sources a test imports through relative specifiers. */
|
|
19
|
+
function importedCandidates(test, text, candidates) {
|
|
20
|
+
const out = new Set();
|
|
21
|
+
for (const [, spec] of text.matchAll(RELATIVE_IMPORT)) {
|
|
22
|
+
const base = path.posix.normalize(path.posix.join(path.posix.dirname(test), spec));
|
|
23
|
+
const hit = SUFFIXES.map((s) => base + s).find((p) => candidates.has(p));
|
|
24
|
+
if (hit) out.add(hit);
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const defaultRead = (projectDir) => (rel) => fs.readFileSync(path.join(projectDir, rel), "utf8");
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {{projectDir: string, files?: string[]|null, read?: (rel: string) => string}} opts
|
|
33
|
+
* @returns {{observable: boolean, reason?: string, phantoms?: Array<object>}}
|
|
34
|
+
*/
|
|
35
|
+
export function sweepPhantoms({ projectDir, files = listProjectFiles(projectDir), read = defaultRead(projectDir) }) {
|
|
36
|
+
if (!Array.isArray(files)) return { observable: false, reason: "not a git repository: the project's tests and sources cannot be listed" };
|
|
37
|
+
const texts = new Map();
|
|
38
|
+
for (const f of files.filter((x) => CODE.test(x))) {
|
|
39
|
+
try { texts.set(f, read(f)); } catch { /* listed but gone from the tree: nothing to read */ }
|
|
40
|
+
}
|
|
41
|
+
const tests = [...texts.keys()].filter((f) => TEST.test(f));
|
|
42
|
+
const sources = [...texts.keys()].filter((f) => !TEST.test(f));
|
|
43
|
+
const analyzed = sources
|
|
44
|
+
.filter((f) => texts.get(f).includes("LitElement"))
|
|
45
|
+
.map((file) => ({ file, analysis: analyzeMemberReachability(texts.get(file), { file }) }))
|
|
46
|
+
.filter(({ analysis }) => analysis.observable && analysis.classes.some((c) => c.observable));
|
|
47
|
+
if (analyzed.length === 0) {
|
|
48
|
+
return { observable: false, reason: "no observable Lit component (LitElement): the reachability analysis only vouches for Lit" };
|
|
49
|
+
}
|
|
50
|
+
const candidates = new Map(analyzed
|
|
51
|
+
.filter(({ analysis }) => analysis.classes.some((c) => c.observable && c.unreachable.length > 0))
|
|
52
|
+
.map((c) => [c.file, c.analysis]));
|
|
53
|
+
const phantoms = [];
|
|
54
|
+
for (const test of tests) {
|
|
55
|
+
const testSource = texts.get(test);
|
|
56
|
+
for (const file of importedCandidates(test, testSource, candidates)) {
|
|
57
|
+
phantoms.push(...detectPhantomUnit({ sourceAnalysis: candidates.get(file), testSource, file: test }).phantoms);
|
|
58
|
+
}
|
|
59
|
+
for (const [file, analysis] of candidates) {
|
|
60
|
+
const found = detectPhantomE2E({ sourceText: texts.get(file), sourceAnalysis: analysis, testSource, file: test }).phantoms;
|
|
61
|
+
// a literal another source also carries may be alive there: never accused
|
|
62
|
+
phantoms.push(...found.filter((p) => !sources.some((s) => s !== file && texts.get(s).includes(p.literal))));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { observable: true, phantoms };
|
|
66
|
+
}
|
package/src/tournament/judge.js
CHANGED
|
@@ -58,33 +58,15 @@ function finalistBlock(board, torDir, coder) {
|
|
|
58
58
|
].filter(Boolean).join("\n");
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const board = JSON.parse(fs.readFileSync(path.join(tournamentDir, "scoreboard.json"), "utf8"));
|
|
68
|
-
const finalists = board.rows.filter((r) => r.finalist).map((r) => r.coder);
|
|
69
|
-
const participants = board.rows.map((r) => r.coder);
|
|
70
|
-
|
|
71
|
-
if (finalists.length === 0) throw new Error("tournament: sin finalistas (todas las suites rojas) — no hay nada que juzgar");
|
|
72
|
-
if (finalists.length === 1) {
|
|
73
|
-
const judgement = { winner: finalists[0], judge: null, escalated: false, walkover: true, board_ranking: board.ranking };
|
|
74
|
-
fs.writeFileSync(path.join(tournamentDir, "judgement.json"), JSON.stringify(judgement, null, 2));
|
|
75
|
-
logger?.info?.(`torneo ${board.id}: ganador por walkover — ${finalists[0]} (único finalista)`);
|
|
76
|
-
return judgement;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const judge = await judgeFn({ config, participants });
|
|
80
|
-
if (!judge) {
|
|
81
|
-
throw new Error(
|
|
82
|
-
"tournament: sin juez limpio — todos los agentes disponibles compitieron o son el host. " +
|
|
83
|
-
"Un participante jamás juzga su propio torneo: quita un coder o añade un agente."
|
|
84
|
-
);
|
|
85
|
-
}
|
|
61
|
+
function walkover({ board, tournamentDir, logger, winner }) {
|
|
62
|
+
const judgement = { winner, judge: null, escalated: false, walkover: true, board_ranking: board.ranking };
|
|
63
|
+
fs.writeFileSync(path.join(tournamentDir, "judgement.json"), JSON.stringify(judgement, null, 2));
|
|
64
|
+
logger?.info?.(`torneo ${board.id}: ganador por walkover — ${winner} (único finalista)`);
|
|
65
|
+
return judgement;
|
|
66
|
+
}
|
|
86
67
|
|
|
87
|
-
|
|
68
|
+
function judgePrompt(board, finalists, tournamentDir) {
|
|
69
|
+
return [
|
|
88
70
|
"Eres el JUEZ de un torneo de código: varios coders resolvieron la MISMA tarea de forma independiente.",
|
|
89
71
|
"La fase determinista ya eliminó a los que suspendieron la suite. Tu trabajo: ordenar a los finalistas",
|
|
90
72
|
"por calidad de solución (diseño, legibilidad, ausencia de sobre-ingeniería), con el scoreboard a la vista.",
|
|
@@ -95,49 +77,74 @@ export async function judgeTournament({ tournamentDir, config = {}, logger = con
|
|
|
95
77
|
'Responde SOLO con JSON: {"ranking": ["coder", ...], "reasons": {"coder": "por qué"}, "tie": false}',
|
|
96
78
|
"Declara tie:true únicamente si de verdad no puedes distinguirlos.",
|
|
97
79
|
].join("\n\n");
|
|
80
|
+
}
|
|
98
81
|
|
|
99
|
-
|
|
100
|
-
const res = await agentFn({ judge, prompt, config, logger });
|
|
101
|
-
if (!res?.ok) throw new Error(`tournament: el juez ${judge} falló: ${res?.error || "sin salida"}`);
|
|
82
|
+
function parseVerdict(output, judge, finalists) {
|
|
102
83
|
let verdict;
|
|
103
84
|
try {
|
|
104
|
-
verdict = JSON.parse(String(
|
|
85
|
+
verdict = JSON.parse(String(output).replace(/^[^{]*/, "").replace(/[^}]*$/, ""));
|
|
105
86
|
} catch {
|
|
106
87
|
throw new Error(`tournament: el juez ${judge} no devolvió un veredicto parseable`);
|
|
107
88
|
}
|
|
108
89
|
// An LLM verdict is untrusted input: the ranking must name FINALISTS and
|
|
109
90
|
// nobody else — a hallucinated coder must fail loud, never get crowned.
|
|
110
|
-
|
|
111
|
-
if (!Array.isArray(verdict.ranking) || verdict.ranking.length === 0 || !verdict.ranking.every(isFinalist)) {
|
|
91
|
+
if (!Array.isArray(verdict.ranking) || verdict.ranking.length === 0 || !verdict.ranking.every((c) => finalists.includes(c))) {
|
|
112
92
|
throw new Error(`tournament: ranking del juez inválido (${JSON.stringify(verdict.ranking)}) — debe nombrar solo finalistas: ${finalists.join(", ")}`);
|
|
113
93
|
}
|
|
94
|
+
return verdict;
|
|
95
|
+
}
|
|
114
96
|
|
|
115
|
-
|
|
116
|
-
|
|
97
|
+
// Only FIRST place crowns a winner: a judge that reorders lower positions
|
|
98
|
+
// but agrees on who wins is agreement, not a dispute for solomon.
|
|
99
|
+
async function crownedBy({ verdict, board, finalists, solomonFn }) {
|
|
117
100
|
const judgeTop = verdict.ranking?.[0];
|
|
118
101
|
const boardTop = board.ranking[0];
|
|
119
|
-
const
|
|
120
|
-
|
|
102
|
+
const escalated = Boolean(verdict.tie || judgeTop !== boardTop);
|
|
103
|
+
if (!escalated) return { winner: judgeTop, solomon: null, escalated };
|
|
104
|
+
if (!solomonFn) {
|
|
105
|
+
const why = verdict.tie ? "empate declarado" : `discrepancia juez(${judgeTop}) vs scoreboard(${boardTop})`;
|
|
106
|
+
throw new Error(`tournament: ${why} y sin árbitro configurado`);
|
|
107
|
+
}
|
|
108
|
+
const solomon = await solomonFn({
|
|
109
|
+
judgeRanking: verdict.ranking,
|
|
110
|
+
boardRanking: board.ranking,
|
|
111
|
+
reasons: verdict.reasons,
|
|
112
|
+
tie: Boolean(verdict.tie),
|
|
113
|
+
});
|
|
114
|
+
if (!finalists.includes(solomon?.winner)) {
|
|
115
|
+
throw new Error(`tournament: solomon devolvió un ganador inválido (${solomon?.winner}) — debe ser finalista`);
|
|
116
|
+
}
|
|
117
|
+
return { winner: solomon.winner, solomon, escalated };
|
|
118
|
+
}
|
|
121
119
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Judge a scored tournament: rank finalists, escalate on tie/disagreement.
|
|
122
|
+
* @returns {Promise<{winner: string, judge: string|null, escalated: boolean, walkover?: boolean}>}
|
|
123
|
+
*/
|
|
124
|
+
export async function judgeTournament({ tournamentDir, config = {}, logger = console, deps = {} }) {
|
|
125
|
+
const { judgeFn = defaultJudge, agentFn = defaultAgent, solomonFn = null } = deps;
|
|
126
|
+
const board = JSON.parse(fs.readFileSync(path.join(tournamentDir, "scoreboard.json"), "utf8"));
|
|
127
|
+
const finalists = board.rows.filter((r) => r.finalist).map((r) => r.coder);
|
|
128
|
+
const participants = board.rows.map((r) => r.coder);
|
|
129
|
+
|
|
130
|
+
if (finalists.length === 0) throw new Error("tournament: sin finalistas (todas las suites rojas) — no hay nada que juzgar");
|
|
131
|
+
if (finalists.length === 1) return walkover({ board, tournamentDir, logger, winner: finalists[0] });
|
|
132
|
+
|
|
133
|
+
const judge = await judgeFn({ config, participants });
|
|
134
|
+
if (!judge) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
"tournament: sin juez limpio — todos los agentes disponibles compitieron o son el host. " +
|
|
137
|
+
"Un participante jamás juzga su propio torneo: quita un coder o añade un agente."
|
|
138
|
+
);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
const prompt = judgePrompt(board, finalists, tournamentDir);
|
|
142
|
+
logger?.info?.(`torneo ${board.id}: juez=${judge} sobre ${finalists.length} finalistas`);
|
|
143
|
+
const res = await agentFn({ judge, prompt, config, logger });
|
|
144
|
+
if (!res?.ok) throw new Error(`tournament: el juez ${judge} falló: ${res?.error || "sin salida"}`);
|
|
145
|
+
const verdict = parseVerdict(res.output, judge, finalists);
|
|
146
|
+
const { winner, solomon, escalated } = await crownedBy({ verdict, board, finalists, solomonFn });
|
|
147
|
+
|
|
141
148
|
const judgement = {
|
|
142
149
|
winner, judge, escalated,
|
|
143
150
|
judge_ranking: verdict.ranking, judge_reasons: verdict.reasons || {},
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-BUG-0458 (ADR 0040): with several gh accounts and parallel sessions, another session can
|
|
3
|
+
* switch the active account between `gh auth switch` and the gh calls kj makes, so kj posted a
|
|
4
|
+
* review under the wrong account. At startup kj pins GH_TOKEN to the repo's account, so every
|
|
5
|
+
* gh it runs inherits that token whatever the active account is by then. The account is the
|
|
6
|
+
* configured one, or else the owner of the origin remote. A token already set is never touched.
|
|
7
|
+
*/
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
// The owner segment of github.com remotes: https, ssh and ssh host aliases
|
|
11
|
+
// (github.com-work:owner/repo.git). Any other host is not gh's to answer for.
|
|
12
|
+
const OWNER = /(?:^|[@/])github\.com(?:-[\w.-]+)?[:/]([^/:\s]+)\/[^/\s]+?(?:\.git)?\/?$/;
|
|
13
|
+
|
|
14
|
+
/** The owner of a github.com remote URL, or null for any other host. */
|
|
15
|
+
export function remoteOwner(url) {
|
|
16
|
+
return OWNER.exec(String(url ?? "").trim())?.[1] ?? null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const quiet = (cmd, args, cwd) => execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Pin GH_TOKEN for every gh child of this process.
|
|
23
|
+
* @returns {{account: string|null, reason: string|null}} the pinned account, or why none was
|
|
24
|
+
*/
|
|
25
|
+
export function pinGhAccount({ env = process.env, cwd = process.cwd(), account = null, exec = quiet } = {}) {
|
|
26
|
+
if (env.GH_TOKEN || env.GITHUB_TOKEN) return { account: null, reason: null };
|
|
27
|
+
let login = account;
|
|
28
|
+
if (!login) {
|
|
29
|
+
try { login = remoteOwner(exec("git", ["remote", "get-url", "origin"], cwd)); } catch { login = null; }
|
|
30
|
+
}
|
|
31
|
+
if (!login) return { account: null, reason: null };
|
|
32
|
+
let token;
|
|
33
|
+
try { token = exec("gh", ["auth", "token", "--user", login], cwd); } catch { token = ""; }
|
|
34
|
+
if (!token) return { account: null, reason: `gh has no logged-in account ${login}: gh calls use the active account` };
|
|
35
|
+
env.GH_TOKEN = token;
|
|
36
|
+
return { account: login, reason: null };
|
|
37
|
+
}
|
package/src/utils/git.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { runCommand } from "./process.js";
|
|
2
2
|
import { getRunContext } from "./run-context.js";
|
|
3
3
|
import { identityEnv } from "../git/run-identity.js";
|
|
4
|
-
import { labelKarajanPr } from "../git/karajan-label.js";
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Module-scoped runner — legacy back-compat for callers outside a
|
|
@@ -313,15 +312,3 @@ export async function hasRemote(cwd = null) {
|
|
|
313
312
|
export async function pushBranch(branch, cwd = null) {
|
|
314
313
|
await runGit(["push", "origin", branch], cwd ? { cwd } : {});
|
|
315
314
|
}
|
|
316
|
-
|
|
317
|
-
export async function createPullRequest({ baseBranch, branch, title, body, cwd = null }) {
|
|
318
|
-
const args = ["pr", "create", "--base", baseBranch, "--head", branch, "--title", title, "--body", body];
|
|
319
|
-
const res = await runCommand("gh", args, cwd ? { cwd } : {});
|
|
320
|
-
if (res.exitCode !== 0) {
|
|
321
|
-
throw new Error(`gh ${args.join(" ")} failed: ${res.stderr || res.stdout}`);
|
|
322
|
-
}
|
|
323
|
-
const url = res.stdout.trim();
|
|
324
|
-
// KJC-TSK-1123: the PR Karajan opens carries its label; a failure is logged, the PR stays.
|
|
325
|
-
await labelKarajanPr({ url, cwd });
|
|
326
|
-
return url;
|
|
327
|
-
}
|
|
@@ -46,6 +46,9 @@ export async function migrateKjToKarajan({ dryRun = false, force = false } = {})
|
|
|
46
46
|
// the developer's real HOME. Migrator's own tests pass `force: true`.
|
|
47
47
|
if (process.env.VITEST && !force) return { migrated: false, reason: "vitest-skip" };
|
|
48
48
|
|
|
49
|
+
// KJC-BUG-0439: a KARAJAN_HOME chosen on purpose (CI, a sandbox, a canary) never receives
|
|
50
|
+
// the person's default ~/.kj; only an explicit KJ_HOME → KARAJAN_HOME pair is migrated.
|
|
51
|
+
if (process.env.KARAJAN_HOME && !process.env.KJ_HOME) return { migrated: false, reason: "explicit-home" };
|
|
49
52
|
const sourceDir = process.env.KJ_HOME || path.join(os.homedir(), ".kj");
|
|
50
53
|
const targetDir = process.env.KARAJAN_HOME || path.join(os.homedir(), ".karajan");
|
|
51
54
|
|
|
@@ -64,10 +67,13 @@ export async function migrateKjToKarajan({ dryRun = false, force = false } = {})
|
|
|
64
67
|
if (dryRun) return { migrated: false, reason: "dry-run", sourceDir, targetDir };
|
|
65
68
|
|
|
66
69
|
const backupDir = path.join(targetDir, "backup");
|
|
67
|
-
|
|
70
|
+
// The backup copies the person's files: readable by them only (KJC-BUG-0439).
|
|
71
|
+
await fs.mkdir(backupDir, { recursive: true, mode: 0o700 });
|
|
72
|
+
await fs.chmod(backupDir, 0o700);
|
|
68
73
|
const ts = new Date().toISOString().replaceAll(/[:.]/g, "-");
|
|
69
74
|
const backupPath = path.join(backupDir, `kj-pre-migration-${ts}.tar.gz`);
|
|
70
75
|
await execa("tar", ["-czf", backupPath, "-C", path.dirname(sourceDir), path.basename(sourceDir)]);
|
|
76
|
+
await fs.chmod(backupPath, 0o600);
|
|
71
77
|
|
|
72
78
|
const counts = {};
|
|
73
79
|
for (const sub of MOVE_DIRS) {
|