@bongos/core 1.19.907 → 1.19.909

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 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.907",
6
- "core_contract": "1.19.907",
7
- "source_commit": "71cb07155a010b68421b31a9154436b140d5557d",
5
+ "core_version": "1.19.909",
6
+ "core_contract": "1.19.909",
7
+ "source_commit": "a378172e3f20f9432a63ee723274cb46acd87e5d",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-20T13:52:16.387Z",
9
+ "built_at": "2026-09-20T14:45:23.165Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 502,
12
+ "docs_redacted": 503,
13
13
  "agent_docs_stubbed": 26,
14
14
  "functional_verbatim": 2283,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2811,
20
- "tree_sha256": "b58d926d1a79850ef28b158610d628958dbbf0c143ba5327cabbcd5ad2f527d3",
19
+ "file_count": 2812,
20
+ "tree_sha256": "8f49b63378d69ecb4c8d7d3d6b4ad472ba735ba1ca96922843be33ff8d475d89",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2059,10 +2059,15 @@
2059
2059
  "mode": "0000644",
2060
2060
  "sha256": "9b301cd17f96c3f0b49010f046b994e112a5e1f971797964b836bc578578dd70"
2061
2061
  },
2062
+ {
2063
+ "path": "docs/adr/0314-module-protection-is-per-path-not-per-module.md",
2064
+ "mode": "0000644",
2065
+ "sha256": "541a0ae0223982fe53fcf96e3c4a9509e94101d04db0a87395f39cc1bcd881e8"
2066
+ },
2062
2067
  {
2063
2068
  "path": "docs/adr/README.md",
2064
2069
  "mode": "0000644",
2065
- "sha256": "85bb537649c78b08e36e2de94dfde8449760ca163577a7fac63603da4074dd37"
2070
+ "sha256": "4bd65e814cacf8b886b542bf7741dff8f61e9aad3e101db1af2bd28e0eb1a49e"
2066
2071
  },
2067
2072
  {
2068
2073
  "path": "docs/api-reference.md",
@@ -2097,12 +2102,12 @@
2097
2102
  {
2098
2103
  "path": "docs/copy-inventory.md",
2099
2104
  "mode": "0000644",
2100
- "sha256": "b018bdef70a2e46e00fa7e88d4ee489255d8d1ff9eaeecba18eb3c39ec41878d"
2105
+ "sha256": "3a121e993f87d7379836af082bebb8c857427481eb265d477b4b55584cdd604b"
2101
2106
  },
2102
2107
  {
2103
2108
  "path": "docs/copy-registry.json",
2104
2109
  "mode": "0000644",
2105
- "sha256": "88c3ac9d52e16fef3c82e09f4a8b4e8d8fa46f490e68bf370d2fd49a90bfbdee"
2110
+ "sha256": "77e94a0337636dd8e444bfca6d749c9a16a937ed8e98b372072c17f5b43cc3fe"
2106
2111
  },
2107
2112
  {
2108
2113
  "path": "docs/design/apex-pass-2-direction.md",
@@ -2632,7 +2637,7 @@
2632
2637
  {
2633
2638
  "path": "docs/module-api-changelog.md",
2634
2639
  "mode": "0000644",
2635
- "sha256": "efe7ce69b4aed84c057d34db6b6bd2bcbb8a8f7b326bab16320277f314db5a46"
2640
+ "sha256": "9acd2adf3d2f21d23fcc9cc626a26ca331972eb6980c5e0c9f8b883c9094b40f"
2636
2641
  },
2637
2642
  {
2638
2643
  "path": "docs/modules-contract.md",
@@ -4662,22 +4667,22 @@
4662
4667
  {
4663
4668
  "path": "modules/hall-ui/public/collab-lib.js",
4664
4669
  "mode": "0000644",
4665
- "sha256": "bfadae8700cc22ea13f8bc39a3e2a1876b58da6151032e9a973402bc9bd0aaa7"
4670
+ "sha256": "baa893e9f8bd207416ee271f429964a85f7d6e1d44235c98a0a9dc20c7998049"
4666
4671
  },
4667
4672
  {
4668
4673
  "path": "modules/hall-ui/public/collab.css",
4669
4674
  "mode": "0000644",
4670
- "sha256": "884118a3b38b0c1ed5e1ac9c90c0b5994714bbabde67959f67c49e7f49536983"
4675
+ "sha256": "ee8845701481932fd9c222b5bb1714aaa552c2c0967d97fc91eef166fe6ab712"
4671
4676
  },
4672
4677
  {
4673
4678
  "path": "modules/hall-ui/public/collab.html",
4674
4679
  "mode": "0000644",
4675
- "sha256": "186a0828e1c3e44388146b94c56f239ea6b2392bd599f143c7c73029da35da32"
4680
+ "sha256": "9876929d0ce989d12dadcdfa6da5cd93ca660e0b291a24a467a920632245dd65"
4676
4681
  },
4677
4682
  {
4678
4683
  "path": "modules/hall-ui/public/collab.js",
4679
4684
  "mode": "0000644",
4680
- "sha256": "91a0751cebcbc179f28ad1cac7a6c447f90369ae40f29e5f60c1bd934ac68f85"
4685
+ "sha256": "465198338e63c762a8f099df8a43e449e804097074c62285243f4876085f6dc5"
4681
4686
  },
4682
4687
  {
4683
4688
  "path": "modules/hall-ui/public/collab.states.json",
@@ -7782,12 +7787,12 @@
7782
7787
  {
7783
7788
  "path": "package-lock.json",
7784
7789
  "mode": "0000644",
7785
- "sha256": "55fbc4950d743f2078280ad1311903120e44c8b93a84d7403fa1107321aee16c"
7790
+ "sha256": "97fdcfbc606ed913875732b87e7cfa0cf105349b9a25a8c1831fd9d59f5d0b76"
7786
7791
  },
7787
7792
  {
7788
7793
  "path": "package.json",
7789
7794
  "mode": "0000644",
7790
- "sha256": "6b23fd601399995c8b9b2dfc1881d2e4047b2345c61549f0a9661c4cb31ed0c5"
7795
+ "sha256": "f060d07f1520e822ce6bc8294d6970fa66f87ae4c23aa6bdf2e7f64197aeec60"
7791
7796
  },
7792
7797
  {
7793
7798
  "path": "public-docs/index.html",
@@ -9782,7 +9787,7 @@
9782
9787
  {
9783
9788
  "path": "src/module-api.js",
9784
9789
  "mode": "0000644",
9785
- "sha256": "e7dbc1db9bf741ba4a85b171e30b9b5f05a4cf38b3b7422d8df81943c66eb7bb"
9790
+ "sha256": "22cc35de1bea324e4016468dcd0c95e1fa859c2d467127fa3acef7dc18432dbd"
9786
9791
  },
9787
9792
  {
9788
9793
  "path": "src/module-loader/catalog.js",
@@ -10622,7 +10627,7 @@
10622
10627
  {
10623
10628
  "path": "tests/collab_page.mjs",
10624
10629
  "mode": "0000644",
10625
- "sha256": "b710669fabe9e747fe37abf429356e80c1f7a36cd63ac308dda2a933243e2e6d"
10630
+ "sha256": "3e6ae0773f3997f82764f51373dd2e12b965d05366f7615346ba4f504151be77"
10626
10631
  },
10627
10632
  {
10628
10633
  "path": "tests/compose_demo_credentials.mjs",
@@ -11607,7 +11612,7 @@
11607
11612
  {
11608
11613
  "path": "tests/hall_collab_world.mjs",
11609
11614
  "mode": "0000644",
11610
- "sha256": "d005de58e0eabd019f616ee6c77123c5880b67034ce33dc1b01d687a2b284b10"
11615
+ "sha256": "0374c15f7bf4cfdb4cccadb51f5c62e2331413e6970a69599cb1f51899d79b0a"
11611
11616
  },
11612
11617
  {
11613
11618
  "path": "tests/hall_error_envelope.mjs",
@@ -12272,7 +12277,7 @@
12272
12277
  {
12273
12278
  "path": "tests/module-scope-map.mjs",
12274
12279
  "mode": "0000644",
12275
- "sha256": "d55b193ee9dcba85a9d51f108352de17f453dfb413b9b391028c9d8f7ff7fab0"
12280
+ "sha256": "b2609440d099b93b61c63c3cdaa4cf6e65c6dc107b8aa4d06d9cb81c9ec5e2c9"
12276
12281
  },
12277
12282
  {
12278
12283
  "path": "tests/module_api.mjs",
@@ -0,0 +1,133 @@
1
+ # ADR 0314 — A module owns a protected path without the whole module becoming protected
2
+
3
+ - **Status:** accepted (design only — the implementation is a follow-up, see *Not built here*)
4
+ - **Date:** 2026-09-20
5
+ - **Task:** [task 1004070](https://cloudbongos.com/builders#/task/1004070), goal 1000095 — BONGOS-V2
6
+ - **From:** idea 1000689; owner decision 2026-09-19 — *option 1, narrow what counts as protected*
7
+ - **Supersedes nothing.** Refines the derivation in [ADR 0086](0086-goal-scoped-work-hierarchy.md) §7.
8
+
9
+ ## The question
10
+
11
+ `protected` on a module is DERIVED — a module is protected iff any of its globs
12
+ overlaps `PROTECTED_GLOBS` ([ADR 0086](0086-goal-scoped-work-hierarchy.md) §7,
13
+ the SEC #863 "second list that drifts" rule). A protected module makes creating a
14
+ goal scoped to it Archon-only.
15
+
16
+ Three modules derive protected because of exactly one file that is not their
17
+ domain — a CLI they own that happens to sit on a protected path:
18
+
19
+ | Module | Protected solely by | of N globs |
20
+ |---|---|---|
21
+ | `onboarding` | `scripts/gds/setup.js` | 1 of 5 |
22
+ | `autonomy` | `scripts/gds/conductor-dispatch.js` | 1 of 6 |
23
+ | `copy-desk` | `scripts/gds/copy-inventory.js` | 1 of 4 |
24
+
25
+ `onboarding` is the case that raised it: a public goal was <redacted>
26
+ for a Metic because the module owns one CLI that installs the pre-push hook.
27
+
28
+ ## Two corrections to the numbers the task was written on
29
+
30
+ Re-measured on this tree (`isProtectedModule` over `moduleKeys()`, 2026-09-20):
31
+
32
+ - **12 of 26 modules derive protected, not 11.** The task's re-measurement of
33
+ 2026-09-19 was already one short.
34
+ - **`agents` is the twelfth**, protected by `modules/agents/` — which is itself a
35
+ `PROTECTED_GLOBS` entry. It is absent from the task's list of seven that "stay
36
+ protected", so a reader working from that list would have been surprised by it.
37
+
38
+ Both corrections argue for the same thing, below: **pin the set by name.**
39
+
40
+ `government` and `agents` are each protected by exactly one glob too — so "1 of N"
41
+ is not the discriminator. The real one is **whether the protecting glob is the
42
+ module's own territory**: `modules/government/` and `modules/agents/` *are* those
43
+ modules; `scripts/gds/setup.js` merely lives near `onboarding`.
44
+
45
+ ## The options, and why the obvious one is wrong
46
+
47
+ You cannot subtract. The glob format has no wildcards, so "onboarding minus
48
+ setup.js" is inexpressible. And you cannot delete the glob: `MODULE_GLOBS` is also
49
+ the territory deciding **who may author a task touching that path**, and the shared
50
+ floor is only `tests/` — so an unowned path is authorable by no goal at all.
51
+
52
+ That left **(a) reassign each incidental CLI to an already-protected module**. It
53
+ was measured rather than reasoned about, by applying the edit to the source and
54
+ re-requiring in a fresh process — `MODULE_SCOPE_MAP` is frozen and built from a
55
+ copy at load, so mutating `MODULE_GLOBS` in-process proves nothing.
56
+
57
+ With `copy-inventory.js` and `conductor-dispatch.js` moved to `devsecops`:
58
+
59
+ | Goal scoped to | Authoring | Before | After (a) |
60
+ |---|---|---|---|
61
+ | `copy-desk` | its own generator | authorable | **denied, out_of_scope** |
62
+ | `autonomy` | its own conductor | authorable | **denied, out_of_scope** |
63
+ | `devsecops` | the generator **and its two outputs** | — | **denied, out_of_scope** |
64
+
65
+ The third row is the one that decides it. `docs/copy-registry.json` and
66
+ `docs/copy-inventory.md` stay `copy-desk`'s, so the generator and the artifacts it
67
+ writes end up either side of a module boundary and **the routine "change a UI
68
+ string, regenerate the registry" becomes authorable by no single goal.** That is
69
+ not a hypothetical workflow: it is the ship-check freshness gate every UI task
70
+ hits, because every row in the registry carries a line number and any edit to a
71
+ scanned file shifts them.
72
+
73
+ So (a) does not relocate a cost. It converts a correct ownership statement into a
74
+ false one, damages the authoring wall for two of the three modules, and buys the
75
+ flip with a workflow the project runs daily. It is right for exactly one of the
76
+ three — `setup.js` → `devsecops`, which already owns `.husky/` and the hooks.
77
+
78
+ ## Decision
79
+
80
+ **Protection becomes per-path at the goal-scope wall, as it already is at the
81
+ other three walls.** A module may own a protected path; owning one no longer makes
82
+ the whole module Archon-only to scope a goal to.
83
+
84
+ The derivation is not weakened and no flag is hand-set — the rule stays computed
85
+ from `PROTECTED_GLOBS`, the single source. What changes is the *question* the
86
+ derivation answers: not "does this module touch the trust core" but "is the
87
+ territory this goal is being scoped to, itself protected".
88
+
89
+ Three reasons this is the honest shape rather than the convenient one:
90
+
91
+ - **Per-path is already the rule everywhere else.** `matchProtected` /
92
+ `checkPermissionPaths` decides task-authoring ([ADR 0086](0086-goal-scoped-work-hierarchy.md) wall 2),
93
+ the grader and gate decide ship, and the post-push main audit decides the
94
+ dishonest case. All three ask about the path. Only goal-creation asks about the
95
+ module, and that inconsistency is the whole defect.
96
+ - **The wall that matters does not move.** A Metic-floor path stays Metic-floor:
97
+ a sub-Metic builder touching `scripts/gds/setup.js` is refused exactly as
98
+ before, by the same check. What changes is who may *create a goal* whose
99
+ territory happens to include it — a question about scoping, not about writing.
100
+ - **It keeps a command with its domain** (design 02§1), which is why those three
101
+ globs are where they are.
102
+
103
+ ## Not built here
104
+
105
+ Per the task: an ADR first, implementation as a follow-up. This decision builds no
106
+ new derivation mechanism. Filed separately:
107
+
108
+ - The implementation of the per-path goal-scope wall.
109
+ - **`builder-settings`, deliberately out of scope.** Its overlap is
110
+ `src/bongos/routes/me.js` caught by the blanket `src/bongos/routes/` prefix, so
111
+ narrowing it means either finishing the strangler move of `me.js` into
112
+ `modules/builder-settings/` or narrowing a blanket protected glob — a security
113
+ change with its own blast radius.
114
+ - **Nine protected paths are owned by no module at all** — `gen-repo-map.js`,
115
+ `gen-file-map.js`, `gen-session-index.js`, `install-git-hooks.js`, `cli-lib.js`,
116
+ `run-unit-tests.js`, `git-merge-regen.js`, `scripts/gds/hooks/` and
117
+ `.gitattributes`. By the rule above, no goal can author a task on any of them.
118
+ Found while checking whether deletion was viable; it is a pre-existing hole,
119
+ not one this decision opens.
120
+
121
+ ## Shipped with this ADR
122
+
123
+ One thing is built, because it guards the decision rather than implementing it:
124
+ a test pinning the **full protected key set by name**. A count assertion passes on
125
+ the wrong twelve, and both corrections above are exactly the drift it catches — a
126
+ future glob edit that silently unprotects `kernel` or `lifecycle` now reds the
127
+ build.
128
+
129
+ ## Does not decide
130
+
131
+ The floor a protected path carries (unchanged, Metic, from the surface registry);
132
+ whether `scripts/gds/` should have a shared-floor entry the way `tests/` does;
133
+ or whether the nine unowned paths are assigned or given a home of their own.
@@ -405,3 +405,4 @@ This keeps the decision history honest and traceable.
405
405
  | 0311 | [**An idea is a permanent origin tag, and the payout follows its descent** ([task 1004076](https://cloudbongos.com/builders#/task/1004076), goal 1000095 — BONGOS-V2). An idea today is a CONTAINER for work: it leaves `idea_inbox` when it names a goal and lives on as a task, so the better-aimed the thought the faster it vanishes from whoever had it. Reported as a small confusion — a thought filed from the hall routed into a goal, the confirmation line cleared on the next keystroke, and five minutes later the owner could not find it. The idea becomes an **immutable origin tag** instead, the only thing that can answer where a piece of work came from, and **the ideator payout follows its whole descent** rather than stopping at the earliest lineage link (`awardIdeaTaskShipCredit`), decaying by depth — full at the direct task, halving each hop — so an infinite descent is worth at most twice the direct task. That is what gives the tag teeth: if it does not survive a fork, someone does not get paid. **Size and brightness are two channels, not one number**: size is the sum of those payouts and never falls, brightness is how much of that work is still alive, so a thought whose work the project tore out renders LARGE AND DEAD rather than looking like one that never mattered. **Karma was rejected as the basis** — `karma_log` carries no task key and no idea key, deliberately ([`migrations/040_builder_karma.sql`](../../migrations/040_builder_karma.sql)), so "the karma a thought caused" is not computable; credits already are idea-attributed. **The task hierarchy is NOT widened** to carry the descent — it is capped at one level and refuses cross-version parents because ship rollup and claim safety lean on it, and provenance is a different relation from work decomposition. **Nothing records that shipped work was later removed** (`shipped` is terminal, `merged_into_task_id` is CHECK-constrained to abandoned rows), so the first `cascade.js` rule PROPOSES a removal and a person confirms it — derived-only would dim for refactors and renames, declared-only would rot. A ratified **Full Idea becomes a goal or a criterion**, carrying a recommendation the board rules on, because scope is the thing you learn last; the five questions are replaced and VERSIONED so prior filings keep their own. Dropped three tasks this invalidated and amended a fourth whose title asserted the exact exclusivity it reverses. Does not decide: the decay rate beyond its default, what precisely counts as removed, how the edges are physically recorded, or how any of it is drawn.](0311-an-idea-is-a-permanent-origin-tag.md) | ideas / economy |
406
406
  | 0312 | [**Provenance is a SECOND ideas-owned edge table, written by the seams that already know** ([task 1004080](https://cloudbongos.com/builders#/task/1004080), goal 1000095 — BONGOS-V2). The spike [ADR 0311](0311-an-idea-is-a-permanent-origin-tag.md) deferred: how the descent edges are physically recorded. A descent (`idea → task → rewrite → split → removal`) is four levels deep and crosses a version boundary, and the task hierarchy refuses both — `attachParent` enforces `SELF_PARENT`, `VERSION_MISMATCH`, `PARENT_HAS_PARENT` and `TARGET_HAS_CHILDREN`, all load-bearing for ship rollup and claim safety. The spike was told to CONFIRM `ideas_task_lineage` rather than assume it, and confirmed one half while refuting the other: the table is right as the **idea→task anchor** (bipartite, no depth cap, no version predicate) but CANNOT carry the task→task edge, because a derivation is a property of the task PAIR and [ADR 0236](0236-many-ideas-one-task-the-lineage-key-is-the-pair.md) made co-source ideas both legal and common — a `derived_from_task_id` column would write the same derivation once per co-source idea and let two rows disagree about where a task came from, the same under-keying that silently lost attribution on prod once already. A merge also gives a task SEVERAL parents, so the relation is a **DAG, not a tree**. Decision: a new `ideas_task_descent` (`task_id`, `parent_task_id`, CHECK'd `relation`, PK on the pair, indexed both ways), with NO depth cap and NO version predicate, and **cycles refused at write time** because the payout walks this graph and a cycle is a non-terminating walk rather than a cosmetic error. On who writes it — the question the task actually set, since an edge depending on memory will have holes — **three seams already know and none is telling anyone**: `cascade.js` encodes the originating task id inside `source_ref` as a STRING nothing can join on, `tasks.merged_into_task_id` already records the fold, and `parent_task_id` already records the split. Only `rewrite` and cross-version `merge` stay manual, which is honest — they are editorial acts with no automatic signal, and their absence degrades the picture rather than corrupting it. What it fixes: `sourceIdeaForTask` and `awardIdeaTaskShipCredit` are both one-hop `LIMIT 1` point reads, so a DERIVED task resolves to `no_source_idea` and the origin is not paid at all. Does not decide: the decay rate beyond its default, what counts as removed ([ADR 0313](0313-a-removal-is-proposed-by-the-ship-and-confirmed-by-a-person.md)), or how any of it is drawn.](0312-provenance-is-a-second-ideas-owned-edge-table.md) | ideas / lifecycle |
407
407
  | 0313 | [**A removal is derived from the SERVER's own merge diff and confirmed by a person** ([task 1004081](https://cloudbongos.com/builders#/task/1004081), goal 1000095 — BONGOS-V2). The other half [ADR 0311](0311-an-idea-is-a-permanent-origin-tag.md) deferred: what counts as removed, so that brightness can fall at all. Nothing records it today — `shipped` is terminal (`rot.js` says so outright) and `merged_into_task_id` is CHECK'd to `abandoned` rows, describing only work cut BEFORE it landed. Owner decision 13 set the shape (system proposes, person confirms); this settles the detail, and corrects two premises the task was written on. **`cascade.js` is no longer empty** — it carries a production rule and [ADR 0242](0242-a-cascade-is-a-declaration-table-on-the-event-that-already-exists.md) fixed the contract (append an entry, never touch the runner), so there is a precedent to copy including its anti-recursion guard. **`netNegativeBonusAmount` cannot be the trigger** despite being the named nearest signal: it is an aggregate with NO TARGET (it says deletion happened, never what or whose), it is CLIENT-SUPPLIED via `signals.diff_stats`, and `grader-score.js` already documents the forgery it defends against — and brightness sits beside a payout, so a forgeable target-less counter cannot carry it. **Nothing durable records what any ship touched**: `committed_files` and `diff_stats` are client-supplied at grade time and never persisted, `touches[]` is advisory ([ADR 0049](0049-split-parallel-safety-contract.md)), so a removal cannot be reconstructed from history — only derived as a ship lands. Decision: derive it from the SERVER's own merge diff ([ADR 0055](0055-server-mediated-branch-publish.md) — the server holds the repo and does the merge, so it computes the diff rather than being told one), and attribute the deleted hunks through **`claims.published_head_sha`** (`migrations/171`), NOT by parsing `(task NNNN)` out of commit text — only 7 of `main`'s last 15 commits carry a task id, and a convention that holds most of the time cannot underwrite a payout. An UNATTRIBUTABLE deletion files nothing: a wrong proposal costs attention and dims a real star. A rename, a move and a rewrite-in-place are NOT removals, and a cold body is not a remnant — removal applies only below a task that actually shipped. The proposal is an appended `CASCADES` entry filing a QUESTION; confirmation is Metic+/`task.promote` on the existing awaiting-nod surface; an unanswered one rots on the normal clock. Only a CONFIRMED proposal dims a star. The derivation must be NON-BLOCKING — landing work matters more than dimming a star on time. Brightness can only fall forward from here, so historical sizes need a marker or the number lies.](0313-a-removal-is-proposed-by-the-ship-and-confirmed-by-a-person.md) | ideas / lifecycle |
408
+ | 0314 | [**A module owns a protected path without the whole module becoming protected** ([task 1004070](https://cloudbongos.com/builders#/task/1004070), goal 1000095 — BONGOS-V2). Three modules derive `protected` — and so are Archon-only to scope a goal to — because of ONE file that is not their domain: `onboarding` on `scripts/gds/setup.js`, `autonomy` on `scripts/gds/conductor-dispatch.js`, `copy-desk` on `scripts/gds/copy-inventory.js`. The owner chose *narrow what counts as protected*; the task set the fork and required the choice be justified before any glob moved. **Deleting the glob is out**: `MODULE_GLOBS` is also the territory deciding who may AUTHOR a task on a path, and the shared floor is only `tests/`, so an unowned path is authorable by no goal at all. That left reassigning each CLI to an already-protected module — **measured, not reasoned about**, by applying the edit to the source and re-requiring in a fresh process (`MODULE_SCOPE_MAP` is frozen and built from a COPY of `MODULE_GLOBS` at load, so an in-process mutation proves nothing and passes green). The measurement kills it: `copy-desk` and `autonomy` lose the ability to author on their own generator and their own conductor, and — the row that decides it — `devsecops` as the new owner **cannot author the generator together with the two artifacts it writes**, because `docs/copy-registry.json` and `docs/copy-inventory.md` stay `copy-desk`'s. So *change a UI string, regenerate the registry* becomes authorable by no single goal, and that is the ship-check freshness gate every UI task hits. Decision: **protection becomes per-path at the goal-scope wall, as it already is at the other three** — `matchProtected` decides task-authoring, the grader and gate decide ship, the post-push audit decides the dishonest case; only goal-creation asked about the MODULE, and that inconsistency is the whole defect. The derivation stays computed from `PROTECTED_GLOBS` and no flag is hand-set ([ADR 0086](0086-goal-scoped-work-hierarchy.md) §7 / SEC #863), and the Metic floor on those paths does not move. **Corrects the task's own numbers**: 12 of 26 modules derive protected, not 11, and `agents` (protected by `modules/agents/`, itself a protected glob) is absent from its list of seven — which is why the one thing this ADR SHIPS is a test pinning the full protected key set **by name**, a count assertion passing on the wrong twelve being exactly how that drifted. Implementation is deferred to task 1004109, per the task's own instruction to stop after the ADR. Also filed: `builder-settings` (task 1004110), whose overlap rides the blanket `src/bongos/routes/` prefix and so needs either the `me.js` strangler move or its own security ADR; and **nine protected paths owned by no module at all** (task 1004111) — the freshness generators, the merge-driver pair and `cli-lib.js` among them — found while checking whether deletion was viable, a pre-existing hole rather than one this opens. Does not decide: the floor a protected path carries, whether `scripts/gds/` deserves a shared-floor entry the way `tests/` has one, or how the nine unowned paths get a home.](0314-module-protection-is-per-path-not-per-module.md) | modules / permissions |
@@ -152,7 +152,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
152
152
  | `8b736e365191` | certain | All builders | `modules/hall-ui/public/hall-render.js:1238` |
153
153
  | `b5e03617b319` | certain | All goals | `modules/hall-ui/public/goals.html:53` |
154
154
  | `e08d410571e9` | certain | All kinds | `modules/hall-ui/public/sessions.js:286` |
155
- | `9c34bccb06ef` | certain | All open asks | `modules/hall-ui/public/collab.html:71` |
155
+ | `9c34bccb06ef` | certain | All open asks | `modules/hall-ui/public/collab.html:73` |
156
156
  | `bea7817e57a4` | certain | All surfaces | `modules/hall-ui/public/copy-desk.js:301` |
157
157
  | `627c683a5e5d` | certain | All time | `modules/hall-ui/public/hall-widgets.js:130` |
158
158
  | `b38c01691857` | certain | All your work → | `modules/hall-ui/public/hall-widgets.js:110` |
@@ -163,7 +163,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
163
163
  | `69f0ca4a4d84` | certain | Analytics scope | `modules/hall-ui/public/hall-render.js:1236` |
164
164
  | `14bdceaac078` | certain | Answer the five questions and this spark becomes a Full Idea. | `modules/hall-ui/public/ideas.js:692` |
165
165
  | `0293418d5e8e` | certain | Answerable for: | `modules/hall-ui/public/profile.js:72` |
166
- | `05761b08ec8f` | certain | Answering is optional and declining costs nothing. Nobody is assigned by being asked. | `modules/hall-ui/public/collab.html:97` |
166
+ | `05761b08ec8f` | certain | Answering is optional and declining costs nothing. Nobody is assigned by being asked. | `modules/hall-ui/public/collab.html:100` |
167
167
  | `52ed0e2e3470` | certain | Any rank | `modules/hall-ui/public/task-lib.js:62` |
168
168
  | `8832c7204180` | certain | Anyone in the owning project | `modules/hall-ui/public/settings.html:291` |
169
169
  | `7d2ab55d4112` | certain | Anyone on this instance | `modules/hall-ui/public/settings.html:292` |
@@ -176,7 +176,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
176
176
  | `5fcfdf8975cf` | certain | As configuration: | `modules/hall-ui/public/government.js:829` |
177
177
  | `01a0f29ff406` | certain | As first filed | `modules/hall-ui/public/idea.js:252` |
178
178
  | `688cc6dd38ee` | certain | Asked of you | `modules/hall-ui/public/collab.html:48` |
179
- | `b2656d0b2402` | certain | Asked {…} | `modules/hall-ui/public/collab.js:58` |
179
+ | `b2656d0b2402` | certain | Asked {…} | `modules/hall-ui/public/collab.js:66` |
180
180
  | `b2656d0b2402` | certain | Asked {…} | `modules/hall-ui/public/watch.js:488` |
181
181
  | `8c8ec2597036` | certain | Assigned {…} to builder {…}. | `modules/hall-ui/public/government.js:463` |
182
182
  | `46714066f2da` | certain | Audit log | `modules/hall-ui/public/watch.html:141` |
@@ -275,7 +275,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
275
275
  | `12ee050d0180` | certain | Copy desk | `modules/hall-ui/public/copy-desk.html:27` |
276
276
  | `1214ea8e0d1b` | certain | Copy desk — {{worldName}} | `modules/hall-ui/public/copy-desk.html:6` |
277
277
  | `1214ea8e0d1b` | certain | Copy desk — {{worldName}} | `modules/hall-ui/public/copy-desk.html:6` |
278
- | `30764a4f0e28` | certain | Copy for Session Start | `modules/hall-ui/public/collab.js:90` |
278
+ | `30764a4f0e28` | certain | Copy for Session Start | `modules/hall-ui/public/collab.js:98` |
279
279
  | `ebc8a6c16a22` | certain | Copy parallel prompt | `modules/hall-ui/public/work.html:146` |
280
280
  | `a873e7c29d80` | certain | Copy prompt | `modules/hall-ui/public/task.js:795` |
281
281
  | `a873e7c29d80` | certain | Copy prompt | `modules/hall-ui/public/task.js:816` |
@@ -303,7 +303,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
303
303
  | `90691bceaf68` | certain | Could not load | `modules/hall-ui/public/task.js:982` |
304
304
  | `589a4cfc5442` | certain | Could not load idea {…}. | `modules/hall-ui/public/ideas.js:735` |
305
305
  | `192c893fc64e` | certain | Could not load modules. | `modules/hall-ui/public/modules.js:318` |
306
- | `9912c3835136` | certain | Could not load requests — reload to try again. | `modules/hall-ui/public/collab.js:529` |
306
+ | `9912c3835136` | certain | Could not load requests — reload to try again. | `modules/hall-ui/public/collab.js:551` |
307
307
  | `7775fec0fa85` | certain | Could not load sessions. | `modules/hall-ui/public/sessions.js:399` |
308
308
  | `7775fec0fa85` | certain | Could not load sessions. | `modules/hall-ui/public/sessions.js:437` |
309
309
  | `a2f2e8babef5` | certain | Could not load that page. Try again. | `modules/hall-ui/public/blockers.js:274` |
@@ -603,7 +603,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
603
603
  | `c7a670e9552d` | certain | Merged into | `modules/hall-ui/public/profile.js:489` |
604
604
  | `dce0546c9e34` | certain | Merging work does not put it in front of anyone. This is the step that does: it moves this platform — or any p | `modules/hall-ui/public/deploy.html:39` |
605
605
  | `7637613c231b` | certain | Metic rank and above may vote | `modules/hall-ui/public/builders.js:781` |
606
- | `91328a97fe68` | certain | Mingle pairs you with someone from another role. Saying no costs nothing. | `modules/hall-ui/public/collab.html:87` |
606
+ | `91328a97fe68` | certain | Mingle pairs you with someone from another role. Saying no costs nothing. | `modules/hall-ui/public/collab.html:90` |
607
607
  | `d7ce4a3bb745` | certain | Minimize the welcome panel | `modules/hall-ui/public/hall-widgets.js:52` |
608
608
  | `19b0ed5781ae` | certain | Minimum rank required to claim this task | `modules/hall-ui/public/task-lib.js:65` |
609
609
  | `64e91044df43` | certain | Mint a fresh CLI token using this browser session and paste it into your terminal — the quick way back in when | `modules/hall-ui/public/settings.html:145` |
@@ -644,7 +644,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
644
644
  | `fbb887a9b678` | certain | No goal has criteria met yet — work is in the task queue. | `modules/hall-ui/public/roadmap.js:128` |
645
645
  | `6771a8966ec3` | certain | No goals declared yet — this version is tracked by its {…} done-when criteria. | `modules/hall-ui/public/roadmap.js:106` |
646
646
  | `abc64639b134` | certain | No goals seeded yet. | `modules/hall-ui/public/roadmap.js:205` |
647
- | `139ec3636c22` | certain | No introductions right now. One appears here when a pairing round runs. | `modules/hall-ui/public/collab.html:90` |
647
+ | `139ec3636c22` | certain | No introductions right now. One appears here when a pairing round runs. | `modules/hall-ui/public/collab.html:93` |
648
648
  | `3b9823dd4579` | certain | No maintenance declared — nobody is on the hook for this module as the core advances. | `modules/hall-ui/public/modules.js:92` |
649
649
  | `64a4281934d1` | certain | No match. Try a page name, or an id like | `modules/hall-ui/public/palette.js:182` |
650
650
  | `4f89817b92e5` | certain | No onboarding steps recorded yet. | `modules/hall-ui/public/hall-render.js:941` |
@@ -662,13 +662,13 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
662
662
  | `c9800f8b6bc8` | certain | No versions declared yet. | `modules/hall-ui/public/roadmap.js:427` |
663
663
  | `4654849396a3` | certain | No work reviewed yet. | `modules/hall-ui/public/hall-widgets.js:150` |
664
664
  | `086665af6208` | certain | No {…} sessions recorded. Choose another kind to widen the ledger. | `modules/hall-ui/public/sessions.js:145` |
665
- | `1967d2176f2e` | certain | Nobody has an open ask right now. | `modules/hall-ui/public/collab.html:75` |
666
- | `c65c9f5c02e3` | certain | Nobody is waiting on you. Requests reach you by role — | `modules/hall-ui/public/collab.html:53` |
665
+ | `1967d2176f2e` | certain | Nobody has an open ask right now. | `modules/hall-ui/public/collab.html:78` |
666
+ | `c65c9f5c02e3` | certain | Nobody is waiting on you. Requests reach you by role — | `modules/hall-ui/public/collab.html:54` |
667
667
  | `91c409a4aea5` | certain | Nobody vouched for this name — anyone can file a request naming anyone. Confirm who is asking before you admit | `modules/hall-ui/public/watch.js:374` |
668
668
  | `c72e0bdb834a` | certain | Not declared in module.json — this is who ships the module. | `modules/hall-ui/public/modules.js:62` |
669
669
  | `361b353ded5e` | certain | Not enough shipped work yet to chart. | `modules/hall-ui/public/hall-widgets.js:184` |
670
670
  | `90230572d017` | certain | Not found | `modules/hall-ui/public/task.js:970` |
671
- | `8226c979db4d` | certain | Not now | `modules/hall-ui/public/collab.js:435` |
671
+ | `8226c979db4d` | certain | Not now | `modules/hall-ui/public/collab.js:457` |
672
672
  | `5ffcdc1a0176` | certain | Not scored — this builder’s own profile waived every signal, or the session predates this measure. | `modules/hall-ui/public/sessions.js:100` |
673
673
  | `61250b49a90b` | certain | Not scored. | `modules/hall-ui/public/idea.js:84` |
674
674
  | `8d6afe6def47` | certain | Not signed in | `modules/hall-ui/public/settings.html:40` |
@@ -780,7 +780,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
780
780
  | `b03e17693dcd` | certain | Read it as a table | `modules/hall-ui/public/drachmae.js:148` |
781
781
  | `b03e17693dcd` | certain | Read it as a table | `modules/hall-ui/public/watch.js:1132` |
782
782
  | `b6fcfdd652a7` | certain | Read it here | `modules/hall-ui/public/work.html:45` |
783
- | `053df132e22c` | certain | Read the full ask | `modules/hall-ui/public/collab.js:90` |
783
+ | `053df132e22c` | certain | Read the full ask | `modules/hall-ui/public/collab.js:98` |
784
784
  | `c5f19a888a15` | certain | Read the handoff notes | `modules/hall-ui/public/task.js:481` |
785
785
  | `f9e998f4af3d` | certain | Read to the end and the button below unlocks. It is an honour system — nothing here checks your comprehension. | `modules/hall-ui/public/primer.html:100` |
786
786
  | `26e6c9879035` | certain | Reading the diagrams | `modules/hall-ui/public/diagrams.html:39` |
@@ -847,7 +847,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
847
847
  | `42cbc20a6ec8` | certain | Saves when you press Save; your next | `modules/hall-ui/public/settings.html:136` |
848
848
  | `645dd8117cf3` | certain | Say why it is being retired — the reason goes in the record. | `modules/hall-ui/public/task.js:924` |
849
849
  | `fc9d783ed0f5` | certain | Say why it is better. Whoever reviews this decides it from your reason, without asking you. | `modules/hall-ui/public/copy-desk.js:446` |
850
- | `12c1ec21a2c6` | certain | Say yes | `modules/hall-ui/public/collab.js:435` |
850
+ | `12c1ec21a2c6` | certain | Say yes | `modules/hall-ui/public/collab.js:457` |
851
851
  | `46711f7bc3df` | certain | Search every string the product shows a person, by what it says or the file it lives in. | `modules/hall-ui/public/copy-desk.html:65` |
852
852
  | `5d2ba8e72fa1` | certain | Search failed. | `modules/hall-ui/public/copy-desk.js:343` |
853
853
  | `373a7639bcb9` | certain | Sections in this primer | `modules/hall-ui/public/primer.html:52` |
@@ -882,7 +882,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
882
882
  | `ac44b08aec92` | certain | Ship it | `modules/hall-ui/public/task.js:710` |
883
883
  | `b94a6b5b3d8c` | certain | Shipped with the core | `modules/hall-ui/public/modules.html:56` |
884
884
  | `0c0a50ba1666` | certain | Shipped {…}{…}, {…}. | `modules/hall-ui/public/task.js:691` |
885
- | `6dde12eb0c78` | certain | Show all {…} | `modules/hall-ui/public/collab.js:253` |
885
+ | `6dde12eb0c78` | certain | Show all {…} | `modules/hall-ui/public/collab.js:275` |
886
886
  | `14d32a35c15f` | certain | Show my cross-project record | `modules/hall-ui/public/settings.js:514` |
887
887
  | `dc0f9fe50a65` | certain | Show the setup guide again | `modules/hall-ui/public/index.html:64` |
888
888
  | `2af4c0f2b4b9` | certain | Showing {…} of {…}. | `modules/hall-ui/public/ideas.js:638` |
@@ -905,7 +905,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
905
905
  | `87cee78ca134` | certain | Skip to content | `modules/hall-ui/public/shell.js:663` |
906
906
  | `688ca7b6e30f` | certain | Slop — reads as machine-written | `modules/hall-ui/public/copy-desk.html:90` |
907
907
  | `82df09a08476` | certain | So the Archon knows who you are | `modules/hall-ui/public/hall-render.js:294` |
908
- | `7da6a848cd26` | certain | Someone helped | `modules/hall-ui/public/collab.js:203` |
908
+ | `7da6a848cd26` | certain | Someone helped | `modules/hall-ui/public/collab.js:211` |
909
909
  | `e7b592185803` | certain | Someone is holding a claim on this task. It has to be released before the task can be retired. | `modules/hall-ui/public/task.js:753` |
910
910
  | `867df58c9aaa` | certain | Something else | `modules/hall-ui/public/copy-desk.html:95` |
911
911
  | `c377d8ed744b` | certain | Something went wrong. Try again. | `modules/hall-ui/public/profile-connections.js:275` |
@@ -1132,14 +1132,14 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1132
1132
  | `23c68db6db4e` | certain | You created this task, but it has already moved past planning. Retiring it now is a change to the record, so i | `modules/hall-ui/public/task.js:776` |
1133
1133
  | `cf0c107a39fc` | certain | You created this task, but it has shipped. Shipped work is immutable history — nobody can retire it, whatever | `modules/hall-ui/public/task.js:769` |
1134
1134
  | `b91183c02c63` | certain | You created this task, but someone is holding a claim on it. Once that is released you can retire it yourself. | `modules/hall-ui/public/task.js:774` |
1135
- | `da274462bb30` | certain | You have no open asks. | `modules/hall-ui/public/collab.html:64` |
1135
+ | `da274462bb30` | certain | You have no open asks. | `modules/hall-ui/public/collab.html:66` |
1136
1136
  | `6b759151f722` | certain | You have reached the end. The button below is now active. | `modules/hall-ui/public/primer.js:309` |
1137
1137
  | `6d5a4ca58dd6` | certain | You haven’t joined any goals yet. | `modules/hall-ui/public/goals.js:83` |
1138
1138
  | `8e527ab12334` | certain | You {…}. One vote per member. | `modules/hall-ui/public/board-room.js:348` |
1139
1139
  | `8e1915b19274` | certain | You're in the {…} server with your rank role. | `modules/hall-ui/public/settings.js:833` |
1140
1140
  | `d8c542541c15` | certain | You're not signed in yet. Sign in to manage your connections, your keys, your dev box and the way the hall beh | `modules/hall-ui/public/settings.html:41` |
1141
1141
  | `3a6ed99ec333` | certain | You've been approved before — sign in and pick up where you left off. | `modules/hall-ui/public/hall-render.js:367` |
1142
- | `7d26333b5fb0` | certain | Your asks | `modules/hall-ui/public/collab.html:61` |
1142
+ | `7d26333b5fb0` | certain | Your asks | `modules/hall-ui/public/collab.html:62` |
1143
1143
  | `f96bc227dba0` | certain | Your choice | `modules/hall-ui/public/settings.html:359` |
1144
1144
  | `f20a4adfd5a8` | certain | Your dev box builds itself in a minute or two and the app chimes when it's ready. It lives in the menu bar on | `modules/hall-ui/public/hall-render.js:825` |
1145
1145
  | `909b0b7c6ffa` | certain | Your display name | `modules/hall-ui/public/settings.html:84` |
@@ -1284,7 +1284,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1284
1284
  | `594962dc9ec3` | certain | s into the view containers below (falling back to | `modules/hall-ui/public/index.html:52` |
1285
1285
  | `2a42549b34ae` | certain | said, when flagged: | `modules/hall-ui/public/copy-desk.js:124` |
1286
1286
  | `9bec9eb4829e` | certain | scope: {…} | `modules/hall-ui/public/goals-render.js:836` |
1287
- | `a13ee0cde7f2` | certain | set yours in Settings | `modules/hall-ui/public/collab.html:54` |
1287
+ | `a13ee0cde7f2` | certain | set yours in Settings | `modules/hall-ui/public/collab.html:55` |
1288
1288
  | `a6f55d7aa6f7` | certain | shipped (live) | `modules/hall-ui/public/roadmap.js:298` |
1289
1289
  | `026ba1289d4d` | certain | shipped · | `modules/hall-ui/public/goals-render.js:758` |
1290
1290
  | `ec6d8c900bc8` | certain | sign in | `modules/hall-ui/public/profile-charts.js:502` |
@@ -541,7 +541,7 @@
541
541
  "text": "Asked {…}",
542
542
  "occurrences": 2,
543
543
  "at": [
544
- "modules/hall-ui/public/collab.js:58",
544
+ "modules/hall-ui/public/collab.js:66",
545
545
  "modules/hall-ui/public/watch.js:488"
546
546
  ]
547
547
  },
@@ -2412,7 +2412,7 @@
2412
2412
  "surface": "builders-hall",
2413
2413
  "text": "All open asks",
2414
2414
  "file": "modules/hall-ui/public/collab.html",
2415
- "line": 71,
2415
+ "line": 73,
2416
2416
  "origin": "html-text",
2417
2417
  "confidence": "certain"
2418
2418
  },
@@ -2511,7 +2511,7 @@
2511
2511
  "surface": "builders-hall",
2512
2512
  "text": "Answering is optional and declining costs nothing. Nobody is assigned by being asked.",
2513
2513
  "file": "modules/hall-ui/public/collab.html",
2514
- "line": 97,
2514
+ "line": 100,
2515
2515
  "origin": "html-text",
2516
2516
  "confidence": "certain"
2517
2517
  },
@@ -2628,7 +2628,7 @@
2628
2628
  "surface": "builders-hall",
2629
2629
  "text": "Asked {…}",
2630
2630
  "file": "modules/hall-ui/public/collab.js",
2631
- "line": 58,
2631
+ "line": 66,
2632
2632
  "origin": "js-markup",
2633
2633
  "confidence": "certain"
2634
2634
  },
@@ -3519,7 +3519,7 @@
3519
3519
  "surface": "builders-hall",
3520
3520
  "text": "Copy for Session Start",
3521
3521
  "file": "modules/hall-ui/public/collab.js",
3522
- "line": 90,
3522
+ "line": 98,
3523
3523
  "origin": "js-markup",
3524
3524
  "confidence": "certain"
3525
3525
  },
@@ -3771,7 +3771,7 @@
3771
3771
  "surface": "builders-hall",
3772
3772
  "text": "Could not load requests — reload to try again.",
3773
3773
  "file": "modules/hall-ui/public/collab.js",
3774
- "line": 529,
3774
+ "line": 551,
3775
3775
  "origin": "js-text-assign",
3776
3776
  "confidence": "certain"
3777
3777
  },
@@ -6471,7 +6471,7 @@
6471
6471
  "surface": "builders-hall",
6472
6472
  "text": "Mingle pairs you with someone from another role. Saying no costs nothing.",
6473
6473
  "file": "modules/hall-ui/public/collab.html",
6474
- "line": 87,
6474
+ "line": 90,
6475
6475
  "origin": "html-text",
6476
6476
  "confidence": "certain"
6477
6477
  },
@@ -6840,7 +6840,7 @@
6840
6840
  "surface": "builders-hall",
6841
6841
  "text": "No introductions right now. One appears here when a pairing round runs.",
6842
6842
  "file": "modules/hall-ui/public/collab.html",
6843
- "line": 90,
6843
+ "line": 93,
6844
6844
  "origin": "html-text",
6845
6845
  "confidence": "certain"
6846
6846
  },
@@ -7002,7 +7002,7 @@
7002
7002
  "surface": "builders-hall",
7003
7003
  "text": "Nobody has an open ask right now.",
7004
7004
  "file": "modules/hall-ui/public/collab.html",
7005
- "line": 75,
7005
+ "line": 78,
7006
7006
  "origin": "html-text",
7007
7007
  "confidence": "certain"
7008
7008
  },
@@ -7011,7 +7011,7 @@
7011
7011
  "surface": "builders-hall",
7012
7012
  "text": "Nobody is waiting on you. Requests reach you by role —",
7013
7013
  "file": "modules/hall-ui/public/collab.html",
7014
- "line": 53,
7014
+ "line": 54,
7015
7015
  "origin": "html-text",
7016
7016
  "confidence": "certain"
7017
7017
  },
@@ -7056,7 +7056,7 @@
7056
7056
  "surface": "builders-hall",
7057
7057
  "text": "Not now",
7058
7058
  "file": "modules/hall-ui/public/collab.js",
7059
- "line": 435,
7059
+ "line": 457,
7060
7060
  "origin": "js-markup",
7061
7061
  "confidence": "certain"
7062
7062
  },
@@ -8064,7 +8064,7 @@
8064
8064
  "surface": "builders-hall",
8065
8065
  "text": "Read the full ask",
8066
8066
  "file": "modules/hall-ui/public/collab.js",
8067
- "line": 90,
8067
+ "line": 98,
8068
8068
  "origin": "js-markup",
8069
8069
  "confidence": "certain"
8070
8070
  },
@@ -8667,7 +8667,7 @@
8667
8667
  "surface": "builders-hall",
8668
8668
  "text": "Say yes",
8669
8669
  "file": "modules/hall-ui/public/collab.js",
8670
- "line": 435,
8670
+ "line": 457,
8671
8671
  "origin": "js-markup",
8672
8672
  "confidence": "certain"
8673
8673
  },
@@ -8982,7 +8982,7 @@
8982
8982
  "surface": "builders-hall",
8983
8983
  "text": "Show all {…}",
8984
8984
  "file": "modules/hall-ui/public/collab.js",
8985
- "line": 253,
8985
+ "line": 275,
8986
8986
  "origin": "js-text-assign",
8987
8987
  "confidence": "certain"
8988
8988
  },
@@ -9189,7 +9189,7 @@
9189
9189
  "surface": "builders-hall",
9190
9190
  "text": "Someone helped",
9191
9191
  "file": "modules/hall-ui/public/collab.js",
9192
- "line": 203,
9192
+ "line": 211,
9193
9193
  "origin": "js-markup",
9194
9194
  "confidence": "certain"
9195
9195
  },
@@ -11232,7 +11232,7 @@
11232
11232
  "surface": "builders-hall",
11233
11233
  "text": "You have no open asks.",
11234
11234
  "file": "modules/hall-ui/public/collab.html",
11235
- "line": 64,
11235
+ "line": 66,
11236
11236
  "origin": "html-text",
11237
11237
  "confidence": "certain"
11238
11238
  },
@@ -11295,7 +11295,7 @@
11295
11295
  "surface": "builders-hall",
11296
11296
  "text": "Your asks",
11297
11297
  "file": "modules/hall-ui/public/collab.html",
11298
- "line": 61,
11298
+ "line": 62,
11299
11299
  "origin": "html-text",
11300
11300
  "confidence": "certain"
11301
11301
  },
@@ -12600,7 +12600,7 @@
12600
12600
  "surface": "builders-hall",
12601
12601
  "text": "set yours in Settings",
12602
12602
  "file": "modules/hall-ui/public/collab.html",
12603
- "line": 54,
12603
+ "line": 55,
12604
12604
  "origin": "html-text",
12605
12605
  "confidence": "certain"
12606
12606
  },
@@ -2288,5 +2288,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
2288
2288
  landed since 1.19.905 with no explicit bump. run 35514202453. (task 1002620)
2289
2289
  1.19.907 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2290
2290
  landed since 1.19.906 with no explicit bump. run 35514739370. (task 1002620)
2291
+ 1.19.908 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2292
+ landed since 1.19.907 with no explicit bump. run 35516559010. (task 1002620)
2293
+ 1.19.909 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2294
+ landed since 1.19.908 with no explicit bump. run 35517426559. (task 1002620)
2291
2295
  ---------------------------------------------------------------------------
2292
2296
  ```
@@ -73,6 +73,75 @@
73
73
  return 'waiting ' + weeks + ' weeks';
74
74
  }
75
75
 
76
+ // A PILL MARKS A ROW WORSE THAN ITS ROOM (task 1004067, from idea 1001065).
77
+ //
78
+ // Task 1004041 fixed half of this: the words now carry the real age, and the
79
+ // two calm bands lost their ring. What it could not fix from inside a single
80
+ // row is the case the report is actually about — a room where EVERY row is
81
+ // genuinely past a week. There the condition is not too loose; it is correct
82
+ // on all fourteen, and fourteen correct alarms are still not a signal. The
83
+ // instrument is wrong, not the threshold: a per-row badge can only say "this
84
+ // row is bad", and it has no way to say "this row is bad AND so is every other
85
+ // one", which is the fact a reader needs.
86
+ //
87
+ // So the room states its own condition once, in the block head, and a row
88
+ // wears the pill only when it is MORE SEVERE than that baseline. On a mixed
89
+ // board no band is dominant, nothing is muted, and the page reads exactly as
90
+ // it does today. On a uniformly stale board the alarm is said once instead of
91
+ // fourteen times, and the per-row words still carry each real age — so nothing
92
+ // goes silently dead, which is the criterion the bands exist to satisfy.
93
+ //
94
+ // Two dials, both deliberately blunt. A room under BASELINE_MIN_ROWS is not a
95
+ // wall — four pills can be read one at a time, so a short list keeps every
96
+ // one. BASELINE_SHARE is below 1 because thirteen-of-fourteen reads as a wall
97
+ // just as much as fourteen does, and a rule that only fires on unanimity is a
98
+ // rule that almost never fires.
99
+ var BASELINE_MIN_ROWS = 4;
100
+ var BASELINE_SHARE = 0.7;
101
+
102
+ // Only a band that actually WEARS a pill can be a baseline (severity >= 2 —
103
+ // aging and stale; collab.css rings those two and leaves the calm pair as
104
+ // plain text). A room that is uniformly 'just asked' has no alarm to move to
105
+ // the head, and announcing one there would invent the very noise this removes.
106
+ function roomBaseline(reqs, nowMs) {
107
+ var list = Array.isArray(reqs) ? reqs : [];
108
+ if (list.length < BASELINE_MIN_ROWS) return null;
109
+ var counts = {};
110
+ for (var i = 0; i < list.length; i += 1) {
111
+ var id = ageBand(list[i] && list[i].created_at, nowMs).id;
112
+ counts[id] = (counts[id] || 0) + 1;
113
+ }
114
+ // BANDS is severest-first, and two bands cannot both clear a share above a
115
+ // half, so the first match is the only match.
116
+ for (var j = 0; j < BANDS.length; j += 1) {
117
+ var band = BANDS[j];
118
+ var n = counts[band.id] || 0;
119
+ if (band.severity >= 2 && n / list.length >= BASELINE_SHARE) {
120
+ return { band: band, count: n, total: list.length };
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+
126
+ // At or below the room's baseline, a row is the rule and reads quiet; above
127
+ // it, a row is the exception and keeps its pill. With no baseline nothing is
128
+ // quieted, which is what makes this inert on every mixed board.
129
+ function isRoomQuiet(band, baseline) {
130
+ if (!band || !baseline) return false;
131
+ return band.severity <= baseline.band.severity;
132
+ }
133
+
134
+ // The fact the pills used to repeat, said once. The band's own label supplies
135
+ // the words, so the head and the rows can never describe the same threshold
136
+ // two different ways.
137
+ function roomAgeLine(baseline) {
138
+ if (!baseline) return '';
139
+ var many = baseline.count === baseline.total
140
+ ? 'all ' + baseline.total
141
+ : baseline.count + ' of ' + baseline.total;
142
+ return many + ' ' + baseline.band.label;
143
+ }
144
+
76
145
  // Oldest first WITHIN the page, because the thing most at risk of dying
77
146
  // silently should be read first. This is in ADDITION to the band, never
78
147
  // instead of it.
@@ -336,6 +405,8 @@
336
405
 
337
406
  window.OTBCollabLib = {
338
407
  BANDS: BANDS, ageBand: ageBand, ageLabel: ageLabel,
408
+ roomBaseline: roomBaseline, isRoomQuiet: isRoomQuiet, roomAgeLine: roomAgeLine,
409
+ BASELINE_MIN_ROWS: BASELINE_MIN_ROWS, BASELINE_SHARE: BASELINE_SHARE,
339
410
  byAgeOldestFirst: byAgeOldestFirst, byNewestFirst: byNewestFirst,
340
411
  askerOf: askerOf, contextOf: contextOf,
341
412
  directionLine: directionLine, boardLine: boardLine,
@@ -43,6 +43,33 @@
43
43
  padding-right: 0;
44
44
  }
45
45
 
46
+ /* THE ROOM'S OWN CONDITION IS NOT A PER-ROW WARNING (task 1004067). When a
47
+ block is uniformly old, every row's band is correct and every row's pill is
48
+ therefore identical — the wall the report describes. A row at or below its
49
+ room's baseline drops to the same plain quiet text the calm bands wear, so
50
+ the only chips left in the column are the rows that are worse than their
51
+ room. Two classes, so this outranks the band ink it replaces without an
52
+ !important. The WORDS are untouched: each row still states its own real age,
53
+ which is what keeps an old ask visibly aging rather than silently dead. */
54
+ .cb-age.cb-age--quiet {
55
+ color: var(--ink-soft);
56
+ border-color: transparent;
57
+ padding-left: 0;
58
+ padding-right: 0;
59
+ font-weight: 400;
60
+ }
61
+
62
+ /* The fact the chips used to repeat, stated once in the head. It sits in the
63
+ band's own ink because here it IS the exception — one line on a page, not one
64
+ per row — and it is the only thing on the head that is allowed to be loud. */
65
+ .cb-block__age {
66
+ font-size: 13px;
67
+ letter-spacing: 0;
68
+ color: var(--ink-soft);
69
+ }
70
+ .cb-block__age--aging { color: var(--warn); }
71
+ .cb-block__age--stale { color: var(--danger); font-weight: 600; }
72
+
46
73
  /* ---- the figure strip -----------------------------------------------------
47
74
  The shell's .stat-row, unchanged, except that this page's third tile holds
48
75
  WORDS (the roles you answer for) rather than a figure. The shell's own
@@ -46,6 +46,7 @@
46
46
  <section class="scroll cb-block" aria-labelledby="cb-for-me-h">
47
47
  <div class="cb-block__head">
48
48
  <h2 class="scroll__h" id="cb-for-me-h">Asked of you <span class="cb-block__n" id="cb-for-me-n"></span></h2>
49
+ <span class="cb-block__age" id="cb-for-me-age" hidden></span>
49
50
  <span class="cb-faces" id="cb-for-me-faces" aria-hidden="true"></span>
50
51
  </div>
51
52
  <ul class="ov-rows" id="cb-for-me"></ul>
@@ -59,6 +60,7 @@
59
60
  <section class="scroll cb-block" aria-labelledby="cb-mine-h">
60
61
  <div class="cb-block__head">
61
62
  <h2 class="scroll__h" id="cb-mine-h">Your asks <span class="cb-block__n" id="cb-mine-n"></span></h2>
63
+ <span class="cb-block__age" id="cb-mine-age" hidden></span>
62
64
  </div>
63
65
  <ul class="ov-rows" id="cb-mine"></ul>
64
66
  <div class="empty-state cb-empty" id="cb-mine-empty" hidden><p>You have no open asks.</p></div>
@@ -69,6 +71,7 @@
69
71
  <section class="scroll cb-block cb-block--all" aria-labelledby="cb-all-h">
70
72
  <div class="cb-block__head">
71
73
  <h2 class="scroll__h" id="cb-all-h">All open asks <span class="cb-block__n" id="cb-all-n"></span></h2>
74
+ <span class="cb-block__age" id="cb-all-age" hidden></span>
72
75
  <span class="cb-faces" id="cb-all-faces" aria-hidden="true"></span>
73
76
  </div>
74
77
  <ul class="ov-rows" id="cb-all"></ul>
@@ -53,9 +53,17 @@
53
53
  // exact timestamp stays on the title, where a reader who wants the date can
54
54
  // still reach it — which is also why the separate date line that used to sit
55
55
  // under this pill is gone: the same fact twice is one fact too many.
56
- function ageChip(req, nowMs) {
56
+ //
57
+ // `baseline` is the block's own room condition (L.roomBaseline), and it is the
58
+ // second half of the same verdict: where the label stops fourteen old rows
59
+ // reading as fourteen copies of one alarm, the baseline stops them WEARING
60
+ // fourteen copies of one alarm. A row at or below its room's baseline is the
61
+ // rule, not the exception, so it drops the ring and the danger ink and keeps
62
+ // only its words — the head says the room's condition once instead.
63
+ function ageChip(req, nowMs, baseline) {
57
64
  const band = L.ageBand(req.created_at, nowMs);
58
- return `<span class="fact-pill cb-age cb-age--${band.id}" title="Asked ${escapeHtml(String(req.created_at || ''))}">${escapeHtml(L.ageLabel(req.created_at, nowMs))}</span>`;
65
+ const quiet = L.isRoomQuiet(band, baseline) ? ' cb-age--quiet' : '';
66
+ return `<span class="fact-pill cb-age cb-age--${band.id}${quiet}" title="Asked ${escapeHtml(String(req.created_at || ''))}">${escapeHtml(L.ageLabel(req.created_at, nowMs))}</span>`;
59
67
  }
60
68
 
61
69
  // A DOM id per row that is stable, unique and safe to put in an attribute. The
@@ -175,14 +183,14 @@
175
183
  // chat mirror). What the page owes a helper is the request, its age, its
176
184
  // context, and who to answer — and their face. A by-name ask is the personal
177
185
  // call on this reader, so its row is the LIT one (.cb-row--live).
178
- function forMeRow(req, nowMs, extra, key) {
186
+ function forMeRow(req, nowMs, extra, key, baseline) {
179
187
  const live = req.needs_builder_id != null ? ' cb-row--live' : '';
180
188
  return `<li class="ov-row ov-row--lead cb-row${live}" data-id="${escapeHtml(String(req.id))}"${extra ? ' data-extra hidden' : ''}>
181
189
  ${faceHtml(L.faceOf(req, 'in'))}
182
190
  <div class="ov-row__main">${askHtml(req, key)}
183
191
  <div class="ov-row__facts">${directionFact(req, 'in')}${contextChip(req)}</div>
184
192
  </div>
185
- <div class="ov-row__right">${ageChip(req, nowMs)}</div>
193
+ <div class="ov-row__right">${ageChip(req, nowMs, baseline)}</div>
186
194
  </li>`;
187
195
  }
188
196
 
@@ -198,12 +206,12 @@
198
206
  // which is precisely the distinction a reader could no longer make. Moving the
199
207
  // pill down to the facts line puts the warning with the other facts about the
200
208
  // ask and leaves the right column as what it is here: the place you act.
201
- function mineRow(req, nowMs, extra, key) {
209
+ function mineRow(req, nowMs, extra, key, baseline) {
202
210
  const id = escapeHtml(String(req.id));
203
211
  return `<li class="ov-row ov-row--lead cb-row" data-id="${id}"${extra ? ' data-extra hidden' : ''}>
204
212
  ${faceHtml(L.faceOf(req, 'out'))}
205
213
  <div class="ov-row__main">${askHtml(req, key)}
206
- <div class="ov-row__facts">${directionFact(req, 'out')}${contextChip(req)}${ageChip(req, nowMs)}</div>
214
+ <div class="ov-row__facts">${directionFact(req, 'out')}${contextChip(req)}${ageChip(req, nowMs, baseline)}</div>
207
215
  </div>
208
216
  <div class="ov-row__right">
209
217
  <div class="ov-row__actions">
@@ -216,13 +224,13 @@
216
224
 
217
225
  // A row on the board of everyone's asks: the asker's face, the third-person
218
226
  // line, no actions — settling is the asker's, in their own block.
219
- function allRow(req, nowMs, extra, key) {
227
+ function allRow(req, nowMs, extra, key, baseline) {
220
228
  return `<li class="ov-row ov-row--lead cb-row" data-id="${escapeHtml(String(req.id))}"${extra ? ' data-extra hidden' : ''}>
221
229
  ${faceHtml(L.faceOf(req, 'in'))}
222
230
  <div class="ov-row__main">${askHtml(req, key)}
223
231
  <div class="ov-row__facts">${boardFact(req)}${contextChip(req)}</div>
224
232
  </div>
225
- <div class="ov-row__right">${ageChip(req, nowMs)}</div>
233
+ <div class="ov-row__right">${ageChip(req, nowMs, baseline)}</div>
226
234
  </li>`;
227
235
  }
228
236
 
@@ -235,13 +243,27 @@
235
243
  function paintBlock(key, rows, rowFn, nowMs, faces) {
236
244
  const list = rows.slice().sort(L.byNewestFirst);
237
245
  const { shown, hidden } = L.splitShown(list, SHOWN);
246
+ // Over the WHOLE list, not the shown three: the door hides rows, it does not
247
+ // change what the room is, and a baseline that flipped when someone pressed
248
+ // "Show all" would re-alarm the rows it had just quieted.
249
+ const baseline = L.roomBaseline(list, nowMs);
238
250
  const host = $(`#cb-${key}`);
239
251
  if (host) {
240
- host.innerHTML = shown.map((r) => rowFn(r, nowMs, false, key)).join('')
241
- + hidden.map((r) => rowFn(r, nowMs, true, key)).join('');
252
+ host.innerHTML = shown.map((r) => rowFn(r, nowMs, false, key, baseline)).join('')
253
+ + hidden.map((r) => rowFn(r, nowMs, true, key, baseline)).join('');
242
254
  }
243
255
  const n = $(`#cb-${key}-n`);
244
256
  if (n) n.textContent = list.length ? String(list.length) : '';
257
+ // The room's condition, said once, beside the count that scopes it.
258
+ const age = $(`#cb-${key}-age`);
259
+ if (age) {
260
+ const line = L.roomAgeLine(baseline);
261
+ age.textContent = line;
262
+ age.hidden = !line;
263
+ age.className = line
264
+ ? `cb-block__age cb-block__age--${baseline.band.id}`
265
+ : 'cb-block__age';
266
+ }
245
267
  const empty = $(`#cb-${key}-empty`);
246
268
  if (empty) empty.hidden = list.length > 0;
247
269
  const facesEl = $(`#cb-${key}-faces`);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.907",
3
+ "version": "1.19.909",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.907",
9
+ "version": "1.19.909",
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.907",
3
+ "version": "1.19.909",
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/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.907'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.909'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -136,6 +136,98 @@ test('the two calm bands are not chips, and the date is not said twice', () => {
136
136
  assert.match(js, /title="Asked \$\{escapeHtml\(String\(req\.created_at/, 'the exact date is still reachable');
137
137
  });
138
138
 
139
+ // ---- THE ROOM'S OWN CONDITION (task 1004067, from idea 1001065) -----------
140
+ //
141
+ // The report: fourteen of fourteen rows wore the "waiting over a week" pill, so
142
+ // the loudest thing on the page carried no information. Task 1004041 made the
143
+ // WORDS differ, which is why these tests are about the TREATMENT — fourteen
144
+ // differently-worded rows in identical danger ink and identical rings still read
145
+ // as a wall. Each case below is run through the lib, not grepped, so a rule that
146
+ // is wrong fails here rather than looking plausible in a diff.
147
+ const room = (...ages) => ages.map((d) => ({ created_at: daysAgo(d) }));
148
+
149
+ test('a uniformly old room states its condition once instead of on every row', () => {
150
+ const L = lib();
151
+ // The reported board, reproduced: fourteen rows, every one past a week.
152
+ const rows = room(8, 9, 11, 13, 15, 16, 18, 21, 24, 27, 30, 34, 38, 41);
153
+ const baseline = L.roomBaseline(rows, NOW);
154
+ assert.ok(baseline, 'fourteen of fourteen is a room condition, not fourteen warnings');
155
+ assert.equal(baseline.band.id, 'stale');
156
+ assert.equal(L.roomAgeLine(baseline), 'all 14 waiting over a week');
157
+ const loud = rows.filter((r) => !L.isRoomQuiet(L.ageBand(r.created_at, NOW), baseline));
158
+ assert.equal(loud.length, 0, 'no row is worse than a room where every row is the same');
159
+ // And nothing is silently dead: each row still says its own real age.
160
+ const words = new Set(rows.map((r) => L.ageLabel(r.created_at, NOW)));
161
+ assert.ok(words.size > 1, 'the per-row words still carry the differing ages');
162
+ });
163
+
164
+ test('the pill is kept for the row that is worse than its room', () => {
165
+ const L = lib();
166
+ // Six of eight sitting a few days, two genuinely old. The two are the whole
167
+ // point of the column, and before this they were indistinguishable from the six.
168
+ const rows = room(4, 4, 5, 5, 5, 6, 21, 30);
169
+ const baseline = L.roomBaseline(rows, NOW);
170
+ assert.equal(baseline.band.id, 'aging');
171
+ assert.equal(L.roomAgeLine(baseline), '6 of 8 waiting a few days');
172
+ const loud = rows
173
+ .filter((r) => !L.isRoomQuiet(L.ageBand(r.created_at, NOW), baseline))
174
+ .map((r) => L.ageLabel(r.created_at, NOW));
175
+ assert.deepEqual(loud, ['waiting 3 weeks', 'waiting 4 weeks'], 'only the exceptions keep a chip');
176
+ });
177
+
178
+ test('a mixed room is left exactly as it was', () => {
179
+ const L = lib();
180
+ // No band is dominant here, so nothing is quieted and nothing is announced —
181
+ // which is what keeps this inert on the board as it usually looks.
182
+ const rows = room(0.2, 0.5, 1, 2, 4, 5, 9, 21);
183
+ assert.equal(L.roomBaseline(rows, NOW), null);
184
+ assert.equal(L.roomAgeLine(null), '');
185
+ for (const r of rows) {
186
+ assert.equal(L.isRoomQuiet(L.ageBand(r.created_at, NOW), null), false, 'no baseline quiets nothing');
187
+ }
188
+ });
189
+
190
+ test('a short list keeps every pill, and a calm room announces nothing', () => {
191
+ const L = lib();
192
+ // Three chips can be read one at a time; a wall needs a crowd.
193
+ assert.equal(L.roomBaseline(room(30, 30, 30), NOW), null, 'under the minimum, every row keeps its own');
194
+ assert.equal(L.BASELINE_MIN_ROWS, 4);
195
+ // A room that is uniformly calm has no alarm to move to the head. Announcing
196
+ // one there would invent the noise this removes.
197
+ assert.equal(L.roomBaseline(room(0.1, 0.2, 0.3, 0.4, 0.5), NOW), null, 'all fresh says nothing');
198
+ assert.equal(L.roomBaseline(room(1, 1, 2, 2, 2), NOW), null, 'all waiting-a-day says nothing either');
199
+ });
200
+
201
+ test('the room line and the rows cannot describe the threshold two ways', () => {
202
+ const L = lib();
203
+ // The head's words come from the band's own label, so there is one vocabulary.
204
+ for (const b of L.BANDS) {
205
+ const line = L.roomAgeLine({ band: b, count: 9, total: 12 });
206
+ assert.equal(line, `9 of 12 ${b.label}`, `${b.id} reads in its own words`);
207
+ }
208
+ });
209
+
210
+ test('the quiet treatment is wired through the render, not just defined', () => {
211
+ const js = read('modules', 'hall-ui', 'public', 'collab.js');
212
+ assert.match(js, /function ageChip\(req, nowMs, baseline\)/, 'the chip is told about its room');
213
+ assert.match(js, /L\.isRoomQuiet\(band, baseline\)/, 'through the lib, so the rule is the tested one');
214
+ assert.match(js, /cb-age--quiet/, 'and the class reaches the markup');
215
+ // Over the WHOLE list: a baseline computed on the shown three would flip when
216
+ // a reader pressed "Show all" and re-alarm the rows it had just quieted.
217
+ assert.match(js, /const baseline = L\.roomBaseline\(list, nowMs\)/, 'the room is the whole block');
218
+ const html = read('modules', 'hall-ui', 'public', 'collab.html');
219
+ for (const key of ['for-me', 'mine', 'all']) {
220
+ assert.ok(html.includes(`id="cb-${key}-age"`), `the ${key} head has somewhere to say it`);
221
+ }
222
+ const css = read('modules', 'hall-ui', 'public', 'collab.css');
223
+ assert.match(
224
+ css,
225
+ /\.cb-age\.cb-age--quiet\s*\{[^}]*border-color: transparent/,
226
+ 'the quieted row drops its ring',
227
+ );
228
+ assert.match(css, /\.cb-age\.cb-age--quiet\s*\{[^}]*color: var\(--ink-soft\)/, 'and the danger ink with it');
229
+ });
230
+
139
231
  test('the alarm does not sit beside the accent control', () => {
140
232
  // DESIGN.md spends the accent only on what is pressable or live. On "Your
141
233
  // asks" the age pill (danger ink) used to share the right column with the
@@ -230,7 +230,11 @@ test('the door and the copy control are on the row, and the copy control is reac
230
230
  // One request can appear in three blocks at once, so the id is block-scoped.
231
231
  assert.match(js, /function askId\(key, id\)/);
232
232
  assert.match(js, /cb-ask-\$\{key\}-/);
233
- assert.match(js, /rowFn\(r, nowMs, false, key\)/, 'and paintBlock hands the key down');
233
+ // A bracket class, not a closing paren: paintBlock also hands the row its
234
+ // block's age baseline (task 1004067), and pinning the exact arity would red
235
+ // this on an argument the invariant — that the KEY reaches the row — does not
236
+ // care about.
237
+ assert.match(js, /rowFn\(r, nowMs, false, key[,)]/, 'and paintBlock hands the key down');
234
238
 
235
239
  const css = read('collab.css');
236
240
  for (const cls of ['.cb-ask ', '.cb-ask__door', '.cb-ask__copy', '.cb-ask__full']) {
@@ -262,8 +266,12 @@ test('the age is a state pill in the row\'s right column, and every band has its
262
266
  assert.match(css, /\.cb-age--stale\s*\{[^}]*var\(--danger\)/, 'stale is the danger ink');
263
267
  assert.match(css, /\.cb-age--aging\s*\{[^}]*var\(--warn\)/, 'aging is the warn ink');
264
268
  const js = read('collab.js');
265
- assert.match(js, /class="fact-pill cb-age cb-age--\$\{band\.id\}"/, 'the pill is the shell\'s fact-pill');
266
- assert.match(js, /<div class="ov-row__right">\$\{ageChip\(req, nowMs\)\}/, 'in the right column, so a glance down the list reads the ages');
269
+ // Both pins stop short of the closing character, because the band's class is
270
+ // now followed by the room's quiet modifier and ageChip by the baseline that
271
+ // decides it (task 1004067). What they assert is unchanged: the shell's pill,
272
+ // in the right column.
273
+ assert.match(js, /class="fact-pill cb-age cb-age--\$\{band\.id\}/, 'the pill is the shell\'s fact-pill');
274
+ assert.match(js, /<div class="ov-row__right">\$\{ageChip\(req, nowMs[,)]/, 'in the right column, so a glance down the list reads the ages');
267
275
  });
268
276
 
269
277
  // ---- the copy desk: quieter, and no fact lost ----------------------------------
@@ -54,6 +54,77 @@ test('`protected` is DERIVED from the imported PROTECTED_GLOBS, never hand-set',
54
54
  assert.ok(prot.length < scope.moduleKeys().length, 'expected at least one non-protected module');
55
55
  });
56
56
 
57
+ // THE PROTECTED SET IS PINNED BY NAME (task 1004070, ADR 0314).
58
+ //
59
+ // The assertion above says the roster contains both kinds; it passes on any
60
+ // twelve. That is the shape a silent unprotection hides in — a glob edit that
61
+ // dropped `migrations/` off `lifecycle` would keep the count and leave the ship
62
+ // pipeline scopeable by anyone. It has already drifted once in the other
63
+ // direction: task 1004070 was written against a re-measurement of 11, and
64
+ // `agents` (protected by `modules/agents/`, itself a PROTECTED_GLOBS entry) was
65
+ // missing from it.
66
+ //
67
+ // So the set is spelled out. A module ARRIVING here is a deliberate line in a
68
+ // diff; a module LEAVING is the one that must never happen quietly.
69
+ const PROTECTED_MODULES = [
70
+ 'agents',
71
+ 'autonomy',
72
+ 'builder-settings',
73
+ 'copy-desk',
74
+ 'dev-box',
75
+ 'devsecops',
76
+ 'government',
77
+ 'grading',
78
+ 'kernel',
79
+ 'lifecycle',
80
+ 'methodology',
81
+ 'onboarding',
82
+ ];
83
+
84
+ test('the protected module set is exactly this, by name', () => {
85
+ assert.deepEqual(
86
+ scope.protectedModules().slice().sort(),
87
+ [...PROTECTED_MODULES].sort(),
88
+ 'the derived protected set changed — if that was intended, edit PROTECTED_MODULES in the same diff and say why',
89
+ );
90
+ });
91
+
92
+ test('the seven trust-core modules can never leave the protected set', () => {
93
+ // Stated separately from the pin above because these are not interchangeable
94
+ // with the rest: their territory IS the trust / ship / authz core, so an edit
95
+ // that unprotects one is a security regression rather than a roster change.
96
+ // ADR 0314 narrows which MODULES derive protected; it must never reach these.
97
+ for (const k of ['kernel', 'lifecycle', 'government', 'grading', 'dev-box', 'devsecops', 'methodology']) {
98
+ assert.equal(scope.scopeForModule(k).protected, true, `${k} must stay protected`);
99
+ }
100
+ });
101
+
102
+ test('narrowing the module roster must not narrow the PATH wall', () => {
103
+ // The other half of ADR 0314's verification, and the one that would make this
104
+ // a false success: whatever happens to the module flag, a sub-Metic builder
105
+ // touching a protected CLI is still refused, by the per-file check.
106
+ // Each file is scoped to the module that OWNS it, deliberately: check() runs
107
+ // scope-subset before the path wall, so scoping them all to one module would
108
+ // get `out_of_scope` and prove nothing about protection. The owning module is
109
+ // what makes the scope wall pass, leaving the path wall as the only thing that
110
+ // can refuse — which is exactly the claim under test.
111
+ const owned = [
112
+ ['onboarding', 'scripts/gds/setup.js'],
113
+ ['autonomy', 'scripts/gds/conductor-dispatch.js'],
114
+ ['copy-desk', 'scripts/gds/copy-inventory.js'],
115
+ ];
116
+ for (const [mod, f] of owned) {
117
+ assert.ok(PROTECTED_GLOBS.includes(f), `fixture assumption: ${f} is a protected surface`);
118
+ assert.equal(gsc.scopeSubset([mod], [f]).ok, true, `fixture assumption: ${mod} owns ${f}`);
119
+ const r = gsc.check([mod], [f], 'thetes');
120
+ assert.equal(r.ok, false, `a sub-Metic builder must still be refused on ${f}`);
121
+ assert.equal(r.code, 'protected_path', `${f} must be refused BY THE PATH WALL, not by scope`);
122
+ // ...and Metic+ passes, so the refusal is the rank floor and not the path
123
+ // being unreachable to everyone.
124
+ assert.equal(gsc.check([mod], [f], 'metic').ok, true, `a Metic must still be able to author on ${f}`);
125
+ }
126
+ });
127
+
57
128
  test('isProtected actually consults PROTECTED_GLOBS (overlap, prefix-aware both ways)', () => {
58
129
  assert.ok(PROTECTED_GLOBS.includes('migrations/'), 'fixture assumption: migrations/ is protected');
59
130
  assert.equal(scope.isProtected(['migrations/']), true, 'a protected glob must compute protected');