@bongos/core 1.19.693 → 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 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.693",
6
- "core_contract": "1.19.693",
7
- "source_commit": "64897221c9ad47833068dacf7644c0261cbc526b",
5
+ "core_version": "1.19.694",
6
+ "core_contract": "1.19.694",
7
+ "source_commit": "7cd0b705f047458dfa2038bd842adee75333a643",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-13T01:48:28.390Z",
9
+ "built_at": "2026-09-13T01:54:36.629Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 477,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2628,
20
- "tree_sha256": "8d4afe14f879514c8da9a3f8309944b8d40be680675f2a22a0d6a70d69cb64a8",
20
+ "tree_sha256": "0ab80bfe776899e5734f702ca69263e38455dae234ba01b3d422d6ef131e6c49",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -1387,7 +1387,7 @@
1387
1387
  {
1388
1388
  "path": "docs/adr/0177-goal-membership-kind.md",
1389
1389
  "mode": "0000644",
1390
- "sha256": "922dc12e699d3631907292e0a5550806ac88a160394c65c8223f65e58a46bc6f"
1390
+ "sha256": "d1005ba9359abceca6481a5e25b39579169fe7f63a87769f4cfd450419755343"
1391
1391
  },
1392
1392
  {
1393
1393
  "path": "docs/adr/0178-the-copy-desk-flag-and-queue.md",
@@ -1467,7 +1467,7 @@
1467
1467
  {
1468
1468
  "path": "docs/adr/0191-majority-and-the-author-who-may-not-decide-alone.md",
1469
1469
  "mode": "0000644",
1470
- "sha256": "0ae7f6a69d9006c54210cea4f6a846445a6181f148bcb4de979011b46cca5188"
1470
+ "sha256": "ffdf5efeddb55e4e4378a67e2d06fe3038074b9c0afc90ef8e69214a381c3e58"
1471
1471
  },
1472
1472
  {
1473
1473
  "path": "docs/adr/0192-a-category-orients-and-authorises-nothing.md",
@@ -1762,7 +1762,7 @@
1762
1762
  {
1763
1763
  "path": "docs/adr/0250-strict-versioning-the-version-boundary-is-the-scope-gate.md",
1764
1764
  "mode": "0000644",
1765
- "sha256": "da4549c19609d1f549e6f5a57b530c330765f1778f0a9b09a15a208f4575f986"
1765
+ "sha256": "13b4d7c6e33ad7e2be2b57ac7805bc4ae5dcdf34f9063aa799ef456f59d50e6a"
1766
1766
  },
1767
1767
  {
1768
1768
  "path": "docs/adr/0251-a-suggestion-of-who-to-invite-is-a-recruiting-surface-and-owes-the-opt-out.md",
@@ -2802,7 +2802,7 @@
2802
2802
  {
2803
2803
  "path": "docs/module-api-changelog.md",
2804
2804
  "mode": "0000644",
2805
- "sha256": "f7893e7defc9a2b255d4c3377037279fbc74ddd0900ca84eb23a9ff8d5fc8c5d"
2805
+ "sha256": "578d0d708917e956889cfbd990258b1cda804052474f07087d9af589f387425e"
2806
2806
  },
2807
2807
  {
2808
2808
  "path": "docs/modules-contract.md",
@@ -7782,12 +7782,12 @@
7782
7782
  {
7783
7783
  "path": "package-lock.json",
7784
7784
  "mode": "0000644",
7785
- "sha256": "37f563e1f70d64e638bb3ea45f7808f53a0f5a405c81f78ff9f818f75122f19f"
7785
+ "sha256": "929bc69d15f44bc6648df7b2f3a9e22d72501486ae2abb9b0491cfcfabbd6683"
7786
7786
  },
7787
7787
  {
7788
7788
  "path": "package.json",
7789
7789
  "mode": "0000644",
7790
- "sha256": "059bbd9276dad23f7d647e3a93d5886abaaf7415c6056cffce375153bce2d8f7"
7790
+ "sha256": "cc0356495a5cf8f4822c0e6efff4b17ef7a1dcd69da2f724a5089b99be71aea7"
7791
7791
  },
7792
7792
  {
7793
7793
  "path": "public-docs/index.html",
@@ -8187,7 +8187,7 @@
8187
8187
  {
8188
8188
  "path": "scripts/gds/docs-entropy.js",
8189
8189
  "mode": "0000644",
8190
- "sha256": "a4357b36e7e005895021f55db27f83d40e7fded3534ed9ee3d50162945e2cea7"
8190
+ "sha256": "06608b947a6593f7e7385eee68ab011015480418254373cab8e3d557643857c1"
8191
8191
  },
8192
8192
  {
8193
8193
  "path": "scripts/gds/doctor.js",
@@ -8417,7 +8417,7 @@
8417
8417
  {
8418
8418
  "path": "scripts/gds/linkify-refs.js",
8419
8419
  "mode": "0000644",
8420
- "sha256": "7f24e01984e5f010eaad1f572d9e29cc1c62747bd15ff73c202d3febcd23cf2a"
8420
+ "sha256": "1eecbbc7a090450f01129335b710f984b5eac1c20271247f03f7cc98e62c697e"
8421
8421
  },
8422
8422
  {
8423
8423
  "path": "scripts/gds/lint-infra-exec.js",
@@ -9552,7 +9552,7 @@
9552
9552
  {
9553
9553
  "path": "src/module-api.js",
9554
9554
  "mode": "0000644",
9555
- "sha256": "bfab1f6315f23c5d1d0ec1e97b4c90ac8f2fa0ce632c39ac15473209076df0fb"
9555
+ "sha256": "60f2d47936df6dee6b06ea525d1a6d397d563ed6ee3e052b566936b6e5515af8"
9556
9556
  },
9557
9557
  {
9558
9558
  "path": "src/module-loader/catalog.js",
@@ -11527,7 +11527,7 @@
11527
11527
  {
11528
11528
  "path": "tests/linkify_refs.mjs",
11529
11529
  "mode": "0000644",
11530
- "sha256": "4a78d21f4f27443bc923bd4b4b114198702e6ccb4e4cd3c2df61c906f405c4c2"
11530
+ "sha256": "ba5164ec77b5541b22b50ed7611dfbbe61731f62b3dfbb678f7de4f77282e918"
11531
11531
  },
11532
11532
  {
11533
11533
  "path": "tests/live_channel.mjs",
@@ -12822,7 +12822,7 @@
12822
12822
  {
12823
12823
  "path": "tests/stop_refcheck.mjs",
12824
12824
  "mode": "0000644",
12825
- "sha256": "a95e75e1cd3fe6cb6d48a8a082a8d2339eaae8589e5091fe83d88e6f106cc717"
12825
+ "sha256": "8489e33aa194a414d591e76a0e5d40b5131cab61172771b467469ecb9b9203ba"
12826
12826
  },
12827
12827
  {
12828
12828
  "path": "tests/strand_watch.mjs",
@@ -101,7 +101,7 @@ that does NOT carry over.**
101
101
 
102
102
  **Decision 3′ — this rename keeps a compatibility window, and the sender writes BOTH
103
103
  spellings.** This is a deliberate, named divergence from decision 3 above *and* from the
104
- sequence task #1003090 itself prescribed ("the API field renamed with no alias"); the
104
+ sequence task [#1003090](/builders#/task/1003090) itself prescribed ("the API field renamed with no alias"); the
105
105
  task wrote that sequence before anyone had looked at who calls the endpoint. Decision 3
106
106
  holds wherever every caller ships in one release, which is why it stays the default — and
107
107
  it does not hold here. The goal-membership rename could ship without an alias because every
@@ -120,7 +120,7 @@ gates *per instance*). So:
120
120
  - the trigger therefore maps **values as well as columns**, in both directions, which
121
121
  `core_220`'s did not have to.
122
122
 
123
- **And a trap both triggers share, found by the grader on #1003090.** The INSERT branch has
123
+ **And a trap both triggers share, found by the grader on [#1003090](/builders#/task/1003090).** The INSERT branch has
124
124
  to tell "the caller wrote the new column" from "the column's DEFAULT filled it" — and it
125
125
  cannot, if the new column has a DEFAULT: **Postgres materializes a column default into
126
126
  `NEW` before any BEFORE ROW trigger fires.** With a default in place, an old-release
@@ -21,7 +21,7 @@ single click.** Two independent facts made that invisible:
21
21
  *no ballot at all* — so the Board Room showed nine sittings and no way to vote
22
22
  on any of them. From the owner's seat the feature genuinely did not exist.
23
23
  2. **Nobody else had been told.** The Discord announcer was the only summons, and
24
- it is best-effort by construction. That half is fixed separately in #1003271.
24
+ it is best-effort by construction. That half is fixed separately in [#1003271](/builders#/task/1003271).
25
25
 
26
26
  The lesson worth keeping: **a rule enforced by removing the control is
27
27
  indistinguishable from a broken feature.** The anti-self rule was correct in
@@ -100,7 +100,7 @@ both sides.
100
100
 
101
101
  ## 4. The clock is a consequence of majority, not an independent fix
102
102
 
103
- #1003271 was originally scoped to add a sitting clock, because nine items with
103
+ [#1003271](/builders#/task/1003271) was originally scoped to add a sitting clock, because nine items with
104
104
  `window_minutes: null` could never lapse. **Implementing that would have destroyed
105
105
  the very ideas it was meant to unstick**, and the reason is one line — `tallyOutcome`:
106
106
 
@@ -123,7 +123,7 @@ Under majority on a four-member board, **three affirmative votes are needed for
123
123
  anything to pass.** That is a real bar, and on a quiet board the honest outcome of
124
124
  an idea nobody engages with is now *returned* rather than silently ratified. That
125
125
  is the point of the owner's choice — but it means a generous window and a working
126
- summons (#1003271) are load-bearing, not cosmetic. A majority board with no
126
+ summons ([#1003271](/builders#/task/1003271)) are load-bearing, not cosmetic. A majority board with no
127
127
  summons would return everything.
128
128
 
129
129
  **A solo instance still cannot ratify its own ideas.** One member who is also the
@@ -138,5 +138,5 @@ wider, not a bootstrap exception.
138
138
 
139
139
  Voting stays hall-only. ADR 0175 §8's argument is untouched by anything here: an
140
140
  objection must carry a reason and name the section it faults, and a reaction cannot
141
- express that. The owner re-affirmed this on 2026-08-25 when asked directly. #1003271
141
+ express that. The owner re-affirmed this on 2026-08-25 when asked directly. [#1003271](/builders#/task/1003271)
142
142
  improves the *notification* and leaves the ballot where it is.
@@ -2,7 +2,7 @@
2
2
 
3
3
  - **Status:** Accepted
4
4
  - **Date:** 2026-09-06
5
- - **Tasks:** [#1003589](https://cloudbongos.com/builders#/task/1003589) (this decision), seeded by the planning session [#1003586](https://cloudbongos.com/builders#/task/1003586). The implementation is BV1.R03–R30 under the same goal.
5
+ - **Tasks:** [#1003589](https://cloudbongos.com/builders#/task/1003589) (this decision), seeded by the planning session `#1003586`. The implementation is BV1.R03–R30 under the same goal.
6
6
  - **Goal:** [#1000086](https://cloudbongos.com/builders#/goal/1000086) — Strict versioning: a goal cannot outlive its tasks, and a version's goal set is closed.
7
7
  - **Builds on:** [ADR 0086](<redacted>.md) (Project → Version → Goal → Criteria → Task — the hierarchy this decision finally closes at the top), [ADR 0015](<redacted>.md) (deps are load-bearing, the rank prefix is decorative), [ADR 0183](<redacted>.md) (a criterion satisfies itself when its tasks ship).
8
8
  - **Amends:** [ADR 0157](<redacted>.md) — for one key, `version.create`. See §6.
@@ -1845,5 +1845,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1845
1845
  landed since 1.19.691 with no explicit bump. run 34730821372. (task 1002620)
1846
1846
  1.19.693 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1847
1847
  landed since 1.19.692 with no explicit bump. run 34731402927. (task 1002620)
1848
+ 1.19.694 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1849
+ landed since 1.19.693 with no explicit bump. run 34731674808. (task 1002620)
1848
1850
  ---------------------------------------------------------------------------
1849
1851
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.693",
3
+ "version": "1.19.694",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.693",
9
+ "version": "1.19.694",
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.693",
3
+ "version": "1.19.694",
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",
@@ -160,6 +160,16 @@ function parseFile(absPath) {
160
160
  const href = m[2].trim();
161
161
  // Skip external URLs, mailto, and obvious non-paths (e.g. ellipsis placeholders)
162
162
  if (/^(https?:|mailto:|ftp:)/.test(href)) continue;
163
+ // The hall deep-link resolver in its ROOT-RELATIVE form (task 1003542). The
164
+ // absolute form is already skipped above as external; linkify-refs.js emits
165
+ // this one whenever the instance origin is absent or loopback — which is now
166
+ // the normal case on a core checkout, since writing http://localhost:3000 into
167
+ // a committed doc is the worse bug. It is an APP ROUTE, not a file path, so
168
+ // counting it as a stale link is a false positive: 28 of them appeared the
169
+ // moment the linkifier could see this instance's 7-digit ids.
170
+ // Deliberately NOT a blanket `^/` skip — a genuinely wrong root-relative file
171
+ // link should still be reported as rot.
172
+ if (/^\/builders#/.test(href)) continue;
163
173
  if (/^\.{2,}$/.test(href)) continue; // skip ... or .. used as placeholders
164
174
  links.push({ href, line: lineNo });
165
175
  }
@@ -57,12 +57,45 @@ const path = require('node:path');
57
57
  // `https://amazonprimea.com`). When branding is unreadable the base is the
58
58
  // root-relative '/builders#/…' — never a hardcoded founder host. Reading branding is
59
59
  // a sync file read (no network), safe at module scope for this token-free CI gate.
60
+ // A LOOPBACK origin is treated exactly like an unreadable one — root-relative,
61
+ // never written out (task 1003542; the committed-doc half of task 1002975).
62
+ //
63
+ // The header above promises a per-dev localhost override "must never leak into a
64
+ // committed doc link", and the API_BASE half of that holds. But the BRANDING half
65
+ // did not: config/branding.neutral.json — the neutral core's own default, used by
66
+ // every checkout with no instance branding — sets buildersOrigin to
67
+ // http://localhost:3000. So `--apply` on a core checkout rewrote bare refs into
68
+ // http://localhost:3000/builders#/task/N and committed them.
69
+ //
70
+ // That was survivable only while the gate was blind: it matched \d{1,5} and this
71
+ // instance's ids are 7 digits, so --apply found nothing to rewrite. Widening the
72
+ // width in this task ARMS it — the first honest run wanted to rewrite 33 refs
73
+ // across 10 committed docs, ADRs included. Hence this guard lands with the widening
74
+ // rather than after it.
75
+ //
76
+ // Root-relative is the right fallback, not a guess: it is what the catch below
77
+ // already returns, and it resolves correctly on whatever host actually serves the
78
+ // hall — which is more than a baked absolute origin can promise.
79
+ function isLoopbackOrigin(origin) {
80
+ try {
81
+ // URL keeps an IPv6 literal in its brackets ('[::1]'), so strip them before
82
+ // comparing — a caught-by-its-own-test detail, not a hypothetical.
83
+ const host = new URL(origin).hostname.replace(/^\[|\]$/g, '');
84
+ return host === 'localhost' || host === '::1' || host === '0.0.0.0'
85
+ || host.endsWith('.localhost')
86
+ || /^127\./.test(host); // the whole 127.0.0.0/8 loopback block, not just .0.1
87
+ } catch (_) {
88
+ return false; // not a parseable absolute URL → not a loopback origin
89
+ }
90
+ }
91
+
60
92
  function resolverHallRoot() {
61
93
  try {
62
94
  const d = (require('../../src/branding').branding().domains) || {};
63
95
  const origin = (typeof d.buildersOrigin === 'string' && d.buildersOrigin.trim())
64
96
  || (typeof d.publicOrigin === 'string' && d.publicOrigin.trim())
65
97
  || '';
98
+ if (!origin || isLoopbackOrigin(origin)) return '/builders#';
66
99
  return `${String(origin).replace(/\/+$/, '')}/builders#`;
67
100
  } catch (_) {
68
101
  return '/builders#';
@@ -94,8 +127,24 @@ function discoverDocs() {
94
127
  }
95
128
  };
96
129
  for (const root of ['CLAUDE.md', 'README.md', 'docs']) walk(root);
130
+ // The skip marker matched only the literal "GENERATED FILE" — and NO generator in
131
+ // this repo writes that (task 1003542). gen-api-docs.js heads both of its outputs
132
+ // with "GENERATED by scripts/gds/gen-api-docs.js", so docs/api-reference.md and
133
+ // docs/routes-permissions.md were being linkified despite the rule above: their
134
+ // rewrites survive exactly until the next regen, and meanwhile ship-check reports
135
+ // api-docs STALE. Two gates fighting over one file, each correctly.
136
+ //
137
+ // Latent until now for the same reason as the loopback leak: the matcher was too
138
+ // narrow to rewrite anything, so nothing was ever written into a generated file.
139
+ // Widening it in this task is what surfaced this.
140
+ //
141
+ // NOTE docs/copy-inventory.md heads with "**Generated** by" — bolded, so not
142
+ // contiguous and still unmatched here. Left alone deliberately: it carries no bare
143
+ // refs today, and inventing a looser pattern risks skipping a hand-authored doc
144
+ // that merely mentions being generated. Fix its marker at the generator if it ever
145
+ // grows one.
97
146
  return out.filter((rel) =>
98
- !/GENERATED FILE/i.test(fs.readFileSync(path.resolve(process.cwd(), rel), 'utf8').slice(0, 200))
147
+ !/GENERATED (?:FILE|by)\b/i.test(fs.readFileSync(path.resolve(process.cwd(), rel), 'utf8').slice(0, 200))
99
148
  );
100
149
  }
101
150
 
@@ -131,7 +180,27 @@ const LIST_GLUE_RE = /^[\s,–—-]*(?:and|&|\+|or|to|through)?[\s,–—-]*$/i;
131
180
  // '/builders#/…', so the mislink-cleanup path still recognizes a link a PRIOR run
132
181
  // committed under a DIFFERENT host — the legacy `amazonprimea.com` links, a
133
182
  // co-tenant's, or this instance's own — not only the current RESOLVER_BASE host.
134
- const RESOLVER_LINK_RE = /\[#(\d{1,5})\]\((?:https?:\/\/[^/)\s]+)?\/builders#\/task\/(\d{1,5})\)/g;
183
+ // How many digits a bare "#NNN" may carry — ONE source of truth, because the
184
+ // width had drifted out of date in five places at once (task 1003542).
185
+ //
186
+ // It was {1,5}, a PRE-IMPORT id width. This instance renumbered imported tasks
187
+ // +1,000,000, so every live id is SEVEN digits — and both enforcement points
188
+ // silently stopped enforcing: `linkify-refs.js --check` (the CI gate) passed
189
+ // trivially on a doc containing a bare #1003365, and .claude/hooks/stop-refcheck.js
190
+ // fast-path-exited before it even fetched the allow-list. A guard that matches
191
+ // nothing reports clean, which is indistinguishable from a guard that works.
192
+ //
193
+ // 7 keeps the property {1,5} was chosen for — not over-matching PR-shaped numbers.
194
+ // GitHub PR numbers on this repo are 3 digits, well inside the old range, so the
195
+ // bound was never what separated them; classifyKind's `PR #NNN` left-context test
196
+ // is, and it is untouched here.
197
+ //
198
+ // Exported as the SOURCE STRING, not a RegExp: a /g/ regex carries lastIndex
199
+ // between calls, so a shared instance would skip matches depending on who scanned
200
+ // last. Each site builds its own.
201
+ const REF_DIGITS = '\\d{1,7}';
202
+
203
+ const RESOLVER_LINK_RE = new RegExp(`\\[#(${REF_DIGITS})\\]\\((?:https?:\\/\\/[^/)\\s]+)?\\/builders#\\/task\\/(${REF_DIGITS})\\)`, 'g');
135
204
 
136
205
  // Classify a ref by its left context (+ list-glue carry). Returns one of
137
206
  // 'github' | 'idea' | 'blocker' | 'entity' | null (null = candidate task ref).
@@ -194,7 +263,7 @@ function unlinkEntitySegment(segment, leftContext, stats) {
194
263
  // {tasks, ideas, blockers} Sets. Returns the transformed segment; mutates `stats`.
195
264
  function linkifySegment(segment, ids, leftContext, stats) {
196
265
  let out = '';
197
- const re = /#(\d{1,5})/g;
266
+ const re = new RegExp(`#(${REF_DIGITS})`, 'g');
198
267
  let m;
199
268
  let last = 0;
200
269
  let prevEnd = 0; // index in `segment` just past the previous match
@@ -303,7 +372,7 @@ function upgradeLineCodeSpans(line, ids, stats) {
303
372
  let prevEnd = 0;
304
373
  let hadPrev = false;
305
374
  let prevKind = null; // only 'idea' | 'blocker' carry here
306
- const re = /`#(\d{1,5})`/g;
375
+ const re = new RegExp('`#(' + REF_DIGITS + ')`', 'g');
307
376
  let m;
308
377
  while ((m = re.exec(line)) !== null) {
309
378
  const num = m[1];
@@ -496,4 +565,12 @@ module.exports = {
496
565
  linkifyText, linkifySegment, unlinkEntitySegment, upgradeLineCodeSpans,
497
566
  classifyKind, normalizeIdSets, discoverDocs,
498
567
  RESOLVER_BASE, RESOLVER_BASE_IDEA, RESOLVER_BASE_BLOCKER,
568
+ // task 1003542: the bare-ref digit width, exported so the OTHER enforcement
569
+ // point (.claude/hooks/stop-refcheck.js) reads it here instead of keeping its
570
+ // own copy — the drift that let 7-digit ids past both guards at once.
571
+ REF_DIGITS,
572
+ // task 1003542: the loopback test behind the resolver base. Exported because
573
+ // RESOLVER_BASE is computed once at module load, so the DECISION is the only
574
+ // part a unit test can reach without re-requiring under a mocked branding.
575
+ isLoopbackOrigin,
499
576
  };
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.693'; // 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);