@bongos/core 1.20.83 → 1.21.2
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 +186 -81
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +7 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0051-full-session-transcript-corpus.md +3 -1
- package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +3 -3
- package/docs/adr/README.md +1 -1
- package/docs/api/openapi.json +153 -4
- package/docs/api-reference.md +5 -3
- package/docs/architecture.md +3 -0
- package/docs/copy-inventory.md +60 -59
- package/docs/copy-registry.json +72 -63
- package/docs/design/project-startup-direction.md +63 -0
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +8 -0
- package/docs/page-inventory.json +9 -3
- package/docs/page-readings.json +427 -410
- package/docs/recipes/private-npm-distribution.md +1 -1
- package/migrations/core_269_session_transcripts.sql +35 -0
- package/migrations/core_270_craft_achievements.sql +128 -0
- package/modules/economy/achievements.js +199 -4
- package/modules/economy/credits.js +16 -0
- package/modules/economy/reward.js +3 -0
- package/modules/hall-ui/public/builders.js +1 -1
- package/modules/hall-ui/public/genesis-home.js +17 -3
- package/modules/hall-ui/public/hall-render.js +24 -2
- package/modules/hall-ui/public/index-genesis-demo.states.json +13 -0
- package/modules/hall-ui/public/roster.css +21 -0
- package/modules/hall-ui/public/roster.html +3 -0
- package/modules/hall-ui/public/roster.js +121 -21
- package/modules/hall-ui/public/roster.states.json +26 -1
- package/modules/hall-ui/records/builders-roster.md +20 -0
- package/modules/lifecycle/publish-reconciler.js +25 -0
- package/modules/lifecycle/workflow-dispatch.js +21 -11
- package/modules/npm-release/release.js +10 -1
- package/modules/onboarding/founding-birth.js +116 -0
- package/modules/onboarding/genesis-home.js +72 -5
- package/modules/onboarding/port.js +9 -2
- package/modules/onboarding/routes/onboarding.js +8 -1
- package/modules/provisioning/birth.js +64 -0
- package/modules/provisioning/dns-resolves.js +54 -0
- package/modules/provisioning/migrations/provisioning_036_dns_resolved.sql +30 -0
- package/modules/provisioning/pollers/liveness-sweep.js +5 -0
- package/modules/provisioning/provisioning.js +7 -6
- package/modules/provisioning/routes/provisioning.js +6 -2
- package/modules/provisioning/tests/liveness.mjs +48 -0
- package/modules/public-landing/public/projects-dns-pending.states.json +85 -0
- package/modules/public-landing/public/projects.html +40 -15
- package/modules/sessions/db.js +58 -0
- package/modules/sessions/routes/sessions.js +97 -0
- package/modules/status-ui/public/status.js +39 -7
- package/modules/status-ui/public/style.css +24 -0
- package/modules/ui-design/kit/fixtures/founding-genesis-demo.json +176 -0
- package/modules/ui-design/kit/fixtures/me-founding-demo.json +27 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances-demo.json +3 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances-dns-pending.json +49 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances.json +3 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +45 -0
- package/scripts/gds/provision.js +77 -53
- package/scripts/gds/session-digest.js +11 -3
- package/scripts/gds/session-transcript-build.js +155 -0
- package/scripts/gds/session-transcript-upload.js +29 -0
- package/scripts/gds/ship-session-upload.js +6 -1
- package/scripts/gds/upgrade-commit-identity.js +32 -0
- package/scripts/gds/upgrade.js +1 -1
- package/scripts/hall-preview/refresh-fixtures.js +2 -0
- package/src/bongos/db.js +13 -0
- package/src/bongos/role-stats.js +36 -2
- package/src/bongos/routes/public.js +13 -2
- package/src/bongos/routes.js +3 -0
- package/src/branding.js +9 -0
- package/src/module-api.js +1 -1
- package/tests/achievements_reconcile.mjs +13 -0
- package/tests/craft_achievements.mjs +349 -0
- package/tests/founding_birth.mjs +200 -0
- package/tests/founding_builders.mjs +2 -1
- package/tests/founding_mode.mjs +3 -1
- package/tests/genesis_home.mjs +71 -2
- package/tests/hall_builders_page_pass.mjs +2 -1
- package/tests/hub_map_orb_states.mjs +5 -1
- package/tests/leaderboard_roles.mjs +272 -0
- package/tests/npm_release_release.mjs +37 -2
- package/tests/projects_hub.mjs +5 -3
- package/tests/projects_hub_dns_ready.mjs +153 -0
- package/tests/projects_hub_pre_uat.mjs +9 -5
- package/tests/provision_dns_first.mjs +171 -0
- package/tests/provision_settings_apply.mjs +6 -1
- package/tests/provisioning_settings_env.mjs +49 -0
- package/tests/session_transcript.mjs +273 -0
- package/tests/ui_design_kit.mjs +1 -1
- package/tests/upgrade_commit_identity.mjs +89 -0
- package/tests/wizard_demo.mjs +29 -3
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// scripts/gds/session-transcript-build.js — the PURE transcript→full-transcript builder.
|
|
2
|
+
//
|
|
3
|
+
// ADR 0051 / task 1001004: every session is stored as its FULL per-turn content
|
|
4
|
+
// (user prompts, assistant text, tool calls AND their results), not just the
|
|
5
|
+
// metadata + 160-char snippets buildSessionDigest keeps. This is the sibling of
|
|
6
|
+
// session-digest-build.js and shares its scrubber: EVERY string leaves this file
|
|
7
|
+
// through scrubSecrets (modules/security/secret-scrub.js, the task-1003 scrubber),
|
|
8
|
+
// on the builder's box, before anything is uploaded. A string that reaches the
|
|
9
|
+
// output unscrubbed is a defect — keep that single choke point (`clean`).
|
|
10
|
+
//
|
|
11
|
+
// Bounded on purpose: each text field is capped and the whole transcript has a
|
|
12
|
+
// byte ceiling (head + tail kept, like buildSessionDigest's turn cap), so one
|
|
13
|
+
// runaway tool result cannot make an upload that the server must reject.
|
|
14
|
+
// Extended-thinking blocks are NOT captured: they are model-internal, often
|
|
15
|
+
// opaque, and ADR 0051 asks for what the builder asked and what they were told.
|
|
16
|
+
const { scrubSecrets } = require('../../modules/security/secret-scrub');
|
|
17
|
+
|
|
18
|
+
const MAX_FIELD_CHARS = 20000; // one prompt / reply / tool result / tool input
|
|
19
|
+
const MAX_TRANSCRIPT_BYTES = 12 * 1024 * 1024; // whole upload ceiling (the server enforces the same number)
|
|
20
|
+
const MAX_TRANSCRIPT_TURNS = 20000; // the route's maxItems; the builder trims to it so the server never has to refuse
|
|
21
|
+
const MAX_TURN_BYTES = 200 * 1024; // under the route's 256 KB per-turn cap, so a many-tool-call turn is shrunk here rather than refused there
|
|
22
|
+
const MAX_RAW_CHARS = 200000; // bound on what the scrubber is ever handed (entropy-sweep cost)
|
|
23
|
+
|
|
24
|
+
// clean — the one exit for every string: drop NULs (Postgres rejects them in
|
|
25
|
+
// jsonb), SCRUB, then cap. Scrub BEFORE the final cap so a credential straddling
|
|
26
|
+
// the cap boundary is redacted whole rather than sliced into a fragment too short
|
|
27
|
+
// for any scrub shape to recognise. The scrubber is only ever handed MAX_RAW_CHARS
|
|
28
|
+
// so a multi-MB tool result cannot make the entropy sweep unbounded.
|
|
29
|
+
function clean(text, extraSecrets) {
|
|
30
|
+
if (typeof text !== 'string' || text.length === 0) return undefined;
|
|
31
|
+
const flat = text.replace(/\0/g, '');
|
|
32
|
+
if (!flat) return undefined;
|
|
33
|
+
const bounded = flat.length > MAX_RAW_CHARS ? flat.slice(0, MAX_RAW_CHARS) : flat;
|
|
34
|
+
const scrubbed = scrubSecrets(bounded, extraSecrets);
|
|
35
|
+
if (!scrubbed) return undefined;
|
|
36
|
+
if (scrubbed.length <= MAX_FIELD_CHARS && flat.length === bounded.length) return scrubbed;
|
|
37
|
+
const omitted = (flat.length - bounded.length) + Math.max(0, scrubbed.length - MAX_FIELD_CHARS);
|
|
38
|
+
return `${scrubbed.slice(0, MAX_FIELD_CHARS)}…[+${omitted} chars]`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resultText(content) {
|
|
42
|
+
if (typeof content === 'string') return content;
|
|
43
|
+
if (Array.isArray(content)) {
|
|
44
|
+
return content.map((p) => (p && typeof p.text === 'string' ? p.text : '')).filter(Boolean).join('\n');
|
|
45
|
+
}
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// shrinkTurn — cut every string field of an oversize turn to an even share of a
|
|
50
|
+
// fixed character budget (in place), marking the cut. Already scrubbed, so this
|
|
51
|
+
// only ever removes text.
|
|
52
|
+
function shrinkTurn(t) {
|
|
53
|
+
const fields = [];
|
|
54
|
+
if (typeof t.text === 'string') fields.push([t, 'text']);
|
|
55
|
+
for (const c of t.tool_calls || []) if (typeof c.input === 'string') fields.push([c, 'input']);
|
|
56
|
+
for (const r of t.tool_results || []) if (typeof r.text === 'string') fields.push([r, 'text']);
|
|
57
|
+
const cap = Math.max(200, Math.floor(40000 / Math.max(1, fields.length)));
|
|
58
|
+
for (const [o, k] of fields) {
|
|
59
|
+
if (o[k].length > cap) o[k] = `${o[k].slice(0, cap)}…[+${o[k].length - cap} chars]`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// buildSessionTranscript — parsed Claude Code transcript entries →
|
|
64
|
+
// { turns, turnCount, byteSize, truncated }. Pure; exported for tests.
|
|
65
|
+
function buildSessionTranscript(entries, opts = {}) {
|
|
66
|
+
const extra = opts.extraSecrets || [];
|
|
67
|
+
const maxBytes = opts.maxBytes || MAX_TRANSCRIPT_BYTES;
|
|
68
|
+
const maxTurns = opts.maxTurns || MAX_TRANSCRIPT_TURNS;
|
|
69
|
+
const turns = [];
|
|
70
|
+
let i = 0;
|
|
71
|
+
for (const e of Array.isArray(entries) ? entries : []) {
|
|
72
|
+
if (!e || typeof e !== 'object' || !e.message) continue;
|
|
73
|
+
const ts = typeof e.timestamp === 'string' ? e.timestamp : undefined;
|
|
74
|
+
const msg = e.message;
|
|
75
|
+
if (e.type === 'assistant') {
|
|
76
|
+
const row = { i, role: 'assistant', ts };
|
|
77
|
+
const texts = [];
|
|
78
|
+
const calls = [];
|
|
79
|
+
for (const c of Array.isArray(msg.content) ? msg.content : []) {
|
|
80
|
+
if (c && c.type === 'text' && typeof c.text === 'string') texts.push(c.text);
|
|
81
|
+
else if (c && c.type === 'tool_use') {
|
|
82
|
+
let input;
|
|
83
|
+
try { input = clean(JSON.stringify(c.input), extra); } catch (_) { input = undefined; }
|
|
84
|
+
calls.push({ name: String(c.name || '?'), input });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const text = clean(texts.join('\n'), extra);
|
|
88
|
+
if (text) row.text = text;
|
|
89
|
+
if (calls.length) row.tool_calls = calls;
|
|
90
|
+
if (!text && !calls.length) continue; // pure-thinking line: nothing readable
|
|
91
|
+
turns.push(row);
|
|
92
|
+
i++;
|
|
93
|
+
} else if (e.type === 'user') {
|
|
94
|
+
const row = { i, role: 'user', ts };
|
|
95
|
+
if (typeof msg.content === 'string') {
|
|
96
|
+
const text = clean(msg.content, extra);
|
|
97
|
+
if (text) row.text = text;
|
|
98
|
+
} else if (Array.isArray(msg.content)) {
|
|
99
|
+
const texts = [];
|
|
100
|
+
const results = [];
|
|
101
|
+
for (const c of msg.content) {
|
|
102
|
+
if (c && c.type === 'text' && typeof c.text === 'string') texts.push(c.text);
|
|
103
|
+
else if (c && c.type === 'tool_result') {
|
|
104
|
+
const r = { text: clean(resultText(c.content), extra) };
|
|
105
|
+
if (c.is_error) r.is_error = true;
|
|
106
|
+
results.push(r);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const text = clean(texts.join('\n'), extra);
|
|
110
|
+
if (text) row.text = text;
|
|
111
|
+
if (results.length) row.tool_results = results;
|
|
112
|
+
}
|
|
113
|
+
if (!row.text && !row.tool_results) continue;
|
|
114
|
+
turns.push(row);
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Byte ceiling: keep the head (what was asked) and the tail (how it ended),
|
|
120
|
+
// drop the middle, and SAY so — a silently shortened record reads as complete.
|
|
121
|
+
// Serialize each turn once; an oversize turn (many tool calls, each at the field
|
|
122
|
+
// cap) is shrunk field-by-field to fit, so the route's per-turn cap never refuses
|
|
123
|
+
// a transcript the builder could have trimmed. Sizes include the comma each turn
|
|
124
|
+
// costs in the array, and the total the 2 bytes of brackets, so this total is the
|
|
125
|
+
// SAME number the route measures against its ceiling.
|
|
126
|
+
const sizes = turns.map((t, idx) => {
|
|
127
|
+
let json = JSON.stringify(t);
|
|
128
|
+
let bytes = Buffer.byteLength(json, 'utf8');
|
|
129
|
+
if (bytes > MAX_TURN_BYTES) {
|
|
130
|
+
shrinkTurn(t);
|
|
131
|
+
json = JSON.stringify(t);
|
|
132
|
+
bytes = Buffer.byteLength(json, 'utf8');
|
|
133
|
+
}
|
|
134
|
+
turns[idx] = t;
|
|
135
|
+
return bytes + 1;
|
|
136
|
+
});
|
|
137
|
+
let total = sizes.reduce((a, b) => a + b, 2);
|
|
138
|
+
let out = turns;
|
|
139
|
+
let truncated = false;
|
|
140
|
+
if (total > maxBytes || turns.length > maxTurns) {
|
|
141
|
+
truncated = true;
|
|
142
|
+
const headBudget = Math.floor(maxBytes * 0.6);
|
|
143
|
+
const tailBudget = maxBytes - headBudget;
|
|
144
|
+
let h = 0; let headEnd = 0;
|
|
145
|
+
const headCount = Math.floor(maxTurns * 0.6);
|
|
146
|
+
while (headEnd < turns.length && headEnd < headCount && h + sizes[headEnd] <= headBudget) h += sizes[headEnd++];
|
|
147
|
+
let t = 0; let tailStart = turns.length;
|
|
148
|
+
while (tailStart > headEnd && (turns.length - tailStart) < maxTurns - headEnd && t + sizes[tailStart - 1] <= tailBudget) t += sizes[--tailStart];
|
|
149
|
+
out = turns.slice(0, headEnd).concat(turns.slice(tailStart));
|
|
150
|
+
total = h + t + 2;
|
|
151
|
+
}
|
|
152
|
+
return { turns: out, turnCount: turns.length, byteSize: total, truncated };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = { buildSessionTranscript, MAX_FIELD_CHARS, MAX_TRANSCRIPT_BYTES, MAX_TRANSCRIPT_TURNS };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// scripts/gds/session-transcript-upload.js — send a session's FULL scrubbed
|
|
2
|
+
// transcript to the corpus (ADR 0051, task 1001004).
|
|
3
|
+
//
|
|
4
|
+
// Shared by the SessionEnd hook (session-digest.js — fires for EVERY session) and
|
|
5
|
+
// the on-ship upload (ship-session-upload.js — covers the mid-session tail), so
|
|
6
|
+
// both build and send the transcript the same way. BEST-EFFORT and NON-FATAL like
|
|
7
|
+
// the digest upload beside it: returns a one-line outcome string, never throws.
|
|
8
|
+
// It must run AFTER the digest POST — the server creates the transcript row only
|
|
9
|
+
// from the caller's own session_records row.
|
|
10
|
+
const { buildSessionTranscript } = require('./session-transcript-build');
|
|
11
|
+
|
|
12
|
+
async function uploadSessionTranscript(api, sessionId, entries, extraSecrets) {
|
|
13
|
+
try {
|
|
14
|
+
const t = buildSessionTranscript(entries, { extraSecrets });
|
|
15
|
+
if (!t.turns.length) return 'transcript: nothing to store.';
|
|
16
|
+
const r = await api.sessions.putSessionsSidTranscript({
|
|
17
|
+
sid: sessionId,
|
|
18
|
+
body: { turns: t.turns, truncated: t.truncated },
|
|
19
|
+
});
|
|
20
|
+
if (r && r.ok) {
|
|
21
|
+
return `transcript: stored ${t.turns.length}/${t.turnCount} turns (${Math.round(t.byteSize / 1024)} KB${t.truncated ? ', truncated to the size ceiling' : ''}).`;
|
|
22
|
+
}
|
|
23
|
+
return `transcript: skipped (PUT ${r && r.status}${r && r.data && r.data.error ? ' ' + JSON.stringify(r.data.error) : ''}).`;
|
|
24
|
+
} catch (e) {
|
|
25
|
+
return `transcript: skipped — ${e && e.message ? e.message : e}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { uploadSessionTranscript };
|
|
@@ -13,6 +13,7 @@ const path = require('node:path');
|
|
|
13
13
|
const { cliClient, loadSession } = require('./cli-lib');
|
|
14
14
|
const { resolveTranscriptPath } = require('./ship-memory-sync.js');
|
|
15
15
|
const { apiErrorLine } = require('./ship-io.js');
|
|
16
|
+
const { uploadSessionTranscript } = require('./session-transcript-upload.js');
|
|
16
17
|
const { REPO_ROOT } = require('./ship-state.js');
|
|
17
18
|
|
|
18
19
|
// ---------- Session upload-on-ship (6D.1 / ADR 0027) ----------
|
|
@@ -268,11 +269,15 @@ async function uploadSessionOnShip(taskId) {
|
|
|
268
269
|
metrics: digest.metrics,
|
|
269
270
|
model_usage: digest.metrics.model_usage || {}, // per-model tokens → cost-plus reward (task 1005)
|
|
270
271
|
};
|
|
271
|
-
const
|
|
272
|
+
const api = await cliClient({ allowUnauthenticated: true });
|
|
273
|
+
const r = await api.sessions.postSessions({ body: payload });
|
|
272
274
|
if (r.ok) {
|
|
273
275
|
console.log(
|
|
274
276
|
` session upload: stored digest (${digest.metrics.assistant_turns} assistant turns, ${digest.totalTokens} tokens, ${digest.metrics.tool_error_count} tool errors, ${digest.metrics.harness_retry_count} harness retries).`
|
|
275
277
|
);
|
|
278
|
+
// ADR 0051: the full scrubbed transcript rides after the digest (the server
|
|
279
|
+
// creates it from the digest's row). Non-fatal — its outcome is a log line.
|
|
280
|
+
console.log(` session upload: ${await uploadSessionTranscript(api, sessionId, entries, extraSecrets)}`);
|
|
276
281
|
} else if (r.status === 401) {
|
|
277
282
|
recordUploadSkip(taskId, {
|
|
278
283
|
code: 'session_invalid',
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// scripts/gds/upgrade-commit-identity.js — WHO the pin commit is signed by when the project's
|
|
4
|
+
// git names nobody (task 1004514). Kept out of upgrade.js, which sits at the 1500-line cap.
|
|
5
|
+
//
|
|
6
|
+
// THE BUG. persistPin staged the new pin and ran `git commit`. In a repo with no
|
|
7
|
+
// user.name/user.email — none in the repo, none global for the account the upgrade runs as —
|
|
8
|
+
// git refuses ("Please tell me who you are"), so the move reported success with the pin
|
|
9
|
+
// STAGED but never committed. The next move then read its own leftovers as a dirty tree and
|
|
10
|
+
// spent its one automatic fix (wedge-remedy.js) committing them. Seen live on 2026-10-01: a
|
|
11
|
+
// throwaway hosted project on cloudbongos.com, and hermeslines-marketing, whose repo carries no
|
|
12
|
+
// identity while every scaffolded one does (provision-repo.js sets it at scaffold time).
|
|
13
|
+
//
|
|
14
|
+
// THE FIX IS A FALLBACK, NEVER AN OVERRIDE. A repo or account that names someone keeps
|
|
15
|
+
// signing as them. Only an empty answer gets the provisioner's own identity, the one the
|
|
16
|
+
// scaffold commits as, through git's AUTHOR/COMMITTER env — so nothing is written into the
|
|
17
|
+
// owner's git config, and the argv stays exactly what it was.
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Env for `git commit` in `instanceDir`: null when git already names a committer there, else
|
|
21
|
+
* the provisioner's identity. Asks git itself, so repo, global and system config all count.
|
|
22
|
+
*/
|
|
23
|
+
function commitIdentityEnv(instanceDir, run, env = process.env) {
|
|
24
|
+
const r = run('git', ['config', 'user.email'], { cwd: instanceDir, encoding: 'utf8' });
|
|
25
|
+
if (r && !r.error && r.status === 0 && String(r.stdout || '').trim()) return null;
|
|
26
|
+
if (env.GIT_AUTHOR_EMAIL || env.GIT_COMMITTER_EMAIL) return null; // the caller already chose
|
|
27
|
+
const { provisionerBotName, provisionerBotEmail } = require('./instance-apex.js');
|
|
28
|
+
const name = provisionerBotName(), email = provisionerBotEmail(env);
|
|
29
|
+
return { ...env, GIT_AUTHOR_NAME: name, GIT_AUTHOR_EMAIL: email, GIT_COMMITTER_NAME: name, GIT_COMMITTER_EMAIL: email };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { commitIdentityEnv };
|
package/scripts/gds/upgrade.js
CHANGED
|
@@ -352,7 +352,7 @@ function persistPin({ instanceDir, fromVersion, toVersion, commitPin, push = tru
|
|
|
352
352
|
err(' ! could not stage the pin files — commit + push them by hand or the next deploy reverts this bump.');
|
|
353
353
|
return { applicable: true, committed: false, pushed: false, dirty, error: 'git add failed' };
|
|
354
354
|
}
|
|
355
|
-
const commit = run('git', ['commit', '-m', message], { cwd: instanceDir, encoding: 'utf8' });
|
|
355
|
+
const idEnv = require('./upgrade-commit-identity').commitIdentityEnv(instanceDir, run), commit = run('git', ['commit', '-m', message], { cwd: instanceDir, encoding: 'utf8', ...(idEnv ? { env: idEnv } : {}) }); // a repo naming nobody still commits (task 1004514)
|
|
356
356
|
if (!commit || commit.error || commit.status !== 0) {
|
|
357
357
|
err(' ! could not commit the pin — commit + push it by hand or the next deploy reverts this bump.');
|
|
358
358
|
return { applicable: true, committed: false, pushed: false, dirty, error: 'git commit failed' };
|
|
@@ -76,6 +76,8 @@ const SNAPSHOTS = [
|
|
|
76
76
|
['public/grades?days=7', 'public__grades'],
|
|
77
77
|
['public/repo-health?days=30', 'public__repo-health'],
|
|
78
78
|
['public/bounty-table', 'public__bounty-table'],
|
|
79
|
+
// the roster and the Standing card read the public list, with its roles (task 1004437)
|
|
80
|
+
['public/leaderboard', 'public__leaderboard'],
|
|
79
81
|
['public/cost-summary', 'public__cost-summary'],
|
|
80
82
|
['public/progress', 'public__progress'],
|
|
81
83
|
['public/recent-shipped', 'public__recent-shipped'],
|
package/src/bongos/db.js
CHANGED
|
@@ -207,6 +207,18 @@ async function getBuilderRoleChoice(builderId) {
|
|
|
207
207
|
return rows[0] ?? null;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
// The same two facts for a whole list of builders, in ONE statement (task
|
|
211
|
+
// 1004437): the public leaderboard names every row's roles. Map<idString, row>.
|
|
212
|
+
async function getBuilderRoleChoices(builderIds) {
|
|
213
|
+
const ids = (builderIds || []).map(String).filter((v) => /^[1-9]\d*$/.test(v));
|
|
214
|
+
if (!ids.length) return new Map();
|
|
215
|
+
const { rows } = await pool.query(
|
|
216
|
+
`SELECT id, preferred_disciplines, main_discipline FROM builders WHERE id = ANY($1::bigint[])`,
|
|
217
|
+
[ids]
|
|
218
|
+
);
|
|
219
|
+
return new Map(rows.map((r) => [String(r.id), r]));
|
|
220
|
+
}
|
|
221
|
+
|
|
210
222
|
// The builder-PREFS db slice — the five per-builder preference jsonb columns on
|
|
211
223
|
// `builders` (skill_model_prefs / sound_prefs / behavior_prefs / event_sound_prefs)
|
|
212
224
|
// — was carved into modules/builder-settings/db.js (BV1.R80 / ADR 0093 §1). Core
|
|
@@ -1103,6 +1115,7 @@ module.exports = {
|
|
|
1103
1115
|
updateBuilderDisciplines,
|
|
1104
1116
|
UPDATE_BUILDER_DISCIPLINES_SQL,
|
|
1105
1117
|
getBuilderRoleChoice,
|
|
1118
|
+
getBuilderRoleChoices,
|
|
1106
1119
|
RANKS,
|
|
1107
1120
|
setBuilderRank,
|
|
1108
1121
|
getLatestRankChangeForBuilder,
|
package/src/bongos/role-stats.js
CHANGED
|
@@ -149,7 +149,41 @@ async function readRoleStats(builderId, { shippedCount = 0, getRoleChoice } = {}
|
|
|
149
149
|
return composeRoleStats({ choice, byCraft, shippedCount, artist, ideator });
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
//
|
|
152
|
+
// readRolesForBuilders — the same main/shown rules for a WHOLE LIST at once
|
|
153
|
+
// (task 1004437 / WA6.RP07): the public leaderboard names each builder's roles
|
|
154
|
+
// and ranks a role by the credits earned in it, so every row needs its `shown`
|
|
155
|
+
// crafts and its ledger split. Two reads for the whole page of rows, never one
|
|
156
|
+
// per row: the role choices (`getRoleChoices`, one statement) and economy's
|
|
157
|
+
// `creditsByCraft`, which already batches. Returns Map<idString, { main,
|
|
158
|
+
// main_basis, shown, credits_by_craft }>, every id asked for present. Counts and
|
|
159
|
+
// sums only, as the profile's tally is: nothing here is behind a sign-in.
|
|
160
|
+
async function readRolesForBuilders(builderIds, { getRoleChoices } = {}) {
|
|
161
|
+
const ids = [...new Set((builderIds || []).map(String).filter((v) => /^[1-9]\d*$/.test(v)))];
|
|
162
|
+
const out = new Map();
|
|
163
|
+
if (!ids.length) return out;
|
|
164
|
+
const reward = seams.resolveOptional('reward');
|
|
165
|
+
const [choices, byCraftMap] = await Promise.all([
|
|
166
|
+
getRoleChoices ? getRoleChoices(ids) : new Map(),
|
|
167
|
+
reward && typeof reward.creditsByCraft === 'function' ? reward.creditsByCraft(ids) : null,
|
|
168
|
+
]);
|
|
169
|
+
for (const id of ids) {
|
|
170
|
+
const choice = (choices && choices.get(id)) || null;
|
|
171
|
+
const byCraft = byCraftMap ? (byCraftMap.get(id) || null) : null;
|
|
172
|
+
const preferred = (choice && choice.preferred_disciplines) || [];
|
|
173
|
+
const { main, basis } = resolveMainRole({ mainDiscipline: choice && choice.main_discipline, preferred, byCraft });
|
|
174
|
+
out.set(id, {
|
|
175
|
+
main,
|
|
176
|
+
main_basis: basis,
|
|
177
|
+
shown: shownCrafts({ main, preferred, byCraft }),
|
|
178
|
+
credits_by_craft: byCraft
|
|
179
|
+
? { engineer: byCraft.engineer, artist: byCraft.artist, ideator: byCraft.ideator, other: byCraft.other, total: byCraft.total }
|
|
180
|
+
: null,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Only the readers are exported: the pure halves above are reached through them
|
|
153
187
|
// (tests/profile_role_stats.mjs drives them via the real route), and an export
|
|
154
188
|
// nothing imports is dead surface to the knip ratchet.
|
|
155
|
-
module.exports = { readRoleStats };
|
|
189
|
+
module.exports = { readRoleStats, readRolesForBuilders };
|
|
@@ -37,6 +37,7 @@ const { clientBranding, branding } = require('../../branding');
|
|
|
37
37
|
const { clientModules } = require('../../modules');
|
|
38
38
|
const { publicReadRateLimitMiddleware } = require('../middleware/rate-limit');
|
|
39
39
|
const { parseId, corsPublicGet, parsePagination, pageMeta } = require('./_helpers');
|
|
40
|
+
const { readRolesForBuilders } = require('../role-stats');
|
|
40
41
|
// task 1003208: structured logging — was console.*. A core file logs through
|
|
41
42
|
// the kernel logger directly; only MODULES go via api.logger (the doorway).
|
|
42
43
|
const log = require('../logger').logger.child({ src: 'public' });
|
|
@@ -235,9 +236,19 @@ module.exports = function buildPublicRouter() {
|
|
|
235
236
|
// 100 preserves the prior ceiling. `page` is additive (readers of .leaderboard
|
|
236
237
|
// unchanged). Mirrors the authed /leaderboard.
|
|
237
238
|
const { limit, offset } = parsePagination(req, { defaultLimit: 100, maxLimit: 200 });
|
|
238
|
-
const rows = await seams.resolveOptional('reward').getLeaderboardWithAchievements(offset + limit, 5);
|
|
239
|
+
const rows = (await seams.resolveOptional('reward').getLeaderboardWithAchievements(offset + limit, 5)).slice(offset, offset + limit);
|
|
240
|
+
// task 1004437 (WA6.RP07): each row names its roles, main first, and carries
|
|
241
|
+
// the credits each craft earned, so a role filter ranks by a figure the row
|
|
242
|
+
// itself publishes (ADR 0255). Two reads for the page, not one per row. A
|
|
243
|
+
// failure costs the roles, never the board: the rows still go out.
|
|
244
|
+
try {
|
|
245
|
+
const roles = await readRolesForBuilders(rows.map((r) => r.id), { getRoleChoices: db.getBuilderRoleChoices });
|
|
246
|
+
for (const r of rows) r.roles = roles.get(String(r.id)) || null;
|
|
247
|
+
} catch (err) {
|
|
248
|
+
log.error({ err }, 'leaderboard roles failed');
|
|
249
|
+
}
|
|
239
250
|
res.set('Cache-Control', 'public, max-age=60');
|
|
240
|
-
res.json({ leaderboard: rows
|
|
251
|
+
res.json({ leaderboard: rows, page: pageMeta(limit, offset) });
|
|
241
252
|
});
|
|
242
253
|
|
|
243
254
|
// rank: public — est-vs-actual aggregate.
|
package/src/bongos/routes.js
CHANGED
|
@@ -93,6 +93,9 @@ function buildGdsRouter() {
|
|
|
93
93
|
if (
|
|
94
94
|
req.path === '/memory/sync' ||
|
|
95
95
|
req.path === '/sessions' ||
|
|
96
|
+
// ADR 0051 / task 1001004: the full-transcript upload (MB-scale), which
|
|
97
|
+
// mounts its own 16 MB parser in modules/sessions/routes/sessions.js.
|
|
98
|
+
/^\/sessions\/[A-Za-z0-9_-]+\/transcript$/.test(req.path) ||
|
|
96
99
|
req.path === '/llm-cache/store' ||
|
|
97
100
|
/^\/tasks\/\d+\/publish-branch$/.test(req.path) ||
|
|
98
101
|
// task 1003109: the ship-time task visual — a base64 image up to ~5.3 MB
|
package/src/branding.js
CHANGED
|
@@ -94,6 +94,15 @@ const ENV_OVERRIDES = [
|
|
|
94
94
|
['NIGHT_TINT', ['project', 'nightTint']],
|
|
95
95
|
['LOOK_ACCENT', ['project', 'lookAccent']],
|
|
96
96
|
['LOGO_URL', ['project', 'logoUrl']],
|
|
97
|
+
// The birth marker (task 1004506): set by the platform only on a project created
|
|
98
|
+
// through the startup flow, so a project made before that never has it (spec D8).
|
|
99
|
+
// FOUNDING_BIRTH is when the platform made the row; DEMO_PEOPLE / DEMO_HOURS are the
|
|
100
|
+
// demo's time box, empty for a real project. The onboarding module reads them once to
|
|
101
|
+
// start genesis (modules/onboarding/founding-birth.js). SERVER-ONLY: clientBranding
|
|
102
|
+
// publishes no `founding` block.
|
|
103
|
+
['FOUNDING_BIRTH', ['founding', 'birth']],
|
|
104
|
+
['DEMO_PEOPLE', ['founding', 'demoPeople']],
|
|
105
|
+
['DEMO_HOURS', ['founding', 'demoHours']],
|
|
97
106
|
];
|
|
98
107
|
|
|
99
108
|
const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.
|
|
78
|
+
const CORE_VERSION = '1.21.2'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -100,6 +100,19 @@ function fakeClient({ eligible = new Set(), unlocked = new Set(), credits = 0 })
|
|
|
100
100
|
if (s.includes("reason = 'ship.net_negative'")) {
|
|
101
101
|
return { rows: [{ n: eligible.has('subtractor') ? 3 : 0 }], rowCount: 1 };
|
|
102
102
|
}
|
|
103
|
+
// the craft achievements (task 1004438): ONE facts read answers all seven
|
|
104
|
+
if (s.startsWith('select bool_or(reason like $2) as ignited')) {
|
|
105
|
+
return {
|
|
106
|
+
rows: [{
|
|
107
|
+
ignited: eligible.has('first-thought-ignited'),
|
|
108
|
+
full_idea: eligible.has('first-full-idea'),
|
|
109
|
+
ratified: eligible.has('first-board-ratification'),
|
|
110
|
+
pages: eligible.has('ten-pages-approved') ? 10 : eligible.has('first-page-approved') ? 1 : 0,
|
|
111
|
+
best_thought_tasks: eligible.has('thought-built-ten') ? 10 : 0,
|
|
112
|
+
}],
|
|
113
|
+
rowCount: 1,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
103
116
|
return no;
|
|
104
117
|
});
|
|
105
118
|
|