@bongos/core 1.19.907 → 1.19.910

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.
@@ -541,7 +541,7 @@
541
541
  "text": "Asked {…}",
542
542
  "occurrences": 2,
543
543
  "at": [
544
- "modules/hall-ui/public/collab.js:58",
544
+ "modules/hall-ui/public/collab.js:66",
545
545
  "modules/hall-ui/public/watch.js:488"
546
546
  ]
547
547
  },
@@ -2412,7 +2412,7 @@
2412
2412
  "surface": "builders-hall",
2413
2413
  "text": "All open asks",
2414
2414
  "file": "modules/hall-ui/public/collab.html",
2415
- "line": 71,
2415
+ "line": 73,
2416
2416
  "origin": "html-text",
2417
2417
  "confidence": "certain"
2418
2418
  },
@@ -2511,7 +2511,7 @@
2511
2511
  "surface": "builders-hall",
2512
2512
  "text": "Answering is optional and declining costs nothing. Nobody is assigned by being asked.",
2513
2513
  "file": "modules/hall-ui/public/collab.html",
2514
- "line": 97,
2514
+ "line": 100,
2515
2515
  "origin": "html-text",
2516
2516
  "confidence": "certain"
2517
2517
  },
@@ -2628,7 +2628,7 @@
2628
2628
  "surface": "builders-hall",
2629
2629
  "text": "Asked {…}",
2630
2630
  "file": "modules/hall-ui/public/collab.js",
2631
- "line": 58,
2631
+ "line": 66,
2632
2632
  "origin": "js-markup",
2633
2633
  "confidence": "certain"
2634
2634
  },
@@ -3519,7 +3519,7 @@
3519
3519
  "surface": "builders-hall",
3520
3520
  "text": "Copy for Session Start",
3521
3521
  "file": "modules/hall-ui/public/collab.js",
3522
- "line": 90,
3522
+ "line": 98,
3523
3523
  "origin": "js-markup",
3524
3524
  "confidence": "certain"
3525
3525
  },
@@ -3771,7 +3771,7 @@
3771
3771
  "surface": "builders-hall",
3772
3772
  "text": "Could not load requests — reload to try again.",
3773
3773
  "file": "modules/hall-ui/public/collab.js",
3774
- "line": 529,
3774
+ "line": 551,
3775
3775
  "origin": "js-text-assign",
3776
3776
  "confidence": "certain"
3777
3777
  },
@@ -6471,7 +6471,7 @@
6471
6471
  "surface": "builders-hall",
6472
6472
  "text": "Mingle pairs you with someone from another role. Saying no costs nothing.",
6473
6473
  "file": "modules/hall-ui/public/collab.html",
6474
- "line": 87,
6474
+ "line": 90,
6475
6475
  "origin": "html-text",
6476
6476
  "confidence": "certain"
6477
6477
  },
@@ -6840,7 +6840,7 @@
6840
6840
  "surface": "builders-hall",
6841
6841
  "text": "No introductions right now. One appears here when a pairing round runs.",
6842
6842
  "file": "modules/hall-ui/public/collab.html",
6843
- "line": 90,
6843
+ "line": 93,
6844
6844
  "origin": "html-text",
6845
6845
  "confidence": "certain"
6846
6846
  },
@@ -7002,7 +7002,7 @@
7002
7002
  "surface": "builders-hall",
7003
7003
  "text": "Nobody has an open ask right now.",
7004
7004
  "file": "modules/hall-ui/public/collab.html",
7005
- "line": 75,
7005
+ "line": 78,
7006
7006
  "origin": "html-text",
7007
7007
  "confidence": "certain"
7008
7008
  },
@@ -7011,7 +7011,7 @@
7011
7011
  "surface": "builders-hall",
7012
7012
  "text": "Nobody is waiting on you. Requests reach you by role —",
7013
7013
  "file": "modules/hall-ui/public/collab.html",
7014
- "line": 53,
7014
+ "line": 54,
7015
7015
  "origin": "html-text",
7016
7016
  "confidence": "certain"
7017
7017
  },
@@ -7056,7 +7056,7 @@
7056
7056
  "surface": "builders-hall",
7057
7057
  "text": "Not now",
7058
7058
  "file": "modules/hall-ui/public/collab.js",
7059
- "line": 435,
7059
+ "line": 457,
7060
7060
  "origin": "js-markup",
7061
7061
  "confidence": "certain"
7062
7062
  },
@@ -8064,7 +8064,7 @@
8064
8064
  "surface": "builders-hall",
8065
8065
  "text": "Read the full ask",
8066
8066
  "file": "modules/hall-ui/public/collab.js",
8067
- "line": 90,
8067
+ "line": 98,
8068
8068
  "origin": "js-markup",
8069
8069
  "confidence": "certain"
8070
8070
  },
@@ -8667,7 +8667,7 @@
8667
8667
  "surface": "builders-hall",
8668
8668
  "text": "Say yes",
8669
8669
  "file": "modules/hall-ui/public/collab.js",
8670
- "line": 435,
8670
+ "line": 457,
8671
8671
  "origin": "js-markup",
8672
8672
  "confidence": "certain"
8673
8673
  },
@@ -8982,7 +8982,7 @@
8982
8982
  "surface": "builders-hall",
8983
8983
  "text": "Show all {…}",
8984
8984
  "file": "modules/hall-ui/public/collab.js",
8985
- "line": 253,
8985
+ "line": 275,
8986
8986
  "origin": "js-text-assign",
8987
8987
  "confidence": "certain"
8988
8988
  },
@@ -9189,7 +9189,7 @@
9189
9189
  "surface": "builders-hall",
9190
9190
  "text": "Someone helped",
9191
9191
  "file": "modules/hall-ui/public/collab.js",
9192
- "line": 203,
9192
+ "line": 211,
9193
9193
  "origin": "js-markup",
9194
9194
  "confidence": "certain"
9195
9195
  },
@@ -11232,7 +11232,7 @@
11232
11232
  "surface": "builders-hall",
11233
11233
  "text": "You have no open asks.",
11234
11234
  "file": "modules/hall-ui/public/collab.html",
11235
- "line": 64,
11235
+ "line": 66,
11236
11236
  "origin": "html-text",
11237
11237
  "confidence": "certain"
11238
11238
  },
@@ -11295,7 +11295,7 @@
11295
11295
  "surface": "builders-hall",
11296
11296
  "text": "Your asks",
11297
11297
  "file": "modules/hall-ui/public/collab.html",
11298
- "line": 61,
11298
+ "line": 62,
11299
11299
  "origin": "html-text",
11300
11300
  "confidence": "certain"
11301
11301
  },
@@ -12600,7 +12600,7 @@
12600
12600
  "surface": "builders-hall",
12601
12601
  "text": "set yours in Settings",
12602
12602
  "file": "modules/hall-ui/public/collab.html",
12603
- "line": 54,
12603
+ "line": 55,
12604
12604
  "origin": "html-text",
12605
12605
  "confidence": "certain"
12606
12606
  },
package/docs/file-map.md CHANGED
@@ -140,6 +140,9 @@
140
140
  │ ├── ui-design/ ← CORE-DOMAIN module (task 1003324 / ADR 0197, default-on): the design capability every builder on every instance gets — ships METHOD, never a world. Absorbs ADR 0081's layer: adapters/{claude-design,figma}/index.js (moved from src/ui/adapters/), scripts/{design-sync,figma-design-sync,validate-design}.js (moved from scripts/gds/, which keeps thin shims), docs/design-contract.md (moved from docs/). Declaration-only: contributes.skills design (the /design playbook, new) + design-sync + figma-design-sync; no routes/port/migrations, so disabling it touches no served surface. CLAUDE.md separates the platform FLOORS (the Fifteen Rule, the dark twins, no page :root, the 24px floor, AA in both modes, the kill switch) from the instance's WORLD (its packs + DESIGN.md). kit/ (task 1003320) is the look-before-you-ship kit: lib.js (the floors, the states/probes contracts, the action + expectation vocabularies, the WCAG maths, the stub spawner), serve.js (any surface through the doorway's real transforms + fixtures/), render.js (states × 1440/390/320 × dark/light, audited), probe.js (PASS/FAIL contracts), check-mock.js (platform rules + the instance's DESIGN.md taste bans, then the detector), tells.js (task 1003329 / ADR 0223: the anti-pattern detector as two tiers — floors FAIL, craft tells WARN, kit-ignore waivers per file; the rendered twins live in lib.js factsInPage), contrast.js + png.js; a page's <page>.states.json / <page>.probes.json sit BESIDE the surface (modules/public-landing/public/, modules/hall-ui/public/). Recipe: docs/recipes/ui-look-before-you-ship.md. skills/ (task 1003321 / ADR 0198) is the vendoring policy: policy.json (licence allowlist, verdicts that may land, sidecar fields, preamble markers), PREAMBLE.md (the platform preamble every vendored SKILL.md opens with), README.md (the roster — both origins scanned dangerous on 2026-08-28, landed as rebuild specs; nothing vendored — and the landing recipe); a landable copy goes under skills/vendor/<name>/ with PROVENANCE.md + LICENSE and is materialised into an instance's .claude/skills/ by claude-materialize.js step 1b. skills/<name>/ (task 1003325 / ADR 0220) are the in-house rebuilds from the specs, first-party and counted: design-taste-frontend (the default taste skill), design-taste-frontend-v1 (the earlier generation, kept distinct), high-end-visual-design (the polish pass), redesign-existing-projects (the in-place upgrade); each opens with the same preamble and carries a "Rebuilt from" section instead of a sidecar. impeccable/ (task 1003326 / ADR 0221) is the design-craft method as ONE skill with sub-commands: SKILL.md (the router, the menu, the craft floor, the bounded finish) + reference/<sub-command>.md (eighteen playbooks, one loaded per request); no hooks, no bypass agent, no telemetry, no live server. minimalist-ui/, industrial-brutalist-ui/, gpt-taste/, stitch-design-taste/ (task 1003327 / ADR 0230) are the four looks, composition grammars with no colours of their own: the palette is the instance's pack and the texture is a named entry in styles/; a substrate is an archetype that still ships both modes, gpt-taste's selection is derived from the route rather than rolled, and stitch-design-taste writes a DESIGN.md-shaped contract document whose thirteen are transcribed from the pack. image-to-code/, imagegen-frontend-web/, imagegen-frontend-mobile/, brandkit/ (task 1003328 / ADR 0222) are the image family under the hero-only rule: plates and hero objects only, the pair (JPEG dark plate + WebP alpha cutout) with a <file>.json sidecar beside each, through the instance's art pipeline key; the grove specimen's styles/grove/assets/ is the worked example. style/ (task 1003475 / ADR 0231) is the module's first FIRST-PARTY skill (no upstream, so a `## First party` section instead of `## Rebuilt from`): the /style session in which an owner authors their own look in four bounded steps, rendering the nearest library entries before asking, drafting inside the contract, retuning by measurement, and handing over the one-step adoption without ever writing config/branding.json. styles/ (task 1003323 / ADR 0219) is the style library: <name>/pack.json (the theme.ui overlay, exactly the fifteen) + DESIGN.md (the look's derived recipes, thirteen in lockstep with the pack) + mock.html (the specimen); chrome-world (pinned to the neutral pack), expedition, grove, blueprint (the first cool look, authored through /style at task 1003475); STYLE=<name> on the kit lays a look over the instance's pack through GDS_BRANDING_FILE; tests/ui_design_styles.mjs runs the world/cosmos/hall token suites once per alternate (UI_DESIGN_PACK). config/design-tokens.* + design-sources.json STAY in config/ as the host boundary; public-landing keeps its own key (first customer, not owner).
141
141
  │ ├── character-anim/ ← consistent-character generative animation (ADR 0088) — graded painterly frame sets + manifest, the otb-character-review skill. CLAUDE.md, module.json.
142
142
  │ ├── provisioning/ ← stands up + operates a Cloud Bongos instance's own infra (server/DB/domain/DNS/TLS) via the API (ADR 0111); web-tier request+state only, no cloud tokens — a separate control-plane runner drains the intents. Distinct from the `hosting` module (ADR 0092), which brokers compute workloads. CLAUDE.md, module.json.
143
+ │ ├── government/ ← CORE-DOMAIN module (ADR 0151 / BV1 goal 1000030, renamed governance→government by ADR 0174, default-on): Discord-style configurable ranks + permissions. The PERMISSION is the atomic authority unit and the four ranks (xenos/thetes/metic/archon) are the positions in the government; the module owns the code-defined permission catalog — each key flagged `system` (trust-boundary), `substrate` (enforced out of band) or operational (delegable) — plus the compatibility seed that reproduces the old rank thresholds exactly. ADR 0016 is preserved: authority is still resolved server-side, per request, uncached. The word `role` is deliberately absent — it is reserved for the crafts. CLAUDE.md, module.json, catalog.js, ranks.js, resolver.js, config.js, db.js, board.js + board-membership.js + membership-predicate.js, session-scopes.{js,json}, protected-surfaces.{js,json}, routes/ (government + rank-sync), pollers/ (board-expiry), migrations/.
144
+ │ ├── specialities/ ← CORE-DOMAIN module (task 1003855 / ADR 0296, default-on): role SUB-SPECIALITIES — a named bundle of knowledge, skills and an interaction contract sitting UNDER a craft. A craft says HOW you work (engineer/ideator/artist/ui); a speciality says WHAT you work on, and how a subject-matter expert wants to be engaged inside it. Deliberately thin over three surfaces that already exist — discipline-modes.json for craft→playbook, behavior_prefs.interaction for the per-session contract (a speciality contract rides the SAME GET /me → Conductor path rather than adding a second one), and the ADR 0083 manifest for declared skills; the only new machinery is the knowledge bundle. Training is two-fold: documents the builder supplied, plus learnings captured from sessions worked in the speciality (pointing at existing `learnings` rows, never a second corpus). Owned per-builder OR per-project, each carrying its owner's sharing decision (private/project/public) and a sellable flag that records INTENT only — ADR 0107 defers the marketplace, so there is nothing to list on. CLAUDE.md, module.json, specialities.js, db.js, routes/, migrations/.
145
+ │ ├── agents/ ← declarative agent registry (BV1 goal 1000038 Phase 1) — the one module that ships `default:false`, so shipped is not enabled and an instance opts in per ADR 0083. `.claude/agents/*.md` are reconciled at deploy into `agents_definitions` plus the per-fire `agents_runs` ledger; files-are-source (an instance-authored DB row is never clobbered), and `author_rank` is stamped from the author's LIVE DB rank because a committed file cannot grant authority (ADR 0016). Spawns consume the EXISTING read-only `grade` port, so v1 agents are observe-and-report only — no write principals — and every fire bills through the shared LLM cost cache. `spend.requiresPayer` (task 1003884): a fire spends real money for whoever called it, so the core REFUSES to enable the module on an instance whose join door is `open` with no payer configured — an account created seconds ago would otherwise spend the instance's money with nobody to bill. lib/spend-ceiling.js is the per-payer monthly ceiling, failing closed when it is exhausted OR unreadable; lib/fire-budget.js is the separate RATE ceiling. CLAUDE.md, module.json, spawn.js, lib/, routes/, pollers/ (event-router), migrations/.
143
146
  │ └── demo/ ← BV1.R52 acceptance-proof module: one rank-gated write route + one namespaced migration proving the loader mounts a scaffolded module with zero core edits. CLAUDE.md, module.json.
144
147
  │
145
148
  ├── scripts/
@@ -2288,5 +2288,11 @@ is load-bearing: the script throws rather than guess if it is missing, and
2288
2288
  landed since 1.19.905 with no explicit bump. run 35514202453. (task 1002620)
2289
2289
  1.19.907 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2290
2290
  landed since 1.19.906 with no explicit bump. run 35514739370. (task 1002620)
2291
+ 1.19.908 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2292
+ landed since 1.19.907 with no explicit bump. run 35516559010. (task 1002620)
2293
+ 1.19.909 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2294
+ landed since 1.19.908 with no explicit bump. run 35517426559. (task 1002620)
2295
+ 1.19.910 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2296
+ landed since 1.19.909 with no explicit bump. run 35519713709. (task 1002620)
2291
2297
  ---------------------------------------------------------------------------
2292
2298
  ```
@@ -73,6 +73,75 @@
73
73
  return 'waiting ' + weeks + ' weeks';
74
74
  }
75
75
 
76
+ // A PILL MARKS A ROW WORSE THAN ITS ROOM (task 1004067, from idea 1001065).
77
+ //
78
+ // Task 1004041 fixed half of this: the words now carry the real age, and the
79
+ // two calm bands lost their ring. What it could not fix from inside a single
80
+ // row is the case the report is actually about — a room where EVERY row is
81
+ // genuinely past a week. There the condition is not too loose; it is correct
82
+ // on all fourteen, and fourteen correct alarms are still not a signal. The
83
+ // instrument is wrong, not the threshold: a per-row badge can only say "this
84
+ // row is bad", and it has no way to say "this row is bad AND so is every other
85
+ // one", which is the fact a reader needs.
86
+ //
87
+ // So the room states its own condition once, in the block head, and a row
88
+ // wears the pill only when it is MORE SEVERE than that baseline. On a mixed
89
+ // board no band is dominant, nothing is muted, and the page reads exactly as
90
+ // it does today. On a uniformly stale board the alarm is said once instead of
91
+ // fourteen times, and the per-row words still carry each real age — so nothing
92
+ // goes silently dead, which is the criterion the bands exist to satisfy.
93
+ //
94
+ // Two dials, both deliberately blunt. A room under BASELINE_MIN_ROWS is not a
95
+ // wall — four pills can be read one at a time, so a short list keeps every
96
+ // one. BASELINE_SHARE is below 1 because thirteen-of-fourteen reads as a wall
97
+ // just as much as fourteen does, and a rule that only fires on unanimity is a
98
+ // rule that almost never fires.
99
+ var BASELINE_MIN_ROWS = 4;
100
+ var BASELINE_SHARE = 0.7;
101
+
102
+ // Only a band that actually WEARS a pill can be a baseline (severity >= 2 —
103
+ // aging and stale; collab.css rings those two and leaves the calm pair as
104
+ // plain text). A room that is uniformly 'just asked' has no alarm to move to
105
+ // the head, and announcing one there would invent the very noise this removes.
106
+ function roomBaseline(reqs, nowMs) {
107
+ var list = Array.isArray(reqs) ? reqs : [];
108
+ if (list.length < BASELINE_MIN_ROWS) return null;
109
+ var counts = {};
110
+ for (var i = 0; i < list.length; i += 1) {
111
+ var id = ageBand(list[i] && list[i].created_at, nowMs).id;
112
+ counts[id] = (counts[id] || 0) + 1;
113
+ }
114
+ // BANDS is severest-first, and two bands cannot both clear a share above a
115
+ // half, so the first match is the only match.
116
+ for (var j = 0; j < BANDS.length; j += 1) {
117
+ var band = BANDS[j];
118
+ var n = counts[band.id] || 0;
119
+ if (band.severity >= 2 && n / list.length >= BASELINE_SHARE) {
120
+ return { band: band, count: n, total: list.length };
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+
126
+ // At or below the room's baseline, a row is the rule and reads quiet; above
127
+ // it, a row is the exception and keeps its pill. With no baseline nothing is
128
+ // quieted, which is what makes this inert on every mixed board.
129
+ function isRoomQuiet(band, baseline) {
130
+ if (!band || !baseline) return false;
131
+ return band.severity <= baseline.band.severity;
132
+ }
133
+
134
+ // The fact the pills used to repeat, said once. The band's own label supplies
135
+ // the words, so the head and the rows can never describe the same threshold
136
+ // two different ways.
137
+ function roomAgeLine(baseline) {
138
+ if (!baseline) return '';
139
+ var many = baseline.count === baseline.total
140
+ ? 'all ' + baseline.total
141
+ : baseline.count + ' of ' + baseline.total;
142
+ return many + ' ' + baseline.band.label;
143
+ }
144
+
76
145
  // Oldest first WITHIN the page, because the thing most at risk of dying
77
146
  // silently should be read first. This is in ADDITION to the band, never
78
147
  // instead of it.
@@ -336,6 +405,8 @@
336
405
 
337
406
  window.OTBCollabLib = {
338
407
  BANDS: BANDS, ageBand: ageBand, ageLabel: ageLabel,
408
+ roomBaseline: roomBaseline, isRoomQuiet: isRoomQuiet, roomAgeLine: roomAgeLine,
409
+ BASELINE_MIN_ROWS: BASELINE_MIN_ROWS, BASELINE_SHARE: BASELINE_SHARE,
339
410
  byAgeOldestFirst: byAgeOldestFirst, byNewestFirst: byNewestFirst,
340
411
  askerOf: askerOf, contextOf: contextOf,
341
412
  directionLine: directionLine, boardLine: boardLine,
@@ -43,6 +43,33 @@
43
43
  padding-right: 0;
44
44
  }
45
45
 
46
+ /* THE ROOM'S OWN CONDITION IS NOT A PER-ROW WARNING (task 1004067). When a
47
+ block is uniformly old, every row's band is correct and every row's pill is
48
+ therefore identical — the wall the report describes. A row at or below its
49
+ room's baseline drops to the same plain quiet text the calm bands wear, so
50
+ the only chips left in the column are the rows that are worse than their
51
+ room. Two classes, so this outranks the band ink it replaces without an
52
+ !important. The WORDS are untouched: each row still states its own real age,
53
+ which is what keeps an old ask visibly aging rather than silently dead. */
54
+ .cb-age.cb-age--quiet {
55
+ color: var(--ink-soft);
56
+ border-color: transparent;
57
+ padding-left: 0;
58
+ padding-right: 0;
59
+ font-weight: 400;
60
+ }
61
+
62
+ /* The fact the chips used to repeat, stated once in the head. It sits in the
63
+ band's own ink because here it IS the exception — one line on a page, not one
64
+ per row — and it is the only thing on the head that is allowed to be loud. */
65
+ .cb-block__age {
66
+ font-size: 13px;
67
+ letter-spacing: 0;
68
+ color: var(--ink-soft);
69
+ }
70
+ .cb-block__age--aging { color: var(--warn); }
71
+ .cb-block__age--stale { color: var(--danger); font-weight: 600; }
72
+
46
73
  /* ---- the figure strip -----------------------------------------------------
47
74
  The shell's .stat-row, unchanged, except that this page's third tile holds
48
75
  WORDS (the roles you answer for) rather than a figure. The shell's own
@@ -46,6 +46,7 @@
46
46
  <section class="scroll cb-block" aria-labelledby="cb-for-me-h">
47
47
  <div class="cb-block__head">
48
48
  <h2 class="scroll__h" id="cb-for-me-h">Asked of you <span class="cb-block__n" id="cb-for-me-n"></span></h2>
49
+ <span class="cb-block__age" id="cb-for-me-age" hidden></span>
49
50
  <span class="cb-faces" id="cb-for-me-faces" aria-hidden="true"></span>
50
51
  </div>
51
52
  <ul class="ov-rows" id="cb-for-me"></ul>
@@ -59,6 +60,7 @@
59
60
  <section class="scroll cb-block" aria-labelledby="cb-mine-h">
60
61
  <div class="cb-block__head">
61
62
  <h2 class="scroll__h" id="cb-mine-h">Your asks <span class="cb-block__n" id="cb-mine-n"></span></h2>
63
+ <span class="cb-block__age" id="cb-mine-age" hidden></span>
62
64
  </div>
63
65
  <ul class="ov-rows" id="cb-mine"></ul>
64
66
  <div class="empty-state cb-empty" id="cb-mine-empty" hidden><p>You have no open asks.</p></div>
@@ -69,6 +71,7 @@
69
71
  <section class="scroll cb-block cb-block--all" aria-labelledby="cb-all-h">
70
72
  <div class="cb-block__head">
71
73
  <h2 class="scroll__h" id="cb-all-h">All open asks <span class="cb-block__n" id="cb-all-n"></span></h2>
74
+ <span class="cb-block__age" id="cb-all-age" hidden></span>
72
75
  <span class="cb-faces" id="cb-all-faces" aria-hidden="true"></span>
73
76
  </div>
74
77
  <ul class="ov-rows" id="cb-all"></ul>
@@ -53,9 +53,17 @@
53
53
  // exact timestamp stays on the title, where a reader who wants the date can
54
54
  // still reach it — which is also why the separate date line that used to sit
55
55
  // under this pill is gone: the same fact twice is one fact too many.
56
- function ageChip(req, nowMs) {
56
+ //
57
+ // `baseline` is the block's own room condition (L.roomBaseline), and it is the
58
+ // second half of the same verdict: where the label stops fourteen old rows
59
+ // reading as fourteen copies of one alarm, the baseline stops them WEARING
60
+ // fourteen copies of one alarm. A row at or below its room's baseline is the
61
+ // rule, not the exception, so it drops the ring and the danger ink and keeps
62
+ // only its words — the head says the room's condition once instead.
63
+ function ageChip(req, nowMs, baseline) {
57
64
  const band = L.ageBand(req.created_at, nowMs);
58
- return `<span class="fact-pill cb-age cb-age--${band.id}" title="Asked ${escapeHtml(String(req.created_at || ''))}">${escapeHtml(L.ageLabel(req.created_at, nowMs))}</span>`;
65
+ const quiet = L.isRoomQuiet(band, baseline) ? ' cb-age--quiet' : '';
66
+ return `<span class="fact-pill cb-age cb-age--${band.id}${quiet}" title="Asked ${escapeHtml(String(req.created_at || ''))}">${escapeHtml(L.ageLabel(req.created_at, nowMs))}</span>`;
59
67
  }
60
68
 
61
69
  // A DOM id per row that is stable, unique and safe to put in an attribute. The
@@ -175,14 +183,14 @@
175
183
  // chat mirror). What the page owes a helper is the request, its age, its
176
184
  // context, and who to answer — and their face. A by-name ask is the personal
177
185
  // call on this reader, so its row is the LIT one (.cb-row--live).
178
- function forMeRow(req, nowMs, extra, key) {
186
+ function forMeRow(req, nowMs, extra, key, baseline) {
179
187
  const live = req.needs_builder_id != null ? ' cb-row--live' : '';
180
188
  return `<li class="ov-row ov-row--lead cb-row${live}" data-id="${escapeHtml(String(req.id))}"${extra ? ' data-extra hidden' : ''}>
181
189
  ${faceHtml(L.faceOf(req, 'in'))}
182
190
  <div class="ov-row__main">${askHtml(req, key)}
183
191
  <div class="ov-row__facts">${directionFact(req, 'in')}${contextChip(req)}</div>
184
192
  </div>
185
- <div class="ov-row__right">${ageChip(req, nowMs)}</div>
193
+ <div class="ov-row__right">${ageChip(req, nowMs, baseline)}</div>
186
194
  </li>`;
187
195
  }
188
196
 
@@ -198,12 +206,12 @@
198
206
  // which is precisely the distinction a reader could no longer make. Moving the
199
207
  // pill down to the facts line puts the warning with the other facts about the
200
208
  // ask and leaves the right column as what it is here: the place you act.
201
- function mineRow(req, nowMs, extra, key) {
209
+ function mineRow(req, nowMs, extra, key, baseline) {
202
210
  const id = escapeHtml(String(req.id));
203
211
  return `<li class="ov-row ov-row--lead cb-row" data-id="${id}"${extra ? ' data-extra hidden' : ''}>
204
212
  ${faceHtml(L.faceOf(req, 'out'))}
205
213
  <div class="ov-row__main">${askHtml(req, key)}
206
- <div class="ov-row__facts">${directionFact(req, 'out')}${contextChip(req)}${ageChip(req, nowMs)}</div>
214
+ <div class="ov-row__facts">${directionFact(req, 'out')}${contextChip(req)}${ageChip(req, nowMs, baseline)}</div>
207
215
  </div>
208
216
  <div class="ov-row__right">
209
217
  <div class="ov-row__actions">
@@ -216,13 +224,13 @@
216
224
 
217
225
  // A row on the board of everyone's asks: the asker's face, the third-person
218
226
  // line, no actions — settling is the asker's, in their own block.
219
- function allRow(req, nowMs, extra, key) {
227
+ function allRow(req, nowMs, extra, key, baseline) {
220
228
  return `<li class="ov-row ov-row--lead cb-row" data-id="${escapeHtml(String(req.id))}"${extra ? ' data-extra hidden' : ''}>
221
229
  ${faceHtml(L.faceOf(req, 'in'))}
222
230
  <div class="ov-row__main">${askHtml(req, key)}
223
231
  <div class="ov-row__facts">${boardFact(req)}${contextChip(req)}</div>
224
232
  </div>
225
- <div class="ov-row__right">${ageChip(req, nowMs)}</div>
233
+ <div class="ov-row__right">${ageChip(req, nowMs, baseline)}</div>
226
234
  </li>`;
227
235
  }
228
236
 
@@ -235,13 +243,27 @@
235
243
  function paintBlock(key, rows, rowFn, nowMs, faces) {
236
244
  const list = rows.slice().sort(L.byNewestFirst);
237
245
  const { shown, hidden } = L.splitShown(list, SHOWN);
246
+ // Over the WHOLE list, not the shown three: the door hides rows, it does not
247
+ // change what the room is, and a baseline that flipped when someone pressed
248
+ // "Show all" would re-alarm the rows it had just quieted.
249
+ const baseline = L.roomBaseline(list, nowMs);
238
250
  const host = $(`#cb-${key}`);
239
251
  if (host) {
240
- host.innerHTML = shown.map((r) => rowFn(r, nowMs, false, key)).join('')
241
- + hidden.map((r) => rowFn(r, nowMs, true, key)).join('');
252
+ host.innerHTML = shown.map((r) => rowFn(r, nowMs, false, key, baseline)).join('')
253
+ + hidden.map((r) => rowFn(r, nowMs, true, key, baseline)).join('');
242
254
  }
243
255
  const n = $(`#cb-${key}-n`);
244
256
  if (n) n.textContent = list.length ? String(list.length) : '';
257
+ // The room's condition, said once, beside the count that scopes it.
258
+ const age = $(`#cb-${key}-age`);
259
+ if (age) {
260
+ const line = L.roomAgeLine(baseline);
261
+ age.textContent = line;
262
+ age.hidden = !line;
263
+ age.className = line
264
+ ? `cb-block__age cb-block__age--${baseline.band.id}`
265
+ : 'cb-block__age';
266
+ }
245
267
  const empty = $(`#cb-${key}-empty`);
246
268
  if (empty) empty.hidden = list.length > 0;
247
269
  const facesEl = $(`#cb-${key}-faces`);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.907",
3
+ "version": "1.19.910",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.907",
9
+ "version": "1.19.910",
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.907",
3
+ "version": "1.19.910",
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",
@@ -225,7 +225,7 @@ function checkGeneratedArtifactsFresh() {
225
225
  try {
226
226
  execFileSync(process.execPath, [path.join(__dirname, 'gen-file-map.js'), '--check'], { cwd: REPO_ROOT, stdio: 'pipe' });
227
227
  } catch {
228
- violations.push('docs/file-map.md generated sections (.claude/skills/, .claude/scheduled-tasks/) are STALE or a skill/routine has no note in docs/file-map.notes.json — run `node scripts/gds/gen-file-map.js` and add any missing note, then commit (ADR 0066).');
228
+ violations.push('docs/file-map.md generated sections (.claude/skills/, .claude/scheduled-tasks/) are STALE, a skill/routine has no note in docs/file-map.notes.json, or a directory under modules/ has no file-map entry at all — run `node scripts/gds/gen-file-map.js --check` for the exact list, then fix and commit (ADR 0066; module coverage: task 1004066).');
229
229
  }
230
230
  try {
231
231
  // Also structurally validates the spec (exit 1 on invalid OpenAPI 3.1) beyond the drift check (exit 2). Task 1918.
@@ -16,6 +16,16 @@
16
16
  // • every skill/routine dir on disk MUST have a note → a new skill can't land undescribed;
17
17
  // • every note MUST map to a dir that exists → the map can't carry ghosts;
18
18
  // • the rendered block MUST match what's committed → no stale tree.
19
+ // MODULE COVERAGE (task 1004066). The same honor system that let a skill land unmapped let a whole
20
+ // MODULE land unmapped: `modules/agents`, `modules/government` and `modules/specialities` were all
21
+ // on disk with nothing in the map, and the two that got fixed before this got fixed BY HAND — which
22
+ // is the point, since hand-patching a drifting map does not stop the drift. The modules/ block is
23
+ // not generated (each entry is a paragraph of prose no directory walk could write, and generating
24
+ // the fenced repo tree is the expensive option this task ruled out), so what is enforced is the
25
+ // cheap half: a set comparison asserting every directory under modules/ is documented SOMEWHERE in
26
+ // the map — either a `modules/<key>/…` path reference or a `<key>/` entry inside the modules block.
27
+ // A new module now fails --check until someone writes its line.
28
+ //
19
29
  // scripts/gds/ is deliberately NOT generated: it is a curated highlight reel of ~20 of ~70 files
20
30
  // (generating it would bloat it with low-signal entries), and it sits mid-tree inside the big
21
31
  // fenced repo tree where HTML-comment markers can't live cleanly. It stays hand-curated.
@@ -51,6 +61,13 @@ const SECTIONS = [
51
61
  { id: 'scheduled-tasks', dir: '.claude/scheduled-tasks', notesKey: 'scheduled-tasks' },
52
62
  ];
53
63
 
64
+ // modules/ is NOT generated (task 1004066): each entry is a paragraph of prose no directory walk
65
+ // could write, and generating it would force the whole fenced repo tree through a generator —
66
+ // explicitly out of scope. What IS enforceable is COVERAGE: every module on disk must be documented
67
+ // somewhere in the map. That set comparison is the cheap half of the same contract the skills
68
+ // section already carries, and it is what stops a whole vertical slice landing unmapped.
69
+ const MODULES_DIR = 'modules';
70
+
54
71
  const begin = (id) => `<!-- BEGIN GENERATED FILE-MAP ${id} (scripts/gds/gen-file-map.js — do not hand-edit; notes live in docs/file-map.notes.json) -->`;
55
72
  const end = (id) => `<!-- END GENERATED FILE-MAP ${id} -->`;
56
73
 
@@ -107,6 +124,72 @@ function buildModel() {
107
124
  return SECTIONS.map((s) => buildSection(s, notes));
108
125
  }
109
126
 
127
+ // ── module coverage (task 1004066) ────────────────────────────────────────────
128
+ // A module counts as documented when the map either references its PATH (`modules/<key>/…`
129
+ // anywhere) or carries a `<key>/` entry line inside the `modules/` tree block. Both forms are in
130
+ // use today, so the check accepts both rather than inventing a third the map would fail on.
131
+
132
+ // Both matchers CAPTURE the whole path segment rather than testing for a given key. That is what
133
+ // makes the prefix hazard structural instead of something a lookahead has to remember: a mention of
134
+ // `modules/agents-extra` yields the key `agents-extra`, so it can never vouch for `agents`. It also
135
+ // means the map is scanned ONCE per run instead of twice per module.
136
+ const PATH_REF_RE = /modules\/([A-Za-z0-9_.-]+)/g;
137
+ const ENTRY_RE = /[├└]──\s+([A-Za-z0-9_.-]+)\//g;
138
+
139
+ // The slice of the fenced repo tree between the `├── modules/` header and the next sibling at the
140
+ // same depth. Scoping the entry search here matters: `.claude/skills/` lists dirs in the same
141
+ // `├── name/` shape, so an unscoped match would let a SKILL named after a module vouch for it.
142
+ function modulesBlock(mapText) {
143
+ const lines = String(mapText).split(/\r?\n/);
144
+ const start = lines.findIndex((l) => new RegExp(`^[├└]── ${MODULES_DIR}/`).test(l));
145
+ if (start === -1) return '';
146
+ let stop = lines.length;
147
+ for (let i = start + 1; i < lines.length; i += 1) {
148
+ if (/^[├└]── /.test(lines[i])) { stop = i; break; }
149
+ }
150
+ return lines.slice(start, stop).join('\n');
151
+ }
152
+
153
+ // Every module key the map documents: any `modules/<key>` path reference anywhere, plus any
154
+ // `<key>/` entry line inside the modules block. Both forms are in use today, so both count.
155
+ function documentedModuleKeys(mapText, block) {
156
+ const keys = new Set();
157
+ for (const m of String(mapText).matchAll(PATH_REF_RE)) keys.add(m[1]);
158
+ for (const m of String(block).matchAll(ENTRY_RE)) keys.add(m[1]);
159
+ return keys;
160
+ }
161
+
162
+ // The single-key form — the readable one to assert against.
163
+ function isModuleDocumented(mapText, block, key) {
164
+ return documentedModuleKeys(mapText, block).has(key);
165
+ }
166
+
167
+ // Every immediate subdirectory of modules/, or [] when the tree carries no modules/ at all (a
168
+ // consumer instance that enabled none — nothing to document).
169
+ function listModules() {
170
+ return listSubdirs(path.join(REPO_ROOT, MODULES_DIR));
171
+ }
172
+
173
+ // The set comparison. Returns { onDisk, undocumented, defects } — `defects` is the non-vacuity
174
+ // channel: this check breaks by comparing two empty sets and reporting nothing, so a state that
175
+ // makes the comparison meaningless (modules on disk but no readable map, or no modules/ block to
176
+ // read entries out of) is reported as a SCAN DEFECT rather than passing quietly.
177
+ function moduleCoverage(mapText) {
178
+ const onDisk = listModules();
179
+ const defects = [];
180
+ if (!onDisk.length) return { onDisk, undocumented: [], defects };
181
+ const text = typeof mapText === 'string' ? mapText : (fs.existsSync(MAP_PATH) ? fs.readFileSync(MAP_PATH, 'utf8') : '');
182
+ if (!text.trim()) {
183
+ defects.push(`${onDisk.length} module(s) on disk but docs/file-map.md is missing or empty — the module-coverage check cannot run`);
184
+ return { onDisk, undocumented: [], defects };
185
+ }
186
+ const block = modulesBlock(text);
187
+ if (!block) defects.push('docs/file-map.md has no `modules/` block in its repo tree — module entries cannot be read (path references are still checked)');
188
+ const documented = documentedModuleKeys(text, block);
189
+ const undocumented = onDisk.filter((key) => !documented.has(key));
190
+ return { onDisk, undocumented, defects };
191
+ }
192
+
110
193
  // Render one section's fenced tree block (the bytes that live between its markers). Arrows are
111
194
  // column-aligned to the longest label, matching the hand-formatted look of the rest of the map.
112
195
  function renderSection(model) {
@@ -144,6 +227,7 @@ function computeWrites() {
144
227
  const stale = model.flatMap((m) => m.stale);
145
228
  const writes = [];
146
229
  let markersMissing = [];
230
+ let mapText = '';
147
231
  if (fs.existsSync(MAP_PATH)) {
148
232
  let cur = fs.readFileSync(MAP_PATH, 'utf8');
149
233
  let next = cur;
@@ -152,8 +236,10 @@ function computeWrites() {
152
236
  next = injectSection(next, m);
153
237
  }
154
238
  if (!sameContent(next, cur)) writes.push({ full: MAP_PATH, content: next, rel: 'docs/file-map.md' });
239
+ mapText = next; // coverage reads the POST-generation text, so a regen never changes the verdict
155
240
  }
156
- return { model, writes, missing, stale, markersMissing };
241
+ const modules = moduleCoverage(mapText);
242
+ return { model, writes, missing, stale, markersMissing, modules };
157
243
  }
158
244
 
159
245
  function main() {
@@ -162,7 +248,7 @@ function main() {
162
248
  process.stdout.write(JSON.stringify(buildModel(), null, 2) + '\n');
163
249
  return;
164
250
  }
165
- const { writes, missing, stale, markersMissing } = computeWrites();
251
+ const { writes, missing, stale, markersMissing, modules } = computeWrites();
166
252
 
167
253
  // A missing note (dir without a note) or a stale note (note without a dir) is a hard failure in
168
254
  // BOTH modes: in --check it fails CI; in write mode we still report it (and render a visible
@@ -171,6 +257,8 @@ function main() {
171
257
  if (missing.length) problems.push(`notes MISSING for: ${missing.join(', ')} — add a one-line note to docs/file-map.notes.json`);
172
258
  if (stale.length) problems.push(`notes are STALE (no such dir) — remove from docs/file-map.notes.json: ${stale.join(', ')}`);
173
259
  if (markersMissing.length) problems.push(`docs/file-map.md is missing generated markers for: ${markersMissing.join(', ')}`);
260
+ if (modules.undocumented.length) problems.push(`modules with NO docs/file-map.md entry: ${modules.undocumented.map((k) => `${MODULES_DIR}/${k}`).join(', ')} — add an entry to the modules/ block of docs/file-map.md (task 1004066)`);
261
+ for (const d of modules.defects) problems.push(`module-coverage SCAN DEFECT — ${d}`);
174
262
 
175
263
  if (checkOnly) {
176
264
  if (writes.length || problems.length) {
@@ -197,6 +285,12 @@ if (require.main === module) main();
197
285
  module.exports = {
198
286
  materializedNames,
199
287
  SECTIONS,
288
+ MODULES_DIR,
289
+ listModules,
290
+ modulesBlock,
291
+ documentedModuleKeys,
292
+ isModuleDocumented,
293
+ moduleCoverage,
200
294
  begin,
201
295
  end,
202
296
  buildModel,
@@ -27,7 +27,7 @@ const STEPS = [
27
27
  { key: 'fitness', label: 'fitness.js (architecture + budgets + freshness gate)', argv: ['scripts/gds/fitness.js'], fix: 'read the ✗ lines above; each names its fix' },
28
28
  { key: 'repo-map', label: 'nested CLAUDE.md symbol blocks fresh', argv: ['scripts/gds/gen-repo-map.js', '--check-block'], fix: 'node scripts/gds/gen-repo-map.js' },
29
29
  { key: 'session-index', label: 'CLAUDE.md §13 session snippet fresh', argv: ['scripts/gds/gen-session-index.js', '--check-block'], fix: 'node scripts/gds/gen-session-index.js' },
30
- { key: 'file-map', label: 'docs/file-map.md skills/routines sections fresh', argv: ['scripts/gds/gen-file-map.js', '--check'], fix: 'node scripts/gds/gen-file-map.js (+ a note in docs/file-map.notes.json for a new skill)' },
30
+ { key: 'file-map', label: 'docs/file-map.md skills/routines sections fresh + every module documented', argv: ['scripts/gds/gen-file-map.js', '--check'], fix: 'node scripts/gds/gen-file-map.js (+ a note in docs/file-map.notes.json for a new skill, or a modules/ entry in docs/file-map.md for a new module)' },
31
31
  { key: 'api-docs', label: 'docs/api/openapi.json + api-reference fresh', argv: ['scripts/gds/gen-api-docs.js', '--check'], fix: 'node scripts/gds/gen-api-docs.js' },
32
32
  { key: 'copy-registry', label: 'docs/copy-registry.json fresh', argv: ['scripts/gds/copy-inventory.js', '--check'], fix: 'node scripts/gds/copy-inventory.js', optional: true },
33
33
  { key: 'linkify', label: 'no bare #NNN task refs in committed markdown', argv: ['scripts/gds/linkify-refs.js', '--check'], fix: 'node scripts/gds/linkify-refs.js', optional: true },