@bongos/core 1.19.524 → 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 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.524",
6
- "core_contract": "1.19.524",
7
- "source_commit": "2bcf689e8a274d2d6fe6d95f0ae0cc94d8de9ad2",
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-03T23:36:45.727Z",
9
+ "built_at": "2026-09-04T00:50:39.702Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 431,
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": 2424,
20
- "tree_sha256": "5b603403c990cba86562e75005310c23fd6ec06bfc6934190cb2f7b1edd8f596",
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": "e3bb2c282df45ec41150fd041df3c2525d653184858c2bb0a6c7fff670f4b85c"
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": "804238f39cba79b0503aee0079bde0272398c96b713e26c4f32298cd1db0fc06"
1755
+ "sha256": "e848eaf9b035df899cc6f6d80f98cb8b01c2e9079fc5f32db0dd7b3a73d107ef"
1751
1756
  },
1752
1757
  {
1753
1758
  "path": "docs/copy-registry.json",
1754
1759
  "mode": "0000644",
1755
- "sha256": "2df6e1780324aa67c25e347010082441d8267ff754ca3bb76c4071228f0c579d"
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": "dc3cb8274947a5bed3315b467d104b852e5ef4a9aea5642763765e2c3bfc5650"
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": "e92bc2c42efc40d1d9f0feed680e4363a6698bb82fcceab119765fcdea0a26f5"
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": "5a5de2593a47336fa82f1919bb904d02a42ed19d1420eaddafaf831312a50541"
4550
+ "sha256": "3e953b00bbd57a248ed233b98b2451d8f49760bd066d5a58466860495c4d2e35"
4546
4551
  },
4547
4552
  {
4548
4553
  "path": "modules/hall-ui/public/collab.states.json",
@@ -7297,12 +7302,12 @@
7297
7302
  {
7298
7303
  "path": "package-lock.json",
7299
7304
  "mode": "0000644",
7300
- "sha256": "bc7bbba6148fa9d568b64a75b65ef180df5552277546b777ffdee8316f72105c"
7305
+ "sha256": "a874a97765e31f2c3f6c022d648a27336ae780b1506d065c8f96ea6ddbc017cc"
7301
7306
  },
7302
7307
  {
7303
7308
  "path": "package.json",
7304
7309
  "mode": "0000644",
7305
- "sha256": "f9383e8ec30bfc6a70fc13c2484732cbe20e97caac115737fb4942dd2e6c3588"
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": "40fd7f7332ab9375d8078e846d565e282b7daf64abfdb6b22eb21380e7cdae17"
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": "5b2872cd852218e4af5c6002318a1f72c677c417bcc43bf47a9fa8e23edb14f2"
9565
+ "sha256": "98408d2e16e0ca596a31e9e97588584717dae126e863323088b5457dfef24420"
9561
9566
  },
9562
9567
  {
9563
9568
  "path": "tests/conductor.mjs",
@@ -10372,7 +10377,7 @@
10372
10377
  {
10373
10378
  "path": "tests/hall_collab_world.mjs",
10374
10379
  "mode": "0000644",
10375
- "sha256": "b597173a0d280286f0f5ff5d13e0065b2c1dc311024d1f13b5c2d0c3915daf45"
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.
@@ -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.
@@ -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
- **1646 strings** across 3 surfaces · 1428 distinct pieces of copy · 1646 certain, 0 heuristic · 154 shown in more than one place.
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 | 1188 |
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` — 1188 strings
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:173` |
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:251` |
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:173` |
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:173` |
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:90` |
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` |
@@ -3,9 +3,9 @@
3
3
  "generator": "scripts/gds/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 1646,
7
- "unique_ids": 1428,
8
- "certain": 1646,
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": 1188
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": 173,
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": 251,
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": 173,
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": 173,
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": 90,
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",
@@ -1485,5 +1485,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1485
1485
  landed since 1.19.522 with no explicit bump. run 33816191865. (task 1002620)
1486
1486
  1.19.524 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1487
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)
1488
1490
  ---------------------------------------------------------------------------
1489
1491
  ```
@@ -51,19 +51,42 @@
51
51
  return (ta || 0) - (tb || 0);
52
52
  }
53
53
 
54
- // Who a request is addressed to, in words. A craft request and a by-name
55
- // request read differently on purpose: "asked of builders" is a call anyone of
56
- // that craft may take, "asked of you" is personal and should feel it. A
57
- // by-name ask that is not yours names the person when the server joined a
58
- // name, and says "one person" when it did not.
59
- function addressedTo(req, viewerId) {
60
- if (req && req.needs_builder_id != null) {
61
- if (String(req.needs_builder_id) === String(viewerId)) return 'asked of you by name';
62
- var name = req.needs_builder_name || req.needs_builder_login;
63
- return name ? 'asked of ' + name + ' by name' : 'asked of one person';
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
- if (req && req.needs_craft) return 'asked of ' + req.needs_craft + 's';
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
- addressedTo: addressedTo, askerOf: askerOf, contextOf: contextOf,
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
- // Who to answer: the asker's name, as a door to their profile.
63
- function askerFact(req) {
64
- const name = L.askerOf(req);
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
- const who = req.requested_by != null
67
- ? `<a href="/builders/profile?id=${escapeHtml(String(req.requested_by))}">${escapeHtml(name)}</a>`
66
+ return id != null
67
+ ? `<a href="/builders/profile?id=${escapeHtml(String(id))}">${escapeHtml(name)}</a>`
68
68
  : escapeHtml(name);
69
- return `<span class="ov-fact">asked by ${who}</span>`;
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">${askerFact(req)}${contextChip(req)}</div>
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"><span class="ov-fact">${escapeHtml(L.addressedTo(req, null))}</span>${contextChip(req)}</div>
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">
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.524",
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.524",
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.524",
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.524'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
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');
@@ -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.match(L.addressedTo({ needs_craft: 'artist' }, 1), /artists/);
102
- assert.match(L.addressedTo({ needs_builder_id: 7 }, 7), /by name/);
103
- assert.match(L.addressedTo({ needs_builder_id: 7 }, 9), /one person/);
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', () => {
@@ -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
- assert.match(js, /\/builders\/profile\?id=\$\{escapeHtml\(String\(req\.requested_by\)\)\}/, 'and links the hall\'s per-builder profile');
81
- assert.match(js, /asked by \$\{who\}/, 'as a fact on the row');
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 `$&amp;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
- assert.match(L.addressedTo({ needs_builder_id: 7, needs_builder_name: 'Preview Alpha' }, 9), /Preview Alpha by name/, 'a named addressee is named');
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');