@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
|
@@ -26,10 +26,14 @@
|
|
|
26
26
|
// really does belong with a goal scoped to `hall-ui`.
|
|
27
27
|
|
|
28
28
|
const { tokenize } = require('./criterion-suggest.js');
|
|
29
|
-
const {
|
|
29
|
+
const { versionAcceptsNewWork } = require('./db-tasks.js');
|
|
30
|
+
const { candidateGoals, generalGoalTitle } = require('./goal-advisory.js');
|
|
30
31
|
const api = require('../../src/module-api');
|
|
31
32
|
// Through the published logger seam, not a raw console call (est-advisory.js precedent).
|
|
32
33
|
const proseErr = (err) => api.logger('lifecycle').error({ err: err && err.message }, 'GET /goals/suggestions failed');
|
|
34
|
+
// The capture-time twin has its own message: it is not an HTTP route, and a log
|
|
35
|
+
// line that names the wrong caller is a wrong turn taken during an incident.
|
|
36
|
+
const captureErr = (err) => api.logger('lifecycle').error({ err: err && err.message }, 'suggestGoalForCapture failed — filing without a goal');
|
|
33
37
|
|
|
34
38
|
// Matches suggestCriteria's default so the two suggesters feel the same.
|
|
35
39
|
const SUGGEST_LIMIT = 5;
|
|
@@ -82,6 +86,70 @@ function suggestGoals(goals, draft = {}, opts = {}) {
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
|
|
89
|
+
// suggestGoalForCapture — ONE goal for a filing that arrived with no goal named
|
|
90
|
+
// (task 1003067 / BV1.R11). The HTTP suggester above hands a human five options to
|
|
91
|
+
// choose from; a Discord message has nobody to choose, so this collapses the same
|
|
92
|
+
// ranking to a single confident answer or none at all.
|
|
93
|
+
//
|
|
94
|
+
// WHY IT RESOLVES THE VERSION ITSELF. The vector this was built for — Discord
|
|
95
|
+
// #bugs — used to carry a hardcoded version id, and because that id was a SHIPPED
|
|
96
|
+
// version every bug reported through it landed in a tomb nobody reads (idea
|
|
97
|
+
// 1000771, the failure goal 1000071 exists to fix). A filing vector should not be
|
|
98
|
+
// in the business of naming a version at all, so it does not: the GOAL carries the
|
|
99
|
+
// version, and this picks the goal.
|
|
100
|
+
//
|
|
101
|
+
// It therefore ranks across EVERY version still open to work (versionAcceptsNewWork,
|
|
102
|
+
// task 1003061), not one. An earlier cut of this declined whenever more than one
|
|
103
|
+
// version was open — prudent-looking, and a dead feature: this instance runs two
|
|
104
|
+
// building versions concurrently, so it would have returned null for every report
|
|
105
|
+
// ever filed. "Which version" is not a question worth asking when the answer is
|
|
106
|
+
// already attached to the goal that won.
|
|
107
|
+
//
|
|
108
|
+
// Catch-all goals are excluded for every one of those versions. candidateGoals
|
|
109
|
+
// drops the catch-all for ONE version id, which is the wrong shape here, so the
|
|
110
|
+
// filtering happens up front — a "<version> — general" bucket is never a home
|
|
111
|
+
// (that is R03's fallback question, a different one).
|
|
112
|
+
//
|
|
113
|
+
// Returns { id, title, version_id, score } or NULL. Null is the ordinary case and
|
|
114
|
+
// a good one: no goal means the filing lands in the inbox, exactly as every
|
|
115
|
+
// goal-unaware capture always has. Never throws — a suggester that takes down a
|
|
116
|
+
// bug report is worse than one that shrugs.
|
|
117
|
+
async function suggestGoalForCapture({ title = '', body = '' } = {}, deps = {}) {
|
|
118
|
+
const db = deps.db || require('./db.js');
|
|
119
|
+
try {
|
|
120
|
+
if (!String(title || '').trim() && !String(body || '').trim()) return null;
|
|
121
|
+
|
|
122
|
+
// Independent reads — issued together rather than in sequence.
|
|
123
|
+
const [versions, allGoals] = await Promise.all([db.listVersions(), db.listGoals({ status: 'open' })]);
|
|
124
|
+
const openIds = new Set(
|
|
125
|
+
(versions || []).filter((v) => versionAcceptsNewWork(v && v.status)).map((v) => String(v.id))
|
|
126
|
+
);
|
|
127
|
+
// Nothing is being built ⇒ nowhere for new work to go ⇒ the inbox.
|
|
128
|
+
if (openIds.size === 0) return null;
|
|
129
|
+
|
|
130
|
+
const goals = allGoals || [];
|
|
131
|
+
const homes = goals.filter((g) => g
|
|
132
|
+
&& openIds.has(String(g.version_id))
|
|
133
|
+
&& g.title !== generalGoalTitle(g.version_id));
|
|
134
|
+
if (homes.length === 0) return null;
|
|
135
|
+
|
|
136
|
+
// versionId is passed null: the catch-alls are already gone, and passing one
|
|
137
|
+
// version's id here would only re-filter for that version. includeZero stays
|
|
138
|
+
// false, so a filing that matches nothing gets NO goal rather than the
|
|
139
|
+
// first one dressed up as a suggestion.
|
|
140
|
+
const [best] = suggestGoals(homes, { title, body }, { versionId: null, limit: 1 });
|
|
141
|
+
if (!best) return null;
|
|
142
|
+
|
|
143
|
+
// suggestGoals normalises to { id, title, scope_modules, … } and drops
|
|
144
|
+
// version_id, so read it back off the row that won.
|
|
145
|
+
const row = homes.find((g) => String(g.id) === String(best.id));
|
|
146
|
+
return { id: best.id, title: best.title, version_id: row ? row.version_id : null, score: best.score };
|
|
147
|
+
} catch (err) {
|
|
148
|
+
captureErr(err);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
85
153
|
// goalSuggestHandler — GET /goals/suggestions. Handler lives here, not inline in
|
|
86
154
|
// routes/goals.js, because that file is over the fitness oversize threshold and
|
|
87
155
|
// the ratchet is shrink-only; collaborators are injected so this module still
|
|
@@ -168,4 +236,4 @@ function criteriaSuggestHandler({ db, suggestCriteria, parseId }) {
|
|
|
168
236
|
};
|
|
169
237
|
}
|
|
170
238
|
|
|
171
|
-
module.exports = { suggestGoals, goalSuggestHandler, criteriaSuggestHandler, SUGGEST_LIMIT };
|
|
239
|
+
module.exports = { suggestGoals, suggestGoalForCapture, goalSuggestHandler, criteriaSuggestHandler, SUGGEST_LIMIT };
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
const db = require('./db');
|
|
20
20
|
const classifier = require('./task-classifier');
|
|
21
|
+
const goalSuggest = require('./goal-suggest');
|
|
21
22
|
const helpRequests = require('./help-requests');
|
|
22
23
|
const taskRecommendations = require('./task-recommendations');
|
|
23
24
|
const activity = require('./activity');
|
|
@@ -35,6 +36,12 @@ module.exports = {
|
|
|
35
36
|
// (consumer: core cascade-dispatch.js, the dormant System-3 lib).
|
|
36
37
|
get VOTABLE_TASK_STATUSES() { return classifier.VOTABLE_TASK_STATUSES; },
|
|
37
38
|
classifyTaskKind: (...a) => classifier.classifyTaskKind(...a),
|
|
39
|
+
// suggestGoalForCapture: ONE goal for a filing that named none, or null (task
|
|
40
|
+
// 1003067). Consumer: the discord #bugs vector, which files through the ideas
|
|
41
|
+
// routing path and needs a home to offer it. Lives behind the port because
|
|
42
|
+
// "which goal, on which version" is a lifecycle judgement and discord may not
|
|
43
|
+
// import lifecycle (the one-way module rule).
|
|
44
|
+
suggestGoalForCapture: (...a) => goalSuggest.suggestGoalForCapture(...a),
|
|
38
45
|
|
|
39
46
|
// --- work-tracking READS the staying-core read routes resolve --------------
|
|
40
47
|
// public.js (the status surface): version/ship feed + grade trend.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.456",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.456",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.456",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
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.456'; // 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');
|
package/tests/discord_bugs.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// Unit tests for Discord #bugs → GDS tasks (kind='bug') (Phase 3 F8, ADR 0047).
|
|
4
4
|
// Pure screening/landing + orchestration via injected fakes — no Discord, no DB:
|
|
5
5
|
// - screenBug: message → { ok, title, bodyMd }; blank/short/text-free rejected.
|
|
6
|
-
// -
|
|
6
|
+
// - the landing: Metic+ → 'ready'; everyone else linked → 'backlog'. Decided by
|
|
7
|
+
// the ideas landing matrix since task 1003067, not by this module.
|
|
7
8
|
// - recordInboundBug: files via createTask under the anti-abuse guard —
|
|
8
9
|
// linked-only (unlinked rejected), rank-aware landing, per-builder
|
|
9
10
|
// rate-limit, body screen; audits surface='discord_bot'; never throws.
|
|
@@ -23,7 +24,9 @@ function t(name, fn) {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const GOOD_ID = '123456789012345678';
|
|
26
|
-
|
|
27
|
+
// task 1003067: versionId and credits are gone from this vector's config — the
|
|
28
|
+
// goal carries the version and the reward auto-assigns at the create layer.
|
|
29
|
+
const CFG = { rateLimitPerHour: 5 };
|
|
27
30
|
|
|
28
31
|
console.log('Discord #bugs → GDS tasks (Phase 3 F8):');
|
|
29
32
|
|
|
@@ -51,21 +54,16 @@ await t('screenBug: long first line is capped (≤200, ellipsis)', () => {
|
|
|
51
54
|
assert.ok(s.title.endsWith('…'));
|
|
52
55
|
});
|
|
53
56
|
|
|
54
|
-
// ---- decideBugLanding ------------------------------------------------------
|
|
55
|
-
await t('decideBugLanding: Metic+ → ready', () => {
|
|
56
|
-
assert.equal(bg.decideBugLanding('metic'), 'ready');
|
|
57
|
-
assert.equal(bg.decideBugLanding('archon'), 'ready');
|
|
58
|
-
assert.equal(bg.decideBugLanding('ARCHON'), 'ready');
|
|
59
|
-
});
|
|
60
|
-
await t('decideBugLanding: Xenos/Thetes/unknown → backlog', () => {
|
|
61
|
-
for (const r of ['xenos', 'thetes', 'periokoi', '', null, undefined]) {
|
|
62
|
-
assert.equal(bg.decideBugLanding(r), 'backlog');
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
57
|
|
|
66
58
|
// ---- recordInboundBug (fakes) ---------------------------------------------
|
|
59
|
+
// The fake capture mirrors the real landing rule rather than hardcoding a status,
|
|
60
|
+
// so a matrix change shows up here as a failure instead of a stale fixture.
|
|
61
|
+
// task 1003067: the fake defers to the REAL landing matrix, so this fixture can
|
|
62
|
+
// never quietly disagree with the rule the server actually applies.
|
|
63
|
+
const { decideLanding } = require('../modules/ideas/routing.js');
|
|
64
|
+
const READY_OR_BACKLOG = (rank) => (rank == null ? null : decideLanding({ goalId: 7, kind: 'bug', filerRank: rank }).status);
|
|
67
65
|
function makeDeps(over = {}) {
|
|
68
|
-
const calls = { create: [], audit: [], count: [], persist: [] };
|
|
66
|
+
const calls = { create: [], audit: [], count: [], persist: [], suggest: [] };
|
|
69
67
|
const deps = {
|
|
70
68
|
getBuilderByDiscordUserId: async () => (over.builder === undefined ? null : over.builder),
|
|
71
69
|
countRecentDiscordBugFilings: async (id, since) => {
|
|
@@ -73,7 +71,29 @@ function makeDeps(over = {}) {
|
|
|
73
71
|
if (over.countThrows) throw new Error('count boom');
|
|
74
72
|
return over.recent ?? 0;
|
|
75
73
|
},
|
|
76
|
-
|
|
74
|
+
// task 1003067: the vector files through ideas.capture now. `calls.create`
|
|
75
|
+
// keeps its name — it is still "what this vector asked to be created" — but
|
|
76
|
+
// the argument shape is a CAPTURE, and the landing status comes back from the
|
|
77
|
+
// matrix rather than being posted in.
|
|
78
|
+
suggestGoalForCapture: async (draft) => {
|
|
79
|
+
calls.suggest.push(draft);
|
|
80
|
+
if (over.suggestThrows) throw new Error('suggest boom');
|
|
81
|
+
return over.goal === undefined ? { id: 7, title: 'A goal', version_id: 'BONGOS-V1', score: 4 } : over.goal;
|
|
82
|
+
},
|
|
83
|
+
captureIdea: async (a) => {
|
|
84
|
+
calls.create.push(a);
|
|
85
|
+
if (over.createThrows) throw new Error('db boom');
|
|
86
|
+
// routeIdeaToGoal refuses a landing by THROWING, which rolls the idea row
|
|
87
|
+
// back with it — the case the goal-dropping retry exists for.
|
|
88
|
+
if (over.refuseRouted && a.goalId != null) throw { code: 'ROUTE_NOT_AUTHORIZED', gate: 'protected_scope' };
|
|
89
|
+
const routes = a.goalId != null && READY_OR_BACKLOG(a.filerRank) !== null;
|
|
90
|
+
return {
|
|
91
|
+
id: over.ideaId ?? 900,
|
|
92
|
+
routing: routes
|
|
93
|
+
? { routed: true, task_id: over.taskId ?? 555, goal_id: a.goalId, status: READY_OR_BACKLOG(a.filerRank) }
|
|
94
|
+
: { routed: false, reason: 'no_goal' },
|
|
95
|
+
};
|
|
96
|
+
},
|
|
77
97
|
insertAuditLog: async (a) => { calls.audit.push(a); if (over.auditThrows) throw new Error('audit boom'); return { id: 1 }; },
|
|
78
98
|
persistImages: async (urls, opts) => {
|
|
79
99
|
calls.persist.push({ urls, opts });
|
|
@@ -90,13 +110,16 @@ await t('recordInboundBug: linked Metic → filed at ready, kind=bug, attributed
|
|
|
90
110
|
const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'Boats vanish at the dock', messageId: 'm1' }, deps, CFG);
|
|
91
111
|
assert.equal(r.outcome, 'filed');
|
|
92
112
|
assert.equal(r.taskId, 555);
|
|
113
|
+
assert.equal(r.ideaId, 900, 'the idea row is the attribution the old path threw away');
|
|
93
114
|
assert.equal(r.status, 'ready');
|
|
94
|
-
assert.equal(calls.create[0].kind, 'bug');
|
|
95
|
-
assert.equal(calls.create[0].
|
|
96
|
-
assert.equal(calls.create[0].
|
|
97
|
-
assert.equal(calls.create[0].
|
|
98
|
-
|
|
99
|
-
|
|
115
|
+
assert.equal(calls.create[0].kind, 'bug', 'the channel IS the classification');
|
|
116
|
+
assert.equal(calls.create[0].filerRank, 'metic', 'the LIVE rank is handed to the matrix');
|
|
117
|
+
assert.equal(calls.create[0].capturedBy, 9, 'and the reporter is attached to the capture');
|
|
118
|
+
assert.equal(calls.create[0].goalId, 7, 'the suggester found it a home');
|
|
119
|
+
// task 1003067: no versionId is passed AT ALL. It used to be the hardcoded
|
|
120
|
+
// 'GDS-V3' — a shipped version, which is idea 1000771. The goal carries it now.
|
|
121
|
+
assert.equal(calls.create[0].versionId, undefined, 'this vector no longer names a version');
|
|
122
|
+
assert.match(calls.create[0].bodyMd, /reported from Discord #bugs by mona/);
|
|
100
123
|
assert.equal(calls.audit[0].surface, 'discord_bot');
|
|
101
124
|
assert.equal(calls.audit[0].responseStatus, 201);
|
|
102
125
|
assert.equal(calls.audit[0].builderId, 9);
|
|
@@ -106,18 +129,74 @@ await t('recordInboundBug: linked Xenos → filed at backlog (needs confirm)', a
|
|
|
106
129
|
const { deps, calls } = makeDeps({ builder: { id: 4, github_login: 'newbie', rank: 'xenos' } });
|
|
107
130
|
const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'Typo on the welcome sign', messageId: 'm2' }, deps, CFG);
|
|
108
131
|
assert.equal(r.outcome, 'filed');
|
|
109
|
-
assert.equal(r.status, 'backlog');
|
|
110
|
-
assert.equal(calls.create[0].
|
|
132
|
+
assert.equal(r.status, 'backlog', 'a sub-Metic report waits for a nod, as before');
|
|
133
|
+
assert.equal(calls.create[0].filerRank, 'xenos');
|
|
111
134
|
});
|
|
112
135
|
|
|
113
|
-
await t('recordInboundBug: UNLINKED author →
|
|
136
|
+
await t('recordInboundBug: UNLINKED author → an inbox idea, never a task [changed by 1003067]', async () => {
|
|
137
|
+
// WAS: dropped entirely ('unlinked'), because this path minted claimable work
|
|
138
|
+
// and ADR 0047 would not let an unaccountable author near that. It no longer
|
|
139
|
+
// mints work, so the report is kept — as an idea row with no author and NO
|
|
140
|
+
// GOAL, which is exactly what #ideas has always done for the same person.
|
|
141
|
+
// The security property survives structurally: no goal ⇒ the matrix cannot
|
|
142
|
+
// route ⇒ no task exists to claim.
|
|
114
143
|
const { deps, calls } = makeDeps({ builder: null });
|
|
115
144
|
const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'something is broken here', messageId: 'm3' }, deps, CFG);
|
|
116
|
-
assert.equal(r.outcome, '
|
|
117
|
-
assert.equal(r.
|
|
145
|
+
assert.equal(r.outcome, 'filed');
|
|
146
|
+
assert.equal(r.ideaId, 900, 'the report is recorded rather than silently dropped');
|
|
147
|
+
assert.equal(r.taskId, null, 'and it is NOT work');
|
|
148
|
+
assert.equal(r.status, null);
|
|
149
|
+
assert.equal(calls.create[0].capturedBy, null, 'unattributed, and honestly so');
|
|
150
|
+
assert.equal(calls.create[0].goalId, null, 'no home is offered to an unlinked author');
|
|
151
|
+
assert.equal(calls.suggest.length, 0, 'we do not even ask for one');
|
|
152
|
+
assert.match(calls.create[0].bodyMd, /unlinked author/);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await t('recordInboundBug: an unlinked author is NOT rate-limited by builder id (there is none)', async () => {
|
|
156
|
+
const { deps, calls } = makeDeps({ builder: null, recent: 999 });
|
|
157
|
+
const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'another unlinked report', messageId: 'm3b' }, deps, CFG);
|
|
158
|
+
assert.equal(r.outcome, 'filed', 'the per-builder cap cannot express an author with no builder row');
|
|
159
|
+
assert.equal(calls.count.length, 0, 'so it is not consulted');
|
|
160
|
+
assert.equal(r.taskId, null, 'the bound that matters is that none of it becomes work');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await t('recordInboundBug: a suggester failure still files — to the inbox [1003067]', async () => {
|
|
164
|
+
// A suggester that throws must not lose the bug report. No goal ⇒ inbox row.
|
|
165
|
+
const { deps, calls } = makeDeps({ builder: { id: 9, github_login: 'mona', rank: 'metic' }, suggestThrows: true });
|
|
166
|
+
const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'the suggester is down', messageId: 'm3c' }, deps, CFG);
|
|
167
|
+
assert.equal(r.outcome, 'error', 'it surfaces as an error rather than pretending to have filed');
|
|
118
168
|
assert.equal(calls.create.length, 0);
|
|
119
169
|
});
|
|
120
170
|
|
|
171
|
+
await t('recordInboundBug: a REFUSED goal drops the guess and still files the report [1003067]', async () => {
|
|
172
|
+
// The suggester picks the home; the goal can still refuse it (protected scope, a
|
|
173
|
+
// scope-wall verdict, an archive mid-flight). Because routing is one transaction
|
|
174
|
+
// that refusal takes the idea row with it — so a goal WE guessed at would lose a
|
|
175
|
+
// report the reporter did nothing wrong to file. Drop the guess, keep the report.
|
|
176
|
+
const { deps, calls } = makeDeps({ builder: { id: 9, github_login: 'mona', rank: 'metic' }, refuseRouted: true });
|
|
177
|
+
const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'a bug whose words match a protected goal', messageId: 'm7' }, deps, CFG);
|
|
178
|
+
assert.equal(r.outcome, 'filed', 'the report survives the refusal');
|
|
179
|
+
assert.equal(calls.create.length, 2, 'one routed attempt, then one homeless retry');
|
|
180
|
+
assert.equal(calls.create[0].goalId, 7, 'the first carried the suggested home');
|
|
181
|
+
assert.equal(calls.create[1].goalId, null, 'the retry drops it');
|
|
182
|
+
assert.equal(r.taskId, null, 'and it lands in the inbox, where triage will see it');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await t('recordInboundBug: a refusal with NO goal to drop is a real error, not a retry loop', async () => {
|
|
186
|
+
const { deps, calls } = makeDeps({ builder: { id: 9, github_login: 'mona', rank: 'metic' }, goal: null, refuseRouted: true, createThrows: true });
|
|
187
|
+
const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'the database is simply down', messageId: 'm8' }, deps, CFG);
|
|
188
|
+
assert.equal(r.outcome, 'error', 'nothing to drop ⇒ surface it rather than retrying forever');
|
|
189
|
+
assert.equal(calls.create.length, 1, 'exactly one attempt');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await t('recordInboundBug: NO confident goal → the report still lands, in the inbox', async () => {
|
|
193
|
+
const { deps, calls } = makeDeps({ builder: { id: 9, github_login: 'mona', rank: 'metic' }, goal: null });
|
|
194
|
+
const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'a bug matching no goal at all', messageId: 'm3d' }, deps, CFG);
|
|
195
|
+
assert.equal(r.outcome, 'filed');
|
|
196
|
+
assert.equal(r.taskId, null, 'no home ⇒ no task, which is the homeless-inbox contract (C2)');
|
|
197
|
+
assert.equal(calls.create[0].goalId, null);
|
|
198
|
+
});
|
|
199
|
+
|
|
121
200
|
await t('recordInboundBug: over rate-limit → ignored + audited 429, nothing created', async () => {
|
|
122
201
|
const { deps, calls } = makeDeps({ builder: { id: 9, github_login: 'mona', rank: 'metic' }, recent: 5 });
|
|
123
202
|
const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'yet another bug report', messageId: 'm4' }, deps, CFG);
|
|
@@ -170,7 +249,7 @@ await t('recordInboundBug: audit failure does NOT undo the filing', async () =>
|
|
|
170
249
|
assert.equal(calls.create.length, 1);
|
|
171
250
|
});
|
|
172
251
|
|
|
173
|
-
await t('recordInboundBug:
|
|
252
|
+
await t('recordInboundBug: a capture error → {error}, never throws', async () => {
|
|
174
253
|
const { deps } = makeDeps({ builder: { id: 9, github_login: 'mona', rank: 'metic' }, createThrows: true });
|
|
175
254
|
const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'valid bug body here', messageId: 'm' }, deps, CFG);
|
|
176
255
|
assert.equal(r.outcome, 'error');
|
|
@@ -202,8 +281,8 @@ await t('recordInboundBug: text + images → persists images + embeds durable li
|
|
|
202
281
|
assert.equal(r.images, 1);
|
|
203
282
|
assert.equal(calls.persist.length, 1);
|
|
204
283
|
assert.deepEqual(calls.persist[0].urls, ['https://cdn.discordapp.com/a.png']);
|
|
205
|
-
assert.match(calls.create[0].
|
|
206
|
-
assert.match(calls.create[0].
|
|
284
|
+
assert.match(calls.create[0].bodyMd, /Attachments \(1\)/);
|
|
285
|
+
assert.match(calls.create[0].bodyMd, /!\[attachment 1\]\(\/api\/gds\/bug-attachments\//);
|
|
207
286
|
});
|
|
208
287
|
|
|
209
288
|
await t('recordInboundBug: image-only (empty text) → filed with screenshot title', async () => {
|
|
@@ -225,7 +304,7 @@ await t('recordInboundBug: image persist failure → still files (without images
|
|
|
225
304
|
);
|
|
226
305
|
assert.equal(r.outcome, 'filed');
|
|
227
306
|
assert.equal(r.images, 0);
|
|
228
|
-
assert.doesNotMatch(calls.create[0].
|
|
307
|
+
assert.doesNotMatch(calls.create[0].bodyMd, /Attachments/);
|
|
229
308
|
});
|
|
230
309
|
|
|
231
310
|
await t('recordInboundBug: all images refused (stored empty) → filed, no Attachments block', async () => {
|
|
@@ -236,7 +315,7 @@ await t('recordInboundBug: all images refused (stored empty) → filed, no Attac
|
|
|
236
315
|
);
|
|
237
316
|
assert.equal(r.outcome, 'filed');
|
|
238
317
|
assert.equal(r.images, 0);
|
|
239
|
-
assert.doesNotMatch(calls.create[0].
|
|
318
|
+
assert.doesNotMatch(calls.create[0].bodyMd, /Attachments/);
|
|
240
319
|
});
|
|
241
320
|
|
|
242
321
|
// ---- combineParts ----------------------------------------------------------
|
|
@@ -29,19 +29,21 @@
|
|
|
29
29
|
// F10 make the rate-limit COUNT fail, to slip past the cap → denied (fail-closed)
|
|
30
30
|
// F11 file through Discord #bugs at all, as a sub-Metic → 'backlog', never 'ready'
|
|
31
31
|
// F12 file through Discord #ideas naming a goal → that vector cannot route
|
|
32
|
+
// F15 reach #bugs' old direct-createTask bypass → it no longer exists
|
|
33
|
+
// F16 file through #bugs as an UNLINKED author → an inbox row, never work
|
|
32
34
|
// F13 declare touches[] outside the goal to widen blast area → the real wall refuses
|
|
33
35
|
// F14 file against a SHIPPED version's goal → refused (version not building)
|
|
34
36
|
// P1 a Metic+ files a bug in an ordinary goal → 'ready' (the C1 grant)
|
|
35
37
|
// P2 a Metic+ files into protected scope → allowed, floored to metic
|
|
36
38
|
//
|
|
37
39
|
// AND THE ANTI-DRIFT PIN (the point of the file, really). The rows above are true
|
|
38
|
-
// of the
|
|
40
|
+
// of the capture vectors that exist TODAY. Nothing stops a fifth being added
|
|
39
41
|
// next month that quietly skips all of it — which is exactly how the 1000771 bug
|
|
40
42
|
// got in. So the last section ENUMERATES the vectors from source and fails when
|
|
41
43
|
// the roster changes, forcing a new vector's author to come here and add its row.
|
|
42
44
|
//
|
|
43
45
|
// PURE: the decision cores only (decideLanding, authorizeRoutedLanding,
|
|
44
|
-
//
|
|
46
|
+
// rateLimitPerHour) — no DB, no network. The transaction that
|
|
45
47
|
// obeys them is proven in tests/idea_routing.mjs; the end-to-end attribution trail
|
|
46
48
|
// in tests/idea_route_attribution.mjs.
|
|
47
49
|
//
|
|
@@ -72,7 +74,7 @@ const read = (rel) => readFileSync(path.join(ROOT, rel), 'utf8');
|
|
|
72
74
|
const routing = require('../modules/ideas/routing.js');
|
|
73
75
|
const { decideLanding, authorizeRoutedLanding, routedPriority, PROTECTED_SCOPE_FLOOR } = routing;
|
|
74
76
|
const bugs = require('../modules/discord/discord-bugs.js');
|
|
75
|
-
const {
|
|
77
|
+
const { rateLimitPerHour } = bugs;
|
|
76
78
|
const { scopeIncludesProtected, isModuleKey } = require('../src/bongos/module-scope-map.js');
|
|
77
79
|
|
|
78
80
|
const { test, summary } = makeRunner();
|
|
@@ -215,11 +217,15 @@ await test('[F10] a rate-limit COUNT failure DENIES — you cannot break the cou
|
|
|
215
217
|
});
|
|
216
218
|
|
|
217
219
|
await test('[F11] #bugs lands a sub-Metic at backlog and only Metic+ at ready', () => {
|
|
220
|
+
// Since R11 this is the SAME rule as F1/P1 — the channel has no landing logic of
|
|
221
|
+
// its own. Asserted through the matrix, which is now the only place it exists.
|
|
218
222
|
for (const rank of SUB_METIC) {
|
|
219
|
-
assert.equal(
|
|
223
|
+
assert.equal(landing({ kind: 'bug', filerRank: rank }).status, 'backlog',
|
|
224
|
+
`${JSON.stringify(rank)} must not reach ready`);
|
|
220
225
|
}
|
|
221
226
|
for (const rank of TRUSTED) {
|
|
222
|
-
assert.equal(
|
|
227
|
+
assert.equal(landing({ kind: 'bug', filerRank: rank }).status, 'ready',
|
|
228
|
+
`${rank} is the C1 straight-through grant`);
|
|
223
229
|
}
|
|
224
230
|
});
|
|
225
231
|
|
|
@@ -228,7 +234,7 @@ await test('[F9+F11] the flood, composed: at the cap, EVERY landed row is still
|
|
|
228
234
|
const landedStatuses = [];
|
|
229
235
|
for (let i = 0; i < cap + 25; i++) {
|
|
230
236
|
if (i >= cap) continue; // the counter refuses past the cap (F9/F10)
|
|
231
|
-
landedStatuses.push(
|
|
237
|
+
landedStatuses.push(landing({ kind: 'bug', filerRank: 'xenos' }).status);
|
|
232
238
|
}
|
|
233
239
|
assert.equal(landedStatuses.length, cap, 'the cap bounds how many rows exist at all');
|
|
234
240
|
assert.ok(landedStatuses.every((s) => s === 'backlog'),
|
|
@@ -297,6 +303,42 @@ await test('priority cannot be weaponised: an out-of-band suggestion is clamped,
|
|
|
297
303
|
assert.equal(routedPriority(1), 1, 'a sane suggestion still rides through');
|
|
298
304
|
});
|
|
299
305
|
|
|
306
|
+
// =========================================================================
|
|
307
|
+
// F15–F16 — the #bugs bypass is retired (task 1003067 / BV1.R11)
|
|
308
|
+
// =========================================================================
|
|
309
|
+
|
|
310
|
+
await test('[F15] #bugs mints NO task of its own — the bypass that lost attribution is gone', () => {
|
|
311
|
+
const src = read('modules/discord/discord-bugs.js');
|
|
312
|
+
const code = src.split('\n').filter((l) => !l.trim().startsWith('//')).join('\n');
|
|
313
|
+
assert.ok(!/\bdeps\.createTask\(/.test(code),
|
|
314
|
+
'the vector must file through ideas.capture, so every report leaves an idea row + lineage');
|
|
315
|
+
assert.match(code, /deps\.captureIdea\(/, 'and it does');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
await test('[F15b] #bugs names NO version — a vector that cannot name one cannot name a dead one', () => {
|
|
319
|
+
const src = read('modules/discord/discord-bugs.js');
|
|
320
|
+
const code = src.split('\n').filter((l) => !l.trim().startsWith('//')).join('\n');
|
|
321
|
+
// This is idea 1000771 in one assertion: the old default was 'GDS-V3', shipped,
|
|
322
|
+
// so every bug reported here landed in a closed version's catch-all.
|
|
323
|
+
assert.ok(!/GDS-V3|DEFAULT_BUGS_VERSION_ID|DISCORD_BUGS_VERSION_ID/.test(code),
|
|
324
|
+
'no version id may be hardcoded or env-configured on this path any more');
|
|
325
|
+
assert.ok(!/versionId:/.test(code), 'and none is passed into the capture');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
await test('[F16] an unlinked #bugs author gets an inbox row and CANNOT reach work', () => {
|
|
329
|
+
// The report is kept (it used to be dropped), but with no rank and no goal, so
|
|
330
|
+
// the matrix has nothing to route: structurally incapable of becoming a task.
|
|
331
|
+
const noRank = decideLanding({ goalId: null, kind: 'bug', filerRank: null });
|
|
332
|
+
assert.equal(noRank.route, false, 'no goal ⇒ the inbox');
|
|
333
|
+
// And even if a goal somehow reached it, an unknown filer never hard-lands:
|
|
334
|
+
const withGoal = decideLanding({ goalId: 5, kind: 'bug', filerRank: null });
|
|
335
|
+
assert.equal(withGoal.status, 'backlog', 'never claimable on an unknown filer');
|
|
336
|
+
const src = read('modules/discord/discord-bugs.js');
|
|
337
|
+
const code = src.split('\n').filter((l) => !l.trim().startsWith('//')).join('\n');
|
|
338
|
+
assert.match(code, /if \(builder\) \{[\s\S]{0,200}suggestGoalForCapture/,
|
|
339
|
+
'a goal is only ever sought for a LINKED builder');
|
|
340
|
+
});
|
|
341
|
+
|
|
300
342
|
// =========================================================================
|
|
301
343
|
// THE ANTI-DRIFT PIN — a fifth vector cannot arrive unaudited
|
|
302
344
|
// =========================================================================
|
|
@@ -308,7 +350,7 @@ const KNOWN_FILING_VECTORS = {
|
|
|
308
350
|
'modules/ideas/inbox.js': 'routeIdeaToGoal — the ONE transaction every routed filing runs (F1–F8, F13, F14)',
|
|
309
351
|
'modules/ideas/routes/inbox.js': 'POST /inbox — the web + CLI door onto it (F3)',
|
|
310
352
|
'modules/discord/discord-inbound.js': 'Discord #ideas — files an idea, never routes (F12)',
|
|
311
|
-
'modules/discord/discord-bugs.js': 'Discord #bugs —
|
|
353
|
+
'modules/discord/discord-bugs.js': 'Discord #bugs — files through ideas.capture since R11 (F9–F11, F15, F16)',
|
|
312
354
|
};
|
|
313
355
|
|
|
314
356
|
await test('[pin] every declared vector still reaches a task-minting path', () => {
|
|
@@ -336,20 +378,17 @@ await test('[pin] no OTHER module file mints a task from a filing without a row
|
|
|
336
378
|
`a new filing vector must be added to KNOWN_FILING_VECTORS and given a matrix row: ${unaudited.join(', ')}`);
|
|
337
379
|
});
|
|
338
380
|
|
|
339
|
-
await test('[pin]
|
|
340
|
-
//
|
|
341
|
-
//
|
|
381
|
+
await test('[pin] there is ONE trusted-tier definition, and #bugs no longer keeps a copy', () => {
|
|
382
|
+
// This pin used to assert that two spellings of "Metic+" AGREED — routing.js and
|
|
383
|
+
// discord-bugs.js each carried one. R11 deleted the second, which is the better
|
|
384
|
+
// outcome: agreement you have to test for is a duplication you could have removed.
|
|
342
385
|
const routingSrc = read('modules/ideas/routing.js');
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const viaBugs = decideBugLanding(rank);
|
|
350
|
-
assert.equal(viaRouting, viaBugs,
|
|
351
|
-
`the two vectors must agree on ${JSON.stringify(rank)} (routing=${viaRouting}, bugs=${viaBugs})`);
|
|
352
|
-
}
|
|
386
|
+
assert.match(routingSrc, /METIC_PLUS = new Set\(\['metic', 'archon'\]\)/,
|
|
387
|
+
'the one definition, in the matrix');
|
|
388
|
+
const bugsCode = read('modules/discord/discord-bugs.js')
|
|
389
|
+
.split('\n').filter((l) => !l.trim().startsWith('//')).join('\n');
|
|
390
|
+
assert.ok(!/READY_RANKS/.test(bugsCode), '#bugs must not reintroduce a local rank set');
|
|
391
|
+
assert.ok(!/decideBugLanding/.test(bugsCode), 'nor a local landing decision');
|
|
353
392
|
});
|
|
354
393
|
|
|
355
394
|
summary();
|
package/tests/goal_subtitle.mjs
CHANGED
|
@@ -184,20 +184,26 @@ test('an unset subtitle is hidden; a set one is shown trimmed', () => {
|
|
|
184
184
|
assert.equal(els.sub.textContent, 'the grep path');
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
-
test('on a LIST the subtitle is a
|
|
187
|
+
test('on a LIST the subtitle is a visible line; on the CARD it stays a tooltip', () => {
|
|
188
188
|
// task 1003206: the row/card painters moved to goals-render.js — the second half
|
|
189
189
|
// of the carve goals-lib.js began — so read the family, not one filename.
|
|
190
190
|
const js = goalsPageSource();
|
|
191
|
-
//
|
|
191
|
+
// Both helpers live in goals-lib.js — the file that exists because
|
|
192
192
|
// goals-page.js was over the excess-lines ratchet (task 1003135).
|
|
193
193
|
const lib = read('modules', 'hall-ui', 'public', 'goals-lib.js');
|
|
194
194
|
assert.match(lib, /function subtitleAttr/);
|
|
195
|
-
assert.match(lib, /
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
195
|
+
assert.match(lib, /function subtitleLine/);
|
|
196
|
+
assert.match(lib, /escapeHtml\(t\)/, 'escaped, since it lands in an attribute or a text node');
|
|
197
|
+
// task 1003307 gave the ROW its line back on the owner's call: 35 of 72 goals
|
|
198
|
+
// carry a subtitle, and a tooltip does not exist on a touch screen. The CARD
|
|
199
|
+
// has no room for a line, so it keeps the tooltip (task 1003224's shape).
|
|
200
|
+
const row = js.indexOf('class="gr-title"');
|
|
201
|
+
assert.ok(row > 0, 'the row title is still rendered');
|
|
202
|
+
assert.match(js.slice(row, row + 200), /subtitleLine\(g\)/, 'the row renders the visible line');
|
|
203
|
+
assert.doesNotMatch(js.slice(row, row + 160), /subtitleAttr\(g\)/, 'the row does not also carry the tooltip');
|
|
204
|
+
const card = js.indexOf('class="goal-card__title gr-title"');
|
|
205
|
+
assert.ok(card > 0, 'the card title is still rendered');
|
|
206
|
+
assert.match(js.slice(card, card + 160), /subtitleAttr\(g\)/, 'the card carries the tooltip');
|
|
201
207
|
});
|
|
202
208
|
|
|
203
209
|
test('every CSS token the new styles use actually exists', () => {
|