@bongos/core 1.21.21 → 1.21.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +60 -60
- package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +1 -0
- package/docs/copy-inventory.md +105 -108
- package/docs/copy-registry.json +129 -166
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +246 -243
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +2 -2
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +5 -7
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +2 -2
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/record.css +4 -19
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +56 -5
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/task.html +6 -9
- package/modules/hall-ui/public/task.js +15 -50
- package/modules/hall-ui/public/task.states.json +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.css +4 -0
- package/modules/hall-ui/public/tweak-editor.html +5 -3
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/hall-navigation.md +7 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/provision-repo.js +9 -2
- package/src/module-api.js +1 -1
- package/tests/hall_fullscreen.mjs +88 -12
- package/tests/hall_record_world.mjs +41 -63
- package/tests/hall_tweak_editor.mjs +3 -2
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/provision.mjs +7 -2
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"ignoreConsole": ["/api/(gds|bongos)/", "\\b401\\b", "\\b404\\b", "Unauthorized", "Not Found", "load failed"],
|
|
9
9
|
"states": {
|
|
10
10
|
"record": { "auth": true, "actions": [["wait", 600]], "expect": { "visible": ["#task-body", "#task-grade", "#task-sessions", "#copy-prompt-btn"] } },
|
|
11
|
-
"from-goal": { "auth": true, "url": "/task/2166?from=%2Fgoals%23%2Fgoal%2F1000081", "actions": [["wait", 600]], "expect": { "visible": ["#
|
|
11
|
+
"from-goal": { "auth": true, "url": "/task/2166?from=%2Fgoals%23%2Fgoal%2F1000081", "actions": [["wait", 600]], "expect": { "visible": ["#topbar-back", "#copy-prompt-btn"], "attr": [["#topbar-back", "href", "/goals#/goal/1000081"]] } },
|
|
12
12
|
"missing": { "auth": true, "url": "/task/1003999", "actions": [["wait", 600]], "expect": { "visible": ["#task-h"] } }
|
|
13
13
|
},
|
|
14
14
|
"reducedMotion": { "auth": true, "actions": [["wait", 600]] }
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
</div>
|
|
139
139
|
</div>
|
|
140
140
|
|
|
141
|
-
<script src="/builders/shell.js?v=2026-10-02-
|
|
141
|
+
<script src="/builders/shell.js?v=2026-10-02-back"></script>
|
|
142
142
|
<script src="/builders/palette.js" defer></script>
|
|
143
143
|
|
|
144
144
|
<script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
|
|
@@ -177,6 +177,10 @@ html[data-fullscreen="on"] .tw-bar { width: min(1200px, calc(100% - 144px)); }
|
|
|
177
177
|
/* With the chrome showing, the shell's fixed top bar holds the window's top,
|
|
178
178
|
so the stuck bar stops under it instead of sliding beneath it. */
|
|
179
179
|
html:not([data-fullscreen="on"]) .tw-bar { top: calc(var(--topbar-h, 56px) + 12px); }
|
|
180
|
+
/* task 1004477: the bar's exit is the shell's Back link, filled by shell.js.
|
|
181
|
+
With the chrome showing, the top bar already carries it, so the bar shows it
|
|
182
|
+
only in full screen and the page never offers two. */
|
|
183
|
+
html:not([data-fullscreen="on"]) .tw-stage .tw-back { display: none; }
|
|
180
184
|
.tw-stage .tw-back {
|
|
181
185
|
display: inline-flex;
|
|
182
186
|
align-items: center;
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
data-fullscreen-switch: the shell's full-screen switch (task 1004327). The
|
|
30
30
|
artboard is the editor over the whole window, so the editor opens full
|
|
31
31
|
screen the first time (tweak-editor.js), and the switch is remembered. -->
|
|
32
|
-
<body data-page="tweak-editor" data-page-title="Tweak a page" data-fullscreen-switch>
|
|
32
|
+
<body data-page="tweak-editor" data-page-title="Tweak a page" data-fullscreen-switch data-back-fallback="/studio">
|
|
33
33
|
|
|
34
34
|
<div class="app">
|
|
35
35
|
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
@@ -49,7 +49,9 @@
|
|
|
49
49
|
<!-- A <div>, not a <header>: the shell's top bar is the page's one
|
|
50
50
|
<header> (the kit's one-header-main-h1 floor). -->
|
|
51
51
|
<div class="tw-bar" id="tw-bar">
|
|
52
|
-
|
|
52
|
+
<!-- task 1004477: the shell's exit, shown only in full screen, where the
|
|
53
|
+
top bar (and its Back link) is hidden. The shell fills it. -->
|
|
54
|
+
<a class="tw-back" data-shell-exit href="/builders/studio">Back to studio</a>
|
|
53
55
|
<span class="tw-name"><span id="tw-name">Tweak a page</span> <span class="tw-surface" id="tw-surface"></span></span>
|
|
54
56
|
<span class="tw-counter" id="tw-counter" aria-live="polite"></span>
|
|
55
57
|
<div class="tw-actions">
|
|
@@ -140,7 +142,7 @@
|
|
|
140
142
|
|
|
141
143
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
142
144
|
|
|
143
|
-
<script src="/builders/shell.js?v=2026-10-02-
|
|
145
|
+
<script src="/builders/shell.js?v=2026-10-02-back"></script>
|
|
144
146
|
<script src="/builders/palette.js" defer></script>
|
|
145
147
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
146
148
|
<script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
|
|
163
163
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
164
164
|
chrome exists before the page script runs. -->
|
|
165
|
-
<script src="/builders/shell.js?v=2026-10-02-
|
|
165
|
+
<script src="/builders/shell.js?v=2026-10-02-back"></script>
|
|
166
166
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
167
167
|
<script src="/builders/palette.js" defer></script>
|
|
168
168
|
<!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
|
|
172
172
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
173
173
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
174
|
-
<script src="/builders/shell.js?v=2026-10-02-
|
|
174
|
+
<script src="/builders/shell.js?v=2026-10-02-back"></script>
|
|
175
175
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
176
176
|
<script src="/builders/palette.js" defer></script>
|
|
177
177
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Hall navigation — the Back link and the standard full-screen switch (
|
|
1
|
+
# Hall navigation — the Back link and the standard full-screen switch (tasks 1004430, 1004477)
|
|
2
2
|
|
|
3
3
|
Covers: every shelled hall page (`shell.js`, `style.css`), plus `goal-map.html` joining the chrome.
|
|
4
4
|
|
|
@@ -7,16 +7,19 @@ Covers: every shelled hall page (`shell.js`, `style.css`), plus `goal-map.html`
|
|
|
7
7
|
- `shell.js` renders `#topbar-back` ("← <label>") at the head of the top bar when the page was reached from INSIDE another hall page. The origin is `?from=` first, then a same-origin `document.referrer`; the label is that page's `NAV` label (or "task N" / "idea N" / "goal N" for a record).
|
|
8
8
|
- **A sidebar hop is not a drill-down.** A sidebar link click writes a one-shot `sessionStorage['hall-nav-hop']` naming its target; the page it lands on reads and deletes it, and shows no Back link. Without this every page reached from the sidebar would carry a Back link, which is just history.
|
|
9
9
|
- When the origin came from the referrer, the click runs `history.back()`, so a list comes back with its scroll and filters; the `href` is still the real address for a new tab.
|
|
10
|
-
-
|
|
10
|
+
- **No page draws its own Back link (task 1004477).** `task.html`, `idea.html` and the tweak editor used to, and the bar skipped its link on those pages. Now the shell decides the way back for every page, with two hooks:
|
|
11
|
+
- `<body data-back-fallback="/work">` names where Back goes when there is no origin (a deep-linked task falls back to Tasks, an idea to Your thinking, the tweak editor to Studio). It is honoured only as a path on this host; a sidebar hop still shows nothing.
|
|
12
|
+
- A link marked `[data-shell-exit]` is a second exit a page places: the task record's actions row and its two failure renders, the primer and drachmae error notes, and the tweak editor's bar. The shell fills its `href` and its words ("Back to goal 12", "Back to tasks"), and with no way back at all it goes home. A page that renders one after boot calls `OTBShell.fillExits(root)`.
|
|
13
|
+
- The origin's own `?from=` is dropped so back links never chain, and a `?from=` naming the page itself is refused.
|
|
11
14
|
- `/profile?id=N` highlights **Builders**, not your own Profile, unless `/me` says the id is yours.
|
|
12
15
|
|
|
13
16
|
## Full screen is standard
|
|
14
17
|
|
|
15
18
|
- Every shelled page gets the switch. `<body data-fullscreen-switch="off">` opts out. The bare attribute means "this page lays itself out for full screen" (sky, studio, tweak editor); a page without it is **plain**, and the shell sets `html[data-fullscreen-plain="on"]` so `style.css` pads `.app-content` clear of the corner button.
|
|
16
|
-
- In full screen the button is a labelled pill, an icon plus "Exit full screen", in `--bg-card` with a ring and a drop shadow, not the hall's near-clear glass. Under 640px it drops the words, and the tweak editor keeps the 36px round button because its bar is laid out for one.
|
|
19
|
+
- In full screen the button is a labelled pill, an icon plus "Exit full screen", in `--bg-card` with a ring and a drop shadow, not the hall's near-clear glass. Under 640px it drops the words, and the tweak editor keeps the 36px round button because its bar is laid out for one. Full screen hides the top bar and its Back link, so the tweak editor's bar carries the shell's exit and shows it only in full screen (`tweak-editor.css`); with the chrome showing, the page never offers two. On the black sky in the light theme the glass button was a dark icon on black, and because the choice is remembered per page, `/thinking` looked stuck with no bars. Entering full screen (or a page opening in it) also shows a 4.5s `#fullscreen-hint` saying how to leave.
|
|
17
20
|
|
|
18
21
|
## The project sky joined the hall
|
|
19
22
|
|
|
20
23
|
`goal-map.html` now uses thinking.html's structure: sidebar and top-bar mounts, `style.css`, `shell.js`, the sky inside `<main id="hall-main" class="sky-stage">`, and a `<div class="brand">` because the shell's bar is the page's one `<header>`. In `thinking.css` both pages share one stage rule, `body[data-page] .sky-stage`. No hall page is standalone any more (`tests/hall_audit.mjs` `STANDALONE_EXEMPT` is empty).
|
|
21
24
|
|
|
22
|
-
Tests: `tests/hall_fullscreen.mjs` (the standard switch, `plain`, Back link cases), `tests/goal_map_page.mjs`, `tests/hall_audit.mjs` §8.
|
|
25
|
+
Tests: `tests/hall_fullscreen.mjs` (the standard switch, `plain`, Back link, fallback and exit cases), `tests/hall_record_world.mjs` §10 (the record places its exit), `tests/goal_map_page.mjs`, `tests/hall_audit.mjs` §8.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.23",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.23",
|
|
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.21.
|
|
3
|
+
"version": "1.21.23",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -8882,5 +8882,17 @@
|
|
|
8882
8882
|
"id": "1004544",
|
|
8883
8883
|
"text": "The governor city and the street now open inside the normal hall, with the sidebar and top bar around them like every other page. A full-screen button in the corner hides both so the city or the street fills the window again,"
|
|
8884
8884
|
}
|
|
8885
|
+
],
|
|
8886
|
+
"1.21.22": [
|
|
8887
|
+
{
|
|
8888
|
+
"id": "1004556",
|
|
8889
|
+
"text": "New projects are now always started on the latest released version of Cloud Bongos, even when the service that sets them up is running an older one. Until now, a lagging setup service passed its own old version to every new pr"
|
|
8890
|
+
}
|
|
8891
|
+
],
|
|
8892
|
+
"1.21.23": [
|
|
8893
|
+
{
|
|
8894
|
+
"id": "1004477",
|
|
8895
|
+
"text": "Every page in the builders hall now goes back the same way: one Back button in the top bar. Task and idea pages used to draw their own back links, and the tweak editor had its own too. Opened from a goal, a task now shows \"Bac"
|
|
8896
|
+
}
|
|
8885
8897
|
]
|
|
8886
8898
|
}
|
|
@@ -515,8 +515,14 @@ function runningCoreVersion({ instanceRoot, coreV }) {
|
|
|
515
515
|
// publish as candidates, the platform hall runs ahead of the registry's `latest` label by
|
|
516
516
|
// design — and the ladder below vends whatever the hall runs, so every new project would
|
|
517
517
|
// be born on a version nobody released. When the hall is AHEAD of the release, the project
|
|
518
|
-
// gets the release instead. Equal
|
|
519
|
-
//
|
|
518
|
+
// gets the release instead. Equal → the running core, byte-exact, as before.
|
|
519
|
+
//
|
|
520
|
+
// BEHIND the release is real too (task 1004556): the runner that scaffolds is not always
|
|
521
|
+
// the platform hall. On cloudbongos.com it ran from a patch-channel install that could not
|
|
522
|
+
// cross the 1.21.0 minor, so it sat on 1.20.82 and every new project was born there,
|
|
523
|
+
// without founding mode (1.21+). The release is what ADR 0361 D5 promises a new project,
|
|
524
|
+
// and a released core has already run on the platform hall, so the runner's lag must not
|
|
525
|
+
// set the project's version: behind → the release, packed from the registry, too.
|
|
520
526
|
//
|
|
521
527
|
// No readable label → the running core, deliberately (fail-open). Until candidates are
|
|
522
528
|
// armed the running core IS the release; vendoring is the path that works with no
|
|
@@ -525,6 +531,7 @@ function runningCoreVersion({ instanceRoot, coreV }) {
|
|
|
525
531
|
function projectCoreDecision({ running, released }) {
|
|
526
532
|
if (!released || !isStable(released)) return { version: running, source: 'running', reason: 'no readable released (latest) label — using the core this hall runs' };
|
|
527
533
|
if (compareSemver(running, released) > 0) return { version: released, source: 'released', reason: `this hall runs ${running}, ahead of the release` };
|
|
534
|
+
if (compareSemver(running, released) < 0) return { version: released, source: 'released', reason: `this runner is on ${running}, behind the release` };
|
|
528
535
|
return { version: running, source: 'running', reason: `this hall runs the released core ${released}` };
|
|
529
536
|
}
|
|
530
537
|
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.23'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -28,10 +28,12 @@ const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
|
28
28
|
// is SHARED across boots on purpose: a second boot over the same store is a reload.
|
|
29
29
|
// `optIn`: true = the bare attribute (the page lays itself out for full screen),
|
|
30
30
|
// null = no attribute (a plain page — the switch is standard since task
|
|
31
|
-
// 1004430), 'off' = the page opts out. `referrer` / `search` / `session`
|
|
32
|
-
//
|
|
31
|
+
// 1004430), 'off' = the page opts out. `referrer` / `search` / `session` drive
|
|
32
|
+
// the top bar's Back link (task 1004430); `fallback` is the page's
|
|
33
|
+
// <body data-back-fallback> and `exits` how many [data-shell-exit] links the
|
|
34
|
+
// page placed for the shell to fill (task 1004477).
|
|
33
35
|
function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = false,
|
|
34
|
-
referrer = '', search = '', session = {},
|
|
36
|
+
referrer = '', search = '', session = {}, fallback = null, exits = 0, path = null, meId = null } = {}) {
|
|
35
37
|
const els = new Map();
|
|
36
38
|
const windowListeners = {};
|
|
37
39
|
const timers = [];
|
|
@@ -65,9 +67,10 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
|
|
|
65
67
|
return { href, dataset: {}, getAttribute: (k) => (k === 'href' ? href : null),
|
|
66
68
|
classList: { add: (c) => cls.add(c), remove: (c) => cls.delete(c), contains: (c) => cls.has(c) } };
|
|
67
69
|
});
|
|
70
|
+
const exitLinks = Array.from({ length: exits }, () => makeEl(null));
|
|
68
71
|
const attr = optIn === true ? '' : optIn === false ? null : optIn;
|
|
69
72
|
const body = {
|
|
70
|
-
dataset: { page },
|
|
73
|
+
dataset: fallback == null ? { page } : { page, backFallback: fallback },
|
|
71
74
|
hasAttribute: (k) => k === 'data-fullscreen-switch' && attr !== null,
|
|
72
75
|
getAttribute: (k) => (k === 'data-fullscreen-switch' ? attr : null),
|
|
73
76
|
appendChild() {},
|
|
@@ -77,8 +80,9 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
|
|
|
77
80
|
referrer,
|
|
78
81
|
documentElement: { dataset: {}, style: {}, classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } } },
|
|
79
82
|
getElementById,
|
|
80
|
-
querySelector: (
|
|
81
|
-
querySelectorAll: (sel) => (sel === '#app-sidebar .nav-item' ? navItems
|
|
83
|
+
querySelector: () => null,
|
|
84
|
+
querySelectorAll: (sel) => (sel === '#app-sidebar .nav-item' ? navItems
|
|
85
|
+
: sel === '[data-shell-exit]' ? exitLinks : []),
|
|
82
86
|
createElement() { return makeEl(null); },
|
|
83
87
|
addEventListener() {}, dispatchEvent() {},
|
|
84
88
|
get cookie() { return ''; }, set cookie(_v) {},
|
|
@@ -123,6 +127,8 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
|
|
|
123
127
|
plain: () => root.dataset.fullscreenPlain === 'on',
|
|
124
128
|
active: () => navItems.filter((a) => a.classList.contains('nav-item--active')).map((a) => a.href),
|
|
125
129
|
topbar: () => topbar._html,
|
|
130
|
+
exits: () => exitLinks.map((a) => ({ href: a.getAttribute('href'), text: a.textContent, wired: !!(a._on && a._on.click) })),
|
|
131
|
+
shell: () => windowObj.OTBShell,
|
|
126
132
|
session,
|
|
127
133
|
resizes: () => resizes,
|
|
128
134
|
// Press a key the way the browser dispatches it: every window listener,
|
|
@@ -170,14 +176,69 @@ test('Back: ?from= wins over the referrer, and a record origin is named as the r
|
|
|
170
176
|
assert.match(p.topbar(), />task 42<\/span>/);
|
|
171
177
|
});
|
|
172
178
|
|
|
173
|
-
test('Back: none after a sidebar hop, from another site, from the page itself
|
|
179
|
+
test('Back: none after a sidebar hop, from another site, or from the page itself', () => {
|
|
174
180
|
const hop = boot({ page: 'goals', optIn: false, referrer: 'https://h/work', session: { 'hall-nav-hop': '/goals' } });
|
|
175
181
|
assert.doesNotMatch(hop.topbar(), /topbar-back/, 'the sidebar is not a drill-down');
|
|
176
182
|
assert.equal(hop.session['hall-nav-hop'], undefined, 'the note is one-shot');
|
|
177
183
|
assert.doesNotMatch(boot({ page: 'goals', optIn: false, referrer: 'https://elsewhere.example/roster' }).topbar(), /topbar-back/);
|
|
178
184
|
assert.doesNotMatch(boot({ page: 'goals', optIn: false, referrer: 'https://h/goals' }).topbar(), /topbar-back/);
|
|
179
|
-
|
|
180
|
-
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// task 1004477: no page draws its own Back any more, so the shell's is the
|
|
188
|
+
// record's too — the task page opened from a goal shows Back to that goal.
|
|
189
|
+
test('Back: a record page gets the bar\'s Back like every other page', () => {
|
|
190
|
+
const p = boot({ page: 'task', optIn: false, path: '/task/9', search: '?from=' + encodeURIComponent('/goals#/goal/12'), fallback: '/work' });
|
|
191
|
+
assert.match(p.topbar(), /<a class="topbar-back" id="topbar-back" href="\/goals#\/goal\/12"/);
|
|
192
|
+
assert.match(p.topbar(), />goal 12<\/span>/);
|
|
193
|
+
assert.doesNotMatch(SHELL, /rec-back|data-own-back/, 'the suppression for page-drawn links is gone');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('Back: with no origin, the page\'s data-back-fallback is the way back', () => {
|
|
197
|
+
const task = boot({ page: 'task', optIn: false, path: '/task/9', fallback: '/work' });
|
|
198
|
+
assert.match(task.topbar(), /href="\/work"/, 'a deep-linked task falls back to the board');
|
|
199
|
+
assert.match(task.topbar(), />Tasks<\/span>/);
|
|
200
|
+
const idea = boot({ page: 'idea', optIn: false, path: '/idea/4', referrer: 'https://elsewhere.example/', fallback: '/thinking' });
|
|
201
|
+
assert.match(idea.topbar(), /href="\/thinking"/, 'a cross-site arrival falls back too');
|
|
202
|
+
assert.doesNotMatch(boot({ page: 'goals', optIn: false }).topbar(), /topbar-back/, 'a page with no fallback still shows none');
|
|
203
|
+
assert.doesNotMatch(boot({ page: 'studio', optIn: false, path: '/studio', fallback: '/studio' }).topbar(), /topbar-back/,
|
|
204
|
+
'a fallback to the page itself is not a way back');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('Back: a hostile ?from= is refused, and the fallback stands in', () => {
|
|
208
|
+
const bad = ['//evil.example', '/\\evil.example', 'javascript:alert(1)', 'https://evil.example/x',
|
|
209
|
+
'/api/gds/auth/web/callback', '/lib/bongos-client.js', '/task/9'];
|
|
210
|
+
for (const from of bad) {
|
|
211
|
+
const p = boot({ page: 'task', optIn: false, path: '/task/9', search: '?from=' + encodeURIComponent(from), fallback: '/work' });
|
|
212
|
+
assert.match(p.topbar(), /id="topbar-back" href="\/work"/, `from=${JSON.stringify(from)}`);
|
|
213
|
+
}
|
|
214
|
+
for (const fallback of ['//evil.example', 'https://evil.example/', '/api/x', 'work']) {
|
|
215
|
+
assert.doesNotMatch(boot({ page: 'task', optIn: false, path: '/task/9', fallback }).topbar(), /topbar-back/,
|
|
216
|
+
`a fallback is a path on this host or nothing: ${JSON.stringify(fallback)}`);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('Back: a sidebar hop beats the fallback — the sidebar is still not a drill-down', () => {
|
|
221
|
+
const p = boot({ page: 'studio', optIn: false, path: '/studio', fallback: '/work', session: { 'hall-nav-hop': '/studio' } });
|
|
222
|
+
assert.doesNotMatch(p.topbar(), /topbar-back/);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('exits: every [data-shell-exit] link carries the bar\'s way back, in a sentence', () => {
|
|
226
|
+
const p = boot({ page: 'task', optIn: false, path: '/task/9', search: '?from=' + encodeURIComponent('/goals#/goal/12'), exits: 2 });
|
|
227
|
+
assert.deepEqual(p.exits(), [
|
|
228
|
+
{ href: '/goals#/goal/12', text: 'Back to goal 12', wired: true },
|
|
229
|
+
{ href: '/goals#/goal/12', text: 'Back to goal 12', wired: true },
|
|
230
|
+
]);
|
|
231
|
+
const board = boot({ page: 'task', optIn: false, path: '/task/9', fallback: '/work', exits: 1 });
|
|
232
|
+
assert.deepEqual(board.exits()[0], { href: '/work', text: 'Back to tasks', wired: true }, 'a nav label reads lowercase mid-sentence');
|
|
233
|
+
const lost = boot({ page: 'primer', optIn: false, path: '/primer', exits: 1 });
|
|
234
|
+
assert.deepEqual(lost.exits()[0], { href: '/', text: 'Back to home', wired: true }, 'with no way back at all, an exit goes home');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('exits: a page that renders one later asks the shell, and asking twice wires it once', () => {
|
|
238
|
+
const p = boot({ page: 'task', optIn: false, path: '/task/9', fallback: '/work', exits: 1 });
|
|
239
|
+
assert.equal(typeof p.shell().fillExits, 'function', 'the shell exposes it');
|
|
240
|
+
p.shell().fillExits();
|
|
241
|
+
assert.deepEqual(p.exits()[0], { href: '/work', text: 'Back to tasks', wired: true });
|
|
181
242
|
});
|
|
182
243
|
|
|
183
244
|
test('someone else\'s profile highlights Builders; your own hands the mark back to Profile', async () => {
|
|
@@ -191,9 +252,24 @@ test('someone else\'s profile highlights Builders; your own hands the mark back
|
|
|
191
252
|
assert.deepEqual(boot({ page: 'profile', optIn: false }).active(), ['/profile'], 'no id: your own profile');
|
|
192
253
|
});
|
|
193
254
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
255
|
+
// task 1004477: the pages that used to draw their own. Source checks, labelled
|
|
256
|
+
// as such: what each page now CARRIES is markup, and the behaviour of what it
|
|
257
|
+
// carries is the shell's, driven above.
|
|
258
|
+
test('no hall page draws its own back link; each carries the shell\'s hooks instead', () => {
|
|
259
|
+
const editor = read('tweak-editor.html');
|
|
260
|
+
assert.match(editor, /<a class="tw-back" data-shell-exit href="\/builders\/studio">/,
|
|
261
|
+
'the editor\'s bar keeps an exit, because full screen hides the top bar');
|
|
262
|
+
assert.match(editor, /<body [^>]*data-back-fallback="\/studio"/);
|
|
263
|
+
assert.match(read('tweak-editor.css'), /html:not\(\[data-fullscreen="on"\]\) \.tw-stage \.tw-back \{ display: none; \}/,
|
|
264
|
+
'and shows it only in full screen, so the page never offers two');
|
|
265
|
+
assert.match(read('idea.html'), /<body [^>]*data-back-fallback="\/thinking"/);
|
|
266
|
+
for (const f of ['primer.html', 'drachmae.html']) {
|
|
267
|
+
assert.match(read(f), /<a class="btn btn--ghost" data-shell-exit href="\/builders">/, `${f}: its error exit is the shell's`);
|
|
268
|
+
}
|
|
269
|
+
for (const f of fs.readdirSync(HALL).filter((n) => n.endsWith('.html'))) {
|
|
270
|
+
assert.doesNotMatch(read(f), /class="rec-back|data-own-back/, `${f} draws no back link of its own`);
|
|
271
|
+
}
|
|
272
|
+
assert.doesNotMatch(read('record.css'), /\.rec-back/, 'and the archetype sheet styles none');
|
|
197
273
|
});
|
|
198
274
|
|
|
199
275
|
test('Back: an origin\'s own ?from= is dropped, so one page\'s way back never chains into the next', () => {
|
|
@@ -306,7 +306,8 @@ const CLAIM_ACTION_SRC = read('claim-action.js');
|
|
|
306
306
|
// all of them, because what these tests are actually about is the REQUEST the
|
|
307
307
|
// page issues — `requests` carries paths only and is left alone so the older
|
|
308
308
|
// assertions on it keep meaning what they meant.
|
|
309
|
-
function renderTask(task, { lastClaim = null, viewer = null, goal = null, goalsList = null, patch = null, write = null, status = 200, search = '', referrer = '', withReader = true, grade, sessionLogs, claims } = {}) {
|
|
309
|
+
function renderTask(task, { lastClaim = null, viewer = null, goal = null, goalsList = null, patch = null, write = null, status = 200, search = '', referrer = '', withReader = true, grade, sessionLogs, claims, shell = false } = {}) {
|
|
310
|
+
const fills = []; // task 1004477: each time the page asked the shell to fill its exits
|
|
310
311
|
const requests = [];
|
|
311
312
|
const writes = [];
|
|
312
313
|
const toasts = [];
|
|
@@ -400,6 +401,9 @@ function renderTask(task, { lastClaim = null, viewer = null, goal = null, goalsL
|
|
|
400
401
|
confirm: () => true,
|
|
401
402
|
navigator: { clipboard: { writeText: async () => {} } },
|
|
402
403
|
};
|
|
404
|
+
// task 1004477: the shell is what fills a [data-shell-exit] link. `shell: true`
|
|
405
|
+
// stands it in so a test can see the page ask; absent, the page must still work.
|
|
406
|
+
if (shell) windowObj.OTBShell = { fillExits(root) { fills.push(root); } };
|
|
403
407
|
class CustomEvent { constructor(type, init) { this.type = type; this.detail = init && init.detail; } }
|
|
404
408
|
const sandbox = { window: windowObj, document: documentObj, location: windowObj.location, console, setTimeout, clearTimeout, Date, URL, URLSearchParams, CustomEvent };
|
|
405
409
|
sandbox.globalThis = sandbox;
|
|
@@ -410,6 +414,7 @@ function renderTask(task, { lastClaim = null, viewer = null, goal = null, goalsL
|
|
|
410
414
|
assert.ok(onReady, 'task.js registers its DOMContentLoaded boot');
|
|
411
415
|
return {
|
|
412
416
|
boot: onReady,
|
|
417
|
+
fills,
|
|
413
418
|
html: () => documentObj.getElementById('task-body').innerHTML,
|
|
414
419
|
sub: () => documentObj.getElementById('task-sub').textContent,
|
|
415
420
|
el: (id) => documentObj.getElementById(id),
|
|
@@ -536,77 +541,50 @@ test('the idea grade badge IS the archetype pill, not a private copy of it', ()
|
|
|
536
541
|
'idea.css carried a near-verbatim copy of .rec-pill down to the token triples — a hundred lines waiting to drift');
|
|
537
542
|
});
|
|
538
543
|
|
|
539
|
-
// ---- 10. the way out — the record exits
|
|
544
|
+
// ---- 10. the way out — the record exits through the shell's Back ------------
|
|
540
545
|
//
|
|
541
|
-
// task 1003468
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
assert.match(r.html(), /<a class="td-btn" href="\/goals#\/goal\/1000081">Back to goal 1000081<\/a>/);
|
|
557
|
-
assert.doesNotMatch(r.html(), /Open the work board/, 'the hardcoded exit is gone from the happy path');
|
|
558
|
-
assert.equal(r.el('task-back').href, '/goals#/goal/1000081', 'the crumb points at the goal');
|
|
559
|
-
assert.equal(r.el('task-back-label').textContent, 'Back to goal 1000081');
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
test('opened from the board, the exit is the board — on the tab it was opened from', async () => {
|
|
563
|
-
const r = renderTask(BASE_TASK, { search: '?from=' + encodeURIComponent('/work#backlog') });
|
|
564
|
-
await r.boot(); await settle();
|
|
565
|
-
assert.match(r.html(), /href="\/work#backlog">Back to the work board</);
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
test('without ?from=, a same-origin referrer is the way back', async () => {
|
|
569
|
-
const r = renderTask(BASE_TASK, { referrer: 'https://builders.cloudbongos.com/sessions?page=2' });
|
|
570
|
-
await r.boot(); await settle();
|
|
571
|
-
assert.match(r.html(), /href="\/sessions\?page=2">Back to sessions</);
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
test('a cross-origin referrer, a broken one, or none, falls back to the work board', async () => {
|
|
575
|
-
for (const referrer of ['', 'https://example.org/elsewhere', 'not a url']) {
|
|
576
|
-
const r = renderTask(BASE_TASK, { referrer });
|
|
546
|
+
// task 1003468 taught the record to exit to where it was opened from; task
|
|
547
|
+
// 1004477 moved that decision into shell.js, which now makes it for every hall
|
|
548
|
+
// page (tests/hall_fullscreen.mjs drives the real shell through ?from=, the
|
|
549
|
+
// referrer, the hostile values and the fallback). What stays the record's is
|
|
550
|
+
// PLACING the exit: an actions-row link in every render, the two failures
|
|
551
|
+
// included, marked [data-shell-exit] so the shell fills it with the same
|
|
552
|
+
// address and words as its top-bar Back — and asking the shell to, once the
|
|
553
|
+
// render that placed it has landed.
|
|
554
|
+
|
|
555
|
+
const EXIT_SLOT = /<a class="td-btn" data-shell-exit href="\/work">Back to tasks<\/a>/;
|
|
556
|
+
|
|
557
|
+
test('every render places one shell exit — the record and both honest failures', async () => {
|
|
558
|
+
const cases = [['record', 200], ['not found', 404], ['feed unavailable', 500]];
|
|
559
|
+
for (const [name, status] of cases) {
|
|
560
|
+
const r = renderTask(BASE_TASK, { status, shell: true });
|
|
577
561
|
await r.boot(); await settle();
|
|
578
|
-
assert.
|
|
579
|
-
assert.
|
|
562
|
+
assert.equal((r.html().match(/data-shell-exit/g) || []).length, 1, `${name}: one exit`);
|
|
563
|
+
assert.match(r.html(), EXIT_SLOT, `${name}: the static href is what a reader without the shell still gets`);
|
|
564
|
+
assert.doesNotMatch(r.html(), /Open the work board/, `${name}: the old hardcoded exit stays gone`);
|
|
565
|
+
assert.ok(r.fills.length >= 1, `${name}: the page asked the shell to fill the exit it placed`);
|
|
580
566
|
}
|
|
581
567
|
});
|
|
582
568
|
|
|
583
|
-
test('
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
'
|
|
589
|
-
];
|
|
590
|
-
for (const from of bad) {
|
|
591
|
-
const r = renderTask(BASE_TASK, { search: '?from=' + encodeURIComponent(from) });
|
|
592
|
-
await r.boot(); await settle();
|
|
593
|
-
assert.match(r.html(), /href="\/work">Back to the work board</, `from=${JSON.stringify(from)}`);
|
|
594
|
-
assert.equal(r.el('task-back').href, '/work', `the crumb refuses from=${JSON.stringify(from)} too`);
|
|
595
|
-
}
|
|
569
|
+
test('the page no longer decides the way back: ?from= is the shell\'s to read', async () => {
|
|
570
|
+
const r = renderTask(BASE_TASK, { search: '?from=' + encodeURIComponent('/goals#/goal/1000081'), shell: true });
|
|
571
|
+
await r.boot(); await settle();
|
|
572
|
+
assert.doesNotMatch(r.html(), /href="\/goals#\/goal\/1000081"/, 'task.js writes no origin of its own into the exit');
|
|
573
|
+
assert.doesNotMatch(TASK_SRC, /\bsafeHallPath\b|\breferrerPath\b|\bexitTarget\b|\bapplyExit\b/,
|
|
574
|
+
'the second copy of the origin logic is gone, so the two cannot drift');
|
|
596
575
|
});
|
|
597
576
|
|
|
598
|
-
test('the
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
assert.doesNotMatch(r.html(), /Open the work board/, `status ${status} still hardcodes the board`);
|
|
604
|
-
}
|
|
577
|
+
test('without a shell the record still renders, and its exit still goes somewhere', async () => {
|
|
578
|
+
const r = renderTask(BASE_TASK);
|
|
579
|
+
await r.boot(); await settle();
|
|
580
|
+
assert.match(r.html(), EXIT_SLOT);
|
|
581
|
+
assert.equal(r.fills.length, 0);
|
|
605
582
|
});
|
|
606
583
|
|
|
607
|
-
test('the crumb is
|
|
608
|
-
assert.
|
|
609
|
-
assert.match(taskHtml, /<
|
|
584
|
+
test('the crumb is the record\'s address only; the fallback rides the body', () => {
|
|
585
|
+
assert.doesNotMatch(taskHtml, /rec-back|task-back/, 'the page-drawn back link is gone');
|
|
586
|
+
assert.match(taskHtml, /<body [^>]*data-back-fallback="\/work"/, 'a deep-linked task still has a way back');
|
|
587
|
+
assert.match(taskHtml, /<p class="rec-sub" id="task-sub">/);
|
|
610
588
|
});
|
|
611
589
|
|
|
612
590
|
// ---- 11. the description is markdown, and renders as it (task 1003465) --------
|
|
@@ -635,12 +635,13 @@ try {
|
|
|
635
635
|
assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
|
|
636
636
|
assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
|
|
637
637
|
assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
|
|
638
|
-
assert.match(HTML, /shell\.js\?v=2026-10-02-
|
|
638
|
+
assert.match(HTML, /shell\.js\?v=2026-10-02-back/);
|
|
639
639
|
assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
|
|
640
640
|
for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
|
|
641
641
|
assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
|
|
642
642
|
}
|
|
643
|
-
|
|
643
|
+
// task 1004477: the bar's exit is the shell's, which fills its words.
|
|
644
|
+
assert.match(HTML, /<a class="tw-back" data-shell-exit href="\/builders\/studio">Back to studio</);
|
|
644
645
|
assert.match(HTML, />submit page</);
|
|
645
646
|
assert.match(HTML, />back to the studio</);
|
|
646
647
|
});
|
|
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
87
87
|
// the file and left the query alone, so a returning browser kept the cached copy —
|
|
88
88
|
// a fix that reaches nobody looks identical to a fix that does not work.
|
|
89
89
|
|
|
90
|
-
const SHELL_SHA = '
|
|
90
|
+
const SHELL_SHA = 'e5094c169d5ee6ef';
|
|
91
91
|
|
|
92
92
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
93
93
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|
package/tests/provision.mjs
CHANGED
|
@@ -1078,12 +1078,17 @@ await ta('scaffoldStandaloneRepo: a REGISTRY-pinned consumer control plane PACKS
|
|
|
1078
1078
|
// ADR 0361: merges publish as candidates and the platform hall runs them, so the core
|
|
1079
1079
|
// this hall runs is routinely AHEAD of the registry's `latest` label. A new project —
|
|
1080
1080
|
// and the pin refresh that runs in the same provisioning pass — must get the release.
|
|
1081
|
-
t('projectCoreDecision: the hall ahead of the release → the release; level or unreadable → the running core', () => {
|
|
1081
|
+
t('projectCoreDecision: the hall ahead of OR behind the release → the release; level or unreadable → the running core', () => {
|
|
1082
1082
|
assert.deepEqual(
|
|
1083
1083
|
(({ version, source }) => ({ version, source }))(RP.projectCoreDecision({ running: '1.19.60', released: '1.19.50' })),
|
|
1084
1084
|
{ version: '1.19.50', source: 'released' });
|
|
1085
1085
|
assert.equal(RP.projectCoreDecision({ running: '1.19.50', released: '1.19.50' }).source, 'running', 'level → byte-exact running core, no registry pack');
|
|
1086
|
-
|
|
1086
|
+
// task 1004556: a runner BEHIND the release (a patch-channel install stuck on 1.20.82
|
|
1087
|
+
// while 1.21.x was released) must not hand its stale core to a new project
|
|
1088
|
+
assert.deepEqual(
|
|
1089
|
+
(({ version, source }) => ({ version, source }))(RP.projectCoreDecision({ running: '1.20.82', released: '1.21.20' })),
|
|
1090
|
+
{ version: '1.21.20', source: 'released' }, 'behind the release → the release, across a minor too');
|
|
1091
|
+
assert.equal(RP.projectCoreDecision({ running: '1.19.40', released: '1.19.50' }).source, 'released', 'and within a minor');
|
|
1087
1092
|
assert.equal(RP.projectCoreDecision({ running: '1.19.60', released: null }).version, '1.19.60', 'no label → fail-open to the running core');
|
|
1088
1093
|
assert.equal(RP.projectCoreDecision({ running: '1.19.60', released: '1.19.50-rc.1' }).source, 'running', 'a prerelease label is not a release');
|
|
1089
1094
|
assert.equal(RP.projectCoreDecision({ running: '1.19.60', released: '1.19.50;touch /tmp/x' }).source, 'running', 'a non-semver label never reaches a shell');
|