@bongos/core 1.19.988 → 1.19.990
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 +30 -20
- package/docs/api/openapi.json +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/core-release-pipeline.md +5 -0
- package/docs/recipes/upgrading-the-core.md +1 -0
- package/modules/hall-ui/public/style.css +172 -0
- package/modules/hall-ui/public/work.css +1 -170
- package/modules/lifecycle/routes/artist-gate.js +24 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/upgrade-artist-gate.js +46 -0
- package/scripts/gds/upgrade.js +13 -15
- package/src/module-api.js +1 -1
- package/tests/artist_gate_route.mjs +70 -1
- package/tests/hall_board_world.mjs +9 -2
- package/tests/upgrade_artist_gate.mjs +144 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.990",
|
|
6
|
+
"core_contract": "1.19.990",
|
|
7
|
+
"source_commit": "b1a0a6f54282f522323d1e84921384ce4af0d190",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-23T19:
|
|
9
|
+
"built_at": "2026-09-23T19:56:56.227Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 518,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2396,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2940,
|
|
20
|
+
"tree_sha256": "2568c6298b1a5aa67172a39885d5bf19ef7a05679f3032125d57660ee881e2ca",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2152,7 +2152,7 @@
|
|
|
2152
2152
|
{
|
|
2153
2153
|
"path": "docs/api/openapi.json",
|
|
2154
2154
|
"mode": "0000644",
|
|
2155
|
-
"sha256": "
|
|
2155
|
+
"sha256": "8b1f2a53680ae8b68be5d54a82664413fa29186fb580825f5a1b0e3ce3677525"
|
|
2156
2156
|
},
|
|
2157
2157
|
{
|
|
2158
2158
|
"path": "docs/architecture.md",
|
|
@@ -2712,7 +2712,7 @@
|
|
|
2712
2712
|
{
|
|
2713
2713
|
"path": "docs/module-api-changelog.md",
|
|
2714
2714
|
"mode": "0000644",
|
|
2715
|
-
"sha256": "
|
|
2715
|
+
"sha256": "807ee161f37480d116b1e3c75835fcde8eb347dc994839a69c0f3aeaa43e273e"
|
|
2716
2716
|
},
|
|
2717
2717
|
{
|
|
2718
2718
|
"path": "docs/modules-contract.md",
|
|
@@ -2832,7 +2832,7 @@
|
|
|
2832
2832
|
{
|
|
2833
2833
|
"path": "docs/recipes/core-release-pipeline.md",
|
|
2834
2834
|
"mode": "0000644",
|
|
2835
|
-
"sha256": "
|
|
2835
|
+
"sha256": "12e2fdc72d1bed7d524b50960dea3a67791b098b7dbafaff9c6f95cd356b185a"
|
|
2836
2836
|
},
|
|
2837
2837
|
{
|
|
2838
2838
|
"path": "docs/recipes/failed-ship-recovery-proof.md",
|
|
@@ -2962,7 +2962,7 @@
|
|
|
2962
2962
|
{
|
|
2963
2963
|
"path": "docs/recipes/upgrading-the-core.md",
|
|
2964
2964
|
"mode": "0000644",
|
|
2965
|
-
"sha256": "
|
|
2965
|
+
"sha256": "e035ebbda54d0d94f38bbcc9502689318c4f9bb2fe5bcca2592aa710dca5ffb0"
|
|
2966
2966
|
},
|
|
2967
2967
|
{
|
|
2968
2968
|
"path": "docs/recipes/windows-builders.md",
|
|
@@ -5407,7 +5407,7 @@
|
|
|
5407
5407
|
{
|
|
5408
5408
|
"path": "modules/hall-ui/public/style.css",
|
|
5409
5409
|
"mode": "0000644",
|
|
5410
|
-
"sha256": "
|
|
5410
|
+
"sha256": "b8a9b64e1febe57eddbb1005335712a19e581e69d16bb42346eba24bd73acfb7"
|
|
5411
5411
|
},
|
|
5412
5412
|
{
|
|
5413
5413
|
"path": "modules/hall-ui/public/task-detail.js",
|
|
@@ -5492,7 +5492,7 @@
|
|
|
5492
5492
|
{
|
|
5493
5493
|
"path": "modules/hall-ui/public/work.css",
|
|
5494
5494
|
"mode": "0000644",
|
|
5495
|
-
"sha256": "
|
|
5495
|
+
"sha256": "68399594a00fdffaf9833d3105d8c9e2f5872642b0f49868edae6cc526fa9021"
|
|
5496
5496
|
},
|
|
5497
5497
|
{
|
|
5498
5498
|
"path": "modules/hall-ui/public/work.html",
|
|
@@ -6042,7 +6042,7 @@
|
|
|
6042
6042
|
{
|
|
6043
6043
|
"path": "modules/lifecycle/routes/artist-gate.js",
|
|
6044
6044
|
"mode": "0000644",
|
|
6045
|
-
"sha256": "
|
|
6045
|
+
"sha256": "9b6990b00e071e2b47c20fc790f975db63563b700ffbd4a6b0c60dfd138f0def"
|
|
6046
6046
|
},
|
|
6047
6047
|
{
|
|
6048
6048
|
"path": "modules/lifecycle/routes/claims.js",
|
|
@@ -8162,12 +8162,12 @@
|
|
|
8162
8162
|
{
|
|
8163
8163
|
"path": "package-lock.json",
|
|
8164
8164
|
"mode": "0000644",
|
|
8165
|
-
"sha256": "
|
|
8165
|
+
"sha256": "47f81c7b7d52feec484d30c948a6f511be97d949d51008e3951536b6d5a16e4d"
|
|
8166
8166
|
},
|
|
8167
8167
|
{
|
|
8168
8168
|
"path": "package.json",
|
|
8169
8169
|
"mode": "0000644",
|
|
8170
|
-
"sha256": "
|
|
8170
|
+
"sha256": "eb28fe24fd57a826eee9a4c24dae6980d2ba6cfd59f866bdf40b4ededb09bab0"
|
|
8171
8171
|
},
|
|
8172
8172
|
{
|
|
8173
8173
|
"path": "public-docs/index.html",
|
|
@@ -9739,10 +9739,15 @@
|
|
|
9739
9739
|
"mode": "0000644",
|
|
9740
9740
|
"sha256": "6418f51f933eac634a7df854af1e3a6dbde25e152e3dca0ce8df7d37d19c1d0c"
|
|
9741
9741
|
},
|
|
9742
|
+
{
|
|
9743
|
+
"path": "scripts/gds/upgrade-artist-gate.js",
|
|
9744
|
+
"mode": "0000644",
|
|
9745
|
+
"sha256": "caf07b70b066fe1388037ce1c622ea3aed82cbf43f67ffb918214159cbe4444e"
|
|
9746
|
+
},
|
|
9742
9747
|
{
|
|
9743
9748
|
"path": "scripts/gds/upgrade.js",
|
|
9744
9749
|
"mode": "0000644",
|
|
9745
|
-
"sha256": "
|
|
9750
|
+
"sha256": "70a7de81b25a17cb7ab7e57f92577b6d977f89911b784f33df668b650fb208b0"
|
|
9746
9751
|
},
|
|
9747
9752
|
{
|
|
9748
9753
|
"path": "scripts/gds/validate-design.js",
|
|
@@ -10202,7 +10207,7 @@
|
|
|
10202
10207
|
{
|
|
10203
10208
|
"path": "src/module-api.js",
|
|
10204
10209
|
"mode": "0000644",
|
|
10205
|
-
"sha256": "
|
|
10210
|
+
"sha256": "dcb76f459cc34113dd39ecd6229efe70abf82efc64da2c8c7ff35f514c8b17ec"
|
|
10206
10211
|
},
|
|
10207
10212
|
{
|
|
10208
10213
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10507,7 +10512,7 @@
|
|
|
10507
10512
|
{
|
|
10508
10513
|
"path": "tests/artist_gate_route.mjs",
|
|
10509
10514
|
"mode": "0000644",
|
|
10510
|
-
"sha256": "
|
|
10515
|
+
"sha256": "3337ab686f2fa4bef95d0eabcf9b134a9b919d40480750e07a500b8007ae9659"
|
|
10511
10516
|
},
|
|
10512
10517
|
{
|
|
10513
10518
|
"path": "tests/asset_version.mjs",
|
|
@@ -12077,7 +12082,7 @@
|
|
|
12077
12082
|
{
|
|
12078
12083
|
"path": "tests/hall_board_world.mjs",
|
|
12079
12084
|
"mode": "0000644",
|
|
12080
|
-
"sha256": "
|
|
12085
|
+
"sha256": "48c6176c97d4cc167c57aeaf77baa78667731125c5ff2369564f08cbe9a146c7"
|
|
12081
12086
|
},
|
|
12082
12087
|
{
|
|
12083
12088
|
"path": "tests/hall_builders_page_pass.mjs",
|
|
@@ -14549,6 +14554,11 @@
|
|
|
14549
14554
|
"mode": "0000644",
|
|
14550
14555
|
"sha256": "8bd1c116b5459b54a847208c01669159414fed17b3704d621e1dc5acfb00f255"
|
|
14551
14556
|
},
|
|
14557
|
+
{
|
|
14558
|
+
"path": "tests/upgrade_artist_gate.mjs",
|
|
14559
|
+
"mode": "0000644",
|
|
14560
|
+
"sha256": "a9b5fadd7f829fd54a9d976d82832eb536e7d988c466647860b0257109feec50"
|
|
14561
|
+
},
|
|
14552
14562
|
{
|
|
14553
14563
|
"path": "tests/upgrade_persist_pin.mjs",
|
|
14554
14564
|
"mode": "0000644",
|
package/docs/api/openapi.json
CHANGED
|
@@ -1065,7 +1065,7 @@
|
|
|
1065
1065
|
"artist-gate"
|
|
1066
1066
|
],
|
|
1067
1067
|
"summary": "GET /artist-gate",
|
|
1068
|
-
"description": "GET /artist-gate — this project's artist strictness and what it is holding. rank: any authenticated builder. The payload is ordinary board state — a policy value the instance already publishes as its own behaviour, and task ids/titles any builder can read on the hall — so a floor above requireBuilder would gate a fact the board already shows. Nothing here is writable. Response: mode 'off' | 'advisory' | 'strict' the project's setting as resolved since ISO string | null when it was last RAISED to strict holds boolean does anything hold a deploy now? truncated boolean `reviews` hit the scan cap — a floor reviews [{ id, created_at }] the open reviews that count halt { code, message, detail } | null the refusal, pre-composed `halt` is composed HERE rather than by the caller on purpose: ADR 0241 §4's second guard is that the refusal must name what holds the deploy and which control releases it, and a sentence assembled independently by each caller is a sentence that eventually says something different. One composer, one wording.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
1068
|
+
"description": "GET /artist-gate — this project's artist strictness and what it is holding. rank: any authenticated builder. The payload is ordinary board state — a policy value the instance already publishes as its own behaviour, and task ids/titles any builder can read on the hall — so a floor above requireBuilder would gate a fact the board already shows. Nothing here is writable. Response: mode 'off' | 'advisory' | 'strict' the project's setting as resolved since ISO string | null when it was last RAISED to strict holds boolean does anything hold a deploy now? truncated boolean `reviews` hit the scan cap — a floor reviews [{ id, created_at }] the open reviews that count halt { code, message, detail } | null the refusal, pre-composed `halt` is composed HERE rather than by the caller on purpose: ADR 0241 §4's second guard is that the refusal must name what holds the deploy and which control releases it, and a sentence assembled independently by each caller is a sentence that eventually says something different. One composer, one wording. A caller on this machine that reached the app DIRECTLY may also read it, without a session (task 1004205; owner decision 2026-09-23): that is how `bongos upgrade` on the box asks before moving the pin, and the box holds no builder session. Everything else still passes requireBuilder.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
1069
1069
|
"x-rank": "any-builder",
|
|
1070
1070
|
"x-source": "modules/lifecycle/routes/artist-gate.js",
|
|
1071
1071
|
"responses": {
|
|
@@ -2450,5 +2450,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2450
2450
|
landed since 1.19.986 with no explicit bump. run 35906802480. (task 1002620)
|
|
2451
2451
|
1.19.988 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2452
2452
|
landed since 1.19.987 with no explicit bump. run 35909292753. (task 1002620)
|
|
2453
|
+
1.19.989 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2454
|
+
landed since 1.19.988 with no explicit bump. run 35911817390. (task 1002620)
|
|
2455
|
+
1.19.990 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2456
|
+
landed since 1.19.989 with no explicit bump. run 35912578496. (task 1002620)
|
|
2453
2457
|
---------------------------------------------------------------------------
|
|
2454
2458
|
```
|
|
@@ -207,6 +207,11 @@ There are three ways past it, and the first two are the ordinary ones:
|
|
|
207
207
|
the audit log as a self-override. It is the valve that stops a solo project deadlocking,
|
|
208
208
|
not a routine step: the right answer to being gated is to fix the work or fix the grader.
|
|
209
209
|
|
|
210
|
+
**`bongos upgrade` asks the same gate** (task 1004205): the vendored `upgrade.js --from` run
|
|
211
|
+
refuses before the pin moves, with the same halt, the same three ways past it and the same two
|
|
212
|
+
unknowns below — it reuses `go-live.js`'s reader. Until then only `go-live.js` asked, and an
|
|
213
|
+
instance that moved its pin by hand moved it past open reviews.
|
|
214
|
+
|
|
210
215
|
`go-live.js` prints the gate's level on every plan, held or not, so "why did nothing stop?" is
|
|
211
216
|
answerable without going and looking. Two different unknowns, two different verdicts:
|
|
212
217
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
0. **downgrade guard** (task [#1004009](https://example.com/builders#/task/1004009)) — a `--to` **older** than this instance's core is refused before anything moves, in one line naming both versions and the newest published release. "This instance's core" is the **newer** of what is installed and what is pinned, because a half-applied bump leaves the two apart and the pin is the thing about to be overwritten. `--force` does not waive it; `--allow-downgrade` does, and the run then says `DOWNGRADE` in its first line, its last line and its `core_upgrades` ledger note. Migrations are never reverted, so going backwards across one leaves the DB ahead of the code; and an instance subscribed to an update channel is taken forward again by the next sweep, so set it to `pinned` first if the downgrade is meant to hold. Auto-rollback (step 8) restores its own pin and is unaffected.
|
|
12
12
|
1. **pre-flight** — module-compat vs the target core + a clean git tree (skip with `--force`).
|
|
13
|
+
1a. **the artist gate** (task [#1004205](https://example.com/builders#/task/1004205), [ADR 0241](../adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md)) — asks the instance being upgraded, at the origin of `--health-url`, whether an unresolved artist review holds its deploy, and **refuses before anything moves** if one does. `--force` does not waive it, and a dry run reports it. Every change to a page files such a review (owner decision 2026-09-23), so on a `strict` project an upgrade waits until each has been looked at. The ways past it are ADR 0241's, unchanged: resolve or dismiss the review, lower `artist_gate` to `advisory`, or the project owner releases it (see [the release pipeline](core-release-pipeline.md)). It uses `go-live.js`'s own reader, so both paths decide one answer the same way: a gate that is there and errors halts; one that cannot be read (no `--health-url`, an older instance, the instance down) is printed as a note. A **downgrade** is never held — rolling back must not wait on a review. Before this, only `go-live.js` asked, so the vendored `--from` path this instance used moved its pin past open reviews. The instance answers this local request without a session (the box has none); a request through the proxy still needs one. So the **first** upgrade that carries this asks an instance still running the older route, gets a 401 and can only print the note — every upgrade after it holds.
|
|
13
14
|
2. **bump the pin** — vendor the new `bongos-core-<version>.tgz` and rewrite the `"@bongos/core": "file:vendor/…"` dependency.
|
|
14
15
|
3. **`npm install`** — `node_modules/@bongos/core` becomes the new version.
|
|
15
16
|
3b. **verify the integrity pin** (task [#1002216](https://example.com/builders#/task/1002216)) — recompute `tree_sha256` over the installed core and check the vendored `.tgz` bytes against `tarball.sha256`, comparing to the trusted reference manifest (the committed `vendor/…manifest.json`; the installed inner manifest for `--registry`). Runs **before** migrate so a corrupted core never touches the DB. A **mismatch is fatal → rollback**; a missing reference warns (fatal with `--require-pin`); opt out with `--skip-pin-verify`.
|
|
@@ -2661,6 +2661,178 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
|
|
|
2661
2661
|
text-transform: var(--label-case);
|
|
2662
2662
|
color: var(--ink-soft);
|
|
2663
2663
|
}
|
|
2664
|
+
/* How old the task is (task 1002955, OTBKit.ageChipHtml). Quiet by default —
|
|
2665
|
+
it's orientation, not a call to act — but a task past the stale threshold
|
|
2666
|
+
steps up from faint to soft so a queue that has been sitting reads at a
|
|
2667
|
+
glance. The exact date is on the tooltip, hence `cursor: help`. Deliberately
|
|
2668
|
+
NOT --warn: that token is the verification vocabulary and as bare text on the
|
|
2669
|
+
dark card it loses contrast. --ink-soft steps up legibly in both modes. */
|
|
2670
|
+
.task__age {
|
|
2671
|
+
font-family: var(--font-mono);
|
|
2672
|
+
font-size: 11.5px;
|
|
2673
|
+
font-variant-numeric: tabular-nums;
|
|
2674
|
+
color: var(--ink-faint);
|
|
2675
|
+
white-space: nowrap;
|
|
2676
|
+
cursor: help;
|
|
2677
|
+
}
|
|
2678
|
+
.task__age--stale { color: var(--ink-soft); }
|
|
2679
|
+
|
|
2680
|
+
/* The four tag variants that carry meaning beyond "a fact". Each keeps the
|
|
2681
|
+
outlined shape style.css gives .task__tag and only changes what it says. */
|
|
2682
|
+
.task__tag--newcomer {
|
|
2683
|
+
background: var(--ok-weak);
|
|
2684
|
+
color: var(--ok);
|
|
2685
|
+
border-color: color-mix(in srgb, var(--ok) 30%, transparent);
|
|
2686
|
+
}
|
|
2687
|
+
.task__tag--mine {
|
|
2688
|
+
background: var(--accent-tint);
|
|
2689
|
+
color: var(--accent-ink);
|
|
2690
|
+
border-color: color-mix(in srgb, var(--accent) 35%, transparent);
|
|
2691
|
+
}
|
|
2692
|
+
/* task 1498 / ADR 0084: per-task rank floor. Outlined + emphasized (not a
|
|
2693
|
+
filled tag) so a gated task reads as "authority required", distinct from the
|
|
2694
|
+
filled newcomer tag. --warn is the verification vocabulary, kept on purpose. */
|
|
2695
|
+
.task__tag--rank {
|
|
2696
|
+
background: transparent;
|
|
2697
|
+
color: var(--warn);
|
|
2698
|
+
border-color: color-mix(in srgb, var(--warn) 45%, transparent);
|
|
2699
|
+
}
|
|
2700
|
+
/* task 1752: the open (no floor) state — muted so "Any rank" reads as "open to
|
|
2701
|
+
all", not as a warning like the gated --rank tag above. */
|
|
2702
|
+
.task__tag--rank-open {
|
|
2703
|
+
background: transparent;
|
|
2704
|
+
color: var(--ink-faint);
|
|
2705
|
+
border-color: var(--rule-soft);
|
|
2706
|
+
}
|
|
2707
|
+
/* ---- #616 / task 2170 / task 1003304: claim + copy on the claimable card ----
|
|
2708
|
+
The card's geometry, tags, credits figure, checkbox and copy button all live
|
|
2709
|
+
in this work-board section (they are the re-skin), and so do the primary door,
|
|
2710
|
+
the touches chip and the whole-card selection target (promoted from work.css,
|
|
2711
|
+
task 1003659, so their cascade position is the same on every page). */
|
|
2712
|
+
|
|
2713
|
+
/* The page's one primary action, and the only accent FILL on the board.
|
|
2714
|
+
--accent-on is the dark ink that goes on the salmon: white on it fails AA and
|
|
2715
|
+
DESIGN.md bans it outright, which is what the old `color: #fff` was. */
|
|
2716
|
+
.task__claim {
|
|
2717
|
+
display: inline-flex;
|
|
2718
|
+
align-items: center;
|
|
2719
|
+
justify-content: center;
|
|
2720
|
+
min-height: 34px;
|
|
2721
|
+
min-width: 74px;
|
|
2722
|
+
padding: 0 16px;
|
|
2723
|
+
background: var(--accent);
|
|
2724
|
+
border: 0;
|
|
2725
|
+
color: var(--accent-on);
|
|
2726
|
+
border-radius: var(--radius-pill);
|
|
2727
|
+
font-size: 13.5px;
|
|
2728
|
+
font-weight: 500;
|
|
2729
|
+
text-transform: lowercase;
|
|
2730
|
+
cursor: pointer;
|
|
2731
|
+
white-space: nowrap;
|
|
2732
|
+
transition: background 150ms, opacity 150ms;
|
|
2733
|
+
}
|
|
2734
|
+
.task__claim:hover:not(:disabled) { background: var(--accent-lift); color: var(--accent-on); }
|
|
2735
|
+
.task__claim:disabled { background: var(--glass); color: var(--ink-faint); cursor: default; }
|
|
2736
|
+
|
|
2737
|
+
/* Files a claimable task is responsible for — a compact count chip in the meta
|
|
2738
|
+
row; the full path list rides the tooltip (task 2170 density pass). The mono
|
|
2739
|
+
fact voice: this is a number the ledger produced. */
|
|
2740
|
+
.task__touches {
|
|
2741
|
+
font-family: var(--font-mono);
|
|
2742
|
+
font-size: 11.5px;
|
|
2743
|
+
font-variant-numeric: tabular-nums;
|
|
2744
|
+
color: var(--ink-faint);
|
|
2745
|
+
white-space: nowrap;
|
|
2746
|
+
cursor: help;
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
/* task 1753: the whole card is clickable → toggles selection (task 1869). */
|
|
2750
|
+
.task { cursor: pointer; }
|
|
2751
|
+
|
|
2752
|
+
@media (max-width: 640px) {
|
|
2753
|
+
.task__actions { grid-column: 1 / -1; justify-self: start; padding-top: 6px; }
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
/* ---- task 1759: the bottom bulk-action toolbar (promoted from work.css, task 1003659) ---- */
|
|
2757
|
+
.bulk-toolbar {
|
|
2758
|
+
position: fixed;
|
|
2759
|
+
left: var(--sidebar-w);
|
|
2760
|
+
right: 0;
|
|
2761
|
+
bottom: 0;
|
|
2762
|
+
/* The chrome tier, not above it: the toast (z 100) has to stay readable over
|
|
2763
|
+
this bar, and the drawer + its scrim have to cover it. */
|
|
2764
|
+
z-index: var(--z-topbar);
|
|
2765
|
+
display: flex;
|
|
2766
|
+
align-items: center;
|
|
2767
|
+
gap: 16px;
|
|
2768
|
+
flex-wrap: wrap;
|
|
2769
|
+
padding: 12px 20px;
|
|
2770
|
+
/* TWO layers, and the bottom one is why. The topbar and the rail can be bare
|
|
2771
|
+
--glass because the page GROUND is behind them; this bar sits over the task
|
|
2772
|
+
cards, and in light --glass is a 5% ink tint with the blur at 0px, so the
|
|
2773
|
+
card underneath read straight through it and the buttons landed on top of a
|
|
2774
|
+
task title (looked at, 1280w light). Painting --bg under the tint makes the
|
|
2775
|
+
bar opaque without giving up the glass character or spending a literal. */
|
|
2776
|
+
background: linear-gradient(var(--glass), var(--glass)), var(--bg);
|
|
2777
|
+
border-top: var(--line) solid var(--glass-edge);
|
|
2778
|
+
backdrop-filter: blur(var(--glass-blur));
|
|
2779
|
+
-webkit-backdrop-filter: blur(var(--glass-blur));
|
|
2780
|
+
color: var(--ink);
|
|
2781
|
+
}
|
|
2782
|
+
.bulk-toolbar[hidden] { display: none; }
|
|
2783
|
+
.bulk-toolbar__count {
|
|
2784
|
+
font-family: var(--label-face);
|
|
2785
|
+
font-size: var(--label-size);
|
|
2786
|
+
letter-spacing: var(--label-space);
|
|
2787
|
+
text-transform: var(--label-case);
|
|
2788
|
+
color: var(--ink);
|
|
2789
|
+
white-space: nowrap;
|
|
2790
|
+
}
|
|
2791
|
+
.bulk-toolbar__actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-left: auto; }
|
|
2792
|
+
.bulk-toolbar__btn {
|
|
2793
|
+
display: inline-flex;
|
|
2794
|
+
align-items: center;
|
|
2795
|
+
justify-content: center;
|
|
2796
|
+
min-height: 30px;
|
|
2797
|
+
padding: 0 14px;
|
|
2798
|
+
background: var(--glass);
|
|
2799
|
+
border: var(--line) solid var(--glass-edge);
|
|
2800
|
+
color: var(--ink);
|
|
2801
|
+
border-radius: var(--radius-pill);
|
|
2802
|
+
font-size: 13px;
|
|
2803
|
+
text-transform: lowercase;
|
|
2804
|
+
cursor: pointer;
|
|
2805
|
+
white-space: nowrap;
|
|
2806
|
+
transition: background 150ms, opacity 150ms;
|
|
2807
|
+
}
|
|
2808
|
+
.bulk-toolbar__btn:hover:not(:disabled) { background: var(--glass-strong); }
|
|
2809
|
+
.bulk-toolbar__btn:disabled { color: var(--ink-faint); border-color: var(--rule-soft); cursor: not-allowed; }
|
|
2810
|
+
.bulk-toolbar__clear {
|
|
2811
|
+
min-height: 30px;
|
|
2812
|
+
padding: 0 10px;
|
|
2813
|
+
background: transparent;
|
|
2814
|
+
border: none;
|
|
2815
|
+
color: var(--ink-soft);
|
|
2816
|
+
font-size: 13px;
|
|
2817
|
+
text-transform: lowercase;
|
|
2818
|
+
text-decoration: underline;
|
|
2819
|
+
text-underline-offset: 4px;
|
|
2820
|
+
cursor: pointer;
|
|
2821
|
+
}
|
|
2822
|
+
.bulk-toolbar__clear:hover { color: var(--ink); }
|
|
2823
|
+
|
|
2824
|
+
/* The shell's .app-content already ends in 108px of bottom air at desktop
|
|
2825
|
+
width, which is what clears the bar — but at 768 it drops to 48px for the
|
|
2826
|
+
phone, and there the bar is TALLER (its actions wrap to their own row). So
|
|
2827
|
+
the phone width buys the room back, scoped to this page: goals.html loads
|
|
2828
|
+
this sheet too and has no bulk toolbar to clear. */
|
|
2829
|
+
@media (max-width: 768px) {
|
|
2830
|
+
/* At the drawer breakpoint the sidebar leaves the flow, so the bar spans. */
|
|
2831
|
+
.bulk-toolbar { left: 0; padding: 10px 14px; gap: 10px; }
|
|
2832
|
+
.bulk-toolbar__actions { margin-left: 0; width: 100%; }
|
|
2833
|
+
body[data-page="work"] .app-content { padding-bottom: 200px; }
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2664
2836
|
.task__credits { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; padding-top: 2px; }
|
|
2665
2837
|
/* A credit figure is DATA, not live — the display face in ink, never accent
|
|
2666
2838
|
(DESIGN.md, The One Warm Thing Rule + The Tabular Rule). */
|
|
@@ -46,93 +46,6 @@
|
|
|
46
46
|
color: var(--ink-faint);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/* ---- #616 / task 2170 / task 1003304: claim + copy on the claimable card ----
|
|
50
|
-
The card's geometry, tags, credits figure, checkbox and copy button all live
|
|
51
|
-
in style.css's work-board section now (they are the re-skin). What stays here
|
|
52
|
-
is the page-local behaviour: the primary door, the two meta chips, and the
|
|
53
|
-
fact that the whole card is a selection target. */
|
|
54
|
-
|
|
55
|
-
/* The page's one primary action, and the only accent FILL on the board.
|
|
56
|
-
--accent-on is the dark ink that goes on the salmon: white on it fails AA and
|
|
57
|
-
DESIGN.md bans it outright, which is what the old `color: #fff` was. */
|
|
58
|
-
.task__claim {
|
|
59
|
-
display: inline-flex;
|
|
60
|
-
align-items: center;
|
|
61
|
-
justify-content: center;
|
|
62
|
-
min-height: 34px;
|
|
63
|
-
min-width: 74px;
|
|
64
|
-
padding: 0 16px;
|
|
65
|
-
background: var(--accent);
|
|
66
|
-
border: 0;
|
|
67
|
-
color: var(--accent-on);
|
|
68
|
-
border-radius: var(--radius-pill);
|
|
69
|
-
font-size: 13.5px;
|
|
70
|
-
font-weight: 500;
|
|
71
|
-
text-transform: lowercase;
|
|
72
|
-
cursor: pointer;
|
|
73
|
-
white-space: nowrap;
|
|
74
|
-
transition: background 150ms, opacity 150ms;
|
|
75
|
-
}
|
|
76
|
-
.task__claim:hover:not(:disabled) { background: var(--accent-lift); color: var(--accent-on); }
|
|
77
|
-
.task__claim:disabled { background: var(--glass); color: var(--ink-faint); cursor: default; }
|
|
78
|
-
|
|
79
|
-
/* Files a claimable task is responsible for — a compact count chip in the meta
|
|
80
|
-
row; the full path list rides the tooltip (task 2170 density pass). The mono
|
|
81
|
-
fact voice: this is a number the ledger produced. */
|
|
82
|
-
.task__touches {
|
|
83
|
-
font-family: var(--font-mono);
|
|
84
|
-
font-size: 11.5px;
|
|
85
|
-
font-variant-numeric: tabular-nums;
|
|
86
|
-
color: var(--ink-faint);
|
|
87
|
-
white-space: nowrap;
|
|
88
|
-
cursor: help;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/* How old the task is (task 1002955, OTBKit.ageChipHtml). Quiet by default —
|
|
92
|
-
it's orientation, not a call to act — but a task past the stale threshold
|
|
93
|
-
steps up from faint to soft so a queue that has been sitting reads at a
|
|
94
|
-
glance. The exact date is on the tooltip, hence `cursor: help`. Deliberately
|
|
95
|
-
NOT --warn: that token is the verification vocabulary and as bare text on the
|
|
96
|
-
dark card it loses contrast. --ink-soft steps up legibly in both modes. */
|
|
97
|
-
.task__age {
|
|
98
|
-
font-family: var(--font-mono);
|
|
99
|
-
font-size: 11.5px;
|
|
100
|
-
font-variant-numeric: tabular-nums;
|
|
101
|
-
color: var(--ink-faint);
|
|
102
|
-
white-space: nowrap;
|
|
103
|
-
cursor: help;
|
|
104
|
-
}
|
|
105
|
-
.task__age--stale { color: var(--ink-soft); }
|
|
106
|
-
|
|
107
|
-
/* The three tag variants that carry meaning beyond "a fact". Each keeps the
|
|
108
|
-
outlined shape style.css gives .task__tag and only changes what it says. */
|
|
109
|
-
.task__tag--newcomer {
|
|
110
|
-
background: var(--ok-weak);
|
|
111
|
-
color: var(--ok);
|
|
112
|
-
border-color: color-mix(in srgb, var(--ok) 30%, transparent);
|
|
113
|
-
}
|
|
114
|
-
.task__tag--mine {
|
|
115
|
-
background: var(--accent-tint);
|
|
116
|
-
color: var(--accent-ink);
|
|
117
|
-
border-color: color-mix(in srgb, var(--accent) 35%, transparent);
|
|
118
|
-
}
|
|
119
|
-
/* task 1498 / ADR 0084: per-task rank floor. Outlined + emphasized (not a
|
|
120
|
-
filled tag) so a gated task reads as "authority required", distinct from the
|
|
121
|
-
filled newcomer tag. --warn is the verification vocabulary, kept on purpose. */
|
|
122
|
-
.task__tag--rank {
|
|
123
|
-
background: transparent;
|
|
124
|
-
color: var(--warn);
|
|
125
|
-
border-color: color-mix(in srgb, var(--warn) 45%, transparent);
|
|
126
|
-
}
|
|
127
|
-
/* task 1752: the open (no floor) state — muted so "Any rank" reads as "open to
|
|
128
|
-
all", not as a warning like the gated --rank tag above. */
|
|
129
|
-
.task__tag--rank-open {
|
|
130
|
-
background: transparent;
|
|
131
|
-
color: var(--ink-faint);
|
|
132
|
-
border-color: var(--rule-soft);
|
|
133
|
-
}
|
|
134
|
-
/* task 1753: the whole card is clickable → toggles selection (task 1869). */
|
|
135
|
-
.task { cursor: pointer; }
|
|
136
49
|
.backlog-count {
|
|
137
50
|
font-family: var(--font-mono);
|
|
138
51
|
font-size: 11px;
|
|
@@ -205,11 +118,8 @@
|
|
|
205
118
|
.history__goal { display: inline-block; margin-top: 3px; color: var(--ink-soft); font-size: 12.5px; }
|
|
206
119
|
.history__when { color: var(--ink-soft); font-size: 12.5px; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
207
120
|
|
|
208
|
-
@media (max-width: 640px) {
|
|
209
|
-
.task__actions { grid-column: 1 / -1; justify-self: start; padding-top: 6px; }
|
|
210
|
-
}
|
|
211
121
|
|
|
212
|
-
/* ---- task 1759: multi-select
|
|
122
|
+
/* ---- task 1759: multi-select checkbox (the toolbar itself is in style.css) ----
|
|
213
123
|
Re-cut for the 24px floor and the world (task 1003304). The floating dark
|
|
214
124
|
pill is gone: it painted an INVERTED surface (background: var(--ink)) with
|
|
215
125
|
three rgba(255,255,255,…) literals on top, which was correct only for as long
|
|
@@ -250,85 +160,6 @@
|
|
|
250
160
|
transform: rotate(42deg);
|
|
251
161
|
}
|
|
252
162
|
|
|
253
|
-
.bulk-toolbar {
|
|
254
|
-
position: fixed;
|
|
255
|
-
left: var(--sidebar-w);
|
|
256
|
-
right: 0;
|
|
257
|
-
bottom: 0;
|
|
258
|
-
/* The chrome tier, not above it: the toast (z 100) has to stay readable over
|
|
259
|
-
this bar, and the drawer + its scrim have to cover it. */
|
|
260
|
-
z-index: var(--z-topbar);
|
|
261
|
-
display: flex;
|
|
262
|
-
align-items: center;
|
|
263
|
-
gap: 16px;
|
|
264
|
-
flex-wrap: wrap;
|
|
265
|
-
padding: 12px 20px;
|
|
266
|
-
/* TWO layers, and the bottom one is why. The topbar and the rail can be bare
|
|
267
|
-
--glass because the page GROUND is behind them; this bar sits over the task
|
|
268
|
-
cards, and in light --glass is a 5% ink tint with the blur at 0px, so the
|
|
269
|
-
card underneath read straight through it and the buttons landed on top of a
|
|
270
|
-
task title (looked at, 1280w light). Painting --bg under the tint makes the
|
|
271
|
-
bar opaque without giving up the glass character or spending a literal. */
|
|
272
|
-
background: linear-gradient(var(--glass), var(--glass)), var(--bg);
|
|
273
|
-
border-top: var(--line) solid var(--glass-edge);
|
|
274
|
-
backdrop-filter: blur(var(--glass-blur));
|
|
275
|
-
-webkit-backdrop-filter: blur(var(--glass-blur));
|
|
276
|
-
color: var(--ink);
|
|
277
|
-
}
|
|
278
|
-
.bulk-toolbar[hidden] { display: none; }
|
|
279
|
-
.bulk-toolbar__count {
|
|
280
|
-
font-family: var(--label-face);
|
|
281
|
-
font-size: var(--label-size);
|
|
282
|
-
letter-spacing: var(--label-space);
|
|
283
|
-
text-transform: var(--label-case);
|
|
284
|
-
color: var(--ink);
|
|
285
|
-
white-space: nowrap;
|
|
286
|
-
}
|
|
287
|
-
.bulk-toolbar__actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-left: auto; }
|
|
288
|
-
.bulk-toolbar__btn {
|
|
289
|
-
display: inline-flex;
|
|
290
|
-
align-items: center;
|
|
291
|
-
justify-content: center;
|
|
292
|
-
min-height: 30px;
|
|
293
|
-
padding: 0 14px;
|
|
294
|
-
background: var(--glass);
|
|
295
|
-
border: var(--line) solid var(--glass-edge);
|
|
296
|
-
color: var(--ink);
|
|
297
|
-
border-radius: var(--radius-pill);
|
|
298
|
-
font-size: 13px;
|
|
299
|
-
text-transform: lowercase;
|
|
300
|
-
cursor: pointer;
|
|
301
|
-
white-space: nowrap;
|
|
302
|
-
transition: background 150ms, opacity 150ms;
|
|
303
|
-
}
|
|
304
|
-
.bulk-toolbar__btn:hover:not(:disabled) { background: var(--glass-strong); }
|
|
305
|
-
.bulk-toolbar__btn:disabled { color: var(--ink-faint); border-color: var(--rule-soft); cursor: not-allowed; }
|
|
306
|
-
.bulk-toolbar__clear {
|
|
307
|
-
min-height: 30px;
|
|
308
|
-
padding: 0 10px;
|
|
309
|
-
background: transparent;
|
|
310
|
-
border: none;
|
|
311
|
-
color: var(--ink-soft);
|
|
312
|
-
font-size: 13px;
|
|
313
|
-
text-transform: lowercase;
|
|
314
|
-
text-decoration: underline;
|
|
315
|
-
text-underline-offset: 4px;
|
|
316
|
-
cursor: pointer;
|
|
317
|
-
}
|
|
318
|
-
.bulk-toolbar__clear:hover { color: var(--ink); }
|
|
319
|
-
|
|
320
|
-
/* The shell's .app-content already ends in 108px of bottom air at desktop
|
|
321
|
-
width, which is what clears the bar — but at 768 it drops to 48px for the
|
|
322
|
-
phone, and there the bar is TALLER (its actions wrap to their own row). So
|
|
323
|
-
the phone width buys the room back, scoped to this page: goals.html loads
|
|
324
|
-
this sheet too and has no bulk toolbar to clear. */
|
|
325
|
-
@media (max-width: 768px) {
|
|
326
|
-
/* At the drawer breakpoint the sidebar leaves the flow, so the bar spans. */
|
|
327
|
-
.bulk-toolbar { left: 0; padding: 10px 14px; gap: 10px; }
|
|
328
|
-
.bulk-toolbar__actions { margin-left: 0; width: 100%; }
|
|
329
|
-
body[data-page="work"] .app-content { padding-bottom: 200px; }
|
|
330
|
-
}
|
|
331
|
-
|
|
332
163
|
/* ---- "Only mine" lens (the board's tab-wide filter; goal board's To-do bar
|
|
333
164
|
borrows the same class) --------------------------------------------------
|
|
334
165
|
Deliberately QUIETER than the filter bar below it: a checkbox in the label
|
|
@@ -50,7 +50,12 @@ module.exports = function buildArtistGateRouter() {
|
|
|
50
50
|
// second guard is that the refusal must name what holds the deploy and which
|
|
51
51
|
// control releases it, and a sentence assembled independently by each caller is
|
|
52
52
|
// a sentence that eventually says something different. One composer, one wording.
|
|
53
|
-
|
|
53
|
+
//
|
|
54
|
+
// A caller on this machine that reached the app DIRECTLY may also read it,
|
|
55
|
+
// without a session (task 1004205; owner decision 2026-09-23): that is how
|
|
56
|
+
// `bongos upgrade` on the box asks before moving the pin, and the box holds no
|
|
57
|
+
// builder session. Everything else still passes requireBuilder.
|
|
58
|
+
router.get('/artist-gate', unlessDirectLocalCall(api.requireBuilder), async (req, res) => {
|
|
54
59
|
try {
|
|
55
60
|
const gate = api.artistGate;
|
|
56
61
|
const mode = gate.artistGateMode();
|
|
@@ -99,3 +104,21 @@ module.exports = function buildArtistGateRouter() {
|
|
|
99
104
|
|
|
100
105
|
return router;
|
|
101
106
|
};
|
|
107
|
+
|
|
108
|
+
// A request from this machine with NO proxy hop. Public traffic always reaches
|
|
109
|
+
// the app through the proxy (Caddy, behind Cloudflare), which sets a forwarding
|
|
110
|
+
// header, so a loopback peer without one is a local process — the rule
|
|
111
|
+
// src/bongos/metrics.js applies to its local scraper. The payload is ids, dates
|
|
112
|
+
// and the policy level: nothing a process already on the box could not read.
|
|
113
|
+
function isDirectLocalCall(req) {
|
|
114
|
+
const h = req.headers || {};
|
|
115
|
+
if (h['x-forwarded-for'] || h['x-real-ip'] || h.forwarded || h['cf-connecting-ip']) return false;
|
|
116
|
+
const peer = (req.socket && req.socket.remoteAddress) || '';
|
|
117
|
+
return peer === '127.0.0.1' || peer === '::1' || peer === '::ffff:127.0.0.1';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Runs `requireSession` for everyone except a direct local call (above), which
|
|
121
|
+
// goes straight to the handler with no builder attached.
|
|
122
|
+
function unlessDirectLocalCall(requireSession) {
|
|
123
|
+
return (req, res, next) => (isDirectLocalCall(req) ? next() : requireSession(req, res, next));
|
|
124
|
+
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.990",
|
|
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.990",
|
|
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.990",
|
|
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",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// scripts/gds/upgrade-artist-gate.js — the artist gate's deploy hold on the
|
|
4
|
+
// UPGRADE path (task 1004205; ADR 0241).
|
|
5
|
+
//
|
|
6
|
+
// WHY. On a `strict` project an unresolved artist review holds the DEPLOY, never
|
|
7
|
+
// the ship. Only go-live.js asked the gate, and cloudbongos.com moves its pin
|
|
8
|
+
// with the vendored `upgrade.js --from` instead, so on this instance an open
|
|
9
|
+
// review held nothing (2026-09-23: the gate answered holds:true and the upgrade
|
|
10
|
+
// never read it). Every UI change now files such a review (owner decision
|
|
11
|
+
// 2026-09-23, task 1004201), so this is where "the owner sees it before users
|
|
12
|
+
// do" is actually enforced.
|
|
13
|
+
//
|
|
14
|
+
// It reuses go-live.js's reader and judgement verbatim, so both deploy paths
|
|
15
|
+
// decide the same answer the same way: a hold or a broken gate halts, a gate
|
|
16
|
+
// that cannot be read at all is a note. It asks the instance being upgraded,
|
|
17
|
+
// at the origin of --health-url, because the box runs the instance it upgrades;
|
|
18
|
+
// the instance answers a direct local request without a session (the route).
|
|
19
|
+
// The FIRST upgrade carrying this asks an instance that still wants a session,
|
|
20
|
+
// so it can only print the unread note — every upgrade after that holds.
|
|
21
|
+
// Kept out of upgrade.js, which sits at the line cap.
|
|
22
|
+
|
|
23
|
+
const { readArtistGate, artistGateEntries } = require('./go-live');
|
|
24
|
+
|
|
25
|
+
const GATE_TIMEOUT_MS = 10_000;
|
|
26
|
+
|
|
27
|
+
function unreadNote(why) {
|
|
28
|
+
return { halts: [], notes: [{ code: 'ARTIST_GATE_UNREAD', message: `could not read this instance's artist gate (${why}) — if the project runs artist_gate 'strict', an unresolved artist review may be holding this deploy and this upgrade cannot see it` }] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// checkArtistGate — { halts, notes } for this instance's gate, go-live's shape.
|
|
32
|
+
async function checkArtistGate({ healthUrl }, deps = {}) {
|
|
33
|
+
if (!healthUrl) return unreadNote('no --health-url to reach the instance by');
|
|
34
|
+
let origin;
|
|
35
|
+
try { origin = new URL(healthUrl).origin; } catch (_) { return unreadNote(`--health-url ${healthUrl} is not a URL`); }
|
|
36
|
+
const fetchImpl = deps.fetchImpl || fetch;
|
|
37
|
+
const call = async (method, urlPath) => {
|
|
38
|
+
const res = await fetchImpl(`${origin}${urlPath}`, { method, signal: AbortSignal.timeout(GATE_TIMEOUT_MS) });
|
|
39
|
+
let data = null;
|
|
40
|
+
try { data = await res.json(); } catch (_) { /* not JSON: readArtistGate reads it as "not a gate" */ }
|
|
41
|
+
return { status: res.status, ok: res.ok, data };
|
|
42
|
+
};
|
|
43
|
+
return artistGateEntries(await readArtistGate(call));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { checkArtistGate };
|
package/scripts/gds/upgrade.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Turns the former stub (which only ran a module-vs-core compatibility PRE-CHECK) into the real
|
|
6
6
|
// channel a two-repo consumer uses to pull the Cloud Bongos core from version N to N+1:
|
|
7
7
|
//
|
|
8
|
-
// pre-flight clean git tree + module-compat vs the TARGET core version
|
|
8
|
+
// pre-flight clean git tree + module-compat vs the TARGET core version + the artist gate (ADR 0241)
|
|
9
9
|
// → bump pin vendor the new core .tgz, rewrite the package.json `file:` dependency
|
|
10
10
|
// → npm install node_modules/@bongos/core becomes the new version
|
|
11
11
|
// → migrate `npm run migrate` → the core's migrate.sh applies additive `core_*` migrations
|
|
@@ -13,13 +13,10 @@
|
|
|
13
13
|
// → restart restart the instance's service so the new core is live
|
|
14
14
|
// → verify installed package version == target (+ optional /healthz + ledger row)
|
|
15
15
|
//
|
|
16
|
-
// SCOPE (ADR 0100 §2, reshaped by ADR 0103 §4):
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// with `start`/`migrate` npm scripts and a materialized .claude/. Every step below maps to something
|
|
21
|
-
// that consumer actually has. A published-registry source, rollback, and multi-consumer shapes are
|
|
22
|
-
// deliberately OUT — they are follow-ups a second consumer's needs will define.
|
|
16
|
+
// SCOPE (ADR 0100 §2, reshaped by ADR 0103 §4): first built for emersonian-circles (task 1917), an
|
|
17
|
+
// npm-dependency instance pinning "@bongos/core": "file:vendor/bongos-core-<ver>.tgz" with
|
|
18
|
+
// `start`/`migrate` npm scripts and a materialized .claude/; the registry source (ADR 0134) and
|
|
19
|
+
// auto-rollback (task 2149) came later, with the consumers that needed them.
|
|
23
20
|
//
|
|
24
21
|
// Runs from the consumer instance root (default) or `--instance <dir>`. This script ships IN the core
|
|
25
22
|
// package, so once a consumer is on core vN+1 its own `bongos upgrade` is this code. The FIRST bump
|
|
@@ -33,6 +30,7 @@ const crypto = require('node:crypto');
|
|
|
33
30
|
const { spawnSync } = require('node:child_process');
|
|
34
31
|
const { arg, hasFlag } = require('./cli-lib');
|
|
35
32
|
const { parseSemver, compareSemver, isStable, listAvailableVersions } = require('./update-channel');
|
|
33
|
+
const { checkArtistGate } = require('./upgrade-artist-gate');
|
|
36
34
|
|
|
37
35
|
const CORE_PKG = '@bongos/core';
|
|
38
36
|
const ARTIFACT = 'bongos-core';
|
|
@@ -1146,13 +1144,13 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
1146
1144
|
else log(` ✓ pre-flight: database${db ? ` "${db}"` : ''} reachable as the current user`);
|
|
1147
1145
|
}
|
|
1148
1146
|
|
|
1149
|
-
//
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
//
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1147
|
+
// 1a. artist gate (ADR 0241, task 1004205): an open review holds the pin, --force too; a downgrade never waits.
|
|
1148
|
+
const gate = isDowngrade ? { halts: [], notes: [] } : await (deps.checkArtistGate || checkArtistGate)(opts, deps);
|
|
1149
|
+
for (const n of gate.notes) err(` ! ${n.message}`);
|
|
1150
|
+
if (gate.halts.length) return { ok: false, artistGate: true, error: gate.halts.map((h) => [h.message, h.detail].filter(Boolean).join('\n ')).join('\n') };
|
|
1151
|
+
// 1b. mandatory post-restart health check (audit H9 / task 1002222, F5): a bump that restarts the
|
|
1152
|
+
// service must prove it serves, so --health-url is required up front, before anything moves.
|
|
1153
|
+
// (No managed restart ⇒ nothing is brought live here ⇒ the operator restarts + verifies.)
|
|
1156
1154
|
const willRestart = !!(opts.service || opts.restartCmd);
|
|
1157
1155
|
if (!dryRun && opts.healthCheck !== false && willRestart && !opts.healthUrl) {
|
|
1158
1156
|
return { ok: false, error: 'post-restart health check is mandatory for a bump that restarts the service, but no --health-url was given.\n Pass --health-url <url> to confirm the new core serves after restart, or --no-health-check to explicitly skip the confirmation.' };
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.990'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -37,7 +37,12 @@ db.listOpenArtistReviews = async (opts) => { queries.push(opts); return { review
|
|
|
37
37
|
|
|
38
38
|
let mode = 'strict';
|
|
39
39
|
let since = null;
|
|
40
|
-
|
|
40
|
+
let signedIn = true;
|
|
41
|
+
api.requireBuilder = (req, res, next) => {
|
|
42
|
+
if (!signedIn) return res.status(401).json({ error: { code: 'unauthenticated' } });
|
|
43
|
+
req.builder = { id: 7, rank: 'thetes' };
|
|
44
|
+
next();
|
|
45
|
+
};
|
|
41
46
|
// The route must resolve the level through the doorway's ONE reader; stubbing it
|
|
42
47
|
// here is also what proves it does not read branding().project behind its back.
|
|
43
48
|
api.artistGate = { ...gateModule, artistGateMode: () => mode, artistGateSince: () => since };
|
|
@@ -171,4 +176,68 @@ test('a database failure is a 500 with no internals, never a quiet "nothing hold
|
|
|
171
176
|
db.listOpenArtistReviews = async (opts) => { queries.push(opts); return { reviews: rows, truncated: scanTruncated }; };
|
|
172
177
|
});
|
|
173
178
|
|
|
179
|
+
// ---- who may read it without a session (task 1004205) ---------------------------
|
|
180
|
+
|
|
181
|
+
function getWith(path, headers) {
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
http.get(`${base}${path}`, { headers }, (res) => {
|
|
184
|
+
let d = '';
|
|
185
|
+
res.on('data', (c) => { d += c; });
|
|
186
|
+
res.on('end', () => { let j = null; try { j = JSON.parse(d); } catch { /* non-JSON */ } resolve({ status: res.statusCode, body: j }); });
|
|
187
|
+
}).on('error', reject);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
test('the box asking its OWN instance directly reads the gate with no session — how `bongos upgrade` asks', async () => {
|
|
192
|
+
reset({ open: [review()] });
|
|
193
|
+
signedIn = false;
|
|
194
|
+
try {
|
|
195
|
+
const r = await getWith('/artist-gate', {});
|
|
196
|
+
assert.equal(r.status, 200);
|
|
197
|
+
assert.equal(r.body.holds, true);
|
|
198
|
+
assert.deepEqual(r.body.reviews, [{ id: '9001', created_at: '2026-09-12T00:00:00.000Z' }]);
|
|
199
|
+
} finally { signedIn = true; }
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('anything that came through a proxy still needs a session — public traffic never rides the local exception', async () => {
|
|
203
|
+
reset({ open: [review()] });
|
|
204
|
+
signedIn = false;
|
|
205
|
+
try {
|
|
206
|
+
for (const headers of [
|
|
207
|
+
{ 'X-Forwarded-For': '203.0.113.9' },
|
|
208
|
+
{ 'X-Real-IP': '203.0.113.9' },
|
|
209
|
+
{ Forwarded: 'for=203.0.113.9' },
|
|
210
|
+
{ 'CF-Connecting-IP': '203.0.113.9' },
|
|
211
|
+
]) {
|
|
212
|
+
const r = await getWith('/artist-gate', headers);
|
|
213
|
+
assert.equal(r.status, 401, JSON.stringify(headers));
|
|
214
|
+
}
|
|
215
|
+
} finally { signedIn = true; }
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('a peer that is not this machine is not local, forwarding header or not — it needs a session', async () => {
|
|
219
|
+
reset({ open: [review()] });
|
|
220
|
+
signedIn = false;
|
|
221
|
+
try {
|
|
222
|
+
const out = await new Promise((resolve) => {
|
|
223
|
+
const r = { statusCode: 200 };
|
|
224
|
+
const res = {
|
|
225
|
+
status(c) { r.statusCode = c; return this; },
|
|
226
|
+
json(b) { r.body = b; resolve(r); return this; },
|
|
227
|
+
set() { return this; },
|
|
228
|
+
fail(code, o) { r.statusCode = (o && o.status) || 400; resolve(r); },
|
|
229
|
+
};
|
|
230
|
+
buildRouter().handle({ method: 'GET', url: '/artist-gate', headers: {}, socket: { remoteAddress: '203.0.113.9' } }, res, () => resolve({ statusCode: 404 }));
|
|
231
|
+
});
|
|
232
|
+
assert.equal(out.statusCode, 401);
|
|
233
|
+
} finally { signedIn = true; }
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('a signed-in builder behind the proxy reads it exactly as before', async () => {
|
|
237
|
+
reset({ open: [review()] });
|
|
238
|
+
const r = await getWith('/artist-gate', { 'X-Forwarded-For': '203.0.113.9' });
|
|
239
|
+
assert.equal(r.status, 200);
|
|
240
|
+
assert.equal(r.body.holds, true);
|
|
241
|
+
});
|
|
242
|
+
|
|
174
243
|
test.after(() => server.close());
|
|
@@ -104,7 +104,7 @@ test('a prose link buys BOTH axes — padding for width, negative margin to give
|
|
|
104
104
|
// ---- 3. the world's own bans ------------------------------------------------
|
|
105
105
|
|
|
106
106
|
test('nothing on the board paints text white on the accent', () => {
|
|
107
|
-
const claim = rule(
|
|
107
|
+
const claim = rule(styleCss, '.task__claim');
|
|
108
108
|
assert.ok(claim, '.task__claim has a rule');
|
|
109
109
|
assert.match(claim, /background:\s*var\(--accent\)/, 'the claim button is the page\'s one accent fill');
|
|
110
110
|
assert.match(claim, /color:\s*var\(--accent-on\)/,
|
|
@@ -115,6 +115,13 @@ test('nothing on the board paints text white on the accent', () => {
|
|
|
115
115
|
'the literal this task removed must not come back');
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
test('the shared task + bulk-toolbar rules live in style.css (the shared base), not the page-local work.css', () => {
|
|
119
|
+
for (const sel of ['.task__age', '.task__age--stale', '.task__tag--newcomer', '.task__tag--mine', '.task__tag--rank', '.task__tag--rank-open', '.task__claim', '.task__touches', '.task', '.bulk-toolbar', '.bulk-toolbar__count', '.bulk-toolbar__actions', '.bulk-toolbar__btn', '.bulk-toolbar__clear']) {
|
|
120
|
+
assert.ok(rule(styleCss, sel), `style.css carries ${sel}, beside .task__tag`);
|
|
121
|
+
assert.equal(rule(workCss, sel), null, `work.css must not restate ${sel}`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
118
125
|
test('work.css spends no compat alias — that was the sheet\'s removal list', () => {
|
|
119
126
|
const aliases = ['--surface', '--surface-2', '--border', '--border-strong', '--accent-weak', '--accent-text'];
|
|
120
127
|
const bare = workCss.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
@@ -124,7 +131,7 @@ test('work.css spends no compat alias — that was the sheet\'s removal list', (
|
|
|
124
131
|
});
|
|
125
132
|
|
|
126
133
|
test('the bulk toolbar carries no rgba literal and is opaque over the cards', () => {
|
|
127
|
-
const bar = rule(
|
|
134
|
+
const bar = rule(styleCss, '.bulk-toolbar');
|
|
128
135
|
assert.ok(bar, '.bulk-toolbar has a rule');
|
|
129
136
|
assert.doesNotMatch(bar, /rgba?\(/i,
|
|
130
137
|
'the three rgba(255,255,255,…) literals went with the inverted --ink surface that needed them');
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// tests/upgrade_artist_gate.mjs — the artist gate holds the pin on the UPGRADE
|
|
2
|
+
// path too (task 1004205; ADR 0241).
|
|
3
|
+
//
|
|
4
|
+
// go-live.js consulted the gate; the vendored `upgrade.js --from` this instance
|
|
5
|
+
// actually moves its pin with did not, so an open review held nothing. These
|
|
6
|
+
// pin the gate reader against a fake instance, and runUpgrade refusing BEFORE
|
|
7
|
+
// anything moves — with a downgrade (rolling back) never held.
|
|
8
|
+
//
|
|
9
|
+
// Run: node tests/upgrade_artist_gate.mjs
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { makeRunner } from './helpers.mjs';
|
|
16
|
+
import { checkArtistGate } from '../scripts/gds/upgrade-artist-gate.js';
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const u = require('../scripts/gds/upgrade.js');
|
|
20
|
+
const { test, summary } = makeRunner();
|
|
21
|
+
|
|
22
|
+
// A fake instance: answers GET <origin>/api/bongos/artist-gate with `status` + `body`.
|
|
23
|
+
function instance(status, body) {
|
|
24
|
+
const asked = [];
|
|
25
|
+
const fetchImpl = async (url, init) => {
|
|
26
|
+
asked.push({ url, method: init && init.method });
|
|
27
|
+
return { status, ok: status >= 200 && status < 300, json: async () => { if (body === undefined) throw new Error('not json'); return body; } };
|
|
28
|
+
};
|
|
29
|
+
return { asked, fetchImpl };
|
|
30
|
+
}
|
|
31
|
+
const HOLD = {
|
|
32
|
+
ok: true, mode: 'strict', holds: true, reviews: [{ id: '1004199' }],
|
|
33
|
+
halt: { code: 'ARTIST_REVIEW_UNRESOLVED', message: '1 unresolved artist review hold this deploy', detail: 'Held by task 1004199. Two ways out.' },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ---- the reader ---------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
await test('it asks the instance being upgraded, at the ORIGIN of --health-url', async () => {
|
|
39
|
+
const i = instance(200, { ok: true, mode: 'strict', holds: false, reviews: [], halt: null });
|
|
40
|
+
const r = await checkArtistGate({ healthUrl: 'http://127.0.0.1:3000/api/bongos/healthz' }, { fetchImpl: i.fetchImpl });
|
|
41
|
+
assert.deepEqual(i.asked, [{ url: 'http://127.0.0.1:3000/api/bongos/artist-gate', method: 'GET' }]);
|
|
42
|
+
assert.deepEqual(r, { halts: [], notes: [] }, 'strict with nothing open holds nothing');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await test('an open review on a strict project HALTS, naming the review and the way out', async () => {
|
|
46
|
+
const r = await checkArtistGate({ healthUrl: 'https://cloudbongos.com/api/bongos/healthz' }, { fetchImpl: instance(200, HOLD).fetchImpl });
|
|
47
|
+
assert.equal(r.halts.length, 1);
|
|
48
|
+
assert.equal(r.halts[0].code, 'ARTIST_REVIEW_UNRESOLVED');
|
|
49
|
+
assert.match(r.halts[0].detail, /1004199/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await test('advisory and off hold nothing', async () => {
|
|
53
|
+
for (const mode of ['advisory', 'off']) {
|
|
54
|
+
const r = await checkArtistGate({ healthUrl: 'http://x/h' }, { fetchImpl: instance(200, { mode, holds: false, reviews: [], halt: null }).fetchImpl });
|
|
55
|
+
assert.deepEqual(r.halts, [], mode);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await test('a gate that is THERE and broken halts; one that cannot be read at all is a note', async () => {
|
|
60
|
+
assert.equal((await checkArtistGate({ healthUrl: 'http://x/h' }, { fetchImpl: instance(500, {}).fetchImpl })).halts[0].code, 'ARTIST_GATE_UNREADABLE');
|
|
61
|
+
for (const [status, body] of [[404, {}], [200, undefined], [200, { hello: 'an SPA page' }]]) {
|
|
62
|
+
const r = await checkArtistGate({ healthUrl: 'http://x/h' }, { fetchImpl: instance(status, body).fetchImpl });
|
|
63
|
+
assert.deepEqual(r.halts, [], `${status} → no halt`);
|
|
64
|
+
assert.equal(r.notes[0].code, 'ARTIST_GATE_UNREAD', `${status} → said out loud, never read as "nothing holds" silently`);
|
|
65
|
+
}
|
|
66
|
+
const down = await checkArtistGate({ healthUrl: 'http://x/h' }, { fetchImpl: async () => { throw new Error('ECONNREFUSED'); } });
|
|
67
|
+
assert.equal(down.notes[0].code, 'ARTIST_GATE_UNREAD');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await test('with no --health-url there is nothing to ask — a note, never a silent pass', async () => {
|
|
71
|
+
for (const healthUrl of [undefined, 'not a url']) {
|
|
72
|
+
const r = await checkArtistGate({ healthUrl }, { fetchImpl: async () => { throw new Error('must not be called'); } });
|
|
73
|
+
assert.deepEqual(r.halts, []);
|
|
74
|
+
assert.equal(r.notes[0].code, 'ARTIST_GATE_UNREAD');
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ---- runUpgrade -----------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
function consumer(version = '1.14.1') {
|
|
81
|
+
const dir = mkdtempSync(join(tmpdir(), 'otb-upgrade-gate-'));
|
|
82
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify({
|
|
83
|
+
name: 'c', version: '0.1.0', dependencies: { '@bongos/core': `file:vendor/bongos-core-${version}.tgz` }, scripts: { migrate: 'true', start: 'true' },
|
|
84
|
+
}, null, 2) + '\n');
|
|
85
|
+
mkdirSync(join(dir, 'node_modules', '@bongos', 'core'), { recursive: true });
|
|
86
|
+
writeFileSync(join(dir, 'node_modules', '@bongos', 'core', 'package.json'), JSON.stringify({ name: '@bongos/core', version }) + '\n');
|
|
87
|
+
return dir;
|
|
88
|
+
}
|
|
89
|
+
function tarball(version) {
|
|
90
|
+
const src = mkdtempSync(join(tmpdir(), 'src-'));
|
|
91
|
+
writeFileSync(join(src, `bongos-core-${version}.tgz`), 'TGZ');
|
|
92
|
+
return join(src, `bongos-core-${version}.tgz`);
|
|
93
|
+
}
|
|
94
|
+
const run = (calls) => (cmd, args) => { calls.push(`${cmd} ${(args || []).join(' ')}`); return cmd === 'git' ? { status: 0, stdout: '' } : { status: 0 }; };
|
|
95
|
+
const holding = async () => ({ halts: [{ code: 'ARTIST_REVIEW_UNRESOLVED', message: '1 unresolved artist review hold this deploy', detail: 'Held by task 1004199.' }], notes: [] });
|
|
96
|
+
|
|
97
|
+
await test('runUpgrade REFUSES a held pin before anything moves — --force included', async () => {
|
|
98
|
+
for (const force of [false, true]) {
|
|
99
|
+
const dir = consumer();
|
|
100
|
+
const calls = [];
|
|
101
|
+
const res = await u.runUpgrade({ to: '1.15.0', from: tarball('1.15.0'), instance: dir, force, skipMigrate: true },
|
|
102
|
+
{ log: () => {}, err: () => {}, run: run(calls), checkArtistGate: holding });
|
|
103
|
+
assert.equal(res.ok, false);
|
|
104
|
+
assert.equal(res.artistGate, true);
|
|
105
|
+
assert.match(res.error, /unresolved artist review/);
|
|
106
|
+
assert.match(res.error, /1004199/, 'names the review');
|
|
107
|
+
assert.equal(u.readPinnedCoreVersion(dir), '1.14.1', 'the pin never moved');
|
|
108
|
+
assert.ok(!calls.some((c) => c.startsWith('npm')), 'nothing installed');
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await test('a dry run reports the hold too — that is what a dry run is for', async () => {
|
|
113
|
+
const res = await u.runUpgrade({ to: '1.15.0', from: tarball('1.15.0'), instance: consumer(), dryRun: true, skipMigrate: true },
|
|
114
|
+
{ log: () => {}, err: () => {}, run: run([]), checkArtistGate: holding });
|
|
115
|
+
assert.equal(res.ok, false);
|
|
116
|
+
assert.equal(res.artistGate, true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await test('nothing open: the upgrade proceeds exactly as before, and a note is printed, not swallowed', async () => {
|
|
120
|
+
const errs = [];
|
|
121
|
+
const res = await u.runUpgrade({ to: '1.15.0', from: tarball('1.15.0'), instance: consumer(), dryRun: true, skipMigrate: true },
|
|
122
|
+
{ log: () => {}, err: (m) => errs.push(m), run: run([]), checkArtistGate: async () => ({ halts: [], notes: [{ code: 'ARTIST_GATE_UNREAD', message: 'could not read this instance\'s artist gate (x)' }] }) });
|
|
123
|
+
assert.equal(res.ok, true);
|
|
124
|
+
assert.ok(errs.some((m) => /could not read this instance's artist gate/.test(m)));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await test('a DOWNGRADE is never held — rolling back must not wait on a review', async () => {
|
|
128
|
+
let asked = 0;
|
|
129
|
+
const res = await u.runUpgrade({ to: '1.14.0', from: tarball('1.14.0'), instance: consumer('1.14.1'), allowDowngrade: true, dryRun: true, skipMigrate: true },
|
|
130
|
+
{ log: () => {}, err: () => {}, run: run([]), checkArtistGate: async () => { asked++; return holding(); } });
|
|
131
|
+
assert.equal(asked, 0);
|
|
132
|
+
assert.equal(res.ok, true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await test('the gate runs from the pre-flight, before the pin step, and the reader is go-live\'s own', () => {
|
|
136
|
+
const whole = readFileSync(new URL('../scripts/gds/upgrade.js', import.meta.url), 'utf8');
|
|
137
|
+
const src = whole.slice(whole.indexOf('async function runUpgrade('));
|
|
138
|
+
const gateAt = src.indexOf('checkArtistGate)(opts, deps)');
|
|
139
|
+
assert.ok(gateAt > 0 && gateAt < src.indexOf('writePin(instanceDir'), 'checked before the pin is written');
|
|
140
|
+
const gateSrc = readFileSync(new URL('../scripts/gds/upgrade-artist-gate.js', import.meta.url), 'utf8');
|
|
141
|
+
assert.match(gateSrc, /require\('\.\/go-live'\)/, 'one reader and one judgement for both deploy paths');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
summary();
|