@bongos/core 1.19.614 → 1.19.615

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.
@@ -3,9 +3,9 @@
3
3
  "generator": "scripts/gds/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 1729,
7
- "unique_ids": 1497,
8
- "certain": 1729,
6
+ "strings": 1731,
7
+ "unique_ids": 1499,
8
+ "certain": 1731,
9
9
  "heuristic": 0,
10
10
  "duplicate_ids": 163
11
11
  },
@@ -13,7 +13,7 @@
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
15
  "files": 78,
16
- "strings": 1256
16
+ "strings": 1258
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -241,7 +241,7 @@
241
241
  "occurrences": 4,
242
242
  "at": [
243
243
  "modules/hall-ui/public/gate.js:219",
244
- "modules/hall-ui/public/government.js:1104",
244
+ "modules/hall-ui/public/government.js:1129",
245
245
  "modules/hall-ui/public/harbor.js:337",
246
246
  "modules/hall-ui/public/watch.js:1241"
247
247
  ]
@@ -301,7 +301,7 @@
301
301
  "occurrences": 3,
302
302
  "at": [
303
303
  "modules/hall-ui/public/gate.js:222",
304
- "modules/hall-ui/public/government.js:1107",
304
+ "modules/hall-ui/public/government.js:1132",
305
305
  "modules/hall-ui/public/harbor.js:340"
306
306
  ]
307
307
  },
@@ -1915,7 +1915,7 @@
1915
1915
  "surface": "builders-hall",
1916
1916
  "text": ", // membership",
1917
1917
  "file": "modules/hall-ui/public/government.js",
1918
- "line": 876,
1918
+ "line": 899,
1919
1919
  "origin": "js-markup",
1920
1920
  "confidence": "certain"
1921
1921
  },
@@ -1946,6 +1946,15 @@
1946
1946
  "origin": "js-markup",
1947
1947
  "confidence": "certain"
1948
1948
  },
1949
+ {
1950
+ "id": "574acf2f93bb",
1951
+ "surface": "builders-hall",
1952
+ "text": ", never quietly ratified — so a clock is worth proposing. The recommended sitting is",
1953
+ "file": "modules/hall-ui/public/government.js",
1954
+ "line": 619,
1955
+ "origin": "js-markup",
1956
+ "confidence": "certain"
1957
+ },
1949
1958
  {
1950
1959
  "id": "741e355954ac",
1951
1960
  "surface": "builders-hall",
@@ -2491,7 +2500,7 @@
2491
2500
  "surface": "builders-hall",
2492
2501
  "text": "As configuration:",
2493
2502
  "file": "modules/hall-ui/public/government.js",
2494
- "line": 751,
2503
+ "line": 771,
2495
2504
  "origin": "js-markup",
2496
2505
  "confidence": "certain"
2497
2506
  },
@@ -2563,7 +2572,7 @@
2563
2572
  "surface": "builders-hall",
2564
2573
  "text": "Available from the {…} rank up.",
2565
2574
  "file": "modules/hall-ui/public/government.js",
2566
- "line": 1107,
2575
+ "line": 1132,
2567
2576
  "origin": "js-text-assign",
2568
2577
  "confidence": "certain"
2569
2578
  },
@@ -2860,7 +2869,7 @@
2860
2869
  "surface": "builders-hall",
2861
2870
  "text": "Cast objection",
2862
2871
  "file": "modules/hall-ui/public/government.js",
2863
- "line": 962,
2872
+ "line": 987,
2864
2873
  "origin": "js-markup",
2865
2874
  "confidence": "certain"
2866
2875
  },
@@ -2941,7 +2950,7 @@
2941
2950
  "surface": "builders-hall",
2942
2951
  "text": "Charter library",
2943
2952
  "file": "modules/hall-ui/public/government.js",
2944
- "line": 721,
2953
+ "line": 741,
2945
2954
  "origin": "js-markup",
2946
2955
  "confidence": "certain"
2947
2956
  },
@@ -4111,7 +4120,7 @@
4111
4120
  "surface": "builders-hall",
4112
4121
  "text": "Could not vote: {…}",
4113
4122
  "file": "modules/hall-ui/public/government.js",
4114
- "line": 990,
4123
+ "line": 1015,
4115
4124
  "origin": "js-notice",
4116
4125
  "confidence": "certain"
4117
4126
  },
@@ -4615,7 +4624,7 @@
4615
4624
  "surface": "builders-hall",
4616
4625
  "text": "Every closed amendment sitting, ratified and returned alike, with the rule it was decided under.",
4617
4626
  "file": "modules/hall-ui/public/government.js",
4618
- "line": 760,
4627
+ "line": 780,
4619
4628
  "origin": "js-markup",
4620
4629
  "confidence": "certain"
4621
4630
  },
@@ -5254,7 +5263,7 @@
5254
5263
  "surface": "builders-hall",
5255
5264
  "text": "How it has changed",
5256
5265
  "file": "modules/hall-ui/public/government.js",
5257
- "line": 760,
5266
+ "line": 780,
5258
5267
  "origin": "js-markup",
5259
5268
  "confidence": "certain"
5260
5269
  },
@@ -5326,7 +5335,7 @@
5326
5335
  "surface": "builders-hall",
5327
5336
  "text": "How this project is governed.",
5328
5337
  "file": "modules/hall-ui/public/government.js",
5329
- "line": 1142,
5338
+ "line": 1167,
5330
5339
  "origin": "js-text-assign",
5331
5340
  "confidence": "certain"
5332
5341
  },
@@ -5587,7 +5596,7 @@
5587
5596
  "surface": "builders-hall",
5588
5597
  "text": "It cleared the completeness grade at {…}/100.",
5589
5598
  "file": "modules/hall-ui/public/government.js",
5590
- "line": 932,
5599
+ "line": 957,
5591
5600
  "origin": "js-markup",
5592
5601
  "confidence": "certain"
5593
5602
  },
@@ -7252,7 +7261,7 @@
7252
7261
  "surface": "builders-hall",
7253
7262
  "text": "Permissions is the Archon administration surface. The Constitution and the Board Room are yours to read.",
7254
7263
  "file": "modules/hall-ui/public/government.js",
7255
- "line": 1121,
7264
+ "line": 1146,
7256
7265
  "origin": "js-markup",
7257
7266
  "confidence": "certain"
7258
7267
  },
@@ -8062,7 +8071,7 @@
8062
8071
  "surface": "builders-hall",
8063
8072
  "text": "Restricted.",
8064
8073
  "file": "modules/hall-ui/public/government.js",
8065
- "line": 1104,
8074
+ "line": 1129,
8066
8075
  "origin": "js-text-assign",
8067
8076
  "confidence": "certain"
8068
8077
  },
@@ -8359,7 +8368,7 @@
8359
8368
  "surface": "builders-hall",
8360
8369
  "text": "Selectable today",
8361
8370
  "file": "modules/hall-ui/public/government.js",
8362
- "line": 721,
8371
+ "line": 741,
8363
8372
  "origin": "js-markup",
8364
8373
  "confidence": "certain"
8365
8374
  },
@@ -9133,7 +9142,7 @@
9133
9142
  "surface": "builders-hall",
9134
9143
  "text": "The developmental ladder: {…}. Formality is adopted only where informal coordination breaks.",
9135
9144
  "file": "modules/hall-ui/public/government.js",
9136
- "line": 728,
9145
+ "line": 748,
9137
9146
  "origin": "js-markup",
9138
9147
  "confidence": "certain"
9139
9148
  },
@@ -9412,7 +9421,7 @@
9412
9421
  "surface": "builders-hall",
9413
9422
  "text": "The seven government structures, for getting a team up to speed before deciding when to flip a switch.",
9414
9423
  "file": "modules/hall-ui/public/government.js",
9415
- "line": 721,
9424
+ "line": 741,
9416
9425
  "origin": "js-markup",
9417
9426
  "confidence": "certain"
9418
9427
  },
@@ -9763,7 +9772,7 @@
9763
9772
  "surface": "builders-hall",
9764
9773
  "text": "This sends the idea back to its author. Say why, and which section.",
9765
9774
  "file": "modules/hall-ui/public/government.js",
9766
- "line": 962,
9775
+ "line": 987,
9767
9776
  "origin": "js-markup",
9768
9777
  "confidence": "certain"
9769
9778
  },
@@ -9952,7 +9961,7 @@
9952
9961
  "surface": "builders-hall",
9953
9962
  "text": "Vote yes",
9954
9963
  "file": "modules/hall-ui/public/government.js",
9955
- "line": 962,
9964
+ "line": 987,
9956
9965
  "origin": "js-markup",
9957
9966
  "confidence": "certain"
9958
9967
  },
@@ -10294,7 +10303,7 @@
10294
10303
  "surface": "builders-hall",
10295
10304
  "text": "Why — the reason the author will act on",
10296
10305
  "file": "modules/hall-ui/public/government.js",
10297
- "line": 962,
10306
+ "line": 987,
10298
10307
  "origin": "js-markup",
10299
10308
  "confidence": "certain"
10300
10309
  },
@@ -10492,7 +10501,7 @@
10492
10501
  "surface": "builders-hall",
10493
10502
  "text": "You {…}. One vote per member.",
10494
10503
  "file": "modules/hall-ui/public/government.js",
10495
- "line": 1034,
10504
+ "line": 1059,
10496
10505
  "origin": "js-markup",
10497
10506
  "confidence": "certain"
10498
10507
  },
@@ -10654,7 +10663,7 @@
10654
10663
  "surface": "builders-hall",
10655
10664
  "text": "Your own idea. You may vote, but it can’t be decided by your vote alone, and a vote on your own idea earns no karma.",
10656
10665
  "file": "modules/hall-ui/public/government.js",
10657
- "line": 960,
10666
+ "line": 985,
10658
10667
  "origin": "js-markup",
10659
10668
  "confidence": "certain"
10660
10669
  },
@@ -10843,7 +10852,7 @@
10843
10852
  "surface": "builders-hall",
10844
10853
  "text": "active builder{…}: {…}",
10845
10854
  "file": "modules/hall-ui/public/government.js",
10846
- "line": 721,
10855
+ "line": 741,
10847
10856
  "origin": "js-markup",
10848
10857
  "confidence": "certain"
10849
10858
  },
@@ -10915,7 +10924,7 @@
10915
10924
  "surface": "builders-hall",
10916
10925
  "text": "as configuration: {…}",
10917
10926
  "file": "modules/hall-ui/public/government.js",
10918
- "line": 707,
10927
+ "line": 727,
10919
10928
  "origin": "js-markup",
10920
10929
  "confidence": "certain"
10921
10930
  },
@@ -11455,7 +11464,7 @@
11455
11464
  "surface": "builders-hall",
11456
11465
  "text": "means expressible through the real configuration — a rank-based membership plus a pass rule — with no new code. This project has",
11457
11466
  "file": "modules/hall-ui/public/government.js",
11458
- "line": 721,
11467
+ "line": 741,
11459
11468
  "origin": "js-markup",
11460
11469
  "confidence": "certain"
11461
11470
  },
@@ -11527,7 +11536,7 @@
11527
11536
  "surface": "builders-hall",
11528
11537
  "text": "no clock",
11529
11538
  "file": "modules/hall-ui/public/government.js",
11530
- "line": 602,
11539
+ "line": 621,
11531
11540
  "origin": "js-markup",
11532
11541
  "confidence": "certain"
11533
11542
  },
@@ -11635,7 +11644,7 @@
11635
11644
  "surface": "builders-hall",
11636
11645
  "text": "objected to",
11637
11646
  "file": "modules/hall-ui/public/government.js",
11638
- "line": 824,
11647
+ "line": 847,
11639
11648
  "origin": "js-markup",
11640
11649
  "confidence": "certain"
11641
11650
  },
@@ -12080,6 +12089,15 @@
12080
12089
  "origin": "js-markup",
12081
12090
  "confidence": "certain"
12082
12091
  },
12092
+ {
12093
+ "id": "edd5e9f56c25",
12094
+ "surface": "builders-hall",
12095
+ "text": "unanimous agreement",
12096
+ "file": "modules/hall-ui/public/government.js",
12097
+ "line": 601,
12098
+ "origin": "js-markup",
12099
+ "confidence": "certain"
12100
+ },
12083
12101
  {
12084
12102
  "id": "f98966dc7cac",
12085
12103
  "surface": "builders-hall",
@@ -12202,7 +12220,7 @@
12202
12220
  "surface": "builders-hall",
12203
12221
  "text": "your vote",
12204
12222
  "file": "modules/hall-ui/public/government.js",
12205
- "line": 819,
12223
+ "line": 842,
12206
12224
  "origin": "js-markup",
12207
12225
  "confidence": "certain"
12208
12226
  },
@@ -12454,7 +12472,7 @@
12454
12472
  "surface": "builders-hall",
12455
12473
  "text": "{…} proposed",
12456
12474
  "file": "modules/hall-ui/public/government.js",
12457
- "line": 626,
12475
+ "line": 645,
12458
12476
  "origin": "js-markup",
12459
12477
  "confidence": "certain"
12460
12478
  },
@@ -12634,7 +12652,7 @@
12634
12652
  "surface": "builders-hall",
12635
12653
  "text": "{…} · who votes, and how it {…}",
12636
12654
  "file": "modules/hall-ui/public/government.js",
12637
- "line": 1043,
12655
+ "line": 1068,
12638
12656
  "origin": "js-markup",
12639
12657
  "confidence": "certain"
12640
12658
  },
@@ -12706,7 +12724,7 @@
12706
12724
  "surface": "builders-hall",
12707
12725
  "text": "{…}s only",
12708
12726
  "file": "modules/hall-ui/public/government.js",
12709
- "line": 1121,
12727
+ "line": 1146,
12710
12728
  "origin": "js-markup",
12711
12729
  "confidence": "certain"
12712
12730
  },
@@ -1677,5 +1677,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1677
1677
  landed since 1.19.612 with no explicit bump. run 34311691842. (task 1002620)
1678
1678
  1.19.614 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1679
1679
  landed since 1.19.613 with no explicit bump. run 34314327601. (task 1002620)
1680
+ 1.19.615 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1681
+ landed since 1.19.614 with no explicit bump. run 34314595235. (task 1002620)
1680
1682
  ---------------------------------------------------------------------------
1681
1683
  ```
@@ -47,7 +47,10 @@ const db = require('./db');
47
47
  // file (the reward-port delegation lesson: a destructured binding is frozen at
48
48
  // load and un-patchable).
49
49
  const governmentConfig = require('./config');
50
- const { loadGovernmentConfig, PASS_RULES, MAX_WINDOW_MINUTES } = governmentConfig;
50
+ const {
51
+ loadGovernmentConfig, PASS_RULES, MAX_WINDOW_MINUTES,
52
+ RECOMMENDED_WINDOW_MINUTES, CLOCK_SAFE_PASS_RULES, isClockSafe,
53
+ } = governmentConfig;
51
54
  const {
52
55
  isBoardMember, resolveBoardMembers, parseMembershipPredicate, wouldBeBoardMember,
53
56
  } = require('./board-membership');
@@ -288,8 +291,11 @@ async function castVote({ itemId, voterId, direction, reasonMd = null, sectionKe
288
291
 
289
292
  // ── close + tally (BV1.R10 of goal 1000069, ADR 0175 §3) ────────────────────
290
293
  //
291
- // EVERY RULE DECLARED, NONE EMERGENT. The tally is one line: a single reasoned
292
- // objection returns the idea; anything else — including ZERO votes — passes.
294
+ // EVERY RULE DECLARED, NONE EMERGENT. Under the two ORIGINAL rules the tally is
295
+ // one line: a single reasoned objection returns the idea; anything else —
296
+ // including ZERO votes — passes. `majority` and `unanimous` (ADR 0191, ADR 0267)
297
+ // are the rules where absence stops being free, each in its own way; read
298
+ // tallyOutcome for the whole counting story.
293
299
  // Absent members are NULLED by construction rather than by branch: the tally
294
300
  // only ever reads the votes that were cast, so a member who never voted simply
295
301
  // does not exist in it, and absence can never block (§2 — the sitting model).
@@ -301,7 +307,9 @@ async function castVote({ itemId, voterId, direction, reasonMd = null, sectionKe
301
307
  // Archon ratifies or returns, done); consent closes early only on FULL
302
308
  // TURNOUT (every current member has voted — recorded positions and the
303
309
  // vote karma are worth the wait, and no later vote can change the outcome
304
- // anyway since there is no vote-changing in v1);
310
+ // anyway since there is no vote-changing in v1); majority and unanimous
311
+ // close the moment the outcome is FIXED (carried, or unreachable, or —
312
+ // under unanimous — objected to), and otherwise on full turnout;
305
313
  // • on expiry — R11's poller calls the same closeItem, so the clock and the
306
314
  // turnout can never disagree about what a close means.
307
315
  // An unknown pass_rule closes NOTHING after a vote (fail-closed — only the
@@ -390,6 +398,24 @@ function tallyOutcome(votes, { constitution = {}, members = null, authorId = nul
390
398
  && yes.some((v) => !isAuthorVote(v, authorId));
391
399
  return carried ? 'passed' : 'returned';
392
400
  }
401
+ if (constitution.pass_rule === 'unanimous') {
402
+ // TWO CONDITIONS, AND THE SECOND IS THE ONE PEOPLE FORGET. Unanimity is not
403
+ // "nobody objected" — an empty room objects to nothing. It is "everyone who
404
+ // spoke said yes, AND somebody said yes", so a sitting that nobody attended
405
+ // RETURNS rather than passing on silence. That is what makes the clock safe
406
+ // here (ADR 0191 §4's condition) and what separates this rule from consent.
407
+ //
408
+ // ABSENCE IS NOT COUNTED AT ALL — there is deliberately no membership
409
+ // denominator (owner decision 2026-09-08: silence must not block, which is
410
+ // why the clock exists). A member who never voted is neither a yes nor a no;
411
+ // they simply are not in the reckoning, exactly as under first_ratifier.
412
+ //
413
+ // The ADR 0191 author rule is unchanged and rides the same two helpers: an
414
+ // author's 'no' cannot return their own idea (decidingVotes drops it), and
415
+ // the affirmative voice that carries it must not be the author's own.
416
+ if (decidingVotes(list, authorId).some((v) => v.direction === 'no')) return 'returned';
417
+ return list.some((v) => v.direction === 'yes' && !isAuthorVote(v, authorId)) ? 'passed' : 'returned';
418
+ }
393
419
  // first_ratifier / consent / anything unknown: one reasoned objection returns
394
420
  // the idea, anything else — including ZERO votes — passes. The author's own
395
421
  // vote is excluded so they cannot return their own idea.
@@ -418,6 +444,16 @@ function majorityDecided({ votes, members, authorId }) {
418
444
  return !hasMajority(yes.length + notYetVoted, memberCount);
419
445
  }
420
446
 
447
+ // Has every CURRENT member cast a vote? The full-turnout courtesy three of the
448
+ // four rules grant, in one place rather than three: by then every position is on
449
+ // the record and no later vote can change the outcome (there is no vote-changing
450
+ // in v1). An empty board has no turnout to complete, so it is never full.
451
+ function fullTurnout(votes, members) {
452
+ if (!members || !members.length) return false;
453
+ const voters = new Set((votes || []).map((v) => String(v.voter_id)));
454
+ return members.every((m) => voters.has(String(m.builder_id)));
455
+ }
456
+
421
457
  // Should a just-cast vote close the item now, under the SNAPSHOTTED rule?
422
458
  //
423
459
  // EVERY RULE NEEDS A NON-AUTHOR VOTE FIRST (task 1003273): an author's cast is
@@ -431,18 +467,29 @@ function shouldCloseAfterVote({ constitution, votes, members, authorId = null }
431
467
  if (!deciding.length) return false; // nobody but the author has spoken
432
468
  if (majorityDecided({ votes, members, authorId })) return true;
433
469
  // Full turnout closes it too, when the constitution says so — the same
434
- // courtesy consent gets, and by then every position is on the record.
470
+ // courtesy consent gets.
471
+ if (!(constitution || {}).close_early_on_full_turnout) return false;
472
+ return fullTurnout(votes, members);
473
+ }
474
+ if (rule === 'unanimous') {
475
+ if (!deciding.length) return false; // nobody but the author has spoken
476
+ // A REASONED OBJECTION ENDS IT IMMEDIATELY, and that is a kindness rather
477
+ // than impatience: there is no vote-changing in v1, so once a non-author
478
+ // 'no' is on the record the outcome is fixed at 'returned' whatever the
479
+ // absent members do. Holding the sitting open would only delay the feedback
480
+ // the author needs to revise and re-sit — which is the whole point of this
481
+ // rule (ADR 0267). Same reasoning as majorityDecided's unreachable branch.
482
+ if (deciding.some((v) => v.direction === 'no')) return true;
483
+ // A single yes does NOT end it. Unlike first_ratifier, a later member must
484
+ // still be able to object — the sitting runs its clock, and only full
485
+ // turnout (every position on the record) can close it early.
435
486
  if (!(constitution || {}).close_early_on_full_turnout) return false;
436
- if (!members || !members.length) return false;
437
- const voters = new Set((votes || []).map((v) => String(v.voter_id)));
438
- return members.every((m) => voters.has(String(m.builder_id)));
487
+ return fullTurnout(votes, members);
439
488
  }
440
489
  if (rule === 'consent') {
441
490
  if (!(constitution || {}).close_early_on_full_turnout) return false;
442
- if (!members || !members.length) return false; // an empty board has no turnout to complete
443
491
  if (!deciding.length) return false;
444
- const voters = new Set((votes || []).map((v) => String(v.voter_id)));
445
- return members.every((m) => voters.has(String(m.builder_id)));
492
+ return fullTurnout(votes, members);
446
493
  }
447
494
  return false; // unknown rule: only the clock closes it
448
495
  }
@@ -510,6 +557,10 @@ async function closeItem(item, { cause = 'vote' } = {}) {
510
557
  : null;
511
558
  // Only `majority` needs the membership denominator; the absence-passes rules
512
559
  // read nothing but the cast votes, so we do not spend the query on them.
560
+ // `unanimous` does not need it either — it has no denominator by design — and
561
+ // is listed nowhere here on purpose: the members list it DOES use is the
562
+ // full-turnout early close, which lives on the vote path (shouldCloseAfterVote
563
+ // resolves it there), never in the tally.
513
564
  const members = constitution.pass_rule === 'majority'
514
565
  ? await resolveBoardMembers(constitution)
515
566
  : null;
@@ -859,7 +910,27 @@ async function constitutionView({ historyLimit = 50 } = {}) {
859
910
  outcome: r.outcome,
860
911
  decided_under: r.decided_under || null,
861
912
  }));
862
- return { board: live, history, headcount, recommended: recommendedCharter(headcount) };
913
+ return {
914
+ board: live,
915
+ history,
916
+ headcount,
917
+ recommended: recommendedCharter(headcount),
918
+ // THE CLOCK ADVICE THE AMENDMENT FORM NEEDS (task 1003733, ADR 0267 §3).
919
+ // A proposer choosing a pass rule has to choose a window in the same breath
920
+ // — a constitution is ratified whole, never by diff — and the two choices
921
+ // are not independent: a deadline is only safe under a rule whose expiry
922
+ // means RETURN (ADR 0191 §4). So the server states which rules that is and
923
+ // what to put in the box, rather than leaving the hall to hardcode a number
924
+ // and the proposer to guess whether a clock is even honest here.
925
+ // `window_minutes: null` stays a legal answer under a clock-safe rule; it
926
+ // means "wait indefinitely for a voice", which the hall says out loud.
927
+ clock: {
928
+ recommended_window_minutes: RECOMMENDED_WINDOW_MINUTES,
929
+ max_window_minutes: MAX_WINDOW_MINUTES,
930
+ clock_safe_pass_rules: CLOCK_SAFE_PASS_RULES,
931
+ live_rule_is_clock_safe: isClockSafe(live.pass_rule),
932
+ },
933
+ };
863
934
  }
864
935
 
865
936
  // ── R13's read: the feedback a returned idea carries back to its author ─────
@@ -155,6 +155,35 @@ const BOARD_DEFAULTS = Object.freeze({
155
155
  close_early_on_full_turnout: true,
156
156
  });
157
157
 
158
+ // THE CLOCK, AS A NUMBER SOMEBODY CAN ADOPT (task 1003733, ADR 0267 §3).
159
+ //
160
+ // The note above says why BOARD_DEFAULTS ships CLOCKLESS and must keep doing so:
161
+ // its rule is `first_ratifier`, under which expiry means PASS, so a default
162
+ // clock would ratify what nobody read. But "the platform must not arm this for
163
+ // you" is not the same as "you must invent a number" — and the owner asking for
164
+ // unanimity asked for the timer in the same breath, because under a rule where
165
+ // absence is nulled the deadline is the only thing that ends a quiet sitting.
166
+ //
167
+ // So the recommended sitting length lives here as a named constant rather than
168
+ // as a magic number the Board Room's charter library retypes. Twenty-four hours
169
+ // matches the pace the rest of the pipeline runs at (the 2026-08-25 amendment
170
+ // reached for the same figure independently), and is long enough that a member
171
+ // asleep in another timezone still gets a vote.
172
+ //
173
+ // A constitution may still legitimately name `window_minutes: null` under a
174
+ // clock-safe rule — that is "wait indefinitely for a voice", a real state a
175
+ // project can choose, and the hall says so out loud rather than pretending.
176
+ const RECOMMENDED_WINDOW_MINUTES = 1440;
177
+
178
+ // Which rules a clock is SAFE under — ADR 0191 §4's condition, as data instead
179
+ // of as a paragraph: a deadline may only be given to a rule whose expiry means
180
+ // RETURN. Under `first_ratifier` and `consent` the tally passes on absence, so
181
+ // the expiry sweep would ratify unread items; under `majority` and `unanimous`
182
+ // it returns them. The Board Room reads this to decide whether offering a clock
183
+ // alongside a proposed rule is honest.
184
+ const CLOCK_SAFE_PASS_RULES = Object.freeze(['majority', 'unanimous']);
185
+ const isClockSafe = (rule) => CLOCK_SAFE_PASS_RULES.includes(rule);
186
+
158
187
  // The pass rules that EXIST, not the ones a project should pick. `first_ratifier`
159
188
  // is the day-one default and what an unreadable value falls back to.
160
189
  //
@@ -166,14 +195,23 @@ const BOARD_DEFAULTS = Object.freeze({
166
195
  // UNOPPOSED. Silence is assent, deliberately (owner decision 6).
167
196
  // • majority — strictly more than half of the RESOLVED MEMBERSHIP must
168
197
  // vote yes. Silence is NOT assent: an item that never reaches a majority
169
- // RETURNS at expiry. This is the only rule under which not voting is a
170
- // meaningful act, and the only one where a clock is safe — see the note on
171
- // window_minutes below.
172
- // Adding `majority` cannot widen the fail-closed fallback: sanitizeBoard still
173
- // falls back to first_ratifier, and majority is the STRICTER end of this axis
174
- // anyway (it demands affirmative votes from a majority, where the other two
175
- // pass on absence).
176
- const PASS_RULES = Object.freeze(['first_ratifier', 'consent', 'majority']);
198
+ // RETURNS at expiry. Not voting is a meaningful act here, and a clock is
199
+ // safe — see the note on window_minutes below.
200
+ // unanimous — nobody who spoke objected, and somebody spoke. One
201
+ // reasoned objection returns it; so does a sitting that ends with no
202
+ // affirmative voice at all. Silence neither blocks nor assents: an absent
203
+ // member drops out of the reckoning entirely (there is NO membership
204
+ // denominator here, which is exactly what separates it from `majority`),
205
+ // and absence alone can never carry an item either. A clock is safe under
206
+ // this rule for the same reason it is safe under `majority` — expiry with
207
+ // nothing cast RETURNS. Owner decision 2026-09-08 (ADR 0267): a Full Idea
208
+ // is ratified by unanimity, and a sitting that does not carry goes back to
209
+ // its author to be revised and re-sat, as many times as it takes.
210
+ // Adding `majority` or `unanimous` cannot widen the fail-closed fallback:
211
+ // sanitizeBoard still falls back to first_ratifier, and both of the added rules
212
+ // sit at the STRICTER end of this axis anyway (each demands an affirmative
213
+ // vote, where the original two pass on absence).
214
+ const PASS_RULES = Object.freeze(['first_ratifier', 'consent', 'majority', 'unanimous']);
177
215
 
178
216
  // A membership predicate names WHO SITS. The grammar and its parser live in
179
217
  // ./membership-predicate — ONE implementation, shared with R06's resolver
@@ -321,6 +359,9 @@ module.exports = {
321
359
  applyBoardAmendment,
322
360
  BOARD_DEFAULTS,
323
361
  PASS_RULES,
362
+ RECOMMENDED_WINDOW_MINUTES,
363
+ CLOCK_SAFE_PASS_RULES,
364
+ isClockSafe,
324
365
  MAX_WINDOW_MINUTES,
325
366
  NEUTRAL_PATH,
326
367
  INSTANCE_PATH,
@@ -594,12 +594,31 @@
594
594
  + 'Silence is <em>not</em> agreement here, so an item that never reaches a majority is returned to its author. '
595
595
  + 'An author may vote on their own idea, but it can never be carried by their vote alone.';
596
596
  }
597
+ // ADR 0267. Silence is the axis again, and unanimity sits between the other
598
+ // three on it: an absent member does not block (that is what the clock is
599
+ // for), but absence alone cannot carry anything either.
600
+ if (board.pass_rule === 'unanimous') {
601
+ return 'Items pass by <strong>unanimous agreement</strong>: everyone who votes must vote yes, and at least one member must. '
602
+ + 'A single reasoned objection returns the item to its author to be revised and put back to the board — as many rounds as it takes. '
603
+ + 'Being away does not block anything, but a sitting that ends with nobody in favour is returned rather than passed. '
604
+ + 'An author may vote on their own idea, but it can never be carried by their vote alone.';
605
+ }
597
606
  return `Pass rule: <code>${escapeHtml(String(board.pass_rule || 'unset'))}</code>.`;
598
607
  }
599
608
 
600
- function windowSentence(board) {
609
+ // The clock, and the advice the SERVER computes about it (task 1003733). A
610
+ // proposer choosing a pass rule must choose a window in the same breath — a
611
+ // constitution is ratified whole — and the two are not independent: a deadline
612
+ // is only honest under a rule whose expiry RETURNS the item (ADR 0191 §4).
613
+ // Which rules those are, and what to put in the box, come from
614
+ // GET /government/constitution's `clock` block; nothing is derived here, so
615
+ // the page cannot drift from what config.js actually considers safe.
616
+ function windowSentence(board, clock) {
601
617
  if (board.window_minutes === null || board.window_minutes === undefined) {
602
- return 'There is <strong>no clock</strong> an item stays open until a vote decides it.';
618
+ const advice = clock && clock.live_rule_is_clock_safe && clock.recommended_window_minutes
619
+ ? ` Under this pass rule a deadline would be safe — an expired sitting is <em>returned</em>, never quietly ratified — so a clock is worth proposing. The recommended sitting is <strong>${fmtWindow(clock.recommended_window_minutes)}</strong>.`
620
+ : '';
621
+ return `There is <strong>no clock</strong> — an item stays open until a vote decides it.${advice}`;
603
622
  }
604
623
  const early = board.close_early_on_full_turnout
605
624
  ? ', closing early the moment every member has voted'
@@ -636,8 +655,9 @@
636
655
  // trajectories, the ladder — and deliberately not the configuration machinery
637
656
  // (the scale argument in ADR 0175: formality is adopted only where informal
638
657
  // coordination breaks). `selectable` means EXPRESSIBLE TODAY through the real
639
- // config — a rank-predicate membership plus first_ratifier|consent so nobody
640
- // is misled about what a settings change can and cannot reach. Source: the
658
+ // config — a rank-predicate membership plus one of the four pass rules
659
+ // (first_ratifier | consent | majority | unanimous) so nobody is misled
660
+ // about what a settings change can and cannot reach. Source: the
641
661
  // Six-Variables synthesis (Verro Labs, Aug 2026), Parts III–IV, via the spec.
642
662
  // EVERY SELECTABLE CHARTER CARRIES A MACHINE-CHECKABLE PREDICATE (task 1003095).
643
663
  //
@@ -752,7 +772,7 @@
752
772
  <ul class="ov-rows">
753
773
  ${dialRow('Who sits on the board', membershipSentence(b))}
754
774
  ${dialRow('What counts as passing', passRuleSentence(b))}
755
- ${dialRow('How long a sitting runs', windowSentence(b))}
775
+ ${dialRow('How long a sitting runs', windowSentence(b, data.clock))}
756
776
  </ul>
757
777
  <p class="ov-foot">As configuration: <code>${escapeHtml(JSON.stringify(b))}</code>. Changing it is a board act — propose an amendment, and it is decided under the rule above.</p>
758
778
  `;
@@ -805,6 +825,9 @@
805
825
  // it waits indefinitely for a majority, which is a real state a project can
806
826
  // configure itself into (task 1003273).
807
827
  if (rule === 'majority') return 'no clock — waits for a majority';
828
+ // Same honesty for unanimity: with no deadline it waits indefinitely for
829
+ // someone to be in favour, which is a state a project can configure (ADR 0267).
830
+ if (rule === 'unanimous') return 'no clock — waits for a unanimous yes';
808
831
  return 'no clock';
809
832
  }
810
833
 
@@ -907,12 +930,14 @@
907
930
  first_ratifier: 'by first ratifier',
908
931
  consent: 'by consent',
909
932
  majority: 'by majority',
933
+ unanimous: 'by unanimous agreement',
910
934
  };
911
935
 
912
936
  const PASS_RULE_PLAIN = {
913
937
  first_ratifier: 'The first vote decides it: a yes ratifies, an objection returns it to its author.',
914
938
  consent: 'It passes unless someone objects — silence is agreement, and one reasoned objection returns it.',
915
939
  majority: 'It passes only when more than half the board votes yes. Silence is not agreement here.',
940
+ unanimous: 'Everyone who votes must vote yes, and at least one member must. One objection sends it back for revision.',
916
941
  };
917
942
 
918
943
  function decidedByHtml(item) {
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.614",
3
+ "version": "1.19.615",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.614",
9
+ "version": "1.19.615",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",