@bongos/core 1.19.25 → 1.19.27
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 +21 -16
- package/modules/grading/db.js +18 -7
- package/modules/grading/grader.js +36 -3
- package/modules/hall-ui/public/builders.js +13 -6
- package/modules/lifecycle/db.js +64 -22
- package/modules/lifecycle/routes/tasks.js +4 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +5 -1
- package/tests/grade_aggregates_unavailable.mjs +114 -0
- package/tests/grade_server_authoritative.mjs +87 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.27",
|
|
6
|
+
"core_contract": "1.19.27",
|
|
7
|
+
"source_commit": "a8f09266a4b9cdbdf70ea92de3c2a26553aacab2",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-11T13:
|
|
9
|
+
"built_at": "2026-08-11T13:38:48.421Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 285,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1205,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1511,
|
|
20
|
+
"tree_sha256": "b85699092f300b5bab1ab32e95763e4f032d2ee8f688466f4c4dc939048174bd",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -2822,7 +2822,7 @@
|
|
|
2822
2822
|
{
|
|
2823
2823
|
"path": "modules/grading/db.js",
|
|
2824
2824
|
"mode": "0000644",
|
|
2825
|
-
"sha256": "
|
|
2825
|
+
"sha256": "b651b73c65b0da33284430b430638dbc39b34db43846a05ab431deaabcc44b3b"
|
|
2826
2826
|
},
|
|
2827
2827
|
{
|
|
2828
2828
|
"path": "modules/grading/grader-rubric.json",
|
|
@@ -2882,7 +2882,7 @@
|
|
|
2882
2882
|
{
|
|
2883
2883
|
"path": "modules/grading/grader.js",
|
|
2884
2884
|
"mode": "0000644",
|
|
2885
|
-
"sha256": "
|
|
2885
|
+
"sha256": "bc2b580d2be98f5b2acb9a0dd9416dd3c2cad387bbe61a6823e37102d3468675"
|
|
2886
2886
|
},
|
|
2887
2887
|
{
|
|
2888
2888
|
"path": "modules/grading/module.json",
|
|
@@ -2932,7 +2932,7 @@
|
|
|
2932
2932
|
{
|
|
2933
2933
|
"path": "modules/hall-ui/public/builders.js",
|
|
2934
2934
|
"mode": "0000644",
|
|
2935
|
-
"sha256": "
|
|
2935
|
+
"sha256": "5bdd907f10ae15f8efdb55adce3ce23991a376122eb79c549e025f8f4845fae5"
|
|
2936
2936
|
},
|
|
2937
2937
|
{
|
|
2938
2938
|
"path": "modules/hall-ui/public/chime.wav",
|
|
@@ -3352,7 +3352,7 @@
|
|
|
3352
3352
|
{
|
|
3353
3353
|
"path": "modules/lifecycle/db.js",
|
|
3354
3354
|
"mode": "0000644",
|
|
3355
|
-
"sha256": "
|
|
3355
|
+
"sha256": "85c856016e548399538734098d2f08d3920fb52814759ffa314ed26d98bf32eb"
|
|
3356
3356
|
},
|
|
3357
3357
|
{
|
|
3358
3358
|
"path": "modules/lifecycle/done-when.js",
|
|
@@ -3457,7 +3457,7 @@
|
|
|
3457
3457
|
{
|
|
3458
3458
|
"path": "modules/lifecycle/routes/tasks.js",
|
|
3459
3459
|
"mode": "0000644",
|
|
3460
|
-
"sha256": "
|
|
3460
|
+
"sha256": "aa179ab79508a002e8f580fb839767d8aaa8b6f02a46d021bb61633dd9f8f9a4"
|
|
3461
3461
|
},
|
|
3462
3462
|
{
|
|
3463
3463
|
"path": "modules/lifecycle/routes/versions.js",
|
|
@@ -4002,12 +4002,12 @@
|
|
|
4002
4002
|
{
|
|
4003
4003
|
"path": "package-lock.json",
|
|
4004
4004
|
"mode": "0000644",
|
|
4005
|
-
"sha256": "
|
|
4005
|
+
"sha256": "a32cc5b3e33b39013ebe6508049b2ce0dac4c6ac8edce2adf396af265a0c4df8"
|
|
4006
4006
|
},
|
|
4007
4007
|
{
|
|
4008
4008
|
"path": "package.json",
|
|
4009
4009
|
"mode": "0000644",
|
|
4010
|
-
"sha256": "
|
|
4010
|
+
"sha256": "aea4f2f3266f8107e00346fb410bb214997bc0cda76e7ecba36b7f2210b9b661"
|
|
4011
4011
|
},
|
|
4012
4012
|
{
|
|
4013
4013
|
"path": "public-docs/index.html",
|
|
@@ -5627,7 +5627,7 @@
|
|
|
5627
5627
|
{
|
|
5628
5628
|
"path": "src/module-api.js",
|
|
5629
5629
|
"mode": "0000644",
|
|
5630
|
-
"sha256": "
|
|
5630
|
+
"sha256": "f75a298d5c776e9bb7ba33c75061ca193c73b0004eea7c0f6706f02b8daad0f0"
|
|
5631
5631
|
},
|
|
5632
5632
|
{
|
|
5633
5633
|
"path": "src/module-loader/loader.js",
|
|
@@ -6564,6 +6564,11 @@
|
|
|
6564
6564
|
"mode": "0000644",
|
|
6565
6565
|
"sha256": "a2d76ca391999e54b0767c3cba8eba34e84d5c704c3ac98370195ef9889bd152"
|
|
6566
6566
|
},
|
|
6567
|
+
{
|
|
6568
|
+
"path": "tests/grade_aggregates_unavailable.mjs",
|
|
6569
|
+
"mode": "0000644",
|
|
6570
|
+
"sha256": "a55ae09e94648035479ff214ac26c2c84f2fc3c166237f06d296a6fd6608ec48"
|
|
6571
|
+
},
|
|
6567
6572
|
{
|
|
6568
6573
|
"path": "tests/grade_attempts.mjs",
|
|
6569
6574
|
"mode": "0000644",
|
|
@@ -6582,7 +6587,7 @@
|
|
|
6582
6587
|
{
|
|
6583
6588
|
"path": "tests/grade_server_authoritative.mjs",
|
|
6584
6589
|
"mode": "0000644",
|
|
6585
|
-
"sha256": "
|
|
6590
|
+
"sha256": "b0af74bc3b3e203716b13fbf3fc174a90f51daf659d5c5e5400725e85a0da4fc"
|
|
6586
6591
|
},
|
|
6587
6592
|
{
|
|
6588
6593
|
"path": "tests/grade_unavailable_display.mjs",
|
package/modules/grading/db.js
CHANGED
|
@@ -18,22 +18,33 @@
|
|
|
18
18
|
const api = require('../../src/module-api');
|
|
19
19
|
const { pool } = api;
|
|
20
20
|
|
|
21
|
-
// Rolling grade stats for a builder over the last `days`: { n, avg_score,
|
|
22
|
-
// Windows by graded_at; `shipped_by` attributes the
|
|
23
|
-
// the work. Returns a zero row when the builder
|
|
21
|
+
// Rolling grade stats for a builder over the last `days`: { n, avg_score,
|
|
22
|
+
// n_passed, n_unavailable }. Windows by graded_at; `shipped_by` attributes the
|
|
23
|
+
// grade to the builder who did the work. Returns a zero row when the builder
|
|
24
|
+
// has no grades in the window.
|
|
25
|
+
//
|
|
26
|
+
// Quality stats count ONLY quality-graded rows (task 1002652): a grader outage
|
|
27
|
+
// (#943 — signals.panel_outcome='unavailable') or a json-null gating axis
|
|
28
|
+
// (functional_fidelity / code_quality) is bucketed as n_unavailable, never a
|
|
29
|
+
// 0-score fail. The predicate is a documented twin of lifecycle/db.js
|
|
30
|
+
// `gradeQualityWhere` (module boundary — keep the two in sync).
|
|
24
31
|
async function builderRollingAvgGrade(builderId, days = 14) {
|
|
32
|
+
const q = `(COALESCE(g.signals->>'panel_outcome','') <> 'unavailable'
|
|
33
|
+
AND COALESCE(jsonb_typeof(g.rubric_scores->'functional_fidelity'),'') <> 'null'
|
|
34
|
+
AND COALESCE(jsonb_typeof(g.rubric_scores->'code_quality'),'') <> 'null')`;
|
|
25
35
|
const { rows } = await pool.query(
|
|
26
36
|
`SELECT
|
|
27
|
-
COUNT(*)::int
|
|
28
|
-
COALESCE(AVG(g.score), 0)::numeric(3,1)
|
|
29
|
-
COALESCE(SUM(CASE WHEN g.passed THEN 1 ELSE 0 END), 0)::int AS n_passed
|
|
37
|
+
COUNT(*) FILTER (WHERE ${q})::int AS n,
|
|
38
|
+
COALESCE(AVG(g.score) FILTER (WHERE ${q}), 0)::numeric(3,1) AS avg_score,
|
|
39
|
+
COALESCE(SUM(CASE WHEN g.passed THEN 1 ELSE 0 END) FILTER (WHERE ${q}), 0)::int AS n_passed,
|
|
40
|
+
COUNT(*) FILTER (WHERE NOT ${q})::int AS n_unavailable
|
|
30
41
|
FROM task_grades g
|
|
31
42
|
JOIN tasks t ON t.id = g.task_id
|
|
32
43
|
WHERE t.shipped_by = $1
|
|
33
44
|
AND g.graded_at >= now() - ($2 || ' days')::interval`,
|
|
34
45
|
[builderId, String(days)]
|
|
35
46
|
);
|
|
36
|
-
return rows[0] || { n: 0, avg_score: 0, n_passed: 0 };
|
|
47
|
+
return rows[0] || { n: 0, avg_score: 0, n_passed: 0, n_unavailable: 0 };
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
module.exports = { builderRollingAvgGrade };
|
|
@@ -645,6 +645,37 @@ function serverSideGuards(gradeResult, { committedFiles, builderRank, diffStats,
|
|
|
645
645
|
if (clientPassed) downgradeTo = 'fail';
|
|
646
646
|
}
|
|
647
647
|
|
|
648
|
+
// --- Guard 1b: vacuous permission-path input (task 1002646 / report 1000021 G-1) ---
|
|
649
|
+
// The pre-pass above pairs the SERVER-known rank with CLIENT-supplied
|
|
650
|
+
// committed_files, so omitting the list (or sending []) made the sub-floor
|
|
651
|
+
// protected-path check pass VACUOUSLY — zero files, zero hits, hardFail:false —
|
|
652
|
+
// a trusted-rank check applied to an untrusted, omittable diff. The server
|
|
653
|
+
// still cannot re-derive the feature-branch diff at grade time (boundary note
|
|
654
|
+
// above; the task-1002645 published tip is recorded at ci-land, AFTER the
|
|
655
|
+
// grade), but it can refuse to treat "no input" as "no hits": a caller below
|
|
656
|
+
// the registry's lowest floor submitting NO file list makes the check
|
|
657
|
+
// UNPROVABLE → fail closed. Honest ships are unaffected (ship.js always sends
|
|
658
|
+
// the real committed list — task 878 — and a floor-clearing rank never enters
|
|
659
|
+
// this branch). The dishonest-LIST case (a sub-floor caller sending an
|
|
660
|
+
// innocuous list while touching protected paths) stays owned by gate-review CI
|
|
661
|
+
// + the post-push main audit, which read the REAL diff. Unknown rank keeps
|
|
662
|
+
// failing open at this layer (isBelowFloor → false) — the main audit reads the
|
|
663
|
+
// real author rank from the DB and owns that case, exactly as for the pre-pass.
|
|
664
|
+
const submittedFileCount = Array.isArray(committedFiles)
|
|
665
|
+
? committedFiles.filter((f) => typeof f === 'string' && f.trim()).length
|
|
666
|
+
: 0;
|
|
667
|
+
if (permissionPathCheck.isBelowFloor(builderRank) && submittedFileCount === 0) {
|
|
668
|
+
// Resolve the floor label from the registry at runtime (the D-16/N-3
|
|
669
|
+
// stale-string rule) — never a hardcoded rank name.
|
|
670
|
+
const floorRank = Object.keys(permissionPathCheck.RANK_TIER)
|
|
671
|
+
.find((r) => permissionPathCheck.RANK_TIER[r] === permissionPathCheck.MIN_TIER) || 'the floor';
|
|
672
|
+
guardsTripped.push({ guard: 'permission_path_unprovable', severity: 'blocker',
|
|
673
|
+
summary: `rank '${builderRank}' is below the protected-path floor ('${floorRank}') and the grade submitted no committed_files — the permission-path check cannot run on an empty list, so a client PASS cannot stand (fail closed; task 1002646 / SR-9). Remedy: re-run the ship — ship.js submits the real committed file list; a genuinely file-less (DB-only) change needs a ${floorRank}+ builder to land it via /tasks/:id/confirm.` });
|
|
674
|
+
extraIssues.push({ severity: 'blocker', file: '(grade)', summary:
|
|
675
|
+
`[server permission-path] no committed_files submitted by a rank below the '${floorRank}' floor — the protected-path check is unprovable and fails closed (task 1002646)`, worker: 'server-permission-path' });
|
|
676
|
+
if (clientPassed) downgradeTo = 'fail';
|
|
677
|
+
}
|
|
678
|
+
|
|
648
679
|
// --- Guard 2: implausible client verdict (SR-9) ---
|
|
649
680
|
// A client PASS with an empty issues[] on a non-trivial diff is not a credible
|
|
650
681
|
// default-fail panel verdict. Downgrade to manual-confirm (a human lands it via
|
|
@@ -697,7 +728,7 @@ function serverSideGuards(gradeResult, { committedFiles, builderRank, diffStats,
|
|
|
697
728
|
...base,
|
|
698
729
|
signals: {
|
|
699
730
|
...(base.signals || {}),
|
|
700
|
-
server_guard: { applied: ['route_rank', 'permission_path', 'implausible_pass', 'forged_net_negative'], ok: true, tripped: [] },
|
|
731
|
+
server_guard: { applied: ['route_rank', 'permission_path', 'permission_path_unprovable', 'implausible_pass', 'forged_net_negative'], ok: true, tripped: [] },
|
|
701
732
|
},
|
|
702
733
|
};
|
|
703
734
|
}
|
|
@@ -729,7 +760,7 @@ function serverSideGuards(gradeResult, { committedFiles, builderRank, diffStats,
|
|
|
729
760
|
// body already ran; this is the authoritative post-hoc adjustment).
|
|
730
761
|
confidence: forwardedConfidence,
|
|
731
762
|
server_guard: {
|
|
732
|
-
applied: ['route_rank', 'permission_path', 'implausible_pass', 'forged_net_negative'],
|
|
763
|
+
applied: ['route_rank', 'permission_path', 'permission_path_unprovable', 'implausible_pass', 'forged_net_negative'],
|
|
733
764
|
ok: guardsTripped.length === 0,
|
|
734
765
|
downgraded_to: downgradeTo,
|
|
735
766
|
client_passed: clientPassed,
|
|
@@ -739,7 +770,9 @@ function serverSideGuards(gradeResult, { committedFiles, builderRank, diffStats,
|
|
|
739
770
|
// the code. Named here so clients render it as a wall, not a quality
|
|
740
771
|
// FAIL the builder should fix-and-retry (the envelope used to teach
|
|
741
772
|
// exactly that wrong lesson). route_rank stays a code finding.
|
|
742
|
-
|
|
773
|
+
// task 1002646: permission_path_unprovable is the same wall — the rank
|
|
774
|
+
// floor refusing an unverifiable diff — so it renders the same way.
|
|
775
|
+
authorization_refusal: guardsTripped.some((g) => g.guard === 'permission_path' || g.guard === 'permission_path_unprovable'),
|
|
743
776
|
},
|
|
744
777
|
},
|
|
745
778
|
notes_md: `${base.notes_md || ''}${guardNote}`,
|
|
@@ -1457,15 +1457,20 @@
|
|
|
1457
1457
|
return;
|
|
1458
1458
|
}
|
|
1459
1459
|
tbody.innerHTML = grades.map((g) => {
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1460
|
+
// A grader OUTAGE row (panel_outcome 'unavailable') carries a placeholder
|
|
1461
|
+
// score of 0 — label it, never render it as a 0.0 fail (task 1002652).
|
|
1462
|
+
const isOutage = g.panel_outcome === 'unavailable';
|
|
1463
|
+
const score = isOutage ? '—' : Number(g.score).toFixed(1);
|
|
1464
|
+
const verdict = isOutage
|
|
1465
|
+
? `<span style="color:var(--ink-soft,#8a8577)">not graded (grader outage)</span>`
|
|
1466
|
+
: g.passed
|
|
1467
|
+
? `<span style="color:var(--olive,#5b7a4d);font-weight:600">passed</span>`
|
|
1468
|
+
: `<span style="color:#a04040;font-weight:600">fell short</span>`;
|
|
1464
1469
|
return `
|
|
1465
1470
|
<tr>
|
|
1466
1471
|
<td class="ledger__num">#${g.task_id}</td>
|
|
1467
1472
|
<td>${escapeHtml(g.title || '')}</td>
|
|
1468
|
-
<td class="ledger__drachmae">${score} / 10</td>
|
|
1473
|
+
<td class="ledger__drachmae">${isOutage ? '—' : `${score} / 10`}</td>
|
|
1469
1474
|
<td>${verdict}</td>
|
|
1470
1475
|
<td class="ledger__date">${fmtDate(g.graded_at)}</td>
|
|
1471
1476
|
</tr>
|
|
@@ -1603,7 +1608,9 @@
|
|
|
1603
1608
|
if (!isEco) {
|
|
1604
1609
|
charts.push(chartCard('Cost / work ($)', (data.cost_trend || []).map((r) => r.per_work), 'var(--terracotta)',
|
|
1605
1610
|
`total ${usd(h.total_cost_usd)}`));
|
|
1606
|
-
|
|
1611
|
+
// n counts quality-graded rows only (task 1002652); a day whose grades
|
|
1612
|
+
// were all outages has n=0 and must not plot as a 0% dip.
|
|
1613
|
+
charts.push(chartCard('Grade pass rate', (data.grade_trend || []).filter((r) => Number(r.n) > 0).map((r) => r.n_passed / r.n), 'var(--accent)',
|
|
1607
1614
|
'share of work passing review'));
|
|
1608
1615
|
}
|
|
1609
1616
|
gridEl.innerHTML = charts.join('');
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -83,15 +83,34 @@ const staleClaimHourPredicate = (param) =>
|
|
|
83
83
|
AND c.claimed_at < now() - (${param}::int * interval '1 hour'))`;
|
|
84
84
|
|
|
85
85
|
// grade pass-count fragment: SUM of passed-as-1. `col` is the (possibly
|
|
86
|
-
// alias-qualified) boolean column ('passed' or 'g.passed');
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
// alias-qualified) boolean column ('passed' or 'g.passed'); { where } restricts
|
|
87
|
+
// the sum to quality-graded rows via FILTER (task 1002652 — every aggregate
|
|
88
|
+
// site passes gradeQualityWhere, and keeps coalesce on because FILTER can zero
|
|
89
|
+
// out a group). { coalesce: false } preserved for callers that want a raw SUM.
|
|
90
|
+
|
|
91
|
+
const gradePassCount = (col, { coalesce = true, where = null } = {}) => {
|
|
92
|
+
const filter = where ? ` FILTER (WHERE ${where})` : '';
|
|
93
|
+
const inner = `SUM(CASE WHEN ${col} THEN 1 ELSE 0 END)${filter}`;
|
|
92
94
|
return coalesce ? `COALESCE(${inner}, 0)::int` : `${inner}::int`;
|
|
93
95
|
};
|
|
94
96
|
|
|
97
|
+
// Quality-graded row predicate (task 1002652). Two task_grades shapes carry
|
|
98
|
+
// score 0 / passed=false that are NOT quality verdicts and must never enter an
|
|
99
|
+
// aggregate: a grader OUTAGE (#943 — signals.panel_outcome='unavailable',
|
|
100
|
+
// rubric axes json-null) and a panel row whose gating axis
|
|
101
|
+
// (functional_fidelity / code_quality) is json-null (an errored worker, not a
|
|
102
|
+
// judged 0). jsonb_typeof + COALESCE keep legacy rows counted (missing keys /
|
|
103
|
+
// SQL-NULL rubric_scores → typeof NULL → '' ≠ 'null') and keep the predicate
|
|
104
|
+
// NULL-safe under NOT (a bare NULL operand would drop rows from the
|
|
105
|
+
// n_unavailable bucket).
|
|
106
|
+
|
|
107
|
+
const gradeQualityWhere = (alias = '') => {
|
|
108
|
+
const a = alias ? `${alias}.` : '';
|
|
109
|
+
return `(COALESCE(${a}signals->>'panel_outcome','') <> 'unavailable'
|
|
110
|
+
AND COALESCE(jsonb_typeof(${a}rubric_scores->'functional_fidelity'),'') <> 'null'
|
|
111
|
+
AND COALESCE(jsonb_typeof(${a}rubric_scores->'code_quality'),'') <> 'null')`;
|
|
112
|
+
};
|
|
113
|
+
|
|
95
114
|
// The shared `bounds`/`days` DAY_SERIES_CTE for the costSummary day-series
|
|
96
115
|
// queries moved with costSummary into modules/economy/cost.js (BV1.R77 / ADR
|
|
97
116
|
// 0093 §§1-2) — its only two callers left the monolith.
|
|
@@ -4856,9 +4875,13 @@ async function recentShippedTasksByBuilder(builderId, limit = 10) {
|
|
|
4856
4875
|
// this is safe to call freely.
|
|
4857
4876
|
|
|
4858
4877
|
async function recentGradesForBuilder(builderId, limit = 5) {
|
|
4878
|
+
// panel_outcome rides along so consumers can LABEL an outage row ("not
|
|
4879
|
+
// graded") instead of rendering its placeholder 0.0 as a fail (task 1002652).
|
|
4880
|
+
// A list read stays unfiltered — honesty here means labeling, not hiding.
|
|
4859
4881
|
const { rows } = await pool.query(
|
|
4860
4882
|
`SELECT g.task_id, g.score, g.passed, g.threshold, g.graded_at,
|
|
4861
4883
|
g.grader_kind, g.grader_version, g.rubric_scores,
|
|
4884
|
+
g.signals->>'panel_outcome' AS panel_outcome,
|
|
4862
4885
|
COALESCE(g.signals->'issues', '[]'::jsonb) AS issues,
|
|
4863
4886
|
COALESCE((g.signals->>'confidence')::numeric, NULL) AS confidence,
|
|
4864
4887
|
t.title, t.kind, t.discipline
|
|
@@ -4876,33 +4899,43 @@ async function recentGradesForBuilder(builderId, limit = 5) {
|
|
|
4876
4899
|
// public status tile + leaderboard hover.
|
|
4877
4900
|
|
|
4878
4901
|
async function rollingAvgGrade(days = 7) {
|
|
4902
|
+
// Quality stats aggregate ONLY quality-graded rows; outage / null-axis rows
|
|
4903
|
+
// are counted apart as n_unavailable (task 1002652 — 21 outage rows once
|
|
4904
|
+
// read as a month of pass_rate 0.000).
|
|
4905
|
+
const q = gradeQualityWhere();
|
|
4879
4906
|
const { rows } = await pool.query(
|
|
4880
4907
|
`SELECT
|
|
4881
|
-
COUNT(*)::int
|
|
4882
|
-
COALESCE(AVG(score), 0)::numeric(3,1)
|
|
4883
|
-
${gradePassCount('passed')} AS n_passed,
|
|
4884
|
-
COALESCE(MIN(score), 0)::numeric(3,1)
|
|
4885
|
-
COALESCE(MAX(score), 0)::numeric(3,1)
|
|
4908
|
+
COUNT(*) FILTER (WHERE ${q})::int AS n,
|
|
4909
|
+
COALESCE(AVG(score) FILTER (WHERE ${q}), 0)::numeric(3,1) AS avg_score,
|
|
4910
|
+
${gradePassCount('passed', { where: q })} AS n_passed,
|
|
4911
|
+
COALESCE(MIN(score) FILTER (WHERE ${q}), 0)::numeric(3,1) AS min_score,
|
|
4912
|
+
COALESCE(MAX(score) FILTER (WHERE ${q}), 0)::numeric(3,1) AS max_score,
|
|
4913
|
+
COUNT(*) FILTER (WHERE NOT ${q})::int AS n_unavailable
|
|
4886
4914
|
FROM task_grades
|
|
4887
4915
|
WHERE graded_at >= now() - ($1 || ' days')::interval`,
|
|
4888
4916
|
[String(days)]
|
|
4889
4917
|
);
|
|
4890
|
-
return rows[0] || { n: 0, avg_score: 0, n_passed: 0, min_score: 0, max_score: 0 };
|
|
4918
|
+
return rows[0] || { n: 0, avg_score: 0, n_passed: 0, min_score: 0, max_score: 0, n_unavailable: 0 };
|
|
4891
4919
|
}
|
|
4892
4920
|
|
|
4893
4921
|
// Per-builder rolling avg, last `days` days. Powers the /builders profile.
|
|
4894
4922
|
|
|
4895
4923
|
async function gradeByBuilder(days = 30) {
|
|
4924
|
+
// pass_rate is NULL (not 0.000) for a builder whose window holds only
|
|
4925
|
+
// outage / null-axis rows — "no graded work", never a fabricated fail-streak
|
|
4926
|
+
// (task 1002652). fmtPct-style consumers already render null as '—'.
|
|
4927
|
+
const q = gradeQualityWhere('g');
|
|
4896
4928
|
const { rows } = await pool.query(
|
|
4897
4929
|
`SELECT
|
|
4898
4930
|
t.shipped_by AS builder_id,
|
|
4899
4931
|
b.github_login,
|
|
4900
4932
|
b.display_name,
|
|
4901
4933
|
b.avatar_url,
|
|
4902
|
-
COUNT(*)::int
|
|
4903
|
-
COALESCE(AVG(g.score), 0)::numeric(3,1)
|
|
4904
|
-
${gradePassCount('g.passed')} AS n_passed,
|
|
4905
|
-
ROUND(
|
|
4934
|
+
COUNT(*) FILTER (WHERE ${q})::int AS n,
|
|
4935
|
+
COALESCE(AVG(g.score) FILTER (WHERE ${q}), 0)::numeric(3,1) AS avg_score,
|
|
4936
|
+
${gradePassCount('g.passed', { where: q })} AS n_passed,
|
|
4937
|
+
ROUND(AVG(CASE WHEN g.passed THEN 1 ELSE 0 END) FILTER (WHERE ${q}), 3) AS pass_rate,
|
|
4938
|
+
COUNT(*) FILTER (WHERE NOT ${q})::int AS n_unavailable,
|
|
4906
4939
|
MAX(g.graded_at) AS last_graded_at
|
|
4907
4940
|
FROM task_grades g
|
|
4908
4941
|
JOIN tasks t ON t.id = g.task_id
|
|
@@ -4920,12 +4953,17 @@ async function gradeByBuilder(days = 30) {
|
|
|
4920
4953
|
// chart on status.amazonprimea.com.
|
|
4921
4954
|
|
|
4922
4955
|
async function gradeTrendByDay(days = 30) {
|
|
4956
|
+
// A day whose rows are all outages keeps its row (n=0, avg_score NULL,
|
|
4957
|
+
// n_unavailable>0) so a chart can show "grader down", never a 0.0-score day
|
|
4958
|
+
// (task 1002652 — the 2026-07-12..19 zero-score week was five outage days).
|
|
4959
|
+
const q = gradeQualityWhere();
|
|
4923
4960
|
const { rows } = await pool.query(
|
|
4924
4961
|
`SELECT
|
|
4925
4962
|
to_char(date_trunc('day', graded_at), 'YYYY-MM-DD') AS day,
|
|
4926
|
-
COUNT(*)::int AS n,
|
|
4927
|
-
AVG(score)::numeric(3,1) AS avg_score,
|
|
4928
|
-
${gradePassCount('passed', {
|
|
4963
|
+
COUNT(*) FILTER (WHERE ${q})::int AS n,
|
|
4964
|
+
AVG(score) FILTER (WHERE ${q})::numeric(3,1) AS avg_score,
|
|
4965
|
+
${gradePassCount('passed', { where: q })} AS n_passed,
|
|
4966
|
+
COUNT(*) FILTER (WHERE NOT ${q})::int AS n_unavailable
|
|
4929
4967
|
FROM task_grades
|
|
4930
4968
|
WHERE graded_at >= now() - ($1 || ' days')::interval
|
|
4931
4969
|
GROUP BY date_trunc('day', graded_at)
|
|
@@ -4989,11 +5027,14 @@ async function builderAnalytics(builderId, days = 90) {
|
|
|
4989
5027
|
[builderId, d]
|
|
4990
5028
|
),
|
|
4991
5029
|
// Grade pass-rate per day (quality holding up as throughput rises?).
|
|
5030
|
+
// Quality-graded rows only — outage / null-axis rows would read as
|
|
5031
|
+
// fail-days (task 1002652); headline.grade_pass_rate derives from these
|
|
5032
|
+
// rows so it inherits the same honesty.
|
|
4992
5033
|
pool.query(
|
|
4993
5034
|
`SELECT to_char(date_trunc('day', g.graded_at), 'YYYY-MM-DD') AS day,
|
|
4994
|
-
COUNT(*)::int AS n,
|
|
4995
|
-
${gradePassCount('g.passed', {
|
|
4996
|
-
AVG(g.score)::numeric(3,1) AS avg_score
|
|
5035
|
+
COUNT(*) FILTER (WHERE ${gradeQualityWhere('g')})::int AS n,
|
|
5036
|
+
${gradePassCount('g.passed', { where: gradeQualityWhere('g') })} AS n_passed,
|
|
5037
|
+
AVG(g.score) FILTER (WHERE ${gradeQualityWhere('g')})::numeric(3,1) AS avg_score
|
|
4997
5038
|
FROM task_grades g
|
|
4998
5039
|
JOIN tasks t ON t.id = g.task_id
|
|
4999
5040
|
WHERE t.shipped_by = $1
|
|
@@ -5457,6 +5498,7 @@ module.exports = {
|
|
|
5457
5498
|
rollingAvgGrade,
|
|
5458
5499
|
gradeByBuilder,
|
|
5459
5500
|
gradeTrendByDay,
|
|
5501
|
+
gradeQualityWhere, // exported for tests/grade_aggregates_unavailable.mjs (task 1002652)
|
|
5460
5502
|
builderAnalytics,
|
|
5461
5503
|
ecosystemAnalytics,
|
|
5462
5504
|
createOverrideRequest,
|
|
@@ -1431,8 +1431,10 @@ module.exports = function buildTasksRouter() {
|
|
|
1431
1431
|
// forged {ff:10,cq:10,issues:[]} to self-pass. serverSideGuards re-runs the
|
|
1432
1432
|
// two DETERMINISTIC pre-passes here (route-rank against the server's own
|
|
1433
1433
|
// route files; permission-path against the caller's LIVE, authoritative
|
|
1434
|
-
// rank — req.builder.rank, read fresh from the DB per request),
|
|
1435
|
-
//
|
|
1434
|
+
// rank — req.builder.rank, read fresh from the DB per request), fails a
|
|
1435
|
+
// sub-floor PASS that submitted NO committed_files closed (the check is
|
|
1436
|
+
// unprovable on an empty list — task 1002646 / report 1000021 G-1), rejects
|
|
1437
|
+
// an implausible empty-issues PASS on a non-trivial diff, and neutralizes an
|
|
1436
1438
|
// obviously-forged net-negative credit signal (SR-18). It is DOWNGRADE-ONLY:
|
|
1437
1439
|
// it can flip a client PASS to a hard FAIL or manual-confirm, but never
|
|
1438
1440
|
// upgrades a fail and never relaxes the existing gate — so a legitimate
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.27",
|
|
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.27",
|
|
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.27",
|
|
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/src/module-api.js
CHANGED
|
@@ -477,8 +477,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
477
477
|
// landed since 1.19.23 with no explicit bump. run 31450459825. (task 1002620)
|
|
478
478
|
// 1.19.25 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
479
479
|
// landed since 1.19.24 with no explicit bump. run 31495418623. (task 1002620)
|
|
480
|
+
// 1.19.26 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
481
|
+
// landed since 1.19.25 with no explicit bump. run 31495996665. (task 1002620)
|
|
482
|
+
// 1.19.27 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
483
|
+
// landed since 1.19.26 with no explicit bump. run 31497212656. (task 1002620)
|
|
480
484
|
// ---------------------------------------------------------------------------
|
|
481
|
-
const CORE_VERSION = '1.19.
|
|
485
|
+
const CORE_VERSION = '1.19.27'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
482
486
|
|
|
483
487
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
484
488
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// tests/grade_aggregates_unavailable.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit tests for task 1002652 — a grader OUTAGE row (#943: score 0, passed
|
|
4
|
+
// false, signals.panel_outcome='unavailable', rubric axes json-null) and a
|
|
5
|
+
// null-gating-axis panel row must never enter a quality aggregate. 21 outage
|
|
6
|
+
// rows once read as a month of pass_rate 0.000 on /public/grades and the hall.
|
|
7
|
+
//
|
|
8
|
+
// The SQL itself runs against live Postgres in production; here we pin the
|
|
9
|
+
// QUERY CONTRACT the same way the other pool.query readers are pinned (see
|
|
10
|
+
// versions_route.mjs note): pool.query is stubbed to capture SQL, and each
|
|
11
|
+
// aggregate's captured text must carry the quality predicate + the
|
|
12
|
+
// n_unavailable bucket. gradeQualityWhere's NULL-safety (COALESCE around every
|
|
13
|
+
// operand) is what keeps legacy rows counted AND the NOT-bucket total — a bare
|
|
14
|
+
// NULL operand under NOT would silently drop rows from both buckets.
|
|
15
|
+
//
|
|
16
|
+
// Run: node tests/grade_aggregates_unavailable.mjs
|
|
17
|
+
|
|
18
|
+
import { strict as assert } from 'node:assert';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const { pool } = require('../src/bongos/pool.js');
|
|
23
|
+
const db = require('../modules/lifecycle/db.js');
|
|
24
|
+
const gradingDb = require('../modules/grading/db.js');
|
|
25
|
+
|
|
26
|
+
let passed = 0;
|
|
27
|
+
let failed = 0;
|
|
28
|
+
async function test(name, fn) {
|
|
29
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
30
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Capture every SQL text pool.query sees while `fn` runs; always restore.
|
|
34
|
+
async function captureSql(fn) {
|
|
35
|
+
const captured = [];
|
|
36
|
+
const orig = pool.query;
|
|
37
|
+
pool.query = async (sql) => { captured.push(String(sql)); return { rows: [] }; };
|
|
38
|
+
try { await fn(); } finally { pool.query = orig; }
|
|
39
|
+
return captured;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const PREDICATE_MARKS = [
|
|
43
|
+
"panel_outcome", "'unavailable'",
|
|
44
|
+
"jsonb_typeof", "functional_fidelity", "code_quality",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function assertQualityFiltered(sql, label) {
|
|
48
|
+
for (const mark of PREDICATE_MARKS) {
|
|
49
|
+
assert.ok(sql.includes(mark), `${label}: SQL must carry quality-predicate mark "${mark}"`);
|
|
50
|
+
}
|
|
51
|
+
assert.ok(sql.includes('FILTER (WHERE'), `${label}: aggregates must FILTER on the predicate`);
|
|
52
|
+
assert.ok(sql.includes('n_unavailable'), `${label}: must bucket excluded rows as n_unavailable`);
|
|
53
|
+
// NULL-safety: every jsonb_typeof operand must be COALESCE-wrapped, or NOT
|
|
54
|
+
// (predicate) goes NULL on legacy rows and drops them from n_unavailable.
|
|
55
|
+
const typeofCount = (sql.match(/jsonb_typeof/g) || []).length;
|
|
56
|
+
const guardedCount = (sql.match(/COALESCE\(jsonb_typeof/g) || []).length;
|
|
57
|
+
assert.equal(guardedCount, typeofCount, `${label}: every jsonb_typeof must be COALESCE-guarded`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await test('gradeQualityWhere: parenthesized, alias-qualified, NULL-safe on all three operands', () => {
|
|
61
|
+
const bare = db.gradeQualityWhere();
|
|
62
|
+
const aliased = db.gradeQualityWhere('g');
|
|
63
|
+
assert.ok(bare.startsWith('(') && bare.trimEnd().endsWith(')'), 'must parenthesize (composes under NOT/AND)');
|
|
64
|
+
assert.ok(aliased.includes('g.signals') && aliased.includes('g.rubric_scores'), 'alias must qualify both columns');
|
|
65
|
+
assert.ok(!bare.includes('g.'), 'bare form must not smuggle an alias');
|
|
66
|
+
assert.ok(bare.includes("COALESCE(") && bare.includes("panel_outcome"), 'panel_outcome operand must be COALESCE-guarded');
|
|
67
|
+
assert.equal((bare.match(/COALESCE\(jsonb_typeof/g) || []).length, 2, 'both rubric axes COALESCE-guarded');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await test('rollingAvgGrade excludes outage/null-axis rows and buckets n_unavailable', async () => {
|
|
71
|
+
const sql = (await captureSql(() => db.rollingAvgGrade(7)))[0];
|
|
72
|
+
assertQualityFiltered(sql, 'rollingAvgGrade');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await test('gradeByBuilder: quality-filtered, and pass_rate is NULL (not 0.000) with no quality rows', async () => {
|
|
76
|
+
const sql = (await captureSql(() => db.gradeByBuilder(30)))[0];
|
|
77
|
+
assertQualityFiltered(sql, 'gradeByBuilder');
|
|
78
|
+
// The old shape ROUND(COALESCE(AVG(...),0),3) fabricated a 0.000 fail-streak
|
|
79
|
+
// for an all-outage window — the lars589 symptom. COALESCE must be gone from
|
|
80
|
+
// the pass_rate expression so it reads NULL ("no graded work") instead.
|
|
81
|
+
assert.ok(!sql.includes('ROUND(COALESCE(AVG'), 'pass_rate must not coalesce an empty window to 0');
|
|
82
|
+
assert.ok(/AVG\(CASE WHEN g\.passed THEN 1 ELSE 0 END\) FILTER/.test(sql), 'pass_rate must average quality rows only');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await test('gradeTrendByDay keeps outage-only days as n=0 + n_unavailable, never a 0.0-score day', async () => {
|
|
86
|
+
const sql = (await captureSql(() => db.gradeTrendByDay(30)))[0];
|
|
87
|
+
assertQualityFiltered(sql, 'gradeTrendByDay');
|
|
88
|
+
assert.ok(!/WHERE[^G]*unavailable[^G]*GROUP BY/s.test(sql.replace(/FILTER \(WHERE[^)]+\)+/g, '')),
|
|
89
|
+
'exclusion must be FILTER-based (top-level WHERE would drop outage-only days from the series)');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await test('recentGradesForBuilder labels rather than hides: selects panel_outcome, no FILTER', async () => {
|
|
93
|
+
const sql = (await captureSql(() => db.recentGradesForBuilder(1, 5)))[0];
|
|
94
|
+
assert.ok(sql.includes("signals->>'panel_outcome'"), 'must select panel_outcome for display labeling');
|
|
95
|
+
assert.ok(!sql.includes('FILTER (WHERE'), 'the list read stays unfiltered — labeling, not hiding');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await test('builderAnalytics grade-trend query is quality-filtered (feeds headline.grade_pass_rate)', async () => {
|
|
99
|
+
const sqls = await captureSql(() => db.builderAnalytics(1, 90));
|
|
100
|
+
const gradeSql = sqls.find((s) => s.includes('task_grades') && s.includes('graded_at'));
|
|
101
|
+
assert.ok(gradeSql, 'builderAnalytics must query task_grades');
|
|
102
|
+
for (const mark of PREDICATE_MARKS) {
|
|
103
|
+
assert.ok(gradeSql.includes(mark), `builderAnalytics grade query must carry "${mark}"`);
|
|
104
|
+
}
|
|
105
|
+
assert.ok(gradeSql.includes('FILTER (WHERE'), 'builderAnalytics grade query must FILTER');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await test('grading/db builderRollingAvgGrade (the documented twin) carries the same predicate', async () => {
|
|
109
|
+
const sql = (await captureSql(() => gradingDb.builderRollingAvgGrade(1, 14)))[0];
|
|
110
|
+
assertQualityFiltered(sql, 'builderRollingAvgGrade');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
114
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -141,6 +141,93 @@ await test('route-rank hard-fail (forged routes dir) flips a client PASS to FAIL
|
|
|
141
141
|
}
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
+
// ========================================================================
|
|
145
|
+
// Guard 1b — vacuous permission-path input fails CLOSED (task 1002646 /
|
|
146
|
+
// report 1000021 G-1). The pre-pass pairs the server-known rank with
|
|
147
|
+
// CLIENT-supplied committed_files, so omitting the list (or sending []) made
|
|
148
|
+
// the sub-floor protected-path check pass vacuously — the exact bypass the
|
|
149
|
+
// report demonstrated. A sub-floor caller submitting NO file list now fails
|
|
150
|
+
// closed; honest ships always send the real list (task 878).
|
|
151
|
+
// ========================================================================
|
|
152
|
+
|
|
153
|
+
const ppc = require('../src/bongos/permission-path-check.js');
|
|
154
|
+
const REGISTRY_FLOOR = Object.keys(ppc.RANK_TIER).find((r) => ppc.RANK_TIER[r] === ppc.MIN_TIER);
|
|
155
|
+
|
|
156
|
+
await test('1002646 sub-floor PASS with committed_files ABSENT → hard FAIL (omission no longer bypasses the floor)', () => {
|
|
157
|
+
const g = grader.serverSideGuards(
|
|
158
|
+
clientGrade({ issues: [{ severity: 'nit', file: 'a', summary: 'x' }] }),
|
|
159
|
+
{ committedFiles: null, builderRank: 'thetes', diffStats: { files_changed: 1, lines_added: 3, lines_removed: 1 } }
|
|
160
|
+
);
|
|
161
|
+
assert.equal(g.passed, false, 'omitting committed_files must not make the protected-path floor vacuous');
|
|
162
|
+
assert.equal(g.signals.server_guard.downgraded_to, 'fail');
|
|
163
|
+
assert.equal(g.signals.confidence, 0, 'fail forwards confidence 0 so the server gate also fails');
|
|
164
|
+
assert.ok(g.signals.server_guard.tripped.some((t) => t.guard === 'permission_path_unprovable'));
|
|
165
|
+
assert.ok(g.issues.some((i) => i.worker === 'server-permission-path'));
|
|
166
|
+
assert.equal(g.signals.server_guard.authorization_refusal, true,
|
|
167
|
+
'the unprovable trip is the same rank wall as permission_path — not a quality verdict (task 1002614 shape)');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await test('1002646 sub-floor PASS with committed_files [] → hard FAIL (empty list is the same omission)', () => {
|
|
171
|
+
const g = grader.serverSideGuards(
|
|
172
|
+
clientGrade({ issues: [{ severity: 'nit', file: 'a', summary: 'x' }] }),
|
|
173
|
+
{ committedFiles: [], builderRank: 'xenos', diffStats: { files_changed: 1, lines_added: 3, lines_removed: 1 } }
|
|
174
|
+
);
|
|
175
|
+
assert.equal(g.passed, false);
|
|
176
|
+
assert.equal(g.signals.server_guard.downgraded_to, 'fail');
|
|
177
|
+
assert.ok(g.signals.server_guard.tripped.some((t) => t.guard === 'permission_path_unprovable'));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await test('1002646 whitespace-only entries do not count as a submitted list → hard FAIL', () => {
|
|
181
|
+
const g = grader.serverSideGuards(
|
|
182
|
+
clientGrade({ issues: [{ severity: 'nit', file: 'a', summary: 'x' }] }),
|
|
183
|
+
{ committedFiles: [' ', ''], builderRank: 'thetes', diffStats: { files_changed: 1, lines_added: 3, lines_removed: 1 } }
|
|
184
|
+
);
|
|
185
|
+
assert.equal(g.passed, false, 'padding the list with blanks is still an empty list');
|
|
186
|
+
assert.ok(g.signals.server_guard.tripped.some((t) => t.guard === 'permission_path_unprovable'));
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await test('1002646 the refusal names the registry-resolved floor + the remedy (wave-5 speech standard)', () => {
|
|
190
|
+
const g = grader.serverSideGuards(
|
|
191
|
+
clientGrade({ issues: [{ severity: 'nit', file: 'a', summary: 'x' }] }),
|
|
192
|
+
{ committedFiles: null, builderRank: 'xenos', diffStats: { files_changed: 1, lines_added: 3, lines_removed: 1 } }
|
|
193
|
+
);
|
|
194
|
+
const trip = g.signals.server_guard.tripped.find((t) => t.guard === 'permission_path_unprovable');
|
|
195
|
+
assert.ok(trip, 'the guard must be recorded');
|
|
196
|
+
assert.ok(trip.summary.includes(`'${REGISTRY_FLOOR}'`),
|
|
197
|
+
'the floor name comes from the registry at runtime, never a hardcoded rank string (D-16/N-3)');
|
|
198
|
+
assert.match(trip.summary, /re-run the ship/i, 'carries the remedy');
|
|
199
|
+
assert.match(trip.summary, /\/tasks\/:id\/confirm/, 'names the file-less-change exit too');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await test('1002646 a floor-clearing rank with an absent list is NOT blocked (guard scoped to the floor)', () => {
|
|
203
|
+
const g = grader.serverSideGuards(clientGrade(), {
|
|
204
|
+
committedFiles: null, builderRank: 'metic',
|
|
205
|
+
diffStats: { files_changed: 1, lines_added: 2, lines_removed: 1 },
|
|
206
|
+
});
|
|
207
|
+
assert.equal(g.passed, true, 'the floor does not apply to metic+ — no new refusal for them (ADR 0162)');
|
|
208
|
+
assert.equal(g.signals.server_guard.ok, true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await test('1002646 an honest sub-floor ship with a real, unprotected list still passes (no new gate on honest work)', () => {
|
|
212
|
+
const g = grader.serverSideGuards(
|
|
213
|
+
clientGrade({ issues: [{ severity: 'nit', file: 'docs/notes.md', summary: 'x' }] }),
|
|
214
|
+
{ committedFiles: ['docs/notes.md'], builderRank: 'thetes', diffStats: { files_changed: 1, lines_added: 5, lines_removed: 1 } }
|
|
215
|
+
);
|
|
216
|
+
assert.equal(g.passed, true,
|
|
217
|
+
'a real submitted list keeps working; the dishonest-LIST residual is owned by gate-review CI + the post-push main audit');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await test('1002646 downgrade-only: a sub-floor client FAIL with an absent list stays FAIL (no behavior change)', () => {
|
|
221
|
+
const failGrade = grader.validateAndScore(
|
|
222
|
+
{ functional_fidelity: 3, code_quality: 3, confidence: 1, issues: [{ severity: 'blocker', file: 'a', summary: 'broken' }] },
|
|
223
|
+
{}
|
|
224
|
+
);
|
|
225
|
+
const g = grader.serverSideGuards(failGrade, {
|
|
226
|
+
committedFiles: null, builderRank: 'xenos', diffStats: { files_changed: 1, lines_added: 1, lines_removed: 1 },
|
|
227
|
+
});
|
|
228
|
+
assert.equal(g.passed, false, 'still failed — the guard only ever downgrades');
|
|
229
|
+
});
|
|
230
|
+
|
|
144
231
|
// ========================================================================
|
|
145
232
|
// Guard 2 — implausible empty-issues PASS on a non-trivial diff → manual-confirm.
|
|
146
233
|
// ========================================================================
|