@bongos/core 1.19.1036 → 1.19.1037
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/.bongos-core.json +43 -48
- package/.claude/skills/grade-recover/SKILL.md +1 -1
- package/docs/adr/0302-the-third-identical-failure-stops-the-session-and-asks.md +3 -2
- package/docs/adr/0329-the-rendered-fit-check-is-a-kit-floor-and-an-advisory-ship-check-row.md +2 -2
- package/docs/adr/README.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/modules/economy/credits.js +1 -1
- package/modules/grading/grader-config.js +3 -12
- package/modules/grading/grader-no-artifact.js +227 -2
- package/modules/grading/grader-prompt.js +1 -1
- package/modules/grading/grader-rubric.json +1 -86
- package/modules/grading/grader-score.js +1 -169
- package/modules/grading/grader-subagent.js +1 -51
- package/modules/grading/grader.js +26 -211
- package/modules/lifecycle/db-ship.js +3 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +10 -0
- package/scripts/gds/claim.js +2 -1
- package/scripts/gds/grade-smoke.js +3 -3
- package/scripts/gds/render-check.js +1 -1
- package/scripts/gds/ship-check.js +1 -1
- package/scripts/gds/ship-flow.js +1 -1
- package/scripts/gds/ship-grade-steps.js +2 -2
- package/scripts/gds/visual-review-nudge.js +2 -1
- package/src/module-api.js +1 -1
- package/tests/adr_0162_gate_citation.mjs +167 -0
- package/tests/grader.mjs +48 -126
- package/tests/grader_unbacked_score.mjs +1 -1
- package/tests/regrade_actual_minutes.mjs +18 -17
- package/tests/render_check.mjs +1 -1
- package/tests/skill_grade_audit.mjs +3 -3
- package/tests/skill_grader_health.mjs +2 -2
- package/scripts/gds/audit-ships.js +0 -319
- package/tests/audit_ships_git_argv.mjs +0 -173
|
@@ -75,23 +75,28 @@
|
|
|
75
75
|
// module (which receives it via dependency injection to avoid a circular
|
|
76
76
|
// require).
|
|
77
77
|
//
|
|
78
|
-
// ##
|
|
78
|
+
// ## Retired (task 1002842)
|
|
79
79
|
//
|
|
80
|
-
//
|
|
81
|
-
// scripts/gds/audit-ships.js
|
|
80
|
+
// The v1 deterministic process-audit grader (gradeDeterministic and its nine
|
|
81
|
+
// per-criterion scorers) and its one caller, scripts/gds/audit-ships.js, are
|
|
82
|
+
// deleted: the live panel grades every ship at ship time, so a retroactive
|
|
83
|
+
// heuristic audit has nothing left to cover. aggregateShots / runOneShot moved
|
|
84
|
+
// to grader-no-artifact.js, the species that depends on them; they are still
|
|
85
|
+
// re-exported here, and the trivial-diff fast path below still runs one shot.
|
|
82
86
|
|
|
83
87
|
const path = require('node:path');
|
|
84
88
|
const fs = require('node:fs');
|
|
85
89
|
const { spawn } = require('node:child_process');
|
|
86
90
|
|
|
87
|
-
const { ASSUMED_BUILDER_MODEL, DEFAULT_GRADER_MODEL,
|
|
91
|
+
const { ASSUMED_BUILDER_MODEL, DEFAULT_GRADER_MODEL, FALLTHROUGH_CONFIDENCE, RUBRIC, THRESHOLD, VERSION, generatedProvenance, permissionPathPrePass, routeRankPrePass, workers } = require('./grader-config.js');
|
|
88
92
|
const { clamp, isTrivialDiff, median, r1, selectGraderModel, workerModelFor } = require('./grader-model.js');
|
|
89
93
|
const { buildPrompt, extractJson } = require('./grader-prompt.js');
|
|
90
94
|
// task 1004011: the second grade species — a grade over the CLAIM on an
|
|
91
95
|
// --allow-empty ship, where there is no diff for the panel above to read.
|
|
92
|
-
|
|
93
|
-
const {
|
|
94
|
-
const {
|
|
96
|
+
// task 1002842: the generalist-shot plumbing now lives there too.
|
|
97
|
+
const { GRADER_KIND: NO_ARTIFACT_GRADER_KIND, aggregateShots, buildNoArtifactPrompt, gradeNoArtifact, runOneShot } = require('./grader-no-artifact.js');
|
|
98
|
+
const { changeMagnitude, hasPanelEvidence, isSubstantiveIssue, serverSideGuards, validateAndScore } = require('./grader-score.js');
|
|
99
|
+
const { buildContextBundle, diffAddedLinesByFile, extractAssistantResult, extractCostUsd, runSubagent, runSubagentCached } = require('./grader-subagent.js');
|
|
95
100
|
// task 1002903: the severity gate's unbacked low scores, named once for the record.
|
|
96
101
|
const { unbackedLowScores } = require('./grader-workers/worker-verdict.js');
|
|
97
102
|
// task 1002839: the central engine the code panel below registers into (ADR 0089 §1).
|
|
@@ -478,8 +483,8 @@ function aggregatePanel(workerResults, meta = {}) {
|
|
|
478
483
|
grader_version: VERSION,
|
|
479
484
|
// #486: the panel's own API spend (USD), for cost_log (source=grader).
|
|
480
485
|
panel_cost_usd: panelCostUsd,
|
|
481
|
-
// rubric_scores keep the v3 keys so the trend dashboard
|
|
482
|
-
//
|
|
486
|
+
// rubric_scores keep the v3 keys so the trend dashboard and the server's
|
|
487
|
+
// validateAndScore re-validation continue to read them. Phase 2.1 added
|
|
483
488
|
// security_score; Phase 2.2 adds efficiency_score. Both additive —
|
|
484
489
|
// consumers that don't know about them just ignore the fields.
|
|
485
490
|
rubric_scores: {
|
|
@@ -651,12 +656,17 @@ async function gradeSubagent({ task, valueSummary, diff, changedFiles, diffStats
|
|
|
651
656
|
});
|
|
652
657
|
}
|
|
653
658
|
|
|
654
|
-
// ----------
|
|
659
|
+
// ---------- the trivial-diff fast path (#513) ----------
|
|
655
660
|
//
|
|
656
|
-
//
|
|
657
|
-
// (
|
|
658
|
-
//
|
|
659
|
-
|
|
661
|
+
// A small copy/docs-only diff gets ONE generalist shot instead of the four-worker
|
|
662
|
+
// panel — "lighter, not skipped" (ADR 0089 §7 keeps it). The name is historical:
|
|
663
|
+
// the v3 multi-shot GENERATION is retired (task 1002842 removed its 3-shot default
|
|
664
|
+
// and rubric block), and this is what is left of it — the working fast path.
|
|
665
|
+
// Moving it onto a v4 panel needs a reviewer type that reports both
|
|
666
|
+
// functional_fidelity and code_quality, which no v4 worker does; that is a cost
|
|
667
|
+
// and behaviour decision, not a cleanup. `runShot` is a test seam defaulted to
|
|
668
|
+
// the real spawner (the same pattern as gradeNoArtifact); production passes none.
|
|
669
|
+
async function gradeSubagentV3MultiShot({ task, valueSummary, diff, changedFiles, diffStats, shots = 1, builderModel, runOpts = {}, runShot = runOneShot }) {
|
|
660
670
|
const prompt = buildPrompt({ task, valueSummary, diff, changedFiles, diffStats, generatedProvenance: await generatedProvenance.verifyGeneratedProvenanceAsync({ changedFiles, cwd: runOpts.cwd }) }); // task 1002795
|
|
661
671
|
const graderModel = selectGraderModel({ builderModel });
|
|
662
672
|
const N = Math.max(1, Math.trunc(shots));
|
|
@@ -664,7 +674,7 @@ async function gradeSubagentV3MultiShot({ task, valueSummary, diff, changedFiles
|
|
|
664
674
|
const startedAt = Date.now();
|
|
665
675
|
const shotPromises = [];
|
|
666
676
|
for (let i = 0; i < N; i++) {
|
|
667
|
-
shotPromises.push(
|
|
677
|
+
shotPromises.push(runShot({ prompt, model: graderModel, shotIndex: i, runOpts }));
|
|
668
678
|
}
|
|
669
679
|
const shotResults = await Promise.all(shotPromises);
|
|
670
680
|
const totalMs = Date.now() - startedAt;
|
|
@@ -676,189 +686,6 @@ async function gradeSubagentV3MultiShot({ task, valueSummary, diff, changedFiles
|
|
|
676
686
|
});
|
|
677
687
|
}
|
|
678
688
|
|
|
679
|
-
// =========================================================================
|
|
680
|
-
// Legacy v1 (deterministic) — kept for scripts/gds/audit-ships.js only.
|
|
681
|
-
// =========================================================================
|
|
682
|
-
|
|
683
|
-
function scoreEstActualBand(actualMinutes, estMinutes) {
|
|
684
|
-
if (!Number.isFinite(actualMinutes) || actualMinutes <= 0) return 5;
|
|
685
|
-
if (!Number.isFinite(estMinutes) || estMinutes <= 0) return 6;
|
|
686
|
-
const ratio = actualMinutes / estMinutes;
|
|
687
|
-
if (ratio >= 0.5 && ratio <= 2.0) return 10;
|
|
688
|
-
if (ratio < 0.5) {
|
|
689
|
-
if (ratio <= 0.1) return 0;
|
|
690
|
-
return r1(10 * ((ratio - 0.1) / 0.4));
|
|
691
|
-
}
|
|
692
|
-
if (ratio > 2.0) {
|
|
693
|
-
if (ratio >= 10) return 0;
|
|
694
|
-
return r1(10 * (1 - (ratio - 2.0) / 8.0));
|
|
695
|
-
}
|
|
696
|
-
return 5;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
function scoreValueSummary(valueSummary) {
|
|
700
|
-
const s = (valueSummary || '').trim();
|
|
701
|
-
if (s.length === 0) return 0;
|
|
702
|
-
if (s.length < 40) return 5;
|
|
703
|
-
if (s.length < 120) return 8;
|
|
704
|
-
return /\b(add|fix|update|remove|refactor|wire|land|migrate|enable|disable|introduce|deprecate|ship|deploy|prevent|capture|gate|allocate|carry|expose|surface)\b/i.test(s)
|
|
705
|
-
? 10
|
|
706
|
-
: 8;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
function scoreDiffHygiene(diffStats) {
|
|
710
|
-
if (!diffStats) return 5;
|
|
711
|
-
const files = Number(diffStats.files_changed) || 0;
|
|
712
|
-
const added = Number(diffStats.lines_added) || 0;
|
|
713
|
-
const removed = Number(diffStats.lines_removed) || 0;
|
|
714
|
-
if (files === 0 || added + removed === 0) return 0;
|
|
715
|
-
if (files > 50) return 5;
|
|
716
|
-
return 10;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
function scoreScannerSmoke(verification) {
|
|
720
|
-
if (!verification) return 5;
|
|
721
|
-
return verification.scanner_clean && verification.smoke_clean ? 10 : 5;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
function scoreScopingSignal(scopingConfidence) {
|
|
725
|
-
if (scopingConfidence == null) return 6;
|
|
726
|
-
const c = Number(scopingConfidence);
|
|
727
|
-
if (c <= 0) return 2;
|
|
728
|
-
if (c === 1) return 4;
|
|
729
|
-
if (c === 2) return 6;
|
|
730
|
-
if (c === 3) return 7;
|
|
731
|
-
if (c === 4) return 8;
|
|
732
|
-
return 10;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function scoreLocNetNegative(diffStats) {
|
|
736
|
-
if (!diffStats) return 5;
|
|
737
|
-
const added = Number(diffStats.lines_added) || 0;
|
|
738
|
-
const removed = Number(diffStats.lines_removed) || 0;
|
|
739
|
-
if (removed > added) return 10;
|
|
740
|
-
if (removed === added) return 5;
|
|
741
|
-
return 0;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function scoreLearningRow(learningRow) {
|
|
745
|
-
if (!learningRow) return 0;
|
|
746
|
-
const body = (learningRow.body_md || '').trim();
|
|
747
|
-
if (body.length > 100) return 10;
|
|
748
|
-
if (body.length > 0) return 5;
|
|
749
|
-
return 0;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
function scoreArtRubricAvg(value) {
|
|
753
|
-
if (!Number.isFinite(value)) return null;
|
|
754
|
-
return clamp(r1(value), 0, 10);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
function blendWeightsForKind(kind) {
|
|
758
|
-
const blended = {};
|
|
759
|
-
for (const [name, def] of Object.entries(V1.weights.common)) {
|
|
760
|
-
blended[name] = def.weight;
|
|
761
|
-
}
|
|
762
|
-
const kindBonus = V1.weights.kind_specific[kind];
|
|
763
|
-
if (kindBonus) {
|
|
764
|
-
for (const [name, def] of Object.entries(kindBonus)) {
|
|
765
|
-
if (name === 'loc_net_negative' && 'diff_hygiene' in blended) delete blended.diff_hygiene;
|
|
766
|
-
if (name === 'learning_row' && 'diff_hygiene' in blended) delete blended.diff_hygiene;
|
|
767
|
-
blended[name] = def.weight;
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
const total = Object.values(blended).reduce((a, b) => a + b, 0);
|
|
771
|
-
for (const k of Object.keys(blended)) blended[k] = blended[k] / total;
|
|
772
|
-
return blended;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
// Legacy v1 entry. Renamed from `grade()` in V3.R18 (#209). Now ONLY
|
|
776
|
-
// called from scripts/gds/audit-ships.js (retroactive #204 audit). The
|
|
777
|
-
// runtime ship path uses gradeSubagent (v3 multi-shot).
|
|
778
|
-
function gradeDeterministic({ task, verification, actualMinutes, valueSummary, learningRow }) {
|
|
779
|
-
const kind = task?.kind || 'unclassified';
|
|
780
|
-
const weights = blendWeightsForKind(kind);
|
|
781
|
-
const rubricScores = {};
|
|
782
|
-
const signals = {
|
|
783
|
-
kind,
|
|
784
|
-
est_minutes: task?.est_minutes ?? null,
|
|
785
|
-
actual_minutes: actualMinutes ?? null,
|
|
786
|
-
scoping_confidence: task?.scoping_confidence ?? null,
|
|
787
|
-
scanner_clean: verification?.scanner_clean ?? null,
|
|
788
|
-
smoke_clean: verification?.smoke_clean ?? null,
|
|
789
|
-
diff_stats: verification?.diff_stats ?? null,
|
|
790
|
-
art_rubric_avg: verification?.art_rubric_avg ?? null,
|
|
791
|
-
value_summary_chars: (valueSummary || '').trim().length,
|
|
792
|
-
};
|
|
793
|
-
|
|
794
|
-
if ('est_actual_band' in weights) {
|
|
795
|
-
rubricScores.est_actual_band = scoreEstActualBand(actualMinutes, task?.est_minutes);
|
|
796
|
-
}
|
|
797
|
-
if ('value_summary' in weights) {
|
|
798
|
-
rubricScores.value_summary = scoreValueSummary(valueSummary);
|
|
799
|
-
}
|
|
800
|
-
if ('diff_hygiene' in weights) {
|
|
801
|
-
rubricScores.diff_hygiene = scoreDiffHygiene(verification?.diff_stats);
|
|
802
|
-
}
|
|
803
|
-
if ('scanner_smoke' in weights) {
|
|
804
|
-
rubricScores.scanner_smoke = scoreScannerSmoke(verification);
|
|
805
|
-
}
|
|
806
|
-
if ('scoping_signal' in weights) {
|
|
807
|
-
rubricScores.scoping_signal = scoreScopingSignal(task?.scoping_confidence);
|
|
808
|
-
}
|
|
809
|
-
if ('loc_net_negative' in weights) {
|
|
810
|
-
rubricScores.loc_net_negative = scoreLocNetNegative(verification?.diff_stats);
|
|
811
|
-
}
|
|
812
|
-
if ('learning_row' in weights) {
|
|
813
|
-
rubricScores.learning_row = scoreLearningRow(learningRow);
|
|
814
|
-
}
|
|
815
|
-
if ('art_rubric_avg' in weights) {
|
|
816
|
-
const s = scoreArtRubricAvg(verification?.art_rubric_avg);
|
|
817
|
-
if (s == null) {
|
|
818
|
-
const w = weights.art_rubric_avg;
|
|
819
|
-
delete weights.art_rubric_avg;
|
|
820
|
-
const remaining = Object.values(weights).reduce((a, b) => a + b, 0);
|
|
821
|
-
if (remaining > 0) {
|
|
822
|
-
for (const k of Object.keys(weights)) weights[k] = weights[k] / remaining;
|
|
823
|
-
}
|
|
824
|
-
} else {
|
|
825
|
-
rubricScores.art_rubric_avg = s;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
let score = 0;
|
|
830
|
-
for (const [name, w] of Object.entries(weights)) {
|
|
831
|
-
if (rubricScores[name] != null) score += rubricScores[name] * w;
|
|
832
|
-
}
|
|
833
|
-
score = r1(clamp(score, 0, 10));
|
|
834
|
-
|
|
835
|
-
const threshold = V1.threshold;
|
|
836
|
-
const passed = score >= threshold;
|
|
837
|
-
|
|
838
|
-
const lines = [];
|
|
839
|
-
lines.push(`Auto-grader ${V1.version} (deprecated) — task_kind=${kind}.`);
|
|
840
|
-
lines.push(`Score ${score}/10 vs threshold ${threshold} → ${passed ? 'PASS' : 'FAIL'}.`);
|
|
841
|
-
lines.push('');
|
|
842
|
-
lines.push('Per-criterion:');
|
|
843
|
-
for (const [name, w] of Object.entries(weights)) {
|
|
844
|
-
const s = rubricScores[name];
|
|
845
|
-
if (s == null) continue;
|
|
846
|
-
const contrib = r1(s * w);
|
|
847
|
-
lines.push(` - ${name.padEnd(20)} ${String(s).padStart(4)}/10 × ${w.toFixed(2)} = ${contrib}`);
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
return {
|
|
851
|
-
score,
|
|
852
|
-
passed,
|
|
853
|
-
threshold,
|
|
854
|
-
grader_kind: 'auto',
|
|
855
|
-
grader_version: V1.version,
|
|
856
|
-
rubric_scores: rubricScores,
|
|
857
|
-
signals,
|
|
858
|
-
notes_md: lines.join('\n'),
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
|
-
|
|
862
689
|
module.exports = {
|
|
863
690
|
// v4 surface (active panel-based gate)
|
|
864
691
|
aggregatePanel,
|
|
@@ -888,25 +715,13 @@ module.exports = {
|
|
|
888
715
|
runSubagent,
|
|
889
716
|
runSubagentCached, // System 5 (task 1359): opt-in content-addressed cached path
|
|
890
717
|
runOneShot,
|
|
891
|
-
gradeSubagentV3MultiShot, //
|
|
718
|
+
gradeSubagentV3MultiShot, // the trivial-diff fast path (#513) — one generalist shot
|
|
892
719
|
extractJson,
|
|
893
720
|
extractAssistantResult,
|
|
894
721
|
extractCostUsd, // #486: parse total_cost_usd from the envelope
|
|
895
722
|
THRESHOLD,
|
|
896
723
|
VERSION,
|
|
897
724
|
DEFAULT_GRADER_MODEL,
|
|
898
|
-
DEFAULT_SHOTS,
|
|
899
725
|
ASSUMED_BUILDER_MODEL,
|
|
900
|
-
// legacy v1 (audit-ships.js only)
|
|
901
|
-
gradeDeterministic,
|
|
902
|
-
scoreEstActualBand,
|
|
903
|
-
scoreValueSummary,
|
|
904
|
-
scoreDiffHygiene,
|
|
905
|
-
scoreScannerSmoke,
|
|
906
|
-
scoreScopingSignal,
|
|
907
|
-
scoreLocNetNegative,
|
|
908
|
-
scoreLearningRow,
|
|
909
|
-
scoreArtRubricAvg,
|
|
910
|
-
blendWeightsForKind,
|
|
911
726
|
RUBRIC,
|
|
912
727
|
};
|
|
@@ -277,9 +277,9 @@ async function resolveClaim({ claimId, builderId, outcome, notesMd, valueSummary
|
|
|
277
277
|
// The ordering below is NOT for the grader, whatever this comment used to
|
|
278
278
|
// say. V3.R18 (#209) moved the grader out of resolveClaim — see the block
|
|
279
279
|
// after this one — and the live panel never receives actual_minutes at all
|
|
280
|
-
// (modules/grading/grader-prompt.js buildPrompt prints est_minutes only;
|
|
281
|
-
//
|
|
282
|
-
//
|
|
280
|
+
// (modules/grading/grader-prompt.js buildPrompt prints est_minutes only; the
|
|
281
|
+
// one scorer that read actual_minutes was retired with the v1 grader in task
|
|
282
|
+
// 1002842). The seal must
|
|
283
283
|
// still precede the recompute, because the recompute SUMs the row it writes.
|
|
284
284
|
// tests/regrade_actual_minutes.mjs pins the panel half of that.
|
|
285
285
|
await sealSessionLog(client, {
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.1037",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.1037",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.1037",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -7497,5 +7497,15 @@
|
|
|
7497
7497
|
"id": "1004174",
|
|
7498
7498
|
"text": "The owner of a hosted project can now upgrade it themselves: the \"you're behind\" banner on their project links to the Deploy page on the hub, which lets a project's owner move their own project, and only their own."
|
|
7499
7499
|
}
|
|
7500
|
+
],
|
|
7501
|
+
"1.19.1037": [
|
|
7502
|
+
{
|
|
7503
|
+
"id": "1002842",
|
|
7504
|
+
"text": "The grader's retired first generation and an unused one-off audit script are removed, and the pieces a newer reviewer still depends on now live with it — nothing about how work is graded changes."
|
|
7505
|
+
},
|
|
7506
|
+
{
|
|
7507
|
+
"id": "1004280",
|
|
7508
|
+
"text": "The project's own notes no longer claim a past decision banned all new checks; each one now gives its real reason, and a test stops the wrong claim coming back."
|
|
7509
|
+
}
|
|
7500
7510
|
]
|
|
7501
7511
|
}
|
package/scripts/gds/claim.js
CHANGED
|
@@ -739,7 +739,8 @@ async function main() {
|
|
|
739
739
|
// last thing the builder reads before deciding how to work — task 1267.
|
|
740
740
|
printDisciplineMode(task);
|
|
741
741
|
// task 1004202: a task that changes a page is told to use /design and get its
|
|
742
|
-
// human's OK on the before/after. A nudge, never a gate
|
|
742
|
+
// human's OK on the before/after. A nudge, never a gate: gating on that OK would
|
|
743
|
+
// put a person's approval in the land path, which ADR 0162 retired.
|
|
743
744
|
try {
|
|
744
745
|
const nudge = visualReviewNudge(task);
|
|
745
746
|
if (nudge) console.log(`\n${nudge.join('\n')}`);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Subagent grader smoke test — GDS-V3 task V3.R18 (#209) → MAS Phase 1 #395.
|
|
3
3
|
//
|
|
4
4
|
// Runs two synthetic ship scenarios end-to-end through the v3 multi-shot
|
|
5
|
-
// subagent grader (
|
|
5
|
+
// subagent grader (the worker panel, cross-family model, unanimous-PASS gate).
|
|
6
6
|
// Asserts:
|
|
7
7
|
//
|
|
8
8
|
// - Known-GOOD ship: a tiny, complete change → grade passes (unanimous
|
|
@@ -148,8 +148,8 @@ function summarize(result, label) {
|
|
|
148
148
|
// -------- Main --------
|
|
149
149
|
|
|
150
150
|
(async () => {
|
|
151
|
-
console.log(`Running subagent grader smoke (v${grader.VERSION}, model=${grader.DEFAULT_GRADER_MODEL}, ${grader.
|
|
152
|
-
console.log(
|
|
151
|
+
console.log(`Running subagent grader smoke (v${grader.VERSION}, model=${grader.DEFAULT_GRADER_MODEL}, ${grader.WORKERS.DEFAULT_PANEL.length}-worker panel, threshold=${grader.THRESHOLD})`);
|
|
152
|
+
console.log(`Two scenarios will run sequentially. Each runs the ${grader.WORKERS.DEFAULT_PANEL.length}-worker panel.`);
|
|
153
153
|
|
|
154
154
|
let assertFailures = 0;
|
|
155
155
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// count that still ran past its card (task 1003480) and a second grid child landing on the first (task 1003078)
|
|
8
8
|
// both shipped looking correct in code and were found only by opening a browser.
|
|
9
9
|
//
|
|
10
|
-
// A SIGNAL, never a gate (ADR
|
|
10
|
+
// A SIGNAL, never a gate (ADR 0329, decisions 3-4): /ship-check shows it as an advisory row; ship.js and the grader never
|
|
11
11
|
// read it. The measurement is the kit's `text-fit` + `sibling-overlap` floors (modules/ui-design/kit/lib.js);
|
|
12
12
|
// this file only picks the pages, runs the kit's renderer at 1280 + 390, and keeps those two floors.
|
|
13
13
|
//
|
|
@@ -25,7 +25,7 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
|
25
25
|
|
|
26
26
|
// Each step: a label, the argv to run (node scripts run with THIS node), the command that heals it,
|
|
27
27
|
// and how to read a non-zero exit. `optional` steps are skipped when their script is absent. An `advisory`
|
|
28
|
-
// step is a signal, never a gate (ADR
|
|
28
|
+
// step is a signal, never a gate (ADR 0329, decision 3): a non-zero exit reads `warn` (3 = no browser reads `skipped`).
|
|
29
29
|
// `onlyWith` names the flag that adds a step; `skipWith` the flag that leaves one out.
|
|
30
30
|
const STEPS = [
|
|
31
31
|
{ key: 'fitness', label: 'fitness.js (architecture + budgets + freshness gate)', argv: ['scripts/gds/fitness.js'], fix: 'read the ✗ lines above; each names its fix' },
|
package/scripts/gds/ship-flow.js
CHANGED
|
@@ -496,7 +496,7 @@ async function shipMain() {
|
|
|
496
496
|
passed,
|
|
497
497
|
// touches[] cleanup [4/8] (task 879): the drift scanner is gone; with no
|
|
498
498
|
// scanner there is nothing to drift, so this audit-trail signal
|
|
499
|
-
// (grader
|
|
499
|
+
// (read by the retired v1 grader, task 1002842) is vacuously clean.
|
|
500
500
|
scanner_clean: true,
|
|
501
501
|
smoke_clean: smoke.clean,
|
|
502
502
|
},
|
|
@@ -314,7 +314,7 @@ function formatOutageDiagnostic(g, cliFinding) {
|
|
|
314
314
|
|
|
315
315
|
// cliFinding: the preflight `claude CLI` row, injected by printGradeOutcome so a
|
|
316
316
|
// dark panel can say "your CLI is logged out" instead of leaving the builder to
|
|
317
|
-
// guess. Report-only — it never changes the outcome
|
|
317
|
+
// guess. Report-only — it never changes the outcome: it explains an outage, it does not decide one.
|
|
318
318
|
function formatGradeOutcome(g, taskId, cliFinding = null) {
|
|
319
319
|
const out = [];
|
|
320
320
|
// #943: a TOTAL grader outage (every gating worker failed to spawn / the
|
|
@@ -466,7 +466,7 @@ function printGradeOutcome(g, taskId) {
|
|
|
466
466
|
// task 1003313: on a TOTAL outage only, ask the existing preflight whether the
|
|
467
467
|
// local `claude` CLI is launchable and logged in — the two faults that produce
|
|
468
468
|
// this exact all-workers-dead signature. Report-only and outage-only, so the
|
|
469
|
-
// happy path pays nothing and no ship
|
|
469
|
+
// happy path pays nothing and no ship's outcome depends on the probe.
|
|
470
470
|
let cliFinding = null;
|
|
471
471
|
if (g?.signals?.panel_outcome === 'unavailable') {
|
|
472
472
|
try {
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// real browser, and get the human's OK on the before/after before shipping — the
|
|
6
6
|
// project default for every builder since the owner's 2026-09-23 decision.
|
|
7
7
|
//
|
|
8
|
-
// A NUDGE, never a gate
|
|
8
|
+
// A NUDGE, never a gate: as a gate, the human's OK would be a person's approval in
|
|
9
|
+
// the land path, which ADR 0162 retired. "Is this UI work" is the ONE predicate the
|
|
9
10
|
// artist review and the grader's copy lens already use (lifecycle/cascade.js), so
|
|
10
11
|
// the three cannot disagree. Nothing has changed yet at claim time, so it is fed
|
|
11
12
|
// the paths the task NAMES: its touches[] and any modules/*/public/ path written
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.1037'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// tests/adr_0162_gate_citation.mjs — "no new gates" is not what ADR 0162 decided; no surface
|
|
2
|
+
// may cite 0162 as the reason something is a signal and not a gate (task 1004280).
|
|
3
|
+
//
|
|
4
|
+
// WHAT WENT WRONG. Task 1003573 corrected one slogan cited to ADR 0162 ("verdicts are advisory,
|
|
5
|
+
// never self-executing" — ADR 0158 §2's rule; tests/adr_0162_citation.mjs pins that). A sibling
|
|
6
|
+
// slogan rode along: "no new gates (ADR 0162)", "a signal, never a gate (ADR 0162)", "NOT A
|
|
7
|
+
// PIPELINE GATE (ADR 0162)". It is broader than the ADR. 0162 retired REVIEW gates — a gate that
|
|
8
|
+
// needs a particular person's approval before work can land — and its own table keeps four
|
|
9
|
+
// automatic controls, three of which fail CLOSED (the grader among them). ADR 0241 ("What ADR 0162
|
|
10
|
+
// actually forbids") states the test 0162 leaves behind: an automatic, state-read, rank-consistent
|
|
11
|
+
// gate with a named escape hatch is admissible. So "this is not a gate because 0162" cites a rule
|
|
12
|
+
// 0162 does not make, and it hides the surface's real reason for staying advisory.
|
|
13
|
+
//
|
|
14
|
+
// The surfaces keep their choice (most were right to stay advisory); only the reason changed:
|
|
15
|
+
// 0162 for "no human approval in the land path", and the surface's own reason otherwise.
|
|
16
|
+
//
|
|
17
|
+
// WHAT THIS PINS. (1) The premise, read from the ADRs: 0162 keeps fail-closed automatic controls
|
|
18
|
+
// and never says "no new gates"; 0241 says what 0162 forbids. (2) No tracked surface cites 0162
|
|
19
|
+
// with the gate shorthand, in any of the shapes the 2026-09-25 sweep found.
|
|
20
|
+
//
|
|
21
|
+
// Run: node --test tests/adr_0162_gate_citation.mjs
|
|
22
|
+
|
|
23
|
+
import { test } from 'node:test';
|
|
24
|
+
import assert from 'node:assert/strict';
|
|
25
|
+
import { readFileSync } from 'node:fs';
|
|
26
|
+
import { spawnSync } from 'node:child_process';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
|
|
30
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
|
+
const read = (rel) => readFileSync(path.join(ROOT, rel), 'utf8');
|
|
32
|
+
|
|
33
|
+
const ADR_0162 = 'docs/adr/0162-review-gates-retired-rank-consistent-ci-only.md';
|
|
34
|
+
const ADR_0241 = 'docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md';
|
|
35
|
+
|
|
36
|
+
// A match is a line that gives 0162 as the reason something is not (or must not be) a gate.
|
|
37
|
+
const GATE_SHORTHANDS = [
|
|
38
|
+
// The advisory claim, then 0162 as its source: "no new gates (ADR 0162)", "A SIGNAL, never a
|
|
39
|
+
// gate (ADR 0162)", "NOT A PIPELINE GATE (ADR 0162)", "no ship is newly gated (ADR 0162)",
|
|
40
|
+
// "WITHOUT adding a gate (ADR 0162)", "It gates NOTHING (ADR 0162 / ADR 0241)", "a new gate on
|
|
41
|
+
// the ship path (ADR 0162)", "It had to stay a signal (ADR 0162)", "never flips its exit (ADR 0162)".
|
|
42
|
+
/\b(?:no new gates?|never a gate|not a (?:pipeline )?gate|newly gated|adding a gate|gates nothing|new gate on the ship path|stay a signal|never flips its exit)\b[^\n()]{0,40}\(ADR 0162\b/i,
|
|
43
|
+
// 0162, then the shorthand as its gloss: "ADR 0162 — no new gates", "(ADR 0162: no new gates)",
|
|
44
|
+
// "[ADR 0162](…) (no new gates)".
|
|
45
|
+
/\b0162\b[\])]?\s*(?:\(|—|–|:)\s*no new gates?\b/i,
|
|
46
|
+
// A describe-only posture cited to 0162: "(describe, never confirm/flip — ADR 0162)". That is
|
|
47
|
+
// 0158 §2's rule — the sibling test's subject, in a shape it does not match.
|
|
48
|
+
/\bnever confirm\/flip\s*[—–-]\s*ADR 0162\b/i,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// Deliberately NOT corrected, each with its reason. Shrink-only: when one is fixed, delete its
|
|
52
|
+
// row — the last test fails until you do, so a stale exemption cannot outlive what it excused.
|
|
53
|
+
const KNOWN_DEFERRED = new Map([
|
|
54
|
+
// HARD-FLOOR surfaces (scripts/gds/gate-review.js HARD_FLOOR_GLOBS lists `.claude/hooks/`):
|
|
55
|
+
// even a comment-only edit escalates to the owner, which a citation fix does not justify on
|
|
56
|
+
// its own. Correct them with the next real edit (the redteam-patrol.yml precedent, task 1003573).
|
|
57
|
+
['.claude/hooks/allow-rule-guard.js', 'hard-floor gate surface'],
|
|
58
|
+
['.claude/hooks/git-guard.js', 'hard-floor gate surface'],
|
|
59
|
+
// A hall PAGE script: any edit under modules/*/public/ is UI work, so its ship files an artist
|
|
60
|
+
// review that holds the deploy (ADR 0241). A comment fix does not justify that; correct it
|
|
61
|
+
// with the next real edit to the page.
|
|
62
|
+
['modules/hall-ui/public/studio.js', 'page script — an edit files an artist review'],
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const HISTORICAL = /^docs\/session-logs\//;
|
|
66
|
+
// This file and its sibling quote the shapes they hunt, as examples.
|
|
67
|
+
const SELF = new Set(['tests/adr_0162_gate_citation.mjs', 'tests/adr_0162_citation.mjs']);
|
|
68
|
+
const TEXT = /\.(?:md|js|mjs|cjs|json|ya?ml|sql|sh|html|txt)$/;
|
|
69
|
+
// A line that NAMES the slogan as its subject is discussing it, not citing by it.
|
|
70
|
+
const DISCUSSES_SLOGAN = /paraphrase|shorthand/i;
|
|
71
|
+
// Markdown link targets carry the ADR's filename, which would sit between a citation and its gloss.
|
|
72
|
+
const stripLinkTargets = (s) => s.replace(/\]\([^)\s]*\)/g, ']');
|
|
73
|
+
const continuation = (s) => (s || '').replace(/^\s*(?:\/\/+|#+|--|\*|>|-)?\s*/, '');
|
|
74
|
+
|
|
75
|
+
function offendersIn(rel, text) {
|
|
76
|
+
const lines = stripLinkTargets(text).split('\n');
|
|
77
|
+
const hits = [];
|
|
78
|
+
for (let i = 0; i < lines.length; i++) {
|
|
79
|
+
const here = lines[i];
|
|
80
|
+
if (DISCUSSES_SLOGAN.test(here)) continue;
|
|
81
|
+
// Join the next line so a citation wrapped mid-phrase still reads as one; only a match
|
|
82
|
+
// that STARTS on this line is this line's.
|
|
83
|
+
const window = `${here} ${continuation(lines[i + 1])}`;
|
|
84
|
+
if (!/0162/.test(window)) continue;
|
|
85
|
+
for (const re of GATE_SHORTHANDS) {
|
|
86
|
+
const m = re.exec(window);
|
|
87
|
+
if (m && m.index < here.length) { hits.push(`${rel}:${i + 1}: ${here.trim().slice(0, 140)}`); break; }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return hits;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function trackedTextFiles() {
|
|
94
|
+
const r = spawnSync('git', ['ls-files', '-z'], { cwd: ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
95
|
+
if (r.status !== 0) return null; // a packaged core is not a git checkout
|
|
96
|
+
return r.stdout.split('\0').filter((f) => f && TEXT.test(f));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
test('the premise: ADR 0162 keeps automatic gates that fail closed, and never says "no new gates"', () => {
|
|
100
|
+
const adr0162 = read(ADR_0162);
|
|
101
|
+
assert.match(adr0162, /Review requirements on `main` are \*\*retired permanently\*\*/, 'what 0162 retired');
|
|
102
|
+
const failsClosed = adr0162.split('\n').filter((l) => /^\|.*\|\s*closed\s*\|\s*$/.test(l));
|
|
103
|
+
assert.ok(failsClosed.length >= 3,
|
|
104
|
+
`0162's compensating-controls table should keep >=3 controls that fail CLOSED (found ${failsClosed.length}) — `
|
|
105
|
+
+ 'if the ADR changed, re-ask whether "no new gates" became its rule before re-pointing these citations');
|
|
106
|
+
assert.doesNotMatch(adr0162, /no new gates?/i, 'ADR 0162 never says "no new gates"');
|
|
107
|
+
assert.match(read(ADR_0241), /^## What ADR 0162 actually forbids$/m, 'ADR 0241 is where the narrower rule is written');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('the detector recognises every shape the sweep corrected, and passes the true citations', () => {
|
|
111
|
+
const flag = (line) => offendersIn('probe.md', line).length > 0;
|
|
112
|
+
for (const bad of [
|
|
113
|
+
'- **ADR 0162 — no new gates.** This skill describes and recovers; it never inserts a human-approval step',
|
|
114
|
+
'// A SIGNAL, never a gate (ADR 0162): /ship-check shows it as an advisory row; ship.js and the grader never',
|
|
115
|
+
'// step is a signal, never a gate (ADR 0162): a non-zero exit reads `warn` (3 = no browser reads `skipped`).',
|
|
116
|
+
'// A NUDGE, never a gate (ADR 0162). "Is this UI work" is the ONE predicate the',
|
|
117
|
+
' // human\'s OK on the before/after. A nudge, never a gate (ADR 0162).',
|
|
118
|
+
'// guess. Report-only — it never changes the outcome (ADR 0162: no new gates).',
|
|
119
|
+
' // happy path pays nothing and no ship is newly gated (ADR 0162).',
|
|
120
|
+
'- **Relates to:** [ADR 0162](0162-review-gates-retired-rank-consistent-ci-only.md) (no new gates), [ADR 0197](0197-the-ui-design-module.md) (the kit)',
|
|
121
|
+
'or both. It had to stay a signal (ADR 0162).',
|
|
122
|
+
'- **Block the fourth attempt.** That is a new gate on the ship path (ADR 0162), and a wrong',
|
|
123
|
+
'shows them as an ADVISORY row that never flips its exit (ADR 0162). Not a grader worker',
|
|
124
|
+
'// NOT A PIPELINE GATE (ADR 0162): it gates no merge, no CI check, no ship.',
|
|
125
|
+
'// It gates NOTHING (ADR 0162 / ADR 0241): nothing here holds a ship or a deploy.',
|
|
126
|
+
'// pass and WITHOUT adding a gate (ADR 0162). Every case below that has a low',
|
|
127
|
+
'// read-only (describe, never confirm/flip — ADR 0162).',
|
|
128
|
+
]) assert.ok(flag(bad), `should flag: ${bad}`);
|
|
129
|
+
|
|
130
|
+
for (const good of [
|
|
131
|
+
'- **Related:** ADR 0162 (no new human gates on the ship path)',
|
|
132
|
+
'No review requirement — gates are CI checks, not human approvals (ADR 0162).',
|
|
133
|
+
'- **Relates to:** [ADR 0162](0162-review-gates-retired-rank-consistent-ci-only.md) (review gates retired: no human approval in the land path)',
|
|
134
|
+
'no confirms, no status flips, and no new approval step on anyone\'s ship (ADR 0162). The remediation lines',
|
|
135
|
+
'// A NUDGE, never a gate: as a gate, the human\'s OK would be a person\'s approval in\n// the land path, which ADR 0162 retired.',
|
|
136
|
+
'// A SIGNAL, never a gate (ADR 0329, decisions 3-4): /ship-check shows it as an advisory row',
|
|
137
|
+
'It never holds a builder\'s ship, which is what keeps it compatible with ADR 0162.',
|
|
138
|
+
'This matters, because the shorthand "no new gates (ADR 0162)" is broader than the ADR',
|
|
139
|
+
]) assert.ok(!flag(good), `should pass: ${good}`);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('no tracked surface cites ADR 0162 as the reason something is not a gate', (t) => {
|
|
143
|
+
const files = trackedTextFiles();
|
|
144
|
+
if (!files) return t.skip('not a git checkout — nothing tracked to scan');
|
|
145
|
+
const offenders = [];
|
|
146
|
+
for (const rel of files) {
|
|
147
|
+
if (SELF.has(rel) || HISTORICAL.test(rel) || KNOWN_DEFERRED.has(rel)) continue;
|
|
148
|
+
let text;
|
|
149
|
+
try { text = readFileSync(path.join(ROOT, rel), 'utf8'); } catch { continue; }
|
|
150
|
+
if (!text.includes('0162')) continue;
|
|
151
|
+
offenders.push(...offendersIn(rel, text));
|
|
152
|
+
}
|
|
153
|
+
assert.deepEqual(offenders, [],
|
|
154
|
+
'these cite ADR 0162 for "no new gates" / "a signal, never a gate". 0162 retired gates that need a '
|
|
155
|
+
+ 'particular person\'s approval in the land path, and keeps automatic gates that fail closed (ADR 0241). '
|
|
156
|
+
+ 'Cite 0162 for "no human approval in the land path", and name the surface\'s own reason for staying advisory.');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('every deferred exemption still has something to excuse (shrink-only)', (t) => {
|
|
160
|
+
if (!trackedTextFiles()) return t.skip('not a git checkout');
|
|
161
|
+
for (const [rel, why] of KNOWN_DEFERRED) {
|
|
162
|
+
let text = '';
|
|
163
|
+
try { text = read(rel); } catch { /* deleted: the exemption is stale too */ }
|
|
164
|
+
assert.ok(offendersIn(rel, text).length > 0,
|
|
165
|
+
`${rel} (${why}) no longer cites 0162 for the gate shorthand — delete its KNOWN_DEFERRED row`);
|
|
166
|
+
}
|
|
167
|
+
});
|