@bongos/core 1.19.1032 → 1.19.1034
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 +322 -117
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/grade-audit/SKILL.md +1 -1
- package/.claude/skills/grade-recover/SKILL.md +2 -1
- package/.claude/skills/grader-health/SKILL.md +5 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -0
- package/docs/adr/0175-the-board-room-full-idea-ratification.md +3 -2
- package/docs/adr/0178-the-copy-desk-flag-and-queue.md +3 -3
- package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
- package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +1 -1
- package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +1 -1
- package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +11 -3
- package/docs/adr/README.md +4 -4
- package/docs/api/openapi.json +1 -1
- package/docs/architecture.md +4 -3
- package/docs/canonical-permissions.md +2 -2
- package/docs/copy-inventory.md +34 -30
- package/docs/copy-registry.json +69 -33
- package/docs/file-map.md +4 -3
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/autobongos-windows-host.md +3 -1
- package/docs/recipes/local-dev.md +45 -3
- package/docs/recipes/packaging-the-core.md +3 -1
- package/docs/recipes/redteam-patrol.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/cadence.js +52 -0
- package/modules/autonomy/gauge.js +57 -9
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/grading/grade-engine.js +267 -0
- package/modules/grading/grader-subagent.js +4 -3
- package/modules/grading/grader-workers/index.js +67 -11
- package/modules/grading/grader-workers/worker-verdict.js +32 -2
- package/modules/grading/grader.js +109 -88
- package/modules/hall-ui/public/deploy.js +35 -0
- package/modules/hall-ui/public/project-settings.css +7 -0
- package/modules/hall-ui/records/builders-roster.md +10 -0
- package/modules/hall-ui/records/catalog.md +10 -0
- package/modules/hall-ui/records/collaboration.md +14 -0
- package/modules/hall-ui/records/goal-dependency-forest.md +11 -0
- package/modules/hall-ui/records/oversight-family.md +19 -0
- package/modules/hall-ui/records/panel-family.md +19 -0
- package/modules/hall-ui/records/reading-rooms.md +12 -0
- package/modules/hall-ui/records/record-family.md +13 -0
- package/modules/hall-ui/records/reference-room.md +9 -0
- package/modules/hall-ui/records/roster-and-ledger.md +9 -0
- package/modules/hall-ui/records/sky.md +23 -0
- package/modules/lifecycle/dependency-advisory.js +2 -1
- package/modules/lifecycle/routes/tasks.js +19 -4
- package/modules/lifecycle/ship-preflight.js +91 -0
- package/modules/provisioning/control-plane-guard.js +14 -13
- package/modules/provisioning/core-upgrade.js +30 -17
- package/modules/provisioning/public-refusal.js +44 -0
- package/modules/provisioning/routes/disconnect.js +2 -1
- package/modules/provisioning/routes/env-manifest.js +5 -4
- package/modules/provisioning/routes/provisioning.js +16 -15
- package/modules/provisioning/routes/repo-private.js +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +7481 -0
- package/scripts/gds/address-park-sweep.sh +3 -1
- package/scripts/gds/autobongos-run.js +91 -3
- package/scripts/gds/backfill-cost.js +20 -9
- package/scripts/gds/baseline-staleness.js +1 -1
- package/scripts/gds/claim.js +2 -2
- package/scripts/gds/credential-presence.js +281 -0
- package/scripts/gds/db-backup-nightly.sh +9 -4
- package/scripts/gds/db-backup-offsite.sh +4 -2
- package/scripts/gds/deploy-main-audit-gate.sh +11 -4
- package/scripts/gds/dev-lib.js +212 -5
- package/scripts/gds/dev.js +102 -50
- package/scripts/gds/doctor.js +3 -2
- package/scripts/gds/grade-correlation-audit.js +280 -0
- package/scripts/gds/grade-replay.js +237 -0
- package/scripts/gds/hooks/pre-push.js +7 -3
- package/scripts/gds/main-audit.js +19 -10
- package/scripts/gds/memory-map.js +3 -1
- package/scripts/gds/onboarding-config.js +3 -2
- package/scripts/gds/package-core.js +72 -1
- package/scripts/gds/plain-cards.js +4 -0
- package/scripts/gds/preflight.js +12 -4
- package/scripts/gds/provision-core-upgrade.js +282 -80
- package/scripts/gds/provision-repo-auth.js +15 -0
- package/scripts/gds/provision.js +1 -1
- package/scripts/gds/publish-credential-check.js +3 -0
- package/scripts/gds/push-main.js +14 -3
- package/scripts/gds/push-path-brief.js +3 -2
- package/scripts/gds/redteam-patrol.js +4 -4
- package/scripts/gds/release-notes.js +271 -0
- package/scripts/gds/run-routine.js +18 -5
- package/scripts/gds/run-unit-tests.js +14 -3
- package/scripts/gds/seed-board-room-tasks.js +1 -1
- package/scripts/gds/session-cost.js +12 -3
- package/scripts/gds/ship-deploy-target.js +11 -6
- package/scripts/gds/ship-flow.js +5 -3
- package/scripts/gds/ship-grade-steps.js +33 -0
- package/scripts/gds/ship-land.js +241 -16
- package/scripts/gds/ship-merge.js +3 -2
- package/scripts/gds/ship-preflight-steps.js +86 -11
- package/scripts/gds/ship.js +1 -1
- package/scripts/gds/stray-unit-sweep.sh +3 -1
- package/scripts/gds/upgrade-migrate.js +67 -0
- package/scripts/gds/upgrade.js +9 -7
- package/scripts/hall-preview/README.md +20 -3
- package/scripts/hall-preview/live-identity.js +1 -1
- package/scripts/hall-preview/refresh-fixtures.js +71 -11
- package/scripts/hall-preview/scrub-identity.js +296 -0
- package/src/bongos/routes/security.js +3 -3
- package/src/build-info.js +4 -2
- package/src/module-api.js +1 -1
- package/tests/adr_0089_scope.mjs +70 -0
- package/tests/adr_0162_citation.mjs +184 -0
- package/tests/autobongos_cadence.mjs +74 -0
- package/tests/autobongos_loop.mjs +114 -0
- package/tests/autonomy_gauge.mjs +56 -0
- package/tests/consumer_layout_boot.mjs +363 -31
- package/tests/core_upgrade_door.mjs +40 -6
- package/tests/core_upgrade_runner.mjs +329 -5
- package/tests/credential_presence.mjs +153 -0
- package/tests/db_backup_nightly.mjs +21 -3
- package/tests/db_backup_offsite.mjs +81 -0
- package/tests/deploy_audit_gate.mjs +5 -2
- package/tests/deploy_page_projects.mjs +63 -2
- package/tests/dev_lib.mjs +2 -1
- package/tests/dev_windows.mjs +324 -0
- package/tests/env_knobs_neutral_prefix.mjs +203 -0
- package/tests/fixtures/grade-replay/recorded-panel-grades.json +27 -0
- package/tests/government_protected_surfaces.mjs +10 -0
- package/tests/grade_correlation_audit.mjs +159 -0
- package/tests/grade_engine.mjs +249 -0
- package/tests/grade_engine_replay.mjs +99 -0
- package/tests/grader_prompt_window_bound.mjs +132 -0
- package/tests/grader_unbacked_score.mjs +194 -0
- package/tests/hall_preview_scrub.mjs +214 -0
- package/tests/hall_records.mjs +85 -0
- package/tests/helpers.mjs +25 -0
- package/tests/instance_db_migrated_on_code_move.mjs +8 -1
- package/tests/integration_parked_ratchet.mjs +24 -1
- package/tests/plain_cards.mjs +1 -1
- package/tests/provisioning_public_refusal.mjs +195 -0
- package/tests/publish_status_branch.mjs +128 -0
- package/tests/redteam_patrol.mjs +1 -1
- package/tests/release_notes.mjs +327 -0
- package/tests/shell_stat_portability.mjs +49 -0
- package/tests/ship_land_conflict.mjs +181 -0
- package/tests/ship_land_red_pr.mjs +236 -0
- package/tests/ship_preflight.mjs +226 -3
- package/tests/start_env_guard.mjs +16 -0
- package/tests/sweep_app_account.mjs +15 -2
- package/tests/upgrade.mjs +20 -1
- package/tests/upgrade_migrate.mjs +195 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// scripts/gds/grade-correlation-audit.js — do the grader's panel workers fail
|
|
5
|
+
// DIFFERENTLY, or are we paying four times for two votes? (task 1002843,
|
|
6
|
+
// ADR 0089 §5 — the structural answer to "Nine Judges, Two Effective Votes").
|
|
7
|
+
//
|
|
8
|
+
// WHY. Aggregation cannot manufacture independence a panel does not have. ADR
|
|
9
|
+
// 0089 therefore makes panel composition a measured choice, and nothing measured
|
|
10
|
+
// it: /grade-audit and /grader-health are qualitative sweeps with no inter-judge
|
|
11
|
+
// statistics. This reads PERSISTED grades — never re-grades, never spawns a
|
|
12
|
+
// model, never writes anything — and reports:
|
|
13
|
+
// (a) pairwise agreement between the panel workers on the same task: raw
|
|
14
|
+
// agreement, Cohen's kappa (agreement corrected for chance) and the phi
|
|
15
|
+
// correlation of their objections;
|
|
16
|
+
// (b) each worker's Cohen's kappa against the OWNER'S decisions;
|
|
17
|
+
// (c) which workers look redundant with another and which carry independent
|
|
18
|
+
// signal, plus the panel's effective number of independent votes.
|
|
19
|
+
//
|
|
20
|
+
// A VOTE is binary: a worker OBJECTS (verdict concerns or fail) or CLEARS (pass).
|
|
21
|
+
// A worker that errored without a finding produced no signal and casts no vote
|
|
22
|
+
// on that task — the same missing-signal rule the gate applies (#473). The
|
|
23
|
+
// deterministic pseudo-judges and the non-gating reader are not voters.
|
|
24
|
+
//
|
|
25
|
+
// THE OWNER'S DECISIONS — the only labels used, per the task:
|
|
26
|
+
// accept an approved override request, or a manual confirm of work whose
|
|
27
|
+
// stored grade did not pass (task confirmed/shipped anyway);
|
|
28
|
+
// reject a denied override request.
|
|
29
|
+
// A passing grade that auto-landed is NOT a label: that is the grader agreeing
|
|
30
|
+
// with itself, and counting it would inflate every worker's kappa. The labelled
|
|
31
|
+
// set is therefore a selected one — tasks the panel did not pass, plus the ones
|
|
32
|
+
// someone argued with — and the report says so.
|
|
33
|
+
//
|
|
34
|
+
// THE MINIMUM-N GUARD. Every statistic is REFUSED, with the reason, below
|
|
35
|
+
// MIN_N observations or when either side has fewer than MIN_PER_CLASS of a
|
|
36
|
+
// class (a kappa over a constant rater is undefined, and one over five labels is
|
|
37
|
+
// noise). --min-n may raise the bar, never lower it. Report-only: no gate reads
|
|
38
|
+
// this, and nothing changes status (ADR 0162).
|
|
39
|
+
//
|
|
40
|
+
// Usage:
|
|
41
|
+
// node scripts/gds/grade-correlation-audit.js [--recent N] [--min-n N] [--json]
|
|
42
|
+
// pull the N most recently finished tasks' grades (default 400) + the
|
|
43
|
+
// decided override requests (Metic+ read) through the API
|
|
44
|
+
// ... --save <file> also write the pulled dataset, to re-run offline
|
|
45
|
+
// ... --file <file> audit a saved dataset instead of the live API
|
|
46
|
+
|
|
47
|
+
const fs = require('node:fs');
|
|
48
|
+
const { arg, hasFlag, cliClient } = require('./cli-lib');
|
|
49
|
+
const { fetchRecentGrades, trimRecordedGrade } = require('./grade-replay');
|
|
50
|
+
|
|
51
|
+
const PANEL = ['narc', 'quality', 'hacker', 'efficiency'];
|
|
52
|
+
const MIN_N = 30;
|
|
53
|
+
const MIN_PER_CLASS = 5;
|
|
54
|
+
const REDUNDANT_KAPPA = 0.8;
|
|
55
|
+
const INDEPENDENT_KAPPA = 0.4;
|
|
56
|
+
|
|
57
|
+
// A worker's vote on one grade: 1 objects, 0 clears, null no signal.
|
|
58
|
+
function voteOf(w) {
|
|
59
|
+
if (!w || typeof w !== 'object') return null;
|
|
60
|
+
const issues = Array.isArray(w.issues) ? w.issues : [];
|
|
61
|
+
const substantive = issues.some((i) => i && (i.severity === 'blocker' || i.severity === 'major'));
|
|
62
|
+
if (w.error && !substantive) return null;
|
|
63
|
+
if (w.verdict === 'pass') return 0;
|
|
64
|
+
if (w.verdict === 'concerns' || w.verdict === 'fail') return 1;
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// { kind -> vote } for the panel workers of one grade.
|
|
69
|
+
function panelVotes(workers) {
|
|
70
|
+
const out = {};
|
|
71
|
+
for (const w of Array.isArray(workers) ? workers : []) {
|
|
72
|
+
if (!w || !PANEL.includes(w.kind)) continue;
|
|
73
|
+
const v = voteOf(w);
|
|
74
|
+
if (v != null) out[w.kind] = v;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Why a binary series cannot carry a statistic, or null when it can.
|
|
80
|
+
function refusal(pairs, minN) {
|
|
81
|
+
if (pairs.length < minN) return `n=${pairs.length} is below the minimum of ${minN}`;
|
|
82
|
+
for (const side of [0, 1]) {
|
|
83
|
+
const ones = pairs.filter((p) => p[side] === 1).length;
|
|
84
|
+
const zeros = pairs.length - ones;
|
|
85
|
+
if (Math.min(ones, zeros) < MIN_PER_CLASS) {
|
|
86
|
+
return `${side === 0 ? 'the first' : 'the second'} rater has only ${Math.min(ones, zeros)} of one class (need ${MIN_PER_CLASS}+ of each)`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Cohen's kappa + raw agreement + phi for [[a, b], ...] binary pairs. Pure; the
|
|
93
|
+
// caller has already applied the minimum-N guard.
|
|
94
|
+
function agreementStats(pairs) {
|
|
95
|
+
const n = pairs.length;
|
|
96
|
+
let n11 = 0; let n10 = 0; let n01 = 0; let n00 = 0;
|
|
97
|
+
for (const [a, b] of pairs) {
|
|
98
|
+
if (a === 1 && b === 1) n11++; else if (a === 1) n10++; else if (b === 1) n01++; else n00++;
|
|
99
|
+
}
|
|
100
|
+
const po = (n11 + n00) / n;
|
|
101
|
+
const pa = (n11 + n10) / n;
|
|
102
|
+
const pb = (n11 + n01) / n;
|
|
103
|
+
const pe = pa * pb + (1 - pa) * (1 - pb);
|
|
104
|
+
const kappa = pe === 1 ? null : (po - pe) / (1 - pe);
|
|
105
|
+
const den = Math.sqrt((n11 + n10) * (n01 + n00) * (n11 + n01) * (n10 + n00));
|
|
106
|
+
const phi = den === 0 ? null : (n11 * n00 - n10 * n01) / den;
|
|
107
|
+
return { n, agreement: po, kappa, phi, table: { both_object: n11, only_first: n10, only_second: n01, both_clear: n00 } };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function guardedStats(pairs, minN) {
|
|
111
|
+
const why = refusal(pairs, minN);
|
|
112
|
+
return why ? { n: pairs.length, refused: why } : agreementStats(pairs);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Effective number of independent votes for k raters whose mean pairwise
|
|
116
|
+
// correlation is rho (the design-effect formula). A negative mean is clamped to
|
|
117
|
+
// 0 — this is a ceiling on independence, not a claim of anti-correlation.
|
|
118
|
+
function effectiveVotes(k, rho) {
|
|
119
|
+
const r = Math.max(0, rho);
|
|
120
|
+
return k / (1 + (k - 1) * r);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The owner's decision per task id, from the override ledger and the tasks'
|
|
124
|
+
// fates. Returns Map(task_id -> { label: 'accept'|'reject', source }). An
|
|
125
|
+
// approval outranks a denial on the same task: it was the later decision.
|
|
126
|
+
function ownerLabels({ grades, overrides }) {
|
|
127
|
+
const labels = new Map();
|
|
128
|
+
for (const o of overrides || []) {
|
|
129
|
+
if (o.status === 'approved') labels.set(String(o.task_id), { label: 'accept', source: 'override_approved' });
|
|
130
|
+
else if (o.status === 'denied' && !labels.has(String(o.task_id))) labels.set(String(o.task_id), { label: 'reject', source: 'override_denied' });
|
|
131
|
+
}
|
|
132
|
+
for (const g of grades || []) {
|
|
133
|
+
const id = String(g.task_id);
|
|
134
|
+
if (labels.has(id)) continue;
|
|
135
|
+
const landed = g.task_status === 'confirmed' || g.task_status === 'shipped';
|
|
136
|
+
if (landed && g.passed === false) labels.set(id, { label: 'accept', source: 'manual_confirm_after_fail' });
|
|
137
|
+
}
|
|
138
|
+
return labels;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function classify(kappa) {
|
|
142
|
+
if (kappa == null) return 'undetermined';
|
|
143
|
+
if (kappa >= REDUNDANT_KAPPA) return 'redundant';
|
|
144
|
+
if (kappa >= INDEPENDENT_KAPPA) return 'overlapping';
|
|
145
|
+
return 'independent';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// One stored grade → the slim row the audit reads (review prose hashed, as in
|
|
149
|
+
// the replay corpus): { task_id, task_status, passed, workers }, or null when
|
|
150
|
+
// it is not a panel grade.
|
|
151
|
+
function toAuditRow({ task_id, task_status, grade }) {
|
|
152
|
+
const row = trimRecordedGrade({ task_id, grade });
|
|
153
|
+
return row ? { task_id: row.task_id, task_status: task_status || null, passed: !!(grade && grade.passed === true), workers: row.workers } : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The whole report, pure over a dataset { grades: [toAuditRow…], overrides: [{task_id, status}] }.
|
|
157
|
+
function audit(dataset, { minN = MIN_N } = {}) {
|
|
158
|
+
const floor = Math.max(MIN_N, Number(minN) || MIN_N);
|
|
159
|
+
const panels = (dataset.grades || [])
|
|
160
|
+
.map((g) => ({ task_id: String(g.task_id), votes: panelVotes(g.workers) }))
|
|
161
|
+
.filter((p) => Object.keys(p.votes).length > 0);
|
|
162
|
+
const labels = ownerLabels(dataset);
|
|
163
|
+
// 1 when the worker's vote disagrees with the owner (objected to accepted
|
|
164
|
+
// work, or cleared rejected work); only on labelled tasks it voted on.
|
|
165
|
+
const errorOf = (p, kind) => (labels.has(p.task_id) && kind in p.votes
|
|
166
|
+
? Number(p.votes[kind] !== (labels.get(p.task_id).label === 'reject' ? 1 : 0))
|
|
167
|
+
: null);
|
|
168
|
+
|
|
169
|
+
const pairwise = [];
|
|
170
|
+
for (let i = 0; i < PANEL.length; i++) {
|
|
171
|
+
for (let j = i + 1; j < PANEL.length; j++) {
|
|
172
|
+
const a = PANEL[i]; const b = PANEL[j];
|
|
173
|
+
const pairs = panels.filter((p) => a in p.votes && b in p.votes).map((p) => [p.votes[a], p.votes[b]]);
|
|
174
|
+
const stats = guardedStats(pairs, floor);
|
|
175
|
+
// Error correlation: do the two get the SAME tasks wrong against the owner?
|
|
176
|
+
const errPairs = panels.map((p) => [errorOf(p, a), errorOf(p, b)]).filter(([x, y]) => x != null && y != null);
|
|
177
|
+
pairwise.push({ a, b, ...stats, reading: stats.refused ? 'undetermined' : classify(stats.kappa), errors: guardedStats(errPairs, floor) });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// The panel's effective vote count is a claim about ALL of it, so it needs
|
|
181
|
+
// every pair — one computable pair extrapolated to four workers would be the
|
|
182
|
+
// over-reading this guard exists to stop.
|
|
183
|
+
const phis = pairwise.filter((p) => !p.refused && p.phi != null).map((p) => p.phi);
|
|
184
|
+
const meanPhi = phis.length === pairwise.length ? phis.reduce((s, x) => s + x, 0) / phis.length : null;
|
|
185
|
+
|
|
186
|
+
const byLabel = { accept: 0, reject: 0 };
|
|
187
|
+
for (const v of labels.values()) byLabel[v.label]++;
|
|
188
|
+
const perWorker = PANEL.map((kind) => {
|
|
189
|
+
const pairs = panels
|
|
190
|
+
.filter((p) => labels.has(p.task_id) && kind in p.votes)
|
|
191
|
+
.map((p) => [p.votes[kind], labels.get(p.task_id).label === 'reject' ? 1 : 0]);
|
|
192
|
+
return { kind, ...guardedStats(pairs, floor) };
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
min_n: floor,
|
|
197
|
+
panels: panels.length,
|
|
198
|
+
pairwise,
|
|
199
|
+
mean_pairwise_phi: meanPhi,
|
|
200
|
+
effective_votes: meanPhi == null ? null : effectiveVotes(PANEL.length, meanPhi),
|
|
201
|
+
labels: { total: labels.size, ...byLabel },
|
|
202
|
+
vs_owner: perWorker,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------- the CLI ----------
|
|
207
|
+
|
|
208
|
+
async function fetchDataset(recent) {
|
|
209
|
+
const client = await cliClient();
|
|
210
|
+
const grades = (await fetchRecentGrades({ recent, client })).map(toAuditRow).filter(Boolean);
|
|
211
|
+
const overrides = [];
|
|
212
|
+
for (const status of ['approved', 'denied']) {
|
|
213
|
+
const r = await client.overrideRequests.getOverrideRequests({ query: { status } });
|
|
214
|
+
if (!r.ok) throw new Error(`GET /override-requests?status=${status} → HTTP ${r.status} (a Metic+ read)`);
|
|
215
|
+
for (const o of (r.data && r.data.requests) || []) overrides.push({ task_id: String(o.task_id), status: o.status, decided_at: o.decided_at || null });
|
|
216
|
+
}
|
|
217
|
+
return { pulled_at: new Date().toISOString(), grades, overrides };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const pct = (x) => `${(x * 100).toFixed(1)}%`;
|
|
221
|
+
const num = (x) => (x == null ? 'n/a' : x.toFixed(2));
|
|
222
|
+
const statLine = (s) => (s.refused ? `REFUSED — ${s.refused}` : `n=${s.n} agree ${pct(s.agreement)} kappa ${num(s.kappa)} phi ${num(s.phi)}`);
|
|
223
|
+
|
|
224
|
+
function printReport(r) {
|
|
225
|
+
const out = [];
|
|
226
|
+
out.push(`grade correlation audit — ${r.panels} recorded panel grade(s), minimum n = ${r.min_n}`);
|
|
227
|
+
out.push('A vote is OBJECTS (concerns/fail) or CLEARS (pass); an errored worker with no finding casts none.');
|
|
228
|
+
out.push('');
|
|
229
|
+
out.push('(a) Do the workers fail differently? — agreement between each pair on the same task');
|
|
230
|
+
for (const p of r.pairwise) {
|
|
231
|
+
out.push(` ${`${p.a} × ${p.b}`.padEnd(22)} ${statLine(p)}${p.refused ? '' : ` → ${p.reading}`}`);
|
|
232
|
+
out.push(` ${''.padEnd(22)} same tasks wrong vs the owner: ${statLine(p.errors)}`);
|
|
233
|
+
}
|
|
234
|
+
const cleared = r.pairwise.filter((p) => !p.refused).length;
|
|
235
|
+
out.push(r.effective_votes == null
|
|
236
|
+
? ` effective independent votes: not computable — needs all ${r.pairwise.length} pairs, ${cleared} cleared the minimum`
|
|
237
|
+
: ` mean pairwise phi ${num(r.mean_pairwise_phi)} → about ${r.effective_votes.toFixed(1)} effective independent vote(s) out of 4`);
|
|
238
|
+
out.push('');
|
|
239
|
+
out.push(`(b) Each worker against the owner's decisions — ${r.labels.total} labelled task(s): ${r.labels.accept} accepted, ${r.labels.reject} rejected`);
|
|
240
|
+
for (const w of r.vs_owner) out.push(` ${w.kind.padEnd(22)} ${statLine(w)}`);
|
|
241
|
+
out.push('');
|
|
242
|
+
const redundant = r.pairwise.filter((p) => p.reading === 'redundant').map((p) => `${p.a}+${p.b}`);
|
|
243
|
+
const independent = r.pairwise.filter((p) => p.reading === 'independent').map((p) => `${p.a}+${p.b}`);
|
|
244
|
+
out.push('(c) Reading');
|
|
245
|
+
out.push(` redundant pairs (kappa >= 0.8): ${redundant.join(', ') || 'none'}`);
|
|
246
|
+
out.push(` independent pairs (kappa < 0.4): ${independent.join(', ') || 'none'}`);
|
|
247
|
+
out.push('');
|
|
248
|
+
out.push('Caveats: labels exist only where a person decided — override requests and manual confirms of work the');
|
|
249
|
+
out.push('panel did not pass — so they are a selected set, not a sample. A grade that auto-landed on a pass is');
|
|
250
|
+
out.push('never a label (that is the grader agreeing with itself). Only each task\'s LATEST grade keeps per-worker');
|
|
251
|
+
out.push('verdicts (grade_attempts stores pass/score only), so a re-graded task counts once, as its final round.');
|
|
252
|
+
console.log(out.join('\n'));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function main() {
|
|
256
|
+
const file = arg('--file');
|
|
257
|
+
const dataset = file
|
|
258
|
+
? JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
259
|
+
: await fetchDataset(Number(arg('--recent')) || 400);
|
|
260
|
+
const save = arg('--save');
|
|
261
|
+
if (save && !file) fs.writeFileSync(save, `${JSON.stringify(dataset)}\n`);
|
|
262
|
+
const report = audit(dataset, { minN: Number(arg('--min-n')) || MIN_N });
|
|
263
|
+
if (hasFlag('--json')) console.log(JSON.stringify(report, null, 2));
|
|
264
|
+
else printReport(report);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = {
|
|
268
|
+
MIN_N,
|
|
269
|
+
agreementStats,
|
|
270
|
+
audit,
|
|
271
|
+
effectiveVotes,
|
|
272
|
+
ownerLabels,
|
|
273
|
+
panelVotes,
|
|
274
|
+
toAuditRow,
|
|
275
|
+
voteOf,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
if (require.main === module) {
|
|
279
|
+
main().catch((err) => { console.error(`grade-correlation-audit: ${err.message}`); process.exit(2); });
|
|
280
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// scripts/gds/grade-replay.js — the differential replay over RECORDED grades
|
|
5
|
+
// (task 1002839, ADR 0089 §1). Read-only: it never grades, never spawns a model
|
|
6
|
+
// and never writes a row.
|
|
7
|
+
//
|
|
8
|
+
// WHY. How a grade is aggregated and gated IS the live ship gate, so a change to
|
|
9
|
+
// either is only behaviour-preserving if real recorded grades still come out the
|
|
10
|
+
// way production recorded them. Every persisted panel grade carries its judges'
|
|
11
|
+
// verdicts (signals.workers, the deterministic pseudo-judges included), so the
|
|
12
|
+
// grade engine's aggregate + gate stages can be re-run over them at zero cost and
|
|
13
|
+
// compared field by field with what the server stored.
|
|
14
|
+
//
|
|
15
|
+
// WHAT IS COMPARED — the decision, not the prose: panel_outcome, the four verdict
|
|
16
|
+
// counts, the worker count, the panel's own cost, the forwarded confidence (skipped
|
|
17
|
+
// when the server-side guards downgraded it — that number is theirs, not the
|
|
18
|
+
// panel's), and the five rubric axes the server re-records.
|
|
19
|
+
//
|
|
20
|
+
// Usage:
|
|
21
|
+
// node scripts/gds/grade-replay.js replay the committed corpus
|
|
22
|
+
// node scripts/gds/grade-replay.js --file <corpus> replay another corpus file
|
|
23
|
+
// node scripts/gds/grade-replay.js --live [--recent N]
|
|
24
|
+
// pull the N most recently finished tasks' grades from the API (default 150)
|
|
25
|
+
// ... --write-fixture <path> write the AGREEING replays, trimmed (issue text and
|
|
26
|
+
// stderr hashed), one per panel shape — how the test corpus is made
|
|
27
|
+
// ... --json machine-readable summary
|
|
28
|
+
// Exit 0 when every replayed grade agrees, 1 when any does not. A report, not a
|
|
29
|
+
// gate: nothing reads the exit code but the person who ran it.
|
|
30
|
+
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
const path = require('node:path');
|
|
33
|
+
const crypto = require('node:crypto');
|
|
34
|
+
const { arg, hasFlag, cliClient } = require('./cli-lib');
|
|
35
|
+
// Through the module facade: requiring grader.js is what registers the code panel
|
|
36
|
+
// as the engine's default work-type.
|
|
37
|
+
const engine = require('../../modules/grading/grader').gradeEngine;
|
|
38
|
+
|
|
39
|
+
const DEFAULT_CORPUS = path.join(__dirname, '..', '..', 'tests', 'fixtures', 'grade-replay', 'recorded-panel-grades.json');
|
|
40
|
+
const COUNT_FIELDS = ['pass_count', 'concerns_count', 'fail_count', 'error_count', 'worker_count'];
|
|
41
|
+
const RUBRIC_AXES = ['functional_fidelity', 'code_quality', 'economy', 'security_score', 'efficiency_score'];
|
|
42
|
+
const PER_SHAPE = 1;
|
|
43
|
+
|
|
44
|
+
const num = (v) => (v == null || v === '' ? null : Number(v));
|
|
45
|
+
|
|
46
|
+
// The decision a stored grade row records. The rubric axes are the server's
|
|
47
|
+
// re-derivation (validateAndScore) of the panel's medians — equal to them for
|
|
48
|
+
// every grade since the null-axis fixes (tasks 1002653 / 1003318).
|
|
49
|
+
function recordedDecision(grade) {
|
|
50
|
+
const s = (grade && grade.signals) || {};
|
|
51
|
+
const guard = s.server_guard || {};
|
|
52
|
+
const out = { panel_outcome: s.panel_outcome ?? null };
|
|
53
|
+
for (const k of COUNT_FIELDS) out[k] = s[k] ?? null;
|
|
54
|
+
out.panel_cost_usd = num(s.panel_cost_usd);
|
|
55
|
+
out.confidence = guard.downgraded_to ? undefined : num(s.confidence);
|
|
56
|
+
out.rubric = {};
|
|
57
|
+
for (const a of RUBRIC_AXES) out.rubric[a] = num(grade && grade.rubric_scores ? grade.rubric_scores[a] : null);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The same decision, read off a replayed grade-result.
|
|
62
|
+
function replayedDecision(result) {
|
|
63
|
+
const s = (result && result.signals) || {};
|
|
64
|
+
const out = { panel_outcome: s.panel_outcome ?? null };
|
|
65
|
+
for (const k of COUNT_FIELDS) out[k] = s[k] ?? null;
|
|
66
|
+
out.panel_cost_usd = num(s.panel_cost_usd);
|
|
67
|
+
out.confidence = num(s.confidence);
|
|
68
|
+
out.rubric = {};
|
|
69
|
+
for (const a of RUBRIC_AXES) out.rubric[a] = num(result && result.rubric_scores ? result.rubric_scores[a] : null);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const same = (a, b) => a === b || (typeof a === 'number' && typeof b === 'number' && Math.abs(a - b) < 1e-9);
|
|
74
|
+
|
|
75
|
+
// Every field on which a replay disagrees with the record, as
|
|
76
|
+
// { field, recorded, replayed }. An `undefined` recorded value is not compared.
|
|
77
|
+
//
|
|
78
|
+
// One pairing is NOT a disagreement: a rubric axis the panel left null (no judge
|
|
79
|
+
// reported it) that the row stores as 0. That 0 is the server's, not the panel's —
|
|
80
|
+
// validateAndScore coerced a missing axis with Number(null) === 0 until tasks
|
|
81
|
+
// 1002653 / 1003318 made it record null. A real 0/10 median replays as 0, so the
|
|
82
|
+
// pairing can only mean that coercion; any other value against a null still counts.
|
|
83
|
+
function compareDecision(recorded, replayed) {
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const k of ['panel_outcome', ...COUNT_FIELDS, 'panel_cost_usd', 'confidence']) {
|
|
86
|
+
if (recorded[k] === undefined) continue;
|
|
87
|
+
if (!same(recorded[k], replayed[k])) out.push({ field: k, recorded: recorded[k], replayed: replayed[k] });
|
|
88
|
+
}
|
|
89
|
+
for (const a of RUBRIC_AXES) {
|
|
90
|
+
const r = (recorded.rubric || {})[a];
|
|
91
|
+
const p = (replayed.rubric || {})[a];
|
|
92
|
+
if (r === undefined || same(r, p) || (r === 0 && p === null)) continue;
|
|
93
|
+
out.push({ field: `rubric.${a}`, recorded: r, replayed: p });
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const shortHash = (s) => `h:${crypto.createHash('sha256').update(String(s)).digest('hex').slice(0, 10)}`;
|
|
99
|
+
|
|
100
|
+
// A worker, reduced to what aggregate + gate read. Issue summaries, their file
|
|
101
|
+
// paths and stderr are hashed: the gate reads a summary and a file only to
|
|
102
|
+
// de-duplicate (the hash of the trimmed text preserves exactly that), and a
|
|
103
|
+
// fixture should not carry verbatim review prose.
|
|
104
|
+
function trimWorker(w) {
|
|
105
|
+
const t = {
|
|
106
|
+
kind: w.kind,
|
|
107
|
+
verdict: w.verdict,
|
|
108
|
+
error: w.error ?? null,
|
|
109
|
+
scores: w.scores || {},
|
|
110
|
+
confidence: w.confidence ?? null,
|
|
111
|
+
model: w.model ?? null,
|
|
112
|
+
cost_usd: w.cost_usd ?? null,
|
|
113
|
+
retried: !!w.retried,
|
|
114
|
+
issues: (Array.isArray(w.issues) ? w.issues : []).filter((i) => i && typeof i === 'object').map((i) => ({
|
|
115
|
+
severity: i.severity ?? null,
|
|
116
|
+
file: i.file == null ? null : shortHash(i.file),
|
|
117
|
+
summary: shortHash(String(i.summary ?? '').trim()),
|
|
118
|
+
})),
|
|
119
|
+
};
|
|
120
|
+
if (w.exit_code != null) t.exit_code = w.exit_code;
|
|
121
|
+
if (w.stderr_tail) t.stderr_tail = shortHash(w.stderr_tail);
|
|
122
|
+
return t;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// One recorded grade → a corpus row, or null when it is not a panel grade.
|
|
126
|
+
function trimRecordedGrade({ task_id, graded_at = null, grade }) {
|
|
127
|
+
const s = (grade && grade.signals) || {};
|
|
128
|
+
if (!Array.isArray(s.workers) || s.workers.length === 0) return null;
|
|
129
|
+
return { task_id: String(task_id), graded_at, recorded: recordedDecision(grade), workers: s.workers.map(trimWorker) };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The panel's shape — outcome plus each judge's kind/verdict/error/retry — so
|
|
133
|
+
// the fixture keeps variety instead of 200 copies of an all-pass panel.
|
|
134
|
+
function shapeKey(row) {
|
|
135
|
+
return [row.recorded.panel_outcome, ...row.workers.map((w) => `${w.kind}:${w.verdict}${w.error ? `!${w.error}` : ''}${w.retried ? '~r' : ''}`)].join('|');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Replay one corpus row (or one raw recorded grade) through the engine.
|
|
139
|
+
function replayRow(row) {
|
|
140
|
+
const result = engine.replayRecorded({ signals: { workers: row.workers } });
|
|
141
|
+
return { result, mismatches: compareDecision(row.recorded, replayedDecision(result)) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The most recently finished tasks' STORED grades, raw and newest first:
|
|
145
|
+
// [{ task_id, task_status, at, grade }]. Read-only GETs. Shared with the
|
|
146
|
+
// correlation audit (scripts/gds/grade-correlation-audit.js), which needs the
|
|
147
|
+
// task's fate beside its grade. The per-task reads run a few at a time — one
|
|
148
|
+
// after another made a 400-task pull take minutes for no reason.
|
|
149
|
+
const FETCH_CONCURRENCY = 6;
|
|
150
|
+
async function fetchRecentGrades({ recent = 150, statuses = ['shipped', 'completed', 'confirmed'], client = null } = {}) {
|
|
151
|
+
const api = client || await cliClient();
|
|
152
|
+
const tasks = [];
|
|
153
|
+
for (const status of statuses) {
|
|
154
|
+
const r = await api.tasks.getTasks({ query: { status, limit: 1000 } });
|
|
155
|
+
if (!r.ok) throw new Error(`GET /tasks?status=${status} → HTTP ${r.status}`);
|
|
156
|
+
for (const t of r.data.tasks || []) tasks.push({ id: String(t.id), status: t.status || status, at: t.shipped_at || t.updated_at || '' });
|
|
157
|
+
}
|
|
158
|
+
tasks.sort((a, b) => String(b.at).localeCompare(String(a.at)));
|
|
159
|
+
const picked = tasks.slice(0, recent);
|
|
160
|
+
const out = new Array(picked.length).fill(null);
|
|
161
|
+
for (let i = 0; i < picked.length; i += FETCH_CONCURRENCY) {
|
|
162
|
+
await Promise.all(picked.slice(i, i + FETCH_CONCURRENCY).map(async (t, j) => {
|
|
163
|
+
const r = await api.tasks.getTasksId({ id: t.id, query: { include: 'grade' } });
|
|
164
|
+
if (r.ok && r.data && r.data.grade) out[i + j] = { task_id: t.id, task_status: t.status, at: t.at, grade: r.data.grade };
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
return out.filter(Boolean);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function fetchLive(recent) {
|
|
171
|
+
const rows = [];
|
|
172
|
+
for (const g of await fetchRecentGrades({ recent })) {
|
|
173
|
+
const row = trimRecordedGrade({ task_id: g.task_id, graded_at: g.at, grade: g.grade });
|
|
174
|
+
if (row) rows.push(row);
|
|
175
|
+
}
|
|
176
|
+
return rows;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function readCorpus(file) {
|
|
180
|
+
const doc = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
181
|
+
return Array.isArray(doc) ? doc : doc.grades;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function main() {
|
|
185
|
+
const live = hasFlag('--live');
|
|
186
|
+
const rows = live
|
|
187
|
+
? await fetchLive(Number(arg('--recent')) || 150)
|
|
188
|
+
: readCorpus(arg('--file') || DEFAULT_CORPUS);
|
|
189
|
+
const report = rows.map((row) => ({ row, ...replayRow(row) }));
|
|
190
|
+
const disagree = report.filter((r) => r.mismatches.length);
|
|
191
|
+
|
|
192
|
+
const fixturePath = arg('--write-fixture');
|
|
193
|
+
if (fixturePath) {
|
|
194
|
+
const perShape = new Map();
|
|
195
|
+
const keep = [];
|
|
196
|
+
for (const r of report) {
|
|
197
|
+
if (r.mismatches.length) continue;
|
|
198
|
+
const k = shapeKey(r.row);
|
|
199
|
+
const n = perShape.get(k) || 0;
|
|
200
|
+
if (n >= PER_SHAPE) continue;
|
|
201
|
+
perShape.set(k, n + 1);
|
|
202
|
+
keep.push(r.row);
|
|
203
|
+
}
|
|
204
|
+
const note = 'Recorded panel grades for the differential replay (tests/grade_engine_replay.mjs). Generated by scripts/gds/grade-replay.js --live --write-fixture: only grades whose replay agreed with the stored row, one per panel shape. Issue summaries, their files and stderr are hashed.';
|
|
205
|
+
fs.mkdirSync(path.dirname(fixturePath), { recursive: true });
|
|
206
|
+
// One grade per line: small in a diff, and a regenerated corpus diffs by row.
|
|
207
|
+
fs.writeFileSync(fixturePath, `{"note":${JSON.stringify(note)},\n"grades":[\n${keep.map((g) => JSON.stringify(g)).join(',\n')}\n]}\n`);
|
|
208
|
+
console.error(`wrote ${keep.length} grades (${perShape.size} panel shapes) → ${fixturePath}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (hasFlag('--json')) {
|
|
212
|
+
console.log(JSON.stringify({
|
|
213
|
+
replayed: report.length,
|
|
214
|
+
agree: report.length - disagree.length,
|
|
215
|
+
disagree: disagree.map((r) => ({ task_id: r.row.task_id, graded_at: r.row.graded_at, mismatches: r.mismatches })),
|
|
216
|
+
}, null, 2));
|
|
217
|
+
} else {
|
|
218
|
+
console.log(`grade replay: ${report.length} recorded panel grade(s), ${report.length - disagree.length} agree, ${disagree.length} disagree`);
|
|
219
|
+
for (const r of disagree) {
|
|
220
|
+
const what = r.mismatches.map((m) => `${m.field} recorded=${JSON.stringify(m.recorded)} replayed=${JSON.stringify(m.replayed)}`).join('; ');
|
|
221
|
+
console.log(` task ${r.row.task_id}${r.row.graded_at ? ` (${r.row.graded_at})` : ''}: ${what}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
process.exitCode = disagree.length ? 1 : 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
compareDecision,
|
|
229
|
+
fetchRecentGrades,
|
|
230
|
+
replayRow,
|
|
231
|
+
shapeKey,
|
|
232
|
+
trimRecordedGrade,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
if (require.main === module) {
|
|
236
|
+
main().catch((err) => { console.error(`grade-replay: ${err.message}`); process.exit(2); });
|
|
237
|
+
}
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
// 1. NOBODY pushes refs/heads/main directly. Under Option A (ADR 0043) the
|
|
19
19
|
// GDS lands graded work; a builder pushing main bypasses the grader, the
|
|
20
20
|
// confirm gate, parallel-safety, and credits — exactly the #863 incident.
|
|
21
|
-
// Override for the server/operator deploy identity only:
|
|
21
|
+
// Override for the server/operator deploy identity only: <PREFIX>_ALLOW_MAIN_PUSH=1
|
|
22
|
+
// (BONGOS_ on a vanilla instance; the legacy OTB_ALLOW_MAIN_PUSH is still read).
|
|
22
23
|
// 2. A builder BELOW Metic may not push a diff that touches a permission-
|
|
23
24
|
// sensitive path (rank / permission / ship-grade / deploy / migration
|
|
24
25
|
// machinery — see src/bongos/permission-path-check.js).
|
|
@@ -33,6 +34,9 @@ const path = require('node:path');
|
|
|
33
34
|
|
|
34
35
|
const ppc = require(path.join(__dirname, '..', '..', '..', 'src', 'bongos', 'permission-path-check.js'));
|
|
35
36
|
const { apiCall, productName } = require(path.join(__dirname, '..', 'cli-lib.js'));
|
|
37
|
+
// The main-push override resolves through the branded prefix, the same reader
|
|
38
|
+
// push-main.js's canonical spelling is written for (task 1001441).
|
|
39
|
+
const { resolveEnv, envPrefix } = require(path.join(__dirname, '..', '..', '..', 'src', 'instance-config.js'));
|
|
36
40
|
|
|
37
41
|
const ZERO = /^0+$/;
|
|
38
42
|
const MAIN_REF = 'refs/heads/main';
|
|
@@ -122,7 +126,7 @@ async function run() {
|
|
|
122
126
|
// The operator / server deploy identity is the ONLY thing allowed to push
|
|
123
127
|
// main, and it sets this env explicitly. (Belt-and-braces; under full Option A
|
|
124
128
|
// builders never push main at all.)
|
|
125
|
-
const allowMain =
|
|
129
|
+
const allowMain = resolveEnv('ALLOW_MAIN_PUSH') === '1';
|
|
126
130
|
|
|
127
131
|
const stdin = require('node:fs').readFileSync(0, 'utf8');
|
|
128
132
|
const refs = stdin.split('\n').map((l) => l.trim()).filter(Boolean).map((l) => {
|
|
@@ -141,7 +145,7 @@ async function run() {
|
|
|
141
145
|
'A direct push to main skips the grader, the confirm gate, parallel-safety, and',
|
|
142
146
|
'credits — this is exactly the breach SEC #863 closes (ADR 0043).',
|
|
143
147
|
'',
|
|
144
|
-
|
|
148
|
+
`(Operator/deploy identity only: set ${envPrefix()}_ALLOW_MAIN_PUSH=1 to override.)`,
|
|
145
149
|
]);
|
|
146
150
|
}
|
|
147
151
|
}
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
const { execSync } = require('node:child_process');
|
|
51
51
|
const ppc = require(require('node:path').join(__dirname, '..', '..', 'src', 'bongos', 'permission-path-check.js'));
|
|
52
52
|
const { cliClient, cliExit, API_BASE } = require('./cli-lib');
|
|
53
|
+
const { resolveEnv, envPrefix } = require(require('node:path').join(__dirname, '..', '..', 'src', 'instance-config.js'));
|
|
53
54
|
|
|
54
55
|
// ---------- pure analyzer (tested in tests/main_audit.mjs) ----------
|
|
55
56
|
|
|
@@ -314,23 +315,31 @@ function arg(name, fallback = null) {
|
|
|
314
315
|
return i >= 0 ? (process.argv[i + 1] && !process.argv[i + 1].startsWith('--') ? process.argv[i + 1] : true) : fallback;
|
|
315
316
|
}
|
|
316
317
|
|
|
318
|
+
// requestedRange — the range this run was asked to audit, or null for the default
|
|
319
|
+
// window. An explicit --range wins; otherwise the <PREFIX>_AUDIT_RANGE env knob
|
|
320
|
+
// (BONGOS_AUDIT_RANGE on a vanilla instance; the legacy OTB_AUDIT_RANGE is still
|
|
321
|
+
// read, and warned about). The deploy gate and the CI workflow hand the range over
|
|
322
|
+
// as --range, because neither can compute the branded env prefix from a shell
|
|
323
|
+
// (task 1001441).
|
|
324
|
+
function requestedRange(argValue, env = process.env) {
|
|
325
|
+
if (argValue && argValue !== true) return argValue;
|
|
326
|
+
return resolveEnv('AUDIT_RANGE', { env }) || null;
|
|
327
|
+
}
|
|
328
|
+
|
|
317
329
|
async function main() {
|
|
318
330
|
const verifyHead = process.argv.includes('--verify-head');
|
|
319
331
|
const doRevert = process.argv.includes('--revert');
|
|
320
332
|
const strict = process.argv.includes('--strict');
|
|
321
333
|
const useDb = process.argv.includes('--db');
|
|
322
|
-
let range = arg('--range');
|
|
323
334
|
// #863-followup (findings #8/#14): the verify-head window was a fixed
|
|
324
335
|
// HEAD~30..HEAD — a poisoned commit buried under >30 commits (merges inflate
|
|
325
336
|
// the count) or landed before a rebase/force-push could escape it. The deploy
|
|
326
|
-
// gate now passes the EXACT previously-deployed SHA
|
|
327
|
-
// (deploy-main-audit-gate.sh
|
|
328
|
-
//
|
|
337
|
+
// gate now passes the EXACT previously-deployed SHA as --range
|
|
338
|
+
// <prev-deployed-sha>..HEAD (deploy-main-audit-gate.sh), so the gate audits
|
|
339
|
+
// precisely what this deploy introduces — no window to slip under.
|
|
329
340
|
// Fallbacks widen to HEAD~200 (verify-head) / a deeper origin scan otherwise.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
if (!range || range === true) {
|
|
341
|
+
let range = requestedRange(arg('--range'));
|
|
342
|
+
if (!range) {
|
|
334
343
|
const ref = verifyHead ? 'HEAD' : 'origin/main';
|
|
335
344
|
if (!verifyHead) {
|
|
336
345
|
// Default: recent origin/main. Fetch first so we're auditing the truth.
|
|
@@ -344,7 +353,7 @@ async function main() {
|
|
|
344
353
|
console.error(
|
|
345
354
|
`main-audit: DID NOT RUN — cannot build a default range for ${ref}: its first-parent history is `
|
|
346
355
|
+ `${Number.isFinite(depth) ? `${depth} commit(s)` : 'unreadable'} deep. `
|
|
347
|
-
+
|
|
356
|
+
+ `Pass --range <A..B> or set ${envPrefix()}_AUDIT_RANGE. This is a check that did not execute, not a clean audit.`
|
|
348
357
|
);
|
|
349
358
|
cliExit(2);
|
|
350
359
|
return;
|
|
@@ -411,7 +420,7 @@ if (require.main === module) {
|
|
|
411
420
|
|
|
412
421
|
module.exports = {
|
|
413
422
|
analyzeCommits, resolveRank, parseCommits, rowsToMap,
|
|
414
|
-
clampWindow, defaultRange, firstParentDepth, DEFAULT_WINDOW,
|
|
423
|
+
clampWindow, defaultRange, firstParentDepth, DEFAULT_WINDOW, requestedRange,
|
|
415
424
|
// Exported so a test can assert the address is LIVE without a database. The --db
|
|
416
425
|
// path was dead for as long as the db.js carve has been landed precisely because
|
|
417
426
|
// nothing checked that this require target still exports what it is called for.
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
'use strict';
|
|
23
23
|
const fs = require('node:fs');
|
|
24
24
|
const path = require('node:path');
|
|
25
|
+
const { resolveEnv } = require('../../src/instance-config');
|
|
25
26
|
|
|
26
27
|
// Budgets, in bytes of MEMORY.md (~4 chars/token for markdown prose).
|
|
27
28
|
const WARN_BYTES = 16000; // ~4K tok — drifting up; warn so it gets tidied.
|
|
@@ -157,7 +158,8 @@ module.exports = {
|
|
|
157
158
|
if (require.main === module) {
|
|
158
159
|
const args = process.argv.slice(2);
|
|
159
160
|
const dirArg = args.indexOf('--dir');
|
|
160
|
-
|
|
161
|
+
// <PREFIX>_MEMORY_DIR (BONGOS_ on a vanilla instance; the legacy OTB_ still read).
|
|
162
|
+
const memoryDir = dirArg >= 0 ? args[dirArg + 1] : resolveEnv('MEMORY_DIR');
|
|
161
163
|
if (!memoryDir) { console.error('usage: memory-map.js --dir <memoryDir> [--apply]'); process.exit(2); }
|
|
162
164
|
const mapPath = path.join(memoryDir, 'MEMORY.md');
|
|
163
165
|
let text; try { text = fs.readFileSync(mapPath, 'utf8'); } catch { console.error('no MEMORY.md at', mapPath); process.exit(0); }
|
|
@@ -27,11 +27,12 @@ const MANIFEST_PATH = path.join(SPOOL_DIR, 'manifest.json');
|
|
|
27
27
|
// How long the onboarding window stays open after the consent decision.
|
|
28
28
|
// Friction-mining is for the first stretch of a builder's life; after that
|
|
29
29
|
// they're not "onboarding" and shipping their transcripts would be noise (and
|
|
30
|
-
// a privacy overreach). Override for tests via
|
|
30
|
+
// a privacy overreach). Override for tests via <PREFIX>_ONBOARDING_WINDOW_DAYS
|
|
31
|
+
// (BONGOS_ on a vanilla instance; the legacy OTB_ spelling is still read).
|
|
31
32
|
const DEFAULT_WINDOW_DAYS = 14;
|
|
32
33
|
|
|
33
34
|
function windowDays() {
|
|
34
|
-
const env = Number(
|
|
35
|
+
const env = Number(ic.resolveEnv('ONBOARDING_WINDOW_DAYS'));
|
|
35
36
|
return Number.isFinite(env) && env > 0 ? env : DEFAULT_WINDOW_DAYS;
|
|
36
37
|
}
|
|
37
38
|
|