@bongos/core 1.19.664 → 1.19.666

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.
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+ // scripts/hall-preview/task-visual.js — a stand-in ship-time visual for the
3
+ // preview harness (task 1003829).
4
+ //
5
+ // WHY THIS EXISTS. Everything else in this harness is answered from a checked-in
6
+ // JSON fixture, but a ship-time visual is an IMAGE: the API stub answers
7
+ // /api/bongos/* with JSON, so a page that renders <img src="/api/bongos/
8
+ // task-visuals/task-1899-….png"> got a 404 body and a broken frame. The Studio's
9
+ // review card (task 1003829) makes that image the subject of the card, and a
10
+ // surface whose subject cannot be rendered cannot be reviewed — the whole point
11
+ // of this harness.
12
+ //
13
+ // It is a GENERATED placeholder, not a checked-in screenshot: a few flat bands
14
+ // at a real screenshot's aspect ratio, so a reviewer can judge the plate's size,
15
+ // fit, caption and crop without a binary in the repo. Deliberately obvious —
16
+ // nobody should mistake it for a real ship's visual.
17
+ //
18
+ // A ~40-line PNG encoder rather than a dependency, matching the kit's own
19
+ // png.js (a ~50-line DEcoder for the same reason). One IDAT, filter type 0.
20
+ const zlib = require('node:zlib');
21
+
22
+ function chunk(type, data) {
23
+ const len = Buffer.alloc(4);
24
+ len.writeUInt32BE(data.length);
25
+ const body = Buffer.concat([Buffer.from(type, 'ascii'), data]);
26
+ const crc = Buffer.alloc(4);
27
+ crc.writeUInt32BE(crc32(body) >>> 0);
28
+ return Buffer.concat([len, body, crc]);
29
+ }
30
+
31
+ let CRC_TABLE = null;
32
+ function crc32(buf) {
33
+ if (!CRC_TABLE) {
34
+ CRC_TABLE = new Int32Array(256);
35
+ for (let n = 0; n < 256; n++) {
36
+ let c = n;
37
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
38
+ CRC_TABLE[n] = c;
39
+ }
40
+ }
41
+ let c = -1;
42
+ for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
43
+ return c ^ -1;
44
+ }
45
+
46
+ // An RGB PNG from a per-pixel colour function. RGB (colour type 2), 8-bit,
47
+ // non-interlaced — the shape the kit's decoder also speaks.
48
+ function encodeRgb(w, h, at) {
49
+ const stride = w * 3;
50
+ const raw = Buffer.alloc((stride + 1) * h);
51
+ for (let y = 0; y < h; y++) {
52
+ const row = y * (stride + 1);
53
+ raw[row] = 0; // filter: none
54
+ for (let x = 0; x < w; x++) {
55
+ const [r, g, b] = at(x, y);
56
+ const p = row + 1 + x * 3;
57
+ raw[p] = r; raw[p + 1] = g; raw[p + 2] = b;
58
+ }
59
+ }
60
+ const ihdr = Buffer.alloc(13);
61
+ ihdr.writeUInt32BE(w, 0);
62
+ ihdr.writeUInt32BE(h, 4);
63
+ ihdr[8] = 8; ihdr[9] = 2; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
64
+ return Buffer.concat([
65
+ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
66
+ chunk('IHDR', ihdr),
67
+ chunk('IDAT', zlib.deflateSync(raw, { level: 9 })),
68
+ chunk('IEND', Buffer.alloc(0)),
69
+ ]);
70
+ }
71
+
72
+ // The stand-in: a wide plate of flat bands with a paler inset, so its edges,
73
+ // aspect ratio and any crop are all visible at a glance. Cached — the harness
74
+ // may serve it on every paint.
75
+ let CACHED = null;
76
+ function placeholderPng() {
77
+ if (CACHED) return CACHED;
78
+ const W = 1200, H = 700;
79
+ const BANDS = [[38, 34, 30], [74, 66, 58], [176, 96, 62], [214, 200, 184], [246, 243, 238]];
80
+ CACHED = encodeRgb(W, H, (x, y) => {
81
+ const inset = x > W * 0.08 && x < W * 0.92 && y > H * 0.14 && y < H * 0.86;
82
+ const band = BANDS[Math.min(BANDS.length - 1, Math.floor((y / H) * BANDS.length))];
83
+ if (!inset) return band;
84
+ return band.map((c) => Math.min(255, Math.round(c + (255 - c) * 0.42)));
85
+ });
86
+ return CACHED;
87
+ }
88
+
89
+ module.exports = { placeholderPng, encodeRgb };
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.664'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.666'; // 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');
@@ -372,6 +372,112 @@ test('bestMatchingClaim: single claim returned; empty/null → null', () => {
372
372
  assert.equal(dispatch.bestMatchingClaim(null, ['x']), null);
373
373
  });
374
374
 
375
+ // ---------------------------------------------------------------------------
376
+ // Attribution: which claim does this diff belong to? (task 1003834)
377
+ //
378
+ // The regression these pin is not hypothetical — it happened, and the fixture
379
+ // below is the real data from 2026-09-10.
380
+ // ---------------------------------------------------------------------------
381
+
382
+ // The observed failure, verbatim. Reviewing task 1003829's diff from its own
383
+ // worktree, the dispatch announced task 1003704 — a concurrent rename claim
384
+ // whose touches[] are four coarse directory prefixes. The reviewer was briefed
385
+ // on unrelated work and had to reason its way out.
386
+ const CLAIM_1003704 = {
387
+ task_id: '1003704',
388
+ title: 'C3b: rename gds-session.json and the gds_session cookie',
389
+ worktree_name: 'task-1003704',
390
+ touches: ['scripts/gds/', 'src/bongos/', 'modules/', 'clients/'],
391
+ };
392
+ const CLAIM_1003829 = {
393
+ task_id: '1003829',
394
+ title: 'The Studio shows an artist no visuals',
395
+ worktree_name: 'task-1003829',
396
+ touches: ['modules/hall-ui/public/studio.js', 'modules/hall-ui/public/studio.html', 'modules/hall-ui/public/studio.css'],
397
+ };
398
+ // The diff as it actually was: three studio files plus the wider blast radius a
399
+ // real change carries. Under a RAW COUNT, 1003704 matches 8 of these and
400
+ // 1003829 only 3 — which is how the wrong task won.
401
+ const DIFF_1003829 = [
402
+ 'modules/hall-ui/public/studio.js',
403
+ 'modules/hall-ui/public/studio.html',
404
+ 'modules/hall-ui/public/studio.css',
405
+ 'modules/hall-ui/public/hall-kit.js',
406
+ 'modules/hall-ui/public/studio.states.json',
407
+ 'scripts/hall-preview/server.js',
408
+ 'scripts/hall-preview/task-visual.js',
409
+ 'scripts/hall-preview/fixtures/tasks__detail.json',
410
+ 'docs/copy-inventory.md',
411
+ 'docs/copy-registry.json',
412
+ 'tests/hall_studio_world.mjs',
413
+ 'tests/task_visuals.mjs',
414
+ ];
415
+
416
+ test('attribution: the worktree decides, even when another claim out-matches the diff', () => {
417
+ const claims = [CLAIM_1003704, CLAIM_1003829];
418
+ const got = dispatch.attributeClaim(claims, DIFF_1003829, 'task-1003829');
419
+ assert.equal(got.claim.task_id, '1003829', 'the claim BOUND to this worktree wins');
420
+ assert.equal(got.signal, 'worktree');
421
+ // And it wins regardless of claim order, so this cannot pass by accident.
422
+ assert.equal(dispatch.attributeClaim([CLAIM_1003829, CLAIM_1003704], DIFF_1003829, 'task-1003704').claim.task_id, '1003704');
423
+ });
424
+
425
+ test('attribution: specificity beats breadth, so the honest touches[] no longer loses', () => {
426
+ // The same fixture with NO worktree signal — running from the main checkout,
427
+ // or a pre-worktree_name claim. The heuristic alone must now get it right.
428
+ const got = dispatch.attributeClaim([CLAIM_1003704, CLAIM_1003829], DIFF_1003829, null);
429
+ assert.equal(got.claim.task_id, '1003829', 'three exact paths outweigh four directory prefixes');
430
+ assert.equal(got.signal, 'overlap');
431
+ // Proof the OLD scoring is what was wrong: by raw count, 1003704 matches more.
432
+ const rawCount = (c) => DIFF_1003829.filter((f) => (c.touches || []).some((t) => f === t || f.startsWith(t.endsWith('/') ? t : t + '/'))).length;
433
+ assert.ok(rawCount(CLAIM_1003704) > rawCount(CLAIM_1003829), 'the broad claim really does match more files');
434
+ assert.ok(dispatch.claimOverlapScore(CLAIM_1003829, DIFF_1003829) > dispatch.claimOverlapScore(CLAIM_1003704, DIFF_1003829), 'and still scores lower');
435
+ });
436
+
437
+ test('attribution: volume still counts — a genuinely broad claim keeps its diff', () => {
438
+ // Specificity must not invert the heuristic: a rename touching fifty files
439
+ // under scripts/gds/ should not lose to a claim sharing one exact file.
440
+ const broad = { task_id: 'broad', touches: ['scripts/gds/'] };
441
+ const narrow = { task_id: 'narrow', touches: ['scripts/gds/one.js'] };
442
+ const files = Array.from({ length: 50 }, (_, i) => `scripts/gds/f${i}.js`).concat('scripts/gds/one.js');
443
+ assert.equal(dispatch.attributeClaim([narrow, broad], files, null).claim.task_id, 'broad');
444
+ });
445
+
446
+ test('attribution: an empty touches[] is unreachable by overlap — the worktree saves it', () => {
447
+ // Most tasks in this repo declare no touches at all, so they score zero
448
+ // against every diff. That is the other half of why the worktree leads.
449
+ const bare = { task_id: 'bare', worktree_name: 'task-bare', touches: [] };
450
+ assert.equal(dispatch.claimOverlapScore(bare, ['a.js', 'b.js']), 0);
451
+ assert.equal(dispatch.attributeClaim([CLAIM_1003704, bare], ['a.js'], 'task-bare').claim.task_id, 'bare');
452
+ // With no worktree match it cannot win, and the signal says the answer is weak.
453
+ const guess = dispatch.attributeClaim([bare, CLAIM_1003704], ['docs/x.md'], 'somewhere-else');
454
+ assert.equal(guess.signal, 'fallback', 'zero overlap everywhere is reported as no signal, not as a match');
455
+ });
456
+
457
+ test('attribution: a worktree bound to no claim does not borrow one silently', () => {
458
+ const claims = [CLAIM_1003704, CLAIM_1003829];
459
+ const got = dispatch.attributeClaim(claims, DIFF_1003829, 'baseline-main-2');
460
+ assert.equal(got.signal, 'overlap', 'it falls back rather than refusing');
461
+ // …and the header SAYS so, which is the part that would have saved the reader.
462
+ const note = dispatch.attributionNote({ id: got.claim.task_id, attribution: got.signal, claim_count: claims.length });
463
+ assert.match(note, /attribution: guessed from touches\[\] overlap, across 2 open claims/);
464
+ assert.match(note, /discount findings about intent/);
465
+ });
466
+
467
+ test('attribution: the note stays quiet when it would only add noise', () => {
468
+ // Certain attribution, and the single-claim case, have nothing to warn about.
469
+ assert.equal(dispatch.attributionNote({ id: '1', attribution: 'worktree', claim_count: 4 }), '');
470
+ assert.equal(dispatch.attributionNote({ id: '1', attribution: 'overlap', claim_count: 1 }), '');
471
+ assert.equal(dispatch.attributionNote({ id: null, attribution: 'overlap', claim_count: 9 }), '');
472
+ });
473
+
474
+ test('attribution: claimForWorktree ignores claims with no recorded worktree', () => {
475
+ // Pre-column claims carry null; they must not match an absent cwd name.
476
+ assert.equal(dispatch.claimForWorktree([{ task_id: 'x', worktree_name: null }], 'task-x'), null);
477
+ assert.equal(dispatch.claimForWorktree([{ task_id: 'x', worktree_name: 'task-x' }], null), null);
478
+ assert.equal(dispatch.claimForWorktree(null, 'task-x'), null);
479
+ });
480
+
375
481
  test('isCovered now delegates to the canonical matcher (still correct)', () => {
376
482
  // Regression guard for the dedup: isCovered is a thin wrapper over
377
483
  // path-match.matchOne (relocated from the deleted touches-scan in task 879),