@bongos/core 1.19.692 → 1.19.694

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.
@@ -433,6 +433,14 @@ module.exports = function buildMeRouter() {
433
433
  // builder is uncapped (no cap set — the default; ADR 0085), in which case
434
434
  // level is always 'ok'. The whole object is null only if the read errored.
435
435
  cost: costStatus,
436
+ // task 1003453: the task-kind vocabulary, so a surface that offers a kind
437
+ // filter BUILDS it from the enum instead of retyping it. The board and the
438
+ // goals page each carried their own hardcoded copy, and both had gone stale
439
+ // against the DB — 'verify' existed, was claimable, and could not be
440
+ // filtered for. The lifecycle port's VALID_KINDS is the SSOT; [] when the
441
+ // lifecycle module is off, which reads as "no vocabulary served" and leaves
442
+ // the filter with just its '— any —' row rather than a wrong list.
443
+ task_kinds: lc() ? lc().TASK_KINDS : [],
436
444
  // task 1268: the per-builder "wandering" knob, resolved to an effective
437
445
  // level (clamped by rank) + its behavior contract. The Conductor + the
438
446
  // /settings UI read this; the builder-settings port's describeWandering is
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.692'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.694'; // 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');
@@ -13,7 +13,7 @@ import { strict as assert } from 'node:assert';
13
13
  import { createRequire } from 'node:module';
14
14
 
15
15
  const require = createRequire(import.meta.url);
16
- const { linkifyText, RESOLVER_BASE, RESOLVER_BASE_IDEA, RESOLVER_BASE_BLOCKER, discoverDocs } =
16
+ const { linkifyText, RESOLVER_BASE, RESOLVER_BASE_IDEA, RESOLVER_BASE_BLOCKER, discoverDocs, isLoopbackOrigin } =
17
17
  require('../scripts/gds/linkify-refs.js');
18
18
 
19
19
  let passed = 0;
@@ -31,6 +31,44 @@ t('a bare task ref is linkified', () => {
31
31
  assert.equal(run('shipped in #475 today'), `shipped in ${link(475)} today`);
32
32
  });
33
33
 
34
+ // task 1003542: every live id on this instance is SEVEN digits — imported tasks were
35
+ // renumbered +1,000,000 — but the matcher was \d{1,5}, a PRE-IMPORT width. So the
36
+ // --check CI gate passed trivially on a doc containing a bare #1003365: it matched
37
+ // nothing and reported clean, which looks exactly like a gate that works.
38
+ t('a bare SEVEN-digit task ref is linkified — the width every live id here has', () => {
39
+ const ids = new Set([1003365]);
40
+ assert.equal(run('the fix landed in #1003365.', ids), `the fix landed in ${link(1003365)}.`);
41
+ });
42
+
43
+ t('an EIGHT-digit number is still left bare — widening did not become "match anything"', () => {
44
+ // The bound still exists: a long bare number in prose must not be rewritten into
45
+ // a resolver link just because it is long.
46
+ assert.equal(run('the run took 10033650 ms', new Set([1003365])), 'the run took 10033650 ms');
47
+ });
48
+
49
+ // task 1003542: --apply WRITES these links into committed markdown, so the resolver
50
+ // base must never be a dev origin. config/branding.neutral.json — the neutral core's
51
+ // own default — sets buildersOrigin to http://localhost:3000, so before this guard a
52
+ // core checkout rewrote refs into http://localhost:3000/builders#/task/N. It only
53
+ // stayed hidden because the matcher was too narrow to rewrite anything here; widening
54
+ // it in this task is what made the leak reachable.
55
+ t('a loopback origin is refused as a resolver base (root-relative instead)', () => {
56
+ for (const o of ['http://localhost:3000', 'https://127.0.0.1:8443', 'http://0.0.0.0:80',
57
+ 'http://[::1]:3000', 'http://app.localhost:3000']) {
58
+ assert.equal(isLoopbackOrigin(o), true, `${o} must not be written into a committed doc`);
59
+ }
60
+ });
61
+
62
+ t('a real instance origin is still used as the resolver base', () => {
63
+ for (const o of ['https://cloudbongos.com', 'https://hermeslines-marketing.cloudbongos.com']) {
64
+ assert.equal(isLoopbackOrigin(o), false, `${o} is a real host and must survive`);
65
+ }
66
+ // Not a parseable absolute URL — the caller already handles '' / root-relative,
67
+ // and this must not throw on the way past.
68
+ assert.equal(isLoopbackOrigin('/builders#'), false);
69
+ assert.equal(isLoopbackOrigin(''), false);
70
+ });
71
+
34
72
  t('a parenthesized task ref is linkified (these are task refs here, not PRs)', () => {
35
73
  assert.equal(run('R01-followup (#308, 2026-05-13)'), `R01-followup (${link(308)}, 2026-05-13)`);
36
74
  });
@@ -45,11 +45,30 @@ function t(name, fn) {
45
45
 
46
46
  // 286/526/870 are "real task ids" for this test; 5/84 stand in for ids that may
47
47
  // also be a blocker/migration number (entity classification ignores id membership).
48
- const IDS = new Set([5, 84, 170, 286, 526, 870]);
48
+ const IDS = new Set([5, 84, 170, 286, 526, 870, 1003365]);
49
49
  const ev = (s) => evaluate(s, IDS);
50
50
 
51
+ // task 1003542: 1003365 is the SEVEN-digit shape every live id on this instance
52
+ // has, after imported tasks were renumbered +1,000,000. Both enforcement points
53
+ // matched only \d{1,5} — a PRE-IMPORT width — so this hook fast-path-exited on
54
+ // REF_RE before it even fetched the allow-list, and the turn-end nudge could
55
+ // never fire for a real id. A guard that matches nothing reports clean, which is
56
+ // indistinguishable from a guard that works, so the width needs a case of its own.
57
+
51
58
  // ---- blocks (a bare ref that would 404 in chat) ----------------------------
52
59
 
60
+ t('a bare SEVEN-digit task ref blocks — the width every live id on this instance has', () => {
61
+ const r = ev('the fix for that landed in #1003365.');
62
+ assert.equal(r.block, true, 'a 7-digit bare ref must be caught, not fast-path-skipped');
63
+ assert.ok(r.refs.includes('#1003365'), 'and must be surfaced by its full 7 digits');
64
+ });
65
+
66
+ t('an EIGHT-digit number is still left alone — widening did not become "match anything"', () => {
67
+ // The upper bound still exists. Nothing on this instance is 8 digits, and a long
68
+ // bare number in prose (a timestamp, an amount) must not start blocking turns.
69
+ assert.equal(ev('the run took 10033650 ms').block, false);
70
+ });
71
+
53
72
  t('a bare task ref blocks', () => {
54
73
  const r = ev('So the only genuine C9 code remaining is landing #286.');
55
74
  assert.equal(r.block, true);
@@ -5,8 +5,12 @@
5
5
  // drive the pure builder + the generic core rule with no disk I/O.
6
6
  import assert from 'node:assert/strict';
7
7
  import { test } from 'node:test';
8
+ import { readFileSync, readdirSync } from 'node:fs';
9
+ import { createRequire } from 'node:module';
8
10
  import classifier from '../modules/lifecycle/task-classifier.js';
9
11
 
12
+ const require = createRequire(import.meta.url);
13
+
10
14
  const { classifyTaskKind, buildBlockerVendorRe } = classifier;
11
15
 
12
16
  test('buildBlockerVendorRe: empty / non-array → null (vanilla has no vendors)', () => {
@@ -39,3 +43,53 @@ test('generic blocker terms still classify without any vendor config', () => {
39
43
  assert.equal(classifyTaskKind('Unblock the release', '').kind, 'blocker-resolution');
40
44
  assert.equal(classifyTaskKind('Rotate the API key', '').kind, 'blocker-resolution');
41
45
  });
46
+
47
+ // ---- the kind vocabulary is ONE list, and everything reads it (task 1003453) --
48
+ //
49
+ // The board's Kind filter hardcoded its options and had gone stale: 'verify' was
50
+ // added to VALID_KINDS (and to the DB CHECK by migration 187) but never to the
51
+ // filter, so verify tasks existed, were claimable, and could not be filtered for.
52
+ // The goals page carried a second copy of the same stale list. Both now build the
53
+ // lens from the vocabulary the server serves, and these pin every hop of that.
54
+
55
+ test('the DB CHECK constraint lists exactly VALID_KINDS', () => {
56
+ // Migration 187's own header records this drift in the other direction — the
57
+ // app gained 'verify', the CHECK did not, and POST /tasks 500'd for months.
58
+ // Comparing the two is the guard neither side had.
59
+ const sql = readFileSync(new URL('../migrations/187_tasks_kind_check_add_verify.sql', import.meta.url), 'utf8');
60
+ const inClause = /CHECK \(kind IN \(([\s\S]*?)\)\)/.exec(sql);
61
+ assert.ok(inClause, 'the CHECK constraint is still written as `kind IN (...)`');
62
+ const fromSql = [...inClause[1].matchAll(/'([a-z-]+)'/g)].map((m) => m[1]);
63
+ assert.deepEqual([...fromSql].sort(), [...classifier.VALID_KINDS].sort(),
64
+ 'the DB CHECK and VALID_KINDS must name the same kinds — this is the drift migration 187 exists to record');
65
+ });
66
+
67
+ test('the lifecycle port serves the vocabulary as a JSON-safe array', () => {
68
+ const lifecycle = require('../modules/lifecycle/lifecycle.js');
69
+ const kinds = lifecycle.TASK_KINDS;
70
+ assert.ok(Array.isArray(kinds), 'an array — a Set does not survive JSON.stringify, which is how it reaches the hall');
71
+ assert.deepEqual([...kinds].sort(), [...classifier.VALID_KINDS].sort());
72
+ assert.ok(kinds.includes('verify'), 'the kind this whole task is about');
73
+ assert.notEqual(kinds, lifecycle.TASK_KINDS, 'a fresh copy each read — a caller must not be able to mutate the vocabulary');
74
+ });
75
+
76
+ // Scans the WHOLE surface — every .js AND .html — because the first cut of this
77
+ // test read .js only and matched a JS array literal, so it could not see the third
78
+ // copy: a static <option> list in ideas.html carrying the identical stale
79
+ // vocabulary. A guard that cannot see one of the places the bug lives is worse
80
+ // than no guard, because it is read as coverage. The grader caught that; this is
81
+ // the shape that would have caught it first.
82
+ test('no hall page carries its own copy of the kind list', () => {
83
+ const dir = new URL('../modules/hall-ui/public/', import.meta.url);
84
+ const pages = readdirSync(dir).filter((f) => f.endsWith('.js') || f.endsWith('.html'));
85
+ assert.ok(pages.length > 20, 'the surface was found — an empty scan must not pass silently');
86
+ // Three kinds in sequence is the signature of a spelled-out vocabulary, in a JS
87
+ // array ('cleanup', 'decision', …) or in markup (value="cleanup" … value="decision").
88
+ const SPELLED = /(['"])cleanup\1[\s\S]{0,80}?(['"])decision\2[\s\S]{0,120}?(['"])blocker-resolution\3/;
89
+ for (const page of pages) {
90
+ if (page === 'dom-utils.js') continue; // the one filler is allowed to name them
91
+ const src = readFileSync(new URL(page, dir), 'utf8');
92
+ assert.doesNotMatch(src, SPELLED,
93
+ `${page} spells out the kind vocabulary — fill it from /me task_kinds via OTB.fillKindFilter instead`);
94
+ }
95
+ });