@bongos/core 1.19.656 → 1.19.658

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.
@@ -9,25 +9,14 @@
9
9
  rows are the oversight family's (oversight.css); the ?v= cache-bust is
10
10
  stamped from this file's content hash at boot (src/asset-version.js).
11
11
 
12
- TWO RULES ARE DUPLICATED FROM government.css, KNOWINGLY. `.gov-grid-note` is
13
- the government family's framing-line voice, and the sitting card spends it in
14
- three places (decidedByHtml, the own-idea note, the objection hint) while
15
- the Constitution panel still spends it over there. Copying two short rules is
16
- the cheaper of two bad options: the alternative is this page loading a
17
- stylesheet named for a different room in order to inherit them, which is what
18
- the first cut of this carve did and what the review called out. The NAMES stay
12
+ `.gov-grid-note` AND `.gov-held-note` LIVE IN oversight.css (task 1003761).
13
+ They were duplicated here under a note calling the copy the cheaper of two bad
14
+ options, the alternative being this page loading a stylesheet named for a
15
+ different room. oversight.css is named for neither room and is already loaded
16
+ by both pages, which is the option that note did not weigh. The NAMES stay
19
17
  `gov-*` because board-room.js emits them and
20
18
  tests/government_board_room_copy.mjs reads them as literal markup. */
21
19
 
22
- /* The framing line under a heading — the family voice, duplicated per the header
23
- note above rather than inherited across pages. */
24
- .gov-grid-note { margin: 0 0 14px; font-size: 13.5px; line-height: 1.5; color: var(--ink-soft); max-width: var(--measure); }
25
- .gov-grid-note strong { color: var(--ink); font-weight: 500; }
26
- .gov-grid-note code { font-family: var(--font-mono); font-size: 12.5px; color: var(--ink); overflow-wrap: anywhere; }
27
-
28
- /* "your vote" beside the reader's own row in the open ballot. */
29
- .gov-held-note { font-size: 12.5px; color: var(--ok); white-space: nowrap; }
30
-
31
20
  /* ---- the Board Room: one sitting, one panel -------------------------------------
32
21
  The card order is task 1003270's (title, ballot, the idea, the apparatus); the
33
22
  surface is the shell's .scroll (task 1003469). The subject of the vote is the
@@ -65,6 +65,9 @@
65
65
  <script src="/builders/palette.js" defer></script>
66
66
  <script src="/builders/dom-utils.js?v=2026-06-16-board"></script>
67
67
  <script src="/builders/hall-kit.js"></script>
68
+ <!-- task 1003761: the vocabulary this page and /government both speak. Must load
69
+ BEFORE board-room.js, which destructures window.OTBBoardLib at IIFE entry. -->
70
+ <script src="/builders/board-lib.js?v=2026-09-10-boardlib1"></script>
68
71
  <script src="/builders/board-room.js?v=2026-09-09-board-carve"></script>
69
72
  </body>
70
73
  </html>
@@ -35,14 +35,23 @@
35
35
  // reason and names one section. That requirement is why voting lives in the
36
36
  // hall and not on a chat reaction.
37
37
  //
38
- // ON THE DUPLICATED HELPERS. `pill` / `groupHead` / `when` / `fetchJson` /
39
- // `sendJson` are per-page in this module by convention (gate.js, harbor.js and
40
- // watch.js each carry their own a hall page script is a self-contained IIFE
41
- // over the DOM). Two are a real drift risk rather than boilerplate and are
42
- // called out where they sit: `membershipRankKeys` mirrors the SERVER's predicate
43
- // grammar, and `fmtWindow` is also read by the Constitution panel that stayed in
44
- // government.js. tests/government_board_vote_ui.mjs holds the first equal to the
45
- // server; keep the second in step by hand.
38
+ // ON THE SHARED HELPERS (task 1003761). The vocabulary this page and the
39
+ // Constitution panel on /government BOTH speak now lives in board-lib.js, loaded
40
+ // before this script as window.OTBBoardLib: the ladder and `membershipRankKeys`
41
+ // (which mirror the SERVER's predicate grammar), `rankNames`, `fmtWindow`, the
42
+ // two pass-rule maps, and the `pill` / `groupHead` / `when` row pieces. The carve
43
+ // that split the two pages copied all of it, which is the two-copies state
44
+ // modules/government/CLAUDE.md forbids — "the grammar is written ONCE".
45
+ // tests/government_constitution_view.mjs holds the lib's parser equal to the
46
+ // server's. (This header used to name tests/government_board_vote_ui.mjs for
47
+ // that; it never asserted the parser, so the copy that lived here was pinned by
48
+ // nothing.)
49
+ //
50
+ // `fetchJson` / `sendJson` stay per-page: a hall page script is a self-contained
51
+ // IIFE over the DOM, and those two carry this page's own error copy.
52
+ //
53
+ // SECTION_LABELS and fmtRemaining stay HERE, not in the lib — this page is their
54
+ // only reader, and the lib is for what the two pages share.
46
55
  //
47
56
  // The function ORDER here is deliberate and matches the pre-carve source:
48
57
  // tests/government_board_room_copy.mjs slices this file by function-name
@@ -57,8 +66,13 @@
57
66
  // /lib/bongos-client.js. baseUrl:'' passes the full `${API}${path}` URLs below
58
67
  // through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
59
68
  const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
60
- const { escapeHtml, fmtInt } = window.OTB;
69
+ const { escapeHtml } = window.OTB;
61
70
  const { emptyStateHtml } = window.OTBKit;
71
+ // The vocabulary /government and this page share (task 1003761). board-lib.js
72
+ // is loaded before this script; see the header note. The ladder and fmtInt went
73
+ // with it — groupHead was this page's only fmtInt reader.
74
+ const { membershipRankKeys, rankNames, fmtWindow,
75
+ PASS_RULE_PLAIN, pill, groupHead, when } = window.OTBBoardLib;
62
76
  const toast = (msg) => window.OTB.toast(msg);
63
77
 
64
78
  // ---- fetch helpers (mirror gate.js) --------------------------------------
@@ -98,52 +112,6 @@
98
112
  };
99
113
  const friendlyError = (code) => ERROR_COPY[code] || null;
100
114
 
101
- // ---- shared row pieces ---------------------------------------------------
102
-
103
- // A state pill in the shell's vocabulary. `mod` is one of the .fact-pill
104
- // states (ok / warn / danger / info / quiet); the label is what the reader sees.
105
- const pill = (mod, label) => `<span class="fact-pill fact-pill--${mod}">${escapeHtml(label)}</span>`;
106
- const groupHead = (name, n, note) =>
107
- `<div class="ov-group__head"><span class="ov-group__name">${escapeHtml(name)}</span>` +
108
- (n === undefined ? '' : `<span class="ov-group__n">${fmtInt(n)}</span>`) +
109
- (note ? `<span class="ov-group__note">${escapeHtml(note)}</span>` : '') +
110
- '</div>';
111
- // A timestamp the way the rows have always shown one: "2026-08-30 14:05".
112
- const when = (iso) => escapeHtml(String(iso || '').slice(0, 16).replace('T', ' '));
113
-
114
- // The predicate grammar, in plain language (task 1003094). This MIRRORS
115
- // modules/government/membership-predicate.js — browser code cannot import the
116
- // server module, the established pattern here (the five section labels do the
117
- // same) — and tests/government_board_vote_ui.mjs holds the two equal.
118
- //
119
- // Rendering the raw predicate was fine while the only form was `rank:<key>`.
120
- // It is not fine now: `rank:metic+` would read as "builders holding the metic+
121
- // rank", which names a rank that does not exist and hides the very thing the
122
- // suffix says.
123
- const STANDARD_LADDER = ['xenos', 'thetes', 'metic', 'archon'];
124
-
125
- function membershipRankKeys(predicate) {
126
- const m = String(predicate || '');
127
- if (!m.startsWith('rank:')) return null;
128
- const body = m.slice(5);
129
- if (!body) return null;
130
- const keys = [];
131
- for (const element of body.split(',')) {
132
- const match = /^([a-z][a-z0-9_-]*)(\+?)$/.exec(element);
133
- if (!match) return null;
134
- let expanded;
135
- if (match[2] === '+') {
136
- const i = STANDARD_LADDER.indexOf(match[1]);
137
- if (i === -1) return null; // "and above" is undefined off the ladder
138
- expanded = STANDARD_LADDER.slice(i);
139
- } else {
140
- expanded = [match[1]];
141
- }
142
- for (const k of expanded) if (keys.indexOf(k) === -1) keys.push(k);
143
- }
144
- return keys.length ? keys : null;
145
- }
146
-
147
115
  // ---- the Board Room panel (R16) --------------------------------------------
148
116
 
149
117
  // The room renders what the server assembled: open items with the database's
@@ -257,37 +225,9 @@
257
225
  //
258
226
  // Read from the item's SNAPSHOT constitution (the rules in force when the
259
227
  // window opened), never today's config — the same rules the server tallies
260
- // under. Rank KEYS never surface: the display label comes from the branding
261
- // pack via OTB.rankLabel, as on every other hall surface. The one place a raw
262
- // predicate still shows is an UNREADABLE one, where the broken string is the
263
- // actionable fact and hiding it would help nobody.
264
- function rankNames(keys) {
265
- const labels = keys.map((k) => escapeHtml(window.OTB.rankLabel(k)));
266
- if (labels.length === 1) return labels[0];
267
- return `${labels.slice(0, -1).join(', ')} or ${labels[labels.length - 1]}`;
268
- }
269
-
270
- // A sitting length in the unit a person would say it in. The config stores
271
- // minutes; "2880 minutes" is a number nobody converts in their head.
272
- //
273
- // DUPLICATED, KNOWINGLY: government.js keeps its own copy for the Constitution
274
- // panel's windowSentence. Two readers, two files, one wording — if you change
275
- // the phrasing here, change it there.
276
- function fmtWindow(minutes) {
277
- const m = Number(minutes);
278
- if (!Number.isFinite(m) || m <= 0) return `${escapeHtml(String(minutes))} minutes`;
279
- const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
280
- if (m % 1440 === 0) return plural(m / 1440, 'day');
281
- if (m % 60 === 0) return plural(m / 60, 'hour');
282
- return plural(m, 'minute');
283
- }
284
-
285
- const PASS_RULE_PLAIN = {
286
- first_ratifier: 'The first vote decides it: a yes ratifies, an objection returns it to its author.',
287
- consent: 'It passes unless someone objects — silence is agreement, and one reasoned objection returns it.',
288
- majority: 'It passes only when more than half the board votes yes. Silence is not agreement here.',
289
- unanimous: 'Everyone who votes must vote yes, and at least one member must. One objection sends it back for revision.',
290
- };
228
+ // under. `rankNames`, `fmtWindow` and `PASS_RULE_PLAIN` come from board-lib.js
229
+ // (task 1003761) the Constitution panel on /government says the same three
230
+ // things and used to say them from its own copies.
291
231
 
292
232
  function decidedByHtml(item) {
293
233
  const c = item.constitution || {};
@@ -23,11 +23,9 @@
23
23
  color: var(--ink);
24
24
  text-shadow: var(--type-halo);
25
25
  }
26
- .gov-grid-note { margin: 0 0 14px; font-size: 13.5px; line-height: 1.5; color: var(--ink-soft); max-width: var(--measure); }
27
- .gov-grid-note strong { color: var(--ink); font-weight: 500; }
28
- .gov-grid-note code, .ov-row__sub code, .ov-row__title code, .ov-foot code {
29
- font-family: var(--font-mono); font-size: 12.5px; color: var(--ink); overflow-wrap: anywhere;
30
- }
26
+ /* `.gov-grid-note` (and the `.ov-*` code rule it shared a declaration with) moved
27
+ to oversight.css in task 1003761 /board-room renders it too and had a copy.
28
+ `.gov-h` stays: this page is its only renderer. */
31
29
 
32
30
  /* Holds the ladder-rank reset button. Empty for every other rank, so it collapses
33
31
  rather than reserving a gap — hence no margin on the empty box itself. */
@@ -57,9 +55,13 @@
57
55
  }
58
56
 
59
57
  /* ---- the Board Room: MOVED OUT to board-room.css (ADR 0266) ----------------
60
- The sitting card, the ballot controls and `.gov-held-note` went with the room
61
- when it became its own page. Nothing on THIS page renders them, so they are
62
- not kept here "just in case" board-room.css is where they live. */
58
+ The sitting card and the ballot controls went with the room when it became its
59
+ own page, and nothing here renders them.
60
+ CORRECTION (task 1003761): `.gov-held-note` went with them under the same
61
+ "nothing on THIS page renders them" reasoning, and that was wrong —
62
+ renderRankList marks a held Archon rank with one, so it rendered unstyled here
63
+ from the carve until this task. It lives in oversight.css now, which both
64
+ pages load. */
63
65
 
64
66
  /* ---- the charter library ------------------------------------------------------- */
65
67
  /* The configuration line under a charter: what an owner would type, in the
@@ -139,6 +139,9 @@
139
139
  <script src="/builders/palette.js" defer></script>
140
140
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
141
141
  <script src="/builders/hall-kit.js"></script>
142
+ <!-- task 1003761: the vocabulary this page and /board-room both speak. Must load
143
+ BEFORE government.js, which destructures window.OTBBoardLib at IIFE entry. -->
144
+ <script src="/builders/board-lib.js?v=2026-09-10-boardlib1"></script>
142
145
  <script src="/builders/government.js?v=2026-09-02-gov-rows"></script>
143
146
  </body>
144
147
  </html>
@@ -79,6 +79,10 @@
79
79
  const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
80
80
  const { escapeHtml, fmtInt } = window.OTB;
81
81
  const { emptyStateHtml } = window.OTBKit;
82
+ // The vocabulary this page and the Board Room share (task 1003761).
83
+ // board-lib.js is loaded before this script.
84
+ const { STANDARD_LADDER, membershipRankKeys, rankNames, fmtWindow,
85
+ PASS_RULE_NAME, pill, groupHead, when } = window.OTBBoardLib;
82
86
  const toast = (msg) => window.OTB.toast(msg);
83
87
 
84
88
  // ---- fetch helpers (mirror gate.js) --------------------------------------
@@ -137,19 +141,6 @@
137
141
 
138
142
  const selectedRank = () => ranks.find((r) => r.rank_key === selectedRankKey) || null;
139
143
 
140
- // ---- shared row pieces ---------------------------------------------------
141
-
142
- // A state pill in the shell's vocabulary. `mod` is one of the .fact-pill
143
- // states (ok / warn / danger / info / quiet); the label is what the reader sees.
144
- const pill = (mod, label) => `<span class="fact-pill fact-pill--${mod}">${escapeHtml(label)}</span>`;
145
- const groupHead = (name, n, note) =>
146
- `<div class="ov-group__head"><span class="ov-group__name">${escapeHtml(name)}</span>` +
147
- (n === undefined ? '' : `<span class="ov-group__n">${fmtInt(n)}</span>`) +
148
- (note ? `<span class="ov-group__note">${escapeHtml(note)}</span>` : '') +
149
- '</div>';
150
- // A timestamp the way the rows have always shown one: "2026-08-30 14:05".
151
- const when = (iso) => escapeHtml(String(iso || '').slice(0, 16).replace('T', ' '));
152
-
153
144
  // ---- render: rank list ---------------------------------------------------
154
145
 
155
146
  // One rank, one selectable oversight row: the label, the DESCRIPTION the
@@ -539,39 +530,12 @@
539
530
  // switch can never be flipped quietly, and every change is auditable against
540
531
  // the constitution of its day.
541
532
 
542
- // The predicate grammar, in plain language (task 1003094). This MIRRORS
543
- // modules/government/membership-predicate.js browser code cannot import the
544
- // server module, the established pattern here (the five section labels do the
545
- // same) and tests/government_board_vote_ui.mjs holds the two equal.
546
- //
547
- // Rendering the raw predicate was fine while the only form was `rank:<key>`.
548
- // It is not fine now: `rank:metic+` would read as "builders holding the metic+
549
- // rank", which names a rank that does not exist and hides the very thing the
550
- // suffix says. A constitution panel that misdescribes who governs is the
551
- // failure this whole task exists to fix, one layer up.
552
- const STANDARD_LADDER = ['xenos', 'thetes', 'metic', 'archon'];
553
-
554
- function membershipRankKeys(predicate) {
555
- const m = String(predicate || '');
556
- if (!m.startsWith('rank:')) return null;
557
- const body = m.slice(5);
558
- if (!body) return null;
559
- const keys = [];
560
- for (const element of body.split(',')) {
561
- const match = /^([a-z][a-z0-9_-]*)(\+?)$/.exec(element);
562
- if (!match) return null;
563
- let expanded;
564
- if (match[2] === '+') {
565
- const i = STANDARD_LADDER.indexOf(match[1]);
566
- if (i === -1) return null; // "and above" is undefined off the ladder
567
- expanded = STANDARD_LADDER.slice(i);
568
- } else {
569
- expanded = [match[1]];
570
- }
571
- for (const k of expanded) if (keys.indexOf(k) === -1) keys.push(k);
572
- }
573
- return keys.length ? keys : null;
574
- }
533
+ // The predicate grammar and the ladder it expands against come from
534
+ // board-lib.js (task 1003761). They MIRROR modules/government/
535
+ // membership-predicate.js browser code cannot import the server module
536
+ // and the carve that took the Board Room onto its own page had left a second
537
+ // copy here. A constitution panel that misdescribes who governs is the failure
538
+ // this whole area exists to fix, and two copies is how it gets there.
575
539
 
576
540
  function membershipSentence(board) {
577
541
  const raw = String(board.membership || '');
@@ -682,18 +646,10 @@
682
646
  close_early_on_full_turnout: 'Closing early on full turnout',
683
647
  };
684
648
 
685
- // A list of rank keys as their display labels, in a phrase a person would say
686
- // ("Metic or Archon"). Carved-region residue with a twist (ADR 0266): the copy
687
- // that lived here went to board-room.js with the sitting card, and this one
688
- // came BACK because the divergence warning below (task 1003739, ADR 0268)
689
- // landed a new reader on the Constitution side while the room was moving out.
690
- // Its only caller is fieldValueHtml; membershipSentence above spells its own
691
- // labels inline, so do not route that through here expecting one wording.
692
- function rankNames(keys) {
693
- const labels = keys.map((k) => escapeHtml(window.OTB.rankLabel(k)));
694
- if (labels.length === 1) return labels[0];
695
- return `${labels.slice(0, -1).join(', ')} or ${labels[labels.length - 1]}`;
696
- }
649
+ // `rankNames` comes from board-lib.js (task 1003761). Its only caller here is
650
+ // fieldValueHtml; membershipSentence above spells its own labels inline because
651
+ // it wraps each in <strong>, so do not route that through it expecting one
652
+ // wording.
697
653
 
698
654
  // One constitution VALUE, in the words a person would use — the same rule the
699
655
  // dials keep, applied to the two halves of a divergence. Without this the
@@ -716,32 +672,11 @@
716
672
 
717
673
  // One dial, one row: the dial's name as the title, the live sentence as the
718
674
  // sub line. The sentence is trusted HTML from the helpers above.
719
- // A sitting length in the unit a person would say it in. The config stores
720
- // minutes; "2880 minutes" is a number nobody converts in their head.
721
675
  //
722
- // Lived in the Board Room region until ADR 0266 carved that room onto its own
723
- // page. It stayed because windowSentence above reads it; board-room.js keeps
724
- // its own copy for the sitting card. Two readers, two files, one wording — if
725
- // you change the phrasing here, change it there.
726
- function fmtWindow(minutes) {
727
- const m = Number(minutes);
728
- if (!Number.isFinite(m) || m <= 0) return `${escapeHtml(String(minutes))} minutes`;
729
- const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
730
- if (m % 1440 === 0) return plural(m / 1440, 'day');
731
- if (m % 60 === 0) return plural(m / 60, 'hour');
732
- return plural(m, 'minute');
733
- }
734
-
735
- // The short name of a pass rule, for a place a whole sentence will not fit —
736
- // the change-history line below. Also carved-region residue (ADR 0266): the
737
- // sentence-length twin PASS_RULE_PLAIN went to board-room.js with the card,
738
- // this one stayed with its only reader, historyRow.
739
- const PASS_RULE_NAME = {
740
- first_ratifier: 'by first ratifier',
741
- consent: 'by consent',
742
- majority: 'by majority',
743
- unanimous: 'by unanimous agreement',
744
- };
676
+ // `fmtWindow` and `PASS_RULE_NAME` come from board-lib.js (task 1003761).
677
+ // fmtWindow had a twin in board-room.js under a "keep the phrasing in step by
678
+ // hand" note; PASS_RULE_NAME sits beside its sentence-length twin
679
+ // PASS_RULE_PLAIN there, so the two renderings of one pass rule cannot drift.
745
680
 
746
681
  function dialRow(name, sentenceHtml) {
747
682
  return `<li class="ov-row"><div class="ov-row__main"><span class="ov-row__title">${escapeHtml(name)}</span><p class="ov-row__sub">${sentenceHtml}</p></div></li>`;
@@ -794,6 +729,14 @@
794
729
  // `predicate: null` means the predicate is the owner's to choose (Foundation
795
730
  // names their own custom rank), which is honest rather than unchecked — the
796
731
  // test asserts such a charter says so in its `how`.
732
+ //
733
+ // A SECOND test pins these too, for a different reason. The `rank:xenos+` row
734
+ // spells the full ladder out in `seatsRanks`, and that literal is why this file
735
+ // stays in tests/rank_tier_single_source.mjs's STATIC_LADDER_FILES even though
736
+ // task 1003761 moved the page's real ladder to board-lib.js. It is documentation
737
+ // of the grammar rather than a second implementation of it — but a worked
738
+ // example that drifts teaches a ladder nobody has, so it is held to the
739
+ // canonical order.
797
740
  const CHARTERS = [
798
741
  { key: 'monarchy', name: 'Monarchy / BDFL', selectable: true,
799
742
  predicate: 'rank:archon', seatsRanks: ['archon'],
@@ -919,8 +862,11 @@
919
862
  // What stayed behind, and why, so neither reads as a leftover:
920
863
  // • fmtWindow — windowSentence (the Constitution's clock dial) reads it.
921
864
  // • PASS_RULE_NAME — historyRow (the amendment history line) reads it.
922
- // Both are duplicated in board-room.js rather than shared; a hall page script
923
- // is a self-contained IIFE, which is the convention here.
865
+ // Both READERS are still here; the two helpers themselves are not. Task 1003761
866
+ // hoisted them into board-lib.js, which this page destructures at IIFE entry.
867
+ // This block used to end "both are duplicated in board-room.js rather than
868
+ // shared; a hall page script is a self-contained IIFE, which is the convention
869
+ // here" — that was the justification for the copies, and it is retired.
924
870
  //
925
871
  // The old deep link is redirected below in init() — a fragment never reaches
926
872
  // the server, so #board-room could not be caught by a route.
@@ -178,6 +178,37 @@ a.fact-chip:hover { color: var(--accent-ink); border-color: var(--accent-ink); t
178
178
  .ov-danger { color: var(--danger); }
179
179
  .ov-ok { color: var(--ok); }
180
180
 
181
+ /* ---- the government family's two shared lines (task 1003761) -----------------
182
+ Both are rendered by /government AND /board-room, so they belong to the family
183
+ sheet both pages load rather than to either page's own. ADR 0266's carve had
184
+ put them elsewhere, and each half of that was wrong in its own way:
185
+
186
+ `.gov-grid-note` was COPIED into board-room.css under a header note calling
187
+ the duplication the cheaper of two bad options — the alternative it weighed
188
+ was one page loading a sheet named for the other page's room. This sheet is
189
+ named for neither room, which is the option that was missing.
190
+
191
+ `.gov-held-note` was MOVED to board-room.css under a note in government.css
192
+ saying "nothing on THIS page renders them". government.js does: it marks a
193
+ held Archon rank with one (renderRankList). So that line has rendered unstyled
194
+ on /government since the carve — a live defect, not a tidiness point.
195
+
196
+ The NAMES stay `gov-*`: board-room.js and government.js emit them, and
197
+ tests/government_board_room_copy.mjs reads `.gov-grid-note` as literal markup.
198
+ The `.ov-*` selectors on the code rule came with it from government.css, where
199
+ they were already sharing the declaration. */
200
+
201
+ /* The framing line under a heading — the family's voice. */
202
+ .gov-grid-note { margin: 0 0 14px; font-size: 13.5px; line-height: 1.5; color: var(--ink-soft); max-width: var(--measure); }
203
+ .gov-grid-note strong { color: var(--ink); font-weight: 500; }
204
+ .gov-grid-note code, .ov-row__sub code, .ov-row__title code, .ov-foot code {
205
+ font-family: var(--font-mono); font-size: 12.5px; color: var(--ink); overflow-wrap: anywhere;
206
+ }
207
+
208
+ /* "your vote" beside the reader's own row in the open ballot (/board-room), and
209
+ "held" beside an Archon-held rank (/government). */
210
+ .gov-held-note { font-size: 12.5px; color: var(--ok); white-space: nowrap; }
211
+
181
212
  /* Detail lines under a row: a reason per line (the gate's holds), or a key and
182
213
  a value (the harbor's make and cost). Hairlines between, none on the first. */
183
214
  .ov-lines { list-style: none; margin: 10px 0 0; padding: 0; max-width: 72ch; }
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.656",
3
+ "version": "1.19.658",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.656",
9
+ "version": "1.19.658",
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.656",
3
+ "version": "1.19.658",
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",
@@ -298,11 +298,85 @@ function checkGovernmentRenameIdentity() {
298
298
  }
299
299
 
300
300
 
301
+ // ── Check 19d — "stealth" is the PRODUCT's word, not the gate's (task 1003152) ─
302
+ //
303
+ // The third vocabulary wall, and the same defect as ADR 0174's "role": one word,
304
+ // two live meanings, close to OPPOSITE. `platform_identity_projects.visibility =
305
+ // 'stealth'` is a project that IS on the public orbs map — anonymous, rendered as
306
+ // a black hole, refusing applications, but published and visible; only its
307
+ // identity is withheld. The pre-launch gate is the reverse: the whole instance is
308
+ // UNREADABLE without a password. ADR 0182 locks the product sense to the orb
309
+ // metaphor via goal 1000046's own criterion, so the PRODUCT keeps the word and
310
+ // the INFRA half renamed (owner decision, 2026-09-09).
311
+ //
312
+ // SCOPED TO THE INFRA SURFACE, for the same reason check 19 is scoped to the
313
+ // government surface: ~16 files carry the product sense legitimately, and a
314
+ // repo-wide ban would have to allowlist every one of them — an allowlist longer
315
+ // than the rule. The honest unit is a SURFACE.
316
+ //
317
+ // NO ALLOWLIST. Two exemptions, both properties of a line rather than a file:
318
+ // a line naming one of the four documented COMPATIBILITY identifiers (the live
319
+ // cookie, the HMAC label, the legacy submit path, the deprecated env var — see
320
+ // prelaunch-gate.js's header for why each stayed), and a line CITING the rename
321
+ // or the other meaning. Everything else is drift.
322
+ // tests/prelaunch_gate.mjs is deliberately NOT here, the same way check 19's
323
+ // mutation test sits outside the government surface: that file's job includes
324
+ // PLANTING the banned word to prove the rule can fail, and a wall that forbids
325
+ // its own negative test can only be satisfied by deleting the evidence.
326
+ const PRELAUNCH_SURFACE = [
327
+ 'src/bongos/prelaunch-gate.js',
328
+ 'src/bongos/platform-visibility-gate.js',
329
+ ];
330
+ // serve-internal.js is 1,500+ lines and mounts every surface on the instance;
331
+ // scanning it whole would drag in prose about the product's visibility. The
332
+ // lines that name the GATE are the only ones that can regress here.
333
+ const PRELAUNCH_LINE_SCOPED = [
334
+ { file: 'src/bongos/serve-internal.js', mustMention: /prelaunch|PRE-LAUNCH/i },
335
+ ];
336
+ const PRELAUNCH_COMPAT = /cb_stealth|stealth-v1|__stealth|STEALTH_PASSWORD/;
337
+ const PRELAUNCH_CITATION = /task 1003152|ADR 0182|stealth-gate\.js|platform_identity_projects/;
338
+
339
+ function checkPrelaunchVocabulary() {
340
+ const violations = [];
341
+ let scanned = 0;
342
+ const scanFile = (rel, lineFilter = null) => {
343
+ const abs = path.join(CORE_ROOT, rel);
344
+ let src;
345
+ try { src = fs.readFileSync(abs, 'utf8'); } catch { return; }
346
+ scanned += 1;
347
+ src.split('\n').forEach((line, i) => {
348
+ if (lineFilter && !lineFilter.test(line)) return;
349
+ if (!/stealth/i.test(line)) return;
350
+ if (PRELAUNCH_COMPAT.test(line)) return;
351
+ if (PRELAUNCH_CITATION.test(line)) return;
352
+ violations.push(`${rel}:${i + 1}: "stealth" — on the pre-launch surface the word is PRELAUNCH. `
353
+ + '"stealth" belongs to the PRODUCT\'s visibility axis (ADR 0182), which means almost the opposite.'
354
+ + ` → ${line.trim().slice(0, 90)}`);
355
+ });
356
+ };
357
+ for (const rel of PRELAUNCH_SURFACE) scanFile(rel);
358
+ for (const { file, mustMention } of PRELAUNCH_LINE_SCOPED) scanFile(file, mustMention);
359
+ return {
360
+ name: 'the pre-launch gate is PRELAUNCH; "stealth" is the product\'s word (task 1003152)',
361
+ ok: violations.length === 0,
362
+ hardFail: violations.length > 0,
363
+ violations,
364
+ warnings: [],
365
+ note: `${scanned} pre-launch-surface file(s) scanned (${PRELAUNCH_SURFACE.length} whole plus `
366
+ + `${PRELAUNCH_LINE_SCOPED.length} line-scoped to the lines naming the gate); no allowlist — `
367
+ + 'a documented compatibility identifier or a line CITING the rename are the only exemptions.',
368
+ };
369
+ }
370
+
301
371
  module.exports = {
302
372
  GOAL_VOCAB_CITATION,
303
373
  GOAL_VOCAB_LINE_SCOPED,
304
374
  GOAL_VOCAB_SURFACE,
305
375
  LIFECYCLE_DB_FILES,
376
+ PRELAUNCH_CITATION,
377
+ PRELAUNCH_COMPAT,
378
+ PRELAUNCH_LINE_SCOPED,
379
+ PRELAUNCH_SURFACE,
306
380
  RENAME_IDENTITY_RE,
307
381
  VOCAB_ALLOW,
308
382
  VOCAB_BANNED,
@@ -312,6 +386,7 @@ module.exports = {
312
386
  VOCAB_SURFACE,
313
387
  checkGoalMembershipVocabulary,
314
388
  checkGovernmentRenameIdentity,
389
+ checkPrelaunchVocabulary,
315
390
  checkVocabularyWall,
316
391
  vocabFilesToScan,
317
392
  };
@@ -36,7 +36,7 @@ const { resolveCoreRoot, resolveInstanceRoot } = require('../../src/instance-con
36
36
  const { CORE_ROOT, INSTANCE_ROOT, MODULE_ROOTS, REPO_ROOT, rel, walkJs, walkJsAllRoots } = require('./fitness-lib.js');
37
37
  const { IDENTITY_SCAN_ALLOWED, IDENTITY_SCAN_EXCLUDE, IDENTITY_SCAN_EXTRA_FILES, IDENTITY_SCAN_EXTS, IDENTITY_SCAN_ROOTS, IDENTITY_SCAN_SKIP_DIRS, PROMPT_IDENTITY_LITERALS, checkDesignContract, checkKernelPromptsNoHardcodedIdentity, checkNeutralConfigsNoHostIdentity, collectIdentityFiles, scanPromptIdentity, stripCommentsForIdentity } = require('./fitness-checks-identity.js');
38
38
  const { WRITE_VALIDATION_BASELINE, WRITE_VALIDATION_EXEMPT, checkWriteRoutesValidated, scanUnvalidatedWriteRoutes } = require('./fitness-checks-write-validation.js');
39
- const { GOAL_VOCAB_CITATION, GOAL_VOCAB_LINE_SCOPED, GOAL_VOCAB_SURFACE, LIFECYCLE_DB_FILES, RENAME_IDENTITY_RE, VOCAB_ALLOW, VOCAB_BANNED, VOCAB_CITATION, VOCAB_EXTS, VOCAB_HISTORICAL_REF, VOCAB_SURFACE, checkGoalMembershipVocabulary, checkGovernmentRenameIdentity, checkVocabularyWall, vocabFilesToScan } = require('./fitness-checks-vocabulary.js');
39
+ const { GOAL_VOCAB_CITATION, GOAL_VOCAB_LINE_SCOPED, GOAL_VOCAB_SURFACE, LIFECYCLE_DB_FILES, PRELAUNCH_SURFACE, RENAME_IDENTITY_RE, VOCAB_ALLOW, VOCAB_BANNED, VOCAB_CITATION, VOCAB_EXTS, VOCAB_HISTORICAL_REF, VOCAB_SURFACE, checkGoalMembershipVocabulary, checkGovernmentRenameIdentity, checkPrelaunchVocabulary, checkVocabularyWall, vocabFilesToScan } = require('./fitness-checks-vocabulary.js');
40
40
  // ============================================================================
41
41
  // Check 1 — core ↔ host/game import boundary (ADR 0062 §1 cut-line)
42
42
  // ============================================================================
@@ -1374,7 +1374,7 @@ const CHECKS = [
1374
1374
  checkErrorEnvelope, // Check 17 — BV1 R03 / task 1990: no route hand-rolls a flat error (ADR 0116)
1375
1375
  checkVocabularyWall, // Check 19 — ADR 0174 / goal 1000068 R07: "role" is a craft, never a rank
1376
1376
  checkGoalMembershipVocabulary, // Check 19b — ADR 0177 / task 1003013: the FOURTH "role" (goal membership)
1377
- checkGovernmentRenameIdentity, // Check 19c task 1003120: `X` `X`, the rename arrow a sweep ate both sides of (the shape checks 19/19b are blind to)
1377
+ checkGovernmentRenameIdentity, checkPrelaunchVocabulary, // 19c task 1003120 (`X`→`X` rename arrow) + 19d task 1003152 ("stealth" is the product's word) — sharing a line: this file is AT the 1,500 budget (Check 23/24 precedent below; task 1003825 buys it back)
1378
1378
  checkWriteRoutesValidated, // Check 18 — BV1 R12 / task 1999: every body-reading write route validates (ADR 0118)
1379
1379
  require('./fitness-ratchets.js').checkQualityRatchets, // Check 20 — task 1003129: quality budgets only tighten (mechanics + knip CI step live in fitness-ratchets.js)
1380
1380
  require('./doc-cli-guard.js').checkDocNamesRealCli, // Check 22 — task 1003165 / G01: a doc that names a CLI invocation must match the real CLI
@@ -1436,7 +1436,7 @@ if (require.main === module) main();
1436
1436
 
1437
1437
  module.exports = {
1438
1438
  checkVocabularyWall, VOCAB_ALLOW, VOCAB_CITATION, VOCAB_EXTS, vocabFilesToScan,
1439
- checkGoalMembershipVocabulary, GOAL_VOCAB_SURFACE, checkGovernmentRenameIdentity, RENAME_IDENTITY_RE, // 19c task 1003120
1439
+ checkGoalMembershipVocabulary, GOAL_VOCAB_SURFACE, checkGovernmentRenameIdentity, RENAME_IDENTITY_RE, checkPrelaunchVocabulary, PRELAUNCH_SURFACE, // 19c task 1003120, 19d task 1003152
1440
1440
  runAll,
1441
1441
  resolvesToHostGame,
1442
1442
  checkCoreHostBoundary,
@@ -24,8 +24,8 @@
24
24
  // - What a member is shown must not be cached for a stranger: a passed
25
25
  // response leaves marked private, whatever the route behind it said.
26
26
  //
27
- // It is NOT the pre-launch gate (stealth-gate.js): that one is a shared
28
- // password for an unannounced instance, inert unless STEALTH_PASSWORD is set,
27
+ // It is NOT the pre-launch gate (prelaunch-gate.js): that one is a shared
28
+ // password for an unannounced instance, inert unless PRELAUNCH_PASSWORD is set,
29
29
  // and it mounts first. This one is a per-project policy with accounts behind it.
30
30
  // Both can be on at once; each refuses on its own terms.
31
31
 
@@ -67,7 +67,9 @@ const API = `(?:${ALL_API_PREFIXES.map(escRe).join('|')})`;
67
67
  // /downloads/* the installer script and the Dev Box updater — a
68
68
  // cookie-less `curl … | sh`, and release redirects carry
69
69
  // no project data.
70
- // /__stealth the pre-launch gate's own submit, when both gates are on.
70
+ // /__prelaunch the pre-launch gate's own submit, when both gates are on.
71
+ // /__stealth is its pre-rename alias (task 1003152) and is
72
+ // exempt for as long as the gate still accepts it.
71
73
  const EXEMPT = [
72
74
  new RegExp(`^${API}/auth(/|$)`),
73
75
  new RegExp(`^${API}/instance$`),
@@ -76,6 +78,7 @@ const EXEMPT = [
76
78
  /^\/healthz$/,
77
79
  /^\/version$/,
78
80
  /^\/metrics$/,
81
+ /^\/__prelaunch$/,
79
82
  /^\/__stealth$/,
80
83
  /^\/downloads\//,
81
84
  ];
@@ -127,7 +130,7 @@ function signInHref(req) {
127
130
  }
128
131
 
129
132
  // One leading slash, no '//' (protocol-relative), no backslash, no control
130
- // bytes — the stealth gate's rule; anything else returns to '/'.
133
+ // bytes — the pre-launch gate's rule; anything else returns to '/'.
131
134
  function relativeReturn(raw) {
132
135
  if (typeof raw !== 'string' || !raw.length || raw.length > 512) return null;
133
136
  if (!raw.startsWith('/') || raw.startsWith('//') || raw.includes('\\')) return null;