@bongos/core 1.19.774 → 1.19.776
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 +20 -20
- package/config/modules.neutral.json +1 -1
- package/docs/adr/0290-disconnect-is-the-owner-initiated-exception-to-address-retention.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/modules/provisioning/starter-bundles.js +32 -30
- package/modules/ui-design/kit/serve.js +2 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/db-backup-nightly.sh +7 -3
- package/scripts/gds/provision-disconnect.js +2 -2
- package/src/module-api.js +1 -1
- package/tests/db_backup_nightly.mjs +21 -1
- package/tests/project_type_editable.mjs +8 -8
- package/tests/provisioning_module_selection.mjs +14 -14
- package/tests/provisioning_recommendations.mjs +17 -25
- package/tests/provisioning_starter_bundles.mjs +31 -9
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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.776",
|
|
6
|
+
"core_contract": "1.19.776",
|
|
7
|
+
"source_commit": "c0b7c8b7cbf3c7bc785585da449da6ea57197c32",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-16T18:
|
|
9
|
+
"built_at": "2026-09-16T18:38:32.418Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 486,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 2662,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "c7dff0b9c97a50075315c645e421fe4fac5bfa6d21bf5c6cdfaea55e04dccd5c",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -422,7 +422,7 @@
|
|
|
422
422
|
{
|
|
423
423
|
"path": "config/modules.neutral.json",
|
|
424
424
|
"mode": "0000644",
|
|
425
|
-
"sha256": "
|
|
425
|
+
"sha256": "9bdd8eb1fbfb9053ed0fb921c0f669dcdaad2fe8b2629ce4cc9457dca995afbe"
|
|
426
426
|
},
|
|
427
427
|
{
|
|
428
428
|
"path": "docker-compose.yml",
|
|
@@ -1957,7 +1957,7 @@
|
|
|
1957
1957
|
{
|
|
1958
1958
|
"path": "docs/adr/0290-disconnect-is-the-owner-initiated-exception-to-address-retention.md",
|
|
1959
1959
|
"mode": "0000644",
|
|
1960
|
-
"sha256": "
|
|
1960
|
+
"sha256": "37ab80018f5d9e6fdb35271be80ca07f10e6e593ef33b9a7a0a30fe9ea8c5fb3"
|
|
1961
1961
|
},
|
|
1962
1962
|
{
|
|
1963
1963
|
"path": "docs/adr/README.md",
|
|
@@ -2527,7 +2527,7 @@
|
|
|
2527
2527
|
{
|
|
2528
2528
|
"path": "docs/module-api-changelog.md",
|
|
2529
2529
|
"mode": "0000644",
|
|
2530
|
-
"sha256": "
|
|
2530
|
+
"sha256": "7112a78b80e84df9950cdde4bc399b4c82f54138c044c1291b37c884def9f5f0"
|
|
2531
2531
|
},
|
|
2532
2532
|
{
|
|
2533
2533
|
"path": "docs/modules-contract.md",
|
|
@@ -6372,7 +6372,7 @@
|
|
|
6372
6372
|
{
|
|
6373
6373
|
"path": "modules/provisioning/starter-bundles.js",
|
|
6374
6374
|
"mode": "0000644",
|
|
6375
|
-
"sha256": "
|
|
6375
|
+
"sha256": "85f4b8e39801614457ed31f36220a0d2491c3d7dd8cd85ea04771d4ce77a79c2"
|
|
6376
6376
|
},
|
|
6377
6377
|
{
|
|
6378
6378
|
"path": "modules/provisioning/tests/liveness.mjs",
|
|
@@ -7317,7 +7317,7 @@
|
|
|
7317
7317
|
{
|
|
7318
7318
|
"path": "modules/ui-design/kit/serve.js",
|
|
7319
7319
|
"mode": "0000644",
|
|
7320
|
-
"sha256": "
|
|
7320
|
+
"sha256": "b041a60e0babb4b9d409f6542f748471c86235b8015922bdc501a5c0b8a2625f"
|
|
7321
7321
|
},
|
|
7322
7322
|
{
|
|
7323
7323
|
"path": "modules/ui-design/kit/tells.js",
|
|
@@ -7627,12 +7627,12 @@
|
|
|
7627
7627
|
{
|
|
7628
7628
|
"path": "package-lock.json",
|
|
7629
7629
|
"mode": "0000644",
|
|
7630
|
-
"sha256": "
|
|
7630
|
+
"sha256": "02c6bd2ec2e4afdc61d69e399a42ee98e762d908c69a3b84c9d27e8823b1f6cf"
|
|
7631
7631
|
},
|
|
7632
7632
|
{
|
|
7633
7633
|
"path": "package.json",
|
|
7634
7634
|
"mode": "0000644",
|
|
7635
|
-
"sha256": "
|
|
7635
|
+
"sha256": "2a771101f553f4e0207b498d8172bcef587e9115fbb3ff301539851f282e99ba"
|
|
7636
7636
|
},
|
|
7637
7637
|
{
|
|
7638
7638
|
"path": "public-docs/index.html",
|
|
@@ -7987,7 +7987,7 @@
|
|
|
7987
7987
|
{
|
|
7988
7988
|
"path": "scripts/gds/db-backup-nightly.sh",
|
|
7989
7989
|
"mode": "0000644",
|
|
7990
|
-
"sha256": "
|
|
7990
|
+
"sha256": "93f0b743b90a0cfdc91806ecb10da20456b98c0fac0f2b8eb3d690bccf79e532"
|
|
7991
7991
|
},
|
|
7992
7992
|
{
|
|
7993
7993
|
"path": "scripts/gds/db-backup-offsite.sh",
|
|
@@ -8487,7 +8487,7 @@
|
|
|
8487
8487
|
{
|
|
8488
8488
|
"path": "scripts/gds/provision-disconnect.js",
|
|
8489
8489
|
"mode": "0000644",
|
|
8490
|
-
"sha256": "
|
|
8490
|
+
"sha256": "81cdd20b43aa1fc8b39a4fa06aada292aa00e84724feb803ba96bf15ce92cca9"
|
|
8491
8491
|
},
|
|
8492
8492
|
{
|
|
8493
8493
|
"path": "scripts/gds/provision-net.js",
|
|
@@ -9492,7 +9492,7 @@
|
|
|
9492
9492
|
{
|
|
9493
9493
|
"path": "src/module-api.js",
|
|
9494
9494
|
"mode": "0000644",
|
|
9495
|
-
"sha256": "
|
|
9495
|
+
"sha256": "74ef2cad88c2d19fb73139455608b8e200f22f7e835c925910f6e28b3590c2a0"
|
|
9496
9496
|
},
|
|
9497
9497
|
{
|
|
9498
9498
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10357,7 +10357,7 @@
|
|
|
10357
10357
|
{
|
|
10358
10358
|
"path": "tests/db_backup_nightly.mjs",
|
|
10359
10359
|
"mode": "0000644",
|
|
10360
|
-
"sha256": "
|
|
10360
|
+
"sha256": "cb731d19610530cc07457271aed57d5e04bec3d42bf85f246601b6cbdd02b3fd"
|
|
10361
10361
|
},
|
|
10362
10362
|
{
|
|
10363
10363
|
"path": "tests/dead_deps.mjs",
|
|
@@ -12132,7 +12132,7 @@
|
|
|
12132
12132
|
{
|
|
12133
12133
|
"path": "tests/project_type_editable.mjs",
|
|
12134
12134
|
"mode": "0000644",
|
|
12135
|
-
"sha256": "
|
|
12135
|
+
"sha256": "85ac90a891c8f6f19d7780dd1bae94964f770fbaa9620322c4bc3df16ce790d6"
|
|
12136
12136
|
},
|
|
12137
12137
|
{
|
|
12138
12138
|
"path": "tests/project_visibility_db.mjs",
|
|
@@ -12242,7 +12242,7 @@
|
|
|
12242
12242
|
{
|
|
12243
12243
|
"path": "tests/provisioning_module_selection.mjs",
|
|
12244
12244
|
"mode": "0000644",
|
|
12245
|
-
"sha256": "
|
|
12245
|
+
"sha256": "dffa4b500d3afe770764e562236ee97b6d581ed3db301ee3f984c5caa6f6f35e"
|
|
12246
12246
|
},
|
|
12247
12247
|
{
|
|
12248
12248
|
"path": "tests/provisioning_onboard_mode.mjs",
|
|
@@ -12257,7 +12257,7 @@
|
|
|
12257
12257
|
{
|
|
12258
12258
|
"path": "tests/provisioning_recommendations.mjs",
|
|
12259
12259
|
"mode": "0000644",
|
|
12260
|
-
"sha256": "
|
|
12260
|
+
"sha256": "587c6e640493939be17f6f293ceba8d22d125c79e43c070793b0325458132c71"
|
|
12261
12261
|
},
|
|
12262
12262
|
{
|
|
12263
12263
|
"path": "tests/provisioning_restart.mjs",
|
|
@@ -12287,7 +12287,7 @@
|
|
|
12287
12287
|
{
|
|
12288
12288
|
"path": "tests/provisioning_starter_bundles.mjs",
|
|
12289
12289
|
"mode": "0000644",
|
|
12290
|
-
"sha256": "
|
|
12290
|
+
"sha256": "5996c2dac6fbb427fba2eac98058ec9cea529fd2aa74d4b51e4085263af798ae"
|
|
12291
12291
|
},
|
|
12292
12292
|
{
|
|
12293
12293
|
"path": "tests/provisioning_teardown_intent.mjs",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_note": "Feature-module selection — the VANILLA (customer-0) default: every optional feature OFF. A fresh Cloud Bongos instance is the methodology core only (builders/tasks/claims/credits/grading/memory/recall) with the engineer discipline. To enable a module on an instance, copy this to config/modules.json and flip flags (or set <ENVPREFIX>_MODULE_<NAME>=1). Sibling of config/branding.neutral.json. Owned by R54 / task 1193 (src/modules.js, ADR 0062 §4). Known modules: game, art-pipeline, discord, dev-box.",
|
|
2
|
+
"_note": "Feature-module selection — the VANILLA (customer-0) default: every optional feature OFF. A fresh Cloud Bongos instance is the methodology core only (builders/tasks/claims/credits/grading/memory/recall) with the engineer discipline. To enable a module on an instance, copy this to config/modules.json and flip flags (or set <ENVPREFIX>_MODULE_<NAME>=1). Sibling of config/branding.neutral.json. Owned by R54 / task 1193 (src/modules.js, ADR 0062 §4). Known modules: game, art-pipeline, discord. RETIRED: dev-box (goal 1000091, task 1003886) — no longer offered in any starter bundle and not in the creation picker's catalog. The flag stays declared and false until modules/dev-box/ is deleted, so an instance carrying an older config/modules.json cannot silently re-enable it.",
|
|
3
3
|
"modules": {
|
|
4
4
|
"game": false,
|
|
5
5
|
"art-pipeline": false,
|
|
@@ -35,6 +35,6 @@ A failed erase or catalog step returns a retryable `{ ok:false }`; a teardown th
|
|
|
35
35
|
- The manage page's copy changed with the capability. The teardown truth now ends "Taking it offline erases nothing." rather than "Nothing here erases a project.", and the fold points at the new card instead of admitting the capability is missing.
|
|
36
36
|
- The `project.catalog` port has three verbs. `tests/catalog_only_client.mjs` still pins the list.
|
|
37
37
|
- If the runner dies mid-disconnect, the reconcile sweep re-enqueues a *teardown* for a `tearing_down` row, not a disconnect. The project is left offline, with its row, and the owner presses Disconnect again. This is accepted rather than giving the reconcile a memory of intent.
|
|
38
|
-
- The nightly prune's `<db>-*.sql.gz` glob
|
|
38
|
+
- The nightly prune's `<db>-*.sql.gz` glob could still reach a longer-named sibling's dumps older than 30 days. It was filed as its own row and fixed in [task 1003948](https://cloudbongos.com/builders#/task/1003948): the prune and its retained count now match the same exact filename.
|
|
39
39
|
|
|
40
40
|
Proofs: `tests/provisioning_disconnect.mjs` (queue + route), `tests/provision_disconnect.mjs` (runner order, shapes, failure, guards), `tests/project_catalog_forget.mjs` (claim rules), `tests/hub_disconnect_ui.mjs` + the kit probe (the card).
|
|
@@ -2022,5 +2022,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2022
2022
|
landed since 1.19.772 with no explicit bump. run 35118731311. (task 1002620)
|
|
2023
2023
|
1.19.774 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2024
2024
|
landed since 1.19.773 with no explicit bump. run 35132992149. (task 1002620)
|
|
2025
|
+
1.19.775 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2026
|
+
landed since 1.19.774 with no explicit bump. run 35133750943. (task 1002620)
|
|
2027
|
+
1.19.776 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2028
|
+
landed since 1.19.775 with no explicit bump. run 35135604764. (task 1002620)
|
|
2025
2029
|
---------------------------------------------------------------------------
|
|
2026
2030
|
```
|
|
@@ -70,6 +70,21 @@ const ALWAYS_ON_CORE = Object.freeze([
|
|
|
70
70
|
// refuse them by construction rather than by anyone remembering.
|
|
71
71
|
const HUB_ONLY_MODULES = Object.freeze(['platform-identity', 'provisioning']);
|
|
72
72
|
|
|
73
|
+
// Modules that were once offered and are being WITHDRAWN. A retired key is still
|
|
74
|
+
// a module on disk — its manifest, routes and tables are deleted by later tasks —
|
|
75
|
+
// but it must stop being OFFERED the moment the withdrawal starts, or an owner
|
|
76
|
+
// creating a project today opts into something that is on its way out.
|
|
77
|
+
//
|
|
78
|
+
// WHY A LIST AND NOT JUST DELETING THE ROW. The integrity test below pins
|
|
79
|
+
// OPTIONAL_MODULES against the on-disk `default: false` manifests, so that a new
|
|
80
|
+
// opt-in module cannot ship without owner-facing copy. Dropping a row while its
|
|
81
|
+
// manifest is still on disk would trip exactly that guard. Naming the withdrawal
|
|
82
|
+
// keeps the guard honest in both directions: the catalog may omit a RETIRED
|
|
83
|
+
// module and may omit nothing else. The entry is deleted when the module is
|
|
84
|
+
// (goal 1000091, task 1003892) — at which point the manifest is gone and the
|
|
85
|
+
// plain drift check covers it again.
|
|
86
|
+
const RETIRED_MODULES = Object.freeze(['dev-box']);
|
|
87
|
+
|
|
73
88
|
// The OPT-IN catalog as the creation picker renders it (BV1.R20, task 1002339):
|
|
74
89
|
// every module an owner may actually choose, in the order the picker lists them,
|
|
75
90
|
// with copy written for someone who has met none of them.
|
|
@@ -90,11 +105,6 @@ const OPTIONAL_MODULES = Object.freeze([
|
|
|
90
105
|
label: 'Extra agents',
|
|
91
106
|
summary: 'More AI agents alongside the ones every project gets, for reading, research and review.',
|
|
92
107
|
}),
|
|
93
|
-
Object.freeze({
|
|
94
|
-
key: 'dev-box',
|
|
95
|
-
label: 'Dev boxes',
|
|
96
|
-
summary: 'A cloud workspace where a change can be tried out before anyone else sees it. Nothing is charged until someone asks for one.',
|
|
97
|
-
}),
|
|
98
108
|
Object.freeze({
|
|
99
109
|
key: 'discord',
|
|
100
110
|
label: 'Discord',
|
|
@@ -122,8 +132,10 @@ const OPTIONAL_MODULE_KEYS = Object.freeze(OPTIONAL_MODULES.map((m) => m.key));
|
|
|
122
132
|
// stay there. Decision: ADR 0243 (BV1.R22, task 1002341).
|
|
123
133
|
//
|
|
124
134
|
// Deliberately tiny: ADR 0237 already made the per-type preset the opinion, and
|
|
125
|
-
// today's opt-in catalog is
|
|
126
|
-
// a preset the owner's answer contradicts, not to re-derive it.
|
|
135
|
+
// today's opt-in catalog is two modules wide — so this table's job is to correct
|
|
136
|
+
// a preset the owner's answer contradicts, not to re-derive it. The two rules that
|
|
137
|
+
// added and removed `dev-box` by team shape went with the module (task 1003886):
|
|
138
|
+
// a rule about a module nobody can select adjusts nothing.
|
|
127
139
|
//
|
|
128
140
|
// A rule may only ever name an OPT-IN module. Adding or removing an always-on one
|
|
129
141
|
// would be a toggle that toggles nothing (ADR 0237 §3's argument, and
|
|
@@ -136,20 +148,6 @@ const TEAM_SHAPE_RULES = Object.freeze([
|
|
|
136
148
|
module: 'discord',
|
|
137
149
|
reason: 'You said an open community is building this, so somewhere for it to gather is switched on.',
|
|
138
150
|
}),
|
|
139
|
-
Object.freeze({
|
|
140
|
-
id: 'a-team-shares-one-environment',
|
|
141
|
-
teamShape: 'small-team',
|
|
142
|
-
effect: 'add',
|
|
143
|
-
module: 'dev-box',
|
|
144
|
-
reason: 'A small team gets one shared environment to build in, so nobody is debugging their own laptop.',
|
|
145
|
-
}),
|
|
146
|
-
Object.freeze({
|
|
147
|
-
id: 'building-alone-needs-no-shared-environment',
|
|
148
|
-
teamShape: 'solo',
|
|
149
|
-
effect: 'remove',
|
|
150
|
-
module: 'dev-box',
|
|
151
|
-
reason: 'You are building this alone, so the shared cloud environment is off — your own machine is the setup.',
|
|
152
|
-
}),
|
|
153
151
|
]);
|
|
154
152
|
|
|
155
153
|
// The team shapes the rules above can fire on. Derived from the table rather than
|
|
@@ -167,16 +165,17 @@ const RULED_TEAM_SHAPES = Object.freeze([...new Set(TEAM_SHAPE_RULES.map((r) =>
|
|
|
167
165
|
// here has not met any of these modules yet.
|
|
168
166
|
//
|
|
169
167
|
// The lists are SHORT on purpose. A preselect an owner has to audit is worse than
|
|
170
|
-
// none, and today's opt-in catalog is genuinely small (`agents`, `
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
168
|
+
// none, and today's opt-in catalog is genuinely small (`agents`, `discord`); a
|
|
169
|
+
// bundle grows as the catalog does, which is exactly why this is a declaration and
|
|
170
|
+
// not a rule engine. `dev-box` was preselected by the game and business bundles
|
|
171
|
+
// until it was retired (goal 1000091, task 1003886) — withdrawing it left both
|
|
172
|
+
// bundles one module wide, which is the floor working as intended, not a gap to
|
|
173
|
+
// backfill with a module an owner did not ask for.
|
|
175
174
|
const STARTER_BUNDLES = Object.freeze({
|
|
176
175
|
game: Object.freeze({
|
|
177
176
|
label: 'Game starter',
|
|
178
|
-
summary: 'Somewhere for players to gather,
|
|
179
|
-
modules: Object.freeze(['
|
|
177
|
+
summary: 'Somewhere for players to gather, hear what you are building, and send their ideas back.',
|
|
178
|
+
modules: Object.freeze(['discord']),
|
|
180
179
|
}),
|
|
181
180
|
research: Object.freeze({
|
|
182
181
|
label: 'Research starter',
|
|
@@ -185,8 +184,8 @@ const STARTER_BUNDLES = Object.freeze({
|
|
|
185
184
|
}),
|
|
186
185
|
business: Object.freeze({
|
|
187
186
|
label: 'Business starter',
|
|
188
|
-
summary: '
|
|
189
|
-
modules: Object.freeze(['agents'
|
|
187
|
+
summary: 'Extra agents to look a change over before customers ever see it.',
|
|
188
|
+
modules: Object.freeze(['agents']),
|
|
190
189
|
}),
|
|
191
190
|
'non-profit': Object.freeze({
|
|
192
191
|
label: 'Non-profit starter',
|
|
@@ -259,6 +258,8 @@ function adjustBundle(bundle, teamShape) {
|
|
|
259
258
|
for (const rule of TEAM_SHAPE_RULES) {
|
|
260
259
|
if (rule.teamShape !== teamShape) continue;
|
|
261
260
|
if (rule.effect === 'add' && !held.has(rule.module)) held.add(rule.module);
|
|
261
|
+
// `remove` is declared but named by no live rule since dev-box was retired
|
|
262
|
+
// (task 1003886). Kept for the next rule that needs it, not exercised today.
|
|
262
263
|
else if (rule.effect === 'remove' && held.has(rule.module)) held.delete(rule.module);
|
|
263
264
|
else continue;
|
|
264
265
|
adjustments.push({ rule: rule.id, effect: rule.effect, module: rule.module, reason: rule.reason });
|
|
@@ -374,6 +375,7 @@ function effectiveModules(row) {
|
|
|
374
375
|
module.exports = {
|
|
375
376
|
ALWAYS_ON_CORE,
|
|
376
377
|
HUB_ONLY_MODULES,
|
|
378
|
+
RETIRED_MODULES,
|
|
377
379
|
OPTIONAL_MODULES,
|
|
378
380
|
STARTER_BUNDLES,
|
|
379
381
|
TEAM_SHAPE_RULES,
|
|
@@ -121,15 +121,14 @@ const STARTER_BUNDLES_FIXTURE = {
|
|
|
121
121
|
'security', 'sessions', 'status-ui', 'ui-design',
|
|
122
122
|
],
|
|
123
123
|
bundles: [
|
|
124
|
-
{ type: 'game', bundle: ['
|
|
124
|
+
{ type: 'game', bundle: ['discord'] },
|
|
125
125
|
{ type: 'research', bundle: ['agents'] },
|
|
126
|
-
{ type: 'business', bundle: ['agents'
|
|
126
|
+
{ type: 'business', bundle: ['agents'] },
|
|
127
127
|
{ type: 'non-profit', bundle: ['discord'] },
|
|
128
128
|
{ type: 'not-sure', bundle: [] },
|
|
129
129
|
],
|
|
130
130
|
optional: [
|
|
131
131
|
{ key: 'agents', label: 'Extra agents', summary: 'More AI agents alongside the ones every project gets, for reading, research and review.' },
|
|
132
|
-
{ key: 'dev-box', label: 'Dev boxes', summary: 'A cloud workspace where a change can be tried out before anyone else sees it. Nothing is charged until someone asks for one.' },
|
|
133
132
|
{ key: 'discord', label: 'Discord', summary: 'A Discord server that carries your build news, and lets people send ideas and bug reports back.' },
|
|
134
133
|
],
|
|
135
134
|
};
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.776",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.776",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.776",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Runs as user `lars` via the gds-db-backup.timer systemd unit (see
|
|
5
5
|
# scripts/gds/gds-db-backup.timer + .service in this repo). Output lands
|
|
6
|
-
# in /var/backups/gds/ as YYYY-MM-
|
|
6
|
+
# in /var/backups/gds/ as <db>-YYYY-MM-DDTHHMMSSZ.sql.gz (UTC timestamp), and
|
|
7
7
|
# files older than 30 days are pruned.
|
|
8
8
|
#
|
|
9
9
|
# Authentication: peer auth via the local Unix socket — the invoking unix
|
|
@@ -59,6 +59,10 @@ RETENTION_DAYS="${GDS_BACKUP_RETENTION_DAYS:-30}"
|
|
|
59
59
|
# Timestamp format. UTC so dumps sort sanely regardless of TZ.
|
|
60
60
|
TS=$(date -u +"%Y-%m-%dT%H%M%SZ")
|
|
61
61
|
OUT="${BACKUP_DIR}/${DB_NAME}-${TS}.sql.gz"
|
|
62
|
+
# Exactly the name above. Every project's unit shares BACKUP_DIR, so a looser
|
|
63
|
+
# `${DB_NAME}-*` would let `test` prune and count `test-nk`'s dumps (task 1003948).
|
|
64
|
+
D='[0-9]'
|
|
65
|
+
DUMP_GLOB="${DB_NAME}-${D}${D}${D}${D}-${D}${D}-${D}${D}T${D}${D}${D}${D}${D}${D}Z.sql.gz"
|
|
62
66
|
|
|
63
67
|
# ---- preconditions ----------------------------------------------------------
|
|
64
68
|
|
|
@@ -173,7 +177,7 @@ fi
|
|
|
173
177
|
# Done AFTER the new dump lands so a streak of failed runs can't erase
|
|
174
178
|
# everything: we never delete on a day we didn't successfully dump.
|
|
175
179
|
PRUNED=$(find "$BACKUP_DIR" -maxdepth 1 -type f \
|
|
176
|
-
-name "$
|
|
180
|
+
-name "$DUMP_GLOB" \
|
|
177
181
|
-mtime +"${RETENTION_DAYS}" \
|
|
178
182
|
-print -delete | wc -l)
|
|
179
183
|
|
|
@@ -183,5 +187,5 @@ fi
|
|
|
183
187
|
|
|
184
188
|
# ---- emit a one-line summary for journald + monitoring ---------------------
|
|
185
189
|
|
|
186
|
-
REMAINING=$(find "$BACKUP_DIR" -maxdepth 1 -type f -name "$
|
|
190
|
+
REMAINING=$(find "$BACKUP_DIR" -maxdepth 1 -type f -name "$DUMP_GLOB" | wc -l)
|
|
187
191
|
echo "[$(date -u +%FT%TZ)] gds-db-backup: ok · ${REMAINING} dump(s) retained · ${BYTES} bytes latest"
|
|
@@ -28,8 +28,8 @@ const { catalogOriginForInstance, ownerGithubIdForInstance } = require('../../mo
|
|
|
28
28
|
const { deleteInstance } = require('../../modules/provisioning/disconnect.js');
|
|
29
29
|
|
|
30
30
|
// The dump filename db-backup-nightly.sh writes — <db>-YYYY-MM-DDTHHMMSSZ.sql.gz — matched
|
|
31
|
-
// EXACTLY
|
|
32
|
-
// `test-nk`'s dumps
|
|
31
|
+
// EXACTLY, as the nightly prune's DUMP_GLOB does (task 1003948): a looser `<db>-*.sql.gz` would
|
|
32
|
+
// let a project named `test` erase `test-nk`'s dumps. PURE.
|
|
33
33
|
function dumpGlob(inst) {
|
|
34
34
|
const d = '[0-9]';
|
|
35
35
|
return `${dbName(inst)}-${d.repeat(4)}-${d.repeat(2)}-${d.repeat(2)}T${d.repeat(6)}Z.sql.gz`;
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.776'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -29,12 +29,19 @@ const ENV = 'gds_backup_'.toUpperCase();
|
|
|
29
29
|
// A fake pg_dump: a plain-format dump of `rows` rows, closed the way pg_dump closes one
|
|
30
30
|
// (including the \unrestrict line patched servers append) unless it is `cut` short, and
|
|
31
31
|
// exiting `exitCode`.
|
|
32
|
-
|
|
32
|
+
// `seed` pre-places dumps in the shared dir as [filename, ageInDays].
|
|
33
|
+
function runBackup({ rows = 40, cut = false, exitCode = 0, env = {}, seed = [] } = {}) {
|
|
33
34
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'db-backup-'));
|
|
34
35
|
const bin = path.join(tmp, 'bin');
|
|
35
36
|
const dir = path.join(tmp, 'backups');
|
|
36
37
|
fs.mkdirSync(bin);
|
|
37
38
|
fs.mkdirSync(dir);
|
|
39
|
+
for (const [name, ageDays] of seed) {
|
|
40
|
+
const f = path.join(dir, name);
|
|
41
|
+
fs.writeFileSync(f, '');
|
|
42
|
+
const t = new Date(Date.now() - ageDays * 86400000);
|
|
43
|
+
fs.utimesSync(f, t, t);
|
|
44
|
+
}
|
|
38
45
|
const body = Array.from({ length: rows }, (_, i) => `INSERT INTO public.t VALUES (${i});`).join('\n');
|
|
39
46
|
const close = cut ? '' : '\n--\n-- PostgreSQL database dump complete\n--\n\n\\unrestrict abc123\n';
|
|
40
47
|
fs.writeFileSync(path.join(bin, 'pg_dump'),
|
|
@@ -73,6 +80,19 @@ test('an explicit operator size floor is still enforced', { skip }, () => {
|
|
|
73
80
|
assert.match(r.stderr, /floor/);
|
|
74
81
|
});
|
|
75
82
|
|
|
83
|
+
test('the prune and the count reach only this database\'s own dumps, never a longer-named sibling\'s (task 1003948)', { skip }, () => {
|
|
84
|
+
const r = runBackup({ seed: [
|
|
85
|
+
['smallproj-2026-01-01T000000Z.sql.gz', 40],
|
|
86
|
+
['smallproj-nk-2026-01-01T000000Z.sql.gz', 40],
|
|
87
|
+
['smallproj-2-2026-09-01T000000Z.sql.gz', 2],
|
|
88
|
+
] });
|
|
89
|
+
assert.equal(r.status, 0, r.stderr);
|
|
90
|
+
assert.ok(!r.dumps.includes('smallproj-2026-01-01T000000Z.sql.gz'), 'its own expired dump is pruned');
|
|
91
|
+
assert.ok(r.dumps.includes('smallproj-nk-2026-01-01T000000Z.sql.gz'), 'a sibling\'s expired dump is left for the sibling\'s own unit');
|
|
92
|
+
assert.match(r.stdout, /pruned 1 dump\(s\)/);
|
|
93
|
+
assert.match(r.stdout, /ok · 1 dump\(s\) retained/, 'the count is this database\'s dumps only');
|
|
94
|
+
});
|
|
95
|
+
|
|
76
96
|
test('no fixed size floor is baked in for every instance', () => {
|
|
77
97
|
const src = fs.readFileSync(SCRIPT, 'utf8');
|
|
78
98
|
assert.doesNotMatch(src, new RegExp(`${ENV}MIN_BYTES:-[1-9]`), 'a default floor fails every database smaller than it');
|
|
@@ -98,7 +98,7 @@ test('updateInstanceDetail can change the type and something else in one stateme
|
|
|
98
98
|
test('every SET clause binds a PLACEHOLDER — a bare index is not valid SQL', async () => {
|
|
99
99
|
const db = recordingDb({ id: 41 });
|
|
100
100
|
await realUpdateInstanceDetail(db, 41, {
|
|
101
|
-
description: 'Hello.', detail: { team_shape: 'solo' }, modules: ['
|
|
101
|
+
description: 'Hello.', detail: { team_shape: 'solo' }, modules: ['agents'], type: 'business',
|
|
102
102
|
});
|
|
103
103
|
const { text, params } = db.calls[0];
|
|
104
104
|
const sets = text.slice(text.indexOf('SET '), text.indexOf(' WHERE '));
|
|
@@ -114,10 +114,10 @@ test('every SET clause binds a PLACEHOLDER — a bare index is not valid SQL', a
|
|
|
114
114
|
|
|
115
115
|
test('the modules branch still binds its JSON, not its own parameter index', async () => {
|
|
116
116
|
const db = recordingDb({ id: 41 });
|
|
117
|
-
await realUpdateInstanceDetail(db, 41, { modules: ['
|
|
117
|
+
await realUpdateInstanceDetail(db, 41, { modules: ['agents', 'discord'] });
|
|
118
118
|
const { text, params } = db.calls[0];
|
|
119
119
|
assert.match(text, /modules = \$\d+::jsonb/, 'the placeholder carries the cast, the index does not replace it');
|
|
120
|
-
assert.ok(params.some((p) => typeof p === 'string' && p.includes('
|
|
120
|
+
assert.ok(params.some((p) => typeof p === 'string' && p.includes('agents')),
|
|
121
121
|
'the serialized selection reaches the statement');
|
|
122
122
|
});
|
|
123
123
|
|
|
@@ -313,7 +313,7 @@ function runKind({ inst, fetchImpl, catalogMods } = {}) {
|
|
|
313
313
|
return { sandbox, els, calls, band: () => el('mKindPick').innerHTML, msg: () => el('mKindMsg') };
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
const INST = (over = {}) => ({ id: 18, type: 'game', modules: { selected: ['
|
|
316
|
+
const INST = (over = {}) => ({ id: 18, type: 'game', modules: { selected: ['agents'], source: 'bundle' }, ...over });
|
|
317
317
|
|
|
318
318
|
test('the band paints every kind from TYPE_META, with the declared one pressed', () => {
|
|
319
319
|
const h = runKind({ inst: INST() });
|
|
@@ -353,7 +353,7 @@ function okFetch(body) {
|
|
|
353
353
|
test('a pick sends the kind on the own-scoped detail channel and repaints from the PLATFORM\'S answer', async () => {
|
|
354
354
|
const h = runKind({
|
|
355
355
|
inst: INST({ type: 'game' }),
|
|
356
|
-
fetchImpl: okFetch({ type: 'research', modules: { selected: ['
|
|
356
|
+
fetchImpl: okFetch({ type: 'research', modules: { selected: ['agents'], source: 'picked' } }),
|
|
357
357
|
});
|
|
358
358
|
h.sandbox.doKindPick('research');
|
|
359
359
|
await new Promise((r) => setImmediate(r));
|
|
@@ -369,7 +369,7 @@ test('when the extras followed the kind, the band above is repainted AND the own
|
|
|
369
369
|
// the type's starter bundle (ADR 0240 §2) — so changing the kind moves the extras.
|
|
370
370
|
// Leaving that band painted from the old kind would be a lie on the same page.
|
|
371
371
|
const h = runKind({
|
|
372
|
-
inst: INST({ type: 'game', modules: { selected: ['
|
|
372
|
+
inst: INST({ type: 'game', modules: { selected: ['agents'], source: 'bundle' } }),
|
|
373
373
|
fetchImpl: okFetch({ type: 'research', modules: { selected: [], source: 'bundle' } }),
|
|
374
374
|
});
|
|
375
375
|
h.sandbox.doKindPick('research');
|
|
@@ -380,8 +380,8 @@ test('when the extras followed the kind, the band above is repainted AND the own
|
|
|
380
380
|
|
|
381
381
|
test('a set the owner picked for themselves never moves, and the receipt does not claim it did', async () => {
|
|
382
382
|
const h = runKind({
|
|
383
|
-
inst: INST({ type: 'game', modules: { selected: ['
|
|
384
|
-
fetchImpl: okFetch({ type: 'business', modules: { selected: ['
|
|
383
|
+
inst: INST({ type: 'game', modules: { selected: ['agents'], source: 'picked' } }),
|
|
384
|
+
fetchImpl: okFetch({ type: 'business', modules: { selected: ['agents'], source: 'picked' } }),
|
|
385
385
|
});
|
|
386
386
|
h.sandbox.doKindPick('business');
|
|
387
387
|
await new Promise((r) => setImmediate(r));
|
|
@@ -235,12 +235,12 @@ await ta('POST /instances: the picker\'s ids ride the create request through to
|
|
|
235
235
|
|
|
236
236
|
// Act — the body the wizard files
|
|
237
237
|
const res = await post('/provisioning/instances', {
|
|
238
|
-
slug: 'mercury', hosting_shape: 'standalone', type: 'game', modules: ['
|
|
238
|
+
slug: 'mercury', hosting_shape: 'standalone', type: 'game', modules: ['agents', 'discord'],
|
|
239
239
|
});
|
|
240
240
|
|
|
241
241
|
// Assert
|
|
242
242
|
assert.equal(res.status, 201, `expected 201, got ${res.status} ${res.raw}`);
|
|
243
|
-
assert.deepEqual(lastCreateArgs && lastCreateArgs.modules, ['
|
|
243
|
+
assert.deepEqual(lastCreateArgs && lastCreateArgs.modules, ['agents', 'discord']);
|
|
244
244
|
});
|
|
245
245
|
|
|
246
246
|
await ta('POST /instances: an omitted modules key is still a valid create — skipping is supported, not an error', async () => {
|
|
@@ -324,17 +324,17 @@ await ta('PATCH /detail: the reply carries modules, resolved from the row the UP
|
|
|
324
324
|
detailRow = { id: 41, owner_builder_id: 7, slug: 'mercury', status: 'active', type: 'game', modules: null, detail: {} };
|
|
325
325
|
lastUpdatedRow = null;
|
|
326
326
|
|
|
327
|
-
// Act — the owner says
|
|
328
|
-
const res = await patch('/provisioning/instances/41/detail', { detail: { team_shape: '
|
|
327
|
+
// Act — the owner says an open community is building it
|
|
328
|
+
const res = await patch('/provisioning/instances/41/detail', { detail: { team_shape: 'community' } });
|
|
329
329
|
|
|
330
330
|
// Assert — the field is there, and it is the POST-patch resolution: a route
|
|
331
|
-
// that answered off `inst` instead of `updated` would still
|
|
331
|
+
// that answered off `inst` instead of `updated` would still omit 'discord'.
|
|
332
332
|
assert.equal(res.status, 200, res.raw);
|
|
333
333
|
assert.ok(lastUpdatedRow, 'the route ran the update');
|
|
334
334
|
assert.deepEqual(res.body.modules, effectiveModules(lastUpdatedRow));
|
|
335
|
-
assert.deepEqual(res.body.modules.selected, ['discord'], '
|
|
335
|
+
assert.deepEqual(res.body.modules.selected, ['discord'], 'community keeps the gathering place in the game bundle');
|
|
336
336
|
assert.equal(res.body.modules.source, 'bundle');
|
|
337
|
-
assert.
|
|
337
|
+
assert.deepEqual(res.body.modules.adjustments, [], 'the game bundle already carried it, so nothing is claimed');
|
|
338
338
|
detailRow = null; lastUpdatedRow = null;
|
|
339
339
|
});
|
|
340
340
|
|
|
@@ -387,14 +387,14 @@ await ta('PATCH /detail: an owner who skipped the picker can add one module late
|
|
|
387
387
|
await ta('PATCH /detail: a module can be REMOVED later too — the list replaces, it never merges', async () => {
|
|
388
388
|
// The half a merging write would make impossible, which is why
|
|
389
389
|
// updateInstanceDetail replaces `modules` and merges `detail`.
|
|
390
|
-
detailRow = { id: 41, owner_builder_id: 7, slug: 'mercury', status: 'active', type: 'game', modules: ['
|
|
390
|
+
detailRow = { id: 41, owner_builder_id: 7, slug: 'mercury', status: 'active', type: 'game', modules: ['agents', 'discord'], detail: {} };
|
|
391
391
|
lastUpdatedRow = null;
|
|
392
392
|
|
|
393
393
|
const res = await patch('/provisioning/instances/41/detail', { modules: ['discord'] });
|
|
394
394
|
|
|
395
395
|
assert.equal(res.status, 200, res.raw);
|
|
396
396
|
assert.deepEqual(lastUpdatedRow.modules, ['discord']);
|
|
397
|
-
assert.deepEqual(res.body.modules.selected, ['discord'], '
|
|
397
|
+
assert.deepEqual(res.body.modules.selected, ['discord'], 'agents is gone, not still merged in');
|
|
398
398
|
assert.equal(res.body.modules.source, 'chosen');
|
|
399
399
|
detailRow = null; lastUpdatedRow = null;
|
|
400
400
|
});
|
|
@@ -517,17 +517,17 @@ await ta('PATCH /detail: another builder’s project is a 404, not a module chan
|
|
|
517
517
|
// — not merely "close to one".
|
|
518
518
|
|
|
519
519
|
await ta('PATCH /detail: answer then CLEAR — the row reads back exactly as one that was never asked', async () => {
|
|
520
|
-
// Arrange — a
|
|
520
|
+
// Arrange — a research project that skipped the picker, so its modules resolve from
|
|
521
521
|
// the bundle and the interview answer is the only thing that can move them.
|
|
522
|
-
const virgin = { id: 41, owner_builder_id: 7, slug: 'mercury', status: 'active', type: '
|
|
522
|
+
const virgin = { id: 41, owner_builder_id: 7, slug: 'mercury', status: 'active', type: 'research', modules: null, detail: {} };
|
|
523
523
|
const neverAnswered = effectiveModules(virgin);
|
|
524
524
|
assert.equal(neverAnswered.source, 'bundle', 'the fixture really is an unanswered project');
|
|
525
525
|
|
|
526
526
|
detailRow = { ...virgin };
|
|
527
527
|
lastUpdatedRow = null;
|
|
528
|
-
const answered = await patch('/provisioning/instances/41/detail', { detail: { team_shape: '
|
|
528
|
+
const answered = await patch('/provisioning/instances/41/detail', { detail: { team_shape: 'community' } });
|
|
529
529
|
assert.equal(answered.status, 200, answered.raw);
|
|
530
|
-
assert.deepEqual(answered.body.detail, { team_shape: '
|
|
530
|
+
assert.deepEqual(answered.body.detail, { team_shape: 'community' });
|
|
531
531
|
assert.notDeepEqual(answered.body.modules.selected, neverAnswered.selected,
|
|
532
532
|
'the fixture is only meaningful if the answer really moved the bundle');
|
|
533
533
|
|
|
@@ -543,7 +543,7 @@ await ta('PATCH /detail: answer then CLEAR — the row reads back exactly as one
|
|
|
543
543
|
assert.deepEqual(cleared.body.detail, {}, 'and the read surface says the interview is unanswered');
|
|
544
544
|
assert.deepEqual(cleared.body.modules, neverAnswered, 'source, preset and adjustments are the never-answered ones');
|
|
545
545
|
assert.equal(cleared.body.modules.source, 'bundle');
|
|
546
|
-
assert.deepEqual(cleared.body.modules.selected, bundleForProjectType('
|
|
546
|
+
assert.deepEqual(cleared.body.modules.selected, bundleForProjectType('research').bundle, 'the UNADJUSTED preset');
|
|
547
547
|
assert.deepEqual(cleared.body.modules.adjustments, [], 'no rule fired, so nothing claims to have');
|
|
548
548
|
assert.deepEqual(cleared.body.publish, answered.body.publish, 'clearing an answer is not a publish change');
|
|
549
549
|
detailRow = null; lastUpdatedRow = null;
|
|
@@ -101,42 +101,34 @@ test('an open community adds somewhere to gather to a bundle that lacks one', ()
|
|
|
101
101
|
assert.ok(recs.bundle.adjustments[0].reason.length > 0, 'an adjustment always says why');
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
-
test('a
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// Assert
|
|
112
|
-
assert.deepEqual(recs.bundle.bundle, ['agents', 'dev-box']);
|
|
113
|
-
assert.deepEqual(recs.bundle.adjustments.map((a) => a.module), ['dev-box']);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test('a solo builder drops the shared build environment from the bundle', () => {
|
|
117
|
-
// Arrange — the business bundle is `agents` + `dev-box`, so the removal bites.
|
|
118
|
-
const answers = { type: 'business', team_shape: 'solo' };
|
|
104
|
+
test('a community adds the gathering place to a bundle that lacks it', () => {
|
|
105
|
+
// The surviving bundle rule's ADD path. The two rules that added and removed
|
|
106
|
+
// `dev-box` by team shape went with the module (task 1003886), which leaves
|
|
107
|
+
// `effect: 'remove'` declared in adjustBundle but named by no live rule - it is
|
|
108
|
+
// kept for the next rule that needs it, not because anything exercises it today.
|
|
109
|
+
// Arrange - the research bundle is `agents` alone, so the addition bites.
|
|
110
|
+
const answers = { type: 'research', team_shape: 'community' };
|
|
119
111
|
|
|
120
112
|
// Act
|
|
121
113
|
const recs = recommendationsForDetails(answers);
|
|
122
114
|
|
|
123
115
|
// Assert
|
|
124
|
-
assert.deepEqual(recs.bundle.bundle, ['agents']);
|
|
125
|
-
assert.
|
|
126
|
-
assert.equal(recs.bundle.adjustments[0].effect, '
|
|
127
|
-
assert.equal(recs.bundle.adjustments[0].module, 'dev-box');
|
|
116
|
+
assert.deepEqual(recs.bundle.bundle, ['agents', 'discord']);
|
|
117
|
+
assert.deepEqual(recs.bundle.adjustments.map((a) => a.module), ['discord']);
|
|
118
|
+
assert.equal(recs.bundle.adjustments[0].effect, 'add');
|
|
128
119
|
});
|
|
129
120
|
|
|
130
121
|
test('a rule does not fire for a team shape it does not name', () => {
|
|
131
|
-
// Arrange
|
|
132
|
-
|
|
122
|
+
// Arrange - `community` adds discord; `solo` names no rule, so the business
|
|
123
|
+
// bundle must come back exactly as declared.
|
|
124
|
+
const answers = { type: 'business', team_shape: 'solo' };
|
|
133
125
|
|
|
134
126
|
// Act
|
|
135
127
|
const recs = recommendationsForDetails(answers);
|
|
136
128
|
|
|
137
129
|
// Assert
|
|
138
|
-
assert.
|
|
139
|
-
assert.deepEqual(recs.bundle.adjustments
|
|
130
|
+
assert.deepEqual(recs.bundle.bundle, ['agents'], 'the community addition did not leak onto another shape');
|
|
131
|
+
assert.deepEqual(recs.bundle.adjustments, [], 'and nothing is claimed');
|
|
140
132
|
});
|
|
141
133
|
|
|
142
134
|
test('a rule whose module the bundle already carries records no adjustment', () => {
|
|
@@ -304,8 +296,8 @@ test('a caller may mutate the result without corrupting the declaration', () =>
|
|
|
304
296
|
|
|
305
297
|
// Assert
|
|
306
298
|
const second = recommendationsForDetails({ type: 'game', team_shape: 'small-team' });
|
|
307
|
-
assert.deepEqual(second.bundle.bundle, ['
|
|
308
|
-
assert.deepEqual(second.bundle.modules, [...ALWAYS_ON_CORE, '
|
|
299
|
+
assert.deepEqual(second.bundle.bundle, ['discord']);
|
|
300
|
+
assert.deepEqual(second.bundle.modules, [...ALWAYS_ON_CORE, 'discord']);
|
|
309
301
|
assert.equal(second.invites.seeking.limit, 3);
|
|
310
302
|
});
|
|
311
303
|
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
OPTIONAL_MODULES,
|
|
38
38
|
STARTER_BUNDLES,
|
|
39
39
|
HUB_ONLY_MODULES,
|
|
40
|
+
RETIRED_MODULES,
|
|
40
41
|
RULED_TEAM_SHAPES,
|
|
41
42
|
bundleForProjectType,
|
|
42
43
|
allStarterBundles,
|
|
@@ -335,6 +336,9 @@ test('the opt-in catalog declared for the picker IS the on-disk opt-in set', ()
|
|
|
335
336
|
const onDisk = manifestsOnDisk();
|
|
336
337
|
const optInOnDisk = [...onDisk.values()]
|
|
337
338
|
.filter((m) => m.default !== true && !HUB_ONLY_MODULES.includes(m.key))
|
|
339
|
+
// A RETIRED module is still on disk while its removal lands, and must already
|
|
340
|
+
// be gone from the catalog — so it is the one permitted absence (task 1003886).
|
|
341
|
+
.filter((m) => !RETIRED_MODULES.includes(m.key))
|
|
338
342
|
.map((m) => m.key)
|
|
339
343
|
.sort();
|
|
340
344
|
|
|
@@ -346,6 +350,24 @@ test('the opt-in catalog declared for the picker IS the on-disk opt-in set', ()
|
|
|
346
350
|
'the picker\'s catalog has drifted from the manifests — a module was added, removed, or flipped its default');
|
|
347
351
|
});
|
|
348
352
|
|
|
353
|
+
test('a RETIRED module is withdrawn everywhere an owner could still reach it', () => {
|
|
354
|
+
// The retirement is only real if it holds at every offer point at once. A key
|
|
355
|
+
// left in any one of them is still a module a new project can end up with,
|
|
356
|
+
// which is exactly what the withdrawal exists to stop (task 1003886).
|
|
357
|
+
for (const key of RETIRED_MODULES) {
|
|
358
|
+
assert.ok(!OPTIONAL_MODULES.some((m) => m.key === key),
|
|
359
|
+
`retired module "${key}" is still a row the creation picker offers`);
|
|
360
|
+
for (const [type, bundle] of Object.entries(STARTER_BUNDLES)) {
|
|
361
|
+
assert.ok(!bundle.modules.includes(key),
|
|
362
|
+
`retired module "${key}" is still preselected by the "${type}" bundle`);
|
|
363
|
+
}
|
|
364
|
+
// Posting it directly is a real answer that resolves to nothing, not a
|
|
365
|
+
// non-answer: the key is DROPPED, exactly as an unknown one would be.
|
|
366
|
+
assert.deepEqual(normalizeModuleSelection([key]), [],
|
|
367
|
+
`retired module "${key}" is still accepted when an owner posts it directly`);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
349
371
|
test('every opt-in module carries owner-facing copy, and no module jargon rides along', () => {
|
|
350
372
|
for (const row of OPTIONAL_MODULES) {
|
|
351
373
|
assert.ok(row.label && row.label.length <= 40, `"${row.key}" needs a short label`);
|
|
@@ -411,24 +433,24 @@ test('effectiveModules: an answered picker wins over the bundle, including "none
|
|
|
411
433
|
});
|
|
412
434
|
|
|
413
435
|
test('effectiveModules: a skipped picker after an ANSWERED interview yields the TAILORED bundle', () => {
|
|
414
|
-
// BV1.R22 / ADR 0243 — the property ADR 0240
|
|
436
|
+
// BV1.R22 / ADR 0243 — the property ADR 0240 leans the skip on ("the
|
|
415
437
|
// preselect and the fallback are the same declaration"), now that the panel
|
|
416
438
|
// preselects the team-shape-adjusted set. Without this the picker would show a
|
|
417
|
-
//
|
|
418
|
-
const eff = effectiveModules({ type: '
|
|
439
|
+
// community owner one thing and the project would quietly get another.
|
|
440
|
+
const eff = effectiveModules({ type: 'research', modules: null, detail: { team_shape: 'community' } });
|
|
419
441
|
|
|
420
|
-
assert.deepEqual(eff.selected, ['discord'], 'the
|
|
442
|
+
assert.deepEqual(eff.selected, ['agents', 'discord'], 'the gathering place the rule adds is really there');
|
|
421
443
|
assert.equal(eff.source, 'bundle', 'and it is still an UNANSWERED set, not a stored one');
|
|
422
|
-
assert.deepEqual(eff.adjustments.map((a) => a.module), ['
|
|
423
|
-
assert.match(eff.adjustments[0].reason, /
|
|
444
|
+
assert.deepEqual(eff.adjustments.map((a) => a.module), ['discord']);
|
|
445
|
+
assert.match(eff.adjustments[0].reason, /open community/, 'carrying the reason the panel showed');
|
|
424
446
|
});
|
|
425
447
|
|
|
426
448
|
test("effectiveModules: the owner's own answer is never re-adjusted by a rule", () => {
|
|
427
449
|
// A rule corrects a PRESELECT the owner did not make. Once they have toggled,
|
|
428
450
|
// re-applying it would overrule them with an answer they had already refused.
|
|
429
|
-
const eff = effectiveModules({ type: '
|
|
451
|
+
const eff = effectiveModules({ type: 'research', modules: [], detail: { team_shape: 'community' } });
|
|
430
452
|
|
|
431
|
-
assert.deepEqual(eff.selected, [
|
|
453
|
+
assert.deepEqual(eff.selected, [], 'community adds discord, and the owner switched everything off');
|
|
432
454
|
assert.equal(eff.source, 'chosen');
|
|
433
455
|
assert.deepEqual(eff.adjustments, [], 'and no reason is handed back for a choice they made themselves');
|
|
434
456
|
});
|
|
@@ -439,7 +461,7 @@ test('effectiveModules: a detail that says nothing usable adjusts nothing', () =
|
|
|
439
461
|
// importing it.
|
|
440
462
|
for (const detail of [undefined, null, {}, { team_shape: null }, { team_shape: 'a-huge-corporation' }, 'not-an-object', []]) {
|
|
441
463
|
const eff = effectiveModules({ type: 'game', modules: null, detail });
|
|
442
|
-
assert.deepEqual(eff.selected, ['
|
|
464
|
+
assert.deepEqual(eff.selected, ['discord'], `detail=${JSON.stringify(detail)} falls back to the type's bundle`);
|
|
443
465
|
assert.deepEqual(eff.adjustments, []);
|
|
444
466
|
}
|
|
445
467
|
});
|