@bongos/core 1.19.1042 → 1.19.1044
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 +64 -39
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +129 -3
- package/docs/api-reference.md +5 -3
- package/docs/copy-inventory.md +38 -38
- package/docs/copy-registry.json +41 -41
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +4 -1
- package/migrations/core_256_blocker_unlink_releases_task.sql +84 -0
- package/modules/hall-ui/public/style.css +4 -0
- package/modules/hall-ui/public/task.css +6 -0
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/task.js +17 -0
- package/modules/hall-ui/records/record-family.md +2 -0
- package/modules/ideas/blockers.js +60 -0
- package/modules/ideas/routes/blockers.js +41 -0
- package/modules/npm-release/module.json +8 -2
- package/modules/npm-release/public/task-widget.js +79 -0
- package/modules/npm-release/routes/task-where.js +48 -0
- package/modules/npm-release/work.js +70 -9
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/exec-path-guard.js +1 -1
- package/scripts/gds/seam-wiring-guard.js +4 -2
- package/scripts/hall-preview/server.js +14 -1
- package/src/bongos/serve-internal.js +18 -4
- package/src/module-api.js +1 -1
- package/src/module-loader/loader.js +13 -3
- package/tests/blocker_unlink.mjs +251 -0
- package/tests/hall_record_world.mjs +23 -1
- package/tests/module_loader.mjs +27 -0
- package/tests/npm_release_where.mjs +323 -0
package/docs/file-map.md
CHANGED
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
│ ├── hall-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R85, default-on): the authenticated builders' hall at builders.<apex>, moved from public-builders/ (~13k LOC, the largest). NO routes/db/port — declares contributes.webSurfaces:[{host:"builders.",dir:"public"}] for discovery, but UNLIKE status-ui keeps its DEDICATED serving block in serve-internal (the /builders shim + canonical redirects + citizen/Archon page gates + asset-stamping + hall-widget injection), partitioned out of the generic web-surface loop and served from BUILDERS_DIR. CLAUDE.md, module.json, records/ (one file per page family — what each family's redesign settled; CLAUDE.md points at them, and a new record is a new file there so concurrent hall tasks stop colliding on one insertion point, task 1003499), public/{index.html,builders.js,style.css,work,watch,gate,settings,ranks,sessions,primer,diagrams,atlas,...}.{html,js},cursors/,chime.wav. Four ARCHETYPE sheets sit beside the per-page ones: `record.css` (the task + idea records, task 1003305), `room.css` (the READING ROOMS — the primer + the diagrams index, task 1003306: the reading measure and rhythm, the table of contents, the progress hairline and the framed figure diagram-viewer.js upgrades on both pages), `oversight.css` (the OVERSIGHT family — government, watch, gate, task 1003469 (harbor was its fourth until task 1003890 removed the dev box): the row, the figure band inside a panel, the group head, the inline form, the grant toggle and the shared confirm modal; named oversight because the vocabulary wall bans "governance" on the government surface) and `panel.css` (the PANEL family — settings, profile, task 1003308 (pair was its third until task 1003890 removed the dev box): the row, the switch, the field with its label above, the chip control, the four voices and the sign-in gate; the hall's my-projects page was DELETED in the same task — "my projects" is the hub's user-level page, and the hall keeps no link or redirect; the family's fourth page `not-ready.html` was deleted by task 1003450, along with the emptied `requireNonXenosPage` gate that was the only thing redirecting to it). A page sheet then holds only what one page has and the others do not — `primer.css` the acknowledge gate, `diagrams.css` the legend strip, `gate.css` the hold reasons and file chips, `watch.css` the severities, the report reader and the grade trend, `settings.css` the rail layout, the group head, the token ladder and the voices grid, `profile.css` the identity band and the shelf, `people.css` the directory's identity cell (task 1003470 made the legacy door `/scouting` ship the same body and script as `/people`; task 1003440 then deleted the door — `scouting.html` and `scouting.states.json` are gone and the URL 301s to `/people`), `thinking.css` THE SKY's one sheet (task 1004231 / sky part 3 — `thinking.html` at `/thinking` is the prototype the owner adopted as the design of record, `docs/design/mocks/sky/`, built over `GET /sky` + `GET /sky/mine`; its script is the seven `sky-*.js` files on one shared `window.OTBSky`; it replaced THE ROOM of task 1004098, which survives as the list drawer, and the old `/sky` page, whose URL 301s here; `idea-objects.js` beside it is the shared five-object vocabulary).
|
|
138
138
|
│ ├── lifecycle/ ← CORE-DOMAIN carve (BV1.R86, default-on): the build state machine — tasks · claims · versions · done-when · goals · dependencies · claim→ship→grade→credit · github-push + merge-lock + conflict-resolve + publish-reconciler. The LAST, most-coupled carve; registers the `lifecycle` kernel port (createTask, classifyTaskKind, tallyPeerVotes, work-tracking reads). CLAUDE.md, module.json, routes/{tasks,claims,versions,done-when,goals,dependencies,gate-approvals,analytics}.js, lifecycle.js, github-push.js, merge-lock.js, conflict-resolve.js, publish-reconciler.js, task-visuals.js + task-visual-db.js + routes/visuals.js (the optional ship-time visual, task 1003109 — the route file is NOT named task-*.js on purpose; see the module CLAUDE.md).
|
|
139
139
|
│ ├── copy-desk/ ← CORE-DOMAIN module (task 1003113 / R02 of goal 1000074, default-on): the COPY half of the design contract — ADR 0081 gave design TOKENS a repo-owned source of truth, text had none. Owns the FLAG (anyone in any role marks a string or a whole surface as needing an artist's attention, with a reason required in BOTH the route and a CHECK constraint) and the QUEUE an artist reads it from. READS R01's committed registry (docs/copy-registry.json, generated by scripts/gds/copy-inventory.js) and never writes it. NO LIVE CMS is the hard non-goal: no column here could hold replacement text, and copy_no_cms.mjs pins that. Key is `copy-desk`, not `copy` — the branding contract already owns a `copy` block, so the bare word reds the "core carries no module code" fitness check. R03 (task 1003114, ADR 0233) added the PROPOSAL: an artist writes the replacement wording and it becomes a Bongos task carrying a fenced copy-proposal patch — no row, NO second migration (a proposals table is the forbidden column with extra steps), and scripts/gds/copy-apply.js lands it as a real diff under a claim. CLAUDE.md, module.json, registry.js, flags.js, proposals.js, routes/copy-desk.js, migrations/copy_desk_001_flags.sql, tests/{copy_flags,copy_queue,copy_no_cms,copy_proposals}.mjs. Its hall surface lives with hall-ui (public/copy-desk.{html,js,css}), per the one-key-per-web-surface precedent.
|
|
140
|
-
│ ├── npm-release/ ← the deploy page for a project that publishes an npm package from GitHub (task 1002622, default-OFF — on only in the Bongos hall, which publishes @bongos/core). "Where the work is": every task finished in the last 14 days, placed in the stage it has actually reached (not merged · merged, no version yet · published, not running here · running here, not released · released · in no version), plus the version history — each version's tasks. The version↔task join reads the release-notes.json the package itself carries (scripts/gds/release-notes.js writes it at pack time), streamed out of the NEWEST version's npm tarball in-process (no child process, no sync call) when the running core's own copy stops short. module.json, work.js (the reading + the stage placement — the packument, tarball stream and tar reader it uses are the core's src/bongos/package-registry.js, reached through the doorway's readPackageRegistry since task 1004296), routes/work.js (GET /npm-release/work, gated core.pin.move like the page), public/work.{js,css} (the two sections /deploy lends it: #nr-work, #nr-versions). Tests sit at the repo root — tests/npm_release_{work,page}.mjs — because a default-off module's own tests/ is skipped by the unit runner.
|
|
140
|
+
│ ├── npm-release/ ← the deploy page for a project that publishes an npm package from GitHub (task 1002622, default-OFF — on only in the Bongos hall, which publishes @bongos/core). "Where the work is": every task finished in the last 14 days, placed in the stage it has actually reached (not merged · merged, no version yet · published, not running here · running here, not released · released · in no version), plus the version history — each version's tasks. The version↔task join reads the release-notes.json the package itself carries (scripts/gds/release-notes.js writes it at pack time), streamed out of the NEWEST version's npm tarball in-process (no child process, no sync call) when the running core's own copy stops short. module.json, work.js (the reading + the stage placement — the packument, tarball stream and tar reader it uses are the core's src/bongos/package-registry.js, reached through the doorway's readPackageRegistry since task 1004296), routes/work.js (GET /npm-release/work, gated core.pin.move like the page), public/work.{js,css} (the two sections /deploy lends it: #nr-work, #nr-versions). Task 1004301 added the same line on a task's own page: routes/task-where.js (GET /npm-release/task/:id, any signed-in builder — it shows nothing not already public) + public/task-widget.js, injected into task.html by the loader's uiSections seam (hallWidgetScripts file task-widget.js) to fill #task-contrib on otb:task-shown. Tests sit at the repo root — tests/npm_release_{work,page}.mjs — because a default-off module's own tests/ is skipped by the unit runner.
|
|
141
141
|
│ ├── ui-design/ ← CORE-DOMAIN module (task 1003324 / ADR 0197, default-on): the design capability every builder on every instance gets — ships METHOD, never a world. Absorbs ADR 0081's layer: adapters/{claude-design,figma}/index.js (moved from src/ui/adapters/), scripts/{design-sync,figma-design-sync,validate-design}.js (moved from scripts/gds/, which keeps thin shims), docs/design-contract.md (moved from docs/). Declaration-only: contributes.skills design (the /design playbook, new) + design-sync + figma-design-sync; no routes/port/migrations, so disabling it touches no served surface. CLAUDE.md separates the platform FLOORS (the Fifteen Rule, the dark twins, no page :root, the 24px floor, AA in both modes, the kill switch) from the instance's WORLD (its packs + DESIGN.md). kit/ (task 1003320) is the look-before-you-ship kit: lib.js (the floors, the states/probes contracts, the action + expectation vocabularies, the WCAG maths, the stub spawner), serve.js (any surface through the doorway's real transforms + fixtures/), render.js (states × 1440/390/320 × dark/light, audited), probe.js (PASS/FAIL contracts), check-mock.js (platform rules + the instance's DESIGN.md taste bans, then the detector), tells.js (task 1003329 / ADR 0223: the anti-pattern detector as two tiers — floors FAIL, craft tells WARN, kit-ignore waivers per file; the rendered twins live in lib.js factsInPage), contrast.js + png.js; a page's <page>.states.json / <page>.probes.json sit BESIDE the surface (modules/public-landing/public/, modules/hall-ui/public/). Recipe: docs/recipes/ui-look-before-you-ship.md. skills/ (task 1003321 / ADR 0198) is the vendoring policy: policy.json (licence allowlist, verdicts that may land, sidecar fields, preamble markers), PREAMBLE.md (the platform preamble every vendored SKILL.md opens with), README.md (the roster — both origins scanned dangerous on 2026-08-28, landed as rebuild specs; nothing vendored — and the landing recipe); a landable copy goes under skills/vendor/<name>/ with PROVENANCE.md + LICENSE and is materialised into an instance's .claude/skills/ by claude-materialize.js step 1b. skills/<name>/ (task 1003325 / ADR 0220) are the in-house rebuilds from the specs, first-party and counted: design-taste-frontend (the default taste skill), design-taste-frontend-v1 (the earlier generation, kept distinct), high-end-visual-design (the polish pass), redesign-existing-projects (the in-place upgrade); each opens with the same preamble and carries a "Rebuilt from" section instead of a sidecar. impeccable/ (task 1003326 / ADR 0221) is the design-craft method as ONE skill with sub-commands: SKILL.md (the router, the menu, the craft floor, the bounded finish) + reference/<sub-command>.md (eighteen playbooks, one loaded per request); no hooks, no bypass agent, no telemetry, no live server. minimalist-ui/, industrial-brutalist-ui/, gpt-taste/, stitch-design-taste/ (task 1003327 / ADR 0230) are the four looks, composition grammars with no colours of their own: the palette is the instance's pack and the texture is a named entry in styles/; a substrate is an archetype that still ships both modes, gpt-taste's selection is derived from the route rather than rolled, and stitch-design-taste writes a DESIGN.md-shaped contract document whose thirteen are transcribed from the pack. image-to-code/, imagegen-frontend-web/, imagegen-frontend-mobile/, brandkit/ (task 1003328 / ADR 0222) are the image family under the hero-only rule: plates and hero objects only, the pair (JPEG dark plate + WebP alpha cutout) with a <file>.json sidecar beside each, through the instance's art pipeline key; the grove specimen's styles/grove/assets/ is the worked example. style/ (task 1003475 / ADR 0231) is the module's first FIRST-PARTY skill (no upstream, so a `## First party` section instead of `## Rebuilt from`): the /style session in which an owner authors their own look in four bounded steps, rendering the nearest library entries before asking, drafting inside the contract, retuning by measurement, and handing over the one-step adoption without ever writing config/branding.json. styles/ (task 1003323 / ADR 0219) is the style library: <name>/pack.json (the theme.ui overlay, exactly the fifteen) + DESIGN.md (the look's derived recipes, thirteen in lockstep with the pack) + mock.html (the specimen); chrome-world (pinned to the neutral pack), expedition, grove, blueprint (the first cool look, authored through /style at task 1003475); STYLE=<name> on the kit lays a look over the instance's pack through GDS_BRANDING_FILE; tests/ui_design_styles.mjs runs the world/cosmos/hall token suites once per alternate (UI_DESIGN_PACK). config/design-tokens.* + design-sources.json STAY in config/ as the host boundary; public-landing keeps its own key (first customer, not owner).
|
|
142
142
|
│ ├── character-anim/ ← consistent-character generative animation (ADR 0088) — graded painterly frame sets + manifest, the otb-character-review skill. CLAUDE.md, module.json.
|
|
143
143
|
│ ├── provisioning/ ← stands up + operates a Cloud Bongos instance's own infra (server/DB/domain/DNS/TLS) via the API (ADR 0111); web-tier request+state only, no cloud tokens — a separate control-plane runner drains the intents. Distinct from the `hosting` module (ADR 0092), which brokers compute workloads. CLAUDE.md, module.json.
|
|
@@ -2571,5 +2571,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2571
2571
|
1.19.0 precedent, riding the next CI patch as joinabilityMode did.
|
|
2572
2572
|
1.19.1042 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2573
2573
|
landed since 1.19.1041 with no explicit bump. run 36254852933. (task 1002620)
|
|
2574
|
+
1.19.1043 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2575
|
+
landed since 1.19.1042 with no explicit bump. run 36258640780. (task 1002620)
|
|
2576
|
+
1.19.1044 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2577
|
+
landed since 1.19.1043 with no explicit bump. run 36260641494. (task 1002620)
|
|
2574
2578
|
---------------------------------------------------------------------------
|
|
2575
2579
|
```
|
package/docs/modules-contract.md
CHANGED
|
@@ -98,7 +98,10 @@ Every module must have a `module.json` at its root (`modules/<key>/module.json`)
|
|
|
98
98
|
"pollers": ["box-lifecycle"], // background pollers (start/stop lifecycle)
|
|
99
99
|
"disciplines": ["artist"], // builder disciplines this module adds
|
|
100
100
|
"skills": ["otb-tile-generate"], // slash-command skills it ships
|
|
101
|
-
"uiSections": ["boxes"], // hall/status UI sections gated by window.__MODULES__
|
|
101
|
+
"uiSections": ["boxes"], // hall/status UI sections gated by window.__MODULES__. Ship public/hall-widget.js
|
|
102
|
+
// and it is injected into the hall index; ship public/task-widget.js and it
|
|
103
|
+
// is injected into a task's page before task.js, to fill #task-contrib on
|
|
104
|
+
// `otb:task-shown` (task 1004301). The page itself names no module.
|
|
102
105
|
"webSurfaces": [{ "host": "status.", "dir": "public" }], // static web surfaces: a Host PREFIX (BV1.R84)…
|
|
103
106
|
// …or { "apexRoot": true, "dir": "public" } for the
|
|
104
107
|
// apex root (ADR 0218). One or the other, never both.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
-- core_256_blocker_unlink_releases_task.sql — unlinking a blocker from a task
|
|
2
|
+
-- must release that task when it was the last thing holding it (task 1003518).
|
|
3
|
+
--
|
|
4
|
+
-- THE GAP. `POST /blockers/:id/link` parks a 'ready' task in 'blocked' so the
|
|
5
|
+
-- resolve trigger (009, tightened by core_223) has something to promote. Nothing
|
|
6
|
+
-- removed a link. Post-core_223 a task is released only when its LAST open
|
|
7
|
+
-- blocker resolves, so a wrong link — wrong task id, a duplicate filed by two
|
|
8
|
+
-- people, a blocker later understood to be irrelevant — was permanent. The only
|
|
9
|
+
-- escape was to resolve a blocker that was never real, putting a falsehood in
|
|
10
|
+
-- the blocker ledger to clear a bookkeeping artifact. core_223's own header
|
|
11
|
+
-- names this: "there is no unlink route to detach one first."
|
|
12
|
+
--
|
|
13
|
+
-- WHY A TRIGGER AND NOT THE ROUTE. tests/blocker_resolve_guard.mjs pins that
|
|
14
|
+
-- modules/ideas/blockers.js never writes `tasks.status = 'ready'` itself: "if it
|
|
15
|
+
-- ever started UPDATE-ing tasks.status, this guard would be bypassable and the
|
|
16
|
+
-- fix would need to live in two places." An unlink that promoted in JS would be
|
|
17
|
+
-- exactly that second place — a hand-copy of the predicate below, free to drift
|
|
18
|
+
-- from it. Putting the release here keeps ONE definition of "this task has
|
|
19
|
+
-- nothing open against it any more", and has a second payoff: the direct
|
|
20
|
+
-- `DELETE FROM task_blockers` that was the only workaround before this task now
|
|
21
|
+
-- does the right thing too, instead of silently leaving the task stranded.
|
|
22
|
+
--
|
|
23
|
+
-- NO `b.id <> …` EXCLUSION, AND THAT IS THE DIFFERENCE FROM core_223. That
|
|
24
|
+
-- migration runs in a BEFORE UPDATE trigger, where the blocker being resolved
|
|
25
|
+
-- still reads 'open' in the table and would count itself as an open blocker —
|
|
26
|
+
-- so without the exclusion nothing would ever promote. This is an AFTER DELETE
|
|
27
|
+
-- trigger: the link row is already gone when the body runs, so the NOT EXISTS
|
|
28
|
+
-- already sees exactly the blockers that remain. Adding the exclusion here would
|
|
29
|
+
-- be cargo-cult; omitting it is deliberate.
|
|
30
|
+
--
|
|
31
|
+
-- Deliberately NOT gating on dependencies, for core_223's reason: the
|
|
32
|
+
-- dep-promotion triggers (163) only ever flip 'backlog' -> 'ready', never
|
|
33
|
+
-- 'blocked' -> 'ready', so a task this left in 'blocked' for unshipped deps
|
|
34
|
+
-- would have no release path at all. The claim path re-checks DEPS_NOT_SHIPPED
|
|
35
|
+
-- as its second line of defence. Blocker-clearing and dep-clearing stay separate.
|
|
36
|
+
--
|
|
37
|
+
-- ONLY 'blocked' IS REWRITTEN. A task that was never parked (link arrived while
|
|
38
|
+
-- it was active/backlog/shipped — see LINK_BLOCKABLE_STATUSES) is left exactly
|
|
39
|
+
-- as it is, so an unlink can never launder a task past the metic+archon promote
|
|
40
|
+
-- gate.
|
|
41
|
+
--
|
|
42
|
+
-- Legacy markdown-anchor links (a task_blockers row with blocker_ref and no
|
|
43
|
+
-- blocker_id) are out of scope, the same scope core_223 and 176 draw: the JOIN
|
|
44
|
+
-- below only sees structured links, so a legacy row neither blocks nor releases.
|
|
45
|
+
--
|
|
46
|
+
-- CASCADE SAFETY. task_blockers.task_id and .blocker_id are both ON DELETE
|
|
47
|
+
-- CASCADE. Nothing in the repo hard-deletes a task (abandon is terminal) or a
|
|
48
|
+
-- blocker (resolve is), so in practice this fires only on a real unlink. If a
|
|
49
|
+
-- blocker ever were deleted outright, releasing the tasks it held is the correct
|
|
50
|
+
-- reading — the thing that was blocking them no longer exists.
|
|
51
|
+
--
|
|
52
|
+
-- Idempotent: CREATE OR REPLACE for the function, DROP IF EXISTS + CREATE for
|
|
53
|
+
-- the trigger (a trigger has no CREATE OR REPLACE before PG 14, and this must
|
|
54
|
+
-- apply cleanly on a re-run).
|
|
55
|
+
|
|
56
|
+
BEGIN;
|
|
57
|
+
|
|
58
|
+
CREATE OR REPLACE FUNCTION trg_release_task_on_blocker_unlink()
|
|
59
|
+
RETURNS TRIGGER AS $$
|
|
60
|
+
BEGIN
|
|
61
|
+
UPDATE tasks
|
|
62
|
+
SET status = 'ready', updated_at = now()
|
|
63
|
+
WHERE id = OLD.task_id
|
|
64
|
+
AND status = 'blocked'
|
|
65
|
+
-- … and nothing open is left against this task (core_223's predicate,
|
|
66
|
+
-- minus the self-exclusion an AFTER DELETE trigger does not need).
|
|
67
|
+
AND NOT EXISTS (
|
|
68
|
+
SELECT 1
|
|
69
|
+
FROM task_blockers tb
|
|
70
|
+
JOIN blockers b ON b.id = tb.blocker_id
|
|
71
|
+
WHERE tb.task_id = OLD.task_id
|
|
72
|
+
AND b.status = 'open'
|
|
73
|
+
);
|
|
74
|
+
RETURN OLD;
|
|
75
|
+
END;
|
|
76
|
+
$$ LANGUAGE plpgsql;
|
|
77
|
+
|
|
78
|
+
DROP TRIGGER IF EXISTS release_task_on_blocker_unlink ON task_blockers;
|
|
79
|
+
CREATE TRIGGER release_task_on_blocker_unlink
|
|
80
|
+
AFTER DELETE ON task_blockers
|
|
81
|
+
FOR EACH ROW
|
|
82
|
+
EXECUTE FUNCTION trg_release_task_on_blocker_unlink();
|
|
83
|
+
|
|
84
|
+
COMMIT;
|
|
@@ -3151,6 +3151,10 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
|
|
|
3151
3151
|
min-width: 0;
|
|
3152
3152
|
max-width: 100%;
|
|
3153
3153
|
}
|
|
3154
|
+
/* The 24px floor (task 1004301 found it failing): at 13px the base field's padding left a
|
|
3155
|
+
number input 21px tall beside a select that clears it — the task page's "Wait for task"
|
|
3156
|
+
box. Sized to sit level with its select. */
|
|
3157
|
+
.td-field input { min-height: 26px; box-sizing: border-box; }
|
|
3154
3158
|
.td-form .td-actions, .td-form-hint, .td-form .td-action-note { grid-column: 1 / -1; }
|
|
3155
3159
|
.td-form .td-actions { margin: 4px 0 0; }
|
|
3156
3160
|
.td-form-hint { font-size: 12.5px; color: var(--ink-soft); margin: 0; }
|
|
@@ -139,3 +139,9 @@
|
|
|
139
139
|
.tk-axis:nth-child(3) { border-left: 0; padding-left: 0; }
|
|
140
140
|
.tk-lrow { grid-template-columns: 1fr; gap: 6px; }
|
|
141
141
|
}
|
|
142
|
+
|
|
143
|
+
/* The module slot under the rail (task 1004301): a module's task-widget.js may write one
|
|
144
|
+
line of its own here (the npm-release module says which version carries the task).
|
|
145
|
+
Empty unless a module fills it, and then it takes no room at all. */
|
|
146
|
+
.tk-contrib { margin: var(--sp-md) 0 0; }
|
|
147
|
+
.tk-contrib:empty { display: none; }
|
|
@@ -69,6 +69,6 @@
|
|
|
69
69
|
<script src="/builders/brand-apply.js?v=2026-06-19-r56" defer></script>
|
|
70
70
|
<script src="/builders/md-reader.js" defer></script>
|
|
71
71
|
<script src="/builders/hall-visual.js?v=2026-09-16-visual1" defer></script>
|
|
72
|
-
<script src="/builders/task.js?v=2026-
|
|
72
|
+
<script src="/builders/task.js?v=2026-09-26-taskwhere" defer></script>
|
|
73
73
|
</body>
|
|
74
74
|
</html>
|
|
@@ -634,6 +634,11 @@
|
|
|
634
634
|
['Priority', t.priority == null ? null : 'P' + t.priority],
|
|
635
635
|
]));
|
|
636
636
|
parts.push(lifecycleRail(t, creditVal));
|
|
637
|
+
// A slot a MODULE may fill (task 1004301): the module's public/task-widget.js is
|
|
638
|
+
// injected just before this script and writes into it on `otb:task-shown` below. The
|
|
639
|
+
// page itself names no module and draws nothing here — with none enabled it stays
|
|
640
|
+
// empty, and `:empty` in task.css collapses it.
|
|
641
|
+
parts.push('<div class="tk-contrib" id="task-contrib" aria-live="polite"></div>');
|
|
637
642
|
|
|
638
643
|
// Claim history (task 751). The most recent holder leads; since task 1003437
|
|
639
644
|
// the read also carries the FULL claim ledger (?include=claims), so a task
|
|
@@ -806,6 +811,7 @@
|
|
|
806
811
|
|
|
807
812
|
const body = $('#task-body');
|
|
808
813
|
if (body) body.innerHTML = parts.join('');
|
|
814
|
+
announceShown(t);
|
|
809
815
|
|
|
810
816
|
const copyBtn = $('#copy-prompt-btn');
|
|
811
817
|
if (copyBtn) {
|
|
@@ -958,6 +964,17 @@
|
|
|
958
964
|
// The two honest failures. Both keep the <h1> — the empty and error renders are
|
|
959
965
|
// renders like any other, and a page whose only heading lives on the happy path
|
|
960
966
|
// has none in the states a reader is most likely to hit.
|
|
967
|
+
// Tell any module widget which task is on screen (task 1004301). Fired after EVERY
|
|
968
|
+
// render, because render() rebuilds #task-body wholesale and the slot a widget filled a
|
|
969
|
+
// moment ago is gone. The last task shown is also left on window.OTBTaskShown for a
|
|
970
|
+
// widget that loads after the first announcement. Only the id and the status travel:
|
|
971
|
+
// a widget that needs more asks its own module's route, which applies its own gate.
|
|
972
|
+
function announceShown(t) {
|
|
973
|
+
const detail = { id: String(t.id), status: t.status || null };
|
|
974
|
+
window.OTBTaskShown = detail;
|
|
975
|
+
if (typeof CustomEvent === 'function') document.dispatchEvent(new CustomEvent('otb:task-shown', { detail }));
|
|
976
|
+
}
|
|
977
|
+
|
|
961
978
|
function renderNotFound(id) {
|
|
962
979
|
const body = $('#task-body');
|
|
963
980
|
if (body) body.innerHTML = `
|
|
@@ -11,3 +11,5 @@
|
|
|
11
11
|
**The task record's grade panel, session log and claim ledger (task 1003437).** Two net-new regions the owner approved at the composition round (direction doc §6 decision 4) sit between Shipped and Actions on `task.html`. Four rules carry them: all three ride the ONE task read as opt-in siblings — `GET /tasks/:id?include=last_claim,grade,session_logs,claims`; the two new reads (`getTaskSessionLogs`, `getTaskClaims` in `modules/lifecycle/db-claim-reads.js`) are keyed by `task_id`, newest first, **rank never returned**; absent → **NOTHING** (the ship-visual precedent), except a grade row whose panel was `unavailable`, which is data and says so in one line; and verdicts and outcomes wear the fixed verification vocabulary through `.rec-pill--ok/warn/danger`, **never the accent**. Pinned in `tests/hall_record_world.mjs` (page) and `tests/task_detail_includes.mjs` (route + reads).
|
|
12
12
|
|
|
13
13
|
**The way into and out of a task record (task 1003468, ADR 0229).** Since 1759/1869 a to-do card's BODY toggles selection so a builder can bulk-claim — which is why the owner could not open a card, the only door being a 26px ⤢ glyph nobody found. Now **the title is the door**: `.task__link` on both surfaces that render the card (`work.js`, `goals-render.js`), bought up to the 24px floor with the `.scroll__lede a` recipe; both click handlers let it through; the glyph is gone. **The link carries where it was opened from** — `OTBTaskDetail.href(id)` appends `?from=<path+query+hash>` (a query param so the origin survives a reload and keeps a goal's `#/goal/N`, which a referrer drops) — and `task.js` resolves the exit once (`?from=` → same-origin referrer → the work board), points the crumb **before** the fetch so the way back exists in every state, and refuses anything that is not a path on this host. **Never write a bare `/task/N` link from a list again — call `OTBTaskDetail.href(id)`.** Pinned by §10 of `tests/hall_record_world.mjs` and §7 of `tests/hall_board_world.mjs`.
|
|
14
|
+
|
|
15
|
+
**The task page carries a module slot (task 1004301).** Under the lifecycle rail sits `#task-contrib`, empty and collapsed (`.tk-contrib:empty`) unless a module fills it, and `task.js` announces every render as `otb:task-shown` with `{ id, status }` (also left on `window.OTBTaskShown` for a late widget). The page names no module: the server injects each enabled module's `public/task-widget.js` just before `task.js` (the loader's uiSections seam, `hallWidgetScripts({ file: 'task-widget.js' })`), so both run deferred in order and the listener exists before the first announcement. Render rebuilds `#task-body` wholesale, which is why the announcement is per render, not per load. The first filler is npm-release's line — "In 1.19.1036 · running here · not released to other projects yet", or "Done, but not merged" for a stranded ship. Tests: `tests/hall_record_world.mjs` (slot + announcement), `tests/npm_release_where.mjs` (the widget and the seam).
|
|
@@ -147,6 +147,65 @@ async function linkBlockerToTask({ blockerId, taskId }) {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
// Unlink a blocker from a task — the missing inverse of linkBlockerToTask
|
|
151
|
+
// (task 1003518). Linking is one-way and Metic+, so before this a wrong link
|
|
152
|
+
// (wrong task id, a duplicate filed by two people, a blocker later understood
|
|
153
|
+
// to be irrelevant) could only be undone by a direct DELETE on task_blockers.
|
|
154
|
+
//
|
|
155
|
+
// LIKE resolveBlocker, THIS ONLY OBSERVES. The release lives in a DB trigger
|
|
156
|
+
// (core_256), for the reason tests/blocker_resolve_guard.mjs states about the
|
|
157
|
+
// resolve path: a promotion written here would be a second copy of core_223's
|
|
158
|
+
// predicate, free to drift from it. One DELETE, then a read-back of what the
|
|
159
|
+
// trigger did — the same shape resolveBlocker uses for `promoted_tasks`.
|
|
160
|
+
//
|
|
161
|
+
// The three statements share a transaction so `released` cannot be a lie told
|
|
162
|
+
// by a concurrent writer between the delete and the read-back.
|
|
163
|
+
//
|
|
164
|
+
// `poolOverride` is injected by tests only, and is named so it cannot shadow the
|
|
165
|
+
// module-level `pool` this file closes over.
|
|
166
|
+
async function unlinkBlockerFromTask({ blockerId, taskId }, { pool: poolOverride = pool } = {}) {
|
|
167
|
+
const client = await poolOverride.connect();
|
|
168
|
+
try {
|
|
169
|
+
await client.query('BEGIN');
|
|
170
|
+
// Status BEFORE, so `released` means "this unlink moved it", not merely
|
|
171
|
+
// "it is ready now" — a task linked while it was never blockable would
|
|
172
|
+
// otherwise report a release that never happened (the task-1002559 lesson:
|
|
173
|
+
// a write that reports success without saying what changed hides a dead
|
|
174
|
+
// loop). FOR UPDATE so the row cannot move under the read-back below.
|
|
175
|
+
const { rows: before } = await client.query(
|
|
176
|
+
`SELECT status FROM tasks WHERE id = $1 FOR UPDATE`,
|
|
177
|
+
[taskId]
|
|
178
|
+
);
|
|
179
|
+
const { rows: removed } = await client.query(
|
|
180
|
+
`DELETE FROM task_blockers
|
|
181
|
+
WHERE task_id = $1 AND blocker_id = $2
|
|
182
|
+
RETURNING task_id`,
|
|
183
|
+
[taskId, blockerId]
|
|
184
|
+
);
|
|
185
|
+
// Answer honestly when nothing was there: a missing link is a refusal, not
|
|
186
|
+
// a silent success a caller would read as "detached".
|
|
187
|
+
if (removed.length === 0) {
|
|
188
|
+
await client.query('ROLLBACK');
|
|
189
|
+
const e = new Error('no such blocker link on that task');
|
|
190
|
+
e.code = 'LINK_NOT_FOUND';
|
|
191
|
+
throw e;
|
|
192
|
+
}
|
|
193
|
+
const { rows: after } = await client.query(
|
|
194
|
+
`SELECT status FROM tasks WHERE id = $1`,
|
|
195
|
+
[taskId]
|
|
196
|
+
);
|
|
197
|
+
await client.query('COMMIT');
|
|
198
|
+
const was = before[0] && before[0].status;
|
|
199
|
+
const now = after[0] && after[0].status;
|
|
200
|
+
return { unlinked: true, released: was === 'blocked' && now === 'ready' };
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (err.code !== 'LINK_NOT_FOUND') await client.query('ROLLBACK');
|
|
203
|
+
throw err;
|
|
204
|
+
} finally {
|
|
205
|
+
client.release();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
150
209
|
module.exports = {
|
|
151
210
|
listOpenBlockers,
|
|
152
211
|
countOpenBlockers,
|
|
@@ -154,6 +213,7 @@ module.exports = {
|
|
|
154
213
|
createBlocker,
|
|
155
214
|
resolveBlocker,
|
|
156
215
|
linkBlockerToTask,
|
|
216
|
+
unlinkBlockerFromTask,
|
|
157
217
|
blocksOnLink,
|
|
158
218
|
LINK_BLOCKABLE_STATUSES,
|
|
159
219
|
};
|
|
@@ -145,5 +145,46 @@ module.exports = function buildBlockersRouter() {
|
|
|
145
145
|
}
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
+
// Unlink a blocker from a task — the inverse of /link (task 1003518). Both ids
|
|
149
|
+
// sit in the PATH, matching DELETE /tasks/:id/dependencies/:depId, the closest
|
|
150
|
+
// existing shape: an edge between two rows, removed by naming both.
|
|
151
|
+
//
|
|
152
|
+
// Post-core_223 a task is released only when its LAST open blocker resolves,
|
|
153
|
+
// so a wrong link is permanent: the only way out was to resolve a blocker that
|
|
154
|
+
// was never real, putting a falsehood in the blocker ledger to clear a
|
|
155
|
+
// bookkeeping artifact. This is the honest way out.
|
|
156
|
+
//
|
|
157
|
+
// Promotion is the TRIGGER's job here exactly as it is on /resolve: core_256
|
|
158
|
+
// fires AFTER DELETE on task_blockers and re-uses core_223's predicate, so
|
|
159
|
+
// "nothing open is left against this task" has one definition, not two.
|
|
160
|
+
// blockers.unlinkBlockerFromTask only deletes and reads back what happened.
|
|
161
|
+
//
|
|
162
|
+
// `released` says whether the task actually moved, never a bare {ok:true}
|
|
163
|
+
// (task 1002559 — a write that reports success without saying what changed is
|
|
164
|
+
// how a dead loop hides).
|
|
165
|
+
// Metic+ (#360 / ADR 0018) — same blocker-review grant as link and resolve.
|
|
166
|
+
// rank: metic+archon — unlink a blocker from a task; same blocker-review grant as link.
|
|
167
|
+
router.delete('/blockers/:id/link/:taskId', api.requireBuilder, api.requirePermission('blocker.review'), async (req, res) => {
|
|
168
|
+
// Both ids strict positive integers (`positiveInt`), via the shared parseId
|
|
169
|
+
// rather than a hand-rolled check: a `7.5` would otherwise pass a bare
|
|
170
|
+
// isFinite and then simply miss on the DELETE, answering "not linked" for
|
|
171
|
+
// what is really a malformed id. The sibling link/resolve routes predate
|
|
172
|
+
// that option; this one has no reason to be looser than it can be.
|
|
173
|
+
const id = parseId(req, res, { code: 'bad_id', positiveInt: true });
|
|
174
|
+
if (id === null) return;
|
|
175
|
+
const taskId = parseId(req, res, { param: 'taskId', code: 'bad_task_id', positiveInt: true });
|
|
176
|
+
if (taskId === null) return;
|
|
177
|
+
try {
|
|
178
|
+
const { released } = await blockers.unlinkBlockerFromTask({ blockerId: id, taskId });
|
|
179
|
+
res.json({ ok: true, unlinked: true, released });
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (err.code === 'LINK_NOT_FOUND') {
|
|
182
|
+
return res.fail('link_not_found', { status: 404, message: 'that blocker is not linked to that task' });
|
|
183
|
+
}
|
|
184
|
+
console.error('[gds] DELETE /blockers/:id/link/:taskId', err);
|
|
185
|
+
res.fail('unlink_failed', { status: 500, message: 'internal error' });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
148
189
|
return router;
|
|
149
190
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"key": "npm-release",
|
|
3
3
|
"title": "npm release",
|
|
4
|
-
"description": "The deploy page for a project that publishes an npm package from its GitHub repo — where every recently shipped task has got to (not merged, merged, in a version, live, released) and each version's list of work, read from the release notes the package itself carries (task 1002622). Built for the Bongos hall, which publishes @bongos/core; off everywhere else.",
|
|
4
|
+
"description": "The deploy page for a project that publishes an npm package from its GitHub repo — where every recently shipped task has got to (not merged, merged, in a version, live, released) and each version's list of work, read from the release notes the package itself carries (task 1002622) — and the same line on a task's own page (task 1004301). Built for the Bongos hall, which publishes @bongos/core; off everywhere else.",
|
|
5
5
|
"coreVersion": "^1.19.0",
|
|
6
6
|
"default": false,
|
|
7
7
|
"author": "Cloud Bongos",
|
|
@@ -12,7 +12,13 @@
|
|
|
12
12
|
"status": "core-maintained"
|
|
13
13
|
},
|
|
14
14
|
"contributes": {
|
|
15
|
-
"routes": [
|
|
15
|
+
"routes": [
|
|
16
|
+
"work",
|
|
17
|
+
"task-where"
|
|
18
|
+
],
|
|
19
|
+
"uiSections": [
|
|
20
|
+
"task-where"
|
|
21
|
+
]
|
|
16
22
|
},
|
|
17
23
|
"provides": [],
|
|
18
24
|
"consumes": []
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// modules/npm-release/public/task-widget.js — the line a task's own page shows about where
|
|
2
|
+
// its code is (task 1004301): "In 1.19.1036 · running here · not released yet", or that a
|
|
3
|
+
// task marked done never reached main — which is how a stranded ship becomes visible to the
|
|
4
|
+
// person who shipped it.
|
|
5
|
+
//
|
|
6
|
+
// HOW IT GETS THERE. The core's task page names no module. It renders an empty slot
|
|
7
|
+
// (#task-contrib) and announces each render as `otb:task-shown`; the server injects this file
|
|
8
|
+
// just before task.js because the module declares a uiSection and ships task-widget.js
|
|
9
|
+
// (src/module-loader/loader.js hallWidgetScripts, file: 'task-widget.js'). Module off: this
|
|
10
|
+
// file is never injected, its asset 404s, and the slot stays empty.
|
|
11
|
+
//
|
|
12
|
+
// THE PAGE KNOWS NOTHING (deploy.js's rule, kept). The stage and version are the server's
|
|
13
|
+
// reading; a partial reading says so; any failure draws no line rather than a wrong one.
|
|
14
|
+
|
|
15
|
+
(() => {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const API = '/api/bongos';
|
|
19
|
+
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
20
|
+
const client = window.BongosClient && window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
21
|
+
const DONE = ['completed', 'confirmed', 'shipped'];
|
|
22
|
+
|
|
23
|
+
// One reading per task per page load: the page re-renders on every action, and each
|
|
24
|
+
// render re-announces, but where the code is does not change between clicks. Bounded —
|
|
25
|
+
// a page shows one task, and the overlay can walk to a few more; nothing needs more than
|
|
26
|
+
// the last handful.
|
|
27
|
+
const cache = new Map();
|
|
28
|
+
const CACHE_MAX = 8;
|
|
29
|
+
|
|
30
|
+
// The line for each stage, in the deploy page's own words (its stage names), shortened to
|
|
31
|
+
// read as one sentence under a task's lifecycle rail.
|
|
32
|
+
const REST = {
|
|
33
|
+
unversioned: () => 'Merged, waiting for the next published version.',
|
|
34
|
+
not_live: (w) => `published, not running here yet${w.offered ? ' · already offered to other projects' : ''}.`,
|
|
35
|
+
live_unreleased: () => 'running here · not released to other projects yet.',
|
|
36
|
+
released: (w) => `${w.live ? 'running here · ' : ''}released to other projects.`,
|
|
37
|
+
// Said with the reason a builder would recognise, the way "Done, but not merged" is: this
|
|
38
|
+
// is most often work that changed no code, and only sometimes a merge that lost its name.
|
|
39
|
+
no_version: () => 'Shipped, though no published version lists it — usually a change that needed no code, like a scope edit or a check, and now and then a merge whose note did not name this task.',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function lineHtml(w) {
|
|
43
|
+
if (w.stage === 'not_merged') {
|
|
44
|
+
return '<p class="rec-meta nr-where nr-where--stranded"><b>Done, but not merged.</b> The ledger marks this task done, and its code is not on main yet — a ship that has not landed.</p>';
|
|
45
|
+
}
|
|
46
|
+
const rest = REST[w.stage];
|
|
47
|
+
if (!rest) return '';
|
|
48
|
+
const lead = w.version ? `In <b>${esc(w.version)}</b> · ` : '';
|
|
49
|
+
const partial = w.partial ? ' (a partial reading: the registry or the release notes could not be read)' : '';
|
|
50
|
+
return `<p class="rec-meta nr-where">${lead}${esc(rest(w) + partial)}</p>`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function reading(id) {
|
|
54
|
+
if (cache.has(id)) return cache.get(id);
|
|
55
|
+
const p = (async () => {
|
|
56
|
+
if (!client) return null;
|
|
57
|
+
const res = await client.request('GET', `${API}/npm-release/task/${encodeURIComponent(id)}`);
|
|
58
|
+
return res && res.status === 200 && res.data ? res.data : null;
|
|
59
|
+
})().catch(() => null);
|
|
60
|
+
cache.set(id, p);
|
|
61
|
+
while (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
|
|
62
|
+
return p;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function show(detail) {
|
|
66
|
+
if (!detail || !/^\d{1,12}$/.test(String(detail.id || ''))) return;
|
|
67
|
+
if (!DONE.includes(detail.status)) return; // nothing to place until it is done
|
|
68
|
+
const w = await reading(String(detail.id));
|
|
69
|
+
// The page may have re-rendered while the read was in flight: write into the slot that
|
|
70
|
+
// exists NOW, and only for the task it is still showing.
|
|
71
|
+
const slot = document.getElementById('task-contrib');
|
|
72
|
+
const shown = window.OTBTaskShown;
|
|
73
|
+
if (!slot || !w || (shown && String(shown.id) !== String(detail.id))) return;
|
|
74
|
+
slot.innerHTML = lineHtml(w);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
document.addEventListener('otb:task-shown', (e) => { show(e && e.detail); });
|
|
78
|
+
if (window.OTBTaskShown) show(window.OTBTaskShown);
|
|
79
|
+
})();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/npm-release/routes/task-where.js — GET /npm-release/task/:id, the one line a
|
|
4
|
+
// task's own page shows about where its code is (task 1004301): which version carries it,
|
|
5
|
+
// whether this hall runs that version, whether it is released — or that a task marked done
|
|
6
|
+
// never reached main. See readTaskWhere in ../work.js for the reading.
|
|
7
|
+
//
|
|
8
|
+
// ANY SIGNED-IN BUILDER, NOT THE DEPLOY PAGE'S ATOM. Every builder can open a task page, so
|
|
9
|
+
// the question the task asked was what this line may show them. It shows nothing that is
|
|
10
|
+
// not already public: the version naming the task is in the package's own release notes on
|
|
11
|
+
// npm, the core this hall runs is what the public GET /version reports, and "released" is
|
|
12
|
+
// the registry's `latest` label. The deploy page stays on `core.pin.move` for what it adds
|
|
13
|
+
// beyond that — the ledger of upgrades, the door. requireBuilder rather than nothing, like
|
|
14
|
+
// the task page's own read, so the line is never a signed-out probe.
|
|
15
|
+
//
|
|
16
|
+
// READ-ONLY.
|
|
17
|
+
|
|
18
|
+
const express = require('express');
|
|
19
|
+
const api = require('../../../src/module-api');
|
|
20
|
+
const { readTaskWhere } = require('../work');
|
|
21
|
+
|
|
22
|
+
const log = api.logger('npm-release');
|
|
23
|
+
|
|
24
|
+
module.exports = function buildNpmReleaseTaskWhereRouter({ read = readTaskWhere } = {}) {
|
|
25
|
+
const router = express.Router();
|
|
26
|
+
|
|
27
|
+
router.get(
|
|
28
|
+
'/npm-release/task/:id',
|
|
29
|
+
api.requireBuilder,
|
|
30
|
+
async (req, res) => {
|
|
31
|
+
const id = String((req.params && req.params.id) || '');
|
|
32
|
+
if (!/^\d{1,12}$/.test(id)) {
|
|
33
|
+
res.status(400).json({ error: { code: 'bad_task_id', message: 'A task id is a number.' } });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const where = await read({ pool: api.pool, id });
|
|
38
|
+
if (!where) { res.status(404).json({ error: { code: 'task_not_found', message: `No task ${id}.` } }); return; }
|
|
39
|
+
res.json(where);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
log.error(`npm-release task-where read failed: ${e && e.message}`);
|
|
42
|
+
res.status(500).json({ error: { code: 'task_where_failed', message: 'Could not read where this task\'s code is right now.' } });
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return router;
|
|
48
|
+
};
|
|
@@ -57,9 +57,16 @@ function configuredPackage(env = process.env) {
|
|
|
57
57
|
return v || CORE_PACKAGE;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// READ ONCE PER PROCESS. The file ships inside the installed package, so it cannot change
|
|
61
|
+
// while this process runs — an upgrade replaces the package AND restarts the server. The
|
|
62
|
+
// deploy page's reading reached it rarely; a task's own page (task 1004301) reaches it on
|
|
63
|
+
// every view by every builder, and a synchronous disk read has no business on that path.
|
|
64
|
+
let installedNotesMemo;
|
|
60
65
|
function readInstalledNotes() {
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
if (installedNotesMemo !== undefined) return installedNotesMemo;
|
|
67
|
+
try { installedNotesMemo = JSON.parse(fs.readFileSync(INSTALLED_NOTES, 'utf8')); }
|
|
68
|
+
catch { installedNotesMemo = null; }
|
|
69
|
+
return installedNotesMemo;
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
function boundText(text) {
|
|
@@ -194,12 +201,9 @@ function clampInt(v, dflt, max) {
|
|
|
194
201
|
* `now`, `store` and `installedNotes` are the seams tests drive; `live` defaults to this
|
|
195
202
|
* process's own core version when the package is the core.
|
|
196
203
|
*/
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
} = {}) {
|
|
201
|
-
const windowDays = clampInt(days, DEFAULT_DAYS, MAX_DAYS);
|
|
202
|
-
const pageLimit = clampInt(limit, DEFAULT_LIMIT, MAX_LIMIT);
|
|
204
|
+
// What versions exist and what each carried — the half of the reading both the deploy page
|
|
205
|
+
// and a task's own page need (task 1004301 split it out of readWork unchanged).
|
|
206
|
+
async function readVersions({ pkg, live, installedNotes, get, now, store }) {
|
|
203
207
|
const running = live !== undefined ? live : (pkg === CORE_PACKAGE ? api.CORE_VERSION : null);
|
|
204
208
|
const liveVersion = VERSION_RE.test(String(running || '')) ? running : null;
|
|
205
209
|
|
|
@@ -225,6 +229,17 @@ async function readWork({
|
|
|
225
229
|
const coversTo = versions.length ? versions[versions.length - 1].version : null;
|
|
226
230
|
const published = registry.ok ? registry.published : {};
|
|
227
231
|
const released = registry.ok ? registry.latest : null;
|
|
232
|
+
return { liveVersion, registry, versions, source, notesError, coversTo, published, released };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function readWork({
|
|
236
|
+
pool, pkg = configuredPackage(), live, days, before, limit,
|
|
237
|
+
installedNotes = readInstalledNotes, get, now, store,
|
|
238
|
+
} = {}) {
|
|
239
|
+
const windowDays = clampInt(days, DEFAULT_DAYS, MAX_DAYS);
|
|
240
|
+
const pageLimit = clampInt(limit, DEFAULT_LIMIT, MAX_LIMIT);
|
|
241
|
+
const { liveVersion, registry, versions, source, notesError, coversTo, published, released } =
|
|
242
|
+
await readVersions({ pkg, live, installedNotes, get, now, store });
|
|
228
243
|
|
|
229
244
|
const tasks = await recentTasks(pool, windowDays);
|
|
230
245
|
const stages = placeTasks({ tasks, versions, live: liveVersion, released, coveredAt: coversTo ? published[coversTo] || null : null });
|
|
@@ -255,4 +270,50 @@ async function readWork({
|
|
|
255
270
|
};
|
|
256
271
|
}
|
|
257
272
|
|
|
258
|
-
|
|
273
|
+
/**
|
|
274
|
+
* Where ONE task's code is (task 1004301) — the line its own page shows: the same
|
|
275
|
+
* placement the deploy page's list makes, for one task and without the 14-day window.
|
|
276
|
+
*
|
|
277
|
+
* Returns null for an id that is not a task. For a task not yet declared done, `stage` is
|
|
278
|
+
* null: there is no code to place. Otherwise `stage` is one of the deploy page's six, with
|
|
279
|
+
* the version that carries it when there is one.
|
|
280
|
+
*
|
|
281
|
+
* WHAT IT DISCLOSES, because every builder can open a task page: the version that names the
|
|
282
|
+
* task (the package's own release notes, published on npm), whether that version is the one
|
|
283
|
+
* this hall runs (GET /version already says which core runs), and whether it is released
|
|
284
|
+
* (the registry's `latest` label). Nothing here is not already public, which is why its
|
|
285
|
+
* route asks only for a signed-in builder — unlike the deploy page, whose ledger view and
|
|
286
|
+
* upgrade door are the owner's.
|
|
287
|
+
*/
|
|
288
|
+
async function readTaskWhere({ pool, id, pkg = configuredPackage(), live, installedNotes = readInstalledNotes, get, now, store } = {}) {
|
|
289
|
+
const taskId = String(id == null ? '' : id);
|
|
290
|
+
if (!/^\d{1,12}$/.test(taskId)) return null;
|
|
291
|
+
const { rows } = await pool.query(
|
|
292
|
+
`SELECT id, status, value_summary, COALESCE(shipped_at, updated_at) AS at FROM tasks WHERE id = $1::bigint`,
|
|
293
|
+
[taskId],
|
|
294
|
+
);
|
|
295
|
+
if (!rows.length) return null;
|
|
296
|
+
const r = rows[0];
|
|
297
|
+
const task = { id: String(r.id), status: r.status, summary: r.value_summary || null, at: r.at instanceof Date ? r.at.toISOString() : r.at };
|
|
298
|
+
const base = { task_id: task.id, package: pkg, status: task.status };
|
|
299
|
+
if (!['completed', 'confirmed', 'shipped'].includes(task.status)) return { ...base, stage: null };
|
|
300
|
+
|
|
301
|
+
const { liveVersion, registry, versions, notesError, coversTo, published, released } =
|
|
302
|
+
await readVersions({ pkg, live, installedNotes, get, now, store });
|
|
303
|
+
const stages = placeTasks({ tasks: [task], versions, live: liveVersion, released, coveredAt: coversTo ? published[coversTo] || null : null });
|
|
304
|
+
const stage = Object.keys(stages).find((k) => stages[k].length) || null;
|
|
305
|
+
const row = stage ? stages[stage][0] : {};
|
|
306
|
+
return {
|
|
307
|
+
...base,
|
|
308
|
+
stage,
|
|
309
|
+
version: row.version || null,
|
|
310
|
+
offered: !!row.offered,
|
|
311
|
+
live: liveVersion,
|
|
312
|
+
released,
|
|
313
|
+
// A partial reading is said, not hidden: without the notes a shipped task reads as "in no
|
|
314
|
+
// version", which would be a claim the page cannot back.
|
|
315
|
+
partial: !registry.ok || !!notesError,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
module.exports = { readWork, readTaskWhere };
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.1044",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.1044",
|
|
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.
|
|
3
|
+
"version": "1.19.1044",
|
|
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",
|