@bongos/core 1.19.441 → 1.19.442

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.
Files changed (31) hide show
  1. package/.bongos-core.json +69 -29
  2. package/clients/bongos-client/index.d.ts +1 -2
  3. package/docs/api/openapi.json +3 -20
  4. package/docs/api-reference.md +1 -1
  5. package/docs/copy-inventory.md +111 -107
  6. package/docs/copy-registry.json +191 -144
  7. package/docs/design/projects-hub-direction-v2.md +23 -0
  8. package/docs/module-api-changelog.md +2 -0
  9. package/modules/platform-identity/routes/projects.js +2 -1
  10. package/modules/provisioning/provisioning.js +11 -2
  11. package/modules/provisioning/routes/provisioning.js +10 -5
  12. package/modules/ui-design/kit/fixtures/featured-hub.json +1 -1
  13. package/modules/ui-design/kit/serve.js +29 -4
  14. package/package-lock.json +2 -2
  15. package/package.json +1 -1
  16. package/public-landing/assets/cosmos.css +8 -0
  17. package/public-landing/assets/world/r4-card-chrome.jpg +0 -0
  18. package/public-landing/assets/world/r4-card-chrome.jpg.json +14 -0
  19. package/public-landing/assets/world/r4-card-deepfield.jpg +0 -0
  20. package/public-landing/assets/world/r4-card-deepfield.jpg.json +14 -0
  21. package/public-landing/assets/world/r4-card-dust.jpg +0 -0
  22. package/public-landing/assets/world/r4-card-dust.jpg.json +14 -0
  23. package/public-landing/assets/world/r4-card-nebula.jpg +0 -0
  24. package/public-landing/assets/world/r4-card-nebula.jpg.json +14 -0
  25. package/public-landing/projects-floor.states.json +28 -1
  26. package/public-landing/projects.html +101 -1
  27. package/public-landing/projects.probes.json +6 -0
  28. package/public-landing/projects.states.json +1 -0
  29. package/src/module-api.js +1 -1
  30. package/tests/projects_hub.mjs +86 -3
  31. package/tests/provisioning_settings.mjs +49 -4
@@ -312,3 +312,26 @@ Three mastheads, rendered dark and light at 1440 / 390 / 320 (ALL CLEAN) and mea
312
312
  **Retargeted.** Every table pin: the self row's extraction (`<li class="tblRow" id="selfRow">`), the generated-row template, the six-cell parity test (now "every row lays the same five parts", which reads the planet expression too, so dropping the planet cannot pass), the column-source test (now fact-source), the fixed-tracks test (now asserts the two views' tracks line for line and that **no table rule outlived the table**), the `—`/0 pin, the Runs pin, `.rowHall`'s box, the placeholder-href guard, and `projects_hub_pre_uat.mjs`'s keyboard-sort test. One test is new: the sort control's own contract.
313
313
 
314
314
  **Flagged to the copy desk, not changed here.** `typeHtml`'s "not declared" now speaks in the row's voice but is still the formatter the create wizard's review reads through `typeLabel`; the two are not in conflict, but a copy pass over the row's four fact lines as a set would be worth one sitting.
315
+
316
+ ## 19. The card's backdrop: a plate set the owner picks, beside the planet (task 1003352)
317
+
318
+ **Date:** 2026-08-30 · deferred out of the planet round (§16) when the owner said, on that review, *"this could also house project card backdrop customization."* It waited because a card plate set is an art round of its own and the free image coverage had ended; the owner confirmed the spend for this session.
319
+
320
+ **The art round.** Four **5:2** bands — the shape a poster card's banner actually is. Round 3's four space plates are 16:9 field grounds; a 5:2 crop of one puts its density wherever it happens to fall, so the round generated for the card's own composition instead: the interest centre-right, the left third and both lower corners near-black, because the card puts its disc and name lower-left and its two figure callouts upper-left and lower-right.
321
+
322
+ | Key | What it is |
323
+ |---|---|
324
+ | `nebula` | a violet-and-bronze nebula wash, sparse stars — the field's own language in the card's shape |
325
+ | `dust` | warm bronze dust lanes, very low contrast; a haze rather than a shape |
326
+ | `deepfield` | a deep field of very distant galaxies on near-black; the quietest of the four |
327
+ | `chrome` | a sweeping ribbon of liquid chrome — the world's own material with no object in it |
328
+
329
+ **Two things worth recording about the round.** `nano_banana_pro` was asked first (the model round 3 used); after twenty-five minutes not one of its four jobs had left `waiting`, while the same four prompts on `soul_location` returned in fifteen seconds — a stuck provider queue, not a bad request. And the nebula came back too dark to read at the card's 560px: a **gamma** lift (1.75) raises the wash without crushing the black the disc and the name sit on, which is what a contrast lift did instead when it was tried first. Both are in the sidecars, so the plates can be remade.
330
+
331
+ **The key is the planet's kind, not a new kind.** `card_backdrop` joins `PLANET_VOCAB` rather than getting a vocabulary of its own, because everything true of the planet is true of it: a closed list, no default, the platform's to draw, pushed to no instance, restarting nothing. That one line is the whole server change — `planetChoice()` loops the vocab, the settings route builds both its validator and its `settingKeys` from it, and `publicInstance()` spreads it — so the key reaches the row, the own-scoped read and the public wire with no new branch. Two edits beyond it: `planetChoicesForHosts` now admits a host that chose **only** a backdrop, and `/projects/featured` carries the third key.
332
+
333
+ **On the page.** `openCard` reads `backdropPlate(p.card_backdrop) || safeHttpUrl(p.art_url)` — the owner's plate stands in front of the project's own picture, and a key this platform does not draw resolves to `''` rather than a broken `src`. The picker is **its own section beside the planet**, not a control inside it: the owner's words were "beside the planet", and one heading is how a project is drawn *everywhere* while the other is one surface's backdrop, so folding them together would have made both headings lie. It is the planet section's twin otherwise — one band of hairline cells (five: "its own picture" and the four plates), the preview at the planet preview's width as a 5:2 band, one save door, the settings PATCH, the receipt the page repaints from. The preview's caption speaks for the ROW, so a pick that is ahead of the row reads **"not saved yet"** rather than "nothing chosen" under a plate the owner can already see.
334
+
335
+ **What the grade caught, and the class it closed.** The first round shipped `card_backdrop` into `PLANET_VOCAB` and stopped there — and the settings route's `validateOrRespond` schema was a **hand-written field list**, not the vocabulary. Strict mode rejects any field a schema does not declare, so `settingKeys` would have copied the key that the validator had already 400'd: every real save from the manage page would have failed. Nothing caught it, because the kit's stub accepted any body key, so 47 probes and a clean render all passed against a request the server refuses. Both halves are fixed rather than patched: the route's schema is now **derived from the vocabularies** (`vocabSchema(SETTINGS_VOCAB)` + `vocabSchema(PLANET_VOCAB)`), so a key can no longer exist in one and not the other; the stub validates against the same closed lists (copied — a kit module may not import core — and pinned to their source by a test); and `provisioning_settings.mjs` now PATCHes the **real** router with `card_backdrop`, refuses an off-vocabulary one, and walks every declared key of both vocabularies asserting a 200, which is the guard that would have failed on the first round.
336
+
337
+ **The kit.** The read path renders where the hub's own catalog is served — `projects-floor.states.json` is the sibling allowed to alias a fixture, so `featured-hub.json` gives Example a `card_backdrop` and the new `card-backdrop` state pins the plate's `src` on the card. `render.js --page /projects` **ALL CLEAN** (with `in-manage-card`), the two sibling states files ALL CLEAN, `probe.js` **47/47** (two new: the pick saves with the route's no-restart receipt; handing it back restores the project's own picture). `projects_hub` 138/138, `projects_hub_pre_uat` 56/56, `<redacted>` 34/34, `provisioning_settings` 26/26, `provisioning_settings_env` 12/12, `validate-design --strict` valid, `fitness` all checks passed, the copy registry regenerated. DESIGN.md carries the round-4 table.
@@ -1319,5 +1319,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1319
1319
  landed since 1.19.439 with no explicit bump. run 33321131006. (task 1002620)
1320
1320
  1.19.441 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1321
1321
  landed since 1.19.440 with no explicit bump. run 33323478836. (task 1002620)
1322
+ 1.19.442 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1323
+ landed since 1.19.441 with no explicit bump. run 33326781131. (task 1002620)
1322
1324
  ---------------------------------------------------------------------------
1323
1325
  ```
@@ -148,7 +148,8 @@ module.exports = function projectsRoutes() {
148
148
  // the wire always carries both keys: null = the platform's own pick
149
149
  const planetOf = (p) => {
150
150
  const c = (planets && planets[hostOfOrigin(p.origin)]) || {};
151
- return { planet_template: c.planet_template || null, planet_accent: c.planet_accent || null };
151
+ return { planet_template: c.planet_template || null, planet_accent: c.planet_accent || null,
152
+ card_backdrop: c.card_backdrop || null };
152
153
  };
153
154
  // The enrichment's keys (builders, tags, owner, owner_avatar, faces,
154
155
  // shipped_30d, shipped_30d_capped, hall_origin) never collide with the
@@ -142,7 +142,16 @@ const PLANET_TEMPLATES = Object.freeze([
142
142
  ...PLANET_SHAPES.flatMap((s) => PLANET_MATERIALS.flatMap((m) => ['tint', 'wash', 'plain'].map((y) => `${s}-${m}-${y}`))),
143
143
  ]);
144
144
  const PLANET_ACCENTS = Object.freeze(['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent']);
145
- const PLANET_VOCAB = Object.freeze({ planet_template: PLANET_TEMPLATES, planet_accent: PLANET_ACCENTS });
145
+ // The card's backdrop (task 1003352) is the same KIND of key as the planet, so it
146
+ // rides the same vocab: a closed list of platform-drawn plates, null = the
147
+ // project's own art (or none). It is not a policy either — nothing is pushed to
148
+ // the instance and nothing restarts; the card simply reads the row.
149
+ const CARD_BACKDROPS = Object.freeze(['nebula', 'dust', 'deepfield', 'chrome']);
150
+ // Named PLANET_VOCAB for the keys it began with; what it actually holds is every
151
+ // key the PLATFORM draws a project with — no default, no env transport, no restart.
152
+ const PLANET_VOCAB = Object.freeze({
153
+ planet_template: PLANET_TEMPLATES, planet_accent: PLANET_ACCENTS, card_backdrop: CARD_BACKDROPS,
154
+ });
146
155
 
147
156
  // The optional project-detail interview (BV1.R15, task 1002334) — the "tell us
148
157
  // about your project" step every creation form offers and every owner may skip.
@@ -1088,7 +1097,7 @@ async function planetChoicesForHosts(db, hosts) {
1088
1097
  for (const r of rows) {
1089
1098
  if (out[r.host]) continue;
1090
1099
  const choice = planetChoice(r);
1091
- if (choice.planet_template || choice.planet_accent) out[r.host] = choice;
1100
+ if (Object.values(choice).some(Boolean)) out[r.host] = choice;
1092
1101
  }
1093
1102
  return out;
1094
1103
  }
@@ -796,12 +796,17 @@ module.exports = function provisioningRoutes() {
796
796
  router.patch('/provisioning/instances/:id/settings', auth.requireBuilder, async (req, res) => {
797
797
  const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
798
798
  if (!id) return;
799
+ // The schema IS the vocabularies, not a copy of them (task 1003352): strict
800
+ // mode rejects any field the schema does not declare, so a key added to a
801
+ // vocabulary but forgotten here reaches settingKeys and then 400s on every
802
+ // real save — which is exactly what card_backdrop did until the grader caught
803
+ // it. Deriving both from the same source makes that drift unrepresentable.
804
+ // The platform-drawn keys are nullable on purpose: null hands the pick back.
805
+ const vocabSchema = (vocab) => Object.fromEntries(
806
+ Object.entries(vocab).map(([key, values]) => [key, { type: 'string', enum: values }]));
799
807
  if (validateOrRespond(req, res, {
800
- platform_visibility: { type: 'string', enum: provisioning.SETTINGS_VOCAB.platform_visibility },
801
- joinability: { type: 'string', enum: provisioning.SETTINGS_VOCAB.joinability },
802
- // the planet (task 1003347): nullable on purpose — null hands the pick back to the platform
803
- planet_template: { type: 'string', enum: provisioning.PLANET_VOCAB.planet_template },
804
- planet_accent: { type: 'string', enum: provisioning.PLANET_VOCAB.planet_accent },
808
+ ...vocabSchema(provisioning.SETTINGS_VOCAB),
809
+ ...vocabSchema(provisioning.PLANET_VOCAB),
805
810
  })) return;
806
811
  const settingKeys = [...Object.keys(provisioning.SETTINGS_VOCAB), ...Object.keys(provisioning.PLANET_VOCAB)];
807
812
  const patch = {};
@@ -1 +1 @@
1
- {"_":"The hub's catalog (task 1003345): the landing's featured.json plus Tidewater, an all-null row nobody owns — the card-floor state opens ITS card, because Demo is owned by the signed-in stub (provisioning-instances.json) and an owned orb's plain click opens the manage view, never the card. Served under the name \"featured\" by projects.states.json's stub.fixtures alias; the landing keeps featured.json.","projects":[{"origin":"https://amazonprimea.com","name":"Off the Boats","tagline":"A singular shop, and a broader videogame in a persistent ancient Greek world that collides with modern day commerce & products.","art_url":"/assets/world/r2-chrome-blob-one.jpg","type":"game","template":"bead-wash","planet_template":null,"planet_accent":null,"created_at":"2026-07-15T22:12:57.732Z","builders":11,"tags":["art-pipeline","character-anim","demo","game"],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":["https://avatars.githubusercontent.com/u/283020391?v=4","https://avatars.githubusercontent.com/u/64553469?v=4","https://avatars.githubusercontent.com/u/290184380?v=4","https://avatars.githubusercontent.com/u/295990394?v=4","https://avatars.githubusercontent.com/u/35337514?v=4"],"shipped_30d":0,"shipped_30d_capped":false,"hall_origin":"https://builders.amazonprimea.com"},{"origin":"https://demo.cloudbongos.com","name":"Demo","tagline":"A live demo instance on Cloud Bongos.","art_url":null,"type":null,"template":"pair-gold-wash","planet_template":"pair-obsidian-tint","planet_accent":"sea","created_at":"2026-07-14T19:16:38.160Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null},{"origin":"https://emersonian-circles.cloudbongos.com","name":"Emersonian Circles","tagline":"An independent project built on Cloud Bongos.","art_url":null,"type":null,"template":null,"planet_template":null,"planet_accent":null,"created_at":"2026-07-25T02:21:55.641Z","builders":1,"tags":[],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":[],"shipped_30d":30,"shipped_30d_capped":true,"hall_origin":null},{"origin":"https://tidewater.cloudbongos.com","name":"Tidewater","tagline":"A project that has declared nothing yet.","art_url":null,"type":null,"template":null,"planet_template":null,"planet_accent":null,"created_at":"2026-07-20T08:00:00.000Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null}]}
1
+ {"_":"The hub's catalog (task 1003345): the landing's featured.json plus Tidewater, an all-null row nobody owns — the card-floor state opens ITS card, because Demo is owned by the signed-in stub (provisioning-instances.json) and an owned orb's plain click opens the manage view, never the card. Served under the name \"featured\" by projects.states.json's stub.fixtures alias; the landing keeps featured.json.","projects":[{"origin":"https://amazonprimea.com","name":"Off the Boats","tagline":"A singular shop, and a broader videogame in a persistent ancient Greek world that collides with modern day commerce & products.","art_url":"/assets/world/r2-chrome-blob-one.jpg","type":"game","template":"bead-wash","planet_template":null,"planet_accent":null,"card_backdrop":"nebula","created_at":"2026-07-15T22:12:57.732Z","builders":11,"tags":["art-pipeline","character-anim","demo","game"],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":["https://avatars.githubusercontent.com/u/283020391?v=4","https://avatars.githubusercontent.com/u/64553469?v=4","https://avatars.githubusercontent.com/u/290184380?v=4","https://avatars.githubusercontent.com/u/295990394?v=4","https://avatars.githubusercontent.com/u/35337514?v=4"],"shipped_30d":0,"shipped_30d_capped":false,"hall_origin":"https://builders.amazonprimea.com"},{"origin":"https://demo.cloudbongos.com","name":"Demo","tagline":"A live demo instance on Cloud Bongos.","art_url":null,"type":null,"template":"pair-gold-wash","planet_template":"pair-obsidian-tint","planet_accent":"sea","created_at":"2026-07-14T19:16:38.160Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null},{"origin":"https://emersonian-circles.cloudbongos.com","name":"Emersonian Circles","tagline":"An independent project built on Cloud Bongos.","art_url":null,"type":null,"template":null,"planet_template":null,"planet_accent":null,"created_at":"2026-07-25T02:21:55.641Z","builders":1,"tags":[],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":[],"shipped_30d":30,"shipped_30d_capped":true,"hall_origin":null},{"origin":"https://tidewater.cloudbongos.com","name":"Tidewater","tagline":"A project that has declared nothing yet.","art_url":null,"type":null,"template":null,"planet_template":null,"planet_accent":null,"created_at":"2026-07-20T08:00:00.000Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null}]}
@@ -137,15 +137,40 @@ const ASKED = new Map();
137
137
  // the run — the page repaints from the row it re-reads, so the row must carry the choice; a
138
138
  // PATCH with only planet keys gets the route's no-restart receipt, a policy key the push
139
139
  const PLANETS = new Map();
140
- const PLANET_KEYS = ['planet_template', 'planet_accent'];
141
- const NO_PLANET = { planet_template: null, planet_accent: null };
140
+ const PLANET_KEYS = ['planet_template', 'planet_accent', 'card_backdrop'];
141
+ const NO_PLANET = { planet_template: null, planet_accent: null, card_backdrop: null };
142
142
  function readBody(req, cb) {
143
143
  let d = '';
144
144
  req.on('data', (c) => { d += c; });
145
145
  req.on('end', () => { let b = {}; try { b = JSON.parse(d || '{}'); } catch { b = {}; } cb(b); });
146
146
  }
147
+ // The real route validates STRICTLY against its vocabularies (an undeclared key
148
+ // or an off-vocabulary value is a 400). The stub mirrored none of that, so the
149
+ // probes stayed green on a body the server would have refused — which is how a
150
+ // missing card_backdrop entry in the route's schema shipped past a full probe
151
+ // run (task 1003352, grade round 2). The lists are copied on purpose: a kit
152
+ // module may not import core. They are small, closed, and pinned by a test.
153
+ const STUB_VOCAB = {
154
+ platform_visibility: ['public', 'gated'],
155
+ joinability: ['open', 'apply', 'invite_only'],
156
+ planet_template: null, // the template set is long; shape is checked, not membership
157
+ planet_accent: ['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent'],
158
+ card_backdrop: ['nebula', 'dust', 'deepfield', 'chrome'],
159
+ };
160
+ function settingsRejection(body) {
161
+ for (const [key, value] of Object.entries(body)) {
162
+ if (!(key in STUB_VOCAB)) return `unknown field: ${key}`;
163
+ if (value === null) continue; // null hands a pick back
164
+ if (typeof value !== 'string') return `${key} must be a string`;
165
+ const allowed = STUB_VOCAB[key];
166
+ if (allowed && !allowed.includes(value)) return `${key} is not one of: ${allowed.join(', ')}`;
167
+ }
168
+ return null;
169
+ }
147
170
  function settingsPatch(res, id, body) {
148
- if (PLANET_KEYS.some((k) => k in body)) PLANETS.set(String(id), { planet_template: body.planet_template || null, planet_accent: body.planet_accent || null });
171
+ const bad = settingsRejection(body);
172
+ if (bad) return json(res, 400, { error: { code: 'validation_failed', message: bad } });
173
+ if (PLANET_KEYS.some((k) => k in body)) PLANETS.set(String(id), Object.fromEntries(PLANET_KEYS.map((k) => [k, body[k] || null])));
149
174
  const policy = ['platform_visibility', 'joinability'].some((k) => k in body);
150
175
  const push = policy ? FLOW_RECEIPTS.settings.push
151
176
  : { queued: false, restart: false, message: 'Saved. The sky, your list and the card draw it this way from here; nothing on your project restarts for it.' };
@@ -154,7 +179,7 @@ function settingsPatch(res, id, body) {
154
179
  // the row carries its planet: a saved pick for the run, else the fixture's own (or null)
155
180
  function withPlanet(inst) {
156
181
  const saved = PLANETS.get(String(inst.id));
157
- return { ...inst, planet_template: inst.planet_template || null, planet_accent: inst.planet_accent || null, ...(saved || {}) };
182
+ return { ...inst, ...Object.fromEntries(PLANET_KEYS.map((k) => [k, inst[k] || null])), ...(saved || {}) };
158
183
  }
159
184
  function flowReceipt(rest, method) {
160
185
  if (method === 'POST' && rest === '/provisioning/instances') return FLOW_RECEIPTS.provision;
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.441",
3
+ "version": "1.19.442",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.441",
9
+ "version": "1.19.442",
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.441",
3
+ "version": "1.19.442",
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",
@@ -1377,6 +1377,14 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
1377
1377
  #view-manage .pickPreview{display:grid;gap:12px;justify-items:center;padding-top:6px;}
1378
1378
  #view-manage .pickWords{margin:0;font-size:14px;font-weight:400;line-height:1.45;color:var(--ink);text-align:center;max-width:168px;}
1379
1379
  #view-manage .pickWords small{display:block;font-size:12.5px;color:var(--ink-soft);}
1380
+ /* the card's backdrop preview (task 1003352): the poster's own 5:2 band at the
1381
+ width the planet preview stands at, so the two sections read as one column.
1382
+ No ground of its own — "its own picture" is an empty hairline frame, which is
1383
+ the honest drawing of a project that may have no art at all. */
1384
+ #view-manage .cardBand{display:block;width:168px;aspect-ratio:5/2;background-size:cover;
1385
+ background-position:center;background-repeat:no-repeat;background-color:var(--bg);
1386
+ border:1px solid var(--rule-soft);}
1387
+ #view-manage .cardBand[data-own]{border-style:dashed;}
1380
1388
  #view-manage .pickBands{display:grid;border-bottom:1px solid var(--rule);}
1381
1389
  #view-manage .pickBand{display:grid;grid-template-columns:88px 1fr;gap:0 18px;align-items:center;border-top:1px solid var(--rule);}
1382
1390
  #view-manage .pickLabel{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);padding:14px 0;}
@@ -0,0 +1,14 @@
1
+ {
2
+ "kind": "card-band",
3
+ "model": "soul_location (Higgsfield) — nano_banana_pro was asked first; its queue had not started a job after 25 minutes, so the round moved to the model that answered",
4
+ "job_id": "a9e4cb59-7436-4f8f-9289-aba9b1fd567e",
5
+ "aspect_ratio": "5:2",
6
+ "source_aspect_ratio": "21:9",
7
+ "generated": "2026-08-30T17:09Z",
8
+ "round": 4,
9
+ "task": 1003352,
10
+ "prompt": "Ultra-wide studio photograph on a pure black ground: a single sweeping ribbon of liquid chrome, polished mirror metal with soft neutral specular highlights and a faint cool blue and warm bronze reflection, curving gently through the centre-right of the frame and fading into black at both ends. Abstract material study, not an object, no recognisable shape, no product. The left third and both lower corners stay almost pure black and empty. No text, no watermark, no logo, no border, no reflection of a studio or a person. Pure black dominates the frame.",
11
+ "source": ".impeccable/mocks/world-assets/r4-card-chrome-src.jpg",
12
+ "encoded": "ffmpeg crop=2560:1024:0:24 (21:9 → 5:2, centred), JPEG -q:v 4",
13
+ "use": "the poster card's backdrop on /projects, chosen by an owner as card_backdrop=\"chrome\": a sweeping ribbon of liquid chrome — the world's own material with no object in it. Dark only (ADR 0204); composed so the left third and the lower corners stay near-black, where the card puts its disc, its name and its two figure callouts."
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "kind": "card-band",
3
+ "model": "soul_location (Higgsfield) — nano_banana_pro was asked first; its queue had not started a job after 25 minutes, so the round moved to the model that answered",
4
+ "job_id": "debaaba3-6094-4670-9b08-c578fc6e8bb9",
5
+ "aspect_ratio": "5:2",
6
+ "source_aspect_ratio": "21:9",
7
+ "generated": "2026-08-30T17:09Z",
8
+ "round": 4,
9
+ "task": 1003352,
10
+ "prompt": "Ultra-wide deep-field astronomical photograph on a pure black ground: many very small, very distant galaxies and faint stars scattered sparsely across an almost entirely black frame, a few with the faintest cool blue or warm amber tint, no bright object anywhere, extremely quiet and even. No planets, no moons, no large nebula, no ground plane, no horizon, no lens flare, no text, no watermark, no logo, no border. Pure black dominates the frame.",
11
+ "source": ".impeccable/mocks/world-assets/r4-card-deepfield-src.jpg",
12
+ "encoded": "ffmpeg crop=2560:1024:0:24 (21:9 → 5:2, centred), JPEG -q:v 4",
13
+ "use": "the poster card's backdrop on /projects, chosen by an owner as card_backdrop=\"deepfield\": a deep field of very distant galaxies on near-black; the quietest of the four. Dark only (ADR 0204); composed so the left third and the lower corners stay near-black, where the card puts its disc, its name and its two figure callouts."
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "kind": "card-band",
3
+ "model": "soul_location (Higgsfield) — nano_banana_pro was asked first; its queue had not started a job after 25 minutes, so the round moved to the model that answered",
4
+ "job_id": "7ec35328-3943-4765-8dd7-8392fb8c48c3",
5
+ "aspect_ratio": "5:2",
6
+ "source_aspect_ratio": "21:9",
7
+ "generated": "2026-08-30T17:09Z",
8
+ "round": 4,
9
+ "task": 1003352,
10
+ "prompt": "Ultra-wide cinematic deep-space photograph on a pure black ground: soft interstellar dust lanes in warm bronze and dark amber sweeping diagonally across the centre-right, very low contrast, a haze rather than a shape, a scatter of tiny faint stars. The left third and both lower corners stay almost pure black and empty. No planets, no moons, no nebula pillars, no ground plane, no horizon, no lens flare, no text, no watermark, no logo, no border. Pure black dominates the frame.",
11
+ "source": ".impeccable/mocks/world-assets/r4-card-dust-src.jpg",
12
+ "encoded": "ffmpeg crop=2560:1024:0:24 (21:9 → 5:2, centred), eq=gamma=1.20, JPEG -q:v 4",
13
+ "use": "the poster card's backdrop on /projects, chosen by an owner as card_backdrop=\"dust\": warm bronze dust lanes, very low contrast; a haze rather than a shape. Dark only (ADR 0204); composed so the left third and the lower corners stay near-black, where the card puts its disc, its name and its two figure callouts."
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "kind": "card-band",
3
+ "model": "soul_location (Higgsfield) — nano_banana_pro was asked first; its queue had not started a job after 25 minutes, so the round moved to the model that answered",
4
+ "job_id": "35ad2c18-aaa3-413c-9fd8-958e5017d77c",
5
+ "aspect_ratio": "5:2",
6
+ "source_aspect_ratio": "21:9",
7
+ "generated": "2026-08-30T17:09Z",
8
+ "round": 4,
9
+ "task": 1003352,
10
+ "prompt": "Ultra-wide cinematic deep-space photograph on a pure black ground: a faint nebula wash in muted violet and deep blue with a soft bronze warmth drifting through the centre-right of the frame, very sparse pin-sharp stars of varied brightness, gentle depth. The left third and both lower corners stay almost pure black and empty. No planets, no moons, no ground plane, no horizon, no lens flare, no text, no watermark, no logo, no border. Pure black dominates the frame.",
11
+ "source": ".impeccable/mocks/world-assets/r4-card-nebula-src.jpg",
12
+ "encoded": "ffmpeg crop=2560:1024:0:24 (21:9 → 5:2, centred), eq=gamma=1.75:saturation=1.30, JPEG -q:v 4 — the render was too dark to read at the card's 560px; a gamma lift raises the wash without crushing the black the disc and the name sit on (contrast would have)",
13
+ "use": "the poster card's backdrop on /projects, chosen by an owner as card_backdrop=\"nebula\": a violet-and-bronze nebula wash, sparse stars — the field's own language in the card's shape. Dark only (ADR 0204); composed so the left third and the lower corners stay near-black, where the card puts its disc, its name and its two figure callouts."
14
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "_": "The all-null floor card (task 1003301) on the hub's own catalog (task 1003345): the same page, the same stub, the catalog aliased to featured-hub.json — the landing's three plus Tidewater, a null row nobody owns. Demo, the landing catalog's floor row, is owned by the signed-in stub, and an owned orb's plain click opens its manage view, never its card. Rendered by name: node modules/ui-design/kit/render.js --states public-landing/projects-floor.states.json",
2
+ "_": "The all-null floor card (task 1003301) on the hub's own catalog (task 1003345): the same page, the same stub, the catalog aliased to featured-hub.json — the landing's three plus Tidewater, a null row nobody owns. Demo, the landing catalog's floor row, is owned by the signed-in stub, and an owned orb's plain click opens its manage view, never its card. Since task 1003352 it also carries the card BACKDROP state: featured-hub gives Off the Boats a card_backdrop, so the owner's plate can be seen standing in front of the project's own picture. Rendered by name: node modules/ui-design/kit/render.js --states public-landing/projects-floor.states.json",
3
3
  "page": "/projects",
4
4
  "surface": "public-landing",
5
5
  "modes": [
@@ -58,6 +58,33 @@
58
58
  "#cardListed"
59
59
  ]
60
60
  }
61
+ },
62
+ "card-backdrop": {
63
+ "auth": true,
64
+ "actions": [
65
+ [
66
+ "dispatchClick",
67
+ "#orbField a.worldOrb[data-origin=\"https://amazonprimea.com\"]"
68
+ ],
69
+ [
70
+ "wait",
71
+ 600
72
+ ]
73
+ ],
74
+ "expect": {
75
+ "dialog": true,
76
+ "visible": [
77
+ "#cardArt",
78
+ "#cardOpen"
79
+ ],
80
+ "attr": [
81
+ [
82
+ "#cardArt",
83
+ "src",
84
+ "/assets/world/r4-card-nebula.jpg"
85
+ ]
86
+ ]
87
+ }
61
88
  }
62
89
  }
63
90
  }
@@ -769,6 +769,18 @@ summary{min-height:24px;padding:3px 0;}
769
769
  </details>
770
770
  </div>
771
771
 
772
+ <!-- its card (task 1003352): the poster card's backdrop, a plate the
773
+ platform drew. Beside the planet, on the same settings channel, and
774
+ like it: nothing is pushed to the project and nothing restarts. -->
775
+ <div class="card mSec" id="mCard">
776
+ <h2>Its card</h2>
777
+ <div id="mCardPick"><p class="finePrint">Reading the project…</p></div>
778
+ <details class="finePrint" id="mCardWhat">
779
+ <summary>What the choice changes</summary>
780
+ <p>The backdrop behind this project’s name on its card — the panel that opens when someone picks it out of the map. Four plates the platform drew, or the project’s own picture if you have given it one. Saving here changes nothing on the project itself: no restart, nothing to apply.</p>
781
+ </details>
782
+ </div>
783
+
772
784
  <!-- live since task 1003042: the teardown control, for a project in ANY
773
785
  state that still holds something. The server used to refuse a
774
786
  'requested' or an 'error' project outright — and force-teardown reused
@@ -1820,7 +1832,7 @@ summary{min-height:24px;padding:3px 0;}
1820
1832
  var href = p.isSelf ? '/' : safeHttpUrl(p.origin);
1821
1833
  /* the art: catalog text an Archon typed, scheme-checked server-side and
1822
1834
  again here before it becomes a src */
1823
- cardArt(p.isSelf ? '' : safeHttpUrl(p.art_url));
1835
+ cardArt(p.isSelf ? '' : (backdropPlate(p.card_backdrop) || safeHttpUrl(p.art_url)));
1824
1836
  $('cardDot').style.setProperty('--acc', p.isSelf ? 'var(--accent)' : accentOf(p));
1825
1837
  $('cardName').textContent = p.name || '';
1826
1838
  $('cardTag').textContent = p.tagline || '';
@@ -4851,6 +4863,7 @@ summary{min-height:24px;padding:3px 0;}
4851
4863
  /* its planet (task 1003347) reads off the row — fetch-free too; a pick in
4852
4864
  progress survives the tick because only a different project re-renders it */
4853
4865
  renderPick(inst);
4866
+ renderCardPick(inst);
4854
4867
  /* the page names its project — the heading holds #mName; and in the tab */
4855
4868
  document.title = (inst.slug ? inst.slug + ' — ' : '') + BASE_TITLE;
4856
4869
  /* the card's own story, on the project's own page (task 1003232): the
@@ -5432,6 +5445,20 @@ summary{min-height:24px;padding:3px 0;}
5432
5445
  var PICK_STYLES = [['tint', 'tinted'], ['wash', 'washed'], ['plain', 'plain']];
5433
5446
  var PICK_ACCENTS = [['sea', 'sea'], ['terracotta', 'terracotta'], ['gold-soft', 'soft gold'], ['sun-lit', 'sunlit'], ['accent', 'the accent']];
5434
5447
  var PICK_FIELDS = { mPickShape: 'shape', mPickMaterial: 'material', mPickStyle: 'style', mPickAccent: 'accent' };
5448
+ /* ── the card's backdrop (task 1003352): the same KIND of key as the planet —
5449
+ a closed list of platform-drawn plates, saved on the same settings channel,
5450
+ pushed to no instance and restarting nothing. '' is "its own picture": the
5451
+ card falls back to the project's art_url, which is all it read before. The
5452
+ plates are the world's own material (DESIGN.md, round 4), dark only. ── */
5453
+ var CARD_BACKDROPS = [['', 'its own picture'], ['nebula', 'nebula'], ['dust', 'dust'], ['deepfield', 'deep field'], ['chrome', 'chrome']];
5454
+ var CARD_PLATES = { nebula: 'r4-card-nebula.jpg', dust: 'r4-card-dust.jpg', deepfield: 'r4-card-deepfield.jpg', chrome: 'r4-card-chrome.jpg' };
5455
+ var CARD_WORDS = { nebula: 'a violet and bronze nebula wash', dust: 'warm bronze dust lanes',
5456
+ deepfield: 'a deep field of distant galaxies', chrome: 'a sweep of liquid chrome' };
5457
+ /* a key → the plate's path, or '' for anything this platform does not draw
5458
+ (an unknown key from an older row must never become a broken src) */
5459
+ function backdropPlate(key) {
5460
+ return CARD_PLATES[key] ? '/assets/world/' + CARD_PLATES[key] : '';
5461
+ }
5435
5462
  var mPick = null; /* the pending choice: { shape, material, style, accent } */
5436
5463
  var mPickFor = null; /* which project it belongs to — a different one starts afresh */
5437
5464
  var mPickTouched = false; /* the owner changed something since the seed — a late catalog must not undo it */
@@ -5540,6 +5567,79 @@ summary{min-height:24px;padding:3px 0;}
5540
5567
  });
5541
5568
  }
5542
5569
 
5570
+ /* ── its card (task 1003352; the owner's ask on the planet round: "this could
5571
+ also house project card backdrop customization"). Its own section BESIDE the
5572
+ planet rather than inside it, so each heading stays true — one is how the
5573
+ project is drawn everywhere, the other is one surface's backdrop. Same
5574
+ mechanics as the planet, key for key: reads off the row, saves on the
5575
+ settings PATCH, and the reply is a receipt the card repaints from. ── */
5576
+ var mCard = null; /* the pending choice: the backdrop key, or '' */
5577
+ var mCardFor = null; /* which project it belongs to */
5578
+ function renderCardPick(inst) {
5579
+ if (mCard === null || mCardFor !== inst.id) { mCard = inst.card_backdrop || ''; mCardFor = inst.id; }
5580
+ var plate = backdropPlate(mCard);
5581
+ $('mCardPick').innerHTML =
5582
+ '<div class="pick">' +
5583
+ '<div class="pickPreview"><span class="cardBand"' +
5584
+ (plate ? ' style="background-image:url(' + esc(plate) + ')"' : ' data-own') + ' aria-hidden="true"></span>' +
5585
+ /* the line speaks for the ROW, so a pick that is ahead of the row says
5586
+ so — the preview above it is already showing the plate */
5587
+ '<p class="pickWords">' + esc(CARD_WORDS[mCard] || 'the project’s own picture, when it has one') +
5588
+ '<small>' + (mCard !== (inst.card_backdrop || '') ? 'not saved yet'
5589
+ : inst.card_backdrop ? 'your choice' : 'nothing chosen') + '</small></p></div>' +
5590
+ '<div class="pickBands">' + pickBand('Backdrop', 'mCardBackdrop', CARD_BACKDROPS, mCard) + '</div>' +
5591
+ '</div>' +
5592
+ '<div class="actions"><button type="button" class="btn small" data-card-save>Save its card</button></div>' +
5593
+ '<div class="projMsg" id="mCardMsg" role="status" aria-live="polite"></div>';
5594
+ }
5595
+ $('mCardPick').addEventListener('change', function (ev) {
5596
+ var t = ev.target;
5597
+ if (!t || t.type !== 'radio' || t.name !== 'mCardBackdrop' || !manageInst) return;
5598
+ mCard = t.value;
5599
+ renderCardPick(manageInst); /* the preview follows the pick, keeping the cell's focus */
5600
+ var again = document.querySelector('#mCardPick input[value="' + t.value + '"]');
5601
+ if (again) again.focus({ preventScroll: true });
5602
+ });
5603
+ $('mCardPick').addEventListener('click', function (ev) {
5604
+ var t = ev.target.closest && ev.target.closest('button');
5605
+ if (t && t.hasAttribute('data-card-save')) doCardSave();
5606
+ });
5607
+ function doCardSave() {
5608
+ if (!manageInst || mCard === null) return;
5609
+ var box = $('mCardMsg');
5610
+ document.querySelectorAll('#mCardPick .actions button').forEach(function (b) { b.disabled = true; });
5611
+ box.className = 'projMsg'; box.textContent = 'Saving…';
5612
+ fetch(API + '/provisioning/instances/' + encodeURIComponent(manageId) + '/settings', {
5613
+ method: 'PATCH', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
5614
+ body: JSON.stringify({ card_backdrop: mCard || null })
5615
+ }).then(function (res) {
5616
+ return jsonOrEmpty(res).then(function (data) {
5617
+ if (res.ok) {
5618
+ /* the row now holds the choice; the card reads it from there */
5619
+ manageInst.card_backdrop = (data.planet || {}).card_backdrop || null;
5620
+ var row = byId[manageInst.id];
5621
+ if (row) row.card_backdrop = manageInst.card_backdrop;
5622
+ mCard = null;
5623
+ renderCardPick(manageInst);
5624
+ var done = $('mCardMsg');
5625
+ done.className = 'projMsg ok';
5626
+ done.textContent = (data.push && data.push.message) || 'Saved.';
5627
+ loadCatalog();
5628
+ return;
5629
+ }
5630
+ var e = apiError(data, res.status);
5631
+ if (res.status === 401) { box.className = 'projMsg err'; box.innerHTML = signInAgain(); return; }
5632
+ box.className = 'projMsg err';
5633
+ box.textContent = e.message || 'The choice could not be saved — try again in a moment.';
5634
+ });
5635
+ }).catch(function () {
5636
+ box.className = 'projMsg err';
5637
+ box.textContent = 'Couldn’t reach the platform — check your connection and try again.';
5638
+ }).then(function () {
5639
+ document.querySelectorAll('#mCardPick .actions button').forEach(function (b) { b.disabled = false; });
5640
+ });
5641
+ }
5642
+
5543
5643
  /* ── how people join (task 1003044): the joinability toggle — the visibility
5544
5644
  section's shape, key for key: the CHOICE from the platform's settings row,
5545
5645
  what the project RUNS from its own manifest (brand.project.joinability),
@@ -131,6 +131,12 @@
131
131
  { "name": "manage flows: try again from the snagged project's own page re-files it too", "auth": true, "width": 1440, "url": "/projects?view=manage&id=103",
132
132
  "steps": [["wait",900],["click","[data-m-retry]"],["wait",900]],
133
133
  "expect": {"text":[["#mMsg","Queued. The platform picks it up from here."]],"hasClass":[["#mMsg","ok",true]],"hidden":["#mAddress"]} },
134
+ { "name": "manage flows: its card (task 1003352) — pick the nebula plate; save; the receipt lands and the preview takes the plate", "auth": true, "width": 1440, "url": "/projects?view=manage&id=101",
135
+ "steps": [["wait",900],["check","#mCardPick input[name=\"mCardBackdrop\"][value=\"nebula\"]"],["wait",150],["click","[data-card-save]"],["wait",700]],
136
+ "expect": {"text":[["#mCardMsg","Saved. The sky, your list and the card draw it this way from here; nothing on your project restarts for it."]],"hasClass":[["#mCardMsg","ok",true]],"count":[["#mCardPick .cardBand[data-own]",0]]} },
137
+ { "name": "manage flows: its card — handing it back to the platform restores the project's own picture", "continue": true,
138
+ "steps": [["check","#mCardPick input[name=\"mCardBackdrop\"][value=\"\"]"],["wait",150],["click","[data-card-save]"],["wait",700]],
139
+ "expect": {"hasClass":[["#mCardMsg","ok",true]],"count":[["#mCardPick .cardBand[data-own]",1]]} },
134
140
  { "name": "manage flows: how people join — pick a cell, save, the receipt lands under the choice", "auth": true, "width": 1440, "url": "/projects?view=manage&id=101",
135
141
  "steps": [["wait",900],["check","input[name=\"mJoinPick\"][value=\"apply\"]"],["click","[data-join-save]"],["wait",600]],
136
142
  "expect": {"text":[["#mJoinMsg","Saved. The project restarts to apply it."],["#mName","demo"]],"checked":"input[name=\"mJoinPick\"][value=\"apply\"]","visible":["#mOrb .worldOrb","#mFacts .ledger .k"]} },
@@ -21,6 +21,7 @@
21
21
  "in-manage": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mMain", "#mChip .chip", "#mOrb .worldOrb", "#mFacts .ledger .k", "#mActs", "#mAddress"], "hidden": ["#qWrap", "#mGate", "#mMissing", "#mErr"], "text": [["#mName", "demo"], ["#mTitle", "demo"]] } },
22
22
  "in-manage-address": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mAddress", "#mAddress h2", "#mDom .plain", "#mDomName", "#mDom [data-m-attach]", "#mDomWhat summary"], "count": [["#mDom .attach input[type=text]", 1], ["#mDom .attach button", 1]] } },
23
23
  "in-manage-planet": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mPlanetPick", "#mPick .pick .pickPreview .worldOrb .disc", "#mPick [data-pick-save]", "#mPick [data-pick-reset]", "#mPickWhat summary"], "count": [["#mPick .pickCells[role=\"radiogroup\"]", 3], ["#mPick .pickCell", 10], ["#mPick .pickCell.on", 3], ["#mPick .pickSw", 5], ["#mPick .pickSw.on", 1]], "attr": [["#mPick .pickPreview .worldOrb", "data-template", "pair-obsidian-tint"], ["#mOrb .worldOrb", "data-template", "pair-obsidian-tint"]], "checked": "#mPick input[name=\"mPickAccent\"][value=\"sea\"]", "text": [["#mPlanet .v small", "your choice."]] } },
24
+ "in-manage-card": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mCard", "#mCardPick .pick .pickPreview .cardBand", "#mCardPick [data-card-save]", "#mCardWhat summary"], "count": [["#mCardPick .pickCells[role=\"radiogroup\"]", 1], ["#mCardPick .pickCell", 5], ["#mCardPick .pickCell.on", 1]], "checked": "#mCardPick input[name=\"mCardBackdrop\"][value=\"\"]", "attr": [["#mCardPick .cardBand", "data-own", ""]] } },
24
25
  "in-manage-snag": { "auth": true, "url": "/projects?view=manage&id=103", "expect": { "visible": ["#mMain", "#mErr", "#mActs"], "hidden": ["#mAddress"], "text": [["#mName", "tidepool"]] } },
25
26
  "in-manage-gone": { "auth": true, "url": "/projects?view=manage&id=104", "expect": { "visible": ["#mMain", "#mActs"], "hidden": ["#mErr", "#mAddress"], "text": [["#mName", "old-lantern"]] } },
26
27
  "in-manage-missing": { "auth": true, "url": "/projects?view=manage&id=999", "expect": { "visible": ["#mMissing"], "hidden": ["#mMain", "#mGate"] } },
package/src/module-api.js CHANGED
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
33
33
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
34
34
  // the entry to that file. Look for a version's history there, not here.
35
35
  // ---------------------------------------------------------------------------
36
- const CORE_VERSION = '1.19.441'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
36
+ const CORE_VERSION = '1.19.442'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
37
37
 
38
38
  // A namespaced logger so a module's log lines are attributable + consistent.
39
39
  // Usage: const log = api.logger('dev-box'); log.info('mounted');