@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.
- package/.bongos-core.json +26 -26
- package/docs/adr/0177-goal-membership-kind.md +2 -2
- package/docs/adr/0191-majority-and-the-author-who-may-not-decide-alone.md +4 -4
- package/docs/adr/0250-strict-versioning-the-version-boundary-is-the-scope-gate.md +1 -1
- package/docs/copy-inventory.md +29 -31
- package/docs/copy-registry.json +30 -48
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/dom-utils.js +43 -0
- package/modules/hall-ui/public/goals-page.js +10 -3
- package/modules/hall-ui/public/ideas.html +6 -9
- package/modules/hall-ui/public/ideas.js +15 -0
- package/modules/hall-ui/public/work.js +7 -7
- package/modules/lifecycle/lifecycle.js +6 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/docs-entropy.js +10 -0
- package/scripts/gds/linkify-refs.js +81 -4
- package/src/bongos/routes/me.js +8 -0
- package/src/module-api.js +1 -1
- package/tests/linkify_refs.mjs +39 -1
- package/tests/stop_refcheck.mjs +20 -1
- package/tests/task_classifier_vocab.mjs +54 -0
package/src/bongos/routes/me.js
CHANGED
|
@@ -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.
|
|
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');
|
package/tests/linkify_refs.mjs
CHANGED
|
@@ -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
|
});
|
package/tests/stop_refcheck.mjs
CHANGED
|
@@ -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
|
+
});
|