@bongos/core 1.19.528 → 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 +27 -22
- 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 +2 -0
- 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/projects_hub_module_picker.mjs +217 -4
- package/tests/provisioning_starter_bundles.mjs +90 -1
|
@@ -1493,5 +1493,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1493
1493
|
landed since 1.19.526 with no explicit bump. run 33827382028. (task 1002620)
|
|
1494
1494
|
1.19.528 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1495
1495
|
landed since 1.19.527 with no explicit bump. run 33829339717. (task 1002620)
|
|
1496
|
+
1.19.529 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1497
|
+
landed since 1.19.528 with no explicit bump. run 33877197857. (task 1002620)
|
|
1496
1498
|
---------------------------------------------------------------------------
|
|
1497
1499
|
```
|
|
@@ -39,42 +39,20 @@
|
|
|
39
39
|
|
|
40
40
|
'use strict';
|
|
41
41
|
|
|
42
|
-
const {
|
|
42
|
+
const { bundleForAnswers, TEAM_SHAPE_RULES, ALWAYS_ON_CORE } = require('./starter-bundles');
|
|
43
43
|
const { PROJECT_DETAIL_VOCAB } = require('./provisioning');
|
|
44
44
|
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
45
|
+
// `TEAM_SHAPE_RULES` MOVED to starter-bundles.js (BV1.R22, task 1002341; ADR 0243)
|
|
46
|
+
// and is re-exported here so this file's public surface is unchanged. It went to
|
|
47
|
+
// the bundle layer because a bundle adjustment is not advice about the bundle —
|
|
48
|
+
// it IS the bundle once the owner has answered, and `effectiveModules` is the
|
|
49
|
+
// function ADR 0240 §2 hangs the whole skip contract on. A rule the creation
|
|
50
|
+
// picker applies but that fallback does not is two lists kept in agreement by
|
|
51
|
+
// hand, which is the failure ADR 0240 §2 was written to make impossible.
|
|
51
52
|
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
const TEAM_SHAPE_RULES = Object.freeze([
|
|
56
|
-
Object.freeze({
|
|
57
|
-
id: 'a-community-needs-somewhere-to-gather',
|
|
58
|
-
teamShape: 'community',
|
|
59
|
-
effect: 'add',
|
|
60
|
-
module: 'discord',
|
|
61
|
-
reason: 'You said an open community is building this, so somewhere for it to gather is switched on.',
|
|
62
|
-
}),
|
|
63
|
-
Object.freeze({
|
|
64
|
-
id: 'a-team-shares-one-environment',
|
|
65
|
-
teamShape: 'small-team',
|
|
66
|
-
effect: 'add',
|
|
67
|
-
module: 'dev-box',
|
|
68
|
-
reason: 'A small team gets one shared environment to build in, so nobody is debugging their own laptop.',
|
|
69
|
-
}),
|
|
70
|
-
Object.freeze({
|
|
71
|
-
id: 'building-alone-needs-no-shared-environment',
|
|
72
|
-
teamShape: 'solo',
|
|
73
|
-
effect: 'remove',
|
|
74
|
-
module: 'dev-box',
|
|
75
|
-
reason: 'You are building this alone, so the shared cloud environment is off — your own machine is the setup.',
|
|
76
|
-
}),
|
|
77
|
-
]);
|
|
53
|
+
// What stays HERE is the advice: a governance preset that is only ever previewed,
|
|
54
|
+
// and an invite SEEK nothing in this module may run. Neither is a thing a project
|
|
55
|
+
// silently ends up with, so neither belongs in the read-time resolution.
|
|
78
56
|
|
|
79
57
|
// The governance presets, by key. A preset is a NAME plus the one sentence that
|
|
80
58
|
// says what living under it feels like — never a set of roles or permissions,
|
|
@@ -167,25 +145,6 @@ function normalizeTeamShape(value) {
|
|
|
167
145
|
return PROJECT_DETAIL_VOCAB.team_shape.includes(value) ? value : null;
|
|
168
146
|
}
|
|
169
147
|
|
|
170
|
-
// Apply the team-shape rules to one type's opt-in bundle. Returns the adjusted
|
|
171
|
-
// list plus the adjustments that ACTUALLY changed it — a rule whose module the
|
|
172
|
-
// bundle already carries (or already lacks) records nothing, because a stated
|
|
173
|
-
// reason for a change that did not happen is a lie the owner cannot check.
|
|
174
|
-
// Never touches the always-on core. PURE.
|
|
175
|
-
function adjustBundle(bundle, teamShape) {
|
|
176
|
-
const modules = [...bundle];
|
|
177
|
-
const adjustments = [];
|
|
178
|
-
for (const rule of TEAM_SHAPE_RULES) {
|
|
179
|
-
if (rule.teamShape !== teamShape) continue;
|
|
180
|
-
const at = modules.indexOf(rule.module);
|
|
181
|
-
if (rule.effect === 'add' && at === -1) modules.push(rule.module);
|
|
182
|
-
else if (rule.effect === 'remove' && at !== -1) modules.splice(at, 1);
|
|
183
|
-
else continue;
|
|
184
|
-
adjustments.push({ rule: rule.id, effect: rule.effect, module: rule.module, reason: rule.reason });
|
|
185
|
-
}
|
|
186
|
-
return { modules, adjustments };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
148
|
// The whole recommendation for one set of creation answers:
|
|
190
149
|
//
|
|
191
150
|
// {
|
|
@@ -211,8 +170,10 @@ function adjustBundle(bundle, teamShape) {
|
|
|
211
170
|
function recommendationsForDetails(answers) {
|
|
212
171
|
const given = answers && typeof answers === 'object' && !Array.isArray(answers) ? answers : {};
|
|
213
172
|
const teamShape = normalizeTeamShape(given.team_shape);
|
|
214
|
-
|
|
215
|
-
|
|
173
|
+
// The SAME resolver the picker preselects from and effectiveModules falls back
|
|
174
|
+
// to (ADR 0243) — so the recommendation, the panel and the project a skipped
|
|
175
|
+
// picker ends up with are one function's answer, not three that agree.
|
|
176
|
+
const preset = bundleForAnswers(given.type, teamShape);
|
|
216
177
|
|
|
217
178
|
const gov = GOVERNANCE_RULES.find((r) => r.teamShape === teamShape) || GOVERNANCE_DEFAULT;
|
|
218
179
|
const invite = INVITE_RULES.find((r) => r.teamShape === teamShape) || INVITE_DEFAULT;
|
|
@@ -226,9 +187,9 @@ function recommendationsForDetails(answers) {
|
|
|
226
187
|
label: preset.label,
|
|
227
188
|
summary: preset.summary,
|
|
228
189
|
core: [...ALWAYS_ON_CORE],
|
|
229
|
-
bundle:
|
|
230
|
-
modules:
|
|
231
|
-
adjustments,
|
|
190
|
+
bundle: preset.bundle,
|
|
191
|
+
modules: preset.modules,
|
|
192
|
+
adjustments: preset.adjustments,
|
|
232
193
|
},
|
|
233
194
|
governance: {
|
|
234
195
|
preset: gov.preset,
|
|
@@ -36,7 +36,7 @@ const auth = api;
|
|
|
36
36
|
const { pool, validateOrRespond, parseId } = api;
|
|
37
37
|
const provisioning = require('../provisioning');
|
|
38
38
|
const { onboardPlan, stepsForShape } = require('../onboard-plan');
|
|
39
|
-
const { ALWAYS_ON_CORE, OPTIONAL_MODULES,
|
|
39
|
+
const { ALWAYS_ON_CORE, OPTIONAL_MODULES, starterBundleAnswerTable, optionalModules, effectiveModules } = require('../starter-bundles');
|
|
40
40
|
const { recommendationsForDetails } = require('../recommendations');
|
|
41
41
|
const { makeAskRateLimiter, clientIp } = require('../rate-limit');
|
|
42
42
|
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
@@ -840,6 +840,8 @@ module.exports = function provisioningRoutes() {
|
|
|
840
840
|
// project is now on the map, and one who cleared it learns it just left.
|
|
841
841
|
// Re-deriving it from a follow-up GET would let the two answers disagree.
|
|
842
842
|
publish: publishVerdict(updated),
|
|
843
|
+
// `team_shape` MOVES a bundle-sourced project's modules — effect with cause (ADR 0243 §5).
|
|
844
|
+
modules: effectiveModules(updated),
|
|
843
845
|
});
|
|
844
846
|
} catch (err) {
|
|
845
847
|
log.error('[provisioning] PATCH /provisioning/instances/:id/detail', err);
|
|
@@ -1337,8 +1339,10 @@ module.exports = function provisioningRoutes() {
|
|
|
1337
1339
|
// row for. The alternatives were both worse: `GET /modules` needs a session
|
|
1338
1340
|
// (this wizard is read signed-out) and `GET /public/modules` serves booleans,
|
|
1339
1341
|
// not copy. Additive — `core` and `bundles` keep their shape.
|
|
1342
|
+
// `byTeamShape` re-resolves each bundle for every answer the About step can give,
|
|
1343
|
+
// riding THIS payload rather than a second read (task 1002341; ADR 0243 §4).
|
|
1340
1344
|
router.get('/provisioning/starter-bundles', (req, res) => {
|
|
1341
|
-
res.json({ core: [...ALWAYS_ON_CORE], bundles:
|
|
1345
|
+
res.json({ core: [...ALWAYS_ON_CORE], bundles: starterBundleAnswerTable(), optional: optionalModules() });
|
|
1342
1346
|
});
|
|
1343
1347
|
|
|
1344
1348
|
// GET /provisioning/recommendations?type=<project type>&team_shape=<interview answer>
|
|
@@ -106,6 +106,60 @@ const OPTIONAL_MODULES = Object.freeze([
|
|
|
106
106
|
// so the declaration above stays the single place a choice is written down.
|
|
107
107
|
const OPTIONAL_MODULE_KEYS = Object.freeze(OPTIONAL_MODULES.map((m) => m.key));
|
|
108
108
|
|
|
109
|
+
// How the interview's team-shape answer ADJUSTS the type's starter bundle, in
|
|
110
|
+
// application order. Each entry is one condition (`teamShape`) and one effect on
|
|
111
|
+
// ONE opt-in module, carrying the sentence the picker shows when it fires.
|
|
112
|
+
//
|
|
113
|
+
// WHY THE TABLE LIVES HERE and not in recommendations.js beside the governance
|
|
114
|
+
// and invite tables it shipped with (task 1002336, ADR 0239). A bundle adjustment
|
|
115
|
+
// is not advice about the bundle — it IS the bundle, once the owner has answered.
|
|
116
|
+
// `effectiveModules` below is the function ADR 0240 §2 leans the whole skip
|
|
117
|
+
// contract on ("the preselect and the fallback are the same declaration — one
|
|
118
|
+
// function, called twice"), and a rule the picker applies but that function does
|
|
119
|
+
// not is exactly two lists kept in agreement by hand. So the rules sit at the
|
|
120
|
+
// bundle layer, `effectiveModules` applies them, and recommendations.js re-exports
|
|
121
|
+
// them unchanged — its governance preview and invite seam are genuinely advice and
|
|
122
|
+
// stay there. Decision: ADR 0243 (BV1.R22, task 1002341).
|
|
123
|
+
//
|
|
124
|
+
// Deliberately tiny: ADR 0237 already made the per-type preset the opinion, and
|
|
125
|
+
// today's opt-in catalog is three modules wide — so this table's job is to correct
|
|
126
|
+
// a preset the owner's answer contradicts, not to re-derive it.
|
|
127
|
+
//
|
|
128
|
+
// A rule may only ever name an OPT-IN module. Adding or removing an always-on one
|
|
129
|
+
// would be a toggle that toggles nothing (ADR 0237 §3's argument, and
|
|
130
|
+
// `tests/provisioning_recommendations.mjs` fails the build over it).
|
|
131
|
+
const TEAM_SHAPE_RULES = Object.freeze([
|
|
132
|
+
Object.freeze({
|
|
133
|
+
id: 'a-community-needs-somewhere-to-gather',
|
|
134
|
+
teamShape: 'community',
|
|
135
|
+
effect: 'add',
|
|
136
|
+
module: 'discord',
|
|
137
|
+
reason: 'You said an open community is building this, so somewhere for it to gather is switched on.',
|
|
138
|
+
}),
|
|
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
|
+
]);
|
|
154
|
+
|
|
155
|
+
// The team shapes the rules above can fire on. Derived from the table rather than
|
|
156
|
+
// imported from provisioning.js's PROJECT_DETAIL_VOCAB, because importing it would
|
|
157
|
+
// point this file UP at the module that requires it. It costs nothing to derive:
|
|
158
|
+
// an off-vocabulary shape matches no rule and adjusts nothing, which is already
|
|
159
|
+
// the right answer — `tests/provisioning_recommendations.mjs` pins the two lists
|
|
160
|
+
// against each other so a fourth interview answer cannot arrive unhandled.
|
|
161
|
+
const RULED_TEAM_SHAPES = Object.freeze([...new Set(TEAM_SHAPE_RULES.map((r) => r.teamShape))]);
|
|
162
|
+
|
|
109
163
|
// The curated presets, keyed by the declared project type (the closed vocabulary
|
|
110
164
|
// owner-decided 2026-08-13 and mirrored in provisioning.PROJECT_TYPES + the
|
|
111
165
|
// provisioning_007 CHECK). `label` and `summary` are the owner-facing copy the
|
|
@@ -192,6 +246,69 @@ function optionalModules() {
|
|
|
192
246
|
return OPTIONAL_MODULES.map((m) => ({ ...m }));
|
|
193
247
|
}
|
|
194
248
|
|
|
249
|
+
// Apply the team-shape rules to one type's opt-in bundle. Returns the adjusted
|
|
250
|
+
// list plus the adjustments that ACTUALLY changed it — a rule whose module the
|
|
251
|
+
// bundle already carries (or already lacks) records nothing, because a stated
|
|
252
|
+
// reason for a change that did not happen is a lie the owner cannot check.
|
|
253
|
+
// A null/unknown/off-vocabulary shape fires no rule and adjusts nothing, which is
|
|
254
|
+
// the same answer `normalizeProjectDetail` gives such a value on the way in.
|
|
255
|
+
// Never touches the always-on core. PURE.
|
|
256
|
+
function adjustBundle(bundle, teamShape) {
|
|
257
|
+
const held = new Set(bundle);
|
|
258
|
+
const adjustments = [];
|
|
259
|
+
for (const rule of TEAM_SHAPE_RULES) {
|
|
260
|
+
if (rule.teamShape !== teamShape) continue;
|
|
261
|
+
if (rule.effect === 'add' && !held.has(rule.module)) held.add(rule.module);
|
|
262
|
+
else if (rule.effect === 'remove' && held.has(rule.module)) held.delete(rule.module);
|
|
263
|
+
else continue;
|
|
264
|
+
adjustments.push({ rule: rule.id, effect: rule.effect, module: rule.module, reason: rule.reason });
|
|
265
|
+
}
|
|
266
|
+
// CATALOG order, not append order — `normalizeModuleSelection` stores a chosen
|
|
267
|
+
// set this way and the picker re-derives its toggles this way, so an adjusted
|
|
268
|
+
// bundle that appended would make the same set of modules read as a different
|
|
269
|
+
// list depending on whether the owner answered the picker or skipped it.
|
|
270
|
+
return { modules: OPTIONAL_MODULE_KEYS.filter((k) => held.has(k)), adjustments };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// What the creation flow preselects for one set of ANSWERS — `bundleForProjectType`
|
|
274
|
+
// with the team-shape rules applied, in the same `{ type, label, summary, core,
|
|
275
|
+
// bundle, modules }` shape plus the `adjustments` delta that got it there.
|
|
276
|
+
//
|
|
277
|
+
// This is the single resolver the picker paints from and `effectiveModules` falls
|
|
278
|
+
// back to, so an owner who answers the interview and then skips the picker gets
|
|
279
|
+
// the set the panel was showing them (ADR 0240 §2's property, extended to the
|
|
280
|
+
// interview's answer — BV1.R22, ADR 0243). PURE; arrays are fresh.
|
|
281
|
+
function bundleForAnswers(type, teamShape) {
|
|
282
|
+
const preset = bundleForProjectType(type);
|
|
283
|
+
const { modules, adjustments } = adjustBundle(preset.bundle, teamShape);
|
|
284
|
+
return {
|
|
285
|
+
...preset,
|
|
286
|
+
bundle: modules,
|
|
287
|
+
modules: [...ALWAYS_ON_CORE, ...modules],
|
|
288
|
+
adjustments,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Every curated bundle resolved for every team shape the rules can fire on, plus
|
|
293
|
+
// the no-answer resolution — the whole preselect table in ONE read.
|
|
294
|
+
//
|
|
295
|
+
// The picker needs this shape rather than a per-answer query because ADR 0240 §4's
|
|
296
|
+
// rule is that what may be chosen and what is preselected have to arrive together
|
|
297
|
+
// or the panel can preselect a key it has no row for. The owner flips the type
|
|
298
|
+
// question and the team-shape question freely on two earlier panels; re-preselecting
|
|
299
|
+
// must not cost a round trip, and a round trip it did pay could land after they had
|
|
300
|
+
// moved on. PURE — five types by four answers is twenty small rows.
|
|
301
|
+
function starterBundleAnswerTable() {
|
|
302
|
+
return allStarterBundles().map((preset) => {
|
|
303
|
+
const byTeamShape = {};
|
|
304
|
+
for (const shape of RULED_TEAM_SHAPES) {
|
|
305
|
+
const { modules, adjustments } = adjustBundle(preset.bundle, shape);
|
|
306
|
+
byTeamShape[shape] = { bundle: modules, adjustments };
|
|
307
|
+
}
|
|
308
|
+
return { ...preset, byTeamShape };
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
195
312
|
// A MODULE SELECTION as it may be STORED (BV1.R20, task 1002339): opt-in keys
|
|
196
313
|
// only, deduped, in catalog order. Everything else is dropped — an always-on
|
|
197
314
|
// module (it was never a choice), a hub-only one (the control plane's own), and
|
|
@@ -218,22 +335,39 @@ function normalizeModuleSelection(input) {
|
|
|
218
335
|
|
|
219
336
|
// What a row's modules READ as: the always-on floor, plus whatever the owner
|
|
220
337
|
// chose — or, when they never answered, the starter bundle for their declared
|
|
221
|
-
// type. `source` says which of the two
|
|
222
|
-
// empty set from an unanswered one without
|
|
338
|
+
// type ADJUSTED by what they told the interview. `source` says which of the two
|
|
339
|
+
// it is, so a surface can tell a chosen empty set from an unanswered one without
|
|
340
|
+
// re-deriving the rule.
|
|
223
341
|
//
|
|
224
342
|
// This is what makes skipping the picker a supported default rather than a
|
|
225
343
|
// project with nothing on it: the flow's preselect and the server's fallback are
|
|
226
344
|
// the SAME declaration, so the owner gets exactly the set the panel showed them.
|
|
227
|
-
//
|
|
345
|
+
// The team-shape adjustment is INSIDE that fallback (BV1.R22, ADR 0243) for
|
|
346
|
+
// exactly that reason — the picker paints the adjusted set, so a picker skipped
|
|
347
|
+
// after an answered interview has to resolve to the adjusted set too, or the
|
|
348
|
+
// panel's promise stops being true the moment the interview is answered.
|
|
349
|
+
//
|
|
350
|
+
// Reads `detail.team_shape` straight off the row rather than through
|
|
351
|
+
// provisioning.js's `normalizeProjectDetail` — importing it would point this file
|
|
352
|
+
// up at the module that requires it, and it would buy nothing: an off-vocabulary
|
|
353
|
+
// shape fires no rule and adjusts nothing, which is the answer the normalizer
|
|
354
|
+
// would have produced anyway.
|
|
355
|
+
//
|
|
356
|
+
// PURE — takes a row-shaped `{ type, modules, detail }`, touches no database.
|
|
228
357
|
function effectiveModules(row) {
|
|
229
358
|
const chosen = normalizeModuleSelection(row && row.modules);
|
|
230
|
-
const
|
|
359
|
+
const detail = row && row.detail && typeof row.detail === 'object' ? row.detail : null;
|
|
360
|
+
const preset = bundleForAnswers(row && row.type, detail ? detail.team_shape : null);
|
|
231
361
|
const selected = chosen === null ? preset.bundle : chosen;
|
|
232
362
|
return {
|
|
233
363
|
core: [...ALWAYS_ON_CORE],
|
|
234
364
|
selected,
|
|
235
365
|
modules: [...ALWAYS_ON_CORE, ...selected],
|
|
236
366
|
source: chosen === null ? 'bundle' : 'chosen',
|
|
367
|
+
// Why the unanswered set is what it is, in the owner's own words — empty when
|
|
368
|
+
// nothing adjusted it, and never populated for a `chosen` set, because an
|
|
369
|
+
// owner's own toggle needs no reason given back to them.
|
|
370
|
+
adjustments: chosen === null ? preset.adjustments : [],
|
|
237
371
|
};
|
|
238
372
|
}
|
|
239
373
|
|
|
@@ -242,9 +376,14 @@ module.exports = {
|
|
|
242
376
|
HUB_ONLY_MODULES,
|
|
243
377
|
OPTIONAL_MODULES,
|
|
244
378
|
STARTER_BUNDLES,
|
|
379
|
+
TEAM_SHAPE_RULES,
|
|
380
|
+
RULED_TEAM_SHAPES,
|
|
245
381
|
bundleForProjectType,
|
|
246
382
|
allStarterBundles,
|
|
247
383
|
optionalModules,
|
|
384
|
+
adjustBundle,
|
|
385
|
+
bundleForAnswers,
|
|
386
|
+
starterBundleAnswerTable,
|
|
248
387
|
normalizeModuleSelection,
|
|
249
388
|
effectiveModules,
|
|
250
389
|
};
|
|
@@ -328,6 +328,14 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
328
328
|
.tpill:hover::before{border-color:var(--ink-faint);}
|
|
329
329
|
.tpill[aria-pressed=true]{background:none;border-color:var(--rule-soft);color:var(--ink);}
|
|
330
330
|
.tpill[aria-pressed=true]::before{background:var(--accent);border-color:var(--accent);outline:4px solid color-mix(in srgb,var(--accent) 22%,transparent);}
|
|
331
|
+
/* Why the picker preselected what it did (task 1002341) — one line per rule that
|
|
332
|
+
moved something, between the band and the always-on clause. Wears .reassure's
|
|
333
|
+
weight on purpose: this is the panel explaining itself, never a thing to act on,
|
|
334
|
+
so it must not compete with the toggles it is explaining. */
|
|
335
|
+
.modWhy{display:none;}
|
|
336
|
+
.modWhy.on{display:grid;gap:6px;margin:0 0 14px;max-width:62ch;}
|
|
337
|
+
.modWhy span{font-size:13.5px;font-weight:400;color:var(--ink-soft);line-height:1.5;}
|
|
338
|
+
|
|
331
339
|
/* the module picker (task 1002339) wears the SAME band as the type and team
|
|
332
340
|
questions — one hairline cell per choice, the dot on the chosen ones. The only
|
|
333
341
|
difference is that a module carries a one-line summary under its name, so its
|
|
@@ -1017,11 +1025,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1017
1025
|
to make, when in fact they were never choices. -->
|
|
1018
1026
|
<section class="panel" id="panel-modules" aria-labelledby="mod-h">
|
|
1019
1027
|
<h1 id="mod-h">What it comes with</h1>
|
|
1020
|
-
<p class="lede">Optional — skip it and you get the suggested set below. These are extras on top of everything every project already has, and you can switch any of them on or off later.</p>
|
|
1028
|
+
<p class="lede">Optional — skip it and you get the suggested set below, shaped by what you just told us. These are extras on top of everything every project already has, and you can switch any of them on or off later.</p>
|
|
1021
1029
|
<form data-next="5">
|
|
1022
1030
|
<div class="wizField"><span id="modLbl">Extras for this project</span>
|
|
1023
1031
|
<div class="typeCards" id="modCards" role="group" aria-labelledby="modLbl"></div>
|
|
1024
1032
|
</div>
|
|
1033
|
+
<div class="modWhy" id="modWhy" aria-live="polite"></div>
|
|
1025
1034
|
<div class="reassure" id="modCoreNote"></div>
|
|
1026
1035
|
<div class="hint" id="h-modules"> </div>
|
|
1027
1036
|
<div class="nav">
|
|
@@ -3204,10 +3213,39 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3204
3213
|
if (state.step === 4) renderModules();
|
|
3205
3214
|
else if (state.step === 7) renderReview();
|
|
3206
3215
|
}
|
|
3216
|
+
/* The row for the declared type, or null before the read lands / for a type
|
|
3217
|
+
the table has no row for (including no type at all). */
|
|
3218
|
+
function bundleRow() {
|
|
3219
|
+
if (!starterBundles) return null;
|
|
3220
|
+
return (starterBundles.bundles || []).filter(function (b) { return b.type === state.projType; })[0] || null;
|
|
3221
|
+
}
|
|
3222
|
+
/* The type's preselect AS THE ABOUT STEP'S ANSWER SHAPES IT (task 1002341,
|
|
3223
|
+
BV1.R22). The server resolved every combination into byTeamShape, so
|
|
3224
|
+
answering "just me" three panels back re-preselects here with no round trip
|
|
3225
|
+
and no rule applied twice in two languages — the wizard reads the same
|
|
3226
|
+
table `effectiveModules` falls back to (ADR 0243).
|
|
3227
|
+
|
|
3228
|
+
An older payload without byTeamShape, or a team shape no rule fires on,
|
|
3229
|
+
falls through to the unadjusted bundle: the answer this panel showed before
|
|
3230
|
+
R22, which is never wrong, only untailored. */
|
|
3231
|
+
function tailoredRow() {
|
|
3232
|
+
var row = bundleRow();
|
|
3233
|
+
if (!row) return null;
|
|
3234
|
+
var forShape = row.byTeamShape && state.teamShape ? row.byTeamShape[state.teamShape] : null;
|
|
3235
|
+
return forShape || { bundle: row.bundle || [], adjustments: [] };
|
|
3236
|
+
}
|
|
3207
3237
|
function preselectedModules() {
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3238
|
+
var row = tailoredRow();
|
|
3239
|
+
return row ? row.bundle.slice() : [];
|
|
3240
|
+
}
|
|
3241
|
+
/* Why the preselect is what it is, in the owner's own words — one sentence per
|
|
3242
|
+
rule that ACTUALLY moved something. Empty when nothing did, and empty once
|
|
3243
|
+
the owner has answered for themselves: a reason given back for a toggle they
|
|
3244
|
+
flipped reads as the panel arguing with them. */
|
|
3245
|
+
function preselectReasons() {
|
|
3246
|
+
if (state.modules !== null) return [];
|
|
3247
|
+
var row = tailoredRow();
|
|
3248
|
+
return row ? (row.adjustments || []) : [];
|
|
3211
3249
|
}
|
|
3212
3250
|
/* What the panel paints and the review names: the owner's answer if they
|
|
3213
3251
|
gave one, otherwise the type's preselect. */
|
|
@@ -3233,6 +3271,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3233
3271
|
a moment; only a read that ANSWERED badly gets the clause, so a slow
|
|
3234
3272
|
network never tells the owner something is broken when it isn't. */
|
|
3235
3273
|
band.innerHTML = '';
|
|
3274
|
+
$('modWhy').innerHTML = '';
|
|
3275
|
+
$('modWhy').classList.remove('on');
|
|
3236
3276
|
note.textContent = bundlesFailed
|
|
3237
3277
|
? 'We couldn’t load the list of extras just now. Your project is created with everything it needs either way, and you can add extras from its settings.'
|
|
3238
3278
|
: '';
|
|
@@ -3245,6 +3285,17 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3245
3285
|
+ (on.indexOf(m.key) === -1 ? 'false' : 'true') + '"><span><b>' + esc(m.label)
|
|
3246
3286
|
+ '</b><small>' + esc(m.summary) + '</small></span></button>';
|
|
3247
3287
|
}).join('');
|
|
3288
|
+
/* The tailoring, said out loud (task 1002341). A preselect an owner cannot
|
|
3289
|
+
see the reason for is one they have to audit, which is the thing ADR 0237
|
|
3290
|
+
says is worse than no preselect at all. Rendered as its own block above the
|
|
3291
|
+
core clause, because it is about THIS project's answers while the clause
|
|
3292
|
+
below is about every project. */
|
|
3293
|
+
var why = $('modWhy');
|
|
3294
|
+
var reasons = preselectReasons();
|
|
3295
|
+
why.innerHTML = reasons.length
|
|
3296
|
+
? reasons.map(function (a) { return '<span>' + esc(a.reason) + '</span>'; }).join('')
|
|
3297
|
+
: '';
|
|
3298
|
+
why.classList.toggle('on', reasons.length > 0);
|
|
3248
3299
|
var core = (starterBundles.core || []).length;
|
|
3249
3300
|
note.textContent = core
|
|
3250
3301
|
? 'Every project also starts with the same ' + core + ' core parts — the platform itself, the way it tracks work, and the builders’ hall. They are always on, so they are not part of this choice.'
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.529",
|
|
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.529",
|
|
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.529",
|
|
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",
|
package/src/module-api.js
CHANGED
|
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
45
45
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
46
46
|
// the entry to that file. Look for a version's history there, not here.
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
const CORE_VERSION = '1.19.
|
|
48
|
+
const CORE_VERSION = '1.19.529'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
49
49
|
|
|
50
50
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
51
51
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|