@bongos/core 1.19.442 → 1.19.444
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +470 -455
- package/config/branding.neutral.json +1 -1
- package/docs/adr/0054-session-token-reward-cost-plus.md +14 -0
- package/docs/adr/0218-the-apex-root-web-surface.md +55 -0
- package/docs/adr/README.md +1 -0
- package/docs/architecture.md +2 -1
- package/docs/copy-inventory.md +360 -360
- package/docs/copy-registry.json +456 -456
- package/docs/design/mocks/hub/mine.html +1 -1
- package/docs/design/mocks/hub/picker.html +1 -1
- package/docs/design/mocks/hub/planets.html +1 -1
- package/docs/design/mocks/hub/v1.html +3 -3
- package/docs/design/mocks/hub/v2.html +2 -2
- package/docs/design/mocks/hub/v3.html +5 -5
- package/docs/design/mocks/hub/wizard.html +1 -1
- package/docs/design/mocks/landing/v1.html +1 -1
- package/docs/design/mocks/landing/v2.html +1 -1
- package/docs/design/mocks/landing/v3.html +1 -1
- package/docs/design/mocks/landing-page-mock.html +3 -3
- package/docs/design/mocks/projects-hub-mock.html +1 -1
- package/docs/design/mocks/projects-hub-variants.html +1 -1
- package/docs/design/mocks/projects-page-mock.html +1 -1
- package/docs/file-map.md +3 -3
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +9 -2
- package/docs/recipes/ui-look-before-you-ship.md +4 -4
- package/modules/copy-desk/tests/copy_flags.mjs +4 -4
- package/modules/public-landing/CLAUDE.md +15 -0
- package/modules/public-landing/module.json +15 -0
- package/modules/ui-design/kit/lib.js +2 -2
- package/modules/ui-design/kit/render.js +1 -1
- package/modules/ui-design/kit/serve.js +3 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/copy-inventory.js +3 -3
- package/scripts/gds/fitness-checks-identity.js +18 -1
- package/scripts/gds/publish-manifest.js +1 -1
- package/scripts/gds/session-digest-build.js +49 -17
- package/scripts/gds/session-digest.js +11 -1
- package/scripts/gds/ship-session-upload.js +102 -12
- package/scripts/gds/ship.js +7 -1
- package/src/bongos/module-scope-map.js +9 -7
- package/src/bongos/serve-internal.js +13 -1
- package/src/module-api.js +1 -1
- package/src/module-loader/loader.js +34 -17
- package/src/module-loader/manifest-schema.js +16 -8
- package/src/platform-server.js +50 -18
- package/tests/copy_inventory.mjs +0 -0
- package/tests/cosmos_token_contrast.mjs +6 -6
- package/tests/credit_grant.mjs +236 -0
- package/tests/fitness.mjs +20 -0
- package/tests/landing_legal_pages.mjs +2 -1
- package/tests/landing_page.mjs +7 -5
- package/tests/module-scope-map.mjs +2 -2
- package/tests/module_loader.mjs +4 -1
- package/tests/onboard.mjs +5 -5
- package/tests/projects_hub.mjs +22 -20
- package/tests/projects_hub_pre_uat.mjs +5 -3
- package/tests/public_landing_projects.mjs +2 -2
- package/tests/terms_acceptance.mjs +1 -1
- package/tests/ui_design_kit.mjs +3 -3
- package/tests/world_tokens.mjs +3 -2
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_00.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_01.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_02.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_03.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_04.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_05.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_06.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_07.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_08.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_09.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_10.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_11.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_12.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_13.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_14.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_15.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_16.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_17.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_18.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_19.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_20.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_21.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_22.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_23.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_24.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_25.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_26.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_27.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_28.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_29.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_30.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_31.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_32.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_33.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_34.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_35.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_36.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_37.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_38.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_39.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_40.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_41.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_42.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_43.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/cosmos.css +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/verro-medallion.png +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/og-card.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-bongos-chrome-a-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-bongos-chrome-a-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-bongos-chrome-b.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-bongos-chrome-b.jpg.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blob-one-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blob-one-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blob-one.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blob-one.jpg.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blobs-set-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blobs-set-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blobs-set.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blobs-set.jpg.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-bead-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-bead-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-blob-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-blob-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-pair-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-pair-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-bead-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-bead-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-blob-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-blob-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-pair-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-pair-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-bead-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-bead-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-blob-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-blob-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-pair-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-pair-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-space-nebula.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-space-nebula.jpg.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r4-card-chrome.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r4-card-chrome.jpg.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r4-card-deepfield.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r4-card-deepfield.jpg.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r4-card-dust.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r4-card-dust.jpg.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r4-card-nebula.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r4-card-nebula.jpg.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world.css +0 -0
- /package/{public-landing → modules/public-landing/public}/buddha.png +0 -0
- /package/{public-landing → modules/public-landing/public}/contact.html +0 -0
- /package/{public-landing → modules/public-landing/public}/index.html +0 -0
- /package/{public-landing → modules/public-landing/public}/index.probes.json +0 -0
- /package/{public-landing → modules/public-landing/public}/index.states.json +0 -0
- /package/{public-landing → modules/public-landing/public}/privacy.html +0 -0
- /package/{public-landing → modules/public-landing/public}/projects-empty.states.json +0 -0
- /package/{public-landing → modules/public-landing/public}/projects-floor.states.json +0 -0
- /package/{public-landing → modules/public-landing/public}/projects.html +0 -0
- /package/{public-landing → modules/public-landing/public}/projects.probes.json +0 -0
- /package/{public-landing → modules/public-landing/public}/projects.states.json +0 -0
- /package/{public-landing → modules/public-landing/public}/terms.html +0 -0
- /package/{public-landing → modules/public-landing/public}/world/assets/clouds/cloud-island-large.png +0 -0
- /package/{public-landing → modules/public-landing/public}/world/assets/clouds/cloud-puff.png +0 -0
- /package/{public-landing → modules/public-landing/public}/world/assets/creatures/creature-whale-cargo.png +0 -0
- /package/{public-landing → modules/public-landing/public}/world/assets/creatures/creature-whale-glide.png +0 -0
- /package/{public-landing → modules/public-landing/public}/world/assets/plates/plate-clouds-near.png +0 -0
- /package/{public-landing → modules/public-landing/public}/world/assets/plates/scene-establishing.png +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_note": "Cloud Bongos vanilla brand pack — customer 0, the identity Cloud Bongos itself runs on. The neutral starter every instance resolves over. The LOOK is the chrome world (owner pick 2026-08-27, goal 1000080; built at task 1003298 and recorded in DESIGN.md): a pale grey paper in light and pure black in dark, iridescent liquid-chrome sculpture as the only atmosphere, Manrope light lowercase display type, one warm salmon accent that lights only what is pressable or live. theme.ui is injected by src/bongos/serve-internal.js (R57) as a :root override after every stylesheet: these thirteen colours are LIGHT mode; each surface's stylesheet derives its own dark tier from them (cosmos.css, public-landing/assets/world.css). backdrop is var(--bg) so the body ground follows the mode instead of pinning one. To brand a NEW instance, copy this to config/branding.json and fill it in (docs/branding-fill-prompt.md). See docs/branding-contract.md.",
|
|
2
|
+
"_note": "Cloud Bongos vanilla brand pack — customer 0, the identity Cloud Bongos itself runs on. The neutral starter every instance resolves over. The LOOK is the chrome world (owner pick 2026-08-27, goal 1000080; built at task 1003298 and recorded in DESIGN.md): a pale grey paper in light and pure black in dark, iridescent liquid-chrome sculpture as the only atmosphere, Manrope light lowercase display type, one warm salmon accent that lights only what is pressable or live. theme.ui is injected by src/bongos/serve-internal.js (R57) as a :root override after every stylesheet: these thirteen colours are LIGHT mode; each surface's stylesheet derives its own dark tier from them (cosmos.css, modules/public-landing/public/assets/world.css). backdrop is var(--bg) so the body ground follows the mode instead of pinning one. To brand a NEW instance, copy this to config/branding.json and fill it in (docs/branding-fill-prompt.md). See docs/branding-contract.md.",
|
|
3
3
|
"identity": {
|
|
4
4
|
"productName": "Cloud Bongos",
|
|
5
5
|
"worldName": "Cloud Bongos",
|
|
@@ -61,3 +61,17 @@ Revisit when: (a) per-task token attribution lands (the unit becomes the task, n
|
|
|
61
61
|
- `computeSessionTokenReward` in `src/bongos/db.js` changed accordingly; `tests/credit_grant.mjs` updated; `llm-pricing.js` (the token→USD table) is **unchanged** — only the cents→dollars scale factor moved.
|
|
62
62
|
- **History was re-denominated, not left mixed.** Per the "append-only, reversible" rule above, existing `session.token_reward` rows were **not mutated**; a single compensating `credit_log` row (`reason='redenominate.token_reward_dollars'`, `task_id` → [#1138](https://example.com/builders#/task/1138)) restates the running total from cents to dollars, and the `total_credits` trigger nets it automatically. The original cents rows remain as historical record; the `session_records.token_reward_example` display mirror was divided by 100 to match. At amendment time builder `#1` (example-owner) was the only holder of token rewards, so this touched one builder.
|
|
63
63
|
- **The per-task `credits_reward` path (ADR 0023) is untouched** — it stays a unitless author estimate. The two-conventions tension (§"Two drachma conventions, one ledger") is now *narrower* (both paths read on a ~dollar scale) but not formally reconciled; trigger (c) remains open for a full reconciliation.
|
|
64
|
+
|
|
65
|
+
## Amendment — 2026-08-30 (task 1003435): the priced unit is the SESSION, subagents included
|
|
66
|
+
|
|
67
|
+
**A gap in the Decision's premise, not a change to its formula.** The formula above prices `session_records.model_usage`, and that map was built from exactly one file — the interactive transcript. Claude Code persists Agent/Task-tool spawns in a sibling subtree (`<project-dir>/<session-id>/subagents/*.jsonl`), so a session's subagent tokens were invisible to the reward. The spend ledger had already closed this gap on its side: [ADR 0028](<redacted>.md)'s `session-cost.js` prices that subtree into its own `session:<redacted> cost row. Only the reward still read one file.
|
|
68
|
+
|
|
69
|
+
That inverted the methodology's own instruction. CLAUDE.md tells builders to delegate freely and to route most subagent work to the top model tier; following it moved real, productive spend into a bucket the reward could not see, so the same work paid less when it was parallelized. This is **not** the accepted waste risk recorded in §"The residual rewards waste risk" — that section knowingly accepts paying for tokens that turned out wasteful. This was the opposite failure: guidance-following spend that earned nothing.
|
|
70
|
+
|
|
71
|
+
**Change:** `model_usage` now describes the whole session — the interactive thread plus its subagents subtree. Grader worker transcripts are excluded **by signature**, not by location, reusing `looksLikeGraderTranscript()`, because a grader run can nest inside a `subagents/` dir and its cost is already captured through the worker envelope; re-pricing it would double-count, which is the ADR 0028 invariant.
|
|
72
|
+
|
|
73
|
+
**The deliberate asymmetry, recorded because it looks like a bug otherwise.** Only `model_usage` grew. `detail`, `metrics.tokens`, `assistant_turns` and `total_tokens` remain the **interactive thread's** record. Those feed the BFG session corpus and the inefficiency evaluator ([ADR 0027](<redacted>.md)), which reason about one agent's turn-by-turn behaviour; splicing N subagent transcripts into that per-turn story would distort the very signal it exists to measure, and bloat the payload. So `total_tokens` is deliberately **less than** the sum of `model_usage`, and three counters — `subagent_files`, `subagent_tokens`, `subagent_grader_skipped` — carry the difference explicitly rather than leaving a reader to infer it from a mismatch.
|
|
74
|
+
|
|
75
|
+
**The acceptance test is a reconciliation, not a fixture.** `tests/credit_grant.mjs` runs both real pricing paths over one on-disk fixture and asserts the reward's `true_cost_usd` equals the sum of that session's `session:<redacted> and `session:<redacted> cost rows within independent-rounding epsilon — so a change made to one side and not the other fails the build. Idempotency is unchanged: the award is still a per-session watermark, so a re-ship pays only the increment and never twice.
|
|
76
|
+
|
|
77
|
+
**Not done here, and an owner decision:** past parallel sessions were underpaid. History stays append-only; a backfill would be its own task, with the 2026-06-15 re-denomination above as the worked precedent for the compensating-row shape.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# ADR 0218 — The apex-root web surface: a module may own the front door, and says so with a flag, not an empty prefix
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-08-30
|
|
5
|
+
- **Task:** [task 1003322](https://cloudbongos.com/builders#/task/1003322) (goal 1000080 — the apex frontend pass; the last of the five tasks scoped with the owner on 2026-08-28, after 1003320 the kit, 1003321 the skills, 1003323 the style library and 1003324 the module)
|
|
6
|
+
- **Relates to:** [ADR 0083](<redacted>.md) (the module system, the one-way rule, and §Decision #4 "the kernel names no module key"), [ADR 0093 §1](<redacted>.md) (the web-surface carves: status-UI BV1.R84, hall-UI BV1.R85), [ADR 0197 §8](<redacted>.md) ("the front door keeps its own key" — this carve was named there), [ADR 0062 §7](<redacted>.md) (the core ↔ host split), [ADR 0108 §1](<redacted>.md) (host content vs the core package), [ADR 0152](<redacted>.md) (the landing is served by the core app), `modules/public-landing/CLAUDE.md`
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
Cloud Bongos's own front door — the landing page, the projects hub, the three legal doors and every asset behind them — lived at the repo root in `public-landing/` and was served by a **hardcoded block** in `src/platform-server.js`: `const LANDING_DIR = path.join(__dirname, '..', 'public-landing')`. It was the last first-party web surface not to be a module. The status dashboard graduated to `modules/status-ui/` in BV1.R84 and the builders' hall to `modules/hall-ui/` in BV1.R85; `src/bongos/module-scope-map.js` had carried the key `public-landing` since task 1002690 with a comment saying, honestly, "not yet a carved module, so the glob lists where the code lives TODAY".
|
|
11
|
+
|
|
12
|
+
The obstacle was the seam. `moduleWebSurfaces()` (`src/module-loader/loader.js`) resolves an enabled module's `contributes.webSurfaces` into serving descriptors, and its whole routing vocabulary is a **Host-header prefix**:
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
if (!s || typeof s.host !== 'string' || typeof s.dir !== 'string') continue;
|
|
16
|
+
out.push({ key, host: s.host.toLowerCase(), dir: abs });
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`serve-internal.js` then routes each descriptor with `host.startsWith(s.host)` — `"status."`, `"builders."`. That vocabulary can express "a surface that lives on a subdomain" and nothing else. The apex root is not a prefix of anything; it is **whatever is left** once the subdomains have been taken (`isApexHost` is literally `!isStatusHost && !isBuildersHost`). So the front door could not declare itself with the seam as it stood, and the carve could not happen without teaching the seam a second kind of surface.
|
|
20
|
+
|
|
21
|
+
The hall had already established the other half of the answer. A web-surface module's manifest declaration is the **discovery, scope-map and `window.__MODULES__` record**; it does not have to be what reads the bytes. The hall keeps a dedicated serving block because its serving is far richer than a static dir (the apex `/builders/*` shim, the canonical redirects, the page gates, the widget injection), and `serve-internal` partitions it out of the generic loop on a *host predicate* rather than on its key. The landing's serving is richer in the same way, for different reasons: the apex-only sentinel strip, the branding-token stamp, the `?v=` content stamp, five named routes, an asset handler and a fall-through to the game-less 404 tail.
|
|
22
|
+
|
|
23
|
+
## Decision
|
|
24
|
+
|
|
25
|
+
1. **`modules/public-landing/` is a default-on core-domain WEB-SURFACE module, and the key `public-landing` is kept.** The scope-map key does not change (only its glob does), so goal scopes, task affinity and every existing reference to the key keep resolving. It sits beside `hall-ui` and `status-ui` as the third web surface, and beside them in `publish-manifest.js` so a released core carries the front door it has always carried. Per ADR 0197 §8 it stays its **own** key rather than joining `ui-design`: a customer of a capability is not part of it, and a second instance must be able to use `ui-design` without inheriting Cloud Bongos's page.
|
|
26
|
+
|
|
27
|
+
2. **A web surface declares EITHER a `host` prefix OR `apexRoot: true` — never both, never neither.**
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{ "contributes": { "webSurfaces": [ { "apexRoot": true, "dir": "public" } ] } }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`moduleWebSurfaces()` accepts a surface whose `dir` is a string and which carries exactly one of the two, and returns for an apex-root surface `{ key, apexRoot: true, dir }` — **with no `host` field at all**. Host descriptors are returned byte-identical to before (`{ key, host, dir }`), so every existing consumer and its tests are untouched.
|
|
34
|
+
|
|
35
|
+
3. **`host: ""` is rejected, and this is the reason the flag exists.** An empty prefix reads like the natural spelling of "the root", and it is a trap: the router's match is `host.startsWith(s.host)`, and **every string starts with `""`**. A surface declaring `host: ""` would answer on `builders.<apex>` and `status.<apex>` too, and — worse — `isModuleSurfaceHost()` would then return true for *every* request, which is the predicate the eleven `/builders` guards in `serve-internal.js` use to decide to step aside. The hall would stop serving itself. That failure is total, silent and produced by a declaration that looks correct. A boolean cannot be mistaken for a prefix, so the shape that cannot be written wrongly is the shape we take. The loader's guard is explicit: a surface with an empty-string `host` is skipped, exactly as a surface that declares a directory it does not ship is skipped.
|
|
36
|
+
|
|
37
|
+
4. **The generic static loop stays host-only.** `serve-internal.js` filters the resolved set to descriptors that carry a string `host` before it builds `webSurfaces`, so an apex-root surface is never in that list and therefore never in `isModuleSurfaceHost`. This is the same partition the hall gets, made on the declaration rather than on a host string — and it is what keeps the eleven `/builders` guards honest.
|
|
38
|
+
|
|
39
|
+
5. **The landing keeps its dedicated block, and only its root moves.** `src/platform-server.js` keeps every transform verbatim — `stripApexOnly` → `injectBrandingGlobal` → `stampAssets`, the memoised page cache, the `/`, `/create`, `/projects`, `/privacy`, `/terms`, `/contact` routes, the apex-only static asset handler, the 404 tail. The single change is that `LANDING_DIR` stops being a hardcoded path and becomes `landingDir()`, resolved once through `moduleWebSurfaces({ isEnabled: isModuleEnabled })`. The DONE WHEN is byte-identity, and the way to get byte-identity is to move the root and touch nothing else.
|
|
40
|
+
|
|
41
|
+
6. **Disabling the module removes the front door, and the apex answers the 404 tail.** With `public-landing` disabled the loader returns no apex-root surface, `landingDir()` is `null`, `landingPage()` returns `''`, each of the six routes calls `next()`, the asset handler is never mounted, and the request falls to the game-less tail — a JSON 404 for an API-shaped path, plain text otherwise. Nothing throws and nothing is hardcoded to be missed: this is the module contract's "can be disabled without breaking anything" in its most literal form, and it is why the resolution is a lookup rather than a fallback path.
|
|
42
|
+
|
|
43
|
+
7. **At most one apex-root surface; the first discovered wins and a second is logged.** The apex root is singular by definition — two modules cannot both be "everything that is not a subdomain". Rather than let a second declaration silently shadow or silently lose, `landingDir()` warns with both keys and serves the first. An instance that installs a second landing module gets a line in its log saying so, which is the whole of the error handling this deserves.
|
|
44
|
+
|
|
45
|
+
8. **The kernel still names no module key** (ADR 0083 §Decision #4). `platform-server.js` selects its surface with `surfaces.find((s) => s.apexRoot)` — a predicate over the *declaration*, exactly as `serve-internal.js` partitions the hall with a predicate over the *host*. Neither file's **code** names the key after this change — only comments say where the bytes happen to live today. A future instance that ships its own front door module gets the apex by declaring `apexRoot`, with no core edit.
|
|
46
|
+
|
|
47
|
+
9. **The nested `modules/public-landing/CLAUDE.md` is the newcomer's map of this instance's world** — `DESIGN.md`, the fifteen-token contract and why there are two sheets ([ADR 0196](<redacted>.md)), the direction records and the mocks, which page is which, the test pins that bite, the traps, and a pointer to `modules/ui-design/`'s kit for the look-before-you-ship loop. The module ships the *world*; the method for building one stays in `ui-design`.
|
|
48
|
+
|
|
49
|
+
## Consequences
|
|
50
|
+
|
|
51
|
+
- **The front door is a module like any other.** `GET /modules` lists it, `window.__MODULES__` carries it, the scope map points at `modules/public-landing/`, and the fitness core↔host boundary check now covers it.
|
|
52
|
+
- **The `?v=` stamp and the asset resolution follow the dir automatically.** `assetStamp()` resolves a URL against the surface dir and refuses anything that escapes it; moving the dir moves the jail with it, and the stamp is a content hash, so the served bytes are unchanged.
|
|
53
|
+
- **The kit needed no functional change.** `resolveSurfaceDir()` (`modules/ui-design/kit/lib.js`) already resolved a surface argument as *either* a checkout directory *or* a module key whose manifest declares a `webSurfaces` dir. `--surface public-landing` therefore keeps working across the move, resolving through the manifest instead of the root directory — the seam ADR 0197's kit built for exactly this day.
|
|
54
|
+
- **Byte-identity is the evidence, not the claim — and it has exactly one deliberate exception.** The five apex pages and the two stylesheets were hashed as *served* (through the real `buildPlatformApp()` over an ephemeral listener, on the apex host) before and after the move. The stylesheets match outright. Each HTML page grew by **22 bytes, the same 22 on all five**: `"public-landing":true,` — the module's own entry in the injected `window.__MODULES__`, which is the front door becoming a module *observable in the page*, and the one change this ADR intends. Remove that single substring and all five digests equal their BEFORE values exactly. The 106 moved files hash identically on disk before and after.
|
|
55
|
+
- **What this does not do:** the surface still ships wholesale to every instance and is still stripped by the apex-only sentinels on a child (ADR 0152's arrangement is untouched). A child instance overriding the front door with its own module is now *expressible* — declare `apexRoot`, disable `public-landing` — but nothing in this ADR builds or tests that path.
|
package/docs/adr/README.md
CHANGED
|
@@ -304,5 +304,6 @@ This keeps the decision history honest and traceable.
|
|
|
304
304
|
| 0213 | [**The platform T&C acceptance is the hub's own witness, written once, and consent becomes two columns** ([task 1002913](https://cloudbongos.com/builders#/task/1002913), goal 1000045). ADR 0141 §4 deferred consent for cross-project aggregation to "the platform T&C" and ADR 0171 D3 wrote its own definition around the moment that acceptance "can later be captured" - but no acceptance record existed, and while `/terms` DOES exist (task 1003251) it says nothing about work crossing between projects, so the public-profile premise ("already carries platform T&C consent") was untrue for every account. The scouting.js:6 pointer the task named had also been overwritten by ADR 0210, so the debt was going invisible rather than getting paid. Decision: `terms_accepted_at` + `terms_version` on `platform_identity_accounts` (migration 019), written ONLY by `upsertAccount`'s `activate` branch - one caller, /sso/authorize, where `activate: true` is a source literal and every field comes from the hub's resolved session (the ADR 0205 D3 property; a record a caller could assert is evidence of nothing). COALESCE makes it write-once, so the column keeps meaning WHEN consent was given. The consent axis in `account-visibility.js` becomes `profile_state = 'active' AND terms_accepted_at IS NOT NULL`, composed into every other predicate, which is what makes the tie structural rather than a promise. NO backfill (D4): existing active accounts re-accept on their next hub sign-in - ADR 0171 D3's own rule and P03's ratified rationale - and that visible consequence, plus the wording itself, went to the owner on blocker 1000123. Rejected: an acceptances TABLE (no event stream yet), a click-through gate (an interstitial in a redirect built to be invisible; additive later), backfilling (records a consent that never happened), deriving profile_state from the timestamp, and putting the predicate in `terms-acceptance.js` (a second owner of an effective-visibility answer, which ADR 0202 D4 forbids).](<redacted>.md) | platform identity / consent / privacy |
|
|
305
305
|
| 0216 | [**A fake-pool interpreter must FAIL LOUDLY on what it cannot model; the refusal is uniform, the interpreters stay separate** ([task 1003354](https://cloudbongos.com/builders#/task/1003354), folding in idea 1000877 from the adversarial review of [task 1002968](https://cloudbongos.com/builders#/task/1002968)). Nine test files carry a **fake pool** whose row filter is derived from each statement’s own WHERE text — the design that makes an isolation proof say something about the real query rather than about a canned response. It has one silent failure mode: a term the interpreter does not recognise is simply not applied, so the boundary goes vacuous and every assertion above it keeps passing. Each file guarded a piece of that (unread `$N`, unknown literal, unmodelled `IS NULL`) and every one of those is a **term** guard — and the term list is not the predicate. Three reviews found the same shape. **The missing term:** ADR 0213 made consent two columns and `scouting_owner_access_and_reach.mjs` modelled only the first; a NULL test is a third syntax neither sweep can see, so the suite reported 17 passed while modelling a predicate the database no longer had, inside a file whose own header calls that impossible. **The rewritten joiner:** an interpreter that COLLECTS recognised terms and ANDs them re-imposes an `AND` the SQL may no longer contain — rewrite a predicate’s `AND` to `OR` and Postgres serves every active account on the platform, or lets any builder revoke any session by id, while the fake answers exactly as before. **Recognition is not application:** ADR 0213’s own first draft checked only that a column name was recognised, and a mutation deleting the filter still passed. Decisions: **D1** the governing rule — an interpreter that cannot model a term must REFUSE the statement, never ignore the term; incompleteness is allowed, silence is not, because a refusal fails the build in the session that changed the SQL while a silent drop fails nothing ever. **D2** the mechanism is a CONSUMING parse: each recognised term is spliced out of the text and the residue must be a pure conjunction (`/^[\s()]*(?:AND[\s()]*)*$/`), which subsumes every term guard, catches `OR`/`NOT`/`CASE`, and retires ADR 0213’s `modelled`-set bookkeeping because an unapplied term is by construction still there; a legitimate `OR` (the rename window, the either-side clause, the expiry disjunction, the rescind guard) is consumed as a whole unit. **D3** the interpreters stay SEPARATE and only the refusal is uniform — the term vocabulary differs completely across six tables, so a shared registry would be a small SQL engine and would couple unrelated proofs; instead every copy carries the identical guard and its OWN calibration test, which is strictly better anti-drift than extraction (a helper proves the guard exists somewhere; a calibration test proves it is reached by that file’s own statements). **D4** a modelled term proves nothing until a fixture makes it observable — three fixtures added, each differing from its control in exactly one column, so the mutation that deletes the term has somewhere to land; where a file deliberately lacks one because another owns that matrix, the comment says so. **D5** every copy verified BY MUTATION against the real source — ten mutations, ten reds, each a 0-failure pass beforehand. ⚠ Honest limits: no fitness check asserts a NEW fake pool carries the guard (identifying “a fake pool” needs a heuristic, and a heuristic gate over test internals has a false-positive cost) — the per-file calibration tests hold the nine that exist and a tenth is a review question; `canonical_profile_url.mjs` is untouched because its residue must be EMPTY, strictly stronger already. Rejected: one shared interpreter module; a fitness heuristic; keeping `KNOWN_NULL_TESTS` + `modelled` beside the structure guard; parsing the boolean tree properly; accepting green as verification.](<redacted>.md) | testing / proof integrity |
|
|
306
306
|
| 0217 | [**Rank gates INVITING, not VIEWING: the owner’s answers to the account-security review’s five blockers** ([task 1003358](https://cloudbongos.com/builders#/task/1003358) · goal 1000045; answers blockers 1000121/1000122/1000123/1000124/1000126). The 2026-08-29 account-security batch shipped fourteen tasks and deliberately refused five audience/consent questions. Four turned out to be the same question, and one answer reframed the rest. Decisions: **D1** the governing line — *rank gates the ability to INVITE; it does not gate the ability to VIEW*. Seeing what someone built across projects is not privileged (the T&C already promises that data is public by consent, to search engines included); reaching out to them costs them something, and that is where a floor belongs. So `membership_kind` is **NOT** widened — `membershipKind()` stays FIRST_ADMIN-only, the federated wire contract is unchanged, the My Projects owned/building split keeps its meaning, the cross-version window never opens, and task 1003350 is **abandoned**. A **per-project INVITE rank floor defaulting to archon** replaces it (task 1003362), set by that project’s own archons — strictly better than a founder requesting a platform-side `project.curate` grant per teammate. Viewing gains no gate, on a new cross-project achievements page that per the owner **lives on cloudbongos.com, not the hall variant** (task 1003363) — so `public-landing/` inside the apex-only region, not `modules/hall-ui/`. The risk to hold as it is built: public consented profile data and a PRIVATE account’s recruiter sliver are different material, and the sliver must not ride along — not as a row, a facet count, or a search-result absence that reveals presence. **D2** the sliver keeps its **archon floor** (blocker 1000126), and ADR 0215 D4’s defensive hold becomes a positive agreement: the invite floor also defaults to archon, and the C5 tension dissolves because C5 was about browsing. Whoever builds 1003362 must check whether it IS the console wave’s `project.recruit` atom before shipping a rival knob. **D3** the consent pair — `/terms` said acceptance is recorded “by signing in here” but the stamp is reachable only via `/sso/authorize`, which a CHILD PROJECT reaches, so a hub-only builder was never stamped and (NULL `terms_accepted_at` → `isAccountActive` false) was invisible everywhere, permanently, under no-backfill. Owner took **both halves**: correct the sentence now (task 1003353) and **capture at the hub’s own sign-in** next (task 1003361), through the `platform-identity.hub` seam so child instances see no change. On grandfathering the owner chose **B — grandfather** (task 1003360) against the recommendation, having been shown that it records a consent which did not happen; the ledger does not absorb that silently, so a grandfathered acceptance is **marked** (queryably distinct from a sign-in, via a recorded source not a magic timestamp) and `/terms` **says so**. ADR 0213’s honest property is not “every acceptance was a sign-in” but **the record never claims more than what happened**, and that survives. **D4** `/access-requests/status` **collapses** to `admitted` + one `status` of `member`|`invited`|`not_admitted` (task 1003359, the fix ADR 0209 already named). It does **not** close the oracle and must not claim to — `admitted:true` still means member-or-invited; the 1003339 limiter is what reduces enumeration, and collapsing narrows what leaks about **applicants**. The cost is paid in the same change: `login.js` is the ONLY place telling a declined applicant they were declined, so its 30-minute timeout copy is rewritten to be honest rather than promising an approval that is never coming. ⚠ Also: report 1000027 (the live SSO check-in identity hole) marked **fixed**, held open until now so the ledger would not claim a fix while the hole still served; `verifyHubAssertion`’s purpose-confusion weakness filed as report **1000028** (medium, latent — zero call sites, becomes an admission bypass the moment ADR 0201 D2.1’s vouch is minted, so the fix must land BEFORE the feature that arms it). **New invariant:** any gate added to a viewing surface, or any recruiting/invite surface below the archon floor, contradicts this ADR and needs one that supersedes it.](<redacted>.md) | platform identity / privacy / permissions |
|
|
307
|
+
| 0218 | [**The apex-root web surface: a module may own the front door, and says so with a flag, not an empty prefix** ([task 1003322](https://cloudbongos.com/builders#/task/1003322) · goal 1000080 — the last of the five tasks scoped with the owner on 2026-08-28). Carves the repo-root `public-landing/` into `modules/public-landing/`, the THIRD web-surface module after status-UI (BV1.R84) and hall-UI (BV1.R85) and the first that is not a subdomain. The obstacle: `moduleWebSurfaces()`'s whole routing vocabulary was a Host-header PREFIX, and the apex root is not a prefix of anything — it is whatever is left once the subdomains are taken. So a surface now names where it answers in exactly one of two ways, `{ host: "status.", dir }` or `{ apexRoot: true, dir }`, never both and never neither. **`host: ""` is rejected on purpose:** the routers match with `host.startsWith(s.host)` and every string starts with `""`, so an empty prefix would answer on `builders.`/`status.` too AND make `isModuleSurfaceHost()` true for every request — the predicate eleven `/builders` guards use to step aside — silently un-serving the hall. A boolean cannot be mistaken for a prefix. The landing keeps its DEDICATED serving block (the hall precedent): the apex-only strip, the branding stamp, the `?v=` content stamp and the six routes are untouched; only `LANDING_DIR` changes, from a hardcoded path to a lookup through the loader, resolved by the `apexRoot` PREDICATE so neither `platform-server.js` nor `serve-internal.js` names a module key (ADR 0083 §Decision #4). Disabling the module removes the front door outright and the apex answers the game-less 404 tail — the module contract's "disable without breaking anything" in its most literal form. At most one apex-root surface: the first discovered wins and a second is logged. Also: the BV1.R76 host-identity gate learned that an **apex-only region is exempt** — the server deletes it on every non-hub instance, so it cannot make a child render the hub's brand, which is the failure that gate names. Evidence: the five apex pages hashed as served, before and after, differ by exactly the same 22 bytes — `"public-landing":true,`, the module's own `window.__MODULES__` entry — and are otherwise identical; the 106 moved files hash identically on disk.](<redacted>.md) |
|
|
307
308
|
|
|
308
309
|
> ⚠️ **Numbering collisions are now machine-enforced, not narrated.** Twenty numbers are shared by two ADRs each — assigned in parallel sessions before anything checked. The files keep their filenames (renumbering would break every existing citation, and a number once assigned is never reused), and this table disambiguates each pair as `NNNN-a` / `NNNN-b`. The authoritative list is `LEGACY_DUPLICATE_ADRS` in [`scripts/gds/adr-namespace.js`](../../scripts/gds/adr-namespace.js), frozen by exact filename: a **new** collision — or a third file joining a legacy number, or a rename of either half — hard-fails the `unit` CI gate, as does an index row that points at no file or an ADR with no row. The list may only shrink. This paragraph used to narrate the collisions one by one and had fallen eight behind reality; see [ADR 0195](<redacted>.md) for why the check exists and why the pairs are grandfathered rather than renumbered.
|
package/docs/architecture.md
CHANGED
|
@@ -38,7 +38,7 @@ The product is organized as a small kernel (`src/`) + **19 modules** under `modu
|
|
|
38
38
|
- `discord` — ship broadcast + inbound bot + role-sync; port: `discord.isLinked`
|
|
39
39
|
- `art-pipeline` — pixel-art generation + artist discipline; port: `art.sharedKeyConfigured`
|
|
40
40
|
- (+ `character-anim`, declaration-only — ADR 0088)
|
|
41
|
-
- **Core-domain modules** (`default: true` — always on unless disabled): `memory`, `grading`, `economy`, `ideas`, `security`, `builder-settings`, `onboarding`, `sessions`, plus `lifecycle` (the task/claim/version state machine), `autonomy` (the MAS), the `hall-ui` + `status-ui` internal surfaces, `ui-design` (the design contract, its adapters and the `/design` playbook as a platform capability — [ADR 0197](adr/<redacted>.md)), and the `demo` acceptance-proof module. See [docs/modules-contract.md](modules-contract.md) for the live roster and which default on.
|
|
41
|
+
- **Core-domain modules** (`default: true` — always on unless disabled): `memory`, `grading`, `economy`, `ideas`, `security`, `builder-settings`, `onboarding`, `sessions`, plus `lifecycle` (the task/claim/version state machine), `autonomy` (the MAS), the `hall-ui` + `status-ui` internal surfaces, `public-landing` (the apex front door — [ADR 0218](adr/<redacted>.md)), `ui-design` (the design contract, its adapters and the `/design` playbook as a platform capability — [ADR 0197](adr/<redacted>.md)), and the `demo` acceptance-proof module. See [docs/modules-contract.md](modules-contract.md) for the live roster and which default on.
|
|
42
42
|
- **Module-owned migrations** — `modules/<key>/migrations/*.sql` applied only when the module is enabled. Stem-keyed so moving a migration never re-runs it.
|
|
43
43
|
- **`bongos module new <key>`** — scaffolds a new module directory in one command.
|
|
44
44
|
- **`bongos upgrade`** — the update-consumption channel a two-repo consumer uses to pull the core from version N→N+1 (task 1690, ADR 0100 §2 / ADR 0103 §4): pre-flight (module-compat + clean tree) → bump the vendored-tarball pin → `npm install` → `npm run migrate` (additive `core_*` migrations) → re-materialize `.claude/` → restart → verify. Run bare (no `--to`) it is still just the module-compat pre-check that exits 1 on mismatch; `--to <version>` runs the real bump. (`scripts/gds/upgrade.js`.)
|
|
@@ -241,6 +241,7 @@ markdown describes, the DB enforces.**
|
|
|
241
241
|
|
|
242
242
|
**Multi-host setup (same Node process), routed by `Host` header in [`src/bongos/serve-internal.js`](../src/bongos/serve-internal.js), shared by both entrypoints:**
|
|
243
243
|
|
|
244
|
+
- **the apex root itself** — the public front door (`/`, `/projects`, `/create`, `/privacy`, `/terms`, `/contact`). **Carved to the `public-landing` module in task 1003322** ([ADR 0218](adr/<redacted>.md)): the surface moved from repo-root `public-landing/` to `modules/public-landing/public/` and is a loader-discovered web-surface module — the THIRD, and the first that is not a subdomain. Where `hall-ui`/`status-ui` declare a Host PREFIX, this one declares `{ apexRoot: true, dir }`, because the apex is whatever host is left once the subdomains are taken (`isApexHost` = `!isStatusHost && !isBuildersHost`); `host: ""` is rejected, since `host.startsWith("")` is true for every host and would swallow the other two surfaces. Like the hall it keeps its **dedicated** serving block — in `src/platform-server.js`, now resolving its root through `moduleWebSurfaces()` by the `apexRoot` predicate rather than a hardcoded path — so the apex-only sentinel strip, the brand-token stamp and the `?v=` content stamp are unchanged. Disable the module and the front door vanishes: every route falls through to the game-less 404 tail. Served bytes are identical across the carve apart from the module's own `window.__MODULES__` entry.
|
|
244
245
|
- **`example.com`** — game (Phaser + Colyseus). Static files from `public/`. `/builders` + `/builders/*` now **301-redirect to `builders.example.com/<rest>`** ([#739](https://example.com/builders#/task/739) then promoted to permanent 301 in [#741](https://example.com/builders#/task/741), ADR 0036 cutover); the apex no longer serves the builder surface directly.
|
|
245
246
|
- **`status.example.com`** — public build dashboard. **Carved to the `status-ui` module in BV1.R84** ([ADR 0093](adr/<redacted>.md) §1): the surface moved from `public-status/` to `modules/status-ui/public/`, now served by the loader's web-surface seam (`moduleWebSurfaces` → `serve-internal.js`, routed on the `status.` Host prefix the manifest declares) instead of a hardcoded block — no behavior change. Layout reorganized in [#726](https://example.com/builders#/task/726) (ADR 0036): Pulse / Watch(uptime) / Architects / Works / Treasury / Lately Inscribed / The World — the grade-aggregate, estimation-drift, and repo-health tablets moved to the Archon `/watch` page. **⚠ Served OFF the droplet** (ADR 0029, outage resilience): the live page is **GitHub Pages from the separate repo `example-owner/example-status`** (CNAME → Fastly), reading data cross-origin from the droplet's `/api/gds/public/*` (CORS open) and an availability banner from the prober's `status-data` branch. The droplet still serves `modules/status-ui/public/` (on the `status.` host) as a same-origin fallback, but **DNS points at Pages, not the droplet** — so a droplet deploy does NOT update the live page. Changes to the surface publish to the mirror via `scripts/status-mirror-sync.js` (re-pointed at `modules/status-ui/public/`; status.js/style.css/cursors are byte-identical copies; index.html gets the off-box bits injected), run automatically by `.github/workflows/sync-status-mirror.yml` (needs the `STATUS_MIRROR_TOKEN` secret) ([#737](https://example.com/builders#/task/737)). **⚠ FOLLOW-UP (R84):** that workflow's path trigger still watches `public-status/**` and must be re-pointed at `modules/status-ui/public/**` by an owner/Archon — the GitHub App can't push `.github/workflows/*` (learning 107), so until then the mirror sync won't auto-fire on future status edits.
|
|
246
247
|
- **`builders.example.com`** — dedicated builder surface (`isBuildersHost`, ADR 0036). **Carved to the `hall-ui` module in BV1.R85** ([ADR 0093](adr/<redacted>.md) §1): the surface moved from `public-builders/` to `modules/hall-ui/public/` (~13k LOC, the largest module) and is a loader-discovered web-surface module (`contributes.webSurfaces` host `builders.`). UNLIKE the generic status surface it keeps its **dedicated** serving block in `serve-internal.js` (`BUILDERS_DIR = modules/hall-ui/public/`), partitioned out of the generic web-surface loop on the `isBuildersHost` predicate. A thin rewrite shim maps every non-API path onto the existing `/builders/*` tree (so all serving + gating + asset-stamping is reused), serving the hall (`/`), the Metic+ **`/watch`** monitoring page (gated by `requireGovernmentPage`), **`/settings`** (Voices + Craft prefs), plus ranks/sessions/primer/diagrams. No behavior change — byte-identical (only `window.__MODULES__.enabled` gains `hall-ui:true`). **Additive** — until its DNS A record + cert are live, the whole surface stays reachable at `example.com/builders/*`. No `.github/workflows/*` follow-up: unlike R84, no workflow path-trigger watches `public-builders/**`.
|