@bongos/core 1.19.523 → 1.19.525
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 +27 -22
- package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +185 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +8 -9
- package/docs/copy-registry.json +9 -18
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/collab-lib.js +37 -13
- package/modules/hall-ui/public/collab.js +31 -8
- package/modules/hall-ui/public/oversight.css +1 -1
- package/modules/hall-ui/public/style.css +3 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/collab_page.mjs +60 -3
- package/tests/hall_audit.mjs +66 -0
- package/tests/hall_board_world.mjs +7 -9
- package/tests/hall_collab_world.mjs +22 -3
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.525",
|
|
6
|
+
"core_contract": "1.19.525",
|
|
7
|
+
"source_commit": "05d381d80b50bdf77128d21d9dbe8b99006aff91",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-04T00:50:39.702Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 432,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
14
|
"functional_verbatim": 1969,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2425,
|
|
20
|
+
"tree_sha256": "cd090a2014e8d8d8f09333158a652163b425ad11fef0c4c578802dbd63f91ddf",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1709,10 +1709,15 @@
|
|
|
1709
1709
|
"mode": "0000644",
|
|
1710
1710
|
"sha256": "d8bffd4c66cfddfeadd3c734d029e357f05a8eb804a79e5c3cad4014c9417b56"
|
|
1711
1711
|
},
|
|
1712
|
+
{
|
|
1713
|
+
"path": "docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md",
|
|
1714
|
+
"mode": "0000644",
|
|
1715
|
+
"sha256": "30cf04604592423e7520fc3e0e7ea8bd020747bcf1b8e712ae339e1e9a5eb324"
|
|
1716
|
+
},
|
|
1712
1717
|
{
|
|
1713
1718
|
"path": "docs/adr/README.md",
|
|
1714
1719
|
"mode": "0000644",
|
|
1715
|
-
"sha256": "
|
|
1720
|
+
"sha256": "880547e3bd6629f52c07a17e1d38b70c7f8f80f3bc3984735f13d02effb56b2f"
|
|
1716
1721
|
},
|
|
1717
1722
|
{
|
|
1718
1723
|
"path": "docs/api-reference.md",
|
|
@@ -1747,12 +1752,12 @@
|
|
|
1747
1752
|
{
|
|
1748
1753
|
"path": "docs/copy-inventory.md",
|
|
1749
1754
|
"mode": "0000644",
|
|
1750
|
-
"sha256": "
|
|
1755
|
+
"sha256": "e848eaf9b035df899cc6f6d80f98cb8b01c2e9079fc5f32db0dd7b3a73d107ef"
|
|
1751
1756
|
},
|
|
1752
1757
|
{
|
|
1753
1758
|
"path": "docs/copy-registry.json",
|
|
1754
1759
|
"mode": "0000644",
|
|
1755
|
-
"sha256": "
|
|
1760
|
+
"sha256": "d4fefb8fce99e3903eec6f0796c9e61b81732a9784fb95fa4a3299fa9da3278a"
|
|
1756
1761
|
},
|
|
1757
1762
|
{
|
|
1758
1763
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2597,7 +2602,7 @@
|
|
|
2597
2602
|
{
|
|
2598
2603
|
"path": "docs/module-api-changelog.md",
|
|
2599
2604
|
"mode": "0000644",
|
|
2600
|
-
"sha256": "
|
|
2605
|
+
"sha256": "5b4103ec2cd85e4ab6dd6f2906c069b02f90248d37e7bc0b5de3a4408648667f"
|
|
2601
2606
|
},
|
|
2602
2607
|
{
|
|
2603
2608
|
"path": "docs/modules-contract.md",
|
|
@@ -4527,7 +4532,7 @@
|
|
|
4527
4532
|
{
|
|
4528
4533
|
"path": "modules/hall-ui/public/collab-lib.js",
|
|
4529
4534
|
"mode": "0000644",
|
|
4530
|
-
"sha256": "
|
|
4535
|
+
"sha256": "5930395c551506e374c3e7ff89347f4151612ae336a18d8c13c6a725a3e844b4"
|
|
4531
4536
|
},
|
|
4532
4537
|
{
|
|
4533
4538
|
"path": "modules/hall-ui/public/collab.css",
|
|
@@ -4542,7 +4547,7 @@
|
|
|
4542
4547
|
{
|
|
4543
4548
|
"path": "modules/hall-ui/public/collab.js",
|
|
4544
4549
|
"mode": "0000644",
|
|
4545
|
-
"sha256": "
|
|
4550
|
+
"sha256": "3e953b00bbd57a248ed233b98b2451d8f49760bd066d5a58466860495c4d2e35"
|
|
4546
4551
|
},
|
|
4547
4552
|
{
|
|
4548
4553
|
"path": "modules/hall-ui/public/collab.states.json",
|
|
@@ -4847,7 +4852,7 @@
|
|
|
4847
4852
|
{
|
|
4848
4853
|
"path": "modules/hall-ui/public/oversight.css",
|
|
4849
4854
|
"mode": "0000644",
|
|
4850
|
-
"sha256": "
|
|
4855
|
+
"sha256": "9aa3fd3f662274571e049eb9931f8dd575eef527a624bc6a5d57102061f54936"
|
|
4851
4856
|
},
|
|
4852
4857
|
{
|
|
4853
4858
|
"path": "modules/hall-ui/public/pair.css",
|
|
@@ -5077,7 +5082,7 @@
|
|
|
5077
5082
|
{
|
|
5078
5083
|
"path": "modules/hall-ui/public/style.css",
|
|
5079
5084
|
"mode": "0000644",
|
|
5080
|
-
"sha256": "
|
|
5085
|
+
"sha256": "b37fa5714da911e20f1cab9bca3e4a76314065dd27cc59fad3bbb7ad9314370b"
|
|
5081
5086
|
},
|
|
5082
5087
|
{
|
|
5083
5088
|
"path": "modules/hall-ui/public/task-detail.js",
|
|
@@ -7297,12 +7302,12 @@
|
|
|
7297
7302
|
{
|
|
7298
7303
|
"path": "package-lock.json",
|
|
7299
7304
|
"mode": "0000644",
|
|
7300
|
-
"sha256": "
|
|
7305
|
+
"sha256": "a874a97765e31f2c3f6c022d648a27336ae780b1506d065c8f96ea6ddbc017cc"
|
|
7301
7306
|
},
|
|
7302
7307
|
{
|
|
7303
7308
|
"path": "package.json",
|
|
7304
7309
|
"mode": "0000644",
|
|
7305
|
-
"sha256": "
|
|
7310
|
+
"sha256": "4713013d945cec19710bc535aef5a8908151dadc624afb2253378958a0de7d8e"
|
|
7306
7311
|
},
|
|
7307
7312
|
{
|
|
7308
7313
|
"path": "public-docs/index.html",
|
|
@@ -8972,7 +8977,7 @@
|
|
|
8972
8977
|
{
|
|
8973
8978
|
"path": "src/module-api.js",
|
|
8974
8979
|
"mode": "0000644",
|
|
8975
|
-
"sha256": "
|
|
8980
|
+
"sha256": "122875c28651704555d439dbfec3e148dd9fe3b230e5adaa82b921787ecf508d"
|
|
8976
8981
|
},
|
|
8977
8982
|
{
|
|
8978
8983
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9557,7 +9562,7 @@
|
|
|
9557
9562
|
{
|
|
9558
9563
|
"path": "tests/collab_page.mjs",
|
|
9559
9564
|
"mode": "0000644",
|
|
9560
|
-
"sha256": "
|
|
9565
|
+
"sha256": "98408d2e16e0ca596a31e9e97588584717dae126e863323088b5457dfef24420"
|
|
9561
9566
|
},
|
|
9562
9567
|
{
|
|
9563
9568
|
"path": "tests/conductor.mjs",
|
|
@@ -10357,12 +10362,12 @@
|
|
|
10357
10362
|
{
|
|
10358
10363
|
"path": "tests/hall_audit.mjs",
|
|
10359
10364
|
"mode": "0000644",
|
|
10360
|
-
"sha256": "
|
|
10365
|
+
"sha256": "7e73863dba1bb570f61e3415af6ba84ba74207d9efd11c9032873b2cc1734ee9"
|
|
10361
10366
|
},
|
|
10362
10367
|
{
|
|
10363
10368
|
"path": "tests/hall_board_world.mjs",
|
|
10364
10369
|
"mode": "0000644",
|
|
10365
|
-
"sha256": "
|
|
10370
|
+
"sha256": "31013c61fa0099ae2195148aeef5c810996edafc667ea5bc2e4c7176570e4ee7"
|
|
10366
10371
|
},
|
|
10367
10372
|
{
|
|
10368
10373
|
"path": "tests/hall_catalog_world.mjs",
|
|
@@ -10372,7 +10377,7 @@
|
|
|
10372
10377
|
{
|
|
10373
10378
|
"path": "tests/hall_collab_world.mjs",
|
|
10374
10379
|
"mode": "0000644",
|
|
10375
|
-
"sha256": "
|
|
10380
|
+
"sha256": "051bf806c90c8d62f4ab38b25cb0c8c7b49f68858ab223d74b174cedecc13ac6"
|
|
10376
10381
|
},
|
|
10377
10382
|
{
|
|
10378
10383
|
"path": "tests/hall_error_envelope.mjs",
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# 0241 — The artist gate holds the DEPLOY, not the ship, and it reads a state rather than a judgement
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-09-03
|
|
5
|
+
- **Tasks:** [#1003116](https://cloudbongos.com/builders#/task/1003116) (R05 of goal 1000074, the artist's loop) — the decision task the goal placed BEFORE [#1003115](https://cloudbongos.com/builders#/task/1003115) (R04, the review task), so that no code is wired to a gate whose authority is unsettled
|
|
6
|
+
- **Reconciles:** [ADR 0162](<redacted>.md) — review gates retired on `main`; a verdict is advisory and never self-executing
|
|
7
|
+
- **Related:** 0178 (the copy desk flag + queue, which deliberately blocked nothing and named this decision as the one that would settle it), 0233 (a copy proposal is a task carrying a patch), 0016 (authority is resolved server-side, never from a local file), 0120 (pay on land), 0137 (land → release → deploy are three separate gates)
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
Goal 1000074 gives the artist real authority over what ships — the owner's ABI brief hands them
|
|
12
|
+
the project's text, its story and ideology, its purpose and gravitas. The goal also carries the
|
|
13
|
+
owner's constraint, verbatim: **blocking strictness is per project, not global.** A project whose
|
|
14
|
+
outputs must be closely monitored should be strict; a project where visuals and copy come
|
|
15
|
+
afterwards can run loose.
|
|
16
|
+
|
|
17
|
+
And it carries the collision that constraint creates. ADR 0162 retired review gates: after a
|
|
18
|
+
code-owner review requirement stranded two graded-PASS ships inside a day, review requirements on
|
|
19
|
+
`main` were removed permanently, and the principle recorded was that **no human click sits in the
|
|
20
|
+
land path**. An artist who can block a ship is a new human click in the land path. Two rules, one
|
|
21
|
+
of them about to contradict the other, which is exactly the condition R05 exists to end.
|
|
22
|
+
|
|
23
|
+
## What ADR 0162 actually forbids
|
|
24
|
+
|
|
25
|
+
This matters, because the shorthand it is usually cited by — "a verdict is advisory, never
|
|
26
|
+
self-executing" — is a paraphrase, and reading the paraphrase instead of the ADR would rule out
|
|
27
|
+
things 0162 explicitly keeps.
|
|
28
|
+
|
|
29
|
+
0162's own table of what still guards the safety machinery lists four controls and marks three of
|
|
30
|
+
them **fails closed** — including the subagent grader panel, which runs on every GDS ship and parks
|
|
31
|
+
a failing task at `completed`. So 0162 plainly permits an automatic gate that stops work. What it
|
|
32
|
+
retired was something narrower and structural: a gate GitHub could only express as **accounts**.
|
|
33
|
+
Its reasoning is worth restating because it is the test this ADR applies:
|
|
34
|
+
|
|
35
|
+
1. **Identity-pinned, not rank-based.** GitHub has no concept of GDS ranks, so "code-owner review"
|
|
36
|
+
degenerated into "one specific account must click." That collides with ADR 0016.
|
|
37
|
+
2. **It stranded work with no escape hatch.** A blocked PR had nowhere to go, including the PRs that
|
|
38
|
+
fixed the strand.
|
|
39
|
+
|
|
40
|
+
So the rule 0162 leaves behind is not "nothing may ever block". It is:
|
|
41
|
+
|
|
42
|
+
> A gate is admissible when it is evaluated automatically, from state the system can read,
|
|
43
|
+
> consistently for every builder, with a named escape hatch. A gate is inadmissible when it
|
|
44
|
+
> requires a particular person to act before work can move, and stalls indefinitely when they
|
|
45
|
+
> do not.
|
|
46
|
+
|
|
47
|
+
The grader passes that test. A CODEOWNERS review does not. The artist gate has to be built to pass
|
|
48
|
+
it too, and everything below is that shape.
|
|
49
|
+
|
|
50
|
+
## Decision
|
|
51
|
+
|
|
52
|
+
### 1. The gate reads a STATE, never an opinion
|
|
53
|
+
|
|
54
|
+
The blocking condition is **"is there an unresolved artist review task attached to this work?"** —
|
|
55
|
+
not "did the artist approve it?".
|
|
56
|
+
|
|
57
|
+
That distinction is the whole reconciliation, so it is worth being exact about. An artist's verdict
|
|
58
|
+
never moves anything by itself: an artist saying the copy is bad does not stop a deploy, and an
|
|
59
|
+
artist saying it is fine does not release one. What the system reads is the review task's *status*,
|
|
60
|
+
which is ordinary GDS state that any rank can see and that moves by the ordinary means — the task
|
|
61
|
+
is resolved, dismissed, or shipped. The artist's judgement is what they write **into** that task;
|
|
62
|
+
it stays advisory in precisely 0162's sense.
|
|
63
|
+
|
|
64
|
+
This is not a technicality to route around the rule. It is what makes the gate rank-consistent and
|
|
65
|
+
machine-checkable rather than identity-pinned: a status is a fact, a judgement is a person.
|
|
66
|
+
|
|
67
|
+
### 2. Strictness is a per-project setting, and it holds the DEPLOY
|
|
68
|
+
|
|
69
|
+
Three ordered levels, a project setting like any other:
|
|
70
|
+
|
|
71
|
+
| Level | Artist review tasks | What is held |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `off` | not generated | nothing |
|
|
74
|
+
| `advisory` | generated | nothing |
|
|
75
|
+
| `strict` | generated | **the deploy** — the instance's pin may not move while one is unresolved |
|
|
76
|
+
|
|
77
|
+
**`strict` holds the deploy, not the ship** (owner ruling, 2026-09-03). This is the load-bearing
|
|
78
|
+
choice, and it is what lets a real gate coexist with 0162 rather than merely arguing with it:
|
|
79
|
+
|
|
80
|
+
- **No builder is ever stranded.** Their work is graded, merged, credited and landed on `main` on
|
|
81
|
+
CI alone, exactly as today. The wave-4 strand class that forced 0162 cannot recur, because
|
|
82
|
+
nothing an artist does can hold a builder's ship at `completed` or their credits at unpaid.
|
|
83
|
+
- **It gates what the artist was actually given.** The ABI brief hands the artist what users see —
|
|
84
|
+
no slop appearance, the project's text, whether story and gravitas land. Users see the deployed
|
|
85
|
+
instance, not `main`. ADR 0137 already separates land, release and deploy into three gates; the
|
|
86
|
+
artist's authority attaches to the third one, which is the one their mandate is about.
|
|
87
|
+
- **Nothing is ever un-shipped.** The gate can only delay a pin move, an action that is already
|
|
88
|
+
deliberate, already owner-initiated, and already reversible by doing it later.
|
|
89
|
+
|
|
90
|
+
### 3. The escape hatch is the project owner, and it is never quiet
|
|
91
|
+
|
|
92
|
+
At `strict`, an unresolved review can be released past by the **project owner only** — not the
|
|
93
|
+
builder, not any Metic+, and not the artist (owner ruling, 2026-09-03). The owner's reason is
|
|
94
|
+
recorded because it constrains the implementation: *people can be both artist and builder, so they
|
|
95
|
+
could potentially pass their own work; they should care about passing the grade, or improving the
|
|
96
|
+
grader.*
|
|
97
|
+
|
|
98
|
+
Two things follow, and the second is a reading of that reason rather than a literal instruction —
|
|
99
|
+
flagged as such so it can be corrected:
|
|
100
|
+
|
|
101
|
+
- The right answer to being gated is to **fix the work or fix the grader**, not to wave it through.
|
|
102
|
+
The release valve exists so a project never deadlocks, not as a routine step.
|
|
103
|
+
- A solo instance would deadlock under a strict self-approval bar — on this instance the owner is
|
|
104
|
+
frequently also the builder and the artist. So a self-release is **permitted and recorded as
|
|
105
|
+
such**: releasing past a gate on your own work is allowed, is written to the audit log as a
|
|
106
|
+
self-override with a required rationale, and is visible in the ledger. It costs visibility rather
|
|
107
|
+
than being impossible. If the owner meant the harder rule — that you may never release your own
|
|
108
|
+
work, and a solo project must resolve the review properly or stay undeployed — that is a
|
|
109
|
+
one-line change here and in R04's implementation.
|
|
110
|
+
|
|
111
|
+
The mechanism is the one that already exists for a failed grader verdict: the override-request
|
|
112
|
+
(`POST /tasks/:id/override-request`, rationale required, audit-logged). No new machinery, no second
|
|
113
|
+
authority path, and the escape hatch 0162's incident found missing is present from the first day.
|
|
114
|
+
|
|
115
|
+
### 4. The default is `strict`
|
|
116
|
+
|
|
117
|
+
Owner ruling, 2026-09-03, against the recommendation this ADR's author put forward (`advisory`, on
|
|
118
|
+
the grounds that a gate arriving by default is how 0162's incident happened).
|
|
119
|
+
|
|
120
|
+
The ruling is coherent with §2 and is recorded with its consequence stated plainly: because
|
|
121
|
+
`strict` holds only the deploy, a default-strict project cannot strand a builder — the worst case
|
|
122
|
+
is a project whose pin waits on a review nobody has done yet. That is a visible, owner-fixable
|
|
123
|
+
state, not a silent one, and the owner holds the release valve. **This instance is included**: once
|
|
124
|
+
R04 ships, moving cloudbongos.com's pin will require the artist reviews on that release's copy and
|
|
125
|
+
visual work to be resolved or released.
|
|
126
|
+
|
|
127
|
+
Two guards on that, which R04 must implement rather than assume:
|
|
128
|
+
|
|
129
|
+
- A project at `strict` with **no artist** must still be able to deploy. The review task exists, it
|
|
130
|
+
is unresolved, and the owner releases it — a project with nobody in the role is the normal case
|
|
131
|
+
for a new instance, and a default that bricks new instances is not a default.
|
|
132
|
+
- The gate must say **what** is holding the deploy and **which** control releases it, in one line
|
|
133
|
+
at the point of refusal. 0162's incident was made worse by a blocked state that did not explain
|
|
134
|
+
itself.
|
|
135
|
+
|
|
136
|
+
### 5. Tightening the setting is not retroactive
|
|
137
|
+
|
|
138
|
+
When a project raises its strictness, the new level applies to work that ships **after** the
|
|
139
|
+
change. Already-shipped work is not retroactively gated, and an already-moved pin is never rolled
|
|
140
|
+
back.
|
|
141
|
+
|
|
142
|
+
The alternative — re-gating history — would mean a single setting change could strand a project's
|
|
143
|
+
next deploy behind a backlog of review tasks for work nobody remembers, and would make the setting
|
|
144
|
+
frightening to raise. A project that genuinely wants its existing surface reviewed has a better
|
|
145
|
+
tool already: R02's flag queue, which is exactly the mechanism for "someone should look at this
|
|
146
|
+
copy that already shipped."
|
|
147
|
+
|
|
148
|
+
Loosening is symmetric and immediate: unresolved review tasks stay on the board as ordinary work,
|
|
149
|
+
they simply stop holding the pin.
|
|
150
|
+
|
|
151
|
+
## Consequences
|
|
152
|
+
|
|
153
|
+
- **R04 ([#1003115](https://cloudbongos.com/builders#/task/1003115)) is now buildable**, and its
|
|
154
|
+
brief is narrowed by this ADR: it generates the review task and attaches it, and the gate it
|
|
155
|
+
wires is a state check at the deploy step. It does not build an approval UI, and it must not
|
|
156
|
+
make a verdict self-executing.
|
|
157
|
+
- **A new project setting** is added to the project settings vocabulary
|
|
158
|
+
(`artist_gate ∈ off | advisory | strict`, default `strict`). It is a policy key the running
|
|
159
|
+
instance obeys, the same shape as the join door (ADR 0238).
|
|
160
|
+
- **ADR 0162 is reconciled, not amended.** Nothing in it changes: `main` still carries required
|
|
161
|
+
status checks only, no human click sits in the land path, and no CODEOWNERS file returns. This
|
|
162
|
+
ADR adds a gate on a *different* boundary — the deploy — and adopts 0162's own admissibility
|
|
163
|
+
test to do it.
|
|
164
|
+
- **The paraphrase is corrected.** Several surfaces cite 0162 as "verdicts are advisory, never
|
|
165
|
+
self-executing" (this project's skills and ADRs 0178 and 0233 among them). That shorthand is
|
|
166
|
+
true of *verdicts* and false as a blanket claim about gates — 0162's own table has three
|
|
167
|
+
controls that fail closed. Cite the test in §1 above rather than the slogan.
|
|
168
|
+
- **Accepted trade:** an artist who never does the work can delay a project's deploys. That is the
|
|
169
|
+
point of `strict`, it is per project, it is owner-releasable, and the owner chose it as the
|
|
170
|
+
default with that consequence stated.
|
|
171
|
+
|
|
172
|
+
## Rejected
|
|
173
|
+
|
|
174
|
+
- **Blocking the ship instead of the deploy.** The strongest version of the artist's authority, and
|
|
175
|
+
the one that recreates 0162's incident exactly: a builder's graded, merged work parked behind
|
|
176
|
+
another person's queue, with their credits unpaid (ADR 0120 pays on land).
|
|
177
|
+
- **Advisory at every level.** Consistent with 0162 by doing nothing, but then "blocking strictness
|
|
178
|
+
is per project" configures nothing and the owner's rule is unimplemented.
|
|
179
|
+
- **Any Metic+ as the escape hatch.** Lowest friction, and it makes `strict` indistinguishable from
|
|
180
|
+
`advisory` in practice.
|
|
181
|
+
- **Making the artist's verdict itself the gate.** The direct reading of "artist blocking", and the
|
|
182
|
+
one thing 0162 genuinely forbids: it pins the gate to a person, and stalls indefinitely when that
|
|
183
|
+
person is unavailable.
|
|
184
|
+
- **Retroactive tightening.** See §5.
|
|
185
|
+
- **A global strictness constant.** The owner's constraint rules it out by name.
|
package/docs/adr/README.md
CHANGED
|
@@ -328,5 +328,6 @@ This keeps the decision history honest and traceable.
|
|
|
328
328
|
| 0237 | [**A starter bundle is a preset over the always-on core, keyed by the declared project type** ([task 1002338](https://cloudbongos.com/builders#/task/1002338) · goal 1000046, criterion `proj-module-select`). "During setup a default set of modules is pre-selected" can only mean the OPT-IN modules, since every instance already boots with the same sixteen `default: true` core-domain ones — so a bundle is a short list of opt-in module keys layered on that floor, preselected and freely toggled off by R20's picker ([task 1002339](https://cloudbongos.com/builders#/task/1002339)). **The key is the DECLARED type, and the seeded vocabulary is stale:** the task text (2026-07-17) names *Game / SaaS-Tool / Community*, but task [1002772](https://cloudbongos.com/builders#/task/1002772)'s owner decision (2026-08-13) fixed the live closed set as `game · research · business · non-profit · not-sure` — two CHECK constraints, two `PROJECT_TYPES` constants and five wizard buttons — and "community" survives only as a `team_shape`. Two constraints ride the key: the arrow runs **type → modules, never back** (1002772 rejected inferring type from module flags, which flip all through setup), and the R15 interview must **not re-ask** what `type` declares, so R19 adds no `PROJECT_DETAIL_VOCAB` key and no migration. `not-sure` and *undeclared* resolve to the same modules but are deliberately different answers — `type` is nullable precisely so "nobody said" is never back-filled with a guess, and the resolver returns `type: null` with its own copy. **The declaration is pinned by a test, not a runtime catalog read:** a module may not import `src/module-loader/catalog.js` (the one-way rule, [ADR 0083](<redacted>.md)), so `modules/provisioning/starter-bundles.js` stays PURE like `onboard-plan.js` beside it and `tests/provisioning_starter_bundles.mjs` reads every `modules/*/module.json` off disk — failing the build if a bundle names a module that does not exist, if `ALWAYS_ON_CORE` drifts from the manifests' `default: true` set, if a bundle names an always-on module (a preselect that toggles nothing), or if it names one of the two hub-only modules. The bundles are `game → dev-box, discord`; `research → agents`; `business → agents, dev-box`; `non-profit → discord`; `not-sure → —`, and they are SHORT because today's opt-in catalog is three modules wide and a preselect an owner must audit is worse than none. Served whole at public `GET /provisioning/starter-bundles` (`{ core, bundles }`, same absent gate as `onboard-plan`) so the picker re-preselects without a refetch and R17's recommendations read the same shape. A bundle is **never a publish precondition** — [ADR 0182](<redacted>.md) D4 fixes that bar at name + description alone. Rejected: a runtime catalog read, a `bundle` key in the interview vocabulary, inferring the bundle from the description (the deferred AI layer, [task 1003500](https://cloudbongos.com/builders#/task/1003500)), and keeping the seeded Game/SaaS-Tool/Community triple.](<redacted>.md) | provisioning / project creation |
|
|
329
329
|
| 0239 | [**A recommendation is a table that carries its own reason; governance is a preview and invites are a seam** ([task 1002336](https://cloudbongos.com/builders#/task/1002336) · goal 1000046, criteria `proj-module-select` / `proj-governance-reco` / `proj-invite-builders`). R19 ([ADR 0237](<redacted>.md)) declared the per-type starter bundle and said outright it is "a declaration and not a rule engine — R17/R22 own the layer that reasons"; this is that layer, and the goal pins it **rule-based**, with the AI/embeddings pass deferred ([task 1003500](https://cloudbongos.com/builders#/task/1003500), idea 1000552). **Two inputs, and the free-text description is not one of them:** the DECLARED `type` (a column) and `team_shape` (the one closed-answer key the R15 interview collects, [task 1002334](https://cloudbongos.com/builders#/task/1002334)) — inferring from prose is the deferred layer ADR 0237 §7 already rejected once. The engine is three frozen tables (`TEAM_SHAPE_RULES` add/remove ONE opt-in module; `GOVERNANCE_RULES` name a preset; `INVITE_RULES` emit a seek) and **every row carries its own `reason` sentence**, shipped with the effect rather than reconstructed by whichever surface renders it — a recommendation an owner cannot be told the reason for is one they cannot disagree with. `bundle.adjustments` is a **DELTA**: a rule whose condition matched but whose module the bundle already carried records NOTHING, because "we switched discord on because you said community" over a module a game would have had anyway is a claim the owner can check and find false. **Governance is a named preview, never a setting** — the governance module is planned and unbuilt (goal 1000030), so a preset is a name plus one sentence of what it feels like (`owner-only` / `trusted-team` / `open-contribution`) and every result carries `applied: false`, one flag instead of the same fact hardcoded in R25, R18, the wizard and the project page. **Invites are a SEAM**: `suggested` is always `[]` because ranking builders means querying the leaderboard in the sibling `economy` module, which the one-way rule ([ADR 0083](<redacted>.md)) forbids importing — the engine emits `seeking: { kind: 'top-builders', limit }` (or `null` = suggest nobody, a real answer) and R28 ([task 1002347](https://cloudbongos.com/builders#/task/1002347)) turns it into names. **Skipping is an answer:** every table states a default and each is the NARROWEST option (widening later costs one setting; a role granted to strangers is not recovered by setting it back), an off-vocabulary `team_shape` reads as no answer exactly as `normalizeProjectDetail` drops it, and the route answers 200-with-defaults rather than 400 — refusing would make the supported path ([ADR 0182](<redacted>.md) D4) an error. `tailored: boolean` is the machine-readable half of `proj-detail-interview`'s details-vs-skip split that R18 ([task 1002337](https://cloudbongos.com/builders#/task/1002337)) proves. Served at public `GET /provisioning/recommendations?type=&team_shape=` (same absent gate as `starter-bundles`; no database, pinned by a `pool`-throws stub), returning ADR 0237 §5's bundle shape plus `adjustments` so a recommendation and R20's preselect can never name different modules. Rejected: reading the description, filling `suggested` from `GET /public/leaderboard`, a weighted scoring function, a reason per matched condition, and a 400 on an unknown shape.](<redacted>.md) | provisioning / project creation |
|
|
330
330
|
| 0240 | [**A skipped module picker stores nothing, and the bundle is resolved on read** ([task 1002339](https://cloudbongos.com/builders#/task/1002339) · goal 1000046, criterion `proj-module-select`). [ADR 0237](<redacted>.md) declared the starter bundles and closed with "where a chosen selection lands is R20's decision". It lands in a new **nullable** `modules jsonb` column on `provisioning_instances` (`provisioning_014`), read through `starter-bundles.effectiveModules(row)` and **three-valued**: `NULL` = nobody answered (the picker was skipped, or the caller does not ask) and the declared type's bundle is resolved AT READ TIME; `[]` = the owner switched them all off, a real answer; `["dev-box",…]` = what they picked. `NOT NULL DEFAULT '[]'` was rejected because it collapses the first two forever — the `PROJECT_DETAIL_VOCAB` rule ("absent stays absent, skipped stays skipped") applied to a list. **Skipping is safe because the preselect and the fallback are the same function**, `bundleForProjectType`, called twice — not two lists kept in agreement — so `bongos onboard` and any API caller inherit the bundle by omitting the field, and the canonical sequence in `onboard-plan.js` grows **no new step** (the picker refines `type`, an answer already in it). NOT `detail` (its vocabulary is closed + scalar by construction, and [ADR 0237](<redacted>.md) §2 ruled the interview must not ask this) and NOT `settings` (a setting is a policy the RUNNING instance obeys — web.env, a settings-apply intent, a restart). The picker's catalog copy (`OPTIONAL_MODULES`) is **declared beside the bundles**, because `module.json` descriptions are written for builders, `GET /modules` needs a session the signed-out wizard has not got, and `GET /public/modules` serves booleans — pinned the ADR 0237 way, so a new `default: false` module cannot ship without owner-facing copy. `GET /provisioning/starter-bundles` gains `optional` so the preselect and the catalog arrive in **one** read. The route **refuses** an unknown / always-on / hub-only key (`400 bad_modules`) rather than dropping it silently (`badProjectDetail`'s rule), and a re-request reports `modules_ignored`. The column is a DECLARATION, not yet an input to the control-plane runner. Rejected: storing the preselect on a skip (it writes down an answer nobody gave), a new `onboardPlan` step, and rendering the always-on core as sixteen disabled toggles (a disabled control reads as a choice the owner failed to make).](<redacted>.md) | provisioning / project creation |
|
|
331
|
+
| 0241 | [**The artist gate holds the DEPLOY, not the ship, and it reads a state rather than a judgement** ([task 1003116](https://cloudbongos.com/builders#/task/1003116) · goal 1000074, criterion `artist-blocking-is-per-project` — R05, the DECISION task the goal placed before R04 so no code would be wired to an unsettled gate). Goal 1000074 carries two rules on a collision course: the owner's "blocking strictness is PER PROJECT, not global", and [ADR 0162](<redacted>.md), which retired review gates on `main` after a code-owner requirement stranded two graded-PASS ships in a day. **0162 is usually cited by a paraphrase — "a verdict is advisory, never self-executing" — and the paraphrase is wrong as a blanket claim:** 0162's own table of compensating controls marks three of four as *fails closed*, the subagent grader among them. What it actually retired was a gate GitHub could only express as ACCOUNTS — identity-pinned against [ADR 0016](<redacted>.md), and stranding with no escape hatch. So the surviving test is: **a gate is admissible when it is evaluated automatically from readable state, consistently for every builder, with a named escape hatch; inadmissible when a particular person must act and it stalls when they do not.** Decision: (1) the gate reads a **STATE** — "is there an unresolved artist review task?" — never an opinion, so the artist's judgement is what they write INTO the task and stays advisory in exactly 0162's sense, while what the system reads is ordinary GDS status; (2) strictness is a project setting `artist_gate ∈ off | advisory | strict` and at `strict` it holds the **DEPLOY, not the ship** (owner ruling) — the builder's work is graded, merged, credited and landed on CI alone exactly as today, so 0162's strand class cannot recur, and the gate attaches to the boundary the artist's ABI mandate is actually about (users see the deployed instance, not `main`; [ADR 0137](<redacted>.md) already splits land/release/deploy); (3) the escape hatch is the **project OWNER only** — not the builder, not any Metic+, not the artist — via the existing `POST /tasks/:id/override-request`, because a person can be both artist and builder and could pass their own work; a self-release is permitted but never quiet (required rationale, audit-logged as a self-override) so a solo instance cannot deadlock; (4) the default is **`strict`** (owner ruling, against the author's `advisory` recommendation), which is safe only because §2 holds the deploy — the worst case is a pin that waits, and this instance is included once R04 ships; a project with no artist must still be able to deploy, and the refusal must name what holds it and which control releases it; (5) tightening is **not retroactive** — re-gating history would strand the next deploy behind reviews for work nobody remembers and make the setting frightening to raise; R02's flag queue is the right tool for already-shipped copy. Rejected: blocking the ship (recreates 0162's incident exactly, with credits unpaid — [ADR 0120](<redacted>.md)); advisory at every level (configures nothing); any Metic+ as the hatch (makes `strict` indistinguishable from `advisory`); making the artist's VERDICT the gate (the one thing 0162 genuinely forbids); retroactive tightening; a global constant (ruled out by the owner's constraint by name).](<redacted>.md) | goal 1000074 / the artist's loop |
|
|
331
332
|
|
|
332
333
|
> ⚠️ **Numbering collisions are now machine-enforced, not narrated.** Twenty numbers are shared by two ADRs each — assigned in parallel sessions before anything checked. The files keep their filenames (renumbering would break every existing citation, and a number once assigned is never reused), and this table disambiguates each pair as `NNNN-a` / `NNNN-b`. The authoritative list is `LEGACY_DUPLICATE_ADRS` in [`scripts/gds/adr-namespace.js`](../../scripts/gds/adr-namespace.js), frozen by exact filename: a **new** collision — or a third file joining a legacy number, or a rename of either half — hard-fails the `unit` CI gate, as does an index row that points at no file or an ADR with no row. The list may only shrink. This paragraph used to narrate the collisions one by one and had fallen eight behind reality; see [ADR 0195](<redacted>.md) for why the check exists and why the pairs are grandfathered rather than renumbered.
|
package/docs/copy-inventory.md
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
> Deterministic on a given tree — regenerate and commit rather than hand-editing.
|
|
5
5
|
> Machine-readable twin: [`copy-registry.json`](copy-registry.json).
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**1645 strings** across 3 surfaces · 1427 distinct pieces of copy · 1645 certain, 0 heuristic · 154 shown in more than one place.
|
|
8
8
|
|
|
9
9
|
## Surfaces
|
|
10
10
|
|
|
11
11
|
| surface | lives at | files | strings |
|
|
12
12
|
|---|---|---|---|
|
|
13
|
-
| `builders-hall` | `modules/hall-ui/public` | 73 |
|
|
13
|
+
| `builders-hall` | `modules/hall-ui/public` | 73 | 1187 |
|
|
14
14
|
| `status-dashboard` | `modules/status-ui/public` | 4 | 98 |
|
|
15
15
|
| `landing` | `modules/public-landing/public` | 5 | 360 |
|
|
16
16
|
|
|
@@ -78,7 +78,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
78
78
|
| `60b248791dc3` | 2x | Builders — {{worldName}} | modules/hall-ui/public/roster.html:6, modules/hall-ui/public/roster.html:6 |
|
|
79
79
|
| … | | _114 more in the JSON registry_ | |
|
|
80
80
|
|
|
81
|
-
## `builders-hall` —
|
|
81
|
+
## `builders-hall` — 1187 strings
|
|
82
82
|
|
|
83
83
|
| id | conf | text | where |
|
|
84
84
|
|---|---|---|---|
|
|
@@ -129,7 +129,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
129
129
|
| `a3185a43c230` | certain | A sound plays at each turn of your work — a gentle chime when Claude finishes, a flourish when a task ships or | `modules/hall-ui/public/settings.html:312` |
|
|
130
130
|
| `83b1274f9088` | certain | A tally of recorded work sessions, and the latest five. The full record lives on the Sessions page. | `modules/hall-ui/public/watch.html:103` |
|
|
131
131
|
| `daf73127afff` | certain | A word on why | `modules/hall-ui/public/hall-render.js:284` |
|
|
132
|
-
| `1b326b0be1a4` | certain | A {…} — you are a {…}. Mingle pairs across crafts. | `modules/hall-ui/public/collab.js:
|
|
132
|
+
| `1b326b0be1a4` | certain | A {…} — you are a {…}. Mingle pairs across crafts. | `modules/hall-ui/public/collab.js:196` |
|
|
133
133
|
| `996edffc1966` | certain | Access requests | `modules/hall-ui/public/watch.html:46` |
|
|
134
134
|
| `996edffc1966` | certain | Access requests | `modules/hall-ui/public/watch.html:61` |
|
|
135
135
|
| `303f8d90266f` | certain | Achievements could not be loaded. | `modules/hall-ui/public/builders.js:1154` |
|
|
@@ -284,7 +284,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
284
284
|
| `90691bceaf68` | certain | Could not load | `modules/hall-ui/public/task.js:710` |
|
|
285
285
|
| `589a4cfc5442` | certain | Could not load idea {…}. | `modules/hall-ui/public/ideas.js:629` |
|
|
286
286
|
| `192c893fc64e` | certain | Could not load modules. | `modules/hall-ui/public/modules.js:318` |
|
|
287
|
-
| `8a4b590fa1a0` | certain | Could not load requests. | `modules/hall-ui/public/collab.js:
|
|
287
|
+
| `8a4b590fa1a0` | certain | Could not load requests. | `modules/hall-ui/public/collab.js:274` |
|
|
288
288
|
| `7775fec0fa85` | certain | Could not load sessions. | `modules/hall-ui/public/sessions.js:243` |
|
|
289
289
|
| `7775fec0fa85` | certain | Could not load sessions. | `modules/hall-ui/public/sessions.js:281` |
|
|
290
290
|
| `b2147cc90e36` | certain | Could not load the idea template. Reload, or file from the terminal with capture.js. | `modules/hall-ui/public/ideas.js:611` |
|
|
@@ -608,7 +608,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
608
608
|
| `c72e0bdb834a` | certain | Not declared in module.json — this is who ships the module. | `modules/hall-ui/public/modules.js:62` |
|
|
609
609
|
| `361b353ded5e` | certain | Not enough shipped work yet to chart. | `modules/hall-ui/public/hall-widgets.js:184` |
|
|
610
610
|
| `90230572d017` | certain | Not found | `modules/hall-ui/public/task.js:698` |
|
|
611
|
-
| `8226c979db4d` | certain | Not now | `modules/hall-ui/public/collab.js:
|
|
611
|
+
| `8226c979db4d` | certain | Not now | `modules/hall-ui/public/collab.js:196` |
|
|
612
612
|
| `61250b49a90b` | certain | Not scored. | `modules/hall-ui/public/idea.js:84` |
|
|
613
613
|
| `8d6afe6def47` | certain | Not signed in | `modules/hall-ui/public/settings.html:40` |
|
|
614
614
|
| `a290f6c14df9` | certain | Not taking requests | `modules/hall-ui/public/goals-render.js:859` |
|
|
@@ -761,7 +761,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
761
761
|
| `e8e4e49fe8f4` | certain | Saves when you press Save; shows everywhere at once. | `modules/hall-ui/public/settings.html:88` |
|
|
762
762
|
| `42cbc20a6ec8` | certain | Saves when you press Save; your next | `modules/hall-ui/public/settings.html:136` |
|
|
763
763
|
| `fc9d783ed0f5` | certain | Say why it is better. Whoever reviews this decides it from your reason, without asking you. | `modules/hall-ui/public/copy-desk.js:446` |
|
|
764
|
-
| `12c1ec21a2c6` | certain | Say yes | `modules/hall-ui/public/collab.js:
|
|
764
|
+
| `12c1ec21a2c6` | certain | Say yes | `modules/hall-ui/public/collab.js:196` |
|
|
765
765
|
| `46711f7bc3df` | certain | Search every string the product shows a person, by what it says or the file it lives in. | `modules/hall-ui/public/copy-desk.html:65` |
|
|
766
766
|
| `5d2ba8e72fa1` | certain | Search failed. | `modules/hall-ui/public/copy-desk.js:343` |
|
|
767
767
|
| `373a7639bcb9` | certain | Sections in this primer | `modules/hall-ui/public/primer.html:52` |
|
|
@@ -815,7 +815,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
815
815
|
| `87cee78ca134` | certain | Skip to content | `modules/hall-ui/public/shell.js:490` |
|
|
816
816
|
| `688ca7b6e30f` | certain | Slop — reads as machine-written | `modules/hall-ui/public/copy-desk.html:90` |
|
|
817
817
|
| `82df09a08476` | certain | So the Archon knows who you are | `modules/hall-ui/public/hall-render.js:284` |
|
|
818
|
-
| `7da6a848cd26` | certain | Someone helped | `modules/hall-ui/public/collab.js:
|
|
818
|
+
| `7da6a848cd26` | certain | Someone helped | `modules/hall-ui/public/collab.js:113` |
|
|
819
819
|
| `867df58c9aaa` | certain | Something else | `modules/hall-ui/public/copy-desk.html:95` |
|
|
820
820
|
| `c377d8ed744b` | certain | Something went wrong. Try again. | `modules/hall-ui/public/profile-connections.js:275` |
|
|
821
821
|
| `e0767e80206c` | certain | Sound for {…} | `modules/hall-ui/public/settings.js:311` |
|
|
@@ -1044,7 +1044,6 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1044
1044
|
| `b867876e8b6c` | certain | and press Enter. | `modules/hall-ui/public/hall-render.js:750` |
|
|
1045
1045
|
| `b4033000c537` | certain | as a table | `modules/hall-ui/public/profile-charts.js:202` |
|
|
1046
1046
|
| `d7c511423bc0` | certain | as configuration: {…} | `modules/hall-ui/public/government.js:707` |
|
|
1047
|
-
| `09db3fd4355b` | certain | asked by {…} | `modules/hall-ui/public/collab.js:69` |
|
|
1048
1047
|
| `63bd14f73960` | certain | author trusted | `modules/hall-ui/public/gate.js:141` |
|
|
1049
1048
|
| `93438b9ffa98` | certain | below or type | `modules/hall-ui/public/box-panel.js:226` |
|
|
1050
1049
|
| `1f5e924adf9d` | certain | box — | `modules/hall-ui/public/box-panel.js:336` |
|
package/docs/copy-registry.json
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"generator": "scripts/gds/copy-inventory.js",
|
|
4
4
|
"goal": 1000074,
|
|
5
5
|
"counts": {
|
|
6
|
-
"strings":
|
|
7
|
-
"unique_ids":
|
|
8
|
-
"certain":
|
|
6
|
+
"strings": 1645,
|
|
7
|
+
"unique_ids": 1427,
|
|
8
|
+
"certain": 1645,
|
|
9
9
|
"heuristic": 0,
|
|
10
10
|
"duplicate_ids": 154
|
|
11
11
|
},
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"builders-hall": {
|
|
14
14
|
"at": "modules/hall-ui/public",
|
|
15
15
|
"files": 73,
|
|
16
|
-
"strings":
|
|
16
|
+
"strings": 1187
|
|
17
17
|
},
|
|
18
18
|
"status-dashboard": {
|
|
19
19
|
"at": "modules/status-ui/public",
|
|
@@ -2072,7 +2072,7 @@
|
|
|
2072
2072
|
"surface": "builders-hall",
|
|
2073
2073
|
"text": "A {…} — you are a {…}. Mingle pairs across crafts.",
|
|
2074
2074
|
"file": "modules/hall-ui/public/collab.js",
|
|
2075
|
-
"line":
|
|
2075
|
+
"line": 196,
|
|
2076
2076
|
"origin": "js-markup",
|
|
2077
2077
|
"confidence": "certain"
|
|
2078
2078
|
},
|
|
@@ -3467,7 +3467,7 @@
|
|
|
3467
3467
|
"surface": "builders-hall",
|
|
3468
3468
|
"text": "Could not load requests.",
|
|
3469
3469
|
"file": "modules/hall-ui/public/collab.js",
|
|
3470
|
-
"line":
|
|
3470
|
+
"line": 274,
|
|
3471
3471
|
"origin": "js-text-assign",
|
|
3472
3472
|
"confidence": "certain"
|
|
3473
3473
|
},
|
|
@@ -6383,7 +6383,7 @@
|
|
|
6383
6383
|
"surface": "builders-hall",
|
|
6384
6384
|
"text": "Not now",
|
|
6385
6385
|
"file": "modules/hall-ui/public/collab.js",
|
|
6386
|
-
"line":
|
|
6386
|
+
"line": 196,
|
|
6387
6387
|
"origin": "js-markup",
|
|
6388
6388
|
"confidence": "certain"
|
|
6389
6389
|
},
|
|
@@ -7760,7 +7760,7 @@
|
|
|
7760
7760
|
"surface": "builders-hall",
|
|
7761
7761
|
"text": "Say yes",
|
|
7762
7762
|
"file": "modules/hall-ui/public/collab.js",
|
|
7763
|
-
"line":
|
|
7763
|
+
"line": 196,
|
|
7764
7764
|
"origin": "js-markup",
|
|
7765
7765
|
"confidence": "certain"
|
|
7766
7766
|
},
|
|
@@ -8246,7 +8246,7 @@
|
|
|
8246
8246
|
"surface": "builders-hall",
|
|
8247
8247
|
"text": "Someone helped",
|
|
8248
8248
|
"file": "modules/hall-ui/public/collab.js",
|
|
8249
|
-
"line":
|
|
8249
|
+
"line": 113,
|
|
8250
8250
|
"origin": "js-markup",
|
|
8251
8251
|
"confidence": "certain"
|
|
8252
8252
|
},
|
|
@@ -10302,15 +10302,6 @@
|
|
|
10302
10302
|
"origin": "js-markup",
|
|
10303
10303
|
"confidence": "certain"
|
|
10304
10304
|
},
|
|
10305
|
-
{
|
|
10306
|
-
"id": "09db3fd4355b",
|
|
10307
|
-
"surface": "builders-hall",
|
|
10308
|
-
"text": "asked by {…}",
|
|
10309
|
-
"file": "modules/hall-ui/public/collab.js",
|
|
10310
|
-
"line": 69,
|
|
10311
|
-
"origin": "js-markup",
|
|
10312
|
-
"confidence": "certain"
|
|
10313
|
-
},
|
|
10314
10305
|
{
|
|
10315
10306
|
"id": "63bd14f73960",
|
|
10316
10307
|
"surface": "builders-hall",
|
|
@@ -1483,5 +1483,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1483
1483
|
landed since 1.19.521 with no explicit bump. run 33815020928. (task 1002620)
|
|
1484
1484
|
1.19.523 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1485
1485
|
landed since 1.19.522 with no explicit bump. run 33816191865. (task 1002620)
|
|
1486
|
+
1.19.524 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1487
|
+
landed since 1.19.523 with no explicit bump. run 33818285201. (task 1002620)
|
|
1488
|
+
1.19.525 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1489
|
+
landed since 1.19.524 with no explicit bump. run 33823218193. (task 1002620)
|
|
1486
1490
|
---------------------------------------------------------------------------
|
|
1487
1491
|
```
|
|
@@ -51,19 +51,42 @@
|
|
|
51
51
|
return (ta || 0) - (tb || 0);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
// `addressedTo(req, viewerId)` WAS HERE and is gone (task 1003274). It named
|
|
55
|
+
// the addressee only when the VIEWER was the addressee, which is why the
|
|
56
|
+
// asker's own list — the most common case for that branch — rendered "asked of
|
|
57
|
+
// one person" and produced the owner's report. directionLine below supersedes
|
|
58
|
+
// it on both axes (it names the person AND says which way the ask points), and
|
|
59
|
+
// it had no other caller, so keeping it would have left a second, wronger
|
|
60
|
+
// answer to the same question sitting exported next to the right one.
|
|
61
|
+
|
|
62
|
+
// DIRECTION, in words (owner report, task 1003274). The report is the whole
|
|
63
|
+
// reason this exists: the owner read their own outgoing ask, addressed to Lars,
|
|
64
|
+
// sitting under "Your asks", and concluded it had been sent to the wrong person.
|
|
65
|
+
// Nothing was misrouted — the row simply never said WHO, and never said which
|
|
66
|
+
// way it pointed.
|
|
67
|
+
//
|
|
68
|
+
// DIRECTION IS THE LIST, NOT A COMPARISON. `dir` is 'out' for your own asks and
|
|
69
|
+
// 'in' for what was asked of you, because the two server reads already answer
|
|
70
|
+
// that question by construction (GET /help-requests/mine vs /for-me). Deriving
|
|
71
|
+
// it instead from `requested_by === viewerId` is what the old code did, and it
|
|
72
|
+
// is how a page with no viewer id in hand ends up rendering the anonymous
|
|
73
|
+
// fallback for the single most common case — the asker reading their own list.
|
|
74
|
+
//
|
|
75
|
+
// The tonal split the report asks to KEEP: a craft ask is a call anyone of that
|
|
76
|
+
// craft may take; a by-name ask is personal and says so.
|
|
77
|
+
function directionLine(req, dir) {
|
|
78
|
+
if (!req) return '';
|
|
79
|
+
var craft = req.needs_craft;
|
|
80
|
+
if (dir === 'out') {
|
|
81
|
+
if (req.needs_builder_id != null) {
|
|
82
|
+
var to = req.needs_builder_name || req.needs_builder_login;
|
|
83
|
+
return to ? 'You asked ' + to + ' by name' : 'You asked one person';
|
|
84
|
+
}
|
|
85
|
+
return craft ? 'You asked ' + craft + 's' : 'You asked';
|
|
64
86
|
}
|
|
65
|
-
|
|
66
|
-
return 'asked';
|
|
87
|
+
var who = askerOf(req) || 'Someone';
|
|
88
|
+
if (req.needs_builder_id != null) return who + ' asked you by name';
|
|
89
|
+
return craft ? who + ' asked ' + craft + 's' : who + ' asked';
|
|
67
90
|
}
|
|
68
91
|
|
|
69
92
|
// Who is ASKING, in words — the fact a helper needs to answer. The name the
|
|
@@ -107,7 +130,8 @@
|
|
|
107
130
|
|
|
108
131
|
window.OTBCollabLib = {
|
|
109
132
|
BANDS: BANDS, ageBand: ageBand, byAgeOldestFirst: byAgeOldestFirst,
|
|
110
|
-
|
|
133
|
+
askerOf: askerOf, contextOf: contextOf,
|
|
134
|
+
directionLine: directionLine,
|
|
111
135
|
titleOf: titleOf, groupOf: groupOf,
|
|
112
136
|
};
|
|
113
137
|
})();
|
|
@@ -59,14 +59,37 @@
|
|
|
59
59
|
return `<a class="fact-chip" href="${escapeHtml(ctx.href)}">${escapeHtml(ctx.kind)} #${escapeHtml(String(ctx.id))}</a>`;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// A name as a door to that builder's profile, or plain text when the server
|
|
63
|
+
// sent no id to link to.
|
|
64
|
+
function personLink(id, name) {
|
|
65
65
|
if (!name) return '';
|
|
66
|
-
|
|
67
|
-
? `<a href="/builders/profile?id=${escapeHtml(String(
|
|
66
|
+
return id != null
|
|
67
|
+
? `<a href="/builders/profile?id=${escapeHtml(String(id))}">${escapeHtml(name)}</a>`
|
|
68
68
|
: escapeHtml(name);
|
|
69
|
-
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// DIRECTION, on the row (owner report, task 1003274): "<name> asked you by
|
|
72
|
+
// name" incoming, "You asked <name> by name" outgoing — so an outgoing ask can
|
|
73
|
+
// never be mistaken for an incoming one, which is exactly the mistake the
|
|
74
|
+
// report describes. The sentence itself is L.directionLine (pure, tested); this
|
|
75
|
+
// only splices the profile link over the person's name inside it, so the words
|
|
76
|
+
// a test asserts and the words a builder reads cannot drift.
|
|
77
|
+
function directionFact(req, dir) {
|
|
78
|
+
const line = L.directionLine(req, dir);
|
|
79
|
+
if (!line) return '';
|
|
80
|
+
const name = dir === 'in' ? L.askerOf(req) : (req.needs_builder_name || req.needs_builder_login);
|
|
81
|
+
const id = dir === 'in' ? req.requested_by : req.needs_builder_id;
|
|
82
|
+
const link = personLink(id, name);
|
|
83
|
+
// The replacement is a FUNCTION, not a string. String.prototype.replace
|
|
84
|
+
// treats `$&`, `$$`, "$`" and `$'` specially in a string replacement, and the
|
|
85
|
+
// name here is builder-controlled (a display_name or login) — so a display
|
|
86
|
+
// name containing `$&` would duplicate or mangle the sentence around it. Not
|
|
87
|
+
// an injection (both halves are escaped either way), but a real way to
|
|
88
|
+
// corrupt someone else's row by choosing your own name.
|
|
89
|
+
const html = (name && link)
|
|
90
|
+
? escapeHtml(line).replace(escapeHtml(name), () => link)
|
|
91
|
+
: escapeHtml(line);
|
|
92
|
+
return `<span class="ov-fact">${html}</span>`;
|
|
70
93
|
}
|
|
71
94
|
|
|
72
95
|
// A row for the "asked of you" list. There is deliberately NO "claim this"
|
|
@@ -77,7 +100,7 @@
|
|
|
77
100
|
function forMeRow(req, nowMs) {
|
|
78
101
|
return `<li class="ov-row" data-id="${escapeHtml(String(req.id))}">
|
|
79
102
|
<div class="ov-row__main">${titleHtml(req)}
|
|
80
|
-
<div class="ov-row__facts">${
|
|
103
|
+
<div class="ov-row__facts">${directionFact(req, 'in')}${contextChip(req)}</div>
|
|
81
104
|
</div>
|
|
82
105
|
<div class="ov-row__right">${ageChip(req, nowMs)}${whenLine(req.created_at)}</div>
|
|
83
106
|
</li>`;
|
|
@@ -89,7 +112,7 @@
|
|
|
89
112
|
const id = escapeHtml(String(req.id));
|
|
90
113
|
return `<li class="ov-row" data-id="${id}">
|
|
91
114
|
<div class="ov-row__main">${titleHtml(req)}
|
|
92
|
-
<div class="ov-row__facts"
|
|
115
|
+
<div class="ov-row__facts">${directionFact(req, 'out')}${contextChip(req)}</div>
|
|
93
116
|
</div>
|
|
94
117
|
<div class="ov-row__right">${ageChip(req, nowMs)}
|
|
95
118
|
<div class="ov-row__actions">
|
|
@@ -284,7 +284,7 @@ a.fact-chip:hover { color: var(--accent-ink); border-color: var(--accent-ink); t
|
|
|
284
284
|
align-items: center;
|
|
285
285
|
justify-content: center;
|
|
286
286
|
padding: 20px;
|
|
287
|
-
background:
|
|
287
|
+
background: var(--scrim);
|
|
288
288
|
}
|
|
289
289
|
.ov-modal {
|
|
290
290
|
width: 100%;
|
|
@@ -963,7 +963,7 @@ main:focus { outline: none; }
|
|
|
963
963
|
position: fixed;
|
|
964
964
|
inset: 0;
|
|
965
965
|
z-index: var(--z-scrim);
|
|
966
|
-
background:
|
|
966
|
+
background: var(--scrim);
|
|
967
967
|
}
|
|
968
968
|
|
|
969
969
|
/* Focus: the world's two-tone ring — a 2px accent-derived outline at 3px offset
|
|
@@ -2695,7 +2695,7 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
|
|
|
2695
2695
|
position: fixed;
|
|
2696
2696
|
inset: 0;
|
|
2697
2697
|
z-index: 2000;
|
|
2698
|
-
background:
|
|
2698
|
+
background: var(--scrim);
|
|
2699
2699
|
display: flex;
|
|
2700
2700
|
align-items: center;
|
|
2701
2701
|
justify-content: center;
|
|
@@ -3190,7 +3190,7 @@ a.stat-tile:hover .stat-tile__sub { color: var(--ink-soft); }
|
|
|
3190
3190
|
}
|
|
3191
3191
|
|
|
3192
3192
|
.palette { position: fixed; inset: 0; z-index: 2100; }
|
|
3193
|
-
.palette__scrim { position: absolute; inset: 0; background:
|
|
3193
|
+
.palette__scrim { position: absolute; inset: 0; background: var(--scrim); }
|
|
3194
3194
|
.palette__panel {
|
|
3195
3195
|
position: relative;
|
|
3196
3196
|
margin: 10vh auto 0;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.525",
|
|
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.525",
|
|
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.525",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
45
45
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
46
46
|
// the entry to that file. Look for a version's history there, not here.
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
const CORE_VERSION = '1.19.
|
|
48
|
+
const CORE_VERSION = '1.19.525'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
49
49
|
|
|
50
50
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
51
51
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/collab_page.mjs
CHANGED
|
@@ -97,10 +97,67 @@ test('the page NEVER sends a craft — matching is the server\'s job', () => {
|
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
test('a craft request and a by-name request read differently', () => {
|
|
100
|
+
// `addressedTo` is GONE (task 1003274) — it named the addressee only when the
|
|
101
|
+
// viewer WAS the addressee, so the asker's own list rendered anonymously, which
|
|
102
|
+
// is the whole owner report. directionLine replaced it on both axes and had to
|
|
103
|
+
// take the tonal split with it, which is what this now asserts.
|
|
100
104
|
const L = lib();
|
|
101
|
-
assert.
|
|
102
|
-
assert.match(L.
|
|
103
|
-
assert.match(L.
|
|
105
|
+
assert.equal(L.addressedTo, undefined, 'the superseded helper is not left exported');
|
|
106
|
+
assert.match(L.directionLine({ needs_craft: 'artist' }, 'out'), /artists/);
|
|
107
|
+
assert.match(L.directionLine({ needs_builder_id: 7, needs_builder_name: 'Ada' }, 'out'), /by name/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ── DIRECTION: the owner's report (task 1003274) ────────────────────────────
|
|
111
|
+
//
|
|
112
|
+
// "the ask went to me, not Lars it seems... It says Lars- as your asks but I am
|
|
113
|
+
// not Lars." Nothing was misrouted: the owner was reading their OWN outgoing ask,
|
|
114
|
+
// addressed to Lars, under "Your asks" — and the row said neither who it went to
|
|
115
|
+
// nor which way it pointed. The label read "asked of one person".
|
|
116
|
+
//
|
|
117
|
+
// The reason the old code could not name them is worth keeping: it named the
|
|
118
|
+
// addressee only when the VIEWER was the addressee, so the single most common
|
|
119
|
+
// case for that branch — the asker reading their own list — was the one case it
|
|
120
|
+
// rendered anonymously.
|
|
121
|
+
|
|
122
|
+
test('THE REPORTED CASE: your own by-name ask names the person you asked', () => {
|
|
123
|
+
const L = lib();
|
|
124
|
+
const req = { needs_builder_id: 3, needs_builder_name: 'Lars', requested_by: 1000025 };
|
|
125
|
+
const line = L.directionLine(req, 'out');
|
|
126
|
+
assert.match(line, /Lars/, 'the addressee is named — this is the whole bug');
|
|
127
|
+
assert.match(line, /^You asked/, 'and it says which way it points');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('DIRECTION cannot be mistaken: outgoing and incoming read as opposites', () => {
|
|
131
|
+
const L = lib();
|
|
132
|
+
const out = L.directionLine({ needs_builder_id: 3, needs_builder_name: 'Lars' }, 'out');
|
|
133
|
+
const inc = L.directionLine({ needs_builder_id: 9, requested_by_name: 'Lars' }, 'in');
|
|
134
|
+
assert.match(out, /^You asked Lars by name$/);
|
|
135
|
+
assert.match(inc, /^Lars asked you by name$/);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('direction comes from the LIST, not from comparing ids', () => {
|
|
139
|
+
// The two server reads already answer the question by construction
|
|
140
|
+
// (/help-requests/mine vs /for-me), so directionLine takes no viewer id. That
|
|
141
|
+
// is what makes the asker's own list impossible to render anonymously — there
|
|
142
|
+
// is no comparison left to get wrong.
|
|
143
|
+
const L = lib();
|
|
144
|
+
assert.equal(L.directionLine.length, 2, 'req + dir, and no viewerId');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('the craft tone survives — a craft ask is not a personal one', () => {
|
|
148
|
+
// ADR 0187 §3's split, which the fix was told to keep: "asked of builders" is a
|
|
149
|
+
// call anyone of that craft may take; a by-name ask is personal and says so.
|
|
150
|
+
const L = lib();
|
|
151
|
+
assert.match(L.directionLine({ needs_craft: 'artist' }, 'out'), /^You asked artists$/);
|
|
152
|
+
assert.match(L.directionLine({ needs_craft: 'artist', requested_by_name: 'Lars' }, 'in'),
|
|
153
|
+
/^Lars asked artists$/, 'incoming craft asks say the craft, not "you"');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('an unnamed addressee still says which way it points', () => {
|
|
157
|
+
// An older server sends no join. Losing the name must not lose the direction.
|
|
158
|
+
const L = lib();
|
|
159
|
+
assert.match(L.directionLine({ needs_builder_id: 3 }, 'out'), /^You asked one person$/);
|
|
160
|
+
assert.match(L.directionLine({ needs_builder_id: 3 }, 'in'), /^Someone asked you by name$/);
|
|
104
161
|
});
|
|
105
162
|
|
|
106
163
|
test('context links to the task or goal, and to neither when absent', () => {
|
package/tests/hall_audit.mjs
CHANGED
|
@@ -527,3 +527,69 @@ test('the command palette input rings from the token tier, inside its own box',
|
|
|
527
527
|
assert.match(css, /\.palette__panel\s*\{[^}]*overflow:\s*hidden/, 'the panel still clips, so the inset form is still the right one');
|
|
528
528
|
assert.match(rule[1], /outline-offset:\s*-|inset/, 'the indication sits inside the input, not outside it');
|
|
529
529
|
});
|
|
530
|
+
|
|
531
|
+
// ---- 11. the world has ONE scrim (task 1003568) ---------------------------
|
|
532
|
+
|
|
533
|
+
// A scrim darkens what is behind it — in BOTH modes. That is why it cannot be
|
|
534
|
+
// mixed from a ground token: `--ink` is near-black in light and #ffffff in
|
|
535
|
+
// dark, so `color-mix(in srgb, var(--ink) N%, transparent)` paints a WHITE wash
|
|
536
|
+
// over a dark page. `.palette__scrim` did exactly that, unnoticed, while three
|
|
537
|
+
// other scrims in the same sheet each carried their own copy of the same
|
|
538
|
+
// black-with-alpha literal. Task 1003495 gave the value a home as `--scrim`;
|
|
539
|
+
// this pins that every scrim now spends it.
|
|
540
|
+
const SCRIM_VALUE = 'rgba(0, 0, 0, .55)';
|
|
541
|
+
|
|
542
|
+
test('every scrim in the hall spends --scrim, and none is mixed from a ground token', () => {
|
|
543
|
+
const sheets = fs.readdirSync(HALL).filter((f) => f.endsWith('.css'));
|
|
544
|
+
const offenders = [];
|
|
545
|
+
for (const f of sheets) {
|
|
546
|
+
const css = read(f).replace(/\/\*[\s\S]*?\*\//g, '');
|
|
547
|
+
for (const m of css.matchAll(/([^{}]*(?:scrim|::backdrop)[^{}]*)\{([^{}]*)\}/gi)) {
|
|
548
|
+
const [sel, body] = [m[1].trim().replace(/\s+/g, ' '), m[2]];
|
|
549
|
+
if (!/background(-color)?\s*:/.test(body)) continue; // not the painting rule
|
|
550
|
+
if (!/background(-color)?\s*:\s*var\(--scrim\)/.test(body)) {
|
|
551
|
+
offenders.push(`${f}: \`${sel}\` paints a scrim without var(--scrim)`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
assert.deepEqual(offenders, [], 'a scrim spends the token, never its own value');
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test('no rule washes a whole surface with a ground token — the dark-mode inversion', () => {
|
|
559
|
+
// Mixing --ink into a SURFACE (var(--bg-card)) is the design system working —
|
|
560
|
+
// --surface-2 and --skeleton are meant to flip with the mode. Mixing it into
|
|
561
|
+
// `transparent` as a FLAT WASH over everything behind it is the bug: --ink is
|
|
562
|
+
// #ffffff in dark, so the wash turns white and lifts the page instead of
|
|
563
|
+
// dimming it. That is what .palette__scrim and .ov-modal-bg both did.
|
|
564
|
+
//
|
|
565
|
+
// The same mix INSIDE a gradient is not a wash — the diagram viewer's dotted
|
|
566
|
+
// "drawing board" texture spends it as one colour stop over the card's own
|
|
567
|
+
// ground, and there flipping with the mode is exactly right. So this matches
|
|
568
|
+
// only a mix that is the ENTIRE value of a background declaration.
|
|
569
|
+
const FLAT_INK_WASH = /background(?:-color)?\s*:\s*color-mix\(in srgb,\s*var\(--ink\)\s*\d+%\s*,\s*transparent\s*\)\s*;/g;
|
|
570
|
+
const bad = [];
|
|
571
|
+
for (const f of fs.readdirSync(HALL).filter((x) => x.endsWith('.css'))) {
|
|
572
|
+
const css = read(f).replace(/\/\*[\s\S]*?\*\//g, '');
|
|
573
|
+
for (const m of css.matchAll(FLAT_INK_WASH)) bad.push(`${f}: ${m[0].trim()}`);
|
|
574
|
+
}
|
|
575
|
+
assert.deepEqual(bad, [], 'a full-surface wash mixed from --ink inverts between modes — use var(--scrim)');
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test('the wash guard CATCHES a scrim but spares a texture', () => {
|
|
579
|
+
const FLAT_INK_WASH = /background(?:-color)?\s*:\s*color-mix\(in srgb,\s*var\(--ink\)\s*\d+%\s*,\s*transparent\s*\)\s*;/;
|
|
580
|
+
assert.ok(FLAT_INK_WASH.test('.x { background: color-mix(in srgb, var(--ink) 40%, transparent); }'),
|
|
581
|
+
'a flat wash is caught');
|
|
582
|
+
assert.equal(FLAT_INK_WASH.test('.y { background: radial-gradient(circle, color-mix(in srgb, var(--ink) 5%, transparent) 1px, transparent 1.4px) 0 0 / 22px 22px, var(--bg-card); }'), false,
|
|
583
|
+
'the same mix as a gradient STOP is a texture over a ground, and flips correctly');
|
|
584
|
+
assert.equal(FLAT_INK_WASH.test('.z { background: color-mix(in srgb, var(--ink) 6%, var(--bg-card)); }'), false,
|
|
585
|
+
'mixing into a surface is the design system working as intended');
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test('--scrim is defined once and does NOT flip with the mode', () => {
|
|
589
|
+
const css = read('style.css');
|
|
590
|
+
const defs = [...css.matchAll(/--scrim\s*:/g)];
|
|
591
|
+
assert.equal(defs.length, 1, '--scrim is declared exactly once; a per-mode override would defeat its whole purpose');
|
|
592
|
+
// And no sheet keeps a second copy of the value it holds.
|
|
593
|
+
const copies = [...css.matchAll(new RegExp(SCRIM_VALUE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'))];
|
|
594
|
+
assert.equal(copies.length, 1, `${SCRIM_VALUE} appears only in the --scrim declaration`);
|
|
595
|
+
});
|
|
@@ -439,22 +439,18 @@ test('RUN: the three notices are inbox lines with a kind, a line and — where o
|
|
|
439
439
|
// than scrolling in a row of their own (walk 1003309). The status tab strip
|
|
440
440
|
// overflows too, but its row is overflow-x:auto and scrolls itself, which is
|
|
441
441
|
// why the fix belongs on the control and not on the drawer.
|
|
442
|
-
const ruleFor = (css, selector) => {
|
|
443
|
-
const re = new RegExp(`(?:^|\\r?\\n)${selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\{([^}]*)\\}`);
|
|
444
|
-
const m = re.exec(css);
|
|
445
|
-
assert.ok(m, `${selector} rule present in style.css`);
|
|
446
|
-
return m[1];
|
|
447
|
-
};
|
|
448
442
|
|
|
449
443
|
test('a filter control may shrink below its content, so the drawer never pushes the page', () => {
|
|
450
|
-
const filter =
|
|
444
|
+
const filter = rule(styleCss, '.filter');
|
|
445
|
+
assert.ok(filter, '.filter rule present in style.css');
|
|
451
446
|
assert.match(filter, /min-width:\s*0\b/, '.filter is a flex item: min-width:auto is what pinned it to its content');
|
|
452
447
|
assert.match(filter, /max-width:\s*100%/, 'and it may not exceed the row it sits in');
|
|
453
448
|
|
|
454
449
|
// The select is the thing with the runaway intrinsic width. It keeps its
|
|
455
450
|
// 150px floor for wide screens — a filter narrower than that is unreadable —
|
|
456
451
|
// and gains a cap so a long option label cannot widen it past its cell.
|
|
457
|
-
const select =
|
|
452
|
+
const select = rule(styleCss, '.filter select');
|
|
453
|
+
assert.ok(select, '.filter select rule present in style.css');
|
|
458
454
|
assert.match(select, /max-width:\s*100%/, 'the select is capped to its cell');
|
|
459
455
|
// The floor is expressed so it CANNOT itself pin the control: a bare 150px
|
|
460
456
|
// would refuse to shrink in a cell narrower than 150px, which is the same
|
|
@@ -463,7 +459,9 @@ test('a filter control may shrink below its content, so the drawer never pushes
|
|
|
463
459
|
|
|
464
460
|
// The free-text search box sits in the same wrapping row and would do the
|
|
465
461
|
// same thing at 320 with its 200px floor.
|
|
466
|
-
|
|
462
|
+
const search = rule(styleCss, '.filter input[type="search"]');
|
|
463
|
+
assert.ok(search, '.filter input[type="search"] rule present in style.css');
|
|
464
|
+
assert.match(search, /max-width:\s*100%/,
|
|
467
465
|
'the search box is capped the same way');
|
|
468
466
|
});
|
|
469
467
|
|
|
@@ -77,8 +77,25 @@ test('collab renders WHO IS ASKING, as a door to their profile', () => {
|
|
|
77
77
|
assert.equal(L.askerOf({ requested_by: 92 }), 'builder 92', 'an older server\'s bare id is still a name, never nothing');
|
|
78
78
|
const js = read('collab.js');
|
|
79
79
|
assert.match(js, /L\.askerOf\(req\)/, 'the row asks the lib who is asking');
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// The link is built by personLink(id, name) now rather than inline per row —
|
|
81
|
+
// task 1003274 made BOTH people on a row a door (the asker on an incoming row,
|
|
82
|
+
// the addressee on your own), so the href moved to one helper taking an id.
|
|
83
|
+
assert.match(js, /\/builders\/profile\?id=\$\{escapeHtml\(String\(id\)\)\}/,
|
|
84
|
+
'and links the hall\'s per-builder profile');
|
|
85
|
+
// The wording is L.directionLine's, not this file's: task 1003274 replaced
|
|
86
|
+
// "asked by X" with a sentence that also says WHICH WAY the ask points, because
|
|
87
|
+
// the owner read their own outgoing ask as an incoming one. The sentence is
|
|
88
|
+
// asserted in tests/collab_page.mjs where the lib can be executed.
|
|
89
|
+
assert.match(js, /L\.directionLine\(req, dir\)/, 'as a fact on the row');
|
|
90
|
+
assert.ok(!/asked by \$\{who\}/.test(js), 'the direction-less wording is gone');
|
|
91
|
+
|
|
92
|
+
// The link is spliced with a REPLACER FUNCTION. As a replacement STRING,
|
|
93
|
+
// `$&`/`$$`/"$`"/`$'` inside it are special to String.prototype.replace — and
|
|
94
|
+
// the name it carries is builder-chosen. Measured: a display_name of `$&Bob`
|
|
95
|
+
// rendered as `$&Bobamp;Bob`. Not an injection (both halves are escaped),
|
|
96
|
+
// but a way to corrupt your own row by picking a name.
|
|
97
|
+
assert.match(js, /\.replace\(escapeHtml\(name\), \(\) => link\)/,
|
|
98
|
+
'the replacement is a function, so $-sequences in a name are literal');
|
|
82
99
|
});
|
|
83
100
|
|
|
84
101
|
test('the server names the people on a row through a join, and the writes stay bare', () => {
|
|
@@ -115,7 +132,9 @@ test('"asked of you" reads in runs — by name first, then each craft', () => {
|
|
|
115
132
|
const L = lib();
|
|
116
133
|
assert.equal(L.groupOf({ needs_builder_id: 95 }).id, 'you');
|
|
117
134
|
assert.equal(L.groupOf({ needs_craft: 'artist' }).label, 'asked of artists');
|
|
118
|
-
|
|
135
|
+
// task 1003274: the naming moved to directionLine, which also says WHICH WAY
|
|
136
|
+
// the ask points — the half the owner's report was actually about.
|
|
137
|
+
assert.match(L.directionLine({ needs_builder_id: 7, needs_builder_name: 'Preview Alpha' }, 'out'), /Preview Alpha by name/, 'a named addressee is named');
|
|
119
138
|
const js = read('collab.js');
|
|
120
139
|
assert.match(js, /ov-group__head/, 'each run has the family\'s group head');
|
|
121
140
|
assert.match(js, /Number\(b\.id === 'you'\) - Number\(a\.id === 'you'\)/, 'and the personal run sorts first');
|