@bongos/core 1.19.531 → 1.19.533
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 +23 -18
- package/docs/adr/0244-a-catalog-search-is-a-view-over-the-rows-never-an-edit-to-the-selection.md +116 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +61 -59
- package/docs/copy-registry.json +90 -72
- package/docs/module-api-changelog.md +4 -0
- package/modules/public-landing/public/projects.html +61 -1
- package/modules/public-landing/public/projects.states.json +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/projects_hub.mjs +22 -0
- package/tests/projects_hub_module_picker.mjs +207 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.533",
|
|
6
|
+
"core_contract": "1.19.533",
|
|
7
|
+
"source_commit": "03699f66aaf653282fcbb775d32389f301c822f8",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-04T14:01:59.226Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 436,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
14
|
"functional_verbatim": 1979,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2439,
|
|
20
|
+
"tree_sha256": "2ab0086b311db79080f1d46f9e35341d02d01ed318907fcf85a11f4dd19beb38",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1729,10 +1729,15 @@
|
|
|
1729
1729
|
"mode": "0000644",
|
|
1730
1730
|
"sha256": "17a0018cfa4f42364a1567bee6bc98f8cf84311ce8344b218d4de9eab870b496"
|
|
1731
1731
|
},
|
|
1732
|
+
{
|
|
1733
|
+
"path": "docs/adr/0244-a-catalog-search-is-a-view-over-the-rows-never-an-edit-to-the-selection.md",
|
|
1734
|
+
"mode": "0000644",
|
|
1735
|
+
"sha256": "a115ba411ec945223b0de36514fcbfb253205b79c2125c232081f35af3ec9a45"
|
|
1736
|
+
},
|
|
1732
1737
|
{
|
|
1733
1738
|
"path": "docs/adr/README.md",
|
|
1734
1739
|
"mode": "0000644",
|
|
1735
|
-
"sha256": "
|
|
1740
|
+
"sha256": "b34cab20b46a17a183ce54b2aed2bb09f03c55c47c7f6300b43fc32c86a79421"
|
|
1736
1741
|
},
|
|
1737
1742
|
{
|
|
1738
1743
|
"path": "docs/api-reference.md",
|
|
@@ -1767,12 +1772,12 @@
|
|
|
1767
1772
|
{
|
|
1768
1773
|
"path": "docs/copy-inventory.md",
|
|
1769
1774
|
"mode": "0000644",
|
|
1770
|
-
"sha256": "
|
|
1775
|
+
"sha256": "b42faf85d36a5411a0a1227ca1dd52c7d1f0f741332f03f2fb66d5fa90bc3ea0"
|
|
1771
1776
|
},
|
|
1772
1777
|
{
|
|
1773
1778
|
"path": "docs/copy-registry.json",
|
|
1774
1779
|
"mode": "0000644",
|
|
1775
|
-
"sha256": "
|
|
1780
|
+
"sha256": "f1a02c1bad4249ae04fc7c0758c5e6efd2df2e647be3d89c531a3daeb5281826"
|
|
1776
1781
|
},
|
|
1777
1782
|
{
|
|
1778
1783
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2617,7 +2622,7 @@
|
|
|
2617
2622
|
{
|
|
2618
2623
|
"path": "docs/module-api-changelog.md",
|
|
2619
2624
|
"mode": "0000644",
|
|
2620
|
-
"sha256": "
|
|
2625
|
+
"sha256": "4b74bef8f5b808fa3c7990b8fd727c26be2c929d64805e071489f1ca40a7f339"
|
|
2621
2626
|
},
|
|
2622
2627
|
{
|
|
2623
2628
|
"path": "docs/modules-contract.md",
|
|
@@ -6587,7 +6592,7 @@
|
|
|
6587
6592
|
{
|
|
6588
6593
|
"path": "modules/public-landing/public/projects.html",
|
|
6589
6594
|
"mode": "0000644",
|
|
6590
|
-
"sha256": "
|
|
6595
|
+
"sha256": "16d728f41b1fc8b72fb1ac74d6b97b8bb0709f8a490e660062d21a964b52a4f1"
|
|
6591
6596
|
},
|
|
6592
6597
|
{
|
|
6593
6598
|
"path": "modules/public-landing/public/projects.probes.json",
|
|
@@ -6597,7 +6602,7 @@
|
|
|
6597
6602
|
{
|
|
6598
6603
|
"path": "modules/public-landing/public/projects.states.json",
|
|
6599
6604
|
"mode": "0000644",
|
|
6600
|
-
"sha256": "
|
|
6605
|
+
"sha256": "2cd5e5598d59e8dbf813658c950821bbb9a34b57823bdfb3be7f61d834bf7714"
|
|
6601
6606
|
},
|
|
6602
6607
|
{
|
|
6603
6608
|
"path": "modules/public-landing/public/terms.html",
|
|
@@ -7332,12 +7337,12 @@
|
|
|
7332
7337
|
{
|
|
7333
7338
|
"path": "package-lock.json",
|
|
7334
7339
|
"mode": "0000644",
|
|
7335
|
-
"sha256": "
|
|
7340
|
+
"sha256": "427e2b1d9b7e871c4e28163f4acd078751e2e43c88561fcefd85083721126292"
|
|
7336
7341
|
},
|
|
7337
7342
|
{
|
|
7338
7343
|
"path": "package.json",
|
|
7339
7344
|
"mode": "0000644",
|
|
7340
|
-
"sha256": "
|
|
7345
|
+
"sha256": "7e9fdd8898ce17821b9724972161530d4fb126cd7ec164a02dc290be89a41ae6"
|
|
7341
7346
|
},
|
|
7342
7347
|
{
|
|
7343
7348
|
"path": "public-docs/index.html",
|
|
@@ -9017,7 +9022,7 @@
|
|
|
9017
9022
|
{
|
|
9018
9023
|
"path": "src/module-api.js",
|
|
9019
9024
|
"mode": "0000644",
|
|
9020
|
-
"sha256": "
|
|
9025
|
+
"sha256": "12cf837247460eee2eafd0b97bcbaf4472a0dcea13cd87b116952f4b8f2b75fb"
|
|
9021
9026
|
},
|
|
9022
9027
|
{
|
|
9023
9028
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11247,12 +11252,12 @@
|
|
|
11247
11252
|
{
|
|
11248
11253
|
"path": "tests/projects_hub.mjs",
|
|
11249
11254
|
"mode": "0000644",
|
|
11250
|
-
"sha256": "
|
|
11255
|
+
"sha256": "edcc3d0cc2b9d5e5c44a33b43f98353001ff22222bf2f34c2c3dd28f04f66989"
|
|
11251
11256
|
},
|
|
11252
11257
|
{
|
|
11253
11258
|
"path": "tests/projects_hub_module_picker.mjs",
|
|
11254
11259
|
"mode": "0000644",
|
|
11255
|
-
"sha256": "
|
|
11260
|
+
"sha256": "44981254fda6a07522d650210127fae6d65ed4d06e539f8a555c860c74de8c64"
|
|
11256
11261
|
},
|
|
11257
11262
|
{
|
|
11258
11263
|
"path": "tests/projects_hub_pre_uat.mjs",
|
package/docs/adr/0244-a-catalog-search-is-a-view-over-the-rows-never-an-edit-to-the-selection.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# 0244 — A catalog search is a view over the rows, never an edit to the selection
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-09-04
|
|
5
|
+
- **Tasks:** [#1002340](https://cloudbongos.com/builders#/task/1002340) (this decision — BV1.R21)
|
|
6
|
+
- **Goal:** [#1000046](https://cloudbongos.com/builders#/goal/1000046) — Project creation, criterion
|
|
7
|
+
`proj-module-select` ("during setup a default set of modules is pre-selected…")
|
|
8
|
+
- **Related:** [ADR 0237](<redacted>.md)
|
|
9
|
+
(R19 — the per-type presets, and the file that already said "R21 searches the catalog"),
|
|
10
|
+
[ADR 0240](<redacted>.md)
|
|
11
|
+
(R20 — the picker, and the three-valued `modules` column whose `null` this must not disturb),
|
|
12
|
+
[ADR 0243](<redacted>.md)
|
|
13
|
+
(R22 — the tailored preselect the search now filters)
|
|
14
|
+
|
|
15
|
+
## 1. The problem
|
|
16
|
+
|
|
17
|
+
The picker paints one row per optional module. The catalog is three rows today
|
|
18
|
+
and is meant to grow — the module system's whole point is that an instance adds
|
|
19
|
+
modules — so R21 asks for a keyword search over it.
|
|
20
|
+
|
|
21
|
+
A search over a list of *links* is trivial. A search over a list of **toggles**
|
|
22
|
+
is not, because the list is also an answer. Two things can go wrong the moment a
|
|
23
|
+
row can be hidden:
|
|
24
|
+
|
|
25
|
+
1. **The filter edits the answer.** The natural implementation of "show only the
|
|
26
|
+
matching rows" is to narrow the array the panel renders *and* the array it
|
|
27
|
+
reads back — at which point typing `discord` and pressing Next files a project
|
|
28
|
+
with one module instead of three. The bug is silent: every screen looks right.
|
|
29
|
+
2. **The filter lies about the answer.** Even a filter that only narrows the
|
|
30
|
+
*render* leaves a switched-on module off screen. The owner sees one row, sees
|
|
31
|
+
it unticked, and reasonably concludes nothing else is on. ADR 0237's standard
|
|
32
|
+
for the preselect — "a preselect an owner cannot see the reason for is one
|
|
33
|
+
they have to audit" — applies here in its harsher form: a preselect they
|
|
34
|
+
cannot **see at all** is one they cannot audit.
|
|
35
|
+
|
|
36
|
+
## 2. Decision
|
|
37
|
+
|
|
38
|
+
**The search is a view. `state.modules` is the answer. The view may not write to
|
|
39
|
+
it.**
|
|
40
|
+
|
|
41
|
+
Concretely, in `modules/public-landing/public/projects.html`'s picker block:
|
|
42
|
+
|
|
43
|
+
- `moduleQuery` is a plain variable that holds what is typed. It is **not** part
|
|
44
|
+
of the draft — `saveDraft()` is never called from the input handler — so a
|
|
45
|
+
resumed draft never comes back mid-search, and a search can never be the thing
|
|
46
|
+
that turns an untouched picker into an answered one (ADR 0240's `null`).
|
|
47
|
+
- `visibleModules()` derives, per render, the rows the current query leaves.
|
|
48
|
+
`pickedModules()`, `modulesForCreate()` and `extrasRow()` are untouched and
|
|
49
|
+
keep reading the whole catalog, so what rides the create request and what the
|
|
50
|
+
review names are both independent of what is on screen.
|
|
51
|
+
- **The panel states the part the filter is hiding.** One line under the field:
|
|
52
|
+
`Showing 1 of 3. 2 extras are switched on, including any this search is
|
|
53
|
+
hiding.` It is silent with an empty query, and it counts what is *on*, not what
|
|
54
|
+
is *visible* — which is the whole point of it existing.
|
|
55
|
+
- An empty query is not a search: every row shows, and the line goes quiet. So
|
|
56
|
+
does a query of nothing but spaces.
|
|
57
|
+
|
|
58
|
+
## 3. What "matching" means, and what it deliberately does not
|
|
59
|
+
|
|
60
|
+
`moduleMatches(mod, terms)` is a pure function over one catalog row:
|
|
61
|
+
|
|
62
|
+
> lower-case the row's **label + summary + key** into one haystack; every
|
|
63
|
+
> whitespace-separated term must appear in it as a **substring**.
|
|
64
|
+
|
|
65
|
+
- **AND across terms.** An owner adding a word is saying the list is still too
|
|
66
|
+
long; OR would grow it, which is the opposite of what they asked for.
|
|
67
|
+
- **Substring, not word-boundary.** A half-typed word must still find its module,
|
|
68
|
+
because the field filters on every keystroke and a filter that shows nothing
|
|
69
|
+
until you finish typing reads as a broken one.
|
|
70
|
+
- **The key is searchable even though it is never shown.** It costs nothing and
|
|
71
|
+
it is what a returning owner (or a support answer, or our own docs) actually
|
|
72
|
+
names a module by.
|
|
73
|
+
- **No ranking, no fuzziness, no synonyms.** The catalog is small enough that
|
|
74
|
+
order is the catalog's order, and a rank that reorders toggles under a typing
|
|
75
|
+
hand is a worse experience than a filter that does not.
|
|
76
|
+
- **Not semantic.** "Somewhere for people to chat" finds nothing, and that is the
|
|
77
|
+
scope line: search by meaning is deferred to idea 1000552 with the rest of the
|
|
78
|
+
AI layer for project creation. A lexical filter that admits it is lexical is
|
|
79
|
+
honest; one that half-guesses is not.
|
|
80
|
+
|
|
81
|
+
The whole matcher is a named function on the page precisely so the vm harness in
|
|
82
|
+
`tests/projects_hub_module_picker.mjs` can run it directly — the same reason
|
|
83
|
+
`bundleForAnswers` is a function and not an inline expression.
|
|
84
|
+
|
|
85
|
+
## 4. Consequences
|
|
86
|
+
|
|
87
|
+
- The picker scales past the point where the band is scannable, with no server
|
|
88
|
+
work: the catalog already arrives whole in the one `GET
|
|
89
|
+
/provisioning/starter-bundles` read (ADR 0237), so typing costs no round trip
|
|
90
|
+
and adds no failure mode.
|
|
91
|
+
- BV1.R24's proof path — *create → bundle preselected → search + add → enabled* —
|
|
92
|
+
works because a toggle made while filtered still re-derives the answer in
|
|
93
|
+
**catalog order**, exactly as an unfiltered toggle does.
|
|
94
|
+
- When the catalog fails to load the field is hidden rather than disabled: a
|
|
95
|
+
search box over a list that is not there can only disappoint.
|
|
96
|
+
- A future by-meaning search replaces `moduleMatches` and nothing else. The
|
|
97
|
+
view/answer split is what makes that swap safe.
|
|
98
|
+
|
|
99
|
+
## 5. Alternatives considered
|
|
100
|
+
|
|
101
|
+
- **Filtering the array the panel reads back** (the "obvious" implementation) —
|
|
102
|
+
rejected: it makes the search an editor of the answer, which is failure mode 1
|
|
103
|
+
above, and it is silent.
|
|
104
|
+
- **Hiding matched-out rows with CSS instead of not rendering them** — same
|
|
105
|
+
outcome for the owner, but it puts `aria-pressed` toggles in the accessibility
|
|
106
|
+
tree that a sighted owner cannot see, and the empty-result case still needs its
|
|
107
|
+
own line. Rendering fewer rows is the simpler truth.
|
|
108
|
+
- **Keeping switched-on rows pinned at the top regardless of the query** — it
|
|
109
|
+
makes the count line unnecessary, but it means a search sometimes shows rows
|
|
110
|
+
that do not match, which is a filter that lies in the other direction. The
|
|
111
|
+
count says the same thing without breaking the band's one rule.
|
|
112
|
+
- **Persisting the query in the draft** — a search is a way of looking, not a
|
|
113
|
+
decision. Restoring one on a resumed draft would hide most of the catalog from
|
|
114
|
+
an owner who did not ask for that.
|
|
115
|
+
- **Debouncing the input** — the filter is a pass over three-to-a-few-dozen rows
|
|
116
|
+
of already-loaded data. A delay would only make the field feel slower.
|
package/docs/adr/README.md
CHANGED
|
@@ -331,5 +331,6 @@ This keeps the decision history honest and traceable.
|
|
|
331
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 |
|
|
332
332
|
| 0242 | [**A cross-role cascade is a declaration table on the event that already exists** ([task 1003549](https://cloudbongos.com/builders#/task/1003549) · goal 1000074, criterion `artist-review-task-on-ship` — the split R04 invited in its own brief and [ADR 0241](<redacted>.md)'s dependency). R04 needs "every ship with copy or visuals GENERATES one", and its brief said the infrastructure did not exist, naming the spark queue as the prototype. Reading both surfaces shows the prototype is the wrong half: the spark queue ([task 1002921](https://cloudbongos.com/builders#/task/1002921)) and the copy-desk flag queue ([task 1003113](https://cloudbongos.com/builders#/task/1003113)) are both **PULL** — filtered listings of rows another role already created — and nothing anywhere generated work on a state transition, which is the **PUSH** R04 asks for. (`cascade-dispatch.js` is model-cost routing; named only because its name has misled more than one reader.) **Decision: no new bus.** The kernel seam registry's own header uses this exact scenario as its worked example, `task.shipped` is already emitted post-commit by both ship paths and already has two consumers (discord's broadcast, sessions' deferred reward) — so a cascade is ONE MORE LISTENER on it, registered in `routes/lifecycle.js` beside the port because that mount is the one boot hook tests also run. A cascade is a frozen entry `{ key, discipline, when(task), title(task), body(task) }` — **a declaration table, not a rules engine**: no condition language, no stored rules, no per-project config, no priority ordering; adding one appends an entry and the runner does not change (the [ADR 0237](<redacted>.md)/[0239](<redacted>.md) shape, for their reason — the generalisations worth building are the ones two or three real entries have shown they share). **`CASCADES` ships EMPTY and R04 writes the first rule**, asserted so the emptiness is a decision on the record; the mechanism is proved against the tests' own declarations rather than against its first consumer's shape. Four load-bearing properties, all pinned: (1) **it can never fail the ship it reacts to** — post-commit event, isolated listeners, and a runner that catches everything and returns a report, because ADR 0241 settled that an artist may hold a DEPLOY and never a builder's SHIP and a generator that could fail a ship would smuggle that gate back in; verified against a real failure (no database reachable → one log line, resolves); (2) **idempotent on machinery that already exists** — `source: 'cascade'` + `source_ref: cascade-<key>-<taskId>` through `createTask`'s `(source, source_ref)` read, the same one a copy proposal uses ([ADR 0233](<redacted>.md)), so a reconciler re-sweep or a double emit is a no-op; (3) **it says where it came from** — inherits `goal_id`/`version_id`, carries the origin id in the ref and the prose, but NOT `parent_task_id`, which models a SUBTASK, and cascaded work is not part of the ship that triggered it; (4) **files at `backlog`, never `ready`** — generated work is a request, and auto-filing claimable work would let a rule nobody re-read mint claims. A rule key is PERMANENT (half the source_ref; renaming re-files everything it ever generated), pinned by a duplicate-key check. The mechanism is discipline-agnostic — an ideator or engineer cascade is the same entry with a different `discipline`, which is the generalisation that made this its own task. Accepted limit: cascades are declared in code, not configured per project. Rejected: extending the spark queue (a read-side filter that generates nothing), a new event or poller, a rules engine or per-project config, filing at `ready`, `parent_task_id` as the link, and running the cascade inside the ship transaction (which would make a generation failure a ship failure — the exact coupling ADR 0241 rules out).](<redacted>.md) | goal 1000074 / the artist's loop |
|
|
333
333
|
| 0243 | [**A bundle adjustment belongs to the bundle, not to the advice about it** ([task 1002341](https://cloudbongos.com/builders#/task/1002341) · goal 1000046, criterion `proj-module-select`). R17 ([ADR 0239](<redacted>.md)) built a rule engine and R20 ([ADR 0240](<redacted>.md)) built a picker, and nothing connected them: the picker preselected the type's RAW preset while `GET /provisioning/recommendations` separately applied `TEAM_SHAPE_RULES` to that same preset and returned a better answer no surface rendered. Joining them the obvious way — let the picker apply the rules — breaks the property R20 was built on ([ADR 0240](<redacted>.md) §2: the preselect and the fallback are "one function, called twice"), because an owner who answers the interview, watches the panel preselect `discord`, and then SKIPS the picker would get a project without it. **Decision: `TEAM_SHAPE_RULES` moves from `recommendations.js` down to `starter-bundles.js`** (re-exported, so the public surface is unchanged), `effectiveModules` resolves through the new `bundleForAnswers(type, teamShape)`, and the picker reads the same table the fallback does. The distinction is not a preference: a bundle adjustment is not advice ABOUT the bundle — it IS the bundle once the owner has answered, and it is the only one of R17's three tables that is ever APPLIED (governance is a preview carrying `applied: false`, invites are a seam for R28), so it is the only one that has to live where the resolution happens. `effectiveModules` reads `detail.team_shape` straight off the row rather than through `normalizeProjectDetail` — importing it would point the bundle layer UP at the module that requires it ([ADR 0083](<redacted>.md)) to buy an answer it already has, since an off-vocabulary shape fires no rule and adjusts nothing. `GET /provisioning/starter-bundles` gains `byTeamShape`: every bundle re-resolved for every ruled answer, with the `adjustments` that got it there — twenty small rows, the whole preselect table in ONE read, for the reason `optional` rides the same payload (the owner flips type and team-shape on two EARLIER panels, and a refetch could land after they moved on or paint a key the panel has no row for). `PATCH /provisioning/instances/:id/detail` returns `modules: effectiveModules(updated)` beside its `publish` verdict and for the same reason — changing `team_shape` MOVES the modules of a `source: 'bundle'` project, so the effect comes back with the cause. The panel renders one reason line per adjustment that ACTUALLY fired, and only while the owner has not answered for themselves (a reason handed back for a toggle they flipped reads as the panel arguing with them). An adjusted bundle returns in CATALOG order, never append order, or the same set of modules would read as a different list depending on whether the picker was answered or skipped; a payload without `byTeamShape` degrades to the R20 answer, which is never wrong, only untailored. No new storage — the adjustment is resolved, never written. Rejected: leaving the rules up in `recommendations.js` and having the picker call `/recommendations` (two lists kept in agreement by hand, plus a round trip per keystroke); importing `normalizeProjectDetail`; storing the adjusted bundle on a skip (it writes down an answer nobody gave); moving governance and invites down too (they are never applied, so there is nothing for them to stay in agreement with); and a reason line per matched condition rather than per actual change.](<redacted>.md) | provisioning / project creation |
|
|
334
|
+
| 0244 | [**A catalog search is a view over the rows, never an edit to the selection** ([task 1002340](https://cloudbongos.com/builders#/task/1002340) · goal 1000046, criterion `proj-module-select`). The creation picker ([ADR 0240](<redacted>.md)) paints one toggle per optional module, and the catalog is meant to grow — so R21 adds a keyword search over it. A search over a list of links is trivial; a search over a list of TOGGLES is not, because the list is also an answer. Two failure modes follow from letting a row be hidden: the filter **edits** the answer (narrow the rendered array and the read-back array together, and typing `discord` then pressing Next files one module instead of three — silently, with every screen looking right), or the filter **lies** about it (a switched-on module off screen reads as switched off, which is [ADR 0237](<redacted>.md)’s "a preselect they have to audit" in its harsher form — one they cannot see at all). **Decision: the search is a VIEW and `state.modules` is the answer; the view may not write to it.** `moduleQuery` is a plain variable, never part of the draft, so `saveDraft` is not called from the input handler and a search can never turn an untouched picker into an answered one (ADR 0240’s `null` survives); `visibleModules()` derives the shown rows per render while `pickedModules()`, `modulesForCreate()` and `extrasRow()` keep reading the whole catalog; and the panel STATES what the filter is hiding — one line under the field counting what is **on**, not what is visible. Matching is `moduleMatches(mod, terms)`, a pure function: lower-cased **label + summary + key** as one haystack, every whitespace-separated term an AND’d substring. AND because adding a word means the list is still too long; substring because a filter that shows nothing until you finish typing reads as broken; the key because that is what a returning owner names a module by. No ranking (reordering toggles under a typing hand is worse than not), and deliberately **not semantic** — by-meaning search is deferred with the rest of the creation AI layer (idea 1000552). Rejected: filtering the read-back array (failure mode 1), CSS-hiding non-matches (leaves invisible `aria-pressed` toggles in the a11y tree), pinning switched-on rows above the query (a filter that lies the other way), persisting the query in the draft, and debouncing a pass over already-loaded rows.](<redacted>.md) |
|
|
334
335
|
|
|
335
336
|
> ⚠️ **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.
|