@bongos/core 1.19.391 → 1.19.393

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.
@@ -0,0 +1,108 @@
1
+ # The /projects hub, direction v2: the chrome and the card in the chrome world, three versions (task 1003299)
2
+
3
+ **Date:** 2026-08-27 · **Goal:** 1000080 (Apex UI pass 2) · **Criterion:** `apex-hub-chrome-and-card` (this round is the composition half; the builds are task 1003300, the chrome, and task 1003301, the poster card) · **World:** chrome-blobs, composition A, recorded in `DESIGN.md` from the built landing (task 1003298, ADR 0196).
4
+
5
+ This document records the hub composition round: what it had to do, how it was run, the three versions put in front of the owner, what each changes on the page beyond its skin, what the build will need that no feed carries yet, and the owner's decision. It supersedes `projects-hub-direction.md` (the 2026-08-13 marigold hub) for the LOOK of the bar, the menus, the search and the card; that document's structural decisions (map first, "only mine" in both browse views, "your projects" as a view, the `?view=` contract, the C1 to C9 build constraints) carry as givens. No build file was touched: `public-landing/projects.html`, `public-landing/assets/cosmos.css`, `DESIGN.md` and every test are as they were.
6
+
7
+ ## 0. Starting point: the hub on the new pack, measured
8
+
9
+ Rendered on 2026-08-27 through a local stub running the real serving transforms (`docs/design/mocks/hub/` mocks were verified the same way):
10
+
11
+ - **The bar** (`header.shell`, `cosmos.css:461-538`) is 69px at 1440: the self-typing wordmark at the left; the nav row `Home | Map · Your projects · New project` centred as a pill group; at the right the `Builders' hall` pill (an emoji glyph), a 34px mode toggle, the avatar + login chip and a `Sign out` pill (signed out: the hall pill, the toggle, `Sign in`). It has no ground of its own: the pills float over the sky and a scrim compensates. At 390 it **stacks to 161px** (the mark row, the right-group row, the nav row) and the toolbar adds three more rows (Sky | Table, Only mine, the search), so the sky starts about 300px down. That stack is what the givens retire.
12
+ - **The toolbar** under the bar (`projects.html:413-428`): the `Sky | Table` segmented pill, the `Only mine` switch, the search pill (`#mapQ`, `Search projects by name or description…`, the count `4 projects` inside it).
13
+ - **The sky** on the new pack: black ground (paper in light), the gravity grid, a starfield in dark only, four glossy orbs (`a.worldOrb`, 122px for the platform's own, smaller for the rest) with the name under each, `this platform` in mono under the self orb, the `yours` badge in the accent above an owned orb, the hint rail (`drag pan · scroll zoom · click a project open it`), the footer. The sky, the orbs and the hover card are task 1003258's rule and do not change: the orb is the door, the card is the room.
14
+ - **The table**: a rounded panel with Project / Type / Builders / Runs / Status / Open, the disc at the row's left, `Open ->` pills.
15
+ - **The card** (`projects.html:513-536`): the modal over the sky with the disc, the name at 21px, the tagline, the relationship pill, the facts (Builders / Type / Runs / Owner / Listed) and one `Open site` door. It is the floor of the poster card's degradation ladder: the state every rung degrades to.
16
+ - Since A3 the controls are already Manrope on the pack's grey-paper / black grounds; the shapes and the composition are the marigold pass's.
17
+
18
+ ## 1. The givens (goal 1000080, owner, 2026-08-25 and 26; not re-opened here)
19
+
20
+ 1. One top bar, a real surface with its own ground and a bottom edge, not floating controls; not sticky.
21
+ 2. The logo top-left is the way home.
22
+ 3. `Projects` moves to the right group and becomes a dropdown holding `Map`, `Your projects`, `New project`; the nav row leaves the bar ("this way we dont clutter the bar").
23
+ 4. A profile dropdown on the identity chip: `My projects`, `Sign out`; a `Settings` slot deferred (a place, nothing rendered).
24
+ 5. The map search moves into the bar's left group and hides on the views it cannot filter; `#mineQ` untouched.
25
+ 6. `Home` is removed.
26
+ 7. The Builders'-hall pill leaves the bar and becomes a per-project, member-only affordance on the card (and a glyph on the table row), rendered only where the project's hall address differs from its site address (owner decision (a), 2026-08-26).
27
+ 8. The project card is overhauled into the poster card: a 5:2 art banner with the accent disc surviving, the name, the tagline, the relationship pill, up to five faces and an `+N` chip, two figures (builders, shipped in the last 30 days), the facts (Type / Runs / Owner / Listed), the actions (`Open site`, the hall entry), and a degradation ladder down to today's card with everything null.
28
+
29
+ Not changing: the sky, the orbs, the hover card, the toolbar (Sky | Table, Only mine, the count), the table (except the glyph), the mine view, the new view, the footer.
30
+
31
+ ## 2. Method, as run
32
+
33
+ 1. Code-led (`.impeccable/config.json`: `buildPath: code`); the world is `DESIGN.md`, read and not rewritten; no comps, no images generated (the Higgsfield rule: hero plates only, and the hub has none). The only image in the round is the landing's own social card (`public-landing/assets/world/og-card.jpg`), used as the platform's art on the poster card because the feed carries no art for any project yet.
34
+ 2. Two mappers read the page and the tests first: the hub as it is (every id, class, copy string, feed and view-switch, with line refs) and the test contract (which assertion pins what, and which of the eight givens collides with which test when built). Both appendices went to every builder so the proposals could name their collisions instead of avoiding them.
35
+ 3. One shared brief: the givens, the two-voices rule from the goal (the bar and the triggers are chrome; the panels and the card are cards), the world's rules from `DESIGN.md`, the token mechanics for a mock (cosmos.css linked for the unchanged regions; the world's derived tier declared on `body` with the dark twins; no `:root`; `--line` and the status vocabulary left to cosmos), a **state contract** of twenty states (every bar state signed out and in, both menus open, keyboard focus, the search focused, the three views, the table, seven card rungs, and three boards: the bar states, the card ladder, the menu component), real data only, the poster card's content, the taste pre-flight, the exact copy strings, and one thesis paragraph per version.
36
+ 4. Three builders ran in parallel under `/impeccable` (setup on the surface, the craft floor, the bounded verify rule), each writing one file under `docs/design/mocks/hub/`, each verified through the stub in both modes at 1440 / 390 / 320 across every state (console clean, no overflow, controls at 24px, the bar under 80px, reduced motion dead) and a mechanical rule check (no `:root`, dark twins byte-identical, no literal colour, the state contract, the head comment, the card's vocabulary, the disclosure pattern).
37
+ 5. A review pass per version (a fidelity lens over the renders against `DESIGN.md` and the thesis; a measurement lens with a browser: glyph-pixel contrast, control boxes, the keyboard path through both menus, every state, the bar's height and non-stickiness; a code lens over the file), every must/should handed to a skeptic, the survivors fixed in one batch per version, the fixes confirmed.
38
+
39
+ ## 3. The three versions
40
+
41
+ | # | File | The bar | The menu | The card | Light | 390 | Honest risk |
42
+ |---|---|---|---|---|---|---|---|
43
+ | v1 | `mocks/hub/v1.html` | **The shelf.** The landing's three pill groups set down on a `bg-frame` shelf with a hairline bottom edge, 72px: mark pill, a 46px glass search pill (260 to 340px focused, the count inside it), `Projects` pill + caret, the toggle, the sign-in door or the chip pill | A card: `bg-card`, 16px, hairline edge, 240px, hung 8px under its trigger; 44px lowercase rows; the current view marked by a 6px accent dot; rise + fade 220ms | **The poster as a stack.** The 5:2 banner, the 48px disc half over its lower-left edge, the name at 28px/300, the tagline, a glass relationship pill, five faces + `+N`, a centred two-up figure band, four hairline ledger rows, the landing's door bar (`open site` accent + `builders' hall`) | Paper on paper, told apart by the edge; near-opaque pill tints | One row; the search steps onto a second full-width lip of the shelf; menus drop full width | Closest to what shipped and to the landing, so the least new information; the shelf is one more grey band |
44
+ | v2 | `mocks/hub/v2.html` | **The hairline.** No pill on the bar: the page ground closed by one `rule` hairline, 64px; the mark as plain type; a bare search field with only a bottom hairline (`ink` on focus), the count to its right; lowercase text triggers, the OPEN one underlined in the accent | A full-width **band** that unfolds under the bar and pushes the page down: `bg-card`, hairline bottom edge, three cells each a 24px/300 title over one clause (`the sky of every project`, `the ones you own or build on`, `five questions and a home`); the current cell carries a 2px accent hairline on its top edge; the profile band the same mechanism | **The specimen.** The 5:2 banner with the two figures as leader-line callouts ON the art (feathered grounds, measured 5.9 to 17.9:1), the 54px disc, the name at 32px/300, the relationship as a mono label, faces, a two-column mono ledger, one accent door + `builders' hall` as an underlined text link | Paper with hairlines; nothing inverts; the callout grounds show as soft patches on the art | One 60px row to 320; the search keeps a real narrower field; the bands stack; under 560 the callouts become the two-up band | The boldest reading of the world's structure rule; the band costs 121px of sky while open and the callout patches are visible in light |
45
+ | v3 | `mocks/hub/v3.html` | **The glass strip.** One continuous glass surface over the sky (`void-glass` + blur in dark, a 5% ink tint on paper in light), 64px, a `glass-edge` bottom hairline; the mark, a hairline, the search fused into the plate (no chrome at rest, an inset field on focus), the right end cut into three **segments** by hairlines (`projects`, the toggle, the identity), the open segment filled `glass-strong` | A card-voice panel **welded to the strip's edge** (no top radius, 16px below), 252px, 44px items; the current view marked by a 2px accent bar at its left edge | **The room.** The name and tagline sit on the banner's lower third over a scrim that is a gradient of the card ground (measured 16.97:1 light, 19.01:1 dark); a shelf line across the banner's lower edge holds the disc (with the orb's gloss), the relationship pill and the faces; the figures and the facts are ONE hairline ledger (builders, shipped in 30 days at figure scale, then Runs / Owner / Listed); a full-width door bar | A tint over paper, hairlines only; the art itself stays a photograph (dark) above a paper card | One plate to 320; the search keeps a field taking the plate's slack; the segments keep 44px; panels drop to the strip's width | The most "one surface" and the most different from the landing's pills; the given's "its own ground" is satisfied by glass, which the owner may read as floating; a dark photograph on a paper card is the one ungraded image in light mode |
46
+
47
+ All three: the sky, the orbs, the hover card, the toolbar (Sky | Table, Only mine), the table (plus the hall glyph in the existing Open cell), the mine and new views and the footer verbatim from today's page; the same real data (the platform's own project with the landing's social card as its art, nine builders holding credits, `30+` shipped in 30 days at the feed's cap; Example with no art, 13 builders and a real 0); the twenty states and the three boards; every render clean in both modes at 1440 / 390 / 320 with reduced motion dead; the mechanical rule set green.
48
+
49
+ ### 3.1 What the review found, and what was fixed before the owner saw the sheet
50
+
51
+ Each version was reviewed by three independent lenses (a design director over the renders against `DESIGN.md` and the version's thesis; a verifier with a browser measuring glyph-pixel contrast, control boxes, the keyboard path through both menus, all twenty states, the bar's height and non-stickiness; a senior front-end reviewer over the file), 136 findings in all (37 must, 62 should, 37 nit); every must and should went to a skeptic (99; 15 refuted, 84 survived), the survivors were fixed in one batch per version and the batch confirmed by a fresh measurement. What changed:
52
+
53
+ - **v1** (24 survived, 14 applied, confirm clean): the search pill carried two focus indicators and painted a ground-coloured slug on focus (now one two-tone ring on the pill's `:focus-within`, the whole 46px pill a `<label>` for the field); the `Type` fact never rendered on any rung (Example now shows `Type / Game`, the platform pair-hides it, which is the only rung that proves pair-hiding both ways); the banner crop cut the landing's headline in half and repeated a door pill above the card's own doors (re-framed on the chrome, identical 2.5:1 crop on every art rung); every fact row's hairline broke into two misaligned segments (one rule per row); signed-out states still claimed membership (two `you build here` badges and the self pill; now gated on auth); the current-view mark stayed on `Map` in the mine and new states; the card overflowed short viewports with no scroll (the scrim scrolls under 820px tall); `--label-face` never reached the facts (specificity); the boards restyled the components they exist to compare. Findings recorded, not fixed: `id="cardBuilders"` leaves the facts when Builders becomes a figure, which breaks `projects_hub.mjs:496-498, 586-602` (the build keeps a hidden dt/dd pair or moves the assertion); the LIGHT paragraph's "nothing black on a light page" is not true of dark art on a paper card (see v3's proposal).
54
+ - **v2** (25 survived, 12 applied, confirm found two regressions, closed in a second batch): the hall glyph is now on the platform row only with seven cells on every row; one focus ring on the search; the dt/dd hairlines meet; the band's cells are real targets. **Closed 2026-08-28 (second batch, confirm clean):** the callout ground is one patch per callout, solid to 10px past every glyph edge and feathered by a token-only mask, not a shadow (the caption measures 7.04:1 light / 10.45:1 dark under every column, the figures 17.1 / 19.2; the first batch had thinned it to 1.61 / 3.11, and one patch per figure and one per caption had stacked into a stepped slab); under 480 the search folds into a 44px magnifier that drops one hairline row of the page ground under the bar with the full-width field and its count (a typed query at 320 sits in a 196px field, focus lands on it, Escape or an opening menu folds it; the bar had left 45px for the field, and no floor fits beside four controls); and `Runs` is the address on every rung (Example: example.com), the table keeps the module list.
55
+ - **v3** (35 survived, 17 applied, confirm clean but two nits): the search pill takes the shared two-tone ring; the current-view mark follows the view across the six states; the seven board labels paint in both modes; the strip's light tint separates from the paper by its edge in the measured band; the signed-out states drop the membership claims; the card's scrim keeps the name at 16.97:1 light / 19.01:1 dark. Two nits, closed 2026-08-28: the close button is sticky inside the scrolling card (still in view at 320 × 568 with the door reached); the tray around a lone door goes, so the pill stands alone on the ladder's short rungs.
56
+
57
+ Every version ends `ALL CLEAN` on the render (all states, both modes, 1440 / 390 / 320, reduced motion dead) and `ALL RULES PASS` on the mechanical check after its batch. The refuted findings (15) were either by-design behaviour the brief asks for, a given the reviewer re-opened, or a measurement the skeptic could not reproduce.
58
+
59
+ ## 4. Review
60
+
61
+ Open `docs/design/mocks/hub/hub-sheet.html` in a browser from the checkout. Dark is the default; flip to light. The **state** picker shows the same state in every version's first tile (signed out, signed in, each menu open, keyboard focus, the search with a query, the mine and new views with the search gone, the table with the glyph); the **card** picker walks the ladder in the second tile. The two phone tiles show the menu open and the card at 390. Under each version the three **boards** put every bar state, every card rung and the menu component on one tall page, so a version can be read whole without clicking. `open full` walks any state at full size; the head comment of each version states its thesis, its bar, its menu, its card, its light answer, its 390 answer, what changes beyond the skin, its data and its tokens.
62
+
63
+ ## 5. Beyond the skin: what each version changes on the page, and what it proposes
64
+
65
+ Every item is tagged as the builders tagged it: **given** (one of the eight), **consequence** (follows from a given), **proposal** (the version's own). Test collisions are named from the test map so the build can retarget rather than discover.
66
+
67
+ **Shared by all three (consequences of the givens):**
68
+ - The nav row's height returns to the sky; at 390 the bar goes from a 161px stack to one 56 to 60px row and the sky starts about 244px higher. `.field{height:calc(100vh - 232px)}` assumed the old bar and a three-control toolbar; retarget.
69
+ - The toolbar keeps `Sky | Table` and `Only mine`; `#mapCount` travels with the search into the bar (`4 projects` at rest, `1 of 4` under a query). `projects_hub.mjs:389,393` pin the computation, not the wording; the copy registry regenerates.
70
+ - The relationship pill loses the accent (it states a fact and is not pressable: the One Warm Thing Rule); the PAIRS rows for `.cardRel` / `.tbl .pill` retarget.
71
+ - The hall glyph rides inside the existing `.colGo` cell beside `Open ->`, so the six-cell parity test (`projects_hub.mjs:531-535`) never fires.
72
+ - The mark is the landing's static bongo-glyph + name pill/type. **This retires the self-typing wordmark** (`cloud|bongos`), which the 2026-08-13 pass kept on purpose as the carrier of the "type bongos in any AI" metaphor and which is frozen by `projects_hub.mjs:116-134, 400-434`. All three builders read given 2 ("the logo") as the landing's mark; the owner should say whether the typing stays. If it stays, it types inside the mark's slot in any version.
73
+ - The table's "could not ask" mark is an em dash, banned by the taste rule the card already obeys; the mocks render a middot (`projects_hub.mjs:564-580` pins the dash).
74
+ - One first-level heading per document (today's page carries ten `<h1>`s, one per view); the view headings become `<h2>`.
75
+ - The contract's `in-table` wording ("glyph on every member row") contradicts given 7 ("only where the hall address differs"): Example is a member row whose hall is at its own site. v1 built the given (glyph on the platform row only), v2 and v3 built the wording and flagged it. **Recommendation: address-gated in both places**, one rule for the glyph and the card door.
76
+
77
+ **v1 proposes:** `About` (goal 1000063) as a fourth Projects-menu item under a hairline; the card at 560px (cosmos's `min(420px,100%)` today) with `--shadow-card` dropped; `Runs` as the project's address on the poster while the table keeps the module list (`projects_hub.mjs:586-602` pins Runs = tags); the mine view's empty state as a hairline instead of a 2px dashed box; the status cell's `not checked` in place of `• —`.
78
+
79
+ **v2 proposes:** the toolbar as a second hairline row under the bar; the mine view's empty state as a band of the same grammar; `About` as a fourth cell of the band; the card at 560px; `Runs` as the address on every rung (the table keeps the module list); under 480 the search folded into a 44px magnifier that drops one hairline row under the bar with the full-width field and its count (a 320 bar cannot hold a query beside four controls, and the row is this version's own grammar); the three carried controls under the 24px floor (footer links, the table's `all:unset` sort buttons, the `.search` inputs) floored (`projects_hub_pre_uat.mjs:430-433` pins the sort button rule verbatim).
80
+
81
+ **v3 proposes:** the card at 640px on desktop ("the room earns it"); the hover card taking the panel's glass instead of `--bg-card` + `--shadow-card` (the world has no shadow vocabulary for UI); the search finding by tag as well as by name; the table view scrolling inside its own panel under 500px (today `body{overflow-x:hidden}` makes Status and Open unreachable on a phone); the wizard's bongo pair on the `--chrome-*` / `--skin-*` ramp instead of its marigold hex literals; a decision on dark project art over a paper card in light mode (accept photographs as exempt, or matte them).
82
+
83
+ ## 6. What the build needs that the page does not have yet (recorded so the pick is informed; not design questions)
84
+
85
+ These were found by the test and page mappers and confirmed against the feeds; they belong to tasks 1003300 / 1003301, not to this round.
86
+
87
+ - **No feed carries a per-project hall address.** `clientBranding()` deliberately withholds domains and `{{buildersOrigin}}` is one value per instance, so "hall entry only where the hall address differs from the site address" needs a new field on `/api/bongos/projects/featured` (or `/my-projects`), e.g. `hall_origin`, null where the hall is at `<site>/builders`. On the apex the hall is `builders.cloudbongos.com` (differs); on a child it is `<site>/builders` (same).
88
+ - **No feed carries faces or a 30-day count.** The enrichment (`modules/platform-identity/project-stats.js`) returns `builders`, `tags`, `owner`, `owner_avatar`; the faces need the remote leaderboard's avatar rows kept (the count already comes from them), and `shipped_30d` needs the remote `/api/gds/public/recent-shipped` (row-capped at 30, hence the `N+` rule). `art_url` is already on the feed and is read by nothing.
89
+ - **Tests that pin the retired chrome and must be retargeted in the build** (from `tests/projects_hub.mjs`, `tests/projects_hub_pre_uat.mjs`, `tests/landing_page.mjs`): the `Home` link (`projects_hub.mjs:40, 46, 48`), the hall pill's `data-link-hall href="{{buildersOrigin}}"` (`landing_page.mjs:214-220`, `projects_hub.mjs:105`), the `.shell .navrow` rule in cosmos (`projects_hub_pre_uat.mjs:203`), the card's slice rule (`projects_hub.mjs:487`: the first adjacent `</div></div>` ends the slice, so every new card region must be a direct child and the banner a bare `<img>`), the `<td>` count across the header, `#selfRow` and the row template if the glyph becomes a cell (`projects_hub.mjs:531-535`), and the singular `querySelector('[data-nav="mine"]')` at `projects.html:5307` (a `My projects` item with `data-nav="mine"` would be a second node; hide by `querySelectorAll` or hook it differently). The dropdown items keep `href="?view=map" data-nav="map"` (etc.) as adjacent attributes so `projects_hub.mjs:42-43` holds unchanged. A new bar-ground token goes into cosmos with both dark twins and a `PAIRS` row in `tests/cosmos_token_contrast.mjs`.
90
+ - **Copy.** Every string that changes on the built page re-runs `node scripts/gds/copy-inventory.js` (CI checks the registry).
91
+
92
+ ## 7. The owner's decision
93
+
94
+ **2026-08-28: v2, the hairline.** Move forward with v2's bar, menus, search and card ("in terms of template preference I would say the hairline is the best to move forward with"); v1 and v3 stay on disk as built. The raises and rulings made with the pick:
95
+
96
+ - **The self-typing wordmark lives on the landing only** ("can probably only live on the landing page"). The hub carries the static mark, the bongo glyph and "Cloud Bongos" as plain type, as all three mocks did; `projects_hub.mjs:116-134, 400-434` retarget to the static mark, and the "fixed slot measured via `#markMeasure`" line in task 1003300 is void on the hub.
97
+ - **Sky and Table become two views in the Projects menu**: Sky / Table / Your projects / New project (+ About under a hairline, §5). The `Sky | Table` pill leaves the toolbar. **Only mine moves beside the search in the bar as a filter**, member-gated as today. With `#mapCount` already in the bar, nothing is left in the toolbar row: it goes, and the sky starts under the bar's hairline. Retargets on top of §6: `.field{height:calc(100vh - 232px)}`, the `#sub-sky` / `#sub-table` `aria-pressed` pins, the `.switch` placement, and the menu-item pins (`Map</a>` becomes `Sky</a>` + `Table</a>`) in `tests/projects_hub.mjs`; the copy registry regenerates.
98
+ - **The map's look improves beyond this round**: the planets as **templates** styled like the landing's hero blocks (the chrome-world objects, in different colours, shapes and styles), plus the field itself, filed as its own task, task 1003319, under this goal after 1003300.
99
+ - **The §5 proposals travel with the version** unless the owner strikes one: the toolbar as a hairline row (moot: the toolbar goes), the mine view's empty state as a band of the same grammar, `About` as a cell of the band, the card at 560px, the three under-floor controls floored, `Runs` as the address on every rung (the table keeps the module list), the search folded into a magnifier under 480.
100
+ - **Not ruled on, adopted by recommendation until the owner says otherwise**: the hall glyph and the card's hall door are address-gated in both the table and the card (given 7's reading, one rule); dark project art on the paper card in light mode is accepted as photographs are, no matte.
101
+
102
+ The criterion `apex-hub-chrome-and-card` moves to the builds: task 1003300 (the chrome, now with the Sky / Table menu, the Only mine filter in the bar and no toolbar), task 1003301 (the card), and task 1003319 (the planets and the field). Both build tasks carry the pick as an amendment to their descriptions (written 2026-08-28).
103
+
104
+ ## 8. What carries forward
105
+
106
+ - **Task 1003300** builds v2's bar, menus (Sky / Table / Your projects / New project) and search, with Only mine beside the search and the toolbar retired, into `public-landing/projects.html` + `cosmos.css` (the hub's sheet moves onto the world's derived tier the way `world.css` did, keeping `--line` and the status vocabulary), retargets the tests in §6, regenerates the copy registry.
107
+ - **Task 1003301** builds the picked card and its ladder, adds the enrichment fields (§6) inside the same `Promise.allSettled` with the shared null object, gates the hall entry on the new hall-address field and membership, and keeps the orbs and the hover card untouched.
108
+ - The unpicked versions stay on disk as built and verified; what they leave behind is listed per version in §3.
@@ -0,0 +1,57 @@
1
+ -- platform_identity_011_project_visibility.sql — the project join-door enum + the
2
+ -- missing identity field the publish gate reads (BV1.R04, task 1002323; ADR 0182
3
+ -- Decision 1 + Decision 4). Precedent: `goals.visibility` (core_188, ADR 0112).
4
+ --
5
+ -- ADR 0182 splits what looked like ONE four-valued state into TWO axes:
6
+ --
7
+ -- publishability DERIVED from the identity gate (name + description) → on the map at all
8
+ -- the join door STORED `visibility ∈ (public, private, stealth)` → how it renders, who gets in
9
+ --
10
+ -- so "dark matter" is the ABSENCE of a met bar, never a stored fourth value. There
11
+ -- is deliberately no way to choose it: a stored `unlisted` would compete with
12
+ -- `private` and give one question two sources of truth that can drift apart.
13
+ --
14
+ -- THE BACKFILL is the DEFAULT clause, not a separate UPDATE: every existing row
15
+ -- takes `visibility = 'public'` atomically at ADD COLUMN time, which is today's
16
+ -- behaviour unchanged. `description` is deliberately left NULL on existing rows —
17
+ -- ADR 0182 D4 refuses to seed it from `tagline` ("a description is the publish bar;
18
+ -- collapsing them means editing your card can unpublish your project"), so an
19
+ -- existing row is publishable only once its owner writes a real description.
20
+ --
21
+ -- THIS MIGRATION CHANGES NO LIVE BEHAVIOUR. The public feed predicate is still
22
+ -- `featured = true AND status = 'active'` until R06 (task 1002325) replaces it, so
23
+ -- nothing leaves or joins the map here. When R06 lands, rows with no description
24
+ -- become dark matter — that is the designed consequence, and R16 (task 1002335) is
25
+ -- the surface that tells an owner exactly which field is missing.
26
+ --
27
+ -- Additive + `platform_identity_`-namespaced (ADR 0083 §5). Forward-safe: ADD
28
+ -- COLUMN only, so the previous code release still runs against this schema.
29
+ -- Idempotent (IF NOT EXISTS + the drop-then-add CHECK pattern from migration 006).
30
+
31
+ BEGIN;
32
+
33
+ -- 1. The join door. Three values, all of them VISIBLE (ADR 0182 D2) — `private` is
34
+ -- visible-but-closed exactly as it already is for goals, and `stealth` exposes
35
+ -- EXISTENCE without IDENTITY (redacted server-side in the feed by R06).
36
+ ALTER TABLE platform_identity_projects
37
+ ADD COLUMN IF NOT EXISTS visibility text NOT NULL DEFAULT 'public';
38
+
39
+ ALTER TABLE platform_identity_projects DROP CONSTRAINT IF EXISTS platform_identity_projects_visibility_check;
40
+ ALTER TABLE platform_identity_projects ADD CONSTRAINT platform_identity_projects_visibility_check
41
+ CHECK (visibility IN ('public', 'private', 'stealth'));
42
+
43
+ -- 2. The second half of the identity gate. `name` already exists NOT NULL; this is
44
+ -- the column ADR 0182 D4 found missing. NOT `tagline` — see the header.
45
+ ALTER TABLE platform_identity_projects
46
+ ADD COLUMN IF NOT EXISTS description text;
47
+
48
+ COMMENT ON COLUMN platform_identity_projects.visibility IS
49
+ 'The join door, set by the project owner: public|private|stealth (ADR 0182 D1). The name is a mild lie kept for one word across the goal and project planes: this column does NOT decide visibility — publishability does. Anyone reading visibility = ''public'' and expecting the orb on the map is reading it wrong; check the publish gate (name + description both present).';
50
+
51
+ COMMENT ON COLUMN platform_identity_projects.description IS
52
+ 'The second half of the publish gate (ADR 0182 D4): a project is publishable iff name AND description are both present. Distinct from tagline, which is the Explore card one-liner — collapsing them would mean editing your card can unpublish your project.';
53
+
54
+ COMMENT ON COLUMN platform_identity_projects.status IS
55
+ 'Operational lifecycle, owned by the system/Archon — a deprovisioned project is hidden. One of the three independent mechanisms in ADR 0182 D3 (status | visibility | featured); a project is on the map iff status = ''active'' AND publishable, and featured may only rank among rows already there.';
56
+
57
+ COMMIT;
@@ -233,37 +233,90 @@ async function getClientByOrigin(origin, { pool = defaultPool } = {}) {
233
233
  // provisioning's copy on its own instances table).
234
234
  const PROJECT_TYPES = ['game', 'research', 'business', 'non-profit', 'not-sure'];
235
235
 
236
+ // The stored JOIN DOOR (ADR 0182 D1, task 1002323). Three values, and ALL THREE ARE
237
+ // VISIBLE: `private` is visible-but-closed (the `goals.visibility` meaning, ADR 0112),
238
+ // `stealth` publishes EXISTENCE without IDENTITY. Mirrored by the CHECK in migration 011.
239
+ //
240
+ // There is no fourth value. "Dark matter" — off the map entirely — is DERIVED from the
241
+ // publish gate below, never stored, because a stored `unlisted` would compete with
242
+ // `private` and let two sources of truth for one question drift apart.
243
+ const PROJECT_VISIBILITIES = ['public', 'private', 'stealth'];
244
+
245
+ // The publish gate is IDENTITY ONLY (ADR 0182 D4, owner interview on R16): a name and a
246
+ // description. Not tagline, art, modules, governance, or a completed interview — the bar
247
+ // exists to keep empty husks off the map, not to enforce completeness.
248
+ const PUBLISH_GATE_FIELDS = ['name', 'description'];
249
+
250
+ // Is this project on the map at all? Returns the verdict AND what is missing, because
251
+ // ADR 0182 D4 requires the gate to NAME what is missing rather than merely refuse: a
252
+ // project that cannot be published and is not told why is indistinguishable from a
253
+ // broken one. R16 (task 1002335) renders `missing`; R06 (task 1002325) reads `publishable`.
254
+ function projectPublishability(project) {
255
+ const missing = PUBLISH_GATE_FIELDS.filter((f) => !String(project?.[f] ?? '').trim());
256
+ return { publishable: missing.length === 0, missing };
257
+ }
258
+
259
+ // The boolean alone, for predicates that do not care which field is absent.
260
+ function projectIsPublishable(project) {
261
+ return projectPublishability(project).publishable;
262
+ }
263
+
264
+ // The SAME gate as a SQL fragment, so the feed predicate (R06) and the owner-facing
265
+ // refusal (R16) cannot drift from projectPublishability() or from each other. `alias`
266
+ // qualifies the columns for a joined query (e.g. publishableSql('p')).
267
+ function publishableSql(alias) {
268
+ // The alias is INTERPOLATED, so it is the one thing here that could carry SQL. Callers
269
+ // pass a literal, but a helper that builds SQL should refuse anything that is not a
270
+ // bare identifier rather than trust every future caller to remember that.
271
+ if (alias && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(String(alias))) {
272
+ throw new Error('publishableSql: alias must be a bare SQL identifier');
273
+ }
274
+ const q = alias ? `${alias}.` : '';
275
+ return `(btrim(coalesce(${q}name, '')) <> '' AND btrim(coalesce(${q}description, '')) <> '')`;
276
+ }
277
+
236
278
  // Upsert a catalog row (keyed by origin). Called when a client registers (so every
237
279
  // federated project is catalogued, unfeatured) and by the Archon create/curate route.
238
280
  // Never touches `featured` on update — that is Archon-only via setProjectFeatured.
239
- async function upsertProject({ origin, name, tagline, artUrl, ownerGithubId, type }, { pool = defaultPool } = {}) {
281
+ //
282
+ // `visibility` is likewise absent from the column list, and deliberately: ADR 0182 D5
283
+ // makes the hub row a CACHE, so the hub never AUTHORIZES a door change — it accepts a
284
+ // report from the authenticated instance (R08, task 1002327). Leaving the column out of
285
+ // the upsert is what stops a client re-registration from silently resetting an owner's
286
+ // door back to the 'public' default.
287
+ async function upsertProject({ origin, name, tagline, description, artUrl, ownerGithubId, type }, { pool = defaultPool } = {}) {
240
288
  const norm = String(origin || '').replace(/\/+$/, '');
241
289
  if (!norm) throw new Error('project origin required');
242
290
  // off-vocabulary → null (the DB CHECK is the backstop; a declaration is never invented)
243
291
  const declaredType = PROJECT_TYPES.includes(type) ? type : null;
244
292
  const { rows } = await pool.query(
245
- `INSERT INTO platform_identity_projects (origin, name, tagline, art_url, owner_github_id, type)
246
- VALUES ($1, $2, $3, $4, $5, $6)
293
+ `INSERT INTO platform_identity_projects (origin, name, tagline, description, art_url, owner_github_id, type)
294
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
247
295
  ON CONFLICT (origin) DO UPDATE SET
248
296
  name = EXCLUDED.name,
249
297
  tagline = COALESCE(EXCLUDED.tagline, platform_identity_projects.tagline),
298
+ description = COALESCE(EXCLUDED.description, platform_identity_projects.description),
250
299
  art_url = COALESCE(EXCLUDED.art_url, platform_identity_projects.art_url),
251
300
  owner_github_id = COALESCE(EXCLUDED.owner_github_id, platform_identity_projects.owner_github_id),
252
301
  type = COALESCE(EXCLUDED.type, platform_identity_projects.type),
253
302
  updated_at = now()
254
- RETURNING id, origin, name, tagline, art_url, owner_github_id, type, status, featured`,
255
- [norm, name || norm, tagline ?? null, artUrl ?? null, ownerGithubId ?? null, declaredType],
303
+ RETURNING id, origin, name, tagline, description, art_url, owner_github_id, type, status, featured, visibility`,
304
+ [norm, name || norm, tagline ?? null, description ?? null, artUrl ?? null, ownerGithubId ?? null, declaredType],
256
305
  );
257
306
  return rows[0];
258
307
  }
259
308
 
309
+ // A single catalog row, with the DERIVED publish verdict attached (ADR 0182 D1) — the
310
+ // row alone cannot answer "is this on the map", so every read that returns one answers
311
+ // it here rather than leaving each caller to re-derive the gate.
260
312
  async function getProject(id, { pool = defaultPool } = {}) {
261
313
  const { rows } = await pool.query(
262
- `SELECT id, origin, name, tagline, art_url, owner_github_id, type, status, featured
314
+ `SELECT id, origin, name, tagline, description, art_url, owner_github_id, type, status, featured, visibility
263
315
  FROM platform_identity_projects WHERE id = $1`,
264
316
  [id],
265
317
  );
266
- return rows[0] || null;
318
+ if (!rows[0]) return null;
319
+ return { ...rows[0], ...projectPublishability(rows[0]) };
267
320
  }
268
321
 
269
322
  // The PUBLIC Explore listing — featured + active projects only, public projection
@@ -282,15 +335,20 @@ async function listFeaturedProjects({ pool = defaultPool } = {}) {
282
335
  return rows;
283
336
  }
284
337
 
285
- // The Archon curation list — every active project + its featured flag.
338
+ // The curation list — every active project, its featured flag, its stored door, and the
339
+ // DERIVED publish verdict. `publishable` is computed in SQL from publishableSql() so this
340
+ // list and the public feed (R06) read one definition; `missing` names the absent identity
341
+ // field so a curator can see WHY a row is dark matter instead of guessing (ADR 0182 D4).
286
342
  async function listAllProjects({ pool = defaultPool } = {}) {
287
343
  const { rows } = await pool.query(
288
- `SELECT id, origin, name, tagline, art_url, owner_github_id, type, status, featured, updated_at
344
+ `SELECT id, origin, name, tagline, description, art_url, owner_github_id, type, status,
345
+ featured, visibility, updated_at,
346
+ ${publishableSql()} AS publishable
289
347
  FROM platform_identity_projects
290
348
  WHERE status = 'active'
291
349
  ORDER BY featured DESC, updated_at DESC`,
292
350
  );
293
- return rows;
351
+ return rows.map((r) => ({ ...r, missing: projectPublishability(r).missing }));
294
352
  }
295
353
 
296
354
  // Archon-only: flip the Explore feature flag on a catalog row.
@@ -1115,6 +1173,11 @@ module.exports = {
1115
1173
  getClientByOrigin,
1116
1174
  setClientStatus,
1117
1175
  // projects catalog + Explore
1176
+ PROJECT_VISIBILITIES,
1177
+ PUBLISH_GATE_FIELDS,
1178
+ projectPublishability,
1179
+ projectIsPublishable,
1180
+ publishableSql,
1118
1181
  upsertProject,
1119
1182
  PROJECT_TYPES,
1120
1183
  getProject,
@@ -2,7 +2,8 @@
2
2
  // curated public Explore showcase (ADR 0141 §5, task #1002110).
3
3
  //
4
4
  // GET /projects/featured — PUBLIC Explore listing (featured + active only)
5
- // GET /projects — Archon: full curation list (+ featured flags)
5
+ // GET /projects — Archon: full curation list (+ featured flags, the stored
6
+ // join door, and the DERIVED publish verdict — ADR 0182)
6
7
  // POST /projects — Archon: create / update a catalog entry
7
8
  // PATCH /projects/:id/featured — Archon: the feature / unfeature control
8
9
  //
@@ -41,6 +42,10 @@ module.exports = function projectsRoutes() {
41
42
  origin: { required: true, type: 'string', maxLength: 512 },
42
43
  name: { required: true, type: 'string', maxLength: 200 },
43
44
  tagline: { type: 'string', maxLength: 400 },
45
+ // The second half of the publish gate (ADR 0182 D4, task 1002323) — NOT tagline,
46
+ // which is the Explore card one-liner. Longer cap than the tagline's on purpose:
47
+ // this is the thing that says what the project IS.
48
+ description: { type: 'string', maxLength: 2000 },
44
49
  art_url: { type: 'string', maxLength: 2048 },
45
50
  type: { type: 'string', enum: [...pi.PROJECT_TYPES] },
46
51
  })) return;
@@ -49,6 +54,7 @@ module.exports = function projectsRoutes() {
49
54
  origin: req.body.origin,
50
55
  name: req.body.name,
51
56
  tagline: req.body.tagline,
57
+ description: req.body.description,
52
58
  artUrl: req.body.art_url,
53
59
  type: req.body.type,
54
60
  }, { pool });
@@ -14,6 +14,7 @@
14
14
  import { strict as assert } from 'node:assert';
15
15
  import { createRequire } from 'node:module';
16
16
  import crypto from 'node:crypto';
17
+ import { readFileSync } from 'node:fs';
17
18
 
18
19
  const require = createRequire(import.meta.url);
19
20
  const P = require('../platform-identity.js');
@@ -253,6 +254,111 @@ await ta('setProjectFeatured flips the flag (Archon curation)', async () => {
253
254
  assert.deepEqual(pool.calls[0].params, [1, true]);
254
255
  });
255
256
 
257
+ console.log('\nproject visibility: the stored door + the DERIVED publish gate (BV1.R04, ADR 0182):');
258
+
259
+ t('the stored enum has exactly THREE values — dark matter is derived, never stored', () => {
260
+ assert.deepEqual([...P.PROJECT_VISIBILITIES].sort(), ['private', 'public', 'stealth']);
261
+ // ADR 0182 D2 / "Alternatives rejected": a stored fourth value would compete with
262
+ // `private` and give one question two sources of truth that can drift apart.
263
+ for (const forbidden of ['unlisted', 'dark-matter', 'dark_matter', 'hidden', 'published']) {
264
+ assert.ok(!P.PROJECT_VISIBILITIES.includes(forbidden), `${forbidden} must never be a stored door value`);
265
+ }
266
+ });
267
+
268
+ t('the publish gate is IDENTITY ONLY — name + description, and it NAMES what is missing', () => {
269
+ assert.deepEqual([...P.PUBLISH_GATE_FIELDS].sort(), ['description', 'name']);
270
+ const full = { name: 'Mercury', description: 'A weather sim.', tagline: 't', art_url: 'a', type: 'research' };
271
+ assert.deepEqual(P.projectPublishability(full), { publishable: true, missing: [] });
272
+ assert.equal(P.projectIsPublishable(full), true);
273
+ // ADR 0182 D4: the gate must name what is missing, never merely refuse.
274
+ assert.deepEqual(P.projectPublishability({ name: 'Mercury' }), { publishable: false, missing: ['description'] });
275
+ assert.deepEqual(P.projectPublishability({}), { publishable: false, missing: ['name', 'description'] });
276
+ // Whitespace is not an identity — a husk stays off the map.
277
+ assert.deepEqual(P.projectPublishability({ name: 'M', description: ' ' }).missing, ['description']);
278
+ assert.equal(P.projectIsPublishable(null), false);
279
+ });
280
+
281
+ t('a tagline is NOT a description — the card one-liner can never publish a project', () => {
282
+ // ADR 0182 D4: collapsing them means editing your card can unpublish your project.
283
+ const cardOnly = { name: 'Mercury', tagline: 'a weather sim' };
284
+ assert.equal(P.projectIsPublishable(cardOnly), false);
285
+ assert.deepEqual(P.projectPublishability(cardOnly).missing, ['description']);
286
+ // The bar is identity only — art, type, modules and governance are NOT on it.
287
+ assert.equal(P.projectIsPublishable({ name: 'M', description: 'd' }), true);
288
+ });
289
+
290
+ t('publishableSql is the SAME gate in SQL — one definition for the feed (R06) and the refusal (R16)', () => {
291
+ const bare = P.publishableSql();
292
+ const aliased = P.publishableSql('p');
293
+ for (const sql of [bare, aliased]) {
294
+ assert.match(sql, /\bname\b/);
295
+ assert.match(sql, /\bdescription\b/);
296
+ assert.doesNotMatch(sql, /\btagline\b/, 'the card one-liner is not on the bar');
297
+ assert.doesNotMatch(sql, /\bfeatured\b/, 'editorial promotion is not the publish gate (ADR 0182 D3)');
298
+ assert.doesNotMatch(sql, /\bvisibility\b/, 'the stored door does not decide publishability');
299
+ }
300
+ assert.match(aliased, /p\.name/, 'an alias qualifies both columns');
301
+ assert.match(aliased, /p\.description/);
302
+ assert.doesNotMatch(bare, /\w\.name/, 'no alias when none is asked for');
303
+ assert.equal(P.publishableSql(''), bare, 'an empty alias is the same as none');
304
+ // The alias is the one interpolated thing here — a helper that BUILDS SQL refuses
305
+ // anything that is not a bare identifier rather than trust every future caller.
306
+ assert.throws(() => P.publishableSql('p; DROP TABLE platform_identity_projects --'), /bare SQL identifier/);
307
+ });
308
+
309
+ await ta('upsertProject carries description (COALESCE-preserved) and NEVER writes visibility', async () => {
310
+ const pool = mockPool([{ rows: [{ id: '1', origin: 'https://p', description: 'd', visibility: 'public' }] }]);
311
+ await P.upsertProject({ origin: 'https://p', name: 'Proj', description: 'A weather sim.' }, { pool });
312
+ const c = pool.calls[0];
313
+ assert.match(c.text, /\bdescription\b/, 'the publish-gate field joins the upsert');
314
+ assert.match(c.text, /description\s*=\s*COALESCE/, 'a later descriptionless upsert must not unpublish the project');
315
+ assert.ok(c.params.includes('A weather sim.'), 'the description is parameterized');
316
+ // ADR 0182 D5: the hub row is a CACHE — it never AUTHORIZES a door change. If a client
317
+ // re-registration wrote visibility, it would silently reset an owner's door to 'public'.
318
+ assert.doesNotMatch(c.text, /visibility\s*=/, 'the door is reported (R08), never set by an upsert');
319
+ });
320
+
321
+ await ta('listAllProjects reports the stored door + the DERIVED verdict, computed in SQL', async () => {
322
+ const pool = mockPool([{ rows: [
323
+ { id: '1', name: 'Full', description: 'd', visibility: 'stealth', publishable: true },
324
+ { id: '2', name: 'Husk', description: null, visibility: 'public', publishable: false },
325
+ ] }]);
326
+ const out = await P.listAllProjects({ pool });
327
+ assert.match(pool.calls[0].text, /\bvisibility\b/, 'the stored door is projected');
328
+ assert.match(pool.calls[0].text, /AS publishable/, 'the verdict is derived in SQL, not re-derived per caller');
329
+ // A curator must be able to see WHY a row is dark matter, not just that it is.
330
+ assert.deepEqual(out[0].missing, []);
331
+ assert.deepEqual(out[1].missing, ['description']);
332
+ // All three doors are VISIBLE (D2) — a stealth row is a normal curation row here.
333
+ assert.equal(out[0].visibility, 'stealth');
334
+ });
335
+
336
+ await ta('getProject attaches the derived verdict — the row alone cannot answer "is this on the map"', async () => {
337
+ const pool = mockPool([{ rows: [{ id: '1', name: 'Husk', description: null, visibility: 'public', featured: true, status: 'active' }] }]);
338
+ const out = await P.getProject(1, { pool });
339
+ assert.match(pool.calls[0].text, /\bdescription\b/);
340
+ // featured + active + visibility='public' and STILL off the map: publishability is
341
+ // the axis that decides, exactly as the column comment warns.
342
+ assert.equal(out.publishable, false);
343
+ assert.deepEqual(out.missing, ['description']);
344
+ assert.equal(await P.getProject(2, { pool: mockPool([{ rows: [] }]) }), null);
345
+ });
346
+
347
+ t('migration 011 pins the schema half: three-value CHECK, additive, no tagline seed', () => {
348
+ const sql = readFileSync(new URL('../migrations/platform_identity_011_project_visibility.sql', import.meta.url), 'utf8');
349
+ const stripped = sql.replace(/--[^\n]*/g, ' ');
350
+ assert.match(stripped, /ADD COLUMN IF NOT EXISTS visibility text NOT NULL DEFAULT 'public'/);
351
+ assert.match(stripped, /ADD COLUMN IF NOT EXISTS description text/);
352
+ const check = (stripped.match(/CHECK \(visibility IN \(([^)]*)\)\)/) || [, ''])[1];
353
+ const values = check.split(',').map((v) => v.trim().replace(/'/g, '')).filter(Boolean).sort();
354
+ assert.deepEqual(values, ['private', 'public', 'stealth'], 'the DB CHECK mirrors PROJECT_VISIBILITIES exactly');
355
+ assert.deepEqual(values, [...P.PROJECT_VISIBILITIES].sort(), 'CHECK and constant cannot drift');
356
+ // The backfill IS the DEFAULT clause — and ADR 0182 D4 refuses a tagline seed, so an
357
+ // existing row stays dark matter until its owner writes a real description.
358
+ assert.doesNotMatch(stripped, /SET\s+description\s*=\s*tagline/i, 'description is never seeded from tagline');
359
+ assert.doesNotMatch(stripped, /\bDROP COLUMN\b|\bSET NOT NULL\b/i, 'additive + forward-safe only');
360
+ });
361
+
256
362
  await ta('registerClient auto-catalogues the project (2nd query) when an origin is given', async () => {
257
363
  const pool = mockPool([{ rows: [{ client_id: 'c1', origin: 'https://p' }] }, { rows: [{ id: '9' }] }]);
258
364
  await P.registerClient({ clientId: 'c1', secret: 'z'.repeat(64), name: 'Proj', redirectUris: [], origin: 'https://p', ownerGithubId: 7 }, { pool });
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.391",
3
+ "version": "1.19.393",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.391",
9
+ "version": "1.19.393",
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.391",
3
+ "version": "1.19.393",
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",
@@ -115,6 +115,11 @@ const INTEGRATION = new Set([
115
115
  // and an unverified claim about exactly that is the comment this task fixed.
116
116
  'recall_freshness_plan_db',
117
117
  'reward_mode_cost_plus_only',
118
+ // task 1002323 (BV1.R04): the project visibility half against real SQL. The DB-free
119
+ // sibling in the module pins the JS gate and the migration text; only a real engine can
120
+ // prove the CHECK actually refuses a stored fourth state, that the backfill lands on a
121
+ // pre-migration row, and that publishableSql and projectPublishability agree.
122
+ 'project_visibility_db',
118
123
  'scouting_rollup_fanout', 'session_records',
119
124
  // task 1003217: the cost-row RAISE against real SQL. Every claim in it IS the
120
125
  // SQL — an ON CONFLICT DO UPDATE guarded to move a row only upward, and a
package/src/module-api.js CHANGED
@@ -1222,8 +1222,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1222
1222
  // landed since 1.19.389 with no explicit bump. run 33133575931. (task 1002620)
1223
1223
  // 1.19.391 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1224
1224
  // landed since 1.19.390 with no explicit bump. run 33134514193. (task 1002620)
1225
+ // 1.19.392 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1226
+ // landed since 1.19.391 with no explicit bump. run 33176032603. (task 1002620)
1227
+ // 1.19.393 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1228
+ // landed since 1.19.392 with no explicit bump. run 33177136260. (task 1002620)
1225
1229
  // ---------------------------------------------------------------------------
1226
- const CORE_VERSION = '1.19.391'; // CI auto-patch carrier (ADR 0161); see changelog above
1230
+ const CORE_VERSION = '1.19.393'; // CI auto-patch carrier (ADR 0161); see changelog above
1227
1231
 
1228
1232
  // A namespaced logger so a module's log lines are attributable + consistent.
1229
1233
  // Usage: const log = api.logger('dev-box'); log.info('mounted');