@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 +16 -16
- 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/module-api-changelog.md +2 -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/module-api.js +1 -1
- package/tests/linkify_refs.mjs +39 -1
- package/tests/stop_refcheck.mjs +20 -1
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
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:
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
7785
|
+
"sha256": "929bc69d15f44bc6648df7b2f3a9e22d72501486ae2abb9b0491cfcfabbd6683"
|
|
7786
7786
|
},
|
|
7787
7787
|
{
|
|
7788
7788
|
"path": "package.json",
|
|
7789
7789
|
"mode": "0000644",
|
|
7790
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
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);
|