@bongos/core 1.19.759 → 1.19.761

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 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.759",
6
- "core_contract": "1.19.759",
7
- "source_commit": "707e9f0b5bf49699402632ba77c655817675f27c",
5
+ "core_version": "1.19.761",
6
+ "core_contract": "1.19.761",
7
+ "source_commit": "3412fa5f1d7bb5d383191d64806396f61b136ae5",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-16T03:03:00.033Z",
9
+ "built_at": "2026-09-16T03:36:29.589Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 483,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2623,
20
- "tree_sha256": "72ddc6e8bfc5b6eb4aa5ab50108bbe35d4639bb91cad6318c0d6359b3c86ceec",
20
+ "tree_sha256": "e9ee270455be1db1f07c7c206e5f9ba6a408a2f231634a18330a70ac78a0ed59",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2512,7 +2512,7 @@
2512
2512
  {
2513
2513
  "path": "docs/module-api-changelog.md",
2514
2514
  "mode": "0000644",
2515
- "sha256": "1c38ce9a7932e516d69df1e3deaf499f2a8e10ef545e8d4621919eef11a14b8a"
2515
+ "sha256": "94b5d8c1844e775937a9acec9a78b5c2a789ed6e8e194835c02cd0d2634a4d29"
2516
2516
  },
2517
2517
  {
2518
2518
  "path": "docs/modules-contract.md",
@@ -7567,12 +7567,12 @@
7567
7567
  {
7568
7568
  "path": "package-lock.json",
7569
7569
  "mode": "0000644",
7570
- "sha256": "d3aebc78f2bcf902f69e9f27bb7375410de5851af241dbaebf7aefe729465f69"
7570
+ "sha256": "172d01adc5b82fa70d7079ec41e84a7a99b89b84c40c58d8d383b45c7a18f584"
7571
7571
  },
7572
7572
  {
7573
7573
  "path": "package.json",
7574
7574
  "mode": "0000644",
7575
- "sha256": "83cc5d9eb22a0201d8a6bca2a74392b23eb9713aa78a651200552a3e2710b273"
7575
+ "sha256": "7f3d59f7939972bb1a32c493f4203e0c22286a3296ae69d9a11a06ed2c5d62a9"
7576
7576
  },
7577
7577
  {
7578
7578
  "path": "public-docs/index.html",
@@ -8242,7 +8242,7 @@
8242
8242
  {
8243
8243
  "path": "scripts/gds/linkify-refs.js",
8244
8244
  "mode": "0000644",
8245
- "sha256": "ad292a1bcfd4c9f3ef93260af1b1a34883b78185ba03d0677b51765165915ea1"
8245
+ "sha256": "73be262f96571e9d35ed83f0c9d27e7652f1dad74016c302f8b24d1d9a724333"
8246
8246
  },
8247
8247
  {
8248
8248
  "path": "scripts/gds/lint-infra-exec.js",
@@ -9352,7 +9352,7 @@
9352
9352
  {
9353
9353
  "path": "src/bongos/serve-internal.js",
9354
9354
  "mode": "0000644",
9355
- "sha256": "f4bba033b2052b61d026682b0a06dd3044d77138e1978ad601060fbebbfdc34d"
9355
+ "sha256": "405b8069c27ea596981cd4a2fd6a57b7e3637022cf63962d46a19b452fa675fc"
9356
9356
  },
9357
9357
  {
9358
9358
  "path": "src/bongos/uptime-poller.js",
@@ -9382,7 +9382,7 @@
9382
9382
  {
9383
9383
  "path": "src/module-api.js",
9384
9384
  "mode": "0000644",
9385
- "sha256": "8ec75227e0ef1df2b932e943d085151cbb47133212aae42c86b425fb664a3c4c"
9385
+ "sha256": "3429cc6b1beee6916f92a274dd2e85be55cb7e10236d8d70c4a4a8cf7ac6387c"
9386
9386
  },
9387
9387
  {
9388
9388
  "path": "src/module-loader/catalog.js",
@@ -11067,7 +11067,7 @@
11067
11067
  {
11068
11068
  "path": "tests/hall_page_gate_map.mjs",
11069
11069
  "mode": "0000644",
11070
- "sha256": "c4da76a78326495bad7393797d47ca076f8ad1f49fc3224fa2be337a3b6f109d"
11070
+ "sha256": "10d8376f94e415d20b112a61e8d1f7f8f0f539eba5f685bdac4509624a69c13e"
11071
11071
  },
11072
11072
  {
11073
11073
  "path": "tests/hall_palette.mjs",
@@ -11457,7 +11457,7 @@
11457
11457
  {
11458
11458
  "path": "tests/linkify_refs.mjs",
11459
11459
  "mode": "0000644",
11460
- "sha256": "ba5164ec77b5541b22b50ed7611dfbbe61731f62b3dfbb678f7de4f77282e918"
11460
+ "sha256": "f4e905823f8c3b7f540d3dbc6a9873198a26e022810b21194b2ae45ef9437954"
11461
11461
  },
11462
11462
  {
11463
11463
  "path": "tests/live_channel.mjs",
@@ -1992,5 +1992,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1992
1992
  landed since 1.19.757 with no explicit bump. run 35049006030. (task 1002620)
1993
1993
  1.19.759 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1994
1994
  landed since 1.19.758 with no explicit bump. run 35050268139. (task 1002620)
1995
+ 1.19.760 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1996
+ landed since 1.19.759 with no explicit bump. run 35052040282. (task 1002620)
1997
+ 1.19.761 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1998
+ landed since 1.19.760 with no explicit bump. run 35052410219. (task 1002620)
1995
1999
  ---------------------------------------------------------------------------
1996
2000
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.759",
3
+ "version": "1.19.761",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.759",
9
+ "version": "1.19.761",
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.759",
3
+ "version": "1.19.761",
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",
@@ -200,6 +200,72 @@ const LIST_GLUE_RE = /^[\s,–—-]*(?:and|&|\+|or|to|through)?[\s,–—-]*$/i;
200
200
  // last. Each site builds its own.
201
201
  const REF_DIGITS = '\\d{1,7}';
202
202
 
203
+ // ---- the ordinal-prose guard (task 1002975) --------------------------------
204
+ //
205
+ // THE BUG. With no context word, a bare `#N` was linkified on ID EXISTENCE ALONE:
206
+ // the prose "side door #1" became a link to task 1, while its sibling "side door
207
+ // #2" survived only because no id 2 exists. That is luck, not grammar, and task
208
+ // 1002974 had to repair a committed doc because of it.
209
+ //
210
+ // THE RULE, AND WHY IT IS A WIDTH RULE. The originating row proposed "a preceding
211
+ // bare noun without task/idea/blocker context words → leave alone". Measured
212
+ // against the actual corpus, that is far too broad: running linkifyText over every
213
+ // committed .md with a maximally permissive id set rewrites 231 context-less refs,
214
+ // 77 of them preceded by a bare word — and 49 of those are GENUINE ("the #506",
215
+ // "by #435", "under #436"). Skipping on the preceding noun alone would leave ~49
216
+ // real refs bare, which is exactly the GitHub-mis-autolink harm this script exists
217
+ // to prevent. So the noun test alone would have traded one defect for a worse one.
218
+ //
219
+ // What DOES separate them in the corpus is WIDTH. All 28 word-preceded refs of 1–2
220
+ // digits are ordinal prose — "§Decision #4" (an ADR section, 13 of them), "limiter
221
+ // #4", "limiter #5", "idea fixture #1", "fixture #4", "ask #1", "and #7", "with
222
+ // #5" — and every word-preceded ref of 3+ digits is a real task. Ordinals in
223
+ // English prose are small; the ids these docs cite are not. Hence: no context
224
+ // word, AND a bare word immediately before, AND at most two digits.
225
+ //
226
+ // Both conjuncts are load-bearing. Drop the width and 49 real refs go bare; drop
227
+ // the preceding-word test and "(#1)" or a line-leading "#7" — which the corpus
228
+ // shows are ordinarily genuine — stop linking too.
229
+ //
230
+ // Known cost, accepted: "PMS-V2 #37" and "the #54" are legacy two-digit refs that
231
+ // now stay bare. Two ambiguous links lost against ~26 false ones fixed, and a bare
232
+ // "#37" with no entity word is ambiguous to a human reader as well.
233
+ //
234
+ // This lands in linkifyText rather than beside it because `.claude/hooks/
235
+ // stop-refcheck.js` classifies by CALLING linkifyText — so the same prose stops
236
+ // being flagged in chat by the same change, with no second copy of the rule.
237
+ // The one context word the guard must stand down for. There is deliberately NO
238
+ // task entry in classifyKind — "task #170" is handled by the DEFAULT path (no
239
+ // kind, id exists → link), because a task is the thing this resolver is for. That
240
+ // leaves "task #3" looking exactly like "limiter #4" to a width rule, and the
241
+ // first cut of this guard ate it. Caught by its own test, which is why the test
242
+ // exists. Not folded into ENTITY_RE: that set means "shares the integer space
243
+ // with tasks but is NOT one", and adding `task` there would neutralize every
244
+ // "task #N" into a code span.
245
+ const TASK_CTX_RE = /\b(tasks?)[`'")\s(:]*$/i;
246
+ // Words that INTRODUCE a reference rather than being the noun it counts. The row
247
+ // asked for "a preceding bare NOUN", and these are not nouns: "see #2" is a real
248
+ // ref and the Stop hook is right to block it in chat, which is how this omission
249
+ // was caught — tests/stop_refcheck.mjs went red on exactly that string.
250
+ //
251
+ // Kept deliberately short, and checked against the corpus rather than guessed: the
252
+ // 1–2 digit word-preceded refs in committed docs are preceded by pms-v2 · task ·
253
+ // the · ask · and · decision · limiter · fixture · sr · with · fixing. None of
254
+ // these six appears there, so exempting them re-breaks nothing that was measured.
255
+ // Words like "the", "and", "with" and "of" are NOT here on purpose — they precede
256
+ // ordinal phrases at least as often as references ("ask #1 and #7" means the
257
+ // questions, not two tasks).
258
+ const ORDINAL_EXEMPT_RE = /\b(see|from|per|via|in|at)[`'")\s(:]*$/i;
259
+ const ORDINAL_MAX_DIGITS = 2;
260
+ // A bare word (letters, then letters/digits/'/-) immediately before the '#',
261
+ // separated by spaces or tabs or nothing at all. Anchored at the end of the left
262
+ // context. The whitespace is OPTIONAL because the corpus contains "SR#11" — a
263
+ // security-review finding number, unspaced and just as much a false positive as
264
+ // the spaced ones. Widening it is safe precisely because the width conjunct is
265
+ // what protects real refs: every word-adjacent ref of 3+ digits is untouched
266
+ // either way, so this only ever admits more of the same two-digit class.
267
+ const ORDINAL_PROSE_LEFT_RE = /[A-Za-z][A-Za-z0-9'-]*[ \t]*$/;
268
+
203
269
  const RESOLVER_LINK_RE = new RegExp(`\\[#(${REF_DIGITS})\\]\\((?:https?:\\/\\/[^/)\\s]+)?\\/builders#\\/task\\/(${REF_DIGITS})\\)`, 'g');
204
270
 
205
271
  // Classify a ref by its left context (+ list-glue carry). Returns one of
@@ -313,6 +379,11 @@ function linkifySegment(segment, ids, leftContext, stats) {
313
379
  } else if (kind === 'github') {
314
380
  // "PR #475" — genuinely GitHub's number space; the bare autolink is CORRECT.
315
381
  skipReason = 'github-namespace';
382
+ } else if (!kind && !TASK_CTX_RE.test(left) && !ORDINAL_EXEMPT_RE.test(left)
383
+ && ORDINAL_PROSE_LEFT_RE.test(left) && num.length <= ORDINAL_MAX_DIGITS) {
384
+ // "side door #1", "limiter #4", "§Decision #4" — a noun followed by a small
385
+ // ordinal, not a ref. See ORDINAL_MAX_DIGITS for the corpus evidence.
386
+ skipReason = 'ordinal-prose';
316
387
  } else if (!kind && !ids.tasks.has(Number(num))) {
317
388
  // No context and not a known task id — too ambiguous to touch (a bare PR
318
389
  // ref, a future task, a numeric color). Left bare.
@@ -1164,8 +1164,16 @@ function mountInternalSurfaces(app) {
1164
1164
  // task 1003172 (C02): `collab` joins the same tier — every read behind it is
1165
1165
  // any-builder and scoped to the caller by the SERVER (GET /help-requests/for-me
1166
1166
  // derives crafts from the viewer's own profile), so signed-in is the right floor.
1167
+ // task 1003629 (idea 1000907): the BARE `task` and `idea` slugs join the tier
1168
+ // their own /:id forms already sit on. /builders/task/:id and /builders/idea/:id
1169
+ // are gated by TASK_DETAIL_PAGE_RE / IDEA_DETAIL_PAGE_RE below, but those
1170
+ // regexes require a numeric id, so the id-less slugs fell through to
1171
+ // BUILDERS_PAGE_RE — which resolves them to the SAME task.html / idea.html
1172
+ // shells — and matched no gate at all. The shell carries no data and every
1173
+ // read behind it is gated server-side, so this closed an inconsistency rather
1174
+ // than an exposure; the point is that one URL cannot be two tiers at once.
1167
1175
  const SIGNEDIN_BUILDERS_PAGE_RE =
1168
- /^\/builders\/(?:work|pair|settings|atlas|modules|goals|roadmap|copy-desk|studio|ideas|collab|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
1176
+ /^\/builders\/(?:work|pair|settings|atlas|modules|goals|roadmap|copy-desk|studio|ideas|collab|task|idea|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
1169
1177
  // The diagram image assets under /builders/diagrams/ ride the same tier as the
1170
1178
  // page that embeds them.
1171
1179
  const SIGNEDIN_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
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.759'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.761'; // 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');
@@ -122,15 +122,15 @@ const ORACLE = {
122
122
  '/builders/drachmae': null,
123
123
  '/builders/profile': null,
124
124
 
125
- // NOT deliberate — recorded because it is true, and filed as idea 1000907
126
- // (task 1003448). The detail pages are addressed by a second segment, and
127
- // /builders/task/:id and /builders/idea/:id are both gated at signed-in; the
128
- // id-less slugs still resolve to the same shells through BUILDERS_PAGE_RE and
129
- // match no gate. No data rides the shell and every read behind it is gated
130
- // server-side, so this is an inconsistency rather than an exposure — but it is
131
- // a row that should turn into a real gate (or a redirect) when that is decided.
132
- '/builders/task': null,
133
- '/builders/idea': null,
125
+ // The bare detail slugs, gated by task 1003629 — the decision idea 1000907
126
+ // (task 1003448) asked for. They resolve to the same task.html / idea.html
127
+ // shells their /:id forms do, so they now sit on the same tier: the id-less
128
+ // form was never a different page, only a different spelling that the
129
+ // id-requiring TASK_DETAIL_PAGE_RE / IDEA_DETAIL_PAGE_RE could not see. A
130
+ // gate, not a redirect, because the shell is a real page at signed-in tier
131
+ // and redirecting would have to invent an id it does not have.
132
+ '/builders/task': 'requireBuilderPage',
133
+ '/builders/idea': 'requireBuilderPage',
134
134
 
135
135
  // Gone, not public — the hall ships no my-projects page at all since task
136
136
  // 1003308 (owner's decision: "my projects" is a user-level page the hub owns,
@@ -311,5 +311,75 @@ t('discoverDocs finds CLAUDE.md, recurses docs/ subdirs (only .md), skips the ge
311
311
  }
312
312
  });
313
313
 
314
+ // ---- the ordinal-prose guard (task 1002975) --------------------------------
315
+ //
316
+ // THE BUG task 1002974 had to repair a committed doc for: with no context word, a
317
+ // bare `#N` was linkified on ID EXISTENCE ALONE. "side door #1" became a link to
318
+ // task 1; its sibling "side door #2" survived only because no id 2 exists. Luck,
319
+ // not grammar.
320
+ //
321
+ // The rule is NOT "a preceding noun means skip" — measured over every committed
322
+ // .md, that would leave ~49 genuine refs bare ("the #506", "by #435"), which is
323
+ // the GitHub-mis-autolink harm this script exists to prevent. It is a WIDTH rule:
324
+ // no context word AND a bare word immediately before AND at most two digits.
325
+ // In the corpus all 28 word-preceded 1–2 digit refs are ordinals; every
326
+ // word-preceded 3+ digit ref is a real task.
327
+
328
+ t('ordinal prose after a noun is left alone — the reported "side door #1"', () => {
329
+ const ids = new Set([1, 2]);
330
+ assert.equal(run('the side door #1 is open', ids), 'the side door #1 is open');
331
+ assert.equal(run('the side door #2 is open', ids), 'the side door #2 is open');
332
+ });
333
+
334
+ t('the ordinals found in our OWN docs are left alone', () => {
335
+ const ids = new Set([1, 4, 5, 11]);
336
+ for (const s of [
337
+ 'joins rate-limit.js as limiter #4: 120 reads',
338
+ 'ADR 0083 §Decision #4 forbids kernel machinery from naming one',
339
+ 'live proof against a local origin (idea fixture #1)',
340
+ 'the edge cookie-strip rule (ADR 0053 / SR#11)', // unspaced — still ordinal prose
341
+ ]) assert.equal(run(s, ids), s, s);
342
+ });
343
+
344
+ t('a WORD-preceded ref of three or more digits still linkifies — the width is what separates them', () => {
345
+ const ids = new Set([506, 435]);
346
+ assert.equal(run('the #506 evaluator', ids), `the ${link(506)} evaluator`);
347
+ assert.equal(run('captured by #435 last night', ids), `captured by ${link(435)} last night`);
348
+ });
349
+
350
+ t('a SMALL ref with no preceding word still linkifies — the noun is half the rule', () => {
351
+ const ids = new Set([7]);
352
+ assert.equal(run('#7 is the row', ids), `${link(7)} is the row`);
353
+ assert.equal(run('see (#7) for detail', ids), `see (${link(7)}) for detail`);
354
+ });
355
+
356
+ t('an explicit entity word still wins over the ordinal guard', () => {
357
+ // "task #3" is a real ref even though it is one digit after a word: classifyKind
358
+ // runs first, so the guard never sees it.
359
+ const ids = new Set([3]);
360
+ assert.equal(run('task #3 sat in blocked for days', ids), `task ${link(3)} sat in blocked for days`);
361
+ });
362
+
363
+ t('a REFERRING word is not a noun — "see #2" still linkifies', () => {
364
+ // The Stop hook classifies by calling linkifyText, so this is also what stops it
365
+ // blocking a real bare ref in chat. tests/stop_refcheck.mjs went red on exactly
366
+ // this string when the first cut of the guard treated every preceding word as a
367
+ // noun — which is how the omission was found.
368
+ const ids = new Set([2]);
369
+ for (const [s, expect] of [
370
+ ['see #2 for details', `see ${link(2)} for details`],
371
+ ['from #2 onward', `from ${link(2)} onward`],
372
+ ['landed in #2 last week', `landed in ${link(2)} last week`],
373
+ ]) assert.equal(run(s, ids), expect, s);
374
+ });
375
+
376
+ t('the guard is reported as its own skip reason, not folded into not-a-task', () => {
377
+ const stats = { skipped: {}, linked: 0, rewritten: 0, unlinked: 0 };
378
+ linkifyText('the side door #1 is open', new Set([1]), stats);
379
+ assert.equal(stats.skipped['ordinal-prose'], 1);
380
+ assert.equal(stats.skipped['not-a-task'], undefined,
381
+ 'an ordinal is skipped BECAUSE it reads as prose, not because the id is missing');
382
+ });
383
+
314
384
  console.log(`\nlinkify_refs: ${passed} passed, ${failed} failed`);
315
385
  process.exit(failed ? 1 : 0);