@bongos/core 1.19.493 → 1.19.495

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.
Files changed (88) hide show
  1. package/.bongos-core.json +419 -14
  2. package/.claude/skills/builder-redteam/SKILL.md +2 -2
  3. package/docs/adr/0234-idea-routing-capture-time-promotion-landing-matrix-homeless-inbox.md +183 -0
  4. package/docs/adr/README.md +1 -0
  5. package/docs/design/reviews/hall-v2/README.md +124 -0
  6. package/docs/design/reviews/hall-v2/atlas--archon.webp +0 -0
  7. package/docs/design/reviews/hall-v2/atlas--metic.webp +0 -0
  8. package/docs/design/reviews/hall-v2/atlas--xenos.webp +0 -0
  9. package/docs/design/reviews/hall-v2/collab--archon.webp +0 -0
  10. package/docs/design/reviews/hall-v2/collab--metic.webp +0 -0
  11. package/docs/design/reviews/hall-v2/collab--xenos.webp +0 -0
  12. package/docs/design/reviews/hall-v2/copy-desk--archon.webp +0 -0
  13. package/docs/design/reviews/hall-v2/copy-desk--metic.webp +0 -0
  14. package/docs/design/reviews/hall-v2/copy-desk--xenos.webp +0 -0
  15. package/docs/design/reviews/hall-v2/diagrams--archon.webp +0 -0
  16. package/docs/design/reviews/hall-v2/diagrams--metic.webp +0 -0
  17. package/docs/design/reviews/hall-v2/diagrams--xenos.webp +0 -0
  18. package/docs/design/reviews/hall-v2/drachmae--archon.webp +0 -0
  19. package/docs/design/reviews/hall-v2/drachmae--metic.webp +0 -0
  20. package/docs/design/reviews/hall-v2/drachmae--xenos.webp +0 -0
  21. package/docs/design/reviews/hall-v2/gate--archon.webp +0 -0
  22. package/docs/design/reviews/hall-v2/gate--metic.webp +0 -0
  23. package/docs/design/reviews/hall-v2/gate--xenos.webp +0 -0
  24. package/docs/design/reviews/hall-v2/goals--archon.webp +0 -0
  25. package/docs/design/reviews/hall-v2/goals--metic.webp +0 -0
  26. package/docs/design/reviews/hall-v2/goals--xenos.webp +0 -0
  27. package/docs/design/reviews/hall-v2/government--archon.webp +0 -0
  28. package/docs/design/reviews/hall-v2/government--metic.webp +0 -0
  29. package/docs/design/reviews/hall-v2/government--xenos.webp +0 -0
  30. package/docs/design/reviews/hall-v2/harbor--archon.webp +0 -0
  31. package/docs/design/reviews/hall-v2/harbor--metic.webp +0 -0
  32. package/docs/design/reviews/hall-v2/harbor--xenos.webp +0 -0
  33. package/docs/design/reviews/hall-v2/idea--archon.webp +0 -0
  34. package/docs/design/reviews/hall-v2/idea--metic.webp +0 -0
  35. package/docs/design/reviews/hall-v2/idea--xenos.webp +0 -0
  36. package/docs/design/reviews/hall-v2/ideas--archon.webp +0 -0
  37. package/docs/design/reviews/hall-v2/ideas--metic.webp +0 -0
  38. package/docs/design/reviews/hall-v2/ideas--xenos.webp +0 -0
  39. package/docs/design/reviews/hall-v2/index--archon.webp +0 -0
  40. package/docs/design/reviews/hall-v2/index--metic.webp +0 -0
  41. package/docs/design/reviews/hall-v2/index--xenos.webp +0 -0
  42. package/docs/design/reviews/hall-v2/modules--archon.webp +0 -0
  43. package/docs/design/reviews/hall-v2/modules--metic.webp +0 -0
  44. package/docs/design/reviews/hall-v2/modules--xenos.webp +0 -0
  45. package/docs/design/reviews/hall-v2/not-ready--archon.webp +0 -0
  46. package/docs/design/reviews/hall-v2/not-ready--metic.webp +0 -0
  47. package/docs/design/reviews/hall-v2/not-ready--xenos.webp +0 -0
  48. package/docs/design/reviews/hall-v2/pair--archon.webp +0 -0
  49. package/docs/design/reviews/hall-v2/pair--metic.webp +0 -0
  50. package/docs/design/reviews/hall-v2/pair--xenos.webp +0 -0
  51. package/docs/design/reviews/hall-v2/people--archon.webp +0 -0
  52. package/docs/design/reviews/hall-v2/people--metic.webp +0 -0
  53. package/docs/design/reviews/hall-v2/people--xenos.webp +0 -0
  54. package/docs/design/reviews/hall-v2/primer--archon.webp +0 -0
  55. package/docs/design/reviews/hall-v2/primer--metic.webp +0 -0
  56. package/docs/design/reviews/hall-v2/primer--xenos.webp +0 -0
  57. package/docs/design/reviews/hall-v2/profile--archon.webp +0 -0
  58. package/docs/design/reviews/hall-v2/profile--metic.webp +0 -0
  59. package/docs/design/reviews/hall-v2/profile--xenos.webp +0 -0
  60. package/docs/design/reviews/hall-v2/ranks--archon.webp +0 -0
  61. package/docs/design/reviews/hall-v2/ranks--metic.webp +0 -0
  62. package/docs/design/reviews/hall-v2/ranks--xenos.webp +0 -0
  63. package/docs/design/reviews/hall-v2/roadmap--archon.webp +0 -0
  64. package/docs/design/reviews/hall-v2/roadmap--metic.webp +0 -0
  65. package/docs/design/reviews/hall-v2/roadmap--xenos.webp +0 -0
  66. package/docs/design/reviews/hall-v2/roster--archon.webp +0 -0
  67. package/docs/design/reviews/hall-v2/roster--metic.webp +0 -0
  68. package/docs/design/reviews/hall-v2/roster--xenos.webp +0 -0
  69. package/docs/design/reviews/hall-v2/sessions--archon.webp +0 -0
  70. package/docs/design/reviews/hall-v2/sessions--metic.webp +0 -0
  71. package/docs/design/reviews/hall-v2/sessions--xenos.webp +0 -0
  72. package/docs/design/reviews/hall-v2/settings--archon.webp +0 -0
  73. package/docs/design/reviews/hall-v2/settings--metic.webp +0 -0
  74. package/docs/design/reviews/hall-v2/settings--xenos.webp +0 -0
  75. package/docs/design/reviews/hall-v2/task--archon.webp +0 -0
  76. package/docs/design/reviews/hall-v2/task--metic.webp +0 -0
  77. package/docs/design/reviews/hall-v2/task--xenos.webp +0 -0
  78. package/docs/design/reviews/hall-v2/watch--archon.webp +0 -0
  79. package/docs/design/reviews/hall-v2/watch--metic.webp +0 -0
  80. package/docs/design/reviews/hall-v2/watch--xenos.webp +0 -0
  81. package/docs/design/reviews/hall-v2/work--archon.webp +0 -0
  82. package/docs/design/reviews/hall-v2/work--metic.webp +0 -0
  83. package/docs/design/reviews/hall-v2/work--xenos.webp +0 -0
  84. package/docs/module-api-changelog.md +4 -0
  85. package/package-lock.json +2 -2
  86. package/package.json +1 -1
  87. package/src/module-api.js +1 -1
  88. package/tests/redteam_followup_kind.mjs +44 -0
@@ -0,0 +1,183 @@
1
+ # 0234 — Idea routing: capture-time promotion, the landing matrix, and the homeless-only inbox
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-09-03
5
+ - **Tasks:** [#1003064](https://cloudbongos.com/builders#/task/1003064) (R14 of goal 1000071, "Idea routing") — records the mechanism R01/R02/R05/R07 built and the identity R13 gave the inbox
6
+ - **Spec:** [`docs/specs/<redacted>.md`](../specs/<redacted>.md) — "Design decisions & assumptions" + "Interview decisions"
7
+ - **Related:** ADR 0084 (auto-derived per-task rank floor), ADR 0086 (goal-scoped work hierarchy), ADR 0016 (trust boundary, server-enforced), ADR 0172 (ideator credit lane, the lineage this feeds)
8
+
9
+ ## Context
10
+
11
+ Idea 1000771 named the failure this goal exists to close: Discord `#bugs` created tasks **directly**
12
+ (`createTask`, no `idea_inbox` row) against a **hardcoded, already-shipped** version id. Every bug
13
+ reported through that channel landed in a version catch-all nobody would ever reopen, and — the worse
14
+ half — no `captured_by`, no `ideas_task_lineage` row, so the ideator-credit lane (goal 1000067) had
15
+ nothing to ever pay against. The reporter vanished from the record the moment their report became work.
16
+
17
+ Meanwhile the *other* 49 open `idea_inbox` rows sat waiting for a human triage pass that, on
18
+ promotion, dropped the resulting task into "`<version> — general`" — because nothing in the capture
19
+ path had ever named a goal. Work that plainly had a home (a bug in a live feature, a feature request
20
+ against an open goal) got no closer to that home than a homeless idea did.
21
+
22
+ This ADR records what shipped to fix both: five tasks (R01–R03, R05, R07) plus the promote-path change
23
+ (R13), all now `shipped`.
24
+
25
+ ## Decision
26
+
27
+ ### 1. Routing is capture-time promotion, inside `captureIdea` — never a second door
28
+
29
+ `inbox.captureIdea` (`modules/ideas/inbox.js:225`, task 1003065 / R01) is documented in its own header
30
+ as **"THE ONE CAPTURE CHOKEPOINT"** — every filing vector (hall, `capture.js`, the Discord `#ideas`
31
+ inbound via the `ideas.capture` port, agents, seeds) already funnels through it, so routing was added
32
+ as one optional argument (`goalId`) rather than a sibling `captureAndRoute` function. The module's own
33
+ comment names the reason explicitly: *"A `captureAndRoute` sibling would have been the same code with
34
+ an invitation to bypass it."* That is the direct, structural answer to the Discord `#bugs` lesson — a
35
+ second creation path is exactly the shape idea 1000771 was.
36
+
37
+ When the landing matrix (below) says **don't route**, `captureIdea` runs the pre-routing INSERT
38
+ unchanged — `idea_inbox` status `'open'`, no task. This is the default path and is what every
39
+ goal-unaware caller still gets (routing is strictly additive).
40
+
41
+ When the matrix says **route**, `routeIdeaToGoal` (`modules/ideas/inbox.js:303`) runs **one
42
+ transaction**: the idea row (`status='promoted'`, `reviewed_at` stamped — routing's whole claim is
43
+ that this row never needed a triage pass), the task via the `lifecycle.createTask` kernel port, and an
44
+ `ideas_task_lineage` row with `origin='route'`. The order is forced by foreign keys (idea → task →
45
+ lineage), and the module's own comment states the stakes: *"THE TRANSACTION IS THE POINT, not an
46
+ optimisation... A route that committed the task but lost the lineage row would manufacture exactly
47
+ that bug, at scale, on the path whose whole promise is that attribution survives (criterion C3)."* If
48
+ any step fails, the whole thing rolls back and the filer's report is never silently lost mid-flight.
49
+
50
+ `captureIdea` treats `filerRank`/`grade` as **read-only inputs to the decision** — it performs no
51
+ authority check itself; that is R05's job (below), a deliberately separate, load-bearing gate.
52
+
53
+ ### 2. The landing matrix, as shipped (`modules/ideas/routing.js`, task 1003065 / R01 + task 1003066 / R05)
54
+
55
+ `decideLanding()` is a pure, DB-free function — no clock, no rank lookup — so every cell is pinned by
56
+ `tests/idea_routing.mjs` without touching Postgres. Its posture is explicitly **fail-closed**: every
57
+ uncertainty resolves toward "don't route" or "route to backlog", never toward `'ready'`.
58
+
59
+ | Filed thing | Filer rank | Landing (as shipped) |
60
+ |---|---|---|
61
+ | No goal named | any | `idea_inbox`, `status='open'` — the default path (reason: `no_goal`) |
62
+ | `grade='full'` (Full Idea) | any | Never routes — the Board Room's subject (reason: `full_idea_never_routes`) |
63
+ | `kind` ∈ {`criterion-proposal`, `decision`} | any | Never routes — judgment work (reason: `judgment_kind`) |
64
+ | `kind='bug'` with a goal | Metic or Archon | Task in the goal, `status='ready'` (reason: `bug_trusted_filer`) |
65
+ | `kind='bug'` with a goal | sub-Metic / unknown | Task in the goal, `status='backlog'` (reason: `bug_awaiting_nod`) |
66
+ | Anything else with a goal (feature, cleanup, refactor, infra, or an unrecognised kind) | any | Task in the goal, `status='backlog'` (reason: `soft_landing`) |
67
+
68
+ This matches the spec's table (`docs/specs/<redacted>.md` "Landing matrix") exactly for the
69
+ hard-land/soft-land split. Priority on a routed task defaults to the idea's own `suggested_priority`,
70
+ clamped to 1–5, else `3` (`routedPriority`) — precisely the "decided without interview" line the spec
71
+ records.
72
+
73
+ `decideLanding` only decides the **status**; it grants nothing. `authorizeRoutedLanding`
74
+ (`modules/ideas/routing.js:169`, task 1003066 / R05) is the **binding** gate `routeIdeaToGoal` calls
75
+ before it writes anything, and it closes a hole the matrix alone left open: a routed task declares no
76
+ `touches[]`, so the ordinary ADR 0084/0085 rank-floor derivation would land it at `xenos` even when the
77
+ goal's `scope_modules` reach protected territory (the permission/ship-grade/deploy/migration core).
78
+ Two rules, ordered and fail-closed (the `authorizeGoalTaskCreate` precedent):
79
+
80
+ - **Touches declared** (a programmatic filer that knows its own blast radius) → `goalScopeCheck.check`
81
+ runs verbatim, refusals and all — the same wall `POST /goals/:id/tasks` applies to an authored task.
82
+ - **Touches absent** (every human filing — demanding touches[] as the price of reporting a bug is
83
+ exactly the triage tax this goal removes) → protection moves to the **claim side**: a goal whose
84
+ scope reaches protected territory raises the task's `requires_rank` floor to `metic`
85
+ (`PROTECTED_SCOPE_FLOOR`), and — the part R05 tightened after its first cut — a **sub-Metic filer is
86
+ refused outright** (`ROUTE_NOT_AUTHORIZED` / `protected_scope`, HTTP 403) rather than merely having
87
+ the floor stamped on afterward. The refusal costs the filer nothing they had before: omit `goal_id`
88
+ and the report files to the inbox exactly as it always did.
89
+
90
+ **Membership is deliberately not required to route into a goal**, and the module's comment names this
91
+ as the one place the routed path diverges from the authored one on purpose: `authorizeGoalTaskCreate`
92
+ refuses a non-member (`not_a_member`) because *authoring* inside someone's workspace is a member's
93
+ privilege, but *reporting* is not — criterion C5's promise ("the filer never has to know the goal map")
94
+ and C1's ("a bug filed anywhere lands in its home") both require that a non-member can still route a
95
+ report in. The safety membership would have carried moves instead to the rank floor (claim side) and
96
+ the matrix (ready side), and is pinned by a test rather than left to erode quietly.
97
+
98
+ ### 3. The schema: one advisory hint column, one widened CHECK (task 1003060 / R02)
99
+
100
+ `migrations/<redacted>.sql` adds `idea_inbox.suggested_goal_id` (nullable FK
101
+ `ON DELETE SET NULL`) — **not** where a routed idea's goal lives (that is `tasks.goal_id`, set by
102
+ construction), but the leftover hint for rows that did **not** convert: soft-landed/homeless ideas, a
103
+ Discord classifier's guess, Full Ideas and criterion-proposals. The migration's own comment is explicit
104
+ that the column is "load-bearing for NOTHING — no rank check, no scope wall, and no payout may read
105
+ it." It lives in **core** migrations, not a module migration, because `idea_inbox` is a core table and
106
+ `fitness.js`'s `checkModuleMigrationsAdditive` forbids a module `ALTER TABLE` against it.
107
+
108
+ `modules/ideas/migrations/<redacted>.sql` widens `ideas_task_lineage`'s CHECK to
109
+ admit a third `origin`: `'route'`, alongside the pre-existing `'promote'` (a human triaged it) and
110
+ `'develop'` (a spark became a Full Idea). The distinction is deliberate — "how much triage routing
111
+ actually saved" (criterion C6) is only answerable if capture-time conversions are tagged differently
112
+ from later human promotions in the ledger.
113
+
114
+ ### 4. The suggester answers "which goal" *before* anything is created (task 1003062 / R07)
115
+
116
+ `modules/lifecycle/goal-suggest.js` lifts the candidate logic `buildGoalAdvisory` already had (goal
117
+ 1000071's "Reuse, do not rebuild" table) into a **pure, creation-free read**: `suggestGoals(goals,
118
+ draft, opts)` ranks open goals against a draft title/body by weighted token overlap (title terms
119
+ weight 2, body terms weight 1 — the `criterion-suggest.js` precedent), returning `[]` rather than five
120
+ arbitrary goals when nothing scores (the `includeZero` default). `GET /goals/suggestions` exposes it to
121
+ a human picker (the hall filing surface, the CLI prompt); `suggestGoalForCapture({ title, body })`
122
+ collapses the same ranking to a **single confident answer or null**, for a vector with nobody to choose
123
+ — currently Discord `#bugs` (below). It ranks across **every** version still open to work
124
+ (`versionAcceptsNewWork`, task 1003061 / R03), not one, because an earlier "decline if more than one
125
+ version is open" design would have returned null on every report this instance (which runs two
126
+ building versions concurrently) ever files.
127
+
128
+ ### 5. The inbox's new identity: homeless-only (task 1003071 / R13)
129
+
130
+ `scripts/gds/triage.js`'s `resolveGoalDefault(idea, versionId)` gives the promote path a goal by a
131
+ strict precedence — `idea.suggested_goal_id` (a human or filing surface already named this goal; a
132
+ heuristic must not overrule it) **before** the R07 suggester (only when there is no stored hint) — and
133
+ `createTask({ ..., goalId })` finally sends `goal_id` on `POST /tasks`, a field the route has accepted
134
+ since task 1763 with no client ever sending it. `.claude/skills/idea-triage/SKILL.md` states the
135
+ contract plainly: *"the inbox holds only HOMELESS work — bugs and goal-bound captures now route to a
136
+ task at filing time and never reach the inbox."* Triage shrinks to the judgment calls a routing rule
137
+ cannot make for itself: Full Ideas (Board Room material), `criterion-proposal`/`decision` rows, and
138
+ anything nobody could suggest a home for.
139
+
140
+ ## Divergence from the spec, recorded rather than silently reconciled
141
+
142
+ The spec's interview decision 2 says: *"Discord-filed bugs soft-land with a suggested goal. The report
143
+ files as an idea with a classifier-suggested goal attached and surfaces in the pending queue for a
144
+ one-click route. Nothing lands unvetted."* **That is not what R11 shipped.** `modules/discord/discord-bugs.js`
145
+ (task 1003067 / R11, retiring the hardcoded-`GDS-V3` path idea 1000771 named) instead resolves a
146
+ suggested goal via `suggestGoalForCapture` and hands it straight into `captureIdea` as `goalId` — so a
147
+ **linked Metic+ builder's Discord bug hard-lands `'ready'` immediately**, through the *same* landing
148
+ matrix every other filing vector uses, rather than always parking at `'backlog'` pending a one-click
149
+ nod. The file's own header explains the choice: a local copy of the rank split would only ever drift
150
+ from the one definition in `modules/ideas/routing.js` (`tests/filing_abuse_matrix.mjs` exists precisely
151
+ to prevent that divergence), and "same outcomes as before" (the pre-routing #bugs behaviour already
152
+ hard-landed a trusted linked builder's report). An **unlinked** Discord author still gets no goal and
153
+ no rank, so the matrix cannot route their report into work at all — it lands as a homeless idea, same
154
+ as `#ideas` has always given that person, which is the actual "nothing lands unvetted" guarantee that
155
+ survived.
156
+
157
+ The companion interview decision — **"Discord `#ideas` files with HINTS only (`suggested_goal_id`),
158
+ never routes"** — is recorded here as the design intent, but **is not yet built**: R12 (task 1003068,
159
+ "Discord `#ideas` carries routing hints") is still `status='ready'`, not shipped. Today,
160
+ `modules/discord/discord-inbound.js`'s `#ideas` capture carries no goal hint at all; every `#ideas`
161
+ filing lands exactly as it did before this goal, an ordinary homeless `idea_inbox` row. When R12 ships,
162
+ its hint must land as `suggested_goal_id` only (never `goal_id`) — consistent with every other
163
+ "classifier guess, not a route" vector this ADR documents — or it would silently convert `#ideas` into
164
+ a second bypass of the kind idea 1000771 already named once.
165
+
166
+ ## Consequences
167
+
168
+ - **Attribution survives routing by construction, not by discipline.** `tests/idea_routing.mjs` pins
169
+ the matrix; `tests/filing_abuse_matrix.mjs` (task 1003073 / R06) pins that a Xenos mass-filing bugs
170
+ never lands anything claimable, never lands in protected scope, and never escalates via filing;
171
+ task 1003072 / R04 is the proof that a routed bug's lineage + `captured_by` survive end-to-end to a
172
+ shipped task.
173
+ - **A caller lying about `filerRank` moves a bug from `'backlog'` to `'ready'` and nothing else** — the
174
+ matrix's own header states this as the reason R06's abuse matrix tests the *route* (server-derived
175
+ rank), not `captureIdea` (which trusts whatever rank it is handed, by design, because the route is
176
+ the only legitimate caller).
177
+ - **The inbox count is now a real signal.** With bugs and goal-bound features routed away at filing
178
+ time, `idea_inbox`'s open count should trend toward "things that genuinely have no home yet" —
179
+ measured by task 1003074 / R15's before/after sweep of the ~50 rows open when the spec was written
180
+ (not yet shipped at the time of this ADR).
181
+ - **R08–R10, R12, R15 remain open** (hall filing surface, CLI `--goal`, hall reassignment affordance,
182
+ Discord `#ideas` hints, the reclassification-sweep proof). This ADR documents the mechanism and the
183
+ matrix those tasks build against; it does not claim the whole spec is delivered.
@@ -321,5 +321,6 @@ This keeps the decision history honest and traceable.
321
321
  | 0232 | [**Rot is derived, not swept; and `water` is the only new verb** ([task 1003279](https://cloudbongos.com/builders#/task/1003279) · goal 1000072, criterion `legibility-sort-and-staleness`). The inbox half [ADR 0226](<redacted>.md) split out and left open. Owner decision (2026-08-25): a goal or task past the timer raises a **needs-attention line** — not an automatic status change (it mutates real work with no human in the loop, and a wrong timer then silently freezes it) and not a nightly digest (a notification is not an action, and one you scroll past is indistinguishable from the silent decay it was meant to fix). The build note asked for an idempotent poller; the decision is that **there is no poller and no items table** — a line is a `SELECT` comparing the thing's rot clock to the timer, so "exactly one item per stale thing, not one per sweep" is true by arithmetic rather than by deduplication code that must be written correctly and then stay correct, and clearing falls out for free (an edit moves `updated_at`, a decision moves `status`, watering moves the mark). A stored item would have been a cache of a query whose inputs it could not observe. **The exclusions are the load-bearing part**: rot is work nobody picked up (`backlog`/`ready`, and `open` goals), and `active` is deliberately excluded because the claim-silence timer already reports it in hours — listing it here in days would put one rot in two inboxes under two numbers, the 6h-vs-24h split one tier up; `blocked`, `completed`/`confirmed` and the terminal states are excluded for the same reason, each naming the surface that owns it. The clock is **not a new definition of touched** — it is `task-lib.js`'s published `max(updated_at, shipped_at)` (task 1003278) plus a water mark, pinned by a test asserting the domain expression appears verbatim in migration `core_231`'s index expressions, because drift there is silent. The timer is a **second** timer and defends itself as a different question (days vs hours, DB vs env, asks a human vs releases a claim), stored in a new unseeded generic `project_settings` table — no row means "never configured", which is what lets a settings page tell that apart from "set to the default" — with ADR 0226's precedence and its reporting-degrades fail posture, and an unreachable DB explicitly not treated as a bad value. Three of the four verbs (**prioritise/prune/kill**) reuse `promote`/`demote`/`abandon`/`archive`, the task-1003066 precedent, so they add no permission; only `water` is new, gated on ownership OR `builder.roster.read` inside the handler (a creator holding no atom must still get through). Verbs are **served with each row** rather than hardcoded, since a goal legitimately gets two where a task gets four. Rejected: an items table, a sweep, a digest, auto-demotion, and a fourth verb on goals that would render a dead button.](<redacted>.md) | GDS / legibility + hall |
322
322
 
323
323
  | 0233 | [**A copy proposal is a task carrying a patch, and the applier is the only writer** ([task 1003114](https://cloudbongos.com/builders#/task/1003114) · goal 1000074, criterion `artist-edits-land-as-tasks`). Goal 1000074 asks for two things that pull against each other in its own text: the artist must change copy WITHOUT an engineer, and there must be NO LIVE CMS because every copy change is a claimed, graded, shipped task. R02 shipped the half that needs no reconciliation (the flag) and [ADR 0178](<redacted>.md) §3 pinned the absence of any column that could hold replacement text, naming this task as the one that would have to keep it true. Decisions: (1) the shape is **borrowed from the design half, not invented** — ADR 0081 already has a tool authoring and an `adapter.export()` landing the output as a committed diff under a claim inside a path guard, so the artist is the tool, the copy desk is where they author, and the proposal is the export. (2) A proposal is a **TASK carrying a fenced `copy-proposal` patch**, never a row: the ledger is where a proposal belongs anyway, nothing renders a task description as product copy, a reviewer can read the change without running anything, and `copy-desk` gains NO migration (the obvious-but-wrong R03 is a `copy_desk_proposals` table, which is the forbidden column with extra steps). The module gains `consumes: ["lifecycle"]` and calls `createTask` through the port — it can put a request INTO the ledger and nothing can pull copy OUT of it. (3) `scripts/gds/copy-apply.js` **re-derives the position** rather than trusting the patch, which required teaching R01s extractor to record a source SPAN and deliberately dropping it from the committed registry: an offset goes stale the moment anything above it changes, while the id is designed to survive exactly that movement, so the span is recomputed at the moment of the edit by the same code that decided the string was copy. (4) The **placeholder arithmetic** is what makes a patch mechanical rather than a note for an engineer — the applier puts the sources own `${expr}` back, k-th hole to k-th expression, so an artist may MOVE a value within a sentence but never add or drop one, checked at proposal time and again against the real source. (5) Every ambiguity is a **named refusal** (`target_gone`, `ambiguous_target`, `no_exact_span`, `placeholder_mismatch`, `path_outside_surface` — the design adapters path guard, derived from R01s own SURFACES), because a wrong edit is worse than no edit; after writing, the applier regenerates the registry and checks that what it wrote is what the inventory now reads. (6) Proposing is **open to every rank** (an artist is a craft, not a rank) and lands at `backlog`, so a proposal is a request that passes ordinary triage. Rejected: a proposals table, a column beside the flag, trusting the patchs line number, an escape-aware index map to buy spans for nine strings, auto-closing the flag a proposal answers.](<redacted>.md) | copy-desk / the artist loop |
324
+ | 0234 | [**Idea routing: capture-time promotion, the landing matrix, and the homeless-only inbox** ([task 1003064](https://cloudbongos.com/builders#/task/1003064) · goal 1000071, "Idea routing"). Idea 1000771 named the failure: Discord `#bugs` created tasks DIRECTLY against a hardcoded, already-shipped version — no `captured_by`, no lineage, so the ideator-credit lane had nothing to pay against — while every other open idea sat waiting for a human triage pass that dropped its promotion into a version catch-all, because nothing in capture ever named a goal. Records what R01/R02/R05/R07/R13 shipped. `captureIdea` (`modules/ideas/inbox.js`) stays the ONE capture chokepoint — routing is one optional `goalId` argument, never a second creation door — and when the matrix routes, `routeIdeaToGoal` writes the idea row, the task (via the `lifecycle.createTask` port) and an `ideas_task_lineage` row (`origin='route'`) in ONE transaction, so a route can never mint attribution-free work the way `#bugs` did. `modules/ideas/routing.js`'s `decideLanding` is PURE and fail-closed: a bug from a Metic+ filer hard-lands `ready`, a sub-Metic bug or any other kind with a goal soft-lands `backlog`, a Full Idea or `criterion-proposal`/`decision` never routes, no goal named stays the pre-routing inbox path. `authorizeRoutedLanding` is the BINDING gate underneath the matrix's decision — a goal reaching protected scope raises the task's rank floor to Metic and REFUSES a sub-Metic filer outright, while membership is deliberately NOT required (reporting isn't authoring). `core_222`'s `suggested_goal_id` is an advisory hint only — the real home is `tasks.goal_id`, set by construction — and `/idea-triage` (`scripts/gds/triage.js`) now sends `goal_id` on promotion, so the inbox is homeless-only. Divergence recorded: R11 (Discord `#bugs`) hard-lands a linked Metic+ builder's bug immediately through the SAME shared matrix rather than the spec's "always soft-land, pending queue" design, because a local copy of the rank split would only drift from the one definition; and R12 (Discord `#ideas` hints) is NOT YET SHIPPED, so `#ideas` carries no goal hint today despite the interview decision that it should, hint-only, never route.](<redacted>.md) | ideas / lifecycle |
324
325
 
325
326
  > ⚠️ **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.
@@ -0,0 +1,124 @@
1
+ # The hall in the world — verification walk (task 1003309, goal 1000081)
2
+
3
+ Walked 2026-09-02 against origin/main `88baa433` (the tree with every goal-1000081 redesign task landed, through PR 518). This folder is the record the task asked for: every page, every rank, both modes, the gates, keyboard and focus, reduced motion, console errors, residual palette, and the unit gate — with one contact sheet per page-lane and the defect list filed as tasks.
4
+
5
+ ## Verdict
6
+
7
+ **Passed with four defects filed** (tasks 1003495, 1003496, 1003497, 1003498 — all `ready`, all under goal 1000081). Nothing found blocks the hall; nothing found was left unfiled.
8
+
9
+ ## What was walked
10
+
11
+ The 26 pages that carry a `*.states.json` (`modules/hall-ui/public/`), each rendered through the ui-design kit (`modules/ui-design/kit/render.js`, ADR 0197) at 1440 / 390 / 320 in dark and light plus a reduced-motion pass per mode, against three preview harnesses serving the same fixtures as **xenos**, **metic** and **archon** (`scripts/hall-preview/server.js --fixture-me --rank <r>`). The kit's own signed-out stub rendered each page's signed-out states. The archon lane rendered every width; xenos and metic skipped 320 (the floors there are rank-independent).
12
+
13
+ | | |
14
+ |---|---|
15
+ | renders | **1,232** (78 page-lanes: 26 pages × 3 ranks) + 2 reduced-motion passes per page-lane |
16
+ | floors measured per render | console-errors · failed-requests · horizontal-overflow · one-header-main-h1 · controls-24px · top-bar-80px · reduced-motion |
17
+ | page-lanes with a problem | 19, every one classified below |
18
+ | real defects | 4 (filed) · harness limitations: 3 kinds · correct rank refusals: 7 pages |
19
+
20
+ Sheets: `<page>--<rank>.webp`, one per page-lane — dark rows above light, 1440 → 390 → 320 left to right, one tile per state. (The atlas sheets are from the second run, with `atlas.json` generated.)
21
+
22
+ ## Page × rank
23
+
24
+ | page | xenos | metic | archon |
25
+ |---|---|---|---|
26
+ | atlas | 20 renders · clean · 3 tells | 20 renders · clean · 3 tells | 30 renders · clean · 3 tells |
27
+ | collab | 8 · clean | 8 · clean | 12 · clean |
28
+ | copy-desk | 20 · clean | 20 · clean | 30 · clean |
29
+ | diagrams | 8 · harness (sign-in redirect) | 8 · harness | 12 · harness |
30
+ | drachmae | 16 · stub (signed-out feeds) | 16 · stub | 24 · stub |
31
+ | gate | 12 · refused (metic+) | 12 · clean | 18 · clean |
32
+ | goals | 20 · clean | 20 · clean | 30 · clean |
33
+ | government | 20 · refused (permissions view is archon) | 20 · refused (same) | 30 · clean |
34
+ | harbor | 12 · refused (metic+) | 12 · clean | 18 · clean |
35
+ | idea | 8 · clean | 8 · clean | 12 · clean |
36
+ | ideas | 8 · clean | 8 · clean | 12 · clean |
37
+ | index | 16 · clean | 16 · clean | 24 · clean |
38
+ | modules | 20 · clean | 20 · clean | 30 · clean |
39
+ | not-ready | 8 · clean | 8 · clean | 12 · clean |
40
+ | pair | 8 · clean | 8 · clean | 12 · clean |
41
+ | people | 12 · clean | 12 · clean | 18 · clean |
42
+ | primer | 12 · harness (sign-in redirect) | 12 · harness | 18 · harness |
43
+ | profile | 20 · **defect 1003497** (signed-out links) | 20 · **defect 1003497** | 30 · **defect 1003497** |
44
+ | ranks | 8 · clean | 8 · clean | 12 · clean |
45
+ | roadmap | 8 · clean | 8 · clean | 12 · clean |
46
+ | roster | 12 · clean | 12 · clean | 18 · clean |
47
+ | sessions | 8 · refused (metic+) | 8 · clean | 12 · clean |
48
+ | settings | 16 · clean | 16 · clean | 24 · clean |
49
+ | task | 12 · clean | 12 · clean | 18 · clean |
50
+ | watch | 16 · refused (metic+) | 16 · clean | 24 · clean |
51
+ | work | 24 · clean | 24 · clean | 36 · **defect 1003498** (320, filters open) |
52
+
53
+ "clean" = every floor held in every render of that lane. Tells (the kit's advisory lines, never problems) are summarised under *Tells* below.
54
+
55
+ ## Every problem, classified
56
+
57
+ | kind | count | where | what it is |
58
+ |---|---|---|---|
59
+ | `failed-requests` on `/api/gds/auth/web/start?return=…` | 236 | diagrams, primer — all lanes | **harness limitation.** The sign-in redirect target has no fixture on the preview harness, so each signed-out render logs one failed request per navigation. Live answers that URL with the real sign-in door (see *Gates*). |
60
+ | `dispatchClick: nothing matches #svg .ab-node` / `#ab-index-rows …` | 28 | atlas — all lanes, first run | **harness limitation.** The atlas reads a generated, gitignored `atlas.json`; `atlas.states.json` says to run `scripts/gds/gen-atlas.js` first. Re-run with it generated: **all three lanes clean** (the sheets here are that run). |
61
+ | `failed-requests` on `drachmae-and-karma.md` + `public/repo-info` (`127.0.0.1`) | 28 | drachmae — all lanes | **stub limitation.** The kit's own signed-out stub carries no hall feeds; `drachmae.states.json` does not list these two as expected 404s the way `atlas.states.json` does. States-file hygiene, not a page defect (the page renders its honest empty). |
62
+ | `controls-24px: a "roster" 37x18 \| a "sign in" 40x18` | 14 | profile `out` — all lanes, both modes | **defect → task 1003497.** The signed-out empty state's two inline links are 18px tall. Never rendered before: the harness always answers `/me`, so this state only appears against the kit's stub. |
63
+ | `horizontal-overflow: scrollWidth 358 > 320` | 2 | work `filters-open` @320 — archon, both modes | **defect → task 1003498.** The filter drawer's goal `<select>` (332px) will not shrink; measured on the harness, it and its label are the elements past the edge. |
64
+ | `expected visible: #gov-rank-list` | 8 | government `permissions` — xenos, metic | **correct refusal.** `GET /government/ranks` is `government.manage` (archon); the view does not render for a metic. |
65
+ | `expected visible: #gate-body` / `#harbor-body` / `#sessions-scroll` / `#stats-scroll` / `#watch-*` / `#pane-*`, `page.click: Timeout` | 32 | gate, harbor, sessions, watch — xenos | **correct refusal.** These pages are `requireGovernmentPage` (metic+); the harness serves the shell, the API 403s, the page shows nothing. On live the page gate refuses the shell itself (below). |
66
+
67
+ ## Gates
68
+
69
+ - `tests/hall_page_gate_map.mjs`, `government_page_gates.mjs`, `auth_page_gate.mjs`, `scouting_page_gate.mjs`: **21 / 21 pass** — every hall URL is wired to the gate the oracle names.
70
+ - Live, signed out (`curl`, no cookie): **every** `/builders/*` URL probed — work, pair, settings, atlas, modules, goals, roadmap, primer, diagrams, watch, harbor, gate, sessions, people, government, roster, `/builders/`, ranks, drachmae, profile, not-ready, task/1003491, idea/1000904, ideas — answers **401** with the instance's own sign-in door (`noindex`), and so does the apex `/`. That is wider than the oracle's router-level map (which leaves roster, ranks, drachmae, profile, not-ready and `/builders/` ungated) because this project runs `project.platformVisibility = gated` — the member door in `src/bongos/platform-visibility-gate.js` (task 1003043, ADR 0190) refuses strangers on every surface before any page gate runs. Both layers behave as designed.
71
+ - Rank refusals on the harness (the table above) match the catalog floors: gate, harbor, sessions, watch, government → metic+; government's permissions view → archon.
72
+
73
+ ## Keyboard and focus
74
+
75
+ - Measured on the harness as archon, after a real `Tab` (keyboard modality): every visible focusable element (`a[href], button, input, select, textarea, summary, [tabindex]`) focused in turn and its computed outline / box-shadow read — **work board 140 / 140 ringed, profile 47 / 47, task page 46 / 46** (the new grade panel and session-log disclosure included); first Tab stop on all three is the skip link, and the kit's focus-order fact on all 1,232 renders reads `Skip to content › Builders' Hall › Home › Roadmap …`.
76
+ - Static: three `outline: none` sites in the hall's sheets. `diagram-viewer.css:47` is restored by `.dgv__stage:focus-visible` (inset ring); `style.css:531` `main:focus` is the skip link's programmatic target; **`style.css` `.palette__input` has no replacement → task 1003496** (the command palette input, hidden until Ctrl+K, so the measured pages could not reach it).
77
+ - `controls-24px` held on every render except the profile's signed-out state (task 1003497).
78
+
79
+ ## Reduced motion, console, overflow, structure
80
+
81
+ - Reduced motion: **every page-lane, both modes: `dead — running 0, rAF 0`** (nothing animates and no frame loop runs under `prefers-reduced-motion`).
82
+ - Console errors: **none** on any of the 1,232 renders (the floors' `console-errors` never fired; the states files' `ignoreConsole` lists cover only the expected 401/403/404 noise).
83
+ - Horizontal overflow: none except work @320 with the filter drawer open (task 1003498).
84
+ - One `<header>`, one `<main>`, one `<h1>`, top bar ≤ 80px: held on every render.
85
+
86
+ ## Residual palette (hex / rgb outside `style.css`)
87
+
88
+ `grep -nE "#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(" modules/hall-ui/public/*.{css,html,js} | grep -v ^style.css`, comments removed:
89
+
90
+ | site | verdict |
91
+ |---|---|
92
+ | `builders.js:563–568` — Standing's recent-grades column: `color:#a04040`, `var(--olive,#5b7a4d)`, `var(--ink-soft,#8a8577)` inline | **defect → task 1003495** (verification state must wear `--ok/--danger`) |
93
+ | `copy-desk.css:76` — `.cd-dialog::backdrop { background: rgba(0,0,0,.55) }` | **folded into 1003495** |
94
+ | `atlas.css:24–51`, `atlas.js:22–56` — the `--ab-*` node palette | **exempt** — the atlas's semantic node palette is fixed by ADR 0094 (its own states file says so) |
95
+ | `settings.css:157–158` — `#5865F2` on the Discord button | **accepted** — a third-party brand colour |
96
+ | `diagram-viewer.css:10`, `work.css:11,209` | comments only |
97
+
98
+ ## Tells (advisory, reviewed, not filed)
99
+
100
+ - `tiny-type` on `#shell-user-rank`, `.fact-pill`, `.task__tag`, `.prow__tag`, `.sessions-row__badge`, `.badge`, `.rm-goal__ready`, `.gate-reason__floor`, `small` — all **10.5px**, the world's label tier (`--label-size`), 0.5px under the kit's 11px line. A token decision, consistent everywhere; flagged for the owner to raise the tier if wanted, not a defect.
101
+ - `proportional-numerals` on date and id cells (profile ledger, sessions, gate) — cosmetic; worth `tabular-nums` on ledger cells in a later polish.
102
+ - `prose-measure` on `li.rank-card` (~146ch) on the ranks page; `headline-lines` (an h1 at 4 lines) on the task page at 320 with a long title — inherent to the content.
103
+
104
+ ## Unit gate
105
+
106
+ - CI `unit` (npm test + fitness + knip + audit) **green on this exact tree**: PR 518 (`88baa433`) and PR 516 before it.
107
+ - Local, this worktree: the hall's own 18 suites `tests/hall_*.mjs` — **285 / 285 pass**; the four gate suites 21 / 21; `fitness.js` all checks passed. A full local `npm test` on this Windows box reports 28 suite failures, none in hall-ui: generated-artifact freshness (`api_client`, `copy_inventory`), worktree/git path resolution (`next_number`, `box_infra_resolve`, `lib_sh_resolution`, `migrate_root_resolution`…) and Windows process aborts (`0xC0000409`) — environment, not the tree; CI is the gate of record.
108
+
109
+ ## Reproduce
110
+
111
+ ```bash
112
+ # in a checkout at the verified tree
113
+ node scripts/gds/gen-atlas.js # the atlas's generated feed
114
+ node scripts/hall-preview/server.js --port 4641 --fixture-me --rank xenos # and 4642 metic, 4643 archon
115
+ for s in modules/hall-ui/public/*.states.json; do
116
+ node modules/ui-design/kit/render.js --states "$s" --base-in http://builders.localhost:4643 \
117
+ --sizes 1440,390,320 --out /tmp/walk/archon/$(basename "$s" .states.json) # no --base-out: the kit's stub renders signed-out
118
+ done
119
+ # sheets: normalise every <mode>-<width>-<state>.png to 480×300 (ffmpeg scale+pad), then tile=6xN → <page>--<rank>.webp
120
+ node --test tests/hall_page_gate_map.mjs tests/government_page_gates.mjs tests/auth_page_gate.mjs tests/scouting_page_gate.mjs
121
+ node --test tests/hall_*.mjs
122
+ ```
123
+
124
+ Lanes must be launched as async processes — a synchronous spawn serialises the three ranks.
@@ -1423,5 +1423,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1423
1423
  landed since 1.19.491 with no explicit bump. run 33693742388. (task 1002620)
1424
1424
  1.19.493 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1425
1425
  landed since 1.19.492 with no explicit bump. run 33694576680. (task 1002620)
1426
+ 1.19.494 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1427
+ landed since 1.19.493 with no explicit bump. run 33697218855. (task 1002620)
1428
+ 1.19.495 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1429
+ landed since 1.19.494 with no explicit bump. run 33766103175. (task 1002620)
1426
1430
  ---------------------------------------------------------------------------
1427
1431
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.493",
3
+ "version": "1.19.495",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.493",
9
+ "version": "1.19.495",
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.493",
3
+ "version": "1.19.495",
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.493'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
48
+ const CORE_VERSION = '1.19.495'; // 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');