@bongos/core 1.21.112 → 1.21.114
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 +616 -186
- package/.claude/skills/ask-for-help/SKILL.md +1 -1
- package/.claude/skills/bongos-feedback/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -0
- package/GOVERNMENT.md +128 -11
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +28 -0
- package/clients/bongos-client/index.cjs +28 -0
- package/clients/bongos-client/index.d.ts +45 -2
- package/clients/bongos-client/index.mjs +28 -0
- package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
- package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
- package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
- package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
- package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
- package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
- package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
- package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
- package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
- package/docs/adr/README.md +10 -1
- package/docs/api/openapi.json +989 -87
- package/docs/api-reference.md +18 -4
- package/docs/copy-inventory.md +343 -262
- package/docs/copy-registry.json +1223 -426
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/packs/engineer.md +2 -1
- package/docs/page-inventory.json +91 -6
- package/docs/page-readings.json +5017 -4458
- package/modules/builder-settings/law-locks.js +202 -0
- package/modules/builder-settings/routes/settings.js +66 -6
- package/modules/builder-settings/settings.js +7 -0
- package/modules/discord/ship-broadcast.js +22 -13
- package/modules/government/asks-db.js +54 -0
- package/modules/government/asks.js +269 -0
- package/modules/government/board-idea-reads.js +68 -0
- package/modules/government/board.js +21 -57
- package/modules/government/build-rights.js +148 -0
- package/modules/government/buildouts.js +160 -0
- package/modules/government/counts.js +81 -0
- package/modules/government/db.js +242 -0
- package/modules/government/decision-board.js +234 -8
- package/modules/government/decision-rules.js +2 -0
- package/modules/government/docket.js +114 -6
- package/modules/government/law-board.js +131 -0
- package/modules/government/laws.js +241 -0
- package/modules/government/leads.js +170 -0
- package/modules/government/migrations/government_027_laws.sql +143 -0
- package/modules/government/migrations/government_028_asks.sql +57 -0
- package/modules/government/migrations/government_029_leads.sql +135 -0
- package/modules/government/migrations/government_030_buildouts.sql +111 -0
- package/modules/government/migrations/government_031_treasury.sql +107 -0
- package/modules/government/module.json +2 -1
- package/modules/government/pollers/treasury-borrow.js +62 -0
- package/modules/government/positions.js +107 -0
- package/modules/government/presets.js +149 -0
- package/modules/government/routes/government.js +425 -2
- package/modules/government/routing.js +154 -0
- package/modules/government/treasury-db.js +166 -0
- package/modules/government/treasury.js +241 -0
- package/modules/hall-ui/public/atlas.html +2 -2
- package/modules/hall-ui/public/blockers.html +2 -2
- package/modules/hall-ui/public/board-room.html +2 -2
- package/modules/hall-ui/public/board-room.js +2 -0
- package/modules/hall-ui/public/city-draw.js +604 -11
- package/modules/hall-ui/public/city.css +157 -1
- package/modules/hall-ui/public/city.html +37 -7
- package/modules/hall-ui/public/city.js +441 -21
- package/modules/hall-ui/public/city.states.json +110 -5
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/commands.html +2 -2
- package/modules/hall-ui/public/copy-desk.html +2 -2
- package/modules/hall-ui/public/craft-mode.js +112 -29
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/diagram-viewer.css +12 -0
- package/modules/hall-ui/public/diagram-viewer.js +4 -0
- package/modules/hall-ui/public/diagrams.html +2 -2
- package/modules/hall-ui/public/drachmae.html +2 -2
- package/modules/hall-ui/public/fleet.html +2 -2
- package/modules/hall-ui/public/gate.html +2 -2
- package/modules/hall-ui/public/goal-map.html +2 -2
- package/modules/hall-ui/public/goals.html +2 -2
- package/modules/hall-ui/public/gov-feeds.js +141 -0
- package/modules/hall-ui/public/gov-living.js +1002 -0
- package/modules/hall-ui/public/gov-positions.js +97 -0
- package/modules/hall-ui/public/government.html +2 -2
- package/modules/hall-ui/public/hall-render.js +3 -0
- package/modules/hall-ui/public/idea.html +2 -2
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +3 -3
- package/modules/hall-ui/public/modules.html +2 -2
- package/modules/hall-ui/public/my-agents.css +334 -0
- package/modules/hall-ui/public/my-agents.html +72 -0
- package/modules/hall-ui/public/my-agents.js +603 -0
- package/modules/hall-ui/public/my-agents.states.json +37 -0
- package/modules/hall-ui/public/primer.html +2 -2
- package/modules/hall-ui/public/profile-connections.js +40 -28
- package/modules/hall-ui/public/profile.html +2 -2
- package/modules/hall-ui/public/project-settings.html +2 -2
- package/modules/hall-ui/public/ranks.html +2 -2
- package/modules/hall-ui/public/roadmap.html +2 -2
- package/modules/hall-ui/public/room.css +8 -0
- package/modules/hall-ui/public/roster.html +2 -2
- package/modules/hall-ui/public/sessions.html +2 -2
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +116 -16
- package/modules/hall-ui/public/street-card.js +18 -1
- package/modules/hall-ui/public/street-deck.css +299 -0
- package/modules/hall-ui/public/street-deck.js +1145 -0
- package/modules/hall-ui/public/street-depth.js +712 -0
- package/modules/hall-ui/public/street-desk.js +5 -2
- package/modules/hall-ui/public/street-draw.js +109 -16
- package/modules/hall-ui/public/street-inside.js +137 -22
- package/modules/hall-ui/public/street-office.js +705 -0
- package/modules/hall-ui/public/street.css +161 -11
- package/modules/hall-ui/public/street.html +41 -11
- package/modules/hall-ui/public/street.js +78 -66
- package/modules/hall-ui/public/street.states.json +129 -21
- package/modules/hall-ui/public/studio.html +2 -2
- package/modules/hall-ui/public/style.css +5 -1
- package/modules/hall-ui/public/task.html +2 -2
- package/modules/hall-ui/public/thinking.html +2 -2
- package/modules/hall-ui/public/tweak-editor.html +2 -2
- package/modules/hall-ui/public/watch.html +2 -2
- package/modules/hall-ui/public/watch.js +14 -1
- package/modules/hall-ui/public/work.html +2 -2
- package/modules/hall-ui/records/living-city.md +57 -0
- package/modules/hall-ui/records/my-agents.md +21 -0
- package/modules/hall-ui/records/oversight-family.md +1 -0
- package/modules/hall-ui/records/profile-connections.md +10 -0
- package/modules/hall-ui/records/reading-rooms.md +2 -0
- package/modules/ideas/routes/blockers.js +92 -3
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/moving.js +17 -1
- package/modules/lifecycle/routes/moving.js +6 -0
- package/modules/lifecycle/routes/project-settings.js +23 -0
- package/modules/onboarding/approval-broadcast.js +8 -0
- package/modules/onboarding/routes/access-requests.js +16 -6
- package/modules/platform-identity/connections.js +13 -2
- package/modules/platform-identity/routes/connections.js +7 -1
- package/modules/public-landing/public/account-private.states.json +2 -2
- package/modules/public-landing/public/account.html +65 -5
- package/modules/public-landing/public/account.states.json +3 -3
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
- package/modules/public-landing/public/assets/world.css +4 -4
- package/modules/public-landing/public/connections-empty.states.json +14 -0
- package/modules/public-landing/public/connections-inactive.states.json +14 -0
- package/modules/public-landing/public/connections.html +450 -0
- package/modules/public-landing/public/connections.probes.json +414 -0
- package/modules/public-landing/public/connections.states.json +17 -0
- package/modules/public-landing/public/contact.html +2 -2
- package/modules/public-landing/public/guild.html +2 -2
- package/modules/public-landing/public/index.html +2 -2
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/privacy.html +2 -2
- package/modules/public-landing/public/projects.html +2 -2
- package/modules/public-landing/public/projects.probes.json +6 -2
- package/modules/public-landing/public/terms.html +2 -2
- package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
- package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
- package/modules/ui-design/kit/fixtures/connections.json +22 -0
- package/modules/ui-design/kit/serve.js +95 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +128 -0
- package/scripts/discord/README.md +8 -5
- package/scripts/discord/post.js +12 -1
- package/scripts/gds/artifact-audit.js +20 -3
- package/scripts/gds/ask.js +164 -0
- package/scripts/gds/autobongos-run.js +20 -5
- package/scripts/gds/autobongos-tweak-lane.js +262 -0
- package/scripts/gds/autobongos-verify.js +21 -2
- package/scripts/gds/claim.js +2 -0
- package/scripts/gds/government-walk.js +638 -4
- package/scripts/gds/pulse.js +9 -6
- package/scripts/gds/ship-escalation.js +22 -3
- package/scripts/gds/ship-finish.js +5 -4
- package/scripts/gds/ship-merge-gate.js +18 -6
- package/scripts/gds/tweak-halt.js +1 -0
- package/scripts/hall-preview/README.md +41 -1
- package/scripts/hall-preview/pref-writes.js +140 -0
- package/scripts/hall-preview/refresh-fixtures.js +101 -7
- package/scripts/hall-preview/scrub-identity.js +366 -7
- package/scripts/hall-preview/server.js +27 -1
- package/scripts/render-diagrams.sh +4 -2
- package/src/bongos/auth-idp.js +6 -1
- package/src/bongos/project-settings.js +41 -0
- package/src/bongos/route-rank-check.js +27 -0
- package/src/bongos/routes/me.js +68 -7
- package/src/bongos/serve-internal.js +5 -1
- package/src/module-api.js +7 -1
- package/src/platform-server.js +10 -0
- package/tests/application_lifecycle.mjs +47 -5
- package/tests/autobongos_tweak_lane.mjs +208 -0
- package/tests/conductor_main_e2e.mjs +21 -0
- package/tests/connections_api.mjs +18 -0
- package/tests/craft_mode_nav.mjs +5 -3
- package/tests/craft_mode_tallies.mjs +52 -16
- package/tests/discord_approvals.mjs +11 -0
- package/tests/discord_channels.mjs +20 -0
- package/tests/fixtures/gov-seated.mjs +64 -0
- package/tests/fixtures/government-ask-cards.json +1284 -0
- package/tests/government_act_alone_buildings.mjs +1 -0
- package/tests/government_agent_asks.mjs +322 -0
- package/tests/government_asks.mjs +408 -0
- package/tests/government_build_rights.mjs +253 -0
- package/tests/government_buildouts.mjs +382 -0
- package/tests/government_counts.mjs +143 -0
- package/tests/government_decision_rules.mjs +5 -1
- package/tests/government_laws.mjs +488 -0
- package/tests/government_laws_session.mjs +321 -0
- package/tests/government_leads.mjs +375 -0
- package/tests/government_positions.mjs +127 -0
- package/tests/government_presets.mjs +215 -0
- package/tests/government_routes.mjs +8 -1
- package/tests/government_routing.mjs +118 -0
- package/tests/government_seed.mjs +5 -0
- package/tests/government_treasury.mjs +394 -0
- package/tests/governor_docket.mjs +26 -2
- package/tests/hall_buildouts.mjs +208 -0
- package/tests/hall_city.mjs +14 -6
- package/tests/hall_city_living.mjs +617 -0
- package/tests/hall_gov_counts.mjs +336 -0
- package/tests/hall_gov_living.mjs +636 -0
- package/tests/hall_gov_real_site.mjs +655 -0
- package/tests/hall_matter_card.mjs +6 -1
- package/tests/hall_my_agents.mjs +481 -0
- package/tests/hall_nav.mjs +107 -4
- package/tests/hall_page_gate_map.mjs +4 -0
- package/tests/hall_preview_governor_fixtures.mjs +30 -3
- package/tests/hall_preview_scrub.mjs +59 -1
- package/tests/hall_preview_security.mjs +169 -0
- package/tests/hall_reading_world.mjs +44 -0
- package/tests/hall_roster_world.mjs +6 -2
- package/tests/hall_street.mjs +18 -2
- package/tests/hall_street_amend.mjs +1 -1
- package/tests/hall_street_deck.mjs +609 -0
- package/tests/hall_street_depth.mjs +559 -0
- package/tests/hall_street_desk.mjs +1 -1
- package/tests/hall_street_inside.mjs +29 -5
- package/tests/hall_tweak_editor.mjs +4 -1
- package/tests/hub_account_page.mjs +94 -3
- package/tests/hub_connections_page.mjs +507 -0
- package/tests/hub_invite_badge.mjs +1 -1
- package/tests/idp_device_exchange.mjs +18 -0
- package/tests/landing_legal_pages.mjs +8 -5
- package/tests/landing_page.mjs +23 -4
- package/tests/nav_permission_atoms.mjs +4 -1
- package/tests/profile_connections_ui.mjs +88 -25
- package/tests/projects_hub.mjs +15 -0
- package/tests/render_check.mjs +2 -2
- package/tests/ship_broadcast_routing.mjs +57 -16
- package/tests/ship_broadcast_server.mjs +40 -7
- package/tests/ship_merge_gate.mjs +19 -0
- package/tests/ui_design_kit.mjs +2 -2
- package/tests/watch_applications_queue.mjs +23 -0
package/tests/hall_nav.mjs
CHANGED
|
@@ -24,11 +24,18 @@ import { fileURLToPath } from 'node:url';
|
|
|
24
24
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
25
|
const HALL = path.join(ROOT, 'modules/hall-ui/public');
|
|
26
26
|
const SRC = fs.readFileSync(path.join(HALL, 'shell.js'), 'utf8');
|
|
27
|
+
const POSITIONS_SRC = fs.readFileSync(path.join(HALL, 'gov-positions.js'), 'utf8');
|
|
28
|
+
const FX = path.join(ROOT, 'scripts/hall-preview/fixtures');
|
|
29
|
+
const fx = (name) => JSON.parse(fs.readFileSync(path.join(FX, name + '.json'), 'utf8'));
|
|
27
30
|
|
|
28
31
|
// Boot shell.js under a DOM stub and return { sidebarHtml, viewHidden }.
|
|
29
32
|
// Elements are memoised per id so the assertions can inspect what the router did
|
|
30
33
|
// to them (#view-standing etc.) after boot.
|
|
31
|
-
|
|
34
|
+
// `requests` (task 1004685) answers the shell's own reads by path — { '<path>':
|
|
35
|
+
// { status, data } } — so the lazily computed positions can settle; anything not
|
|
36
|
+
// named never settles, as before. `positions: true` pre-loads gov-positions.js the
|
|
37
|
+
// way the page's lazy <script> would.
|
|
38
|
+
function boot({ page = 'settings', hash = '', pathname = '/settings', search = '', requests = null, positions = false } = {}) {
|
|
32
39
|
const els = new Map();
|
|
33
40
|
const makeEl = () => {
|
|
34
41
|
const el = {
|
|
@@ -50,7 +57,7 @@ function boot({ page = 'settings', hash = '', pathname = '/settings' } = {}) {
|
|
|
50
57
|
const location = {
|
|
51
58
|
hostname: 'builders.cloudbongos.com',
|
|
52
59
|
origin: 'https://builders.cloudbongos.com',
|
|
53
|
-
pathname, hash,
|
|
60
|
+
pathname, hash, search,
|
|
54
61
|
assign() {},
|
|
55
62
|
replace(target) { replaced.push(String(target)); },
|
|
56
63
|
};
|
|
@@ -76,6 +83,9 @@ function boot({ page = 'settings', hash = '', pathname = '/settings' } = {}) {
|
|
|
76
83
|
el.dataset.gate = gate;
|
|
77
84
|
const mod = attr(tag, 'data-module');
|
|
78
85
|
if (mod) el.dataset.module = mod;
|
|
86
|
+
// task 1004685: the governing-surface flag the position gate reads.
|
|
87
|
+
const pos = attr(tag, 'data-position');
|
|
88
|
+
if (pos) el.dataset.position = pos;
|
|
79
89
|
out.push(el);
|
|
80
90
|
}
|
|
81
91
|
return out;
|
|
@@ -130,7 +140,7 @@ function boot({ page = 'settings', hash = '', pathname = '/settings' } = {}) {
|
|
|
130
140
|
// Index pages never reach probeMe (builders.js owns /me there); a non-index
|
|
131
141
|
// boot would, so hand back a never-settling promise rather than fake auth —
|
|
132
142
|
// these assertions are about the rendered model, not the gated state.
|
|
133
|
-
BongosClient: { createClient: () => ({ request: () => new Promise(() => {}) }) },
|
|
143
|
+
BongosClient: { createClient: () => ({ request: (method, p) => (requests && p in requests ? Promise.resolve(requests[p]) : new Promise(() => {})) }) },
|
|
134
144
|
matchMedia: () => ({ matches: false, addEventListener() {} }),
|
|
135
145
|
localStorage: { getItem() { return null; }, setItem() {} },
|
|
136
146
|
requestAnimationFrame() {},
|
|
@@ -145,6 +155,7 @@ function boot({ page = 'settings', hash = '', pathname = '/settings' } = {}) {
|
|
|
145
155
|
};
|
|
146
156
|
sandbox.globalThis = sandbox;
|
|
147
157
|
vm.createContext(sandbox);
|
|
158
|
+
if (positions) vm.runInContext(POSITIONS_SRC, sandbox, { filename: 'gov-positions.js' });
|
|
148
159
|
vm.runInContext(SRC, sandbox, { filename: 'shell.js' });
|
|
149
160
|
|
|
150
161
|
return {
|
|
@@ -271,7 +282,10 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
271
282
|
// same atom and for the same reason (owner call; it leads the Governing mode).
|
|
272
283
|
assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'goal-map', 'work', 'blockers', 'board-room', 'city', 'thinking', 'studio'],
|
|
273
284
|
'the unlabeled group is the project itself (task 1002425 order)');
|
|
274
|
-
|
|
285
|
+
// task 1004685 (the living-city prototype, round-3 board 6): 'my-agents' joins You —
|
|
286
|
+
// how your own Claude works for you, the personal half of "the city steers
|
|
287
|
+
// everyone, My agents steers yours". Its subject is the viewer's own agents.
|
|
288
|
+
assert.deepEqual(nav[1].items.map((i) => i.id), ['standing', 'profile', 'my-agents'], 'You = your own numbers and your own agents');
|
|
275
289
|
// task 1003444: 'people' LEFT the group — the owner deleted the hall's
|
|
276
290
|
// cross-project directory (ADR 0337 D5); its home is the hub, and /people
|
|
277
291
|
// redirects to /roster. Builders is the hall's directory now.
|
|
@@ -725,3 +739,92 @@ test('no glyph path data reads as an IP address to the release leak scan', () =>
|
|
|
725
739
|
const hits = block.match(/\d+\.\d+\.\d+\.\d+/g) || [];
|
|
726
740
|
assert.deepEqual(hits, [], 'separate the numbers with spaces so no four chain on dots');
|
|
727
741
|
});
|
|
742
|
+
|
|
743
|
+
// ── GOVERNING IS A POSITION (task 1004685, the living-city prototype) ─────────
|
|
744
|
+
//
|
|
745
|
+
// Round-3 board 8: you pick a craft, but you HOLD a seat or you don't. The
|
|
746
|
+
// governing surfaces carry `position: true`, and the gate loop hides them from a
|
|
747
|
+
// builder whose seats are KNOWN to be none — /me's server-resolved `positions`
|
|
748
|
+
// block (task 1004713). These drive the real shell and the real gov-positions.js.
|
|
749
|
+
|
|
750
|
+
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
|
751
|
+
const GOVERNING = ['board-room', 'city', 'watch', 'gate', 'sessions', 'project-settings', 'deploy', 'fleet', 'government'];
|
|
752
|
+
const lead = (building, title) => ({ kind: 'lead', seat: 'lead:' + building, building, title });
|
|
753
|
+
const block = (held) => ({ held, title: held.length ? held[0].title : null, has_position: held.length > 0 });
|
|
754
|
+
|
|
755
|
+
// task 1004713: the seats are /me's server-resolved `positions` block, handed to
|
|
756
|
+
// applyAccess exactly as probeMe and hall-render.js hand it. No read is answered
|
|
757
|
+
// (`requests: {}`): the shell asks nothing else to know them.
|
|
758
|
+
async function seated({ builder, positions, search = '' }) {
|
|
759
|
+
const b = boot({ positions: true, search, requests: {} });
|
|
760
|
+
b.shell.applyAccess({ authed: true, rank: builder.rank, permissions: { 'board.vote.cast': true }, builder, positions });
|
|
761
|
+
await tick();
|
|
762
|
+
await tick();
|
|
763
|
+
return b;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
test('POSITION: exactly the governing surfaces carry the flag — the City, the Board Room and Government', () => {
|
|
767
|
+
const html = boot().sidebarHtml;
|
|
768
|
+
const flagged = [...html.matchAll(/id="nav-([^"]+)"[^>]*data-position="1"/g)].map((m) => m[1]);
|
|
769
|
+
assert.deepEqual(flagged, GOVERNING);
|
|
770
|
+
assert.match(html, /class="nav-group__head" data-gate="government" data-position="1" data-group="government"/,
|
|
771
|
+
'the Government head carries it too, so no label is left over an emptied group');
|
|
772
|
+
const nav = parseNav(html);
|
|
773
|
+
assert.ok(!nav.find((g) => g.label === 'You').items.some((i) => flagged.includes(i.id)), 'your own pages never need a seat');
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
test('POSITION: a Lead keeps every governing surface, and the seat names it', async () => {
|
|
777
|
+
const b = await seated({ builder: { id: '424242', rank: 'thetes' }, positions: block([lead('planning', 'Planning Lead')]) });
|
|
778
|
+
assert.equal(b.shell.positions().title, 'Planning Lead');
|
|
779
|
+
// a thetes Lead: the seat opens the governing tabs their rank and permissions allow
|
|
780
|
+
for (const id of ['board-room', 'city', 'my-agents']) assert.equal(b.navHidden(id), false, `${id} shows`);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
test('POSITION: the fixture viewer holds what the fixture /me says', async () => {
|
|
784
|
+
const me = fx('me');
|
|
785
|
+
const b = await seated({ builder: me.builder, positions: me.positions });
|
|
786
|
+
assert.equal(b.shell.positions().title, me.positions.title);
|
|
787
|
+
assert.equal(b.navHidden('city'), false);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
test('POSITION: a builder with no seat sees no governing surface, and only those go', async () => {
|
|
791
|
+
// an archon the server says holds no seat: rank, craft and permissions do not make one
|
|
792
|
+
const b = await seated({ builder: { id: '424242', rank: 'archon', preferred_disciplines: ['engineer'] }, positions: block([]) });
|
|
793
|
+
assert.equal(b.shell.positions().has_position, false);
|
|
794
|
+
for (const id of ['board-room', 'city', 'watch', 'project-settings', 'government']) assert.equal(b.navHidden(id), true, `${id} hides`);
|
|
795
|
+
for (const id of ['home', 'work', 'goals', 'my-agents', 'profile', 'primer']) assert.equal(b.navHidden(id), false, `${id} still shows`);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
test('POSITION: unknown is today\'s nav — a /me with no positions block hides nothing', async () => {
|
|
799
|
+
for (const positions of [null, undefined]) {
|
|
800
|
+
const b = await seated({ builder: { id: '424242', rank: 'metic' }, positions });
|
|
801
|
+
assert.equal(b.shell.positions(), null, 'the seats could not be read');
|
|
802
|
+
for (const id of ['board-room', 'city', 'watch']) assert.equal(b.navHidden(id), false, `${id} shows, as it did before`);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test('POSITION: the shell hands /me\'s positions block to applyAccess, on every page and on the index', () => {
|
|
807
|
+
assert.match(SRC, /positions: me\.positions \|\| null/, 'probeMe passes the block');
|
|
808
|
+
const render = fs.readFileSync(path.join(HALL, 'hall-render.js'), 'utf8');
|
|
809
|
+
assert.match(render, /positions: \(ctx\.lastMeState && ctx\.lastMeState\.positions\) \|\| null/, 'the index passes it too');
|
|
810
|
+
assert.doesNotMatch(SRC, /proto\/governor-overlay/, 'the shell never reads the prototype overlay for seats');
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
test('POSITION: ?as=nobody views the hall with no seat, and can only hide', async () => {
|
|
814
|
+
const me = fx('me');
|
|
815
|
+
const b = await seated({ builder: me.builder, positions: me.positions, search: '?as=nobody' });
|
|
816
|
+
assert.equal(b.shell.positions().as, 'nobody');
|
|
817
|
+
for (const id of GOVERNING) assert.equal(b.navHidden(id), true, `${id} hides for the stand-in`);
|
|
818
|
+
assert.equal(b.navHidden('my-agents'), false);
|
|
819
|
+
// a xenos stand-in stays a xenos: the pretend rank never rises above the real one
|
|
820
|
+
const x = await seated({ builder: { id: '9', rank: 'xenos' }, positions: block([lead('planning', 'Planning Lead')]), search: '?as=nobody' });
|
|
821
|
+
assert.equal(x.navHidden('roadmap'), true, 'a member page stays hidden from a real xenos');
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
test('My agents draws its own glyph and lands on a real page', () => {
|
|
825
|
+
const item = parseNav(boot().sidebarHtml).flatMap((g) => g.items).find((i) => i.id === 'my-agents');
|
|
826
|
+
assert.ok(item, 'the item renders');
|
|
827
|
+
assert.equal(item.href, '/my-agents');
|
|
828
|
+
assert.equal(item.gate, 'signed');
|
|
829
|
+
assert.ok(hrefResolves(item), 'my-agents.html exists');
|
|
830
|
+
});
|
|
@@ -49,6 +49,10 @@ const ORACLE = {
|
|
|
49
49
|
// task 1004473: the Commands page — its two reads (installed skills, CLI verbs)
|
|
50
50
|
// are requireBuilder, so sign-in is the whole gate.
|
|
51
51
|
'/builders/commands': 'requireBuilderPage',
|
|
52
|
+
// task 1004685 (the living-city prototype): My agents — the viewer's own agent
|
|
53
|
+
// settings. Every read behind it is the viewer's own (GET /me, /me/* prefs,
|
|
54
|
+
// /specialities) and requireBuilder, so sign-in is the whole gate.
|
|
55
|
+
'/builders/my-agents': 'requireBuilderPage',
|
|
52
56
|
'/builders/goals': 'requireBuilderPage',
|
|
53
57
|
// task 1003479: the goal map, on the goal board's tier. task 1004232 (sky part 4):
|
|
54
58
|
// the URL now serves THE PROJECT SKY (the same page as /thinking, in its whole-project
|
|
@@ -26,6 +26,7 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
26
26
|
const FX = path.join(ROOT, 'scripts/hall-preview/fixtures');
|
|
27
27
|
const require = createRequire(import.meta.url);
|
|
28
28
|
const docketLib = require(path.join(ROOT, 'modules/government/docket.js'));
|
|
29
|
+
const routing = require(path.join(ROOT, 'modules/government/routing.js'));
|
|
29
30
|
const DR = require(path.join(ROOT, 'modules/government/decision-rules.js'));
|
|
30
31
|
const DB = require(path.join(ROOT, 'modules/government/decision-board.js'));
|
|
31
32
|
const A = require(path.join(ROOT, 'modules/hall-ui/public/street-amend.js'));
|
|
@@ -41,8 +42,23 @@ function countStates(ms) {
|
|
|
41
42
|
test('the docket fixture: counts and the home line are derived from its matters, as buildDocket derives them', () => {
|
|
42
43
|
const d = fx('government__docket');
|
|
43
44
|
assert.ok(d.matters.length > 7, 'the dense docket (task 1004569), not the first seven');
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
// routed (task 1004705): `yours` is the matters on the viewer's own seats, and the line counts those
|
|
46
|
+
const yours = d.matters.filter((m) => m.route && m.route.yours).length;
|
|
47
|
+
assert.equal(d.routed, true);
|
|
48
|
+
assert.deepEqual(d.counts, { ...countStates(d.matters), yours });
|
|
49
|
+
assert.equal(d.line, docketLib.homeLine({ needs: yours, moving: d.counts.moving }), 'the city line under start governing agrees with the street header');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('the docket fixture: every route is the server\'s rule over the leads fixture and the holders (task 1004705)', () => {
|
|
53
|
+
const d = fx('government__docket');
|
|
54
|
+
const L = fx('government__leads');
|
|
55
|
+
const holders = {};
|
|
56
|
+
d.buildings.forEach((b) => { holders[b.key] = b.holders; });
|
|
57
|
+
const seats = { leads: L.buildings.filter((b) => b.lead).map((b) => ({ building: b.building, builder_id: b.lead.builder.id })), owner: L.owner, holders };
|
|
58
|
+
for (const m of d.matters) {
|
|
59
|
+
const { route, ...bare } = m;
|
|
60
|
+
assert.deepEqual(route, routing.routeMatter(bare, d.viewer.id, seats, Date.parse(d.generated_at)), m.id);
|
|
61
|
+
}
|
|
46
62
|
});
|
|
47
63
|
|
|
48
64
|
test('the docket fixture: every building counts exactly the matters standing at it', () => {
|
|
@@ -64,9 +80,20 @@ test('the docket fixture: the fairness reading is derived from the matters at Di
|
|
|
64
80
|
assert.deepEqual({ value: f.value, figure: f.figure, line: f.line, hot: f.hot }, want);
|
|
65
81
|
});
|
|
66
82
|
|
|
83
|
+
// The decision-table rows a constitution fixture says are STORED: an area carries an
|
|
84
|
+
// updated_at only when a row was written for it (decision-rules.js resolveOne). The
|
|
85
|
+
// fixture is a live snapshot since task 1004685, and the live project has written its
|
|
86
|
+
// rows (all "as today"), so the route's block is resolveAll over those rows, not [].
|
|
87
|
+
function storedRows(block) {
|
|
88
|
+
return block.areas.filter((a) => a.updated_at).map((a) => ({
|
|
89
|
+
area: a.area, decided_by: a.decided_by, rule: a.as_today ? null : a.rule,
|
|
90
|
+
set_by_item_id: a.set_by_item_id, updated_at: a.updated_at,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
|
|
67
94
|
test('the constitution fixture: it carries the decision_rules block the route serves, so every building can draft an amendment', () => {
|
|
68
95
|
const c = fx('government__constitution');
|
|
69
|
-
assert.deepEqual(c.decision_rules, JSON.parse(JSON.stringify(DB.constitutionBlock(DR.resolveAll(
|
|
96
|
+
assert.deepEqual(c.decision_rules, JSON.parse(JSON.stringify(DB.constitutionBlock(DR.resolveAll(storedRows(c.decision_rules), { board: c.board })))), 'the server\'s own block for this board');
|
|
70
97
|
for (const area of c.decision_rules.areas.map((r) => r.area)) {
|
|
71
98
|
const d = A.draftFor(area, c);
|
|
72
99
|
assert.equal(d.ok, true, area + ': change how this works drafts');
|
|
@@ -29,7 +29,7 @@ const require = createRequire(import.meta.url);
|
|
|
29
29
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
30
30
|
const FIXTURES = path.join(ROOT, 'scripts', 'hall-preview', 'fixtures');
|
|
31
31
|
const {
|
|
32
|
-
createScrubber, isOwnAccountFixture, highestStandIn, rewriteScalars, GHOST_AVATAR_URL,
|
|
32
|
+
createScrubber, createTextScrubber, isOwnAccountFixture, highestStandIn, rewriteScalars, GHOST_AVATAR_URL,
|
|
33
33
|
} = require('../scripts/hall-preview/scrub-identity.js');
|
|
34
34
|
const { refreshFixtures, scrubFixturesOnDisk } = require('../scripts/hall-preview/refresh-fixtures.js');
|
|
35
35
|
|
|
@@ -200,6 +200,64 @@ test('--scrub-only rewrites a hand-written fixture in place, once', () => {
|
|
|
200
200
|
}
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
+
// task 1004685: the governor docket names people in its prose, so a run ends with a
|
|
204
|
+
// pass over every string for the people its structured fields already named.
|
|
205
|
+
test('prose: a person the run met in a field is replaced inside sentences too', () => {
|
|
206
|
+
const s = createScrubber();
|
|
207
|
+
s.scrub({ builders: [
|
|
208
|
+
{ id: '3', github_login: 'Real-Person9', display_name: 'Real Person' },
|
|
209
|
+
{ id: '4', github_login: 'willOtherHuman', display_name: 'willOtherHuman' },
|
|
210
|
+
{ id: '5', github_login: 'IAMSOMEONEELSE', display_name: 'IAMSOMEONEELSE' },
|
|
211
|
+
{ id: '6', github_login: 'discord-bot-system', display_name: 'Discord Bot (system)' },
|
|
212
|
+
{ id: '7', github_login: 'bobbytabl', display_name: 'bobbytabl' },
|
|
213
|
+
{ id: '8', github_login: 'largefellow-x', display_name: 'largefellow-x' },
|
|
214
|
+
] });
|
|
215
|
+
const prose = createTextScrubber(s.knownPeople());
|
|
216
|
+
const out = prose({
|
|
217
|
+
detail: "From real-person9's feedback: Real Person says Real: stop the runner",
|
|
218
|
+
owner: 'Owner: **Will / Someoneelse**. The agent will stop when Discord is down.',
|
|
219
|
+
path: 'not /home/real/app, and mail 290184380+IAMSOMEONEELSE@users.noreply.github.com',
|
|
220
|
+
link: 'see https://github.com/Real-Person9/cloud-bongos/blob/main/x.md',
|
|
221
|
+
repo: { owner: 'Real-Person9', repo: 'cloud-bongos' },
|
|
222
|
+
named: "Bobby Tables's first run. Bobby again. Large files stay.",
|
|
223
|
+
});
|
|
224
|
+
assert.equal(out.named, "Builder 5's first run. Builder 5 again. Large files stay.",
|
|
225
|
+
'a capitalised word a login starts with is its first name, surname and all; a common word is not');
|
|
226
|
+
assert.equal(out.detail, "From builder-1's feedback: Builder 1 says Builder 1: stop the runner",
|
|
227
|
+
'a login, a display name and the first name the login starts with');
|
|
228
|
+
assert.equal(out.owner, 'Owner: **Builder 2 / Builder 3**. The agent will stop when Discord is down.',
|
|
229
|
+
'a first name that is a common word only counts capitalised; an all-caps login gives up its nickname; a system account gives up nothing');
|
|
230
|
+
assert.equal(out.path, 'not /home/builder-1/app, and mail builder@example.com');
|
|
231
|
+
assert.equal(out.link, 'see https://github.com/Real-Person9/cloud-bongos/blob/main/x.md', 'the repo\'s own coordinates stay');
|
|
232
|
+
assert.equal(out.repo.owner, 'Real-Person9', 'as the manifest\'s repo.owner does');
|
|
233
|
+
assert.deepEqual(createTextScrubber(createScrubber().knownPeople())({ a: 'Real Person' }), { a: 'Real Person' },
|
|
234
|
+
'a pass that met nobody changes nothing, which keeps --scrub-only a fixed point');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('a building\'s decision word is vocabulary, not a login, on the docket\'s decision too', () => {
|
|
238
|
+
const s = createScrubber();
|
|
239
|
+
const d = s.scrub({ buildings: [{ key: 'review', decision: { rule: 'decide', decided_by: 'gate' } }, { key: 'charter', decision: { rule: 'unanimous', decided_by: 'board' } }] });
|
|
240
|
+
assert.deepEqual(d.buildings.map((b) => b.decision.decided_by), ['gate', 'board']);
|
|
241
|
+
assert.deepEqual(s.knownPeople().logins, [], 'and the prose pass is never taught to rewrite "the stealth gate"');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('a re-run keeps the numbers the committed roster gives each builder', async () => {
|
|
245
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hall-fixtures-'));
|
|
246
|
+
try {
|
|
247
|
+
fs.writeFileSync(path.join(dir, 'builders__roster.json'), JSON.stringify({ builders: [{ id: '7', github_login: 'builder-40', display_name: 'Builder 40' }] }));
|
|
248
|
+
const responses = {
|
|
249
|
+
'/builders/roster': { builders: [{ id: '7', github_login: 'other-human', display_name: 'Other Human' }] },
|
|
250
|
+
'/builders/directory': { builders: [{ id: '7', github_login: 'other-human' }, { id: '8', github_login: 'a-newcomer' }] },
|
|
251
|
+
};
|
|
252
|
+
const api = { request: async (_m, p) => (p in responses ? { ok: true, status: 200, data: responses[p] } : { ok: false, status: 404 }) };
|
|
253
|
+
await refreshFixtures({ api, dir, snapshots: [['builders/directory', 'builders__directory']], log: () => {} });
|
|
254
|
+
const out = JSON.parse(fs.readFileSync(path.join(dir, 'builders__directory.json'), 'utf8'));
|
|
255
|
+
assert.deepEqual(out.builders.map((b) => b.github_login), ['builder-40', 'builder-41'], 'builder 7 keeps 40; a newcomer numbers above it');
|
|
256
|
+
} finally {
|
|
257
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
203
261
|
test('every committed fixture is already scrubbed — no real person is checked in', () => {
|
|
204
262
|
const files = fs.readdirSync(FIXTURES).filter((f) => f.endsWith('.json')).sort();
|
|
205
263
|
const texts = files.map((f) => fs.readFileSync(path.join(FIXTURES, f), 'utf8'));
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// tests/hall_preview_security.mjs — no committed hall-preview fixture carries the
|
|
2
|
+
// project's security content (task 1004685, after its security review).
|
|
3
|
+
//
|
|
4
|
+
// The governor docket the harness snapshots is read at a rank floor, and it holds
|
|
5
|
+
// the open red-team reports (file and line references, exploit chains, proofs of
|
|
6
|
+
// concept) and the owner's blockers (host config diffs, recovery runbooks). A
|
|
7
|
+
// committed fixture would hand those to anyone with repo read access. The fix is
|
|
8
|
+
// in the WRITER (scrub-identity.js createSecurityRedactor, run by every snapshot
|
|
9
|
+
// and by --scrub-only); these tests pin it:
|
|
10
|
+
// 1. every red-team record keeps only the allowed keys and the withheld title;
|
|
11
|
+
// 2. every blocker keeps no body, and no title that names a secret or a host;
|
|
12
|
+
// 3. the security backlog's tasks keep their shape, not their words;
|
|
13
|
+
// 4. no fixture anywhere carries the strings the review found;
|
|
14
|
+
// 5. every committed fixture is already a fixed point of the pass;
|
|
15
|
+
// 6. the checks are not vacuous: put one body back and they fail.
|
|
16
|
+
//
|
|
17
|
+
// Run: node --test tests/hall_preview_security.mjs
|
|
18
|
+
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import assert from 'node:assert/strict';
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import os from 'node:os';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
const FIXTURES = path.join(ROOT, 'scripts', 'hall-preview', 'fixtures');
|
|
30
|
+
const S = require('../scripts/hall-preview/scrub-identity.js');
|
|
31
|
+
const { refreshFixtures } = require('../scripts/hall-preview/refresh-fixtures.js');
|
|
32
|
+
|
|
33
|
+
const FILES = fs.readdirSync(FIXTURES).filter((f) => f.endsWith('.json')).sort();
|
|
34
|
+
const load = () => FILES.map((f) => ({ file: f, data: JSON.parse(fs.readFileSync(path.join(FIXTURES, f), 'utf8')) }));
|
|
35
|
+
|
|
36
|
+
// The strings the review found in the committed fixtures, and anything like them.
|
|
37
|
+
// (GRADER_BYPASS is the env var's spelling: /me's own `grader_bypass` flag is a field name.)
|
|
38
|
+
// The prior instance's brand comes from PRIOR_INSTANCE_FLOOR, the one definition package-core's
|
|
39
|
+
// leak scan refuses. This file ships in the core package, so it must not spell the word out
|
|
40
|
+
// itself; resolving it from the floor keeps the check and the scan in step (task 1004722).
|
|
41
|
+
// It is the floor's one-word token; the multi-word one is the module authorship credit,
|
|
42
|
+
// which modules.json legitimately carries and which this check never named.
|
|
43
|
+
const { PRIOR_INSTANCE_FLOOR } = require('../scripts/gds/package-core.js');
|
|
44
|
+
const PRIOR_BRAND = PRIOR_INSTANCE_FLOOR.tokens.find((t) => /^[a-z0-9]+$/i.test(t));
|
|
45
|
+
assert.ok(PRIOR_BRAND, 'PRIOR_INSTANCE_FLOOR must name a one-word brand, or the brand checks below are vacuous');
|
|
46
|
+
const LEAKED = [/committed_files/i, /auth-idp/i, /verifyHubAssertion/i, /serverSideGuards/i, new RegExp(PRIOR_BRAND, 'i'), /frame-ancestors/i,
|
|
47
|
+
/\bcaddy/i, /bot token/i, /GRADER_BYPASS/, /reconcile-on-read/i, /X-Frame-Options/i, /Content-Security-Policy/i, /\/etc\/cloudbongos/i];
|
|
48
|
+
// Inside a red-team record not even these may appear.
|
|
49
|
+
const IN_REPORT = new RegExp(String.raw`committed_files|auth-idp|\bPoC\b|frame-ancestors|${PRIOR_BRAND}|exploit|token|repro|\.js\b|:\d+-\d+`, 'i');
|
|
50
|
+
const BLOCKER_BODY = ['body', 'body_md', 'notes', 'note', 'details', 'detail', 'description', 'resolution_note', 'steps', 'facts'];
|
|
51
|
+
|
|
52
|
+
function records(v, where, out = []) {
|
|
53
|
+
if (Array.isArray(v)) v.forEach((x, i) => records(x, `${where}[${i}]`, out));
|
|
54
|
+
else if (v && typeof v === 'object') {
|
|
55
|
+
out.push({ where, o: v });
|
|
56
|
+
for (const [k, x] of Object.entries(v)) records(x, `${where}.${k}`, out);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
function strings(v, out = []) {
|
|
61
|
+
if (typeof v === 'string') out.push(v);
|
|
62
|
+
else if (Array.isArray(v)) v.forEach((x) => strings(x, out));
|
|
63
|
+
else if (v && typeof v === 'object') Object.values(v).forEach((x) => strings(x, out));
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Every way a set of fixtures can leak, as a list (empty = clean).
|
|
68
|
+
function leaksIn(fixtures) {
|
|
69
|
+
const leaks = [];
|
|
70
|
+
const secGoals = new Set(S.securityGoalIdsIn(fixtures.map((f) => f.data)));
|
|
71
|
+
for (const { file, data } of fixtures) {
|
|
72
|
+
for (const { where, o } of records(data, file)) {
|
|
73
|
+
if (S.isReportRecord(o)) {
|
|
74
|
+
for (const k of Object.keys(o)) if (!S.REPORT_KEYS.has(k) && !S.TITLE_KEYS.has(k)) leaks.push(`${where}: a red-team record carries "${k}"`);
|
|
75
|
+
for (const k of S.TITLE_KEYS) if (typeof o[k] === 'string' && o[k] !== S.REPORT_TITLE) leaks.push(`${where}.${k}: a red-team title is not withheld`);
|
|
76
|
+
for (const s of strings(o)) if (IN_REPORT.test(s)) leaks.push(`${where}: red-team prose "${s.slice(0, 40)}"`);
|
|
77
|
+
} else if (S.isBlockerRecord(o)) {
|
|
78
|
+
for (const k of BLOCKER_BODY) if (k in o) leaks.push(`${where}: a blocker carries its "${k}"`);
|
|
79
|
+
if (typeof o.title === 'string' && S.SENSITIVE_TEXT.test(o.title)) leaks.push(`${where}.title: a blocker title names a secret or a host`);
|
|
80
|
+
}
|
|
81
|
+
if ('title' in o && (o.security_sensitive === true || (o.goal_id != null && secGoals.has(String(o.goal_id))))) {
|
|
82
|
+
if (o.title !== S.TASK_TITLE) leaks.push(`${where}: a security-backlog task keeps its title`);
|
|
83
|
+
if (typeof o.description === 'string' && o.description) leaks.push(`${where}: a security-backlog task keeps its description`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const text = JSON.stringify(data);
|
|
87
|
+
for (const re of LEAKED) if (re.test(text)) leaks.push(`${file}: contains ${re}`);
|
|
88
|
+
}
|
|
89
|
+
return leaks;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
test('every committed fixture is clean: red-team records, blockers, the security backlog, the review\'s strings', () => {
|
|
93
|
+
const fixtures = load();
|
|
94
|
+
assert.deepEqual(leaksIn(fixtures), []);
|
|
95
|
+
// and the checks had something to check
|
|
96
|
+
const all = fixtures.flatMap(({ file, data }) => records(data, file));
|
|
97
|
+
assert.ok(all.filter(({ o }) => S.isReportRecord(o)).length >= 10, 'the docket\'s reports, its rulings and the overlay\'s cards were all examined');
|
|
98
|
+
assert.ok(all.filter(({ o }) => S.isBlockerRecord(o)).length >= 9, 'and every blocker');
|
|
99
|
+
assert.ok(all.filter(({ o }) => o.title === S.TASK_TITLE).length >= 10, 'and the security backlog');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('the checks are not vacuous: one body put back, one title restored, one host named — each is caught', () => {
|
|
103
|
+
const docket = () => load().map((f) => (f.file === 'government__docket.json' ? f : null)).filter(Boolean);
|
|
104
|
+
const withBody = docket();
|
|
105
|
+
withBody[0].data.matters.find((m) => m.source === 'redteam_reports').detail = 'An invented chain of steps against widget.js:10-20';
|
|
106
|
+
assert.ok(leaksIn(withBody).some((l) => l.includes('carries "detail"')));
|
|
107
|
+
const titled = docket();
|
|
108
|
+
titled[0].data.matters.find((m) => m.source === 'redteam_reports').title = 'Security report: a real finding';
|
|
109
|
+
assert.ok(leaksIn(titled).some((l) => l.includes('title is not withheld')));
|
|
110
|
+
const blocker = docket();
|
|
111
|
+
blocker[0].data.matters.find((m) => m.source === 'blockers').detail = 'Steps on the host';
|
|
112
|
+
assert.ok(leaksIn(blocker).some((l) => l.includes('a blocker carries its "detail"')));
|
|
113
|
+
const host = docket();
|
|
114
|
+
host[0].data.line = 'see builders.someones-instance.com';
|
|
115
|
+
assert.equal(leaksIn(host).length, 0, 'an unknown host is the redactor\'s to catch, not this list\'s');
|
|
116
|
+
assert.match(S.securityRedactorFor([host[0].data])(host[0].data).line, /instance\.example/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('this file never spells a prior-instance word whole, so the release leak scan stays green', () => {
|
|
120
|
+
assert.ok(PRIOR_INSTANCE_FLOOR.tokens.length > 0, 'the floor must name at least one word, or this check is vacuous');
|
|
121
|
+
const own = fs.readFileSync(fileURLToPath(import.meta.url), 'utf8').toLowerCase();
|
|
122
|
+
for (const w of PRIOR_INSTANCE_FLOOR.tokens) assert.ok(!own.includes(w.toLowerCase()), 'resolve prior-instance words from PRIOR_INSTANCE_FLOOR, never inline them');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('every committed fixture is already a fixed point of the security pass', () => {
|
|
126
|
+
const fixtures = load();
|
|
127
|
+
const secure = S.securityRedactorFor(fixtures.map((f) => f.data));
|
|
128
|
+
const dirty = fixtures.filter(({ data }) => JSON.stringify(secure(data)) !== JSON.stringify(data)).map((f) => f.file);
|
|
129
|
+
assert.deepEqual(dirty, [], 'run `node scripts/hall-preview/refresh-fixtures.js --scrub-only` and commit the result');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('the redactor: a report keeps its shape, a blocker its title, a security task its place', () => {
|
|
133
|
+
const secure = S.createSecurityRedactor({ instanceNames: ['someones'], securityGoals: ['9'] });
|
|
134
|
+
const out = secure({
|
|
135
|
+
matters: [
|
|
136
|
+
{ id: 'redteam_reports:7', source: 'redteam_reports', building: 'disputes', state: 'needs', short: 'high report', title: 'Security report: an invented finding', detail: 'Target: widget.js:117 — an invented PoC', proposer: { id: '3' }, acts: [{ key: 'confirm' }] },
|
|
137
|
+
{ id: 'blockers:8', source: 'blockers', title: 'Invented host step: set frame-ancestors on the widget proxy', detail: 'an invented runbook' },
|
|
138
|
+
{ id: 'blockers:9', source: 'blockers', title: 'Decide how far modules are trusted', detail: 'Options a, b, c' },
|
|
139
|
+
],
|
|
140
|
+
history: [{ id: 'rulings:5', source: 'rulings', title: 'Security report: an invented ruling', at: '2026-09-01' }],
|
|
141
|
+
tasks: [
|
|
142
|
+
{ id: '1', goal_id: '9', title: 'An invented security task', description: 'an invented detail', touches: ['src/widget.js'], status: 'ready' },
|
|
143
|
+
{ id: '2', goal_id: '4', title: 'Draw the city', description: 'see builders.someones.com and the someones repo', status: 'ready' },
|
|
144
|
+
],
|
|
145
|
+
});
|
|
146
|
+
assert.deepEqual(out.matters[0], { id: 'redteam_reports:7', source: 'redteam_reports', building: 'disputes', state: 'needs', short: 'high report', title: S.REPORT_TITLE, acts: [{ key: 'confirm' }] });
|
|
147
|
+
assert.deepEqual(out.matters[1], { id: 'blockers:8', source: 'blockers', title: S.BLOCKER_TITLE });
|
|
148
|
+
assert.deepEqual(out.matters[2], { id: 'blockers:9', source: 'blockers', title: 'Decide how far modules are trusted' }, 'a harmless title stays; the body never does');
|
|
149
|
+
assert.equal(out.history[0].title, S.REPORT_TITLE);
|
|
150
|
+
assert.deepEqual(out.tasks[0], { id: '1', goal_id: '9', title: S.TASK_TITLE, description: '', touches: [], status: 'ready' });
|
|
151
|
+
assert.equal(out.tasks[1].description, 'see builders.instance.example and the instance-name repo', 'a customer\'s domain and name');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('the WRITER redacts: refreshFixtures never puts a red-team body or a blocker body on disk', async () => {
|
|
155
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hall-fixtures-'));
|
|
156
|
+
try {
|
|
157
|
+
const docket = { matters: [
|
|
158
|
+
{ id: 'redteam_reports:7', source: 'redteam_reports', state: 'needs', title: 'Security report: an invented finding about widgets', detail: 'an invented chain of steps' },
|
|
159
|
+
{ id: 'blockers:8', source: 'blockers', state: 'needs', title: 'Host step', detail: 'an invented host runbook' },
|
|
160
|
+
] };
|
|
161
|
+
const api = { request: async (_m, p) => (p === '/government/docket' ? { ok: true, status: 200, data: docket } : { ok: false, status: 404 }) };
|
|
162
|
+
await refreshFixtures({ api, dir, snapshots: [['government/docket', 'government__docket']], log: () => {} });
|
|
163
|
+
const text = fs.readFileSync(path.join(dir, 'government__docket.json'), 'utf8');
|
|
164
|
+
for (const s of ['an invented chain', 'an invented host runbook', 'about widgets']) assert.ok(!text.includes(s), `${s} must never reach the fixture directory`);
|
|
165
|
+
assert.ok(text.includes(S.REPORT_TITLE));
|
|
166
|
+
} finally {
|
|
167
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
@@ -297,6 +297,50 @@ test('the room frames BOTH figures the viewer upgrades', () => {
|
|
|
297
297
|
assert.match(inner[1], /border:\s*0/);
|
|
298
298
|
});
|
|
299
299
|
|
|
300
|
+
// ---- 7b. the figure follows the theme (task 1003639) ----------------------
|
|
301
|
+
//
|
|
302
|
+
// mermaid-cli renders ONE light figure per diagram, at deploy, with `-b white`
|
|
303
|
+
// baked into the SVG root. On the hall's dark mode the canvas was a white plate
|
|
304
|
+
// on black. The fix is CSS, so it is silent in every test that does not look:
|
|
305
|
+
// a lost twin is a dark page that is right after shell.js runs and wrong on
|
|
306
|
+
// first paint (or the reverse), and a lost backgroundColor line brings the
|
|
307
|
+
// plate back as a black one. Rendered and measured for the ship: 16.1:1 on a
|
|
308
|
+
// node, 11.4:1 on an edge label, 18.8:1 on the note, dark, at 137%.
|
|
309
|
+
|
|
310
|
+
// The body of `<sel> { … }` inside the media block's braces, comments stripped.
|
|
311
|
+
const mediaRule = (css, sel) => {
|
|
312
|
+
const esc = sel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
313
|
+
const m = decomment(css).match(new RegExp(`@media \\(prefers-color-scheme: dark\\)\\s*\\{\\s*${esc}\\s*\\{([^}]*)\\}`));
|
|
314
|
+
return m ? m[1] : null;
|
|
315
|
+
};
|
|
316
|
+
const TURN = /^\s*filter:\s*invert\(1\) hue-rotate\(180deg\);\s*$/;
|
|
317
|
+
|
|
318
|
+
test('dark turns the canvas figure, in both twins, byte-identical', () => {
|
|
319
|
+
const sw = rule(viewerCss, 'html[data-theme="dark"] .dgv__pan svg');
|
|
320
|
+
const media = mediaRule(viewerCss, 'html:not([data-theme]) .dgv__pan svg');
|
|
321
|
+
assert.ok(sw, 'the [data-theme] switch shell.js owns turns the figure');
|
|
322
|
+
assert.ok(media, 'and so does the media block, which is first paint before shell.js runs');
|
|
323
|
+
assert.equal(media, sw, 'the twins are byte-identical, or the two paints disagree');
|
|
324
|
+
assert.match(sw, TURN, 'invert, then turn the hues back round: dark ink reads light, a purple edge stays purple');
|
|
325
|
+
assert.doesNotMatch(rule(viewerCss, '.dgv__pan svg'), /filter/, 'light keeps the render exactly as mermaid drew it');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('the fallback PNG turns with it, on BOTH figures the viewer upgrades', () => {
|
|
329
|
+
const both = (pre) => `${pre} .primer-figure img, ${pre} figure.diagram img`;
|
|
330
|
+
const sw = rule(roomCss, both('html[data-theme="dark"]'));
|
|
331
|
+
const media = mediaRule(roomCss, both('html:not([data-theme])'));
|
|
332
|
+
assert.ok(sw && media, 'the PNG shows before the SVG arrives and whenever it cannot — a dark reader must not get a white flash');
|
|
333
|
+
assert.equal(media, sw, 'byte-identical twins');
|
|
334
|
+
assert.match(sw, TURN, 'the same turn as the canvas, so the two paths read as one figure');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('the viewer drops the white ground mermaid bakes into the SVG', () => {
|
|
338
|
+
assert.match(viewerJs, /parsed\.style\.backgroundColor = '';/,
|
|
339
|
+
'without it the inverted figure is a black plate on the dark canvas instead of a white one');
|
|
340
|
+
assert.ok(viewerJs.indexOf("parsed.style.backgroundColor = '';") < viewerJs.indexOf('buildViewer(img, document.importNode(parsed'),
|
|
341
|
+
'and it happens before the SVG is imported into the canvas');
|
|
342
|
+
});
|
|
343
|
+
|
|
300
344
|
// ---- 8. every room has exactly one h1 -------------------------------------
|
|
301
345
|
|
|
302
346
|
const ROOMS = [['primer.html', primerHtml], ['diagrams.html', diagramsHtml], ['roadmap.html', roadmapHtml], ['ranks.html', ranksHtml]];
|
|
@@ -280,9 +280,13 @@ test('the harness feeds: rollups for every goal and the category set', () => {
|
|
|
280
280
|
const goals = readJson('goals.json').goals;
|
|
281
281
|
const rollups = readJson('goals__rollups.json').rollups;
|
|
282
282
|
const byGoal = new Set(rollups.map((r) => String(r.goal_id)));
|
|
283
|
-
|
|
283
|
+
// every goal a page draws a rollup for (an archived goal is a pill, no counters):
|
|
284
|
+
// the snapshot of the live read (task 1004685) carries no row for the archived ones
|
|
285
|
+
for (const g of goals.filter((x) => x.status !== 'archived')) assert.ok(byGoal.has(String(g.id)), `goal ${g.id} has a rollup row`);
|
|
284
286
|
assert.ok(rollups.some((r) => r.next_ready_id), 'at least one row has a claimable next task');
|
|
285
|
-
|
|
287
|
+
// the blocked-only goal was a hand-made row; the live snapshot (task 1004685) has
|
|
288
|
+
// none, so the harness shows a goal with blocked work beside ready work instead
|
|
289
|
+
assert.ok(rollups.some((r) => r.blocked > 0), 'at least one goal holds blocked work');
|
|
286
290
|
assert.ok(goals.some((g) => g.subtitle), 'some goals carry a subtitle');
|
|
287
291
|
assert.ok(goals.some((g) => g.category && g.category.slug), 'some goals carry a category');
|
|
288
292
|
const cats = readJson('work-categories.json').categories;
|
package/tests/hall_street.mjs
CHANGED
|
@@ -487,7 +487,7 @@ test('the page holds the prototype\'s header, the street, the readings and the c
|
|
|
487
487
|
assert.match(html, /<a class="street-mark" id="streetHome" href="\/builders\/city"[^>]*>\{\{productName\}\}</);
|
|
488
488
|
for (const id of ['hudNeeds', 'hudMoving', 'hudShare', 'streetMode', 'streetSigns', 'streetPins', 'streetDoors', 'streetCard']) assert.match(html, new RegExp(`id="${id}"`), id);
|
|
489
489
|
const scripts = [...html.matchAll(/<script src="\/builders\/([a-z-]+)\.js/g)].map((m) => m[1]);
|
|
490
|
-
assert.deepEqual(scripts, ['shell-trail', 'craft-mode', 'shell', 'palette', 'city-draw', 'street-draw', 'street-card', 'street-inside', 'street-desk', 'street-amend', 'street'], 'the hall shell (task 1004544), the shared kit, then the card (task 1004528), the interiors (task 1004526), the floor desk (task 1004558) and the drafted amendment (task 1004530)');
|
|
490
|
+
assert.deepEqual(scripts.filter((s) => !/^street-(deck|office)$/.test(s)), ['shell-trail', 'craft-mode', 'shell', 'palette', 'gov-living', 'gov-feeds', 'city-draw', 'street-draw', 'street-card', 'street-inside', 'street-depth', 'street-desk', 'street-amend', 'street'], 'the hall shell (task 1004544), the living model and its one reader (task 1004685: city-draw.js counts with it, so it loads first), the shared kit, then the card (task 1004528), the interiors (task 1004526), the living street\'s exterior (street-depth.js), the floor desk (task 1004558) and the drafted amendment (task 1004530); the deck rides beside them');
|
|
491
491
|
assert.equal(City.STREET_HREF, '/builders/street', 'the city\'s door is this page');
|
|
492
492
|
});
|
|
493
493
|
|
|
@@ -582,6 +582,7 @@ async function bootPage({ feeds = {}, stored = null, osDark = true, hash = '', s
|
|
|
582
582
|
vm.runInContext(read('street-draw.js'), ctx);
|
|
583
583
|
vm.runInContext(read('street-card.js'), ctx);
|
|
584
584
|
vm.runInContext(read('street-inside.js'), ctx);
|
|
585
|
+
vm.runInContext(read('street-depth.js'), ctx);
|
|
585
586
|
vm.runInContext(read('street-desk.js'), ctx);
|
|
586
587
|
vm.runInContext(read('street.js'), ctx);
|
|
587
588
|
await window.GovStreetPage.ready;
|
|
@@ -685,7 +686,7 @@ test('no two shown labels meet on a dense docket at 1440, 1024, 390 and 320 wide
|
|
|
685
686
|
|
|
686
687
|
test('the header counts wear the pins\' colours, so they double as the legend', () => {
|
|
687
688
|
const html = read('street.html');
|
|
688
|
-
assert.match(html, /<span class="street-hud__dot" id="hudDot" aria-hidden="true"><\/span>\s*<span class="street-label">
|
|
689
|
+
assert.match(html, /<span class="street-hud__dot" id="hudDot" aria-hidden="true"><\/span>\s*<span class="street-label">to do<\/span>/);
|
|
689
690
|
assert.match(html, /<span class="street-hud__dot street-hud__dot--moving" aria-hidden="true"><\/span>\s*<span class="street-label">moving<\/span>/);
|
|
690
691
|
const css = read('street.css').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
691
692
|
assert.match(css, /\.street-hud__dot \{[^}]*border: 1\.5px solid var\(--accent\)/, 'needs you is orange, a ring while nothing waits');
|
|
@@ -735,3 +736,18 @@ test('arriving through a door marks that building, and a failed docket says so',
|
|
|
735
736
|
assert.ok(!q.calls.some((c) => /earnings/.test(c)), 'no viewer, no earnings read');
|
|
736
737
|
assert.equal(q.byId.get('hudShare').textContent, '—');
|
|
737
738
|
});
|
|
739
|
+
|
|
740
|
+
// task 1004718: a wide window scales the street to its width, and the height
|
|
741
|
+
// it does not need goes under the readings, never as empty sky over the street.
|
|
742
|
+
test('a stage taller than the street gives its spare height to the foot, not the sky', async () => {
|
|
743
|
+
const p = await bootPage({ feeds: FEEDS([]), width: 1440 });
|
|
744
|
+
const win = p.byId.get('streetWindow'), sizer = p.byId.get('streetSizer');
|
|
745
|
+
win.clientHeight = 900;
|
|
746
|
+
p.key('resize', {});
|
|
747
|
+
const h = parseFloat(sizer.style.height);
|
|
748
|
+
assert.ok(h < 900, 'the street is width-bound here');
|
|
749
|
+
assert.equal(win.style.flex, '0 0 ' + h + 'px', 'the window shrinks to the street, so the readings sit right under it');
|
|
750
|
+
win.clientHeight = 300;
|
|
751
|
+
p.key('resize', {});
|
|
752
|
+
assert.equal(win.style.flex, '', 'a height-bound street fills its window, nothing to give back');
|
|
753
|
+
});
|
|
@@ -258,7 +258,7 @@ async function bootPage({ hash = '#building=review', cons = constitution(), answ
|
|
|
258
258
|
};
|
|
259
259
|
const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date, console, encodeURIComponent, decodeURIComponent, isNaN, isFinite, Map, setTimeout, clearTimeout });
|
|
260
260
|
ctx.globalThis = ctx;
|
|
261
|
-
for (const f of ['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-desk.js', 'street-amend.js', 'street.js']) vm.runInContext(read(f), ctx);
|
|
261
|
+
for (const f of ['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-depth.js', 'street-desk.js', 'street-amend.js', 'street.js']) vm.runInContext(read(f), ctx);
|
|
262
262
|
await window.GovStreetPage.ready;
|
|
263
263
|
await tick();
|
|
264
264
|
return { window, byId, focused, gets, posts, history };
|