@bongos/core 1.19.890 → 1.19.892

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.890",
6
- "core_contract": "1.19.890",
7
- "source_commit": "fb73c80a33c2f4e3aaf8d9ae3bb0295b6ec73e3b",
5
+ "core_version": "1.19.892",
6
+ "core_contract": "1.19.892",
7
+ "source_commit": "10a12417f89994d74e98a3d69ffb32e6c923fcc1",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-19T21:31:12.716Z",
9
+ "built_at": "2026-09-19T21:54:33.560Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 497,
12
+ "docs_redacted": 498,
13
13
  "agent_docs_stubbed": 26,
14
- "functional_verbatim": 2271,
14
+ "functional_verbatim": 2272,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2794,
20
- "tree_sha256": "8e1cc8300c4c29b876e8dc18b16384db7ade73e5948c7e009c0b69f9dadea8a7",
19
+ "file_count": 2796,
20
+ "tree_sha256": "3a0ce12eaade900074c0776354d30caac78be604f0107b72a537da133fbfc114",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -1972,7 +1972,7 @@
1972
1972
  {
1973
1973
  "path": "docs/adr/0296-a-role-speciality-is-data-over-three-existing-surfaces.md",
1974
1974
  "mode": "0000644",
1975
- "sha256": "440116d384964c06055b1c7c07c30a962b164f519c9bbf3ce910570d186f1308"
1975
+ "sha256": "894a648aef61f34914e8ec9040fd241d012ce089f9a32c9fbc9e302c297e8912"
1976
1976
  },
1977
1977
  {
1978
1978
  "path": "docs/adr/0297-a-tasks-creator-may-retire-it.md",
@@ -2039,10 +2039,15 @@
2039
2039
  "mode": "0000644",
2040
2040
  "sha256": "2aabad9f28113f3e6087b4e850d46a232aaa03c68ce9e793d04b2263e06998f5"
2041
2041
  },
2042
+ {
2043
+ "path": "docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md",
2044
+ "mode": "0000644",
2045
+ "sha256": "9c8eff827f7c676cb8fb73475383812746d31c9ca35e2ec4ccb3845c48d2cd63"
2046
+ },
2042
2047
  {
2043
2048
  "path": "docs/adr/README.md",
2044
2049
  "mode": "0000644",
2045
- "sha256": "22f4f461a476d43d15a9f77626bdce552736d1a5d96210b5dbd638eb758fbec5"
2050
+ "sha256": "ca64ff12d272ebf2a1b7f6752193ad464c6b7c03118c6f9555a58d8b4a0f6e0c"
2046
2051
  },
2047
2052
  {
2048
2053
  "path": "docs/api-reference.md",
@@ -2607,7 +2612,7 @@
2607
2612
  {
2608
2613
  "path": "docs/module-api-changelog.md",
2609
2614
  "mode": "0000644",
2610
- "sha256": "7fb640ef029a52e521d43e2828ca0201a7e53c7a94f23fcebd58aefd6f6820fe"
2615
+ "sha256": "9f273320150c23f11a8fae0c8f807ea57a78e35fcee9dde0bd945881fadd6889"
2611
2616
  },
2612
2617
  {
2613
2618
  "path": "docs/modules-contract.md",
@@ -7212,7 +7217,7 @@
7212
7217
  {
7213
7218
  "path": "modules/specialities/migrations/specialities_001_specialities.sql",
7214
7219
  "mode": "0000644",
7215
- "sha256": "055210da2726c1d17457900a9b44566c58f371b3206365c5352474114be57251"
7220
+ "sha256": "21532391b179024f92eee8589576f612ed786b93e8ba69b817069c3ecc1fa393"
7216
7221
  },
7217
7222
  {
7218
7223
  "path": "modules/specialities/migrations/specialities_002_offered.sql",
@@ -7232,7 +7237,7 @@
7232
7237
  {
7233
7238
  "path": "modules/specialities/specialities.js",
7234
7239
  "mode": "0000644",
7235
- "sha256": "413477a716f3a60abc0763bccfb10a593c4eb19301131de0230eb3d72ffee299"
7240
+ "sha256": "4d6d29e4391ab1dcc6659e88ec858dc0276aa9f2b0e6d84eafa1b9f65c04d9be"
7236
7241
  },
7237
7242
  {
7238
7243
  "path": "modules/status-ui/CLAUDE.md",
@@ -7707,12 +7712,12 @@
7707
7712
  {
7708
7713
  "path": "package-lock.json",
7709
7714
  "mode": "0000644",
7710
- "sha256": "f9267b40d96aa3395f93b456da92cb8ff99f579340e1e9ac57e2264764e7b293"
7715
+ "sha256": "3ef1a7c4eafee4e9e92df1e0c1389acf252ff0d0f32689718cf81ee4c46ec04b"
7711
7716
  },
7712
7717
  {
7713
7718
  "path": "package.json",
7714
7719
  "mode": "0000644",
7715
- "sha256": "4185cf704dd2b4d4dddd72ad802fbc6861c66181b69bbc66eaafdec7e603da35"
7720
+ "sha256": "52b00c86bbde5eb29b9637f15988898313b2e5318ba50d83c30fff24a699021d"
7716
7721
  },
7717
7722
  {
7718
7723
  "path": "public-docs/index.html",
@@ -8017,7 +8022,7 @@
8017
8022
  {
8018
8023
  "path": "scripts/gds/build-cli-package.js",
8019
8024
  "mode": "0000644",
8020
- "sha256": "630bdedb252e6d77a3427d962fca94094c747c377679ec1157644d76f142b6e8"
8025
+ "sha256": "ca0566c509fb1ea81e7c874e2676bbcb965803509c0289c577bc8f15791e6f5c"
8021
8026
  },
8022
8027
  {
8023
8028
  "path": "scripts/gds/bump-version.js",
@@ -9207,7 +9212,7 @@
9207
9212
  {
9208
9213
  "path": "scripts/gds/status.js",
9209
9214
  "mode": "0000644",
9210
- "sha256": "4a1efefdb099512b31538d279b8be36cc19549fb9601d48ab47dbf43437ea1a2"
9215
+ "sha256": "191b49e9ccbcfc7b855d834535ad5e9de0970d6e91126c7a0591e7790d196a89"
9211
9216
  },
9212
9217
  {
9213
9218
  "path": "scripts/gds/strand-watch.js",
@@ -9282,7 +9287,7 @@
9282
9287
  {
9283
9288
  "path": "scripts/gds/version-literal-guard.js",
9284
9289
  "mode": "0000644",
9285
- "sha256": "7e27012820d6e2fb43c84140ae6f90d4ca9345658937aa75cc9f2c28cfdaf41e"
9290
+ "sha256": "2c0e605876762f8c6607af49374f56bc0717a1aad08c3ffdbbae383ffaf1d9dd"
9286
9291
  },
9287
9292
  {
9288
9293
  "path": "scripts/gds/version-select.js",
@@ -9717,7 +9722,7 @@
9717
9722
  {
9718
9723
  "path": "src/module-api.js",
9719
9724
  "mode": "0000644",
9720
- "sha256": "7b2ddf6ecca1f92f1ae2ba7a70e0e6aeb9726ec0fa9c4a5375f075755b036796"
9725
+ "sha256": "c0c018df9a35a17f31c17c566ded5bb5878c5c1189f62cd20ad1ea3e3e43d612"
9721
9726
  },
9722
9727
  {
9723
9728
  "path": "src/module-loader/catalog.js",
@@ -13589,6 +13594,11 @@
13589
13594
  "mode": "0000644",
13590
13595
  "sha256": "4b92c96ae496736b0f44182e64d42a24f108cb51fbeb5cf7cd5814ad336b14f6"
13591
13596
  },
13597
+ {
13598
+ "path": "tests/status_default_version.mjs",
13599
+ "mode": "0000644",
13600
+ "sha256": "9a80ed43b685ea537944886a1a4a02d35580c109bd777373e30b63afd9aa73df"
13601
+ },
13592
13602
  {
13593
13603
  "path": "tests/status_mirror_sync.mjs",
13594
13604
  "mode": "0000644",
@@ -13877,7 +13887,7 @@
13877
13887
  {
13878
13888
  "path": "tests/version_literals.mjs",
13879
13889
  "mode": "0000644",
13880
- "sha256": "17e872131c17bae7a061debc1fd3ee354ab704678b475edd6a8b1dfdd63ceb2f"
13890
+ "sha256": "3b706518920bc48e1bcd5a0185a5829a179fa0e73a6ed491320327d2cb7c8c68"
13881
13891
  },
13882
13892
  {
13883
13893
  "path": "tests/version_override_visibility.mjs",
@@ -27,7 +27,7 @@ The owner's phrasing is that specialities "are more or less MODULES", and the pu
27
27
  |---|---|
28
28
  | craft → playbook | `discipline-modes.json` |
29
29
  | the per-session interaction contract | `behavior_prefs.interaction` → `GET /me` → the Conductor hook |
30
- | declared skills | the ADR 0083 manifest contribution seam |
30
+ | declared skills | **not built by this ADR** — see [ADR 0310](0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md), which decides the shape (a speciality OFFERS skills; the adopter enables them one at a time, walked through each) and leaves the build to its own tasks |
31
31
  | **the knowledge bundle** | **new — `speciality_documents` + `speciality_learnings`** |
32
32
 
33
33
  Only the fourth is new machinery. In particular **a speciality contract rides the existing injection path rather than adding a second one**: `GET /me` gains a slice, the Conductor relays it, and the composition stays server-side exactly as it does for the interaction profile.
@@ -0,0 +1,81 @@
1
+ # ADR 0310 — A speciality OFFERS skills; the adopter turns them on, one at a time, having been shown what each does
2
+
3
+ **Status:** accepted · **Date:** 2026-09-19 · **Task:** [#1003978](https://cloudbongos.com/builders#/task/1003978) · **Goal:** 1000095 · **Supersedes nothing; completes [ADR 0296](0296-a-role-speciality-is-data-over-three-existing-surfaces.md)**
4
+
5
+ ## Context
6
+
7
+ [ADR 0296](0296-a-role-speciality-is-data-over-three-existing-surfaces.md) defines a speciality as *"a named bundle of knowledge, skills and an interaction contract that sits UNDER a craft"*, and names three ingredients over three existing surfaces. **Two of the three shipped.** `contract_md` is the interaction contract; `specialities_documents` + `specialities_learnings` are the knowledge.
8
+
9
+ The third never existed. There is no skills column, no skills field on the draft, nothing in `validateDraft`, nothing in the routes, nothing in the injected contract. `grep -i skill` across the module's schema, domain and routes returns only the header sentences describing a thing that is not there — in the ADR, in the migration header, in `specialities.js`, and in the module's `CLAUDE.md` table.
10
+
11
+ That is prose asserting behaviour the code does not have, and it is worse here than usual because it is the feature's own definition. Task 1003978 filed it, and deliberately withheld an implementation: *"Do NOT start by adding a `skills` column. Decide (2) first"* — (2) being whether naming a skill grants anything.
12
+
13
+ **Why it matters rather than being a doc typo.** The point of a speciality is that an expert's way of working arrives in a session. Two-thirds of that is context and tone; the third is *what the session can DO*. An economics speciality that brings a costing skill is materially different from one that only changes how Claude talks. Without it, a speciality changes framing and never capability.
14
+
15
+ ## The owner's ruling (2026-09-19)
16
+
17
+ Asked whether specialities should carry skills at all, and if so how:
18
+
19
+ > *"Specialities should carry skills but these should be user based, giving people edges in the beginning and then eventually being able to share these specialities with others. But also, if say two people are economist ideator specialists, then they decide which skills they want to use and which are relevant to them. It does not become super blanketed."*
20
+
21
+ Asked whether an adopted speciality's skills should arrive switched on or off:
22
+
23
+ > *"They should be off, and you should walk through what comes with each, people need to learn the system and understand things instead of preventing learning."*
24
+
25
+ Two rulings, and the second is the sharper one. It is not a safe default chosen to avoid surprise — it is a **position about learning**. The moment of adoption is the moment a person is most willing to find out what a tool does, and handing them a working toolkit silently spends that moment. Being walked through each skill and choosing it is how the system gets understood.
26
+
27
+ ## Decision
28
+
29
+ ### 1. A speciality DECLARES candidate skills. It grants nothing.
30
+
31
+ This answers task 1003978's question (2), and the answer is the strong one: **naming a skill can never confer it.** Skills are materialised into a checkout by other machinery (`claude-materialize.js`), and permission is rank-gated server-side with no caching ([ADR 0016](<redacted>.md)). A speciality may say *"in this speciality, reach for `/recall` and `/status` first"*; it may not hand anyone a capability they did not already have.
32
+
33
+ The task warned that a column *"would imply an authority it must not have"*. It does not, given point 2 — because the declaration is a menu, and a menu is not an entitlement. What makes that safe is not the storage shape but the fact that enabling a skill still passes every existing gate, unchanged.
34
+
35
+ ### 2. The adopter chooses, per person, and starts with nothing on
36
+
37
+ Two layers, because one layer is the blanket the owner ruled against:
38
+
39
+ | Layer | Holds | Why there |
40
+ |---|---|---|
41
+ | `specialities_catalog` | the skills this speciality OFFERS | the speciality is the thing being shared |
42
+ | `specialities_adoptions` | the skills THIS builder turned on | already per-builder, already the row an adoption creates |
43
+
44
+ Two people holding the same economist ideator speciality share its knowledge and its contract, and need not share a single enabled skill. That is the ruling, and the second table is where it becomes true rather than documented.
45
+
46
+ **Default: nothing enabled.** An adoption begins with an empty set.
47
+
48
+ ### 3. Adoption is a walkthrough, not a toggle list
49
+
50
+ The consequence of the default that carries the actual intent. Adopting a speciality must **present each offered skill in turn — what it does, when its author reaches for it, what it costs** — and let the person enable it there, then. A settings page with eleven unchecked boxes satisfies the letter of "off by default" and defeats the ruling entirely.
51
+
52
+ The bar: **a person who has finished adopting a speciality can say what each skill in it does.** If the flow does not produce that, it is not built yet, whatever the schema says.
53
+
54
+ ### 4. Sharing needs no new mechanism
55
+
56
+ Already shipped in `specialities_catalog`: `owner_kind` (`builder` | `project`), `visibility` (`private` → `project` → `public`), `offered`, `sellable`. The owner's *"edges in the beginning and then eventually being able to share"* is exactly that ladder, and it exists. Skills ride it for free: a speciality shared with its offered set intact is shared whole, and each new adopter still starts from nothing enabled and walks through.
57
+
58
+ ## What this is NOT
59
+
60
+ - **Not a grant of authority.** Repeated because it is the invariant most likely to erode.
61
+ - **Not a way to declare a skill that does not exist.** A speciality may name only skills the instance actually has, or adoption offers a walkthrough of something that will 404 at the moment a person most needs it.
62
+ - **Not blanketed by craft.** A speciality's skills belong to the speciality, never to the craft above it.
63
+ - **Not authored by us.** [ADR 0296](0296-a-role-speciality-is-data-over-three-existing-surfaces.md): the deliverable is the mechanism, not a catalogue of specialities.
64
+
65
+ ## Consequences
66
+
67
+ **The four documentation sites stop lying today, and do not start lying differently.** They currently claim a shipped ingredient. Until the build lands they must say *declared, decided, not yet built*, pointing here — the ADR, the migration header, `specialities.js`, and the module `CLAUDE.md`.
68
+
69
+ **A skill's own description becomes load-bearing.** The walkthrough is only as good as the text it shows, and skill descriptions were written to be *matched* by a model, not *read* by a newcomer. Whoever builds the walkthrough will find this.
70
+
71
+ **Adoption gets slower, on purpose.** That is the cost the ruling accepts.
72
+
73
+ ## Build order
74
+
75
+ Deliberately separate tasks, because the middle one is where the ruling lives and it should not be a sub-bullet of a schema change:
76
+
77
+ 1. **Schema + validation** — the offered set on the catalog, the enabled set on the adoption, `validateDraft`, the routes. Refuses a skill the instance does not have.
78
+ 2. **The walkthrough** — the adoption flow that presents each skill and takes a decision. The ruling's real deliverable.
79
+ 3. **Injection** — the enabled set reaches the session through the contract the Conductor already injects; a line naming the skills this builder leans on, per [ADR 0296](0296-a-role-speciality-is-data-over-three-existing-surfaces.md)'s pattern of using surfaces that exist.
80
+
81
+ Nothing is built by this ADR. It is the decision task 1003978 asked to be made first.
@@ -401,3 +401,4 @@ This keeps the decision history honest and traceable.
401
401
  | 0307 | [**Autobongos: a deterministic supervisor, agentic workers** ([task 1003900](https://cloudbongos.com/builders#/task/1003900), goal 1000119). An always-on machine works the queue unattended inside owner-allowlisted goals. Most of the loop already existed wired to the wrong trigger — `sequence.js`'s ten-reason halt matrix (ADR 0153), an open blocker as the hold state, the Windows-aware headless spawn seam, `overnight-builder` — but the trigger was cron rather than capacity, and the **supervisor was itself a Claude session**. Decided: the supervisor is a **program**, only the worker is a session; the gauge is the statusLine's `rate_limits.five_hour`/`seven_day`, which per Anthropic's own note is absent until a session's first API response and so **cannot be read cold** (a statusline script persists it, a stale file is *unknown*, never *headroom*); **one session per task, metering not estimating** (a successor cannot be claimed until its predecessor ships, so a batch produces the same order while risking a half-done window, and a fresh session avoids a checkout going stale behind main); **completion is read from the ledger, never the session** (a worker that stopped needing a decision returns `subtype: success`, identical to one that finished); **no terminal stop, only speeds** (three failures → a probing state that self-heals; the only true stop is the server-side kill switch); the machine holds **no push credential**, so a grader outage stops the run rather than shipping unreviewed. The load-bearing finding was **tested before it was decided**: a worker told plainly to stop and ask instead settled four judgement calls and wrote them to disk — competent, and fatal to any design that waits for raised hands. Hence **gate before, record after, nothing in between**: the halt matrix refuses ambiguous work at pick time, and the worker's verdict must declare what the spec did not determine. Rejected: per-call `--permission-prompt-tool` (a thousand weak gates, and one slow supervisor stalls every worker), `--max-budget-usd` (caps dollars; on a subscription the constraint is capacity, so the bound is wall-clock), extending `overnight-builder`, and a terminal stop. Deliberately not built: mid-flight question answering, portability, per-task estimation.](0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md) | autonomy / sessions |
402
402
  | 0308 | [**An eleventh working area: Autobongos** ([task 1003900](https://cloudbongos.com/builders#/task/1003900), goal 1000119). Amends [ADR 0264](0264-the-ten-working-areas.md), which nine days earlier collapsed 38 goals into **ten** areas held until 5,000 builders. Area 11 is Autobongos ([goal 1000119](https://cloudbongos.com/builders#/goal/1000119), owner Lars). **Area 6 was the obvious home and was rejected**: area 6 is about a human's experience of the platform, and this area has no human in it — which limits to spend, which fences hold unsupervised, how it recovers with nobody at the keyboard. Those get less attention next to work with a user in front of it, not better answers. It **reopens** goal 1000028 (*Autonomy groundwork*), archived in the 38→10 cut with no successor and carried as `succeeds_goal_id`, so the lineage is in the ledger rather than the prose — and ADR 0264 §3 had already left "fold, keep as an eleventh, or close" as a live owner question. Admitting it cost something recorded: BONGOS-V2 is `building`, so its goal set is closed (ADR 0250) and `POST /goals` refuses `version_not_planning` — an Archon reopened it with a stored `admit_reason`, which is the cut holding well enough that adding to it left evidence. Does not decide: the ten-area discipline stands (one exception, written reason, 5,000-builder horizon unchanged), 0264 §3's other two open rows, or a second goal for the area.](0308-an-eleventh-working-area.md) | methodology / goals |
403
403
  | 0309 | [**Module enablement is a database row, above the committed pack and below env** ([task 1002857](https://cloudbongos.com/builders#/task/1002857), goal 1000091). The hall Modules-tab toggle ([ADR 0107](0107-module-upstreaming-contribution-to-core-catalog.md) §9) wrote `config/modules.json` and reloaded the cache, so it looked like it worked — but that file is the committed instance pack and the pull-deploy runs `git reset --hard origin/main`, so the toggle reverted on the next deploy, **silently**. Enablement moves to `module_enablement_overrides` (migration `core_248`), read as one precedence layer: neutral pack → instance pack → **DB override** → env → default. **Above the pack** because the pack is what a deploy restores, so an override below it would be erased by the event it exists to survive; **below env** because `<PREFIX>_MODULE_<KEY>` is the incident knob and must not need a reachable database. **Auto-committing to `main` stays rejected** ([ADR 0016](<redacted>.md)): durability belongs in the DB, not in an HTTP handler that writes to the default branch. Two open design points settled: the **scope key** is the resolved instance pack path (the only axis two instances sharing one database differ on, so one sibling's toggle cannot flip the other's module), and the DB is read into a **snapshot** refreshed at boot and on write rather than inline, because `loadModules()` is synchronous and making it async would ripple into every consumer of a module flag. An unreachable database is a **no-op** — no row means "no opinion", not "off". Does not decide: whether a toggle should also restart the process so remounting is immediate.](0309-durable-module-enablement-db-override.md) | modules / ops |
404
+ | 0310 | [**A speciality OFFERS skills; the adopter turns them on, one at a time, having been shown what each does** ([task 1003978](https://cloudbongos.com/builders#/task/1003978), goal 1000095). Completes [ADR 0296](0296-a-role-speciality-is-data-over-three-existing-surfaces.md), which defined a speciality as knowledge + skills + an interaction contract and shipped two of the three: `grep -i skill` across the module's schema, domain and routes returns only the header sentences describing a thing that is not there — prose asserting behaviour the code does not have, and the feature's own definition at that. Task 1003978 withheld an implementation until the authority question was answered, and the answer is the strong one: **naming a skill GRANTS NOTHING**. Skills are materialised by `claude-materialize.js` and permission is rank-gated server-side with no caching ([ADR 0016](<redacted>.md)); a speciality offers a menu, and a menu is not an entitlement. **Two layers, because one layer is the blanket the owner ruled against**: the catalog holds what a speciality OFFERS, the already-per-builder `specialities_adoptions` row holds what THIS builder turned on — so two economist ideators share a speciality's knowledge and contract and need not share one enabled skill. **The default is nothing enabled, and that is a position about LEARNING rather than a safe default**: the owner's ruling is that adoption must *walk through what comes with each* because *people need to learn the system*. A settings page of unchecked boxes satisfies the letter and defeats the intent, so the bar is a user test — a person who has finished adopting can say what each skill does. Sharing needs no new mechanism: `owner_kind` / `visibility` / `offered` / `sellable` already ship the private→project→public ladder, and skills ride it. Builds nothing; the three-task order (schema, walkthrough, injection) is filed, and the four documentation sites that claimed a shipped ingredient now say decided-not-built.](0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md) | specialities / methodology |
@@ -2254,5 +2254,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
2254
2254
  landed since 1.19.888 with no explicit bump. run 35470239834. (task 1002620)
2255
2255
  1.19.890 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2256
2256
  landed since 1.19.889 with no explicit bump. run 35470624284. (task 1002620)
2257
+ 1.19.891 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2258
+ landed since 1.19.890 with no explicit bump. run 35471575521. (task 1002620)
2259
+ 1.19.892 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2260
+ landed since 1.19.891 with no explicit bump. run 35471773244. (task 1002620)
2257
2261
  ---------------------------------------------------------------------------
2258
2262
  ```
@@ -1,5 +1,5 @@
1
1
  -- specialities_001_specialities.sql — role sub-specialities: a named bundle of
2
- -- context, skills and an interaction contract that sits UNDER a craft
2
+ -- context, skills (ADR 0310 — decided 2026-09-19, NOT built: no column here holds them) and an interaction contract that sits UNDER a craft
3
3
  -- (task 1003855, goal 1000095; ADR 0296).
4
4
  --
5
5
  -- WHAT A SPECIALITY IS, AND WHY IT IS DATA RATHER THAN CODE. The owner's framing
@@ -1,5 +1,5 @@
1
1
  // modules/specialities/specialities.js — role SUB-SPECIALITIES: a named bundle of
2
- // knowledge, skills and an interaction contract that sits UNDER a craft
2
+ // knowledge, skills (ADR 0310 — decided, NOT yet built) and an interaction contract that sits UNDER a craft
3
3
  // (task 1003855, goal 1000095; ADR 0296).
4
4
  //
5
5
  // WHAT THIS IS FOR. A craft says how you work (engineer / ideator / artist / ui).
@@ -14,7 +14,12 @@
14
14
  // resolved into GET /me and injected by the
15
15
  // Conductor hook — a speciality contract rides
16
16
  // that SAME path, it does not add a second one
17
- // • declared skills the ADR 0083 manifest contribution seam
17
+ // • declared skills NOT BUILT. Decided in ADR 0310 (task 1003978)
18
+ // and deliberately not implemented there: a speciality
19
+ // will OFFER skills, and each adopter enables them one
20
+ // at a time after being shown what each does. Nothing
21
+ // in this file, the schema or the routes carries it yet —
22
+ // `grep -i skill` here finds only prose.
18
23
  // The genuinely new part is the knowledge bundle, which is why the migration adds
19
24
  // tables for it and this file carries the rules over them.
20
25
  //
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.890",
3
+ "version": "1.19.892",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.890",
9
+ "version": "1.19.892",
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.890",
3
+ "version": "1.19.892",
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",
@@ -67,6 +67,10 @@ const FILES = [
67
67
  'scripts/gds/start.js',
68
68
  'scripts/gds/context-pack.js', // start.js renders through this
69
69
  'scripts/gds/status.js',
70
+ // status.js resolves its default version at runtime instead of naming one
71
+ // (task 1004071) -- so the packaged CLI needs this too, or `bongos status`
72
+ // dies on a missing module. tests/cli_package.mjs caught exactly that.
73
+ 'scripts/gds/version-select.js',
70
74
  'scripts/gds/task.js',
71
75
  'scripts/gds/recall.js',
72
76
  'scripts/gds/cost.js',
@@ -19,14 +19,24 @@
19
19
  //
20
20
  // Exit codes: 0 ok · 1 network/api error · 2 bad invocation · 3 not found.
21
21
 
22
- const { API_BASE, cliClient, assertInstanceMatch } = require('./cli-lib');
22
+ const { API_BASE, cliClient, assertInstanceMatch, cliExit } = require('./cli-lib');
23
23
  const { resolveEnv } = require('../../src/instance-config');
24
+ const { resolveDefaultVersion } = require('./version-select.js');
24
25
 
25
- // The active internal version; override with <PREFIX>_STATUS_DEFAULT_VERSION (or pass
26
- // --version). Bumped GDS-V4 → BONGOS-V1 (task 1709, ADR 0103): GDS-V4 was
27
- // consolidated into BONGOS-V1 (the Cloud Bongos platform version) and frozen, so
28
- // the bare `/status` card should default to the live version, not a sealed one.
29
- const DEFAULT_VERSION = resolveEnv('STATUS_DEFAULT_VERSION') || 'BONGOS-V1';
26
+ // The active internal version is RESOLVED, never named here. Override with
27
+ // <PREFIX>_STATUS_DEFAULT_VERSION or --version.
28
+ //
29
+ // WHY THERE IS NO LITERAL (task 1004071). This line used to read
30
+ // const DEFAULT_VERSION = resolveEnv('STATUS_DEFAULT_VERSION') || 'BONGOS-V1';
31
+ // under a comment promising "the bare /status card should default to the live
32
+ // version, not a sealed one" — the same fix task 1709 had already applied once,
33
+ // for GDS-V4 → BONGOS-V1. BONGOS-V1 then sealed, nobody bumped the literal, and
34
+ // /status served a shipped version's closing scoreboard (140/203 satisfied) as
35
+ // the project's current status. It reads as a healthy project, which is why it
36
+ // sat: nothing in the output says `shipped`, and /status is what a session runs
37
+ // precisely when it does NOT already know. Bumping the literal again would only
38
+ // re-arm the same bomb for whoever seals BONGOS-V2, so the rule is derived from
39
+ // `versions.status` instead — the selection capture.js has always used.
30
40
 
31
41
  // Compact, status-first ordering for the "remaining: …" summaries.
32
42
  const STATUS_ORDER = ['shipped', 'confirmed', 'completed', 'claimed', 'ready', 'blocked', 'backlog'];
@@ -53,7 +63,36 @@ function parseArgs(argv) {
53
63
  else if (!criterion) criterion = a.toUpperCase();
54
64
  else return { error: `unexpected argument: ${a}` };
55
65
  }
56
- return { version: version || DEFAULT_VERSION, criterion, json, widget, hook };
66
+ // No `versionExplicit` companion flag: `version !== null` already IS that
67
+ // fact, and resolveVersion returns `defaulted`/`source` for anyone who needs
68
+ // to act on it. A second field carrying the same truth is one that can drift.
69
+ return { version: version || null, criterion, json, widget, hook };
70
+ }
71
+
72
+ // Resolve which version this invocation is about. Precedence: an explicit
73
+ // --version (or bare token) > the env override > the live BUILDING version.
74
+ // Returns { version, defaulted, source }; version is NULL when nothing is
75
+ // building, which the caller must treat as "stop and say so", never as a guess.
76
+ // Deps are injectable so the precedence is unit-tested without a session.
77
+ async function resolveVersion(opts, deps = {}) {
78
+ if (opts.version) return { version: opts.version, defaulted: false, source: 'flag' };
79
+ const env = (deps.resolveEnv || resolveEnv)('STATUS_DEFAULT_VERSION');
80
+ if (env) return { version: env, defaulted: true, source: 'env' };
81
+ const resolve = deps.resolveDefaultVersion || resolveDefaultVersion;
82
+ const building = await resolve();
83
+ if (building) return { version: building, defaulted: true, source: 'building' };
84
+ return { version: null, defaulted: true, source: 'none' };
85
+ }
86
+
87
+ // One line naming a version the USER did not, so a cross-version answer cannot
88
+ // pass unnoticed. `Cn` is version-scoped and the versions number independently
89
+ // — BONGOS-V1 C8 is `money-one-unit`, BONGOS-V2 C8 is `wa6-kernel-and-packs` —
90
+ // so a silently-defaulted version answers a different question than the one
91
+ // asked. Empty when the user named the version: they already know.
92
+ function defaultedVersionNote(r) {
93
+ if (!r || !r.defaulted || !r.version) return '';
94
+ const why = r.source === 'env' ? 'from STATUS_DEFAULT_VERSION' : 'the version currently building';
95
+ return `(no version given — showing ${r.version}, ${why}. Pass --version to pick another.)`;
57
96
  }
58
97
 
59
98
  async function fetchProgress(version) {
@@ -325,7 +364,38 @@ async function main() {
325
364
  // apiCall/cliClient guard, so without this `bongos status` would silently print another
326
365
  // instance's progress. Skip only in --hook mode (the automated card path stays fail-open).
327
366
  if (!opts.hook) await assertInstanceMatch();
328
- const data = await fetchProgress(opts.version);
367
+
368
+ // Resolve the version before anything fetches. --hook stays FAIL-OPEN like the
369
+ // instance check above: the automated card path must degrade, not throw, so a
370
+ // lookup that cannot answer emits an empty envelope rather than killing the hook.
371
+ //
372
+ // COST: one extra sequential GET /versions before the progress fetch, and it
373
+ // cannot be parallelised — the id IS the second call's path. It is paid only
374
+ // when no version was named: --version and STATUS_DEFAULT_VERSION both
375
+ // short-circuit it, so an instance that minds the round trip on the
376
+ // session-start card path sets the env var once and pays nothing. That is the
377
+ // deliberate trade against the alternative, which is the literal that spent
378
+ // months reporting a sealed version as current.
379
+ const picked = await resolveVersion(opts).catch(() => ({ version: null, defaulted: true, source: 'none' }));
380
+ if (!picked.version) {
381
+ if (opts.hook) {
382
+ process.stdout.write(JSON.stringify({
383
+ widgets_enabled: false, html: '', markdown: '',
384
+ warnings: ['status: no version is currently `building` — nothing to report.'],
385
+ }) + '\n');
386
+ return;
387
+ }
388
+ console.error('status: no version is currently `building` — cannot resolve a default.');
389
+ console.error(' Open one, or pass --version explicitly (e.g. --version BONGOS-V2).');
390
+ // cliExit, NOT process.exit: resolveVersion above just made a network round
391
+ // trip, and a hard exit while the request machinery is tearing down aborts
392
+ // natively on Windows + Node 24 (UV_HANDLE_CLOSING) — exit 127 AFTER the
393
+ // message prints, so a caller reads "tooling broken" instead of "no building
394
+ // version". The other exits in this file are pre-request or have substantial
395
+ // rendering between; this branch has neither. See scripts/gds/CLAUDE.md.
396
+ return cliExit(3);
397
+ }
398
+ const data = await fetchProgress(picked.version);
329
399
 
330
400
  // Resolve the requested criterion (if any) once — every output mode needs it.
331
401
  let c = null;
@@ -333,7 +403,10 @@ async function main() {
333
403
  const n = Number(opts.criterion.slice(1));
334
404
  c = data.criteria.find((x) => x.cnum === n);
335
405
  if (!c) {
336
- console.error(`status: ${opts.version} has no criterion ${opts.criterion} (it has C1–C${data.criteria.length}).`);
406
+ // Name the version, not just the criterion: on a DEFAULTED version this is
407
+ // the reader's only clue that C13 was looked up somewhere they didn't pick.
408
+ console.error(`status: ${picked.version} has no criterion ${opts.criterion} (it has C1–C${data.criteria.length}).`);
409
+ if (picked.defaulted) console.error(` ${defaultedVersionNote(picked)}`);
337
410
  process.exit(3);
338
411
  }
339
412
  }
@@ -356,9 +429,12 @@ async function main() {
356
429
  // else the whole version rollup).
357
430
  if (opts.json) { console.log(JSON.stringify(c || data, null, 2)); return; }
358
431
 
359
- // Default human output — the existing plain-text rollup, unchanged.
360
- if (c) { printOne(data, c); return; }
361
- printAll(data);
432
+ // Default human output — the existing plain-text rollup, plus one line when
433
+ // the version was resolved rather than chosen (--json/--widget/--hook are
434
+ // machine interfaces and stay byte-identical).
435
+ if (c) printOne(data, c); else printAll(data);
436
+ const note = defaultedVersionNote(picked);
437
+ if (note) console.log(` ${note}\n`);
362
438
  }
363
439
 
364
440
  if (require.main === module) {
@@ -368,4 +444,4 @@ if (require.main === module) {
368
444
  });
369
445
  }
370
446
 
371
- module.exports = { parseArgs, renderStatusWidget, renderStatusMarkdown };
447
+ module.exports = { parseArgs, resolveVersion, defaultedVersionNote, renderStatusWidget, renderStatusMarkdown };
@@ -35,13 +35,31 @@ const VERSION_LITERAL = String.raw`['"][A-Z][A-Z0-9]*-V\d+['"]`;
35
35
  const VERSION_FIELD = String.raw`version_id|versionId|suggested_version|target_version|DEFAULT_VERSION|VERSION_ID`;
36
36
  const ASSIGNED = new RegExp(`(?:${VERSION_FIELD})\\s*['"]?\\s*[:=][^,;)]*${VERSION_LITERAL}`);
37
37
  const DEFAULTED = new RegExp(`--version[^\\n]*?\\|\\|\\s*${VERSION_LITERAL}`);
38
+ // A version field FALLING BACK to a literal through `||`. ASSIGNED cannot see
39
+ // this shape: its `[^,;)]*` stops at the first `)`, so any call between the `=`
40
+ // and the literal hides it — `DEFAULT_VERSION = resolveEnv('X') || 'BONGOS-V1'`
41
+ // scanned CLEAN while `DEFAULT_VERSION = 'BONGOS-V1'` was caught (task 1004071).
42
+ // That is the exact line that served a sealed version as /status's default for
43
+ // months. Kept as its own narrow pattern rather than loosening ASSIGNED to
44
+ // `[^;]*`, which would fire on any line merely mentioning a field and a literal
45
+ // — the over-broad source assertion this repo keeps relearning (learning 1000196).
46
+ const FALLBACK = new RegExp(`(?:${VERSION_FIELD})\\s*['"]?\\s*[:=][^;]*?\\|\\|\\s*${VERSION_LITERAL}`);
38
47
 
39
48
  // path::reason. A justified entry, one line, like every other guard here.
49
+ //
50
+ // A REASON MAY CITE THE CODE'S SHAPE, NEVER THE DATABASE'S CONTENTS (task
51
+ // 1004071). status.js was exempted because "TWO versions are currently building
52
+ // — so 'most recent building' is not a derivable answer here." True the day it
53
+ // was written; false once CB-V1 sealed. Nothing re-checks prose, so the entry
54
+ // outlived its justification in silence and /status went on serving a SHIPPED
55
+ // version's closing scoreboard as the project's current status. An entry whose
56
+ // truth depends on rows is a timer nobody set. `version_literals.mjs` now fails
57
+ // CI on an entry that is outright DEAD; it cannot police one that is merely
58
+ // stale, which is why that judgement has to be made here, when the entry is
59
+ // written.
40
60
  const VERSION_LITERAL_ALLOW = new Map([
41
61
  ['scripts/gds/grade-smoke.js',
42
62
  'GOOD_TASK / BAD_TASK are in-memory fixtures handed to grader.gradeSubagent; they are never POSTed, so no version_closed can refuse them and the literal cannot rot into a failure.'],
43
- ['scripts/gds/status.js',
44
- 'DEFAULT_VERSION is an owner-facing dashboard default with an env override, and TWO versions are currently building — so "most recent building" is not a derivable answer here. Retargeting what /status shows by default is an owner decision, not a refactor.'],
45
63
  ]);
46
64
 
47
65
  function isExempt(rel) {
@@ -67,7 +85,7 @@ function scanScripts(files, read) {
67
85
  scanned += 1;
68
86
  if (VERSION_LITERAL_ALLOW.has(rel)) continue;
69
87
  stripComments(src, rel).forEach((line, i) => {
70
- if (ASSIGNED.test(line) || DEFAULTED.test(line)) {
88
+ if (ASSIGNED.test(line) || DEFAULTED.test(line) || FALLBACK.test(line)) {
71
89
  violations.push(
72
90
  `${rel}:${i + 1} names a version id where one should be RESOLVED: ${line.trim().slice(0, 90)}`
73
91
  + ' — a literal works until that version ships, then version_closed refuses every write and the'
@@ -119,4 +137,4 @@ function main() {
119
137
 
120
138
  if (require.main === module) main();
121
139
 
122
- module.exports = { checkNoVersionLiterals, scanScripts, stripComments, isExempt, VERSION_LITERAL_ALLOW, ASSIGNED, DEFAULTED };
140
+ module.exports = { checkNoVersionLiterals, scanScripts, stripComments, isExempt, VERSION_LITERAL_ALLOW, ASSIGNED, DEFAULTED, FALLBACK };
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.890'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.892'; // 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');
@@ -0,0 +1,136 @@
1
+ // tests/status_default_version.mjs — /status resolves its default version at
2
+ // RUNTIME and never falls back to a sealed one (task 1004071).
3
+ //
4
+ // THE INCIDENT. `scripts/gds/status.js:29` read
5
+ // const DEFAULT_VERSION = resolveEnv('STATUS_DEFAULT_VERSION') || 'BONGOS-V1';
6
+ // while the comment three lines above stated the rule it was meant to keep:
7
+ // "the bare /status card should default to the live version, not a sealed one."
8
+ // BONGOS-V1 was later sealed and the literal was never bumped, so bare `/status`
9
+ // served a shipped version's closing scoreboard — 140/203 satisfied — as the
10
+ // project's CURRENT status. The output looks perfectly healthy, which is why it
11
+ // sat: nothing in the header says `shipped`, and /status is exactly what a
12
+ // session runs when it does NOT already know the answer and so cannot tell.
13
+ //
14
+ // WHY THE GUARD DID NOT CATCH IT. version-literal-guard.js already hunts this
15
+ // shape and already owns the remedy (version-select.js). status.js was on its
16
+ // allowlist, justified by "TWO versions are currently building — so 'most recent
17
+ // building' is not a derivable answer here." That was TRUE when written
18
+ // (BONGOS-V1 + CB-V1). It is false now: one version is building. The entry did
19
+ // not become wrong through anyone's error — the world moved under a justification
20
+ // that was a claim about live state, with nothing re-checking it. That staleness
21
+ // is the class defect, and `version_literals.mjs` now pins it.
22
+ //
23
+ // The rule these tests hold: an EXPLICIT choice always wins (flag, then env),
24
+ // a default is RESOLVED, and when nothing is building we stop rather than guess.
25
+ import { strict as assert } from 'node:assert';
26
+ import { test } from 'node:test';
27
+ import { createRequire } from 'node:module';
28
+ import path from 'node:path';
29
+ import { fileURLToPath } from 'node:url';
30
+
31
+ const require = createRequire(import.meta.url);
32
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
33
+ const status = require(path.join(ROOT, 'scripts', 'gds', 'status.js'));
34
+ const { parseArgs, resolveVersion } = status;
35
+
36
+ // A deps bundle with nothing configured: no env override, nothing building.
37
+ const deps = (over = {}) => ({
38
+ resolveEnv: () => null,
39
+ resolveDefaultVersion: async () => null,
40
+ ...over,
41
+ });
42
+
43
+ // ---- parseArgs no longer bakes the default in -----------------------------
44
+ test('parseArgs leaves version NULL when none is given, so main can resolve it', () => {
45
+ const o = parseArgs(['node', 'status.js']);
46
+ assert.equal(o.version, null, 'a literal default here is the bug this task fixes');
47
+ assert.equal(o.criterion, null, 'and nothing was mistaken for a criterion');
48
+ });
49
+
50
+ test('an explicit --version is still carried through', () => {
51
+ const o = parseArgs(['node', 'status.js', '--version', 'BONGOS-V2']);
52
+ assert.equal(o.version, 'BONGOS-V2');
53
+
54
+ });
55
+
56
+ test('a bare version token is carried through too', () => {
57
+ const o = parseArgs(['node', 'status.js', 'BONGOS-V2']);
58
+ assert.equal(o.version, 'BONGOS-V2');
59
+
60
+ });
61
+
62
+ test('a bare Cn token is a criterion, not a version', () => {
63
+ const o = parseArgs(['node', 'status.js', 'C8']);
64
+ assert.equal(o.criterion, 'C8');
65
+ assert.equal(o.version, null, 'C8 must not be mistaken for a version id');
66
+ });
67
+
68
+ // ---- the resolution order -------------------------------------------------
69
+ test('an explicit flag beats both the env override and the building lookup', async () => {
70
+ const r = await resolveVersion({ version: 'GDS-V3' }, deps({
71
+ resolveEnv: () => 'ENV-V9',
72
+ resolveDefaultVersion: async () => 'BONGOS-V2',
73
+ }));
74
+ assert.equal(r.version, 'GDS-V3');
75
+ assert.equal(r.defaulted, false, 'the user chose it, so nothing was defaulted');
76
+ assert.equal(r.source, 'flag');
77
+ });
78
+
79
+ test('the env override beats the building lookup, and still counts as defaulted', async () => {
80
+ const r = await resolveVersion({ version: null }, deps({
81
+ resolveEnv: () => 'ENV-V9',
82
+ resolveDefaultVersion: async () => 'BONGOS-V2',
83
+ }));
84
+ assert.equal(r.version, 'ENV-V9');
85
+ assert.equal(r.source, 'env');
86
+ assert.equal(r.defaulted, true, 'the user did not name it on this invocation');
87
+ });
88
+
89
+ test('with no flag and no env, the BUILDING version is resolved at runtime', async () => {
90
+ const r = await resolveVersion({ version: null }, deps({
91
+ resolveDefaultVersion: async () => 'BONGOS-V2',
92
+ }));
93
+ assert.equal(r.version, 'BONGOS-V2');
94
+ assert.equal(r.source, 'building');
95
+ assert.equal(r.defaulted, true);
96
+ });
97
+
98
+ test('THE INCIDENT: when nothing is building we return null, never a sealed guess', async () => {
99
+ // version-select.js already refuses to guess here, and status must not
100
+ // reintroduce the guess downstream of it. A sealed version's final scoreboard
101
+ // presented as current status is worse than an error, because it looks fine.
102
+ const r = await resolveVersion({ version: null }, deps());
103
+ assert.equal(r.version, null);
104
+ assert.equal(r.source, 'none');
105
+ });
106
+
107
+ test('a resolved default never returns a SHIPPED version', async () => {
108
+ // Wired through the real selection rule rather than a stub, so this test fails
109
+ // if pickDefaultVersion is ever loosened to fall back to the newest-any-status.
110
+ const { pickDefaultVersion } = require(path.join(ROOT, 'scripts', 'gds', 'version-select.js'));
111
+ const versions = [
112
+ { id: 'BONGOS-V1', status: 'shipped', started_at: '2026-06-20' },
113
+ { id: 'CB-V1', status: 'shipped', started_at: '2026-06-21' },
114
+ { id: 'BONGOS-V2', status: 'building', started_at: '2026-06-22' },
115
+ ];
116
+ const r = await resolveVersion({ version: null }, deps({
117
+ resolveDefaultVersion: async () => pickDefaultVersion(versions),
118
+ }));
119
+ assert.equal(r.version, 'BONGOS-V2', 'the only building row wins over two newer-looking sealed ones');
120
+ });
121
+
122
+ // ---- the drill-in must say which version it answered about ----------------
123
+ test('a DEFAULTED version is announced, so a cross-version answer cannot pass unnoticed', () => {
124
+ // Cn is version-scoped and the versions number independently: BONGOS-V1 C8 is
125
+ // `money-one-unit`, BONGOS-V2 C8 is `wa6-kernel-and-packs`. Asking for "C8"
126
+ // against a silently-defaulted version answers a different question than the
127
+ // one asked, and the old output gave the reader nothing to notice it by.
128
+ const note = status.defaultedVersionNote({ version: 'BONGOS-V2', defaulted: true, source: 'building' });
129
+ assert.match(note, /BONGOS-V2/, 'it names the version it resolved');
130
+ assert.match(note, /building/, 'and why that one');
131
+ });
132
+
133
+ test('an explicitly chosen version is NOT announced — no noise when the user said it', () => {
134
+ const note = status.defaultedVersionNote({ version: 'BONGOS-V2', defaulted: false, source: 'flag' });
135
+ assert.equal(note, '', 'the user already knows which version they asked for');
136
+ });
@@ -199,3 +199,95 @@ test('ship-flow.js imports every cross-module function it calls', () => {
199
199
  }
200
200
  }
201
201
  });
202
+
203
+ // ---- an exemption must not outlive its justification ----------------------
204
+ // Task 1004071. scripts/gds/status.js sat on VERSION_LITERAL_ALLOW justified by
205
+ // "TWO versions are currently building — so 'most recent building' is not a
206
+ // derivable answer here." That was true the day it was written (BONGOS-V1 and
207
+ // CB-V1). It is false now: one version builds. Nothing noticed, because the
208
+ // reason is a claim about LIVE STATE and an allowlist holds prose, which no CI
209
+ // step can re-check. The entry did not become wrong through anyone's error —
210
+ // the world moved under it. Hence the rule now written beside the Map: a reason
211
+ // may cite the CODE's shape, never the database's contents.
212
+ //
213
+ // SCANNING AN ALLOWLISTED PATH PROVES NOTHING. scanScripts applies the allowlist
214
+ // itself (`if (VERSION_LITERAL_ALLOW.has(rel)) continue`), so handing it an
215
+ // exempt path returns zero violations whatever the file says — the first draft
216
+ // of this test read that vacuous [] as "no literal" and reported every entry as
217
+ // dead, including grade-smoke.js, which plainly still carries two. The source is
218
+ // therefore re-scanned under a synthetic path that is on neither the allowlist
219
+ // nor isExempt's patterns, which is the only way to see what the guard WOULD say.
220
+ const PROBE = 'scripts/gds/__allowlist_probe__.js';
221
+ const scanUnexempt = (src) => g.scanScripts([PROBE], () => src);
222
+
223
+ test('the probe path really is unexempt — otherwise every check below is vacuous', () => {
224
+ assert.ok(!g.VERSION_LITERAL_ALLOW.has(PROBE), 'probe must not be allowlisted');
225
+ assert.ok(!g.isExempt(PROBE), 'probe must not match an isExempt pattern');
226
+ // And it must actually fire on a known-bad line, or a silent-empty scanner
227
+ // would let every assertion below pass for the wrong reason (learning: a
228
+ // static scan breaks by finding nothing, and green looks identical to working).
229
+ assert.equal(scanUnexempt(" version_id: 'PMS-V1',").violations.length, 1);
230
+ });
231
+
232
+ test('no allowlist entry is DEAD — each exempt file still contains the shape it is exempt for', () => {
233
+ for (const [rel] of g.VERSION_LITERAL_ALLOW) {
234
+ let src = null;
235
+ try { src = read(...rel.split('/')); } catch { /* handled below */ }
236
+ assert.ok(src !== null, `${rel} is allowlisted but does not exist — drop the entry`);
237
+ assert.ok(
238
+ scanUnexempt(src).violations.length > 0,
239
+ `${rel} is on VERSION_LITERAL_ALLOW but no longer names a version literal — `
240
+ + 'the exemption is dead and must be removed, or it will silently cover a '
241
+ + 'literal someone reintroduces later.'
242
+ );
243
+ }
244
+ });
245
+
246
+ test('THE INCIDENT: status.js is no longer exempt, and names no version literal', () => {
247
+ // Both halves must hold. Dropping the entry while leaving the literal reds the
248
+ // guard; fixing the literal while leaving the entry leaves status.js covered by
249
+ // nothing for good. Scanned under the probe path so this stays honest even if
250
+ // someone re-adds the allowlist entry.
251
+ assert.ok(
252
+ !g.VERSION_LITERAL_ALLOW.has('scripts/gds/status.js'),
253
+ 'status.js resolves its default at runtime now — it must not stay exempt'
254
+ );
255
+ assert.deepEqual(scanUnexempt(read('scripts', 'gds', 'status.js')).violations, []);
256
+ });
257
+
258
+ // ---- the fallback shape ASSIGNED could not see ----------------------------
259
+ test('THE BLIND SPOT: a version field falling back to a literal through || is caught', () => {
260
+ // The literal line that shipped, verbatim. ASSIGNED misses it because its
261
+ // `[^,;)]*` stops at the `)` of resolveEnv(...) — so wrapping a literal in any
262
+ // call defeated the guard, while the bare assignment was caught.
263
+ const bad = "const DEFAULT_VERSION = resolveEnv('STATUS_DEFAULT_VERSION') || 'BONGOS-V1';";
264
+ assert.equal(g.ASSIGNED.test(bad), false, 'documents WHY a third pattern was needed');
265
+ assert.equal(g.FALLBACK.test(bad), true);
266
+ assert.equal(scanUnexempt(bad).violations.length, 1, 'and the scanner reports it');
267
+ });
268
+
269
+ test('the bare assignment ASSIGNED already caught still fails', () => {
270
+ assert.equal(scanUnexempt("const DEFAULT_VERSION = 'BONGOS-V1';").violations.length, 1);
271
+ });
272
+
273
+ test('FALLBACK stays narrow — it does not fire on a version field alone', () => {
274
+ // The over-broad source assertion this repo keeps relearning (learning 1000196).
275
+ // Loosening ASSIGNED to `[^;]*` would have flagged all of these.
276
+ for (const ok of [
277
+ " const v = versions.find((x) => x.version_id === id) || null;",
278
+ " if (!version_id) throw new Error('need a version');",
279
+ " console.log(`resolved ${version_id}`);",
280
+ " const label = version_id || 'unknown';",
281
+ ]) {
282
+ assert.deepEqual(scanUnexempt(ok).violations, [], `must not flag: ${ok.trim()}`);
283
+ }
284
+ });
285
+
286
+ test('a comment showing the old bad line does not re-trip the guard', () => {
287
+ // status.js now quotes its own former literal in the explanatory comment. If
288
+ // comment-stripping regressed, the file it documents would red the guard.
289
+ assert.deepEqual(
290
+ scanUnexempt("// const DEFAULT_VERSION = resolveEnv('X') || 'BONGOS-V1';").violations,
291
+ []
292
+ );
293
+ });