@bongos/core 1.19.1014 → 1.19.1016
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 +145 -140
- package/.claude/skills/planning-session/SKILL.md +1 -1
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md +4 -1
- package/docs/adr/0331-the-sky-is-the-ideators-home.md +18 -1
- package/docs/adr/0335-an-invite-is-a-pre-approved-row-on-the-projects-own-instance.md +113 -0
- package/docs/adr/0336-a-guilds-record-is-the-sum-of-what-its-members-already-show.md +201 -0
- package/docs/adr/README.md +2 -0
- package/docs/api/openapi.json +4 -0
- package/docs/api-reference.md +1 -1
- package/docs/architecture.md +4 -1
- package/docs/copy-inventory.md +139 -110
- package/docs/copy-registry.json +872 -296
- package/docs/file-map.md +0 -1
- package/docs/module-api-changelog.md +4 -0
- package/migrations/core_253_idea_filed_via.sql +59 -0
- package/modules/agents/pollers/event-router.js +2 -0
- package/modules/discord/discord-bugs.js +2 -0
- package/modules/discord/discord-inbound.js +3 -0
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +125 -52
- package/modules/hall-ui/public/goal-map.states.json +29 -5
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/people.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +11 -0
- package/modules/hall-ui/public/sky-data.js +41 -9
- package/modules/hall-ui/public/sky-draw.js +3 -2
- package/modules/hall-ui/public/sky-interaction.js +14 -7
- package/modules/hall-ui/public/sky-layout.js +2 -1
- package/modules/hall-ui/public/sky-panel.js +1 -1
- package/modules/hall-ui/public/sky-tour.js +10 -6
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.css +6 -3
- package/modules/hall-ui/public/thinking.html +16 -10
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/ideas/born-of.js +55 -0
- package/modules/ideas/inbox.js +23 -8
- package/modules/ideas/projection.js +48 -0
- package/modules/ideas/routes/inbox.js +20 -0
- package/modules/lifecycle/sky.js +25 -4
- package/modules/platform-identity/activity-snapshots.js +47 -0
- package/modules/platform-identity/migrations/platform_identity_022_activity_snapshots.sql +46 -0
- package/modules/platform-identity/platform-identity.js +13 -0
- package/modules/platform-identity/tests/activity-snapshots.mjs +68 -0
- package/modules/platform-identity/tests/platform-identity.mjs +21 -0
- package/modules/ui-design/skills/impeccable/SKILL.md +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/capture.js +11 -0
- package/scripts/gds/run-unit-tests.js +4 -0
- package/src/bongos/serve-internal.js +4 -0
- package/src/module-api.js +1 -1
- package/tests/activity_snapshots_db.mjs +97 -0
- package/tests/discord_bugs.mjs +1 -0
- package/tests/discord_inbound.mjs +1 -0
- package/tests/goal_map_page.mjs +360 -211
- package/tests/guild_namespace_reserved.mjs +76 -0
- package/tests/hall_audit.mjs +1 -0
- package/tests/hall_nav.mjs +6 -1
- package/tests/hall_page_gate_map.mjs +3 -2
- package/tests/hall_sky.mjs +33 -16
- package/tests/helpers.mjs +21 -5
- package/tests/idea_filed_via.mjs +185 -0
- package/tests/landing_page.mjs +8 -0
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/profile_rollup_consent.mjs +5 -1
- package/tests/project_admin_console_model.mjs +105 -0
- package/tests/render_check.mjs +6 -4
- package/tests/sky_read.mjs +19 -2
- package/tests/ui_design_skills.mjs +9 -0
- package/modules/hall-ui/public/goal-map-lib.js +0 -349
- package/modules/hall-ui/public/goal-map.css +0 -117
- package/modules/hall-ui/public/goal-map.js +0 -232
- package/modules/public-landing/public/world/assets/clouds/cloud-island-large.png +0 -0
- package/modules/public-landing/public/world/assets/clouds/cloud-puff.png +0 -0
- package/modules/public-landing/public/world/assets/creatures/creature-whale-cargo.png +0 -0
- package/modules/public-landing/public/world/assets/creatures/creature-whale-glide.png +0 -0
- package/modules/public-landing/public/world/assets/plates/plate-clouds-near.png +0 -0
- package/modules/public-landing/public/world/assets/plates/scene-establishing.png +0 -0
- package/tests/goal_map_lib.mjs +0 -214
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
const express = require('express');
|
|
36
36
|
const api = require('../../../src/module-api');
|
|
37
37
|
const inbox = require('../inbox');
|
|
38
|
+
const bornOf = require('../born-of');
|
|
38
39
|
// The trail read to the FULL depth of the descent (task 1004089, BV2.R09). Its
|
|
39
40
|
// own file because inbox.js is AT the size ratchet and because a task->task
|
|
40
41
|
// walk is a different relation from the idea->task trail.
|
|
@@ -95,6 +96,12 @@ function registerSeams() {
|
|
|
95
96
|
if (!api.hasProvider('ideas.capture')) {
|
|
96
97
|
api.registerProvider('ideas.capture', inbox.captureIdea);
|
|
97
98
|
}
|
|
99
|
+
// task 1004233 (sky part 5): which thoughts became goals or rings, for the
|
|
100
|
+
// lifecycle module's GET /sky to fold in as `born` — through the port, so
|
|
101
|
+
// lifecycle never reads an ideas_ table itself (./born-of.js).
|
|
102
|
+
if (!api.hasProvider('ideas.bornOf')) {
|
|
103
|
+
api.registerProvider('ideas.bornOf', bornOf.readBornOf);
|
|
104
|
+
}
|
|
98
105
|
if (!api.hasProvider('ideas.descent')) {
|
|
99
106
|
api.registerProvider('ideas.descent', {
|
|
100
107
|
ancestorsOfTask: descent.ancestorsOfTask,
|
|
@@ -176,8 +183,19 @@ module.exports = function buildInboxRouter() {
|
|
|
176
183
|
// turned out: 'goal' or 'criterion'. OPTIONAL, permanently (see the
|
|
177
184
|
// refusal below and ideas_009's header).
|
|
178
185
|
recommendation: { type: 'string', maxLength: 16 },
|
|
186
|
+
// task 1004233 (sky part 5): WHICH DOOR. Read off the session — never a
|
|
187
|
+
// claim — except 'planning' (capture.js --via planning); see below.
|
|
188
|
+
filed_via: { type: 'string', maxLength: 16 },
|
|
179
189
|
})) return;
|
|
180
190
|
const body = req.body || {};
|
|
191
|
+
// task 1004233 (sky part 5): the door is a FACT read off the session that
|
|
192
|
+
// made the request (projection.js resolveFiledVia: a web cookie is the hall,
|
|
193
|
+
// a device-flow token is the CLI, anything else the API). The body may add
|
|
194
|
+
// only 'planning' — the one door no session can tell from a CLI capture —
|
|
195
|
+
// and any other claim is refused, so a filer cannot say they came through a
|
|
196
|
+
// door they did not.
|
|
197
|
+
const door = inbox.resolveFiledVia(req.gdsSession && req.gdsSession.source, body.filed_via);
|
|
198
|
+
if (!door.ok) return res.fail('bad_filed_via', 400, door.detail);
|
|
181
199
|
if (body.kind === 'criterion-proposal') {
|
|
182
200
|
// FAIL CLOSED (task 1003197 (D)). This was inline and read
|
|
183
201
|
// `if (rank && rank !== 'metic' && …)`, so an ABSENT rank ADMITTED. The
|
|
@@ -253,6 +271,7 @@ module.exports = function buildInboxRouter() {
|
|
|
253
271
|
goalId: body.goal_id ?? null,
|
|
254
272
|
filerRank: req.builder?.rank ?? null,
|
|
255
273
|
grade,
|
|
274
|
+
filedVia: door.value,
|
|
256
275
|
// task 1003691 (ADR 0262): the vector, hardcoded — NEVER read from the
|
|
257
276
|
// body. This route is the authenticated API door, so a bug filed here
|
|
258
277
|
// with no goal routes to the version's maintenance goal instead of
|
|
@@ -367,6 +386,7 @@ module.exports = function buildInboxRouter() {
|
|
|
367
386
|
err.code === 'TITLE_REQUIRED' ? 400 :
|
|
368
387
|
err.code === 'BAD_KIND' ? 400 :
|
|
369
388
|
err.code === 'BAD_PRIORITY' ? 400 :
|
|
389
|
+
err.code === 'BAD_FILED_VIA' ? 400 :
|
|
370
390
|
// task 1003065 (BV1.R01) — the routing refusals. A named goal that is
|
|
371
391
|
// not a live home is the FILER'S mistake to fix (pick another goal, or
|
|
372
392
|
// omit it and file to the inbox), so these are 4xx with the offending
|
package/modules/lifecycle/sky.js
CHANGED
|
@@ -137,12 +137,17 @@ function foldCriteria(rows = []) {
|
|
|
137
137
|
// satisfied, pending_review, task_ids }], tasks: [{ id, title, status,
|
|
138
138
|
// credits_reward, created_on, idea_id }] }]
|
|
139
139
|
// edges: [{ from, to, count }] — `from` waits on `to`, the graph's own
|
|
140
|
+
// born: { goals: { goal_id: idea_id }, criteria: { criterion_id: idea_id } }
|
|
141
|
+
// — which thoughts became the structure (task
|
|
142
|
+
// 1004233): the ideas module's own read through the
|
|
143
|
+
// `ideas.bornOf` port, ids as strings; an absent
|
|
144
|
+
// port is an EMPTY map, never a guess
|
|
140
145
|
// counts: { goals, edges, criteria, tasks }
|
|
141
146
|
//
|
|
142
147
|
// A criterion or a task whose goal is not among the wells is DROPPED — the wells
|
|
143
148
|
// are the graph's nodes (goals carrying at least one task, in the version asked
|
|
144
149
|
// for), and a ring drawn around no well is a line pointing at nothing.
|
|
145
|
-
function shapeSky({ goals = [], edges = [], extras = [], criteria = [], tasks = [] } = {}, categoryById = new Map()) {
|
|
150
|
+
function shapeSky({ goals = [], edges = [], extras = [], criteria = [], tasks = [], born = null } = {}, categoryById = new Map()) {
|
|
146
151
|
const graph = goalGraph.shapeGraph({ goals, edges }, categoryById);
|
|
147
152
|
const visibleById = new Map(goals.map((g) => [String(g.goal_id), g.visible !== false]));
|
|
148
153
|
const extraById = new Map(extras.map((x) => [String(x.goal_id), x]));
|
|
@@ -189,18 +194,34 @@ function shapeSky({ goals = [], edges = [], extras = [], criteria = [], tasks =
|
|
|
189
194
|
return {
|
|
190
195
|
goals: out,
|
|
191
196
|
edges: graph.edges,
|
|
197
|
+
born: normalizeBorn(born),
|
|
192
198
|
counts: { goals: out.length, edges: graph.edges.length, criteria: criteriaCount, tasks: taskCount },
|
|
193
199
|
};
|
|
194
200
|
}
|
|
195
201
|
|
|
202
|
+
// normalizeBorn — the port's answer as string-keyed, string-valued maps; a null,
|
|
203
|
+
// a throw or a half-shaped answer is the empty map (the page draws no mark).
|
|
204
|
+
function normalizeBorn(born) {
|
|
205
|
+
const strMap = (o) => {
|
|
206
|
+
const out = {};
|
|
207
|
+
if (o && typeof o === 'object') for (const [k, v] of Object.entries(o)) if (v != null) out[String(k)] = String(v);
|
|
208
|
+
return out;
|
|
209
|
+
};
|
|
210
|
+
return { goals: strMap(born && born.goals), criteria: strMap(born && born.criteria) };
|
|
211
|
+
}
|
|
212
|
+
|
|
196
213
|
// sky — read + shape. `categoryIndex` is injectable so the whole path runs in a
|
|
197
214
|
// test with a fake pool and no category table (the goal-graph.js precedent).
|
|
198
215
|
async function sky(opts = {}, deps = {}) {
|
|
199
|
-
|
|
216
|
+
// task 1004233: born of a thought is the IDEAS module's read, reached through
|
|
217
|
+
// its port — one of the two reads is enough to fail quietly into an empty map.
|
|
218
|
+
const readBorn = 'readBornOf' in deps ? deps.readBornOf : (api.resolveOptional ? api.resolveOptional('ideas.bornOf') : null);
|
|
219
|
+
const [rows, byId, born] = await Promise.all([
|
|
200
220
|
skyRows(opts, deps),
|
|
201
221
|
(deps.categoryIndex || goalCategory.categoryIndex)(deps),
|
|
222
|
+
typeof readBorn === 'function' ? readBorn().catch(() => null) : null,
|
|
202
223
|
]);
|
|
203
|
-
return shapeSky(rows, byId);
|
|
224
|
+
return shapeSky({ ...rows, born }, byId);
|
|
204
225
|
}
|
|
205
226
|
|
|
206
227
|
// skyCache — the 60-second memo, keyed by what decides what the viewer may see.
|
|
@@ -258,4 +279,4 @@ function skyHandler({ read = sky, log = console, cache = skyCache() } = {}) {
|
|
|
258
279
|
};
|
|
259
280
|
}
|
|
260
281
|
|
|
261
|
-
module.exports = { sky, skyRows, shapeSky, foldCriteria, skyCache, skyHandler, SKY_CACHE_TTL_MS };
|
|
282
|
+
module.exports = { sky, skyRows, shapeSky, foldCriteria, normalizeBorn, skyCache, skyHandler, SKY_CACHE_TTL_MS };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// modules/platform-identity/activity-snapshots.js — the windowed data plane's
|
|
2
|
+
// append-only history (community spec D2, task 1002977): one row per rollup
|
|
3
|
+
// report WHOSE TOTALS CHANGED, so a leaderboard window (7d/30d/...) can compute
|
|
4
|
+
// "latest minus the last snapshot at-or-before the window start" without a core
|
|
5
|
+
// change (option B) or an unbounded event feed (option C) — both rejected for V1.
|
|
6
|
+
//
|
|
7
|
+
// Reporting-time semantics, not earning-time: a builder who signs in fortnightly
|
|
8
|
+
// lands two weeks of credits in the sign-in week. Honest, accepted V1 limitation
|
|
9
|
+
// (see the spec); self-correcting on the builder's next report.
|
|
10
|
+
//
|
|
11
|
+
// Compaction (daily granularity after 90 days) is a later, volume-triggered task
|
|
12
|
+
// — nothing here prunes. A disconnected/re-registered project's snapshots are
|
|
13
|
+
// likewise not purged here — platform_identity_builder_project_activity (the
|
|
14
|
+
// table this mirrors) has the identical gap today; the future leaderboard reader
|
|
15
|
+
// is where consent (rollupVisibleSql) is enforced, per that table's own precedent.
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { pool: defaultPool } = require('../../src/module-api');
|
|
19
|
+
|
|
20
|
+
// Append one snapshot row iff the totals differ from the last one on record for
|
|
21
|
+
// this (client, builder) pair — ONE statement, not a read then a write: the CTE
|
|
22
|
+
// and the conditional INSERT run in the same round trip, closing the two-round-
|
|
23
|
+
// trip check-then-insert race the earlier design had. A DB-level race remains
|
|
24
|
+
// under READ COMMITTED — two truly concurrent statements for the same pair can
|
|
25
|
+
// each see no conflicting row and both insert — but the worst case is a harmless
|
|
26
|
+
// duplicate snapshot with IDENTICAL totals, which reads the same either way in
|
|
27
|
+
// the windowed-delta arithmetic. A first-ever report (no prior row) always
|
|
28
|
+
// inserts — NOT EXISTS is true against an empty CTE regardless of the
|
|
29
|
+
// comparison. `id DESC` breaks a tie on an identical `reported_at` so "the last
|
|
30
|
+
// snapshot" is always one specific row.
|
|
31
|
+
async function recordActivitySnapshot({ clientId, githubId, credits = 0, tasksShipped = 0, karma = 0 }, { pool = defaultPool } = {}) {
|
|
32
|
+
const { rows } = await pool.query(
|
|
33
|
+
`WITH last AS (
|
|
34
|
+
SELECT credits, tasks_shipped, karma FROM platform_identity_activity_snapshots
|
|
35
|
+
WHERE client_id = $1 AND github_id = $2
|
|
36
|
+
ORDER BY reported_at DESC, id DESC LIMIT 1
|
|
37
|
+
)
|
|
38
|
+
INSERT INTO platform_identity_activity_snapshots (client_id, github_id, credits, tasks_shipped, karma, reported_at)
|
|
39
|
+
SELECT $1, $2, $3, $4, $5, now()
|
|
40
|
+
WHERE NOT EXISTS (SELECT 1 FROM last WHERE credits = $3 AND tasks_shipped = $4 AND karma = $5)
|
|
41
|
+
RETURNING id, reported_at`,
|
|
42
|
+
[clientId, githubId, credits, tasksShipped, karma],
|
|
43
|
+
);
|
|
44
|
+
return rows[0] || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { recordActivitySnapshot };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- platform_identity_022_activity_snapshots.sql — the windowed data plane's ONE
|
|
2
|
+
-- new table (community spec D2, task 1002977): an append-only history of each
|
|
3
|
+
-- (project, builder)'s reported totals, one row per rollup report WHOSE TOTALS
|
|
4
|
+
-- CHANGED (a no-op re-report inserts nothing — upsertActivity skips it via
|
|
5
|
+
-- activity-snapshots.js:recordActivitySnapshot).
|
|
6
|
+
--
|
|
7
|
+
-- A window's value = latest totals (platform_identity_builder_project_activity)
|
|
8
|
+
-- minus the last snapshot at-or-before the window start, summed across projects;
|
|
9
|
+
-- missing baseline counts as zero (D2 option A, chosen over B/C — see the spec).
|
|
10
|
+
-- Reporting-time semantics, not earning-time: the honest, accepted V1 limitation.
|
|
11
|
+
--
|
|
12
|
+
-- Retention: nothing pruned here. A compaction pass (daily granularity after 90
|
|
13
|
+
-- days) is a later, volume-triggered task, not built now.
|
|
14
|
+
--
|
|
15
|
+
-- Additive + platform_identity_-namespaced (ADR 0083 §5). Idempotent.
|
|
16
|
+
|
|
17
|
+
BEGIN;
|
|
18
|
+
|
|
19
|
+
CREATE TABLE IF NOT EXISTS platform_identity_activity_snapshots (
|
|
20
|
+
id bigserial PRIMARY KEY,
|
|
21
|
+
|
|
22
|
+
-- The project (the sso_client that reported) + the builder (stable github id) —
|
|
23
|
+
-- the same pair platform_identity_builder_project_activity is keyed on.
|
|
24
|
+
client_id text NOT NULL,
|
|
25
|
+
github_id bigint NOT NULL,
|
|
26
|
+
|
|
27
|
+
-- The reported totals AT this point in time (a snapshot, not a delta).
|
|
28
|
+
credits integer NOT NULL DEFAULT 0,
|
|
29
|
+
tasks_shipped integer NOT NULL DEFAULT 0,
|
|
30
|
+
karma integer NOT NULL DEFAULT 0,
|
|
31
|
+
|
|
32
|
+
reported_at timestamptz NOT NULL DEFAULT now()
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
-- The windowed read's whole access path: "the last snapshot at-or-before a given
|
|
36
|
+
-- timestamp, for this (project, builder)". DESC so the common query (the most
|
|
37
|
+
-- recent snapshot <= window start) hits the front of the index; `id DESC`
|
|
38
|
+
-- matches activity-snapshots.js's own tiebreak, so its top-1 probe is a pure
|
|
39
|
+
-- index-order LIMIT 1 rather than relying on an incremental sort.
|
|
40
|
+
CREATE INDEX IF NOT EXISTS platform_identity_activity_snapshots_builder_time
|
|
41
|
+
ON platform_identity_activity_snapshots (client_id, github_id, reported_at DESC, id DESC);
|
|
42
|
+
|
|
43
|
+
COMMENT ON TABLE platform_identity_activity_snapshots IS
|
|
44
|
+
'Append-only history of reported totals per (client_id, github_id), one row per rollup report whose totals changed (community spec D2, task 1002977). A leaderboard window computes latest minus the last snapshot at-or-before the window start; missing baseline counts as zero. Reporting-time semantics, not earning-time. Nothing pruned in V1 — a later, volume-triggered compaction task handles retention.';
|
|
45
|
+
|
|
46
|
+
COMMIT;
|
|
@@ -24,6 +24,9 @@ const terms = require('./terms-acceptance');
|
|
|
24
24
|
// reads the SAME rule as a ship link instead of the "different host" check it had —
|
|
25
25
|
// see origin-containment.js for why it is downward only and why it lives there.
|
|
26
26
|
const { urlWithinOrigin } = require('./origin-containment');
|
|
27
|
+
// The windowed data plane's append-only snapshot history (task 1002977, community
|
|
28
|
+
// spec D2) — one call from upsertActivity below; the mechanism itself lives there.
|
|
29
|
+
const { recordActivitySnapshot } = require('./activity-snapshots');
|
|
27
30
|
|
|
28
31
|
const { pool: defaultPool } = api;
|
|
29
32
|
const { ACCOUNT_VISIBILITY, accountVisibleSql, rollupVisibleSql, isRollupVisible, scoutingListableSql } = visibility;
|
|
@@ -677,6 +680,16 @@ async function upsertActivity({ clientId, githubId, credits = 0, tasksShipped =
|
|
|
677
680
|
// Postgres ARRAY literal, which the jsonb column would reject.
|
|
678
681
|
[clientId, githubId, credits | 0, tasksShipped | 0, karma | 0, safeRank, disc, JSON.stringify(ships)],
|
|
679
682
|
);
|
|
683
|
+
// Windowed data plane (task 1002977): one snapshot per report whose totals
|
|
684
|
+
// changed, skipped on a no-op re-report — see activity-snapshots.js.
|
|
685
|
+
// Best-effort, the application-echo.js precedent: descriptive analytics, never
|
|
686
|
+
// authority, so a fault here (e.g. migration 022 not yet applied on this
|
|
687
|
+
// instance) must never fail the rollup report that already committed above.
|
|
688
|
+
try {
|
|
689
|
+
await recordActivitySnapshot({ clientId, githubId, credits: credits | 0, tasksShipped: tasksShipped | 0, karma: karma | 0 }, { pool });
|
|
690
|
+
} catch (err) {
|
|
691
|
+
api.logger('platform-identity').error(`activity snapshot write failed (non-fatal): ${err && err.message}`);
|
|
692
|
+
}
|
|
680
693
|
return rows[0];
|
|
681
694
|
}
|
|
682
695
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// modules/platform-identity/tests/activity-snapshots.mjs
|
|
2
|
+
//
|
|
3
|
+
// DB-free unit suite for the windowed data plane's append-only snapshot history
|
|
4
|
+
// (community spec D2, task 1002977). Recording-fake-pool style, matching
|
|
5
|
+
// tests/platform-identity.mjs: proves the SQL SHAPE (one statement, correct
|
|
6
|
+
// table/columns, bound params, the tiebreak) — a fake pool cannot execute the
|
|
7
|
+
// `WHERE NOT EXISTS` itself, so the skip-on-unchanged BEHAVIOUR is proved
|
|
8
|
+
// against real Postgres in tests/activity_snapshots_db.mjs instead.
|
|
9
|
+
//
|
|
10
|
+
// Run standalone: node modules/platform-identity/tests/activity-snapshots.mjs
|
|
11
|
+
|
|
12
|
+
import { strict as assert } from 'node:assert';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const { recordActivitySnapshot } = require('../activity-snapshots.js');
|
|
17
|
+
|
|
18
|
+
let passed = 0, failed = 0;
|
|
19
|
+
async function ta(name, fn) {
|
|
20
|
+
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
21
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// A recording fake pool: returns queued responses in order and records every call.
|
|
25
|
+
function mockPool(responses = []) {
|
|
26
|
+
const calls = [];
|
|
27
|
+
let i = 0;
|
|
28
|
+
return {
|
|
29
|
+
calls,
|
|
30
|
+
query: async (text, params) => {
|
|
31
|
+
calls.push({ text: String(text), params: params || [] });
|
|
32
|
+
const r = responses[i++];
|
|
33
|
+
return r || { rows: [], rowCount: 0 };
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log('recordActivitySnapshot (community spec D2, task 1002977):');
|
|
39
|
+
|
|
40
|
+
await ta('one statement: a CTE for the last snapshot, guarding a conditional INSERT', async () => {
|
|
41
|
+
const pool = mockPool([{ rows: [{ id: '1', reported_at: 'T1' }] }]);
|
|
42
|
+
const out = await recordActivitySnapshot({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
|
|
43
|
+
assert.equal(pool.calls.length, 1, 'one round trip, not a read then a write');
|
|
44
|
+
const c = pool.calls[0];
|
|
45
|
+
assert.match(c.text, /WITH last AS/);
|
|
46
|
+
assert.match(c.text, /FROM platform_identity_activity_snapshots/);
|
|
47
|
+
assert.match(c.text, /ORDER BY reported_at DESC, id DESC LIMIT 1/, 'a deterministic tiebreak on an identical reported_at');
|
|
48
|
+
assert.match(c.text, /INSERT INTO platform_identity_activity_snapshots/);
|
|
49
|
+
assert.match(c.text, /WHERE NOT EXISTS \(SELECT 1 FROM last WHERE credits = \$3 AND tasks_shipped = \$4 AND karma = \$5\)/);
|
|
50
|
+
assert.deepEqual(c.params, ['c1', 42, 100, 5, 3]);
|
|
51
|
+
assert.deepEqual(out, { id: '1', reported_at: 'T1' });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await ta('a skipped report (the fake pool answers no rows, as a real WHERE NOT EXISTS would on a match) returns null', async () => {
|
|
55
|
+
const pool = mockPool([{ rows: [] }]);
|
|
56
|
+
const out = await recordActivitySnapshot({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
|
|
57
|
+
assert.equal(pool.calls.length, 1);
|
|
58
|
+
assert.equal(out, null);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await ta('missing totals default to zero, like upsertActivity\'s own defaults', async () => {
|
|
62
|
+
const pool = mockPool([{ rows: [{ id: '3', reported_at: 'T3' }] }]);
|
|
63
|
+
await recordActivitySnapshot({ clientId: 'c1', githubId: 42 }, { pool });
|
|
64
|
+
assert.deepEqual(pool.calls[0].params, ['c1', 42, 0, 0, 0]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
68
|
+
if (failed > 0) process.exit(1);
|
|
@@ -596,6 +596,27 @@ await ta('upsertActivity keeps a legitimate core rank key', async () => {
|
|
|
596
596
|
assert.equal(pool.calls[0].params[5], 'archon');
|
|
597
597
|
});
|
|
598
598
|
|
|
599
|
+
await ta('upsertActivity also appends to the snapshot history (task 1002977, community spec D2) — deleting that call would leave this red', async () => {
|
|
600
|
+
const pool = mockPool([{ rows: [{ id: '1' }] }, { rows: [{ id: 's1', reported_at: 'T' }] }]);
|
|
601
|
+
await P.upsertActivity({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
|
|
602
|
+
assert.equal(pool.calls.length, 2, 'the rollup upsert, then the snapshot statement');
|
|
603
|
+
assert.match(pool.calls[1].text, /INSERT INTO platform_identity_activity_snapshots/);
|
|
604
|
+
assert.deepEqual(pool.calls[1].params, ['c1', 42, 100, 5, 3]);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
await ta('upsertActivity survives a snapshot-write failure (best-effort — the application-echo.js precedent): the rollup report still succeeds', async () => {
|
|
608
|
+
const pool = {
|
|
609
|
+
calls: [],
|
|
610
|
+
query: async (text, params) => {
|
|
611
|
+
pool.calls.push({ text: String(text), params });
|
|
612
|
+
if (/INSERT INTO platform_identity_activity_snapshots/.test(text)) throw new Error('relation "platform_identity_activity_snapshots" does not exist');
|
|
613
|
+
return { rows: [{ id: '1' }] };
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
const out = await P.upsertActivity({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
|
|
617
|
+
assert.equal(out.id, '1', 'the rollup upsert result still returns despite the snapshot write throwing');
|
|
618
|
+
});
|
|
619
|
+
|
|
599
620
|
await ta('getMembership reads one (client, builder) row — the rollup membership requirement', async () => {
|
|
600
621
|
const row = { id: '9', client_id: 'c1', github_id: '42', membership_kind: 'member', signin_verified_at: 'T' };
|
|
601
622
|
const pool = mockPool([{ rows: [row] }]);
|
|
@@ -42,7 +42,7 @@ What changed in translation, on purpose. The method's **three persisted homes**
|
|
|
42
42
|
Read four live signals, then print two or three recommended next commands with a one-line reason each, followed by the full list grouped by category. Signals, all read locally and none written:
|
|
43
43
|
|
|
44
44
|
1. **The world files**: does `DESIGN.md` exist at the repo root; is `config/branding.json` present over the neutral pack; is `docs/project-context.template.md` filled or still the scaffold. A scaffold with no answers recommends `shape` first; a missing `DESIGN.md` alone does not make the project greenfield (the neutral pack is the world).
|
|
45
|
-
2. **The last kit report** for the target, if any: `report.json` under
|
|
45
|
+
2. **The last kit report** for the target, if any: `report.json` under the `--out` dir the last `render.js` run named — pass one explicitly on a shared machine, since the un-named default falls back to a world-readable OS temp dir (`os.tmpdir()`'s `ui-design-kit/<surface>/<page>/`: `%TMP%` on Windows, `$TMPDIR` on mac/linux). This is only ever read to shape the printed suggestion below, never trusted as authority. Problems listed recommend `audit` on that page; a clean report with no critique yet recommends `critique`; no report at all recommends `audit`, because the render is the read.
|
|
46
46
|
3. **Dirty files in version control**: `git status --porcelain` over the surfaces (`modules/*/public/`, `src/ui/`, `public*/`). Dirty UI files recommend `polish` on that path before anything new.
|
|
47
47
|
4. **A stub already listening**: `kit/serve.js` prints its port when it starts; a page already served means the live loop (the playwright plugin) is available without a second server. Never start a duplicate on an occupied port.
|
|
48
48
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.1016",
|
|
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.1016",
|
|
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.1016",
|
|
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/scripts/gds/capture.js
CHANGED
|
@@ -269,6 +269,7 @@ async function main() {
|
|
|
269
269
|
console.error(' ... [--spark] offer it up for someone else to develop');
|
|
270
270
|
console.error(' ... [--full --fields-file f.json] file a Full Idea (five fields; see spark.js --template)');
|
|
271
271
|
console.error(' ... [--no-prompt] never ask anything (default when not a terminal)');
|
|
272
|
+
console.error(' ... [--via planning] a planning session\'s capture — the sky records the door (task 1004233)');
|
|
272
273
|
process.exit(2);
|
|
273
274
|
}
|
|
274
275
|
|
|
@@ -281,6 +282,15 @@ async function main() {
|
|
|
281
282
|
const fieldsFile = arg('--fields-file', args);
|
|
282
283
|
const noPrompt = hasFlag('--no-prompt', args);
|
|
283
284
|
const goalArg = arg('--goal', args);
|
|
285
|
+
// task 1004233 (sky part 5): WHICH DOOR. The server reads 'cli' off this
|
|
286
|
+
// session's token, so this CLI names nothing — except a planning session's
|
|
287
|
+
// capture, which no session can tell from any other CLI filing. Only that one
|
|
288
|
+
// value is accepted here; the server refuses every other claim the same way.
|
|
289
|
+
const viaFromArg = arg('--via', args);
|
|
290
|
+
if (viaFromArg !== null && viaFromArg !== 'planning') {
|
|
291
|
+
console.error(`--via "${viaFromArg}" is not a door this CLI may name. Where a filing came from is read off your session; the one value you may add is --via planning.`);
|
|
292
|
+
process.exit(2);
|
|
293
|
+
}
|
|
284
294
|
|
|
285
295
|
// A BAD --kind used to reach the server and come back a 400 (task 1003694).
|
|
286
296
|
// That is not merely late, it is invisible where it matters: a SCHEDULED routine
|
|
@@ -376,6 +386,7 @@ async function main() {
|
|
|
376
386
|
suggested_priority: priority,
|
|
377
387
|
};
|
|
378
388
|
if (kindFromArg) payload.kind = kindFromArg;
|
|
389
|
+
if (viaFromArg) payload.filed_via = viaFromArg;
|
|
379
390
|
if (goalResolution.goalId != null) payload.goal_id = goalResolution.goalId;
|
|
380
391
|
if (wantFull) {
|
|
381
392
|
// The server validates these against the R141 declaration and scores them
|
|
@@ -83,6 +83,10 @@ const INTEGRATION = new Set([
|
|
|
83
83
|
// rows is NULL rather than a free ship. Only a transaction engine settles
|
|
84
84
|
// those, so this runs here and the DB-free sibling covers the renderer.
|
|
85
85
|
'acclaim_feed_evidence_db',
|
|
86
|
+
// task 1002977: proves the changed-totals skip rule (a single INSERT ...
|
|
87
|
+
// WHERE NOT EXISTS statement) and the upsertActivity wiring against real SQL —
|
|
88
|
+
// a fake pool can prove the query shape but not which reports it inserts.
|
|
89
|
+
'activity_snapshots_db',
|
|
86
90
|
'asset_version', 'auth_flow_slice1', 'backfill_sessions', 'bfg', 'bfg_write',
|
|
87
91
|
'box_connect_e2e', 'box_onboard', 'boxes', 'cf_tunnel', 'cli_token_reissue',
|
|
88
92
|
// consumer_layout_boot builds the core .tgz, `npm install`s it, and boots a live
|
|
@@ -1171,6 +1171,10 @@ function mountInternalSurfaces(app) {
|
|
|
1171
1171
|
// task 1003479 (goal 1000089): `goal-map` — the map of which goals wait on which,
|
|
1172
1172
|
// reached from Goals. It reads GET /goals/graph, which is `requireBuilder`, so a
|
|
1173
1173
|
// page gated any lower would show a signed-out visitor an empty shell.
|
|
1174
|
+
// task 1004232 (sky part 4): `goal-map` is now THE PROJECT SKY — the same page
|
|
1175
|
+
// as `thinking`, opened in its "the whole project" lens — and holds the nav
|
|
1176
|
+
// slot under Goals the map was denied (ADR 0227 §11). Same tier: it reads GET
|
|
1177
|
+
// /sky and GET /sky/mine, the sky's own reads, behind the same sign-in.
|
|
1174
1178
|
// task 1004231 (sky part 3): `thinking` is now THE SKY itself — the prototype
|
|
1175
1179
|
// the owner adopted (docs/design/mocks/sky/, ADR 0331) built over GET /sky +
|
|
1176
1180
|
// GET /sky/mine, with the room inside it as the list drawer. The old ambient
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.1016'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// tests/activity_snapshots_db.mjs — task 1002977 (community spec D2; real DB).
|
|
2
|
+
//
|
|
3
|
+
// Proves the windowed data plane's append-only history against real SQL. The
|
|
4
|
+
// skip-on-unchanged rule lives entirely inside one INSERT ... WHERE NOT EXISTS
|
|
5
|
+
// statement (activity-snapshots.js), so a fake pool can only prove the query
|
|
6
|
+
// SHAPE (modules/platform-identity/tests/activity-snapshots.mjs); only a real
|
|
7
|
+
// Postgres can prove WHICH reports it actually inserts, and that upsertActivity
|
|
8
|
+
// (the existing POST /sso/activity/rollup path) really calls it end to end.
|
|
9
|
+
//
|
|
10
|
+
// Real-DB integration test: self-skips without a reachable Postgres; in the
|
|
11
|
+
// INTEGRATION set (scripts/gds/run-unit-tests.js) so the default DB-free lane
|
|
12
|
+
// never runs it.
|
|
13
|
+
//
|
|
14
|
+
// Run: DATABASE_URL=... node tests/activity_snapshots_db.mjs
|
|
15
|
+
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
import { strict as assert } from 'node:assert';
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const { Pool } = require('pg');
|
|
23
|
+
const pi = require('../modules/platform-identity/platform-identity.js');
|
|
24
|
+
const { recordActivitySnapshot } = require('../modules/platform-identity/activity-snapshots.js');
|
|
25
|
+
|
|
26
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const MIG_DIR = join(here, '..', 'modules', 'platform-identity', 'migrations');
|
|
28
|
+
const migrationSql = (f) => readFileSync(join(MIG_DIR, f), 'utf8');
|
|
29
|
+
|
|
30
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
31
|
+
try {
|
|
32
|
+
await pool.query('SELECT 1');
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.log(`activity_snapshots_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
|
|
35
|
+
await pool.end().catch(() => {});
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// High-numbered fixture ids so a shared DB can never collide with real data.
|
|
40
|
+
const BUILDER = 90002977, OTHER_BUILDER = 90002978;
|
|
41
|
+
const IDS = [BUILDER, OTHER_BUILDER];
|
|
42
|
+
const CLIENT = 't1002977-client';
|
|
43
|
+
const q = (sql, params) => pool.query(sql, params);
|
|
44
|
+
|
|
45
|
+
async function cleanup() {
|
|
46
|
+
await q(`DELETE FROM platform_identity_activity_snapshots WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
|
|
47
|
+
await q(`DELETE FROM platform_identity_builder_project_activity WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let pass = 0;
|
|
51
|
+
const ok = (cond, msg) => { assert.ok(cond, msg); pass++; console.log(` ok - ${msg}`); };
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await q(migrationSql('platform_identity_005_builder_activity.sql'));
|
|
55
|
+
// upsertActivity's INSERT always names recent_ships (012_recent_ships adds the
|
|
56
|
+
// column); without it here, step 2 below fails 42703 on a fresh database.
|
|
57
|
+
await q(migrationSql('platform_identity_012_recent_ships.sql'));
|
|
58
|
+
await q(migrationSql('platform_identity_022_activity_snapshots.sql'));
|
|
59
|
+
await cleanup();
|
|
60
|
+
|
|
61
|
+
// 1 — recordActivitySnapshot directly: the changed-totals skip rule.
|
|
62
|
+
const first = await recordActivitySnapshot({ clientId: CLIENT, githubId: BUILDER, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
|
|
63
|
+
ok(first !== null, 'a first-ever report inserts — nothing to be unchanged from');
|
|
64
|
+
|
|
65
|
+
const unchanged = await recordActivitySnapshot({ clientId: CLIENT, githubId: BUILDER, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
|
|
66
|
+
ok(unchanged === null, 'an identical re-report inserts nothing');
|
|
67
|
+
|
|
68
|
+
const changed = await recordActivitySnapshot({ clientId: CLIENT, githubId: BUILDER, credits: 150, tasksShipped: 5, karma: 3 }, { pool });
|
|
69
|
+
ok(changed !== null, 'a changed report (credits differ) inserts a new row');
|
|
70
|
+
|
|
71
|
+
const { rows: history } = await q(
|
|
72
|
+
`SELECT credits FROM platform_identity_activity_snapshots
|
|
73
|
+
WHERE client_id = $1 AND github_id = $2 ORDER BY reported_at, id`,
|
|
74
|
+
[CLIENT, BUILDER],
|
|
75
|
+
);
|
|
76
|
+
ok(history.length === 2, 'exactly two rows exist — the unchanged report left no trace');
|
|
77
|
+
assert.deepEqual(history.map((r) => r.credits), [100, 150]);
|
|
78
|
+
pass++; console.log(' ok - the history is in report order');
|
|
79
|
+
|
|
80
|
+
// 2 — upsertActivity (the real POST /sso/activity/rollup path) really calls it.
|
|
81
|
+
await pi.upsertActivity({ clientId: CLIENT, githubId: OTHER_BUILDER, credits: 10, tasksShipped: 1, karma: 0 }, { pool });
|
|
82
|
+
await pi.upsertActivity({ clientId: CLIENT, githubId: OTHER_BUILDER, credits: 10, tasksShipped: 1, karma: 0 }, { pool });
|
|
83
|
+
await pi.upsertActivity({ clientId: CLIENT, githubId: OTHER_BUILDER, credits: 20, tasksShipped: 1, karma: 0 }, { pool });
|
|
84
|
+
const { rows: viaRollup } = await q(
|
|
85
|
+
`SELECT credits FROM platform_identity_activity_snapshots
|
|
86
|
+
WHERE client_id = $1 AND github_id = $2 ORDER BY reported_at, id`,
|
|
87
|
+
[CLIENT, OTHER_BUILDER],
|
|
88
|
+
);
|
|
89
|
+
ok(viaRollup.length === 2, 'upsertActivity appends a snapshot per changed report — two calls, one repeat, one row skipped');
|
|
90
|
+
assert.deepEqual(viaRollup.map((r) => r.credits), [10, 20]);
|
|
91
|
+
pass++; console.log(' ok - deleting the call inside upsertActivity would leave this red');
|
|
92
|
+
|
|
93
|
+
console.log(`\nactivity_snapshots_db.mjs: ${pass} passed`);
|
|
94
|
+
} finally {
|
|
95
|
+
await cleanup();
|
|
96
|
+
await pool.end().catch(() => {});
|
|
97
|
+
}
|
package/tests/discord_bugs.mjs
CHANGED
|
@@ -115,6 +115,7 @@ await t('recordInboundBug: linked Metic → filed at ready, kind=bug, attributed
|
|
|
115
115
|
assert.equal(calls.create[0].kind, 'bug', 'the channel IS the classification');
|
|
116
116
|
assert.equal(calls.create[0].filerRank, 'metic', 'the LIVE rank is handed to the matrix');
|
|
117
117
|
assert.equal(calls.create[0].capturedBy, 9, 'and the reporter is attached to the capture');
|
|
118
|
+
assert.equal(calls.create[0].filedVia, 'discord', 'the door is a fact on the row (task 1004233)');
|
|
118
119
|
assert.equal(calls.create[0].goalId, 7, 'the suggester found it a home');
|
|
119
120
|
// task 1003067: no versionId is passed AT ALL. It used to be the hardcoded
|
|
120
121
|
// 'GDS-V3' — a shipped version, which is idea 1000771. The goal carries it now.
|
|
@@ -83,6 +83,7 @@ await t('recordInboundIdea: linked author → idea attributed to builder + audit
|
|
|
83
83
|
assert.equal(r.outcome, 'filed');
|
|
84
84
|
assert.equal(r.ideaId, 77);
|
|
85
85
|
assert.equal(calls.capture[0].capturedBy, 9);
|
|
86
|
+
assert.equal(calls.capture[0].filedVia, 'discord', 'the door is a fact on the row (task 1004233): this path calls the port, so it names it');
|
|
86
87
|
assert.match(calls.capture[0].bodyMd, /filed from Discord #ideas by mona/);
|
|
87
88
|
assert.equal(calls.audit[0].surface, 'discord_bot');
|
|
88
89
|
assert.equal(calls.audit[0].builderId, 9);
|