@bongos/core 1.19.189 → 1.19.191
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 +18 -13
- package/modules/government/routes/government.js +34 -6
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +6 -4
- package/scripts/gds/seed-board-room-tasks.js +196 -0
- package/src/module-api.js +5 -1
- package/tests/government_routes.mjs +69 -2
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.191",
|
|
6
|
+
"core_contract": "1.19.191",
|
|
7
|
+
"source_commit": "0a30e8955f1b4904fc8eca19d56228a7dff4b32e",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-17T01:01:06.099Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 309,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1283,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1613,
|
|
20
|
+
"tree_sha256": "234f75ccb67ec7927d6010bbcbd597c9f256e10615cce8d0cbda596850bc0e34",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -2977,7 +2977,7 @@
|
|
|
2977
2977
|
{
|
|
2978
2978
|
"path": "modules/government/routes/government.js",
|
|
2979
2979
|
"mode": "0000644",
|
|
2980
|
-
"sha256": "
|
|
2980
|
+
"sha256": "c229aec4f02fe970f4a3f798d370eef9b080cd0837f906b574362abda57107ad"
|
|
2981
2981
|
},
|
|
2982
2982
|
{
|
|
2983
2983
|
"path": "modules/government/routes/rank-sync.js",
|
|
@@ -4262,12 +4262,12 @@
|
|
|
4262
4262
|
{
|
|
4263
4263
|
"path": "package-lock.json",
|
|
4264
4264
|
"mode": "0000644",
|
|
4265
|
-
"sha256": "
|
|
4265
|
+
"sha256": "ecea40048b1e131835510177f7e19c5cff45b3449c8115b1f1303bde7961a1a4"
|
|
4266
4266
|
},
|
|
4267
4267
|
{
|
|
4268
4268
|
"path": "package.json",
|
|
4269
4269
|
"mode": "0000644",
|
|
4270
|
-
"sha256": "
|
|
4270
|
+
"sha256": "7f2cdbd17679eff46f8f1d19f593e2f75e909c25e536094b650fe47529547ba7"
|
|
4271
4271
|
},
|
|
4272
4272
|
{
|
|
4273
4273
|
"path": "public-docs/index.html",
|
|
@@ -4892,7 +4892,7 @@
|
|
|
4892
4892
|
{
|
|
4893
4893
|
"path": "scripts/gds/fitness.js",
|
|
4894
4894
|
"mode": "0000644",
|
|
4895
|
-
"sha256": "
|
|
4895
|
+
"sha256": "5cc94859033c924884efa60f50f8a1f7810a85382c42d785d62b804181a195f5"
|
|
4896
4896
|
},
|
|
4897
4897
|
{
|
|
4898
4898
|
"path": "scripts/gds/gate-review.js",
|
|
@@ -5284,6 +5284,11 @@
|
|
|
5284
5284
|
"mode": "0000644",
|
|
5285
5285
|
"sha256": "15d4d176f561e408c6116208b295fe5f03fc5e06ca8bb713f023fb1950d1a89a"
|
|
5286
5286
|
},
|
|
5287
|
+
{
|
|
5288
|
+
"path": "scripts/gds/seed-board-room-tasks.js",
|
|
5289
|
+
"mode": "0000644",
|
|
5290
|
+
"sha256": "a079b2422e842deed0bbcd7ad5ae3ab7e1ec4576a26689a80f67e9a56a2411ba"
|
|
5291
|
+
},
|
|
5287
5292
|
{
|
|
5288
5293
|
"path": "scripts/gds/seed-bongos-app-tasks.js",
|
|
5289
5294
|
"mode": "0000644",
|
|
@@ -5872,7 +5877,7 @@
|
|
|
5872
5877
|
{
|
|
5873
5878
|
"path": "src/module-api.js",
|
|
5874
5879
|
"mode": "0000644",
|
|
5875
|
-
"sha256": "
|
|
5880
|
+
"sha256": "1ecbb2895d1ef012f455fe9827f59dade9bfee689b9a86230122a0334c4bff9d"
|
|
5876
5881
|
},
|
|
5877
5882
|
{
|
|
5878
5883
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6852,7 +6857,7 @@
|
|
|
6852
6857
|
{
|
|
6853
6858
|
"path": "tests/government_routes.mjs",
|
|
6854
6859
|
"mode": "0000644",
|
|
6855
|
-
"sha256": "
|
|
6860
|
+
"sha256": "3b2e0ca134737992590132f089d5f93b479ad6cec83a5d2a932412b3edaf4547"
|
|
6856
6861
|
},
|
|
6857
6862
|
{
|
|
6858
6863
|
"path": "tests/government_seed.mjs",
|
|
@@ -185,11 +185,11 @@ module.exports = function buildGovernmentRouter() {
|
|
|
185
185
|
const ARCHON_REASON = 'the Archon rank-rank holds every permission and is immutable — it is the rank that can never be locked out of this tab (ADR 0172)';
|
|
186
186
|
const ASSIGN_REASON = 'a seeded rank-rank is assigned by rank (R95b sync); change the builder\'s rank instead';
|
|
187
187
|
|
|
188
|
-
// ── one-release
|
|
188
|
+
// ── the one-release LEGACY path aliases: /governance/* -> /government/* ────
|
|
189
189
|
// ADR 0174 committed to keeping the pre-rename paths answering for exactly one
|
|
190
190
|
// release. The case this actually protects is narrow but real: a browser that
|
|
191
191
|
// still has the OLD hall page open when the instance upgrades will keep calling
|
|
192
|
-
// /governance/* against the new server. Everything else that
|
|
192
|
+
// the legacy /governance/* paths against the new server. Everything else that
|
|
193
193
|
// paths (the hall bundle, the generated client) ships INSIDE the same core
|
|
194
194
|
// package and moves in lockstep.
|
|
195
195
|
//
|
|
@@ -201,11 +201,39 @@ module.exports = function buildGovernmentRouter() {
|
|
|
201
201
|
// REMOVE THIS BLOCK one release after the rename ships (goal 1000068 follow-up).
|
|
202
202
|
// The `alias` counter in the log line is how we tell whether anything still
|
|
203
203
|
// calls them before deleting.
|
|
204
|
+
//
|
|
205
|
+
// EXPRESS 4 WILDCARD SYNTAX. This is express 4.22 — the wildcard is a bare `*`
|
|
206
|
+
// and the captured tail is req.params[0]. Express 5's named form (`/*rest`) is
|
|
207
|
+
// NOT a wildcard here, it is a literal path segment, so it silently matches
|
|
208
|
+
// nothing and every legacy path 404s. It shipped that way once (task 1003006)
|
|
209
|
+
// and was only caught by curling the live site after the cutover — the module
|
|
210
|
+
// parsed fine, so nothing local failed.
|
|
204
211
|
const LEGACY_PATH_PREFIX = '/governance';
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
212
|
+
// The legacy noun -> the current one, as a NAMED DATA PAIR rather than an
|
|
213
|
+
// inline regex literal. An inline pattern here was silently rewritten by a
|
|
214
|
+
// repo-wide sweep so that both sides matched, turning the translation into an
|
|
215
|
+
// identity function that redirected to a path which does not exist. Naming the
|
|
216
|
+
// pair and asserting they differ makes that class of damage loud.
|
|
217
|
+
//
|
|
218
|
+
// Every line in this block that spells the legacy vocabulary carries the word
|
|
219
|
+
// LEGACY on purpose: that is what the ADR 0174 fitness allowlist keys on, so
|
|
220
|
+
// the shim can name what it is compatible with without excusing the whole file.
|
|
221
|
+
const LEGACY_SEGMENT = 'roles'; // legacy
|
|
222
|
+
const CURRENT_SEGMENT = 'ranks';
|
|
223
|
+
if (LEGACY_SEGMENT === CURRENT_SEGMENT) {
|
|
224
|
+
throw new Error('government legacy alias: the rename pair collapsed to an identity — a sweep has eaten it');
|
|
225
|
+
}
|
|
226
|
+
router.all(`${LEGACY_PATH_PREFIX}/*`, (req, res, next) => {
|
|
227
|
+
const tail = req.params[0] || ''; // express 4: bare * captures here
|
|
228
|
+
if (!tail) return next();
|
|
229
|
+
// Translate the leading segment, keep everything after it, keep the query.
|
|
230
|
+
const translated = tail === LEGACY_SEGMENT || tail.startsWith(`${LEGACY_SEGMENT}/`)
|
|
231
|
+
? CURRENT_SEGMENT + tail.slice(LEGACY_SEGMENT.length)
|
|
232
|
+
: tail;
|
|
233
|
+
const qs = req.originalUrl.includes('?') ? req.originalUrl.slice(req.originalUrl.indexOf('?')) : '';
|
|
234
|
+
// req.baseUrl is the router's MOUNT point (/api/gds). Redirecting without it
|
|
235
|
+
// sends the caller to /government/... at the site root, which 404s.
|
|
236
|
+
const target = `${req.baseUrl}/government/${translated}${qs}`;
|
|
209
237
|
console.warn(`[government] legacy path hit: ${req.method} ${req.originalUrl} -> ${target} (ADR 0174 alias; remove one release after the rename)`);
|
|
210
238
|
return res.redirect(308, target);
|
|
211
239
|
});
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.191",
|
|
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.191",
|
|
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.191",
|
|
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/fitness.js
CHANGED
|
@@ -1690,11 +1690,13 @@ const VOCAB_ALLOW = [
|
|
|
1690
1690
|
},
|
|
1691
1691
|
{
|
|
1692
1692
|
file: 'modules/government/routes/government.js',
|
|
1693
|
-
match: '
|
|
1693
|
+
match: 'legacy',
|
|
1694
1694
|
why: 'the one-release 308 alias from the pre-rename route paths (ADR 0174). A '
|
|
1695
|
-
+ 'compatibility shim has to name the thing it is compatible with.
|
|
1696
|
-
+ '
|
|
1697
|
-
+ '
|
|
1695
|
+
+ 'compatibility shim has to name the thing it is compatible with. The match is '
|
|
1696
|
+
+ 'the word LEGACY, and the shim block is written so that every line spelling the '
|
|
1697
|
+
+ 'old vocabulary carries it — so the exemption is scoped to the shim rather than '
|
|
1698
|
+
+ 'to the whole file, and a stray "role" elsewhere in this file still reds the '
|
|
1699
|
+
+ 'build. Delete with the alias block one release after the rename ships.',
|
|
1698
1700
|
},
|
|
1699
1701
|
{
|
|
1700
1702
|
file: 'modules/government/module.json',
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Seed the tasks for goal 1000069 "Governance & voting — the Board Room" (BONGOS-V1).
|
|
2
|
+
// Source of truth: docs/specs/bongos-v1-board-room.md (status: final).
|
|
3
|
+
// Idempotent on (source, source_ref). Re-running resolves to the existing rows.
|
|
4
|
+
//
|
|
5
|
+
// Run: node scripts/gds/seed-board-room-tasks.js
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const sess = require(os.homedir() + '/.config/cloudbongos/gds-session.json');
|
|
8
|
+
const BASE = sess.api_base.replace(/\/$/, '');
|
|
9
|
+
const TOKEN = sess.token;
|
|
10
|
+
const VER = 'BONGOS-V1';
|
|
11
|
+
const GOAL = 1000069;
|
|
12
|
+
const SRC = 'planning-session';
|
|
13
|
+
const H = { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' };
|
|
14
|
+
|
|
15
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
16
|
+
async function api(method, path, body, _tries = 0) {
|
|
17
|
+
await sleep(500); // base throttle to stay under the per-IP rate limit
|
|
18
|
+
const res = await fetch(`${BASE}/api/gds${path}`, {
|
|
19
|
+
method, headers: H, body: body ? JSON.stringify(body) : undefined,
|
|
20
|
+
});
|
|
21
|
+
const text = await res.text();
|
|
22
|
+
let json; try { json = JSON.parse(text); } catch { json = { _raw: text.slice(0, 300) }; }
|
|
23
|
+
if (res.status === 429 && _tries < 8) {
|
|
24
|
+
const wait = ((json.error && json.error.details && json.error.details.retry_after_seconds) || 30) + 1;
|
|
25
|
+
console.log(` …rate-limited, waiting ${wait}s`);
|
|
26
|
+
await sleep(wait * 1000);
|
|
27
|
+
return api(method, path, body, _tries + 1);
|
|
28
|
+
}
|
|
29
|
+
return { status: res.status, ok: res.ok, json };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Criterion slugs (position-independent — see the spec's Criteria table).
|
|
33
|
+
const C1 = 'board-full-idea-ratification';
|
|
34
|
+
const C2 = 'rejection-returns-with-reason';
|
|
35
|
+
const C3 = 'who-governs-is-config';
|
|
36
|
+
const C4 = 'timed-sitting-absence-never-blocks';
|
|
37
|
+
const C5 = 'board-room-and-charter-library';
|
|
38
|
+
const C6 = 'discord-notification-mirror';
|
|
39
|
+
|
|
40
|
+
const SPEC = 'See docs/specs/bongos-v1-board-room.md (status: final).';
|
|
41
|
+
|
|
42
|
+
const TASKS = [
|
|
43
|
+
// ---- Phase A — decide + foundation (P5) ----
|
|
44
|
+
{ ref:'R01', kind:'decision', disc:'engineer', prio:5, est:150, crit:[C1,C3,C4], deps:[],
|
|
45
|
+
title:'R01 · ADR: the Board Room government model',
|
|
46
|
+
desc:`Record the government model as one decision so the schema has something to encode.\n\nMUST STATE: the board's TWO subjects (a Full Idea, and a change to the constitution itself) and why nothing else is votable; membership as a configured predicate resolved per request and uncached; the timed SITTING (absent votes are nulled, not blocking) and why that replaces quorum — the clock IS the deadlock exit; CONSENT rather than majority, with silence as consent and one reasoned objection returning the idea; a zero-vote close PASSING; the OPEN ballot; Discord as an outbound notification mirror that casts nothing; and the amendment rule — a constitutional change is decided under whatever rule the CURRENT constitution names, so flipping out of monarchy is a legal monarchy act.\n\nAlso record the two owner decisions taken against recommendation with their trade-offs intact: vote-karma (yes-clicking bias, accepted because item volume is too low to farm) and automatic window-open on grade pass (author loses timing control; concurrent windows become normal).\n\nGrounding: idea 1000738, idea 1000754, the Six Variables of Governance synthesis, ADR 0151 (permissions-as-atoms), ADR 0157, ADR 0162 (verdicts advisory), ADR 0174 (government/ranks/roles). ${SPEC}` },
|
|
47
|
+
|
|
48
|
+
{ ref:'R02', kind:'decision', disc:'engineer', prio:5, est:90, crit:[C1], deps:['R01'],
|
|
49
|
+
title:'R02 · The pass-event contract the ideator credit lane consumes',
|
|
50
|
+
desc:`Agree the pass-event shape with goal 1000067 (IC5, task 1002999) BEFORE the emitter is built, so no parallel vote is invented — ADR 0172 (per-craft compensation) is explicit that stream 3 "consumes the Board Room's pass event; invents no parallel vote".\n\nDone-when: payload fields, the idempotency key, and once-per-idea semantics are written down and referenced by both goals. Stream 3 pays max(30, round(completeness% x 100)) once per idea, reason 'idea.credit.governance_pass'. economy stays OUT of this goal's scope — we emit, 1000067 pays. Deps R01. ${SPEC}` },
|
|
51
|
+
|
|
52
|
+
{ ref:'R03', kind:'feature', disc:'engineer', prio:5, est:210, crit:[C1,C2,C4], deps:['R01'],
|
|
53
|
+
title:'R03 · Schema: board items, votes, and rejections',
|
|
54
|
+
desc:`Module-owned migration under modules/government/. NOTE the fitness rule derives a module migration's legal table namespace from the DIRECTORY NAME (this is what forced tasks 1003004+1003005 to land together), so tables are government_*.\n\nMUST include subject_type ('full_idea' | 'constitutional_amendment') FROM DAY ONE — cheap now, expensive to bolt on. A vote row carries voter, direction, timestamp, and for an objection the free-text reason plus exactly one of the five Full Idea section keys. UNIQUE(item, voter) on the peer_votes precedent (migration 058). Item carries opened_at, closes_at, closed_at, outcome.\n\nRe-read modules/ideas/templates.js for the five section keys — they are the owner's own wording and must not be re-invented. Deps R01. ${SPEC}` },
|
|
55
|
+
|
|
56
|
+
{ ref:'R04', kind:'feature', disc:'engineer', prio:5, est:150, crit:[C1,C3], deps:['R03'],
|
|
57
|
+
title:'R04 · Permission catalog + grant migration for the board keys',
|
|
58
|
+
desc:`Add board.item.open, board.vote.cast, board.config.manage following modules/government/catalog.js's five-step recipe EXACTLY. All three are operational (system:false) — creating or casting confers no authority on anyone; compare the version.create comment which argues this against a symmetry reading.\n\nThe grant migration is LOAD-BEARING not bookkeeping: an already-migrated DB never learns a key added only in code, and the new route then 403s everyone. Copy governance_006_version_create.sql's shape verbatim — one INSERT...SELECT '<rank>', unnest(ARRAY[...]) block PER RANK, because the drift guard tests/governance_seed.mjs scans for that literal form and a combined unnest parses as zero grants.\n\nPin every write route in src/bongos/route-rank-check.js. WATCH THE DEPLOY BOUNDARY: the grant rows and the code constant must land in the SAME deploy (ADR 0174 §Consequences) or the check fails closed. Deps R03. ${SPEC}` },
|
|
59
|
+
|
|
60
|
+
// ---- Phase B — the rule engine (P5 spine, P4 tail) ----
|
|
61
|
+
{ ref:'R05', kind:'feature', disc:'engineer', prio:5, est:180, crit:[C3], deps:['R04'],
|
|
62
|
+
title:'R05 · The constitution: membership, pass rule, and window as config',
|
|
63
|
+
desc:`config/government.json + config/government.neutral.json gain a board block: membership predicate, pass_rule, window_minutes, close_early_on_full_turnout. Platform default and this instance's day-one value are MONARCHY — membership 'rank:archon', pass_rule 'first_ratifier', no clock — expressed as configuration on the real machinery, never as a separate code path.\n\nThe recommended starting point documented alongside it is: all Archons, one vote each, consent, 90-minute window. Deps R04. ${SPEC}` },
|
|
64
|
+
|
|
65
|
+
{ ref:'R06', kind:'feature', disc:'engineer', prio:5, est:150, crit:[C3], deps:['R05'],
|
|
66
|
+
title:'R06 · Board membership resolver — per request, uncached',
|
|
67
|
+
desc:`Resolve the configured predicate to a live member list on every request with NO caching, preserving ADR 0016's instant-demotion property the permission resolver already guarantees.\n\nMembership derives from RANK, never from role: PATCH /me/disciplines is deliberately ungated ("a builder owns their own profile", src/bongos/routes/me.js:311), so a role-derived franchise would be one-request privilege escalation. The spec records why the role-chamber model was rejected. Deps R05. ${SPEC}` },
|
|
68
|
+
|
|
69
|
+
{ ref:'R07', kind:'feature', disc:'engineer', prio:5, est:150, crit:[C1,C4], deps:['R06'],
|
|
70
|
+
title:'R07 · A window opens automatically when a Full Idea passes the grade',
|
|
71
|
+
desc:`The completeness grade is the FLOOR, the vote is the MERIT. A Full Idea crossing the pass bar (60, ADR 0172 per-craft) automatically opens a voting window; a sub-bar idea never reaches the board and instead returns to its author via R13.\n\nConcurrent windows are normal and expected — a direct consequence of automatic opening at a 90-minute default. Reuse modules/ideas/completeness.js; do not re-implement scoring. Deps R06. ${SPEC}` },
|
|
72
|
+
|
|
73
|
+
{ ref:'R08', kind:'feature', disc:'engineer', prio:5, est:210, crit:[C1,C4], deps:['R06'],
|
|
74
|
+
title:'R08 · Cast a vote: open ballot, anti-self, live re-check, karma',
|
|
75
|
+
desc:`POST a vote gated on board.vote.cast. HARD RULES: an author may NOT vote on their own item (server-side, the peer_votes anti-self precedent) — this is the bright line that keeps ratification from being ceremonial; membership is re-checked LIVE on every write; the write is idempotent on (item, voter).\n\nOpen ballot: member, direction and timestamp are visible to all members, which is what makes the vote-karma auditable. Award the voter karma for a vote cast IN-WINDOW (proposed +1 — surface the constant for confirmation). The Full Idea's author earns karma on ratification, mirroring KARMA_RATIFY_DELTA (+3, tuned down from +5 in V3.R28 — do NOT copy the stale +5 still printed in planning-session/SKILL.md and openapi.json). Deps R06. ${SPEC}` },
|
|
76
|
+
|
|
77
|
+
{ ref:'R09', kind:'feature', disc:'engineer', prio:5, est:120, crit:[C2], deps:['R03'],
|
|
78
|
+
title:'R09 · Rejection payload: a reason plus one of the five sections',
|
|
79
|
+
desc:`An objection carries free-text plus EXACTLY ONE of the five Full Idea section keys as the cause. This is the mechanism that makes the hall the only possible voting surface — a Discord reaction cannot express it, so reaction-voting would make every rejection feedback-free.\n\nA sub-bar completeness grade flags its weakest section automatically, using the per-field scores completeness.js already computes. Section keys come from modules/ideas/templates.js. Deps R03. ${SPEC}` },
|
|
80
|
+
|
|
81
|
+
{ ref:'R10', kind:'feature', disc:'engineer', prio:5, est:210, crit:[C4], deps:['R08','R09'],
|
|
82
|
+
title:'R10 · Close + tally: consent, nulled absences, zero-vote pass',
|
|
83
|
+
desc:`Closing rules, all declared rather than emergent: absent members are NULLED — dropped from the denominator, never counted as a no, so absence can never block; silence is consent; ONE reasoned objection returns the idea under C2; a window closing with ZERO votes PASSES (the idea already cleared the grade gate, and absence never blocks); and the window closes EARLY the moment every member has voted, so latency is min(full turnout, window).\n\nThe monarchy pass_rule 'first_ratifier' resolves through this same path. Deps R08,R09. ${SPEC}` },
|
|
84
|
+
|
|
85
|
+
{ ref:'R12', kind:'feature', disc:'engineer', prio:5, est:120, crit:[C1], deps:['R10'],
|
|
86
|
+
title:'R12 · Emit the pass event exactly once per ratified idea',
|
|
87
|
+
desc:`Fire R02's agreed contract on a passing close. Idempotent — a replay, a re-close, or a resubmission that passes again must never double-fire. This is the event goal 1000067 (IC5, task 1002999) is BLOCKED on; shipping it unblocks stream 3. Deps R10. ${SPEC}` },
|
|
88
|
+
|
|
89
|
+
{ ref:'R11', kind:'feature', disc:'engineer', prio:4, est:150, crit:[C4], deps:['R10'],
|
|
90
|
+
title:'R11 · The window expiry poller',
|
|
91
|
+
desc:`A window closes on time with nobody touching it. Nothing in-tree expires a vote today; the nearest precedents are the 7-day lagged peer-vote tally and the ~5-minute publish reconciler — follow the reconciler's per-sweep disposition-logging habit so a stuck window is visible rather than silent. Must handle concurrent windows. Deps R10. ${SPEC}` },
|
|
92
|
+
|
|
93
|
+
{ ref:'R13', kind:'feature', disc:'engineer', prio:4, est:180, crit:[C2], deps:['R09','R10'],
|
|
94
|
+
title:'R13 · Return-to-originator, and resubmission',
|
|
95
|
+
desc:`A vote fail OR a sub-bar grade sends the idea back to its ORIGINATOR carrying its feedback — it never dies and never stalls. The author revises and resubmits, which re-opens a window.\n\nBecause completeness.js scores the five fields per-field, a resubmission can be checked against the flagged section — surface whether the flagged section actually changed. Resubmission is unbounded in v1 (the grade gate already bounds spam) and every past rejection stays visible on the item history. Deps R09,R10. ${SPEC}` },
|
|
96
|
+
|
|
97
|
+
{ ref:'R14', kind:'feature', disc:'engineer', prio:4, est:180, crit:[C3], deps:['R10'],
|
|
98
|
+
title:'R14 · Constitutional amendments as the board\'s second subject',
|
|
99
|
+
desc:`Propose a change to the constitution, then ratify it under whatever rule the CURRENT constitution names. Under monarchy that resolves to "the Archon ratifies", so flipping OUT of monarchy is a legal monarchy act; every change after that belongs to the consent board. This is how a constitution's own amendment procedure normally works, and it is why no bootstrap special case is needed.\n\nThis is the owner's answer to the loophole that motivated the whole feature — the rule that constrains the owner can no longer be weakened by the owner alone. Deps R10. ${SPEC}` },
|
|
100
|
+
|
|
101
|
+
// ---- Phase C — the hall (P4) ----
|
|
102
|
+
{ ref:'R15', kind:'feature', disc:'engineer', prio:4, est:150, crit:[C5], deps:['R05'],
|
|
103
|
+
title:'R15 · Government page IA: Constitution / Board Room / Ranks & Permissions',
|
|
104
|
+
desc:`Government becomes the page that answers "how is this project governed", in three sections: Constitution (what we run + the charter library), Board Room (open items, your vote, the clock), and Ranks & Permissions (today's administration surface, unchanged).\n\nThe tab was renamed from Governance under task 1003007 — build on that, do not re-litigate the naming. Deps R05. ${SPEC}` },
|
|
105
|
+
|
|
106
|
+
{ ref:'R16', kind:'feature', disc:'engineer', prio:4, est:240, crit:[C1,C4,C5], deps:['R08','R15'],
|
|
107
|
+
title:'R16 · The Board Room: open items, the clock, the tally',
|
|
108
|
+
desc:`Open items with their remaining time, your own vote state, a LIVE open-ballot tally (who voted which way and when), and the closed history with outcomes. Must read sensibly with several concurrent windows, which are normal at a 90-minute default with automatic opening. Deps R08,R15. ${SPEC}` },
|
|
109
|
+
|
|
110
|
+
{ ref:'R17', kind:'feature', disc:'engineer', prio:4, est:180, crit:[C2,C5], deps:['R09','R16'],
|
|
111
|
+
title:'R17 · The vote UI: cast, reason box, five-section picker',
|
|
112
|
+
desc:`Cast a vote, and for an objection write the reason and pick exactly one of the five Full Idea sections. The full idea and its five sections must be in front of the voter while they decide — that context is the entire reason voting happens in the hall rather than on a Discord reaction. Deps R09,R16. ${SPEC}` },
|
|
113
|
+
|
|
114
|
+
{ ref:'R18', kind:'feature', disc:'engineer', prio:4, est:150, crit:[C3,C5], deps:['R14','R15'],
|
|
115
|
+
title:'R18 · The Constitution section: what we run, and how it changed',
|
|
116
|
+
desc:`Show the active constitution in plain language — who may vote, how an item passes, how long a window runs — plus the full change history, so a switch can never be flipped quietly. Deps R14,R15. ${SPEC}` },
|
|
117
|
+
|
|
118
|
+
{ ref:'R19', kind:'feature', disc:'engineer', prio:4, est:210, crit:[C5], deps:['R15'],
|
|
119
|
+
title:'R19 · The charter library: seven constitutions and the ladder',
|
|
120
|
+
desc:`Present all seven government structures (Monarchy/BDFL, Timocratic Republic, Direct Democracy, Presidential, Cooperative, Federation, Foundation), the three trajectories (Locked, Laddered, Scheduled), and the developmental ladder's thresholds (~9 named executive, ~25, ~50 written charter + council, ~150 adjudication separates / federate beyond).\n\nMark which are SELECTABLE TODAY versus documented-for-later, so nobody is misled. Name which is recommended at this project's CURRENT builder count. This is the half of the surface that earns its keep at two builders — the owner's stated purpose is getting a team up to speed before deciding when to flip the switch.\n\nSource: the Six Variables of Governance synthesis (Parts III and IV). Deps R15. ${SPEC}` },
|
|
121
|
+
|
|
122
|
+
// ---- Phase D — the mirror (P3) ----
|
|
123
|
+
{ ref:'R20', kind:'feature', disc:'engineer', prio:3, est:90, crit:[C6], deps:['R01'],
|
|
124
|
+
title:'R20 · The board channel, with per-rank view access',
|
|
125
|
+
desc:`Add the board channel to docs/discord/channels.json with per-rank view access (ADR 0037 — the bot creates/edits channels, topics and access from that config file). Independent of the whole rule engine, so this is good early parallel work for a second builder. Deps R01. ${SPEC}` },
|
|
126
|
+
|
|
127
|
+
{ ref:'R21', kind:'feature', disc:'engineer', prio:3, est:180, crit:[C6], deps:['R07','R20'],
|
|
128
|
+
title:'R21 · Announce a window: mention the members, deep-link the hall',
|
|
129
|
+
desc:`On window open, post to the board channel mentioning the members whose vote is wanted, carrying a deep link straight to that item in the hall. The mention IS the "vote within X" notice, and Discord's own mobile app IS the mobile push — no new surface is needed and the bot has no DM capability today (verified).\n\nFollow modules/discord/approval-broadcast + discord-approvals.js for the BROADCAST shape only. Discord is outbound-only here: it reads nothing inbound and casts no vote. Deps R07,R20. ${SPEC}` },
|
|
130
|
+
|
|
131
|
+
{ ref:'R22', kind:'feature', disc:'engineer', prio:3, est:120, crit:[C6], deps:['R10','R21'],
|
|
132
|
+
title:'R22 · Mirror state: closed, and the outcome',
|
|
133
|
+
desc:`Edit the announcement in place as the item closes and resolves, so the channel never shows a stale open window. The item's state lives in the DATABASE — Discord is a mirror of it, never a source. Deps R10,R21. ${SPEC}` },
|
|
134
|
+
|
|
135
|
+
// ---- Phase E — proofs (P3) ----
|
|
136
|
+
{ ref:'R23', kind:'verify', disc:'engineer', prio:3, est:150, crit:[C1,C3], deps:['R12','R17','R22'],
|
|
137
|
+
title:'R23 · PROOF: day-one monarchy, end to end',
|
|
138
|
+
desc:`On the live configuration: a Full Idea passes the completeness grade, a window opens automatically, the Archon ratifies in the hall, and the pass event fires exactly once with the payload IC5 expects. Proves C1 and the day-one half of C3 — and proves the board path really is the code path monarchy runs through. Deps R12,R17,R22. ${SPEC}` },
|
|
139
|
+
|
|
140
|
+
{ ref:'R24', kind:'verify', disc:'engineer', prio:3, est:120, crit:[C2], deps:['R13','R17'],
|
|
141
|
+
title:'R24 · PROOF: a rejection round-trip',
|
|
142
|
+
desc:`Reject a Full Idea with a reason and a flagged section, confirm the author sees both, revise that section, resubmit, and pass. Also confirm the sub-bar-grade path flags the weakest section automatically. Proves C2 end to end. Deps R13,R17. ${SPEC}` },
|
|
143
|
+
|
|
144
|
+
{ ref:'R25', kind:'verify', disc:'engineer', prio:3, est:180, crit:[C3,C4], deps:['R11','R14','R18'],
|
|
145
|
+
title:'R25 · PROOF: flip the switch, then run a real sitting',
|
|
146
|
+
desc:`Change the constitution from monarchy to the consent board BY RATIFICATION under the monarchy rule, then run a timed sitting through to expiry — verifying an absent member's vote is nulled rather than blocking, that a zero-vote close passes, and that a window closes early on full turnout. The single most important proof in the goal: it is the only thing that shows the switch actually works before you need it. Deps R11,R14,R18. ${SPEC}` },
|
|
147
|
+
|
|
148
|
+
{ ref:'R26', kind:'verify', disc:'engineer', prio:3, est:90, crit:[C5], deps:['R19'],
|
|
149
|
+
title:'R26 · PROOF: the charter library reads right at this headcount',
|
|
150
|
+
desc:`Confirm the library's recommendation matches this project's actual builder count, that selectable-today is honestly marked, and that a newcomer can answer "what are we running, and what could we run" from the page alone. Deps R19. ${SPEC}` },
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
async function main() {
|
|
154
|
+
const idByRef = {};
|
|
155
|
+
console.log(`Seeding ${TASKS.length} tasks → ${VER} / goal ${GOAL}\n`);
|
|
156
|
+
|
|
157
|
+
for (const t of TASKS) {
|
|
158
|
+
const source_ref = `board-room-${t.ref}`;
|
|
159
|
+
const body = {
|
|
160
|
+
version_id: VER, goal_id: GOAL, source: SRC, source_ref,
|
|
161
|
+
title: t.title, description: t.desc,
|
|
162
|
+
kind: t.kind, discipline: t.disc, status: 'backlog',
|
|
163
|
+
priority: t.prio, est_minutes: t.est,
|
|
164
|
+
criterion_ids: t.crit,
|
|
165
|
+
};
|
|
166
|
+
const r = await api('POST', '/tasks', body);
|
|
167
|
+
if (r.ok && r.json.task) {
|
|
168
|
+
idByRef[t.ref] = r.json.task.id;
|
|
169
|
+
console.log(` ${t.ref} → ${r.json.task.id} ${t.title.slice(0, 62)}`);
|
|
170
|
+
} else if (r.status === 409 && r.json.task) {
|
|
171
|
+
idByRef[t.ref] = r.json.task.id;
|
|
172
|
+
console.log(` ${t.ref} → ${r.json.task.id} (already seeded)`);
|
|
173
|
+
} else {
|
|
174
|
+
console.error(` ${t.ref} FAILED ${r.status}`, JSON.stringify(r.json).slice(0, 400));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log('\nWiring dependencies…');
|
|
179
|
+
let edges = 0;
|
|
180
|
+
for (const t of TASKS) {
|
|
181
|
+
const id = idByRef[t.ref];
|
|
182
|
+
if (!id || !t.deps.length) continue;
|
|
183
|
+
for (const ref of t.deps) {
|
|
184
|
+
const dep = idByRef[ref];
|
|
185
|
+
if (!dep) { console.error(` ${t.ref} dep ${ref} unresolved`); continue; }
|
|
186
|
+
const r = await api('POST', `/tasks/${id}/dependencies`, { depends_on_task_id: dep });
|
|
187
|
+
if (r.ok || r.status === 409) { edges++; console.log(` ${t.ref} ← ${ref}`); }
|
|
188
|
+
else console.error(` ${t.ref} ← ${ref} FAILED ${r.status}`, JSON.stringify(r.json).slice(0, 300));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log(`\nDone. ${Object.keys(idByRef).length}/${TASKS.length} tasks, ${edges} dependency edges.`);
|
|
193
|
+
console.log('Zero-dep layer (promote to ready): R01');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
main().catch((e) => { console.error(e); process.exit(1); });
|
package/src/module-api.js
CHANGED
|
@@ -805,8 +805,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
805
805
|
// landed since 1.19.187 with no explicit bump. run 31978931152. (task 1002620)
|
|
806
806
|
// 1.19.189 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
807
807
|
// landed since 1.19.188 with no explicit bump. run 31983108640. (task 1002620)
|
|
808
|
+
// 1.19.190 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
809
|
+
// landed since 1.19.189 with no explicit bump. run 31983360426. (task 1002620)
|
|
810
|
+
// 1.19.191 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
811
|
+
// landed since 1.19.190 with no explicit bump. run 31983663234. (task 1002620)
|
|
808
812
|
// ---------------------------------------------------------------------------
|
|
809
|
-
const CORE_VERSION = '1.19.
|
|
813
|
+
const CORE_VERSION = '1.19.191'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
810
814
|
|
|
811
815
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
812
816
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -151,16 +151,19 @@ after(() => {
|
|
|
151
151
|
seams._reset();
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
-
async function call(method, p, body) {
|
|
154
|
+
async function call(method, p, body, opts = {}) {
|
|
155
155
|
calls.length = 0;
|
|
156
156
|
const res = await fetch(`${BASE}${p}`, {
|
|
157
157
|
method,
|
|
158
158
|
headers: body === undefined ? {} : { 'content-type': 'application/json' },
|
|
159
159
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
160
|
+
// `redirect: 'manual'` lets a caller assert the 308 itself rather than
|
|
161
|
+
// silently following it — required by the legacy-alias tests at the bottom.
|
|
162
|
+
...(opts.redirect ? { redirect: opts.redirect } : {}),
|
|
160
163
|
});
|
|
161
164
|
let json = null;
|
|
162
165
|
try { json = await res.json(); } catch { /* no body */ }
|
|
163
|
-
return { status: res.status, json };
|
|
166
|
+
return { status: res.status, json, headers: res.headers };
|
|
164
167
|
}
|
|
165
168
|
|
|
166
169
|
// --- the deny wall -----------------------------------------------------------
|
|
@@ -392,3 +395,67 @@ test('DELETE /governance/assignments validates the builder id param', async () =
|
|
|
392
395
|
const r = await call('DELETE', '/government/assignments/not-a-number/triage-helper');
|
|
393
396
|
assert.equal(r.status, 400);
|
|
394
397
|
});
|
|
398
|
+
|
|
399
|
+
// ── the one-release /governance/* compatibility alias (ADR 0174) ──────────────
|
|
400
|
+
//
|
|
401
|
+
// This block exists because the alias shipped BROKEN and nothing caught it.
|
|
402
|
+
// Three independent defects, none visible to a parse check or a unit test that
|
|
403
|
+
// only imports the module:
|
|
404
|
+
// 1. it was written with express 5's named-wildcard syntax (`/*rest`) on an
|
|
405
|
+
// express 4.22 router, where that is a literal segment — so it matched
|
|
406
|
+
// nothing and every legacy path 404'd;
|
|
407
|
+
// 2. the redirect omitted req.baseUrl, sending callers to /government/... at
|
|
408
|
+
// the site root instead of under the /api/gds mount;
|
|
409
|
+
// 3. a repo-wide roles->ranks sweep rewrote the SOURCE side of the
|
|
410
|
+
// /^\/roles/ -> '/ranks' translation, turning it into an identity that
|
|
411
|
+
// redirected to a path which does not exist.
|
|
412
|
+
//
|
|
413
|
+
// All three only surfaced when the running site was curled after the cutover.
|
|
414
|
+
// These tests drive a real router so they cannot repeat that.
|
|
415
|
+
|
|
416
|
+
// A SECOND server, mounted at /api/gds exactly as production does. The main
|
|
417
|
+
// harness above mounts at the root, so it cannot see a missing req.baseUrl —
|
|
418
|
+
// which was one of the three real defects. Mounting like production is the
|
|
419
|
+
// whole point of these three tests.
|
|
420
|
+
const aliasApp = express();
|
|
421
|
+
aliasApp.use(express.json());
|
|
422
|
+
aliasApp.use(attachFail);
|
|
423
|
+
aliasApp.use('/api/gds', buildGovernmentRouter());
|
|
424
|
+
const aliasServer = aliasApp.listen(0);
|
|
425
|
+
await new Promise((r) => aliasServer.once('listening', r));
|
|
426
|
+
const ALIAS_BASE = `http://127.0.0.1:${aliasServer.address().port}`;
|
|
427
|
+
after(() => aliasServer.close());
|
|
428
|
+
|
|
429
|
+
const hop = (method, p) =>
|
|
430
|
+
fetch(`${ALIAS_BASE}${p}`, { method, redirect: 'manual' });
|
|
431
|
+
|
|
432
|
+
test('the legacy /governance/* alias redirects, under the mount, with the noun translated', async () => {
|
|
433
|
+
const cases = [
|
|
434
|
+
['GET', '/api/gds/governance/roles', '/api/gds/government/ranks'],
|
|
435
|
+
['GET', '/api/gds/governance/permissions', '/api/gds/government/permissions'],
|
|
436
|
+
['GET', '/api/gds/governance/assignments', '/api/gds/government/assignments'],
|
|
437
|
+
['PUT', '/api/gds/governance/roles/metic/permissions/idea.triage',
|
|
438
|
+
'/api/gds/government/ranks/metic/permissions/idea.triage'],
|
|
439
|
+
['POST', '/api/gds/governance/roles/metic/reset', '/api/gds/government/ranks/metic/reset'],
|
|
440
|
+
];
|
|
441
|
+
for (const [method, from, expected] of cases) {
|
|
442
|
+
const r = await hop(method, from);
|
|
443
|
+
assert.equal(r.status, 308, `${method} ${from} must 308 — a 404 means the wildcard is not matching`);
|
|
444
|
+
assert.equal(r.headers.get('location'), expected,
|
|
445
|
+
`${method} ${from} must keep the /api/gds mount AND translate roles→ranks`);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('the alias preserves the query string', async () => {
|
|
450
|
+
const r = await hop('GET', '/api/gds/governance/roles?x=1&y=2');
|
|
451
|
+
assert.equal(r.headers.get('location'), '/api/gds/government/ranks?x=1&y=2');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test('the alias carries no gate of its own — the followed request re-runs the real one', async () => {
|
|
455
|
+
// A 308 preserves method and body, so the wall is enforced on the FOLLOWED
|
|
456
|
+
// request by the real route. The alias must never become a way around it.
|
|
457
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules', 'government', 'routes', 'government.js'), 'utf8');
|
|
458
|
+
const block = src.slice(src.indexOf('LEGACY_PATH_PREFIX'), src.indexOf('res.redirect(308'));
|
|
459
|
+
assert.ok(!/requirePermission|requireBuilder/.test(block),
|
|
460
|
+
'the alias must not carry its own gate — it redirects, and the real route gates the followed request');
|
|
461
|
+
});
|