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
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
9
9
|
import { isAbsolute, join } from "node:path";
|
|
10
10
|
import { runCommand } from "../utils/process.js";
|
|
11
|
-
import { checkVerdict, diffHash, pruneVerdicts } from "../review/verdict-store.js";
|
|
11
|
+
import { checkVerdict, diffHash, pruneVerdicts, saveVerdict } from "../review/verdict-store.js";
|
|
12
12
|
import { runOneShotReview } from "../review/one-shot-review.js";
|
|
13
13
|
import { runSolomonArbitration } from "../review/solomon-arbitration.js";
|
|
14
14
|
import { ensureGateTrackable } from "../review/gate-gitignore.js";
|
|
@@ -34,6 +34,7 @@ import { evaluatePolicyGate } from "../review/policy-gate.js";
|
|
|
34
34
|
import { correctionNote, reviewCorrectionRounds } from "../review/correction-rounds.js";
|
|
35
35
|
import { cooldownAdvisories, cooldownGate, DEFAULT_COOLDOWN_HOURS, formatCooldown } from "../review/dep-cooldown.js";
|
|
36
36
|
import { branchOverlaps, formatOverlaps } from "../review/branch-overlap.js";
|
|
37
|
+
import { gateHookStatus } from "../review/gate-hook.js";
|
|
37
38
|
|
|
38
39
|
// KJC-TSK-0686 (MG-A): card-first is a gate, not a habit. Runs on --staged
|
|
39
40
|
// (before spending sonar/reviewer effort) AND on --check — the pre-commit
|
|
@@ -187,117 +188,102 @@ export async function solomonCommand({ config, logger = null, flags = {} }) {
|
|
|
187
188
|
return res;
|
|
188
189
|
}
|
|
189
190
|
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const gi = await ensureGateTrackable(projectDir);
|
|
202
|
-
if (gi.changed) {
|
|
203
|
-
console.log("✓ .gitignore adjusted: the gate contract (.karajan/review-gate, hooks) is now trackable — commit it");
|
|
204
|
-
}
|
|
205
|
-
const hookProbe = await runCommand("git", ["config", "core.hooksPath"]);
|
|
206
|
-
if (!hookProbe.stdout?.trim()) {
|
|
207
|
-
console.log("⚠ no core.hooksPath configured — run `kj harden` so the pre-commit hook enforces the gate");
|
|
208
|
-
}
|
|
209
|
-
return { installed: true };
|
|
191
|
+
async function installGateAction(projectDir) {
|
|
192
|
+
const fs = await import("node:fs/promises");
|
|
193
|
+
const marker = join(projectDir, ".karajan", "review-gate");
|
|
194
|
+
await fs.mkdir(join(projectDir, ".karajan"), { recursive: true });
|
|
195
|
+
await fs.writeFile(marker, "# Cross-AI review gate enabled (ENV-C1). Commit this file so the whole team inherits the gate.\n");
|
|
196
|
+
// KJC-BUG-0447 (#2655): the marker declares the gate; only a pre-commit that runs it enforces it.
|
|
197
|
+
const hook = gateHookStatus(projectDir);
|
|
198
|
+
if (hook.enforced) console.log("✓ review gate enabled — commits now require an approved cross-AI verdict (kj review --staged)");
|
|
199
|
+
else {
|
|
200
|
+
console.log(`✗ review gate declared (.karajan/review-gate) but NOT enforced: ${hook.reason} — run \`kj harden\` so the pre-commit hook runs it`);
|
|
201
|
+
process.exitCode = 1;
|
|
210
202
|
}
|
|
211
|
-
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const dryRun = Boolean(flags.dryRun || flags.checkOnly);
|
|
217
|
-
const days = Number(flags.pruneDays);
|
|
218
|
-
const res = await pruneVerdicts({ projectDir, dryRun, ...(Number.isFinite(days) && days > 0 ? { maxAgeDays: days } : {}) });
|
|
219
|
-
console.log(
|
|
220
|
-
dryRun
|
|
221
|
-
? `kj review --prune (dry run): ${res.expired.length} of ${res.scanned} verdict(s) older than ${res.maxAgeDays}d would be removed`
|
|
222
|
-
: `kj review --prune: removed ${res.removed} verdict(s) older than ${res.maxAgeDays}d (${res.scanned - res.removed} kept)`,
|
|
223
|
-
);
|
|
224
|
-
return { pruned: res.removed, dryRun };
|
|
203
|
+
// KJC-TSK-0646: a `.karajan/` dir-exclude would silently keep the
|
|
204
|
+
// contract out of git — rewrite it so the team actually inherits it.
|
|
205
|
+
const gi = await ensureGateTrackable(projectDir);
|
|
206
|
+
if (gi.changed) {
|
|
207
|
+
console.log("✓ .gitignore adjusted: the gate contract (.karajan/review-gate, hooks) is now trackable — commit it");
|
|
225
208
|
}
|
|
209
|
+
return { installed: true, enforced: hook.enforced };
|
|
210
|
+
}
|
|
226
211
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
212
|
+
// KJC-BUG-0173: manual GC of the verdict store. Opportunistic pruning runs
|
|
213
|
+
// on every saved verdict; this lets a human sweep on demand, with --dry-run
|
|
214
|
+
// to see what would go without deleting.
|
|
215
|
+
async function pruneAction(projectDir, flags) {
|
|
216
|
+
const dryRun = Boolean(flags.dryRun || flags.checkOnly);
|
|
217
|
+
const days = Number(flags.pruneDays);
|
|
218
|
+
const res = await pruneVerdicts({ projectDir, dryRun, ...(Number.isFinite(days) && days > 0 ? { maxAgeDays: days } : {}) });
|
|
219
|
+
console.log(
|
|
220
|
+
dryRun
|
|
221
|
+
? `kj review --prune (dry run): ${res.expired.length} of ${res.scanned} verdict(s) older than ${res.maxAgeDays}d would be removed`
|
|
222
|
+
: `kj review --prune: removed ${res.removed} verdict(s) older than ${res.maxAgeDays}d (${res.scanned - res.removed} kept)`,
|
|
223
|
+
);
|
|
224
|
+
return { pruned: res.removed, dryRun };
|
|
225
|
+
}
|
|
239
226
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
// imposible: una PR que retiraba 45 ficheros se paro dos veces y acumulo tres
|
|
246
|
-
// excepciones por la misma causa mecanica. Los requisitos de Sonar y de RAG
|
|
247
|
-
// se evaluan sobre lo que sigue VIVO tras el cambio (A, C, M, R); policy, LOC
|
|
248
|
-
// y tests-with-code siguen viendo el diff entero, porque ahi un borrado si
|
|
249
|
-
// cuenta. El borrado danino lo cubre la review cruzada, que lee todo el diff.
|
|
250
|
-
const liveFiles = (await rawDiff(flags.range, ["--name-only", "--diff-filter=ACMR"])).split("\n").map((f) => f.trim()).filter(Boolean);
|
|
227
|
+
/** A gate's rejection: said, exit 1, and the record the caller returns (KJC-TSK-1257). */
|
|
228
|
+
function rejected(reviewer, issues) {
|
|
229
|
+
process.exitCode = 1;
|
|
230
|
+
return { verdict: "rejected", reviewer, issues };
|
|
231
|
+
}
|
|
251
232
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
233
|
+
/**
|
|
234
|
+
* KJC-TSK-0688 (MG-C) + KJC-TSK-0691 (MG-E): oversized-diff policy.
|
|
235
|
+
* Field case: a 522-line PR sailed past the warning with a
|
|
236
|
+
* rationalization — where the project wants teeth, pr_size: "block"
|
|
237
|
+
* rejects deterministically with a NAMED escape. Default stays warn.
|
|
238
|
+
* PL-C (KJC-TSK-0735), fuente única de umbrales: si la policy declara el
|
|
239
|
+
* invariante net_lines_added, SU max es el umbral — el mismo número que
|
|
240
|
+
* aplican kj policy check y el tier C de CI; method_gates queda de fallback.
|
|
241
|
+
* KJC-BUG-0326 (#2085): two tiers. ~150 is the ideal (method_gates.pr_size_warn);
|
|
242
|
+
* up to the limit (the policy's net_lines_added max, else method_gates.pr_size_max,
|
|
243
|
+
* 200) is allowed without asking; above the limit, partition. KJC-TSK-1076: one
|
|
244
|
+
* source, shared with the third-party PR checks.
|
|
245
|
+
* @returns {Promise<object|null>} the rejection, or null to go on
|
|
246
|
+
*/
|
|
247
|
+
async function prSizeGate({ projectDir, config, range }) {
|
|
264
248
|
const { ideal: sizeIdeal, limit: sizeWarn, source: sizeSource } = prSizeLimit({ projectDir, config });
|
|
265
|
-
if (sizeWarn
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
if (net > sizeWarn) {
|
|
281
|
-
// KJC-TSK-0795 AC5: say how much of the weight is the module's OWN tests —
|
|
282
|
-
// partitioning is a decision, and it must never split code from its tests.
|
|
283
|
-
const split = testAdded > 0 ? ` (${added - testAdded} source + ${testAdded} test lines added — partition by feature, never code from its tests)` : "";
|
|
284
|
-
const sizePolicy = config?.method_gates?.pr_size || "warn";
|
|
285
|
-
// ADR 0015: no env escape — an exception is the user's label on the PR, judged in CI.
|
|
286
|
-
if (sizePolicy === "block") {
|
|
287
|
-
const reason = `${net} net lines${split} is over the ${sizeWarn}-line limit (${sizeSource}; method_gates.pr_size: block) — partition the work; an exception is your user's large-pr-justified label on the PR`;
|
|
288
|
-
console.log(`✗ pr-size gate: ${reason}`);
|
|
289
|
-
process.exitCode = 1;
|
|
290
|
-
return { verdict: "rejected", reviewer: "pr-size", issues: [{ severity: "high", description: reason }] };
|
|
291
|
-
} else {
|
|
292
|
-
console.log(`⚠ pr-size: ${net} net lines${split}, over the ${sizeWarn}-line limit (ideal ~${sizeIdeal}, source: ${sizeSource}) — partition the work before committing (method_gates.pr_size: block to harden)`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
249
|
+
if (sizeWarn <= 0) return null;
|
|
250
|
+
const numstat = await rawDiff(range, ["--numstat"]);
|
|
251
|
+
// KJC-BUG-0205: the budget caps CODE growth, so generated output and human
|
|
252
|
+
// documentation are exempt — the same rule CI applies. Counting everything
|
|
253
|
+
// here warned 234 lines on a doc-only PR that CI counted at 104, and two
|
|
254
|
+
// implementations of one rule that disagree teach people to trust neither.
|
|
255
|
+
// KJC-BUG-0434: measured NET, as CI and the policy's net_lines_added do — a rewrite in place is not growth.
|
|
256
|
+
const { added, testAdded } = budgetedAdded(numstat);
|
|
257
|
+
const { net, exempt } = budgetedNet(numstat);
|
|
258
|
+
// Said out loud: an exemption nobody sees is indistinguishable from a gate
|
|
259
|
+
// that is not looking.
|
|
260
|
+
if (exempt > 0) console.log(`ℓ pr-size: ${exempt} line(s) exempt from the budget (generated output and human docs; AI-rule files always count)`);
|
|
261
|
+
if (net > sizeIdeal && net <= sizeWarn) {
|
|
262
|
+
console.log(`ℓ pr-size: ${net} net lines, over the ~${sizeIdeal} ideal, within the ${sizeWarn} limit: allowed, nothing to ask`);
|
|
295
263
|
}
|
|
296
|
-
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
//
|
|
264
|
+
if (net <= sizeWarn) return null;
|
|
265
|
+
// KJC-TSK-0795 AC5: say how much of the weight is the module's OWN tests —
|
|
266
|
+
// partitioning is a decision, and it must never split code from its tests.
|
|
267
|
+
const split = testAdded > 0 ? ` (${added - testAdded} source + ${testAdded} test lines added — partition by feature, never code from its tests)` : "";
|
|
268
|
+
// ADR 0015: no env escape — an exception is the user's label on the PR, judged in CI.
|
|
269
|
+
if ((config?.method_gates?.pr_size || "warn") === "block") {
|
|
270
|
+
const reason = `${net} net lines${split} is over the ${sizeWarn}-line limit (${sizeSource}; method_gates.pr_size: block) — partition the work; an exception is your user's large-pr-justified label on the PR`;
|
|
271
|
+
console.log(`✗ pr-size gate: ${reason}`);
|
|
272
|
+
return rejected("pr-size", [{ severity: "high", description: reason }]);
|
|
273
|
+
}
|
|
274
|
+
console.log(`⚠ pr-size: ${net} net lines${split}, over the ${sizeWarn}-line limit (ideal ~${sizeIdeal}, source: ${sizeSource}) — partition the work before committing (method_gates.pr_size: block to harden)`);
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* KJC-TSK-0705 (PV-B): the outbound privacy boundary at commit time.
|
|
280
|
+
* Denylist data rejects with no escape (ADR 0015: false positives go in the allow list); generic PII
|
|
281
|
+
* warns (privacy.generic: "block" hardens). Added lines only — deletions
|
|
282
|
+
* don't publish. In the SEA binary the privacy module is stubbed and
|
|
283
|
+
* throws: the gate degrades with a note instead of crashing.
|
|
284
|
+
* @returns {object|null} the rejection, or null to go on
|
|
285
|
+
*/
|
|
286
|
+
function privacyGate({ diff, config }) {
|
|
301
287
|
try {
|
|
302
288
|
// KJC-BUG-0203: per file, so a finding can name where it lives and so
|
|
303
289
|
// build output is judged as what it is. Generic heuristics are silenced
|
|
@@ -312,53 +298,56 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
312
298
|
const warns = findings.filter((f) => f.severity === "warn");
|
|
313
299
|
for (const f of warns) console.log(`⚠ privacy: [${f.type}] ${f.source}:${f.line} → ${f.masked} — personal data? move it out before it ships`);
|
|
314
300
|
const hardened = config?.privacy?.generic === "block" && warns.length > 0;
|
|
315
|
-
if (blocks.length
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
return { verdict: "rejected", reviewer: "privacy", issues: [{ severity: "high", description: reason }] };
|
|
321
|
-
}
|
|
301
|
+
if (blocks.length === 0 && !hardened) return null;
|
|
302
|
+
for (const f of blocks) console.log(`✗ privacy: [${f.type}] ${f.source}:${f.line} → ${f.masked}`);
|
|
303
|
+
const reason = `${blocks.length || warns.length} personal-data finding(s) in the staged diff — this must not reach the repo (a confirmed false positive goes in the allow list of ~/.karajan/privacy.yml)`;
|
|
304
|
+
console.log(`✗ privacy gate: ${reason}`);
|
|
305
|
+
return rejected("privacy", [{ severity: "high", description: reason }]);
|
|
322
306
|
} catch (err) {
|
|
323
307
|
// Known degradation: the SEA stub throws its install-from-npm message.
|
|
324
308
|
// Anything else fails CLOSED — a silent skip would defeat the guarantee.
|
|
325
309
|
if (/standalone binary/i.test(err?.message || "")) {
|
|
326
310
|
console.log("⚠ privacy gate unavailable in this build — skipping");
|
|
327
|
-
|
|
328
|
-
const reason = `privacy gate error: ${err.message} — refusing to pass the boundary unchecked`;
|
|
329
|
-
console.log(`✗ ${reason}`);
|
|
330
|
-
process.exitCode = 1;
|
|
331
|
-
return { verdict: "rejected", reviewer: "privacy", issues: [{ severity: "high", description: reason }] };
|
|
311
|
+
return null;
|
|
332
312
|
}
|
|
313
|
+
const reason = `privacy gate error: ${err.message} — refusing to pass the boundary unchecked`;
|
|
314
|
+
console.log(`✗ ${reason}`);
|
|
315
|
+
return rejected("privacy", [{ severity: "high", description: reason }]);
|
|
333
316
|
}
|
|
317
|
+
}
|
|
334
318
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
319
|
+
/**
|
|
320
|
+
* KJC-TSK-0795 AC1: hand the gate the numbers so a delete-only diff is
|
|
321
|
+
* exempt — deleting code adds no behavior to test.
|
|
322
|
+
* @returns {Promise<object|null>} the rejection, or null to go on
|
|
323
|
+
*/
|
|
324
|
+
async function testsWithCodeGate({ config, projectDir, range, changedFiles }) {
|
|
325
|
+
const numstat = (await rawDiff(range, ["--numstat"])).split("\n").map((l) => l.trim()).filter(Boolean)
|
|
338
326
|
.map((l) => { const [a, r, ...f] = l.split(/\s+/); return { file: f.join(" "), added: a === "-" ? 1 : Number(a) || 0, removed: r === "-" ? 0 : Number(r) || 0 }; });
|
|
339
|
-
await markCommentOnlyAdditions(numstat, { range
|
|
327
|
+
await markCommentOnlyAdditions(numstat, { range, projectDir });
|
|
340
328
|
const tests = checkTestsWithCode({ config, stagedFiles: changedFiles, numstat });
|
|
341
329
|
if (tests.mode === "delete-only") console.log(`⚠ tests-with-code: exempt — ${tests.reason}`);
|
|
342
330
|
if (tests.mode === "warn") console.log(`⚠ tests-with-code: ${tests.reason}`);
|
|
343
331
|
if (tests.mode === "exempt") console.log(`⚠ tests-with-code exempt: ${tests.reason}`);
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
332
|
+
if (tests.ok) return null;
|
|
333
|
+
console.log(`✗ tests-with-code gate: ${tests.reason}`);
|
|
334
|
+
return rejected("tests-with-code", [{ severity: "high", description: tests.reason }]);
|
|
335
|
+
}
|
|
349
336
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
337
|
+
const atFile = (v) => (v.file ? ` (${v.file})` : "");
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* KJC-TSK-0734 (PL-B): la policy declarativa como gate determinista, en
|
|
341
|
+
* --staged Y en --check — el pre-commit hereda los dientes sin regenerar
|
|
342
|
+
* hooks. enforcement=warn avisa; deny cierra sin excepción por diff (ADR
|
|
343
|
+
* 0015); class=security cierra además sin arbitraje.
|
|
344
|
+
* @returns {Promise<{rejection: object|null, gate: object, seal: Function}>} `seal` records the chokepoint's decisions
|
|
345
|
+
*/
|
|
346
|
+
async function policyGate({ projectDir, diff, pol, changedFiles, std, range, check }) {
|
|
354
347
|
// KJC-BUG-0208: el mismo modulo que decide el presupuesto decide el metrico
|
|
355
348
|
// del invariante. Sumar aqui el numstat entero daba dos cifras para la misma
|
|
356
349
|
// regla en dos lineas seguidas del mismo comando.
|
|
357
|
-
const { net } = budgetedNet(await rawDiff(
|
|
358
|
-
// GOV-B (KJC-TSK-0746): las permanentes concedidas (kj policy grant)
|
|
359
|
-
// eximen su regla mientras viven; el descarte de líneas corruptas se dice.
|
|
360
|
-
const std = loadStandingExceptions(projectDir);
|
|
361
|
-
if (std.discarded > 0) console.log(`⚠ policy: ${std.discarded} línea(s) corruptas descartadas en policy-exceptions.jsonl — revísalo`);
|
|
350
|
+
const { net } = budgetedNet(await rawDiff(range, ["--numstat"]));
|
|
362
351
|
const gate = evaluatePolicyGate({
|
|
363
352
|
policy: pol.policy, errors: pol.errors, files: changedFiles, netLinesAdded: net,
|
|
364
353
|
diffHashValue: diffHash(diff),
|
|
@@ -368,195 +357,222 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
368
357
|
// ADR 0009 (KJC-BUG-0161): la exención ESTRUCTURAL del supervisor — un
|
|
369
358
|
// diff de .karajan/hooks respaldado por la provenance sellada Y el render
|
|
370
359
|
// canónico no es una edición: es la regeneración de kj harden --commit.
|
|
371
|
-
{
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
gate.ok = gate.denials.length === 0 && !gate.invalid;
|
|
377
|
-
}
|
|
360
|
+
const lifted = liftSealedSupervisorViolations({ projectDir, violations: gate.denials });
|
|
361
|
+
if (lifted.lifted > 0) {
|
|
362
|
+
console.log(`✓ policy: ${lifted.lifted} fichero(s) de supervisor verificados contra la provenance sellada (ADR 0009) — regeneración, no edición`);
|
|
363
|
+
gate.denials = lifted.violations;
|
|
364
|
+
gate.ok = gate.denials.length === 0 && !gate.invalid;
|
|
378
365
|
}
|
|
379
|
-
const
|
|
380
|
-
for (const w of gate.warns) console.log(`⚠ policy [${w.rule_id}] ${w.reason}${at(w)}`);
|
|
366
|
+
for (const w of gate.warns) console.log(`⚠ policy [${w.rule_id}] ${w.reason}${atFile(w)}`);
|
|
381
367
|
for (const e of gate.exempted) console.log(formatExempted(e));
|
|
382
368
|
// GOV-C (KJC-TSK-0747): las decisiones de chokepoint dejan rastro
|
|
383
369
|
// hash-encadenado — cada deny, cada excepción, y el allow del commit.
|
|
384
|
-
const chokepoint =
|
|
370
|
+
const chokepoint = check ? "commit" : "review";
|
|
385
371
|
const seal = (decision, extra = {}) =>
|
|
386
372
|
recordGateDecision(projectDir, { decision, chokepoint, policy_hash: policyFileHash(projectDir), artifact_hash: diffHash(diff), ...extra });
|
|
387
373
|
if (gate.exempted.length > 0) seal("exempt", { rule_ids: gate.exempted.map((e) => e.rule_id) });
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
seal("deny", { rule_ids: gate.denials.map((d) => d.rule_id) });
|
|
394
|
-
const reason = gate.invalid
|
|
395
|
-
? "policy.yml inválida — una regla que miente es peor que ninguna: corrígela"
|
|
396
|
-
: `${gate.denials.length} violación(es) de policy con enforcement=deny — el diff no entra`;
|
|
397
|
-
console.log(`✗ policy gate: ${reason}`);
|
|
398
|
-
process.exitCode = 1;
|
|
399
|
-
return { verdict: "rejected", reviewer: "policy", issues: gate.denials.map((d) => ({ severity: "high", file: d.file, description: `[${d.rule_id}] ${d.reason}` })) };
|
|
374
|
+
if (gate.ok) return { rejection: null, gate, seal };
|
|
375
|
+
for (const d of gate.denials) {
|
|
376
|
+
const sec = d.class === "security" ? " [security — sin escape ni arbitraje]" : "";
|
|
377
|
+
console.log(`✗ policy [${d.rule_id}]${sec} ${d.reason}${atFile(d)}`);
|
|
400
378
|
}
|
|
379
|
+
seal("deny", { rule_ids: gate.denials.map((d) => d.rule_id) });
|
|
380
|
+
const reason = gate.invalid
|
|
381
|
+
? "policy.yml inválida — una regla que miente es peor que ninguna: corrígela"
|
|
382
|
+
: `${gate.denials.length} violación(es) de policy con enforcement=deny — el diff no entra`;
|
|
383
|
+
console.log(`✗ policy gate: ${reason}`);
|
|
384
|
+
const rejection = rejected("policy", gate.denials.map((d) => ({ severity: "high", file: d.file, description: `[${d.rule_id}] ${d.reason}` })));
|
|
385
|
+
return { rejection, gate, seal };
|
|
386
|
+
}
|
|
401
387
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if (!rreq.ok) res = { ok: false, verdict: res.verdict, reason: rreq.reason };
|
|
451
|
-
else if (rreq.mode === "granted" && rreq.grant) console.log(formatRagGrant(rreq.grant));
|
|
452
|
-
}
|
|
453
|
-
console.log(res.ok
|
|
454
|
-
? `✓ verdict ok — approved by ${res.verdict.reviewer} (diff ${res.verdict.diffHash.slice(0, 12)})`
|
|
455
|
-
: `✗ ${res.reason}`);
|
|
456
|
-
// GOV-C: el allow del chokepoint de COMMIT es evidencia — se sella. PL-E
|
|
457
|
-
// (KJC-TSK-0767): con las reglas que AVISARON, para que "nace avisando y
|
|
458
|
-
// gana dientes" se decida con datos (kj policy report), no a ciegas.
|
|
459
|
-
if (res.ok) seal("allow", warnIds.length > 0 ? { warn_rule_ids: warnIds } : {});
|
|
460
|
-
process.exitCode = res.ok ? 0 : 1;
|
|
388
|
+
/**
|
|
389
|
+
* KJC-BUG-0132 (issue #1344): a PURE merge commit stages no content of
|
|
390
|
+
* its own — everything it brings reached the parent branches already
|
|
391
|
+
* reviewed, and there is no diff to bind a verdict to, so demanding one
|
|
392
|
+
* deadlocks the merge. A merge WITH conflict resolutions stages real
|
|
393
|
+
* content and still requires its verdict.
|
|
394
|
+
*/
|
|
395
|
+
async function isPureMerge({ projectDir, diff, range }) {
|
|
396
|
+
if (range || diff.trim()) return false;
|
|
397
|
+
const gitDirRaw = (await runCommand("git", ["rev-parse", "--git-dir"])).stdout?.trim();
|
|
398
|
+
const gitDir = gitDirRaw && !isAbsolute(gitDirRaw) ? join(projectDir, gitDirRaw) : gitDirRaw;
|
|
399
|
+
return Boolean(gitDir && existsSync(join(gitDir, "MERGE_HEAD")));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* KJC-TSK-0838: a reviewed diff with code enters only if the verdict's
|
|
404
|
+
* sonar block proves the analysis ran and covered every staged source.
|
|
405
|
+
* The headless pipeline stamps its verdict after its own sonar stage and
|
|
406
|
+
* carries no block yet (step 3 of the card) — said, never assumed silent.
|
|
407
|
+
*/
|
|
408
|
+
function withSonarProof(res, { config, liveFiles, std }) {
|
|
409
|
+
if (res.verdict.host === "kj-pipeline") {
|
|
410
|
+
// The pipeline scans the whole project (no per-file proof yet), so its
|
|
411
|
+
// stamp must at least show the stage RAN; no block or a skipped stage
|
|
412
|
+
// is refused like any other unanalysed code.
|
|
413
|
+
const stage = res.verdict.sonar;
|
|
414
|
+
if (stage?.ran) return res;
|
|
415
|
+
const why = stage ? `the pipeline's sonar stage did not run: ${stage.reason || "unknown reason"}` : "the pipeline verdict carries no sonar block";
|
|
416
|
+
return { ok: false, verdict: res.verdict, reason: `Sonar is mandatory for code — ${why}` };
|
|
417
|
+
}
|
|
418
|
+
const req = checkSonarRequirement({ config, stagedFiles: liveFiles, sonar: res.verdict.sonar, standingExceptions: std.standing });
|
|
419
|
+
if (!req.ok) return { ok: false, verdict: res.verdict, reason: req.reason };
|
|
420
|
+
if (req.mode === "granted") console.log(formatSonarGrant(req.grant));
|
|
421
|
+
return res;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* KJC-TSK-0849 (ADR 0010, RAG-C): the verdict's rag block is the evidence
|
|
426
|
+
* that the session consulted the RAG about every staged source. The one
|
|
427
|
+
* place the rule cannot be verified is a pipeline verdict: the headless
|
|
428
|
+
* pipeline keeps no session ledger yet. It WARNS and the allow seals the
|
|
429
|
+
* warn under the rule id, so the exception is auditable (kj policy report)
|
|
430
|
+
* and gains teeth with data — never accepted in silence.
|
|
431
|
+
*/
|
|
432
|
+
function withRagProof(res, { config, liveFiles, std, projectDir, warnIds }) {
|
|
433
|
+
if (res.verdict.host === "kj-pipeline") {
|
|
434
|
+
console.log(`⚠ rag [${RAG_RULE_ID}]: the pipeline verdict carries no session ledger — the headless pipeline does not consult the RAG yet; sealed as a warn`);
|
|
435
|
+
if (!warnIds.includes(RAG_RULE_ID)) warnIds.push(RAG_RULE_ID);
|
|
461
436
|
return res;
|
|
462
437
|
}
|
|
438
|
+
const { harness, verified, mismatched, drift } = readRagLedger(projectDir);
|
|
439
|
+
const rreq = checkRagVerdict({ config, stagedFiles: liveFiles, rag: res.verdict.rag, harness, verified, mismatched, drift, standingExceptions: std.standing });
|
|
440
|
+
if (!rreq.ok) return { ok: false, verdict: res.verdict, reason: rreq.reason };
|
|
441
|
+
if (rreq.mode === "granted" && rreq.grant) console.log(formatRagGrant(rreq.grant));
|
|
442
|
+
return res;
|
|
443
|
+
}
|
|
463
444
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
// diff hash — a reviewed diff and a reviewed AND analysed diff must be
|
|
471
|
-
// distinguishable by --check and by the method report.
|
|
472
|
-
let sonarRecord = { ran: false, reason: "--no-sonar: the pre-gate was skipped by flag" };
|
|
473
|
-
if (flags.sonar !== false) {
|
|
474
|
-
// KJC-TSK-0795 AC3: only issues on lines this diff ADDS may veto.
|
|
475
|
-
const touchedLines = addedLinesByFile(await rawDiff(flags.range, ["--unified=0"]));
|
|
476
|
-
const pre = await runSonarPregate({ config, stagedFiles: liveFiles, touchedLines, logger });
|
|
477
|
-
if (!pre.available) {
|
|
478
|
-
// KJC-BUG-0156 (issue #1543): a scan that FAILED must not read like one
|
|
479
|
-
// more config warning — it hid a dead quality gate for 8 straight PRs.
|
|
480
|
-
// Still fail-open (a laptop without Docker still commits), but SAID.
|
|
481
|
-
const chosen = /disabled in config/.test(pre.reason || "");
|
|
482
|
-
console.log(chosen
|
|
483
|
-
? `⚠ sonar pre-gate skipped: ${pre.reason}`
|
|
484
|
-
: `✗ sonar pre-gate UNAVAILABLE — the quality gate did NOT run on this diff: ${pre.reason}`);
|
|
485
|
-
sonarRecord = { ran: false, reason: pre.reason };
|
|
486
|
-
} else {
|
|
487
|
-
sonarRecord = {
|
|
488
|
-
ran: true, projectKey: pre.projectKey, covered: pre.covered ?? [], uncovered: pre.uncovered ?? [],
|
|
489
|
-
blocking: pre.blocking.length, advisory: pre.advisory.length,
|
|
490
|
-
};
|
|
491
|
-
if (sonarRecord.uncovered.length > 0) {
|
|
492
|
-
console.log(`⚠ sonar coverage: ${sonarRecord.uncovered.length} staged source(s) were NOT in the analysis (${sonarRecord.uncovered.slice(0, 5).join(", ")}${sonarRecord.uncovered.length > 5 ? "…" : ""}) — the scan did not see them`);
|
|
493
|
-
}
|
|
494
|
-
const found = [...pre.blocking, ...pre.advisory];
|
|
495
|
-
if (found.length > 0) {
|
|
496
|
-
console.log(`Sonar on the changed lines — ${pre.blocking.length} blocking, ${pre.advisory.length} advisory (project total: ${pre.totalProject}):`);
|
|
497
|
-
for (const f of found) console.log(` - ${formatSonarFinding(f)}`);
|
|
498
|
-
}
|
|
499
|
-
if ((pre.preexisting || []).length > 0) {
|
|
500
|
-
console.log(`⚠ sonar trend: ${pre.preexisting.length} preexisting issue(s) on untouched lines of these files — not this PR's veto, but the debt is real`);
|
|
501
|
-
}
|
|
502
|
-
if (pre.blocking.length > 0) {
|
|
503
|
-
console.log("✗ REJECTED by sonar (deterministic) — fix the blocking findings; the cross-AI reviewer was not invoked.");
|
|
504
|
-
process.exitCode = 1;
|
|
505
|
-
return {
|
|
506
|
-
verdict: "rejected", reviewer: "sonar",
|
|
507
|
-
issues: pre.blocking.map((i) => ({ severity: i.severity, file: undefined, description: formatSonarFinding(i) })),
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
if (pre.advisory.length > 0) {
|
|
511
|
-
task = `${task || "Review the following diff for correctness, security and maintainability."}\n\n`
|
|
512
|
-
+ `Deterministic Sonar findings on these files (fold them into your review):\n`
|
|
513
|
-
+ pre.advisory.map((f) => `- ${formatSonarFinding(f)}`).join("\n");
|
|
514
|
-
}
|
|
515
|
-
}
|
|
445
|
+
/** --check (the pre-commit): the staged diff enters only with an approved verdict that carries its proofs. */
|
|
446
|
+
async function checkPath({ projectDir, config, diff, range, liveFiles, std, gate, seal }) {
|
|
447
|
+
if (await isPureMerge({ projectDir, diff, range })) {
|
|
448
|
+
console.log("✓ pure merge commit — empty staged diff, parents already reviewed; gate passes with trail");
|
|
449
|
+
process.exitCode = 0;
|
|
450
|
+
return { ok: true, merge: true, reason: "pure merge commit (MERGE_HEAD, empty staged diff)" };
|
|
516
451
|
}
|
|
452
|
+
let res = await checkVerdict(projectDir, diff);
|
|
453
|
+
if (res.note) console.log(`ℹ ${res.note}`);
|
|
454
|
+
if (res.ok) res = withSonarProof(res, { config, liveFiles, std });
|
|
455
|
+
const warnIds = gate.warns.map((w) => w.rule_id);
|
|
456
|
+
if (res.ok) res = withRagProof(res, { config, liveFiles, std, projectDir, warnIds });
|
|
457
|
+
console.log(res.ok
|
|
458
|
+
? `✓ verdict ok — approved by ${res.verdict.reviewer} (diff ${res.verdict.diffHash.slice(0, 12)})`
|
|
459
|
+
: `✗ ${res.reason}`);
|
|
460
|
+
// GOV-C: el allow del chokepoint de COMMIT es evidencia — se sella. PL-E
|
|
461
|
+
// (KJC-TSK-0767): con las reglas que AVISARON, para que "nace avisando y
|
|
462
|
+
// gana dientes" se decida con datos (kj policy report), no a ciegas.
|
|
463
|
+
if (res.ok) seal("allow", warnIds.length > 0 ? { warn_rule_ids: warnIds } : {});
|
|
464
|
+
process.exitCode = res.ok ? 0 : 1;
|
|
465
|
+
return res;
|
|
466
|
+
}
|
|
517
467
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
468
|
+
const DEFAULT_REVIEW_TASK = "Review the following diff for correctness, security and maintainability.";
|
|
469
|
+
/** Appends a gate's finding for the reviewer to the task (the default task when there is none). */
|
|
470
|
+
const withNote = (task, note) => (note ? `${task || DEFAULT_REVIEW_TASK}\n\n${note}` : task);
|
|
471
|
+
const listed = (items) => `${items.slice(0, 5).join(", ")}${items.length > 5 ? "…" : ""}`;
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* KJC-TSK-0676: deterministic pre-gate — sonar findings on the changed
|
|
475
|
+
* files come back BEFORE any AI opinion. Blocking severities reject
|
|
476
|
+
* without spending reviewer tokens; the rest travel with the task so
|
|
477
|
+
* the cross-AI reviewer weighs them. Unavailable sonar degrades loudly.
|
|
478
|
+
* KJC-TSK-0838: what sonar saw travels INSIDE the verdict (`record`), bound to the
|
|
479
|
+
* diff hash — a reviewed diff and a reviewed AND analysed diff must be
|
|
480
|
+
* distinguishable by --check and by the method report.
|
|
481
|
+
*/
|
|
482
|
+
async function sonarPregate({ config, logger, range, liveFiles, enabled }) {
|
|
483
|
+
if (!enabled) return { record: { ran: false, reason: "--no-sonar: the pre-gate was skipped by flag" } };
|
|
484
|
+
// KJC-TSK-0795 AC3: only issues on lines this diff ADDS may veto.
|
|
485
|
+
const touchedLines = addedLinesByFile(await rawDiff(range, ["--unified=0"]));
|
|
486
|
+
const pre = await runSonarPregate({ config, stagedFiles: liveFiles, touchedLines, logger });
|
|
487
|
+
if (!pre.available) {
|
|
488
|
+
// KJC-BUG-0156 (issue #1543): a scan that FAILED must not read like one
|
|
489
|
+
// more config warning — it hid a dead quality gate for 8 straight PRs.
|
|
490
|
+
// Still fail-open (a laptop without Docker still commits), but SAID.
|
|
491
|
+
const chosen = /disabled in config/.test(pre.reason || "");
|
|
492
|
+
console.log(chosen
|
|
493
|
+
? `⚠ sonar pre-gate skipped: ${pre.reason}`
|
|
494
|
+
: `✗ sonar pre-gate UNAVAILABLE — the quality gate did NOT run on this diff: ${pre.reason}`);
|
|
495
|
+
return { record: { ran: false, reason: pre.reason } };
|
|
526
496
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
497
|
+
const record = {
|
|
498
|
+
ran: true, projectKey: pre.projectKey, covered: pre.covered ?? [], uncovered: pre.uncovered ?? [],
|
|
499
|
+
blocking: pre.blocking.length, advisory: pre.advisory.length,
|
|
500
|
+
};
|
|
501
|
+
printSonarPregate(pre, record);
|
|
502
|
+
if (pre.blocking.length > 0) {
|
|
503
|
+
console.log("✗ REJECTED by sonar (deterministic) — fix the blocking findings; the cross-AI reviewer was not invoked.");
|
|
504
|
+
return { record, rejection: rejected("sonar", pre.blocking.map((i) => ({ severity: i.severity, file: undefined, description: formatSonarFinding(i) }))) };
|
|
505
|
+
}
|
|
506
|
+
const findings = pre.advisory.map((f) => `- ${formatSonarFinding(f)}`).join("\n");
|
|
507
|
+
const note = pre.advisory.length > 0 ? `Deterministic Sonar findings on these files (fold them into your review):\n${findings}` : null;
|
|
508
|
+
return { record, note };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function printSonarPregate(pre, record) {
|
|
512
|
+
if (record.uncovered.length > 0) {
|
|
513
|
+
console.log(`⚠ sonar coverage: ${record.uncovered.length} staged source(s) were NOT in the analysis (${listed(record.uncovered)}) — the scan did not see them`);
|
|
514
|
+
}
|
|
515
|
+
const found = [...pre.blocking, ...pre.advisory];
|
|
516
|
+
if (found.length > 0) {
|
|
517
|
+
console.log(`Sonar on the changed lines — ${pre.blocking.length} blocking, ${pre.advisory.length} advisory (project total: ${pre.totalProject}):`);
|
|
518
|
+
for (const f of found) console.log(` - ${formatSonarFinding(f)}`);
|
|
543
519
|
}
|
|
544
|
-
if (
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
520
|
+
if ((pre.preexisting || []).length > 0) {
|
|
521
|
+
console.log(`⚠ sonar trend: ${pre.preexisting.length} preexisting issue(s) on untouched lines of these files — not this PR's veto, but the debt is real`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* KJC-TSK-0838: fail-CLOSED for code. Sonar disabled, down, skipped by
|
|
527
|
+
* flag, or blind to a staged source — the diff does not reach the reviewer.
|
|
528
|
+
* Docs-only diffs pass; a live human grant on the rule is the only escape.
|
|
529
|
+
* The verdict says on what grounds the requirement was met (proof, docs-only,
|
|
530
|
+
* human grant): `record.mode`, read back by the method report.
|
|
531
|
+
*/
|
|
532
|
+
function sonarRequirementGate({ config, liveFiles, std, record }) {
|
|
533
|
+
const req = checkSonarRequirement({ config, stagedFiles: liveFiles, sonar: record, standingExceptions: std.standing });
|
|
534
|
+
if (!req.ok) {
|
|
535
|
+
console.log(`✗ ${req.reason}`);
|
|
536
|
+
return rejected("sonar", [{ severity: "high", file: undefined, description: req.reason }]);
|
|
553
537
|
}
|
|
538
|
+
if (req.mode === "granted") console.log(formatSonarGrant(req.grant));
|
|
539
|
+
record.mode = req.mode;
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
554
542
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
543
|
+
/**
|
|
544
|
+
* KJC-TSK-0849 (ADR 0010, RAG-C): the session's RAG ledger must show the
|
|
545
|
+
* RAG answered about every staged source (file or sibling), and the TWINS
|
|
546
|
+
* it returned that the diff does not touch travel to the reviewer by name —
|
|
547
|
+
* the 16-sep bugs were exactly the twin nobody touched.
|
|
548
|
+
*/
|
|
549
|
+
async function ragGate({ config, projectDir, range, liveFiles, std }) {
|
|
550
|
+
const ledger = readRagLedger(projectDir);
|
|
551
|
+
const newFiles = (await rawDiff(range, ["--name-only", "--diff-filter=A"])).split("\n").map((f) => f.trim()).filter(Boolean);
|
|
552
|
+
const req = checkRagRequirement({ config, stagedFiles: liveFiles, newFiles, ledger, standingExceptions: std.standing });
|
|
553
|
+
if (!req.ok) {
|
|
554
|
+
console.log(`✗ ${req.reason}`);
|
|
555
|
+
return { rejection: rejected("rag-first", [{ severity: "high", file: undefined, description: req.reason }]) };
|
|
556
|
+
}
|
|
557
|
+
if (req.mode === "granted") console.log(formatRagGrant(req.grant));
|
|
558
|
+
if (req.mode === "no-harness") console.log("⚠ rag: no Sentinel harness in this tree — no session ledger to check (kj harden installs it)");
|
|
559
|
+
const record = ragBlock(req, ledger);
|
|
560
|
+
const twins = record.twinsUntouched;
|
|
561
|
+
if (twins.length === 0) return { record };
|
|
562
|
+
console.log(`ℹ rag twins: this session's queries also returned ${twins.length} file(s) the diff does not touch — ${listed(twins)}`);
|
|
563
|
+
const note = "The session's RAG queries also returned these files, which the diff does NOT touch — the same concept may live there, so check the change is complete, not only correct:\n"
|
|
564
|
+
+ twins.map((t) => `- ${t}`).join("\n");
|
|
565
|
+
return { record, note };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* BOOT-D (KJC-TSK-0863): green is not proof for what a person SEES. kj does
|
|
570
|
+
* not drive the browser (the host has it); it demands the walkthrough and
|
|
571
|
+
* records it in the verdict, bound to the diff like sonar and rag. It WARNS:
|
|
572
|
+
* the last proof, never the only one, and a gate that fires often teaches
|
|
573
|
+
* people to skip gates.
|
|
574
|
+
*/
|
|
575
|
+
function uiEvidence({ changedFiles, flags, std }) {
|
|
560
576
|
const walked = Array.isArray(flags.walked) ? flags.walked : [];
|
|
561
577
|
const uiReq = checkUiEvidence({
|
|
562
578
|
stagedFiles: changedFiles,
|
|
@@ -564,51 +580,118 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
564
580
|
standingExceptions: std.standing,
|
|
565
581
|
});
|
|
566
582
|
if (uiReq.warn) console.log(`⚠ ${uiReq.reason}`);
|
|
567
|
-
|
|
583
|
+
return uiBlock(uiReq);
|
|
584
|
+
}
|
|
568
585
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
586
|
+
/**
|
|
587
|
+
* MUT-A (KJC-TSK-0716): mutation pre-gate — opt-in (method_gates.mutation),
|
|
588
|
+
* SOLO en --staged (jamás en pre-commit: cuesta minutos; y jamás en --range:
|
|
589
|
+
* el scope es el ÍNDICE y anotaría trabajo ajeno — catch de codex). block
|
|
590
|
+
* cierra ANTES de gastar reviewer; warn viaja como advisory en su task.
|
|
591
|
+
*/
|
|
592
|
+
async function mutationGate({ config, projectDir, flags }) {
|
|
573
593
|
const mut = flags.staged && !flags.range ? await runMutationPregate({ config, projectDir }) : { enabled: false, ok: true };
|
|
574
|
-
if (mut.enabled) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
console.log(`Mutantes supervivientes en el diff — score ${mut.score ?? "n/a"}%:`);
|
|
579
|
-
for (const m of mut.survived) console.log(` ✗ ${formatSurvivor(m)}`);
|
|
580
|
-
if (!mut.ok) {
|
|
581
|
-
console.log("✗ mutation gate: la suite verde no PRUEBA el cambio — mata los supervivientes antes del review (method_gates.mutation: block).");
|
|
582
|
-
process.exitCode = 1;
|
|
583
|
-
return { verdict: "rejected", reviewer: "mutation", issues: mut.survived.map((m) => ({ severity: "high", file: m.file, description: formatSurvivor(m) })) };
|
|
584
|
-
}
|
|
585
|
-
task = `${task || "Review the following diff for correctness, security and maintainability."}\n\n`
|
|
586
|
-
+ `Surviving mutants on the changed lines (weak asserts — fold into your review):\n`
|
|
587
|
-
+ mut.survived.map((m) => `- ${formatSurvivor(m)}`).join("\n");
|
|
588
|
-
}
|
|
594
|
+
if (!mut.enabled) return {};
|
|
595
|
+
if (mut.available === false) {
|
|
596
|
+
console.log(`⚠ mutation pre-gate no disponible: ${mut.reason} — degradado avisando (method_gates.mutation)`);
|
|
597
|
+
return {};
|
|
589
598
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
599
|
+
if (mut.survived.length === 0) return {};
|
|
600
|
+
console.log(`Mutantes supervivientes en el diff — score ${mut.score ?? "n/a"}%:`);
|
|
601
|
+
for (const m of mut.survived) console.log(` ✗ ${formatSurvivor(m)}`);
|
|
602
|
+
if (!mut.ok) {
|
|
603
|
+
console.log("✗ mutation gate: la suite verde no PRUEBA el cambio — mata los supervivientes antes del review (method_gates.mutation: block).");
|
|
604
|
+
return { rejection: rejected("mutation", mut.survived.map((m) => ({ severity: "high", file: m.file, description: formatSurvivor(m) }))) };
|
|
594
605
|
}
|
|
606
|
+
const survivors = mut.survived.map((m) => `- ${formatSurvivor(m)}`).join("\n");
|
|
607
|
+
return { note: `Surviving mutants on the changed lines (weak asserts — fold into your review):\n${survivors}` };
|
|
608
|
+
}
|
|
595
609
|
|
|
596
|
-
|
|
610
|
+
/**
|
|
611
|
+
* KJC-TSK-0722: a dependency published under the cooldown is said, and weighed by the reviewer.
|
|
612
|
+
* KJC-TSK-1056: method_gates.dep_cooldown: block stops it here, before any reviewer token.
|
|
613
|
+
*/
|
|
614
|
+
async function cooldownStep({ diff, config, std }) {
|
|
597
615
|
const cooldownHours = config?.supply_chain?.cooldown_hours ?? DEFAULT_COOLDOWN_HOURS;
|
|
598
616
|
const cooldownResult = await cooldownAdvisories(diff, { thresholdHours: cooldownHours });
|
|
599
617
|
const cooldown = formatCooldown(cooldownResult, cooldownHours);
|
|
600
618
|
for (const line of cooldown.lines) console.log(line);
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
|
|
619
|
+
const stop = cooldownGate({ result: cooldownResult, mode: config?.method_gates?.dep_cooldown, standingExceptions: std.standing });
|
|
620
|
+
if (stop.grant) console.log(`⚠ dep_cooldown lifted by a HUMAN grant [method.dep_cooldown] until ${stop.grant.expiresAt}`);
|
|
621
|
+
if (stop.block) {
|
|
622
|
+
console.log(`✗ ${stop.reason}`);
|
|
623
|
+
return { rejection: rejected("dep-cooldown", cooldownResult.young.map((p) => ({ severity: "high", description: `${p.name}@${p.version} published ${p.hours} h ago` }))) };
|
|
624
|
+
}
|
|
625
|
+
return { note: cooldown.note || null };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
629
|
+
const projectDir = config?.projectDir || process.cwd();
|
|
630
|
+
if (flags.installGate) return installGateAction(projectDir);
|
|
631
|
+
if (flags.prune) return pruneAction(projectDir, flags);
|
|
632
|
+
|
|
633
|
+
const diff = await rawDiff(flags.range);
|
|
634
|
+
|
|
635
|
+
const card = await enforceCardFirst({ config, projectDir });
|
|
636
|
+
if (!card.ok) return { verdict: "rejected", reviewer: "card-first", issues: [{ severity: "high", description: card.reason }] };
|
|
637
|
+
|
|
638
|
+
// KJC-TSK-1086: the Steward's block at commit too, not only where Edit starts work.
|
|
639
|
+
const steward = stewardBlocking({ projectDir, config });
|
|
640
|
+
if (steward) {
|
|
641
|
+
console.log(`✗ ${steward.reason}`);
|
|
606
642
|
process.exitCode = 1;
|
|
607
|
-
return { verdict: "rejected", reviewer: "
|
|
643
|
+
return { verdict: "rejected", reviewer: "steward", issues: [{ severity: "high", description: steward.reason }] };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// KJC-TSK-0687 (MG-B): the verifiable half of TDD — sources without a
|
|
647
|
+
// single test change warn (block via method_gates.tests_with_code).
|
|
648
|
+
const changedFiles = (await rawDiff(flags.range, ["--name-only"])).split("\n").map((f) => f.trim()).filter(Boolean);
|
|
649
|
+
// KJC-BUG-0229: un fichero BORRADO no es una fuente que el scan pueda indexar
|
|
650
|
+
// ni sobre la que el RAG pueda responder, asi que exigirlo era pedir lo
|
|
651
|
+
// imposible: una PR que retiraba 45 ficheros se paro dos veces y acumulo tres
|
|
652
|
+
// excepciones por la misma causa mecanica. Los requisitos de Sonar y de RAG
|
|
653
|
+
// se evaluan sobre lo que sigue VIVO tras el cambio (A, C, M, R); policy, LOC
|
|
654
|
+
// y tests-with-code siguen viendo el diff entero, porque ahi un borrado si
|
|
655
|
+
// cuenta. El borrado danino lo cubre la review cruzada, que lee todo el diff.
|
|
656
|
+
const liveFiles = (await rawDiff(flags.range, ["--name-only", "--diff-filter=ACMR"])).split("\n").map((f) => f.trim()).filter(Boolean);
|
|
657
|
+
|
|
658
|
+
const pol = loadPolicy({ projectDir });
|
|
659
|
+
// KJC-TSK-1257: the pre-gates in their order; the first rejection ends the review.
|
|
660
|
+
const preRejection = await prSizeGate({ projectDir, config, range: flags.range })
|
|
661
|
+
?? privacyGate({ diff, config })
|
|
662
|
+
?? await testsWithCodeGate({ config, projectDir, range: flags.range, changedFiles });
|
|
663
|
+
if (preRejection) return preRejection;
|
|
664
|
+
|
|
665
|
+
// GOV-B (KJC-TSK-0746): las permanentes concedidas (kj policy grant)
|
|
666
|
+
// eximen su regla mientras viven; el descarte de líneas corruptas se dice.
|
|
667
|
+
const std = loadStandingExceptions(projectDir);
|
|
668
|
+
if (std.discarded > 0) console.log(`⚠ policy: ${std.discarded} línea(s) corruptas descartadas en policy-exceptions.jsonl — revísalo`);
|
|
669
|
+
const { rejection: policyRejection, gate, seal } = await policyGate({ projectDir, diff, pol, changedFiles, std, range: flags.range, check: flags.check });
|
|
670
|
+
if (policyRejection) return policyRejection;
|
|
671
|
+
if (flags.check) return checkPath({ projectDir, config, diff, range: flags.range, liveFiles, std, gate, seal });
|
|
672
|
+
|
|
673
|
+
// KJC-TSK-1257: the review path's gates in their order; each may reject, and what they found
|
|
674
|
+
// that does not reject travels to the reviewer inside the task. KJC-BUG-0423 (#2665): --check
|
|
675
|
+
// does not run these, so their rejection is recorded for the diff, replacing an earlier APPROVED.
|
|
676
|
+
const recorded = (rejection) => saveVerdict(projectDir, diff, rejection).then(() => rejection);
|
|
677
|
+
const sonar = await sonarPregate({ config, logger, range: flags.range, liveFiles, enabled: flags.sonar !== false });
|
|
678
|
+
if (sonar.rejection) return recorded(sonar.rejection);
|
|
679
|
+
const sonarRejection = sonarRequirementGate({ config, liveFiles, std, record: sonar.record });
|
|
680
|
+
if (sonarRejection) return recorded(sonarRejection);
|
|
681
|
+
const rag = await ragGate({ config, projectDir, range: flags.range, liveFiles, std });
|
|
682
|
+
if (rag.rejection) return recorded(rag.rejection);
|
|
683
|
+
const uiRecord = uiEvidence({ changedFiles, flags, std });
|
|
684
|
+
const mutation = await mutationGate({ config, projectDir, flags });
|
|
685
|
+
if (mutation.rejection) return recorded(mutation.rejection);
|
|
686
|
+
// KJC-TSK-1052 (TEAM-F): another branch in progress touching the same files is said, never blocks.
|
|
687
|
+
if (flags.staged && !flags.range) {
|
|
688
|
+
for (const line of formatOverlaps(await branchOverlaps({ projectDir, stagedFiles: changedFiles, base: config?.base_branch || "main" }))) console.log(line);
|
|
608
689
|
}
|
|
609
|
-
|
|
690
|
+
const cooldown = await cooldownStep({ diff, config, std });
|
|
691
|
+
if (cooldown.rejection) return recorded(cooldown.rejection);
|
|
692
|
+
const task = [sonar.note, rag.note, mutation.note, cooldown.note].reduce(withNote, flags.task);
|
|
610
693
|
|
|
611
|
-
const record = await runOneShotReview({ diff, task, config, logger, projectDir, sonar:
|
|
694
|
+
const record = await runOneShotReview({ diff, task, config, logger, projectDir, sonar: sonar.record, rag: rag.record, ui: uiRecord });
|
|
612
695
|
printVerdict(record, record.verdict === "approved" ? await reviewCorrectionRounds(projectDir, record) : 0);
|
|
613
696
|
process.exitCode = record.verdict === "approved" ? 0 : 1;
|
|
614
697
|
return record;
|