@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.
@@ -3,9 +3,9 @@
3
3
  "generator": "scripts/gds/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 1761,
7
- "unique_ids": 1523,
8
- "certain": 1761,
6
+ "strings": 1759,
7
+ "unique_ids": 1521,
8
+ "certain": 1759,
9
9
  "heuristic": 0,
10
10
  "duplicate_ids": 165
11
11
  },
@@ -13,7 +13,7 @@
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
15
  "files": 81,
16
- "strings": 1282
16
+ "strings": 1280
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -2056,7 +2056,7 @@
2056
2056
  "surface": "builders-hall",
2057
2057
  "text": "A Full Idea answers five questions, so that someone else can deliver against it without having to interview you first. Open to every role — the score below is the only filter, and it counts substance, not who you are.",
2058
2058
  "file": "modules/hall-ui/public/ideas.html",
2059
- "line": 107,
2059
+ "line": 104,
2060
2060
  "origin": "html-text",
2061
2061
  "confidence": "certain"
2062
2062
  },
@@ -2416,7 +2416,7 @@
2416
2416
  "surface": "builders-hall",
2417
2417
  "text": "Answer the five questions and this spark becomes a Full Idea.",
2418
2418
  "file": "modules/hall-ui/public/ideas.js",
2419
- "line": 586,
2419
+ "line": 601,
2420
2420
  "origin": "js-text-assign",
2421
2421
  "confidence": "certain"
2422
2422
  },
@@ -2726,15 +2726,6 @@
2726
2726
  "origin": "js-notice",
2727
2727
  "confidence": "certain"
2728
2728
  },
2729
- {
2730
- "id": "f0576990e9fc",
2731
- "surface": "builders-hall",
2732
- "text": "Blocker resolution",
2733
- "file": "modules/hall-ui/public/ideas.html",
2734
- "line": 81,
2735
- "origin": "html-text",
2736
- "confidence": "certain"
2737
- },
2738
2729
  {
2739
2730
  "id": "118026595c16",
2740
2731
  "surface": "builders-hall",
@@ -3748,7 +3739,7 @@
3748
3739
  "surface": "builders-hall",
3749
3740
  "text": "Could not load idea {…}.",
3750
3741
  "file": "modules/hall-ui/public/ideas.js",
3751
- "line": 629,
3742
+ "line": 644,
3752
3743
  "origin": "js-text-assign",
3753
3744
  "confidence": "certain"
3754
3745
  },
@@ -3793,7 +3784,7 @@
3793
3784
  "surface": "builders-hall",
3794
3785
  "text": "Could not load the idea template. Reload, or file from the terminal with capture.js.",
3795
3786
  "file": "modules/hall-ui/public/ideas.js",
3796
- "line": 611,
3787
+ "line": 626,
3797
3788
  "origin": "js-text-assign",
3798
3789
  "confidence": "certain"
3799
3790
  },
@@ -3820,7 +3811,7 @@
3820
3811
  "surface": "builders-hall",
3821
3812
  "text": "Could not load the queue just now.",
3822
3813
  "file": "modules/hall-ui/public/ideas.js",
3823
- "line": 550,
3814
+ "line": 565,
3824
3815
  "origin": "js-markup",
3825
3816
  "confidence": "certain"
3826
3817
  },
@@ -4225,7 +4216,7 @@
4225
4216
  "surface": "builders-hall",
4226
4217
  "text": "Counted, not judged: each question is worth the same, and earns its share in proportion to how much real text it carries. Whitespace and padding score nothing.",
4227
4218
  "file": "modules/hall-ui/public/ideas.html",
4228
- "line": 167,
4219
+ "line": 164,
4229
4220
  "origin": "html-text",
4230
4221
  "confidence": "certain"
4231
4222
  },
@@ -4351,7 +4342,7 @@
4351
4342
  "surface": "builders-hall",
4352
4343
  "text": "Develop this idea",
4353
4344
  "file": "modules/hall-ui/public/ideas.js",
4354
- "line": 585,
4345
+ "line": 600,
4355
4346
  "origin": "js-text-assign",
4356
4347
  "confidence": "certain"
4357
4348
  },
@@ -4360,7 +4351,7 @@
4360
4351
  "surface": "builders-hall",
4361
4352
  "text": "Developing a Big Idea Spark",
4362
4353
  "file": "modules/hall-ui/public/ideas.html",
4363
- "line": 119,
4354
+ "line": 116,
4364
4355
  "origin": "html-text",
4365
4356
  "confidence": "certain"
4366
4357
  },
@@ -4396,7 +4387,7 @@
4396
4387
  "surface": "builders-hall",
4397
4388
  "text": "Discard draft",
4398
4389
  "file": "modules/hall-ui/public/ideas.html",
4399
- "line": 145,
4390
+ "line": 142,
4400
4391
  "origin": "html-text",
4401
4392
  "confidence": "certain"
4402
4393
  },
@@ -4828,7 +4819,7 @@
4828
4819
  "surface": "builders-hall",
4829
4820
  "text": "File it",
4830
4821
  "file": "modules/hall-ui/public/ideas.html",
4831
- "line": 99,
4822
+ "line": 96,
4832
4823
  "origin": "html-text",
4833
4824
  "confidence": "certain"
4834
4825
  },
@@ -4855,7 +4846,7 @@
4855
4846
  "surface": "builders-hall",
4856
4847
  "text": "File this idea",
4857
4848
  "file": "modules/hall-ui/public/ideas.html",
4858
- "line": 144,
4849
+ "line": 141,
4859
4850
  "origin": "html-text",
4860
4851
  "confidence": "certain"
4861
4852
  },
@@ -4954,7 +4945,7 @@
4954
4945
  "surface": "builders-hall",
4955
4946
  "text": "Five questions, and a score that counts how much of each you actually answered.",
4956
4947
  "file": "modules/hall-ui/public/ideas.js",
4957
- "line": 621,
4948
+ "line": 636,
4958
4949
  "origin": "js-text-assign",
4959
4950
  "confidence": "certain"
4960
4951
  },
@@ -5188,7 +5179,7 @@
5188
5179
  "surface": "builders-hall",
5189
5180
  "text": "Goals could not be loaded. Refresh to try again.",
5190
5181
  "file": "modules/hall-ui/public/goals-page.js",
5191
- "line": 890,
5182
+ "line": 896,
5192
5183
  "origin": "js-text-assign",
5193
5184
  "confidence": "certain"
5194
5185
  },
@@ -5930,15 +5921,6 @@
5930
5921
  "origin": "js-text-assign",
5931
5922
  "confidence": "certain"
5932
5923
  },
5933
- {
5934
- "id": "19ad2430937d",
5935
- "surface": "builders-hall",
5936
- "text": "Learning capture",
5937
- "file": "modules/hall-ui/public/ideas.html",
5938
- "line": 78,
5939
- "origin": "html-text",
5940
- "confidence": "certain"
5941
- },
5942
5924
  {
5943
5925
  "id": "7c9b7a89353b",
5944
5926
  "surface": "builders-hall",
@@ -6115,7 +6097,7 @@
6115
6097
  "surface": "builders-hall",
6116
6098
  "text": "Loading the five questions…",
6117
6099
  "file": "modules/hall-ui/public/ideas.html",
6118
- "line": 138,
6100
+ "line": 135,
6119
6101
  "origin": "html-text",
6120
6102
  "confidence": "certain"
6121
6103
  },
@@ -6970,7 +6952,7 @@
6970
6952
  "surface": "builders-hall",
6971
6953
  "text": "Nothing is waiting. A spark appears here when its author offers a quick idea up for development.",
6972
6954
  "file": "modules/hall-ui/public/ideas.js",
6973
- "line": 528,
6955
+ "line": 543,
6974
6956
  "origin": "js-markup",
6975
6957
  "confidence": "certain"
6976
6958
  },
@@ -7060,7 +7042,7 @@
7060
7042
  "surface": "builders-hall",
7061
7043
  "text": "One line. This is what the idea is called everywhere else.",
7062
7044
  "file": "modules/hall-ui/public/ideas.html",
7063
- "line": 127,
7045
+ "line": 124,
7064
7046
  "origin": "html-text",
7065
7047
  "confidence": "certain"
7066
7048
  },
@@ -7888,7 +7870,7 @@
7888
7870
  "surface": "builders-hall",
7889
7871
  "text": "Recent history could not be loaded right now.",
7890
7872
  "file": "modules/hall-ui/public/work.js",
7891
- "line": 1232,
7873
+ "line": 1236,
7892
7874
  "origin": "js-markup",
7893
7875
  "confidence": "certain"
7894
7876
  },
@@ -8707,7 +8689,7 @@
8707
8689
  "surface": "builders-hall",
8708
8690
  "text": "Showing {…} of {…}.",
8709
8691
  "file": "modules/hall-ui/public/ideas.js",
8710
- "line": 532,
8692
+ "line": 547,
8711
8693
  "origin": "js-markup",
8712
8694
  "confidence": "certain"
8713
8695
  },
@@ -8752,7 +8734,7 @@
8752
8734
  "surface": "builders-hall",
8753
8735
  "text": "Sign in to filter to your goals",
8754
8736
  "file": "modules/hall-ui/public/goals-page.js",
8755
- "line": 867,
8737
+ "line": 873,
8756
8738
  "origin": "js-text-assign",
8757
8739
  "confidence": "certain"
8758
8740
  },
@@ -9022,7 +9004,7 @@
9022
9004
  "surface": "builders-hall",
9023
9005
  "text": "Suggested as you type; leave it on \"no goal\" and it waits for triage.",
9024
9006
  "file": "modules/hall-ui/public/ideas.html",
9025
- "line": 91,
9007
+ "line": 88,
9026
9008
  "origin": "html-text",
9027
9009
  "confidence": "certain"
9028
9010
  },
@@ -9715,7 +9697,7 @@
9715
9697
  "surface": "builders-hall",
9716
9698
  "text": "The task list could not be loaded right now.",
9717
9699
  "file": "modules/hall-ui/public/work.js",
9718
- "line": 1297,
9700
+ "line": 1301,
9719
9701
  "origin": "js-markup",
9720
9702
  "confidence": "certain"
9721
9703
  },
@@ -10255,7 +10237,7 @@
10255
10237
  "surface": "builders-hall",
10256
10238
  "text": "Welcome, {…}. Claim and release tasks and follow work in progress — no terminal needed.",
10257
10239
  "file": "modules/hall-ui/public/work.js",
10258
- "line": 1223,
10240
+ "line": 1227,
10259
10241
  "origin": "js-text-assign",
10260
10242
  "confidence": "certain"
10261
10243
  },
@@ -10597,7 +10579,7 @@
10597
10579
  "surface": "builders-hall",
10598
10580
  "text": "Write a Full Idea",
10599
10581
  "file": "modules/hall-ui/public/ideas.html",
10600
- "line": 106,
10582
+ "line": 103,
10601
10583
  "origin": "html-text",
10602
10584
  "confidence": "certain"
10603
10585
  },
@@ -12055,7 +12037,7 @@
12055
12037
  "surface": "builders-hall",
12056
12038
  "text": "repro steps, context, anything that helps…",
12057
12039
  "file": "modules/hall-ui/public/ideas.html",
12058
- "line": 87,
12040
+ "line": 84,
12059
12041
  "origin": "html-attr:placeholder",
12060
12042
  "confidence": "certain"
12061
12043
  },
@@ -12361,7 +12343,7 @@
12361
12343
  "surface": "builders-hall",
12362
12344
  "text": "via goal",
12363
12345
  "file": "modules/hall-ui/public/dom-utils.js",
12364
- "line": 322,
12346
+ "line": 364,
12365
12347
  "origin": "js-markup",
12366
12348
  "confidence": "certain"
12367
12349
  },
@@ -12397,7 +12379,7 @@
12397
12379
  "surface": "builders-hall",
12398
12380
  "text": "what the idea is, in one line",
12399
12381
  "file": "modules/hall-ui/public/ideas.html",
12400
- "line": 130,
12382
+ "line": 127,
12401
12383
  "origin": "html-attr:placeholder",
12402
12384
  "confidence": "certain"
12403
12385
  },
@@ -1843,5 +1843,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1843
1843
  landed since 1.19.690 with no explicit bump. run 34729018766. (task 1002620)
1844
1844
  1.19.692 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1845
1845
  landed since 1.19.691 with no explicit bump. run 34730821372. (task 1002620)
1846
+ 1.19.693 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
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)
1846
1850
  ---------------------------------------------------------------------------
1847
1851
  ```
@@ -120,6 +120,48 @@
120
120
  return list && list.length ? list.slice() : ['engineer', 'ideator'];
121
121
  }
122
122
 
123
+ // Fill a Kind <select> from the vocabulary the SERVER serves (GET /me
124
+ // `task_kinds`, sourced from the lifecycle port's VALID_KINDS) — task 1003453.
125
+ // The board and the goals page each carried their own hardcoded copy and both
126
+ // had gone stale against the DB CHECK: 'verify' tasks existed, were claimable,
127
+ // and could not be filtered for on either. One filler here is what stops a
128
+ // third copy being the easiest option, the same reasoning `disciplines()`
129
+ // above was written on.
130
+ //
131
+ // Leading '— any —' is the page's own markup and is KEPT (`sel.length = 1`,
132
+ // the populateGoalFilter idiom). An absent or empty list leaves the select
133
+ // with just that row rather than a wrong list — an older server that serves no
134
+ // vocabulary should offer no lens, not a stale one.
135
+ // `keepFirst` is how many markup options survive: 1 for a FILTER, whose leading
136
+ // '— any —' belongs to the page, and 0 for a FILING select, which is cleared and
137
+ // rebuilt — its markup option is the degraded fallback that still lets someone
138
+ // file if the read never lands. `label` names a kind for a reader (the filters
139
+ // show the raw value, as their literals did; the filing select shows kindLabel).
140
+ function fillKindFilter(sel, kinds, { keepFirst = 1, label = (k) => k, selected = null } = {}) {
141
+ if (!sel) return;
142
+ const list = Array.isArray(kinds) ? kinds.filter((k) => typeof k === 'string' && k) : [];
143
+ if (!list.length) return; // no vocabulary served → leave the markup alone
144
+ const cur = sel.value;
145
+ sel.length = keepFirst;
146
+ for (const k of list) {
147
+ const opt = document.createElement('option');
148
+ opt.value = k;
149
+ opt.textContent = label(k);
150
+ sel.appendChild(opt);
151
+ }
152
+ // A refill must not drop the reader's pick; `selected` is the filing default.
153
+ const want = (cur && list.includes(cur)) ? cur : (list.includes(selected) ? selected : null);
154
+ if (want) sel.value = want;
155
+ }
156
+
157
+ // How a kind is named in a picker — 'blocker-resolution' → 'Blocker resolution'.
158
+ // The same shape disciplineLabel takes below, and it reproduces every label the
159
+ // hand-written <option> list in ideas.html carried, so no copy changes.
160
+ function kindLabel(k) {
161
+ const s = String(k || '').replace(/-/g, ' ');
162
+ return s ? s.charAt(0).toUpperCase() + s.slice(1) : '';
163
+ }
164
+
123
165
  // How a craft is named in a picker. An acronym stays upper-case ('ui' → 'UI');
124
166
  // anything else is Title Case, so a craft a module adds later needs no new entry.
125
167
  function disciplineLabel(d) {
@@ -324,6 +366,7 @@
324
366
 
325
367
  window.OTB = {
326
368
  escapeHtml, $, fmtNum, num, fmtInt, fmtCompact, fmtDate, rankLabel, toast, moduleOn, disciplines, disciplineLabel,
369
+ fillKindFilter, kindLabel, // task 1003453 — one Kind filler, all three surfaces
327
370
  errCode, errMessage, errDetails, // task 1002462 — ADR 0116 envelope readers
328
371
  contributeSettingsPanel, settingsPanels,
329
372
  contributeHallWidget, hallWidgets, hallWidgetIds,
@@ -862,6 +862,12 @@
862
862
  }
863
863
  try {
864
864
  state.me = await getJSON(`${API}/me`).catch(() => null);
865
+ // task 1003453 — the Kind lens is built from the served vocabulary.
866
+ // Optional-called on purpose: a browser holding an older cached dom-utils.js
867
+ // must lose one LENS, not the whole page. boot() is a single try block, so an
868
+ // unguarded call here throws before anything renders — which is exactly what
869
+ // it did to every goals render until this guard went in.
870
+ window.OTB.fillKindFilter?.($('#f-goal-kind'), state.me && state.me.task_kinds);
865
871
  // task 1828 — "My goals" needs an identity; disable it when signed out.
866
872
  const mineSel = $('#f-goal-mine');
867
873
  if (mineSel && !state.me) { mineSel.disabled = true; mineSel.title = 'Sign in to filter to your goals'; }
@@ -1028,9 +1034,10 @@
1028
1034
  ] },
1029
1035
  ],
1030
1036
  overflow: [
1031
- { id: 'f-goal-kind', label: 'Kind', options: [{ value: '', label: '— any —' },
1032
- 'feature', 'bug', 'infra', 'refactor', 'spike', 'learning-capture',
1033
- 'cleanup', 'decision', 'blocker-resolution', 'unclassified'] },
1037
+ // task 1003453: filled from /me.task_kinds once boot lands it, never typed
1038
+ // here this copy had gone stale against the DB CHECK exactly as the
1039
+ // board's had, so 'verify' work could not be filtered for on either.
1040
+ { id: 'f-goal-kind', label: 'Kind', options: [{ value: '', label: '— any —' }] },
1034
1041
  { id: 'f-goal-discipline', label: 'Discipline', options: [{ value: '', label: '— any —' }, 'engineer', 'artist', 'ideator', 'ui', 'unclassified'] },
1035
1042
  { id: 'f-goal-newcomer', label: 'Newcomer', options: [{ value: '', label: '— any —' }, { value: 'yes', label: 'newcomer-friendly only' }] },
1036
1043
  { id: 'f-goal-fit', label: 'Run mode', options: [{ value: '', label: '— any —' },
@@ -69,17 +69,14 @@
69
69
  placeholder="what's wrong, or what you'd like to see">
70
70
 
71
71
  <label class="fi-label" for="fq-kind">Kind</label>
72
+ <!-- task 1003453: the options are FILLED from /me.task_kinds at boot
73
+ (ideas.js), not written here. This markup used to spell the whole
74
+ vocabulary and had gone stale against the DB CHECK exactly as the
75
+ two board filters had — a third copy nobody was watching. The one
76
+ option left is the degraded fallback: if the read never lands, a
77
+ bug can still be filed. -->
72
78
  <select id="fq-kind" class="fi-input">
73
79
  <option value="bug" selected>Bug</option>
74
- <option value="feature">Feature</option>
75
- <option value="infra">Infra</option>
76
- <option value="refactor">Refactor</option>
77
- <option value="spike">Spike</option>
78
- <option value="learning-capture">Learning capture</option>
79
- <option value="cleanup">Cleanup</option>
80
- <option value="decision">Decision</option>
81
- <option value="blocker-resolution">Blocker resolution</option>
82
- <option value="unclassified">Unclassified</option>
83
80
  </select>
84
81
 
85
82
  <label class="fi-label" for="fq-body">Details <span class="fi-hint">(optional)</span></label>
@@ -508,6 +508,21 @@
508
508
  // The picker degrades to "homeless only" — filing itself never depends
509
509
  // on this read succeeding.
510
510
  }
511
+
512
+ // task 1003453 — the kind vocabulary, so this select is not a third hardcoded
513
+ // copy. Its OWN try, deliberately not folded into the Promise.all above: the
514
+ // goal picker is what this form is FOR and the kind list is cosmetic, so a
515
+ // vocabulary read must never be able to take the picker down with it. (It can:
516
+ // put it in that Promise.all and one rejection strands buildingVersionIds and
517
+ // allOpenGoals in the shared catch — two suites caught exactly that.)
518
+ // keepFirst 0 clears the markup's fallback and rebuilds with the served list,
519
+ // keeping 'bug' as the default this form has always opened on.
520
+ try {
521
+ const meRes = await api.me.getMe({});
522
+ window.OTB.fillKindFilter?.(fqKindEl(), meRes.ok && meRes.data && meRes.data.task_kinds, {
523
+ keepFirst: 0, label: window.OTB.kindLabel, selected: 'bug',
524
+ });
525
+ } catch (_) { /* the markup's Bug option stands — a report can still be filed */ }
511
526
  }
512
527
 
513
528
  // ---------------------------------------------------------------------------
@@ -1217,6 +1217,10 @@
1217
1217
  throw err;
1218
1218
  }
1219
1219
  viewer.id = me?.builder?.id ?? null;
1220
+ // task 1003453 — the Kind lens, from the served vocabulary. Optional-called so an
1221
+ // older cached dom-utils.js costs one lens, not the board; see OTB.fillKindFilter.
1222
+ window.OTB.fillKindFilter?.($('#f-kind'), me?.task_kinds);
1223
+ restoreUrlFilter('f-kind'); // a shared URL's kind applies once the options exist
1220
1224
  const sub = $('#work-sub');
1221
1225
  if (sub) {
1222
1226
  const name = me?.builder?.display_name || me?.builder?.github_login || 'builder';
@@ -1364,13 +1368,9 @@
1364
1368
  ],
1365
1369
  overflow: [
1366
1370
  { id: 'f-goal', label: 'Goal', options: [{ value: '', label: '— all goals —' }] },
1367
- {
1368
- id: 'f-kind',
1369
- label: 'Kind',
1370
- options: [{ value: '', label: '— any —' },
1371
- 'feature', 'bug', 'infra', 'refactor', 'spike', 'learning-capture',
1372
- 'cleanup', 'decision', 'blocker-resolution', 'unclassified'],
1373
- },
1371
+ // task 1003453: FILLED from /me.task_kinds at boot, never typed here — the
1372
+ // literal was stale against the DB CHECK, so 'verify' was unfilterable.
1373
+ { id: 'f-kind', label: 'Kind', options: [{ value: '', label: '— any —' }] },
1374
1374
  {
1375
1375
  id: 'f-newcomer',
1376
1376
  label: 'Newcomer',
@@ -37,6 +37,12 @@ module.exports = {
37
37
  // (consumer: core cascade-dispatch.js, the dormant System-3 lib).
38
38
  get VOTABLE_TASK_STATUSES() { return classifier.VOTABLE_TASK_STATUSES; },
39
39
  classifyTaskKind: (...a) => classifier.classifyTaskKind(...a),
40
+ // TASK_KINDS: the kind vocabulary, as an ordered array (consumer: GET /me,
41
+ // which hands it to the hall so a filter can be BUILT from it instead of
42
+ // retyped — task 1003453). VALID_KINDS is a Set and Sets do not survive
43
+ // JSON.stringify, so the port hands over the array rather than making every
44
+ // caller remember to spread it.
45
+ get TASK_KINDS() { return [...classifier.VALID_KINDS]; },
40
46
  // suggestGoalForCapture: ONE goal for a filing that named none, or null (task
41
47
  // 1003067). Consumer: the discord #bugs vector, which files through the ideas
42
48
  // routing path and needs a home to offer it. Lives behind the port because
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.692",
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.692",
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.692",
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
  };