@bongos/core 1.19.626 → 1.19.627
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 +32 -17
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/docs/adr/0270-rank-floor-derives-without-a-prediction.md +59 -0
- package/docs/adr/README.md +1 -0
- package/docs/module-api-changelog.md +2 -0
- package/migrations/core_236_rank_floor_backfill.sql +56 -0
- package/modules/lifecycle/db-rank-authz.js +27 -2
- package/modules/lifecycle/db-tasks.js +82 -13
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/rank_floor_wiring.mjs +135 -0
- package/tests/rank_gate.mjs +45 -0
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.627",
|
|
6
|
+
"core_contract": "1.19.627",
|
|
7
|
+
"source_commit": "9132daa97509f28f38b53cf54a6311f0a43f788d",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-09T18:37:19.003Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 465,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2098,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2587,
|
|
20
|
+
"tree_sha256": "6f56a454e970eef9adac1d969b0e28f75d79cdc9843f666de389fd2766607e55",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/backlog-review/SKILL.md",
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
{
|
|
163
163
|
"path": ".claude/skills/goal-create/SKILL.md",
|
|
164
164
|
"mode": "0000644",
|
|
165
|
-
"sha256": "
|
|
165
|
+
"sha256": "535eb9fd7d3effdc2df7e003bb227c65600055b49dc98bc6f832d3198ba417a0"
|
|
166
166
|
},
|
|
167
167
|
{
|
|
168
168
|
"path": ".claude/skills/goal-review/SKILL.md",
|
|
@@ -1869,10 +1869,15 @@
|
|
|
1869
1869
|
"mode": "0000644",
|
|
1870
1870
|
"sha256": "cc44045ea59f76dabc62fe43993ffab6004add88c7f6142bfe9215f7f2c33c99"
|
|
1871
1871
|
},
|
|
1872
|
+
{
|
|
1873
|
+
"path": "docs/adr/0270-rank-floor-derives-without-a-prediction.md",
|
|
1874
|
+
"mode": "0000644",
|
|
1875
|
+
"sha256": "a16ee8bb0d163fec8ffce252e165de4010954e45a7f61bc205163c70077451ee"
|
|
1876
|
+
},
|
|
1872
1877
|
{
|
|
1873
1878
|
"path": "docs/adr/README.md",
|
|
1874
1879
|
"mode": "0000644",
|
|
1875
|
-
"sha256": "
|
|
1880
|
+
"sha256": "26757599a787417d621bd760d61f156f41b12dc73a195d82b912cc6e956d5642"
|
|
1876
1881
|
},
|
|
1877
1882
|
{
|
|
1878
1883
|
"path": "docs/api-reference.md",
|
|
@@ -2757,7 +2762,7 @@
|
|
|
2757
2762
|
{
|
|
2758
2763
|
"path": "docs/module-api-changelog.md",
|
|
2759
2764
|
"mode": "0000644",
|
|
2760
|
-
"sha256": "
|
|
2765
|
+
"sha256": "ccefe35e8bc2cd1de16f6164b9d2782d9dbd6c5c8660ab75e7a0bf198078d2dc"
|
|
2761
2766
|
},
|
|
2762
2767
|
{
|
|
2763
2768
|
"path": "docs/modules-contract.md",
|
|
@@ -3779,6 +3784,11 @@
|
|
|
3779
3784
|
"mode": "0000644",
|
|
3780
3785
|
"sha256": "c10c4bbbead4a3bafde336f3433a1432818880751f7a21436a6162f0dbde1abf"
|
|
3781
3786
|
},
|
|
3787
|
+
{
|
|
3788
|
+
"path": "migrations/core_236_rank_floor_backfill.sql",
|
|
3789
|
+
"mode": "0000644",
|
|
3790
|
+
"sha256": "06b53d66e710dfbf602011459ec8d69adce8e281632927daea0735f8078b2f86"
|
|
3791
|
+
},
|
|
3782
3792
|
{
|
|
3783
3793
|
"path": "modules/agents/lib/validate.js",
|
|
3784
3794
|
"mode": "0000644",
|
|
@@ -5602,7 +5612,7 @@
|
|
|
5602
5612
|
{
|
|
5603
5613
|
"path": "modules/lifecycle/db-rank-authz.js",
|
|
5604
5614
|
"mode": "0000644",
|
|
5605
|
-
"sha256": "
|
|
5615
|
+
"sha256": "ac33825a705a0d541fc7fe94858f967ef8ef9713a2b7a1f1a03ce268494bb317"
|
|
5606
5616
|
},
|
|
5607
5617
|
{
|
|
5608
5618
|
"path": "modules/lifecycle/db-shared.js",
|
|
@@ -5617,7 +5627,7 @@
|
|
|
5617
5627
|
{
|
|
5618
5628
|
"path": "modules/lifecycle/db-tasks.js",
|
|
5619
5629
|
"mode": "0000644",
|
|
5620
|
-
"sha256": "
|
|
5630
|
+
"sha256": "d8cce5b44106009987d3d32b711a114e4505b4e504bbc6fdfc84f161024ee0cd"
|
|
5621
5631
|
},
|
|
5622
5632
|
{
|
|
5623
5633
|
"path": "modules/lifecycle/db-versions.js",
|
|
@@ -7682,12 +7692,12 @@
|
|
|
7682
7692
|
{
|
|
7683
7693
|
"path": "package-lock.json",
|
|
7684
7694
|
"mode": "0000644",
|
|
7685
|
-
"sha256": "
|
|
7695
|
+
"sha256": "72fea99ed1a6a192978e5b257da8b6c819a649e210da1395d7fdace85b35cd4d"
|
|
7686
7696
|
},
|
|
7687
7697
|
{
|
|
7688
7698
|
"path": "package.json",
|
|
7689
7699
|
"mode": "0000644",
|
|
7690
|
-
"sha256": "
|
|
7700
|
+
"sha256": "22510ead9bfe323615390297a28fab238a3a053defb3849e071f7ad66b84aa0d"
|
|
7691
7701
|
},
|
|
7692
7702
|
{
|
|
7693
7703
|
"path": "public-docs/index.html",
|
|
@@ -9427,7 +9437,7 @@
|
|
|
9427
9437
|
{
|
|
9428
9438
|
"path": "src/module-api.js",
|
|
9429
9439
|
"mode": "0000644",
|
|
9430
|
-
"sha256": "
|
|
9440
|
+
"sha256": "a28b94a91c283d898bed5fe0810e029e31b3ab33760b2c132a320058d3692bff"
|
|
9431
9441
|
},
|
|
9432
9442
|
{
|
|
9433
9443
|
"path": "src/module-loader/catalog.js",
|
|
@@ -12049,10 +12059,15 @@
|
|
|
12049
12059
|
"mode": "0000644",
|
|
12050
12060
|
"sha256": "aea6f9ec053bd9477288025b58ed76c166c2d59a24d7ea3481edadba90113887"
|
|
12051
12061
|
},
|
|
12062
|
+
{
|
|
12063
|
+
"path": "tests/rank_floor_wiring.mjs",
|
|
12064
|
+
"mode": "0000644",
|
|
12065
|
+
"sha256": "f81f559b5428497b2246580c7921ecd1ba5a2f1034cf29eb5e5c21b04675f216"
|
|
12066
|
+
},
|
|
12052
12067
|
{
|
|
12053
12068
|
"path": "tests/rank_gate.mjs",
|
|
12054
12069
|
"mode": "0000644",
|
|
12055
|
-
"sha256": "
|
|
12070
|
+
"sha256": "78626dc42e6d31685352e43e1e8e094a904f59d9d0562caf6b6e126934df973f"
|
|
12056
12071
|
},
|
|
12057
12072
|
{
|
|
12058
12073
|
"path": "tests/rank_tier_single_source.mjs",
|
|
@@ -67,7 +67,7 @@ Everything here is your call. It is presented in Phase 4 as a proposal, not as q
|
|
|
67
67
|
- **`category_id`** — the goal's HUMAN category (task 1003277; ADR 0192 — advisory, it gates nothing). Read the live set with `GET /api/gds/work-categories` (never hard-code the labels), infer the best fit from the goal's outcome and its `scope_modules`, and state your pick in the Phase-4 approval round like every other drafted field. **When nothing in the set fits, that is a designed prompt, not an error**: do NOT silently pick the nearest, and do NOT leave it null and move on — surface the gap in the gray-area round and offer to create a new category (`POST /api/gds/work-categories` with `{"label":"..."}`; a duplicate slug converges on the existing row), then use it. Creating uncategorised stays legal — if the user declines to name one, POST the goal without `category_id` and note that the 201 body will carry a `category_advisory` reminding whoever looks next.
|
|
68
68
|
- **`succeeds_goal_id`** — set it when carrying an achieved goal forward. `POST /goals` validates the predecessor exists *before* creating, so a bad ref can't orphan a goal; it then stamps the predecessor's `succeeded_by_goal_id`. Archiving the predecessor is a **separate** call.
|
|
69
69
|
- **Criteria — 2 to 4.** A goal's criteria are narrower than a version's: each should plausibly anchor 2–5 tasks. Draft `criterion_id` as a stable kebab slug (position-independent, and what `criterion_ids` on `POST /tasks` should reference); `criterion_md` is the outcome prose.
|
|
70
|
-
- **Seed tasks — 3 to 8.** Per criterion, with `kind`, a one-line done-when, `criterion_ids: ["<slug>"]`, and dependency edges. Omit `touches[]` (advisory, ADR 0049 — backfilled from the real diff at ship). Never hand-set `requires_rank
|
|
70
|
+
- **Seed tasks — 3 to 8.** Per criterion, with `kind`, a one-line done-when, `criterion_ids: ["<slug>"]`, and dependency edges. Omit `touches[]` (advisory, ADR 0049 — backfilled from the real diff at ship). Never hand-set `requires_rank`: it auto-derives (ADR 0084) from `needs_migration` and the goal's own scope wall, neither of which needs a prediction (ADR 0270). **Do set `needs_migration: true` on any task that will alter the live database** — it is the input that floors such a task at Metic, and omitting it is how a schema change lands in the open queue. Proof tasks use `kind='verify'`; exploration uses `kind='spike'` (and spikes are deliberately left in backlog).
|
|
71
71
|
|
|
72
72
|
## Phase 4 — Ask, then get sign-off
|
|
73
73
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# ADR 0270 — The rank floor derives from facts, not from a prediction
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-09-09
|
|
4
|
+
**Context:** BONGOS-V2, goal [1000111](https://cloudbongos.com/builders#/task/1003663) (Working area 7 — Government), task [1003663](https://cloudbongos.com/builders#/task/1003663). Repairs an interaction between [ADR 0084](<redacted>.md) (the per-task `requires_rank` floor) and [ADR 0049](<redacted>.md) (the split parallel-safety contract, which retired predictive `touches[]`). Generalises the protected-scope floor task 1003066 (BV1.R05) shipped for the ideas vector.
|
|
5
|
+
**Status:** Accepted.
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
ADR 0084 gave every task a `requires_rank` floor, derived at create time by `deriveRequiredRank(touches, securitySensitive)`: a task touching the rank/authz/ship-grade/deploy/migration core floors at `metic`, everything else stays `xenos`. The floor is enforced at claim time, as the `INSUFFICIENT_RANK` rung of the claim refusal ladder.
|
|
10
|
+
|
|
11
|
+
ADR 0049 then retired **predictive** `touches[]`. It is no longer declared up front; it is backfilled from the real committed diff **at ship** — which is *after* the claim the floor exists to gate. So on every create vector that omits it, `matchProtected()` saw an empty array and the floor could not rise. The derivation did not fail loudly; it silently returned `xenos` forever, because it is computed once at creation and never recomputed when `touches` is later backfilled.
|
|
12
|
+
|
|
13
|
+
Measured on this instance before the fix:
|
|
14
|
+
|
|
15
|
+
- **88 tasks declared `needs_migration: true`; 68 of them (77%) sat at `requires_rank: 'xenos'`.**
|
|
16
|
+
- Among them, task 1003372 — *"Hash `builder_sessions.token` at rest"* — was `status: ready`, claimable by any Xenos. A newcomer could claim a rewrite of the auth token store.
|
|
17
|
+
- Goal 1000111 alone (scope wall: `kernel`, `lifecycle`, `government`, `hall-ui`) held 20 claimable tasks at `xenos`, including two that drop columns from the goal-membership authz tables.
|
|
18
|
+
|
|
19
|
+
Two mechanisms kept it invisible:
|
|
20
|
+
|
|
21
|
+
1. **The authoring routes disagree.** `POST /goals/:id/tasks` makes `touches[]` **required** — it is the blast radius its scope wall checks — so the floor does derive there, and the route comment correctly promises "a protected/sensitive task floors to Metic and a sub-Metic author cannot claim it". Its sibling `POST /tasks` treats `touches[]` as optional, and that is the route `/goal-create` actually files seed tasks through.
|
|
22
|
+
2. **The skill told authors to disable both inputs.** `goal-create/SKILL.md` said, in one line: *omit `touches[]`* … *never hand-set `requires_rank`*. Omit the only input and never set the output — together those guarantee `xenos`.
|
|
23
|
+
|
|
24
|
+
The task that raised this asked for "an Archon-level review of how the floor should now be derived". Its premise — *every* task since ADR 0049 — turned out to be overstated (August 2026: 83 tasks, 56 `xenos`; September: 11 and 9), because other vectors still pass `touches[]`. The gap is vector-specific. That distinction is what makes the fix cheap.
|
|
25
|
+
|
|
26
|
+
## Decision
|
|
27
|
+
|
|
28
|
+
**The floor derives from facts a task already carries at create time, never from a prediction.** `deriveRequiredRank` gains a third argument, `opts`, with two touches-free inputs. Any one of four conditions floors a task at `metic`:
|
|
29
|
+
|
|
30
|
+
| Input | Why it is a fact, not a prediction |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `security_sensitive` | Author-declared, unchanged since ADR 0084. |
|
|
33
|
+
| `needs_migration` | The direct successor to the `'migrations/'` entry `POST /tasks` used to fold into `touches[]` before it became a dedicated boolean. A task altering the live database **is** the migration core. |
|
|
34
|
+
| `scopeProtected` | The owning goal's `scope_modules` wall reaches protected territory. Always available — `goal_id` is required on every task since BV1.R60 — and read through the same `scopeIncludesProtected` primitive the pre-push hook, the grader and main-audit use, so it cannot drift from them. |
|
|
35
|
+
| `matchProtected(touches)` | Retained. Still correct on the vectors that *do* declare touches; simply inert on the ones that don't. |
|
|
36
|
+
|
|
37
|
+
`metic` remains the ceiling. `archon` is never auto-assigned — "only the Archon may claim this" stays a deliberate manual override.
|
|
38
|
+
|
|
39
|
+
Three properties travel with the decision:
|
|
40
|
+
|
|
41
|
+
- **Derived at the lowest create layer.** In `db-tasks.createTask`, after `goalIdEffective` resolves — not in each route. This is the argument ADR 0096 already made for creation-time reward assignment: every vector (`POST /tasks`, `POST /goals/:id/tasks`, idea-promotion, capture, newcomer-restock, copy-desk, kickoff-seed, seed scripts) inherits the floor, and a vector added next year cannot forget it. Patching the two authoring routes separately is precisely how they came to disagree.
|
|
42
|
+
- **Raise-only, everywhere.** `highestRank` still clamps upward only, so an explicit `requires_rank` can raise the bar and never drop below the derived floor. `updateTaskRequiresRank` re-derives with the **same four inputs**; without that, an Archon setting a migration task to `xenos` would have succeeded, silently undoing the create-time protection.
|
|
43
|
+
- **Every setter of a floor input ratchets.** A floor derived only at creation is a floor with a back door: `needs_migration` is PATCH-able (task 1002648, to correct the create-time heuristic), so a Metic could have flipped it true on an existing `xenos` task and left the floor untouched — reopening on the PATCH path exactly the gap this ADR closes on the create path. `updateTaskNeedsMigration` is therefore now the exact sibling of `updateTaskSecuritySensitive`: the same `CASE WHEN … requires_rank IN ('xenos','thetes') THEN 'metic'` ratchet, raising only. `tests/rank_floor_wiring.mjs` asserts the ratchet appears in **exactly two** setters, so a third boolean that starts feeding `deriveRequiredRank` cannot quietly ship without one.
|
|
44
|
+
- **Opt-in on a strict `=== true`.** A stray truthy value cannot wall off the open queue.
|
|
45
|
+
|
|
46
|
+
## What was rejected
|
|
47
|
+
|
|
48
|
+
- **Re-requiring predictive `touches[]` at create.** This is the obvious fix and it is wrong: it re-litigates ADR 0049, whose whole finding was that a wrong up-front prediction blocked or de-credited correct work. The floor is not worth reviving the thing that was deliberately deleted.
|
|
49
|
+
- **Recomputing the floor when `touches` is backfilled at ship.** Too late by construction. The claim it gates has already happened; the recompute would only relabel history.
|
|
50
|
+
- **Deriving from `module_key`.** More precise than the goal wall in principle, but it is null on most tasks (including the one that reported this), so it would leave the same hole.
|
|
51
|
+
- **A new gate or approval step.** Nothing here adds a rung to the claim refusal ladder; the existing `INSUFFICIENT_RANK` rung simply starts receiving the correct input.
|
|
52
|
+
|
|
53
|
+
## Consequences
|
|
54
|
+
|
|
55
|
+
- **A protected goal wall is blunt, and that is accepted.** `scopeIncludesProtected` is true when *any* module in the wall is protected, so a purely `hall-ui` task inside goal 1000111 (`hall-ui` + `kernel` + `lifecycle` + `government`) also floors at `metic`. This is the same semantics the ideas vector has enforced since task 1003066, it fails in the safe direction, and `metic` is the first *trusted working* rank rather than a rare one. The cost is a smaller Xenos queue in mixed-scope goals. The remedy, when it bites, is to narrow the goal's wall — which is the honest fix anyway.
|
|
56
|
+
- **The newcomer queue is unaffected today** — there are currently **zero** claimable `newcomer_friendly` tasks, in protected-scope goals or otherwise. Worth re-checking if the beginner queue is ever restocked inside a protected-scope goal; a `newcomer_friendly` carve-out was deliberately **not** added, because an exception that lowers a safety floor is exactly the kind of thing that should require a decision rather than exist by default.
|
|
57
|
+
- **A one-time backfill**, `migrations/<redacted>.sql`, raises still-claimable (`backlog`/`ready`) `xenos` rows that meet either new condition. It is scoped to claimable work because the floor is a claim-time gate — rewriting shipped or abandoned rows would change no decision that has not already been made. It raises only, and there is no down-migration, for the reason ADR 0084 gave: lowering a safety floor is never automatic.
|
|
58
|
+
- **The protected-module list is spelled once in that migration**, as a point-in-time snapshot. A migration cannot call into JS, and a historical backfill should record the roster as it stood the day it ran. The live authority remains `module-scope-map.scopeIncludesProtected` over `PROTECTED_GLOBS`.
|
|
59
|
+
- **`goal-create/SKILL.md` now tells authors to set `needs_migration: true`** on any task that will alter the live database. It is the input that floors such a task, and omitting it is how a schema change reaches the open queue.
|
package/docs/adr/README.md
CHANGED
|
@@ -361,3 +361,4 @@ This keeps the decision history honest and traceable.
|
|
|
361
361
|
| 0267 | [**Unanimity, and the revise-and-re-sit loop** ([task 1003733](https://cloudbongos.com/builders#/task/1003733) · goal 1000111 — *Government*, owner decision 2026-09-08). A Full Idea is ratified by **unanimous** agreement from round one; a sitting that does not carry RETURNS to its author, who revises and re-sits it, unbounded, until the board is unanimous. `unanimous` joins `PASS_RULES` as a fourth answer on the axis these rules actually differ on — **what silence means**: it is two conditions, not one, because "nobody objected" is true of an empty room. Everyone who spoke must have said yes AND somebody must have spoken, so an unvoted sitting RETURNS, which is exactly what makes the clock the owner asked for safe under it ([ADR 0191](<redacted>.md) §4: a deadline may only be given to a rule whose expiry means return). **There is deliberately no membership denominator** — that is the whole difference from `majority`, and it is the owner's "silence does not block" as code: an absent member is not in the reckoning, so one yes out of four carries. The stated cost is that an attentive minority can ratify on a distracted board; the alternative is one person on holiday stopping the pipeline. Trigger changes follow from the rule: a single yes does NOT close it (a later member must still be able to object — the sitting runs its clock, full turnout closes early), while a reasoned objection closes it AT ONCE (no vote-changing in v1 fixes the outcome, and the author needs the feedback to revise). ADR 0191 §3's author rule rides both unchanged. **`BOARD_DEFAULTS` is NOT moved**: a fresh instance stays the day-one monarchy, because under any non-author-yes rule a solo founder can never ratify their own ideas (0191 §5) — adoption is a board amendment, and no env var can change a constitution. The revise-and-re-sit loop already existed (`POST /inbox/:id/resubmit` re-grades and re-fires the window hook); what the board owes it is now pinned by test — a return CLOSES the item so the next window may open, and pays nothing. Named limits: a zero-vote return carries no human objection (the author gets the mechanical weakest-section flag), and this changes nothing live until the separate defect where a ratified amendment does not survive to the next read is fixed.](<redacted>.md) | government / board room |
|
|
362
362
|
| 0268 | [**The constitution comes from two roots, and a decision that did not take must say so** ([task 1003739](https://cloudbongos.com/builders#/task/1003739) · goal 1000111 — *Government*). On 2026-08-25 cloudbongos.com's board RATIFIED an amendment (rank:metic+ · consent · a 1440-minute sitting) and the constitution never changed — for two weeks `GET /government/constitution` answered the day-one monarchy with the passed amendment sitting in its own `history` array directly beneath the contradicting `board` block. Every sitting since was decided under a rule the board had voted to replace, and it silently un-shipped [ADR 0191](<redacted>.md) (majority) and would have un-shipped [ADR 0267](<redacted>.md) the same way. **Cause:** `modules/government/config.js` resolved BOTH its neutral starter and its instance pack from one `path.resolve(__dirname, '..', '..')`, under a comment saying the roots coincided *today* — true in a single checkout, false on a STANDALONE instance ([ADR 0108](<redacted>.md) §1), where the server runs `node_modules/@cloudbongos/core` with `WorkingDirectory=<instance repo>`. The neutral path stayed right (it really is core content); the INSTANCE path resolved inside the core package, so the host's own `config/government.json` was never read and `applyBoardAmendment` wrote the ratified amendment into `node_modules`, where the nightly core upgrade ([ADR 0161](<redacted>.md) cuts a release per merge) erased it within a day. **Nothing threw and nothing could have** — both paths exist, both are writable, and an absent instance pack is a legal state, so the read fell through to the neutral monarchy exactly as designed. `src/branding.js` had the pattern right three files away. **Fix:** neutral from `resolveCoreRoot()`, instance from `resolveInstanceRoot()`, reached through `src/module-api` — which had exposed NEITHER resolver, and that absence is precisely why the module re-derived the wrong one ([ADR 0083](<redacted>.md) forbids requiring a core internal). Resolved at load, not per call, because `resolveInstanceRoot()` falls back to `process.cwd()` and a lazy resolve would let a `chdir` move the constitution; `applyBoardAmendment` now mkdirs, since the write targets a directory the core does not own. Single-checkout behaviour is byte-identical, asserted rather than claimed. **The detector, which is the durable half:** `constitutionView` carries a `divergence` block comparing the newest PASSED amendment (sanitized as an apply would write it) field-by-field against what is in force, rendered above the hall's dials in words rather than config keys. It REPORTS and never HEALS — the instance pack is also the file a human editing the constitution touches, so a silent re-apply would revert a legitimate hand edit with no way to tell the two apart. A RETURNED amendment is never compared (that is what *returned* means). Does NOT put the 2026-08-25 amendment back in force — that is a board act. Sibling sweep RUN, not promised: one real hit filed as [task 1003745](https://cloudbongos.com/builders#/task/1003745) (quarantine.js's repo-containment refusal is blind to the instance repo).](<redacted>.md) | government / instance roots |
|
|
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
|
+
| 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 |
|
|
@@ -1701,5 +1701,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1701
1701
|
landed since 1.19.624 with no explicit bump. run 34328603214. (task 1002620)
|
|
1702
1702
|
1.19.626 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1703
1703
|
landed since 1.19.625 with no explicit bump. run 34363350286. (task 1002620)
|
|
1704
|
+
1.19.627 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1705
|
+
landed since 1.19.626 with no explicit bump. run 34390069410. (task 1002620)
|
|
1704
1706
|
---------------------------------------------------------------------------
|
|
1705
1707
|
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
-- core_236_rank_floor_backfill.sql — raise the requires_rank floor on work that
|
|
2
|
+
-- was created while the derivation was inert (task 1003663, goal 1000111,
|
|
3
|
+
-- ADR 0270).
|
|
4
|
+
--
|
|
5
|
+
-- WHY THERE IS ANYTHING TO BACK FILL. deriveRequiredRank has always derived the
|
|
6
|
+
-- per-task floor from touches[] + security_sensitive (task 1498, ADR 0085). ADR
|
|
7
|
+
-- 0049 then retired PREDICTIVE touches[] — it is backfilled from the real
|
|
8
|
+
-- committed diff at SHIP, which is after the claim this floor exists to gate —
|
|
9
|
+
-- so on every create vector that omits it the matcher saw an empty array and the
|
|
10
|
+
-- floor never rose. Measured on this instance before the fix: 88 tasks declared
|
|
11
|
+
-- needs_migration and 68 of them (77%) sat at requires_rank='xenos', including
|
|
12
|
+
-- task 1003372, a READY task to rewrite the auth token store at rest.
|
|
13
|
+
--
|
|
14
|
+
-- ADR 0270 gives the derivation two touches-free inputs at create time
|
|
15
|
+
-- (needs_migration, and whether the owning goal's scope wall reaches protected
|
|
16
|
+
-- territory). That fixes every task created from now on. This migration is the
|
|
17
|
+
-- one-time pass over the rows that already exist.
|
|
18
|
+
--
|
|
19
|
+
-- SCOPED TO STILL-CLAIMABLE WORK, on purpose. Only 'backlog' and 'ready' rows are
|
|
20
|
+
-- touched. The floor is enforced at CLAIM time (the INSUFFICIENT_RANK rung of the
|
|
21
|
+
-- claim refusal ladder), so raising it on an already-claimed, shipped or
|
|
22
|
+
-- abandoned row changes no decision that has not already been made — it would
|
|
23
|
+
-- only rewrite history and dirty updated_at across the ledger.
|
|
24
|
+
--
|
|
25
|
+
-- RAISES ONLY, like every other path that writes this column. The WHERE clause
|
|
26
|
+
-- selects requires_rank='xenos', so a row an Archon has already lifted to 'metic'
|
|
27
|
+
-- or 'archon' is left exactly as it is. There is no down-migration for the same
|
|
28
|
+
-- reason ADR 0085 gave: lowering a safety floor is never automatic.
|
|
29
|
+
--
|
|
30
|
+
-- THE MODULE LIST BELOW IS A POINT-IN-TIME SNAPSHOT, not a second source of
|
|
31
|
+
-- truth. The live authority is src/bongos/module-scope-map.scopeIncludesProtected
|
|
32
|
+
-- over PROTECTED_GLOBS (permission-path-check.js) — the same roster the pre-push
|
|
33
|
+
-- hook, the grader and main-audit read, and what the running code calls. A
|
|
34
|
+
-- migration cannot call into JS, and a historical backfill SHOULD record the
|
|
35
|
+
-- roster as it stood the day it ran rather than drift with a later one, so the
|
|
36
|
+
-- nine protected keys are spelled here once and never read again.
|
|
37
|
+
|
|
38
|
+
UPDATE tasks t
|
|
39
|
+
SET requires_rank = 'metic',
|
|
40
|
+
updated_at = now()
|
|
41
|
+
WHERE t.requires_rank = 'xenos'
|
|
42
|
+
AND t.status IN ('backlog', 'ready')
|
|
43
|
+
AND (
|
|
44
|
+
-- The migration core, by the task's own declaration.
|
|
45
|
+
t.needs_migration = true
|
|
46
|
+
-- Or the owning goal's scope wall reaches protected territory.
|
|
47
|
+
OR EXISTS (
|
|
48
|
+
SELECT 1
|
|
49
|
+
FROM goals g
|
|
50
|
+
WHERE g.id = t.goal_id
|
|
51
|
+
AND g.scope_modules && ARRAY[
|
|
52
|
+
'kernel', 'lifecycle', 'grading', 'onboarding', 'builder-settings',
|
|
53
|
+
'autonomy', 'government', 'dev-box', 'devsecops'
|
|
54
|
+
]::text[]
|
|
55
|
+
)
|
|
56
|
+
);
|
|
@@ -146,7 +146,7 @@ function rankAllowsTask(builderRank, requiredRank) {
|
|
|
146
146
|
return have >= needed;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
// deriveRequiredRank — task 1498 / ADR
|
|
149
|
+
// deriveRequiredRank — task 1498 / ADR 0084. The per-task requires_rank floor
|
|
150
150
|
// (V3.R82 #301) shipped INERT: no create/edit path ever set it, so every task
|
|
151
151
|
// sat at the 'xenos' default and the claim gate never bit. We now DERIVE a floor
|
|
152
152
|
// from what a task touches + its sensitivity, reusing the SAME protected-path
|
|
@@ -157,10 +157,35 @@ function rankAllowsTask(builderRank, requiredRank) {
|
|
|
157
157
|
// sub-Metic builder already cannot push to). Everything else stays 'xenos' (the
|
|
158
158
|
// open general queue). NEVER auto-assigns 'archon': "only the Archon may claim
|
|
159
159
|
// this" is a deliberate manual override, not something to infer.
|
|
160
|
+
//
|
|
161
|
+
// task 1003663 / ADR 0270 — WHY THERE ARE NOW FOUR INPUTS, NOT TWO. The floor
|
|
162
|
+
// above derived from touches[] alone, and ADR 0049 then retired PREDICTIVE
|
|
163
|
+
// touches[]: it is backfilled from the real committed diff at SHIP, which is
|
|
164
|
+
// after the claim this floor exists to gate. So the derivation quietly went
|
|
165
|
+
// inert on the vectors that omit it — 68 of the 88 tasks declaring
|
|
166
|
+
// needs_migration sat at 'xenos', including a live-DB migration rewriting the
|
|
167
|
+
// auth token store. The fix is NOT to bring prediction back; it is to derive
|
|
168
|
+
// from facts a task already carries at create time. `opts` takes those:
|
|
169
|
+
// { needsMigration, scopeProtected }.
|
|
160
170
|
// Pure + exported so tests/rank_gate.mjs can exercise it without a DB.
|
|
161
171
|
|
|
162
|
-
function deriveRequiredRank(touches, securitySensitive) {
|
|
172
|
+
function deriveRequiredRank(touches, securitySensitive, opts = {}) {
|
|
163
173
|
if (securitySensitive === true) return 'metic';
|
|
174
|
+
// task 1003663 / ADR 0270 — the two touches-FREE inputs. ADR 0049 retired
|
|
175
|
+
// PREDICTIVE touches[]: it is backfilled from the real diff at ship, so at
|
|
176
|
+
// CREATE time the matchProtected() arm below sees an empty array on most
|
|
177
|
+
// vectors and the floor never rises. These arms restore it from facts a task
|
|
178
|
+
// already carries at create time.
|
|
179
|
+
//
|
|
180
|
+
// needsMigration — the direct successor to the 'migrations/' entry POST /tasks
|
|
181
|
+
// used to fold into touches[] before it became a dedicated boolean. A task
|
|
182
|
+
// that alters the LIVE database is the migration core by definition.
|
|
183
|
+
if (opts.needsMigration === true) return 'metic';
|
|
184
|
+
// scopeProtected — the owning goal's scope wall reaches protected territory.
|
|
185
|
+
// Unlike touches[] this is ALWAYS available (goal_id is required on every task
|
|
186
|
+
// since BV1.R60), and it is computed from the SAME protected roster the
|
|
187
|
+
// pre-push hook, the grader and main-audit read, so it cannot drift from them.
|
|
188
|
+
if (opts.scopeProtected === true) return 'metic';
|
|
164
189
|
const files = Array.isArray(touches) ? touches : [];
|
|
165
190
|
if (files.length > 0 && matchProtected(files).length > 0) return 'metic';
|
|
166
191
|
return 'xenos';
|
|
@@ -23,6 +23,31 @@ const { deriveRequiredRank, highestRank } = require('./db-rank-authz.js');
|
|
|
23
23
|
// -------------------------------------------------------------------------
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
// goalScopeProtected — does this task's owning goal reach protected territory
|
|
27
|
+
// (the permission / ship-grade / deploy / migration core)? The touches-free half
|
|
28
|
+
// of the create-time rank floor (task 1003663, ADR 0270).
|
|
29
|
+
//
|
|
30
|
+
// It reads the goal's `scope_modules` wall through the SAME published primitive
|
|
31
|
+
// modules/ideas/inbox.js already uses for routed filings (task 1003066, BV1.R05)
|
|
32
|
+
// and that the pre-push hook, the grader and main-audit read — so this floor
|
|
33
|
+
// cannot drift from the roster they enforce.
|
|
34
|
+
//
|
|
35
|
+
// Runs on the CALLER'S client when there is one, so it sees the same
|
|
36
|
+
// transaction snapshot as the INSERT it feeds — a goal rescoped mid-transaction
|
|
37
|
+
// cannot land between the check and the row. FAILS CLOSED-ish by omission: an
|
|
38
|
+
// absent goal or a non-array wall yields false, which is the honest answer
|
|
39
|
+
// ("no protected scope declared"), and the other three arms of deriveRequiredRank
|
|
40
|
+
// still apply.
|
|
41
|
+
async function goalScopeProtected(goalId, client) {
|
|
42
|
+
if (goalId == null) return false;
|
|
43
|
+
const { rows } = await (client || pool).query(
|
|
44
|
+
'SELECT scope_modules FROM goals WHERE id = $1',
|
|
45
|
+
[goalId]
|
|
46
|
+
);
|
|
47
|
+
const scope = rows.length ? rows[0].scope_modules : null;
|
|
48
|
+
return Array.isArray(scope) && api.moduleScopeMap.scopeIncludesProtected(scope);
|
|
49
|
+
}
|
|
50
|
+
|
|
26
51
|
// The version statuses that can still receive new work. 'shipped' and 'frozen' are
|
|
27
52
|
// closed: a version whose scope is sealed cannot be told about a bug found afterwards.
|
|
28
53
|
// Mirrors the CHECK constraint on versions.status (migrations/003_pms.sql).
|
|
@@ -124,15 +149,6 @@ async function createTask({
|
|
|
124
149
|
// to name a goal instead.
|
|
125
150
|
allowCatchAll = false,
|
|
126
151
|
}, deps = {}) { // deps.client enlists this INSERT in the CALLER'S transaction (task 1003065, BV1.R01); the argument for it is at modules/ideas/inbox.js routeIdeaToGoal
|
|
127
|
-
// task 1498 / ADR 0085: every task carries a deliberate rank floor. Derive it
|
|
128
|
-
// from touches + sensitivity, then let an explicit requiresRank override only
|
|
129
|
-
// UPWARD (highestRank) — a requested rank below the derived floor is clamped
|
|
130
|
-
// up to the floor (fail-closed; the floor mirrors the trust-boundary paths a
|
|
131
|
-
// sub-Metic builder cannot push to anyway).
|
|
132
|
-
const requiresRankEffective = highestRank(
|
|
133
|
-
deriveRequiredRank(touches, securitySensitive),
|
|
134
|
-
requiresRank ?? 'xenos'
|
|
135
|
-
);
|
|
136
152
|
// task 1677 / ADR 0096 (2026-06-28): creation is now the PRIMARY reward-assignment
|
|
137
153
|
// point — every task is born with a fair drachmae value, not just at promote/claim.
|
|
138
154
|
// When the caller supplies NO positive credits_reward (null/undefined/0), auto-assign
|
|
@@ -181,6 +197,26 @@ async function createTask({
|
|
|
181
197
|
}
|
|
182
198
|
goalIdEffective = await generalGoalIdForVersion(versionId);
|
|
183
199
|
}
|
|
200
|
+
// task 1498 / ADR 0084: every task carries a deliberate rank floor. Derive it,
|
|
201
|
+
// then let an explicit requiresRank override only UPWARD (highestRank) — a
|
|
202
|
+
// requested rank below the derived floor is clamped up to the floor
|
|
203
|
+
// (fail-closed; the floor mirrors the trust-boundary paths a sub-Metic builder
|
|
204
|
+
// cannot push to anyway).
|
|
205
|
+
//
|
|
206
|
+
// task 1003663 / ADR 0270: derived HERE, at the lowest create layer and AFTER
|
|
207
|
+
// goalIdEffective resolves, rather than in each route — the same argument the
|
|
208
|
+
// reward auto-assignment above makes. Every create vector (POST /tasks,
|
|
209
|
+
// POST /goals/:id/tasks, idea-promotion, capture, newcomer-restock, copy-desk,
|
|
210
|
+
// kickoff-seed, seed scripts) inherits the floor, and a vector added next year
|
|
211
|
+
// cannot forget it. Patching the two authoring routes instead is how
|
|
212
|
+
// POST /tasks and POST /goals/:id/tasks came to disagree in the first place.
|
|
213
|
+
const requiresRankEffective = highestRank(
|
|
214
|
+
deriveRequiredRank(touches, securitySensitive, {
|
|
215
|
+
needsMigration,
|
|
216
|
+
scopeProtected: await goalScopeProtected(goalIdEffective, deps.client),
|
|
217
|
+
}),
|
|
218
|
+
requiresRank ?? 'xenos'
|
|
219
|
+
);
|
|
184
220
|
const { rows } = await (deps.client || pool).query(
|
|
185
221
|
`INSERT INTO tasks (version_id, title, description, status, touches,
|
|
186
222
|
est_minutes, est_cost_usd, manual_degree, priority,
|
|
@@ -366,7 +402,7 @@ async function updateTaskNewcomerFriendly(id, newcomerFriendly) { return updateT
|
|
|
366
402
|
// (the === true coercion below preserves the boolean-only contract).
|
|
367
403
|
|
|
368
404
|
async function updateTaskSecuritySensitive(id, securitySensitive) {
|
|
369
|
-
// task 1498 / ADR
|
|
405
|
+
// task 1498 / ADR 0084: flagging a task security_sensitive RAISES its rank
|
|
370
406
|
// floor (a now-sensitive task must require Metic+). Recompute and ratchet
|
|
371
407
|
// requires_rank UP in the same atomic UPDATE so the two columns can't diverge;
|
|
372
408
|
// never lower it (an existing 'metic'/'archon' may be a protected-touch floor
|
|
@@ -386,7 +422,7 @@ async function updateTaskSecuritySensitive(id, securitySensitive) {
|
|
|
386
422
|
return rows[0] ?? null;
|
|
387
423
|
}
|
|
388
424
|
|
|
389
|
-
// task 1498 / ADR
|
|
425
|
+
// task 1498 / ADR 0084: set a task's rank floor explicitly (the Archon override).
|
|
390
426
|
// The DERIVED floor (touches + security_sensitive) is a HARD minimum — a request
|
|
391
427
|
// below it is clamped UP via highestRank, so an override can only RAISE the bar,
|
|
392
428
|
// never open a sensitive task to a lower rank. `requiresRank` is one live rank
|
|
@@ -396,7 +432,15 @@ async function updateTaskSecuritySensitive(id, securitySensitive) {
|
|
|
396
432
|
async function updateTaskRequiresRank(id, requiresRank) {
|
|
397
433
|
const task = await getTask(id);
|
|
398
434
|
if (!task) return null;
|
|
399
|
-
|
|
435
|
+
// task 1003663 / ADR 0270: the override re-derives with the SAME four inputs as
|
|
436
|
+
// creation. Without needsMigration/scopeProtected here, an Archon setting a
|
|
437
|
+
// migration task to 'xenos' would succeed — the floor would compute 'xenos'
|
|
438
|
+
// from an empty touches[] and clamp nothing, silently undoing the create-time
|
|
439
|
+
// protection. An override may still only RAISE.
|
|
440
|
+
const floor = deriveRequiredRank(task.touches, task.security_sensitive, {
|
|
441
|
+
needsMigration: task.needs_migration === true,
|
|
442
|
+
scopeProtected: await goalScopeProtected(task.goal_id, null),
|
|
443
|
+
});
|
|
400
444
|
const effective = highestRank(floor, requiresRank ?? 'xenos');
|
|
401
445
|
const { rows } = await pool.query(
|
|
402
446
|
`UPDATE tasks SET requires_rank = $1, updated_at = now() WHERE id = $2 RETURNING *`,
|
|
@@ -508,7 +552,32 @@ async function updateTaskAutomationTag(id, automationTag) { return updateTaskCol
|
|
|
508
552
|
// task 1002648: correct the create-time migration heuristic's verdict — a false
|
|
509
553
|
// flag makes claiming reserve a migration number that goes unused (task 1002614
|
|
510
554
|
// burned 213 that way). Boolean only; the route validates.
|
|
511
|
-
|
|
555
|
+
//
|
|
556
|
+
// task 1003663 / ADR 0270: needs_migration is now a rank-floor INPUT, so this
|
|
557
|
+
// stopped being a bare column write and became the exact sibling of
|
|
558
|
+
// updateTaskSecuritySensitive above — same CASE, same ratchet, same reasoning.
|
|
559
|
+
// The correction this setter exists for runs BOTH ways: task 1002648 names the
|
|
560
|
+
// true→false direction, but false→true is the one that matters here, and without
|
|
561
|
+
// the ratchet a Metic could PATCH needs_migration onto an existing xenos task and
|
|
562
|
+
// leave the floor at xenos — reopening on the PATCH path exactly the gap ADR 0270
|
|
563
|
+
// closes on the create path. RAISES ONLY: flipping the flag back off never drops
|
|
564
|
+
// the gate, because an existing 'metic'/'archon' may be a protected-scope floor or
|
|
565
|
+
// a deliberate override. An Archon lowers it explicitly via updateTaskRequiresRank,
|
|
566
|
+
// which re-derives from all four inputs.
|
|
567
|
+
async function updateTaskNeedsMigration(id, needsMigration) {
|
|
568
|
+
const mig = needsMigration === true;
|
|
569
|
+
const { rows } = await pool.query(
|
|
570
|
+
`UPDATE tasks
|
|
571
|
+
SET needs_migration = $1,
|
|
572
|
+
requires_rank = CASE WHEN $1 AND requires_rank IN ('xenos', 'thetes')
|
|
573
|
+
THEN 'metic' ELSE requires_rank END,
|
|
574
|
+
updated_at = now()
|
|
575
|
+
WHERE id = $2
|
|
576
|
+
RETURNING *`,
|
|
577
|
+
[mig, id]
|
|
578
|
+
);
|
|
579
|
+
return rows[0] ?? null;
|
|
580
|
+
}
|
|
512
581
|
|
|
513
582
|
// BV1.R53 (task 1422): assign (or clear) the module_key that declares which
|
|
514
583
|
// module this task's work belongs to. Drives module-disjointness at claim time
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.627",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.627",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.627",
|
|
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.
|
|
58
|
+
const CORE_VERSION = '1.19.627'; // 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');
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// tests/rank_floor_wiring.mjs — the rank floor is WIRED, not merely derivable
|
|
2
|
+
// (task 1003663, goal 1000111, ADR 0270).
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS FILE EXISTS SEPARATELY FROM tests/rank_gate.mjs. That file pins the
|
|
5
|
+
// PURE helpers — `deriveRequiredRank` / `highestRank` — by handing them opts it
|
|
6
|
+
// builds itself. That is necessary and it is not sufficient: the whole defect
|
|
7
|
+
// ADR 0270 repairs was a derivation that was perfectly correct and never
|
|
8
|
+
// REACHED, because the input it needed (`touches[]`) stopped arriving at the
|
|
9
|
+
// call site. A test that constructs the inputs by hand cannot catch that class
|
|
10
|
+
// of bug a second time — it asserts the calculator, not the plumbing.
|
|
11
|
+
//
|
|
12
|
+
// So this file drives the real `createTask` against a SQL-aware client and reads
|
|
13
|
+
// the `requires_rank` actually bound into the INSERT, plus the two PATCH setters
|
|
14
|
+
// that can move a floor input after creation.
|
|
15
|
+
//
|
|
16
|
+
// Run: node --test tests/rank_floor_wiring.mjs
|
|
17
|
+
|
|
18
|
+
import assert from 'node:assert/strict';
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import { makeSqlAwareClient, lifecycleDbSource } from './helpers.mjs';
|
|
22
|
+
|
|
23
|
+
process.env.NODE_ENV = 'test';
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const { createTask } = require('../modules/lifecycle/db.js');
|
|
26
|
+
|
|
27
|
+
// The INSERT binds ~25 columns positionally, so a hardcoded params[20] would be a
|
|
28
|
+
// silent lie the first time somebody inserts a column. Read the column list out of
|
|
29
|
+
// the statement under test and resolve the index by NAME.
|
|
30
|
+
function boundValue(sql, params, column) {
|
|
31
|
+
const cols = String(sql).slice(sql.indexOf('(') + 1, sql.indexOf(')'))
|
|
32
|
+
.split(',').map((c) => c.trim());
|
|
33
|
+
const i = cols.indexOf(column);
|
|
34
|
+
assert.notEqual(i, -1, `INSERT INTO tasks no longer binds a '${column}' column — update this test`);
|
|
35
|
+
return params[i];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// One create against a fake goal row; returns the requires_rank the INSERT bound.
|
|
39
|
+
async function createdFloor({ scopeModules, needsMigration = false, requiresRank = null }) {
|
|
40
|
+
let insert = null;
|
|
41
|
+
const { _client } = makeSqlAwareClient(async (sql, params) => {
|
|
42
|
+
if (/^SELECT scope_modules FROM goals/i.test(sql)) {
|
|
43
|
+
return { rows: [{ scope_modules: scopeModules }] };
|
|
44
|
+
}
|
|
45
|
+
if (/^INSERT INTO tasks/i.test(sql)) {
|
|
46
|
+
insert = { sql, params };
|
|
47
|
+
return { rows: [{ id: '9001' }] };
|
|
48
|
+
}
|
|
49
|
+
return { rows: [] };
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await createTask({
|
|
53
|
+
versionId: 'BONGOS-V2',
|
|
54
|
+
title: 'a task',
|
|
55
|
+
goalId: '1000111',
|
|
56
|
+
needsMigration,
|
|
57
|
+
requiresRank,
|
|
58
|
+
}, { client: _client });
|
|
59
|
+
|
|
60
|
+
assert.ok(insert, 'createTask never reached its INSERT');
|
|
61
|
+
return boundValue(insert.sql, insert.params, 'requires_rank');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// createTask — the create path actually reads the goal wall
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
test('a goal whose wall reaches protected territory floors the task at metic', async () => {
|
|
69
|
+
// No touches[], not security_sensitive, not a migration — every pre-0270 input
|
|
70
|
+
// is absent, which is precisely the shape that used to come out 'xenos'.
|
|
71
|
+
assert.equal(await createdFloor({ scopeModules: ['hall-ui', 'kernel'] }), 'metic');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('an unprotected wall leaves the task in the open queue', async () => {
|
|
75
|
+
assert.equal(await createdFloor({ scopeModules: ['hall-ui'] }), 'xenos');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('needs_migration floors at metic even under an unprotected wall', async () => {
|
|
79
|
+
assert.equal(await createdFloor({ scopeModules: ['hall-ui'], needsMigration: true }), 'metic');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('THE REGRESSION: the create path reads the wall, it does not just own a helper', async () => {
|
|
83
|
+
// Mutation guard. If someone drops the goalScopeProtected call from createTask
|
|
84
|
+
// — the exact way this bug happened the first time, by a caller ceasing to
|
|
85
|
+
// supply an input — the pure tests in rank_gate.mjs all still pass and this one
|
|
86
|
+
// fails. That asymmetry is the point of the file.
|
|
87
|
+
assert.equal(await createdFloor({ scopeModules: ['lifecycle'] }), 'metic');
|
|
88
|
+
assert.equal(await createdFloor({ scopeModules: ['government'] }), 'metic');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('an explicit requires_rank still only RAISES, never lowers the derived floor', async () => {
|
|
92
|
+
assert.equal(await createdFloor({ scopeModules: ['kernel'], requiresRank: 'xenos' }), 'metic');
|
|
93
|
+
assert.equal(await createdFloor({ scopeModules: ['hall-ui'], requiresRank: 'archon' }), 'archon');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('a missing goal row is the honest false, not a throw', async () => {
|
|
97
|
+
// scope_modules is NOT NULL DEFAULT '{}', but a goal deleted mid-transaction
|
|
98
|
+
// returns no row at all. That must fall through to the other arms, not crash a
|
|
99
|
+
// create.
|
|
100
|
+
assert.equal(await createdFloor({ scopeModules: undefined }), 'xenos');
|
|
101
|
+
assert.equal(await createdFloor({ scopeModules: undefined, needsMigration: true }), 'metic');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// The PATCH setters — a floor INPUT may be flipped after creation
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// These two write through the module-level pool rather than an injectable client,
|
|
108
|
+
// so they are pinned at the source. The assertion is narrow on purpose: it is the
|
|
109
|
+
// RATCHET that matters (raise on true, never lower), not the surrounding SQL.
|
|
110
|
+
|
|
111
|
+
test('updateTaskNeedsMigration ratchets requires_rank UP, exactly like its security_sensitive sibling', () => {
|
|
112
|
+
const src = lifecycleDbSource();
|
|
113
|
+
const setter = src.slice(src.indexOf('async function updateTaskNeedsMigration'));
|
|
114
|
+
const body = setter.slice(0, setter.indexOf('\n}\n') + 1);
|
|
115
|
+
|
|
116
|
+
assert.match(
|
|
117
|
+
body.replace(/\s+/g, ' '),
|
|
118
|
+
/requires_rank = CASE WHEN \$1 AND requires_rank IN \('xenos', 'thetes'\) THEN 'metic' ELSE requires_rank END/,
|
|
119
|
+
'PATCHing needs_migration=true must raise the floor — otherwise ADR 0270 is closed on the create path and open on the PATCH path'
|
|
120
|
+
);
|
|
121
|
+
// Raise-only: nothing in the setter may write a rank BACK DOWN when the flag
|
|
122
|
+
// clears. An existing metic may be a protected-scope floor or an Archon override.
|
|
123
|
+
assert.doesNotMatch(body, /ELSE 'xenos'/, 'the setter must never lower an existing floor');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('both floor-input setters share one ratchet — neither may drift from the other', () => {
|
|
127
|
+
const src = lifecycleDbSource();
|
|
128
|
+
const ratchet = /requires_rank = CASE WHEN \$1 AND requires_rank IN \('xenos', 'thetes'\)\s+THEN 'metic' ELSE requires_rank END/g;
|
|
129
|
+
const hits = src.match(ratchet) || [];
|
|
130
|
+
assert.equal(
|
|
131
|
+
hits.length, 2,
|
|
132
|
+
`expected the ratchet in exactly 2 setters (security_sensitive + needs_migration), found ${hits.length} — ` +
|
|
133
|
+
'a new boolean that feeds deriveRequiredRank needs one too, and a removed one is a reopened gap'
|
|
134
|
+
);
|
|
135
|
+
});
|
package/tests/rank_gate.mjs
CHANGED
|
@@ -399,6 +399,51 @@ t('empty / null / undefined touches with no sensitivity => xenos (no throw)', ()
|
|
|
399
399
|
assert.equal(db.deriveRequiredRank(undefined, undefined), 'xenos');
|
|
400
400
|
});
|
|
401
401
|
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
// task 1003663 / ADR 0270 — the two touches-FREE inputs. ADR 0049 retired
|
|
404
|
+
// predictive touches[], so at CREATE time the arms above see an empty array on
|
|
405
|
+
// most vectors and the floor never rose: 68 of 88 needs_migration tasks sat at
|
|
406
|
+
// xenos, one of them a ready task to rewrite the auth token store. These pin the
|
|
407
|
+
// replacement, which derives from facts a task carries WITHOUT a prediction.
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
console.log('\nderiveRequiredRank (the touches-free floors — task 1003663):');
|
|
410
|
+
|
|
411
|
+
t('needs_migration floors at metic with NO touches at all', () => {
|
|
412
|
+
assert.equal(db.deriveRequiredRank([], false, { needsMigration: true }), 'metic');
|
|
413
|
+
assert.equal(db.deriveRequiredRank(null, false, { needsMigration: true }), 'metic');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
t('a protected goal scope floors at metic with NO touches at all', () => {
|
|
417
|
+
assert.equal(db.deriveRequiredRank([], false, { scopeProtected: true }), 'metic');
|
|
418
|
+
assert.equal(db.deriveRequiredRank(null, false, { scopeProtected: true }), 'metic');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
t('THE REGRESSION: the exact shape of task 1003372 (ready, migration, no touches)', () => {
|
|
422
|
+
// Before ADR 0270 this returned 'xenos' — a Xenos could claim a task to hash
|
|
423
|
+
// the builder_sessions token store at rest. It must never do so again.
|
|
424
|
+
assert.equal(
|
|
425
|
+
db.deriveRequiredRank([], false, { needsMigration: true, scopeProtected: false }),
|
|
426
|
+
'metic'
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
t('the flags are opt-in: false / absent / non-boolean never RAISE the floor', () => {
|
|
431
|
+
// Only a strict === true raises, so a stray truthy value cannot quietly wall
|
|
432
|
+
// off the open queue (the fail-closed direction here is "stay open").
|
|
433
|
+
assert.equal(db.deriveRequiredRank([], false, {}), 'xenos');
|
|
434
|
+
assert.equal(db.deriveRequiredRank([], false, { needsMigration: false, scopeProtected: false }), 'xenos');
|
|
435
|
+
assert.equal(db.deriveRequiredRank([], false, { needsMigration: 'yes' }), 'xenos');
|
|
436
|
+
assert.equal(db.deriveRequiredRank([], false, { needsMigration: 1 }), 'xenos');
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
t('omitting opts entirely is the pre-0270 two-argument behaviour, unchanged', () => {
|
|
440
|
+
// Every existing call site passes two arguments; none may change meaning.
|
|
441
|
+
assert.equal(db.deriveRequiredRank([], false), 'xenos');
|
|
442
|
+
assert.equal(db.deriveRequiredRank(['public/game/main.js'], false), 'xenos');
|
|
443
|
+
assert.equal(db.deriveRequiredRank(['migrations/145_x.sql'], false), 'metic');
|
|
444
|
+
assert.equal(db.deriveRequiredRank([], true), 'metic');
|
|
445
|
+
});
|
|
446
|
+
|
|
402
447
|
console.log('\nhighestRank (compose floor with an override; raise-only):');
|
|
403
448
|
|
|
404
449
|
t('picks the higher-authority rank either way', () => {
|