@bongos/core 1.19.249 → 1.19.251
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 +110 -30
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +10 -0
- package/clients/bongos-client/index.cjs +10 -0
- package/clients/bongos-client/index.d.ts +14 -0
- package/clients/bongos-client/index.mjs +10 -0
- package/docs/adr/0178-the-copy-desk-flag-and-queue.md +131 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +257 -3
- package/docs/api-reference.md +10 -1
- package/docs/copy-inventory.md +1430 -0
- package/docs/copy-registry.json +13293 -0
- package/docs/file-map.md +1 -0
- package/modules/copy-desk/CLAUDE.md +15 -0
- package/modules/copy-desk/flags.js +245 -0
- package/modules/copy-desk/migrations/copy_desk_001_flags.sql +128 -0
- package/modules/copy-desk/module.json +11 -0
- package/modules/copy-desk/registry.js +317 -0
- package/modules/copy-desk/routes/copy-desk.js +272 -0
- package/modules/copy-desk/tests/copy_flags.mjs +246 -0
- package/modules/copy-desk/tests/copy_no_cms.mjs +104 -0
- package/modules/copy-desk/tests/copy_queue.mjs +257 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +10 -0
- package/modules/hall-ui/public/copy-desk.css +119 -0
- package/modules/hall-ui/public/copy-desk.html +139 -0
- package/modules/hall-ui/public/copy-desk.js +352 -0
- package/modules/hall-ui/public/shell.js +9 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/publish-manifest.js +9 -0
- package/scripts/gds/run-unit-tests.js +11 -1
- package/src/bongos/module-scope-map.js +17 -0
- package/src/bongos/serve-internal.js +11 -1
- package/src/module-api.js +5 -1
- package/tests/copy_desk_flags_db.mjs +250 -0
- package/tests/hall_nav.mjs +7 -1
- package/tests/module_loader.mjs +5 -2
- package/tests/publish_manifest.mjs +82 -0
|
@@ -62,6 +62,7 @@ const PUBLISH_ALLOWLIST = [
|
|
|
62
62
|
'modules/autonomy/',
|
|
63
63
|
'modules/security/',
|
|
64
64
|
'modules/government/', // the same machinery under its post-ADR-0174 name (goal 1000068, task 1003003). Added in lockstep with the protected-surfaces.json entry: publish_manifest asserts every PROTECTED_GLOB is either topology-excluded or publishable, so registering the glob as protected WITHOUT this line default-denies the module from the published core — the open-source methodology would silently lose its permission machinery at the next release.
|
|
65
|
+
'modules/copy-desk/', // the artist's loop over user-facing TEXT (goal 1000074, ADR 0178). default:true, so it MUST be here — and note the ASYMMETRY that makes omitting it worse than useless: its hall surface ships regardless, because modules/hall-ui/ is allowlisted as one subtree. A released core carrying the page and the nav entry but not the routes behind them puts a tab in every instance's hall whose every call 404s. The module and its page are only ever added or removed together; tests/publish_manifest.mjs holds that pair.
|
|
65
66
|
'modules/hall-ui/',
|
|
66
67
|
'modules/status-ui/',
|
|
67
68
|
'modules/dev-box/',
|
|
@@ -104,6 +105,14 @@ const PUBLISH_ALLOWLIST = [
|
|
|
104
105
|
'docs/branding-contract.md',
|
|
105
106
|
'docs/branding-fill-prompt.md',
|
|
106
107
|
'docs/design-contract.md',
|
|
108
|
+
// RUNTIME DATA that happens to live under docs/, not documentation (ADR 0178).
|
|
109
|
+
// modules/copy-desk reads the registry on EVERY queue load, and nothing on a
|
|
110
|
+
// deployed box regenerates it — copy-inventory.js is a commit-it generator run
|
|
111
|
+
// against a source tree, which an instance does not have. Omit these and a
|
|
112
|
+
// released instance answers 503 registry_missing forever. They sit under docs/
|
|
113
|
+
// only because that is where their generator has always written them.
|
|
114
|
+
'docs/copy-registry.json',
|
|
115
|
+
'docs/copy-inventory.md', // the human twin, generated in the same pass
|
|
107
116
|
'docs/file-map.md',
|
|
108
117
|
'docs/repo-map.md',
|
|
109
118
|
'docs/handoff-template.md', // the portable session-handoff format (methodology)
|
|
@@ -73,7 +73,17 @@ const INTEGRATION = new Set([
|
|
|
73
73
|
// platform-server — network + package build + a spawned server, run by its own
|
|
74
74
|
// .github/workflows/consumer-layout-boot.yml, not this DB-free gate (task 2142).
|
|
75
75
|
'consumer_layout_boot',
|
|
76
|
-
'claim_gate_open_blocker', 'claim_gate_rebase', 'clawback_confirmed_backlog',
|
|
76
|
+
'claim_gate_open_blocker', 'claim_gate_rebase', 'clawback_confirmed_backlog',
|
|
77
|
+
// task 1003113: the copy_desk_flags schema half. The module's own tests are
|
|
78
|
+
// DB-free and cover the pure layer; the rules that MATTER here live in the
|
|
79
|
+
// schema — a CHECK-constraint reason floor whose whole argument is that no
|
|
80
|
+
// future write path evades it, a PARTIAL unique index whose partiality is the
|
|
81
|
+
// design, and a close guarded on the open status so two closers cannot both
|
|
82
|
+
// win. None of those are testable without a real transaction engine.
|
|
83
|
+
// (Prose here avoids single-quoted tokens: the pairing guard below parses this
|
|
84
|
+
// set out of the source, so a quoted word in a comment reads as an entry.)
|
|
85
|
+
'copy_desk_flags_db',
|
|
86
|
+
'discord_autojoin', 'discord_oauth',
|
|
77
87
|
// RENAMED by ADR 0174 (goal 1000068) — these entries said governance_* until task
|
|
78
88
|
// 1003091. A curated set keyed by a stem that no longer exists silently stops
|
|
79
89
|
// covering its file: both of these dropped OUT of the integration set and INTO the
|
|
@@ -216,6 +216,23 @@ const MODULE_GLOBS = {
|
|
|
216
216
|
'modules/game/', 'src/preview-server.js',
|
|
217
217
|
],
|
|
218
218
|
'art-pipeline': ['modules/art-pipeline/'],
|
|
219
|
+
// --- the COPY half of the design contract: the artist's loop over user-facing
|
|
220
|
+
// TEXT (goal 1000074). ADR 0081 gave design TOKENS a repo-owned source of
|
|
221
|
+
// truth; text had none. R01 built the inventory (the copy-inventory
|
|
222
|
+
// generator + its committed registry under docs/), R02 the flag + queue on
|
|
223
|
+
// top of it. The generator travels with the domain but stays under
|
|
224
|
+
// scripts/gds/ — the memory/ideas precedent (a command travels with its
|
|
225
|
+
// domain, design 02§1). The generated artifacts are listed as exact paths
|
|
226
|
+
// so a copy task's touches[] can name them without claiming all of docs/.
|
|
227
|
+
// KEY IS 'copy-desk', NOT 'copy': the branding contract already owns a
|
|
228
|
+
// `copy` block (branding.copy.tagline/hallName/notFound), so the bare word
|
|
229
|
+
// collides with kernel code and reds the "core carries no module code"
|
|
230
|
+
// fitness check on the English word alone.
|
|
231
|
+
'copy-desk': [
|
|
232
|
+
'modules/copy-desk/',
|
|
233
|
+
'scripts/gds/copy-inventory.js',
|
|
234
|
+
'docs/copy-registry.json', 'docs/copy-inventory.md',
|
|
235
|
+
],
|
|
219
236
|
// --- the declarative agent registry (goal 1000038, ADR 0083 default:false):
|
|
220
237
|
// agent definitions reconciled at deploy into agents_definitions + a per-fire
|
|
221
238
|
// agents_runs ledger. Carved to modules/agents/ at foundation time, so the
|
|
@@ -922,8 +922,18 @@ function mountInternalSurfaces(app) {
|
|
|
922
922
|
// thetes redirected exactly that reader to /builders/not-ready. Owner's call
|
|
923
923
|
// (task 1002710) — the RANK floor is gone; sign-in is unchanged.
|
|
924
924
|
// The link-hiding in shell.js is only cosmetic; these are the real gates.
|
|
925
|
+
// task 1003113: copy-desk joins this tier rather than a higher one. Filing a
|
|
926
|
+
// flag is open to every rank by design (see the nav comment in shell.js), so
|
|
927
|
+
// the page's only real gate is sign-in — the writes behind it self-gate, and
|
|
928
|
+
// closing someone else's flag is checked server-side at metic-or-author.
|
|
929
|
+
//
|
|
930
|
+
// NB the assignment below must stay `const NAME_RE = /…/;` with NOTHING between
|
|
931
|
+
// the `=` and the literal: tests/hall_page_gate_map.mjs reconstructs the whole
|
|
932
|
+
// URL→gate map by parsing this file statically, and a comment in that gap drops
|
|
933
|
+
// the regex out of its map — which reads as "every page on this tier is
|
|
934
|
+
// ungated". It fails loudly by design; keep prose above the const.
|
|
925
935
|
const SIGNEDIN_BUILDERS_PAGE_RE =
|
|
926
|
-
/^\/builders\/(?:work|pair|settings|atlas|modules|goals|roadmap|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
|
|
936
|
+
/^\/builders\/(?:work|pair|settings|atlas|modules|goals|roadmap|copy-desk|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
|
|
927
937
|
// The diagram image assets under /builders/diagrams/ ride the same tier as the
|
|
928
938
|
// page that embeds them.
|
|
929
939
|
const SIGNEDIN_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
|
package/src/module-api.js
CHANGED
|
@@ -925,8 +925,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
925
925
|
// landed since 1.19.247 with no explicit bump. run 32198208766. (task 1002620)
|
|
926
926
|
// 1.19.249 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
927
927
|
// landed since 1.19.248 with no explicit bump. run 32199293372. (task 1002620)
|
|
928
|
+
// 1.19.250 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
929
|
+
// landed since 1.19.249 with no explicit bump. run 32203357427. (task 1002620)
|
|
930
|
+
// 1.19.251 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
931
|
+
// landed since 1.19.250 with no explicit bump. run 32204008770. (task 1002620)
|
|
928
932
|
// ---------------------------------------------------------------------------
|
|
929
|
-
const CORE_VERSION = '1.19.
|
|
933
|
+
const CORE_VERSION = '1.19.251'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
930
934
|
|
|
931
935
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
932
936
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// tests/copy_desk_flags_db.mjs — the copy_desk_flags schema + SQL against a REAL
|
|
2
|
+
// Postgres (task 1003113 / R02 of goal 1000074).
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS SEPARATELY FROM modules/copy-desk/tests/. Those three files are
|
|
5
|
+
// DB-free by contract and cover the PURE layer — the validators, the queue
|
|
6
|
+
// composition, the no-CMS shape. They cannot cover the half of this feature that
|
|
7
|
+
// lives in the schema, and that half is where the load-bearing rules are:
|
|
8
|
+
//
|
|
9
|
+
// * the REASON FLOOR is a CHECK constraint, not just a route check. The whole
|
|
10
|
+
// argument for putting it there is that no future write path can get around
|
|
11
|
+
// it — which is a claim about Postgres, and can only be tested against
|
|
12
|
+
// Postgres.
|
|
13
|
+
// * the ONE-OPEN-FLAG-PER-(builder, target) rule is a PARTIAL unique index. Its
|
|
14
|
+
// partiality is the design (two builders flagging one sentence is signal; the
|
|
15
|
+
// same builder twice is a double-click; and a closed flag must not block a
|
|
16
|
+
// re-flag when the copy regresses). A fake client cannot exercise that.
|
|
17
|
+
// * the CLOSE IS GUARDED on `status='open'` inside the UPDATE so two people
|
|
18
|
+
// closing at once cannot both win. That is a race, and a race needs a real
|
|
19
|
+
// transaction engine to demonstrate.
|
|
20
|
+
// * the FK on resolved_task_id is what turns a typo'd task number into a named
|
|
21
|
+
// 400 instead of an opaque 500.
|
|
22
|
+
//
|
|
23
|
+
// It SELF-SKIPS (exit 0) when no Postgres is reachable, so it is safe on a bare
|
|
24
|
+
// worktree, and it is listed in run-unit-tests.js's INTEGRATION set so the
|
|
25
|
+
// default DB-free lane never runs it.
|
|
26
|
+
//
|
|
27
|
+
// Run: DATABASE_URL=postgres://... node tests/copy_desk_flags_db.mjs
|
|
28
|
+
//
|
|
29
|
+
// It creates its OWN minimal `builders`/`tasks` stand-ins if they are absent, so
|
|
30
|
+
// it works against a throwaway container as well as a full migrated database, and
|
|
31
|
+
// it cleans up every row it writes.
|
|
32
|
+
|
|
33
|
+
import { createRequire } from 'node:module';
|
|
34
|
+
import { strict as assert } from 'node:assert';
|
|
35
|
+
import fs from 'node:fs';
|
|
36
|
+
import path from 'node:path';
|
|
37
|
+
import url from 'node:url';
|
|
38
|
+
|
|
39
|
+
const require = createRequire(import.meta.url);
|
|
40
|
+
const { Pool } = require('pg');
|
|
41
|
+
|
|
42
|
+
const ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..');
|
|
43
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await pool.query('SELECT 1');
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.log(`copy_desk_flags_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}). Set DATABASE_URL to run.`);
|
|
49
|
+
await pool.end().catch(() => {});
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let passed = 0;
|
|
54
|
+
let failed = 0;
|
|
55
|
+
async function test(name, fn) {
|
|
56
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
57
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// --- fixtures -------------------------------------------------------------
|
|
61
|
+
// Minimal stand-ins ONLY if the real tables are absent (throwaway container).
|
|
62
|
+
// Against a fully migrated DB these are no-ops and the real tables are used.
|
|
63
|
+
await pool.query(`
|
|
64
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (version text PRIMARY KEY);
|
|
65
|
+
CREATE TABLE IF NOT EXISTS builders (id bigserial PRIMARY KEY, github_login text, display_name text);
|
|
66
|
+
CREATE TABLE IF NOT EXISTS tasks (id bigserial PRIMARY KEY);
|
|
67
|
+
`);
|
|
68
|
+
|
|
69
|
+
// Two throwaway builders + one task, at ids far above any real row.
|
|
70
|
+
const A = 900000001;
|
|
71
|
+
const B = 900000002;
|
|
72
|
+
const T = 900000003;
|
|
73
|
+
await pool.query('INSERT INTO builders (id, github_login, display_name) VALUES ($1,$2,$3),($4,$5,$6) ON CONFLICT (id) DO NOTHING',
|
|
74
|
+
[A, 'cdtest-a', 'Copy Desk Test A', B, 'cdtest-b', 'Copy Desk Test B']);
|
|
75
|
+
await pool.query('INSERT INTO tasks (id) VALUES ($1) ON CONFLICT (id) DO NOTHING', [T]);
|
|
76
|
+
|
|
77
|
+
// Apply the migration verbatim — this also proves it is idempotent, since a
|
|
78
|
+
// migrated DB has already run it.
|
|
79
|
+
const MIGRATION = fs.readFileSync(path.join(ROOT, 'modules/copy-desk/migrations/copy_desk_001_flags.sql'), 'utf8');
|
|
80
|
+
await pool.query(MIGRATION);
|
|
81
|
+
|
|
82
|
+
// Every row this test writes carries a surface nothing else uses, so cleanup is
|
|
83
|
+
// exact and it can never delete a real flag.
|
|
84
|
+
const SURF = '__copy_desk_test__';
|
|
85
|
+
const clean = () => pool.query('DELETE FROM copy_desk_flags WHERE surface = $1', [SURF]);
|
|
86
|
+
await clean();
|
|
87
|
+
|
|
88
|
+
const REASON = 'Reads like a stack trace, not a sentence a person would say.';
|
|
89
|
+
// `string_id` uses an `in` check, not `??` — an explicit null is the POINT of
|
|
90
|
+
// several cases below, and `??` would silently substitute the default.
|
|
91
|
+
function ins(o = {}) {
|
|
92
|
+
return pool.query(
|
|
93
|
+
`INSERT INTO copy_desk_flags
|
|
94
|
+
(scope, surface, string_id, reason, kind, text_at_flag, file_at_flag, line_at_flag, registry_schema, flagged_by)
|
|
95
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING *`,
|
|
96
|
+
[o.scope ?? 'string', SURF, ('string_id' in o ? o.string_id : 'abc123'), o.reason ?? REASON,
|
|
97
|
+
o.kind ?? 'other', 'Could not save.', 'a.js', 1, 1, o.by ?? A]
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log('copy_desk_flags — the constraints hold in Postgres, not just in JS');
|
|
102
|
+
|
|
103
|
+
await test('the migration is idempotent (re-running is clean)', async () => {
|
|
104
|
+
await pool.query(MIGRATION);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await test('a well-formed flag inserts and defaults to open + unstamped', async () => {
|
|
108
|
+
const { rows } = await ins();
|
|
109
|
+
assert.equal(rows[0].status, 'open');
|
|
110
|
+
assert.equal(rows[0].resolved_at, null);
|
|
111
|
+
assert.equal(rows[0].resolved_by, null);
|
|
112
|
+
assert.ok(rows[0].flagged_at);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await test('THE REASON FLOOR fires in the DB, not only in the route', async () => {
|
|
116
|
+
await assert.rejects(() => ins({ reason: 'bad', string_id: 'r1' }), (e) => e.code === '23514');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await test('a whitespace-only reason is refused (btrim in the CHECK)', async () => {
|
|
120
|
+
await assert.rejects(() => ins({ reason: ' ', string_id: 'r2' }), (e) => e.code === '23514');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await test("scope 'string' with a NULL string_id is refused — it points nowhere", async () => {
|
|
124
|
+
await assert.rejects(() => ins({ scope: 'string', string_id: null }), (e) => e.code === '23514');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await test("scope 'surface' WITH a string_id is refused — it would be silently narrower than it claims", async () => {
|
|
128
|
+
await assert.rejects(() => ins({ scope: 'surface', string_id: 'x1' }), (e) => e.code === '23514');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await test("scope 'surface' with a NULL string_id inserts", async () => {
|
|
132
|
+
const { rows } = await ins({ scope: 'surface', string_id: null, by: B });
|
|
133
|
+
assert.equal(rows[0].scope, 'surface');
|
|
134
|
+
assert.equal(rows[0].string_id, null);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await test('an unknown kind is refused', async () => {
|
|
138
|
+
await assert.rejects(() => ins({ kind: 'vibes', string_id: 'k1' }), (e) => e.code === '23514');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await test('THE PARTIAL UNIQUE INDEX: the same builder on the same target twice → 23505', async () => {
|
|
142
|
+
await ins({ string_id: 'dup-target', by: A });
|
|
143
|
+
await assert.rejects(() => ins({ string_id: 'dup-target', by: A }), (e) => e.code === '23505');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await test('a DIFFERENT builder may flag the same target — that agreement is signal', async () => {
|
|
147
|
+
const { rows } = await ins({ string_id: 'dup-target', by: B });
|
|
148
|
+
assert.ok(rows[0].id);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await test('the guarded close stamps exactly one row', async () => {
|
|
152
|
+
const { rows: [f] } = await ins({ string_id: 'close-me', by: A });
|
|
153
|
+
const upd = await pool.query(
|
|
154
|
+
`UPDATE copy_desk_flags SET status=$2, resolution_note=$3, resolved_task_id=$4, resolved_by=$5, resolved_at=now()
|
|
155
|
+
WHERE id=$1 AND status='open' RETURNING *`,
|
|
156
|
+
[f.id, 'resolved', 'Rewritten in the ship below.', T, B]);
|
|
157
|
+
assert.equal(upd.rowCount, 1);
|
|
158
|
+
assert.equal(upd.rows[0].status, 'resolved');
|
|
159
|
+
assert.equal(String(upd.rows[0].resolved_task_id), String(T));
|
|
160
|
+
assert.ok(upd.rows[0].resolved_at);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await test('A SECOND CLOSE LOSES THE RACE — 0 rows, never an overwrite of the first verdict', async () => {
|
|
164
|
+
const { rows: [f] } = await ins({ string_id: 'race', by: A });
|
|
165
|
+
const first = await pool.query(
|
|
166
|
+
`UPDATE copy_desk_flags SET status='resolved', resolution_note='first verdict stands', resolved_by=$2, resolved_at=now()
|
|
167
|
+
WHERE id=$1 AND status='open' RETURNING *`, [f.id, A]);
|
|
168
|
+
const second = await pool.query(
|
|
169
|
+
`UPDATE copy_desk_flags SET status='dismissed', resolution_note='second must not win', resolved_by=$2, resolved_at=now()
|
|
170
|
+
WHERE id=$1 AND status='open' RETURNING *`, [f.id, B]);
|
|
171
|
+
assert.equal(first.rowCount, 1);
|
|
172
|
+
assert.equal(second.rowCount, 0);
|
|
173
|
+
const { rows } = await pool.query('SELECT resolution_note FROM copy_desk_flags WHERE id=$1', [f.id]);
|
|
174
|
+
assert.equal(rows[0].resolution_note, 'first verdict stands');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await test('after a close, the SAME builder may flag that target again (the index is partial)', async () => {
|
|
178
|
+
const { rows } = await ins({ string_id: 'close-me', by: A });
|
|
179
|
+
assert.ok(rows[0].id, 'a sentence that regresses must be re-flaggable by the person who caught it before');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await test('a closed row with no resolved_at is refused — "resolved by nobody at no time" is impossible', async () => {
|
|
183
|
+
const { rows: [f] } = await ins({ string_id: 'halfclosed', by: A });
|
|
184
|
+
await assert.rejects(
|
|
185
|
+
() => pool.query("UPDATE copy_desk_flags SET status='resolved' WHERE id=$1", [f.id]),
|
|
186
|
+
(e) => e.code === '23514');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await test('THE FK: a nonexistent resolved_task_id raises 23503 — the route\'s named 400', async () => {
|
|
190
|
+
const { rows: [f] } = await ins({ string_id: 'fk-test', by: A });
|
|
191
|
+
await assert.rejects(
|
|
192
|
+
() => pool.query(
|
|
193
|
+
`UPDATE copy_desk_flags SET status='resolved', resolution_note='names a task that does not exist',
|
|
194
|
+
resolved_by=$2, resolved_at=now(), resolved_task_id=999999999 WHERE id=$1`, [f.id, A]),
|
|
195
|
+
(e) => e.code === '23503');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
console.log("\nthe module's own SQL functions (flags.js), not hand-written queries");
|
|
199
|
+
|
|
200
|
+
// Point the doorway's shared pool at THIS connection so flags.js runs its real
|
|
201
|
+
// queries against the test database.
|
|
202
|
+
const api = require(path.join(ROOT, 'src/module-api.js'));
|
|
203
|
+
api.pool.query = (...a) => pool.query(...a);
|
|
204
|
+
const flagsLib = require(path.join(ROOT, 'modules/copy-desk/flags.js'));
|
|
205
|
+
|
|
206
|
+
await test('insertFlag writes and projects the full row shape', async () => {
|
|
207
|
+
const row = await flagsLib.insertFlag({
|
|
208
|
+
scope: 'string', surface: SURF, string_id: 'lib-1', reason: 'Says nothing a reader can act on.',
|
|
209
|
+
kind: 'unclear', text_at_flag: 'Enter.', file_at_flag: 'i.html', line_at_flag: 4, registry_schema: 1,
|
|
210
|
+
}, A);
|
|
211
|
+
assert.equal(row.status, 'open');
|
|
212
|
+
assert.equal(row.kind, 'unclear');
|
|
213
|
+
assert.equal(row.string_id, 'lib-1');
|
|
214
|
+
assert.ok('resolution_note' in row, 'the RETURNING projection matches the read projection');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await test('listFlags resolves the flagger join and sorts open ahead of closed', async () => {
|
|
218
|
+
const rows = await flagsLib.listFlags({ status: 'all', surface: SURF });
|
|
219
|
+
assert.ok(rows.length > 0);
|
|
220
|
+
assert.ok(rows.some((r) => r.flagged_by_login === 'cdtest-a'), 'the builders join resolves a display name');
|
|
221
|
+
const firstClosed = rows.findIndex((r) => r.status !== 'open');
|
|
222
|
+
if (firstClosed !== -1) {
|
|
223
|
+
assert.ok(rows.slice(firstClosed).every((r) => r.status !== 'open'), 'open rows sort ahead of closed ones');
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await test('listFlags honours the status + surface filters', async () => {
|
|
228
|
+
const open = await flagsLib.listFlags({ status: 'open', surface: SURF });
|
|
229
|
+
assert.ok(open.length > 0);
|
|
230
|
+
assert.ok(open.every((r) => r.status === 'open' && r.surface === SURF));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await test('closeFlag returns null on a second call — the route answers 409 from that', async () => {
|
|
234
|
+
const row = await flagsLib.insertFlag({
|
|
235
|
+
scope: 'string', surface: SURF, string_id: 'lib-close', reason: 'Off voice for the front door.',
|
|
236
|
+
kind: 'off-voice', text_at_flag: 'x', file_at_flag: 'i.html', line_at_flag: 9, registry_schema: 1,
|
|
237
|
+
}, B);
|
|
238
|
+
const first = await flagsLib.closeFlag(row.id, { status: 'dismissed', resolution_note: 'Deliberate house style.', resolved_task_id: null }, A);
|
|
239
|
+
assert.ok(first, 'the first close returns the row');
|
|
240
|
+
const second = await flagsLib.closeFlag(row.id, { status: 'resolved', resolution_note: 'must not win', resolved_task_id: null }, B);
|
|
241
|
+
assert.equal(second, null);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// --- cleanup --------------------------------------------------------------
|
|
245
|
+
await clean();
|
|
246
|
+
await pool.query('DELETE FROM builders WHERE id = ANY($1)', [[A, B]]).catch(() => {});
|
|
247
|
+
|
|
248
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
249
|
+
await pool.end();
|
|
250
|
+
process.exit(failed ? 1 : 0);
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -131,7 +131,13 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
131
131
|
// naming the top rank was wrong), then → 'Government' by ADR 0174: a header
|
|
132
132
|
// reading "Archon" over items a Metic can open would misdescribe the ladder.
|
|
133
133
|
assert.deepEqual(nav.map((g) => g.label), ['', 'You', 'Community', 'Docs', 'Government']);
|
|
134
|
-
|
|
134
|
+
// task 1003113 (R02 of goal 1000074): 'copy-desk' joins the unlabeled group,
|
|
135
|
+
// after Tasks. It belongs with the project's own work and not in Docs — it is a
|
|
136
|
+
// QUEUE somebody acts on, not reference material — and not in You or Community,
|
|
137
|
+
// whose subject is a person rather than the product. Deliberate addition to
|
|
138
|
+
// this oracle: the group is pinned exactly so a new tab in the hall's most
|
|
139
|
+
// valuable nav real estate is a reviewed decision, not a drift.
|
|
140
|
+
assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'work', 'copy-desk'],
|
|
135
141
|
'the unlabeled group is the project itself (task 1002425 order)');
|
|
136
142
|
assert.deepEqual(nav[1].items.map((i) => i.id), ['standing', 'profile'], 'You = your own numbers');
|
|
137
143
|
// People (task 1002577): the /scouting directory graduated into the hall.
|
package/tests/module_loader.mjs
CHANGED
|
@@ -173,7 +173,10 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
|
|
|
173
173
|
// configurable roles/permissions module; and agents (ADR 0083 / goal 1000038) the
|
|
174
174
|
// declarative agent registry — the first module to ship default:false, which is
|
|
175
175
|
// why it is DISCOVERED here (discovery is enablement-independent) while staying
|
|
176
|
-
// off in enabledModules().
|
|
176
|
+
// off in enabledModules(). And copy-desk (task 1003113 / R02 of goal 1000074) —
|
|
177
|
+
// the copy half of the design contract: the flag + the artist's queue over
|
|
178
|
+
// user-facing text, default-on because every instance shows a person words.
|
|
179
|
+
// All are discovered; none has validation
|
|
177
180
|
// errors or coreVersion mismatches. Order is the readdir sort.
|
|
178
181
|
//
|
|
179
182
|
// NB this is the CORE package tree: game / art-pipeline / character-anim / demo are
|
|
@@ -197,7 +200,7 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
|
|
|
197
200
|
// That is precisely how the agents regression reached an open PR (task 1002485).
|
|
198
201
|
loader._reset();
|
|
199
202
|
const snap = loader.loaderSnapshot();
|
|
200
|
-
assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'dev-box', 'discord', 'economy', 'government', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'platform-identity', 'provisioning', 'security', 'sessions', 'status-ui']);
|
|
203
|
+
assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'copy-desk', 'dev-box', 'discord', 'economy', 'government', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'platform-identity', 'provisioning', 'security', 'sessions', 'status-ui']);
|
|
201
204
|
assert.deepEqual(snap.errors, []);
|
|
202
205
|
assert.deepEqual(snap.skipped, []);
|
|
203
206
|
loader._reset();
|
|
@@ -15,7 +15,15 @@
|
|
|
15
15
|
|
|
16
16
|
import { strict as assert } from 'node:assert';
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
|
+
// fs/path/ROOT serve the disk-DERIVED guards at the bottom of this file (task
|
|
19
|
+
// 1003132). They must read the tree rather than a list: an enumerated assertion
|
|
20
|
+
// would miss the next unclassified module exactly as the enumerated allowlist
|
|
21
|
+
// missed the one that prompted them.
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import url from 'node:url';
|
|
18
25
|
|
|
26
|
+
const ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..');
|
|
19
27
|
const require = createRequire(import.meta.url);
|
|
20
28
|
const m = require('../scripts/gds/publish-manifest.js');
|
|
21
29
|
const {
|
|
@@ -179,6 +187,80 @@ t('every PROTECTED_GLOB is either topology-excluded or publishable (the methodol
|
|
|
179
187
|
assert.ok(isPublishable(g), `protected surface "${g}" is neither denied nor publishable — re-decide the carve-out`);
|
|
180
188
|
}
|
|
181
189
|
});
|
|
190
|
+
// ── the publish question, asked of the tree itself (task 1003132) ───────────────────
|
|
191
|
+
//
|
|
192
|
+
// THE GAP THESE CLOSE. modules/copy-desk/ was designed, built, tested against a real
|
|
193
|
+
// Postgres, graded, merged and very nearly RELEASED while publishing nothing. Not one
|
|
194
|
+
// of the 429 unit tests, 20+ fitness checks, or the adversarial grader asks the publish
|
|
195
|
+
// question, and the assertions above enumerate representative paths — so a brand-new
|
|
196
|
+
// module key appears in no list and is simply invisible.
|
|
197
|
+
//
|
|
198
|
+
// The failure is worse than "the feature does not ship", because it is ASYMMETRIC: the
|
|
199
|
+
// module's hall page ships anyway (modules/hall-ui/ is one allowlisted subtree), so a
|
|
200
|
+
// release delivers a nav tab and a page whose every API call 404s.
|
|
201
|
+
//
|
|
202
|
+
// Both guards DERIVE from disk. A list here would rot the same way the allowlist did.
|
|
203
|
+
//
|
|
204
|
+
// Modules deliberately not published, on the record. An entry here is a decision
|
|
205
|
+
// someone made, not a gap someone forgot — and every entry needs the task or idea that
|
|
206
|
+
// records the decision, so this can never become a quiet parking lot.
|
|
207
|
+
const KNOWN_UNPUBLISHED_MODULES = new Map([
|
|
208
|
+
// Same gap as copy-desk, found by the guard below the moment it was written. LATENT
|
|
209
|
+
// rather than live only because agents is default:false — the day a released instance
|
|
210
|
+
// enables it, the loader MODULE_NOT_FOUNDs, which is exactly what the provisioning
|
|
211
|
+
// (task 2135) and platform-identity (task 1002235) allowlist entries exist to prevent.
|
|
212
|
+
// Left for goal 1000038 rather than fixed here: deciding what the published core
|
|
213
|
+
// contains for the agents module is that goal's call, not a drive-by from a copy-desk
|
|
214
|
+
// task. Filed as idea 1000797 with the one-line fix.
|
|
215
|
+
['agents', 'idea 1000797'],
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
t('every module on disk is either publishable or deliberately unpublished (a new module cannot be silently invisible)', () => {
|
|
219
|
+
const modulesDir = path.join(ROOT, 'modules');
|
|
220
|
+
const keys = fs.readdirSync(modulesDir, { withFileTypes: true })
|
|
221
|
+
.filter((d) => d.isDirectory() && fs.existsSync(path.join(modulesDir, d.name, 'module.json')))
|
|
222
|
+
.map((d) => d.name);
|
|
223
|
+
assert.ok(keys.length > 5, `sanity: only ${keys.length} modules found on disk — the scan is broken, not the tree`);
|
|
224
|
+
|
|
225
|
+
const unclassified = keys.filter((k) => {
|
|
226
|
+
const manifest = `modules/${k}/module.json`;
|
|
227
|
+
return !isPublishable(manifest) && !matchesDeny(manifest) && !KNOWN_UNPUBLISHED_MODULES.has(k);
|
|
228
|
+
});
|
|
229
|
+
assert.deepEqual(unclassified, [],
|
|
230
|
+
'these modules are in neither the allowlist nor a deny list, so they publish NOTHING. Add each to PUBLISH_ALLOWLIST, deny it on the record, or register it in KNOWN_UNPUBLISHED_MODULES with the task that decided it.');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
t('every KNOWN_UNPUBLISHED_MODULES entry names a real module and a real decision', () => {
|
|
234
|
+
// Two ways this exception list rots: an entry outlives the module (and silently
|
|
235
|
+
// exempts nothing), or an entry outlives the fix (and silently exempts something
|
|
236
|
+
// already fixed). Both make the guard above weaker than it looks.
|
|
237
|
+
for (const [key, ref] of KNOWN_UNPUBLISHED_MODULES) {
|
|
238
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'modules', key, 'module.json')),
|
|
239
|
+
`KNOWN_UNPUBLISHED_MODULES names "${key}", which is not a module on disk — drop the entry`);
|
|
240
|
+
assert.ok(/\b(task|idea)\s+\d+/.test(ref),
|
|
241
|
+
`KNOWN_UNPUBLISHED_MODULES["${key}"] must cite the task or idea that decided it, got "${ref}"`);
|
|
242
|
+
assert.equal(isPublishable(`modules/${key}/module.json`), false,
|
|
243
|
+
`"${key}" is now publishable — delete its KNOWN_UNPUBLISHED_MODULES entry so the guard covers it again`);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
t('a hall page named for a module ships only if that module ships (never a tab whose API 404s)', () => {
|
|
248
|
+
const hallDir = path.join(ROOT, 'modules/hall-ui/public');
|
|
249
|
+
const modulesDir = path.join(ROOT, 'modules');
|
|
250
|
+
if (!fs.existsSync(hallDir)) return;
|
|
251
|
+
const orphans = [];
|
|
252
|
+
for (const f of fs.readdirSync(hallDir)) {
|
|
253
|
+
if (!f.endsWith('.html')) continue;
|
|
254
|
+
const key = f.replace(/\.html$/, '');
|
|
255
|
+
// Only pages that NAME a module are paired — the hall's own pages (work, goals,
|
|
256
|
+
// settings…) have no module and are not the subject of this rule.
|
|
257
|
+
if (!fs.existsSync(path.join(modulesDir, key, 'module.json'))) continue;
|
|
258
|
+
if (!isPublishable(`modules/${key}/module.json`)) orphans.push(f);
|
|
259
|
+
}
|
|
260
|
+
assert.deepEqual(orphans, [],
|
|
261
|
+
'these hall pages would ship to instances whose backing module does not — a nav tab and a page whose every call 404s. Publish the module, or stop publishing the page.');
|
|
262
|
+
});
|
|
263
|
+
|
|
182
264
|
t('the topology subset of the protected surfaces is never publishable', () => {
|
|
183
265
|
for (const g of PROTECTED_TOPOLOGY_EXCLUDE) {
|
|
184
266
|
assert.equal(isPublishable(g), false, `topology surface "${g}" must not publish`);
|