@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.
Files changed (35) hide show
  1. package/.bongos-core.json +43 -48
  2. package/.claude/skills/grade-recover/SKILL.md +1 -1
  3. package/docs/adr/0302-the-third-identical-failure-stops-the-session-and-asks.md +3 -2
  4. package/docs/adr/0329-the-rendered-fit-check-is-a-kit-floor-and-an-advisory-ship-check-row.md +2 -2
  5. package/docs/adr/README.md +1 -1
  6. package/docs/module-api-changelog.md +2 -0
  7. package/modules/economy/credits.js +1 -1
  8. package/modules/grading/grader-config.js +3 -12
  9. package/modules/grading/grader-no-artifact.js +227 -2
  10. package/modules/grading/grader-prompt.js +1 -1
  11. package/modules/grading/grader-rubric.json +1 -86
  12. package/modules/grading/grader-score.js +1 -169
  13. package/modules/grading/grader-subagent.js +1 -51
  14. package/modules/grading/grader.js +26 -211
  15. package/modules/lifecycle/db-ship.js +3 -3
  16. package/package-lock.json +2 -2
  17. package/package.json +1 -1
  18. package/release-notes.json +10 -0
  19. package/scripts/gds/claim.js +2 -1
  20. package/scripts/gds/grade-smoke.js +3 -3
  21. package/scripts/gds/render-check.js +1 -1
  22. package/scripts/gds/ship-check.js +1 -1
  23. package/scripts/gds/ship-flow.js +1 -1
  24. package/scripts/gds/ship-grade-steps.js +2 -2
  25. package/scripts/gds/visual-review-nudge.js +2 -1
  26. package/src/module-api.js +1 -1
  27. package/tests/adr_0162_gate_citation.mjs +167 -0
  28. package/tests/grader.mjs +48 -126
  29. package/tests/grader_unbacked_score.mjs +1 -1
  30. package/tests/regrade_actual_minutes.mjs +18 -17
  31. package/tests/render_check.mjs +1 -1
  32. package/tests/skill_grade_audit.mjs +3 -3
  33. package/tests/skill_grader_health.mjs +2 -2
  34. package/scripts/gds/audit-ships.js +0 -319
  35. package/tests/audit_ships_git_argv.mjs +0 -173
@@ -1,173 +0,0 @@
1
- // tests/audit_ships_git_argv.mjs — audit-ships' git calls must be argv, not a
2
- // shell string (task 1003223; the class task 1002761 named).
3
- //
4
- // THE BUG. Five calls interpolated `--format=''` into a shell string. A POSIX
5
- // shell strips the quotes and git sees an empty format; the shell Node uses on
6
- // Windows does not, so git receives `--format=''` literally and dies with
7
- // `fatal: invalid --pretty format: ''`. A sixth built a `grep | head | awk`
8
- // pipeline and pinned `shell: '/bin/bash'` to survive at all.
9
- //
10
- // WHY THIS NEEDS A TEST AND NOT JUST A FIX. Every one of those calls sat inside a
11
- // try/catch that returned null or []. So the Windows failure did not crash the
12
- // audit — it produced an audit with no diff stats and zero downstream impact for
13
- // every ship, which is indistinguishable from a clean run. The originating row
14
- // asked exactly this ("check whether the catch degrades to a silently empty audit
15
- // — if so it is the same dangerous shape after all"). It did.
16
- //
17
- // The static half of this file is the real net: it is the half that stays true on
18
- // Linux CI, where the shell-string form would have passed happily forever.
19
- //
20
- // Run: node --test tests/audit_ships_git_argv.mjs
21
-
22
- import { test } from 'node:test';
23
- import assert from 'node:assert/strict';
24
- import fs from 'node:fs';
25
- import path from 'node:path';
26
- import { execSync, execFileSync } from 'node:child_process';
27
- import { fileURLToPath } from 'node:url';
28
- import { createRequire } from 'node:module';
29
-
30
- const require = createRequire(import.meta.url);
31
- const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
32
- const FILE = path.join(ROOT, 'scripts/gds/audit-ships.js');
33
- const audit = require('../scripts/gds/audit-ships.js');
34
- const raw = fs.readFileSync(FILE, 'utf8');
35
-
36
- // Scan CODE, not prose. The file's header explains the bug by quoting the exact
37
- // tokens that are banned ("--format=''", "grep -E", "shell: '/bin/bash'"), so a
38
- // naive scan of the whole file fails on its own documentation — which is exactly
39
- // what happened when this test was first written. Same comments-stripped posture
40
- // as scripts/gds/category-advisory-guard.js.
41
- const src = raw
42
- .replace(/\/\*[\s\S]*?\*\//g, '') // block comments
43
- .split('\n').map((l) => l.replace(/(^|\s)\/\/.*$/, '$1')).join('\n');
44
-
45
- // ---- static: the shape that broke must not come back ----------------------
46
-
47
- test('no execSync anywhere — a shell string is what mangles the quoting', () => {
48
- assert.doesNotMatch(src, /\bexecSync\b/,
49
- 'audit-ships must spawn git through execFileSync with an argv array');
50
- });
51
-
52
- test("no quoted --format='' — the argv spelling is --format= with no quotes", () => {
53
- // Match the SHELL form precisely: two consecutive quotes after the '='. A naive
54
- // /--format='/ also matches the correct argv literal '--format=', where that
55
- // quote is JS string syntax, not a shell quote — it flagged the fix as the bug.
56
- assert.doesNotMatch(src, /--format=''/,
57
- "a quoted empty format is the exact token git rejects on Windows");
58
- assert.match(src, /'--format='/, 'the argv spelling is present');
59
- });
60
-
61
- test('no shell pipeline and no pinned /bin/bash', () => {
62
- assert.doesNotMatch(src, /\bshell:\s*['"]/, 'no shell is needed once calls are argv');
63
- for (const tool of ['grep -E', '| head', '| awk']) {
64
- assert.ok(!src.includes(tool), `${tool} is a POSIX-only dependency`);
65
- }
66
- });
67
-
68
- test('git failures are reported, not swallowed into a clean-looking audit', () => {
69
- // The silent `catch (_) { return null; }` is what made the Windows breakage
70
- // invisible. Every catch around a git call must go through gitFails.
71
- assert.match(raw, /function gitFails\(/, 'the reporting helper exists');
72
- // Scoped to catches returning an empty RESULT SET — null or [] — which is the
73
- // shape that turned a git failure into a clean-looking audit. A catch returning
74
- // `{}` for an unreadable branding pack is a different, legitimate default and is
75
- // deliberately not swept up here.
76
- const silent = src.match(/catch \(_\)\s*\{\s*return (?:null|\[\])/g) || [];
77
- assert.equal(silent.length, 0,
78
- `silent catch(es) returning an empty result: ${silent.join(' | ')}`);
79
- });
80
-
81
- // ---- behavioural: the calls actually work on THIS platform ----------------
82
- //
83
- // These run real git against this repo, which is what makes them meaningful on
84
- // Windows: before the fix, two of them threw here and returned empty.
85
-
86
- const git = (args) => execFileSync('git', args, { cwd: ROOT, encoding: 'utf8' }).trim();
87
- // A NON-MERGE commit. `git show --name-only` prints nothing for a merge by
88
- // default, so anchoring on HEAD makes the file-listing assertions depend on
89
- // whatever happened to be checked out — they failed here for that reason, not
90
- // because the code was wrong. --no-merges picks a commit that really touched files.
91
- const HEAD = git(['rev-list', '--no-merges', '-n', '1', 'HEAD']);
92
-
93
- test('the exact argv git rejected as a shell string succeeds here', () => {
94
- const out = execFileSync('git', ['show', '--shortstat', '--format=', HEAD], { cwd: ROOT, encoding: 'utf8' });
95
- assert.match(out, /\d+ files? changed/, `shortstat should parse, got: ${JSON.stringify(out.slice(0, 80))}`);
96
- });
97
-
98
- test('the shell-string form is what actually fails on this platform', () => {
99
- // Not a hypothetical: assert the OLD form's behaviour so the reason for the fix
100
- // is recorded in a runnable way. On POSIX it succeeds (the quotes are stripped),
101
- // on Windows it throws — either way, argv is the only spelling that is correct
102
- // on both, which is the whole point.
103
- let threw = false;
104
- try { execSync(`git show --shortstat --format='' ${HEAD}`, { cwd: ROOT, encoding: 'utf8' }); }
105
- catch { threw = true; }
106
- if (process.platform === 'win32') {
107
- assert.equal(threw, true, 'on win32 the quoted form must be the one that fails');
108
- } else {
109
- assert.equal(threw, false, 'on POSIX it happens to work — which is why this hid on CI');
110
- }
111
- });
112
-
113
- test('--name-only --format= yields real paths', () => {
114
- const out = execFileSync('git', ['show', '--name-only', '--format=', HEAD], { cwd: ROOT, encoding: 'utf8' });
115
- const files = out.split('\n').map((s) => s.trim()).filter(Boolean);
116
- assert.ok(files.length > 0, 'HEAD touched at least one file');
117
- assert.ok(files.every((f) => !f.includes("'")), 'paths come back unquoted');
118
- });
119
-
120
- // ---- the real functions, not equivalents ---------------------------------
121
- //
122
- // These drive audit-ships' own helpers. Before this task the file executed main()
123
- // on require — it hit the API the moment anything imported it — so its git helpers
124
- // had never been reachable from a test at all. That is the deeper reason the
125
- // Windows breakage went unnoticed for months.
126
-
127
- test('gitDiffStatsForCommit returns real stats on this platform', () => {
128
- const stats = audit.gitDiffStatsForCommit(HEAD);
129
- assert.ok(stats, 'must not be the null the Windows failure returned');
130
- assert.equal(stats.commit_sha, HEAD);
131
- assert.ok(stats.files_changed > 0, `files_changed should be positive, got ${stats.files_changed}`);
132
- });
133
-
134
- test('filesTouchedByCommit returns real paths, not the empty array of a swallowed failure', () => {
135
- const files = audit.filesTouchedByCommit(HEAD);
136
- assert.ok(Array.isArray(files) && files.length > 0, `expected paths, got ${JSON.stringify(files)}`);
137
- assert.ok(files.every((f) => typeof f === 'string' && f.length > 0));
138
- });
139
-
140
- test('the commit list is read once per run, not once per task', () => {
141
- // The shell pipeline this replaced ended in `head -1`, so git stopped at the
142
- // first match. The argv form has no early exit, and findCommitForTask runs once
143
- // per shipped task — up to 500 a run — so a naive port turns one bounded call
144
- // into 500 full history walks. Caught by the grader on this task's first round.
145
- //
146
- // Measured rather than asserted structurally: the second lookup must not spend
147
- // anywhere near the first's time, because it must not touch git at all.
148
- const t0 = process.hrtime.bigint();
149
- audit.findCommitForTask(999999901);
150
- const cold = process.hrtime.bigint() - t0;
151
-
152
- const t1 = process.hrtime.bigint();
153
- audit.findCommitForTask(999999902);
154
- const warm = process.hrtime.bigint() - t1;
155
-
156
- assert.ok(warm * 5n < cold || warm < 2_000_000n,
157
- `second lookup should reuse the cached list (cold ${cold / 1000000n}ms, warm ${warm / 1000000n}ms)`);
158
- });
159
-
160
- test('the helpers still answer safely for a missing sha', () => {
161
- assert.equal(audit.gitDiffStatsForCommit(null), null);
162
- assert.deepEqual(audit.filesTouchedByCommit(null), []);
163
- assert.deepEqual(audit.downstreamImpact(null, ['x']),
164
- { commits_after_touching_files: 0, distinct_files_touched: 0, sample_subjects: [] });
165
- });
166
-
167
- test('a path containing a space survives argv — the hand-rolled quoting is gone', () => {
168
- // git log -- <pathspec> with a space used to need shell quoting; in argv it is
169
- // simply one element. An unmatched pathspec returns empty, never an error.
170
- const out = execFileSync('git', ['log', '-1', '--format=%H', '--', 'no such file.md'], { cwd: ROOT, encoding: 'utf8' });
171
- assert.equal(out.trim(), '', 'an unmatched pathspec is empty, not a crash');
172
- });
173
-