@bongos/core 1.19.671 → 1.19.673

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.
@@ -1801,5 +1801,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1801
1801
  landed since 1.19.669 with no explicit bump. run 34622305231. (task 1002620)
1802
1802
  1.19.671 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1803
1803
  landed since 1.19.670 with no explicit bump. run 34627255469. (task 1002620)
1804
+ 1.19.672 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1805
+ landed since 1.19.671 with no explicit bump. run 34629971417. (task 1002620)
1806
+ 1.19.673 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1807
+ landed since 1.19.672 with no explicit bump. run 34632328471. (task 1002620)
1804
1808
  ---------------------------------------------------------------------------
1805
1809
  ```
@@ -694,7 +694,7 @@ async function updateInstanceSettings(db, id, patch) {
694
694
  // impossible — the very half of "add modules later" an owner needs most. Only an
695
695
  // array ever reaches here, so the column's NULL ("never asked", ADR 0240 §2)
696
696
  // stays reachable only by never answering; no write can un-ask the question.
697
- async function updateInstanceDetail(db, id, { description, detail, modules } = {}) {
697
+ async function updateInstanceDetail(db, id, { description, detail, modules, type } = {}) {
698
698
  const sets = ['updated_at = now()'];
699
699
  const params = [id];
700
700
  if (description !== undefined) {
@@ -713,6 +713,18 @@ async function updateInstanceDetail(db, id, { description, detail, modules } = {
713
713
  params.push(picked === null ? null : JSON.stringify(picked));
714
714
  sets.push(`modules = $${params.length}::jsonb`);
715
715
  }
716
+ // The declared type (task 1003504 — the way back into the wizard's Type step).
717
+ // ASSIGNED, never COALESCEd: this is the owner re-declaring, so the new word has
718
+ // to win even over a curated one. The route is the only caller and it refuses
719
+ // anything outside PROJECT_TYPES, null included — 'not-sure' is the vocabulary's
720
+ // own way to say "I would rather not", so a null here would be a second spelling
721
+ // of an answer that already exists, and the catalog projection could not carry it
722
+ // (it reads the type as COALESCE(new, old), so a null would leave the map showing
723
+ // the declaration the owner just took back).
724
+ if (type !== undefined) {
725
+ params.push(type);
726
+ sets.push(`type = $${params.length}`);
727
+ }
716
728
  const { rows } = await db.query(
717
729
  `UPDATE provisioning_instances SET ${sets.join(', ')} WHERE id = $1 RETURNING *`,
718
730
  params
@@ -0,0 +1,94 @@
1
+ // modules/provisioning/routes/body-validators.js — the LOUD body validators the
2
+ // provisioning routes share (task 1003504).
3
+ //
4
+ // Lifted out of routes/provisioning.js when that file hit the 1500-line size ratchet,
5
+ // the same way ./callback-page.js was. These three are a natural group and the cheapest
6
+ // thing in the file to move: PURE functions with no express, no pool and no auth in
7
+ // them, each answering one question — "is this body field a mistake, and what do I tell
8
+ // the caller?" — and each returning null when clean or a { code, message } to fail with.
9
+ //
10
+ // They exist because the kernel's strict body validator cannot do their jobs: it reaches
11
+ // top-level fields only, so a typo INSIDE `detail` or `modules` would be dropped in
12
+ // silence on the way to storage. Silence is the wrong answer at the boundary.
13
+ //
14
+ // Both the create route and PATCH …/detail run them, so the two can never drift.
15
+
16
+ const provisioning = require('../provisioning');
17
+ const { ALWAYS_ON_CORE, OPTIONAL_MODULES } = require('../starter-bundles');
18
+
19
+ // The project-detail answers, validated LOUDLY (task 1002334). `detail` arrives as
20
+ // a nested object, and the strict body validator only reaches top-level fields — so
21
+ // without this a typo'd answer key would be dropped by normalizeProjectDetail and
22
+ // the caller would be told nothing, which is exactly the silent-drop failure the
23
+ // strict validator exists to prevent (and that a re-request's type_ignored receipt
24
+ // was written to avoid). Returns null when clean, or a { code, message } to fail
25
+ // with. Both the create route and PATCH …/detail run it, so they cannot drift.
26
+ function badProjectDetail(detail) {
27
+ if (detail === undefined || detail === null) return null;
28
+ if (typeof detail !== 'object' || Array.isArray(detail)) {
29
+ return { code: 'bad_detail', message: 'detail must be an object of answers.' };
30
+ }
31
+ const vocab = provisioning.PROJECT_DETAIL_VOCAB;
32
+ for (const [key, value] of Object.entries(detail)) {
33
+ if (!Object.prototype.hasOwnProperty.call(vocab, key)) {
34
+ return { code: 'bad_detail', message: `Unknown detail answer "${key}" — the interview asks: ${Object.keys(vocab).join(', ')}.` };
35
+ }
36
+ // An explicit null is the UN-ANSWER, not an off-vocabulary value (task 1003577):
37
+ // the merge-patch way back to "never answered". Only null — an empty string or any
38
+ // other sentinel stays a typo, so a caller trying to SET one is never silently
39
+ // read as having erased their answer.
40
+ if (value === null) continue;
41
+ if (!vocab[key].includes(value)) {
42
+ return { code: 'bad_detail', message: `"${key}" must be one of: ${vocab[key].join(', ')} — or null to un-answer it.` };
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+
48
+ // The creation picker's module selection, validated LOUDLY (BV1.R20, task
49
+ // 1002339) — badProjectDetail's rule applied to a list. The strict body validator
50
+ // checks that `modules` is an array of strings; it cannot check MEMBERSHIP, and
51
+ // normalizeModuleSelection drops an unknown key silently on the way to storage.
52
+ // Silence is the wrong answer at the boundary: an API caller who names a module
53
+ // that does not exist, or one of the always-on / hub-only ones, has made a
54
+ // mistake and would otherwise be told nothing and handed a project without it.
55
+ // Returns null when clean, or a { code, message } to fail with.
56
+ function badModuleSelection(modules) {
57
+ if (modules === undefined || modules === null) return null;
58
+ if (!Array.isArray(modules)) {
59
+ return { code: 'bad_modules', message: 'modules must be an array of module keys.' };
60
+ }
61
+ const offered = OPTIONAL_MODULES.map((m) => m.key);
62
+ for (const key of modules) {
63
+ if (offered.includes(key)) continue;
64
+ if (ALWAYS_ON_CORE.includes(key)) {
65
+ return { code: 'bad_modules', message: `"${key}" is always on for every project — it is not one of the modules to choose.` };
66
+ }
67
+ return { code: 'bad_modules', message: `Unknown module "${key}" — the choices are: ${offered.join(', ')}.` };
68
+ }
69
+ return null;
70
+ }
71
+
72
+ // The declared project type, validated LOUDLY (task 1003504) — badProjectDetail's rule
73
+ // applied to a single closed-vocabulary value. Returns null when clean, or a
74
+ // { code, message } to fail with.
75
+ //
76
+ // NULL IS REFUSED, and this is the one place the two "un-answer" conventions on
77
+ // PATCH …/detail part company. `detail: { key: null }` un-answers a question because
78
+ // that vocabulary has no word for "never said". The TYPE vocabulary has one —
79
+ // 'not-sure' — so null would only be a second spelling of an answer that already
80
+ // exists. It would also be a lie on the map: the catalog projection carries the type
81
+ // as COALESCE(EXCLUDED.type, existing.type), so a null would leave the row showing the
82
+ // declaration the owner had just taken back, with no second write to correct it.
83
+ function badProjectType(type) {
84
+ if (type === undefined) return null;
85
+ if (type === null) {
86
+ return { code: 'bad_type', message: 'A project always has a kind — send "not-sure" to take the declaration back, not null.' };
87
+ }
88
+ if (!provisioning.PROJECT_TYPES.includes(type)) {
89
+ return { code: 'bad_type', message: `Unknown project type "${type}" — the choices are: ${provisioning.PROJECT_TYPES.join(', ')}.` };
90
+ }
91
+ return null;
92
+ }
93
+
94
+ module.exports = { badProjectDetail, badModuleSelection, badProjectType };
@@ -55,6 +55,8 @@ const { requireAuthorityForMeteredShape } = require('../paid-shape-gate');
55
55
  const { adoptCredentialFault } = require('../credential-preflight');
56
56
  const catalogBridge = require('../catalog-bridge');
57
57
  const { callbackPage } = require('./callback-page');
58
+ // the LOUD body validators (task 1003504) — lifted out when this file hit the size ratchet
59
+ const { badProjectDetail, badModuleSelection, badProjectType } = require('./body-validators');
58
60
  // task 1003208: structured logging (pino via the doorway) — was console.*.
59
61
  const log = api.logger('provisioning');
60
62
 
@@ -107,59 +109,6 @@ function ownerFromTargetRef(ref) {
107
109
  return m ? m[1] : null;
108
110
  }
109
111
 
110
- // The project-detail answers, validated LOUDLY (task 1002334). `detail` arrives as
111
- // a nested object, and the strict body validator only reaches top-level fields — so
112
- // without this a typo'd answer key would be dropped by normalizeProjectDetail and
113
- // the caller would be told nothing, which is exactly the silent-drop failure the
114
- // strict validator exists to prevent (and that a re-request's type_ignored receipt
115
- // was written to avoid). Returns null when clean, or a { code, message } to fail
116
- // with. Both the create route and PATCH …/detail run it, so they cannot drift.
117
- function badProjectDetail(detail) {
118
- if (detail === undefined || detail === null) return null;
119
- if (typeof detail !== 'object' || Array.isArray(detail)) {
120
- return { code: 'bad_detail', message: 'detail must be an object of answers.' };
121
- }
122
- const vocab = provisioning.PROJECT_DETAIL_VOCAB;
123
- for (const [key, value] of Object.entries(detail)) {
124
- if (!Object.prototype.hasOwnProperty.call(vocab, key)) {
125
- return { code: 'bad_detail', message: `Unknown detail answer "${key}" — the interview asks: ${Object.keys(vocab).join(', ')}.` };
126
- }
127
- // An explicit null is the UN-ANSWER, not an off-vocabulary value (task 1003577):
128
- // the merge-patch way back to "never answered". Only null — an empty string or any
129
- // other sentinel stays a typo, so a caller trying to SET one is never silently
130
- // read as having erased their answer.
131
- if (value === null) continue;
132
- if (!vocab[key].includes(value)) {
133
- return { code: 'bad_detail', message: `"${key}" must be one of: ${vocab[key].join(', ')} — or null to un-answer it.` };
134
- }
135
- }
136
- return null;
137
- }
138
-
139
- // The creation picker's module selection, validated LOUDLY (BV1.R20, task
140
- // 1002339) — badProjectDetail's rule applied to a list. The strict body validator
141
- // checks that `modules` is an array of strings; it cannot check MEMBERSHIP, and
142
- // normalizeModuleSelection drops an unknown key silently on the way to storage.
143
- // Silence is the wrong answer at the boundary: an API caller who names a module
144
- // that does not exist, or one of the always-on / hub-only ones, has made a
145
- // mistake and would otherwise be told nothing and handed a project without it.
146
- // Returns null when clean, or a { code, message } to fail with.
147
- function badModuleSelection(modules) {
148
- if (modules === undefined || modules === null) return null;
149
- if (!Array.isArray(modules)) {
150
- return { code: 'bad_modules', message: 'modules must be an array of module keys.' };
151
- }
152
- const offered = OPTIONAL_MODULES.map((m) => m.key);
153
- for (const key of modules) {
154
- if (offered.includes(key)) continue;
155
- if (ALWAYS_ON_CORE.includes(key)) {
156
- return { code: 'bad_modules', message: `"${key}" is always on for every project — it is not one of the modules to choose.` };
157
- }
158
- return { code: 'bad_modules', message: `Unknown module "${key}" — the choices are: ${offered.join(', ')}.` };
159
- }
160
- return null;
161
- }
162
-
163
112
  // Register this module's kernel seam (ADR 0083 / the registerBoxSeams precedent).
164
113
  // Idempotent — the loader calls this factory once at boot, but tests may
165
114
  // instantiate the router repeatedly; gating on hasProvider keeps the port
@@ -716,6 +665,21 @@ module.exports = function provisioningRoutes() {
716
665
  // a later edit. The always-on core stays unremovable by construction:
717
666
  // badModuleSelection refuses its keys and `effectiveModules` re-adds it on read.
718
667
  //
668
+ // `type` RIDES IT TOO (task 1003504), and for the third time the same argument: the
669
+ // wizard's Type step declared something at creation and nothing owner-facing could
670
+ // change it afterwards, so task 1002772 had to narrow the wizard's own promise and
671
+ // stop saying "you can change it later". This is the route that earns the sentence
672
+ // back. It belongs here rather than on the settings channel for the mModules reason —
673
+ // a type is a DECLARATION on the platform row, not a policy the running instance
674
+ // obeys, so nothing is pushed to the project and nothing restarts.
675
+ //
676
+ // One consequence is worth naming, because the owner has to be told it: for a project
677
+ // whose picker was SKIPPED, `modules` is NULL and effectiveModules resolves the type's
678
+ // starter bundle at read time (ADR 0240 §2) — so changing the type moves the extras
679
+ // too. A set the owner picked for themselves is stored and never moves. The reply
680
+ // carries `modules` on every patch precisely so the caller learns which happened
681
+ // without a follow-up GET.
682
+ //
719
683
  // AN ANSWER CAN BE TAKEN BACK (task 1003577). `detail` merges, so before this the
720
684
  // first `team_shape` an owner sent was permanent — and "never answered" is a state
721
685
  // `effectiveModules` reads (ADR 0240), not merely the absence of one. Sending
@@ -731,16 +695,22 @@ module.exports = function provisioningRoutes() {
731
695
  description: { type: 'string', maxLength: provisioning.DESCRIPTION_MAX },
732
696
  detail: { type: 'object' },
733
697
  modules: { type: 'array', itemsType: 'string', maxItems: 50 },
698
+ type: { type: 'string', maxLength: 40 },
734
699
  })) return;
735
700
  const body = req.body || {};
736
701
  const detailFault = badProjectDetail(body.detail);
737
702
  if (detailFault) return res.fail(detailFault.code, { status: 400, message: detailFault.message });
738
703
  const modulesFault = badModuleSelection(body.modules);
739
704
  if (modulesFault) return res.fail(modulesFault.code, { status: 400, message: modulesFault.message });
705
+ // `type: null` reaches here because the kernel validator reads null as ABSENT —
706
+ // which is exactly why the refusal has to be its own check and not an enum rule.
707
+ const typeFault = badProjectType(Object.prototype.hasOwnProperty.call(body, 'type') ? body.type : undefined);
708
+ if (typeFault) return res.fail(typeFault.code, { status: 400, message: typeFault.message });
740
709
  const wantsModules = Array.isArray(body.modules);
741
- if (body.description === undefined && body.detail === undefined && !wantsModules) {
710
+ const wantsType = typeof body.type === 'string';
711
+ if (body.description === undefined && body.detail === undefined && !wantsModules && !wantsType) {
742
712
  return res.fail('bad_detail', { status: 400,
743
- message: 'The body carries nothing to change — send description, detail, modules, or any mix.' });
713
+ message: 'The body carries nothing to change — send description, detail, modules, type, or any mix.' });
744
714
  }
745
715
  try {
746
716
  const inst = await provisioning.getInstanceById(pool, id);
@@ -753,6 +723,7 @@ module.exports = function provisioningRoutes() {
753
723
  if (body.description !== undefined) patch.description = provisioning.normalizeDescription(body.description);
754
724
  if (body.detail !== undefined) patch.detail = body.detail;
755
725
  if (wantsModules) patch.modules = body.modules;
726
+ if (wantsType) patch.type = body.type;
756
727
  const updated = await provisioning.updateInstanceDetail(pool, id, patch);
757
728
  await provisioning.recordEvent(pool, {
758
729
  instanceId: id, ownerBuilderId: inst.owner_builder_id, event: 'detail',
@@ -775,7 +746,12 @@ module.exports = function provisioningRoutes() {
775
746
  // Re-deriving it from a follow-up GET would let the two answers disagree.
776
747
  publish: publishVerdict(updated),
777
748
  // `team_shape` MOVES a bundle-sourced project's modules — effect with cause (ADR 0243 §5).
749
+ // So does `type`, for a project whose picker was skipped — same reason, said once here.
778
750
  modules: effectiveModules(updated),
751
+ // The declaration as it now stands (task 1003504) — the caller repaints from the
752
+ // platform's answer, never from the word it sent, so a refused or clamped write
753
+ // can never leave the picker showing a choice the row does not hold.
754
+ type: updated.type || null,
779
755
  });
780
756
  } catch (err) {
781
757
  log.error('[provisioning] PATCH /provisioning/instances/:id/detail', err);
@@ -829,6 +829,28 @@ summary{min-height:24px;padding:3px 0;}
829
829
  </details>
830
830
  </div>
831
831
 
832
+ <!-- live since task 1003504: the way back into the creation wizard's Type
833
+ step. Task 1002772 shipped the declaration with a deliberately NARROWED
834
+ promise — the wizard stopped saying "you can change it later", because
835
+ nothing owner-facing could. This is the surface that earns the sentence
836
+ back, and it sits beside "Its extras" for that section's own reason and
837
+ on the same channel: a type is a DECLARATION on the platform row, not a
838
+ policy the running instance obeys, so nothing is pushed to the project
839
+ and nothing restarts.
840
+ The one way it differs from its neighbour: this is a choice of ONE, not
841
+ a set of toggles, so picking the kind already chosen is a no-op rather
842
+ than an un-declaration. "Not Sure" is the vocabulary's own way back to
843
+ saying nothing, which is why the route refuses null. The fold is a
844
+ SIBLING of the volatile slot so no re-render can drop it. -->
845
+ <div class="card mSec" id="mKind">
846
+ <h2>Its kind</h2>
847
+ <div id="mKindPick"><p class="finePrint">Reading the project…</p></div>
848
+ <details class="finePrint" id="mKindWhat">
849
+ <summary>What the choice changes</summary>
850
+ <p>How this project is listed — the kind shown beside its name on the map, in the table and on its card. It is a declaration about the project, not a setting the project obeys: nothing is pushed to it and nothing restarts. One thing does follow the kind: if you never picked your extras yourself, they are the set this kind suggests, so changing the kind changes them too — you will be told when that happens. Extras you picked for yourself are stored and never move. Not Sure is a real answer, and it is the way back if you would rather not say.</p>
851
+ </details>
852
+ </div>
853
+
832
854
  <!-- live since task 1002342 (BV1.R23): the way back into the creation
833
855
  wizard's module picker. Step 4 of the wizard is skippable, and an
834
856
  optional step is a default rather than a dead end only if there is a
@@ -1005,7 +1027,7 @@ summary{min-height:24px;padding:3px 0;}
1005
1027
  forced into a wrong answer, and it can change later. -->
1006
1028
  <section class="panel" id="panel-type" aria-labelledby="type-h">
1007
1029
  <h1 id="type-h">What kind of project is it?</h1>
1008
- <p class="lede">Pick the shape that fits best — it’s how the project is listed on the map, and it stays as you pick it for now. Not sure is a fine answer.</p>
1030
+ <p class="lede">Pick the shape that fits best — it’s how the project is listed on the map, and you can change it later from the project’s own page. Not sure is a fine answer.</p>
1009
1031
  <form data-next="3">
1010
1032
  <div class="typeCards" id="typeCards" role="group" aria-label="Project type">
1011
1033
  <button type="button" class="tpill" data-type="game" aria-pressed="false">Game</button>
@@ -5800,6 +5822,9 @@ summary{min-height:24px;padding:3px 0;}
5800
5822
  /* the extras band (task 1002342) rides the same guard: its own toggles
5801
5823
  are the only thing that should ever repaint it, never the tick */
5802
5824
  if (!keepRepo) renderMods(inst);
5825
+ /* the kind picker (task 1003504) rides it for the same reason — and it can
5826
+ move the band above, so the two must never be repainted out of step */
5827
+ if (!keepRepo) renderKind(inst);
5803
5828
  })
5804
5829
  .catch(function (err) {
5805
5830
  var m = String(err && err.message);
@@ -6671,6 +6696,85 @@ summary{min-height:24px;padding:3px 0;}
6671
6696
  repaints from the server's own answer — never from the list we sent — so it
6672
6697
  can never show a set the platform did not accept. ── */
6673
6698
 
6699
+ /* The declared kind (task 1003504). Painted from TYPE_META — the page's single
6700
+ copy of the vocabulary, shared with the table cell and the card — so a sixth
6701
+ kind, or a re-worded one, can never mean two things on one page. Reuses the
6702
+ extras band's own classes: the control is the same control (a labelled choice
6703
+ with its line of explanation), and a second set of rules for it would drift
6704
+ from the first. aria-pressed, not aria-checked, mirrors the wizard's own Type
6705
+ pills — this is that picker, met again. */
6706
+ function kindSlot(html) { $('mKindPick').innerHTML = html; }
6707
+ var kindBusy = false;
6708
+
6709
+ function renderKind(inst) {
6710
+ var now = (inst && inst.type) || null;
6711
+ kindSlot('<div class="modBand" id="mKindBand" role="group" aria-label="Project kind">' +
6712
+ Object.keys(TYPE_META).map(function (k) {
6713
+ return '<button type="button" class="modOpt" data-kind="' + esc(k) + '" aria-pressed="' +
6714
+ (now === k ? 'true' : 'false') + '"' + (kindBusy ? ' disabled' : '') +
6715
+ '><b>' + esc(TYPE_META[k].label) + '</b><small>' + esc(TYPE_META[k].desc) + '</small></button>';
6716
+ }).join('') + '</div>' +
6717
+ (now ? '' : '<p class="finePrint">This project has never said what kind it is. Saying so puts the word beside its name wherever it is listed; it changes nothing else.</p>') +
6718
+ '<div class="projMsg" id="mKindMsg" role="status" aria-live="polite"></div>');
6719
+ }
6720
+
6721
+ /* One pick, sent as the whole declaration. Re-picking the kind already chosen is
6722
+ a NO-OP, not an un-declaration: the vocabulary's way back is "Not Sure", and a
6723
+ toggle-off here would send a null the route refuses anyway. */
6724
+ function doKindPick(key) {
6725
+ if (!manageInst || kindBusy) return;
6726
+ if ((manageInst.type || null) === key) return;
6727
+ kindBusy = true;
6728
+ renderKind(manageInst);
6729
+ var box = $('mKindMsg');
6730
+ box.className = 'projMsg'; box.textContent = 'Saving…';
6731
+ var asked = manageId; /* a stale answer must never repaint a newer project */
6732
+ var hadPicked = modsSource(manageInst) === 'picked';
6733
+ fetch(API + '/provisioning/instances/' + encodeURIComponent(manageId) + '/detail', {
6734
+ method: 'PATCH', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
6735
+ body: JSON.stringify({ type: key })
6736
+ }).then(function (res) {
6737
+ return jsonOrEmpty(res).then(function (data) {
6738
+ kindBusy = false;
6739
+ if (asked !== manageId) return;
6740
+ if (res.ok) {
6741
+ /* the platform's own answer, never the word we sent */
6742
+ manageInst.type = data.type || null;
6743
+ manageInst.modules = data.modules || manageInst.modules;
6744
+ renderKind(manageInst);
6745
+ /* the extras band reads the same row: a bundle-sourced set has just moved
6746
+ with the kind, and leaving it painted from the old one would be a lie */
6747
+ renderMods(manageInst);
6748
+ var ok = $('mKindMsg');
6749
+ ok.className = 'projMsg ok';
6750
+ ok.textContent = hadPicked || modsSource(manageInst) !== 'bundle'
6751
+ ? 'Saved. Nothing on the project itself changes — there is nothing to restart.'
6752
+ : 'Saved. Your extras follow the kind, because you never picked them yourself — they are now the set this kind suggests. Nothing on the project itself changes.';
6753
+ return;
6754
+ }
6755
+ renderKind(manageInst); /* the band goes back to what the row still says */
6756
+ var e = apiError(data, res.status);
6757
+ var m = $('mKindMsg');
6758
+ m.className = 'projMsg err';
6759
+ if (res.status === 401) { m.innerHTML = signInAgain(); return; }
6760
+ m.textContent = e;
6761
+ });
6762
+ }).catch(function () {
6763
+ kindBusy = false;
6764
+ if (asked !== manageId) return;
6765
+ renderKind(manageInst);
6766
+ var m = $('mKindMsg');
6767
+ m.className = 'projMsg err';
6768
+ m.textContent = 'Couldn’t reach the platform just now. Nothing about your project has changed — try again in a moment.';
6769
+ });
6770
+ }
6771
+
6772
+ document.addEventListener('click', function (ev) {
6773
+ var b = ev.target.closest && ev.target.closest('#mKindBand [data-kind]');
6774
+ if (!b) return;
6775
+ doKindPick(b.getAttribute('data-kind'));
6776
+ });
6777
+
6674
6778
  var modsCatalog = null; /* { core, optional } — the public catalog, read once */
6675
6779
  var modsPromise = null;
6676
6780
  var modsFailed = false; /* asked and did not get it; distinct from not yet asked */
@@ -6697,6 +6801,12 @@ summary{min-height:24px;padding:3px 0;}
6697
6801
  function modsSelected(inst) {
6698
6802
  return ((inst && inst.modules && inst.modules.selected) || []).slice();
6699
6803
  }
6804
+ /* 'bundle' = the kind's suggested set, resolved at read time because the picker
6805
+ was skipped; 'picked' = an answer the owner gave and the column stores. The
6806
+ kind picker beside this reads it to say whether a change moved the extras. */
6807
+ function modsSource(inst) {
6808
+ return (inst && inst.modules && inst.modules.source) || null;
6809
+ }
6700
6810
 
6701
6811
  function renderMods(inst) {
6702
6812
  ensureModsCatalog();
@@ -22,6 +22,7 @@
22
22
  "in-manage-address": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mAddress", "#mAddress h2", "#mDom .plain", "#mDomName", "#mDom [data-m-attach]", "#mDomWhat summary"], "count": [["#mDom .attach input[type=text]", 1], ["#mDom .attach button", 1]] } },
23
23
  "in-manage-planet": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mPlanetPick", "#mPick .pick .pickPreview .worldOrb .disc", "#mPick [data-pick-save]", "#mPick [data-pick-reset]", "#mPickWhat summary"], "count": [["#mPick .pickCells[role=\"radiogroup\"]", 3], ["#mPick .pickCell", 10], ["#mPick .pickCell.on", 3], ["#mPick .pickSw", 5], ["#mPick .pickSw.on", 1]], "attr": [["#mPick .pickPreview .worldOrb", "data-template", "pair-obsidian-tint"], ["#mOrb .worldOrb", "data-template", "pair-obsidian-tint"]], "checked": "#mPick input[name=\"mPickAccent\"][value=\"sea\"]", "text": [["#mPlanet .v small", "your choice."]] } },
24
24
  "in-manage-card": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mCard", "#mCardPick .pick .pickPreview .cardBand", "#mCardPick [data-card-save]", "#mCardWhat summary"], "count": [["#mCardPick .pickCells[role=\"radiogroup\"]", 1], ["#mCardPick .pickCell", 5], ["#mCardPick .pickCell.on", 1]], "checked": "#mCardPick input[name=\"mCardBackdrop\"][value=\"\"]", "attr": [["#mCardPick .cardBand", "data-own", ""]] } },
25
+ "in-manage-kind": { "auth": true, "url": "/projects?view=manage&id=101", "expect": {"visible":["#mKind","#mKindPick .modBand","#mKindWhat summary"],"count":[["#mKindPick .modOpt",5],["#mKindPick .modOpt[aria-pressed=\"true\"]",0]],"text":[["#mKindPick .modOpt b","Game"],["#mKindPick .finePrint","This project has never said what kind it is. Saying so puts the word beside its name wherever it is listed; it changes nothing else."]]} },
25
26
  "in-manage-invite": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mInvite", "#mInvite h2", "#mInviteWho", "#mInvite [data-invite-send]"] } },
26
27
  "in-manage-snag": { "auth": true, "url": "/projects?view=manage&id=103", "expect": { "visible": ["#mMain", "#mErr", "#mActs"], "hidden": ["#mAddress"], "text": [["#mName", "tidepool"]] } },
27
28
  "in-manage-gone": { "auth": true, "url": "/projects?view=manage&id=104", "expect": { "visible": ["#mMain", "#mActs"], "hidden": ["#mErr", "#mAddress"], "text": [["#mName", "old-lantern"]] } },
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.671",
3
+ "version": "1.19.673",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.671",
9
+ "version": "1.19.673",
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.671",
3
+ "version": "1.19.673",
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
@@ -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.671'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.673'; // 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');