@bongos/core 1.19.320 → 1.19.321
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 +12 -12
- package/modules/dev-box/box-access.js +20 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness-ratchets.js +54 -6
- package/src/module-api.js +3 -1
- package/tests/box_access.mjs +46 -0
- package/tests/fitness_ratchets.mjs +82 -0
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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.321",
|
|
6
|
+
"core_contract": "1.19.321",
|
|
7
|
+
"source_commit": "242735e2b39608c5500b8c5bd231832a90c5fae7",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-24T01:17:55.486Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 323,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1800,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "5d80f6db5c26850188eb0c3f5ace7c42ff85561d277dc525e24f46cdcf52dee8",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -2827,7 +2827,7 @@
|
|
|
2827
2827
|
{
|
|
2828
2828
|
"path": "modules/dev-box/box-access.js",
|
|
2829
2829
|
"mode": "0000644",
|
|
2830
|
-
"sha256": "
|
|
2830
|
+
"sha256": "a51e16aba67547c0692d9422587f83d8ce40f0e45830463b80bfd188401eb6c0"
|
|
2831
2831
|
},
|
|
2832
2832
|
{
|
|
2833
2833
|
"path": "modules/dev-box/box-credential.js",
|
|
@@ -4642,12 +4642,12 @@
|
|
|
4642
4642
|
{
|
|
4643
4643
|
"path": "package-lock.json",
|
|
4644
4644
|
"mode": "0000644",
|
|
4645
|
-
"sha256": "
|
|
4645
|
+
"sha256": "6f62185348f89cc7ae1c8998a25d4bdccb2e204ad81581555686f37057db1511"
|
|
4646
4646
|
},
|
|
4647
4647
|
{
|
|
4648
4648
|
"path": "package.json",
|
|
4649
4649
|
"mode": "0000644",
|
|
4650
|
-
"sha256": "
|
|
4650
|
+
"sha256": "3ba87e851af1bf1ad2a08cb62d4c875be12d7ec6d86ffda9e5b26e24fee313ae"
|
|
4651
4651
|
},
|
|
4652
4652
|
{
|
|
4653
4653
|
"path": "public-docs/index.html",
|
|
@@ -5292,7 +5292,7 @@
|
|
|
5292
5292
|
{
|
|
5293
5293
|
"path": "scripts/gds/fitness-ratchets.js",
|
|
5294
5294
|
"mode": "0000644",
|
|
5295
|
-
"sha256": "
|
|
5295
|
+
"sha256": "3f32080357abdecb0f0860c5c9313f12283f01774c8367ac22c1c12d0d176411"
|
|
5296
5296
|
},
|
|
5297
5297
|
{
|
|
5298
5298
|
"path": "scripts/gds/fitness.js",
|
|
@@ -6417,7 +6417,7 @@
|
|
|
6417
6417
|
{
|
|
6418
6418
|
"path": "src/module-api.js",
|
|
6419
6419
|
"mode": "0000644",
|
|
6420
|
-
"sha256": "
|
|
6420
|
+
"sha256": "31c5e453080859713ed00c327a6b1fb1a022d2509438523979bad2e0e8feb7bf"
|
|
6421
6421
|
},
|
|
6422
6422
|
{
|
|
6423
6423
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6732,7 +6732,7 @@
|
|
|
6732
6732
|
{
|
|
6733
6733
|
"path": "tests/box_access.mjs",
|
|
6734
6734
|
"mode": "0000644",
|
|
6735
|
-
"sha256": "
|
|
6735
|
+
"sha256": "fcad6c578c2d8698d6ad93fe04f500734d589013590f006910f1bc33fc908642"
|
|
6736
6736
|
},
|
|
6737
6737
|
{
|
|
6738
6738
|
"path": "tests/box_auth_check.mjs",
|
|
@@ -7272,7 +7272,7 @@
|
|
|
7272
7272
|
{
|
|
7273
7273
|
"path": "tests/fitness_ratchets.mjs",
|
|
7274
7274
|
"mode": "0000644",
|
|
7275
|
-
"sha256": "
|
|
7275
|
+
"sha256": "e33edfa4925694d824c042f1fd06a977f1ac14b5c1cb24b5326014ca9f86ece0"
|
|
7276
7276
|
},
|
|
7277
7277
|
{
|
|
7278
7278
|
"path": "tests/fitness_two_root.mjs",
|
|
@@ -158,6 +158,26 @@ const BASE_SPARSE_PATHS = [
|
|
|
158
158
|
// and box-source-fetch re-applies this server-computed list every tick
|
|
159
159
|
// (task 1002745). Generated ESM only, no product runtime.
|
|
160
160
|
'clients',
|
|
161
|
+
// config/ — task 1003227. Without it a box CANNOT RUN ITS OWN QUALITY GATE:
|
|
162
|
+
// `scripts/gds/fitness.js` compares against config/fitness-baselines.json, and
|
|
163
|
+
// with the file absent the ratchet used to report PASS (it read the baselines
|
|
164
|
+
// through a try/catch that returned {}, so every metric had no floor to breach).
|
|
165
|
+
// A planted 5,000-line + 94-cycle regression sailed straight through. So a boxed
|
|
166
|
+
// builder shipped believing they were clean and learned otherwise from red CI,
|
|
167
|
+
// with nothing on the box to reproduce it. The fail-closed half is fixed in
|
|
168
|
+
// fitness-ratchets.js; this is the half that lets the gate actually run.
|
|
169
|
+
//
|
|
170
|
+
// Precedent, and why the "a module glob will pull it" argument does not hold:
|
|
171
|
+
// ADR 0174 put config/government.json in the `government` key's globs and the
|
|
172
|
+
// parent-directory emission below exists precisely because `config` was not in
|
|
173
|
+
// BASE — task 1003018 was RELEASED because its box could never see the file that
|
|
174
|
+
// was its entire deliverable. BASE is the right home, not a per-module accident.
|
|
175
|
+
//
|
|
176
|
+
// Secret posture unchanged: a cone-mode directory of JSON config. The NEUTRAL
|
|
177
|
+
// packs are already public, and an instance pack (config/branding.json,
|
|
178
|
+
// config/government.json) is host content the box's own builder authors — no
|
|
179
|
+
// credentials live here (ADR 0022); those are in /etc and the box env.
|
|
180
|
+
'config',
|
|
161
181
|
];
|
|
162
182
|
|
|
163
183
|
// sparsePathsForScopeKeys — PURE. Given the module KEYS a builder's active claims
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.321",
|
|
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.321",
|
|
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.321",
|
|
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",
|
|
@@ -215,12 +215,52 @@ function knipIssueCount() {
|
|
|
215
215
|
|
|
216
216
|
// ---------- baseline + ratchet ----------
|
|
217
217
|
|
|
218
|
+
// readBaselines — task 1003227. Returns { metrics, error }: `error` is a reason
|
|
219
|
+
// string when the file cannot be trusted, null when it read cleanly.
|
|
220
|
+
//
|
|
221
|
+
// This used to `catch { return {}; }`, which made the gate FAIL OPEN. An empty
|
|
222
|
+
// baseline map leaves every `floor === undefined`, and compareToBaselines classifies
|
|
223
|
+
// an absent floor as a WARNING — so a checkout with no config/ reported the ratchet
|
|
224
|
+
// as PASSED. Measured: with the baselines gone, a planted 5,000-line and 94-cycle
|
|
225
|
+
// regression produced 0 violations. That is not a hypothetical checkout — `config/`
|
|
226
|
+
// is absent from every dev box's sparse slice, so the builders who most needed the
|
|
227
|
+
// gate were the ones silently exempt from it, learning from red CI instead.
|
|
228
|
+
//
|
|
229
|
+
// Distinguish the two absences, because they are not the same fact:
|
|
230
|
+
// - the FILE is missing/malformed -> cannot verify anything -> hard fail (here);
|
|
231
|
+
// - a KEY is missing for one metric -> a genuinely new metric -> stays a warning
|
|
232
|
+
// in compareToBaselines, so adding a metric does not break the build.
|
|
233
|
+
//
|
|
234
|
+
// The file's own header says raising a baseline to pass CI is out of policy;
|
|
235
|
+
// deleting the file must not be the loophole that raising it is not.
|
|
218
236
|
function readBaselines() {
|
|
237
|
+
let raw;
|
|
219
238
|
try {
|
|
220
|
-
|
|
221
|
-
} catch {
|
|
222
|
-
|
|
239
|
+
raw = fs.readFileSync(BASELINE_PATH, 'utf8');
|
|
240
|
+
} catch (e) {
|
|
241
|
+
const missing = e && e.code === 'ENOENT';
|
|
242
|
+
return {
|
|
243
|
+
metrics: {},
|
|
244
|
+
error: missing
|
|
245
|
+
? `config/fitness-baselines.json is MISSING — the quality ratchet cannot verify anything, so it fails CLOSED rather than reporting a pass it did not check. `
|
|
246
|
+
+ `If this is a dev box, config/ was historically outside the sparse checkout (task 1003227); re-sync the box (\`node scripts/gds/box-sync.js\`). `
|
|
247
|
+
+ `To bootstrap a fresh baseline deliberately: node scripts/gds/fitness-ratchets.js --update-baselines`
|
|
248
|
+
: `config/fitness-baselines.json is unreadable (${e && e.message}) — failing CLOSED.`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
let parsed;
|
|
252
|
+
try {
|
|
253
|
+
parsed = JSON.parse(raw);
|
|
254
|
+
} catch (e) {
|
|
255
|
+
return { metrics: {}, error: `config/fitness-baselines.json is not valid JSON (${e && e.message}) — failing CLOSED.` };
|
|
256
|
+
}
|
|
257
|
+
const metrics = parsed && typeof parsed.metrics === 'object' && parsed.metrics !== null
|
|
258
|
+
? parsed.metrics
|
|
259
|
+
: null;
|
|
260
|
+
if (!metrics) {
|
|
261
|
+
return { metrics: {}, error: 'config/fitness-baselines.json has no "metrics" object — failing CLOSED.' };
|
|
223
262
|
}
|
|
263
|
+
return { metrics, error: null };
|
|
224
264
|
}
|
|
225
265
|
|
|
226
266
|
// Pure ratchet verdict — exported for the hermetic test.
|
|
@@ -256,7 +296,11 @@ function computeMetrics({ withDocs = true } = {}) {
|
|
|
256
296
|
// The fitness.js check (in-suite, hermetic — everything except knip).
|
|
257
297
|
function checkQualityRatchets() {
|
|
258
298
|
const metrics = computeMetrics();
|
|
259
|
-
const {
|
|
299
|
+
const { metrics: baselines, error: baselineError } = readBaselines();
|
|
300
|
+
const { violations, warnings } = compareToBaselines(metrics, baselines);
|
|
301
|
+
// task 1003227: an unreadable baseline file is a hard failure, not silence. Put it
|
|
302
|
+
// FIRST so it is the headline rather than buried under per-metric noise.
|
|
303
|
+
if (baselineError) violations.unshift(baselineError);
|
|
260
304
|
return {
|
|
261
305
|
name: 'quality ratchets — size, duplication, cycles, console, doc entropy only tighten (task 1003129)',
|
|
262
306
|
hardFail: violations.length > 0,
|
|
@@ -274,7 +318,9 @@ function main() {
|
|
|
274
318
|
const metrics = computeMetrics();
|
|
275
319
|
const knip = knipIssueCount();
|
|
276
320
|
if (knip !== null) metrics.knip_issue_count = knip;
|
|
277
|
-
|
|
321
|
+
// The bootstrap path deliberately tolerates an absent file — writing it is the
|
|
322
|
+
// whole point — so it reads the metrics and ignores any error (task 1003227).
|
|
323
|
+
const { metrics: existing } = readBaselines();
|
|
278
324
|
if (knip === null && existing.knip_issue_count !== undefined) metrics.knip_issue_count = existing.knip_issue_count;
|
|
279
325
|
fs.writeFileSync(
|
|
280
326
|
BASELINE_PATH,
|
|
@@ -290,7 +336,9 @@ function main() {
|
|
|
290
336
|
console.warn('fitness-ratchets: WARN — knip not runnable here (no node_modules / npx failure); failing OPEN.');
|
|
291
337
|
process.exit(0);
|
|
292
338
|
}
|
|
293
|
-
const {
|
|
339
|
+
const { metrics: knipBaselines, error: knipBaselineError } = readBaselines();
|
|
340
|
+
const { violations, warnings } = compareToBaselines({ knip_issue_count: value }, knipBaselines);
|
|
341
|
+
if (knipBaselineError) violations.unshift(knipBaselineError);
|
|
294
342
|
for (const w of warnings) console.warn(`fitness-ratchets: ${w}`);
|
|
295
343
|
if (violations.length) {
|
|
296
344
|
for (const v of violations) console.error(`fitness-ratchets: FAIL — ${v}`);
|
package/src/module-api.js
CHANGED
|
@@ -1067,8 +1067,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1067
1067
|
// landed since 1.19.318 with no explicit bump. run 32670195088. (task 1002620)
|
|
1068
1068
|
// 1.19.320 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1069
1069
|
// landed since 1.19.319 with no explicit bump. run 32678147546. (task 1002620)
|
|
1070
|
+
// 1.19.321 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1071
|
+
// landed since 1.19.320 with no explicit bump. run 32679324013. (task 1002620)
|
|
1070
1072
|
// ---------------------------------------------------------------------------
|
|
1071
|
-
const CORE_VERSION = '1.19.
|
|
1073
|
+
const CORE_VERSION = '1.19.321'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1072
1074
|
|
|
1073
1075
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1074
1076
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/box_access.mjs
CHANGED
|
@@ -368,5 +368,51 @@ t('a DENIED decision projects to null (the route returns 403, never a scope body
|
|
|
368
368
|
assert.equal(access.scopeMetadata(), null, 'no args → null');
|
|
369
369
|
});
|
|
370
370
|
|
|
371
|
+
// ---------- task 1003227: config/ must be in the box's BASE slice ----------
|
|
372
|
+
//
|
|
373
|
+
// Without it a box cannot run its own quality gate: fitness.js compares against
|
|
374
|
+
// config/fitness-baselines.json, and with the file absent the ratchet used to report
|
|
375
|
+
// PASS (a planted 5,000-line + 94-cycle regression sailed through). So a boxed builder
|
|
376
|
+
// shipped believing they were clean and learned otherwise from red CI, with nothing on
|
|
377
|
+
// the box to reproduce it. Precedent for the same class: ADR 0174 put
|
|
378
|
+
// config/government.json in the `government` globs, config/ was not in BASE, and task
|
|
379
|
+
// 1003018 was RELEASED because its box could never see its own deliverable.
|
|
380
|
+
|
|
381
|
+
t('1003227 BASE includes config/ so a box can read its own fitness baselines', () => {
|
|
382
|
+
const spec = access.decideSourceAccess({
|
|
383
|
+
rank: 'archon', status: 'active', scopeKeys: [], hasActiveClaims: false, moduleScopeMap: {},
|
|
384
|
+
});
|
|
385
|
+
assert.equal(spec.scope, 'base');
|
|
386
|
+
assert.ok(spec.spec.sparsePaths.includes('config'),
|
|
387
|
+
`base scope must carry config/; got ${spec.spec.sparsePaths.join(',')}`);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
t('1003227 a TASK-scoped box carries config/ too (the common case — a claim is held)', () => {
|
|
391
|
+
// The REAL scope map, not a hand-rolled object — the shape matters here.
|
|
392
|
+
const scopeMap = require('../src/bongos/module-scope-map.js');
|
|
393
|
+
const spec = access.decideSourceAccess({
|
|
394
|
+
rank: 'xenos', status: 'active', scopeKeys: ['economy'], hasActiveClaims: true,
|
|
395
|
+
moduleScopeMap: scopeMap,
|
|
396
|
+
});
|
|
397
|
+
assert.equal(spec.scope, 'task');
|
|
398
|
+
assert.ok(spec.spec.sparsePaths.includes('config'), 'task scope must carry config/ as well');
|
|
399
|
+
assert.ok(spec.spec.sparsePaths.includes('modules/economy'),
|
|
400
|
+
`and still layer the claimed module on top; got ${spec.spec.sparsePaths.join(',')}`);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
t('1003227 config/ is a cone-mode DIRECTORY entry (a file path aborts the whole set)', () => {
|
|
404
|
+
// ADR 0148: `git sparse-checkout set` rejects a file path, which kills the entire
|
|
405
|
+
// checkout — so every BASE entry must be a bare directory, no trailing slash.
|
|
406
|
+
for (const p of access.BASE_SPARSE_PATHS) {
|
|
407
|
+
assert.ok(!p.endsWith('/'), `${p} must not carry a trailing slash`);
|
|
408
|
+
// A dotted DIRECTORY (.claude, .devcontainer) is fine; what cone mode rejects is
|
|
409
|
+
// a file, so only test for an extension on the FINAL segment of a non-dotfile.
|
|
410
|
+
const last = p.split('/').pop();
|
|
411
|
+
if (!last.startsWith('.')) {
|
|
412
|
+
assert.ok(!/\.[a-z0-9]+$/i.test(last), `${p} looks like a FILE — cone mode would abort the set`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
371
417
|
console.log(`\nbox_access.mjs: ${passed} passed, ${failed} failed`);
|
|
372
418
|
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -89,3 +89,85 @@ test('the committed baseline file parses and covers every hermetic metric', () =
|
|
|
89
89
|
assert.ok(Number.isInteger(b.metrics[key]), `baseline missing ${key}`);
|
|
90
90
|
}
|
|
91
91
|
});
|
|
92
|
+
|
|
93
|
+
// ---------- task 1003227: the gate must FAIL CLOSED on an unusable baseline ----------
|
|
94
|
+
//
|
|
95
|
+
// readBaselines used to `catch { return {} }`. An empty baseline map leaves every
|
|
96
|
+
// `floor === undefined`, which compareToBaselines classifies as a WARNING — so a
|
|
97
|
+
// checkout with no config/ reported the ratchet PASSED. Measured before the fix: a
|
|
98
|
+
// planted 5,000-line + 94-cycle regression produced ZERO violations. That was not a
|
|
99
|
+
// hypothetical checkout — config/ is absent from every dev box's sparse slice, so the
|
|
100
|
+
// builders who most needed the gate were the ones silently exempt from it.
|
|
101
|
+
//
|
|
102
|
+
// checkQualityRatchets() reads the real repo, so these tests drive compareToBaselines
|
|
103
|
+
// (pure) for the metric-level rules and exercise the file-level rule by pointing at a
|
|
104
|
+
// baseline that does not exist.
|
|
105
|
+
|
|
106
|
+
test('1003227 an ABSENT baseline file makes the gate hard-fail, not pass', async () => {
|
|
107
|
+
const fs = require('node:fs');
|
|
108
|
+
const real = rat.BASELINE_PATH;
|
|
109
|
+
const backup = `${real}.1003227-test-backup`;
|
|
110
|
+
const had = fs.existsSync(real);
|
|
111
|
+
if (had) fs.renameSync(real, backup);
|
|
112
|
+
try {
|
|
113
|
+
const r = rat.checkQualityRatchets();
|
|
114
|
+
assert.equal(r.hardFail, true, 'a gate that cannot read its floors must not report a pass');
|
|
115
|
+
assert.ok(r.violations.length > 0, 'and it must say so as a VIOLATION, not a warning');
|
|
116
|
+
assert.match(r.violations[0], /MISSING/, 'the first violation names the real cause');
|
|
117
|
+
assert.match(r.violations[0], /fails CLOSED/i, 'and states the posture explicitly');
|
|
118
|
+
} finally {
|
|
119
|
+
if (had) fs.renameSync(backup, real);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('1003227 a MALFORMED baseline file also hard-fails', async () => {
|
|
124
|
+
const fs = require('node:fs');
|
|
125
|
+
const real = rat.BASELINE_PATH;
|
|
126
|
+
const backup = `${real}.1003227-test-backup2`;
|
|
127
|
+
const had = fs.existsSync(real);
|
|
128
|
+
if (had) fs.copyFileSync(real, backup);
|
|
129
|
+
try {
|
|
130
|
+
fs.writeFileSync(real, '{ this is not json');
|
|
131
|
+
const r = rat.checkQualityRatchets();
|
|
132
|
+
assert.equal(r.hardFail, true, 'unparseable is no more verifiable than absent');
|
|
133
|
+
assert.match(r.violations[0], /not valid JSON/);
|
|
134
|
+
} finally {
|
|
135
|
+
if (had) { fs.copyFileSync(backup, real); fs.unlinkSync(backup); }
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('1003227 a baseline file with no "metrics" object hard-fails', async () => {
|
|
140
|
+
const fs = require('node:fs');
|
|
141
|
+
const real = rat.BASELINE_PATH;
|
|
142
|
+
const backup = `${real}.1003227-test-backup3`;
|
|
143
|
+
const had = fs.existsSync(real);
|
|
144
|
+
if (had) fs.copyFileSync(real, backup);
|
|
145
|
+
try {
|
|
146
|
+
fs.writeFileSync(real, JSON.stringify({ _comment: 'no metrics key' }));
|
|
147
|
+
const r = rat.checkQualityRatchets();
|
|
148
|
+
assert.equal(r.hardFail, true);
|
|
149
|
+
assert.match(r.violations[0], /no "metrics" object/);
|
|
150
|
+
} finally {
|
|
151
|
+
if (had) { fs.copyFileSync(backup, real); fs.unlinkSync(backup); }
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('1003227 a missing KEY for a NEW metric stays a warning (adding a metric must not brick CI)', () => {
|
|
156
|
+
// The two absences are different facts and must not collapse: no FILE = cannot
|
|
157
|
+
// verify = fail; no KEY = a genuinely new metric = warn.
|
|
158
|
+
const { violations, warnings } = rat.compareToBaselines(
|
|
159
|
+
{ brand_new_metric: 7 },
|
|
160
|
+
{ excess_lines_total: 100 },
|
|
161
|
+
);
|
|
162
|
+
assert.deepEqual(violations, [], 'a new metric with no floor yet must not fail the build');
|
|
163
|
+
assert.match(warnings.join(' '), /no baseline for "brand_new_metric"/);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('1003227 a real regression is still caught when the baselines ARE readable', () => {
|
|
167
|
+
const { violations } = rat.compareToBaselines(
|
|
168
|
+
{ excess_lines_total: 5100, require_cycle_count: 99 },
|
|
169
|
+
{ excess_lines_total: 100, require_cycle_count: 5 },
|
|
170
|
+
);
|
|
171
|
+
assert.equal(violations.length, 2, 'both regressions reported');
|
|
172
|
+
assert.match(violations.join(' '), /never raise the baseline to pass/);
|
|
173
|
+
});
|