@bongos/core 1.19.630 → 1.19.631

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.630",
6
- "core_contract": "1.19.630",
7
- "source_commit": "8ec5a34a79092057e64af1c4c3762d0b3901efb9",
5
+ "core_version": "1.19.631",
6
+ "core_contract": "1.19.631",
7
+ "source_commit": "8202774d7961f042840d27630f4d0e1a6a5e07e9",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-09T23:03:56.905Z",
9
+ "built_at": "2026-09-09T23:08:37.908Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 466,
12
+ "docs_redacted": 467,
13
13
  "agent_docs_stubbed": 24,
14
14
  "functional_verbatim": 2099,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2589,
20
- "tree_sha256": "9937af1178174c35ad4b6fd94cc34d318d04bbc74ec18d9f469b0ba163740e05",
19
+ "file_count": 2590,
20
+ "tree_sha256": "12f67762d32cfaf31bd0af336ddcc207dcacaf27f2be902ae6b5de3aa45e3834",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/backlog-review/SKILL.md",
@@ -1879,10 +1879,15 @@
1879
1879
  "mode": "0000644",
1880
1880
  "sha256": "0d4805df73af0b99c6dfd1c362debaa04d93a8a075f8e4b0cdf11af975bfc62c"
1881
1881
  },
1882
+ {
1883
+ "path": "docs/adr/0272-a-craft-is-an-identity-not-a-toolchain.md",
1884
+ "mode": "0000644",
1885
+ "sha256": "64a49e7f90224dd348da3ac8dce9b75f8585e2c59336f71c3fceb496cb351005"
1886
+ },
1882
1887
  {
1883
1888
  "path": "docs/adr/README.md",
1884
1889
  "mode": "0000644",
1885
- "sha256": "9b75ec13500719a639364bcc9436e9f0cde63119a032c805312153dbb006f390"
1890
+ "sha256": "83e696e6375acfc3d3dd1b27dbe9448a2b4844c9a10bea9af8b74b00442c4681"
1886
1891
  },
1887
1892
  {
1888
1893
  "path": "docs/api-reference.md",
@@ -2767,7 +2772,7 @@
2767
2772
  {
2768
2773
  "path": "docs/module-api-changelog.md",
2769
2774
  "mode": "0000644",
2770
- "sha256": "d70ae49d22a6ae226ad46e3c8d5fbaa0954a8d14697cdb96760f42bc24be7ced"
2775
+ "sha256": "bba1068a4fa47d679635e4b88d714e0246094440aa3df3e1b62e4df16d387aa9"
2771
2776
  },
2772
2777
  {
2773
2778
  "path": "docs/modules-contract.md",
@@ -7697,12 +7702,12 @@
7697
7702
  {
7698
7703
  "path": "package-lock.json",
7699
7704
  "mode": "0000644",
7700
- "sha256": "8ce2c5dbcf03a0638065b823be4fd0bab26ed8695ba9693bc99b090ef6e0f237"
7705
+ "sha256": "cb7b272da5c1fc4642acf70c63ee1781ee34101fe912cc0e907ebb71fd5f1ae3"
7701
7706
  },
7702
7707
  {
7703
7708
  "path": "package.json",
7704
7709
  "mode": "0000644",
7705
- "sha256": "94be508ef7535b26470470845f7631d438c0716cba55a09a5de676eb65ca2cad"
7710
+ "sha256": "9dba32fee8407356a478c9d5038b7d95db52c1a9d8a9e242f0be79d3c4d79f65"
7706
7711
  },
7707
7712
  {
7708
7713
  "path": "public-docs/index.html",
@@ -9442,7 +9447,7 @@
9442
9447
  {
9443
9448
  "path": "src/module-api.js",
9444
9449
  "mode": "0000644",
9445
- "sha256": "9f6fac692c0299219829477c40982314b29fc7fa1854a3544b6cc104ac97791a"
9450
+ "sha256": "f4839dfff1614c47e74577cff4761fb951d17c1702d5bccc77e818348077b9c6"
9446
9451
  },
9447
9452
  {
9448
9453
  "path": "src/module-loader/catalog.js",
@@ -9477,7 +9482,7 @@
9477
9482
  {
9478
9483
  "path": "src/modules.js",
9479
9484
  "mode": "0000644",
9480
- "sha256": "b1d45667d15eca220be3f6df5659834b3a8cacd3f0ef5aafe6afe6bd3688af11"
9485
+ "sha256": "fa0cd5ae0450512802dca281f92385de95dfee7ca24cb711af0985a588a72cc0"
9481
9486
  },
9482
9487
  {
9483
9488
  "path": "src/platform-server.js",
@@ -11612,7 +11617,7 @@
11612
11617
  {
11613
11618
  "path": "tests/modules.mjs",
11614
11619
  "mode": "0000644",
11615
- "sha256": "b518a626745db36f17e109449e40431d950d793ff00f54a5719a1af34cff8712"
11620
+ "sha256": "09ef08f9ab5ad1927ddfea15202c0cf6820dcff12030a3ff87a4c38718c5fa0f"
11616
11621
  },
11617
11622
  {
11618
11623
  "path": "tests/modules_route.mjs",
@@ -0,0 +1,64 @@
1
+ # ADR 0272 — A craft is an identity, not a toolchain: `artist` becomes core
2
+
3
+ **Date:** 2026-09-09
4
+ **Context:** BONGOS-V2, goal [1000111](https://cloudbongos.com/builders#/task/1003719) (Working area 7 — Government), task [1003719](https://cloudbongos.com/builders#/task/1003719), re-filed from task 1002908 under [ADR 0265](<redacted>.md). Supersedes the R59 (task 1198) decision that `artist` rides the `art-pipeline` module. Neighbour of [ADR 0098](<redacted>.md) (the publish manifest) and task 1003487 (module-contributed disciplines).
5
+ **Status:** Accepted.
6
+
7
+ ## Problem
8
+
9
+ Reported from Discord `#bugs` on 2026-08-15, verbatim: *"cant save preferences of role types in account page"*.
10
+
11
+ Reproduced live on cloudbongos.com 2026-09-09:
12
+
13
+ ```
14
+ GET /api/gds/me → preferred_disciplines: ["artist","ideator"]
15
+ PATCH /api/gds/me/disciplines → 422 invalid_discipline { bad: ["artist"] }
16
+ ```
17
+
18
+ The API hands a builder a value and then refuses to accept it back. A builder could not save their own stored profile.
19
+
20
+ `db.updateBuilderDisciplines` validates against `modules.enabledDisciplines()` — core disciplines plus every enabled module's `contributes.disciplines`. R59 put `artist` in the module half, on the reasoning that the craft existed to produce the game's pixel art and so belonged to `art-pipeline`.
21
+
22
+ **That gate had quietly stopped existing.** `art-pipeline` was never converted to a discovered module: there is no `modules/art-pipeline/module.json`, so nothing anywhere declares `contributes.disciplines: ['artist']`. Measured on a full repo checkout, with the art pipeline present on disk:
23
+
24
+ ```
25
+ enabledDisciplines() → ["engineer","ideator","ui"]
26
+ ```
27
+
28
+ So `artist` was offered by **no instance in any configuration** — not a vanilla one, not a full checkout, not production. It is also unreachable by construction on a *published* core: [ADR 0098](<redacted>.md)'s manifest excludes `modules/art-pipeline/` outright for a hard copyright reason (it carries ripped reference maps), so no registry install could ever carry that manifest even if someone wrote one.
29
+
30
+ Meanwhile the craft stayed first-class in every other core surface:
31
+
32
+ | Surface | Accepts `artist`? |
33
+ |---|---|
34
+ | `builders.preferred_disciplines` CHECK (migrations 086, 176) | yes |
35
+ | `tasks.discipline` (`task-classifier.VALID_DISCIPLINES`) | yes |
36
+ | Help-request craft roster (`help-requests.HELP_CRAFTS`) | yes |
37
+ | `modules.enabledDisciplines()` | **no** |
38
+
39
+ A *task* could be an artist task, and a builder could address a help request to the artist craft — but a *person* could not be an artist.
40
+
41
+ **Why the tests were green over it.** `tests/modules.mjs` did assert the artist leg, behind a guard: `if (!EFFECTIVE['art-pipeline']) return;`. Since the module has no manifest, that check was always false and the only real assertion skipped itself. The suite passed by not running.
42
+
43
+ ## Decision
44
+
45
+ **`artist` moves into `CORE_DISCIPLINES`**, which becomes `['engineer', 'artist', 'ideator']` — the migration 176 enum order. Modules still contribute their own (`ui-design` → `ui`), unchanged.
46
+
47
+ The principle: **a craft is an identity, not a toolchain.** It names who a builder is and what work routes to them. An instance can have people who consider themselves artists without shipping a pixel-art pipeline — just as it can have engineers without shipping any particular build tool. Binding the identity to the presence of one optional module's code was the category error, and the copyright exclusion makes that binding permanently unsatisfiable on the only distribution channel instances actually use.
48
+
49
+ The offered set is now a superset of what the DB CHECK accepts, which is the invariant that was violated: **anything the database will store, some instance shape must be able to offer.** `tests/modules.mjs` pins exactly that, over the widest possible module set, with no skip guard.
50
+
51
+ ## What was rejected
52
+
53
+ - **Enable `art-pipeline` on cloudbongos.com.** Impossible, not merely undesirable: the module is not in the published package, and `src/modules.js` throws at boot when a config enables an absent module. The owner's intent ("artist should exist here") could not have been delivered this way.
54
+ - **Write the missing `modules/art-pipeline/module.json`.** It would work only in a full repo checkout and never on a published instance, re-creating the same invisible divergence one layer down.
55
+ - **An instance-declared discipline list in `config/modules.json`.** More architecturally tidy on paper — which crafts an instance recognises is host context ([ADR 0062](<redacted>.md) §7) — but it puts the fix in the *instance* repo, leaving the core's own DB CHECK still describing a craft the core cannot offer. It also would not have fixed the vanilla case, where the craft is equally unofferable.
56
+ - **Strip unofferable crafts from stored rows.** This deletes a builder's stated identity to make a validator's mistake look consistent. The owner was asked and chose that artists should exist here.
57
+
58
+ ## Consequences
59
+
60
+ - **Every instance now offers `artist`**, including vanilla ones with no art tooling. That is the intended reading of the principle above, and it costs nothing: the DB has accepted the value since migration 086, and an instance that has no artists simply has no one selecting it.
61
+ - **Existing stored values round-trip again.** No migration or backfill: the rows were always valid per the CHECK: only the validator disagreed. Builders holding `artist` (the owner among them) can save their profile without losing it.
62
+ - **The copyright exclusion is untouched.** `modules/art-pipeline/` stays out of the published core; nothing here moves art *tooling* anywhere.
63
+ - **The R59 rationale is formally retired.** Its premise — that the craft exists to serve the game's pixel art — no longer holds: `modules/game/` is likewise excluded from the published core, while the craft remained wired into help requests, task classification and the DB.
64
+ - **Not yet visible to users.** cloudbongos.com serves core 1.19.608; this reaches builders only when the instance pin moves (gate 3 of [ADR 0137](<redacted>.md)).
@@ -363,3 +363,4 @@ This keeps the decision history honest and traceable.
363
363
  | 0269 | [**The CLI session store is host-keyed at a fixed anchor; the per-brand file stays the active pointer** ([task 1003741](https://cloudbongos.com/builders#/task/1003741) · goal 1000090 — *Working area 4, Bongos Core distribution*). The CLI resolved its session path from the BRANDING PACK, read out of whatever checkout the process stood in — one slot, that moved. Standalone (the public CLI's whole situation) no checkout means no brand, so every instance shared `~/.config/cloudbongos/gds-session.json` and signing into a second DESTROYED the first; in-repo it landed in `~/.config/<slug>/` where the standalone CLI could never find it (`npx … api GET /me` from a bare dir returned the cloudbongos builder while a valid hermeslines session sat on the same machine). Not theoretical — the owner's config dir carries a hand-made `gds-session.<redacted>.bak.json` and four more of the same shape. **Decision: one file per instance keyed by HOST at a FIXED anchor (`~/.config/<FALLBACK_DIR>/instances/<host>.json`), with the per-brand `gds-session.json` left exactly where it is as the ACTIVE pointer.** Four load-bearing parts. (1) The anchor must never read `configHome()`/`configDirName()`/the pack — a test asserts the function body names none, since routing it back through the brand silently reinstates the split. (2) The active pointer does NOT move: every existing reader (in-repo skills, the card hook, the dev box, `readSessionToken`) looks there, and the store is purely additive. (3) The OUTGOING session is archived BEFORE the incoming one lands — a session written before the store existed is not in it, so without this the first login after upgrading still loses it, the bug surviving its own fix. (4) A stored token is VERIFIED against `/api/gds/me` before being reinstated; unverified falls through to the real device flow, `--force` signs in as somebody else. `login` also names the other instances and how to switch, since sessions are now kept rather than overwritten. Files 0600 in a 0700 dir; an `api_base` naming no host is not stored rather than stored under a guess, so nothing can write outside the store. The dir is `instances/` NOT `sessions/` — `sessions` is a live module key and [ADR 0083](<redacted>.md) §Decision #4 forbids kernel machinery naming one; the fitness gate caught it. Rejected: moving the active pointer (breaks every reader for no visible gain); one file holding a map (changes a format hooks and the dev box parse today); keying under `configHome()` (the bug); restoring without verifying (hands over a dead session that looks live); a new `bongos switch` verb (login is the command already reached for, and the public CLI's surface is deliberately small).](<redacted>.md) | cli / auth / distribution |
364
364
  | 0270 | [**The rank floor derives from facts, not from a prediction** ([task 1003663](https://cloudbongos.com/builders#/task/1003663) · goal 1000111 — *Working area 7, Government*). [ADR 0084](<redacted>.md) derives every task’s claim-time `requires_rank` floor from `touches[]` + `security_sensitive`; [ADR 0049](<redacted>.md) then retired PREDICTIVE `touches[]`, which is now backfilled from the real committed diff at SHIP — after the claim the floor exists to gate. So on every create vector that omits it the matcher saw an empty array, and because the floor is computed ONCE at creation and never recomputed, it returned `xenos` forever. Nothing threw: an empty `touches[]` is a legal state. **Measured before the fix: 88 tasks declared `needs_migration` and 68 of them (77%) sat at `xenos`** — including [task 1003372](https://cloudbongos.com/builders#/task/1003372), a READY task to hash the `builder_sessions` token store at rest, claimable by any newcomer; goal 1000111 alone held 20 such rows. Two mechanisms hid it: the authoring routes DISAGREE (`POST /goals/:id/tasks` REQUIRES `touches[]` as its scope-wall blast radius, so the floor derives there, while its sibling `POST /tasks` — the route `/goal-create` actually files seed tasks through — treats it as optional), and `goal-create/SKILL.md` told authors in ONE line to *omit `touches[]`* and *never hand-set `requires_rank`*: omit the only input, never set the output. **Decision:** the floor derives from facts a task already carries at create time, never from a prediction — `deriveRequiredRank` gains `opts` with two touches-free inputs, `needs_migration` (the direct successor to the `migrations/` entry `POST /tasks` used to fold into `touches[]`) and `scopeProtected` (the owning goal’s `scope_modules` wall, ALWAYS available since `goal_id` is required, read through the same `scopeIncludesProtected` primitive the pre-push hook, grader and main-audit use, so it cannot drift). Derived at the LOWEST create layer in `createTask` after `goalIdEffective` resolves — the ADR 0096 argument — so every vector inherits it and patching the two routes separately (which is how they came to disagree) is not repeated. `updateTaskRequiresRank` re-derives with the same four inputs, else an Archon setting a migration task to `xenos` would silently undo the create-time floor; raise-only throughout; `archon` still never auto-assigned; a strict `=== true` so a stray truthy value cannot wall off the open queue. One-time backfill `core_236` raises still-CLAIMABLE rows only (the floor is a claim-time gate; rewriting shipped rows changes no live decision) and never lowers. **Accepted cost:** a protected goal wall is blunt — any protected module in the wall floors the whole goal, so a `hall-ui` task in a mixed goal floors at Metic too; same semantics the ideas vector has enforced since task 1003066, fails safe, and the remedy is to narrow the wall. Zero claimable `newcomer_friendly` tasks exist today, and no carve-out was added for them on purpose. Rejected: re-requiring predictive `touches[]` (re-litigates ADR 0049); recomputing at ship (too late by construction); deriving from `module_key` (null on most tasks, including the reporter).](<redacted>.md) | permissions / task lifecycle |
365
365
  | 0271 | [**The ideator surface is two halves: Bongos owns the prose, the harness owns the chrome** ([task 1003506](https://cloudbongos.com/builders#/task/1003506) · goal 1000095 — *Working area 6, Governor / Builder / Artist / Ideator experience*, criterion `wa6-kernel-and-packs`; owner idea 1000732, 2026-08-16). The ask — *no need for an ideator to see all the text and technical work, or the inner Bongos logic* — is two problems in one sentence. **The PROSE half ships here**, through three seams that already existed: a `machinery` field on `interaction-prefs` (per-PERSON, default `show`, riding the live Conductor injection rail — advisory 8, which means task [#1002514](https://cloudbongos.com/builders#/task/1002514)’s claim that `contractFor()` has zero consumers is STALE); the `ideator` directive in `discipline-modes.json` (per-ROLE, the file whose own docs say *to give a discipline its own experience later, add an entry here*); and a new surface section in `/ideate`, which the directive names as the only home of the experience. Default `show` means no existing builder changes. **The CHROME half is the harness’s and is deferred to a spike, not declared impossible** — this record was nearly written on the wrong assumption that config cannot reach it. Two real levers exist: **A**, shipped client defaults (`/focus` renders *only your last prompt, a one-line tool-call summary, and the final response*; `alwaysThinkingEnabled:false` stops thinking being generated; `showTurnDuration`/`spinnerTipsEnabled`/`statusLine` trim chrome) — but `/focus` is fullscreen-only (research preview), `viewMode`’s committability is UNVERIFIED, and thinking suppression is per-model; and **B**, an app-owned surface over `claude -p --output-format stream-json`, whose documented streaming-UI pattern (working indicator on a `tool_use` block, render on `result`) IS the ask, is model-independent, and is the recorded bias. **Bypass is REJECTED** despite the idea proposing it, on three grounds ending in a technical one: the least technical role is the worst audience for removing confirmations; the prompt was never the gate ([ADR 0016](<redacted>.md) — rank is checked server-side, and sub-Metic protected-path globs already narrow an ideator); and a project **cannot** ship it — `defaultMode: bypassPermissions` in `.claude/settings.json` silently does not take effect and starts the session in Manual. Rejected also: a hall ROOM on the Studio’s precedent (goal 1000074’s *a place, not a form*) — right for an artist, wrong here, because the ideator’s complaint is about the Claude session’s surface, which the hall does not render; and waiting on per-session declared role, whose dependency is softer than filed since `discipline` + the claim-time fork already carry the craft axis. When declaration lands the contract MOVES into the Ideator pack with nothing undone.](<redacted>.md) | roles / ideator experience / session surface |
366
+ | 0272 | [**A craft is an identity, not a toolchain: `artist` becomes core** ([task 1003719](https://cloudbongos.com/builders#/task/1003719) · goal 1000111 — *Government*). Reported from Discord on 2026-08-15 as "cant save preferences of role types in account page"; reproduced live 2026-09-09: `GET /me` returns `preferred_disciplines: ["artist","ideator"]` and `PATCH /me/disciplines` with that SAME array returns `422 invalid_discipline {bad:["artist"]}` — the API handing a builder a value and refusing to accept it back. **Cause:** `updateBuilderDisciplines` validates against `enabledDisciplines()`, and R59 (task 1198) put `artist` in the module half on the reasoning that the craft existed to make the game pixel art, so it rode `art-pipeline`. **That gate had stopped existing** — art-pipeline was never converted to a discovered module, there is no `modules/art-pipeline/module.json`, and so NOTHING declared `contributes.disciplines: ["artist"]`. Measured on a FULL repo checkout with the pipeline present on disk: `["engineer","ideator","ui"]`. The craft was offered by no instance in any configuration, and is unreachable by construction on a published core anyway, since [ADR 0098](<redacted>.md) excludes `modules/art-pipeline/` for a hard copyright reason (ripped reference maps). Meanwhile it stayed first-class everywhere else: the `builders.preferred_disciplines` CHECK (086/176), `tasks.discipline`, and the help-request craft roster all accept it — so a TASK could be an artist task and a PERSON could not be an artist. **The tests were green because they skipped:** the artist assertion sat behind `if (!EFFECTIVE["art-pipeline"]) return;`, always false, so the only real leg never ran. **Decision:** `artist` joins `CORE_DISCIPLINES` → `["engineer","artist","ideator"]` (migration 176 enum order); modules still contribute their own (ui-design → ui). A craft names WHO A BUILDER IS, not which toolchain ships — an instance can have artists without a pixel-art pipeline, and binding the identity to one optional module was the category error. The new invariant, pinned with no skip guard over the widest module set: anything the DB CHECK will store, some instance shape must offer. Rejected: enabling art-pipeline (impossible — absent from the package, and enabling an absent module throws at boot); writing the missing manifest (works only in a full checkout); an instance-declared discipline list (leaves the core CHECK describing a craft core cannot offer, and does not fix the vanilla case); stripping unofferable crafts from stored rows (deletes a builder stated identity to make a validator mistake look consistent). No migration — the rows were always CHECK-valid; only the validator disagreed.](<redacted>.md) | crafts / instance composition |
@@ -1709,5 +1709,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1709
1709
  landed since 1.19.628 with no explicit bump. run 34412159317. (task 1002620)
1710
1710
  1.19.630 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1711
1711
  landed since 1.19.629 with no explicit bump. run 34415169250. (task 1002620)
1712
+ 1.19.631 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1713
+ landed since 1.19.630 with no explicit bump. run 34415537907. (task 1002620)
1712
1714
  ---------------------------------------------------------------------------
1713
1715
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.630",
3
+ "version": "1.19.631",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.630",
9
+ "version": "1.19.631",
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.630",
3
+ "version": "1.19.631",
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
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
55
55
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
56
56
  // the entry to that file. Look for a version's history there, not here.
57
57
  // ---------------------------------------------------------------------------
58
- const CORE_VERSION = '1.19.630'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
58
+ const CORE_VERSION = '1.19.631'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
59
59
 
60
60
  // A namespaced logger so a module's log lines are attributable + consistent.
61
61
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
package/src/modules.js CHANGED
@@ -56,13 +56,36 @@ const INSTANCE_PATH = path.join(resolveInstanceRoot(), 'config', 'modules.json')
56
56
  // moduleLoader from modules/*/module.json.
57
57
  const MODULE_REGISTRY = {};
58
58
 
59
- // Disciplines the CORE always provides. Modules add to this (e.g. art-pipeline
60
- // adds 'artist'). Ideation is methodology core — the idea_inbox, capture.js and
61
- // /idea-triage ship to every instance regardless of feature modules — so a
62
- // vanilla instance offers engineer + ideator. Only 'artist' is module-gated: it
63
- // exists to produce the game's pixel art, so it rides the art-pipeline module
64
- // (R59 / task 1198). A vanilla instance therefore offers ['engineer','ideator'].
65
- const CORE_DISCIPLINES = ['engineer', 'ideator'];
59
+ // Disciplines the CORE always provides; modules add to this via
60
+ // `contributes.disciplines` (ui-design → 'ui'). Ideation is methodology core —
61
+ // the idea_inbox, capture.js and /idea-triage ship to every instance regardless
62
+ // of feature modules — so engineer + ideator are unconditional.
63
+ //
64
+ // 'artist' JOINED THEM at task 1003719 (ADR 0271), and the reason is that the
65
+ // R59 gate it used to sit behind had quietly stopped existing. R59 said artist
66
+ // "rides the art-pipeline module" because the craft existed to make the game's
67
+ // pixel art — but art-pipeline was never converted to a discovered module (it
68
+ // has no modules/art-pipeline/module.json), so NOTHING declared
69
+ // contributes.disciplines: ['artist'] and the craft was offered by no instance
70
+ // anywhere, not even a full-repo checkout. It is also unreachable by design on a
71
+ // PUBLISHED core: publish-manifest excludes modules/art-pipeline/ outright for a
72
+ // hard copyright reason (the ripped reference maps), so no registry install can
73
+ // ever carry that manifest even if one were written.
74
+ //
75
+ // Meanwhile the craft stayed first-class in every OTHER core surface: the
76
+ // builders.preferred_disciplines CHECK has accepted it since migration 086,
77
+ // tasks.discipline accepts it (task-classifier VALID_DISCIPLINES), and a builder
78
+ // can address a help request to the artist craft (help-requests HELP_CRAFTS). So
79
+ // a TASK could be an artist task and a PERSON could not be an artist — and any
80
+ // builder already holding it (the owner did) could not save their own profile at
81
+ // all, because updateBuilderDisciplines validates against this list and 422'd
82
+ // their stored value back at them.
83
+ //
84
+ // A craft is an IDENTITY, not a toolchain: an instance can have artists without
85
+ // shipping a pixel-art pipeline. Making it core aligns the one outlier list with
86
+ // the DB CHECK and keeps the copyright exclusion untouched. Order mirrors the
87
+ // migration 176 enum (engineer | artist | ideator | ui).
88
+ const CORE_DISCIPLINES = ['engineer', 'artist', 'ideator'];
66
89
 
67
90
  const KNOWN_MODULES = Object.keys(MODULE_REGISTRY);
68
91
 
package/tests/modules.mjs CHANGED
@@ -127,17 +127,37 @@ test('isModuleEnabled throws on an unknown module name (programmer error)', () =
127
127
  assert.throws(() => isModuleEnabled('nope', resolveModules({})), /unknown module/);
128
128
  });
129
129
 
130
- test('disciplines: vanilla offers engineer + ideator (core); art-pipeline adds artist (R59)', () => {
130
+ test('disciplines: engineer + artist + ideator are CORE offered with no modules on', () => {
131
131
  // Ideation is methodology core (idea_inbox / capture / triage ship to every
132
- // instance), so it is offered with no modules on; only 'artist' is module-gated.
133
- // art-pipeline is now a DISCOVERED module (BV1.R48), so pass EFFECTIVE registry.
134
- assert.deepEqual(enabledDisciplines(resolveModules({})), ['engineer', 'ideator']);
135
- // The 'artist' leg reads art-pipeline's OWN module.json contributes, so it only
136
- // means anything where that module is installed (task 1002470). The core half
137
- // above ideation ships to every instance is asserted unconditionally.
138
- if (!EFFECTIVE['art-pipeline']) return;
139
- const m = resolveModules({ instance: { modules: { 'art-pipeline': true } }, registry: SAMPLE_REG });
140
- assert.deepEqual(enabledDisciplines(m), ['engineer', 'ideator', 'artist']);
132
+ // instance). 'artist' joined them at task 1003719 / ADR 0271 see the
133
+ // CORE_DISCIPLINES comment for why the R59 module gate had stopped existing.
134
+ assert.deepEqual(enabledDisciplines(resolveModules({})), ['engineer', 'artist', 'ideator']);
135
+ });
136
+
137
+ test('THE REGRESSION: every craft the DB CHECK accepts is offered by SOME instance shape', () => {
138
+ // The defect this pins (task 1003719): 'artist' was in the
139
+ // builders.preferred_disciplines CHECK (migration 086), in tasks.discipline and
140
+ // in the help-request craft roster, but in NO instance's offered set — so a
141
+ // builder holding it could not save their own profile, and the API 422'd the
142
+ // exact value /me had just handed them.
143
+ //
144
+ // It survived because the old test guarded its only real assertion behind
145
+ // `if (!EFFECTIVE['art-pipeline']) return;`. art-pipeline has no module.json,
146
+ // so that leg skipped and the suite stayed green over an unofferable craft.
147
+ // This test takes no such escape hatch: vanilla + every discovered module is
148
+ // the widest set any instance can offer, and the DB's own enum is the standard
149
+ // it must meet.
150
+ const widest = new Set(enabledDisciplines(resolveModules({ instance: { modules: Object.fromEntries(
151
+ Object.keys(EFFECTIVE).map((k) => [k, true])
152
+ ) }, registry: EFFECTIVE })));
153
+ // Mirrors the CHECK on builders.preferred_disciplines (migrations 086 + 176).
154
+ for (const craft of ['engineer', 'artist', 'ideator', 'ui']) {
155
+ assert.ok(
156
+ widest.has(craft),
157
+ `the DB accepts preferred_disciplines='${craft}' but no instance shape offers it — ` +
158
+ 'updateBuilderDisciplines will 422 a builder who already has it stored'
159
+ );
160
+ }
141
161
  });
142
162
 
143
163
  test('disciplines: ui-design contributes `ui` — the manifest key IS the unlock (task 1003487)', () => {
@@ -195,8 +215,9 @@ test('a branded sibling (GDS_BRANDING_FILE set, no GDS_MODULES_FILE) defaults to
195
215
  // A non-default brand must fall through to the neutral (all-off) module set.
196
216
  const m = loadModules({ env: { GDS_BRANDING_FILE: 'config/branding.cloudbongos.json' } });
197
217
  for (const k of KNOWN_MODULES) assert.equal(m[k], false, `${k} must be off for a branded sibling`);
198
- // The two core crafts, plus 'ui' from ui-designwhich is default-on for EVERY
199
- // instance ("every instance has a face") and is not one of KNOWN_MODULES, so the
200
- // all-off assertion above never reaches it (task 1003487).
201
- assert.deepEqual(enabledDisciplines(m), ['engineer', 'ideator', 'ui']);
218
+ // The three core crafts (engineer + artist + ideator task 1003719 / ADR 0271),
219
+ // plus 'ui' from ui-design, which is default-on for EVERY instance ("every
220
+ // instance has a face") and is not one of KNOWN_MODULES, so the all-off
221
+ // assertion above never reaches it (task 1003487).
222
+ assert.deepEqual(enabledDisciplines(m), ['engineer', 'artist', 'ideator', 'ui']);
202
223
  });