@bongos/core 1.19.527 → 1.19.529
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 +37 -27
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0243-a-bundle-adjustment-belongs-to-the-bundle-not-the-advice-about-it.md +159 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +5 -3
- package/docs/copy-inventory.md +219 -219
- package/docs/copy-registry.json +282 -282
- package/docs/module-api-changelog.md +4 -0
- package/modules/grading/grader-subagent.js +20 -1
- package/modules/grading/grader.js +24 -1
- package/modules/lifecycle/cascade.js +86 -4
- package/modules/lifecycle/tests/cascade.mjs +63 -6
- package/modules/provisioning/recommendations.js +18 -57
- package/modules/provisioning/routes/provisioning.js +6 -2
- package/modules/provisioning/starter-bundles.js +143 -4
- package/modules/public-landing/public/projects.html +55 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/grader_root_outage.mjs +109 -0
- package/tests/projects_hub_module_picker.mjs +217 -4
- package/tests/provisioning_starter_bundles.mjs +90 -1
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.529",
|
|
6
|
+
"core_contract": "1.19.529",
|
|
7
|
+
"source_commit": "80d814daa7174fe0e760966095ee9339a29d7d1d",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-04T13:17:35.687Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 435,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1976,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2435,
|
|
20
|
+
"tree_sha256": "a029bcb3e7adddef9f33b68cd23f96158ebb345a0b1a60e28dc8f2553809d595",
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
1770
|
+
"sha256": "42f1210b3c422d2b05b719c38eabfb3ed25ed31988a757394dfc2caed045ee36"
|
|
1766
1771
|
},
|
|
1767
1772
|
{
|
|
1768
1773
|
"path": "docs/copy-registry.json",
|
|
1769
1774
|
"mode": "0000644",
|
|
1770
|
-
"sha256": "
|
|
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": "
|
|
2620
|
+
"sha256": "8a6ba922b7918d974e9374510472ce2a67c28790e60875b36efe7ceccda84e2f"
|
|
2616
2621
|
},
|
|
2617
2622
|
{
|
|
2618
2623
|
"path": "docs/modules-contract.md",
|
|
@@ -4417,7 +4422,7 @@
|
|
|
4417
4422
|
{
|
|
4418
4423
|
"path": "modules/grading/grader-subagent.js",
|
|
4419
4424
|
"mode": "0000644",
|
|
4420
|
-
"sha256": "
|
|
4425
|
+
"sha256": "8a100cca2391e018cf257e178c253cb21f514a07abda1a8f07da754b76aa83ad"
|
|
4421
4426
|
},
|
|
4422
4427
|
{
|
|
4423
4428
|
"path": "modules/grading/grader-workers/efficiency.js",
|
|
@@ -4472,7 +4477,7 @@
|
|
|
4472
4477
|
{
|
|
4473
4478
|
"path": "modules/grading/grader.js",
|
|
4474
4479
|
"mode": "0000644",
|
|
4475
|
-
"sha256": "
|
|
4480
|
+
"sha256": "4c664f5080513169c35369e5406f32efa64ce24259fefc47f527a94ff8001b1f"
|
|
4476
4481
|
},
|
|
4477
4482
|
{
|
|
4478
4483
|
"path": "modules/grading/module.json",
|
|
@@ -5257,7 +5262,7 @@
|
|
|
5257
5262
|
{
|
|
5258
5263
|
"path": "modules/lifecycle/cascade.js",
|
|
5259
5264
|
"mode": "0000644",
|
|
5260
|
-
"sha256": "
|
|
5265
|
+
"sha256": "2192c61c606a37b4782db0646242ac969bcd318d4305b1247adf028b14f2e484"
|
|
5261
5266
|
},
|
|
5262
5267
|
{
|
|
5263
5268
|
"path": "modules/lifecycle/claim-eligibility.js",
|
|
@@ -5627,7 +5632,7 @@
|
|
|
5627
5632
|
{
|
|
5628
5633
|
"path": "modules/lifecycle/tests/cascade.mjs",
|
|
5629
5634
|
"mode": "0000644",
|
|
5630
|
-
"sha256": "
|
|
5635
|
+
"sha256": "af00ee4629002c8df2448e2146a24108407a36b062859ee1afee293aa60cb209"
|
|
5631
5636
|
},
|
|
5632
5637
|
{
|
|
5633
5638
|
"path": "modules/lifecycle/version-close.js",
|
|
@@ -6057,17 +6062,17 @@
|
|
|
6057
6062
|
{
|
|
6058
6063
|
"path": "modules/provisioning/recommendations.js",
|
|
6059
6064
|
"mode": "0000644",
|
|
6060
|
-
"sha256": "
|
|
6065
|
+
"sha256": "4c933aad1307378496d47960e24c9d797a1f122f28aefa27b769f336e4cf2e75"
|
|
6061
6066
|
},
|
|
6062
6067
|
{
|
|
6063
6068
|
"path": "modules/provisioning/routes/provisioning.js",
|
|
6064
6069
|
"mode": "0000644",
|
|
6065
|
-
"sha256": "
|
|
6070
|
+
"sha256": "69ec9994c833b54466fa0e83a3aa2c09eada6b0aefadf73e6eb50b085996aaa6"
|
|
6066
6071
|
},
|
|
6067
6072
|
{
|
|
6068
6073
|
"path": "modules/provisioning/starter-bundles.js",
|
|
6069
6074
|
"mode": "0000644",
|
|
6070
|
-
"sha256": "
|
|
6075
|
+
"sha256": "78cdbb14f2dc911d78d1dd7e1ae73027734e94a3bbd8826b2f27e343a2daba1d"
|
|
6071
6076
|
},
|
|
6072
6077
|
{
|
|
6073
6078
|
"path": "modules/provisioning/tests/liveness.mjs",
|
|
@@ -6577,7 +6582,7 @@
|
|
|
6577
6582
|
{
|
|
6578
6583
|
"path": "modules/public-landing/public/projects.html",
|
|
6579
6584
|
"mode": "0000644",
|
|
6580
|
-
"sha256": "
|
|
6585
|
+
"sha256": "7147ff51b2eaa3487557fff35f4fc7c3e4f6164fd8054e41c76064bcfe932e08"
|
|
6581
6586
|
},
|
|
6582
6587
|
{
|
|
6583
6588
|
"path": "modules/public-landing/public/projects.probes.json",
|
|
@@ -7322,12 +7327,12 @@
|
|
|
7322
7327
|
{
|
|
7323
7328
|
"path": "package-lock.json",
|
|
7324
7329
|
"mode": "0000644",
|
|
7325
|
-
"sha256": "
|
|
7330
|
+
"sha256": "43ac8c190119cd19cbd7884c854dd616d4dce8947f9435367acbf04c3058d7b1"
|
|
7326
7331
|
},
|
|
7327
7332
|
{
|
|
7328
7333
|
"path": "package.json",
|
|
7329
7334
|
"mode": "0000644",
|
|
7330
|
-
"sha256": "
|
|
7335
|
+
"sha256": "9baba915b700faa5eda5733b68bffd4aaeaa54b5a3076502440c39a944944f3d"
|
|
7331
7336
|
},
|
|
7332
7337
|
{
|
|
7333
7338
|
"path": "public-docs/index.html",
|
|
@@ -9007,7 +9012,7 @@
|
|
|
9007
9012
|
{
|
|
9008
9013
|
"path": "src/module-api.js",
|
|
9009
9014
|
"mode": "0000644",
|
|
9010
|
-
"sha256": "
|
|
9015
|
+
"sha256": "532e7d3dc98b5ff136f1cb7484232b17f2f3c750dad15315493224b2619ef78f"
|
|
9011
9016
|
},
|
|
9012
9017
|
{
|
|
9013
9018
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10389,6 +10394,11 @@
|
|
|
10389
10394
|
"mode": "0000644",
|
|
10390
10395
|
"sha256": "50a785eb6e2c71d5a043c2ec2c37a835777da6397dac3ffc2542acdde14b57a8"
|
|
10391
10396
|
},
|
|
10397
|
+
{
|
|
10398
|
+
"path": "tests/grader_root_outage.mjs",
|
|
10399
|
+
"mode": "0000644",
|
|
10400
|
+
"sha256": "409b21de4ac729cf90c792be28fcc358349e80dca67e47255a0105377d2cfcf8"
|
|
10401
|
+
},
|
|
10392
10402
|
{
|
|
10393
10403
|
"path": "tests/grader_severity_gate.mjs",
|
|
10394
10404
|
"mode": "0000644",
|
|
@@ -11232,7 +11242,7 @@
|
|
|
11232
11242
|
{
|
|
11233
11243
|
"path": "tests/projects_hub_module_picker.mjs",
|
|
11234
11244
|
"mode": "0000644",
|
|
11235
|
-
"sha256": "
|
|
11245
|
+
"sha256": "55b7912d1985ea1e02874b479c9d0453c25df48bfad6287ba9402df50326d6ed"
|
|
11236
11246
|
},
|
|
11237
11247
|
{
|
|
11238
11248
|
"path": "tests/projects_hub_pre_uat.mjs",
|
|
@@ -11322,7 +11332,7 @@
|
|
|
11322
11332
|
{
|
|
11323
11333
|
"path": "tests/provisioning_starter_bundles.mjs",
|
|
11324
11334
|
"mode": "0000644",
|
|
11325
|
-
"sha256": "
|
|
11335
|
+
"sha256": "b63751ddf17d58ec3fdfdbd19c9f9a7569eed1ca9ff157a2a37f7034151eead7"
|
|
11326
11336
|
},
|
|
11327
11337
|
{
|
|
11328
11338
|
"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.
|
package/docs/adr/README.md
CHANGED
|
@@ -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.
|
package/docs/api/openapi.json
CHANGED
|
@@ -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": {
|