@bongos/core 1.18.19 → 1.19.3
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 +547 -197
- package/.claude/skills/builder-box/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +5 -5
- package/.claude/skills/scan-before-install/SKILL.md +165 -0
- package/bin/bongos.js +33 -7
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +63 -41
- package/clients/bongos-client/index.cjs +63 -41
- package/clients/bongos-client/index.d.ts +78 -44
- package/clients/bongos-client/index.mjs +63 -41
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +1 -1
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
- package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
- package/docs/adr/0156-user-set-display-name-wins.md +62 -0
- package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
- package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
- package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
- package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
- package/docs/adr/0161-publish-on-merge.md +34 -0
- package/docs/adr/README.md +9 -0
- package/docs/api/openapi.json +1209 -233
- package/docs/api-reference.md +61 -46
- package/docs/architecture.md +1 -1
- package/docs/canonical-permissions.md +61 -16
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +4 -1
- package/docs/onboarding/diagrams/assertions.json +3 -3
- package/docs/recipes/core-release-pipeline.md +181 -0
- package/docs/recipes/packaging-the-core.md +2 -0
- package/docs/recipes/parallel-goal-waves.md +1 -3
- package/docs/recipes/private-npm-distribution.md +6 -2
- package/docs/recipes/upgrading-the-core.md +2 -0
- package/migrations/core_197_display_name_source.sql +56 -0
- package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
- package/migrations/core_205_versions_track_default.sql +28 -0
- package/modules/autonomy/routes/autonomy.js +1 -1
- package/modules/builder-settings/db.js +7 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
- package/modules/dev-box/routes/box.js +11 -9
- package/modules/discord/auth.js +3 -1
- package/modules/discord/routes/discord.js +8 -2
- package/modules/discord/ship-broadcast.js +48 -1
- package/modules/governance/catalog.js +70 -20
- package/modules/governance/db.js +102 -0
- package/modules/governance/migrations/governance_006_version_create.sql +38 -0
- package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
- package/modules/governance/routes/governance.js +235 -6
- package/modules/grading/grader-workers/index.js +18 -9
- package/modules/hall-ui/public/gate.js +6 -2
- package/modules/hall-ui/public/goals-page.js +6 -2
- package/modules/hall-ui/public/governance.css +100 -0
- package/modules/hall-ui/public/governance.html +77 -0
- package/modules/hall-ui/public/governance.js +353 -0
- package/modules/hall-ui/public/harbor.js +14 -8
- package/modules/hall-ui/public/instances-panel.js +3 -1
- package/modules/hall-ui/public/new-project-wizard.js +6 -0
- package/modules/hall-ui/public/people.html +51 -0
- package/modules/hall-ui/public/people.js +100 -0
- package/modules/hall-ui/public/profile.html +2 -1
- package/modules/hall-ui/public/profile.js +9 -3
- package/modules/hall-ui/public/sessions.js +4 -2
- package/modules/hall-ui/public/shell.js +28 -7
- package/modules/hall-ui/public/task-detail.js +6 -3
- package/modules/hall-ui/public/watch.js +17 -9
- package/modules/ideas/blockers.js +44 -7
- package/modules/ideas/routes/blockers.js +12 -7
- package/modules/ideas/routes/inbox.js +2 -2
- package/modules/lifecycle/db.js +84 -17
- package/modules/lifecycle/github-push.js +37 -10
- package/modules/lifecycle/publish-reconciler.js +128 -31
- package/modules/lifecycle/routes/claims.js +10 -4
- package/modules/lifecycle/routes/dependencies.js +2 -2
- package/modules/lifecycle/routes/done-when.js +4 -4
- package/modules/lifecycle/routes/gate-approvals.js +9 -3
- package/modules/lifecycle/routes/goals.js +5 -5
- package/modules/lifecycle/routes/tasks.js +99 -69
- package/modules/lifecycle/routes/versions.js +13 -2
- package/modules/lifecycle/ship-card.js +38 -7
- package/modules/memory/routes/memory.js +2 -2
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/routes/projects.js +10 -6
- package/modules/platform-identity/routes/scouting.js +2 -2
- package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
- package/modules/provisioning/provisioning.js +24 -6
- package/modules/provisioning/routes/provisioning.js +24 -6
- package/modules/security/artifact-scan/aggregate.js +133 -0
- package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
- package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
- package/modules/security/artifact-scan/detectors/credentials.js +112 -0
- package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
- package/modules/security/artifact-scan/detectors/hooks.js +116 -0
- package/modules/security/artifact-scan/detectors/injection.js +118 -0
- package/modules/security/artifact-scan/detectors/mcp.js +102 -0
- package/modules/security/artifact-scan/detectors/network.js +67 -0
- package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
- package/modules/security/artifact-scan/detectors/permissions.js +91 -0
- package/modules/security/artifact-scan/detectors/shared.js +219 -0
- package/modules/security/artifact-scan/fence-local.js +99 -0
- package/modules/security/artifact-scan/fetch.js +330 -0
- package/modules/security/artifact-scan/floor.js +319 -0
- package/modules/security/artifact-scan/limits.js +66 -0
- package/modules/security/artifact-scan/manifest.js +203 -0
- package/modules/security/artifact-scan/mirror.js +158 -0
- package/modules/security/artifact-scan/quarantine.js +149 -0
- package/modules/security/artifact-scan/rebuild-spec.js +120 -0
- package/modules/security/artifact-scan/report.js +155 -0
- package/modules/security/artifact-scan/sanitize.js +317 -0
- package/modules/security/migrations/security_001_tool_scans.sql +145 -0
- package/modules/security/module.json +2 -2
- package/modules/security/routes/reports.js +11 -7
- package/modules/security/routes/scans.js +115 -0
- package/modules/security/scan-store.js +214 -0
- package/modules/security/tests/aggregate.mjs +283 -0
- package/modules/security/tests/artifact_scan.mjs +591 -0
- package/modules/security/tests/floor.mjs +549 -0
- package/modules/security/tests/mirror.mjs +350 -0
- package/modules/security/tests/sanitize_rebuild.mjs +499 -0
- package/modules/security/tests/scan_store.mjs +250 -0
- package/modules/security/tests/tool_scans_migration.mjs +116 -0
- package/modules/sessions/routes/sessions.js +6 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +13 -3
- package/scripts/gds/abandon.js +1 -1
- package/scripts/gds/audit-authorship.sh +6 -0
- package/scripts/gds/box.js +1 -1
- package/scripts/gds/bump-version.js +76 -0
- package/scripts/gds/claim.js +15 -0
- package/scripts/gds/cli-lib.js +16 -4
- package/scripts/gds/gating.js +28 -13
- package/scripts/gds/gen-api-docs.js +1 -1
- package/scripts/gds/goal.js +234 -0
- package/scripts/gds/init.js +77 -15
- package/scripts/gds/main-audit.js +80 -11
- package/scripts/gds/mirror-redact.js +6 -0
- package/scripts/gds/newcomer-floor.js +2 -2
- package/scripts/gds/provision.js +230 -94
- package/scripts/gds/run-unit-tests.js +9 -1
- package/scripts/gds/sandbox-stage.js +66 -9
- package/scripts/gds/search-ingest-md.js +1 -0
- package/scripts/gds/sequence.js +67 -8
- package/scripts/gds/ship.js +183 -50
- package/scripts/hall-preview/server.js +1 -1
- package/scripts/migrate.sh +5 -5
- package/src/bongos/auth.js +129 -27
- package/src/bongos/db-kernel.js +66 -5
- package/src/bongos/db.js +6 -7
- package/src/bongos/github-repos.js +19 -7
- package/src/bongos/goal-scope-check.js +1 -1
- package/src/bongos/module-scope-map.js +3 -1
- package/src/bongos/permission-path-check.js +1 -1
- package/src/bongos/route-rank-check.js +73 -30
- package/src/bongos/routes/audit-log.js +4 -2
- package/src/bongos/routes/auth.js +8 -4
- package/src/bongos/routes/backup.js +7 -4
- package/src/bongos/routes/builders.js +23 -11
- package/src/bongos/routes/github.js +15 -4
- package/src/bongos/routes/llm-cache.js +5 -2
- package/src/bongos/routes/modules.js +9 -5
- package/src/bongos/routes/security.js +21 -12
- package/src/bongos/serve-internal.js +22 -7
- package/src/module-api.js +96 -13
- package/src/platform-server.js +14 -0
- package/tests/api_client.mjs +18 -2
- package/tests/api_path_404.mjs +3 -0
- package/tests/art_stage.mjs +2 -1
- package/tests/atlas.mjs +32 -4
- package/tests/auth_admission_gate.mjs +17 -5
- package/tests/auth_error_brand.mjs +41 -21
- package/tests/auth_resolve_failure.mjs +155 -0
- package/tests/blocker_link.mjs +88 -0
- package/tests/bongos_cli.mjs +91 -0
- package/tests/bongos_downloads.mjs +2 -1
- package/tests/box_dns_repoint.mjs +2 -0
- package/tests/box_ship_permissions.mjs +2 -0
- package/tests/box_terminal_ssrf.mjs +23 -17
- package/tests/bump_version.mjs +71 -0
- package/tests/claim_error_surface.mjs +23 -0
- package/tests/claude_materialize.mjs +4 -0
- package/tests/cli_box_scope.mjs +40 -0
- package/tests/cloud_bongos_pack.mjs +34 -4
- package/tests/conductor.mjs +2 -1
- package/tests/criterion_authoring_gate.mjs +28 -5
- package/tests/currency_label.mjs +10 -1
- package/tests/deploy_instance.mjs +2 -0
- package/tests/devbox_downloads.mjs +2 -1
- package/tests/display_name.mjs +95 -6
- package/tests/eviction_policy.mjs +2 -1
- package/tests/file_map.mjs +10 -0
- package/tests/first_admin_bootstrap.mjs +10 -2
- package/tests/first_run_engine_check.mjs +2 -1
- package/tests/fitness.mjs +26 -6
- package/tests/gate_approvals.mjs +15 -2
- package/tests/github_push_land_proof.mjs +50 -4
- package/tests/github_repos_helpers.mjs +30 -5
- package/tests/github_token_store.mjs +23 -0
- package/tests/goal_abuse_matrix.mjs +13 -3
- package/tests/goal_cli.mjs +89 -0
- package/tests/governance_abuse_matrix.mjs +386 -0
- package/tests/governance_fold.mjs +156 -0
- package/tests/governance_fold_bench.mjs +50 -0
- package/tests/governance_live_proof.mjs +201 -0
- package/tests/governance_page_gates.mjs +21 -9
- package/tests/governance_parity.mjs +11 -2
- package/tests/governance_require_permission.mjs +125 -1
- package/tests/governance_routes.mjs +334 -0
- package/tests/grader.mjs +7 -3
- package/tests/hall_nav.mjs +14 -4
- package/tests/helpers.mjs +73 -0
- package/tests/hook_core_resolve.mjs +2 -1
- package/tests/host_topology_skips.mjs +97 -0
- package/tests/infra_exec_paths.mjs +14 -4
- package/tests/init.mjs +147 -0
- package/tests/known_failing_ratchet.mjs +89 -0
- package/tests/lifecycle-card.mjs +2 -1
- package/tests/main_audit.mjs +72 -1
- package/tests/memory_isolation.mjs +56 -9
- package/tests/migrate_root_resolution.mjs +1 -1
- package/tests/mirror_redact.mjs +19 -0
- package/tests/module_api.mjs +3 -1
- package/tests/module_loader.mjs +4 -0
- package/tests/modules.mjs +35 -7
- package/tests/modules_route.mjs +29 -5
- package/tests/onboarding_observability.mjs +49 -11
- package/tests/onboarding_route_signals.mjs +1 -0
- package/tests/one_active_claim_per_session.mjs +80 -0
- package/tests/patch_gate_ordering.mjs +102 -0
- package/tests/pre_compact.mjs +2 -1
- package/tests/preview_server.mjs +2 -1
- package/tests/profile_route.mjs +1 -0
- package/tests/provision.mjs +170 -0
- package/tests/provisioning_onboard_mode.mjs +180 -0
- package/tests/publish_manifest.mjs +16 -3
- package/tests/publish_reconciler.mjs +181 -10
- package/tests/rank_tier_single_source.mjs +205 -0
- package/tests/recall_hints.mjs +2 -1
- package/tests/repo_map.mjs +4 -0
- package/tests/route_permission_check.mjs +14 -5
- package/tests/rubric_fixtures.mjs +3 -0
- package/tests/run_routine_two_root.mjs +2 -0
- package/tests/sandbox_stage.mjs +69 -7
- package/tests/search_ingest.mjs +7 -0
- package/tests/security_docs.mjs +28 -6
- package/tests/sequence.mjs +64 -0
- package/tests/session-cards.mjs +2 -1
- package/tests/session_start_freshness.mjs +2 -1
- package/tests/ship_broadcast_server.mjs +63 -1
- package/tests/ship_card.mjs +66 -0
- package/tests/ship_ci_deploy.mjs +66 -1
- package/tests/ship_smoke_gate.mjs +76 -0
- package/tests/versions_route.mjs +15 -0
- package/tests/workflow_exec_paths.mjs +16 -4
- package/tests/worktree_guard.mjs +2 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// tests/rank_tier_single_source.mjs — the rank-tier single-source guard (BV1.R108).
|
|
2
|
+
//
|
|
3
|
+
// The canonical rank → tier map is db-kernel.RANK_TIER, DERIVED from
|
|
4
|
+
// LIVE_RANK_LADDER and exposed on the doorway (api.RANK_TIER). This guard makes
|
|
5
|
+
// the collapse hold three ways:
|
|
6
|
+
//
|
|
7
|
+
// 1. IMPORTABLE code holds the SAME OBJECT, not a copy — modules/lifecycle/db.js
|
|
8
|
+
// (the claim-floor brain) and the doorway getter are Object.is-identical to
|
|
9
|
+
// the kernel map.
|
|
10
|
+
// 2. Every DELIBERATELY-LOCAL copy is pinned byte-equivalent. These exist for
|
|
11
|
+
// documented reasons and may not be force-merged (the D1 decision,
|
|
12
|
+
// modules/discord/discord-channels.js): dependency-free substrate
|
|
13
|
+
// (permission-path-check runs in a bare git hook), source-parsed literals
|
|
14
|
+
// (src/bongos/db.js — gate-review.js + diagram-facts.js regex its text),
|
|
15
|
+
// standalone CLIs (sequence, box-connect-lib), the require-free dev-box
|
|
16
|
+
// module template (boxes.js), zero-dep catalog + its text-parsing auditor
|
|
17
|
+
// (route-rank-check), and the hall browser statics.
|
|
18
|
+
// 3. NO NEW parallel map can land: a repo scan flags any rank→number map or
|
|
19
|
+
// rank-ladder array literal outside the allowlist below, both directions —
|
|
20
|
+
// an unlisted literal fails, and a stale allowlist entry fails too.
|
|
21
|
+
//
|
|
22
|
+
// Run: node tests/rank_tier_single_source.mjs
|
|
23
|
+
|
|
24
|
+
import { strict as assert } from 'node:assert';
|
|
25
|
+
import { test } from 'node:test';
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { execFileSync } from 'node:child_process';
|
|
29
|
+
import { createRequire } from 'node:module';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
|
|
32
|
+
const require = createRequire(import.meta.url);
|
|
33
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
34
|
+
const kernel = require(path.join(ROOT, 'src', 'bongos', 'db-kernel.js'));
|
|
35
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
36
|
+
const coreDb = require(path.join(ROOT, 'src', 'bongos', 'db.js'));
|
|
37
|
+
const lifecycleDb = require(path.join(ROOT, 'modules', 'lifecycle', 'db.js'));
|
|
38
|
+
const ppc = require(path.join(ROOT, 'src', 'bongos', 'permission-path-check.js'));
|
|
39
|
+
const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
|
|
40
|
+
const sequence = require(path.join(ROOT, 'scripts', 'gds', 'sequence.js'));
|
|
41
|
+
const boxConnect = require(path.join(ROOT, 'scripts', 'gds', 'box-connect-lib.js'));
|
|
42
|
+
const boxes = require(path.join(ROOT, 'modules', 'dev-box', 'boxes.js'));
|
|
43
|
+
const catalog = require(path.join(ROOT, 'modules', 'governance', 'catalog.js'));
|
|
44
|
+
|
|
45
|
+
const LADDER = kernel.LIVE_RANK_LADDER;
|
|
46
|
+
const TIER = kernel.RANK_TIER;
|
|
47
|
+
const RANK_KEYS = ['xenos', 'thetes', 'metic', 'archon'];
|
|
48
|
+
|
|
49
|
+
// ===========================================================================
|
|
50
|
+
// 1 — the canonical derivation
|
|
51
|
+
// ===========================================================================
|
|
52
|
+
|
|
53
|
+
test('RANK_TIER is derived from LIVE_RANK_LADDER (1-based) and frozen', () => {
|
|
54
|
+
assert.deepEqual(LADDER, RANK_KEYS, 'the live ladder itself changed — update this guard deliberately');
|
|
55
|
+
assert.ok(Object.isFrozen(TIER));
|
|
56
|
+
assert.deepEqual(TIER, Object.fromEntries(LADDER.map((r, i) => [r, i + 1])));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('the doorway and the lifecycle brain hold the SAME object, not copies', () => {
|
|
60
|
+
assert.ok(Object.is(api.RANK_TIER, TIER), 'api.RANK_TIER must be the kernel map itself');
|
|
61
|
+
assert.ok(Object.is(api.LIVE_RANK_LADDER, LADDER));
|
|
62
|
+
assert.ok(Object.is(lifecycleDb.REQUIRES_RANK_TIER, TIER),
|
|
63
|
+
'modules/lifecycle/db.js must derive from the doorway, never hand-copy');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ===========================================================================
|
|
67
|
+
// 2 — every deliberately-local copy is pinned
|
|
68
|
+
// ===========================================================================
|
|
69
|
+
|
|
70
|
+
test('src/bongos/db.js literal (the source-parsed brain) matches the kernel map', () => {
|
|
71
|
+
assert.deepEqual(coreDb.REQUIRES_RANK_TIER, TIER);
|
|
72
|
+
// The literal must stay PARSEABLE TEXT — gate-review.js's root-of-trust region
|
|
73
|
+
// watch and diagram-facts.js both regex it out of the source.
|
|
74
|
+
const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'db.js'), 'utf8');
|
|
75
|
+
const m = /REQUIRES_RANK_TIER\s*=\s*\{([^}]*)\}/.exec(src);
|
|
76
|
+
assert.ok(m, 'db.js must keep an inline REQUIRES_RANK_TIER object literal');
|
|
77
|
+
assert.deepEqual(parsePairs(m[1]), TIER, 'the db.js literal text drifted from the kernel map');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('1-based local copies match: permission-path-check, sequence CLI', () => {
|
|
81
|
+
assert.deepEqual(ppc.RANK_TIER, TIER, 'permission-path-check drifted — the #863 failure class');
|
|
82
|
+
assert.deepEqual(sequence.RANK_TIER, TIER, 'sequence.js CLI copy drifted');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('0-based local copies match: box-connect-lib CLI, dev-box boxes.js', () => {
|
|
86
|
+
for (const [name, map] of [['box-connect-lib', boxConnect.RANK_ORDER], ['boxes', boxes.RANK_ORDER]]) {
|
|
87
|
+
assert.deepEqual(Object.keys(map).sort(), [...RANK_KEYS].sort(), `${name} keys drifted`);
|
|
88
|
+
for (const r of RANK_KEYS) {
|
|
89
|
+
assert.equal(map[r], TIER[r] - 1, `${name}.RANK_ORDER.${r} must be tier-1 (0-based)`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('ladder mirrors match: governance catalog, route-rank-check', () => {
|
|
95
|
+
assert.deepEqual([...catalog.RANK_ORDER], [...LADDER]);
|
|
96
|
+
assert.deepEqual([...rrc.PERMISSION_RANK_ORDER], [...LADDER]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Files that cannot require server code (browser statics, dependency-free
|
|
100
|
+
// module CLIs) — their literal must be EXACTLY the canonical form. The loose
|
|
101
|
+
// scanner below flags any rank-ish literal in these files; this exact check is
|
|
102
|
+
// what turns "flagged but allowlisted" into "provably not drifted".
|
|
103
|
+
const EXACT_LADDER_RE = /\[\s*'xenos',\s*'thetes',\s*'metic',\s*'archon'\s*\]/;
|
|
104
|
+
const STATIC_LADDER_FILES = [
|
|
105
|
+
'modules/discord/discord-channels.js',
|
|
106
|
+
'modules/memory/search.js',
|
|
107
|
+
'modules/hall-ui/public/ranks.js',
|
|
108
|
+
'modules/hall-ui/public/profile.js',
|
|
109
|
+
'modules/hall-ui/public/watch.js',
|
|
110
|
+
];
|
|
111
|
+
test('static ladder copies carry the exact canonical ladder', () => {
|
|
112
|
+
for (const rel of STATIC_LADDER_FILES) {
|
|
113
|
+
const src = fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
114
|
+
assert.ok(EXACT_LADDER_RE.test(src), `${rel}: ladder literal missing or drifted from canonical order`);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const STATIC_MAP_FILES = ['modules/hall-ui/public/builders.js', 'modules/hall-ui/public/goals-page.js'];
|
|
119
|
+
test('hall static tier maps match the kernel map', () => {
|
|
120
|
+
for (const rel of STATIC_MAP_FILES) {
|
|
121
|
+
const src = fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
122
|
+
const maps = extractRankMaps(src);
|
|
123
|
+
assert.ok(maps.length > 0, `${rel}: expected a rank tier map`);
|
|
124
|
+
for (const map of maps) assert.deepEqual(map, TIER, `${rel}: tier map drifted`);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ===========================================================================
|
|
129
|
+
// 3 — the reintroduction scanner (both directions vs the allowlist)
|
|
130
|
+
// ===========================================================================
|
|
131
|
+
|
|
132
|
+
// Where a rank→number map literal is allowed to exist (each is pinned above).
|
|
133
|
+
const MAP_ALLOWLIST = new Set([
|
|
134
|
+
'src/bongos/db.js',
|
|
135
|
+
'src/bongos/permission-path-check.js',
|
|
136
|
+
'scripts/gds/sequence.js',
|
|
137
|
+
'scripts/gds/box-connect-lib.js',
|
|
138
|
+
'modules/dev-box/boxes.js',
|
|
139
|
+
'modules/hall-ui/public/builders.js',
|
|
140
|
+
'modules/hall-ui/public/goals-page.js',
|
|
141
|
+
]);
|
|
142
|
+
// Where a rank-ladder array literal is allowed to exist.
|
|
143
|
+
const LADDER_ALLOWLIST = new Set([
|
|
144
|
+
'src/bongos/db-kernel.js', // the canonical source itself
|
|
145
|
+
'modules/governance/catalog.js',
|
|
146
|
+
'src/bongos/route-rank-check.js',
|
|
147
|
+
...STATIC_LADDER_FILES,
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
// A rank→digit pair inside an object block. Hex values (RANK_COLORS) never
|
|
151
|
+
// match: \d+ must run straight into , or }.
|
|
152
|
+
const pairRe = (rank) => new RegExp(`(?:^|[{,\\s'"])${rank}['"]?\\s*:\\s*\\d+\\s*[,}]`);
|
|
153
|
+
// ≥3 rank keys with digit values in one object literal = a tier map — including
|
|
154
|
+
// the drifted 3-key shape that caused #863 (a rank silently missing).
|
|
155
|
+
function hasRankMap(src) {
|
|
156
|
+
for (const m of src.matchAll(/\{[^{}]*\}/g)) {
|
|
157
|
+
if (RANK_KEYS.filter((r) => pairRe(r).test(m[0])).length >= 3) return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
function parsePairs(inner) {
|
|
162
|
+
const out = {};
|
|
163
|
+
for (const m of inner.matchAll(/([a-z]+)['"]?\s*:\s*(\d+)/g)) out[m[1]] = Number(m[2]);
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
function extractRankMaps(src) {
|
|
167
|
+
const out = [];
|
|
168
|
+
for (const m of src.matchAll(/\{[^{}]*\}/g)) {
|
|
169
|
+
if (RANK_KEYS.filter((r) => pairRe(r).test(m[0])).length >= 3) {
|
|
170
|
+
out.push(parsePairs(m[0].slice(1, -1)));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
// ≥3 CONSECUTIVE live-rank strings in one array literal = a ladder copy.
|
|
176
|
+
const LOOSE_LADDER_RE = /\[\s*(?:['"](?:xenos|thetes|metic|archon)['"]\s*,?\s*){3,}\]/;
|
|
177
|
+
|
|
178
|
+
test('no rank map or ladder literal exists outside the allowlist (and none is stale)', () => {
|
|
179
|
+
const tracked = execFileSync('git', ['-C', ROOT, 'ls-files', 'src', 'modules', 'scripts'], { encoding: 'utf8' })
|
|
180
|
+
.split('\n').map((l) => l.trim())
|
|
181
|
+
.filter((f) => f.endsWith('.js') && !f.includes('/tests/') && !f.endsWith('.test.js'));
|
|
182
|
+
assert.ok(tracked.length > 50, `suspiciously few tracked files (${tracked.length}) — scanner broken?`);
|
|
183
|
+
|
|
184
|
+
const mapHits = new Set();
|
|
185
|
+
const ladderHits = new Set();
|
|
186
|
+
for (const rel of tracked) {
|
|
187
|
+
const src = fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
188
|
+
if (hasRankMap(src)) mapHits.add(rel);
|
|
189
|
+
if (LOOSE_LADDER_RE.test(src)) ladderHits.add(rel);
|
|
190
|
+
}
|
|
191
|
+
for (const f of mapHits) {
|
|
192
|
+
assert.ok(MAP_ALLOWLIST.has(f),
|
|
193
|
+
`${f} declares a rank→tier map literal. Derive it from api.RANK_TIER / db-kernel.RANK_TIER instead — or, if it genuinely cannot require server code, add it here WITH a pinning assertion above.`);
|
|
194
|
+
}
|
|
195
|
+
for (const f of ladderHits) {
|
|
196
|
+
assert.ok(LADDER_ALLOWLIST.has(f),
|
|
197
|
+
`${f} declares a rank-ladder array literal. Derive it from api.LIVE_RANK_LADDER instead — or, if it genuinely cannot require server code, add it here WITH a pinning assertion above.`);
|
|
198
|
+
}
|
|
199
|
+
for (const f of MAP_ALLOWLIST) {
|
|
200
|
+
assert.ok(mapHits.has(f), `allowlist entry ${f} no longer contains a rank map — prune it`);
|
|
201
|
+
}
|
|
202
|
+
for (const f of LADDER_ALLOWLIST) {
|
|
203
|
+
assert.ok(ladderHits.has(f), `allowlist entry ${f} no longer contains a ladder literal — prune it`);
|
|
204
|
+
}
|
|
205
|
+
});
|
package/tests/recall_hints.mjs
CHANGED
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
|
|
11
11
|
import { strict as assert } from 'node:assert';
|
|
12
12
|
import { createRequire } from 'node:module';
|
|
13
|
+
import { requireHostModule } from './helpers.mjs';
|
|
13
14
|
|
|
14
15
|
const require = createRequire(import.meta.url);
|
|
15
16
|
const { taskQuery, pickActiveClaim, formatHints, DEADLINE_MS } =
|
|
16
|
-
require
|
|
17
|
+
requireHostModule(require, '../.claude/hooks/recall-hints.js');
|
|
17
18
|
|
|
18
19
|
let passed = 0;
|
|
19
20
|
let failed = 0;
|
package/tests/repo_map.mjs
CHANGED
|
@@ -71,6 +71,10 @@ test('each nested CLAUDE.md carries a delimited generated block with signatures'
|
|
|
71
71
|
const doc = readFileSync(path.join(ROOT, dir, 'CLAUDE.md'), 'utf8');
|
|
72
72
|
assert.ok(doc.includes(BEGIN), `${dir}/CLAUDE.md should have a generated-block start marker`);
|
|
73
73
|
assert.ok(doc.includes(END), `${dir}/CLAUDE.md should have a generated-block end marker`);
|
|
74
|
+
// In a PUBLISHED core the nested agent notes are replaced by package-core.js's
|
|
75
|
+
// CORE_AGENT_DOC_STUB, so there is no hand-written prose to preserve (task 1002470).
|
|
76
|
+
// Where the real doc is present the prose must survive the generated-block injection.
|
|
77
|
+
if (doc.includes('@bongos/core — installed platform dependency')) continue;
|
|
74
78
|
// the hand-written prose is preserved alongside the generated block
|
|
75
79
|
assert.match(doc, /Nested CLAUDE\.md — loads on demand/);
|
|
76
80
|
}
|
|
@@ -41,12 +41,16 @@ const rowOf = (src) => rrc.classifyText(src)[0];
|
|
|
41
41
|
// here ONCE by hand. This is the independent side of every classification assertion
|
|
42
42
|
// below — if someone lowers `backup.manage` to 'xenos', the classifier follows the
|
|
43
43
|
// catalog and this file goes red, which is the whole point.
|
|
44
|
+
// ADR 0157 moved 20 atoms archon→metic. `audit_log.read` and `builder.roster.read` are
|
|
45
|
+
// transcribed at their new floor below; the archon entries that remain are the
|
|
46
|
+
// rank/identity core, which is the point of that ADR.
|
|
44
47
|
const HAND_WRITTEN_FLOORS = {
|
|
45
48
|
'builder.rank.set': 'archon',
|
|
46
|
-
'audit_log.read': 'archon',
|
|
47
49
|
'auth.session.revoke': 'archon',
|
|
48
|
-
'builder.roster.read': 'archon',
|
|
49
50
|
'builder.status.set': 'archon',
|
|
51
|
+
'access_request.review': 'archon',
|
|
52
|
+
'audit_log.read': 'metic',
|
|
53
|
+
'builder.roster.read': 'metic',
|
|
50
54
|
'backup.manage': 'metic',
|
|
51
55
|
'idea.triage': 'metic',
|
|
52
56
|
'security.report.file': 'metic',
|
|
@@ -132,13 +136,17 @@ test('P1: the label agrees with the REAL threshold function over the whole rank
|
|
|
132
136
|
});
|
|
133
137
|
|
|
134
138
|
test('P1: AND semantics — a multi-atom gate takes the STRICTEST floor', () => {
|
|
135
|
-
|
|
139
|
+
// Uses `builder.rank.set` as the archon end of the pair: ADR 0157 lowered
|
|
140
|
+
// `audit_log.read` (the previous example) to metic, which would no longer exercise the
|
|
141
|
+
// metic-vs-archon spread this test exists to prove. Rank-setting is the one power that
|
|
142
|
+
// ADR 0157 guarantees stays archon, so it is the stable choice here.
|
|
143
|
+
const src = "router.post('/x', auth.requireBuilder, auth.requirePermission('idea.file', 'builder.rank.set'), h);";
|
|
136
144
|
assert.equal(rankOf(src), 'archon',
|
|
137
145
|
'requirePermission ANDs, so the gate is as strict as its strictest atom (archon), not its loosest');
|
|
138
146
|
const src2 = "router.post('/x', auth.requireBuilder, auth.requirePermission('cost.log', 'backup.manage'), h);";
|
|
139
147
|
assert.equal(rankOf(src2), 'metic+archon');
|
|
140
148
|
// order must not matter
|
|
141
|
-
const src3 = "router.post('/x', auth.requireBuilder, auth.requirePermission('
|
|
149
|
+
const src3 = "router.post('/x', auth.requireBuilder, auth.requirePermission('builder.rank.set', 'idea.file'), h);";
|
|
142
150
|
assert.equal(rankOf(src3), 'archon');
|
|
143
151
|
});
|
|
144
152
|
|
|
@@ -274,7 +282,8 @@ test('P4: gen-api-docs labels a permission-gated route by its floor, not any-bui
|
|
|
274
282
|
const trigger = model.find((r) => r.route === '/backup/trigger');
|
|
275
283
|
const audit = model.find((r) => r.route === '/audit-log');
|
|
276
284
|
assert.equal(trigger.rank, 'metic+archon', 'the published rank must be the real floor');
|
|
277
|
-
|
|
285
|
+
// ADR 0157: `audit_log.read` floor archon→metic, so the published rank follows.
|
|
286
|
+
assert.equal(audit.rank, 'metic+archon');
|
|
278
287
|
assert.deepEqual(trigger.perms, ['backup.manage'], 'the docs also name the authority atom');
|
|
279
288
|
|
|
280
289
|
const spec = genApiDocs.buildOpenapi(model);
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { strict as assert } from 'node:assert';
|
|
24
24
|
import { readFileSync, existsSync } from 'node:fs';
|
|
25
25
|
import { fileURLToPath } from 'node:url';
|
|
26
|
+
import { skipUnlessPresent } from './helpers.mjs';
|
|
26
27
|
import { dirname, resolve } from 'node:path';
|
|
27
28
|
|
|
28
29
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -30,6 +31,8 @@ const ROOT = resolve(HERE, '..');
|
|
|
30
31
|
const FIXTURES_DIR = resolve(ROOT, 'modules', 'art-pipeline', 'template', 'rubric_fixtures');
|
|
31
32
|
const MANIFEST = resolve(FIXTURES_DIR, 'fixtures.json');
|
|
32
33
|
const RUBRIC = resolve(ROOT, 'modules', 'art-pipeline', 'template', 'rubric.json');
|
|
34
|
+
skipUnlessPresent('modules/art-pipeline/template/rubric_fixtures',
|
|
35
|
+
'The art-pipeline module is not installed in this instance.');
|
|
33
36
|
|
|
34
37
|
let passed = 0;
|
|
35
38
|
function test(name, fn) {
|
|
@@ -19,11 +19,13 @@ import fs from 'node:fs';
|
|
|
19
19
|
import os from 'node:os';
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { skipUnlessPresent } from './helpers.mjs';
|
|
22
23
|
|
|
23
24
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
25
|
const REPO = path.resolve(__dirname, '..'); // the core root (this repo), fixed by resolveCoreRoot()
|
|
25
26
|
const RUN_ROUTINE = path.join(REPO, 'scripts', 'gds', 'run-routine.js');
|
|
26
27
|
const CORE_RESOLVE = path.join(REPO, '.claude', 'hooks', '_core-resolve.js');
|
|
28
|
+
skipUnlessPresent('.claude/hooks/_core-resolve.js');
|
|
27
29
|
|
|
28
30
|
let passed = 0, failed = 0;
|
|
29
31
|
function test(name, fn) {
|
package/tests/sandbox_stage.mjs
CHANGED
|
@@ -240,7 +240,10 @@ function boxDeps(markerFile, overrides = {}) {
|
|
|
240
240
|
if (cmd === 'git') return execFileSync(cmd, args, opts);
|
|
241
241
|
return '';
|
|
242
242
|
},
|
|
243
|
-
|
|
243
|
+
// task 1002607: emulate a real PREVIEW — 200 on the health URL, 404 on the
|
|
244
|
+
// /api/gds/healthz discriminator (the game-only entry mounts no /api/gds).
|
|
245
|
+
// A flat always-200 mock would now read as "the MAIN server answered".
|
|
246
|
+
fetchImpl: async (url) => ({ status: String(url).includes('/api/gds/healthz') ? 404 : 200 }),
|
|
244
247
|
markerPath: markerFile,
|
|
245
248
|
cwd: tmp,
|
|
246
249
|
pollMs: 1,
|
|
@@ -346,11 +349,19 @@ await ta('runShipGate: --skip-stage → ok, untouched', async () => {
|
|
|
346
349
|
assert.equal(fs.existsSync(mp), false);
|
|
347
350
|
});
|
|
348
351
|
|
|
349
|
-
|
|
352
|
+
// task 1002607 (§4.4 station 9): a staging failure used to WARN one stderr line
|
|
353
|
+
// and return ok — the review gate silently turned itself off. It now fails
|
|
354
|
+
// CLOSED (like the smoke gate, task 1002605): abort, claim intact, --skip-stage
|
|
355
|
+
// as the explicit bypass, and the reason says plainly the review DID NOT run.
|
|
356
|
+
await ta('1002607 runShipGate: staging failure (health never green) ABORTS — review gate cannot silently turn itself off', async () => {
|
|
350
357
|
const mp = path.join(markerDir, 'm8.json');
|
|
351
358
|
const deps = boxDeps(mp, { fetchImpl: async () => ({ status: 502 }), timeoutMs: 10 });
|
|
352
359
|
const r = await sbx.runShipGate({ taskId: 927, files: ['public/game/a.js'], skipStage: false, tty: true }, deps);
|
|
353
|
-
assert.equal(r.ok,
|
|
360
|
+
assert.equal(r.ok, false, 'a failed stage must not ship as if the review happened');
|
|
361
|
+
assert.equal(r.review, undefined, 'a failure to present is NOT a calm review pause');
|
|
362
|
+
assert.match(r.reason, /DID NOT run/, 'says plainly the review step did not run');
|
|
363
|
+
assert.match(r.reason, /--skip-stage/, 'names the explicit bypass');
|
|
364
|
+
assert.match(r.reason, /claim is still active/i, 'claim-preserving');
|
|
354
365
|
});
|
|
355
366
|
|
|
356
367
|
// ---------- recorded self-approval (#1048), box ----------
|
|
@@ -502,12 +513,62 @@ await ta('stage(box): a HARD verify failure → {ok:false,hard:true}, no marker'
|
|
|
502
513
|
assert.equal(fs.existsSync(mp), false, 'no marker on a hard verify failure');
|
|
503
514
|
});
|
|
504
515
|
|
|
505
|
-
await ta('runShipGate(box): a SOFT failure (preview never healthy)
|
|
516
|
+
await ta('1002607 runShipGate(box): a SOFT failure (preview never healthy) now fails closed — abort, claim intact', async () => {
|
|
506
517
|
const mp = path.join(markerDir, 'soft1.json');
|
|
507
|
-
// sync ok (injected), but health never green →
|
|
518
|
+
// sync ok (injected), but health never green → the ship must NOT continue
|
|
519
|
+
// as if the review ran (that was the silent-disable defect).
|
|
508
520
|
const deps = boxDeps(mp, { fetchImpl: async () => ({ status: 502 }), timeoutMs: 10 });
|
|
509
521
|
const r = await sbx.runShipGate({ taskId: 1068, files: ['public/game/a.js'], skipStage: false, tty: true }, deps);
|
|
510
|
-
assert.equal(r.ok,
|
|
522
|
+
assert.equal(r.ok, false, 'the soft failure aborts instead of silently skipping the review');
|
|
523
|
+
assert.match(r.reason, /sandbox staging failed/);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// ---------- task 1002607: waitHealthy truth table ----------
|
|
527
|
+
|
|
528
|
+
await ta('1002607 waitHealthy: 404 at the health URL is NOT healthy (was: anything < 500 passed)', async () => {
|
|
529
|
+
const r = await sbx.waitHealthy({ fetchImpl: async () => ({ status: 404 }), timeoutMs: 10, pollMs: 1 });
|
|
530
|
+
assert.equal(r.ok, false);
|
|
531
|
+
assert.match(r.reason, /HTTP 404/);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
await ta('1002607 waitHealthy: a 200 answered by the MAIN server (api healthz 200) is an impostor, not a healthy preview', async () => {
|
|
535
|
+
// The full server binds the same 127.0.0.1:3000 — root answers 200, and so does
|
|
536
|
+
// /api/gds/healthz. The game-only preview (ADR 0052) would 404 the latter.
|
|
537
|
+
const r = await sbx.waitHealthy({ fetchImpl: async () => ({ status: 200 }), timeoutMs: 1000, pollMs: 1 });
|
|
538
|
+
assert.equal(r.ok, false, 'a dead preview must not report healthy off the main server');
|
|
539
|
+
assert.match(r.reason, /MAIN server/);
|
|
540
|
+
assert.match(r.reason, /preview did not start/);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
await ta('1002607 waitHealthy: a real preview (root 200, api healthz 404) is healthy', async () => {
|
|
544
|
+
const r = await sbx.waitHealthy({
|
|
545
|
+
fetchImpl: async (url) => ({ status: String(url).includes('/api/gds/healthz') ? 404 : 200 }),
|
|
546
|
+
timeoutMs: 1000, pollMs: 1,
|
|
547
|
+
});
|
|
548
|
+
assert.equal(r.ok, true);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
await ta('1002607 waitHealthy: a discriminator probe that ERRORS counts as "not the main server" (preview healthy)', async () => {
|
|
552
|
+
const r = await sbx.waitHealthy({
|
|
553
|
+
fetchImpl: async (url) => {
|
|
554
|
+
if (String(url).includes('/api/gds/healthz')) throw new Error('ECONNREFUSED on a path the preview never mounts');
|
|
555
|
+
return { status: 200 };
|
|
556
|
+
},
|
|
557
|
+
timeoutMs: 1000, pollMs: 1,
|
|
558
|
+
});
|
|
559
|
+
assert.equal(r.ok, true, 'only a definite 200 on api healthz marks the impostor');
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
t('1002607 apiHealthzProbeUrl: same origin as the health URL, api path; null on garbage', () => {
|
|
563
|
+
assert.equal(sbx.apiHealthzProbeUrl('http://127.0.0.1:3000/'), 'http://127.0.0.1:3000/api/gds/healthz');
|
|
564
|
+
assert.equal(sbx.apiHealthzProbeUrl('http://127.0.0.1:3100/healthz'), 'http://127.0.0.1:3100/api/gds/healthz');
|
|
565
|
+
assert.equal(sbx.apiHealthzProbeUrl('not a url'), null);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
t('1002607 NO_SANDBOX_MSG: names what is actually missing, not the builder\'s clone', () => {
|
|
569
|
+
assert.match(sbx.NO_SANDBOX_MSG, /src\/preview-server\.js/, 'names the missing entry file');
|
|
570
|
+
assert.match(sbx.NO_SANDBOX_MSG, /ADR 0052/, 'points at the contract that defines it');
|
|
571
|
+
assert.doesNotMatch(sbx.NO_SANDBOX_MSG, /this clone has no game preview entry/, 'the old your-clone-is-at-fault phrasing is gone');
|
|
511
572
|
});
|
|
512
573
|
|
|
513
574
|
// ---------- runShipGate: LOCAL preview context + recorded approval (task 1056) ----------
|
|
@@ -529,7 +590,8 @@ function localDeps(markerFile, overrides = {}) {
|
|
|
529
590
|
writeFileSync: (p, d) => fs.writeFileSync(p, d),
|
|
530
591
|
},
|
|
531
592
|
execFileSync: (cmd, args, opts) => (cmd === 'git' ? execFileSync(cmd, args, opts) : ''),
|
|
532
|
-
|
|
593
|
+
// task 1002607: preview-shaped mock — health 200, API-healthz discriminator 404.
|
|
594
|
+
fetchImpl: async (url) => ({ status: String(url).includes('/api/gds/healthz') ? 404 : 200 }),
|
|
533
595
|
markerPath: markerFile,
|
|
534
596
|
cwd: tmp,
|
|
535
597
|
pollMs: 1,
|
package/tests/search_ingest.mjs
CHANGED
|
@@ -30,6 +30,13 @@ test('metic-tagging: read-sensitive doc surfaces → metic', () => {
|
|
|
30
30
|
assert.equal(minRankForDoc('docs/audits/2026-06-08-server-security-audit.md'), 'metic');
|
|
31
31
|
assert.equal(minRankForDoc('docs/adr/0016-trust-boundary-server-enforced-permissions.md'), 'metic');
|
|
32
32
|
assert.equal(minRankForDoc('docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md'), 'metic');
|
|
33
|
+
// task 1002598: docs/governance/ holds the complete gate enumeration (the authority
|
|
34
|
+
// sweep) and the choke-point map, which catalogue live fail-open paths by file:line.
|
|
35
|
+
// Served at xenos they were a searchable exploitation index for the population they
|
|
36
|
+
// describe; the read floor matches security.report.adjudicate, where ADR 0158 §6
|
|
37
|
+
// sends the same class of detail.
|
|
38
|
+
assert.equal(minRankForDoc('docs/governance/rank-choke-point-map-2026-08-06.md'), 'metic');
|
|
39
|
+
assert.equal(minRankForDoc('docs/governance/authority-sweep-2026-07-24.md'), 'metic');
|
|
33
40
|
});
|
|
34
41
|
|
|
35
42
|
test('metic-tagging: ordinary docs → xenos (read-public)', () => {
|
package/tests/security_docs.mjs
CHANGED
|
@@ -18,8 +18,11 @@
|
|
|
18
18
|
// - the slug character-class regex rejects non-kebab slugs
|
|
19
19
|
//
|
|
20
20
|
// Rank wiring (source assertion — mirrors tests/cli_token_reissue.mjs):
|
|
21
|
-
// - all three /security/docs* routes
|
|
22
|
-
//
|
|
21
|
+
// - all three /security/docs* routes gate on the atom
|
|
22
|
+
// auth.requirePermission('security.report.adjudicate') (BV1.R104; was
|
|
23
|
+
// requireRank('archon') and admitted exactly the same builders, ADR 0151)
|
|
24
|
+
// - that atom's floor moved archon→metic in ADR 0157, so the routes now classify
|
|
25
|
+
// 'metic+archon' and are pinned BY ATOM (`perm:…`) rather than by rank
|
|
23
26
|
//
|
|
24
27
|
// We stub the pool by pre-seeding the require cache for src/db.js so the db
|
|
25
28
|
// module under test never opens a socket — the same monkeypatch seam the repo
|
|
@@ -167,18 +170,37 @@ await test('slug character-class regex rejects non-kebab slugs', () => {
|
|
|
167
170
|
|
|
168
171
|
console.log('\nsecurity_docs — rank wiring (source + pin):');
|
|
169
172
|
|
|
170
|
-
await test('all three /security/docs* routes
|
|
173
|
+
await test('all three /security/docs* routes gate on security.report.adjudicate (metic floor)', () => {
|
|
171
174
|
const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'routes', 'security.js'), 'utf8');
|
|
172
175
|
const routes = rrc.classifyText(src).filter((r) => r.route.startsWith('/security/docs'));
|
|
173
176
|
assert.equal(routes.length, 3, 'expected GET list, GET :slug, POST');
|
|
174
177
|
for (const r of routes) {
|
|
175
|
-
|
|
178
|
+
// The ATOM is the authority once governance grows custom roles; the rank is what
|
|
179
|
+
// that atom's catalog floor resolves to today. Assert both, so neither a swapped
|
|
180
|
+
// atom nor an UNINTENDED floor edit can quietly widen who reads the exploit cookbook.
|
|
181
|
+
//
|
|
182
|
+
// ADR 0157 made that widening INTENTIONALLY: `security.report.adjudicate` moved
|
|
183
|
+
// archon→metic with the rest of the operational block, so the engagement-report
|
|
184
|
+
// store is now readable by the working rank. That is the one place in the ADR where
|
|
185
|
+
// delegation carries a genuine CONFIDENTIALITY cost rather than just an
|
|
186
|
+
// administrative one — every other delegated atom widens who may *act*, this one
|
|
187
|
+
// widens who may *read attacker-usable detail*. If that trade is unwanted, the fix
|
|
188
|
+
// is to split the atom the way ADR 0157 §4 split `gate_approval.read` off
|
|
189
|
+
// `gate_approval.decide`: keep a `security.docs.read` at archon for the cookbook and
|
|
190
|
+
// let report adjudication delegate. Recorded here so the next reader sees the choice
|
|
191
|
+
// rather than inferring it from a floor value.
|
|
192
|
+
assert.deepEqual(r.perms, ['security.report.adjudicate'],
|
|
193
|
+
`${r.key} must gate on the adjudicate atom (serves exploit detail)`);
|
|
194
|
+
assert.equal(r.rank, 'metic+archon', `${r.key} must classify as metic+archon (ADR 0157)`);
|
|
176
195
|
}
|
|
177
196
|
});
|
|
178
197
|
|
|
179
|
-
await test('route-rank-check pins all three /security/docs* routes
|
|
198
|
+
await test('route-rank-check pins all three /security/docs* routes by atom', () => {
|
|
199
|
+
// Re-pinned from the rank 'archon' to the ATOM in ADR 0157: the invariant worth
|
|
200
|
+
// guarding is "these gate on the adjudicate permission", which survives a floor move.
|
|
180
201
|
for (const key of ['GET /security/docs', 'GET /security/docs/:slug', 'POST /security/docs']) {
|
|
181
|
-
assert.equal(rrc.EXPECTED_RANKS[key], '
|
|
202
|
+
assert.equal(rrc.EXPECTED_RANKS[key], 'perm:security.report.adjudicate',
|
|
203
|
+
`${key} must be pinned to the adjudicate atom in EXPECTED_RANKS`);
|
|
182
204
|
}
|
|
183
205
|
});
|
|
184
206
|
|
package/tests/sequence.mjs
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
topoOrder,
|
|
14
14
|
chainFrom,
|
|
15
15
|
haltReasons,
|
|
16
|
+
pickNext,
|
|
16
17
|
summarize,
|
|
17
18
|
MIN_SPEC_CHARS,
|
|
18
19
|
} = require('../scripts/gds/sequence.js');
|
|
@@ -263,5 +264,68 @@ t('summarize tolerates missing numbers', () => {
|
|
|
263
264
|
{ count: 1, credits: 0, minutes: 0 });
|
|
264
265
|
});
|
|
265
266
|
|
|
267
|
+
// ------------------------------------------------------- pickNext (--next)
|
|
268
|
+
|
|
269
|
+
// The live regression this closes (goal 1000030, 2026-08-05): a backlog task
|
|
270
|
+
// sorted to position 0 and --next reported "nothing runnable" while two ready
|
|
271
|
+
// tasks sat behind it.
|
|
272
|
+
const GOAL_1000030 = () => [
|
|
273
|
+
task({ id: 1002556, status: 'backlog', title: 'perf: fold the resolve' }),
|
|
274
|
+
task({ id: 1001850, status: 'ready', title: 'R108 — collapse rank-tier maps' }),
|
|
275
|
+
task({ id: 1001858, status: 'ready', title: 'R110 — governance API routes' }),
|
|
276
|
+
];
|
|
277
|
+
const claimable = (...ids) => ({ rank: 'archon', claimableIds: new Set(ids) });
|
|
278
|
+
|
|
279
|
+
t('pickNext offers the head when the head is clear', () => {
|
|
280
|
+
const plan = GOAL_1000030().slice(1);
|
|
281
|
+
const r = pickNext(plan, claimable(1001850, 1001858));
|
|
282
|
+
assert.equal(r.next.id, 1001850);
|
|
283
|
+
assert.deepEqual(r.skipped, []);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
t('pickNext skips a blocked head and offers the first runnable task', () => {
|
|
287
|
+
const r = pickNext(GOAL_1000030(), claimable(1001850, 1001858));
|
|
288
|
+
assert.equal(r.next.id, 1001850, 'must not dead-end on the backlog task at position 0');
|
|
289
|
+
assert.equal(r.skipped.length, 1);
|
|
290
|
+
assert.equal(r.skipped[0].id, 1002556);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
t('pickNext reports WHY each skipped task was passed over', () => {
|
|
294
|
+
const r = pickNext(GOAL_1000030(), claimable(1001850, 1001858));
|
|
295
|
+
assert.ok(r.skipped[0].reasons.some((x) => x.code === 'deps_unmet'));
|
|
296
|
+
assert.ok(r.skipped[0].title, 'skipped entries carry a title for the printout');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
t('pickNext keeps topological order among the runnable — first clear wins', () => {
|
|
300
|
+
const r = pickNext(GOAL_1000030(), claimable(1001850, 1001858));
|
|
301
|
+
assert.equal(r.next.id, 1001850, 'not 1001858, which sorts later');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
t('pickNext still halts when NOTHING in the plan clears', () => {
|
|
305
|
+
const plan = [
|
|
306
|
+
task({ id: 1, status: 'backlog' }),
|
|
307
|
+
task({ id: 2, status: 'shipped' }),
|
|
308
|
+
task({ id: 3, status: 'ready', requires_rank: 'archon' }),
|
|
309
|
+
];
|
|
310
|
+
const r = pickNext(plan, { rank: 'xenos', claimableIds: new Set([3]) });
|
|
311
|
+
assert.equal(r.next, null);
|
|
312
|
+
assert.equal(r.skipped.length, 3, 'every task was tried');
|
|
313
|
+
assert.equal(r.head.id, 1, 'the head is preserved so the halt prints its reasons');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
t('pickNext does not weaken a gate — a ready-but-unclaimable task is skipped, not offered', () => {
|
|
317
|
+
const plan = [task({ id: 9, status: 'ready' })];
|
|
318
|
+
const r = pickNext(plan, claimable(/* 9 absent from the claimable feed */));
|
|
319
|
+
assert.equal(r.next, null);
|
|
320
|
+
assert.ok(r.skipped[0].reasons.some((x) => x.code === 'not_claimable'));
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
t('pickNext on an empty plan returns no pick and nothing skipped', () => {
|
|
324
|
+
const r = pickNext([], claimable());
|
|
325
|
+
assert.equal(r.next, null);
|
|
326
|
+
assert.deepEqual(r.skipped, []);
|
|
327
|
+
assert.equal(r.head, null);
|
|
328
|
+
});
|
|
329
|
+
|
|
266
330
|
console.log(`\nsequence: ${passed} passed, ${failed} failed`);
|
|
267
331
|
process.exit(failed ? 1 : 0);
|
package/tests/session-cards.mjs
CHANGED
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
|
|
13
13
|
import { strict as assert } from 'node:assert';
|
|
14
14
|
import { createRequire } from 'node:module';
|
|
15
|
+
import { requireHostModule } from './helpers.mjs';
|
|
15
16
|
|
|
16
17
|
const require = createRequire(import.meta.url);
|
|
17
|
-
const hook = require
|
|
18
|
+
const hook = requireHostModule(require, '../.claude/hooks/session-cards.js');
|
|
18
19
|
|
|
19
20
|
let passed = 0;
|
|
20
21
|
let failed = 0;
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
|
|
17
17
|
import { strict as assert } from 'node:assert';
|
|
18
18
|
import { createRequire } from 'node:module';
|
|
19
|
+
import { requireHostModule } from './helpers.mjs';
|
|
19
20
|
|
|
20
21
|
const require = createRequire(import.meta.url);
|
|
21
|
-
const { decideFreshness } = require
|
|
22
|
+
const { decideFreshness } = requireHostModule(require, '../.claude/hooks/session-start.js');
|
|
22
23
|
|
|
23
24
|
let passed = 0;
|
|
24
25
|
let failed = 0;
|