@bongos/core 1.19.528 → 1.19.530

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.528",
6
- "core_contract": "1.19.528",
7
- "source_commit": "f6d2603506a4eaa27fbddbddc75a6119738b71a6",
5
+ "core_version": "1.19.530",
6
+ "core_contract": "1.19.530",
7
+ "source_commit": "f0252c888bf69ec2aadf5067e81ab801f6e6ef70",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-04T02:27:38.586Z",
9
+ "built_at": "2026-09-04T13:34:55.894Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 434,
12
+ "docs_redacted": 435,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 1976,
14
+ "functional_verbatim": 1978,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2434,
20
- "tree_sha256": "965d514d2cf50b0b282abb1e8171fb9a9d78b10f11f043086fa2f2af7083ef76",
19
+ "file_count": 2437,
20
+ "tree_sha256": "9d176777fdd02fcc72db1a22651a24c4cbb6d8ff5a4531567c1d07e25511c7e1",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -387,7 +387,7 @@
387
387
  {
388
388
  "path": "clients/bongos-client/index.d.ts",
389
389
  "mode": "0000644",
390
- "sha256": "80c6b676d869ddfe36311794a88028687eecb5dc5fbdb4789d64212894f38397"
390
+ "sha256": "ae23995907a851acd2b2dbae901280926e3041ce64e16407d47ed5ba998d7c74"
391
391
  },
392
392
  {
393
393
  "path": "clients/bongos-client/index.mjs",
@@ -1724,10 +1724,15 @@
1724
1724
  "mode": "0000644",
1725
1725
  "sha256": "4c6f98c2362a5b61e294f1b3b9b055d44a9e47743ef8a71e5cb0f40ddd187e98"
1726
1726
  },
1727
+ {
1728
+ "path": "docs/adr/0243-a-bundle-adjustment-belongs-to-the-bundle-not-the-advice-about-it.md",
1729
+ "mode": "0000644",
1730
+ "sha256": "17a0018cfa4f42364a1567bee6bc98f8cf84311ce8344b218d4de9eab870b496"
1731
+ },
1727
1732
  {
1728
1733
  "path": "docs/adr/README.md",
1729
1734
  "mode": "0000644",
1730
- "sha256": "b03da1a3085e208b68ab5728e97b0e06b7cfb399e74d776ecc32c29074cd2a79"
1735
+ "sha256": "fb28cbf7bb03cc69deb895b5e25956a440449c11f1fd977ee09655116e3fc3b0"
1731
1736
  },
1732
1737
  {
1733
1738
  "path": "docs/api-reference.md",
@@ -1737,7 +1742,7 @@
1737
1742
  {
1738
1743
  "path": "docs/api/openapi.json",
1739
1744
  "mode": "0000644",
1740
- "sha256": "b1792ba4828a887a1bbd1ba14b13eba4dc428d1e3e1528cc23ddd6577e15b646"
1745
+ "sha256": "46e8c08e1e7b5bfa6fe2e709308a466f3764ed320702c93dbe74d5523bb1d74b"
1741
1746
  },
1742
1747
  {
1743
1748
  "path": "docs/architecture.md",
@@ -1762,12 +1767,12 @@
1762
1767
  {
1763
1768
  "path": "docs/copy-inventory.md",
1764
1769
  "mode": "0000644",
1765
- "sha256": "e848eaf9b035df899cc6f6d80f98cb8b01c2e9079fc5f32db0dd7b3a73d107ef"
1770
+ "sha256": "42f1210b3c422d2b05b719c38eabfb3ed25ed31988a757394dfc2caed045ee36"
1766
1771
  },
1767
1772
  {
1768
1773
  "path": "docs/copy-registry.json",
1769
1774
  "mode": "0000644",
1770
- "sha256": "d4fefb8fce99e3903eec6f0796c9e61b81732a9784fb95fa4a3299fa9da3278a"
1775
+ "sha256": "0d9a040a9bac5670b2034e98479d86706070853dbb65b799d770b76e488534a6"
1771
1776
  },
1772
1777
  {
1773
1778
  "path": "docs/design/apex-pass-2-direction.md",
@@ -2612,7 +2617,7 @@
2612
2617
  {
2613
2618
  "path": "docs/module-api-changelog.md",
2614
2619
  "mode": "0000644",
2615
- "sha256": "4f91a1b18b26d90010d2f78214ed38f9c2f469e53a04e08e8b398bbd753998a9"
2620
+ "sha256": "b4be57f7972652783e91059ffccf9b0df8d972611955a66368c32bc14120dddc"
2616
2621
  },
2617
2622
  {
2618
2623
  "path": "docs/modules-contract.md",
@@ -6057,17 +6062,22 @@
6057
6062
  {
6058
6063
  "path": "modules/provisioning/recommendations.js",
6059
6064
  "mode": "0000644",
6060
- "sha256": "8c4b2d8d17b80229c371b82ac9a5a2cf2fa979683ac586e8b833ca0a309a92fc"
6065
+ "sha256": "4c933aad1307378496d47960e24c9d797a1f122f28aefa27b769f336e4cf2e75"
6066
+ },
6067
+ {
6068
+ "path": "modules/provisioning/routes/callback-page.js",
6069
+ "mode": "0000644",
6070
+ "sha256": "6b43ebf71a778277b94de1b9400f690fac9f51b48010e50a159af57195f99f02"
6061
6071
  },
6062
6072
  {
6063
6073
  "path": "modules/provisioning/routes/provisioning.js",
6064
6074
  "mode": "0000644",
6065
- "sha256": "6e78e87a2112974db6db804280c346daa7c38684adace638e3c5734fe6d1ca72"
6075
+ "sha256": "e08199bc9146bd3122ca038637a0e390b9f5e636a102fa290dba414cf6cafdc1"
6066
6076
  },
6067
6077
  {
6068
6078
  "path": "modules/provisioning/starter-bundles.js",
6069
6079
  "mode": "0000644",
6070
- "sha256": "9dd1fe5bd16d22245d62dae05aefb0c74c82772a937da155f1e8d2403f911886"
6080
+ "sha256": "78cdbb14f2dc911d78d1dd7e1ae73027734e94a3bbd8826b2f27e343a2daba1d"
6071
6081
  },
6072
6082
  {
6073
6083
  "path": "modules/provisioning/tests/liveness.mjs",
@@ -6577,7 +6587,7 @@
6577
6587
  {
6578
6588
  "path": "modules/public-landing/public/projects.html",
6579
6589
  "mode": "0000644",
6580
- "sha256": "a93dbf381a39bde8d4424bd3f241d24a72d9ed915e7da48bdb125d15085f6fef"
6590
+ "sha256": "7147ff51b2eaa3487557fff35f4fc7c3e4f6164fd8054e41c76064bcfe932e08"
6581
6591
  },
6582
6592
  {
6583
6593
  "path": "modules/public-landing/public/projects.probes.json",
@@ -7322,12 +7332,12 @@
7322
7332
  {
7323
7333
  "path": "package-lock.json",
7324
7334
  "mode": "0000644",
7325
- "sha256": "dceea98bb14779f8b25de5d336677be85b46644d37eab685a2846de68f3bbc99"
7335
+ "sha256": "c5ca6f4757bfbd6c174c11a75b767eb787bd202c8eda8e9ee1b51b7d76608b2a"
7326
7336
  },
7327
7337
  {
7328
7338
  "path": "package.json",
7329
7339
  "mode": "0000644",
7330
- "sha256": "a551e83f0029826d20ab7a280ca4cda7da3aeb98f06d064b1592e84b278c7882"
7340
+ "sha256": "2795c4357358560ad6802a780c375b49326f09c1c79ea047702cdcc2b8c602dd"
7331
7341
  },
7332
7342
  {
7333
7343
  "path": "public-docs/index.html",
@@ -9007,7 +9017,7 @@
9007
9017
  {
9008
9018
  "path": "src/module-api.js",
9009
9019
  "mode": "0000644",
9010
- "sha256": "a530f0345672c3ba330cec9e0056eccf02041ef54f932d6f9959bd24d5fc6178"
9020
+ "sha256": "a92fb66032716db9e4517d003bde07cb8283a2282d2b3925283c2018cb348598"
9011
9021
  },
9012
9022
  {
9013
9023
  "path": "src/module-loader/catalog.js",
@@ -9747,7 +9757,7 @@
9747
9757
  {
9748
9758
  "path": "tests/design_tokens_sync.mjs",
9749
9759
  "mode": "0000644",
9750
- "sha256": "0454ee692bbbf19d02c7f19c029f4da5a4dd939b84c4d67ebb713d12de179c2a"
9760
+ "sha256": "6fc1f0a595d3042b5b57b708643cfdc18007b3b33bb6684ffbeb46217da4c376"
9751
9761
  },
9752
9762
  {
9753
9763
  "path": "tests/dev_lib.mjs",
@@ -11237,7 +11247,7 @@
11237
11247
  {
11238
11248
  "path": "tests/projects_hub_module_picker.mjs",
11239
11249
  "mode": "0000644",
11240
- "sha256": "5eaa61295f2a22f5b10cb365cee58e485390ccb763a14be15fec00f23aa23087"
11250
+ "sha256": "55b7912d1985ea1e02874b479c9d0453c25df48bfad6287ba9402df50326d6ed"
11241
11251
  },
11242
11252
  {
11243
11253
  "path": "tests/projects_hub_pre_uat.mjs",
@@ -11269,6 +11279,11 @@
11269
11279
  "mode": "0000644",
11270
11280
  "sha256": "9a04386b3047177bb55b6d392db88764f06f92e5c0b20c16ed9e0f8ce520b07d"
11271
11281
  },
11282
+ {
11283
+ "path": "tests/provisioning_callback_page.mjs",
11284
+ "mode": "0000644",
11285
+ "sha256": "74d53ae7f3c93883bdc6db5187c904cc6c641267233d70bfef1a25d5b2a4ca05"
11286
+ },
11272
11287
  {
11273
11288
  "path": "tests/provisioning_default_domain.mjs",
11274
11289
  "mode": "0000644",
@@ -11287,7 +11302,7 @@
11287
11302
  {
11288
11303
  "path": "tests/provisioning_module_selection.mjs",
11289
11304
  "mode": "0000644",
11290
- "sha256": "48fc7c59efae4b7d1b9ab565882473483d7b4d8a40635583a789d0749da16578"
11305
+ "sha256": "4a27e1641d949f261a8ba1b72816a8b077fc8a55862c4eef4091b453b7cfd45c"
11291
11306
  },
11292
11307
  {
11293
11308
  "path": "tests/provisioning_onboard_mode.mjs",
@@ -11327,7 +11342,7 @@
11327
11342
  {
11328
11343
  "path": "tests/provisioning_starter_bundles.mjs",
11329
11344
  "mode": "0000644",
11330
- "sha256": "9ecf7cda8e47a6e17bbc73afdb0c30c867ab967cea66943082cbe15502fd63bc"
11345
+ "sha256": "b63751ddf17d58ec3fdfdbd19c9f9a7569eed1ca9ff157a2a37f7034151eead7"
11331
11346
  },
11332
11347
  {
11333
11348
  "path": "tests/provisioning_teardown_intent.mjs",
@@ -172,7 +172,7 @@ export interface PatchMeSoundPrefsResponse { prefs: unknown; effective: unknown;
172
172
  export interface PatchProjectsIdFeaturedRequest { featured: boolean }
173
173
  export interface PatchProjectsIdFeaturedResponse { project: unknown }
174
174
  export interface PatchProvisioningInstancesIdDetailRequest { description?: string; detail?: Record<string, unknown> }
175
- export interface PatchProvisioningInstancesIdDetailResponse { ok: boolean; description: unknown; detail: unknown; options: unknown; publish: unknown }
175
+ export interface PatchProvisioningInstancesIdDetailResponse { ok: boolean; description: unknown; detail: unknown; options: unknown; publish: unknown; modules: unknown }
176
176
  export interface PatchProvisioningInstancesIdSettingsResponse { ok: boolean; saved: unknown; planet: unknown; push: unknown }
177
177
  export interface PatchTaskRecommendationsIdRequest { status: "declined" | "withdrawn" }
178
178
  export interface PatchTaskRecommendationsIdResponse { ok: boolean; recommendation: unknown }
@@ -0,0 +1,159 @@
1
+ # 0243 — A bundle adjustment belongs to the bundle, not to the advice about it
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-09-03
5
+ - **Tasks:** [#1002341](https://cloudbongos.com/builders#/task/1002341) (this decision — BV1.R22)
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 said the bundle layer "is a declaration and not a rule
10
+ engine"), [ADR 0239](<redacted>.md) (R17 — the rule
11
+ engine, and the `TEAM_SHAPE_RULES` table this moves), [ADR 0240](<redacted>.md)
12
+ (R20 — the picker, the nullable `modules` column, and §2's "one function, called twice" property this
13
+ is written to preserve), [ADR 0083](<redacted>.md) (the one-way
14
+ import rule)
15
+
16
+ ## 1. The problem
17
+
18
+ R17 built a rule engine and R20 built a picker, and nothing connected them. The
19
+ picker preselected `bundleForProjectType(type).bundle` — the type's raw preset —
20
+ while `GET /provisioning/recommendations` separately applied `TEAM_SHAPE_RULES`
21
+ to that same preset and returned a different, better answer that no surface
22
+ rendered. R22 is the join: **surface the recommended modules inside the picker.**
23
+
24
+ Doing it the obvious way breaks the property R20 was built on. ADR 0240 §2:
25
+
26
+ > Skipping is safe because the preselect and the fallback are the same
27
+ > declaration. […] Not two lists kept in agreement — one function, called twice.
28
+
29
+ If the picker applies the team-shape rules and `effectiveModules` does not, then
30
+ an owner who answers the interview, sees the panel preselect `discord`, and skips
31
+ the picker gets a project **without** `discord`. The panel's promise — "skip it
32
+ and you get the suggested set below" — stops being true at exactly the moment the
33
+ interview is answered, which is the moment it starts mattering.
34
+
35
+ ## 2. The rules move to the bundle layer
36
+
37
+ `TEAM_SHAPE_RULES` leaves `recommendations.js` for `starter-bundles.js`.
38
+ `recommendations.js` re-exports it, so its public surface is unchanged.
39
+
40
+ The reasoning is a distinction, not a preference. **A bundle adjustment is not
41
+ advice about the bundle — it IS the bundle, once the owner has answered.** The
42
+ three tables R17 shipped together are not the same kind of thing:
43
+
44
+ | table | what it produces | applied? | home |
45
+ |---|---|---|---|
46
+ | `TEAM_SHAPE_RULES` | the modules a project actually ends up with | **yes**, on read | `starter-bundles.js` |
47
+ | `GOVERNANCE_RULES` | a preset name + one sentence, `applied: false` | never | `recommendations.js` |
48
+ | `INVITE_RULES` | a `seeking` seam for R28 to fill | never | `recommendations.js` |
49
+
50
+ Governance and invites are genuinely advice: nothing in the system silently ends
51
+ up living under a governance preset. A module set is the opposite — it is what a
52
+ project *has*, resolved at read time for every row that never answered the
53
+ picker. A rule that decides that belongs where the resolution happens.
54
+
55
+ So: `adjustBundle` and the new `bundleForAnswers(type, teamShape)` sit at the
56
+ bundle layer, `effectiveModules` calls `bundleForAnswers` instead of
57
+ `bundleForProjectType`, and `recommendationsForDetails` calls the same resolver.
58
+ One function, still called twice — now with the interview's answer inside it.
59
+
60
+ ## 3. `effectiveModules` reads `detail.team_shape` off the row
61
+
62
+ The fallback needs the interview's answer, and the answer already lives on the
63
+ row (`detail`, `provisioning_013`). It is read **straight off the row**, not
64
+ through `provisioning.js`'s `normalizeProjectDetail`, for two reasons:
65
+
66
+ - Importing it would point `starter-bundles.js` **up** at the module that
67
+ requires it — `recommendations.js` already imports `PROJECT_DETAIL_VOCAB` from
68
+ `provisioning.js`, and the bundle layer sits below both.
69
+ - It would buy nothing. An off-vocabulary shape matches no rule and adjusts
70
+ nothing, which is precisely the answer the normalizer would have produced.
71
+
72
+ `RULED_TEAM_SHAPES` is derived from the rule table itself for the same reason.
73
+ `tests/provisioning_recommendations.mjs` pins it against `PROJECT_DETAIL_VOCAB`
74
+ so a fourth interview answer cannot arrive unhandled.
75
+
76
+ `effectiveModules` also gains `adjustments` — why the unanswered set is what it
77
+ is, in the owner's own words. It is `[]` for a `chosen` set: an owner's own
78
+ toggle needs no reason given back to them.
79
+
80
+ ## 4. The whole answer table arrives in one read
81
+
82
+ `GET /provisioning/starter-bundles` already served `core`, `bundles` and
83
+ `optional` together, because (ADR 0240 §4) a picker that fetched its preselect
84
+ and its catalog separately could preselect a key it has no row for. Each bundle
85
+ now also carries `byTeamShape` — the same bundle re-resolved for every answer the
86
+ rules can fire on, each with the `adjustments` that got it there.
87
+
88
+ It rides this payload rather than a second read of `/provisioning/recommendations`
89
+ for the reason `optional` does. The owner flips the type question and the
90
+ team-shape question freely on two **earlier** panels; re-preselecting must not
91
+ cost a round trip, and a round trip it did pay could land after they had moved
92
+ on, or paint a module the panel has no row for. Five types by three ruled shapes,
93
+ plus each type's no-answer resolution, is twenty small rows — the whole preselect
94
+ table in one read of one declaration, computed purely.
95
+
96
+ ## 5. The effect comes back with the cause
97
+
98
+ `PATCH /provisioning/instances/:id/detail` now returns `modules:
99
+ effectiveModules(updated)` alongside the `publish` verdict it already returned,
100
+ and for the same reason. Changing `team_shape` **moves the modules** of any
101
+ project whose owner never answered the picker (`source: 'bundle'`). An owner who
102
+ edits one answer and is told nothing about the other thing it changed would have
103
+ to go looking. A project with `source: 'chosen'` is unmoved and says so.
104
+
105
+ ## 6. The panel says why
106
+
107
+ The picker renders one line per adjustment that actually fired, above the
108
+ always-on clause. ADR 0239 already made every rule carry its own `reason`
109
+ sentence; this is the first surface that shows one.
110
+
111
+ Two rules on when it appears, both from ADR 0239's delta principle:
112
+
113
+ - **Only rules that changed something.** A rule whose module the bundle already
114
+ carried records nothing — "we switched discord on because you said community"
115
+ over a module a game would have had anyway is a claim the owner can check and
116
+ find false.
117
+ - **Only before the owner has answered.** Once `state.modules` is set, the
118
+ reasons go away: a reason given back for a toggle they flipped themselves reads
119
+ as the panel arguing with them.
120
+
121
+ The block wears `.reassure`'s weight deliberately. This is the panel explaining
122
+ itself, never a thing to act on, so it must not compete with the toggles it is
123
+ explaining. A preselect an owner cannot see the reason for is one they have to
124
+ audit, which ADR 0237 calls worse than no preselect at all.
125
+
126
+ ## 7. Consequences
127
+
128
+ - **An adjusted bundle is returned in CATALOG order**, not append order.
129
+ `normalizeModuleSelection` stores a chosen set that way and the picker
130
+ re-derives its toggles that way, so an adjusted bundle that appended would make
131
+ the same set of modules read as a different list depending on whether the owner
132
+ answered the picker or skipped it.
133
+ - **A stale client degrades to the R20 answer.** A payload without `byTeamShape`,
134
+ or a team shape no rule fires on, falls through to the unadjusted bundle — the
135
+ answer this panel gave before R22, which is never wrong, only untailored.
136
+ - **The rules now run on every read of a bundle-sourced row**, not just on the
137
+ recommendations route. They are pure, frozen and three rows long, so this costs
138
+ nothing; it is noted because the blast radius genuinely grew.
139
+ - **Still no new storage.** ADR 0240's three-valued column is untouched, and a
140
+ skipped picker still stores nothing. The adjustment is resolved, never written.
141
+
142
+ ## 8. Rejected
143
+
144
+ - **Leaving the rules in `recommendations.js` and having the picker call
145
+ `/recommendations`** — the two-lists-by-hand failure ADR 0240 §2 exists to make
146
+ impossible, plus a round trip on every keystroke of two earlier panels that
147
+ could land after the owner moved on.
148
+ - **Teaching `effectiveModules` to import `normalizeProjectDetail`** — points the
149
+ bundle layer up at the module that requires it (ADR 0083's one-way rule), to
150
+ buy an answer it already has.
151
+ - **Storing the adjusted bundle when the owner skips** — ADR 0240 §7 rejected
152
+ this for the raw preset and every word still applies: it writes down an answer
153
+ nobody gave, and freezes today's rules onto a project whose owner never
154
+ expressed a view.
155
+ - **Moving governance and invites down with the bundle rules** — they are never
156
+ applied, so there is no resolution for them to stay in agreement with. Moving
157
+ them would be tidiness at the cost of the distinction that makes §2 true.
158
+ - **A reason line per matched condition rather than per actual change** — ADR
159
+ 0239's delta rule, restated at the surface that renders it.
@@ -330,5 +330,6 @@ This keeps the decision history honest and traceable.
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
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
+ | 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 |
333
334
 
334
335
  > ⚠️ **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.
@@ -12014,7 +12014,7 @@
12014
12014
  "provisioning"
12015
12015
  ],
12016
12016
  "summary": "GET /provisioning/starter-bundles",
12017
- "description": "GET /provisioning/starter-bundles — the curated module preset for each declared project type (BV1.R19, task 1002338; goal 1000046 criterion `proj-module-select`). Returns the always-on core once, then one resolved bundle per type: `{ type, label, summary, core, bundle, modules }`. The whole table in one read, because the creation picker (R20, task 1002339) re-preselects as the owner flips the type question and must not refetch to do it; R17's rule engine reads the same shape so a recommendation and a preselect can never name different modules. A DECLARATION, not a query — modules/provisioning/starter-bundles.js is pure, so this route touches no database and no per-request state. rank: public — the signed-out hub wizard (/projects?view=new) reads it while choosing a type, exactly like onboard-plan above; no write, no secret, and the module catalog it names is already public at GET /public/modules. `optional` joined the payload for R20's picker (task 1002339): the opt-in catalog with the owner-facing label + one-line summary each row renders. It lives here rather than at a module-catalog route because the two things a picker needs — what is preselected and what may be chosen — have to arrive in ONE read of ONE declaration, or the panel can preselect a key it has no row for. The alternatives were both worse: `GET /modules` needs a session (this wizard is read signed-out) and `GET /public/modules` serves booleans, not copy. Additive — `core` and `bundles` keep their shape.\n\n**Rank:** `public` — No authentication — any caller.",
12017
+ "description": "GET /provisioning/starter-bundles — the curated module preset for each declared project type (BV1.R19, task 1002338; goal 1000046 criterion `proj-module-select`). Returns the always-on core once, then one resolved bundle per type: `{ type, label, summary, core, bundle, modules }`. The whole table in one read, because the creation picker (R20, task 1002339) re-preselects as the owner flips the type question and must not refetch to do it; R17's rule engine reads the same shape so a recommendation and a preselect can never name different modules. A DECLARATION, not a query — modules/provisioning/starter-bundles.js is pure, so this route touches no database and no per-request state. rank: public — the signed-out hub wizard (/projects?view=new) reads it while choosing a type, exactly like onboard-plan above; no write, no secret, and the module catalog it names is already public at GET /public/modules. `optional` joined the payload for R20's picker (task 1002339): the opt-in catalog with the owner-facing label + one-line summary each row renders. It lives here rather than at a module-catalog route because the two things a picker needs — what is preselected and what may be chosen — have to arrive in ONE read of ONE declaration, or the panel can preselect a key it has no row for. The alternatives were both worse: `GET /modules` needs a session (this wizard is read signed-out) and `GET /public/modules` serves booleans, not copy. Additive — `core` and `bundles` keep their shape. `byTeamShape` re-resolves each bundle for every answer the About step can give, riding THIS payload rather than a second read (task 1002341; ADR 0243 §4).\n\n**Rank:** `public` — No authentication — any caller.",
12018
12018
  "x-rank": "public",
12019
12019
  "x-source": "modules/provisioning/routes/provisioning.js",
12020
12020
  "responses": {
@@ -18250,14 +18250,16 @@
18250
18250
  "description": {},
18251
18251
  "detail": {},
18252
18252
  "options": {},
18253
- "publish": {}
18253
+ "publish": {},
18254
+ "modules": {}
18254
18255
  },
18255
18256
  "required": [
18256
18257
  "ok",
18257
18258
  "description",
18258
18259
  "detail",
18259
18260
  "options",
18260
- "publish"
18261
+ "publish",
18262
+ "modules"
18261
18263
  ]
18262
18264
  },
18263
18265
  "PatchProvisioningInstancesIdSettingsResponse": {