@bongos/core 1.19.662 → 1.19.663
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 +14 -14
- package/docs/module-api-changelog.md +2 -0
- package/modules/lifecycle/db-claims.js +52 -25
- package/modules/lifecycle/db-rank-authz.js +48 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/government_claim_eligibility.mjs +132 -10
- package/tests/government_ownership_scope.mjs +18 -8
- package/tests/government_protected_surfaces.mjs +82 -4
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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.663",
|
|
6
|
+
"core_contract": "1.19.663",
|
|
7
|
+
"source_commit": "6acc4ae42df06ed3334386b6ed58fe312ae7e8a0",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-11T03:17:38.613Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 473,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 2610,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "42ab056c27124e80b07512a8787d678c3d74cef4f3b6dbeac2a248a39fa1dde8",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2787,7 +2787,7 @@
|
|
|
2787
2787
|
{
|
|
2788
2788
|
"path": "docs/module-api-changelog.md",
|
|
2789
2789
|
"mode": "0000644",
|
|
2790
|
-
"sha256": "
|
|
2790
|
+
"sha256": "d5e752a541b45079b1b23eb12eeb282e6f163a676c0d9b9f122fcef2864d120f"
|
|
2791
2791
|
},
|
|
2792
2792
|
{
|
|
2793
2793
|
"path": "docs/modules-contract.md",
|
|
@@ -5647,7 +5647,7 @@
|
|
|
5647
5647
|
{
|
|
5648
5648
|
"path": "modules/lifecycle/db-claims.js",
|
|
5649
5649
|
"mode": "0000644",
|
|
5650
|
-
"sha256": "
|
|
5650
|
+
"sha256": "1b0d394ee1788decd9ea713a4379ef5322eda28a2ac11ad7adaf68298e6f4075"
|
|
5651
5651
|
},
|
|
5652
5652
|
{
|
|
5653
5653
|
"path": "modules/lifecycle/db-deps-criteria.js",
|
|
@@ -5672,7 +5672,7 @@
|
|
|
5672
5672
|
{
|
|
5673
5673
|
"path": "modules/lifecycle/db-rank-authz.js",
|
|
5674
5674
|
"mode": "0000644",
|
|
5675
|
-
"sha256": "
|
|
5675
|
+
"sha256": "fb8180f875828664fcd5cb17aa39f68c70b6cea3c52d03fdbd0b3e835f8b1e64"
|
|
5676
5676
|
},
|
|
5677
5677
|
{
|
|
5678
5678
|
"path": "modules/lifecycle/db-shared.js",
|
|
@@ -7752,12 +7752,12 @@
|
|
|
7752
7752
|
{
|
|
7753
7753
|
"path": "package-lock.json",
|
|
7754
7754
|
"mode": "0000644",
|
|
7755
|
-
"sha256": "
|
|
7755
|
+
"sha256": "014c542d80e35f6e6a5572e169232eedf744e09bae83f6798c2d998ca86a18d5"
|
|
7756
7756
|
},
|
|
7757
7757
|
{
|
|
7758
7758
|
"path": "package.json",
|
|
7759
7759
|
"mode": "0000644",
|
|
7760
|
-
"sha256": "
|
|
7760
|
+
"sha256": "56f59c24967d3719f7341742409c6f57ef6551dbfd9600591d201888701dc926"
|
|
7761
7761
|
},
|
|
7762
7762
|
{
|
|
7763
7763
|
"path": "public-docs/index.html",
|
|
@@ -9512,7 +9512,7 @@
|
|
|
9512
9512
|
{
|
|
9513
9513
|
"path": "src/module-api.js",
|
|
9514
9514
|
"mode": "0000644",
|
|
9515
|
-
"sha256": "
|
|
9515
|
+
"sha256": "3d019bf197d89274ef460bd8b86038aca8418d5071d2d76c43479ba1743c556e"
|
|
9516
9516
|
},
|
|
9517
9517
|
{
|
|
9518
9518
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10857,7 +10857,7 @@
|
|
|
10857
10857
|
{
|
|
10858
10858
|
"path": "tests/government_claim_eligibility.mjs",
|
|
10859
10859
|
"mode": "0000644",
|
|
10860
|
-
"sha256": "
|
|
10860
|
+
"sha256": "05a7c8f8d001bd0d2950619fb46f242d358ecae1a8901a10fbb9ecc7857500e7"
|
|
10861
10861
|
},
|
|
10862
10862
|
{
|
|
10863
10863
|
"path": "tests/government_config.mjs",
|
|
@@ -10902,7 +10902,7 @@
|
|
|
10902
10902
|
{
|
|
10903
10903
|
"path": "tests/government_ownership_scope.mjs",
|
|
10904
10904
|
"mode": "0000644",
|
|
10905
|
-
"sha256": "
|
|
10905
|
+
"sha256": "76b81db7769290d5ef5c26ece86ce0610c5aef6c8984ffdd54a6340db66db3bc"
|
|
10906
10906
|
},
|
|
10907
10907
|
{
|
|
10908
10908
|
"path": "tests/government_page_gates.mjs",
|
|
@@ -10922,7 +10922,7 @@
|
|
|
10922
10922
|
{
|
|
10923
10923
|
"path": "tests/government_protected_surfaces.mjs",
|
|
10924
10924
|
"mode": "0000644",
|
|
10925
|
-
"sha256": "
|
|
10925
|
+
"sha256": "3517e05c1264fbdbeadc533884942926689952c8995548863ca1cfbfc365428e"
|
|
10926
10926
|
},
|
|
10927
10927
|
{
|
|
10928
10928
|
"path": "tests/government_rank_sync.mjs",
|
|
@@ -1773,5 +1773,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1773
1773
|
landed since 1.19.660 with no explicit bump. run 34552525938. (task 1002620)
|
|
1774
1774
|
1.19.662 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1775
1775
|
landed since 1.19.661 with no explicit bump. run 34553965204. (task 1002620)
|
|
1776
|
+
1.19.663 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1777
|
+
landed since 1.19.662 with no explicit bump. run 34557840364. (task 1002620)
|
|
1776
1778
|
---------------------------------------------------------------------------
|
|
1777
1779
|
```
|
|
@@ -15,10 +15,15 @@ const { pool, withTx } = api;
|
|
|
15
15
|
const log = api.logger('lifecycle');
|
|
16
16
|
const seams = api;
|
|
17
17
|
const { touchesOverlap } = api.pathMatch;
|
|
18
|
-
|
|
18
|
+
// CLAIM_ANY / CLAIM_NEWCOMER name the two atoms in the refusal message. The
|
|
19
|
+
// decision itself moved to db-rank-authz.claimGate (task 1003196), so this file
|
|
20
|
+
// no longer calls claimEligibilityAllows / rankAllowsTask / xenosClaimAllowed
|
|
21
|
+
// directly — composing them at three separate call sites is what made the gate
|
|
22
|
+
// un-testable except by counting.
|
|
23
|
+
const { CLAIM_ANY, CLAIM_NEWCOMER } = require('./claim-eligibility.js');
|
|
19
24
|
const { goalMembershipFailures } = require('./claim-feed.js');
|
|
20
25
|
const { unsatisfiedDeps } = require('./db-deps-criteria.js');
|
|
21
|
-
const { claimPermissionsFor, disambiguateWorktreeName, foldWorktreeName
|
|
26
|
+
const { claimGate, claimPermissionsFor, disambiguateWorktreeName, foldWorktreeName } = require('./db-rank-authz.js');
|
|
22
27
|
const { sealSessionLog } = require('./db-shared.js');
|
|
23
28
|
const { needsRebaseTasksForBuilder, shouldRefuseClaimResolve } = require('./db-ship.js');
|
|
24
29
|
const { REWARD_SUGGESTION_CAP, rewardGateAssignment } = require('./db-tasks.js');
|
|
@@ -142,8 +147,18 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
|
|
|
142
147
|
// is kept as an alias on the error object for callers on the pre-rename
|
|
143
148
|
// release. BV1.R106 (ADR 0151): same rule, now decided on the PERMISSIONS
|
|
144
149
|
// task.claim.any / task.claim.newcomer — see ./claim-eligibility.js.
|
|
150
|
+
// task 1003196: the queue+floor composition is decided ONCE, in
|
|
151
|
+
// db-rank-authz.claimGate, so a test can drive the real decision instead of
|
|
152
|
+
// counting `if`s in this file. The refusal SHAPES below are unchanged — that
|
|
153
|
+
// is why claimGate returns which gates refused rather than an error object.
|
|
145
154
|
const newcomerFriendly = task.newcomer_friendly;
|
|
146
|
-
|
|
155
|
+
const claimRefusals = claimGate({
|
|
156
|
+
builderRank,
|
|
157
|
+
newcomerFriendly,
|
|
158
|
+
requiresRank: task.requires_rank,
|
|
159
|
+
heldPermissions,
|
|
160
|
+
});
|
|
161
|
+
if (claimRefusals.includes('queue')) {
|
|
147
162
|
// task 1002613 (map §4.4): the bare code reported a PERMISSION decision as
|
|
148
163
|
// a rank one — no axis, no remedy. Copy INSUFFICIENT_RANK's shape below:
|
|
149
164
|
// name both sides of the wall and the way forward.
|
|
@@ -168,7 +183,7 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
|
|
|
168
183
|
// blocks Xenos+Metic. The DB CHECK on tasks.requires_rank enforces the
|
|
169
184
|
// enum, so we trust the value here. INSUFFICIENT_RANK carries the actual
|
|
170
185
|
// and required ranks so the caller can render a clean error.
|
|
171
|
-
if (
|
|
186
|
+
if (claimRefusals.includes('floor')) {
|
|
172
187
|
throw {
|
|
173
188
|
code: 'INSUFFICIENT_RANK',
|
|
174
189
|
required: task.requires_rank,
|
|
@@ -496,25 +511,32 @@ async function claimTasksBatch({ taskIds, builderId, worktreePrefix, creatorSess
|
|
|
496
511
|
// never checked here, while claimTask checks every rank), so removing it would
|
|
497
512
|
// change who can batch-claim. That pre-existing asymmetry is reported, not
|
|
498
513
|
// fixed, under this task — closing it is an authority decision of its own.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
514
|
+
//
|
|
515
|
+
// V3.R82 (#301): the per-task requires_rank floor rides the same decision.
|
|
516
|
+
// Mirrors the single claimTask gate so a Metic can't slip through batch-claim
|
|
517
|
+
// to grab an Archon-only task. The xenos sandbox check handles the xenos case
|
|
518
|
+
// exhaustively for requires_rank='xenos' (the default); the floor gate refuses
|
|
519
|
+
// requires_rank='metic'/'archon' when the builder is below.
|
|
520
|
+
//
|
|
521
|
+
// task 1003196: both gates now come from db-rank-authz.claimGate. A row can
|
|
522
|
+
// still collect BOTH refusals, and the two passes below are deliberate — this
|
|
523
|
+
// path groups every queue refusal ahead of every floor refusal, and `failures`
|
|
524
|
+
// is returned to the caller in order, so folding them into one pass per row
|
|
525
|
+
// would reorder a response for no reason.
|
|
526
|
+
const refusalsFor = (t) => claimGate({
|
|
527
|
+
builderRank,
|
|
528
|
+
newcomerFriendly: t.newcomer_friendly,
|
|
529
|
+
requiresRank: t.requires_rank,
|
|
530
|
+
heldPermissions,
|
|
531
|
+
queueGateApplies: builderRank === 'xenos',
|
|
532
|
+
});
|
|
533
|
+
for (const t of taskRows) {
|
|
534
|
+
if (refusalsFor(t).includes('queue')) {
|
|
535
|
+
failures.push({ task_id: t.id, reason: 'rank_too_low_for_task', legacy_reason: 'XENOS_RESTRICTED' });
|
|
508
536
|
}
|
|
509
537
|
}
|
|
510
|
-
|
|
511
|
-
// V3.R82 (#301): the per-task requires_rank floor. Mirrors the single
|
|
512
|
-
// claimTask gate so a Metic can't slip through batch-claim to grab an
|
|
513
|
-
// Archon-only task. The xenos sandbox check above handles the xenos case
|
|
514
|
-
// exhaustively for requires_rank='xenos' (the default); this gate refuses
|
|
515
|
-
// requires_rank='metic'/'archon' when the builder is below.
|
|
516
538
|
for (const t of taskRows) {
|
|
517
|
-
if (
|
|
539
|
+
if (refusalsFor(t).includes('floor')) {
|
|
518
540
|
failures.push({
|
|
519
541
|
task_id: t.id,
|
|
520
542
|
reason: 'INSUFFICIENT_RANK',
|
|
@@ -855,14 +877,19 @@ async function validateClaimBatch({ taskIds, builderId }) {
|
|
|
855
877
|
const t = byId.get(id);
|
|
856
878
|
if (!t) { push(id, { reason: 'TASK_NOT_FOUND' }); continue; }
|
|
857
879
|
if (t.status !== 'ready') push(id, { reason: 'TASK_NOT_READY', currentStatus: t.status });
|
|
858
|
-
|
|
859
|
-
|
|
880
|
+
// task 1003196: same composed decision as claimTask and claimTasksBatch, and
|
|
881
|
+
// the same deliberate xenos-only queue short-circuit the batch path carries.
|
|
882
|
+
const refusals = claimGate({
|
|
883
|
+
builderRank,
|
|
860
884
|
newcomerFriendly: t.newcomer_friendly,
|
|
861
|
-
|
|
862
|
-
|
|
885
|
+
requiresRank: t.requires_rank,
|
|
886
|
+
heldPermissions,
|
|
887
|
+
queueGateApplies: builderRank === 'xenos',
|
|
888
|
+
});
|
|
889
|
+
if (refusals.includes('queue')) {
|
|
863
890
|
push(id, { reason: 'rank_too_low_for_task', legacy_reason: 'XENOS_RESTRICTED' });
|
|
864
891
|
}
|
|
865
|
-
if (
|
|
892
|
+
if (refusals.includes('floor')) {
|
|
866
893
|
push(id, { reason: 'INSUFFICIENT_RANK', required: t.requires_rank, actual: builderRank });
|
|
867
894
|
}
|
|
868
895
|
}
|
|
@@ -14,6 +14,7 @@ const api = require('../../src/module-api');
|
|
|
14
14
|
const { pool, LIVE_RANK_LADDER, RANK_TIER } = api;
|
|
15
15
|
const seams = api;
|
|
16
16
|
const { matchProtected } = api.permissionPathCheck;
|
|
17
|
+
const { claimEligibilityAllows } = require('./claim-eligibility.js');
|
|
17
18
|
|
|
18
19
|
// -------------------------------------------------------------------------
|
|
19
20
|
// Builders
|
|
@@ -166,6 +167,51 @@ function rankAllowsTask(builderRank, requiredRank) {
|
|
|
166
167
|
return have >= needed;
|
|
167
168
|
}
|
|
168
169
|
|
|
170
|
+
// ── the COMPOSED per-task claim decision (task 1003196) ──────────────────────
|
|
171
|
+
//
|
|
172
|
+
// Claim eligibility is TWO independent gates — the QUEUE axis (permission atoms,
|
|
173
|
+
// ./claim-eligibility.js) and the FLOOR axis (`requires_rank`, rankAllowsTask
|
|
174
|
+
// above) — and the rule that matters is that BOTH must pass and neither
|
|
175
|
+
// substitutes for the other. That composition used to exist only as a pair of
|
|
176
|
+
// consecutive `if`s repeated at each of the three claim paths, which meant the
|
|
177
|
+
// only thing a test could check was that the source still CONTAINED them:
|
|
178
|
+
// `tests/government_claim_eligibility.mjs` counted occurrences, and neutering
|
|
179
|
+
// claimTask's gate with `&& false` left the count at 3 and the suite green
|
|
180
|
+
// (finding 10 of the task-1002671 verification). Counting is not a behaviour
|
|
181
|
+
// oracle, so the decision lives here, once, where a test can actually drive it.
|
|
182
|
+
//
|
|
183
|
+
// RETURNS THE GATES THAT REFUSED, in ladder order — `[]` means allowed. It
|
|
184
|
+
// deliberately does NOT return a ready-made error: each call site shapes its own
|
|
185
|
+
// refusal and those shapes are API surface (claimTask THROWS
|
|
186
|
+
// `{code:'rank_too_low_for_task', …, message, hint}`, while the batch paths PUSH
|
|
187
|
+
// `{task_id, reason, legacy_reason}` and accumulate both refusals for one row).
|
|
188
|
+
// Returning a decision instead of a wire error is what lets all three delegate
|
|
189
|
+
// without any of them changing what a caller sees.
|
|
190
|
+
//
|
|
191
|
+
// `queueGateApplies` carries a PRE-EXISTING asymmetry rather than quietly fixing
|
|
192
|
+
// it: claimTask runs the queue gate for every rank, while claimTasksBatch and
|
|
193
|
+
// validateClaimBatch run it only when `builderRank === 'xenos'`. That outer
|
|
194
|
+
// short-circuit is semantic, not a fast path — for the SEEDED sets the two agree
|
|
195
|
+
// exactly (thetes+ all hold task.claim.any), but a custom rank granted neither
|
|
196
|
+
// claim atom would be refused by claimTask and admitted by the batch paths.
|
|
197
|
+
// Closing that is an authority decision of its own, already reported and
|
|
198
|
+
// deliberately deferred at the call site; this parameter keeps each path's
|
|
199
|
+
// current meaning exact while still routing it through one decision.
|
|
200
|
+
const CLAIM_GATES = Object.freeze(['queue', 'floor']);
|
|
201
|
+
|
|
202
|
+
function claimGate({ builderRank, newcomerFriendly, requiresRank, heldPermissions, queueGateApplies = true }) {
|
|
203
|
+
const refused = [];
|
|
204
|
+
if (queueGateApplies && !claimEligibilityAllows({
|
|
205
|
+
heldPermissions,
|
|
206
|
+
newcomerFriendly,
|
|
207
|
+
rankRuleAllows: xenosClaimAllowed(builderRank, newcomerFriendly),
|
|
208
|
+
})) {
|
|
209
|
+
refused.push('queue');
|
|
210
|
+
}
|
|
211
|
+
if (!rankAllowsTask(builderRank, requiresRank)) refused.push('floor');
|
|
212
|
+
return Object.freeze(refused);
|
|
213
|
+
}
|
|
214
|
+
|
|
169
215
|
// deriveRequiredRank — task 1498 / ADR 0084. The per-task requires_rank floor
|
|
170
216
|
// (V3.R82 #301) shipped INERT: no create/edit path ever set it, so every task
|
|
171
217
|
// sat at the 'xenos' default and the claim gate never bit. We now DERIVE a floor
|
|
@@ -477,6 +523,8 @@ module.exports = {
|
|
|
477
523
|
RANKS,
|
|
478
524
|
REQUIRES_RANK_TIER,
|
|
479
525
|
THETES_GRADUATION_THRESHOLD,
|
|
526
|
+
CLAIM_GATES,
|
|
527
|
+
claimGate,
|
|
480
528
|
claimPermissionsFor,
|
|
481
529
|
countShippedTasksForBuilder,
|
|
482
530
|
deriveRequiredRank,
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.663",
|
|
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.663",
|
|
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.663",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.663'; // 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');
|
|
@@ -154,16 +154,138 @@ test('the permission predicate cannot see requires_rank at all (structural)', ()
|
|
|
154
154
|
}
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
157
|
+
// ── the COMPOSED gate, driven behaviourally (task 1003196, finding 10) ───────
|
|
158
|
+
//
|
|
159
|
+
// WHAT THIS REPLACED, AND WHY. Until task 1003196 this section grepped the
|
|
160
|
+
// lifecycle source for three occurrences of `rankAllowsTask(builderRank,
|
|
161
|
+
// t.requires_rank)` and three of `claimEligibilityAllows({`. That verifies
|
|
162
|
+
// PRESENCE, not EFFECT: neutering claimTask's gate with `&& false` left both
|
|
163
|
+
// counts at 3 and the suite green. The composition now lives in one pure export,
|
|
164
|
+
// db-rank-authz.claimGate, so these tests drive the real decision instead.
|
|
165
|
+
//
|
|
166
|
+
// claimGate returns the gates that REFUSED, in ladder order — `[]` means allowed.
|
|
167
|
+
|
|
168
|
+
const { claimGate, CLAIM_GATES } = rankAuthz;
|
|
169
|
+
const gatesFor = (o) => [...claimGate(o)];
|
|
170
|
+
|
|
171
|
+
test('claimGate: BOTH axes must pass, and neither substitutes for the other', () => {
|
|
172
|
+
// An Archon holding everything is still stopped by nothing at requires_rank xenos...
|
|
173
|
+
assert.deepEqual(gatesFor({
|
|
174
|
+
builderRank: 'archon', newcomerFriendly: false, requiresRank: 'xenos', heldPermissions: seedSet('archon'),
|
|
175
|
+
}), []);
|
|
176
|
+
|
|
177
|
+
// ...and a builder who clears the QUEUE axis is still refused by the FLOOR axis.
|
|
178
|
+
// This is the collapse the two-gate model exists to prevent: holding
|
|
179
|
+
// task.claim.any must not buy you an archon-floored task.
|
|
180
|
+
assert.deepEqual(gatesFor({
|
|
181
|
+
builderRank: 'metic', newcomerFriendly: false, requiresRank: 'archon', heldPermissions: seedSet('metic'),
|
|
182
|
+
}), ['floor']);
|
|
183
|
+
|
|
184
|
+
// The converse: clearing the FLOOR axis does not buy you the queue. A xenos on a
|
|
185
|
+
// default-floor task that is NOT newcomer_friendly holds only task.claim.newcomer.
|
|
186
|
+
assert.deepEqual(gatesFor({
|
|
187
|
+
builderRank: 'xenos', newcomerFriendly: false, requiresRank: 'xenos', heldPermissions: seedSet('xenos'),
|
|
188
|
+
}), ['queue']);
|
|
189
|
+
|
|
190
|
+
// Both can refuse the same row, and the order is the ladder order.
|
|
191
|
+
assert.deepEqual(gatesFor({
|
|
192
|
+
builderRank: 'xenos', newcomerFriendly: false, requiresRank: 'archon', heldPermissions: seedSet('xenos'),
|
|
193
|
+
}), ['queue', 'floor']);
|
|
194
|
+
|
|
195
|
+
// ...and a newcomer_friendly flag opens the queue without lowering the floor.
|
|
196
|
+
assert.deepEqual(gatesFor({
|
|
197
|
+
builderRank: 'xenos', newcomerFriendly: true, requiresRank: 'metic', heldPermissions: seedSet('xenos'),
|
|
198
|
+
}), ['floor'], 'newcomer_friendly must never lower requires_rank');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('claimGate: the composed decision matches the two axes evaluated separately', () => {
|
|
202
|
+
// The independent oracle: each axis run through its OWN production predicate
|
|
203
|
+
// (db.rankAllowsTask and eligibility.claimEligibilityAllows), composed here in
|
|
204
|
+
// the test. A claimGate that silently dropped a conjunct disagrees immediately.
|
|
205
|
+
let refusalsSeen = 0;
|
|
206
|
+
for (const rank of LIVE_RANKS) {
|
|
207
|
+
for (const nf of NF_VALUES) {
|
|
208
|
+
for (const requiresRank of [...LIVE_RANKS, null]) {
|
|
209
|
+
for (const held of [seedSet(rank), new Set(), new Set([CLAIM_ANY]), new Set([CLAIM_NEWCOMER]), null]) {
|
|
210
|
+
const expected = [];
|
|
211
|
+
if (!claimEligibilityAllows({
|
|
212
|
+
heldPermissions: held, newcomerFriendly: nf, rankRuleAllows: db.xenosClaimAllowed(rank, nf),
|
|
213
|
+
})) expected.push('queue');
|
|
214
|
+
if (!db.rankAllowsTask(rank, requiresRank)) expected.push('floor');
|
|
215
|
+
refusalsSeen += expected.length;
|
|
216
|
+
assert.deepEqual(
|
|
217
|
+
gatesFor({ builderRank: rank, newcomerFriendly: nf, requiresRank, heldPermissions: held }),
|
|
218
|
+
expected,
|
|
219
|
+
`${rank}/nf=${String(nf)}/req=${String(requiresRank)}/held=${held ? [...held].length : 'null'}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
assert.ok(refusalsSeen > 0, 'a matrix that never refuses would pass vacuously');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('claimGate: fail-closed on ranks and floors it does not recognise', () => {
|
|
229
|
+
// An unknown rank clears nothing — the posture rankAllowsTask already takes.
|
|
230
|
+
assert.ok(gatesFor({
|
|
231
|
+
builderRank: 'bogus', newcomerFriendly: true, requiresRank: 'xenos', heldPermissions: new Set([CLAIM_ANY]),
|
|
232
|
+
}).includes('floor'), 'an unrecognised builder rank must not clear the floor');
|
|
233
|
+
// A requires_rank the DB CHECK should have blocked refuses rather than defaults.
|
|
234
|
+
assert.ok(gatesFor({
|
|
235
|
+
builderRank: 'archon', newcomerFriendly: true, requiresRank: 'nonsense', heldPermissions: seedSet('archon'),
|
|
236
|
+
}).includes('floor'), 'an unrecognised requires_rank must fail closed');
|
|
237
|
+
// A null requires_rank is the column default — no extra gate.
|
|
238
|
+
assert.deepEqual(gatesFor({
|
|
239
|
+
builderRank: 'xenos', newcomerFriendly: true, requiresRank: null, heldPermissions: seedSet('xenos'),
|
|
240
|
+
}), []);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('claimGate: queueGateApplies carries the batch paths\' pre-existing asymmetry', () => {
|
|
244
|
+
// claimTask runs the queue gate for EVERY rank; claimTasksBatch and
|
|
245
|
+
// validateClaimBatch run it only when builderRank === 'xenos'. For the SEEDED
|
|
246
|
+
// sets the two agree exactly, which is why the asymmetry has never bitten...
|
|
247
|
+
for (const rank of LIVE_RANKS) {
|
|
248
|
+
for (const nf of NF_VALUES) {
|
|
249
|
+
const held = seedSet(rank);
|
|
250
|
+
assert.deepEqual(
|
|
251
|
+
gatesFor({ builderRank: rank, newcomerFriendly: nf, requiresRank: 'xenos', heldPermissions: held }),
|
|
252
|
+
gatesFor({ builderRank: rank, newcomerFriendly: nf, requiresRank: 'xenos', heldPermissions: held, queueGateApplies: rank === 'xenos' }),
|
|
253
|
+
`${rank}/nf=${String(nf)}: seeded sets must agree across both call-site flavours`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// ...and here is the case where they DIVERGE, pinned so it stays a known,
|
|
258
|
+
// deliberate difference rather than a surprise: a CUSTOM rank granted neither
|
|
259
|
+
// claim atom is refused by claimTask and admitted by the batch paths. Closing
|
|
260
|
+
// this is an authority decision of its own (see the note at the call site).
|
|
261
|
+
const neither = new Set(['cost.log']);
|
|
262
|
+
assert.deepEqual(gatesFor({
|
|
263
|
+
builderRank: 'metic', newcomerFriendly: false, requiresRank: 'xenos', heldPermissions: neither,
|
|
264
|
+
}), ['queue'], 'claimTask refuses a rank holding no claim atom');
|
|
265
|
+
assert.deepEqual(gatesFor({
|
|
266
|
+
builderRank: 'metic', newcomerFriendly: false, requiresRank: 'xenos', heldPermissions: neither, queueGateApplies: false,
|
|
267
|
+
}), [], 'the batch paths admit it — the asymmetry, recorded');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('every claim path delegates to claimGate rather than re-composing the axes', () => {
|
|
271
|
+
// The cheap STRUCTURAL complement to the behavioural tests above. It can still
|
|
272
|
+
// only see presence, so it is no longer the only thing standing: what it adds is
|
|
273
|
+
// that a call site cannot quietly go back to composing the gates itself, which
|
|
274
|
+
// is what let the three copies drift apart in the first place.
|
|
275
|
+
// Counted in db-claims.js alone: the whole-family source also carries the
|
|
276
|
+
// DEFINITION in db-rank-authz.js, which would make the count read 4 and quietly
|
|
277
|
+
// tolerate one call site dropping out.
|
|
278
|
+
const claims = fs.readFileSync(path.join(LIFECYCLE, 'db-claims.js'), 'utf8');
|
|
279
|
+
const delegations = claims.match(/claimGate\(\{/g) || [];
|
|
280
|
+
assert.equal(delegations.length, 3,
|
|
281
|
+
`claimTask, claimTasksBatch and validateClaimBatch must each call claimGate — found ${delegations.length}`);
|
|
282
|
+
// ...and the raw conjuncts are gone from the claim paths, so there is exactly
|
|
283
|
+
// one place where "both gates must pass" is written down.
|
|
284
|
+
for (const banned of ['rankAllowsTask(', 'claimEligibilityAllows(']) {
|
|
285
|
+
assert.ok(!claims.includes(banned),
|
|
286
|
+
`db-claims.js must reach the axes through claimGate, not call "${banned}" itself`);
|
|
287
|
+
}
|
|
288
|
+
assert.deepEqual([...CLAIM_GATES], ['queue', 'floor'], 'the gate names are ladder-ordered');
|
|
167
289
|
});
|
|
168
290
|
|
|
169
291
|
// ── the "authority could not be established" fallback ────────────────────────
|
|
@@ -551,15 +551,25 @@ test('F8: the verb field itself is partitioned with scope and checked against th
|
|
|
551
551
|
|
|
552
552
|
// ═══ G — the R94/R95/R98 invariants this slice must not have disturbed ════════
|
|
553
553
|
|
|
554
|
-
test('G1:
|
|
554
|
+
test('G1: RANK_SEED is still DERIVED from seedForRank, not hand-maintained', () => {
|
|
555
|
+
// task 1003196 (finding 8) trimmed this test, and it is worth saying exactly what
|
|
556
|
+
// it does and does not prove. It USED to re-spell seedForRank's formula
|
|
557
|
+
// (`floor !== null && RANK_ORDER.indexOf(p.floor) <= tier`) and assert it against
|
|
558
|
+
// RANK_SEED — but RANK_SEED is BUILT by seedForRank, so the two sides moved
|
|
559
|
+
// together: the copy could detect drift from the other copies of itself and never
|
|
560
|
+
// a wrong formula. That formula oracle is kept ONCE, in tests/government_catalog.mjs
|
|
561
|
+
// ("a role holds a permission iff its floor is a real rank at-or-below"), with the
|
|
562
|
+
// real independent oracles being government_parity.mjs Part A and the pinned
|
|
563
|
+
// concrete values beside it.
|
|
564
|
+
//
|
|
565
|
+
// What survives here is narrow and genuine: RANK_SEED must remain a DERIVATION.
|
|
566
|
+
// The catalog's "zero day-one change" guarantee (R103) is structural only while
|
|
567
|
+
// the seed is computed from floors — the day someone replaces RANK_SEED with a
|
|
568
|
+
// hand-written list, parity stops being a property of the code and becomes a
|
|
569
|
+
// promise someone has to keep by hand. That is what this catches.
|
|
555
570
|
for (const rank of RANK_ORDER) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
for (const p of PERMISSIONS) {
|
|
559
|
-
assert.equal(held.has(p.key), p.floor !== null && RANK_ORDER.indexOf(p.floor) <= tier,
|
|
560
|
-
`${rank} vs ${p.key}: scope must not touch the floor derivation`);
|
|
561
|
-
}
|
|
562
|
-
assert.deepEqual([...seedForRank(rank)].sort(), [...RANK_SEED[rank]].sort());
|
|
571
|
+
assert.deepEqual([...seedForRank(rank)].sort(), [...RANK_SEED[rank]].sort(),
|
|
572
|
+
`${rank}: RANK_SEED must be what seedForRank computes, never a maintained copy`);
|
|
563
573
|
}
|
|
564
574
|
});
|
|
565
575
|
|
|
@@ -214,11 +214,89 @@ test('every entry maps to a permission the catalog defines as system:true', () =
|
|
|
214
214
|
assert.deepEqual(registry.SYSTEM_PERMISSIONS, ['path.protected.modify']);
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
// Feed a BAD registry through the real loader in a cold subprocess and assert the
|
|
218
|
+
// module refuses to load. The pattern is the sibling probe below ("a missing
|
|
219
|
+
// registry fails CLOSED"): patch fs.readFileSync for the registry path only, then
|
|
220
|
+
// require the module under test and let its load-time throw be the result.
|
|
221
|
+
//
|
|
222
|
+
// WHY A SUBPROCESS AND NOT AN IN-PROCESS ASSERTION (task 1003196, finding 9). The
|
|
223
|
+
// version of this test before that task read the catalog and checked that a
|
|
224
|
+
// grantable key exists and an unknown key returns null — both true regardless of
|
|
225
|
+
// whether the guard exists. Demonstrated: deleting the whole validation loop from
|
|
226
|
+
// modules/government/protected-surfaces.js left the entire suite green. The guard
|
|
227
|
+
// is load-time, so only a fresh module load can exercise it, and the registry is
|
|
228
|
+
// read from disk, so only a patched read can make it bad.
|
|
229
|
+
//
|
|
230
|
+
// This matters because the sibling test above ("every entry maps to a permission
|
|
231
|
+
// the catalog defines as system:true") only checks the COMMITTED file. The
|
|
232
|
+
// load-time throw is what protects an out-of-band registry write — the Government
|
|
233
|
+
// tab editing an instance file at runtime — and nothing was watching it.
|
|
234
|
+
function loadWithRegistry(registryJson) {
|
|
235
|
+
execFileSync(process.execPath, ['-e',
|
|
236
|
+
"const fs=require('node:fs');const real=fs.readFileSync;" +
|
|
237
|
+
'const fake=' + JSON.stringify(JSON.stringify(registryJson)) + ';' +
|
|
238
|
+
"fs.readFileSync=(p,...r)=>(String(p).includes('protected-surfaces.json') ? fake : real(p,...r));" +
|
|
239
|
+
"require('./modules/government/protected-surfaces.js');",
|
|
240
|
+
], { cwd: ROOT, stdio: 'pipe' });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
test('a surface mapped to a GRANTABLE permission fails loud at load', () => {
|
|
244
|
+
// A grantable (system:false) key is the dangerous case ADR 0151 §4 names: a
|
|
245
|
+
// custom rank could be handed the very power the substrate reserves.
|
|
219
246
|
const grantable = catalog.grantableKeys()[0];
|
|
220
|
-
assert.ok(grantable
|
|
221
|
-
assert.equal(catalog.
|
|
247
|
+
assert.ok(grantable, 'the catalog must define at least one grantable key for this probe');
|
|
248
|
+
assert.equal(catalog.isSystem(grantable), false, 'fixture sanity: the probe key must be grantable');
|
|
249
|
+
assert.throws(
|
|
250
|
+
() => loadWithRegistry({
|
|
251
|
+
floor: 'metic',
|
|
252
|
+
permission: grantable,
|
|
253
|
+
surfaces: [{ glob: 'src/probe/grantable-surface.js' }],
|
|
254
|
+
}),
|
|
255
|
+
/which is not system:true/,
|
|
256
|
+
'a surface mapping to a grantable permission must refuse to load',
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('a surface mapped to an UNKNOWN permission fails loud at load', () => {
|
|
261
|
+
assert.equal(catalog.byKey('not.a.real.permission'), null, 'fixture sanity: the key must be unknown');
|
|
262
|
+
assert.throws(
|
|
263
|
+
() => loadWithRegistry({
|
|
264
|
+
floor: 'metic',
|
|
265
|
+
permission: 'not.a.real.permission',
|
|
266
|
+
surfaces: [{ glob: 'src/probe/unknown-surface.js' }],
|
|
267
|
+
}),
|
|
268
|
+
/which the permission catalog does not define/,
|
|
269
|
+
'a surface mapping to a permission the catalog never defines must refuse to load',
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('the probe itself is honest — a GOOD registry loads clean through the same path', () => {
|
|
274
|
+
// Without this, both assertions above would pass if the subprocess threw for an
|
|
275
|
+
// unrelated reason (a bad patch, a missing cwd, a syntax error in the -e script).
|
|
276
|
+
assert.doesNotThrow(() => loadWithRegistry({
|
|
277
|
+
floor: 'metic',
|
|
278
|
+
permission: registry.SYSTEM_PERMISSIONS[0],
|
|
279
|
+
surfaces: [{ glob: 'src/probe/good-surface.js' }],
|
|
280
|
+
}), 'a well-formed registry must still load — otherwise the probes above prove nothing');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('an unknown FLOOR is refused before the catalog check ever sees it', () => {
|
|
284
|
+
// The validation loop's third branch (`!catalog.RANK_ORDER.includes(s.floor)`) is
|
|
285
|
+
// a DRIFT guard, not dead code, and this test records why it cannot be provoked
|
|
286
|
+
// through the registry: the kernel loader keeps its own local RANK_TIER map
|
|
287
|
+
// (permission-path-check.js) and rejects an unknown floor first. The government
|
|
288
|
+
// branch only becomes reachable if those two rank lists ever disagree — which is
|
|
289
|
+
// exactly the drift it exists to catch, and is not craftable from JSON alone.
|
|
290
|
+
assert.throws(
|
|
291
|
+
() => loadWithRegistry({
|
|
292
|
+
floor: 'emperor',
|
|
293
|
+
permission: registry.SYSTEM_PERMISSIONS[0],
|
|
294
|
+
surfaces: [{ glob: 'src/probe/bad-floor.js' }],
|
|
295
|
+
}),
|
|
296
|
+
/unknown floor/,
|
|
297
|
+
);
|
|
298
|
+
assert.deepEqual(catalog.RANK_ORDER, ['xenos', 'thetes', 'metic', 'archon'],
|
|
299
|
+
'if this list and the kernel RANK_TIER drift, the government floor branch wakes up');
|
|
222
300
|
});
|
|
223
301
|
|
|
224
302
|
// ---------- synchronous, no DB, no server ----------
|