@bongos/core 1.19.515 → 1.19.516

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.515",
6
- "core_contract": "1.19.515",
7
- "source_commit": "483cf263844746837e2991c9007345d34eeda8d4",
5
+ "core_version": "1.19.516",
6
+ "core_contract": "1.19.516",
7
+ "source_commit": "b92f5957bf6d2995db7633b965ea029192fb05df",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-03T19:41:10.659Z",
9
+ "built_at": "2026-09-03T19:52:58.155Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 430,
12
+ "docs_redacted": 431,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 1963,
14
+ "functional_verbatim": 1966,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2417,
20
- "tree_sha256": "cdb2ede6764acb200fd89923d7065ef40c7f932f11b0f08ded4b85128eaa7424",
19
+ "file_count": 2421,
20
+ "tree_sha256": "d9225673c69711642ce6ca8ae5ec201351c431974565132d756573f715a2c62e",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -382,7 +382,7 @@
382
382
  {
383
383
  "path": "clients/bongos-client/index.d.ts",
384
384
  "mode": "0000644",
385
- "sha256": "c89abc123754c7b8465f01fcb00ea06ad55e4d6b4f1260f818a7a515e101ef2a"
385
+ "sha256": "2f810adf74673288b2c564c1624f06c5c320e4fe050af55b0cc8bc671ec10143"
386
386
  },
387
387
  {
388
388
  "path": "clients/bongos-client/index.mjs",
@@ -1704,20 +1704,25 @@
1704
1704
  "mode": "0000644",
1705
1705
  "sha256": "42b20cd6d2e8a8d76c980df73087fd404487e4c479034dbe34d8297677bd9387"
1706
1706
  },
1707
+ {
1708
+ "path": "docs/adr/0240-a-skipped-module-picker-stores-nothing-and-resolves-the-bundle-on-read.md",
1709
+ "mode": "0000644",
1710
+ "sha256": "d8bffd4c66cfddfeadd3c734d029e357f05a8eb804a79e5c3cad4014c9417b56"
1711
+ },
1707
1712
  {
1708
1713
  "path": "docs/adr/README.md",
1709
1714
  "mode": "0000644",
1710
- "sha256": "cef542ef427dd81632465f17c6343c6b27f3ab69436eba2130c4fa4b5c9bc4ae"
1715
+ "sha256": "e3bb2c282df45ec41150fd041df3c2525d653184858c2bb0a6c7fff670f4b85c"
1711
1716
  },
1712
1717
  {
1713
1718
  "path": "docs/api-reference.md",
1714
1719
  "mode": "0000644",
1715
- "sha256": "227b43efe3e504cf227f006681a5613e5cb0d4170930f6d979befabc5aeb70c3"
1720
+ "sha256": "b137673ee16cd6f7d32b0b24d12b6c8e844a2e6e415a8118a4d458c382e5724f"
1716
1721
  },
1717
1722
  {
1718
1723
  "path": "docs/api/openapi.json",
1719
1724
  "mode": "0000644",
1720
- "sha256": "6705058ce073c42f2ea87d976c5bbaa124b96ab4231cf825eb5f63adda1dab63"
1725
+ "sha256": "fd2bf9ee3b86b31bff72933594b984324c4387b36a369a6cbb3998389aaa2bcf"
1721
1726
  },
1722
1727
  {
1723
1728
  "path": "docs/architecture.md",
@@ -1742,12 +1747,12 @@
1742
1747
  {
1743
1748
  "path": "docs/copy-inventory.md",
1744
1749
  "mode": "0000644",
1745
- "sha256": "0cc8ccfda8d8e3e7b518fae7a7584bb368d7d2eac6703518404d30f0b6ead30d"
1750
+ "sha256": "e6aba956f5d6f056788f8d43f52dfcf4e242a9dff3bc348f312c875b4cd04be2"
1746
1751
  },
1747
1752
  {
1748
1753
  "path": "docs/copy-registry.json",
1749
1754
  "mode": "0000644",
1750
- "sha256": "1003ea51740a5f71af49e05c66ba088c337185680dd92a391c959e8ea76a3f40"
1755
+ "sha256": "5d0fca4e1e8b63e3679fe656ad773e914f944fa93fac66defeb46bb3642a6d74"
1751
1756
  },
1752
1757
  {
1753
1758
  "path": "docs/design/apex-pass-2-direction.md",
@@ -2592,7 +2597,7 @@
2592
2597
  {
2593
2598
  "path": "docs/module-api-changelog.md",
2594
2599
  "mode": "0000644",
2595
- "sha256": "db7479f4b4df5a83c714d5ff698c358f5219efa9d8a5f1d01c09fac561e7ae9a"
2600
+ "sha256": "98e8b564cdaccd042553e8337d0d67222911bdf7958f02e168f1b200da2183a9"
2596
2601
  },
2597
2602
  {
2598
2603
  "path": "docs/modules-contract.md",
@@ -5989,6 +5994,11 @@
5989
5994
  "mode": "0000644",
5990
5995
  "sha256": "7abe41633cd7f98712c39960fba7175f0bb3340c4568bfbfdc640af911a31e15"
5991
5996
  },
5997
+ {
5998
+ "path": "modules/provisioning/migrations/provisioning_014_module_selection.sql",
5999
+ "mode": "0000644",
6000
+ "sha256": "f4562bd90fdae26c5f7c51851bd2ad7752535bd0dcfb62dd567c11e76330c42b"
6001
+ },
5992
6002
  {
5993
6003
  "path": "modules/provisioning/module.json",
5994
6004
  "mode": "0000644",
@@ -6007,7 +6017,7 @@
6007
6017
  {
6008
6018
  "path": "modules/provisioning/provisioning.js",
6009
6019
  "mode": "0000644",
6010
- "sha256": "67eb2963688cba73fe846b354070e1e43333971425f603b1e84b45552b5381f0"
6020
+ "sha256": "8745258599d655b34204e5878b413308ae48a012b0306bde3297e98e452b3686"
6011
6021
  },
6012
6022
  {
6013
6023
  "path": "modules/provisioning/rate-limit.js",
@@ -6022,12 +6032,12 @@
6022
6032
  {
6023
6033
  "path": "modules/provisioning/routes/provisioning.js",
6024
6034
  "mode": "0000644",
6025
- "sha256": "9285f3f2d426305c060eef4b3b98ae64fc386e4ce487f99d9ad59d9921dc493e"
6035
+ "sha256": "6e78e87a2112974db6db804280c346daa7c38684adace638e3c5734fe6d1ca72"
6026
6036
  },
6027
6037
  {
6028
6038
  "path": "modules/provisioning/starter-bundles.js",
6029
6039
  "mode": "0000644",
6030
- "sha256": "760382a3e361692f9d8457f88122fda498eb6bccecd37b351837fa5c4fa5e6f4"
6040
+ "sha256": "9dd1fe5bd16d22245d62dae05aefb0c74c82772a937da155f1e8d2403f911886"
6031
6041
  },
6032
6042
  {
6033
6043
  "path": "modules/provisioning/tests/liveness.mjs",
@@ -6277,7 +6287,7 @@
6277
6287
  {
6278
6288
  "path": "modules/public-landing/public/assets/cosmos.css",
6279
6289
  "mode": "0000644",
6280
- "sha256": "bb2fa6d0a1c87aeb3e08038faf1d3ae0f06a1a41f9fe6d83f357146bc7e73045"
6290
+ "sha256": "f10975893b3522accc6592246af0b07645c8d910c7637ecc5a006064a0988057"
6281
6291
  },
6282
6292
  {
6283
6293
  "path": "modules/public-landing/public/assets/verro-medallion.png",
@@ -6537,17 +6547,17 @@
6537
6547
  {
6538
6548
  "path": "modules/public-landing/public/projects.html",
6539
6549
  "mode": "0000644",
6540
- "sha256": "8af95785b813eb27e6356fa717a4f20cb73f1220dd9ce15de2d62a18631afdd3"
6550
+ "sha256": "a93dbf381a39bde8d4424bd3f241d24a72d9ed915e7da48bdb125d15085f6fef"
6541
6551
  },
6542
6552
  {
6543
6553
  "path": "modules/public-landing/public/projects.probes.json",
6544
6554
  "mode": "0000644",
6545
- "sha256": "856e0a8a9c4f8b19a05bf1e5e0c0f848a94038e933fa10bbbf35529edbaa9ae9"
6555
+ "sha256": "5801951d08323e2f59aa4e9435e0a7d82c712714a6d1c925768dcfd2bd9a6525"
6546
6556
  },
6547
6557
  {
6548
6558
  "path": "modules/public-landing/public/projects.states.json",
6549
6559
  "mode": "0000644",
6550
- "sha256": "dfe69ecaa3923669bd0c07d440886410e4f67c843e13a1fac4e7b2a1429c3ba3"
6560
+ "sha256": "16fbd27a36c9c32696539c74a815dc9b3f187f5b539b393e3bee7f7dabc4e2a6"
6551
6561
  },
6552
6562
  {
6553
6563
  "path": "modules/public-landing/public/terms.html",
@@ -6972,7 +6982,7 @@
6972
6982
  {
6973
6983
  "path": "modules/ui-design/kit/serve.js",
6974
6984
  "mode": "0000644",
6975
- "sha256": "efb50e76596a5ad009dab58d8e12b590382cc6bf1f819d0ab447cb14a4b4d6a3"
6985
+ "sha256": "4c52462cf61559e9a424107398eca58aa56b3783338827dda6110200fcdfd00a"
6976
6986
  },
6977
6987
  {
6978
6988
  "path": "modules/ui-design/kit/tells.js",
@@ -7282,12 +7292,12 @@
7282
7292
  {
7283
7293
  "path": "package-lock.json",
7284
7294
  "mode": "0000644",
7285
- "sha256": "b8d0e916f8a2507104f19cacd5555124661f47777731eba74b6b9fda82a4fb90"
7295
+ "sha256": "d137b5fec77d066d3e93b56a0af042e80252513dfa759872ae352a99b92170f6"
7286
7296
  },
7287
7297
  {
7288
7298
  "path": "package.json",
7289
7299
  "mode": "0000644",
7290
- "sha256": "0e8413c67527b820811f15b66d4f36ec0f3d3ed573378b7efd5fff4d78bb3400"
7300
+ "sha256": "a212fc90527071642d6693ca0ea6dae91c8ad9c9452a8c19d550f2bf452d1e92"
7291
7301
  },
7292
7302
  {
7293
7303
  "path": "public-docs/index.html",
@@ -8957,7 +8967,7 @@
8957
8967
  {
8958
8968
  "path": "src/module-api.js",
8959
8969
  "mode": "0000644",
8960
- "sha256": "2811b2c8c238aae322342e37baaa5a6fc2fe629a87d7b2978cb8d92aa0d0ea75"
8970
+ "sha256": "ec17c796e1498bc12b3ecce4d194f7a4026081373d7d209fbfb49343368b5498"
8961
8971
  },
8962
8972
  {
8963
8973
  "path": "src/module-loader/catalog.js",
@@ -11167,7 +11177,12 @@
11167
11177
  {
11168
11178
  "path": "tests/projects_hub.mjs",
11169
11179
  "mode": "0000644",
11170
- "sha256": "22461d4febf616202af96aa37fc940a686841bbf68155094526455ae85ab9886"
11180
+ "sha256": "f8e557372a86d42dc0433414a1d5821a350dc545056fcc8104aa151b51e939f7"
11181
+ },
11182
+ {
11183
+ "path": "tests/projects_hub_module_picker.mjs",
11184
+ "mode": "0000644",
11185
+ "sha256": "5eaa61295f2a22f5b10cb365cee58e485390ccb763a14be15fec00f23aa23087"
11171
11186
  },
11172
11187
  {
11173
11188
  "path": "tests/projects_hub_pre_uat.mjs",
@@ -11214,6 +11229,11 @@
11214
11229
  "mode": "0000644",
11215
11230
  "sha256": "dd53361cf8bd7e517e886701753d64c05c3235995785991558805f493cde529c"
11216
11231
  },
11232
+ {
11233
+ "path": "tests/provisioning_module_selection.mjs",
11234
+ "mode": "0000644",
11235
+ "sha256": "48fc7c59efae4b7d1b9ab565882473483d7b4d8a40635583a789d0749da16578"
11236
+ },
11217
11237
  {
11218
11238
  "path": "tests/provisioning_onboard_mode.mjs",
11219
11239
  "mode": "0000644",
@@ -11252,7 +11272,7 @@
11252
11272
  {
11253
11273
  "path": "tests/provisioning_starter_bundles.mjs",
11254
11274
  "mode": "0000644",
11255
- "sha256": "0d3dadff4425e8d014cca29c66c31ff094e78ffbf2fd246f56ba11c5e1cefc93"
11275
+ "sha256": "9ecf7cda8e47a6e17bbc73afdb0c30c867ab967cea66943082cbe15502fd63bc"
11256
11276
  },
11257
11277
  {
11258
11278
  "path": "tests/provisioning_teardown_intent.mjs",
@@ -100,7 +100,7 @@ export interface GetProvisioningInstancesResponse { instances: unknown }
100
100
  export interface GetProvisioningOnboardPlanResponse { mode: unknown; steps: unknown }
101
101
  export interface GetProvisioningRecommendationsResponse { recommendations: unknown }
102
102
  export interface GetProvisioningSlugAvailableResponse { slug: unknown; available: unknown; reason: unknown; address: unknown }
103
- export interface GetProvisioningStarterBundlesResponse { core: unknown; bundles: unknown }
103
+ export interface GetProvisioningStarterBundlesResponse { core: unknown; bundles: unknown; optional: unknown }
104
104
  export interface GetPublicBlockersIdResponse { blocker: unknown }
105
105
  export interface GetPublicBountyTableResponse { table: unknown; currency: unknown }
106
106
  export interface GetPublicGradesResponse { window_days: unknown; rolling: unknown; trend: unknown; attempts: unknown; threshold: unknown; grader_version: unknown }
@@ -329,7 +329,7 @@ export interface PostProvisioningInstancesIdGithubAppRequest { account?: string
329
329
  export interface PostProvisioningInstancesIdGithubAppResponse { state: unknown; account: unknown; post_url: unknown; personal_url: unknown; org_url: unknown; org_login: unknown; manifest: unknown; redirect_url: unknown; expires_at: unknown }
330
330
  export interface PostProvisioningInstancesIdRestartResponse { ok: boolean; queued: unknown; action: unknown; status: unknown; message: unknown }
331
331
  export interface PostProvisioningInstancesIdTeardownResponse { ok: boolean; queued: unknown; action: unknown; status: unknown; superseded: unknown; message: unknown }
332
- export interface PostProvisioningInstancesRequest { slug: string; target_ref?: string; hosting_shape?: string; domain?: string; no_address?: boolean; tier?: string; onboard_mode?: string; type?: string; description?: string; detail?: Record<string, unknown> }
332
+ export interface PostProvisioningInstancesRequest { slug: string; target_ref?: string; hosting_shape?: string; domain?: string; no_address?: boolean; tier?: string; onboard_mode?: string; type?: string; description?: string; detail?: Record<string, unknown>; modules?: string[] }
333
333
  export interface PostSearchMarkRequest { chunk_id?: StringifiableId; useful?: unknown }
334
334
  export interface PostSearchRequest { expand?: unknown; q?: string; source_kind?: string; limit?: unknown }
335
335
  export interface PostSearchResponse { expand?: unknown; chunk?: unknown; query?: unknown; count?: unknown; total_full_tokens?: unknown; results?: unknown; corpus?: unknown }
@@ -0,0 +1,157 @@
1
+ # 0240 — A skipped module picker stores nothing, and the bundle is resolved on read
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-09-03
5
+ - **Tasks:** [#1002339](https://cloudbongos.com/builders#/task/1002339) (this decision — BV1.R20)
6
+ - **Goal:** [#1000046](https://cloudbongos.com/builders#/goal/1000046) — Project creation: visibility and
7
+ customization, criterion `proj-module-select` ("during setup a default set of modules is pre-selected…")
8
+ - **Related:** [ADR 0237](<redacted>.md)
9
+ (R19 — the bundles this picker renders, and the file that closed with "where a chosen selection lands
10
+ is R20's decision"), [ADR 0182](<redacted>.md) D4 (the publish bar a module set
11
+ may not join), [ADR 0083](<redacted>.md) (the one-way import rule),
12
+ [ADR 0111](<redacted>.md) (the enqueue-only web tier), migration
13
+ `provisioning_013` (the `detail` column this deliberately does not use)
14
+
15
+ ## 1. The problem
16
+
17
+ R19 declared the starter bundles and served them; nothing rendered them. R20 is the
18
+ picker: the owner sees what their declared project type preselects, switches
19
+ individual modules on or off, and the answer has to survive into the project.
20
+
21
+ Three questions had no answer yet, and the first is the one everything else hangs
22
+ off:
23
+
24
+ 1. **Where does a chosen selection live?** ADR 0237 §6: *"Nothing is stored yet…
25
+ there is still no module-selection column on any table."*
26
+ 2. **What does *skipping* mean?** The picker is optional like the About step
27
+ before it. "Skip = you get the preselect" is easy to say and easy to get
28
+ subtly wrong.
29
+ 3. **Where does the picker learn what may be chosen?** A bundle names module
30
+ keys; a picker has to render a name and a sentence per key, for an owner who
31
+ has met none of them.
32
+
33
+ ## 2. Three-valued, or the skip is unreadable forever
34
+
35
+ A new nullable `modules jsonb` column on `provisioning_instances`
36
+ (`provisioning_014`), read through `effectiveModules(row)`:
37
+
38
+ | stored | means | what the project gets |
39
+ |---|---|---|
40
+ | `NULL` | nobody answered — the picker was skipped, or the caller does not ask | the declared type's starter bundle, resolved at read time |
41
+ | `[]` | the owner switched them all off | the always-on core, and nothing else |
42
+ | `["dev-box",…]` | what they picked | exactly that |
43
+
44
+ The temptation was `NOT NULL DEFAULT '[]'`, and it is the mistake: it collapses
45
+ rows 1 and 2, so a project that skipped and a project that deliberately declined
46
+ everything become the same row and no later surface can tell them apart. That is
47
+ the rule `PROJECT_DETAIL_VOCAB` already states out loud — *"a detail answer is
48
+ something the owner SAID… Absent stays absent, and skipped stays skipped"* — and
49
+ it applies here for the same reason: R23 ("add modules later") and R24 (the proof
50
+ walk) both need to know whether an owner has ever been asked.
51
+
52
+ **Skipping is safe because the preselect and the fallback are the same
53
+ declaration.** The panel preselects `bundleForProjectType(type).bundle`; an
54
+ omitted `modules` key makes the server resolve `bundleForProjectType(type).bundle`.
55
+ Not two lists kept in agreement — one function, called twice. That is what lets
56
+ the panel promise "skip and you get the suggested set" without the promise being
57
+ a thing anyone has to maintain.
58
+
59
+ It also means **`bongos onboard` and any API caller inherit the bundle for free**.
60
+ Neither asks the question, both omit the field, and both get the type's preselect.
61
+ So the canonical onboarding sequence (`onboard-plan.js`) grows **no new step** —
62
+ the picker is a refinement of an answer already in the sequence (`type`), not a
63
+ new question in it. `tests/onboard.mjs` says one optional step keeps that flag
64
+ meaningful; it still does.
65
+
66
+ ## 3. Not `detail`, not `settings`
67
+
68
+ `provisioning_013`'s own comment named R19 as a reason `detail` was jsonb, so
69
+ `detail.modules` was the obvious home. It is the wrong one:
70
+
71
+ - `detail` holds the **interview's** answers under `PROJECT_DETAIL_VOCAB`, a
72
+ closed map of scalar vocabularies that `normalizeProjectDetail` filters
73
+ key-by-value and `badProjectDetail` 400s on. A list of module keys is not that
74
+ shape. Carrying it there means teaching a normalizer, a route guard and a read
75
+ projection about a value type none of them have — in order to store something
76
+ ADR 0237 §2 already ruled the interview must not ask.
77
+ - `settings` is worse: a setting is a **policy the running instance obeys**. It
78
+ rides `web.env`, queues a `settings-apply` intent and restarts the service.
79
+ A module selection is a declaration made before the instance exists.
80
+
81
+ A column is the honest shape, and it is additive + namespaced (ADR 0083 §5), so
82
+ the previous release still runs against the schema and every pre-existing row
83
+ reads as its bundle rather than as an error.
84
+
85
+ ## 4. The picker's catalog is declared beside the bundles
86
+
87
+ The picker needs, per opt-in module, a label and one owner-facing line. Neither
88
+ existing source can give it:
89
+
90
+ - `module.json`'s `title`/`description` are written for a builder reading a
91
+ module list. *"Outbound ship broadcast + the inbound #ideas/#bugs bot +
92
+ role-sync + channel management + account-link state. Mirror only, never
93
+ authority (ADR 0033)"* is true, and it is not copy you put in front of someone
94
+ who has not built anything yet.
95
+ - `GET /modules` needs a session and imports the live catalog; the create wizard
96
+ is read **signed out**, and a module may not import `src/module-loader/catalog.js`
97
+ (ADR 0083's one-way rule).
98
+ - `GET /public/modules` serves booleans, not copy.
99
+
100
+ So `OPTIONAL_MODULES` joins `STARTER_BUNDLES` in `starter-bundles.js` — the same
101
+ decision ADR 0237 §3 made for the bundle keys, extended to the choices, and kept
102
+ honest the same way: `tests/provisioning_starter_bundles.mjs` reads every
103
+ `modules/*/module.json` and fails the build if the declared opt-in set drifts from
104
+ the on-disk `default: false` set (minus the two hub-only modules). **A new opt-in
105
+ module therefore cannot ship without owner-facing copy**, which is the property
106
+ worth buying.
107
+
108
+ `GET /provisioning/starter-bundles` gains `optional` alongside `core` and
109
+ `bundles`. **One read, one declaration**: a picker that fetched its preselect and
110
+ its catalog separately could preselect a key it has no row for.
111
+
112
+ ## 5. Loud at the boundary, quiet at the store
113
+
114
+ `normalizeModuleSelection` drops an unknown, always-on or hub-only key on the way
115
+ to storage. The **route refuses it** (`400 bad_modules`, naming the choices)
116
+ rather than dropping it silently — `badProjectDetail`'s rule, for `badProjectDetail`'s
117
+ reason: a caller who names a module that does not exist has made a mistake, and
118
+ being told nothing and handed a project without it is the silent-drop failure the
119
+ strict validator exists to prevent. A re-request reports `modules_ignored`, the
120
+ same honesty `type_ignored` carries.
121
+
122
+ ## 6. Consequences
123
+
124
+ - **The picker cannot block a create.** A failed catalog fetch renders a
125
+ "couldn't load" clause, leaves the answer unsaid, and the create still files —
126
+ the server applies the bundle. A picker that could stop a project from existing
127
+ would be worse than no picker.
128
+ - **A module selection is still not a publish precondition** (ADR 0182 D4,
129
+ restated by ADR 0237 §6). Taking or declining a bundle cannot move a project's
130
+ dark-matter state.
131
+ - **The column is a DECLARATION, not yet an input to the runner.** It is held on
132
+ the platform row exactly as `type` and `description` are, until a catalog row
133
+ materializes. `provisioning_intents` carries no payload, and turning a stored
134
+ selection into the child instance's own `config/modules.json` is separate work —
135
+ named here so nobody reads the stored array as already-provisioned.
136
+ - **The wizard renumbered a third time** (gate 0 · name 1 · type 2 · about 3 ·
137
+ **modules 4** · home 5 · address 6 · review 7 · done 8), so `DRAFT_KEY` moved to
138
+ `-v4`: a draft replayed under the old numbering lands on the wrong panel.
139
+
140
+ ## 7. Rejected
141
+
142
+ - **`NOT NULL DEFAULT '[]'`** — collapses "skipped" into "declined everything",
143
+ permanently.
144
+ - **Storing the preselect when the owner skips** — it writes down an answer
145
+ nobody gave, and freezes today's bundle onto a project whose owner never
146
+ expressed a view. Read-time resolution is what `effectiveSettings` already does
147
+ for the same reason.
148
+ - **`detail.modules`** — the interview's vocabulary is scalar by construction, and
149
+ ADR 0237 §2 ruled the interview does not ask this question.
150
+ - **A new step in `onboardPlan`** — the CLI and the skill would have to ask a
151
+ question whose answer they already get by omission. The sequence gains nothing;
152
+ `tests/onboard.mjs`'s "one optional step keeps the flag meaningful" stands.
153
+ - **Rendering the always-on core as sixteen disabled toggles** — a disabled
154
+ control reads as a choice the owner failed to make. The core was never a
155
+ choice, so it is a clause: visible, counted, and outside the band.
156
+ - **Reading the manifests at request time to build the catalog** — costs the
157
+ purity ADR 0237 §3 bought, for copy that is fixed at build time anyway.
@@ -327,5 +327,6 @@ This keeps the decision history honest and traceable.
327
327
  | 0238 | [**The join door is a provisioning SETTINGS key, and the hub column stays the cache** ([task 1002324](https://cloudbongos.com/builders#/task/1002324) · goal 1000046, criterion `proj-visibility-states` — R05). [ADR 0182](<redacted>.md) D5 says R05 "moves host — an instance surface, not a hub one", and on the tree both halves of that sentence pointed at nothing: `platform_identity_projects.visibility` (which R04 shipped) is written by **no route** — every write on `routes/projects.js` is the `project.curate` CURATION gate with no ownership check, `upsertProject` deliberately omits the column, and `owner_github_id` there is documented "advisory attribution (not an authority)" — while "the project's own hall settings" is a page that does not exist (`settings.html` is per-BUILDER; the hall's my-projects page was deleted by task 1003308). **Decision: the door is a key in `provisioning.SETTINGS_VOCAB`** (`visibility ∈ public|private|stealth`, default `public`), set through the existing own-scoped `PATCH /provisioning/instances/:id/settings` and rendered as one more `.mSec` section on the manage view an owner already uses. One line in the vocabulary buys the whole task from machinery already proven: `instanceAccessDenied` (own-scoped, Archon override, **404** not 403 — no existence leak), a validator that IS the vocabulary (so an off-list value 400s and a key can never drift out of the schema), a `provisioning_events` audit row, the `<PREFIX>_VISIBILITY` env the runner patches into `web.env`, and `src/branding.js` mapping it onto `project.visibility` in the public manifest. It is a **POLICY** key, not a platform-drawn one like the planet, for two reasons that outlive today: ADR 0182 D6 makes the door the CEILING `joinability` may only narrow and D1/D5 put admission authority on the instance — a door the instance cannot read cannot be a ceiling — and a key with a DEFAULT belongs in the defaulted vocabulary (`PLANET_VOCAB` keys are nullable because null means "the platform picks", and there is no such thing as the platform picking your join door). Nothing here writes the hub column: it stays ADR 0182 D5's cache, and R08 is the report that will feed it — so today an owner's pick is stored, validated, audited and delivered to their instance, and does not yet move the orb (that gap is R06/R08's, named so the next builder does not hunt a bug). The section deliberately ships **no "what your project runs" read-back line**, unlike its two siblings: a deployed project reports no door until it takes a release carrying the field, and a line that could only ever say "this project's server doesn't report this yet" is worse than none. Rejected: a new owner-scoped write route on the hub catalog (D5 forbids the hub authorising the change); a new instance-side settings page on its own (right long-run home, but today a control with no consumer while R08 is unshipped); a `visibility` COLUMN beside `description` (that one earned a column as half the publish gate and is read by SQL — a closed-vocabulary knob is what the jsonb store exists for); and `PLANET_VOCAB` to dodge the restart (a quieter save today, the ceiling rule lost tomorrow).](<redacted>.md) | provisioning / public-landing |
328
328
  | 0237 | [**A starter bundle is a preset over the always-on core, keyed by the declared project type** ([task 1002338](https://cloudbongos.com/builders#/task/1002338) · goal 1000046, criterion `proj-module-select`). "During setup a default set of modules is pre-selected" can only mean the OPT-IN modules, since every instance already boots with the same sixteen `default: true` core-domain ones — so a bundle is a short list of opt-in module keys layered on that floor, preselected and freely toggled off by R20's picker ([task 1002339](https://cloudbongos.com/builders#/task/1002339)). **The key is the DECLARED type, and the seeded vocabulary is stale:** the task text (2026-07-17) names *Game / SaaS-Tool / Community*, but task [1002772](https://cloudbongos.com/builders#/task/1002772)'s owner decision (2026-08-13) fixed the live closed set as `game · research · business · non-profit · not-sure` — two CHECK constraints, two `PROJECT_TYPES` constants and five wizard buttons — and "community" survives only as a `team_shape`. Two constraints ride the key: the arrow runs **type → modules, never back** (1002772 rejected inferring type from module flags, which flip all through setup), and the R15 interview must **not re-ask** what `type` declares, so R19 adds no `PROJECT_DETAIL_VOCAB` key and no migration. `not-sure` and *undeclared* resolve to the same modules but are deliberately different answers — `type` is nullable precisely so "nobody said" is never back-filled with a guess, and the resolver returns `type: null` with its own copy. **The declaration is pinned by a test, not a runtime catalog read:** a module may not import `src/module-loader/catalog.js` (the one-way rule, [ADR 0083](<redacted>.md)), so `modules/provisioning/starter-bundles.js` stays PURE like `onboard-plan.js` beside it and `tests/provisioning_starter_bundles.mjs` reads every `modules/*/module.json` off disk — failing the build if a bundle names a module that does not exist, if `ALWAYS_ON_CORE` drifts from the manifests' `default: true` set, if a bundle names an always-on module (a preselect that toggles nothing), or if it names one of the two hub-only modules. The bundles are `game → dev-box, discord`; `research → agents`; `business → agents, dev-box`; `non-profit → discord`; `not-sure → —`, and they are SHORT because today's opt-in catalog is three modules wide and a preselect an owner must audit is worse than none. Served whole at public `GET /provisioning/starter-bundles` (`{ core, bundles }`, same absent gate as `onboard-plan`) so the picker re-preselects without a refetch and R17's recommendations read the same shape. A bundle is **never a publish precondition** — [ADR 0182](<redacted>.md) D4 fixes that bar at name + description alone. Rejected: a runtime catalog read, a `bundle` key in the interview vocabulary, inferring the bundle from the description (the deferred AI layer, [task 1003500](https://cloudbongos.com/builders#/task/1003500)), and keeping the seeded Game/SaaS-Tool/Community triple.](<redacted>.md) | provisioning / project creation |
329
329
  | 0239 | [**A recommendation is a table that carries its own reason; governance is a preview and invites are a seam** ([task 1002336](https://cloudbongos.com/builders#/task/1002336) · goal 1000046, criteria `proj-module-select` / `proj-governance-reco` / `proj-invite-builders`). R19 ([ADR 0237](<redacted>.md)) declared the per-type starter bundle and said outright it is "a declaration and not a rule engine — R17/R22 own the layer that reasons"; this is that layer, and the goal pins it **rule-based**, with the AI/embeddings pass deferred ([task 1003500](https://cloudbongos.com/builders#/task/1003500), idea 1000552). **Two inputs, and the free-text description is not one of them:** the DECLARED `type` (a column) and `team_shape` (the one closed-answer key the R15 interview collects, [task 1002334](https://cloudbongos.com/builders#/task/1002334)) — inferring from prose is the deferred layer ADR 0237 §7 already rejected once. The engine is three frozen tables (`TEAM_SHAPE_RULES` add/remove ONE opt-in module; `GOVERNANCE_RULES` name a preset; `INVITE_RULES` emit a seek) and **every row carries its own `reason` sentence**, shipped with the effect rather than reconstructed by whichever surface renders it — a recommendation an owner cannot be told the reason for is one they cannot disagree with. `bundle.adjustments` is a **DELTA**: a rule whose condition matched but whose module the bundle already carried records NOTHING, because "we switched discord on because you said community" over a module a game would have had anyway is a claim the owner can check and find false. **Governance is a named preview, never a setting** — the governance module is planned and unbuilt (goal 1000030), so a preset is a name plus one sentence of what it feels like (`owner-only` / `trusted-team` / `open-contribution`) and every result carries `applied: false`, one flag instead of the same fact hardcoded in R25, R18, the wizard and the project page. **Invites are a SEAM**: `suggested` is always `[]` because ranking builders means querying the leaderboard in the sibling `economy` module, which the one-way rule ([ADR 0083](<redacted>.md)) forbids importing — the engine emits `seeking: { kind: 'top-builders', limit }` (or `null` = suggest nobody, a real answer) and R28 ([task 1002347](https://cloudbongos.com/builders#/task/1002347)) turns it into names. **Skipping is an answer:** every table states a default and each is the NARROWEST option (widening later costs one setting; a role granted to strangers is not recovered by setting it back), an off-vocabulary `team_shape` reads as no answer exactly as `normalizeProjectDetail` drops it, and the route answers 200-with-defaults rather than 400 — refusing would make the supported path ([ADR 0182](<redacted>.md) D4) an error. `tailored: boolean` is the machine-readable half of `proj-detail-interview`'s details-vs-skip split that R18 ([task 1002337](https://cloudbongos.com/builders#/task/1002337)) proves. Served at public `GET /provisioning/recommendations?type=&team_shape=` (same absent gate as `starter-bundles`; no database, pinned by a `pool`-throws stub), returning ADR 0237 §5's bundle shape plus `adjustments` so a recommendation and R20's preselect can never name different modules. Rejected: reading the description, filling `suggested` from `GET /public/leaderboard`, a weighted scoring function, a reason per matched condition, and a 400 on an unknown shape.](<redacted>.md) | provisioning / project creation |
330
+ | 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 |
330
331
 
331
332
  > ⚠️ **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.\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.\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": {
@@ -17317,11 +17317,13 @@
17317
17317
  "type": "object",
17318
17318
  "properties": {
17319
17319
  "core": {},
17320
- "bundles": {}
17320
+ "bundles": {},
17321
+ "optional": {}
17321
17322
  },
17322
17323
  "required": [
17323
17324
  "core",
17324
- "bundles"
17325
+ "bundles",
17326
+ "optional"
17325
17327
  ]
17326
17328
  },
17327
17329
  "GetPublicBlockersIdResponse": {
@@ -20903,6 +20905,13 @@
20903
20905
  },
20904
20906
  "detail": {
20905
20907
  "type": "object"
20908
+ },
20909
+ "modules": {
20910
+ "type": "array",
20911
+ "items": {
20912
+ "type": "string"
20913
+ },
20914
+ "maxItems": 50
20906
20915
  }
20907
20916
  },
20908
20917
  "required": [
@@ -462,7 +462,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
462
462
  | GET | `/api/bongos/provisioning/github-app/status` | `public` | — | GET /provisioning/github-app/status?state=<> — the PUBLIC progress read the onboarding callback page polls (task 2096) to know when setup… |
463
463
  | GET | `/api/bongos/provisioning/health` | `public` | — | GET /provisioning/health — liveness + the core version this module mounted against. |
464
464
  | GET | `/api/bongos/provisioning/instances` | `any-builder` | — | GET /provisioning/instances — the caller's own instances + each open intent. |
465
- | POST | `/api/bongos/provisioning/instances` | `any-builder` | `slug`, `target_ref`, `hosting_shape`, `domain`, `no_address`, `tier`, `onboard_mode`, `type`, `description`, `detail` | POST /provisioning/instances — request a new instance. |
465
+ | POST | `/api/bongos/provisioning/instances` | `any-builder` | `slug`, `target_ref`, `hosting_shape`, `domain`, `no_address`, `tier`, `onboard_mode`, `type`, `description`, `detail`, `modules` | POST /provisioning/instances — request a new instance. |
466
466
  | GET | `/api/bongos/provisioning/instances/:id` | `any-builder` | — | GET /provisioning/instances/:id — read one instance (the manage page's read). |
467
467
  | PATCH | `/api/bongos/provisioning/instances/:id/detail` | `any-builder` | `description`, `detail` | ── The optional project detail (task 1002334, goal 1000046) ────────────── PATCH /provisioning/instances/:id/detail — answer (or re-answe… |
468
468
  | POST | `/api/bongos/provisioning/instances/:id/domain` | `any-builder` | `domain` | POST /provisioning/instances/:id/domain — give the caller's OWN instance its address (task 1002692, gap 5: no path existed to add a domai… |