@bongos/core 1.19.438 → 1.19.440

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "_": "The hub's interaction contract for modules/ui-design/kit/probe.js (task 1003320): the disclosure bands, the views, the card, the fold under 480, signed out. A probe with `continue` keeps the previous page — the contract is a sequence. The manage flows (task 1003345): attach, restart, teardown (the row and the ender), revive, retry and join drive the stub's receipts end to end, so the makeover cannot silently break a door. The catalog is featured-hub.json (stub.fixtures): the signed-in stub owns Demo, and an owned orb's plain click opens its manage view, never its card — the card probes open the landing's other rows and Tidewater, the un-owned floor row.",
2
+ "_": "The hub's interaction contract for modules/ui-design/kit/probe.js (task 1003320): the disclosure bands, the views, the card, the fold under 480, signed out. A probe with `continue` keeps the previous page — the contract is a sequence. The manage flows: since task 1003355 a row carries only its two doors, so attach, restart, revive, retry, teardown (the ender), join and the planet all drive the stub's receipts from the project's OWN page — and one probe holds the row itself to two doors and nothing that acts. The catalog is featured-hub.json (stub.fixtures): the signed-in stub owns Demo, and an owned orb's plain click opens its manage view, never its card — the card probes open the landing's other rows and Tidewater, the un-owned floor row.",
3
3
  "page": "/projects",
4
4
  "surface": "public-landing",
5
5
  "stub": { "fixtures": { "featured": "featured-hub" } },
@@ -108,24 +108,21 @@
108
108
  { "name": "390: a solo Shift tap opens the fold and focuses the field, without overflow", "continue": true,
109
109
  "steps": [["press", "Escape"], ["wait", 100], ["press", "Shift"], ["wait", 150]],
110
110
  "expect": { "hasClass": [["#qWrap", "is-open", true]], "activeId": "mapQ", "noOverflow": true } },
111
- { "name": "manage flows (task 1003345): the address fold on a live row records a domain", "auth": true, "width": 1440, "url": "/projects?view=mine",
112
- "steps": [["wait",600],["click","li[data-inst=\"101\"] details.attachFold summary"],["fill","#dom-101","demo.example.com"],["click","[data-attach=\"101\"]"],["wait",600]],
113
- "expect": {"text":[["#msg-101","Address recorded."]],"hasClass":[["#msg-101","ok",true]]} },
114
- { "name": "manage flows: restart on an unreachable row asks; the row re-reads the project and shows the queued rail, the door leaves while the intent is open", "auth": true, "width": 1440, "url": "/projects?view=mine",
115
- "steps": [["wait",600],["click","[data-restart=\"105\"]"],["wait",900]],
116
- "expect": {"text":[["li[data-inst=\"105\"] .strip .pnote","A restart is queued — this card updates when it starts."],["li[data-inst=\"105\"] .strip .stages li.now .slbl","Restart queued"]],"visible":["li[data-inst=\"105\"] .strip","li[data-inst=\"105\"] .worldOrb.ashen"],"hidden":["[data-restart=\"105\"]"]} },
117
- { "name": "manage flows: tear it down asks twice on a row — the truth first, then the door", "auth": true, "width": 1440, "url": "/projects?view=mine",
118
- "steps": [["wait",600],["click","[data-teardown=\"101\"]"],["wait",200]],
119
- "expect": {"visible":["li[data-inst=\"101\"] .confirmRow","[data-teardown-yes=\"101\"]","[data-teardown-no=\"101\"]"],"hidden":["[data-teardown=\"101\"]"]} },
120
- { "name": "manage flows: the second press queues the teardown; the row re-reads the project and shows the wind-down rail, the door leaves", "continue": true,
121
- "steps": [["click","[data-teardown-yes=\"101\"]"],["wait",900]],
122
- "expect": {"text":[["li[data-inst=\"101\"] .strip .pnote","A teardown is queued — this card updates when it starts."]],"visible":["li[data-inst=\"101\"] .strip.flowDown"],"hidden":["[data-teardown=\"101\"]","[data-teardown-yes=\"101\"]"]} },
123
- { "name": "manage flows: bring it back on a torn-down row re-files the project", "auth": true, "width": 1440, "url": "/projects?view=mine",
124
- "steps": [["wait",600],["click","[data-revive=\"104\"]"],["wait",600]],
125
- "expect": {"text":[["#msg-104","Queued. The platform picks it up from here."]],"hasClass":[["#msg-104","ok",true]],"visible":["li[data-inst=\"104\"] .worldOrb.ghost"]} },
126
- { "name": "manage flows: try again on a snagged row re-files it too", "auth": true, "width": 1440, "url": "/projects?view=mine",
127
- "steps": [["wait",600],["click","[data-retry=\"103\"]"],["wait",600]],
128
- "expect": {"text":[["#msg-103","Queued. The platform picks it up from here."]],"hasClass":[["#msg-103","ok",true]]} },
111
+ { "name": "the row is two doors and nothing that acts (task 1003355): view where a project is live and addressed, manage on every row, no button anywhere in the list", "auth": true, "width": 1440, "url": "/projects?view=mine",
112
+ "steps": [["wait",600]],
113
+ "expect": {"count":[["#projectsList .proj .actions a",7],["#projectsList .proj .actions a[data-manage]",5],["#projectsList .proj .actions button",0],["li[data-inst=\"104\"] .actions a",1]],"text":[["li[data-inst=\"104\"] .actions","Manage"]]} },
114
+ { "name": "manage flows (task 1003355): the address control on the project's own page records a domain", "auth": true, "width": 1440, "url": "/projects?view=manage&id=101",
115
+ "steps": [["wait",900],["fill","#mDomName","demo.example.com"],["click","[data-m-attach]"],["wait",900]],
116
+ "expect": {"text":[["#mDomMsg","Address recorded."],["#mName","demo"]],"hasClass":[["#mDomMsg","ok",true]],"visible":["#mAddress","#mDomWhat summary"]} },
117
+ { "name": "manage flows: restart from the unreachable project's own page; the page re-reads it, the rail says queued and the door leaves while the intent is open", "auth": true, "width": 1440, "url": "/projects?view=manage&id=105",
118
+ "steps": [["wait",900],["click","[data-m-restart]"],["wait",900]],
119
+ "expect": {"text":[["#mMsg","Restart queued."],["#mStrip .strip .stages li.now .slbl","Restart queued"]],"hasClass":[["#mMsg","ok",true]],"visible":["#mStrip .strip","#mOrb .worldOrb.ashen"],"hidden":["[data-m-restart]"]} },
120
+ { "name": "manage flows: bring it back from the torn-down project's own page re-files it", "auth": true, "width": 1440, "url": "/projects?view=manage&id=104",
121
+ "steps": [["wait",900],["click","[data-m-revive]"],["wait",900]],
122
+ "expect": {"text":[["#mMsg","Queued. The platform picks it up from here."]],"hasClass":[["#mMsg","ok",true]],"visible":["#mOrb .worldOrb.ghost"],"hidden":["#mAddress"]} },
123
+ { "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",
124
+ "steps": [["wait",900],["click","[data-m-retry]"],["wait",900]],
125
+ "expect": {"text":[["#mMsg","Queued. The platform picks it up from here."]],"hasClass":[["#mMsg","ok",true]],"hidden":["#mAddress"]} },
129
126
  { "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",
130
127
  "steps": [["wait",900],["check","input[name=\"mJoinPick\"][value=\"apply\"]"],["click","[data-join-save]"],["wait",600]],
131
128
  "expect": {"text":[["#mJoinMsg","Saved. The project restarts to apply it."],["#mName","demo"]],"checked":"input[name=\"mJoinPick\"][value=\"apply\"]","visible":["#mOrb .worldOrb","#mFacts .ledger .k"]} },
@@ -16,11 +16,12 @@
16
16
  "in-map-search": { "auth": true, "actions": [["clickIfVisible", "#qToggle"], ["wait", 200], ["fill", "#mapQ", "emer"], ["wait", 300]], "expect": { "value": [["#mapQ", "emer"]] } },
17
17
  "in-map-mine-on": { "auth": true, "actions": [["check", "#onlyMine"], ["wait", 300]], "expect": { "checked": "#onlyMine" } },
18
18
  "in-table": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 250], ["click", "a[data-sub=\"table\"]"], ["wait", 400]], "expect": { "visible": ["#sub-table-body", ".tblMast", ".mastObj", "#selfHall"], "closed": "#menuProjects", "count": [["#mapRows tr[data-generated] .rowHall", 1]] } },
19
- "in-mine": { "auth": true, "url": "/projects?view=mine", "expect": { "hidden": ["#qWrap", "#empty"], "visible": ["#mineBar", "#projectsList", "#projectsList .proj .worldOrb .disc"], "count": [["#projectsList .proj", 5], ["#projectsList .proj.gone", 1], ["#projectsList .proj .worldOrb[data-shape][data-material][data-style]", 5], ["#projectsList .proj .worldOrb[data-template=\"blob-frost-tint\"]", 1], ["#projectsList .proj .strip", 1], ["#projectsList .proj .worldOrb.ashen", 2], ["#projectsList [data-restart]", 1]] } },
20
- "in-manage": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mMain", "#mChip .chip", "#mOrb .worldOrb", "#mFacts .ledger .k", "#mActs"], "hidden": ["#qWrap", "#mGate", "#mMissing", "#mErr"], "text": [["#mName", "demo"], ["#mTitle", "demo"]] } },
19
+ "in-mine": { "auth": true, "url": "/projects?view=mine", "expect": { "hidden": ["#qWrap", "#empty"], "visible": ["#mineBar", "#projectsList", "#projectsList .proj .worldOrb .disc"], "count": [["#projectsList .proj", 5], ["#projectsList .proj.gone", 1], ["#projectsList .proj .worldOrb[data-shape][data-material][data-style]", 5], ["#projectsList .proj .worldOrb[data-template=\"blob-frost-tint\"]", 1], ["#projectsList .proj .strip", 1], ["#projectsList .proj .worldOrb.ashen", 2], ["#projectsList .proj .actions a", 7], ["#projectsList .proj .actions button", 0]] } },
20
+ "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"]] } },
21
+ "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]] } },
21
22
  "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."]] } },
22
- "in-manage-snag": { "auth": true, "url": "/projects?view=manage&id=103", "expect": { "visible": ["#mMain", "#mErr", "#mActs"], "text": [["#mName", "tidepool"]] } },
23
- "in-manage-gone": { "auth": true, "url": "/projects?view=manage&id=104", "expect": { "visible": ["#mMain", "#mActs"], "hidden": ["#mErr"], "text": [["#mName", "old-lantern"]] } },
23
+ "in-manage-snag": { "auth": true, "url": "/projects?view=manage&id=103", "expect": { "visible": ["#mMain", "#mErr", "#mActs"], "hidden": ["#mAddress"], "text": [["#mName", "tidepool"]] } },
24
+ "in-manage-gone": { "auth": true, "url": "/projects?view=manage&id=104", "expect": { "visible": ["#mMain", "#mActs"], "hidden": ["#mErr", "#mAddress"], "text": [["#mName", "old-lantern"]] } },
24
25
  "in-manage-missing": { "auth": true, "url": "/projects?view=manage&id=999", "expect": { "visible": ["#mMissing"], "hidden": ["#mMain", "#mGate"] } },
25
26
  "out-manage": { "auth": false, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mGate"], "hidden": ["#mMain", "#mMissing"] } },
26
27
  "in-new": { "auth": true, "url": "/projects?view=new", "expect": { "hidden": ["#qWrap"] } },
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.438'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
36
+ const CORE_VERSION = '1.19.440'; // 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');
@@ -1161,7 +1161,8 @@ test('the manage chip defers to chipFor — one site, no second story (C6)', ()
1161
1161
  assert.ok(fn, 'renderManage() must be extractable');
1162
1162
  assert.equal((fn.match(/chipFor\(inst\)/g) || []).length, 1,
1163
1163
  'renderManage must have exactly one chip call site');
1164
- assert.match(fn, /addressRow\(inst\)/, 'the address sentence is the shared, verified one');
1164
+ assert.match(fn, /addressRow\(inst, true\)/,
1165
+ 'the address sentence is the shared, verified one — read on THIS surface, so its pointers name the controls this page has (task 1003355)');
1165
1166
  assert.match(fn, /SHAPE_LABEL/, 'the hosting shape speaks the card\'s vocabulary');
1166
1167
  });
1167
1168
 
@@ -1304,12 +1305,11 @@ test('tearing a project down asks for its NAME — the page\'s heaviest control'
1304
1305
  });
1305
1306
 
1306
1307
  test('the delete section mirrors the server gate — every state but torn_down', () => {
1307
- const fn = (SCRIPT.match(/function canTeardown\(inst\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
1308
- assert.ok(fn, 'canTeardown() must be extractable');
1309
- // the old whitelist is what made a snagged project a dead end (N5)
1310
- assert.doesNotMatch(fn, /inst\.status === 'provisioning' \|\| inst\.status === 'active'/,
1311
- 'the two-state whitelist is gone');
1312
- assert.match(fn, /torn_down/, 'torn_down is the only lifecycle exclusion');
1308
+ // the ender is the ONLY teardown door since task 1003355 took the row's away,
1309
+ // so renderDelete IS the gate: it carries each exclusion as its own honest
1310
+ // state rather than deferring to a predicate nothing else reads
1311
+ assert.doesNotMatch(SCRIPT, /function canTeardown\(/,
1312
+ 'the client mirror went with the row button that was its only caller');
1313
1313
  const render = (SCRIPT.match(/function renderDelete\(inst\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
1314
1314
  assert.ok(render, 'renderDelete() must be extractable');
1315
1315
  // keyed by PROJECT, not a bare flag: a boolean would also suppress the repaint
@@ -1320,6 +1320,16 @@ test('the delete section mirrors the server gate — every state but torn_down',
1320
1320
  for (const s of ['torn_down', 'tearing_down']) {
1321
1321
  assert.match(render, new RegExp(s), `${s} gets its own honest state, not the action`);
1322
1322
  }
1323
+ assert.match(render, /open_intent\.action === 'teardown'/, 'and a queued teardown is the third');
1324
+ // the old whitelist is what made a snagged project a dead end (N5): every
1325
+ // other state — 'requested' and 'error' included — still reaches the ask
1326
+ assert.match(render, /teardownTruth\(inst\)[\s\S]*?data-del-ask/,
1327
+ 'a project that never finished standing up still reaches the ask');
1328
+ // the pointer out of a torn-down project must name a door THIS page has
1329
+ assert.doesNotMatch(render, /view=mine/,
1330
+ 'the revive door moved here in task 1003355 — a pointer at the list would be a dead end');
1331
+ assert.match(render, /canRevive\(inst\) \? ' Bring it back with the button at the top of this page\.' : ''/,
1332
+ 'and the sentence is dropped where no such button rendered');
1323
1333
  });
1324
1334
 
1325
1335
  test('a teardown does not silence the address — the copy says what the runner does', () => {
@@ -79,16 +79,17 @@ test('the restart door appears ONLY where it has a reason to exist, and mirrors
79
79
  assert.match(body, /!inst\.open_intent/, 'the strip tells the story while work is in flight');
80
80
  });
81
81
 
82
- test('the restart door is offered on BOTH surfaces, and the address row points at it', () => {
83
- assert.match(fn('renderCard'), /canRestart\(inst\)[\s\S]*?data-restart=/, 'the list card');
84
- assert.match(fn('renderManage'), /canRestart\(inst\)[\s\S]*?data-m-restart/, 'the project’s own page — the two never disagree');
82
+ test('the restart door lives on the project’s own page, and the address row points at it from wherever it is read', () => {
83
+ // task 1003355: a list row carries only view and manage, so the door has ONE
84
+ // home — a second copy on the row would be the thing the owner asked to remove
85
+ assert.doesNotMatch(fn('renderCard'), /data-restart/, 'the row acts on nothing');
86
+ assert.match(fn('renderManage'), /canRestart\(inst\)[\s\S]*?data-m-restart/, 'the project’s own page');
85
87
  // the door and the sentence that sends the owner to it must ship together —
86
88
  // and the sentence must be gated by the SAME predicate, or a project with no
87
- // button (dedicated shape, or work already queued) is told to scroll to one
89
+ // button (dedicated shape, or work already queued) is told to go looking for one
88
90
  const addr = fn('addressRow');
89
- assert.match(addr, /or restart it yourself below/, 'the unreachable line names the action now that one exists');
90
- assert.match(addr, /canRestart\(inst\) \? ' — or restart it yourself below\.' : '\.'/,
91
- 'the pointer is conditioned on canRestart(), not on last_verified_up alone (ADR 0199 §8)');
91
+ assert.match(addr, /canRestart\(inst\) \? \(onManage \? ' — or restart it yourself below\.' : ' — or restart it from its own page\.'\) : '\.'/,
92
+ 'the pointer is conditioned on canRestart() and names a door the READING surface has (ADR 0199 §8)');
92
93
  });
93
94
 
94
95
  test('a queued restart has its own strip — never the generic "a <action> run is queued" fallthrough', () => {
@@ -102,11 +103,12 @@ test('a queued restart has its own strip — never the generic "a <action> run i
102
103
  });
103
104
 
104
105
  test('the restart sender posts to the restart ROUTE — re-POSTing the slug would be refused on every press', () => {
105
- const body = fn('doRestart');
106
+ const body = fn('mRestart');
106
107
  assert.match(body, /\/restart'/, 'its own endpoint');
107
108
  assert.doesNotMatch(body, /provisioning\/instances',/, 'NOT the create/re-provision POST — PROVISION_ENQUEUE_FROM excludes active');
108
- assert.match(body, /quietActions\(id\)/, 'the row goes quiet while it is in flight');
109
- assert.match(fn('mRestart'), /\/restart'/, 'and the manage page sends the same request');
109
+ assert.match(body, /\$\('mActs'\)\.querySelectorAll\('button'\)\.forEach\(function \(b\) \{ b\.disabled = true; \}\);/,
110
+ 'the doors go quiet while it is in flight');
111
+ assert.doesNotMatch(SCRIPT, /function doRestart\(/, 'and there is no second sender since the row lost its door (task 1003355)');
110
112
  });
111
113
  /* ── the moment a standup lands (task 1003243) ── */
112
114
  test('the landed moment is earned by a VERIFIED probe inside a window, not invented state', () => {
@@ -319,11 +321,14 @@ test('card metadata is span-wrapped so a separator can never strand; the manage
319
321
  assert.match(fn('repaintPlanets'), /if \(manageInst\) paintManagePlanet\(manageInst\);/, 'a catalog that lands after the manage page rendered repaints its head too');
320
322
  });
321
323
 
322
- test('an open teardown confirm survives the minute re-render', () => {
323
- assert.match(SCRIPT, /var cardAsking = null;/);
324
- assert.match(fn('renderCard'), /if \(cardAsking !== null && cardAsking === String\(inst\.id\)\) return;/);
325
- assert.match(fn('askTeardown'), /cardAsking = String\(id\);/);
326
- assert.match(SCRIPT, /function unAskTeardown\(id\) \{ cardAsking = null;/);
324
+ test('the one open teardown confirm — the ender’s — survives the minute re-render', () => {
325
+ // task 1003355: the row's two-press confirm is gone with the row's door, so
326
+ // there is exactly one confirmation on the hub and it is the typed-back name
327
+ assert.doesNotMatch(SCRIPT, /cardAsking/, 'the row holds no confirmation to protect');
328
+ assert.match(SCRIPT, /var mDelAsking = null;/);
329
+ assert.match(fn('renderDelete'), /if \(mDelAsking && mDelAsking === inst\.slug\) return;/,
330
+ 'and it is keyed by PROJECT, so a different project always repaints');
331
+ assert.match(fn('askDelete'), /mDelAsking = inst\.slug;/);
327
332
  });
328
333
 
329
334
  test('the inputs a stranger types into are labelled', () => {
@@ -332,48 +337,54 @@ test('the inputs a stranger types into are labelled', () => {
332
337
  assert.match(SCRIPT, /aria-label="A domain you own, for ' \+ esc\(inst\.slug\) \+ '"/);
333
338
  });
334
339
 
335
- /* ── the custom-domain field sits behind a fold (owner decision 2, 2026-08-24) ── */
336
- test('the domain field is a closed disclosure after the card’s doors — never the card’s loudest control', () => {
337
- const fold = fn('attachFold');
338
- assert.match(fold, /<details class="attachFold"/, 'the wizard’s address-step disclosure shape');
339
- assert.match(fold, /Use a domain I own/, 'labelled the way the wizard labels it');
340
- assert.match(fold, /inst\.status !== 'active' && inst\.status !== 'requested'\) return ''/, 'live + queued only — snagged and torn-down cards carry no fold');
341
- assert.match(fold, /if \(!canAttachDomain\(inst\)\) return ''/, 'the server-mirror gate still comes first');
342
- assert.match(fold, /inst\.open_intent\.action === 'domain-attach'\) return ''/, 'no second attach control while one is wiring');
343
- assert.doesNotMatch(fold, /Use your own domain/, 'the old filled-button label is gone');
340
+ /* ── the custom-domain field is a section of the project's own page (owner,
341
+ 2026-08-29 — task 1003355; it sat behind a fold on the list card until then) ── */
342
+ test('the row carries two doors and nothing that acts; the address field is a section of the project’s own page', () => {
344
343
  const card = fn('renderCard');
345
- assert.doesNotMatch(card, /<div class="attach">/, 'renderCard no longer inlines the bare field');
346
- const actions = card.indexOf('<div class="actions" id="act-');
347
- const foldCall = card.indexOf('body += attachFold(inst, iid);');
348
- assert.ok(actions >= 0 && foldCall > actions, 'Open site → Manage → (fold): the fold renders after the actions row');
349
- assert.match(COSMOS, /\.attachFold summary\{cursor:pointer;/, 'the summary reads as a control');
350
- // the 390px capture: the fold stacks field over button because the row wraps
351
- // and the field's minimum (220px) is narrower than a phone card's body; the
352
- // fold itself sets no width, so it can never be the thing that overflows
344
+ assert.doesNotMatch(card, /<button/, 'no sender survives on a row');
345
+ assert.doesNotMatch(card, /attachFold|<div class="attach">/, 'and no field either');
346
+ assert.match(card, /'<div class="actions">' \+ acts\.join\(''\)/, 'one doors strip, always rendered');
347
+ assert.match(card, /inst\.status === 'active' && inst\.domain\) acts\.push\([\s\S]{0,180}>View<\/a>'\)/,
348
+ 'View only where the project is live and has an address to open');
349
+ assert.match(card, /acts\.push\('<a class="btn small ghost" data-manage="' \+ iid \+ '" href="\?view=manage&id=' \+ iid \+ '">Manage<\/a>'\);/,
350
+ 'Manage on every row, as a real link');
351
+ // the section on the manage page: the heading is the disclosure, so the field
352
+ // needs no fold of its own, and the ender's hairline shapes are reused
353
+ assert.match(HUB, /<div class="card mSec" id="mAddress" hidden>/, 'a settings section like the three above it');
354
+ assert.match(HUB, /<h2>Its address<\/h2>/);
355
+ for (const id of ['mDom', 'mDomWhat']) assert.match(HUB, new RegExp('id="' + id + '"'), '#' + id + ' is real markup');
356
+ const sec = fn('renderAddress');
357
+ assert.match(sec, /canAttachDomain\(inst\) && \(inst\.status === 'active' \|\| inst\.status === 'requested'\)/,
358
+ 'the server-mirror gate first, then live + queued only — a snagged or torn-down project has nothing to point an address at');
359
+ assert.match(sec, /\$\('mAddress'\)\.hidden = !offer;/, 'and the whole section goes, not just the button');
360
+ assert.match(sec, /inst\.open_intent\.action === 'domain-attach'/, 'no second attach control while one is wiring');
361
+ assert.match(sec, /<div class="attach">/, 'the ender’s hairline field');
362
+ assert.match(sec, /data-m-attach/, 'and the manage page’s own marker');
363
+ assert.doesNotMatch(SCRIPT, /function doAttach\(/, 'the row’s sender is gone with the row’s field');
364
+ // the 390px capture: the row wraps and the field's minimum (220px) is
365
+ // narrower than a phone's body, so it can never be the thing that overflows
353
366
  assert.match(COSMOS, /\.attach\{display:flex;gap:10px;margin-top:10px;flex-wrap:wrap;\}/, 'the row wraps at phone width');
354
- assert.match(COSMOS, /\.attach input\[type=text\]\{flex:1;min-width:220px;/, 'the field fits a 390px card and grows on a laptop');
355
- assert.doesNotMatch(COSMOS, /\.attachFold[^{]*\{[^}]*(width|min-width):(?!fit-content)/, 'the fold sets no width of its own');
356
- // and in both themes: the summary reads from tokens, never a literal colour
357
- assert.doesNotMatch((COSMOS.match(/\.attachFold[^\n]*/g) || []).join('\n'), /#[0-9a-f]{3,8}\b/i, 'the fold paints from --ink-faint / --ink-soft, so dark mode needs no twin');
358
- });
359
-
360
- test('the F19 re-kick door survives the fold: open, pre-filled, one button', () => {
361
- const fold = fn('attachFold');
362
- assert.match(fold, /var rekick = inst\.status === 'active' && inst\.domain && inst\.error_note;/);
363
- assert.match(fold, /\(rekick \? ' open' : ''\)/, 'the fold opens on its own for a stalled address');
364
- assert.match(fold, /\(prefill \? ' value="' \+ esc\(inst\.domain\) \+ '"' : ''\)/, 'pre-filled with the address in play');
365
- assert.match(fold, /var prefill = rekick \|\| repoint;/, 'a stalled address and a correctable one both arrive in the field');
366
- assert.match(fold, /rekick \? 'Try the address again' : repoint \? 'Change the address' : 'Use this domain'/, 'and the button says what it does');
367
+ assert.match(COSMOS, /\.attach input\[type=text\]\{flex:1;min-width:220px;/, 'the field fits a 390px page and grows on a laptop');
368
+ assert.doesNotMatch(COSMOS, /attachFold/, 'the fold’s rules went with the fold');
369
+ });
370
+
371
+ test('the F19 re-kick door survives the move: pre-filled, one button, its own sentence', () => {
372
+ const sec = fn('renderAddress');
373
+ assert.match(sec, /var rekick = inst\.status === 'active' && inst\.domain && inst\.error_note;/);
374
+ assert.match(sec, /\(prefill \? ' value="' \+ esc\(inst\.domain\) \+ '"' : ''\)/, 'pre-filled with the address in play');
375
+ assert.match(sec, /var prefill = rekick \|\| repoint;/, 'a stalled address and a correctable one both arrive in the field');
376
+ assert.match(sec, /rekick \? 'Try the address again' : repoint \? 'Change the address' : 'Use this domain'/, 'and the button says what it does');
377
+ assert.match(sec, /rekick \? 'The wiring for this address stopped\./, 'the section says WHY the field is pre-filled — the fold said it by opening itself');
367
378
  assert.match(fn('canAttachDomain'), /inst\.status === 'active' && inst\.domain && inst\.error_note && !inst\.open_intent\) return true;/, 'the route-side re-kick gate is untouched');
368
379
  });
369
380
 
370
- test('the correction door: a bring-your-own address can be changed, and the fold says what that costs (task 1003237)', () => {
371
- const fold = fn('attachFold');
381
+ test('the correction door: a bring-your-own address can be changed, and the section says what that costs (task 1003237)', () => {
382
+ const fold = fn('renderAddress');
372
383
  // the three doors are told apart by what the project is actually addressed as
373
384
  assert.match(fold, /var byo = inst\.domain && inst\.domain !== assignedDefaultFor\(inst\);/,
374
- 'the platform-assigned <slug>.<apex> is NOT a correction — moving off it is the upgrade the fold already offered');
385
+ 'the platform-assigned <slug>.<apex> is NOT a correction — moving off it is the upgrade this control already offered');
375
386
  assert.match(fold, /var repoint = !rekick && byo;/, 'a stalled address keeps the F19 door, which is louder');
376
- assert.match(fold, /'Use a different domain'/, 'the summary names the correction');
387
+ assert.match(fold, /repoint \? 'It answers on an address you brought\./, 'the lead names the correction');
377
388
  assert.match(fold, /attachNote/, 'and the consequence is stated where the button is');
378
389
  assert.match(fold, /won’t use that address any more/, 'letting the old address go is SAID, not discovered');
379
390
  // the door itself: an addressed, live project is no longer excluded
@@ -392,13 +403,23 @@ test('the retired refusal is gone from the card: domain_already_set can no longe
392
403
  'the server stopped sending it (task 1003237) — a branch for it would be unreachable copy');
393
404
  });
394
405
 
395
- test('a heartbeat re-render keeps an open fold open and a half-typed domain typed', () => {
406
+ test('the minute tick keeps a half-typed domain typed, and its receipt on the page', () => {
407
+ // the rule moved with the field (task 1003355): renderManage repaints every
408
+ // section once a minute, so the address section must survive its own repaint
409
+ const sec = fn('renderAddress');
410
+ assert.match(sec, /old\.value !== old\.defaultValue \? old\.value : ''/, 'typing is told apart from the pre-fill');
411
+ assert.match(sec, /document\.activeElement === old/, 'the caret comes back too');
412
+ // lastIndexOf: the in-flight branch above writes the same slot, and returns
413
+ const set = sec.lastIndexOf("$('mDom').innerHTML =");
414
+ assert.ok(sec.indexOf('var typed =') < set && sec.indexOf('if (field && typed) field.value = typed;') > set,
415
+ 'read before the swap, restore after it');
416
+ assert.match(sec, /if \(mDomFor !== inst\.id\) \{ mDomFor = inst\.id; mDomNote = null; \}/,
417
+ 'and the receipt is keyed by PROJECT, so a different project never wears the last one’s');
418
+ assert.match(sec, /\(mDomNote \? mDomNote\.html : ''\)/, 'a repaint re-renders the receipt rather than swallowing it');
419
+ // the row keeps nothing to restore but the door that had focus
396
420
  const card = fn('renderCard');
397
- assert.match(card, /var keepOpen = !!\(oldFold && oldFold\.open\);/);
398
- assert.match(card, /oldInput\.value !== oldInput\.defaultValue \? oldInput\.value : ''/, 'typing is told apart from the re-kick pre-fill');
399
- assert.match(card, /document\.activeElement === oldInput/, 'the caret comes back too');
400
- const set = card.indexOf('li.innerHTML = orbFor(inst)');
401
- assert.ok(card.indexOf('var keepOpen') < set && card.indexOf('if (fold && keepOpen) fold.open = true;') > set, 'read before the swap, restore after it');
421
+ assert.doesNotMatch(card, /oldFold|oldInput|keepOpen/, 'the row holds no field or fold to carry across the swap');
422
+ assert.match(card, /if \(wasInside\) focusCardControl\(inst\.id, prefer\);/);
402
423
  });
403
424
 
404
425
  /* ── the owner's five calls of 2026-08-24 (task 1003250) ── */
@@ -464,7 +485,18 @@ test('an unmapped create failure gets a plain sentence with the raw text folded
464
485
  test('a manifest error says a plain sentence, folds the route text, and sends a domainless project to its address', () => {
465
486
  const steps = fn('renderStepOauthApp');
466
487
  assert.doesNotMatch(steps, /'GitHub setup hit a snag' \+ \(live\.manifestMsg \? ': ' \+ esc\(live\.manifestMsg\)/, 'the raw route text is no longer the headline');
467
- assert.match(steps, /else if \(live\.manifestCode === 'domain_required'\) snag = 'This project needs an address first — attach one from <a href="\?view=mine">your projects page<\/a>/);
488
+ assert.match(steps, /else if \(live\.manifestCode === 'domain_required'\) snag = 'This project needs an address first — attach one from ' \+ projectPageLink\('its own page'\)/,
489
+ 'the address lives on the project’s own page since task 1003355 — the list would be a dead end');
490
+ assert.match(fn('projectPageLink'), /instanceId \? '\?view=manage&id=' \+ encodeURIComponent\(instanceId\) : '\?view=mine'/,
491
+ 'and it is named by id whenever the flow has one');
492
+ // every done-panel pointer that names a CONTROL goes the same way; the ones
493
+ // that only say "watch it there" still name the list, which does show it
494
+ assert.equal((SCRIPT.match(/projectPageLink\(/g) || []).length, 6,
495
+ 'the helper and its five callers: the locked step, the domain_required snag, the domainless standup, the snagged retry, the torn-down revive');
496
+ for (const dead of ['Retry it \\(and see the technical details\\) from <a href="\\?view=mine">',
497
+ 'Bring it back any time from <a href="\\?view=mine">']) {
498
+ assert.doesNotMatch(SCRIPT, new RegExp(dead), 'a control the list no longer has must not be pointed at there');
499
+ }
468
500
  assert.match(steps, /\(raw \? '<details><summary>Technical details<\/summary><p>' \+ esc\(raw\) \+ '<\/p><\/details>' : ''\)/);
469
501
  assert.match(SCRIPT, /e\.code = \(d\.error && d\.error\.code\) \|\| '';/, 'the route code travels with the error');
470
502
  assert.match(SCRIPT, /live\.manifestCode = String\(\(err && err\.code\) \|\| ''\);/);
@@ -486,21 +518,24 @@ test('the rail at the done panel keeps its marker on Create until the project an
486
518
  assert.match(HUB, /<li class="now" data-step="1" aria-current="step">/, 'the static rail starts with the same marker');
487
519
  });
488
520
 
489
- test('card action results announce, nothing under the pointer is deleted, and focus stays on the card', () => {
490
- assert.match(fn('renderCard'), /id="msg-' \+ iid \+ '" role="status" aria-live="polite">/, 'the card slot is a live region');
521
+ test('every action result announces, nothing under the pointer is deleted, and focus stays where it was', () => {
522
+ // task 1003355 moved every sender to the project's own page, so the live
523
+ // regions that matter are that page's; the row keeps its own for the watch's
524
+ // "can't see this project any more"
525
+ assert.match(fn('renderCard'), /id="msg-' \+ iid \+ '" role="status" aria-live="polite">/, 'the row slot is a live region');
491
526
  assert.match(HUB, /<div class="projMsg" id="mMsg" role="status" aria-live="polite"><\/div>/, 'so is the manage page’s');
492
- assert.doesNotMatch(SCRIPT, /act\.innerHTML = '<span class="projMsg">Asking…<\/span>'/, 'no action replaces the row it was pressed in');
493
- assert.match(fn('doTeardown'), /quietActions\(id\);\n\s*msg\(id, 'Asking…'\);/);
494
- assert.match(fn('reProvision'), /quietActions\(id\);\n\s*msg\(id, 'Asking…'\);/);
495
- assert.match(fn('quietActions'), /act\.querySelectorAll\('button'\)\.forEach\(function \(b\) \{ b\.disabled = true; \}\);/);
496
- assert.match(fn('quietActions'), /li\.setAttribute\('data-focus-back', ''\)/, 'a disabled button that held focus leaves a note');
497
- assert.match(fn('askTeardown'), /<div class="confirmRow" tabindex="-1">/);
498
- assert.match(fn('askTeardown'), /if \(row && row\.focus\) row\.focus\(\);/, 'the question takes focus when the button goes');
527
+ assert.match(fn('renderAddress'), /id="mDomMsg" role="status" aria-live="polite"/, 'and the address section’s own');
528
+ for (const sender of ['mReProvision', 'mRestart']) {
529
+ assert.match(fn(sender), /\$\('mActs'\)\.querySelectorAll\('button'\)\.forEach\(function \(b\) \{ b\.disabled = true; \}\);/,
530
+ sender + ' quiets the doors while it is in flight');
531
+ }
532
+ assert.match(fn('mAttach'), /if \(btn\) btn\.disabled = true;/, 'and so does the address control');
533
+ assert.match(fn('askDelete'), /if \(field\) field\.focus\(\);/, 'the typed-back name takes focus when the ask opens');
499
534
  const card = fn('renderCard');
500
535
  assert.match(card, /var wasInside = li\.contains\(document\.activeElement\) && document\.activeElement !== li;/);
501
- assert.match(card, /else if \(focusBack\) focusCardControl\(inst\.id, prefer\);/);
502
- assert.match(fn('focusKeyOf'), /data-teardown-yes'\) \|\| el\.hasAttribute\('data-teardown-no'\)\) return '\[data-teardown\]'/, 'Yes / Keep land back on the door that opened them');
503
- assert.match(fn('focusCardControl'), /li\.querySelector\('\.actions a, \.actions button'\) \|\|\n\s*li\.querySelector\('details\.attachFold summary'\)/, 'nearest control as the fallback');
536
+ assert.match(card, /if \(wasInside\) focusCardControl\(inst\.id, prefer\);/);
537
+ assert.match(fn('focusKeyOf'), /el\.hasAttribute\('data-manage'\) \? '\[data-manage\]' : null/, 'Manage lands back on Manage');
538
+ assert.match(fn('focusCardControl'), /li\.querySelector\('\.actions a'\)/, 'nearest door as the fallback');
504
539
  });
505
540
 
506
541
  /* ── copy follow-ups (task 1003245) ── */
@@ -539,7 +574,14 @@ test('repo visibility: the refusal is one line, the mechanism lives in the fold'
539
574
  test('a failed manage read offers a retry; the no-address sentence offers a door', () => {
540
575
  assert.match(SCRIPT, /a\.innerHTML = 'Couldn’t read this project right now\. <button type="button" class="btn small ghost" id="mAlertRetry">Try again<\/button>';/);
541
576
  assert.match(SCRIPT, /\$\('mAlert'\)\.addEventListener\('click', function \(ev\) \{\n\s*if \(ev\.target\.closest && ev\.target\.closest\('#mAlertRetry'\)\) loadManage\(\);/, 'delegated — the catch rewrites the alert on every failure');
542
- assert.match(fn('addressRow'), /No address yet<\/span> — sign-in on this project can’t be set up until it has one\. <a href="\?view=mine" data-nav-to="mine">Give it an address from your projects<\/a>\./);
577
+ // the door the sentence offers must be one the READING surface has: on the
578
+ // project's own page it is the section below, on a row it is that project's
579
+ // page — never the list, which since task 1003355 carries no field at all
580
+ const addr = fn('addressRow');
581
+ assert.match(addr, /No address yet<\/span> — sign-in on this project can’t be set up until it has one\. '/);
582
+ assert.match(addr, /onManage \? 'Give it an address below\.'/);
583
+ assert.match(addr, /'<a href="\?view=manage&id=' \+ esc\(String\(inst\.id\)\) \+ '" data-manage="' \+ esc\(String\(inst\.id\)\) \+ '">Give it an address on its own page<\/a>\.'/,
584
+ 'and it carries data-manage, so the list’s delegated handler opens it in place');
543
585
  assert.match(SCRIPT, /var a = ev\.target\.closest && ev\.target\.closest\('\[data-nav-to\]'\);\n\s*if \(!a \|\| !isPlainClick\(ev\)\) return;/, 'data-nav-to is delegated, so a link injected after boot switches views in place');
544
586
  assert.doesNotMatch(SCRIPT, /querySelectorAll\('\[data-nav-to\]'\)/, 'the boot-time sweep is gone');
545
587
  });
@@ -186,5 +186,57 @@ await t('the CLI carries the declaration through its deliberate-empty path', ()
186
186
  'and the deliberate-empty land sites must mark themselves as such');
187
187
  });
188
188
 
189
+ // task 1003434 — a recorded tip proves the land even when the claim has no branch.
190
+ // Strands 1003285/1003286: commits on main, no branch on the claim. Once the tip was
191
+ // recorded, POST /ship still refused, because getPublishStatus threw BAD_BRANCH before
192
+ // landProvenForTask ever saw the tip. The status probe is a witness, not a
193
+ // prerequisite: its failure must degrade to status=null, never discard the proof.
194
+ const { deriveShipAssurance } = require(path.join(ROOT, 'modules/lifecycle/ship-assurance.js'));
195
+ const TIP = 'a'.repeat(40);
196
+ console.log('\nthe tip proof does not depend on the branch/PR probe');
197
+ await t('tip in main + getPublishStatus throwing (no branch on the claim) → proof', async () => {
198
+ const seen = [];
199
+ const r = await deriveShipAssurance({
200
+ taskId: 1003285,
201
+ body: {},
202
+ db: { getClaimPublishedSha: async () => TIP },
203
+ githubPush: {
204
+ getPublishStatus: async () => { const e = new Error('BAD_BRANCH'); e.code = 'BAD_BRANCH'; throw e; },
205
+ landProvenForTask: async ({ publishedSha, status }) => {
206
+ seen.push({ publishedSha, status });
207
+ return publishedSha === TIP ? { proven: true, reason: 'published tip in main' } : { proven: false };
208
+ },
209
+ },
210
+ onProbeError: (e) => seen.push({ probeError: e.code }),
211
+ });
212
+ assert.equal(seen.some((s) => s.publishedSha === TIP && s.status === null), true, 'the proof ran with status=null');
213
+ assert.equal(seen.some((s) => s.probeError === 'BAD_BRANCH'), true, 'the probe failure was still reported');
214
+ assert.equal(r.assurance && r.assurance.kind, 'proof');
215
+ });
216
+ await t('no tip + getPublishStatus throwing → still the 409 refusal (nothing to prove)', async () => {
217
+ const r = await deriveShipAssurance({
218
+ taskId: 1003253,
219
+ body: {},
220
+ db: { getClaimPublishedSha: async () => null },
221
+ githubPush: {
222
+ getPublishStatus: async () => { throw new Error('BAD_BRANCH'); },
223
+ landProvenForTask: async () => ({ proven: false, reason: 'no tip, no PR' }),
224
+ },
225
+ });
226
+ assert.equal(r.refusal && r.refusal.code, 'ship_land_not_proven');
227
+ });
228
+ await t('a proof failure (compare API down) still falls through to the refusal, not a throw', async () => {
229
+ const r = await deriveShipAssurance({
230
+ taskId: 1,
231
+ body: {},
232
+ db: { getClaimPublishedSha: async () => TIP },
233
+ githubPush: {
234
+ getPublishStatus: async () => null,
235
+ landProvenForTask: async () => { throw new Error('STATUS_FETCH_FAILED'); },
236
+ },
237
+ });
238
+ assert.equal(r.refusal && r.refusal.code, 'ship_land_not_proven');
239
+ });
240
+
189
241
  console.log(`\n${passed} passed, ${failed} failed`);
190
242
  process.exit(failed ? 1 : 0);