@bongos/core 1.19.442 → 1.19.444
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 +470 -455
- package/config/branding.neutral.json +1 -1
- package/docs/adr/0054-session-token-reward-cost-plus.md +14 -0
- package/docs/adr/0218-the-apex-root-web-surface.md +55 -0
- package/docs/adr/README.md +1 -0
- package/docs/architecture.md +2 -1
- package/docs/copy-inventory.md +360 -360
- package/docs/copy-registry.json +456 -456
- package/docs/design/mocks/hub/mine.html +1 -1
- package/docs/design/mocks/hub/picker.html +1 -1
- package/docs/design/mocks/hub/planets.html +1 -1
- package/docs/design/mocks/hub/v1.html +3 -3
- package/docs/design/mocks/hub/v2.html +2 -2
- package/docs/design/mocks/hub/v3.html +5 -5
- package/docs/design/mocks/hub/wizard.html +1 -1
- package/docs/design/mocks/landing/v1.html +1 -1
- package/docs/design/mocks/landing/v2.html +1 -1
- package/docs/design/mocks/landing/v3.html +1 -1
- package/docs/design/mocks/landing-page-mock.html +3 -3
- package/docs/design/mocks/projects-hub-mock.html +1 -1
- package/docs/design/mocks/projects-hub-variants.html +1 -1
- package/docs/design/mocks/projects-page-mock.html +1 -1
- package/docs/file-map.md +3 -3
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +9 -2
- package/docs/recipes/ui-look-before-you-ship.md +4 -4
- package/modules/copy-desk/tests/copy_flags.mjs +4 -4
- package/modules/public-landing/CLAUDE.md +15 -0
- package/modules/public-landing/module.json +15 -0
- package/modules/ui-design/kit/lib.js +2 -2
- package/modules/ui-design/kit/render.js +1 -1
- package/modules/ui-design/kit/serve.js +3 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/copy-inventory.js +3 -3
- package/scripts/gds/fitness-checks-identity.js +18 -1
- package/scripts/gds/publish-manifest.js +1 -1
- package/scripts/gds/session-digest-build.js +49 -17
- package/scripts/gds/session-digest.js +11 -1
- package/scripts/gds/ship-session-upload.js +102 -12
- package/scripts/gds/ship.js +7 -1
- package/src/bongos/module-scope-map.js +9 -7
- package/src/bongos/serve-internal.js +13 -1
- package/src/module-api.js +1 -1
- package/src/module-loader/loader.js +34 -17
- package/src/module-loader/manifest-schema.js +16 -8
- package/src/platform-server.js +50 -18
- package/tests/copy_inventory.mjs +0 -0
- package/tests/cosmos_token_contrast.mjs +6 -6
- package/tests/credit_grant.mjs +236 -0
- package/tests/fitness.mjs +20 -0
- package/tests/landing_legal_pages.mjs +2 -1
- package/tests/landing_page.mjs +7 -5
- package/tests/module-scope-map.mjs +2 -2
- package/tests/module_loader.mjs +4 -1
- package/tests/onboard.mjs +5 -5
- package/tests/projects_hub.mjs +22 -20
- package/tests/projects_hub_pre_uat.mjs +5 -3
- package/tests/public_landing_projects.mjs +2 -2
- package/tests/terms_acceptance.mjs +1 -1
- package/tests/ui_design_kit.mjs +3 -3
- package/tests/world_tokens.mjs +3 -2
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_00.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_01.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_02.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_03.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_04.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_05.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_06.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_07.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_08.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_09.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_10.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_11.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_12.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_13.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_14.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_15.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_16.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_17.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_18.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_19.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_20.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_21.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_22.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_23.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_24.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_25.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_26.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_27.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_28.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_29.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_30.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_31.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_32.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_33.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_34.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_35.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_36.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_37.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_38.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_39.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_40.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_41.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_42.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_43.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/cosmos.css +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/verro-medallion.png +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/og-card.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-bongos-chrome-a-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-bongos-chrome-a-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-bongos-chrome-b.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-bongos-chrome-b.jpg.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blob-one-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blob-one-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blob-one.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blob-one.jpg.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blobs-set-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blobs-set-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blobs-set.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blobs-set.jpg.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-bead-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-bead-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-blob-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-blob-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-pair-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-pair-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-bead-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-bead-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-blob-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-blob-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-pair-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-pair-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-bead-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-bead-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-blob-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-blob-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-pair-cut.webp +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-pair-cut.webp.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-space-nebula.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r3-space-nebula.jpg.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r4-card-chrome.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r4-card-chrome.jpg.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r4-card-deepfield.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r4-card-deepfield.jpg.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r4-card-dust.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r4-card-dust.jpg.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r4-card-nebula.jpg +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world/r4-card-nebula.jpg.json +0 -0
- /package/{public-landing → modules/public-landing/public}/assets/world.css +0 -0
- /package/{public-landing → modules/public-landing/public}/buddha.png +0 -0
- /package/{public-landing → modules/public-landing/public}/contact.html +0 -0
- /package/{public-landing → modules/public-landing/public}/index.html +0 -0
- /package/{public-landing → modules/public-landing/public}/index.probes.json +0 -0
- /package/{public-landing → modules/public-landing/public}/index.states.json +0 -0
- /package/{public-landing → modules/public-landing/public}/privacy.html +0 -0
- /package/{public-landing → modules/public-landing/public}/projects-empty.states.json +0 -0
- /package/{public-landing → modules/public-landing/public}/projects-floor.states.json +0 -0
- /package/{public-landing → modules/public-landing/public}/projects.html +0 -0
- /package/{public-landing → modules/public-landing/public}/projects.probes.json +0 -0
- /package/{public-landing → modules/public-landing/public}/projects.states.json +0 -0
- /package/{public-landing → modules/public-landing/public}/terms.html +0 -0
- /package/{public-landing → modules/public-landing/public}/world/assets/clouds/cloud-island-large.png +0 -0
- /package/{public-landing → modules/public-landing/public}/world/assets/clouds/cloud-puff.png +0 -0
- /package/{public-landing → modules/public-landing/public}/world/assets/creatures/creature-whale-cargo.png +0 -0
- /package/{public-landing → modules/public-landing/public}/world/assets/creatures/creature-whale-glide.png +0 -0
- /package/{public-landing → modules/public-landing/public}/world/assets/plates/plate-clouds-near.png +0 -0
- /package/{public-landing → modules/public-landing/public}/world/assets/plates/scene-establishing.png +0 -0
|
@@ -87,6 +87,16 @@ const UNATTRIBUTED_MODEL = 'claude-opus-4-8';
|
|
|
87
87
|
function buildSessionDigest(entries, opts = {}) {
|
|
88
88
|
const extra = opts.extraSecrets || [];
|
|
89
89
|
const MAX_TURNS = opts.maxTurns || 4000;
|
|
90
|
+
// usageOnly — build the token metrics but no `detail`. For the callers that
|
|
91
|
+
// want ONLY a transcript's model_usage (the subagents-subtree fold, task
|
|
92
|
+
// 1003435), where the per-turn rows would be discarded anyway and the snippet
|
|
93
|
+
// scrubbing is the expensive part. It exists so that fold can reuse THIS
|
|
94
|
+
// function — the one place the token semantics live — instead of a second copy
|
|
95
|
+
// that could drift, without paying for output nobody reads. The waste is worth
|
|
96
|
+
// avoiding on both upload paths, and on the SessionEnd one it is load-bearing:
|
|
97
|
+
// that hook runs under a 6-second watchdog, so a slow pass there doesn't just
|
|
98
|
+
// cost time, it drops the upload.
|
|
99
|
+
const usageOnly = opts.usageOnly === true;
|
|
90
100
|
const seenMessageIds = new Set();
|
|
91
101
|
const m = {
|
|
92
102
|
assistant_turns: 0,
|
|
@@ -165,15 +175,17 @@ function buildSessionDigest(entries, opts = {}) {
|
|
|
165
175
|
}
|
|
166
176
|
}
|
|
167
177
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
if (!usageOnly) {
|
|
179
|
+
const row = { i, role: 'assistant', ts, in: tin, out: tout, cache_read: tcr, cache_creation: tcc };
|
|
180
|
+
// Marks a line that shares its API call with an earlier one, so a reader
|
|
181
|
+
// can tell a genuinely-zero-usage turn from a de-duplicated repeat.
|
|
182
|
+
if (dupUsage) row.dup = 1;
|
|
183
|
+
if (tools.length) row.tools = tools;
|
|
184
|
+
if (textLen) row.text_len = textLen;
|
|
185
|
+
const snip = snippetOf(firstText, extra);
|
|
186
|
+
if (snip) row.snippet = snip;
|
|
187
|
+
detail.push(row);
|
|
188
|
+
}
|
|
177
189
|
i++;
|
|
178
190
|
} else if (e.type === 'user' && msg) {
|
|
179
191
|
m.user_turns++;
|
|
@@ -206,13 +218,15 @@ function buildSessionDigest(entries, opts = {}) {
|
|
|
206
218
|
}
|
|
207
219
|
}
|
|
208
220
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
221
|
+
if (!usageOnly) {
|
|
222
|
+
const row = { i, role: 'user', ts };
|
|
223
|
+
if (results) row.tool_results = results;
|
|
224
|
+
if (errors) row.tool_errors = errors;
|
|
225
|
+
if (harnessRetries) row.harness_retries = harnessRetries;
|
|
226
|
+
const snip = snippetOf(firstResult, extra);
|
|
227
|
+
if (snip) row.snippet = snip;
|
|
228
|
+
detail.push(row);
|
|
229
|
+
}
|
|
216
230
|
i++;
|
|
217
231
|
}
|
|
218
232
|
}
|
|
@@ -258,4 +272,22 @@ function buildSessionDigest(entries, opts = {}) {
|
|
|
258
272
|
detail: boundedDetail,
|
|
259
273
|
};
|
|
260
274
|
}
|
|
261
|
-
|
|
275
|
+
// mergeModelUsage — fold one model_usage map into another, in place, so a
|
|
276
|
+
// session's tokens can be summed across the several transcripts one session
|
|
277
|
+
// actually writes (the interactive thread + its subagent subtree — task
|
|
278
|
+
// 1003435). PURE apart from mutating `target`, which the caller owns. The four
|
|
279
|
+
// buckets are the session_records model_usage shape, NOT the transcript
|
|
280
|
+
// *_input_tokens shape; keep them in step with buildSessionDigest above.
|
|
281
|
+
function mergeModelUsage(target, addition) {
|
|
282
|
+
for (const [model, u] of Object.entries(addition || {})) {
|
|
283
|
+
const bucket = target[model] || (target[model] =
|
|
284
|
+
{ input: 0, output: 0, cache_read: 0, cache_creation: 0 });
|
|
285
|
+
bucket.input += Number(u && u.input) || 0;
|
|
286
|
+
bucket.output += Number(u && u.output) || 0;
|
|
287
|
+
bucket.cache_read += Number(u && u.cache_read) || 0;
|
|
288
|
+
bucket.cache_creation += Number(u && u.cache_creation) || 0;
|
|
289
|
+
}
|
|
290
|
+
return target;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = { snippetOf, buildSessionDigest, mergeModelUsage, HARNESS_RETRY_RE, UNATTRIBUTED_MODEL };
|
|
@@ -34,7 +34,7 @@ const fs = require('fs');
|
|
|
34
34
|
const os = require('os');
|
|
35
35
|
const path = require('path');
|
|
36
36
|
const { cliClient, loadSession, arg } = require('./cli-lib');
|
|
37
|
-
const { buildSessionDigest, collectEnvSecrets } = require('./ship');
|
|
37
|
+
const { buildSessionDigest, collectEnvSecrets, foldSubagentUsage } = require('./ship');
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
// Claude Code encodes a project transcript dir by replacing every non-alnum
|
|
@@ -144,6 +144,10 @@ async function main() {
|
|
|
144
144
|
if (session.token) extraSecrets.push(session.token);
|
|
145
145
|
|
|
146
146
|
const digest = buildSessionDigest(parseTranscript(transcriptPath), { extraSecrets });
|
|
147
|
+
// task 1003435: fold this session's subagents subtree into model_usage, the
|
|
148
|
+
// same way the on-ship upload does — the whole session's tokens, not just the
|
|
149
|
+
// interactive thread's.
|
|
150
|
+
foldSubagentUsage(digest, transcriptPath, sessionId);
|
|
147
151
|
|
|
148
152
|
// task_ids:[] — the server upsert UNIONs task_ids, so the on-ship task
|
|
149
153
|
// attribution is preserved; this end-of-session upload's job is to refresh
|
|
@@ -157,6 +161,12 @@ async function main() {
|
|
|
157
161
|
ended_at: digest.endedAt,
|
|
158
162
|
detail: digest.detail,
|
|
159
163
|
metrics: digest.metrics,
|
|
164
|
+
// model_usage is its OWN column, and the upsert overwrites it from the
|
|
165
|
+
// body — so omitting it here (as this hook did until task 1003435) wiped
|
|
166
|
+
// the map the on-ship upload had just stored, leaving the reward with
|
|
167
|
+
// nothing to price. It is the LAST upload of the session, so it must carry
|
|
168
|
+
// the fullest picture, not the emptiest.
|
|
169
|
+
model_usage: digest.metrics.model_usage || {},
|
|
160
170
|
};
|
|
161
171
|
const api = await cliClient({ allowUnauthenticated: true });
|
|
162
172
|
const r = await api.sessions.postSessions({ body: payload });
|
|
@@ -43,9 +43,16 @@ const {
|
|
|
43
43
|
// shared with session-digest.js. Re-exported below so this file's public surface
|
|
44
44
|
// and tests/session_records.mjs are unchanged.
|
|
45
45
|
const {
|
|
46
|
-
snippetOf, buildSessionDigest, HARNESS_RETRY_RE, UNATTRIBUTED_MODEL,
|
|
46
|
+
snippetOf, buildSessionDigest, mergeModelUsage, HARNESS_RETRY_RE, UNATTRIBUTED_MODEL,
|
|
47
47
|
} = require('./session-digest-build');
|
|
48
48
|
|
|
49
|
+
// The subagent subtree walker + the grader-transcript signature live in
|
|
50
|
+
// session-cost.js, which already prices that subtree for the SPEND ledger. The
|
|
51
|
+
// reward reads the same tokens, so it must use the SAME two definitions or the
|
|
52
|
+
// two ledgers describe different sessions (task 1003435). session-cost.js guards
|
|
53
|
+
// its own entry point behind require.main, so importing it is side-effect free.
|
|
54
|
+
const { listJsonlRecursive, looksLikeGraderTranscript } = require('./session-cost.js');
|
|
55
|
+
|
|
49
56
|
|
|
50
57
|
// classifyUploadSkip — PURE decision for whether the session upload must skip,
|
|
51
58
|
// and the clear, actionable reason why. Separated from I/O so it is unit-tested
|
|
@@ -109,6 +116,94 @@ function recordUploadSkip(taskId, { code, reason }, opts = {}) {
|
|
|
109
116
|
}
|
|
110
117
|
}
|
|
111
118
|
|
|
119
|
+
// parseJsonl — one transcript file's text into parsed entries, skipping any
|
|
120
|
+
// malformed line (best-effort, exactly like the rest of this path).
|
|
121
|
+
function parseJsonl(raw) {
|
|
122
|
+
const entries = [];
|
|
123
|
+
for (const line of String(raw || '').split('\n')) {
|
|
124
|
+
const t = line.trim();
|
|
125
|
+
if (!t) continue;
|
|
126
|
+
try {
|
|
127
|
+
entries.push(JSON.parse(t));
|
|
128
|
+
} catch (_) {
|
|
129
|
+
/* skip a malformed line — best-effort */
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return entries;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// subagentModelUsage — the per-model TOKEN counts this session's ad-hoc
|
|
136
|
+
// subagents burned, so the cost-plus reward (ADR 0054) can see them.
|
|
137
|
+
//
|
|
138
|
+
// WHY (task 1003435): Agent/Task-tool spawns persist in a sibling subtree,
|
|
139
|
+
// <project-dir>/<session-id>/subagents/*.jsonl — never in the main transcript.
|
|
140
|
+
// session-cost.js already prices that subtree into a `session:subagent` cost_log
|
|
141
|
+
// row, so the SPEND ledger sees it; the digest read one file, so the REWARD did
|
|
142
|
+
// not. That paid a builder who follows CLAUDE.md's "delegate freely" less than
|
|
143
|
+
// one who did identical work in the main thread, and made the two ledgers
|
|
144
|
+
// disagree about the same session.
|
|
145
|
+
//
|
|
146
|
+
// Grader transcripts are excluded BY SIGNATURE, not by location: a grader run
|
|
147
|
+
// can nest inside a subagents/ dir, and its cost is already captured via the
|
|
148
|
+
// worker envelope, so pricing it here would double-count (the ADR 0028
|
|
149
|
+
// invariant). Each file goes through buildSessionDigest so the token semantics —
|
|
150
|
+
// dedup by message.id, skip `<synthetic>`, fold unlabeled turns into a priceable
|
|
151
|
+
// bucket — are the ONE implementation, never a second one that can drift.
|
|
152
|
+
function subagentModelUsage(sessionDir) {
|
|
153
|
+
const modelUsage = {};
|
|
154
|
+
let files = 0;
|
|
155
|
+
let tokens = 0;
|
|
156
|
+
let graderSkipped = 0;
|
|
157
|
+
for (const f of listJsonlRecursive(sessionDir)) {
|
|
158
|
+
if (looksLikeGraderTranscript(f)) { graderSkipped++; continue; }
|
|
159
|
+
let digest;
|
|
160
|
+
try {
|
|
161
|
+
digest = buildSessionDigest(parseJsonl(fs.readFileSync(f, 'utf8')), { usageOnly: true });
|
|
162
|
+
} catch (_) {
|
|
163
|
+
continue; // an unreadable subagent file must never fail a ship
|
|
164
|
+
}
|
|
165
|
+
if (!(digest.totalTokens > 0)) continue;
|
|
166
|
+
files++;
|
|
167
|
+
tokens += digest.totalTokens;
|
|
168
|
+
mergeModelUsage(modelUsage, digest.metrics.model_usage);
|
|
169
|
+
}
|
|
170
|
+
return { modelUsage, files, tokens, graderSkipped };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// foldSubagentUsage — add the session's subagent tokens to `digest`'s
|
|
174
|
+
// model_usage, IN PLACE, and record what was folded.
|
|
175
|
+
//
|
|
176
|
+
// model_usage ONLY, deliberately (task 1003435). model_usage is the reward's
|
|
177
|
+
// input, so it must describe the whole session. `detail`, `metrics.tokens`,
|
|
178
|
+
// `assistant_turns` and `total_tokens` stay the INTERACTIVE thread's record:
|
|
179
|
+
// they feed the BFG corpus + the inefficiency evaluator, which reason about one
|
|
180
|
+
// agent's turn-by-turn behaviour, and splicing N subagent transcripts into that
|
|
181
|
+
// per-turn story would distort the very signal it exists to measure (and bloat
|
|
182
|
+
// the payload). The three subagent_* counters are how a reader tells the two
|
|
183
|
+
// apart instead of inferring a discrepancy from a mismatch.
|
|
184
|
+
function foldSubagentUsage(digest, transcriptPath, sessionId) {
|
|
185
|
+
if (!digest || !digest.metrics || !transcriptPath || !sessionId) return digest;
|
|
186
|
+
// Path-traversal guard. The session id is interpolated into a filesystem path
|
|
187
|
+
// below, and what comes out of that path is priced into a payout — so a value
|
|
188
|
+
// carrying `../` could aim the walk at an arbitrary directory and fold
|
|
189
|
+
// fabricated token counts into the reward's input. Guarded HERE rather than at
|
|
190
|
+
// each call site: uploadSessionOnShip already screens the id before resolving
|
|
191
|
+
// the transcript, but the SessionEnd hook takes it straight off stdin, and any
|
|
192
|
+
// future caller would inherit the same hole. Same shape as the check in
|
|
193
|
+
// uploadSessionOnShip — a plain UUID-ish token, nothing else.
|
|
194
|
+
if (!/^[A-Za-z0-9_-]+$/.test(String(sessionId))) return digest;
|
|
195
|
+
const sessionDir = path.join(path.dirname(transcriptPath), sessionId);
|
|
196
|
+
const sub = subagentModelUsage(sessionDir);
|
|
197
|
+
if (sub.files === 0 && sub.graderSkipped === 0) return digest;
|
|
198
|
+
digest.metrics.model_usage = mergeModelUsage(
|
|
199
|
+
digest.metrics.model_usage || {}, sub.modelUsage
|
|
200
|
+
);
|
|
201
|
+
digest.metrics.subagent_files = sub.files;
|
|
202
|
+
digest.metrics.subagent_tokens = sub.tokens;
|
|
203
|
+
digest.metrics.subagent_grader_skipped = sub.graderSkipped;
|
|
204
|
+
return digest;
|
|
205
|
+
}
|
|
206
|
+
|
|
112
207
|
async function uploadSessionOnShip(taskId) {
|
|
113
208
|
try {
|
|
114
209
|
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
@@ -135,17 +230,7 @@ async function uploadSessionOnShip(taskId) {
|
|
|
135
230
|
recordUploadSkip(taskId, decision);
|
|
136
231
|
return null;
|
|
137
232
|
}
|
|
138
|
-
const
|
|
139
|
-
const entries = [];
|
|
140
|
-
for (const line of raw.split('\n')) {
|
|
141
|
-
const t = line.trim();
|
|
142
|
-
if (!t) continue;
|
|
143
|
-
try {
|
|
144
|
-
entries.push(JSON.parse(t));
|
|
145
|
-
} catch (_) {
|
|
146
|
-
/* skip a malformed line — best-effort */
|
|
147
|
-
}
|
|
148
|
-
}
|
|
233
|
+
const entries = parseJsonl(fs.readFileSync(transcriptPath, 'utf8'));
|
|
149
234
|
// Dynamic secrets to scrub: every secret-shaped env VALUE (Apple/MAC_* CI
|
|
150
235
|
// creds, a baked GDS/DO token, GITHUB_TOKEN, ANTHROPIC_API_KEY, …) plus this
|
|
151
236
|
// run's live GDS session token. Defense in depth on top of the pattern +
|
|
@@ -157,6 +242,10 @@ async function uploadSessionOnShip(taskId) {
|
|
|
157
242
|
const s = await loadSession();
|
|
158
243
|
if (s && s.token) extraSecrets.push(s.token);
|
|
159
244
|
const digest = buildSessionDigest(entries, { extraSecrets });
|
|
245
|
+
// task 1003435: the interactive transcript is only part of what this session
|
|
246
|
+
// spent — fold the subagents subtree into model_usage so the cost-plus reward
|
|
247
|
+
// prices the same tokens the cost ledger already bills.
|
|
248
|
+
foldSubagentUsage(digest, transcriptPath, sessionId);
|
|
160
249
|
// task 1270: surface the session usage so the completion card can show
|
|
161
250
|
// Claude tokens + an estimated cost. Returned even when the POST below fails
|
|
162
251
|
// (we already have the digest) — the card is best-effort either way.
|
|
@@ -215,4 +304,5 @@ module.exports = {
|
|
|
215
304
|
classifyUploadSkip,
|
|
216
305
|
recordUploadSkip,
|
|
217
306
|
uploadSessionOnShip,
|
|
307
|
+
foldSubagentUsage,
|
|
218
308
|
};
|
package/scripts/gds/ship.js
CHANGED
|
@@ -99,7 +99,9 @@ const {
|
|
|
99
99
|
resolveInstanceBase,
|
|
100
100
|
shouldBailOnFetchFailure,
|
|
101
101
|
} = require('./ship-deploy-target.js');
|
|
102
|
-
const {
|
|
102
|
+
const {
|
|
103
|
+
classifyUploadSkip, recordUploadSkip, foldSubagentUsage,
|
|
104
|
+
} = require('./ship-session-upload.js');
|
|
103
105
|
const {
|
|
104
106
|
classifyLandOutcome,
|
|
105
107
|
formatPublishFailure,
|
|
@@ -176,6 +178,10 @@ module.exports = {
|
|
|
176
178
|
// secret corpus test (tests/scrubber_corpus.mjs) can exercise each layer + tune.
|
|
177
179
|
shannonEntropy, looksHighEntropySecret, redactHighEntropy, collectEnvSecrets,
|
|
178
180
|
classifyUploadSkip, recordUploadSkip,
|
|
181
|
+
// task 1003435: the subagents-subtree fold, shared with the SessionEnd hook —
|
|
182
|
+
// both uploads must send the SAME whole-session model_usage or whichever runs
|
|
183
|
+
// last decides what the builder is paid for.
|
|
184
|
+
foldSubagentUsage,
|
|
179
185
|
// #678: re-grade path + the grade-step helpers it shares with shipMain.
|
|
180
186
|
// Exporting these formalizes learning #43 (which had to "replicate ~10
|
|
181
187
|
// lines" because runSubagentGrader wasn't reachable).
|
|
@@ -206,13 +206,15 @@ const MODULE_GLOBS = {
|
|
|
206
206
|
// (moduleWebSurfaces → serve-internal). Its public read endpoints stay SHARED
|
|
207
207
|
// core in routes/public.js (the hall + the off-box mirror consume them too).
|
|
208
208
|
'status-ui': ['modules/status-ui/'],
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
209
|
+
// Task 1003322 (ADR 0218): the front door graduated to modules/public-landing/ —
|
|
210
|
+
// public-landing/ moved to modules/public-landing/public/ and is now a loader-
|
|
211
|
+
// discovered web-surface module. Where hall-ui and status-ui name a Host PREFIX it
|
|
212
|
+
// declares apexRoot:true, since the apex is whatever host is left once the
|
|
213
|
+
// subdomains are taken; like the hall it keeps its DEDICATED serving block (in
|
|
214
|
+
// platform-server.js, re-pointed at the module dir). The KEY is unchanged from
|
|
215
|
+
// task 1002690 — its own key, not provisioning's, because the front door is broader
|
|
216
|
+
// than instance hosting and the web-surface precedent is one key per surface.
|
|
217
|
+
'public-landing': ['modules/public-landing/'],
|
|
216
218
|
// --- the design capability (task 1003324, ADR 0197): the design contract, its
|
|
217
219
|
// adapters + sync CLIs + validator, the /design playbook, and the reserved homes
|
|
218
220
|
// for the kit / the vendored UI skills / the style library. default-on,
|
|
@@ -211,7 +211,8 @@ function dropEmptyHrefLinks(html, values) {
|
|
|
211
211
|
// '<' would otherwise break out of its attribute or close <title> early, and
|
|
212
212
|
// the values now reach four times as many places as when this was written.
|
|
213
213
|
// No token is ever emitted into a <script> or a CSS context (checked across
|
|
214
|
-
// public-landing/, hall-ui/, status-ui/
|
|
214
|
+
// public-landing/, hall-ui/, status-ui/ — all under modules/ since task 1003322)
|
|
215
|
+
// — that would need different escaping.
|
|
215
216
|
// Ampersand first, or the other replacements get double-escaped.
|
|
216
217
|
function esc(s) {
|
|
217
218
|
return String(s == null ? '' : s)
|
|
@@ -701,9 +702,20 @@ function mountInternalSurfaces(app) {
|
|
|
701
702
|
// non-builders host. (The webSurfaces manifest declaration still makes the hall a
|
|
702
703
|
// loader-DISCOVERED module — it feeds the scope map, the fitness keys, docs, and
|
|
703
704
|
// window.__MODULES__; it is the dedicated serving below that reads the bytes.)
|
|
705
|
+
//
|
|
706
|
+
// The APEX-ROOT surface (modules/public-landing/, ADR 0218) is partitioned out the
|
|
707
|
+
// same way, on its own declaration rather than on a host: it carries no host prefix
|
|
708
|
+
// at all, because the apex is whatever is LEFT once the subdomains are taken. It is
|
|
709
|
+
// served by platform-server.js's dedicated landing block (the apex-only strip, the
|
|
710
|
+
// brand-token stamp, the ?v= content stamp, the five routes). Keeping it out of
|
|
711
|
+
// isModuleSurfaceHost is load-bearing, not tidiness — that predicate is what the
|
|
712
|
+
// /builders guards below use to step aside, and a surface matching every host would
|
|
713
|
+
// make all of them self-skip.
|
|
704
714
|
const buildersHostReq = (host) => ({ headers: { host } });
|
|
705
715
|
const allWebSurfaces = moduleWebSurfaces({ isEnabled: isModuleEnabled });
|
|
706
716
|
const webSurfaces = allWebSurfaces
|
|
717
|
+
.filter((s) => typeof s.host === 'string') // APEX-ROOT surfaces carry no host (ADR 0218): they are not
|
|
718
|
+
// routed by prefix here, and must stay OUT of isModuleSurfaceHost
|
|
707
719
|
.filter((s) => !isBuildersHost(buildersHostReq(s.host))) // hall served by the dedicated block, not here
|
|
708
720
|
.map((s) => ({
|
|
709
721
|
key: s.key,
|
package/src/module-api.js
CHANGED
|
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
33
33
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
34
34
|
// the entry to that file. Look for a version's history there, not here.
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
36
|
-
const CORE_VERSION = '1.19.
|
|
36
|
+
const CORE_VERSION = '1.19.444'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
37
37
|
|
|
38
38
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
39
39
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -175,32 +175,49 @@ function hallWidgetScripts({ isEnabled } = {}, opts) {
|
|
|
175
175
|
return out;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
178
|
+
// Static web surfaces contributed by enabled modules (BV1.R84, ADR 0093 §1 /
|
|
179
|
+
// 02-module-map §6 — a web surface is first-class). A module that owns a web surface
|
|
180
|
+
// declares contributes.webSurfaces in its manifest AND ships that static dir at
|
|
181
|
+
// modules/<key>/<dir>/. A surface names WHERE it answers in one of two ways, never
|
|
182
|
+
// both and never neither (ADR 0218):
|
|
183
|
+
// { host: "status.", dir: "public" } → a Host-header PREFIX (status-UI, hall-UI)
|
|
184
|
+
// { apexRoot: true, dir: "public" } → the APEX ROOT (public-landing) — whatever
|
|
185
|
+
// host is left once the subdomains are taken
|
|
186
|
+
// This returns, for each discovered + ENABLED module that declares a surface and
|
|
187
|
+
// actually ships the directory, the SERVING descriptor its server iterates:
|
|
188
|
+
// { key, host, dir } — host is the lowercase Host-header prefix
|
|
189
|
+
// { key, apexRoot: true, dir } — NO host field at all
|
|
190
|
+
// dir is the ABSOLUTE on-disk path of the static directory in both shapes.
|
|
191
|
+
//
|
|
192
|
+
// `host: ""` is REJECTED, not treated as "the root" (ADR 0218 §3): the routers match
|
|
193
|
+
// with host.startsWith(s.host), and every string starts with "" — an empty prefix
|
|
194
|
+
// would answer on builders./status. too and make isModuleSurfaceHost() true for every
|
|
195
|
+
// request, which is the predicate the hall's own /builders guards use to step aside.
|
|
196
|
+
// A boolean cannot be mistaken for a prefix, so apex-root says so with a flag.
|
|
197
|
+
//
|
|
198
|
+
// The twin of mountModuleRoutes / hallWidgetScripts: an enabled module's surface
|
|
199
|
+
// answers, a disabled one vanishes (it's never returned, so nothing routes it — for
|
|
200
|
+
// the apex root that means the front door is simply gone and the 404 tail answers).
|
|
201
|
+
// No-op (empty list) until a module ships a web surface — same SAFE-BY-CONSTRUCTION
|
|
202
|
+
// posture as every other seam here. Pure (reads disk only). Exported + module-agnostic
|
|
203
|
+
// so both servers hardcode no key.
|
|
192
204
|
function moduleWebSurfaces({ isEnabled } = {}, opts) {
|
|
193
205
|
const out = [];
|
|
194
206
|
for (const { key, dir, manifest } of discovered(opts).loaded) {
|
|
195
207
|
if (typeof isEnabled === 'function' && !isEnabled(key)) continue;
|
|
196
208
|
const surfaces = (manifest.contributes && manifest.contributes.webSurfaces) || [];
|
|
197
209
|
for (const s of surfaces) {
|
|
198
|
-
if (!s || typeof s.
|
|
210
|
+
if (!s || typeof s.dir !== 'string') continue;
|
|
211
|
+
const apexRoot = s.apexRoot === true;
|
|
212
|
+
const hasHost = typeof s.host === 'string' && s.host !== '';
|
|
213
|
+
// exactly one of the two, never both and never neither — and `host: ""` counts
|
|
214
|
+
// as NEITHER, which is what keeps an empty prefix from matching every host
|
|
215
|
+
if (apexRoot === hasHost) continue;
|
|
199
216
|
const abs = path.join(dir, s.dir);
|
|
200
217
|
let isDir = false;
|
|
201
218
|
try { isDir = fs.statSync(abs).isDirectory(); } catch { isDir = false; }
|
|
202
|
-
if (!isDir) continue; // declared but not shipped → skip (
|
|
203
|
-
out.push({ key, host: s.host.toLowerCase(), dir: abs });
|
|
219
|
+
if (!isDir) continue; // declared but not shipped → skip (nothing routed)
|
|
220
|
+
out.push(apexRoot ? { key, apexRoot: true, dir: abs } : { key, host: s.host.toLowerCase(), dir: abs });
|
|
204
221
|
}
|
|
205
222
|
}
|
|
206
223
|
return out;
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
// "disciplines": ["artist"], // disciplines this module adds
|
|
35
35
|
// "skills": ["otb-…"], // slash-command skills it ships
|
|
36
36
|
// "uiSections": ["boxes"], // hall/status UI sections it owns
|
|
37
|
-
// "webSurfaces": [{ "host": "status.", "dir": "public" }], //
|
|
37
|
+
// "webSurfaces": [{ "host": "status.", "dir": "public" }], // static web surfaces. Each names a Host
|
|
38
|
+
// // PREFIX (BV1.R84) or, as { "apexRoot": true, "dir": "public" },
|
|
39
|
+
// // the APEX ROOT — never both (ADR 0218).
|
|
38
40
|
// "migrations": true // true => modules/<key>/migrations/ exists (or an array of names)
|
|
39
41
|
// },
|
|
40
42
|
// "provides": ["reward"], // OPTIONAL. seam PORTS this module registers a provider for.
|
|
@@ -84,13 +86,19 @@ const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
|
|
|
84
86
|
const isStr = (v) => typeof v === 'string' && v.length > 0;
|
|
85
87
|
const isStrArray = (v) => Array.isArray(v) && v.every(isStr);
|
|
86
88
|
|
|
87
|
-
// A webSurface entry
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
89
|
+
// A webSurface entry names WHERE the surface answers, in exactly one of two ways
|
|
90
|
+
// (ADR 0218), plus the module-relative directory of static files to serve:
|
|
91
|
+
// { host: "status.", dir: "public" } — a lowercase Host-header PREFIX (status.<apex>)
|
|
92
|
+
// { apexRoot: true, dir: "public" } — the APEX ROOT: whatever host is left once
|
|
93
|
+
// the subdomains are taken (the front door)
|
|
94
|
+
// `dir` is required, non-empty, and must stay inside the module (no absolute / "..").
|
|
95
|
+
// A host surface's `host` must be a NON-EMPTY string: routers match with
|
|
96
|
+
// host.startsWith(host), and every string starts with "", so an empty prefix would
|
|
97
|
+
// answer on every host and silently swallow the other surfaces. That is exactly why
|
|
98
|
+
// the apex root is a boolean flag and not the empty prefix it superficially resembles.
|
|
99
|
+
const isWebSurfaceDir = (v) => isStr(v.dir) && !v.dir.startsWith('/') && !v.dir.split(/[\\/]/).includes('..');
|
|
92
100
|
const isWebSurface = (v) =>
|
|
93
|
-
isObj(v) &&
|
|
101
|
+
isObj(v) && isWebSurfaceDir(v) && (v.apexRoot === true ? !('host' in v) : isStr(v.host));
|
|
94
102
|
const isWebSurfaceArray = (v) => Array.isArray(v) && v.every(isWebSurface);
|
|
95
103
|
|
|
96
104
|
function validateManifest(obj) {
|
|
@@ -172,7 +180,7 @@ function validateManifest(obj) {
|
|
|
172
180
|
}
|
|
173
181
|
}
|
|
174
182
|
if ('webSurfaces' in obj.contributes && !isWebSurfaceArray(obj.contributes.webSurfaces)) {
|
|
175
|
-
E('contributes.webSurfaces: must be an array of { host: "<prefix>.", dir: "<module-relative dir>" } objects (dir inside the module
|
|
183
|
+
E('contributes.webSurfaces: must be an array of { host: "<prefix>.", dir: "<module-relative dir>" } or { apexRoot: true, dir: "<module-relative dir>" } objects — a surface names a Host prefix OR the apex root, never both and never neither (ADR 0218); dir stays inside the module (no absolute path or "..")');
|
|
176
184
|
}
|
|
177
185
|
if ('migrations' in obj.contributes) {
|
|
178
186
|
const m = obj.contributes.migrations;
|
package/src/platform-server.js
CHANGED
|
@@ -54,13 +54,40 @@ const { logger, requestIdMiddleware } = require('./bongos/logger');
|
|
|
54
54
|
const PORT = Number(process.env.PORT) || 3000;
|
|
55
55
|
const HOST = process.env.HOST || '127.0.0.1';
|
|
56
56
|
|
|
57
|
-
// The apex
|
|
58
|
-
// apex host used to serve).
|
|
59
|
-
//
|
|
60
|
-
// the
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
57
|
+
// The apex front door — a game-less instance's home (it replaced the bare 404 the
|
|
58
|
+
// apex host used to serve). Served only on the apex host; status./builders. are
|
|
59
|
+
// handled by mountInternalSurfaces. The branding pack is injected once at boot
|
|
60
|
+
// (theme + window.__BRANDING__), exactly as the hall pages are, so the look and the
|
|
61
|
+
// identity come from config, never from hardcode.
|
|
62
|
+
//
|
|
63
|
+
// THE SURFACE IS A MODULE (task 1003322, ADR 0218). The front door used to be a
|
|
64
|
+
// hardcoded path here; it is now modules/public-landing/public/, resolved through the
|
|
65
|
+
// loader's web-surface seam by the APEX-ROOT declaration ({ apexRoot: true, dir }) —
|
|
66
|
+
// the one shape that seam grew for it, because the apex is not a Host PREFIX like
|
|
67
|
+
// "status."/"builders." but whatever is left once the subdomains are taken.
|
|
68
|
+
//
|
|
69
|
+
// Resolved by PREDICATE, never by key: this file names no module (ADR 0083 §Decision
|
|
70
|
+
// #4), so an instance shipping its own front door gets the apex by declaring apexRoot.
|
|
71
|
+
// Lazy + memoised so importing this module reads no disk and a test can build the app
|
|
72
|
+
// after setting up its own module state.
|
|
73
|
+
//
|
|
74
|
+
// null when NO enabled module claims the apex root — the module is disabled, or the
|
|
75
|
+
// instance ships no landing at all. Every consumer below already treats an absent page
|
|
76
|
+
// as "fall through", so the front door simply vanishes and the 404 tail answers.
|
|
77
|
+
let _landingDir;
|
|
78
|
+
function landingDir() {
|
|
79
|
+
if (_landingDir === undefined) {
|
|
80
|
+
// The apex root is singular by definition — two modules cannot both be "everything
|
|
81
|
+
// that is not a subdomain". Serve the first and SAY so, rather than silently
|
|
82
|
+
// shadowing one of them.
|
|
83
|
+
const claims = moduleLoader.moduleWebSurfaces({ isEnabled: isModuleEnabled }).filter((s) => s.apexRoot);
|
|
84
|
+
if (claims.length > 1) {
|
|
85
|
+
logger.warn({ keys: claims.map((s) => s.key) }, 'more than one enabled module declares an apex-root web surface — serving the first');
|
|
86
|
+
}
|
|
87
|
+
_landingDir = claims.length ? claims[0].dir : null;
|
|
88
|
+
}
|
|
89
|
+
return _landingDir;
|
|
90
|
+
}
|
|
64
91
|
// ── CONTENT-STAMPED STATIC ASSETS (task 1002766) ──
|
|
65
92
|
// The landing HTML is `cf-cache-status: DYNAMIC` and never cached, but the
|
|
66
93
|
// assets it references come back `Cache-Control: public, max-age=14400` — four
|
|
@@ -80,7 +107,7 @@ const LANDING_DIR = path.join(__dirname, '..', 'public-landing');
|
|
|
80
107
|
// shape serve-internal.js already stamps onto the hall's own scripts.
|
|
81
108
|
//
|
|
82
109
|
// Stamped by RESOLUTION, not by a path allow-list: a URL is an asset if it
|
|
83
|
-
// actually names a file under
|
|
110
|
+
// actually names a file under the landing surface. Routes (`/projects`, `/create`)
|
|
84
111
|
// and API paths resolve to nothing and are left alone, so a new asset is
|
|
85
112
|
// stamped the day it is added and nobody has to remember to list it.
|
|
86
113
|
const _assetStamps = new Map();
|
|
@@ -88,9 +115,10 @@ function assetStamp(url) {
|
|
|
88
115
|
if (!_assetStamps.has(url)) {
|
|
89
116
|
let v = '';
|
|
90
117
|
try {
|
|
91
|
-
const
|
|
118
|
+
const root = landingDir();
|
|
119
|
+
const abs = path.resolve(root, '.' + url);
|
|
92
120
|
// never let a template's own text walk out of the landing dir
|
|
93
|
-
if (abs.startsWith(
|
|
121
|
+
if (abs.startsWith(root + path.sep)) {
|
|
94
122
|
v = require('node:crypto').createHash('sha256')
|
|
95
123
|
.update(fs.readFileSync(abs)).digest('hex').slice(0, 8);
|
|
96
124
|
}
|
|
@@ -112,14 +140,14 @@ function landingPage(file) {
|
|
|
112
140
|
try {
|
|
113
141
|
// stamped LAST, so anything a later transform introduces is stamped too
|
|
114
142
|
html = stampAssets(injectBrandingGlobal(stripApexOnly(
|
|
115
|
-
fs.readFileSync(path.join(
|
|
116
|
-
} catch { /* absent → '' */ }
|
|
143
|
+
fs.readFileSync(path.join(landingDir(), file), 'utf8'))));
|
|
144
|
+
} catch { /* absent, or no module claims the apex root → '' */ }
|
|
117
145
|
_landingPages.set(file, html);
|
|
118
146
|
}
|
|
119
147
|
return _landingPages.get(file);
|
|
120
148
|
}
|
|
121
149
|
|
|
122
|
-
// THE APEX CONDITIONAL (task 1002748).
|
|
150
|
+
// THE APEX CONDITIONAL (task 1002748). The landing surface ships wholesale to every
|
|
123
151
|
// instance, so one file has to render two shapes: the platform's own front door,
|
|
124
152
|
// and a child project's. Sections that are true only of the platform — the
|
|
125
153
|
// catalog of OTHER instances, the "capture-resistant commons" claim, the door
|
|
@@ -231,11 +259,15 @@ function buildPlatformApp() {
|
|
|
231
259
|
res.type('html').send(html);
|
|
232
260
|
});
|
|
233
261
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
262
|
+
// Assets fall through to the 404 tail when no module claims the apex root — the
|
|
263
|
+
// handler is not mounted at all rather than pointed at a missing directory.
|
|
264
|
+
const landingAssets = landingDir() && express.static(landingDir(), { index: false, fallthrough: true });
|
|
265
|
+
if (landingAssets) {
|
|
266
|
+
app.use((req, res, next) => {
|
|
267
|
+
if (!isApexHost(req)) return next();
|
|
268
|
+
return landingAssets(req, res, next);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
239
271
|
|
|
240
272
|
// Non-game tail. Anything that did not resolve to /api/gds, the status host,
|
|
241
273
|
// or the builders' hall lands here. There is NO game to fall through to, so:
|
package/tests/copy_inventory.mjs
CHANGED
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Guards the WCAG AA contrast of the cosmos.css token layer (task 1002749).
|
|
2
2
|
//
|
|
3
|
-
// The tokens are the ONLY input here: this test PARSES
|
|
3
|
+
// The tokens are the ONLY input here: this test PARSES the landing surface’s assets/cosmos.css
|
|
4
4
|
// and resolves var()/color-mix() itself, so it measures what the surfaces actually paint.
|
|
5
5
|
// Duplicating the hex values would prove nothing.
|
|
6
6
|
//
|
|
@@ -13,10 +13,10 @@ import fs from 'node:fs';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
);
|
|
16
|
+
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
// the front door is a module since task 1003322 (ADR 0218) — one constant, so the path moves once
|
|
18
|
+
const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
|
|
19
|
+
const CSS = path.join(LANDING, 'assets', 'cosmos.css');
|
|
20
20
|
const src = fs.readFileSync(CSS, 'utf8');
|
|
21
21
|
|
|
22
22
|
// The two sheets that actually ship, comment-stripped. Several static guards
|
|
@@ -76,7 +76,7 @@ const darkMedia = declarations(blockAfter(':root:not([data-mode="light"]){'));
|
|
|
76
76
|
// The dark blocks are :root[data-mode="dark"] and :root:not([data-mode="light"]) —
|
|
77
77
|
// both (0,2,0) — so they outrank the pack's (0,1,0) and win back.
|
|
78
78
|
const packColors = JSON.parse(
|
|
79
|
-
fs.readFileSync(path.join(
|
|
79
|
+
fs.readFileSync(path.join(ROOT, 'config', 'branding.neutral.json'), 'utf8')
|
|
80
80
|
).theme.ui.colors;
|
|
81
81
|
|
|
82
82
|
// mirrors COLOR_VARS in src/bongos/serve-internal.js
|