@bongos/core 1.19.907 → 1.19.910

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/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.907'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.910'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -136,6 +136,98 @@ test('the two calm bands are not chips, and the date is not said twice', () => {
136
136
  assert.match(js, /title="Asked \$\{escapeHtml\(String\(req\.created_at/, 'the exact date is still reachable');
137
137
  });
138
138
 
139
+ // ---- THE ROOM'S OWN CONDITION (task 1004067, from idea 1001065) -----------
140
+ //
141
+ // The report: fourteen of fourteen rows wore the "waiting over a week" pill, so
142
+ // the loudest thing on the page carried no information. Task 1004041 made the
143
+ // WORDS differ, which is why these tests are about the TREATMENT — fourteen
144
+ // differently-worded rows in identical danger ink and identical rings still read
145
+ // as a wall. Each case below is run through the lib, not grepped, so a rule that
146
+ // is wrong fails here rather than looking plausible in a diff.
147
+ const room = (...ages) => ages.map((d) => ({ created_at: daysAgo(d) }));
148
+
149
+ test('a uniformly old room states its condition once instead of on every row', () => {
150
+ const L = lib();
151
+ // The reported board, reproduced: fourteen rows, every one past a week.
152
+ const rows = room(8, 9, 11, 13, 15, 16, 18, 21, 24, 27, 30, 34, 38, 41);
153
+ const baseline = L.roomBaseline(rows, NOW);
154
+ assert.ok(baseline, 'fourteen of fourteen is a room condition, not fourteen warnings');
155
+ assert.equal(baseline.band.id, 'stale');
156
+ assert.equal(L.roomAgeLine(baseline), 'all 14 waiting over a week');
157
+ const loud = rows.filter((r) => !L.isRoomQuiet(L.ageBand(r.created_at, NOW), baseline));
158
+ assert.equal(loud.length, 0, 'no row is worse than a room where every row is the same');
159
+ // And nothing is silently dead: each row still says its own real age.
160
+ const words = new Set(rows.map((r) => L.ageLabel(r.created_at, NOW)));
161
+ assert.ok(words.size > 1, 'the per-row words still carry the differing ages');
162
+ });
163
+
164
+ test('the pill is kept for the row that is worse than its room', () => {
165
+ const L = lib();
166
+ // Six of eight sitting a few days, two genuinely old. The two are the whole
167
+ // point of the column, and before this they were indistinguishable from the six.
168
+ const rows = room(4, 4, 5, 5, 5, 6, 21, 30);
169
+ const baseline = L.roomBaseline(rows, NOW);
170
+ assert.equal(baseline.band.id, 'aging');
171
+ assert.equal(L.roomAgeLine(baseline), '6 of 8 waiting a few days');
172
+ const loud = rows
173
+ .filter((r) => !L.isRoomQuiet(L.ageBand(r.created_at, NOW), baseline))
174
+ .map((r) => L.ageLabel(r.created_at, NOW));
175
+ assert.deepEqual(loud, ['waiting 3 weeks', 'waiting 4 weeks'], 'only the exceptions keep a chip');
176
+ });
177
+
178
+ test('a mixed room is left exactly as it was', () => {
179
+ const L = lib();
180
+ // No band is dominant here, so nothing is quieted and nothing is announced —
181
+ // which is what keeps this inert on the board as it usually looks.
182
+ const rows = room(0.2, 0.5, 1, 2, 4, 5, 9, 21);
183
+ assert.equal(L.roomBaseline(rows, NOW), null);
184
+ assert.equal(L.roomAgeLine(null), '');
185
+ for (const r of rows) {
186
+ assert.equal(L.isRoomQuiet(L.ageBand(r.created_at, NOW), null), false, 'no baseline quiets nothing');
187
+ }
188
+ });
189
+
190
+ test('a short list keeps every pill, and a calm room announces nothing', () => {
191
+ const L = lib();
192
+ // Three chips can be read one at a time; a wall needs a crowd.
193
+ assert.equal(L.roomBaseline(room(30, 30, 30), NOW), null, 'under the minimum, every row keeps its own');
194
+ assert.equal(L.BASELINE_MIN_ROWS, 4);
195
+ // A room that is uniformly calm has no alarm to move to the head. Announcing
196
+ // one there would invent the noise this removes.
197
+ assert.equal(L.roomBaseline(room(0.1, 0.2, 0.3, 0.4, 0.5), NOW), null, 'all fresh says nothing');
198
+ assert.equal(L.roomBaseline(room(1, 1, 2, 2, 2), NOW), null, 'all waiting-a-day says nothing either');
199
+ });
200
+
201
+ test('the room line and the rows cannot describe the threshold two ways', () => {
202
+ const L = lib();
203
+ // The head's words come from the band's own label, so there is one vocabulary.
204
+ for (const b of L.BANDS) {
205
+ const line = L.roomAgeLine({ band: b, count: 9, total: 12 });
206
+ assert.equal(line, `9 of 12 ${b.label}`, `${b.id} reads in its own words`);
207
+ }
208
+ });
209
+
210
+ test('the quiet treatment is wired through the render, not just defined', () => {
211
+ const js = read('modules', 'hall-ui', 'public', 'collab.js');
212
+ assert.match(js, /function ageChip\(req, nowMs, baseline\)/, 'the chip is told about its room');
213
+ assert.match(js, /L\.isRoomQuiet\(band, baseline\)/, 'through the lib, so the rule is the tested one');
214
+ assert.match(js, /cb-age--quiet/, 'and the class reaches the markup');
215
+ // Over the WHOLE list: a baseline computed on the shown three would flip when
216
+ // a reader pressed "Show all" and re-alarm the rows it had just quieted.
217
+ assert.match(js, /const baseline = L\.roomBaseline\(list, nowMs\)/, 'the room is the whole block');
218
+ const html = read('modules', 'hall-ui', 'public', 'collab.html');
219
+ for (const key of ['for-me', 'mine', 'all']) {
220
+ assert.ok(html.includes(`id="cb-${key}-age"`), `the ${key} head has somewhere to say it`);
221
+ }
222
+ const css = read('modules', 'hall-ui', 'public', 'collab.css');
223
+ assert.match(
224
+ css,
225
+ /\.cb-age\.cb-age--quiet\s*\{[^}]*border-color: transparent/,
226
+ 'the quieted row drops its ring',
227
+ );
228
+ assert.match(css, /\.cb-age\.cb-age--quiet\s*\{[^}]*color: var\(--ink-soft\)/, 'and the danger ink with it');
229
+ });
230
+
139
231
  test('the alarm does not sit beside the accent control', () => {
140
232
  // DESIGN.md spends the accent only on what is pressable or live. On "Your
141
233
  // asks" the age pill (danger ink) used to share the right column with the
@@ -93,6 +93,65 @@ test('generation is deterministic + idempotent (a second run is a no-op)', () =>
93
93
  assert.match(r2.stdout, /already current/);
94
94
  });
95
95
 
96
+ // ── module coverage (task 1004066) ────────────────────────────────────────────
97
+ // The check is a set comparison over a directory listing, and a set comparison breaks by
98
+ // comparing two empty sets and passing. Every case below is written so that a matcher which
99
+ // vouches for everything, or a scan which enumerates nothing, FAILS here.
100
+
101
+ test('every dir under modules/ is documented in file-map.md — and the set being compared is not empty', () => {
102
+ const cov = gen.moduleCoverage();
103
+ assert.ok(cov.onDisk.length > 0, 'modules/ enumerated to nothing — the coverage check would be vacuous');
104
+ assert.deepEqual(cov.defects, [], `module-coverage scan defects: ${cov.defects.join('; ')}`);
105
+ assert.deepEqual(cov.undocumented, [], `modules with no file-map entry: ${cov.undocumented.join(', ')}`);
106
+ });
107
+
108
+ test('--check FAILS (exit 2) on a module with no file-map entry — the trap case', () => {
109
+ const probe = path.join(ROOT, 'modules', '__filemap_probe__');
110
+ try {
111
+ mkdirSync(probe, { recursive: true });
112
+ writeFileSync(path.join(probe, 'module.json'), '{"key":"__filemap_probe__"}\n');
113
+ const cov = gen.moduleCoverage();
114
+ assert.ok(cov.onDisk.includes('__filemap_probe__'), 'the probe module must be enumerated');
115
+ assert.deepEqual(cov.undocumented, ['__filemap_probe__'], 'an undocumented module must be the ONLY finding');
116
+ const r = run(['--check']);
117
+ assert.equal(r.code, 2, 'an undocumented module must fail the gate');
118
+ assert.match(r.stderr, /modules with NO docs\/file-map\.md entry/);
119
+ assert.match(r.stderr, /__filemap_probe__/);
120
+ } finally {
121
+ rmSync(probe, { recursive: true, force: true });
122
+ }
123
+ // Asserted on the module finding rather than the exit code: --check reports every problem class
124
+ // at once, and a checkout carrying unrelated local noise (materialised module skills with no
125
+ // note) would otherwise make this read as "the probe is still there".
126
+ const after = run(['--check']);
127
+ assert.equal(gen.moduleCoverage().undocumented.length, 0, 'coverage is clean once the probe module is removed');
128
+ assert.doesNotMatch(after.stderr || '', /modules with NO docs\/file-map\.md entry/);
129
+ });
130
+
131
+ test('the matcher discriminates: a real module passes, an absent one does not, and a prefix cannot vouch', () => {
132
+ const md = readFileSync(path.join(ROOT, 'docs', 'file-map.md'), 'utf8');
133
+ const block = gen.modulesBlock(md);
134
+ assert.ok(block.length > 0, 'the modules/ tree block must be findable — an empty block would make entries unreadable');
135
+ assert.ok(gen.isModuleDocumented(md, block, 'government'), 'a documented module reads as documented');
136
+ assert.equal(gen.isModuleDocumented(md, block, '__no_such_module__'), false, 'an absent module must NOT read as documented');
137
+ // A bare substring test would let `modules/agents-extra` vouch for `agents`.
138
+ assert.equal(
139
+ gen.isModuleDocumented('see modules/agents-extra/thing.js', '', 'agents'),
140
+ false,
141
+ 'a longer path sharing the prefix must not count as an entry',
142
+ );
143
+ // A skill dir listed in the same `├── name/` shape elsewhere in the map must not vouch either:
144
+ // the entry search is scoped to the modules/ block.
145
+ assert.equal(gen.modulesBlock('├── .claude/skills/\n├── recall/ ← a skill\n'), '', 'no modules/ block → no block to read entries from');
146
+ });
147
+
148
+ test('module coverage reports a SCAN DEFECT rather than passing when the map cannot be read', () => {
149
+ const cov = gen.moduleCoverage('');
150
+ assert.ok(cov.onDisk.length > 0);
151
+ assert.equal(cov.defects.length, 1, 'an unreadable map is a defect, not a pass');
152
+ assert.match(cov.defects[0], /cannot run/);
153
+ });
154
+
96
155
  test('every note maps to a real skill/routine (sidecar has no dangling keys)', () => {
97
156
  const notes = JSON.parse(readFileSync(path.join(ROOT, 'docs', 'file-map.notes.json'), 'utf8'));
98
157
  for (const section of gen.SECTIONS) {
@@ -230,7 +230,11 @@ test('the door and the copy control are on the row, and the copy control is reac
230
230
  // One request can appear in three blocks at once, so the id is block-scoped.
231
231
  assert.match(js, /function askId\(key, id\)/);
232
232
  assert.match(js, /cb-ask-\$\{key\}-/);
233
- assert.match(js, /rowFn\(r, nowMs, false, key\)/, 'and paintBlock hands the key down');
233
+ // A bracket class, not a closing paren: paintBlock also hands the row its
234
+ // block's age baseline (task 1004067), and pinning the exact arity would red
235
+ // this on an argument the invariant — that the KEY reaches the row — does not
236
+ // care about.
237
+ assert.match(js, /rowFn\(r, nowMs, false, key[,)]/, 'and paintBlock hands the key down');
234
238
 
235
239
  const css = read('collab.css');
236
240
  for (const cls of ['.cb-ask ', '.cb-ask__door', '.cb-ask__copy', '.cb-ask__full']) {
@@ -262,8 +266,12 @@ test('the age is a state pill in the row\'s right column, and every band has its
262
266
  assert.match(css, /\.cb-age--stale\s*\{[^}]*var\(--danger\)/, 'stale is the danger ink');
263
267
  assert.match(css, /\.cb-age--aging\s*\{[^}]*var\(--warn\)/, 'aging is the warn ink');
264
268
  const js = read('collab.js');
265
- assert.match(js, /class="fact-pill cb-age cb-age--\$\{band\.id\}"/, 'the pill is the shell\'s fact-pill');
266
- assert.match(js, /<div class="ov-row__right">\$\{ageChip\(req, nowMs\)\}/, 'in the right column, so a glance down the list reads the ages');
269
+ // Both pins stop short of the closing character, because the band's class is
270
+ // now followed by the room's quiet modifier and ageChip by the baseline that
271
+ // decides it (task 1004067). What they assert is unchanged: the shell's pill,
272
+ // in the right column.
273
+ assert.match(js, /class="fact-pill cb-age cb-age--\$\{band\.id\}/, 'the pill is the shell\'s fact-pill');
274
+ assert.match(js, /<div class="ov-row__right">\$\{ageChip\(req, nowMs[,)]/, 'in the right column, so a glance down the list reads the ages');
267
275
  });
268
276
 
269
277
  // ---- the copy desk: quieter, and no fact lost ----------------------------------
@@ -54,6 +54,77 @@ test('`protected` is DERIVED from the imported PROTECTED_GLOBS, never hand-set',
54
54
  assert.ok(prot.length < scope.moduleKeys().length, 'expected at least one non-protected module');
55
55
  });
56
56
 
57
+ // THE PROTECTED SET IS PINNED BY NAME (task 1004070, ADR 0314).
58
+ //
59
+ // The assertion above says the roster contains both kinds; it passes on any
60
+ // twelve. That is the shape a silent unprotection hides in — a glob edit that
61
+ // dropped `migrations/` off `lifecycle` would keep the count and leave the ship
62
+ // pipeline scopeable by anyone. It has already drifted once in the other
63
+ // direction: task 1004070 was written against a re-measurement of 11, and
64
+ // `agents` (protected by `modules/agents/`, itself a PROTECTED_GLOBS entry) was
65
+ // missing from it.
66
+ //
67
+ // So the set is spelled out. A module ARRIVING here is a deliberate line in a
68
+ // diff; a module LEAVING is the one that must never happen quietly.
69
+ const PROTECTED_MODULES = [
70
+ 'agents',
71
+ 'autonomy',
72
+ 'builder-settings',
73
+ 'copy-desk',
74
+ 'dev-box',
75
+ 'devsecops',
76
+ 'government',
77
+ 'grading',
78
+ 'kernel',
79
+ 'lifecycle',
80
+ 'methodology',
81
+ 'onboarding',
82
+ ];
83
+
84
+ test('the protected module set is exactly this, by name', () => {
85
+ assert.deepEqual(
86
+ scope.protectedModules().slice().sort(),
87
+ [...PROTECTED_MODULES].sort(),
88
+ 'the derived protected set changed — if that was intended, edit PROTECTED_MODULES in the same diff and say why',
89
+ );
90
+ });
91
+
92
+ test('the seven trust-core modules can never leave the protected set', () => {
93
+ // Stated separately from the pin above because these are not interchangeable
94
+ // with the rest: their territory IS the trust / ship / authz core, so an edit
95
+ // that unprotects one is a security regression rather than a roster change.
96
+ // ADR 0314 narrows which MODULES derive protected; it must never reach these.
97
+ for (const k of ['kernel', 'lifecycle', 'government', 'grading', 'dev-box', 'devsecops', 'methodology']) {
98
+ assert.equal(scope.scopeForModule(k).protected, true, `${k} must stay protected`);
99
+ }
100
+ });
101
+
102
+ test('narrowing the module roster must not narrow the PATH wall', () => {
103
+ // The other half of ADR 0314's verification, and the one that would make this
104
+ // a false success: whatever happens to the module flag, a sub-Metic builder
105
+ // touching a protected CLI is still refused, by the per-file check.
106
+ // Each file is scoped to the module that OWNS it, deliberately: check() runs
107
+ // scope-subset before the path wall, so scoping them all to one module would
108
+ // get `out_of_scope` and prove nothing about protection. The owning module is
109
+ // what makes the scope wall pass, leaving the path wall as the only thing that
110
+ // can refuse — which is exactly the claim under test.
111
+ const owned = [
112
+ ['onboarding', 'scripts/gds/setup.js'],
113
+ ['autonomy', 'scripts/gds/conductor-dispatch.js'],
114
+ ['copy-desk', 'scripts/gds/copy-inventory.js'],
115
+ ];
116
+ for (const [mod, f] of owned) {
117
+ assert.ok(PROTECTED_GLOBS.includes(f), `fixture assumption: ${f} is a protected surface`);
118
+ assert.equal(gsc.scopeSubset([mod], [f]).ok, true, `fixture assumption: ${mod} owns ${f}`);
119
+ const r = gsc.check([mod], [f], 'thetes');
120
+ assert.equal(r.ok, false, `a sub-Metic builder must still be refused on ${f}`);
121
+ assert.equal(r.code, 'protected_path', `${f} must be refused BY THE PATH WALL, not by scope`);
122
+ // ...and Metic+ passes, so the refusal is the rank floor and not the path
123
+ // being unreachable to everyone.
124
+ assert.equal(gsc.check([mod], [f], 'metic').ok, true, `a Metic must still be able to author on ${f}`);
125
+ }
126
+ });
127
+
57
128
  test('isProtected actually consults PROTECTED_GLOBS (overlap, prefix-aware both ways)', () => {
58
129
  assert.ok(PROTECTED_GLOBS.includes('migrations/'), 'fixture assumption: migrations/ is protected');
59
130
  assert.equal(scope.isProtected(['migrations/']), true, 'a protected glob must compute protected');