@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/scripts/gds/pulse.js
CHANGED
|
@@ -43,9 +43,10 @@
|
|
|
43
43
|
// by anyone reading the log or grepping the table.
|
|
44
44
|
// - Tasks return to status='ready', no credits, just like a manual release.
|
|
45
45
|
// - Affected builders get one broadcast each via scripts/discord/post.js
|
|
46
|
-
// (Discord #ship-
|
|
47
|
-
// #
|
|
48
|
-
//
|
|
46
|
+
// (Discord #ship-feed, the builders' channel — task 1002846 keeps the
|
|
47
|
+
// owner-facing #ship-news for shipped features and fixes; outbound moved
|
|
48
|
+
// off Google Chat per ADR 0032 / #607; scripts/chat/ is inbound-only now).
|
|
49
|
+
// Post failure is non-fatal (mirrors ship.js).
|
|
49
50
|
// - Exit codes: 0 = success (including "nothing stale"); 1 = fatal error.
|
|
50
51
|
|
|
51
52
|
const fs = require('node:fs');
|
|
@@ -106,11 +107,13 @@ function parseThresholdHours(args) {
|
|
|
106
107
|
|
|
107
108
|
// Post a single broadcast about an auto-released claim. Best-effort —
|
|
108
109
|
// returns { ok, error } and never throws. Mirrors the pattern in ship.js
|
|
109
|
-
// (synchronous execFile via the local scripts/discord/post.js helper
|
|
110
|
-
//
|
|
110
|
+
// (synchronous execFile via the local scripts/discord/post.js helper, on
|
|
111
|
+
// #ship-feed: a reclaim nudge is builder housekeeping, never owner news —
|
|
112
|
+
// task 1002846; outbound moved off Google Chat per ADR 0032). The line opens
|
|
113
|
+
// with `@name`, never `#<id>`, so the applause bot ignores it.
|
|
111
114
|
function postDiscordPing(text) {
|
|
112
115
|
try {
|
|
113
|
-
execFileSync('node', ['scripts/discord/post.js', text], {
|
|
116
|
+
execFileSync('node', ['scripts/discord/post.js', '--channel', 'ship_feed', text], {
|
|
114
117
|
cwd: REPO_ROOT,
|
|
115
118
|
stdio: 'pipe',
|
|
116
119
|
});
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
// that gets muted, and this one is aimed at the surface a person actually reads.
|
|
36
36
|
|
|
37
37
|
const NO_ARTIFACT_SOURCE = 'no-artifact-close';
|
|
38
|
+
const { postWithAsk } = require('./ask.js');
|
|
38
39
|
|
|
39
40
|
// One blocker per task. Deliberately NOT keyed on the builder or the run: two builders
|
|
40
41
|
// hitting the same stuck task should land on the same blocker, not race to file two.
|
|
@@ -85,6 +86,22 @@ function escalationBody({ taskId, title, floor, summary, notes, builder }) {
|
|
|
85
86
|
return lines.join('\n');
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
// The card the archon answers (task 1004704): the escalation as an ask, so the
|
|
90
|
+
// review queue shows one short question instead of the body above. The body
|
|
91
|
+
// still carries everything; the card is what a person answers at a glance.
|
|
92
|
+
function escalationAsk(taskId) {
|
|
93
|
+
return {
|
|
94
|
+
question: `Close task ${taskId} with no code to grade?`,
|
|
95
|
+
options: [
|
|
96
|
+
{ key: 'close', label: 'Close it', result: 'It closes as done, without a grade.', act: 'go ahead' },
|
|
97
|
+
{ key: 'return', label: 'Send it back', result: 'The claim is released and the task returns to ready.', act: 'need more' },
|
|
98
|
+
],
|
|
99
|
+
recommend: { key: 'close', why: 'The builder says no code was needed; read their reason first.' },
|
|
100
|
+
waits_on: 'lead:release',
|
|
101
|
+
if_nobody_answers: 'The task stays at completed and its builder is not paid.',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
88
105
|
// The terminating refusal. THE ONE INVARIANT: these lines must never send the builder to
|
|
89
106
|
// --regrade, because --regrade on an empty diff sends them straight back to --allow-empty.
|
|
90
107
|
// That mutual advice IS the bug (task 1003950). Pure + exported so a test can assert the
|
|
@@ -108,7 +125,8 @@ function noArtifactRefusalLines({ taskId, floor, escalation }) {
|
|
|
108
125
|
// Fail-open: the close is still blocked, so the builder must not be told it worked.
|
|
109
126
|
const why = escalation && escalation.reason ? ` (${escalation.reason})` : '';
|
|
110
127
|
lines.push(`Could not file the escalation automatically${why}.`);
|
|
111
|
-
lines.push('File it by hand so this does not sit silently — post a blocker naming this task
|
|
128
|
+
lines.push('File it by hand so this does not sit silently — post a blocker naming this task,');
|
|
129
|
+
lines.push('with an ask (check the card first: node scripts/gds/ask.js check <file>):');
|
|
112
130
|
lines.push(' node scripts/gds/api.js POST /blockers --body-file <path-to-json>');
|
|
113
131
|
}
|
|
114
132
|
lines.push('');
|
|
@@ -124,12 +142,12 @@ async function fileNoArtifactEscalation({ taskId, title, floor, summary, notes,
|
|
|
124
142
|
const post = call || require('./cli-lib').apiCall;
|
|
125
143
|
let r;
|
|
126
144
|
try {
|
|
127
|
-
r = await post('POST', '/api/bongos/blockers', {
|
|
145
|
+
r = await postWithAsk((body) => post('POST', '/api/bongos/blockers', body), {
|
|
128
146
|
title: escalationTitle(taskId, title),
|
|
129
147
|
body_md: escalationBody({ taskId, title, floor, summary, notes, builder }),
|
|
130
148
|
source: NO_ARTIFACT_SOURCE,
|
|
131
149
|
source_ref: escalationSourceRef(taskId),
|
|
132
|
-
});
|
|
150
|
+
}, escalationAsk(taskId));
|
|
133
151
|
} catch (err) {
|
|
134
152
|
return { filed: false, reason: `POST /blockers threw: ${err && err.message}` };
|
|
135
153
|
}
|
|
@@ -143,6 +161,7 @@ module.exports = {
|
|
|
143
161
|
escalationSourceRef,
|
|
144
162
|
escalationTitle,
|
|
145
163
|
escalationBody,
|
|
164
|
+
escalationAsk,
|
|
146
165
|
noArtifactRefusalLines,
|
|
147
166
|
fileNoArtifactEscalation,
|
|
148
167
|
};
|
|
@@ -96,10 +96,11 @@ async function mergeAndDeploy({ taskId, summary, advanceToMerge, skipMerge, dbOn
|
|
|
96
96
|
// to fire IN ADDITION to the server's, so a single ship announced FOUR
|
|
97
97
|
// messages (local #ship-news + #ship-feed, then the server's #ship-news +
|
|
98
98
|
// #ship-feed). Defer unconditionally so exactly one broadcast fires — from
|
|
99
|
-
// the server, after the deploy lands. (
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// server-side in ship-broadcast.postShipBroadcast,
|
|
99
|
+
// the server, after the deploy lands. (That one broadcast is the #ship-feed
|
|
100
|
+
// line, plus a plain #ship-news headline for a feature or a bug — the kind
|
|
101
|
+
// filter in chooseBroadcastTargets, task 1002846.) The security-sensitive
|
|
102
|
+
// fail-closed routing lives server-side in ship-broadcast.postShipBroadcast,
|
|
103
|
+
// so it is preserved.
|
|
103
104
|
console.log(' Discord broadcast: deferred to the server — it posts #ship-feed after the deploy lands.');
|
|
104
105
|
}
|
|
105
106
|
return merged;
|
|
@@ -176,19 +176,31 @@ function removeProbe(repoRoot, dir) {
|
|
|
176
176
|
// (task 1004584). It only stands down when the pre-merge base lacks the file
|
|
177
177
|
// too, so a branch that DELETES fitness.js from the core is still measured
|
|
178
178
|
// (and blocked as unmeasurable) rather than waved through.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
//
|
|
180
|
+
// It asks GIT, never the disk (task 1004728). A vendored instance keeps an
|
|
181
|
+
// untracked `scripts -> node_modules/@bongos/core/scripts` link in its main
|
|
182
|
+
// checkout (hidden by .git/info/exclude; grading needs it), so the core's own
|
|
183
|
+
// fitness.js IS on disk there. The disk check read that as "this repo carries
|
|
184
|
+
// the gate", ran the core's fitness against a tree with no core in it, and
|
|
185
|
+
// blocked every land on toast. What the repo carries is what it tracks.
|
|
186
|
+
function tracksFitness(repoRoot, rev) {
|
|
182
187
|
try {
|
|
183
|
-
execFileSync('git', ['cat-file', '-e', `${
|
|
188
|
+
execFileSync('git', ['cat-file', '-e', `${rev}:scripts/gds/fitness.js`], {
|
|
184
189
|
cwd: repoRoot, stdio: 'ignore', shell: false,
|
|
185
190
|
});
|
|
186
|
-
return true;
|
|
191
|
+
return true;
|
|
187
192
|
} catch (_) {
|
|
188
|
-
return false;
|
|
193
|
+
return false;
|
|
189
194
|
}
|
|
190
195
|
}
|
|
191
196
|
|
|
197
|
+
function gateApplies(repoRoot, baseSha) {
|
|
198
|
+
if (tracksFitness(repoRoot, 'HEAD')) return true;
|
|
199
|
+
if (!baseSha) return true;
|
|
200
|
+
// the base had it — this merge removed it; measure (and block)
|
|
201
|
+
return tracksFitness(repoRoot, baseSha);
|
|
202
|
+
}
|
|
203
|
+
|
|
192
204
|
function gateMergeResult({ repoRoot, baseSha, log = console.log }) {
|
|
193
205
|
if (!gateApplies(repoRoot, baseSha)) {
|
|
194
206
|
log(' fitness: this repo carries no scripts/gds/fitness.js (a vendored instance) — gate not applicable');
|
|
@@ -29,6 +29,7 @@ const HALT_NOTES = Object.freeze({
|
|
|
29
29
|
ship_check_stale: 'The automatic apply stopped because the pre-ship checks named something stale it may not fix by itself.',
|
|
30
30
|
grader_unavailable: 'The quality review could not run, so the automatic apply stopped. The round waits for the next try or a person.',
|
|
31
31
|
grade_failed: 'The quality review did not pass the automatic apply. The round waits for a person.',
|
|
32
|
+
worker_stopped: 'The automatic apply stopped before it finished, without saying why. The round waits for the next try or a person.',
|
|
32
33
|
outside_page: 'The automatic apply would have changed a file outside this page, so it stopped.',
|
|
33
34
|
});
|
|
34
35
|
|
|
@@ -23,7 +23,13 @@ node scripts/hall-preview/server.js # neutral pack, port 4600
|
|
|
23
23
|
| `--empty` | drain every array in every fixture — the empty-state screenshot pass |
|
|
24
24
|
| `--fixture-me` | don't read your own account live — serve the checked-in `me.json` snapshot (see below) |
|
|
25
25
|
|
|
26
|
-
Writes are not wired: any `POST/PATCH/DELETE` answers `501 preview_readonly
|
|
26
|
+
Writes are not wired: any `POST/PATCH/DELETE` answers `501 preview_readonly` — except
|
|
27
|
+
the canned writes in `server.js` and, since task 1004685, the five per-builder pref
|
|
28
|
+
PATCHes (`/me/interaction|wandering|model-allocation|render|skill-prefs`), which
|
|
29
|
+
[`pref-writes.js`](pref-writes.js) answers with the real shapers over an in-memory copy
|
|
30
|
+
and lays over later reads (logged as `recorded: PATCH …`; a restart forgets them).
|
|
31
|
+
`?as=nobody` on any hall page views the hall as a builder who holds no governing seat
|
|
32
|
+
(the living-city prototype; session-sticky, `?as=me` clears it).
|
|
27
33
|
A GET with no fixture answers 404 and logs
|
|
28
34
|
`no fixture: <path>` — that log line names the exact file to add.
|
|
29
35
|
|
|
@@ -91,6 +97,40 @@ node scripts/hall-preview/refresh-fixtures.js --scrub-only
|
|
|
91
97
|
`tests/hall_preview_scrub.mjs` fails while any committed fixture still carries a real
|
|
92
98
|
identity.
|
|
93
99
|
|
|
100
|
+
Two more things a run does (task 1004685). It **keeps each builder's number**: the
|
|
101
|
+
committed `builders__roster.json` is joined to the live roster by builder id, so
|
|
102
|
+
`builder-12` is still `builder-12` after a refresh and a new fixture lines up with the
|
|
103
|
+
old ones. And it ends with a **prose pass**: every person the run met in a field is
|
|
104
|
+
replaced wherever a sentence names them too (by login, display name, the first name a
|
|
105
|
+
login starts with, or a nickname cut from an all-caps login), because the governor
|
|
106
|
+
docket's titles and details name people outright. The pass only knows people a field
|
|
107
|
+
named, so a first name nobody's field carries (a person mentioned only in prose) can
|
|
108
|
+
still slip through — read the governor fixtures' diff before committing them.
|
|
109
|
+
|
|
110
|
+
**No security content is ever checked in either** (task 1004685, after its security
|
|
111
|
+
review). The governor docket is read at a rank floor and carries the open red-team
|
|
112
|
+
reports and the owner's blockers, so every run — and `--scrub-only` — ends with
|
|
113
|
+
`createSecurityRedactor` (`scrub-identity.js`): a red-team record keeps only its id,
|
|
114
|
+
state, severity bucket and acts under the title "Security report — details withheld";
|
|
115
|
+
a blocker keeps no body and no title that names a secret, a host step or a customer
|
|
116
|
+
project; the security backlog's tasks (flagged `security_sensitive`, or in a goal of
|
|
117
|
+
category `security`) keep their place but not their words; any prose naming a
|
|
118
|
+
credential, host config or the code a live report points at is emptied; and a
|
|
119
|
+
customer instance's own domain becomes `instance.example`.
|
|
120
|
+
`tests/hall_preview_security.mjs` fails while any committed fixture is not already a
|
|
121
|
+
fixed point of that pass.
|
|
122
|
+
|
|
123
|
+
**The governor reads are live too.** `government/docket`, `constitution`,
|
|
124
|
+
`board/items`, `board/genesis-stages`, `goals/rollups`, `blockers`, `autonomy/fence`,
|
|
125
|
+
`moving`, `sessions/search`, `learnings` (rows only) and the viewer's
|
|
126
|
+
`me/{interaction,wandering,model-allocation}` are in the snapshot list. `/tasks` is the
|
|
127
|
+
one path read two ways: with no query it is the ready list (`tasks.json`), with
|
|
128
|
+
`?status=active` it is the work being built (`tasks__status-active.json`); the server's
|
|
129
|
+
`QUERY_FIXTURES` table picks between them. The government's own reads (`government/laws`,
|
|
130
|
+
`leads`, `treasury`, `presets`, `counts`) are fixtures in the same shape the routes
|
|
131
|
+
answer; the living city and street run on them alone (task 1004717 deleted the prototype
|
|
132
|
+
overlay). A concept no table holds is not drawn: never add sample data for one.
|
|
133
|
+
|
|
94
134
|
## The evidence matrix (per page-redesign ship)
|
|
95
135
|
|
|
96
136
|
Screenshot every changed page at **{light, dark} × {1280w, 390w}**, plus one
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// scripts/hall-preview/pref-writes.js — the harness's SETTINGS RECORDER (task
|
|
2
|
+
// 1004685, the living-city prototype's My agents page).
|
|
3
|
+
//
|
|
4
|
+
// WHY. The harness has no database, so every write answers 501 or one fixed canned
|
|
5
|
+
// fixture. My agents' whole job is changing your own agent settings, and a page
|
|
6
|
+
// that can only ever show "not saved" cannot be reviewed by looking (the trap
|
|
7
|
+
// modules/hall-ui/CLAUDE.md names). So the five per-builder pref PATCH routes are
|
|
8
|
+
// answered HERE, by the same pure shapers the real routes call
|
|
9
|
+
// (modules/builder-settings/routes/settings.js: validatePatch → applyPatch →
|
|
10
|
+
// describeForApi), over an in-memory copy of the served builder's prefs. A 422 is
|
|
11
|
+
// the real route's 422; a 200 is the body the real route would send, contract
|
|
12
|
+
// text included. The recorded state is then laid over GET /me and GET /me/<slice>,
|
|
13
|
+
// so a reload shows the change too.
|
|
14
|
+
//
|
|
15
|
+
// What it is NOT: storage. Nothing is written to disk and a restart forgets
|
|
16
|
+
// everything; each recorded write is logged so the harness console shows what the
|
|
17
|
+
// page sent. Only these five routes are recorded — every other write is refused
|
|
18
|
+
// exactly as before.
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
24
|
+
const lib = (name) => require(path.join(ROOT, 'modules', 'builder-settings', name));
|
|
25
|
+
const interaction = lib('interaction-prefs');
|
|
26
|
+
const wandering = lib('wandering-prefs');
|
|
27
|
+
const models = lib('model-alloc-prefs');
|
|
28
|
+
const render = lib('render-prefs');
|
|
29
|
+
const skills = lib('skill-prefs');
|
|
30
|
+
|
|
31
|
+
// apiPath → the /me slice it answers for (null: the skill prefs, not on /me)
|
|
32
|
+
const ROUTES = {
|
|
33
|
+
'me/interaction': 'interaction',
|
|
34
|
+
'me/wandering': 'wandering',
|
|
35
|
+
'me/model-allocation': 'model_allocation',
|
|
36
|
+
'me/render': 'render',
|
|
37
|
+
'me/skill-prefs': null,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function createPrefRecorder({ log = console.log } = {}) {
|
|
41
|
+
let behavior = null; // the behavior_prefs jsonb, as the routes see it
|
|
42
|
+
let skillPrefs = null; // the skill_model_prefs jsonb
|
|
43
|
+
let rank = null;
|
|
44
|
+
let instanceModels = {};
|
|
45
|
+
const touched = new Set();
|
|
46
|
+
|
|
47
|
+
// Rebuild the stored blobs from what the served /me says, once.
|
|
48
|
+
function seed(me, skillFixture) {
|
|
49
|
+
if (behavior) return;
|
|
50
|
+
const m = me || {};
|
|
51
|
+
behavior = {};
|
|
52
|
+
if (m.interaction && m.interaction.stored) behavior[interaction.RESERVED_KEY] = { ...m.interaction.stored };
|
|
53
|
+
if (m.wandering && m.wandering.override_level != null) behavior[wandering.RESERVED_KEY] = m.wandering.override_level;
|
|
54
|
+
if (m.model_allocation && m.model_allocation.override) behavior[models.RESERVED_KEY] = { ...m.model_allocation.override };
|
|
55
|
+
if (m.render && m.render.override_level) behavior[render.RESERVED_KEY] = m.render.override_level;
|
|
56
|
+
rank = (m.builder && m.builder.rank) || (m.wandering && m.wandering.rank) || null;
|
|
57
|
+
instanceModels = (m.model_allocation && m.model_allocation.instance_default) || {};
|
|
58
|
+
skillPrefs = { ...((skillFixture && skillFixture.prefs) || {}) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function describe(slice) {
|
|
62
|
+
if (slice === 'interaction') return interaction.describeForApi(behavior);
|
|
63
|
+
if (slice === 'wandering') return wandering.describeForApi(behavior, rank);
|
|
64
|
+
if (slice === 'model_allocation') return models.describeForApi(behavior, instanceModels);
|
|
65
|
+
if (slice === 'render') return render.describeForApi(behavior);
|
|
66
|
+
return {
|
|
67
|
+
prefs: skillPrefs, effective: skills.listEffective(skillPrefs), defaults: skills.SKILL_DEFAULTS,
|
|
68
|
+
known_skills: skills.KNOWN_SKILLS, allowed_models: skills.ALLOWED_MODELS,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function handles(method, apiPath) {
|
|
73
|
+
return method === 'PATCH' && Object.prototype.hasOwnProperty.call(ROUTES, apiPath);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// → { status, body }, the real route's answer
|
|
77
|
+
function write(apiPath, body, { me, skillFixture } = {}) {
|
|
78
|
+
seed(me, skillFixture);
|
|
79
|
+
const slice = ROUTES[apiPath];
|
|
80
|
+
const b = body || {};
|
|
81
|
+
const invalid = (errors) => ({ status: 422, body: { error: { code: 'invalid_patch', message: 'invalid_patch', details: { errors } } } });
|
|
82
|
+
let v;
|
|
83
|
+
if (slice === 'interaction') {
|
|
84
|
+
v = interaction.validatePatch(b);
|
|
85
|
+
if (!v.ok) return invalid(v.errors);
|
|
86
|
+
behavior = { ...behavior, [interaction.RESERVED_KEY]: interaction.applyPatch(interaction.storedProfile(behavior), v.patch) };
|
|
87
|
+
} else if (slice === 'wandering') {
|
|
88
|
+
v = wandering.validatePatch(b, { maxLevel: wandering.maxLevelForRank(rank) });
|
|
89
|
+
if (!v.ok) return invalid(v.errors);
|
|
90
|
+
behavior = wandering.applyPatch(behavior, v.patch);
|
|
91
|
+
} else if (slice === 'model_allocation') {
|
|
92
|
+
v = models.validatePatch(b);
|
|
93
|
+
if (!v.ok) return invalid(v.errors);
|
|
94
|
+
behavior = { ...behavior, [models.RESERVED_KEY]: models.applyPatch(models.storedOverride(behavior), v.patch) };
|
|
95
|
+
} else if (slice === 'render') {
|
|
96
|
+
v = render.validatePatch(b);
|
|
97
|
+
if (!v.ok) return invalid(v.errors);
|
|
98
|
+
behavior = render.applyPatch(behavior, v.patch);
|
|
99
|
+
} else {
|
|
100
|
+
v = skills.validatePatch(b);
|
|
101
|
+
if (!v.ok) return invalid(v.errors);
|
|
102
|
+
skillPrefs = skills.applyPatch(skillPrefs, v.patch);
|
|
103
|
+
}
|
|
104
|
+
touched.add(slice === null ? 'skills' : slice);
|
|
105
|
+
log(` recorded: PATCH ${apiPath} ${JSON.stringify(b)}`);
|
|
106
|
+
return { status: 200, body: describe(slice) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Lay the recorded state over a GET's payload; untouched reads pass through.
|
|
110
|
+
function overlay(apiPath, payload) {
|
|
111
|
+
if (!behavior || !payload || typeof payload !== 'object') return payload;
|
|
112
|
+
if (apiPath === 'me') {
|
|
113
|
+
const out = { ...payload };
|
|
114
|
+
// the render level is DERIVED from the interaction profile, so it moves with it
|
|
115
|
+
for (const s of ['interaction', 'wandering', 'model_allocation', 'render']) {
|
|
116
|
+
if (touched.has(s) || (s === 'render' && touched.has('interaction'))) out[s] = describe(s);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
const slice = ROUTES[apiPath];
|
|
121
|
+
if (slice === undefined) return payload;
|
|
122
|
+
const key = slice === null ? 'skills' : slice;
|
|
123
|
+
return touched.has(key) || (key === 'render' && touched.has('interaction')) ? describe(slice) : payload;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { handles, write, overlay };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Read a JSON body off the raw request (the harness mounts no body parser).
|
|
130
|
+
function readJsonBody(req) {
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
let raw = '';
|
|
133
|
+
req.setEncoding('utf8');
|
|
134
|
+
req.on('data', (c) => { raw += c; if (raw.length > 65536) req.destroy(); });
|
|
135
|
+
req.on('end', () => { try { resolve(raw ? JSON.parse(raw) : {}); } catch (_) { resolve(null); } });
|
|
136
|
+
req.on('error', () => resolve(null));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { createPrefRecorder, readJsonBody, ROUTES };
|
|
@@ -12,13 +12,18 @@
|
|
|
12
12
|
// snapshot goes through ./scrub-identity.js before it touches the disk: logins,
|
|
13
13
|
// names, avatars, GitHub ids and emails become stand-ins (`builder-3` / `Builder
|
|
14
14
|
// 3`), and the snapshotting account's own balance and spend become fixed values.
|
|
15
|
+
// Since task 1004685 a run also (1) keeps the numbers the committed roster already
|
|
16
|
+
// gives each builder, and (2) ends with a pass over the PROSE, replacing every person
|
|
17
|
+
// the run met in a field wherever a sentence names them (the governor docket does).
|
|
15
18
|
// Hand-edited a fixture? Re-run the same pass over the files on disk, no network:
|
|
16
19
|
// node scripts/hall-preview/refresh-fixtures.js --scrub-only
|
|
17
20
|
'use strict';
|
|
18
21
|
|
|
19
22
|
const fs = require('fs');
|
|
20
23
|
const path = require('path');
|
|
21
|
-
const {
|
|
24
|
+
const {
|
|
25
|
+
createScrubber, createTextScrubber, securityRedactorFor, isOwnAccountFixture, highestStandIn, rewriteScalars,
|
|
26
|
+
} = require('./scrub-identity');
|
|
22
27
|
|
|
23
28
|
const FIXTURES = path.join(__dirname, 'fixtures');
|
|
24
29
|
|
|
@@ -29,6 +34,16 @@ const trimList = (key, n) => (d) => (Array.isArray(d?.[key]) ? { ...d, [key]: d[
|
|
|
29
34
|
// profile charts' pager back for the same page until "total". Pin total to
|
|
30
35
|
// what the snapshot holds.
|
|
31
36
|
const selfPaged = (d) => (Array.isArray(d?.rows) && d.page ? { ...d, page: { ...d.page, total: d.rows.length } } : d);
|
|
37
|
+
// The living city counts learnings and dates them; it never shows a body, and the
|
|
38
|
+
// bodies are long prose about the host (task 1004685). Keep the row, drop the essay.
|
|
39
|
+
const trimLearnings = (d) => (Array.isArray(d?.learnings)
|
|
40
|
+
? { ...d, learnings: d.learnings.map(({ id, title, tags, task_id, created_at }) => ({ id, title, tags, task_id, created_at })) }
|
|
41
|
+
: d);
|
|
42
|
+
// The active list is read for WHERE work is moving (goal, module, discipline), not
|
|
43
|
+
// for what each task says; a description is the task's own long prose.
|
|
44
|
+
const trimTaskBodies = (d) => (Array.isArray(d?.tasks)
|
|
45
|
+
? { ...d, tasks: d.tasks.map(({ description, value_summary, ...rest }) => rest) }
|
|
46
|
+
: d);
|
|
32
47
|
const SNAPSHOTS = [
|
|
33
48
|
['me', 'me'],
|
|
34
49
|
['me/credits?limit=200', 'me__credits', selfPaged],
|
|
@@ -88,6 +103,40 @@ const SNAPSHOTS = [
|
|
|
88
103
|
// draw them) no timeline. `versions` above is the AUTHENTICATED list; this is
|
|
89
104
|
// the public one the page actually calls, and the two are not interchangeable.
|
|
90
105
|
['public/versions', 'public__versions'],
|
|
106
|
+
|
|
107
|
+
// THE GOVERNOR'S READS (task 1004685). The city and the street drew from
|
|
108
|
+
// hand-generated governor fixtures (tasks 1004524-1004580), so the harness only
|
|
109
|
+
// ever showed the reference design's sample council. These are the reads the
|
|
110
|
+
// pages make (city.js / street.js FEEDS), plus the ones the living city's model
|
|
111
|
+
// (modules/hall-ui/public/gov-living.js) takes: who is moving, the open blockers,
|
|
112
|
+
// the unattended-building switch, the sessions, the viewer's own agent settings.
|
|
113
|
+
['government/docket', 'government__docket'],
|
|
114
|
+
// the counts every surface prints, in one read (task 1004716)
|
|
115
|
+
['government/counts', 'government__counts'],
|
|
116
|
+
['government/constitution', 'government__constitution'],
|
|
117
|
+
// the project's laws in force (task 1004706)
|
|
118
|
+
['government/laws', 'government__laws'],
|
|
119
|
+
// who leads each building (task 1004709): the hub has seated no Lead yet
|
|
120
|
+
['government/leads', 'government__leads'],
|
|
121
|
+
// the pots each goal draws from, the reserve, the spend cap (task 1004712)
|
|
122
|
+
['government/treasury', 'government__treasury'],
|
|
123
|
+
// the strictness presets, and which one the laws in force amount to (task 1004715)
|
|
124
|
+
['government/presets', 'government__presets'],
|
|
125
|
+
['government/board/items', 'government__board__items'],
|
|
126
|
+
['government/board/genesis-stages', 'government__board__genesis-stages'],
|
|
127
|
+
['goals/rollups', 'goals__rollups'],
|
|
128
|
+
['blockers?limit=100', 'blockers'],
|
|
129
|
+
['autonomy/fence', 'autonomy__fence'],
|
|
130
|
+
['moving', 'moving'],
|
|
131
|
+
// the work being built, with its goal; served for /tasks?status=active only
|
|
132
|
+
// (server.js QUERY_FIXTURES), since /tasks with no query is the ready list above
|
|
133
|
+
['tasks?status=active&limit=200', 'tasks__status-active', trimTaskBodies],
|
|
134
|
+
['sessions/search?limit=100', 'sessions__search'],
|
|
135
|
+
// what the project has learned: the city counts it, and no page reads the bodies
|
|
136
|
+
['learnings', 'learnings', trimLearnings],
|
|
137
|
+
['me/interaction', 'me__interaction'],
|
|
138
|
+
['me/wandering', 'me__wandering'],
|
|
139
|
+
['me/model-allocation', 'me__model-allocation'],
|
|
91
140
|
];
|
|
92
141
|
|
|
93
142
|
// Every existing fixture's text — read so a run can number new stand-ins above the
|
|
@@ -101,13 +150,34 @@ function readFixtureTexts(dir) {
|
|
|
101
150
|
}
|
|
102
151
|
}
|
|
103
152
|
|
|
153
|
+
// continuitySeed — the people the committed roster already numbers, keyed by the
|
|
154
|
+
// builder id the live roster and the fixture share, so a re-run keeps builder-12 as
|
|
155
|
+
// builder-12 instead of renumbering every person in the snapshot (task 1004685). No
|
|
156
|
+
// roster on disk, or no live one, is an empty seed: the run numbers from scratch.
|
|
157
|
+
async function continuitySeed(api, dir) {
|
|
158
|
+
let onDisk;
|
|
159
|
+
try {
|
|
160
|
+
onDisk = JSON.parse(fs.readFileSync(path.join(dir, 'builders__roster.json'), 'utf8'));
|
|
161
|
+
} catch (_) { return []; }
|
|
162
|
+
const res = await api.request('GET', '/builders/roster', {});
|
|
163
|
+
if (!res || !res.ok || !Array.isArray(res.data?.builders) || !Array.isArray(onDisk?.builders)) return [];
|
|
164
|
+
const standIn = new Map();
|
|
165
|
+
for (const b of onDisk.builders) {
|
|
166
|
+
const m = /^builder-(\d+)$/i.exec(String(b?.github_login || ''));
|
|
167
|
+
if (m && b.id != null) standIn.set(String(b.id), Number(m[1]));
|
|
168
|
+
}
|
|
169
|
+
return res.data.builders
|
|
170
|
+
.filter((b) => b && b.id != null && standIn.has(String(b.id)))
|
|
171
|
+
.map((b) => ({ login: b.github_login, name: b.display_name, n: standIn.get(String(b.id)) }));
|
|
172
|
+
}
|
|
173
|
+
|
|
104
174
|
// refreshFixtures — GET each snapshot, scrub the people out of it, write it. The api,
|
|
105
175
|
// directory and logger are injected so tests/hall_preview_scrub.mjs drives this exact
|
|
106
176
|
// writer with no network and a temp directory.
|
|
107
177
|
async function refreshFixtures({ api, dir = FIXTURES, snapshots = SNAPSHOTS, log = console.log } = {}) {
|
|
108
178
|
fs.mkdirSync(dir, { recursive: true });
|
|
109
|
-
const scrubber = createScrubber({ firstNumber: highestStandIn(readFixtureTexts(dir)) + 1 });
|
|
110
|
-
|
|
179
|
+
const scrubber = createScrubber({ firstNumber: highestStandIn(readFixtureTexts(dir)) + 1, seed: await continuitySeed(api, dir) });
|
|
180
|
+
const fetched = [];
|
|
111
181
|
for (const [apiPath, name, trim] of snapshots) {
|
|
112
182
|
const res = await api.request('GET', `/${apiPath}`, {});
|
|
113
183
|
if (!res.ok) {
|
|
@@ -116,8 +186,18 @@ async function refreshFixtures({ api, dir = FIXTURES, snapshots = SNAPSHOTS, log
|
|
|
116
186
|
}
|
|
117
187
|
// Scrubbed BEFORE the write, never after: fixtures/ is committed, so a copy of
|
|
118
188
|
// the live response that touched the disk would already be one `git add` away.
|
|
119
|
-
|
|
120
|
-
|
|
189
|
+
fetched.push({ apiPath, name, data: scrubber.scrub(trim ? trim(res.data) : res.data, { ownAccount: isOwnAccountFixture(name) }) });
|
|
190
|
+
}
|
|
191
|
+
// Prose last (task 1004685): every snapshot's structure has taught the registry its
|
|
192
|
+
// people by now, so a name met in file 30 is still found in file 2's sentences.
|
|
193
|
+
const prose = createTextScrubber(scrubber.knownPeople());
|
|
194
|
+
const named = fetched.map((f) => ({ ...f, data: prose(f.data) }));
|
|
195
|
+
// Security content last of all (task 1004685): red-team write-ups, blocker
|
|
196
|
+
// runbooks, credentials and customer instance domains never reach the disk.
|
|
197
|
+
const secure = securityRedactorFor(named.map((f) => f.data));
|
|
198
|
+
let wrote = 0;
|
|
199
|
+
for (const { apiPath, name, data } of named) {
|
|
200
|
+
fs.writeFileSync(path.join(dir, `${name}.json`), `${JSON.stringify(secure(data), null, 2)}\n`);
|
|
121
201
|
wrote += 1;
|
|
122
202
|
log(` wrote ${name}.json (${apiPath})`);
|
|
123
203
|
}
|
|
@@ -129,14 +209,28 @@ async function refreshFixtures({ api, dir = FIXTURES, snapshots = SNAPSHOTS, log
|
|
|
129
209
|
// hand-written ones (several copied real people in by hand) and anything written
|
|
130
210
|
// before the scrub existed. Only values that change are rewritten, so a fixture
|
|
131
211
|
// someone formatted by hand keeps its formatting.
|
|
212
|
+
// Whether two JSON values have the same keys all the way down (values may differ).
|
|
213
|
+
function sameKeys(a, b) {
|
|
214
|
+
if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((x, i) => sameKeys(x, b[i]));
|
|
215
|
+
if (a && typeof a === 'object' && b && typeof b === 'object') {
|
|
216
|
+
const ka = Object.keys(a);
|
|
217
|
+
return ka.length === Object.keys(b).length && ka.every((k) => k in b && sameKeys(a[k], b[k]));
|
|
218
|
+
}
|
|
219
|
+
return (a && typeof a === 'object') === (b && typeof b === 'object');
|
|
220
|
+
}
|
|
221
|
+
|
|
132
222
|
function scrubFixturesOnDisk({ dir = FIXTURES, log = console.log } = {}) {
|
|
133
223
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
|
|
134
224
|
const texts = files.map((f) => fs.readFileSync(path.join(dir, f), 'utf8'));
|
|
135
225
|
const scrubber = createScrubber({ firstNumber: highestStandIn(texts) + 1 });
|
|
226
|
+
const parsed = texts.map((t) => JSON.parse(t));
|
|
227
|
+
const secure = securityRedactorFor(parsed);
|
|
136
228
|
let changed = 0;
|
|
137
229
|
files.forEach((file, idx) => {
|
|
138
|
-
const clean = scrubber.scrub(
|
|
139
|
-
|
|
230
|
+
const clean = secure(scrubber.scrub(parsed[idx], { ownAccount: isOwnAccountFixture(file.slice(0, -'.json'.length)) }));
|
|
231
|
+
// The security pass can DROP fields (a red-team record keeps an allowlist), which
|
|
232
|
+
// a scalar splice cannot express; then the file is written whole.
|
|
233
|
+
const next = sameKeys(parsed[idx], clean) ? rewriteScalars(texts[idx], clean) : `${JSON.stringify(clean, null, 2)}\n`;
|
|
140
234
|
if (next === texts[idx]) return;
|
|
141
235
|
fs.writeFileSync(path.join(dir, file), next);
|
|
142
236
|
changed += 1;
|