@bongos/core 1.19.1073 → 1.19.1074
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 +320 -465
- package/.claude/skills/feedback/SKILL.md +2 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -60
- package/clients/bongos-client/index.cjs +4 -60
- package/clients/bongos-client/index.d.ts +5 -90
- package/clients/bongos-client/index.mjs +4 -60
- package/config/branding.neutral.json +1 -5
- package/config/modules.neutral.json +2 -3
- package/config/scheduled-routines.json +0 -2
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
- package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
- package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
- package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
- package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +2 -0
- package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +171 -1661
- package/docs/api-reference.md +9 -41
- package/docs/architecture.md +16 -1
- package/docs/branding-contract.md +0 -3
- package/docs/copy-inventory.md +454 -479
- package/docs/copy-registry.json +746 -978
- package/docs/file-map.md +5 -34
- package/docs/module-api-changelog.md +2 -0
- package/docs/modules-contract.md +13 -1
- package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
- package/docs/onboarding/diagrams/README.md +3 -3
- package/docs/onboarding/diagrams/assertions.json +2 -22
- package/docs/onboarding/primer.md +9 -28
- package/docs/page-inventory.json +4 -1
- package/docs/page-readings.json +1032 -1077
- package/docs/recipes/render-pilot.md +29 -10
- package/migrations/core_257_module_store_registry.sql +87 -0
- package/migrations/core_258_drop_dev_box_tables.sql +55 -0
- package/modules/agents/module.json +1 -0
- package/modules/autonomy/module.json +1 -0
- package/modules/builder-settings/module.json +1 -0
- package/modules/builder-settings/render-prefs.js +4 -5
- package/modules/copy-desk/docx.js +78 -5
- package/modules/copy-desk/module.json +1 -0
- package/modules/copy-desk/routes/copy-desk.js +9 -9
- package/modules/copy-desk/tests/copy_docx.mjs +107 -1
- package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
- package/modules/discord/module.json +1 -0
- package/modules/economy/module.json +1 -0
- package/modules/economy/routes/credits.js +1 -2
- package/modules/government/catalog.js +13 -17
- package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
- package/modules/government/module.json +1 -0
- package/modules/government/protected-surfaces.json +3 -11
- package/modules/government/resolver.js +3 -46
- package/modules/government/routes/government.js +0 -2
- package/modules/government/session-scopes.js +43 -83
- package/modules/government/session-scopes.json +8 -18
- package/modules/grading/module.json +1 -0
- package/modules/hall-ui/module.json +1 -0
- package/modules/hall-ui/public/board-lib.js +2 -2
- package/modules/hall-ui/public/builders.js +29 -40
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/diagrams.html +3 -3
- package/modules/hall-ui/public/dom-utils.js +1 -1
- package/modules/hall-ui/public/gate.js +2 -2
- package/modules/hall-ui/public/gate.states.json +1 -1
- package/modules/hall-ui/public/hall-render.js +43 -79
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.js +1 -0
- package/modules/hall-ui/public/oversight.css +5 -5
- package/modules/hall-ui/public/palette.js +0 -11
- package/modules/hall-ui/public/primer.js +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings-sessions.js +1 -1
- package/modules/hall-ui/public/settings.css +0 -1
- package/modules/hall-ui/public/settings.html +4 -33
- package/modules/hall-ui/public/settings.js +3 -33
- package/modules/hall-ui/public/settings.states.json +2 -2
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +10 -13
- package/modules/hall-ui/public/task.html +4 -1
- package/modules/hall-ui/public/task.js +36 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
- package/modules/hall-ui/public/tweak-editor.css +16 -0
- package/modules/hall-ui/public/tweak-editor.html +26 -3
- package/modules/hall-ui/public/tweak-editor.js +32 -1
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/hall-ui/records/tweak-editor.md +1 -0
- package/modules/ideas/module.json +1 -0
- package/modules/lifecycle/db-claim-reads.js +1 -58
- package/modules/lifecycle/db-ship.js +0 -5
- package/modules/lifecycle/db.js +0 -4
- package/modules/lifecycle/kickoff-checklist.js +0 -4
- package/modules/lifecycle/lifecycle.js +0 -8
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/routes/tasks.js +5 -7
- package/modules/memory/module.json +1 -0
- package/modules/npm-release/module.json +1 -0
- package/modules/onboarding/module.json +1 -0
- package/modules/onboarding/onboarding-state.js +17 -36
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/module.json +1 -0
- package/modules/platform-identity/platform-identity.js +13 -5
- package/modules/platform-identity/routes/sso.js +2 -2
- package/modules/platform-identity/tests/platform-identity.mjs +17 -3
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/provisioning.js +2 -2
- package/modules/provisioning/starter-bundles.js +5 -25
- package/modules/public-landing/module.json +1 -0
- package/modules/public-landing/public/assets/cosmos.css +1 -1
- package/modules/public-landing/public/contact.html +1 -1
- package/modules/public-landing/public/privacy.html +1 -1
- package/modules/public-landing/public/projects.html +101 -32
- package/modules/public-landing/public/projects.probes.json +2 -2
- package/modules/public-landing/public/projects.states.json +4 -3
- package/modules/public-landing/public/terms.html +1 -1
- package/modules/security/module.json +1 -0
- package/modules/sessions/module.json +1 -0
- package/modules/specialities/module.json +1 -0
- package/modules/status-ui/module.json +1 -0
- package/modules/ui-design/module.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +78 -0
- package/scripts/gds/artifact-format.js +203 -0
- package/scripts/gds/audit-authorship.sh +1 -13
- package/scripts/gds/claim.js +0 -15
- package/scripts/gds/cli-lib.js +8 -8
- package/scripts/gds/diagram-facts.js +12 -23
- package/scripts/gds/do-api.js +13 -80
- package/scripts/gds/doc-cli-guard.js +2 -0
- package/scripts/gds/doctor.js +1 -1
- package/scripts/gds/feedback-latest.js +139 -9
- package/scripts/gds/fitness-checks-identity.js +2 -7
- package/scripts/gds/fitness-checks-write-validation.js +4 -0
- package/scripts/gds/fitness-ratchets.js +4 -4
- package/scripts/gds/fitness.js +10 -10
- package/scripts/gds/gen-diagrams.js +49 -71
- package/scripts/gds/http-api-client.js +12 -19
- package/scripts/gds/init.js +1 -1
- package/scripts/gds/lint-infra-exec.js +1 -1
- package/scripts/gds/local-preview-lib.js +8 -39
- package/scripts/gds/local-preview.js +2 -15
- package/scripts/gds/migration-namespace.js +3 -3
- package/scripts/gds/module-artifact.js +212 -0
- package/scripts/gds/module.js +95 -6
- package/scripts/gds/package-core.js +5 -116
- package/scripts/gds/provision-config.js +2 -2
- package/scripts/gds/provision-net.js +2 -3
- package/scripts/gds/provision.js +9 -9
- package/scripts/gds/publish-manifest.js +1 -2
- package/scripts/gds/regen-instance-docs.js +1 -1
- package/scripts/gds/run-unit-tests.js +2 -16
- package/scripts/gds/sandbox-stage.js +39 -341
- package/scripts/gds/ship-flow.js +6 -6
- package/scripts/gds/ship-land.js +3 -4
- package/scripts/gds/ship-regen.js +10 -41
- package/scripts/gds/ship.js +4 -24
- package/scripts/gds/start.js +0 -15
- package/scripts/hall-preview/README.md +5 -5
- package/scripts/hall-preview/server.js +6 -6
- package/scripts/render-diagrams.sh +1 -1
- package/src/bongos/api-errors.js +1 -1
- package/src/bongos/auth-github.js +2 -3
- package/src/bongos/auth.js +51 -126
- package/src/bongos/db-kernel.js +2 -3
- package/src/bongos/db.js +5 -28
- package/src/bongos/module-scope-map.js +14 -29
- package/src/bongos/module-store.js +141 -0
- package/src/bongos/route-rank-check.js +3 -16
- package/src/bongos/routes/auth.js +0 -116
- package/src/bongos/routes/builders.js +5 -15
- package/src/bongos/routes/instance.js +8 -14
- package/src/bongos/routes/me.js +6 -22
- package/src/bongos/routes/modules.js +83 -0
- package/src/bongos/routes.js +5 -0
- package/src/module-api.js +13 -6
- package/src/module-loader/catalog.js +3 -0
- package/src/module-loader/loader.js +4 -4
- package/src/module-loader/manifest-schema.js +15 -7
- package/src/module-seams.js +2 -3
- package/src/modules.js +42 -14
- package/tests/agents_spend_guard.mjs +1 -1
- package/tests/api_client.mjs +0 -15
- package/tests/auth_page_gate.mjs +10 -10
- package/tests/canonical_profile_url.mjs +1 -3
- package/tests/claim_action.mjs +4 -1
- package/tests/claim_from_task_and_home.mjs +127 -0
- package/tests/consumer_layout_boot.mjs +2 -0
- package/tests/copy_desk_page_docx.mjs +33 -2
- package/tests/core_upgrade_runner.mjs +6 -6
- package/tests/deploy_divergence_line.mjs +11 -11
- package/tests/diagram_facts_offset.mjs +1 -1
- package/tests/do_api.mjs +106 -0
- package/tests/feedback_latest.mjs +122 -0
- package/tests/fitness.mjs +44 -42
- package/tests/gate_approvals.mjs +0 -1
- package/tests/go_live.mjs +3 -3
- package/tests/government_abuse_matrix.mjs +20 -19
- package/tests/government_ownership_scope.mjs +33 -35
- package/tests/government_parity.mjs +2 -2
- package/tests/government_protected_surfaces.mjs +56 -37
- package/tests/government_require_permission.mjs +3 -3
- package/tests/government_seed.mjs +37 -4
- package/tests/government_session_scope.mjs +92 -366
- package/tests/hall_error_envelope.mjs +4 -0
- package/tests/hall_landing_boot.mjs +4 -1
- package/tests/hall_palette.mjs +1 -16
- package/tests/hall_record_world.mjs +6 -0
- package/tests/hall_tweak_editor.mjs +52 -1
- package/tests/host_topology_skips.mjs +1 -2
- package/tests/html_comment_nesting.mjs +137 -0
- package/tests/infra_exec_paths.mjs +9 -10
- package/tests/init.mjs +4 -5
- package/tests/instance_manifest.mjs +18 -25
- package/tests/kickoff_checklist.mjs +1 -1
- package/tests/lib_sh_resolution.mjs +0 -221
- package/tests/local_preview.mjs +0 -12
- package/tests/module-scope-map.mjs +9 -9
- package/tests/module_catalog.mjs +1 -1
- package/tests/module_cli.mjs +15 -15
- package/tests/module_contributions.mjs +6 -6
- package/tests/module_loader.mjs +6 -6
- package/tests/module_manifest.mjs +13 -4
- package/tests/module_store_publish.mjs +307 -0
- package/tests/module_store_publish_route.mjs +121 -0
- package/tests/module_store_registry_migration.mjs +70 -0
- package/tests/modules.mjs +56 -26
- package/tests/onboarding_route_signals.mjs +21 -31
- package/tests/onboarding_state.mjs +35 -49
- package/tests/permission_path.mjs +0 -3
- package/tests/platform_boot.mjs +1 -2
- package/tests/profile_route.mjs +1 -6
- package/tests/projects_hub.mjs +5 -5
- package/tests/projects_hub_app_step.mjs +138 -0
- package/tests/projects_hub_module_picker.mjs +2 -2
- package/tests/projects_hub_pre_uat.mjs +4 -5
- package/tests/provision.mjs +7 -7
- package/tests/provisioning_starter_bundles.mjs +0 -22
- package/tests/publish_branch_route.mjs +10 -18
- package/tests/publish_manifest.mjs +1 -2
- package/tests/rank_tier_single_source.mjs +1 -13
- package/tests/repo_map.mjs +3 -4
- package/tests/runner_drift.mjs +3 -21
- package/tests/sandbox_stage.mjs +57 -390
- package/tests/session_rename_fallback.mjs +0 -21
- package/tests/ship_error_shape.mjs +1 -1
- package/tests/task_visual_slots.mjs +31 -0
- package/tests/terms_acceptance.mjs +1 -1
- package/tests/upgrade.mjs +4 -4
- package/tests/watch_sealed_floor.mjs +0 -1
- package/tests/wizard_draft_resume.mjs +2 -2
- package/tests/wizard_intent_resume.mjs +21 -10
- package/tests/wizard_preselect_why.mjs +18 -18
- package/docs/onboarding/browser-terminal-guide.md +0 -73
- package/docs/recipes/managed-settings-remote-control.md +0 -416
- package/modules/dev-box/CLAUDE.md +0 -15
- package/modules/dev-box/box-access.js +0 -623
- package/modules/dev-box/box-credential.js +0 -94
- package/modules/dev-box/box-onboard.js +0 -486
- package/modules/dev-box/boxes.js +0 -1038
- package/modules/dev-box/db.js +0 -39
- package/modules/dev-box/module.json +0 -19
- package/modules/dev-box/routes/box.js +0 -1223
- package/scripts/gds/box-auth-check.js +0 -160
- package/scripts/gds/box-infra.js +0 -321
- package/scripts/gds/box-sync.js +0 -216
- package/scripts/gds/box.js +0 -1332
- package/scripts/gds/cf-tunnel.js +0 -558
- package/scripts/gds/smoke-box.sh +0 -150
- package/scripts/gds/tree-preflight.js +0 -241
- package/src/bongos/app-pair.js +0 -172
- package/tests/app_pair.mjs +0 -168
- package/tests/box_access.mjs +0 -578
- package/tests/box_auth_check.mjs +0 -114
- package/tests/box_code_staleness.mjs +0 -129
- package/tests/box_connect_e2e.mjs +0 -196
- package/tests/box_cost.mjs +0 -70
- package/tests/box_credential.mjs +0 -125
- package/tests/box_dns_repoint.mjs +0 -153
- package/tests/box_env.mjs +0 -64
- package/tests/box_host_keys.mjs +0 -126
- package/tests/box_infra_resolve.mjs +0 -246
- package/tests/box_onboard.mjs +0 -317
- package/tests/box_scope_session.mjs +0 -119
- package/tests/box_sweep_docs.mjs +0 -119
- package/tests/box_sync.mjs +0 -55
- package/tests/box_sync_scope_report.mjs +0 -126
- package/tests/box_task_scope.mjs +0 -169
- package/tests/box_task_scope_pause.mjs +0 -66
- package/tests/box_terminal_ssrf.mjs +0 -96
- package/tests/boxes.mjs +0 -1585
- package/tests/cf_ruleset_preflight.mjs +0 -142
- package/tests/cf_tunnel.mjs +0 -496
- package/tests/docs_scrubber_damage.mjs +0 -111
- package/tests/module_scope_active_claims.mjs +0 -94
- package/tests/ship_api_client_pathspec.mjs +0 -100
- package/tests/tree_preflight.mjs +0 -243
package/tests/box_access.mjs
DELETED
|
@@ -1,578 +0,0 @@
|
|
|
1
|
-
// tests/box_access.mjs
|
|
2
|
-
//
|
|
3
|
-
// Hermetic unit smoke for the rank-gated SOURCE-access decision core of
|
|
4
|
-
// modules/dev-box/box-access.js (ADR 0031 §6 + §9.2, task #600; extended for the
|
|
5
|
-
// sandbox-for-everyone starter scope, ADR 0052 / #1001). Covers the PURE
|
|
6
|
-
// functions the GET /box/source-access route and `box.js reconcile` both consume
|
|
7
|
-
// — with NO database, NO DigitalOcean, NO network. The DB helpers
|
|
8
|
-
// (listBoxesForReconcile / recordSourceEvent / flagBoxReconcileForBuilder) are
|
|
9
|
-
// thin wrappers exercised on the control plane; the DECISIONS they rely on are
|
|
10
|
-
// pinned here so the route and the reconciler can never drift from one rule.
|
|
11
|
-
//
|
|
12
|
-
// Re-added under #674 (the test was written + passing in #600 but deferred by
|
|
13
|
-
// parallel-claim conflicts on tests/ and docs/file-map.md). Wired into
|
|
14
|
-
// scripts/gds/smoke-box.sh step 1b.
|
|
15
|
-
//
|
|
16
|
-
// Run: node tests/box_access.mjs
|
|
17
|
-
|
|
18
|
-
import { strict as assert } from 'node:assert';
|
|
19
|
-
import { createRequire } from 'node:module';
|
|
20
|
-
|
|
21
|
-
const require = createRequire(import.meta.url);
|
|
22
|
-
const access = require('../modules/dev-box/box-access.js');
|
|
23
|
-
const boxes = require('../modules/dev-box/boxes.js');
|
|
24
|
-
|
|
25
|
-
let passed = 0;
|
|
26
|
-
let failed = 0;
|
|
27
|
-
function t(name, fn) {
|
|
28
|
-
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
29
|
-
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const FLOOR = boxes.DEFAULT_PROVISION_FLOOR; // 'xenos' — everyone meets the default
|
|
33
|
-
|
|
34
|
-
console.log('boxCanHoldSource — only a live/becoming-live droplet holds a clone:');
|
|
35
|
-
|
|
36
|
-
t('boxCanHoldSource: true for active / provisioning / waking', () => {
|
|
37
|
-
assert.equal(access.boxCanHoldSource('active'), true);
|
|
38
|
-
assert.equal(access.boxCanHoldSource('provisioning'), true);
|
|
39
|
-
assert.equal(access.boxCanHoldSource('waking'), true);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
t('boxCanHoldSource: false for parked / destroyed / none / idle / unknown', () => {
|
|
43
|
-
assert.equal(access.boxCanHoldSource('parked'), false);
|
|
44
|
-
assert.equal(access.boxCanHoldSource('destroyed'), false);
|
|
45
|
-
assert.equal(access.boxCanHoldSource('none'), false);
|
|
46
|
-
assert.equal(access.boxCanHoldSource('idle'), false);
|
|
47
|
-
assert.equal(access.boxCanHoldSource(undefined), false);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
console.log('\ncloneSpecForScope — full vs starter, fail-closed to least privilege:');
|
|
51
|
-
|
|
52
|
-
t("cloneSpecForScope('full') → full clone, no sparse paths", () => {
|
|
53
|
-
assert.deepEqual(access.cloneSpecForScope('full'), { scope: 'full', mode: 'full', sparsePaths: null });
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
t("cloneSpecForScope('starter') → sparse clone of the starter surface", () => {
|
|
57
|
-
const spec = access.cloneSpecForScope('starter');
|
|
58
|
-
assert.equal(spec.scope, 'starter');
|
|
59
|
-
assert.equal(spec.mode, 'sparse');
|
|
60
|
-
assert.deepEqual(spec.sparsePaths, access.STARTER_SPARSE_PATHS);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
t('cloneSpecForScope: unknown/missing scope FAILS CLOSED to starter (never full)', () => {
|
|
64
|
-
for (const bad of ['admin', '', undefined, null, 'FULL']) {
|
|
65
|
-
assert.equal(access.cloneSpecForScope(bad).scope, 'starter', `scope ${JSON.stringify(bad)} must collapse to starter`);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
t('cloneSpecForScope: starter sparsePaths is a COPY (mutating it cannot poison the catalog)', () => {
|
|
70
|
-
const spec = access.cloneSpecForScope('starter');
|
|
71
|
-
spec.sparsePaths.push('SHOULD_NOT_PERSIST');
|
|
72
|
-
assert.ok(!access.STARTER_SPARSE_PATHS.includes('SHOULD_NOT_PERSIST'),
|
|
73
|
-
'the returned sparsePaths must not alias the shared STARTER_SPARSE_PATHS array');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
console.log('\nSTARTER_SPARSE_PATHS — the scoped surface (ADR 0052 §9.2):');
|
|
77
|
-
|
|
78
|
-
t('starter surface includes the build tooling + game runtime', () => {
|
|
79
|
-
for (const p of ['.claude', 'scripts', 'src/bongos', 'src/world', 'src/rooms', 'public', '.devcontainer', 'infra', 'tests']) {
|
|
80
|
-
assert.ok(access.STARTER_SPARSE_PATHS.includes(p), `starter scope must include ${p}`);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
t('starter surface EXCLUDES migrations/ (degraded-DB preview needs no schema) and the hall', () => {
|
|
85
|
-
assert.ok(!access.STARTER_SPARSE_PATHS.includes('migrations'), 'migrations/ must stay out of the starter scope');
|
|
86
|
-
// the hall carved to modules/hall-ui/ in BV1.R85; it stays out of a starter box
|
|
87
|
-
assert.ok(!access.STARTER_SPARSE_PATHS.includes('modules/hall-ui'), 'modules/hall-ui (the hall) must stay out of the starter scope');
|
|
88
|
-
assert.ok(!access.STARTER_SPARSE_PATHS.includes('public-builders'), 'the old public-builders/ path must stay out of the starter scope');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
console.log('\ndecideSourceAccess — the live per-fetch gate:');
|
|
92
|
-
|
|
93
|
-
t('decideSourceAccess: active Xenos at the default floor → allowed, starter scope', () => {
|
|
94
|
-
const d = access.decideSourceAccess({ rank: 'xenos', status: 'active' });
|
|
95
|
-
assert.equal(d.allowed, true);
|
|
96
|
-
assert.equal(d.scope, 'starter');
|
|
97
|
-
assert.equal(d.spec.mode, 'sparse');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
t('decideSourceAccess: active Metic → allowed, FULL scope (live rank, not stored scope)', () => {
|
|
101
|
-
const d = access.decideSourceAccess({ rank: 'metic', status: 'active' });
|
|
102
|
-
assert.equal(d.allowed, true);
|
|
103
|
-
assert.equal(d.scope, 'full');
|
|
104
|
-
assert.equal(d.spec.sparsePaths, null);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
t('decideSourceAccess: Archon → full; Thetes → starter', () => {
|
|
108
|
-
assert.equal(access.decideSourceAccess({ rank: 'archon', status: 'active' }).scope, 'full');
|
|
109
|
-
assert.equal(access.decideSourceAccess({ rank: 'thetes', status: 'active' }).scope, 'starter');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
t('decideSourceAccess: inactive builder → BUILDER_INACTIVE (checked BEFORE rank)', () => {
|
|
113
|
-
// A deactivated builder keeps a rank but loses access — and status is checked
|
|
114
|
-
// first, so even a Metic gets denied for inactivity, not a rank reason.
|
|
115
|
-
const d = access.decideSourceAccess({ rank: 'metic', status: 'inactive' });
|
|
116
|
-
assert.deepEqual(d, { allowed: false, reason: 'BUILDER_INACTIVE' });
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
t('decideSourceAccess: rank below an explicit floor → INSUFFICIENT_RANK with required+actual', () => {
|
|
120
|
-
const d = access.decideSourceAccess({ rank: 'xenos', status: 'active', floor: 'metic' });
|
|
121
|
-
assert.deepEqual(d, { allowed: false, reason: 'INSUFFICIENT_RANK', required: 'metic', actual: 'xenos' });
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
t('decideSourceAccess: unknown rank → INSUFFICIENT_RANK (fail closed), actual echoes the bad rank', () => {
|
|
125
|
-
const d = access.decideSourceAccess({ rank: 'wizard', status: 'active', floor: FLOOR });
|
|
126
|
-
assert.equal(d.allowed, false);
|
|
127
|
-
assert.equal(d.reason, 'INSUFFICIENT_RANK');
|
|
128
|
-
assert.equal(d.actual, 'wizard');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
t('decideSourceAccess: missing rank → actual null (not undefined)', () => {
|
|
132
|
-
const d = access.decideSourceAccess({ status: 'active', floor: FLOOR });
|
|
133
|
-
assert.equal(d.allowed, false);
|
|
134
|
-
assert.equal(d.actual, null);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
t('decideSourceAccess: status omitted (falsy) is treated as active — rank alone decides', () => {
|
|
138
|
-
const d = access.decideSourceAccess({ rank: 'metic' });
|
|
139
|
-
assert.equal(d.allowed, true);
|
|
140
|
-
assert.equal(d.scope, 'full');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
console.log('\nreconcileSourceAccess — the slower droplet-level clawback/rescope:');
|
|
144
|
-
|
|
145
|
-
t('reconcileSourceAccess: no box (none/destroyed/missing) → none/no-box', () => {
|
|
146
|
-
assert.deepEqual(access.reconcileSourceAccess({ boxState: 'none', rank: 'metic', status: 'active' }), { action: 'none', reason: 'no-box' });
|
|
147
|
-
assert.deepEqual(access.reconcileSourceAccess({ boxState: 'destroyed', rank: 'metic', status: 'active' }), { action: 'none', reason: 'no-box' });
|
|
148
|
-
assert.deepEqual(access.reconcileSourceAccess({ rank: 'metic', status: 'active' }), { action: 'none', reason: 'no-box' });
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
t('reconcileSourceAccess: inactive builder with a live box → deprovision (the clawback)', () => {
|
|
152
|
-
const d = access.reconcileSourceAccess({ boxScope: 'starter', boxState: 'active', rank: 'xenos', status: 'inactive' });
|
|
153
|
-
assert.deepEqual(d, { action: 'deprovision', reason: 'builder-inactive' });
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
t('reconcileSourceAccess: rank below floor → deprovision/below-floor', () => {
|
|
157
|
-
const d = access.reconcileSourceAccess({ boxScope: 'starter', boxState: 'active', rank: 'xenos', status: 'active', floor: 'metic' });
|
|
158
|
-
assert.deepEqual(d, { action: 'deprovision', reason: 'below-floor' });
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
t('reconcileSourceAccess: stored FULL box but live rank no longer Metic+ → deprovision (fresh box needed)', () => {
|
|
162
|
-
// A full box already holds the whole repo on disk; it cannot be safely
|
|
163
|
-
// downgraded in place, so the reconciler destroys it.
|
|
164
|
-
const d = access.reconcileSourceAccess({ boxScope: 'full', boxState: 'active', rank: 'thetes', status: 'active' });
|
|
165
|
-
assert.deepEqual(d, { action: 'deprovision', reason: 'scope-downgrade-needs-fresh-box' });
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
t('reconcileSourceAccess: stored STARTER box but live rank now Metic+ → rescope to full (graduation)', () => {
|
|
169
|
-
const d = access.reconcileSourceAccess({ boxScope: 'starter', boxState: 'active', rank: 'metic', status: 'active' });
|
|
170
|
-
assert.deepEqual(d, { action: 'rescope', reason: 'graduation', newScope: 'full' });
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
t('reconcileSourceAccess: scope already matches live rank → none/in-sync', () => {
|
|
174
|
-
assert.deepEqual(access.reconcileSourceAccess({ boxScope: 'starter', boxState: 'active', rank: 'xenos', status: 'active' }), { action: 'none', reason: 'in-sync' });
|
|
175
|
-
assert.deepEqual(access.reconcileSourceAccess({ boxScope: 'full', boxState: 'active', rank: 'metic', status: 'active' }), { action: 'none', reason: 'in-sync' });
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
t('reconcileSourceAccess: inactivity outranks an otherwise in-sync full Metic box', () => {
|
|
179
|
-
// status is checked before the scope comparison, so a deactivated Metic with a
|
|
180
|
-
// matching full box is still torn down.
|
|
181
|
-
const d = access.reconcileSourceAccess({ boxScope: 'full', boxState: 'active', rank: 'metic', status: 'inactive' });
|
|
182
|
-
assert.deepEqual(d, { action: 'deprovision', reason: 'builder-inactive' });
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// ===========================================================================
|
|
186
|
-
// TASK-SCOPED source access (ADR 0148 / goal 1000051, R93). BASE_SPARSE_PATHS,
|
|
187
|
-
// the pure sparsePathsForScopeKeys(scopeKeys, moduleScopeMap) cone-filter, and
|
|
188
|
-
// the decideSourceAccess three-way branch. Uses the REAL module-scope-map (a pure
|
|
189
|
-
// core module) as the map the route (R94) will pass in.
|
|
190
|
-
// ===========================================================================
|
|
191
|
-
const scopeMap = require('../src/bongos/module-scope-map.js');
|
|
192
|
-
const BASE = access.BASE_SPARSE_PATHS;
|
|
193
|
-
|
|
194
|
-
console.log('\nBASE_SPARSE_PATHS — the non-product base a task-scoped box layers on:');
|
|
195
|
-
|
|
196
|
-
t('BASE carries the methodology + tooling, NOT the product/game', () => {
|
|
197
|
-
// clients/ is load-bearing: cli-lib.js hard-imports clients/bongos-client, so a
|
|
198
|
-
// cone without it has a fully dead builder CLI on the box (task 1002745).
|
|
199
|
-
// modules/lifecycle is load-bearing for the same reason (task 1003085):
|
|
200
|
-
// scripts/gds/context-pack.js reads the task enums from its task-classifier,
|
|
201
|
-
// and context-pack sits on BOTH /builder-start and /builder-claim. A box with
|
|
202
|
-
// no active claim put nothing in scope, so both commands died at import — and
|
|
203
|
-
// neither could fix it, because a CLAIM is what fetches the module.
|
|
204
|
-
for (const p of ['.devcontainer', '.claude', 'scripts', 'src/bongos', 'docs', 'infra', 'tests', 'clients', 'modules/lifecycle']) {
|
|
205
|
-
assert.ok(BASE.includes(p), `BASE must include ${p}`);
|
|
206
|
-
}
|
|
207
|
-
for (const p of ['src/world', 'src/rooms', 'public', 'art', 'modules/status-ui']) {
|
|
208
|
-
assert.ok(!BASE.includes(p), `BASE must NOT include the product surface ${p}`);
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
console.log('\nsparsePathsForScopeKeys — BASE ∪ the claims’ module DIRS (cone-mode):');
|
|
213
|
-
|
|
214
|
-
t('no keys (empty array) → BASE only, in declared order', () => {
|
|
215
|
-
assert.deepEqual(access.sparsePathsForScopeKeys([], scopeMap), [...BASE]);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
t('one key → BASE + that module’s directory (status-ui → modules/status-ui)', () => {
|
|
219
|
-
const out = access.sparsePathsForScopeKeys(['status-ui'], scopeMap);
|
|
220
|
-
assert.deepEqual(out, [...BASE, 'modules/status-ui']);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
t('cone-filter: dev-box keeps its DIRS, DROPS its file globs (scripts/gds/box.js)', () => {
|
|
224
|
-
const out = access.sparsePathsForScopeKeys(['dev-box'], scopeMap);
|
|
225
|
-
assert.ok(out.includes('modules/dev-box'), 'the module dir is kept');
|
|
226
|
-
assert.ok(out.includes('devbox-app'), 'the app dir is kept');
|
|
227
|
-
assert.ok(!out.some((p) => p.endsWith('.js') || p.endsWith('.sh')), 'no file globs survive the cone filter');
|
|
228
|
-
assert.ok(!out.includes('scripts/gds/box.js'), 'the box CLI file is dropped (BASE scripts covers it)');
|
|
229
|
-
// …and its PARENT is not added either: BASE's `scripts` already covers it, so
|
|
230
|
-
// the ancestor check must stop `scripts/gds` from appearing. This is the half
|
|
231
|
-
// that keeps the file-glob rescue below from over-sharing.
|
|
232
|
-
assert.ok(!out.includes('scripts/gds'), 'a covered parent is not re-emitted');
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// A file glob whose parent is NOT already covered must contribute that parent.
|
|
236
|
-
// Regression for the live failure that released task 1003018 (R05 of goal
|
|
237
|
-
// 1000069): the `government` key's config/government*.json globs were dropped on
|
|
238
|
-
// the reasoning that every file glob sits under a BASE directory — true when
|
|
239
|
-
// written, false once ADR 0174 added them, since `config` is not in BASE. The box
|
|
240
|
-
// could not see two files inside its own scope wall.
|
|
241
|
-
t('file-glob rescue: government contributes `config` for its config/*.json globs', () => {
|
|
242
|
-
const out = access.sparsePathsForScopeKeys(['government'], scopeMap);
|
|
243
|
-
assert.ok(out.includes('modules/government'), 'the module dir is kept');
|
|
244
|
-
assert.ok(out.includes('config'), 'config/government.neutral.json must reach the box via its parent');
|
|
245
|
-
assert.ok(!out.some((p) => p.endsWith('.json')), 'still no file paths — cone mode would reject one');
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
t('file-glob rescue holds for EVERY key in the map — no module can silently lose one', () => {
|
|
249
|
-
// Enumerated, never sampled: a module that gains a file glob under a new parent
|
|
250
|
-
// is covered the day it lands, which is exactly how this bug escaped before.
|
|
251
|
-
for (const key of scopeMap.moduleKeys()) {
|
|
252
|
-
const out = access.sparsePathsForScopeKeys([key], scopeMap);
|
|
253
|
-
for (const glob of scopeMap.scopeForModule(key).globs) {
|
|
254
|
-
if (glob.endsWith('/')) continue;
|
|
255
|
-
const cut = glob.lastIndexOf('/');
|
|
256
|
-
if (cut < 0) continue; // a root file — cone mode includes it automatically
|
|
257
|
-
const parent = glob.slice(0, cut);
|
|
258
|
-
const reachable = out.some((p) => parent === p || parent.startsWith(`${p}/`));
|
|
259
|
-
assert.ok(reachable, `${key}: no cone path materializes ${glob} (parent ${parent})`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
t('the emitted set stays directories only, for every key', () => {
|
|
265
|
-
// A leading dot is a DIRECTORY here ('.devcontainer', '.claude'), so the
|
|
266
|
-
// extension test must require a non-dot character before the dot — otherwise
|
|
267
|
-
// every BASE dotfile-dir reads as a file and this assertion fires on healthy
|
|
268
|
-
// input. (It did, on first write.)
|
|
269
|
-
const looksLikeFile = (seg) => /[^.]\.[a-z0-9]+$/i.test(seg);
|
|
270
|
-
for (const key of scopeMap.moduleKeys()) {
|
|
271
|
-
for (const p of access.sparsePathsForScopeKeys([key], scopeMap)) {
|
|
272
|
-
assert.ok(!looksLikeFile(p.split('/').pop()),
|
|
273
|
-
`${key}: ${p} looks like a file — git sparse-checkout set would abort on it`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
t('cone-filter: the kernel key materializes its real DIR globs (config, src/module-loader)', () => {
|
|
279
|
-
const out = access.sparsePathsForScopeKeys(['kernel'], scopeMap);
|
|
280
|
-
// kernel is mostly individual src/bongos/*.js FILES (dropped — BASE src/bongos
|
|
281
|
-
// covers them via cone ancestor-inclusion) + a few genuine DIR globs.
|
|
282
|
-
assert.ok(out.includes('config'), 'config/ is a real dir the task needs, not in BASE');
|
|
283
|
-
// src/module-loader/ is a real dir not in BASE, but it is NOT emitted verbatim:
|
|
284
|
-
// kernel also carries the file globs src/instance-config.js, src/branding.js,
|
|
285
|
-
// src/modules.js and src/module-api.js, whose rescued parent is `src` — and once
|
|
286
|
-
// `src` is emitted the ancestor check absorbs every path beneath it, this one
|
|
287
|
-
// included. Asserting literal membership would be asserting emission ORDER, not
|
|
288
|
-
// reachability. What the box must actually get is the DIRECTORY's contents, so
|
|
289
|
-
// assert that, using the same idiom as the enumerated rescue test above.
|
|
290
|
-
const reaches = (dir) => out.some((p) => dir === p || dir.startsWith(`${p}/`));
|
|
291
|
-
assert.ok(reaches('src/module-loader'), 'src/module-loader must be materialized (here via the rescued `src`)');
|
|
292
|
-
assert.ok(!out.includes('src/bongos/db.js'), 'the kernel FILE globs are dropped');
|
|
293
|
-
assert.ok(!out.includes('server.js'), 'top-level files never enter the sparse set (cone auto-includes them)');
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
t('N keys → union, deduped + sorted; unknown keys contribute nothing', () => {
|
|
297
|
-
const out = access.sparsePathsForScopeKeys(['status-ui', 'hall-ui'], scopeMap);
|
|
298
|
-
assert.deepEqual(out, [...BASE, 'modules/hall-ui', 'modules/status-ui'], 'union of both dirs, sorted');
|
|
299
|
-
assert.deepEqual(access.sparsePathsForScopeKeys(['status-ui', 'status-ui'], scopeMap), [...BASE, 'modules/status-ui'], 'dedup');
|
|
300
|
-
assert.deepEqual(access.sparsePathsForScopeKeys(['bogus'], scopeMap), [...BASE], 'unknown key → no extra paths');
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
t('missing/!scopeForModule moduleScopeMap → BASE only (guards the missing map)', () => {
|
|
304
|
-
assert.deepEqual(access.sparsePathsForScopeKeys(['dev-box'], null), [...BASE]);
|
|
305
|
-
assert.deepEqual(access.sparsePathsForScopeKeys(['dev-box'], {}), [...BASE]);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
console.log('\ndecideSourceAccess — the ADR 0148 three-way (rank still gates ALLOW/DENY):');
|
|
309
|
-
|
|
310
|
-
t('scopeKeys present + non-empty → TASK scope, BASE ∪ the module dirs (rank no longer caps breadth)', () => {
|
|
311
|
-
const d = access.decideSourceAccess({ rank: 'xenos', status: 'active', scopeKeys: ['status-ui'], moduleScopeMap: scopeMap });
|
|
312
|
-
assert.equal(d.allowed, true);
|
|
313
|
-
assert.equal(d.scope, 'task');
|
|
314
|
-
assert.equal(d.spec.mode, 'sparse');
|
|
315
|
-
assert.deepEqual(d.spec.sparsePaths, [...BASE, 'modules/status-ui'], 'even a Xenos with a claim gets the claim’s module scope');
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
t('scopeKeys present + EMPTY → BASE-only (a live box with no active claim)', () => {
|
|
319
|
-
const d = access.decideSourceAccess({ rank: 'metic', status: 'active', scopeKeys: [], moduleScopeMap: scopeMap });
|
|
320
|
-
assert.equal(d.allowed, true);
|
|
321
|
-
assert.equal(d.scope, 'base');
|
|
322
|
-
assert.deepEqual(d.spec.sparsePaths, [...BASE], 'idle Metic+ now gets BASE-only, not the whole repo (ADR 0148 flagged change)');
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
t('scopeKeys ABSENT → the pre-ADR-0148 RANK fallback is preserved EXACTLY (R93-before-R94 safety)', () => {
|
|
326
|
-
assert.equal(access.decideSourceAccess({ rank: 'metic', status: 'active' }).scope, 'full', 'no claim signal → rank fallback (full)');
|
|
327
|
-
assert.equal(access.decideSourceAccess({ rank: 'xenos', status: 'active' }).scope, 'starter', 'no claim signal → rank fallback (starter)');
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
t('the ALLOW/DENY gates run BEFORE scope selection', () => {
|
|
331
|
-
const inactive = access.decideSourceAccess({ rank: 'metic', status: 'inactive', scopeKeys: ['status-ui'], moduleScopeMap: scopeMap });
|
|
332
|
-
assert.equal(inactive.allowed, false);
|
|
333
|
-
assert.equal(inactive.reason, 'BUILDER_INACTIVE');
|
|
334
|
-
const belowFloor = access.decideSourceAccess({ rank: 'xenos', status: 'active', floor: 'metic', scopeKeys: ['status-ui'], moduleScopeMap: scopeMap });
|
|
335
|
-
assert.equal(belowFloor.allowed, false);
|
|
336
|
-
assert.equal(belowFloor.reason, 'INSUFFICIENT_RANK');
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
console.log('\nscopeMetadata — the credential-FREE projection for GET /box/scope (BV1.R100):');
|
|
340
|
-
|
|
341
|
-
t('task scope → modules deduped+sorted, sparse_paths carried, NO credential fields', () => {
|
|
342
|
-
const scopeKeys = ['dev-box', 'onboarding', 'dev-box']; // dup on purpose
|
|
343
|
-
const decision = access.decideSourceAccess({ rank: 'metic', status: 'active', scopeKeys, moduleScopeMap: scopeMap });
|
|
344
|
-
const meta = access.scopeMetadata({ decision, scopeKeys });
|
|
345
|
-
assert.equal(meta.scope, 'task');
|
|
346
|
-
assert.equal(meta.mode, 'sparse');
|
|
347
|
-
assert.deepEqual(meta.modules, ['dev-box', 'onboarding'], 'deduped + sorted');
|
|
348
|
-
assert.ok(Array.isArray(meta.sparse_paths) && meta.sparse_paths.includes('modules/dev-box'));
|
|
349
|
-
assert.ok(!('credential' in meta) && !('repo_url' in meta), 'never carries credential fields');
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
t('empty claims → base scope, modules is [] (not null) — a live box with no active claim', () => {
|
|
353
|
-
const decision = access.decideSourceAccess({ rank: 'metic', status: 'active', scopeKeys: [], moduleScopeMap: scopeMap });
|
|
354
|
-
const meta = access.scopeMetadata({ decision, scopeKeys: [] });
|
|
355
|
-
assert.equal(meta.scope, 'base');
|
|
356
|
-
assert.deepEqual(meta.modules, []);
|
|
357
|
-
assert.deepEqual(meta.sparse_paths, [...BASE]);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
t('rank fallback (scopeKeys absent) → modules is null (breadth is rank-driven, not per-module)', () => {
|
|
361
|
-
const decision = access.decideSourceAccess({ rank: 'metic', status: 'active' });
|
|
362
|
-
const meta = access.scopeMetadata({ decision, scopeKeys: undefined });
|
|
363
|
-
assert.equal(meta.scope, 'full');
|
|
364
|
-
assert.equal(meta.mode, 'full');
|
|
365
|
-
assert.equal(meta.modules, null);
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
t('a DENIED decision projects to null (the route returns 403, never a scope body)', () => {
|
|
369
|
-
const denied = access.decideSourceAccess({ rank: 'xenos', status: 'inactive', scopeKeys: [], moduleScopeMap: scopeMap });
|
|
370
|
-
assert.equal(denied.allowed, false);
|
|
371
|
-
assert.equal(access.scopeMetadata({ decision: denied, scopeKeys: [] }), null);
|
|
372
|
-
assert.equal(access.scopeMetadata({}), null, 'no decision → null');
|
|
373
|
-
assert.equal(access.scopeMetadata(), null, 'no args → null');
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
// ---------- task 1003227: config/ must be in the box's BASE slice ----------
|
|
377
|
-
//
|
|
378
|
-
// Without it a box cannot run its own quality gate: fitness.js compares against
|
|
379
|
-
// config/fitness-baselines.json, and with the file absent the ratchet used to report
|
|
380
|
-
// PASS (a planted 5,000-line + 94-cycle regression sailed through). So a boxed builder
|
|
381
|
-
// shipped believing they were clean and learned otherwise from red CI, with nothing on
|
|
382
|
-
// the box to reproduce it. Precedent for the same class: ADR 0174 put
|
|
383
|
-
// config/government.json in the `government` globs, config/ was not in BASE, and task
|
|
384
|
-
// 1003018 was RELEASED because its box could never see its own deliverable.
|
|
385
|
-
|
|
386
|
-
t('1003227 BASE includes config/ so a box can read its own fitness baselines', () => {
|
|
387
|
-
const spec = access.decideSourceAccess({
|
|
388
|
-
rank: 'archon', status: 'active', scopeKeys: [], hasActiveClaims: false, moduleScopeMap: {},
|
|
389
|
-
});
|
|
390
|
-
assert.equal(spec.scope, 'base');
|
|
391
|
-
assert.ok(spec.spec.sparsePaths.includes('config'),
|
|
392
|
-
`base scope must carry config/; got ${spec.spec.sparsePaths.join(',')}`);
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
t('1003227 a TASK-scoped box carries config/ too (the common case — a claim is held)', () => {
|
|
396
|
-
// The REAL scope map, not a hand-rolled object — the shape matters here.
|
|
397
|
-
const scopeMap = require('../src/bongos/module-scope-map.js');
|
|
398
|
-
const spec = access.decideSourceAccess({
|
|
399
|
-
rank: 'xenos', status: 'active', scopeKeys: ['economy'], hasActiveClaims: true,
|
|
400
|
-
moduleScopeMap: scopeMap,
|
|
401
|
-
});
|
|
402
|
-
assert.equal(spec.scope, 'task');
|
|
403
|
-
assert.ok(spec.spec.sparsePaths.includes('config'), 'task scope must carry config/ as well');
|
|
404
|
-
assert.ok(spec.spec.sparsePaths.includes('modules/economy'),
|
|
405
|
-
`and still layer the claimed module on top; got ${spec.spec.sparsePaths.join(',')}`);
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
t('1003227 config/ is a cone-mode DIRECTORY entry (a file path aborts the whole set)', () => {
|
|
409
|
-
// ADR 0148: `git sparse-checkout set` rejects a file path, which kills the entire
|
|
410
|
-
// checkout — so every BASE entry must be a bare directory, no trailing slash.
|
|
411
|
-
for (const p of access.BASE_SPARSE_PATHS) {
|
|
412
|
-
assert.ok(!p.endsWith('/'), `${p} must not carry a trailing slash`);
|
|
413
|
-
// A dotted DIRECTORY (.claude, .devcontainer) is fine; what cone mode rejects is
|
|
414
|
-
// a file, so only test for an extension on the FINAL segment of a non-dotfile.
|
|
415
|
-
const last = p.split('/').pop();
|
|
416
|
-
if (!last.startsWith('.')) {
|
|
417
|
-
assert.ok(!/\.[a-z0-9]+$/i.test(last), `${p} looks like a FILE — cone mode would abort the set`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
// ---------------------------------------------------------------------------
|
|
423
|
-
// task 1003087 / idea 1000793 — builder widening that SURVIVES the */10 tick
|
|
424
|
-
//
|
|
425
|
-
// The bug: the cron re-applies the server-computed sparse set, so a builder's
|
|
426
|
-
// own `git sparse-checkout add` is deleted — sometimes mid-command (a live suite
|
|
427
|
-
// died with MODULE_NOT_FOUND that read exactly like a code bug). The widen set is
|
|
428
|
-
// therefore recorded server-side and unioned in HERE, on the same path the cron
|
|
429
|
-
// asks every tick. These pin the two halves that matter: it actually widens, and
|
|
430
|
-
// it cannot be used to climb past the rank scope.
|
|
431
|
-
// ---------------------------------------------------------------------------
|
|
432
|
-
|
|
433
|
-
console.log('\nnormalizeWidenPath — a repo-relative directory, or nothing:');
|
|
434
|
-
|
|
435
|
-
t('accepts a plain repo-relative dir and strips the noise around it', () => {
|
|
436
|
-
assert.equal(access.normalizeWidenPath('docs'), 'docs');
|
|
437
|
-
assert.equal(access.normalizeWidenPath(' docs '), 'docs');
|
|
438
|
-
assert.equal(access.normalizeWidenPath('./docs/'), 'docs');
|
|
439
|
-
assert.equal(access.normalizeWidenPath('docs//adr'), 'docs/adr');
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
t('folds a Windows separator rather than judging the path on it', () => {
|
|
443
|
-
const BS = String.fromCharCode(92);
|
|
444
|
-
assert.equal(access.normalizeWidenPath(`modules${BS}game`), 'modules/game');
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
t('REFUSES traversal, absolute paths and drive letters outright', () => {
|
|
448
|
-
// Refused, never "cleaned up into something that still works" — this string
|
|
449
|
-
// reaches `git sparse-checkout set` on a real box.
|
|
450
|
-
for (const bad of ['../etc', 'a/../../b', '/etc/passwd', 'C:/Windows', '..', '.', '']) {
|
|
451
|
-
assert.equal(access.normalizeWidenPath(bad), null, `${JSON.stringify(bad)} must be refused`);
|
|
452
|
-
}
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
t('refuses a non-string, an over-long path, and odd characters', () => {
|
|
456
|
-
assert.equal(access.normalizeWidenPath(null), null);
|
|
457
|
-
assert.equal(access.normalizeWidenPath(42), null);
|
|
458
|
-
assert.equal(access.normalizeWidenPath({}), null);
|
|
459
|
-
assert.equal(access.normalizeWidenPath('a'.repeat(500)), null);
|
|
460
|
-
assert.equal(access.normalizeWidenPath('modules/ga me'), null);
|
|
461
|
-
assert.equal(access.normalizeWidenPath('modules/rm -rf'), null);
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
console.log('\nadmissibleWidenPaths — rank is the ceiling, and it is the whole point:');
|
|
465
|
-
|
|
466
|
-
t('a Metic (full rank scope) may widen anywhere', () => {
|
|
467
|
-
assert.deepEqual(access.admissibleWidenPaths(['modules/game'], 'metic'), ['modules/game']);
|
|
468
|
-
assert.deepEqual(access.admissibleWidenPaths(['migrations'], 'archon'), ['migrations']);
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
t('a Xenos may NOT widen past the starter surface — that would be rank escalation', () => {
|
|
472
|
-
// The security property of this feature stated as a test: if an arbitrary widen
|
|
473
|
-
// were honored, a convenience flag would hand a starter-scoped builder the whole
|
|
474
|
-
// repo, which is the rank gate defeated rather than a checkout preference.
|
|
475
|
-
assert.deepEqual(access.admissibleWidenPaths(['modules/game'], 'xenos'), []);
|
|
476
|
-
assert.deepEqual(access.admissibleWidenPaths(['migrations'], 'xenos'), []);
|
|
477
|
-
assert.deepEqual(access.admissibleWidenPaths(['modules/lifecycle'], 'thetes'), []);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
t('a Xenos MAY widen INSIDE starter — a task box is narrower than their rank allows', () => {
|
|
481
|
-
// Not a null grant: task scope is BASE union modules, which is narrower than
|
|
482
|
-
// starter, so this is the room to climb back to what rank already permitted.
|
|
483
|
-
assert.deepEqual(access.admissibleWidenPaths(['docs/adr'], 'xenos'), ['docs/adr']);
|
|
484
|
-
assert.deepEqual(access.admissibleWidenPaths(['src/world'], 'xenos'), ['src/world']);
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
t('normalizes, dedupes and sorts, and drops the invalid without failing the rest', () => {
|
|
488
|
-
const got = access.admissibleWidenPaths(['./docs/', 'docs', '../etc', 'art'], 'metic');
|
|
489
|
-
assert.deepEqual(got, ['art', 'docs'], 'one invalid entry must not discard the valid ones');
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
t('is capped, so a widen set can never become a second scope system', () => {
|
|
493
|
-
const many = Array.from({ length: access.MAX_WIDEN_PATHS + 20 }, (_, i) => `docs/d${i}`);
|
|
494
|
-
assert.equal(access.admissibleWidenPaths(many, 'metic').length, access.MAX_WIDEN_PATHS);
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
t('a missing / non-array widen set is simply no widening', () => {
|
|
498
|
-
for (const v of [undefined, null, 'docs', 42, {}]) {
|
|
499
|
-
assert.deepEqual(access.admissibleWidenPaths(v, 'metic'), []);
|
|
500
|
-
}
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
console.log('\nwithWidenPaths — union into the computed set, without redundancy:');
|
|
504
|
-
|
|
505
|
-
t('adds a dir the set does not already cover', () => {
|
|
506
|
-
assert.deepEqual(access.withWidenPaths(['src/bongos'], ['docs']), ['src/bongos', 'docs']);
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
t('skips a dir an ANCESTOR already covers — cone mode pulls the whole subtree', () => {
|
|
510
|
-
assert.deepEqual(access.withWidenPaths(['scripts'], ['scripts/gds']), ['scripts'],
|
|
511
|
-
'adding scripts/gds under scripts is noise in the git command and in the scope readout');
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
t('passes a null sparse set (the full clone) straight through', () => {
|
|
515
|
-
assert.equal(access.withWidenPaths(null, ['docs']), null,
|
|
516
|
-
'a full clone already holds everything — there is nothing to widen INTO');
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
t('an empty widen set returns the computed set untouched', () => {
|
|
520
|
-
const base = ['src/bongos', 'scripts'];
|
|
521
|
-
assert.deepEqual(access.withWidenPaths(base, []), base);
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
console.log('\ndecideSourceAccess — the widen reaches the spec the box actually fetches:');
|
|
525
|
-
|
|
526
|
-
t('a task-scoped box gets its widen unioned into the sparse set', () => {
|
|
527
|
-
const d = access.decideSourceAccess({
|
|
528
|
-
rank: 'metic', status: 'active', scopeKeys: [], hasActiveClaims: false,
|
|
529
|
-
moduleScopeMap: {}, widenPaths: ['modules/game'],
|
|
530
|
-
});
|
|
531
|
-
assert.equal(d.allowed, true);
|
|
532
|
-
assert.ok(d.spec.sparsePaths.includes('modules/game'),
|
|
533
|
-
'the widen must reach the SPEC — this is the value the cron writes into sparse-checkout');
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
t('an out-of-scope widen never reaches the spec for a starter-scoped builder', () => {
|
|
537
|
-
const d = access.decideSourceAccess({
|
|
538
|
-
rank: 'xenos', status: 'active', scopeKeys: [], hasActiveClaims: false,
|
|
539
|
-
moduleScopeMap: {}, widenPaths: ['modules/game'],
|
|
540
|
-
});
|
|
541
|
-
assert.ok(!d.spec.sparsePaths.includes('modules/game'));
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
t('no widenPaths leaves every scope byte-identical to before the feature', () => {
|
|
545
|
-
const withArg = access.decideSourceAccess({
|
|
546
|
-
rank: 'metic', status: 'active', scopeKeys: [], hasActiveClaims: false, moduleScopeMap: {}, widenPaths: [],
|
|
547
|
-
});
|
|
548
|
-
const without = access.decideSourceAccess({
|
|
549
|
-
rank: 'metic', status: 'active', scopeKeys: [], hasActiveClaims: false, moduleScopeMap: {},
|
|
550
|
-
});
|
|
551
|
-
assert.deepEqual(withArg.spec, without.spec, 'a box that asked for nothing must fetch exactly what it always did');
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
t('a full-clone builder is unaffected — sparsePaths stays null', () => {
|
|
555
|
-
const d = access.decideSourceAccess({ rank: 'metic', status: 'active', widenPaths: ['docs'] });
|
|
556
|
-
assert.equal(d.spec.sparsePaths, null);
|
|
557
|
-
assert.equal(d.spec.mode, 'full');
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
t('widening cannot resurrect access for an inactive or below-floor builder', () => {
|
|
561
|
-
// The widen set is read from the box row, so it must not become a way around
|
|
562
|
-
// the ALLOW/DENY gate that runs before any scope is computed.
|
|
563
|
-
const inactive = access.decideSourceAccess({ rank: 'metic', status: 'inactive', widenPaths: ['docs'] });
|
|
564
|
-
assert.equal(inactive.allowed, false);
|
|
565
|
-
assert.equal(inactive.reason, 'BUILDER_INACTIVE');
|
|
566
|
-
assert.equal(inactive.spec, undefined);
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
t('every admitted widen path is still a DIRECTORY — cone mode aborts on a file', () => {
|
|
570
|
-
for (const p of access.admissibleWidenPaths(['docs/adr', 'src/world'], 'metic')) {
|
|
571
|
-
assert.ok(!p.endsWith('/'), `${p} must not carry a trailing slash`);
|
|
572
|
-
const last = p.split('/').pop();
|
|
573
|
-
assert.ok(!/\.[a-z0-9]+$/i.test(last), `${p} looks like a FILE — cone mode would abort the set`);
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
console.log(`\nbox_access.mjs: ${passed} passed, ${failed} failed`);
|
|
578
|
-
process.exit(failed === 0 ? 0 : 1);
|