@bongos/core 1.19.454 → 1.19.456
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 +116 -51
- package/docs/adr/0047-discord-bugs-intake-anti-abuse.md +1 -0
- package/docs/copy-inventory.md +231 -228
- package/docs/copy-registry.json +352 -312
- package/docs/module-api-changelog.md +4 -0
- package/modules/discord/db.js +0 -3
- package/modules/discord/discord-bot.js +8 -1
- package/modules/discord/discord-bugs.js +133 -74
- package/modules/discord/module.json +1 -1
- package/modules/hall-ui/public/atlas.css +41 -32
- package/modules/hall-ui/public/atlas.html +8 -0
- package/modules/hall-ui/public/atlas.states.json +60 -0
- package/modules/hall-ui/public/collab.css +2 -2
- package/modules/hall-ui/public/collab.html +7 -1
- package/modules/hall-ui/public/collab.states.json +60 -0
- package/modules/hall-ui/public/copy-desk.css +21 -16
- package/modules/hall-ui/public/copy-desk.html +9 -1
- package/modules/hall-ui/public/copy-desk.states.json +59 -0
- package/modules/hall-ui/public/drachmae.html +38 -5
- package/modules/hall-ui/public/drachmae.js +20 -2
- package/modules/hall-ui/public/drachmae.states.json +64 -0
- package/modules/hall-ui/public/gate.css +20 -25
- package/modules/hall-ui/public/gate.html +9 -1
- package/modules/hall-ui/public/gate.states.json +60 -0
- package/modules/hall-ui/public/goals-lib.js +22 -5
- package/modules/hall-ui/public/goals-render.js +27 -5
- package/modules/hall-ui/public/goals.css +141 -60
- package/modules/hall-ui/public/goals.html +8 -1
- package/modules/hall-ui/public/goals.states.json +74 -0
- package/modules/hall-ui/public/government.css +35 -36
- package/modules/hall-ui/public/government.html +9 -1
- package/modules/hall-ui/public/government.states.json +59 -0
- package/modules/hall-ui/public/harbor.css +23 -26
- package/modules/hall-ui/public/harbor.html +9 -1
- package/modules/hall-ui/public/harbor.states.json +59 -0
- package/modules/hall-ui/public/modules.css +22 -21
- package/modules/hall-ui/public/modules.html +9 -1
- package/modules/hall-ui/public/modules.states.json +73 -0
- package/modules/hall-ui/public/people.html +7 -1
- package/modules/hall-ui/public/people.states.json +59 -0
- package/modules/hall-ui/public/scouting.html +7 -1
- package/modules/hall-ui/public/scouting.states.json +54 -0
- package/modules/hall-ui/public/sessions.css +1 -1
- package/modules/hall-ui/public/sessions.html +9 -1
- package/modules/hall-ui/public/sessions.states.json +58 -0
- package/modules/hall-ui/public/style.css +125 -5
- package/modules/hall-ui/public/watch.css +36 -39
- package/modules/hall-ui/public/watch.html +9 -1
- package/modules/hall-ui/public/watch.states.json +60 -0
- package/modules/lifecycle/goal-suggest.js +70 -2
- package/modules/lifecycle/lifecycle.js +7 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/discord_bugs.mjs +110 -31
- package/tests/filing_abuse_matrix.mjs +59 -20
- package/tests/goal_subtitle.mjs +14 -8
- package/tests/goal_suggest.mjs +107 -1
|
@@ -1345,5 +1345,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1345
1345
|
landed since 1.19.452 with no explicit bump. run 33553290345. (task 1002620)
|
|
1346
1346
|
1.19.454 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1347
1347
|
landed since 1.19.453 with no explicit bump. run 33554965618. (task 1002620)
|
|
1348
|
+
1.19.455 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1349
|
+
landed since 1.19.454 with no explicit bump. run 33558738299. (task 1002620)
|
|
1350
|
+
1.19.456 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1351
|
+
landed since 1.19.455 with no explicit bump. run 33560584235. (task 1002620)
|
|
1348
1352
|
---------------------------------------------------------------------------
|
|
1349
1353
|
```
|
package/modules/discord/db.js
CHANGED
|
@@ -143,9 +143,6 @@ module.exports = {
|
|
|
143
143
|
markBoardMessageEdited,
|
|
144
144
|
// kernel db helpers, re-exported off the doorway (one `db.*` namespace)
|
|
145
145
|
insertAuditLog: (...a) => api.insertAuditLog(...a),
|
|
146
|
-
// createTask is a LIFECYCLE write — resolve the `lifecycle` kernel port at call
|
|
147
|
-
// time (the doorway leak retired at BV1.R86); used by discord-bugs (#bugs → task).
|
|
148
|
-
createTask: (...a) => api.resolveOptional('lifecycle').createTask(...a),
|
|
149
146
|
getBuilderById: (...a) => api.getBuilderById(...a),
|
|
150
147
|
get LIVE_RANK_LADDER() { return api.LIVE_RANK_LADDER; },
|
|
151
148
|
// the shared pool, for any module file that wants it via db.pool
|
|
@@ -348,7 +348,14 @@ async function start() {
|
|
|
348
348
|
// real filing path (recordInboundBug). Built once at startup.
|
|
349
349
|
const bugAggregator = bugs.createBugAggregator({
|
|
350
350
|
onFlush: (combined) => bugs.recordInboundBug(combined).then((result) => {
|
|
351
|
-
|
|
351
|
+
// task 1003067: a filing now always yields an IDEA; whether it also became
|
|
352
|
+
// a task depends on the landing matrix. Log both, so a homeless report is
|
|
353
|
+
// traceable instead of showing as a bare "filed" with no id to chase.
|
|
354
|
+
log.info(`[discord-bot] #bugs burst → ${result.outcome}${
|
|
355
|
+
result.ideaId
|
|
356
|
+
? ` (idea ${result.ideaId}${result.taskId ? `, task ${result.taskId}, ${result.status}` : ', inbox'}, ${result.images || 0} img)`
|
|
357
|
+
: (result.reason ? ` (${result.reason})` : '')
|
|
358
|
+
}`);
|
|
352
359
|
return result;
|
|
353
360
|
}),
|
|
354
361
|
});
|
|
@@ -1,28 +1,45 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Discord #bugs →
|
|
3
|
+
// Discord #bugs → a routed filing — Phase 3 F8 (ADR 0033 §F8, ADR 0047),
|
|
4
|
+
// retired onto the capture-time routing path by task 1003067 (BV1.R11).
|
|
4
5
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// is the deliberate difference from #ideas (discord-inbound.js): a bug is a
|
|
8
|
-
// small fix a builder wants to drop without context-switching, and it becomes
|
|
9
|
-
// claimable work directly.
|
|
6
|
+
// WHAT CHANGED AND WHY. This path used to call createTask DIRECTLY, against a
|
|
7
|
+
// hardcoded version id. Two things followed from that, and both were bugs:
|
|
10
8
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
9
|
+
// * The hardcoded id was GDS-V3, a SHIPPED version. Every bug reported through
|
|
10
|
+
// Discord resolved into that version's catch-all goal — a bucket nobody ever
|
|
11
|
+
// opens again. That is idea 1000771, the report goal 1000071 was filed over.
|
|
12
|
+
// * Creating the task directly meant NO idea row and NO lineage, so the person
|
|
13
|
+
// who reported the bug vanished from the record the moment it became work.
|
|
14
|
+
// The ideator credit lane (goal 1000067) pays against that lineage, so this
|
|
15
|
+
// vector was quietly minting unattributable work.
|
|
16
|
+
//
|
|
17
|
+
// Now the message becomes a CAPTURE (ideas.capture, the same port #ideas uses)
|
|
18
|
+
// with a suggested goal, and the ideas module's landing matrix decides the rest.
|
|
19
|
+
// The version is no longer named here at all: the goal carries it, and lifecycle
|
|
20
|
+
// resolves which version is open to work (suggestGoalForCapture). A vector that
|
|
21
|
+
// does not know a version id cannot hardcode a dead one.
|
|
22
|
+
//
|
|
23
|
+
// ANTI-ABUSE GUARD (ADR 0047 — the red-team resolution), as it stands now:
|
|
24
|
+
// 1. AN UNLINKED AUTHOR CANNOT CREATE WORK. The original lever was
|
|
25
|
+
// linked-builder-ONLY: an unlinked message was dropped entirely, because
|
|
26
|
+
// this path minted claimable work with no triage gate. It no longer mints
|
|
27
|
+
// anything — an unlinked author's report becomes an idea_inbox row with
|
|
28
|
+
// captured_by NULL and NO goal, which is precisely what #ideas has always
|
|
29
|
+
// done for the same person. The security property ADR 0047 actually bought
|
|
30
|
+
// ("an unlinked author cannot inject claimable work") is intact and now
|
|
31
|
+
// structural; what is gone is the silent drop of a real bug report, which
|
|
32
|
+
// only ever protected us from reading it.
|
|
33
|
+
// 2. RANK-AWARE LANDING, now the ideas landing matrix rather than a local
|
|
34
|
+
// copy of it: a linked Metic+ builder's bug lands 'ready', a linked
|
|
35
|
+
// sub-Metic builder's lands 'backlog' awaiting a nod. Same outcomes as
|
|
36
|
+
// before, one definition instead of two (pinned in
|
|
37
|
+
// tests/filing_abuse_matrix.mjs).
|
|
38
|
+
// 3. PER-BUILDER RATE-LIMIT. At most N filings per LINKED builder per rolling
|
|
39
|
+
// hour (counted from audit_log). Over-limit filings are ignored AND audited
|
|
40
|
+
// so a spammer surfaces in forensics / the red-team view. An unlinked author
|
|
41
|
+
// has no builder id to count against, so they are held to the #ideas posture
|
|
42
|
+
// — an inbox row, never a task — rather than a cap this table cannot express.
|
|
26
43
|
// 4. BODY SCREEN. Empty / too-short / text-free messages are ignored before
|
|
27
44
|
// any DB write; title + body are capped to the GDS limits.
|
|
28
45
|
//
|
|
@@ -40,11 +57,15 @@ const { auditDiscordBot } = require('./discord-audit');
|
|
|
40
57
|
const auth = require('./auth');
|
|
41
58
|
const attachments = require('./bug-attachments');
|
|
42
59
|
const { combineParts, createMessageAggregator } = require('./discord-message-buffer');
|
|
43
|
-
const
|
|
60
|
+
const api = require('../../src/module-api');
|
|
61
|
+
const { LIMITS } = api;
|
|
62
|
+
const log = api.logger('discord');
|
|
44
63
|
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
64
|
+
// NO LOCAL RANK RULE LIVES HERE (task 1003067). READY_RANKS + decideBugLanding
|
|
65
|
+
// used to decide ready-vs-backlog for this channel alone. The ideas landing matrix
|
|
66
|
+
// (modules/ideas/routing.js) now decides it for EVERY filing vector, so a second
|
|
67
|
+
// copy here could only ever drift from it — which is what
|
|
68
|
+
// tests/filing_abuse_matrix.mjs exists to prevent.
|
|
48
69
|
|
|
49
70
|
// Smallest message we'll treat as a real bug report. Anything shorter is noise.
|
|
50
71
|
const MIN_BUG_CHARS = 8;
|
|
@@ -53,13 +74,11 @@ const MIN_BUG_CHARS = 8;
|
|
|
53
74
|
// DISCORD_BUGS_RATE_PER_HOUR overrides (a number); falls back to this.
|
|
54
75
|
const DEFAULT_RATE_PER_HOUR = 5;
|
|
55
76
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// Modest reward for fixing a drive-by bug. Env DISCORD_BUGS_CREDITS overrides.
|
|
62
|
-
const DEFAULT_BUGS_CREDITS = 10;
|
|
77
|
+
// NO DEFAULT VERSION LIVES HERE ANY MORE (task 1003067). DEFAULT_BUGS_VERSION_ID
|
|
78
|
+
// used to be 'GDS-V3' and DISCORD_BUGS_CREDITS set a per-task reward. The version
|
|
79
|
+
// is now carried by the goal the filing routes into — a vector that cannot name a
|
|
80
|
+
// version cannot name a dead one, which is idea 1000771 in one sentence — and the
|
|
81
|
+
// reward auto-assigns at the lowest create layer like every other vector (ADR 0096).
|
|
63
82
|
|
|
64
83
|
// Auto-pairing window: consecutive #bugs messages from the SAME author within
|
|
65
84
|
// this window are combined into ONE bug-task (so "screenshot" + "here's what
|
|
@@ -75,27 +94,11 @@ function rateLimitPerHour(env = process.env) {
|
|
|
75
94
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : DEFAULT_RATE_PER_HOUR;
|
|
76
95
|
}
|
|
77
96
|
|
|
78
|
-
function bugsVersionId(env = process.env) {
|
|
79
|
-
return env.DISCORD_BUGS_VERSION_ID || DEFAULT_BUGS_VERSION_ID;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function bugsCredits(env = process.env) {
|
|
83
|
-
const n = Number(env.DISCORD_BUGS_CREDITS);
|
|
84
|
-
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : DEFAULT_BUGS_CREDITS;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
97
|
function pairWindowMs(env = process.env) {
|
|
88
98
|
const n = Number(env.DISCORD_BUGS_PAIR_WINDOW_MS);
|
|
89
99
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : DEFAULT_PAIR_WINDOW_MS;
|
|
90
100
|
}
|
|
91
101
|
|
|
92
|
-
// PURE — decide where a bug lands from the reporter's LIVE rank. Metic+ →
|
|
93
|
-
// 'ready' (straight-through, claimable now); any other linked rank → 'backlog'
|
|
94
|
-
// (a Metic+ promotes it, the lightweight confirm gate). Unit-tested.
|
|
95
|
-
function decideBugLanding(rank) {
|
|
96
|
-
return READY_RANKS.has(String(rank || '').toLowerCase()) ? 'ready' : 'backlog';
|
|
97
|
-
}
|
|
98
|
-
|
|
99
102
|
// PURE — screen + shape a raw message into { ok, title, bodyMd } or
|
|
100
103
|
// { ok:false, reason }. The body screen rejects blank / too-short / text-free
|
|
101
104
|
// (no letters) messages before any DB write — UNLESS the message carries
|
|
@@ -127,7 +130,18 @@ function screenBug(content, { hasImages = false } = {}) {
|
|
|
127
130
|
const realDeps = {
|
|
128
131
|
getBuilderByDiscordUserId: db.getBuilderByDiscordUserId,
|
|
129
132
|
countRecentDiscordBugFilings: db.countRecentDiscordBugFilings,
|
|
130
|
-
|
|
133
|
+
// The two kernel ports this vector files through (never an import — the one-way
|
|
134
|
+
// module rule, ADR 0083/0093). `ideas.capture` is the same door #ideas uses;
|
|
135
|
+
// `lifecycle` answers which goal, on which still-open version.
|
|
136
|
+
captureIdea: (...a) => api.resolve('ideas.capture')(...a),
|
|
137
|
+
// resolveOptional, not resolve: an instance running without the lifecycle module
|
|
138
|
+
// has no goals to suggest, and that is a reason to file the report WITHOUT a
|
|
139
|
+
// home — never a reason to lose it. Returns null, which lands it in the inbox.
|
|
140
|
+
suggestGoalForCapture: async (...a) => {
|
|
141
|
+
const lifecycle = api.resolveOptional && api.resolveOptional('lifecycle');
|
|
142
|
+
if (!lifecycle || !lifecycle.suggestGoalForCapture) return null;
|
|
143
|
+
return lifecycle.suggestGoalForCapture(...a);
|
|
144
|
+
},
|
|
131
145
|
insertAuditLog: db.insertAuditLog,
|
|
132
146
|
persistImages: attachments.persistImages,
|
|
133
147
|
};
|
|
@@ -144,14 +158,13 @@ async function recordInboundBug({ discordUserId, content, imageUrls = [], messag
|
|
|
144
158
|
if (!screened.ok) return { outcome: 'ignored', reason: screened.reason };
|
|
145
159
|
|
|
146
160
|
const limit = config.rateLimitPerHour ?? rateLimitPerHour();
|
|
147
|
-
const versionId = config.versionId ?? bugsVersionId();
|
|
148
|
-
const credits = config.credits ?? bugsCredits();
|
|
149
161
|
|
|
150
162
|
try {
|
|
151
|
-
// (1)
|
|
152
|
-
// unlinked author
|
|
163
|
+
// (1) WHO POSTED. A linked builder files with their rank and is rate-limited;
|
|
164
|
+
// an unlinked author still gets their report recorded, but only ever as an
|
|
165
|
+
// inbox row (no goal, no rank ⇒ the matrix cannot route it into work). See
|
|
166
|
+
// the guard note in the header for why this replaced the outright drop.
|
|
153
167
|
const builder = await deps.getBuilderByDiscordUserId(discordUserId);
|
|
154
|
-
if (!builder) return { outcome: 'ignored', reason: 'unlinked' };
|
|
155
168
|
|
|
156
169
|
// (3) PER-BUILDER RATE-LIMIT. Count this builder's bug filings in the last
|
|
157
170
|
// hour; over the cap → reject AND audit (so a spammer is visible in
|
|
@@ -160,7 +173,7 @@ async function recordInboundBug({ discordUserId, content, imageUrls = [], messag
|
|
|
160
173
|
let recent = 0;
|
|
161
174
|
let countFailed = false;
|
|
162
175
|
try {
|
|
163
|
-
recent = await deps.countRecentDiscordBugFilings(builder.id, sinceIso);
|
|
176
|
+
if (builder) recent = await deps.countRecentDiscordBugFilings(builder.id, sinceIso);
|
|
164
177
|
} catch (e) {
|
|
165
178
|
// L4 (red-team 2026-06-12): FAIL CLOSED on a count error. The old behaviour
|
|
166
179
|
// proceeded ("fail-open"), which let a DB error be used to slip past the
|
|
@@ -170,7 +183,7 @@ async function recordInboundBug({ discordUserId, content, imageUrls = [], messag
|
|
|
170
183
|
console.error('[discord-bot] bug rate-limit count failed (denying — fail-closed):', e.message);
|
|
171
184
|
countFailed = true;
|
|
172
185
|
}
|
|
173
|
-
if (countFailed || recent >= limit) {
|
|
186
|
+
if (builder && (countFailed || recent >= limit)) {
|
|
174
187
|
const reason = countFailed ? 'rate_limit_count_error' : 'rate_limited';
|
|
175
188
|
await auditDiscordBot(deps, {
|
|
176
189
|
builderId: builder.id,
|
|
@@ -182,8 +195,11 @@ async function recordInboundBug({ discordUserId, content, imageUrls = [], messag
|
|
|
182
195
|
return { outcome: 'ignored', reason, recent };
|
|
183
196
|
}
|
|
184
197
|
|
|
185
|
-
// (2) RANK-AWARE LANDING
|
|
186
|
-
|
|
198
|
+
// (2) RANK-AWARE LANDING is no longer decided here. The rank still comes from
|
|
199
|
+
// the LIVE builder row and never from the message — it is simply handed to the
|
|
200
|
+
// ideas landing matrix, which owns the ready-vs-backlog split for every filing
|
|
201
|
+
// vector. One definition, so this channel cannot drift from the others.
|
|
202
|
+
const filerRank = builder ? builder.rank : null;
|
|
187
203
|
|
|
188
204
|
// Re-host any images on our server (durable — Discord CDN links expire). The
|
|
189
205
|
// guard (host allowlist + content-type + magic-byte sniff + size/count caps)
|
|
@@ -201,7 +217,9 @@ async function recordInboundBug({ discordUserId, content, imageUrls = [], messag
|
|
|
201
217
|
// Provenance footer so a claimer knows where the bug came from + who to ask.
|
|
202
218
|
// The raw Discord snowflake is NOT embedded (it lives in the Archon-only
|
|
203
219
|
// audit_log); only the builder login. Then the durable attachment links.
|
|
204
|
-
const origin =
|
|
220
|
+
const origin = builder
|
|
221
|
+
? `\n\n— reported from Discord #bugs by ${builder.github_login}`
|
|
222
|
+
: '\n\n— reported from Discord #bugs (unlinked author)';
|
|
205
223
|
let description = `${screened.bodyMd}${origin}`;
|
|
206
224
|
if (stored.length > 0) {
|
|
207
225
|
const imgs = stored.map((s, i) => ``).join('\n');
|
|
@@ -209,27 +227,71 @@ async function recordInboundBug({ discordUserId, content, imageUrls = [], messag
|
|
|
209
227
|
}
|
|
210
228
|
description = description.slice(0, LIMITS.DESCRIPTION);
|
|
211
229
|
|
|
212
|
-
|
|
213
|
-
|
|
230
|
+
// THE HOME. Only a linked builder's report is offered one: an unlinked author
|
|
231
|
+
// stays strictly less privileged than any signed-in builder (the #ideas rule),
|
|
232
|
+
// and with no goal the matrix files them to the inbox, which is the point.
|
|
233
|
+
// A null suggestion is the ordinary case and lands in the inbox too.
|
|
234
|
+
let goal = null;
|
|
235
|
+
if (builder) {
|
|
236
|
+
goal = await deps.suggestGoalForCapture({ title: screened.title, body: screened.bodyMd });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// The capture. One transaction inside the ideas module writes the idea row,
|
|
240
|
+
// the task (when the matrix routes it) and the lineage that keeps the reporter
|
|
241
|
+
// attached to the work — the whole reason this vector stopped calling
|
|
242
|
+
// createTask. kind is forced to 'bug' because the channel IS the classification.
|
|
243
|
+
const capture = {
|
|
214
244
|
title: screened.title,
|
|
215
|
-
description,
|
|
216
|
-
status,
|
|
245
|
+
bodyMd: description,
|
|
217
246
|
kind: 'bug',
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
247
|
+
capturedBy: builder ? builder.id : null,
|
|
248
|
+
filerRank,
|
|
249
|
+
goalId: goal ? goal.id : null,
|
|
250
|
+
suggestedPriority: 3,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// THE HOME IS A SUGGESTION, NOT A CONDITION OF BEING HEARD. routeIdeaToGoal is
|
|
254
|
+
// ONE transaction, so a goal that refuses the landing — protected scope, a
|
|
255
|
+
// scope-wall verdict, a goal archived mid-flight — rolls the idea row back too
|
|
256
|
+
// and the report is gone. That is the exact failure this task exists to end, and
|
|
257
|
+
// it would be caused by a goal WE guessed at, not by anything the reporter did.
|
|
258
|
+
// So a refusal drops the guess and files the report homeless: it lands in the
|
|
259
|
+
// inbox for triage, which is strictly what would have happened before we tried.
|
|
260
|
+
let idea;
|
|
261
|
+
try {
|
|
262
|
+
idea = await deps.captureIdea(capture);
|
|
263
|
+
} catch (routeErr) {
|
|
264
|
+
if (!capture.goalId) throw routeErr; // nothing to drop — a real failure
|
|
265
|
+
log.warn({ err: routeErr && (routeErr.code || routeErr.message), goalId: capture.goalId },
|
|
266
|
+
'routed capture refused — filing the report to the inbox instead');
|
|
267
|
+
idea = await deps.captureIdea({ ...capture, goalId: null });
|
|
268
|
+
}
|
|
223
269
|
|
|
270
|
+
const routed = idea && idea.routing && idea.routing.routed ? idea.routing : null;
|
|
224
271
|
await auditDiscordBot(deps, {
|
|
225
|
-
builderId: builder.id,
|
|
272
|
+
builderId: builder ? builder.id : null,
|
|
226
273
|
route: 'discord:bugs',
|
|
227
|
-
requestBodyRedacted: {
|
|
274
|
+
requestBodyRedacted: {
|
|
275
|
+
discord_user_id: discordUserId,
|
|
276
|
+
message_id: messageId,
|
|
277
|
+
idea_id: idea.id,
|
|
278
|
+
task_id: routed ? routed.task_id : null,
|
|
279
|
+
goal_id: routed ? routed.goal_id : null,
|
|
280
|
+
status: routed ? routed.status : null,
|
|
281
|
+
images: stored.length,
|
|
282
|
+
},
|
|
228
283
|
responseStatus: 201,
|
|
229
284
|
logLabel: 'bug audit-log write failed',
|
|
230
285
|
});
|
|
231
286
|
|
|
232
|
-
return {
|
|
287
|
+
return {
|
|
288
|
+
outcome: 'filed',
|
|
289
|
+
ideaId: idea.id,
|
|
290
|
+
taskId: routed ? routed.task_id : null,
|
|
291
|
+
status: routed ? routed.status : null,
|
|
292
|
+
builderId: builder ? builder.id : null,
|
|
293
|
+
images: stored.length,
|
|
294
|
+
};
|
|
233
295
|
} catch (err) {
|
|
234
296
|
console.error('[discord-bot] recordInboundBug failed:', err.message);
|
|
235
297
|
return { outcome: 'error', error: err.message };
|
|
@@ -245,13 +307,10 @@ function createBugAggregator({ onFlush, windowMs = pairWindowMs(), timers = glob
|
|
|
245
307
|
|
|
246
308
|
module.exports = {
|
|
247
309
|
screenBug,
|
|
248
|
-
decideBugLanding,
|
|
249
310
|
recordInboundBug,
|
|
250
311
|
combineParts,
|
|
251
312
|
createBugAggregator,
|
|
252
313
|
realDeps,
|
|
253
314
|
rateLimitPerHour,
|
|
254
|
-
bugsVersionId,
|
|
255
|
-
bugsCredits,
|
|
256
315
|
pairWindowMs,
|
|
257
316
|
};
|
|
@@ -13,40 +13,49 @@
|
|
|
13
13
|
renderer reads at draw time, with dark variants, so the scene adapts to
|
|
14
14
|
the theme without the pack touching the encoding. */
|
|
15
15
|
.atlas-page{
|
|
16
|
-
/*
|
|
16
|
+
/* THE SEMANTIC MAP PALETTE — the sanctioned exception, alongside the shell's
|
|
17
|
+
status vocabulary. ADR 0094's invariant is that a pack's colours must never
|
|
18
|
+
scramble the map's colour-coded MEANING, so these nine are fixed literals on
|
|
19
|
+
purpose and are not the Fifteen Rule's business. Everything BELOW them is
|
|
20
|
+
chrome, and chrome is the world's (task 1003307). */
|
|
17
21
|
--ab-sun:#6366f1; --ab-sky:#0ea5e9; --ab-coral:#f43f5e; --ab-paper:#b3a284;
|
|
18
22
|
--ab-good:#10b981; --ab-core:#f59e0b; --ab-db:#ef4444; --ab-ghost:#94a3b8; --ab-danger:#dc2626;
|
|
19
23
|
/* theme-dependent, non-semantic scene values (read by atlas.js) */
|
|
20
24
|
--ab-label-ink: var(--ink);
|
|
21
|
-
--ab-label-halo: var(--
|
|
25
|
+
--ab-label-halo: var(--bg-card);
|
|
26
|
+
/* Pure white with alpha as an ambient wash — the Fifteen Rule's one legal
|
|
27
|
+
literal shape (a scrim, a mask or a halo). */
|
|
22
28
|
--ab-ambient: rgba(255,255,255,.5);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
--ab-
|
|
27
|
-
|
|
29
|
+
/* Was #c7cdf7 / #3b418c, a private periwinkle nobody could re-theme. The world
|
|
30
|
+
already holds a second hue for exactly this: --sea, which the hall otherwise
|
|
31
|
+
never spends. */
|
|
32
|
+
--ab-glow: color-mix(in srgb, var(--sea) 30%, var(--bg-card));
|
|
33
|
+
/* The badge fills were eight literals plus eight dark twins. They are the
|
|
34
|
+
four-state vocabulary the shell already measures on both grounds in both
|
|
35
|
+
modes (tests/hall_tokens.mjs), so they are that vocabulary now — which is
|
|
36
|
+
also why this rule needs no dark block for them any more. */
|
|
37
|
+
--ab-b-good-bg:var(--ok-weak); --ab-b-good-fg:var(--ok);
|
|
38
|
+
--ab-b-warn-bg:var(--warn-weak); --ab-b-warn-fg:var(--warn);
|
|
39
|
+
--ab-b-danger-bg:var(--danger-weak); --ab-b-danger-fg:var(--danger);
|
|
40
|
+
--ab-b-info-bg:var(--info-weak); --ab-b-info-fg:var(--info);
|
|
28
41
|
color:var(--ink);
|
|
29
|
-
border:
|
|
42
|
+
border:var(--line) solid var(--rule-soft); border-radius:var(--radius);
|
|
30
43
|
background:
|
|
31
44
|
radial-gradient(ellipse 70% 55% at 50% 46%, color-mix(in srgb, var(--accent) 4%, transparent), transparent 70%),
|
|
32
|
-
var(--
|
|
45
|
+
var(--bg-card);
|
|
33
46
|
}
|
|
47
|
+
/* The dark twin, still written twice and still byte-identical — that rule does
|
|
48
|
+
not bend. What shrank is what it has to SAY: the badge pairs flip themselves
|
|
49
|
+
now that they are the status vocabulary, so only the two genuinely scene-level
|
|
50
|
+
values are left (task 1003307). */
|
|
34
51
|
html[data-theme="dark"] .atlas-page{
|
|
35
52
|
--ab-ambient: rgba(255,255,255,.03);
|
|
36
|
-
--ab-glow:
|
|
37
|
-
--ab-b-good-bg:#123227; --ab-b-good-fg:#5eead4;
|
|
38
|
-
--ab-b-warn-bg:#33260d; --ab-b-warn-fg:#fcd34d;
|
|
39
|
-
--ab-b-danger-bg:#371616; --ab-b-danger-fg:#fca5a5;
|
|
40
|
-
--ab-b-info-bg:#0f2740; --ab-b-info-fg:#7dd3fc;
|
|
53
|
+
--ab-glow: color-mix(in srgb, var(--sea) 55%, var(--bg));
|
|
41
54
|
}
|
|
42
55
|
@media (prefers-color-scheme: dark){
|
|
43
56
|
html:not([data-theme]) .atlas-page{
|
|
44
57
|
--ab-ambient: rgba(255,255,255,.03);
|
|
45
|
-
--ab-glow:
|
|
46
|
-
--ab-b-good-bg:#123227; --ab-b-good-fg:#5eead4;
|
|
47
|
-
--ab-b-warn-bg:#33260d; --ab-b-warn-fg:#fcd34d;
|
|
48
|
-
--ab-b-danger-bg:#371616; --ab-b-danger-fg:#fca5a5;
|
|
49
|
-
--ab-b-info-bg:#0f2740; --ab-b-info-fg:#7dd3fc;
|
|
58
|
+
--ab-glow: color-mix(in srgb, var(--sea) 55%, var(--bg));
|
|
50
59
|
}
|
|
51
60
|
}
|
|
52
61
|
.atlas-page *{box-sizing:border-box}
|
|
@@ -54,12 +63,12 @@ html[data-theme="dark"] .atlas-page{
|
|
|
54
63
|
|
|
55
64
|
.atlas-toolbar{display:flex; align-items:center; gap:12px 16px; flex-wrap:wrap; padding:16px 20px 0}
|
|
56
65
|
.ab-chip{font-weight:600; font-size:10.5px; text-transform:uppercase; letter-spacing:.1em;
|
|
57
|
-
color:var(--accent-
|
|
58
|
-
.ab-modes{display:inline-flex; gap:0; border:
|
|
66
|
+
color:var(--accent-ink); background:var(--accent-tint); border:var(--line) solid color-mix(in srgb, var(--accent) 30%, transparent); padding:3px 9px; border-radius: var(--radius-pill)}
|
|
67
|
+
.ab-modes{display:inline-flex; gap:0; border:var(--line) solid var(--rule); border-radius:var(--radius-sm); overflow:hidden}
|
|
59
68
|
.ab-modes button{font-family:var(--font-body); font-weight:500; font-size:12.5px; cursor:pointer; border:0;
|
|
60
|
-
background:var(--
|
|
61
|
-
.ab-modes button + button{border-left:
|
|
62
|
-
.ab-modes button.active{background:var(--accent-
|
|
69
|
+
background:var(--bg-card); color:var(--ink-soft); padding:6px 14px; transition:background .15s, color .15s}
|
|
70
|
+
.ab-modes button + button{border-left:var(--line) solid var(--rule)}
|
|
71
|
+
.ab-modes button.active{background:var(--accent-tint); color:var(--accent-ink)}
|
|
63
72
|
.ab-modes button:focus-visible{outline:2px solid var(--accent); outline-offset:-2px}
|
|
64
73
|
|
|
65
74
|
.ab-caption{padding:13px 20px 0; margin:0; font-size:13px; line-height:1.5; color:var(--ink-soft); max-width:90ch}
|
|
@@ -68,10 +77,10 @@ html[data-theme="dark"] .atlas-page{
|
|
|
68
77
|
.ab-scene{position:relative; width:100%; min-height:500px; padding:6px 0}
|
|
69
78
|
.ab-scene svg{display:block; width:100%; height:auto}
|
|
70
79
|
.ab-hint{position:absolute; left:20px; bottom:12px; font-size:11.5px; color:var(--ink-soft);
|
|
71
|
-
background:color-mix(in srgb, var(--
|
|
80
|
+
background:color-mix(in srgb, var(--bg-card) 75%, transparent); backdrop-filter:blur(4px); padding:5px 11px; border-radius: var(--radius-pill); border:var(--line) solid var(--rule-soft)}
|
|
72
81
|
.ab-empty{position:absolute; inset:0; display:none; align-items:center; justify-content:center; text-align:center; padding:40px}
|
|
73
82
|
.ab-empty.show{display:flex}
|
|
74
|
-
.ab-empty .card{background:var(--
|
|
83
|
+
.ab-empty .card{background:var(--bg-card); border:var(--line) solid var(--rule-soft); border-radius:var(--radius); padding:26px 30px; max-width:460px;
|
|
75
84
|
box-shadow:var(--shadow-pop)}
|
|
76
85
|
.ab-empty h2{margin:0 0 8px; font-size:16px; font-weight:650}
|
|
77
86
|
.ab-empty p{margin:0; font-size:13.5px; line-height:1.55; color:var(--ink-soft)}
|
|
@@ -82,8 +91,8 @@ html[data-theme="dark"] .atlas-page{
|
|
|
82
91
|
.ab-legend .ln{width:18px; border-top:3px solid}
|
|
83
92
|
.ab-foot{padding:10px 20px 20px; font-size:11px; color:var(--ink-faint); font-variant-numeric:tabular-nums}
|
|
84
93
|
|
|
85
|
-
.ab-popup{position:absolute; z-index:6; width:300px; max-width:88%; background:var(--
|
|
86
|
-
border:
|
|
94
|
+
.ab-popup{position:absolute; z-index:6; width:300px; max-width:88%; background:var(--bg-card);
|
|
95
|
+
border:var(--line) solid var(--rule-soft); border-radius:var(--radius); padding:15px 16px; display:none;
|
|
87
96
|
box-shadow:var(--shadow-pop)}
|
|
88
97
|
.ab-popup.show{display:block}
|
|
89
98
|
.ab-popup-close{position:absolute; top:8px; right:11px; cursor:pointer; border:0; background:transparent;
|
|
@@ -91,13 +100,13 @@ html[data-theme="dark"] .atlas-page{
|
|
|
91
100
|
.ab-popup-close:hover{color:var(--ink)}
|
|
92
101
|
.ab-ptitle{font-weight:650; font-size:15px; display:flex; align-items:center;
|
|
93
102
|
gap:8px; flex-wrap:wrap; line-height:1.25; padding-right:18px}
|
|
94
|
-
.ab-ppath{font-family:
|
|
103
|
+
.ab-ppath{font-family: var(--font-mono); font-size:11.5px; color:var(--ink-soft); margin-top:4px; word-break:break-all}
|
|
95
104
|
.ab-pdesc{font-size:12.8px; line-height:1.55; margin:11px 0 0; color:var(--ink-soft)}
|
|
96
105
|
.ab-prow{margin-top:11px; font-size:10.5px; font-weight:600; color:var(--ink-faint); text-transform:uppercase; letter-spacing:.06em}
|
|
97
106
|
.ab-chips{display:flex; flex-wrap:wrap; gap:6px; margin-top:6px}
|
|
98
|
-
.ab-chip2{font-size:11px; font-weight:500; background:var(--
|
|
99
|
-
padding:4px 9px; border-radius:
|
|
100
|
-
.ab-badge{font-weight:600; font-size:10.5px; text-transform:uppercase; letter-spacing:.05em; padding:3px 8px; border-radius:
|
|
107
|
+
.ab-chip2{font-size:11px; font-weight:500; background:var(--glass); border:var(--line) solid var(--rule-soft);
|
|
108
|
+
padding:4px 9px; border-radius: var(--radius-pill); font-variant-numeric:tabular-nums}
|
|
109
|
+
.ab-badge{font-weight:600; font-size:10.5px; text-transform:uppercase; letter-spacing:.05em; padding:3px 8px; border-radius: var(--radius-pill)}
|
|
101
110
|
.ab-b-good{background:var(--ab-b-good-bg); color:var(--ab-b-good-fg)} .ab-b-warn{background:var(--ab-b-warn-bg); color:var(--ab-b-warn-fg)}
|
|
102
111
|
.ab-b-danger{background:var(--ab-b-danger-bg); color:var(--ab-b-danger-fg)} .ab-b-info{background:var(--ab-b-info-bg); color:var(--ab-b-info-fg)}
|
|
103
112
|
|
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
<div class="app-body">
|
|
20
20
|
<header class="app-topbar" id="app-topbar"></header>
|
|
21
21
|
<div class="app-content">
|
|
22
|
+
<!-- task 1003307: the atlas shipped with no page head and no <h1> — the
|
|
23
|
+
map filled the column and the page had no name in the document. The
|
|
24
|
+
board's label voice gives it one without competing with the map. -->
|
|
25
|
+
<div class="page-head page-head--board">
|
|
26
|
+
<h1 class="page-head__h">Repo atlas</h1>
|
|
27
|
+
<p class="page-head__sub">A self-drawing map of the codebase: what each module is, and what depends on it.</p>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
22
30
|
<main id="hall-main">
|
|
23
31
|
<!-- The atlas chrome rides the hall tokens (pack + dark aware, task 1737);
|
|
24
32
|
only the semantic node/edge palette stays instance-independent so a
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "Repo atlas — the self-drawing module map. Its SEMANTIC node palette is fixed by ADR 0094 and is not the token tier's business; everything around it (toolbar, chips, legend, popup, badges) is chrome and rides the world. Written with the re-skin (task 1003307) so this page is MEASURED at three widths in both modes rather than looked at once.",
|
|
3
|
+
"page": "/builders/atlas",
|
|
4
|
+
"surface": "hall-ui",
|
|
5
|
+
"stub": {
|
|
6
|
+
"prefix": "/builders/"
|
|
7
|
+
},
|
|
8
|
+
"modeQuery": false,
|
|
9
|
+
"_feeds": "The kit's own stub carries no feed for this page, so against it the page renders its EMPTY and SKELETON states — which is the point: those are states the floors must hold in. Against the hall-preview harness the same states render populated. Every declared 404 below is a fixture the harness does not carry, named rather than hidden.",
|
|
10
|
+
"ignoreRequests": [
|
|
11
|
+
"/api/(gds|bongos)/me$",
|
|
12
|
+
"/api/(gds|bongos)/me/",
|
|
13
|
+
"/api/(gds|bongos)/goals",
|
|
14
|
+
"/api/(gds|bongos)/inbox/",
|
|
15
|
+
"/api/(gds|bongos)/versions",
|
|
16
|
+
"/api/(gds|bongos)/atlas",
|
|
17
|
+
"/api/(gds|bongos)/public/repo",
|
|
18
|
+
"/builders/atlas.json"
|
|
19
|
+
],
|
|
20
|
+
"ignoreConsole": [
|
|
21
|
+
"/api/(gds|bongos)/",
|
|
22
|
+
"\\b401\\b",
|
|
23
|
+
"\\b403\\b",
|
|
24
|
+
"\\b404\\b",
|
|
25
|
+
"\\b501\\b",
|
|
26
|
+
"Unauthorized",
|
|
27
|
+
"Forbidden",
|
|
28
|
+
"Not Found",
|
|
29
|
+
"Not Implemented",
|
|
30
|
+
"load failed"
|
|
31
|
+
],
|
|
32
|
+
"states": {
|
|
33
|
+
"map": {
|
|
34
|
+
"auth": true,
|
|
35
|
+
"actions": [
|
|
36
|
+
[
|
|
37
|
+
"wait",
|
|
38
|
+
900
|
|
39
|
+
]
|
|
40
|
+
],
|
|
41
|
+
"expect": {
|
|
42
|
+
"visible": [
|
|
43
|
+
"#scene"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"out": {
|
|
48
|
+
"auth": false
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"reducedMotion": {
|
|
52
|
+
"auth": true,
|
|
53
|
+
"actions": [
|
|
54
|
+
[
|
|
55
|
+
"wait",
|
|
56
|
+
600
|
|
57
|
+
]
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
.cb-list { list-style: none; margin: 10px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
6
6
|
|
|
7
7
|
.cb-item {
|
|
8
|
-
border:
|
|
8
|
+
border: var(--line) solid var(--rule);
|
|
9
9
|
border-left: 3px solid var(--rule);
|
|
10
10
|
border-radius: var(--radius-sm);
|
|
11
|
-
background: var(--
|
|
11
|
+
background: var(--bg-card);
|
|
12
12
|
padding: 10px 12px;
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -19,7 +19,13 @@
|
|
|
19
19
|
<div class="app-body">
|
|
20
20
|
<header class="app-topbar" id="app-topbar"></header>
|
|
21
21
|
<div class="app-content">
|
|
22
|
-
|
|
22
|
+
<!-- task 1003307: the board page head. Collab shipped with NO <h1> at all
|
|
23
|
+
(one-header-main-h1 measured 0 on every render), so the re-skin
|
|
24
|
+
carries the structural fix rather than porting the hole. The heading
|
|
25
|
+
is the LABEL voice, not a display line: on a working surface the
|
|
26
|
+
page's own name is the least interesting thing on it. -->
|
|
27
|
+
<div class="page-head page-head--board">
|
|
28
|
+
<h1 class="page-head__h">Collab</h1>
|
|
23
29
|
<p class="page-head__sub" id="cb-sub">Loading…</p>
|
|
24
30
|
</div>
|
|
25
31
|
|