@bongos/core 1.21.50 → 1.21.52
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 +41 -31
- package/docs/copy-inventory.md +173 -140
- package/docs/copy-registry.json +601 -271
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +8 -4
- package/docs/page-readings.json +499 -495
- package/modules/government/docket.js +3 -1
- package/modules/hall-ui/public/street.css +6 -2
- package/modules/hall-ui/public/street.js +42 -20
- package/modules/hall-ui/records/governor-street.md +21 -0
- package/modules/public-landing/public/account.html +12 -101
- package/modules/public-landing/public/assets/hub-bar.js +127 -0
- package/modules/public-landing/public/contact.html +60 -9
- package/modules/public-landing/public/privacy.html +60 -9
- package/modules/public-landing/public/terms.html +60 -9
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/hall-preview/scrub-identity.js +9 -1
- package/src/module-api.js +1 -1
- package/tests/governor_docket.mjs +2 -0
- package/tests/hall_preview_governor_fixtures.mjs +75 -0
- package/tests/hall_preview_scrub.mjs +5 -0
- package/tests/hall_street_amend.mjs +21 -8
- package/tests/hall_street_desk.mjs +41 -0
- package/tests/hub_account_page.mjs +22 -5
- package/tests/landing_legal_pages.mjs +275 -0
|
@@ -18,9 +18,14 @@
|
|
|
18
18
|
<!-- Dark only, like every apex page (task 1003336, ADR 0204): data-mode="dark" is fixed on <html>. -->
|
|
19
19
|
<style>
|
|
20
20
|
/* The reading column only. No :root block — the pack's injected theme owns the
|
|
21
|
-
tokens (landing direction C1); everything below reads them.
|
|
21
|
+
tokens (landing direction C1); everything below reads them. The bar and the
|
|
22
|
+
bands are cosmos.css's own rules, not restyled here. */
|
|
23
|
+
/* the skip link un-hides itself on focus, reversing everything .vh sets (the hub's rule) */
|
|
24
|
+
.skip:focus{position:fixed;top:8px;left:8px;z-index:100;width:auto;height:auto;margin:0;overflow:visible;clip-path:none;white-space:normal;
|
|
25
|
+
font-size:14px;font-weight:800;text-decoration:none;color:var(--ink);background:var(--bg-card);
|
|
26
|
+
border:2px solid var(--ctl-line);border-radius:12px;padding:10px 16px;}
|
|
22
27
|
.legal{position:relative;z-index:1;max-width:720px;margin:0 auto;padding:clamp(6px,1.5vw,18px) clamp(16px,4vw,40px) 48px;}
|
|
23
|
-
.legal h1{font-family:var(--font-display);font-weight:700;font-size:clamp(26px,3.8vw,36px);letter-spacing:-0.5px;color:var(--chrome-ink);text-shadow:var(--chrome-glow);margin
|
|
28
|
+
.legal h1{font-family:var(--font-display);font-weight:700;font-size:clamp(26px,3.8vw,36px);letter-spacing:-0.5px;color:var(--chrome-ink);text-shadow:var(--chrome-glow);margin:18px 0 6px;}
|
|
24
29
|
.legal .lede{color:var(--hero-lede);text-shadow:var(--chrome-glow);margin-bottom:18px;}
|
|
25
30
|
.legal h2{font-family:var(--font-display);font-weight:700;font-size:var(--fs-h3);letter-spacing:-0.3px;line-height:1.25;margin:22px 0 8px;}
|
|
26
31
|
.legal .card h2:first-child{margin-top:0;}
|
|
@@ -33,15 +38,60 @@
|
|
|
33
38
|
</style>
|
|
34
39
|
</head>
|
|
35
40
|
<body>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
<!-- THE BAR and its two bands are the hub's own (projects.html; task 1004489,
|
|
42
|
+
after the account page's task 1004455): same markup, same cosmos.css rules,
|
|
43
|
+
so following a footer link from /projects or /account keeps the chrome. No
|
|
44
|
+
map search (it only filters the map), the band links name /projects in full
|
|
45
|
+
because this page has no views of its own, and no item is the current place.
|
|
46
|
+
assets/hub-bar.js fills the chip from GET /me and runs the bands. -->
|
|
47
|
+
<a class="skip vh" href="#legal">Skip to content</a>
|
|
48
|
+
<header class="shell bar">
|
|
49
|
+
<div class="barLeft">
|
|
50
|
+
<a class="barMark" id="barMark" href="/">
|
|
51
|
+
<svg viewBox="0 0 26 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.4"
|
|
52
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
53
|
+
<ellipse cx="8.4" cy="5.6" rx="6.6" ry="2.9"/>
|
|
54
|
+
<path d="M1.8 5.6v6.6a6.6 2.9 0 0 0 13.2 0V5.6"/>
|
|
55
|
+
<ellipse cx="19.6" cy="8.4" rx="4.7" ry="2.1"/>
|
|
56
|
+
<path d="M14.9 8.4v4.6a4.7 2.1 0 0 0 9.4 0V8.4"/>
|
|
57
|
+
</svg>
|
|
58
|
+
<span>{{productName}}</span>
|
|
59
|
+
</a>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="barRight">
|
|
62
|
+
<button class="trig" type="button" id="projTrig" data-menu="projects" aria-expanded="false" aria-controls="menuProjects">Projects<svg class="cv" viewBox="0 0 20 20" aria-hidden="true"><path d="m5.6 8 4.4 4.4L14.4 8"/></svg></button>
|
|
63
|
+
<!-- the identity chip IS the profile trigger; assets/hub-bar.js fills it from
|
|
64
|
+
GET /me. Signed out it stays hidden and the sign-in door takes its slot. -->
|
|
65
|
+
<button class="trig who" type="button" id="who" data-menu="profile" aria-expanded="false" aria-controls="menuProfile" hidden><img id="whoImg" alt="" hidden><span class="whoName" id="whoLogin"></span><svg class="cv" viewBox="0 0 20 20" aria-hidden="true"><path d="m5.6 8 4.4 4.4L14.4 8"/></svg></button>
|
|
66
|
+
<a class="signinDoor" id="signinTop" hidden href="/api/bongos/auth/web/start?return=/privacy&repos=1">Sign in</a>
|
|
41
67
|
</div>
|
|
42
68
|
</header>
|
|
43
69
|
|
|
44
|
-
<
|
|
70
|
+
<div class="menuBand" id="menuProjects" data-menu="projects" hidden>
|
|
71
|
+
<div class="bandIn">
|
|
72
|
+
<ul class="bandRow">
|
|
73
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=map">Sky</a><span class="bandC">the sky of every project</span></li>
|
|
74
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=map&sub=table">Table</a><span class="bandC">the same projects as rows</span></li>
|
|
75
|
+
<li class="bandCell" id="mineCell"><a class="bandT" href="/projects?view=mine">Your projects</a><span class="bandC">the ones you own or build on</span></li>
|
|
76
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=new">New project</a><span class="bandC">five questions and a home</span></li>
|
|
77
|
+
</ul>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="menuBand" id="menuProfile" data-menu="profile" hidden>
|
|
81
|
+
<div class="bandIn bandProfile">
|
|
82
|
+
<p class="bandWho" id="bandWho"><img id="bandWhoImg" alt="" hidden><span class="whoName" id="bandWhoLogin"></span></p>
|
|
83
|
+
<ul class="bandRow">
|
|
84
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=mine">My projects</a></li>
|
|
85
|
+
<!-- apex-only because /account is: it is served only where platform-identity
|
|
86
|
+
runs, so a child instance loses the item whole (as on projects.html) -->
|
|
87
|
+
<!--apex-only:start--><li class="bandCell"><a class="bandT" href="/account">Account</a></li><!--apex-only:end-->
|
|
88
|
+
<!-- the honest hover target; assets/hub-bar.js upgrades the click to the POST -->
|
|
89
|
+
<li class="bandCell"><a class="bandT signout" href="/api/bongos/auth/logout">Sign out</a></li>
|
|
90
|
+
</ul>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<main class="legal" id="legal">
|
|
45
95
|
<h1>Privacy</h1>
|
|
46
96
|
<p class="lede">What {{productName}} keeps about you and your project, who else sees it, and how long it stays — in plain words. The code that does all of this is open, so every sentence here can be checked.</p>
|
|
47
97
|
|
|
@@ -77,6 +127,7 @@
|
|
|
77
127
|
</section>
|
|
78
128
|
</main>
|
|
79
129
|
|
|
80
|
-
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).<nav class="footNav" aria-label="Site"><a href="/privacy" aria-current="page">Privacy</a><a href="/terms">Terms</a><a href="/contact">Contact</a></nav></footer>
|
|
130
|
+
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).<nav class="footNav" aria-label="Site"><a href="/privacy" aria-current="page">Privacy</a><a href="/terms">Terms</a><a href="/contact">Contact</a><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a></nav></footer>
|
|
131
|
+
<script src="/assets/hub-bar.js"></script>
|
|
81
132
|
</body>
|
|
82
133
|
</html>
|
|
@@ -18,9 +18,14 @@
|
|
|
18
18
|
<!-- Dark only, like every apex page (task 1003336, ADR 0204): data-mode="dark" is fixed on <html>. -->
|
|
19
19
|
<style>
|
|
20
20
|
/* The reading column only. No :root block — the pack's injected theme owns the
|
|
21
|
-
tokens (landing direction C1); everything below reads them.
|
|
21
|
+
tokens (landing direction C1); everything below reads them. The bar and the
|
|
22
|
+
bands are cosmos.css's own rules, not restyled here. */
|
|
23
|
+
/* the skip link un-hides itself on focus, reversing everything .vh sets (the hub's rule) */
|
|
24
|
+
.skip:focus{position:fixed;top:8px;left:8px;z-index:100;width:auto;height:auto;margin:0;overflow:visible;clip-path:none;white-space:normal;
|
|
25
|
+
font-size:14px;font-weight:800;text-decoration:none;color:var(--ink);background:var(--bg-card);
|
|
26
|
+
border:2px solid var(--ctl-line);border-radius:12px;padding:10px 16px;}
|
|
22
27
|
.legal{position:relative;z-index:1;max-width:720px;margin:0 auto;padding:clamp(6px,1.5vw,18px) clamp(16px,4vw,40px) 48px;}
|
|
23
|
-
.legal h1{font-family:var(--font-display);font-weight:700;font-size:clamp(26px,3.8vw,36px);letter-spacing:-0.5px;color:var(--chrome-ink);text-shadow:var(--chrome-glow);margin
|
|
28
|
+
.legal h1{font-family:var(--font-display);font-weight:700;font-size:clamp(26px,3.8vw,36px);letter-spacing:-0.5px;color:var(--chrome-ink);text-shadow:var(--chrome-glow);margin:18px 0 6px;}
|
|
24
29
|
.legal .lede{color:var(--hero-lede);text-shadow:var(--chrome-glow);margin-bottom:18px;}
|
|
25
30
|
.legal h2{font-family:var(--font-display);font-weight:700;font-size:var(--fs-h3);letter-spacing:-0.3px;line-height:1.25;margin:22px 0 8px;}
|
|
26
31
|
.legal .card h2:first-child{margin-top:0;}
|
|
@@ -33,15 +38,60 @@
|
|
|
33
38
|
</style>
|
|
34
39
|
</head>
|
|
35
40
|
<body>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
<!-- THE BAR and its two bands are the hub's own (projects.html; task 1004489,
|
|
42
|
+
after the account page's task 1004455): same markup, same cosmos.css rules,
|
|
43
|
+
so following a footer link from /projects or /account keeps the chrome. No
|
|
44
|
+
map search (it only filters the map), the band links name /projects in full
|
|
45
|
+
because this page has no views of its own, and no item is the current place.
|
|
46
|
+
assets/hub-bar.js fills the chip from GET /me and runs the bands. -->
|
|
47
|
+
<a class="skip vh" href="#legal">Skip to content</a>
|
|
48
|
+
<header class="shell bar">
|
|
49
|
+
<div class="barLeft">
|
|
50
|
+
<a class="barMark" id="barMark" href="/">
|
|
51
|
+
<svg viewBox="0 0 26 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.4"
|
|
52
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
53
|
+
<ellipse cx="8.4" cy="5.6" rx="6.6" ry="2.9"/>
|
|
54
|
+
<path d="M1.8 5.6v6.6a6.6 2.9 0 0 0 13.2 0V5.6"/>
|
|
55
|
+
<ellipse cx="19.6" cy="8.4" rx="4.7" ry="2.1"/>
|
|
56
|
+
<path d="M14.9 8.4v4.6a4.7 2.1 0 0 0 9.4 0V8.4"/>
|
|
57
|
+
</svg>
|
|
58
|
+
<span>{{productName}}</span>
|
|
59
|
+
</a>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="barRight">
|
|
62
|
+
<button class="trig" type="button" id="projTrig" data-menu="projects" aria-expanded="false" aria-controls="menuProjects">Projects<svg class="cv" viewBox="0 0 20 20" aria-hidden="true"><path d="m5.6 8 4.4 4.4L14.4 8"/></svg></button>
|
|
63
|
+
<!-- the identity chip IS the profile trigger; assets/hub-bar.js fills it from
|
|
64
|
+
GET /me. Signed out it stays hidden and the sign-in door takes its slot. -->
|
|
65
|
+
<button class="trig who" type="button" id="who" data-menu="profile" aria-expanded="false" aria-controls="menuProfile" hidden><img id="whoImg" alt="" hidden><span class="whoName" id="whoLogin"></span><svg class="cv" viewBox="0 0 20 20" aria-hidden="true"><path d="m5.6 8 4.4 4.4L14.4 8"/></svg></button>
|
|
66
|
+
<a class="signinDoor" id="signinTop" hidden href="/api/bongos/auth/web/start?return=/terms&repos=1">Sign in</a>
|
|
41
67
|
</div>
|
|
42
68
|
</header>
|
|
43
69
|
|
|
44
|
-
<
|
|
70
|
+
<div class="menuBand" id="menuProjects" data-menu="projects" hidden>
|
|
71
|
+
<div class="bandIn">
|
|
72
|
+
<ul class="bandRow">
|
|
73
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=map">Sky</a><span class="bandC">the sky of every project</span></li>
|
|
74
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=map&sub=table">Table</a><span class="bandC">the same projects as rows</span></li>
|
|
75
|
+
<li class="bandCell" id="mineCell"><a class="bandT" href="/projects?view=mine">Your projects</a><span class="bandC">the ones you own or build on</span></li>
|
|
76
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=new">New project</a><span class="bandC">five questions and a home</span></li>
|
|
77
|
+
</ul>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="menuBand" id="menuProfile" data-menu="profile" hidden>
|
|
81
|
+
<div class="bandIn bandProfile">
|
|
82
|
+
<p class="bandWho" id="bandWho"><img id="bandWhoImg" alt="" hidden><span class="whoName" id="bandWhoLogin"></span></p>
|
|
83
|
+
<ul class="bandRow">
|
|
84
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=mine">My projects</a></li>
|
|
85
|
+
<!-- apex-only because /account is: it is served only where platform-identity
|
|
86
|
+
runs, so a child instance loses the item whole (as on projects.html) -->
|
|
87
|
+
<!--apex-only:start--><li class="bandCell"><a class="bandT" href="/account">Account</a></li><!--apex-only:end-->
|
|
88
|
+
<!-- the honest hover target; assets/hub-bar.js upgrades the click to the POST -->
|
|
89
|
+
<li class="bandCell"><a class="bandT signout" href="/api/bongos/auth/logout">Sign out</a></li>
|
|
90
|
+
</ul>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<main class="legal" id="legal">
|
|
45
95
|
<h1>Terms</h1>
|
|
46
96
|
<p class="lede">The short version: this is free software run by its owner, your code stays yours, and you are responsible for what you ship.</p>
|
|
47
97
|
|
|
@@ -82,6 +132,7 @@
|
|
|
82
132
|
</section>
|
|
83
133
|
</main>
|
|
84
134
|
|
|
85
|
-
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).<nav class="footNav" aria-label="Site"><a href="/privacy">Privacy</a><a href="/terms" aria-current="page">Terms</a><a href="/contact">Contact</a></nav></footer>
|
|
135
|
+
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).<nav class="footNav" aria-label="Site"><a href="/privacy">Privacy</a><a href="/terms" aria-current="page">Terms</a><a href="/contact">Contact</a><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a></nav></footer>
|
|
136
|
+
<script src="/assets/hub-bar.js"></script>
|
|
86
137
|
</body>
|
|
87
138
|
</html>
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.52",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.52",
|
|
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.21.
|
|
3
|
+
"version": "1.21.52",
|
|
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/release-notes.json
CHANGED
|
@@ -9068,5 +9068,17 @@
|
|
|
9068
9068
|
"id": "1004574",
|
|
9069
9069
|
"text": "The builders hall now tells newcomers one sign-in command, npx bongos setup, the same on Mac, Windows and Linux — and copying the setup commands now pastes them line by line instead of jammed onto one line."
|
|
9070
9070
|
}
|
|
9071
|
+
],
|
|
9072
|
+
"1.21.51": [
|
|
9073
|
+
{
|
|
9074
|
+
"id": "1004531",
|
|
9075
|
+
"text": "We walked the whole Governor City against its design, in light and dark and on a phone: the home city, the street, every building, a vote decided from its card, and an amendment drafted from a building. Fixes: drafting a chang"
|
|
9076
|
+
}
|
|
9077
|
+
],
|
|
9078
|
+
"1.21.52": [
|
|
9079
|
+
{
|
|
9080
|
+
"id": "1004489",
|
|
9081
|
+
"text": "The Privacy, Terms and Contact pages now have the same top bar as the projects and account pages — the Projects menu and your profile menu — so following a footer link no longer swaps the whole header."
|
|
9082
|
+
}
|
|
9071
9083
|
]
|
|
9072
9084
|
}
|
|
@@ -63,6 +63,14 @@ const isEmailKey = (k) => k === 'email' || /_email$/.test(k);
|
|
|
63
63
|
// `by` / `*_by` holds a builder ID almost everywhere (created_by, shipped_by); only a
|
|
64
64
|
// non-numeric value is a login (sandbox_review.by, who approved a staged change).
|
|
65
65
|
const isByKey = (k) => k === 'by' || /_by$/.test(k);
|
|
66
|
+
// A building's `decided_by` in the decision table (government decision-rules.js)
|
|
67
|
+
// is a word of its vocabulary, never a person: "gate" (as today), "board" (the
|
|
68
|
+
// charter), "nobody" (vacant). A rank predicate has a colon and never looks like a
|
|
69
|
+
// login. Task 1004531: the constitution fixture carries that table. Only on a
|
|
70
|
+
// table ROW (it names its building in `area`), so a login that happens to be one
|
|
71
|
+
// of the three words anywhere else is still scrubbed.
|
|
72
|
+
const DECISION_WORDS = new Set(['gate', 'board', 'nobody']);
|
|
73
|
+
const isDecisionWord = (k, v, obj) => k === 'decided_by' && DECISION_WORDS.has(v) && !!obj && typeof obj.area === 'string';
|
|
66
74
|
const looksLikeLogin = (v) => /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(v) && !/^\d+$/.test(v);
|
|
67
75
|
|
|
68
76
|
// The login a name-shaped key belongs to, or undefined when the key is not a person's
|
|
@@ -159,7 +167,7 @@ function createScrubber({ firstNumber = 1 } = {}) {
|
|
|
159
167
|
const n = isPersonRecord ? self() : numberFor([`email:${value.toLowerCase()}`]);
|
|
160
168
|
return `builder-${n}@example.com`;
|
|
161
169
|
}
|
|
162
|
-
if (isByKey(key) && looksLikeLogin(value)) {
|
|
170
|
+
if (isByKey(key) && looksLikeLogin(value) && !isDecisionWord(key, value, obj)) {
|
|
163
171
|
if (STAND_IN_LOGIN.test(value)) return value;
|
|
164
172
|
return `builder-${numberFor([`login:${value.toLowerCase()}`])}`;
|
|
165
173
|
}
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.52'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -144,6 +144,8 @@ test('homeLine speaks the design\'s words', () => {
|
|
|
144
144
|
assert.equal(docket.homeLine({ needs: 0, moving: 0, settled: 4 }), 'nothing waits on you.');
|
|
145
145
|
assert.equal(docket.homeLine({ needs: 3, moving: 2, settled: 0 }), '3 matters wait on you · 2 moving');
|
|
146
146
|
assert.equal(docket.homeLine({ needs: 1, moving: 0, settled: 0 }), '1 matter waits on you');
|
|
147
|
+
// the prototype's words when only moving work is left (task 1004531 walkthrough)
|
|
148
|
+
assert.equal(docket.homeLine({ needs: 0, moving: 2, settled: 1 }), 'nothing waits on you · 2 moving');
|
|
147
149
|
});
|
|
148
150
|
|
|
149
151
|
// ── 2. the board, per viewer ─────────────────────────────────────────────────
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// tests/hall_preview_governor_fixtures.mjs — the hall preview's governor
|
|
2
|
+
// fixtures hold the server's own invariants (task 1004531).
|
|
3
|
+
//
|
|
4
|
+
// The Governor City walkthroughs run on the hall preview harness, so its
|
|
5
|
+
// fixtures are what the UAT record is judged on. Two had drifted from the
|
|
6
|
+
// shapes the routes really serve:
|
|
7
|
+
// * the docket's counts, home line and per-building counts were written for
|
|
8
|
+
// the first seven matters and never re-derived as later tasks added more,
|
|
9
|
+
// so the city said "3 matters wait on you", the street header 4, and its
|
|
10
|
+
// pins 13 — buildDocket derives all three from the matters, so live they
|
|
11
|
+
// can never disagree; the fairness reading had drifted the same way, so
|
|
12
|
+
// Disputes read "0 open" under two matters that need you;
|
|
13
|
+
// * the constitution carried no decision_rules block, so "change how this
|
|
14
|
+
// works" could only say it could not read how a building decides.
|
|
15
|
+
//
|
|
16
|
+
// Run: node --test tests/hall_preview_governor_fixtures.mjs
|
|
17
|
+
|
|
18
|
+
import { test } from 'node:test';
|
|
19
|
+
import assert from 'node:assert/strict';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const FX = path.join(ROOT, 'scripts/hall-preview/fixtures');
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const docketLib = require(path.join(ROOT, 'modules/government/docket.js'));
|
|
29
|
+
const DR = require(path.join(ROOT, 'modules/government/decision-rules.js'));
|
|
30
|
+
const DB = require(path.join(ROOT, 'modules/government/decision-board.js'));
|
|
31
|
+
const A = require(path.join(ROOT, 'modules/hall-ui/public/street-amend.js'));
|
|
32
|
+
const fx = (name) => JSON.parse(fs.readFileSync(path.join(FX, name + '.json'), 'utf8'));
|
|
33
|
+
|
|
34
|
+
// buildDocket's countStates, restated: every matter counted once in its state.
|
|
35
|
+
function countStates(ms) {
|
|
36
|
+
const c = { needs: 0, moving: 0, settled: 0 };
|
|
37
|
+
for (const m of ms) c[m.state] += 1;
|
|
38
|
+
return c;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test('the docket fixture: counts and the home line are derived from its matters, as buildDocket derives them', () => {
|
|
42
|
+
const d = fx('government__docket');
|
|
43
|
+
assert.ok(d.matters.length > 7, 'the dense docket (task 1004569), not the first seven');
|
|
44
|
+
assert.deepEqual(d.counts, countStates(d.matters));
|
|
45
|
+
assert.equal(d.line, docketLib.homeLine(d.counts), 'the city line under start governing agrees with the street header');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('the docket fixture: every building counts exactly the matters standing at it', () => {
|
|
49
|
+
const d = fx('government__docket');
|
|
50
|
+
for (const b of d.buildings) {
|
|
51
|
+
const here = d.matters.filter((m) => m.building === b.key);
|
|
52
|
+
assert.deepEqual(b.counts, countStates(here), b.key + ': the state line under the building agrees with its pins');
|
|
53
|
+
assert.deepEqual(b.matter_ids, here.map((m) => m.id), b.key + ': its matters, in docket order');
|
|
54
|
+
}
|
|
55
|
+
assert.equal(d.buildings.reduce((n, b) => n + b.matter_ids.length, 0), d.matters.length, 'every matter stands at one building');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('the docket fixture: the fairness reading is derived from the matters at Disputes, as buildDocket derives it', () => {
|
|
59
|
+
// the reading's figure is the line under Disputes on the street, so a stale
|
|
60
|
+
// one says "0 open" under a building whose pin says two matters need you
|
|
61
|
+
const d = fx('government__docket');
|
|
62
|
+
const f = d.readings.find((r) => r.key === 'fairness');
|
|
63
|
+
const want = docketLib.fairnessReading(d.matters, d.sources);
|
|
64
|
+
assert.deepEqual({ value: f.value, figure: f.figure, line: f.line, hot: f.hot }, want);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('the constitution fixture: it carries the decision_rules block the route serves, so every building can draft an amendment', () => {
|
|
68
|
+
const c = fx('government__constitution');
|
|
69
|
+
assert.deepEqual(c.decision_rules, JSON.parse(JSON.stringify(DB.constitutionBlock(DR.resolveAll([], { board: c.board })))), 'the server\'s own block for this board');
|
|
70
|
+
for (const area of c.decision_rules.areas.map((r) => r.area)) {
|
|
71
|
+
const d = A.draftFor(area, c);
|
|
72
|
+
assert.equal(d.ok, true, area + ': change how this works drafts');
|
|
73
|
+
assert.equal(A.check(d), 'Say why, so the council knows what it is voting on.', area + ': the draft is a real change, waiting only for the reason');
|
|
74
|
+
}
|
|
75
|
+
});
|
|
@@ -130,6 +130,11 @@ test('what is not a person is left exactly alone', () => {
|
|
|
130
130
|
assert.equal(req.requests[0].created_by, '1000025', 'a numeric *_by is a builder id, not a login');
|
|
131
131
|
assert.equal(req.tasks[0].credits_reward, 30, "a task's bounty is not anyone's balance");
|
|
132
132
|
assert.equal(req.modules[0].author, 'Cloud Bongos', "a module's author credit names an organisation");
|
|
133
|
+
// the decision table's decided_by is a word of its vocabulary (task 1004531)
|
|
134
|
+
const table = s.scrub({ decision_rules: { areas: [{ area: 'charter', decided_by: 'board' }, { area: 'rules', decided_by: 'gate' }, { area: 'review', decided_by: 'nobody' }, { area: 'people', decided_by: 'rank:metic+' }] } });
|
|
135
|
+
assert.deepEqual(table.decision_rules.areas.map((a) => a.decided_by), ['board', 'gate', 'nobody', 'rank:metic+'], 'who decides a building is not a person');
|
|
136
|
+
assert.equal(s.scrub({ decided_by: 'Real-Person' }).decided_by.startsWith('builder-'), true, 'a login in decided_by is still scrubbed');
|
|
137
|
+
assert.equal(s.scrub({ id: '4', decided_by: 'gate' }).decided_by.startsWith('builder-'), true, 'off a table row, even a login spelled like a decision word is scrubbed');
|
|
133
138
|
});
|
|
134
139
|
|
|
135
140
|
test('scrubbing is a fixed point, and a new run numbers above the stand-ins already in use', () => {
|
|
@@ -184,7 +184,12 @@ test('the markup: the draft lives under the button, hidden, and the model loads
|
|
|
184
184
|
assert.ok(order[0] > 0 && order[0] < order[1] && order[1] < order[2], 'card, then the draft model, then the page');
|
|
185
185
|
const css = read('street.css').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
186
186
|
assert.ok(css.includes('.inside-draft[hidden] { display: none; }'));
|
|
187
|
-
assert.
|
|
187
|
+
assert.ok(css.includes('.inside-draft__picks { display: grid; grid-template-columns: 1fr; gap: 12px; }'), 'who, then how, stacked: the chips need the width (task 1004531)');
|
|
188
|
+
// the chips ride .desk-chip, which comes later in the sheet at one class: two
|
|
189
|
+
// classes are what let the wrap and the fill win (a single class lost in the browser)
|
|
190
|
+
assert.match(css, /\.inside-draft \.inside-draft__chip \{[^}]*white-space: normal;/, 'a long choice wraps inside its chip');
|
|
191
|
+
assert.match(css, /\.inside-draft \.inside-draft__chip--on \{ background: var\(--ink\);/, 'the picked chip is filled');
|
|
192
|
+
assert.ok(css.indexOf('.desk-chip--quiet, .desk-chip--choice {') > css.indexOf('.inside-draft .inside-draft__chip--on'), 'the desk rule still comes later, so the scope is what wins');
|
|
188
193
|
assert.doesNotMatch(css.slice(css.indexOf('.inside-draft')), /#[0-9a-f]{3,8}\b/i, 'tokens only');
|
|
189
194
|
assert.doesNotMatch(read('street.js'), /not built yet/);
|
|
190
195
|
});
|
|
@@ -261,6 +266,8 @@ async function bootPage({ hash = '#building=review', cons = constitution(), answ
|
|
|
261
266
|
|
|
262
267
|
test('change how this works opens the drafted amendment: struck, inserted, who, how, why', async () => {
|
|
263
268
|
const p = await bootPage();
|
|
269
|
+
const chip = (id, v) => p.byId.get(id).kids.find((k) => k.attrs['data-value'] === v);
|
|
270
|
+
const picked = (id) => p.byId.get(id).kids.find((k) => k.attrs['aria-checked'] === 'true');
|
|
264
271
|
const box = p.byId.get('insideDraft');
|
|
265
272
|
assert.equal(box.hidden, true);
|
|
266
273
|
await p.byId.get('insideAmend').fire('click')[0];
|
|
@@ -270,17 +277,23 @@ test('change how this works opens the drafted amendment: struck, inserted, who,
|
|
|
270
277
|
const del = all(box).find((n) => n.tagName === 'del'), ins = all(box).find((n) => n.tagName === 'ins');
|
|
271
278
|
assert.equal(del.textContent, 'whoever may mark work as meeting its checks, decides alone');
|
|
272
279
|
assert.equal(ins.textContent, 'the archon rank, passes unless someone objects');
|
|
273
|
-
assert.equal(p.focused.el,
|
|
274
|
-
|
|
275
|
-
assert.equal(p.byId.get('insideDraftRule').value, 'consent');
|
|
276
|
-
// a pick redraws the words and keeps focus where it was
|
|
280
|
+
assert.equal(p.focused.el, picked('insideDraftWho'), 'focus lands on the picked chip of who decides');
|
|
281
|
+
// every choice is a one-tap chip in a labelled radio group, never a select (task 1004531)
|
|
277
282
|
const who = p.byId.get('insideDraftWho');
|
|
278
|
-
who.
|
|
279
|
-
|
|
283
|
+
assert.equal(who.attrs.role, 'radiogroup');
|
|
284
|
+
assert.ok(all(box).some((n) => n.attrs.id === 'insideDraftWhoLabel' && n._text === 'who decides'), 'the group is named by its label');
|
|
285
|
+
assert.ok(!all(box).some((n) => n.tagName === 'select'), 'no dropdowns');
|
|
286
|
+
assert.equal(who.kids.length, 6);
|
|
287
|
+
assert.ok(who.kids.every((k) => k.tagName === 'button' && k.attrs.role === 'radio'));
|
|
288
|
+
assert.equal(who.kids.filter((k) => k.attrs['aria-checked'] === 'true').length, 1, 'one picked');
|
|
289
|
+
assert.equal(picked('insideDraftRule').attrs['data-value'], 'consent');
|
|
290
|
+
// a pick is one tap: it redraws the words and keeps focus in the group
|
|
291
|
+
chip('insideDraftWho', 'nobody').fire('click');
|
|
280
292
|
assert.equal(all(box).find((n) => n.tagName === 'ins').textContent, 'no one yet');
|
|
281
293
|
assert.ok(!all(box).some((n) => n.attrs.id === 'insideDraftRule'), 'an empty building has no rule to pick');
|
|
282
294
|
assert.match(textOf(box), /nothing is decided until someone holds it/);
|
|
283
|
-
assert.equal(p.focused.el
|
|
295
|
+
assert.equal(p.focused.el, picked('insideDraftWho'));
|
|
296
|
+
assert.equal(p.focused.el.attrs['data-value'], 'nobody');
|
|
284
297
|
// a feed landing mid-sentence leaves the draft alone
|
|
285
298
|
const before = p.byId.get('insideDraftWhy');
|
|
286
299
|
before.value = 'half a thought';
|
|
@@ -894,3 +894,44 @@ test('walkthrough: a record item keeps its age, and what you did keeps its "now"
|
|
|
894
894
|
assert.deepEqual(desk.record.map((x) => x.text), ['voted yes A returned id… · now', 'The new sign-up · live · 2d', 'Mira holds the Treasurer… · 3w', 'Today it went out · today']);
|
|
895
895
|
assert.ok(desk.record.every((x) => x.text.length <= D.LIMITS.recordLen));
|
|
896
896
|
});
|
|
897
|
+
|
|
898
|
+
// ── the live walkthrough against the prototype (task 1004531) ─────────────────
|
|
899
|
+
|
|
900
|
+
const AR = () => [matter({ id: 'access_requests:11', source: 'access_requests', building: 'people', state: 'needs', proposer: { name: 'Ana Ruiz' }, ref: { id: 11 } })];
|
|
901
|
+
const settle = async () => { for (let i = 0; i < 5; i++) await new Promise((r) => setTimeout(r, 0)); };
|
|
902
|
+
|
|
903
|
+
test('walkthrough 1004531: the newest pip pops once and keeps its node, so a redraw neither replays nor cuts the pop', async () => {
|
|
904
|
+
const p = await bootPage('#building=people', AR());
|
|
905
|
+
const pips = () => p.byId.get('streetTodayPips').children;
|
|
906
|
+
const before = pips()[0];
|
|
907
|
+
p.window.GovStreetPage.draw();
|
|
908
|
+
assert.equal(pips()[0], before, 'an unchanged count keeps the meter as it stands');
|
|
909
|
+
p.part('desk-rows').children[0].children[0].children[2].children[1].fire('click');
|
|
910
|
+
await settle();
|
|
911
|
+
assert.equal(p.byId.get('streetTodayN').textContent, '1 decided');
|
|
912
|
+
const fresh = pips()[0];
|
|
913
|
+
assert.notEqual(fresh, before, 'the count moved, so the pips were drawn again');
|
|
914
|
+
assert.match(fresh.className, /street-today__pip--on/);
|
|
915
|
+
assert.match(fresh.className, /desk-pop/, 'the newest pip pops');
|
|
916
|
+
p.window.GovStreetPage.draw();
|
|
917
|
+
assert.equal(pips()[0], fresh, 'the redraws after the act (the docket read again) leave the popping pip alone');
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
test('walkthrough 1004531: a decision from a street card is a decision today, as one from a desk row is', async () => {
|
|
921
|
+
const p = await bootPage('', AR());
|
|
922
|
+
p.window.GovStreetPage.openCard('access_requests:11');
|
|
923
|
+
await p.window.GovStreetPage.actOn('fine', {});
|
|
924
|
+
await settle();
|
|
925
|
+
assert.equal(p.sent.length, 1, 'the card sent its act');
|
|
926
|
+
assert.equal(p.byId.get('streetTodayN').textContent, '1 decided');
|
|
927
|
+
assert.equal(JSON.parse(p.store.get('gov-desk-today')).n, 1, 'the day keeps its count');
|
|
928
|
+
assert.match(p.byId.get('streetTodayPips').children[0].className, /street-today__pip--on desk-pop/);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
test('walkthrough 1004531: a card act that records nothing takes no pip', async () => {
|
|
932
|
+
const p = await bootPage('', AR(), { refuse: true });
|
|
933
|
+
p.window.GovStreetPage.openCard('access_requests:11');
|
|
934
|
+
await p.window.GovStreetPage.actOn('fine', {});
|
|
935
|
+
await settle();
|
|
936
|
+
assert.equal(p.byId.get('streetTodayN').textContent, '0 decided', 'a refused act is not a decision');
|
|
937
|
+
});
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
// header of its own — the same markup as projects.html, cosmos.css's rules,
|
|
19
19
|
// no map search, links into /projects, Account as the current place — and
|
|
20
20
|
// the bands EXECUTED: the chip from GET /me, open/close, Escape, the arrows,
|
|
21
|
-
// a click outside, and sign out as the POST.
|
|
21
|
+
// a click outside, and sign out as the POST. The bands script is the shared
|
|
22
|
+
// assets/hub-bar.js since task 1004489 (the three footer doors load it too),
|
|
23
|
+
// so the page's own script and that file run together here, as in a browser.
|
|
22
24
|
//
|
|
23
25
|
// DB-free. Run: node --test tests/hub_account_page.mjs
|
|
24
26
|
|
|
@@ -37,6 +39,7 @@ const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
|
|
|
37
39
|
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
38
40
|
const PAGE = fs.readFileSync(path.join(LANDING, 'account.html'), 'utf8').replace(/\r\n/g, '\n');
|
|
39
41
|
const SCRIPT = PAGE.split('<script>').pop().split('</script>')[0];
|
|
42
|
+
const BAR = fs.readFileSync(path.join(LANDING, 'assets', 'hub-bar.js'), 'utf8');
|
|
40
43
|
const HUB = fs.readFileSync(path.join(LANDING, 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
|
|
41
44
|
const COSMOS = fs.readFileSync(path.join(LANDING, 'assets', 'cosmos.css'), 'utf8');
|
|
42
45
|
|
|
@@ -62,7 +65,9 @@ test('portable and dark only: stamped title, balanced sentinels, no :root, no ba
|
|
|
62
65
|
});
|
|
63
66
|
|
|
64
67
|
test('the script builds no markup from strings, and the controls are real markup', () => {
|
|
65
|
-
|
|
68
|
+
for (const [name, src] of [['the page', SCRIPT], ['assets/hub-bar.js', BAR]]) {
|
|
69
|
+
assert.doesNotMatch(src, /innerHTML|insertAdjacentHTML|outerHTML/, `${name}: live DOM is textContent / createElement only`);
|
|
70
|
+
}
|
|
66
71
|
for (const id of ['fName', 'fBio', 'fHandle', 'fLinkWebsite', 'fLinkX', 'fLinkLinkedin', 'fLinkDiscord', 'profileSave', 'handleSave', 'privacySave', 'recordSave']) {
|
|
67
72
|
assert.match(PAGE, new RegExp(`id="${id}"`), `#${id} ships in the markup`);
|
|
68
73
|
}
|
|
@@ -76,8 +81,17 @@ test('the script builds no markup from strings, and the controls are real markup
|
|
|
76
81
|
test('the door returns here, and the page calls only its own-scoped routes', () => {
|
|
77
82
|
assert.match(PAGE, /href="\/api\/bongos\/auth\/web\/start\?return=\/account&repos=1"/, 'sign-in lands back on /account, asking for repo access once (task 1004549)');
|
|
78
83
|
const calls = [...SCRIPT.matchAll(/request\('(GET|PATCH|POST)', '([^']+)'/g)].map((m) => `${m[1]} ${m[2]}`);
|
|
79
|
-
assert.deepEqual([...new Set(calls)].sort(), ['GET /me
|
|
80
|
-
|
|
84
|
+
assert.deepEqual([...new Set(calls)].sort(), ['GET /me/cross-project', 'PATCH /me/cross-project', 'PATCH /me/handle', 'PATCH /me/profile'].sort());
|
|
85
|
+
// the bar's two calls live in the shared asset: the identity read and sign out
|
|
86
|
+
const barCalls = [...BAR.matchAll(/fetch\(API \+ '([^']+)', \{ method: '(GET|POST)'/g)].map((m) => `${m[2]} ${m[1]}`);
|
|
87
|
+
assert.deepEqual(barCalls.sort(), ['GET /me', 'POST /auth/logout']);
|
|
88
|
+
assert.match(BAR, /var API = '\/api\/bongos';/);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('the bar’s script is the shared asset, loaded before the page’s own (task 1004489)', () => {
|
|
92
|
+
assert.match(PAGE, /<script src="\/assets\/hub-bar\.js"><\/script>\n<script>\n\(function \(\) \{/, 'hub-bar.js runs first, so window.HubBar exists when the page script asks for it');
|
|
93
|
+
assert.doesNotMatch(SCRIPT, /function (openBand|closeMenus|openMenu|paintBar)\b|\/auth\/logout/, 'no second copy of the bands, the chip or sign out in the page');
|
|
94
|
+
assert.match(SCRIPT, /var bar = window\.HubBar \|\| \{ paint: function \(\) \{\}, ready: Promise\.resolve\(null\) \};/, 'and the page survives the asset failing to load');
|
|
81
95
|
});
|
|
82
96
|
|
|
83
97
|
// ── 1b. the bar is the hub's (task 1004455) ──────────────────────────────────
|
|
@@ -259,7 +273,10 @@ function boot(answer) {
|
|
|
259
273
|
return Promise.resolve({ status, ok: status >= 200 && status < 300, json: () => Promise.resolve(data) });
|
|
260
274
|
};
|
|
261
275
|
const location = { hostname: 'cloudbongos.com', pathname: '/account' };
|
|
262
|
-
|
|
276
|
+
// one global, as in the browser: hub-bar.js sets window.HubBar, then the page reads it
|
|
277
|
+
const ctx = vm.createContext({ document, fetch, location, window: { location: { assign(p) { dom.assigned = p; } } }, URL, Array, Object, JSON, Promise });
|
|
278
|
+
vm.runInContext(BAR, ctx);
|
|
279
|
+
vm.runInContext(SCRIPT, ctx);
|
|
263
280
|
return dom;
|
|
264
281
|
}
|
|
265
282
|
const settle = async () => { for (let i = 0; i < 8; i++) await new Promise((r) => setImmediate(r)); };
|