@bongos/core 1.19.1015 → 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 +98 -73
- package/.claude/skills/planning-session/SKILL.md +1 -1
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0331-the-sky-is-the-ideators-home.md +12 -0
- 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/file-map.md +0 -1
- package/docs/module-api-changelog.md +3 -1
- 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/sky-data.js +22 -8
- package/modules/hall-ui/public/sky-draw.js +3 -2
- package/modules/hall-ui/public/sky-panel.js +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/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 +14 -0
- package/tests/guild_namespace_reserved.mjs +76 -0
- package/tests/hall_sky.mjs +33 -16
- package/tests/idea_filed_via.mjs +185 -0
- package/tests/landing_page.mjs +8 -0
- package/tests/profile_rollup_consent.mjs +5 -1
- package/tests/project_admin_console_model.mjs +105 -0
- package/tests/sky_read.mjs +19 -2
- package/tests/ui_design_skills.mjs +9 -0
- 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
|
@@ -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
|
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);
|
package/tests/goal_map_page.mjs
CHANGED
|
@@ -354,6 +354,20 @@ await test('the tour walks all three lenses here too, and hands the sky back to
|
|
|
354
354
|
assert.equal(first.timers.pending(), 1, 'a first visit schedules the tour here as well');
|
|
355
355
|
});
|
|
356
356
|
|
|
357
|
+
// ═══ born of a thought (part 5) ══════════════════════════════════════════════
|
|
358
|
+
|
|
359
|
+
await test('a well born of a thought wears it in the ledger and the panel — the fact from GET /sky\'s born, never a guess', async () => {
|
|
360
|
+
const sk = FOUR(); sk.born = { goals: { 9: 21 }, criteria: {} };
|
|
361
|
+
const p = await boot({ sky: sk, versions: versions() });
|
|
362
|
+
assert.equal(goal(p, '9').bornOf, '21');
|
|
363
|
+
assert.equal(goal(p, '7').bornOf, null);
|
|
364
|
+
p.key('l');
|
|
365
|
+
const row = (new RegExp('data-id="9"[\\s\\S]*?</button>').exec(p.rows()) || [''])[0];
|
|
366
|
+
assert.match(row, /born of a thought/);
|
|
367
|
+
p.SKY().pick({ kind: 'goal', ref: goal(p, '9') }, false);
|
|
368
|
+
assert.match(p.panel(), /born of a thought/);
|
|
369
|
+
});
|
|
370
|
+
|
|
357
371
|
// ═══ the words are never markup ══════════════════════════════════════════════
|
|
358
372
|
|
|
359
373
|
await test('a goal title never becomes markup — in the ledger, the panel or the hover card', async () => {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// tests/guild_namespace_reserved.mjs — the guild URL namespace is RESERVED where
|
|
2
|
+
// project slugs are decided (task 1002299, R23 guild design; task 1002912's rule).
|
|
3
|
+
//
|
|
4
|
+
// Project profiles live at the top level (`cloudbongos.com/<slug>`), so every
|
|
5
|
+
// top-level segment the hub uses for another kind of page must be on the reserved
|
|
6
|
+
// project-slug list in the SEO spec, or a project can register the word and shadow
|
|
7
|
+
// the namespace. `u` guards `/u/<handle>`; the guild design names its own segment,
|
|
8
|
+
// and task 1002912 asked for the word to be reserved there, not a route squatted.
|
|
9
|
+
//
|
|
10
|
+
// No slug validator exists yet (the SEO goal's R02 builds it from that list), so the
|
|
11
|
+
// spec IS the contract today. This pins the one property that can drift silently:
|
|
12
|
+
// the segment the guild ADR declares and the segment the spec reserves are the same.
|
|
13
|
+
//
|
|
14
|
+
// Run: node tests/guild_namespace_reserved.mjs
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { makeRunner } from './helpers.mjs';
|
|
20
|
+
|
|
21
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
|
+
const ADR_DIR = path.join(ROOT, 'docs', 'adr');
|
|
23
|
+
const SEO_SPEC = path.join(ROOT, 'docs', 'specs', 'bongos-v1-seo-public-discoverability.md');
|
|
24
|
+
const GUILD_TASK = '1002299';
|
|
25
|
+
|
|
26
|
+
const { test, summary } = makeRunner();
|
|
27
|
+
|
|
28
|
+
// The guild design ADR, found by the task it records rather than by number: ADR
|
|
29
|
+
// numbers get renumbered when two sessions collide, the task id never changes.
|
|
30
|
+
function guildAdr() {
|
|
31
|
+
const hits = fs.readdirSync(ADR_DIR)
|
|
32
|
+
.filter((f) => /^\d{4}-.*\.md$/.test(f))
|
|
33
|
+
.filter((f) => {
|
|
34
|
+
const head = fs.readFileSync(path.join(ADR_DIR, f), 'utf8').replace(/\r\n/g, '\n').split('\n').slice(0, 12).join('\n');
|
|
35
|
+
return new RegExp(`^- \\*\\*Task:\\*\\*.*task ${GUILD_TASK}\\b`, 'm').test(head);
|
|
36
|
+
});
|
|
37
|
+
assert.equal(hits.length, 1, `exactly one ADR records task ${GUILD_TASK}; found ${JSON.stringify(hits)}`);
|
|
38
|
+
return fs.readFileSync(path.join(ADR_DIR, hits[0]), 'utf8').replace(/\r\n/g, '\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The segment the ADR declares on its machine-readable `Namespace:` line.
|
|
42
|
+
function declaredSegment(adr) {
|
|
43
|
+
const m = adr.match(/^- \*\*Namespace:\*\* `\/([a-z0-9-]+)\/<slug>`/m);
|
|
44
|
+
assert.ok(m, 'the guild ADR carries a `- **Namespace:** `/<segment>/<slug>`` header line');
|
|
45
|
+
return m[1];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The reserved words in the SEO spec's Routing bullet: the backticked list after "Reserved:".
|
|
49
|
+
function reservedWords() {
|
|
50
|
+
const spec = fs.readFileSync(SEO_SPEC, 'utf8').replace(/\r\n/g, '\n');
|
|
51
|
+
const routing = spec.split('\n').find((l) => l.startsWith('- **Routing:**'));
|
|
52
|
+
assert.ok(routing, 'the SEO spec still has its Routing bullet');
|
|
53
|
+
const m = routing.match(/Reserved: `([^`]+)`/);
|
|
54
|
+
assert.ok(m, 'the Routing bullet still names its reserved list as `Reserved: `…``');
|
|
55
|
+
return m[1].split(',').map((w) => w.trim()).filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await test('the guild ADR declares one top-level namespace segment', () => {
|
|
59
|
+
const seg = declaredSegment(guildAdr());
|
|
60
|
+
assert.match(seg, /^[a-z0-9-]+$/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await test('the SEO spec reserves the segment the guild ADR declares', () => {
|
|
64
|
+
const seg = declaredSegment(guildAdr());
|
|
65
|
+
const reserved = reservedWords();
|
|
66
|
+
assert.ok(reserved.includes(seg),
|
|
67
|
+
`a project could register "${seg}" and shadow /${seg}/<slug>; reserved list is ${JSON.stringify(reserved)}`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await test('the builder-profile segment it was modelled on is still reserved', () => {
|
|
71
|
+
// The precedent the task names. If `u` ever leaves the list, the guild reservation
|
|
72
|
+
// above has lost the rule it was copied from, and both need a second look.
|
|
73
|
+
assert.ok(reservedWords().includes('u'));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
summary();
|
package/tests/hall_sky.mjs
CHANGED
|
@@ -303,27 +303,44 @@ await test('the claude layer is HIDDEN until filed_via exists, and every thought
|
|
|
303
303
|
assert.ok(p.S().ideas.every((i) => i.origin === 'human'));
|
|
304
304
|
});
|
|
305
305
|
|
|
306
|
-
await test('
|
|
307
|
-
const p = await boot({ mine: mine([star(), idea({ id: 2, title: 'A session filed this', filed_via: '
|
|
306
|
+
await test('filed_via IS A LEDGER FACT (part 5): cli / api / discord wait behind the claude layer, hall / planning always show, and a row from before the column shows and says so', async () => {
|
|
307
|
+
const p = await boot({ mine: mine([star(), idea({ id: 2, title: 'A session filed this', filed_via: 'cli' }), idea({ id: 3, title: 'A person filed this', filed_via: 'hall' }), idea({ id: 4, title: 'Planned', filed_via: 'planning' }), idea({ id: 5, title: 'A poller filed this', filed_via: 'api' }), idea({ id: 6, title: 'From Discord', filed_via: 'discord' })]), sky: sky([well()]) });
|
|
308
308
|
assert.equal(p.S().hasFiledVia, true);
|
|
309
309
|
assert.equal(p.el('claudeBtn').hidden, false);
|
|
310
310
|
const SKY = p.SKY();
|
|
311
|
-
|
|
312
|
-
assert.equal(SKY.inLayer(findIdea(p,
|
|
313
|
-
assert.equal(SKY.inLayer(findIdea(p,
|
|
311
|
+
same(SKY.SESSION_VIAS, ['cli', 'api', 'discord'], 'the doors a session files through');
|
|
312
|
+
for (const id of [2, 5, 6]) assert.equal(SKY.inLayer(findIdea(p, id)), false, `${id} waits behind the pill — off by default`);
|
|
313
|
+
for (const id of [10, 3, 4]) assert.equal(SKY.inLayer(findIdea(p, id)), true, `${id} shows`);
|
|
314
|
+
assert.equal(findIdea(p, 10).filedVia, null, 'a historic row carries NULL, not a guess');
|
|
314
315
|
p.click('claudeBtn');
|
|
315
|
-
assert.equal(SKY.inLayer(findIdea(p,
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
assert.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
316
|
+
for (const id of [2, 5, 6]) assert.equal(SKY.inLayer(findIdea(p, id)), true, 'the pill reveals them');
|
|
317
|
+
SKY.pick({ kind: 'idea', ref: findIdea(p, 10) }, false);
|
|
318
|
+
assert.match(p.panel(), /filed before the ledger recorded where/, 'and the panel says so, in those words');
|
|
319
|
+
SKY.pick({ kind: 'idea', ref: findIdea(p, 3) }, false);
|
|
320
|
+
assert.match(p.panel(), /from the hall/);
|
|
321
|
+
SKY.pick({ kind: 'idea', ref: findIdea(p, 2) }, false);
|
|
322
|
+
assert.match(p.panel(), /from the command line · by a session, under your name/);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await test('BORN OF A THOUGHT IS READ FROM GET /sky (part 5), never guessed: the goal wears the nucleus, the ring the notch, and the thought sits where it became', async () => {
|
|
326
|
+
const sk = sky([well()]); sk.born = { goals: { 7: 10 }, criteria: { 70: 12 } };
|
|
327
|
+
const p = await boot({ mine: mine([star(), coldBody({ id: 12, title: 'It became a ring' })]), sky: sk });
|
|
328
|
+
const S = p.S(); const SKY = p.SKY(); const g = S.goalById.get('7');
|
|
329
|
+
same(S.BORN, { goals: { 7: '10' }, criteria: { 70: '12' } }, 'ids are strings whatever the wire sent');
|
|
330
|
+
assert.equal(g.bornOf, '10'); assert.equal(g.crit[0].bornOf, '12');
|
|
331
|
+
assert.equal(S.becameGoal.get('10'), '7'); assert.equal(S.becameCrit.get('12'), g.crit[0]);
|
|
332
|
+
const shaped = findIdea(p, 10); assert.equal(shaped.shaped.kind, 'goal'); assert.equal(shaped.x, g.x); assert.equal(shaped.y, g.y);
|
|
333
|
+
const ring = findIdea(p, 12); assert.equal(ring.shaped.kind, 'ring');
|
|
334
|
+
assert.ok(Math.abs(Math.hypot(ring.x - g.x, ring.y - g.y) - SKY.ringR(g, 0, 2)) < 1e-6, 'on the first ring');
|
|
335
|
+
assert.equal(SKY.became(shaped), 'a goal — it shaped the space');
|
|
336
|
+
assert.match(SKY.became(ring), /^a criterion — a ring in /);
|
|
325
337
|
p.key('l');
|
|
326
|
-
assert.
|
|
338
|
+
assert.match(p.rows(), /shaped the space/);
|
|
339
|
+
SKY.pick({ kind: 'goal', ref: g }, false);
|
|
340
|
+
assert.match(p.panel(), /born of a thought/);
|
|
341
|
+
const none = await boot({ mine: mine(FIVE()), sky: sky([well()]) });
|
|
342
|
+
assert.equal(none.S().becameGoal.size, 0, 'no born read, no mark — nothing is guessed');
|
|
343
|
+
assert.ok(none.S().goals.every((x) => x.bornOf === null && x.crit.every((c) => c.bornOf === null)));
|
|
327
344
|
});
|
|
328
345
|
|
|
329
346
|
// ═══ the bands — derived, never named by hand ════════════════════════════════
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// tests/idea_filed_via.mjs — WHERE AN IDEA WAS FILED FROM, and WHICH THOUGHTS
|
|
2
|
+
// BECAME THE STRUCTURE (task 1004233 / sky part 5 of 5, goal 1000095; ADR 0331 §4).
|
|
3
|
+
//
|
|
4
|
+
// The sky's prototype guessed two things from text: whether a session or a
|
|
5
|
+
// person filed a thought, and which thoughts became goals or rings. This suite
|
|
6
|
+
// pins the two ledger facts that replaced those guesses:
|
|
7
|
+
// - the column: additive, nullable, no default, NO BACKFILL — a row older
|
|
8
|
+
// than the column stays NULL, because NULL is not 'cli';
|
|
9
|
+
// - the vocabulary: one closed set, spelled in projection.js and mirrored by
|
|
10
|
+
// the migration's CHECK, so a bad value is a named 400 and never a 500;
|
|
11
|
+
// - each writer's value: the route reads the door off the SESSION (web is the
|
|
12
|
+
// hall, cli is the CLI, anything else the API) and lets the body add only
|
|
13
|
+
// 'planning'; capture.js names 'planning' and nothing else; the Discord
|
|
14
|
+
// paths and the agents' sink name their own on the port; a new row always
|
|
15
|
+
// records a door;
|
|
16
|
+
// - the born read: first ratification wins, ids as strings, through the
|
|
17
|
+
// ideas.bornOf port so lifecycle never reads an ideas table itself.
|
|
18
|
+
//
|
|
19
|
+
// DB-free: captureIdea runs against a recording pool (the idea_quick_guard
|
|
20
|
+
// harness), readBornOf against a fake pool, the rest is pure or read from source.
|
|
21
|
+
//
|
|
22
|
+
// Run: node tests/idea_filed_via.mjs
|
|
23
|
+
process.env.NODE_ENV = 'test';
|
|
24
|
+
import { strict as assert } from 'node:assert';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import { makeRunner } from './helpers.mjs';
|
|
30
|
+
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
+
const read = (f) => fs.readFileSync(path.join(ROOT, f), 'utf8');
|
|
34
|
+
const api = require(path.join(ROOT, 'src/module-api.js'));
|
|
35
|
+
let queries = [];
|
|
36
|
+
let nextRows = [];
|
|
37
|
+
api.pool.query = async (sql, params = []) => { queries.push({ sql, params }); return { rows: nextRows }; };
|
|
38
|
+
const inbox = require(path.join(ROOT, 'modules/ideas/inbox.js'));
|
|
39
|
+
const projection = require(path.join(ROOT, 'modules/ideas/projection.js'));
|
|
40
|
+
const { readBornOf } = require(path.join(ROOT, 'modules/ideas/born-of.js'));
|
|
41
|
+
|
|
42
|
+
const { test, summary } = makeRunner();
|
|
43
|
+
const reset = () => { queries = []; nextRows = [{ id: '41', title: 'an idea', status: 'open', grade: 'quick' }]; };
|
|
44
|
+
const FIVE = ['hall', 'cli', 'discord', 'planning', 'api'];
|
|
45
|
+
const migrationFile = fs.readdirSync(path.join(ROOT, 'migrations')).find((f) => /^core_\d+_idea_filed_via\.sql$/.test(f));
|
|
46
|
+
|
|
47
|
+
// ═══ the migration ═══════════════════════════════════════════════════════════
|
|
48
|
+
|
|
49
|
+
await test('the column is a CORE migration: additive, nullable, no default, and the CHECK names exactly the five doors', () => {
|
|
50
|
+
assert.ok(migrationFile, 'migrations/core_NNN_idea_filed_via.sql exists (idea_inbox is a core table — ADR 0083 §Decision #5)');
|
|
51
|
+
const sql = read(path.join('migrations', migrationFile));
|
|
52
|
+
assert.match(sql, /ALTER TABLE idea_inbox\s+ADD COLUMN IF NOT EXISTS filed_via text;/);
|
|
53
|
+
assert.equal(/filed_via text\s+(NOT NULL|DEFAULT)/i.test(sql), false, 'nullable, no default');
|
|
54
|
+
const check = /CHECK \(filed_via IS NULL OR filed_via IN \(([^)]*)\)\)/.exec(sql);
|
|
55
|
+
assert.ok(check, 'a CHECK closes the set');
|
|
56
|
+
assert.deepEqual(check[1].split(',').map((s) => s.trim().replace(/'/g, '')), FIVE);
|
|
57
|
+
assert.match(sql, /IF NOT EXISTS \(\s*SELECT 1 FROM pg_constraint/, 'the CHECK add is guarded — replay-safe');
|
|
58
|
+
assert.ok(sql.includes(`INSERT INTO schema_migrations (version) VALUES ('${migrationFile.replace(/\.sql$/, '')}')`), 'the stem is the version');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await test('NULL IS NOT \'cli\': the migration backfills nothing, and no writer guesses a door from text', () => {
|
|
62
|
+
const sql = read(path.join('migrations', migrationFile));
|
|
63
|
+
assert.equal(/UPDATE idea_inbox/i.test(sql), false, 'no backfill — a historic row stays NULL');
|
|
64
|
+
assert.equal(/title|body_md|ILIKE|~\*/.test(sql.replace(/^--.*$/gm, '')), false, 'nothing in the SQL reads the idea\'s words');
|
|
65
|
+
const src = read('modules/ideas/inbox.js') + read('modules/ideas/projection.js') + read('modules/ideas/routes/inbox.js');
|
|
66
|
+
assert.equal(/filed_via\s*=\s*CASE|classify\([^)]*\)\s*===?\s*'(cli|api)'/.test(src), false, 'no code derives a door from the text either');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ═══ the vocabulary ══════════════════════════════════════════════════════════
|
|
70
|
+
|
|
71
|
+
await test('ONE closed set, spelled in projection.js, carried by every read', () => {
|
|
72
|
+
assert.deepEqual([...projection.FILED_VIA_VALUES], FIVE);
|
|
73
|
+
assert.ok(Object.isFrozen(projection.FILED_VIA_VALUES));
|
|
74
|
+
assert.ok(projection.IDEA_COL_NAMES.includes('filed_via'), 'on the shared column list — so the trail (and GET /sky/mine, which is the trail) returns it');
|
|
75
|
+
assert.match(projection.IDEA_COLS_Q, /\bi\.filed_via\b/);
|
|
76
|
+
for (const v of FIVE) assert.equal(projection.isValidFiledVia(v), true);
|
|
77
|
+
for (const v of ['session', 'person', '', null, undefined, 'HALL']) assert.equal(projection.isValidFiledVia(v), false, `${v} is not a door`);
|
|
78
|
+
assert.deepEqual([inbox.FILED_VIA_VALUES, inbox.resolveFiledVia], [projection.FILED_VIA_VALUES, projection.resolveFiledVia], 'the facade re-exports the one declaration');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ═══ the door is read off the session ════════════════════════════════════════
|
|
82
|
+
|
|
83
|
+
await test('the session says which door: a web cookie is the hall, a device-flow token is the CLI, anything else is the API', () => {
|
|
84
|
+
const { filedViaForSource } = projection;
|
|
85
|
+
assert.equal(filedViaForSource('web'), 'hall');
|
|
86
|
+
assert.equal(filedViaForSource('cli'), 'cli');
|
|
87
|
+
assert.equal(filedViaForSource('box'), 'api');
|
|
88
|
+
assert.equal(filedViaForSource(null), 'api', 'a legacy sourceless session is the API, not a guess');
|
|
89
|
+
assert.equal(filedViaForSource(undefined), 'api');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await test('the body may ADD only \'planning\'; repeating the session\'s own door is harmless; any other claim is refused with the reason', () => {
|
|
93
|
+
const { resolveFiledVia } = projection;
|
|
94
|
+
assert.deepEqual(resolveFiledVia('cli', undefined), { ok: true, value: 'cli' });
|
|
95
|
+
assert.deepEqual(resolveFiledVia('web', null), { ok: true, value: 'hall' });
|
|
96
|
+
assert.deepEqual(resolveFiledVia('cli', 'planning'), { ok: true, value: 'planning' }, 'the planning session is a CLI filing no session can tell apart');
|
|
97
|
+
assert.deepEqual(resolveFiledVia('web', 'planning'), { ok: true, value: 'planning' });
|
|
98
|
+
assert.deepEqual(resolveFiledVia('web', 'hall'), { ok: true, value: 'hall' }, 'naming your own door changes nothing');
|
|
99
|
+
const spoof = resolveFiledVia('cli', 'hall');
|
|
100
|
+
assert.equal(spoof.ok, false, 'a CLI cannot say it came through the hall');
|
|
101
|
+
assert.deepEqual(spoof.detail.allowed, ['planning', 'cli']);
|
|
102
|
+
assert.match(spoof.detail.reason, /read off your session \(cli\)/);
|
|
103
|
+
const junk = resolveFiledVia('web', 'session');
|
|
104
|
+
assert.equal(junk.ok, false);
|
|
105
|
+
assert.match(junk.detail.reason, /must be one of hall, cli, discord, planning, api/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await test('POST /inbox reads the door off req.gdsSession, refuses a bad claim as a named 400, and hands captureIdea the value', () => {
|
|
109
|
+
const src = read('modules/ideas/routes/inbox.js');
|
|
110
|
+
assert.match(src, /filed_via: \{ type: 'string', maxLength: 16 \}/, 'the body field is declared (strict validation would 400 it otherwise)');
|
|
111
|
+
assert.match(src, /inbox\.resolveFiledVia\(req\.gdsSession && req\.gdsSession\.source, body\.filed_via\)/, 'the SESSION source, never the body alone');
|
|
112
|
+
assert.match(src, /res\.fail\('bad_filed_via', 400, door\.detail\)/);
|
|
113
|
+
assert.match(src, /filedVia: door\.value,/);
|
|
114
|
+
assert.match(src, /err\.code === 'BAD_FILED_VIA' \? 400 :/, 'a value that slipped past the route is still a 400, never a constraint 500');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ═══ every writer ════════════════════════════════════════════════════════════
|
|
118
|
+
|
|
119
|
+
await test('captureIdea WRITES the door on the row, defaults a silent port caller to \'api\', and refuses a value outside the set', async () => {
|
|
120
|
+
reset();
|
|
121
|
+
await inbox.captureIdea({ title: 'from the bot', capturedBy: '7', filedVia: 'discord' });
|
|
122
|
+
assert.equal(queries.length, 1);
|
|
123
|
+
assert.match(queries[0].sql, /INSERT INTO idea_inbox[\s\S]*filed_via\)/, 'the column is in the INSERT');
|
|
124
|
+
assert.equal(queries[0].params.at(-1), 'discord', 'and the value is the last bind');
|
|
125
|
+
reset();
|
|
126
|
+
await inbox.captureIdea({ title: 'a program said nothing', capturedBy: null });
|
|
127
|
+
assert.equal(queries[0].params.at(-1), 'api', 'a NEW row always records a door — NULL is for rows older than the column');
|
|
128
|
+
reset();
|
|
129
|
+
await assert.rejects(inbox.captureIdea({ title: 'x', capturedBy: '7', filedVia: 'session' }), (e) => e.code === 'BAD_FILED_VIA' && e.filedVia === 'session' && Array.isArray(e.allowed));
|
|
130
|
+
assert.equal(queries.length, 0, 'refused before any write');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await test('the ROUTED insert (a filing that becomes a task in the same act) writes the door too', () => {
|
|
134
|
+
const src = read('modules/ideas/inbox.js');
|
|
135
|
+
const inserts = [...src.matchAll(/INSERT INTO idea_inbox[\s\S]*?RETURNING \$\{IDEA_COLS\}/g)].map((m) => m[0]);
|
|
136
|
+
assert.equal(inserts.length, 2, 'the inbox insert and the routed insert');
|
|
137
|
+
for (const ins of inserts) assert.match(ins, /filed_via/, 'both name the column');
|
|
138
|
+
assert.match(src, /capturedBy, goalId, landing, filerRank, touches, filedVia,\s*\}, activePool\)/, 'and the routed path is handed the value');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await test('capture.js names \'planning\' and nothing else — every other CLI filing\'s door is read off the session', () => {
|
|
142
|
+
const src = read('scripts/gds/capture.js');
|
|
143
|
+
assert.match(src, /const viaFromArg = arg\('--via', args\);/);
|
|
144
|
+
assert.match(src, /if \(viaFromArg !== null && viaFromArg !== 'planning'\)[\s\S]{0,400}process\.exit\(2\)/, 'a door the CLI may not name is refused before the request');
|
|
145
|
+
assert.match(src, /if \(viaFromArg\) payload\.filed_via = viaFromArg;/, 'sent only when given — a pinned core that predates the field never sees it');
|
|
146
|
+
assert.equal(/filed_via: 'cli'/.test(src), false, 'never claims cli — the session already says so');
|
|
147
|
+
assert.equal(/postInbox\(/.test(read('scripts/gds/spark.js')), false, 'spark.js files no idea row of its own (a spark is made from an existing idea), so its door is the row\'s');
|
|
148
|
+
assert.match(read('.claude/skills/planning-session/SKILL.md'), /--via planning/, 'the planning session is told to name its door');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await test('the Discord doors and the agents\' sink name their own door on the port', () => {
|
|
152
|
+
assert.match(read('modules/discord/discord-inbound.js'), /filedVia: 'discord',/);
|
|
153
|
+
assert.match(read('modules/discord/discord-bugs.js'), /filedVia: 'discord',/);
|
|
154
|
+
assert.match(read('modules/agents/pollers/event-router.js'), /filedVia: 'api',/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ═══ born of a thought ═══════════════════════════════════════════════════════
|
|
158
|
+
|
|
159
|
+
function bornPool({ goals = [], criteria = [] } = {}) {
|
|
160
|
+
const calls = [];
|
|
161
|
+
return { calls, async query(sql) { calls.push(sql); return { rows: /ideas_goal_ratifications/.test(sql) ? goals : criteria }; } };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await test('readBornOf: goals from the goal ratifications, criteria from the proposal audit, the FIRST ratification wins, ids as strings', async () => {
|
|
165
|
+
const pool = bornPool({
|
|
166
|
+
goals: [{ goal_id: '7', idea_id: '10' }, { goal_id: '7', idea_id: '99' }, { goal_id: 8, idea_id: 11 }],
|
|
167
|
+
criteria: [{ criterion_id: '70', idea_id: '12' }, { criterion_id: '71', idea_id: null }],
|
|
168
|
+
});
|
|
169
|
+
const born = await readBornOf({ pool });
|
|
170
|
+
assert.deepEqual(born, { goals: { 7: '10', 8: '11' }, criteria: { 70: '12' } });
|
|
171
|
+
assert.equal(pool.calls.length, 2);
|
|
172
|
+
assert.match(pool.calls[0], /FROM ideas_goal_ratifications\s+ORDER BY ratified_at, id/);
|
|
173
|
+
assert.match(pool.calls[1], /FROM criterion_proposal_audit\s+ORDER BY ratified_at, id/);
|
|
174
|
+
assert.deepEqual(await readBornOf({ pool: bornPool() }), { goals: {}, criteria: {} }, 'nothing ratified is an empty map');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await test('the read is served through the ideas.bornOf port, beside ideas.capture, so lifecycle\'s GET /sky never reads an ideas table', () => {
|
|
178
|
+
assert.match(read('modules/ideas/routes/inbox.js'), /api\.registerProvider\('ideas\.bornOf', bornOf\.readBornOf\)/);
|
|
179
|
+
const sky = read('modules/lifecycle/sky.js');
|
|
180
|
+
assert.match(sky, /resolveOptional\('ideas\.bornOf'\)/);
|
|
181
|
+
assert.equal(/ideas_goal_ratifications|criterion_proposal_audit|require\(['"][^'"]*ideas/.test(sky), false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
console.log('');
|
|
185
|
+
summary();
|
package/tests/landing_page.mjs
CHANGED
|
@@ -600,3 +600,11 @@ test('the chip is the profile trigger, built with createElement; Sign out is the
|
|
|
600
600
|
assert.match(CODE, /var signout = document\.querySelector\('#menuProfile a\.signout'\);\s*if \(signout\) signout\.addEventListener\('click', function \(e\) \{ e\.preventDefault\(\); doLogout\(\); \}\);/, 'the honest href, upgraded to the POST');
|
|
601
601
|
assert.doesNotMatch(CODE, /className = 'pill signout'/, 'no sign-out pill in the bar any more');
|
|
602
602
|
});
|
|
603
|
+
|
|
604
|
+
test('the retired gate assets (world/assets/clouds|creatures|plates) are gone — the chrome landing ships its plates from assets/world/ instead (task 1003650)', () => {
|
|
605
|
+
assert.ok(!fs.existsSync(path.join(LANDING, 'world')), 'modules/public-landing/public/world/ no longer exists');
|
|
606
|
+
assert.doesNotMatch(HTML, /\bworld\/assets\//, 'the page links no path under the retired world/assets/ folder');
|
|
607
|
+
assert.doesNotMatch(WORLD, /\bworld\/assets\//, 'world.css references no path under the retired folder');
|
|
608
|
+
// the chrome landing's own plates (task 1003298) live at assets/world/ and are untouched by this cleanup
|
|
609
|
+
assert.ok(fs.existsSync(path.join(LANDING, 'assets', 'world', 'r2-bongos-chrome-b.jpg')), 'the chrome plate at assets/world/ is untouched (task 1003649 converts it separately)');
|
|
610
|
+
});
|