@bongos/core 1.20.66 → 1.20.68
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 +35 -30
- package/docs/copy-inventory.md +215 -204
- package/docs/copy-registry.json +450 -340
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +3 -1
- package/docs/page-readings.json +110 -100
- package/modules/hall-ui/public/goals-lib.js +37 -5
- package/modules/hall-ui/public/goals-render.js +14 -5
- package/modules/hall-ui/public/task.js +25 -2
- package/modules/lifecycle/goal-rollup.js +13 -7
- package/modules/lifecycle/task-status.js +4 -1
- package/modules/public-landing/public/account-private.states.json +1 -1
- package/modules/public-landing/public/account.html +143 -26
- package/modules/public-landing/public/account.probes.json +30 -0
- package/modules/public-landing/public/account.states.json +7 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/hall-preview/refresh-fixtures.js +5 -1
- package/src/module-api.js +1 -1
- package/tests/claim_from_task_and_home.mjs +22 -1
- package/tests/dep_blocked_ready_cards.mjs +70 -0
- package/tests/goal_sort_staleness.mjs +8 -3
- package/tests/goals_lib.mjs +58 -2
- package/tests/hub_account_page.mjs +235 -12
|
@@ -305,8 +305,17 @@
|
|
|
305
305
|
function lifecycleRail(t, credits) {
|
|
306
306
|
const at = RAIL_AT[t.status] ?? 0;
|
|
307
307
|
const landed = t.status === 'shipped' && credits != null;
|
|
308
|
+
// task 1004487 — the first beat's caption is what tells a reader WHICH of
|
|
309
|
+
// the three unclaimed states this is, so it has to read the dependency
|
|
310
|
+
// graph too: a dep added after promotion leaves the row at `ready` while
|
|
311
|
+
// the claim gate refuses it, and "waiting for a builder" would then be the
|
|
312
|
+
// one sentence on the page that is wrong.
|
|
313
|
+
const waitingOnDep = (t.dependencies || []).some((d) => d.status !== 'shipped');
|
|
314
|
+
const claimCaption = (t.status === 'ready' && waitingOnDep)
|
|
315
|
+
? CLAIM_CAPTION.blocked
|
|
316
|
+
: (CLAIM_CAPTION[t.status] || 'one builder, one worktree');
|
|
308
317
|
const beats = [
|
|
309
|
-
['Claim',
|
|
318
|
+
['Claim', claimCaption],
|
|
310
319
|
['Build', 'the code and the docs'],
|
|
311
320
|
['Grade', 'the panel reads the diff'],
|
|
312
321
|
['Ship', 'merged, then deployed'],
|
|
@@ -727,8 +736,22 @@
|
|
|
727
736
|
// owed rebase, a module/touches conflict, …) is still possible and still
|
|
728
737
|
// surfaces, because claimAction.claim() carries the same friendlyClaimError
|
|
729
738
|
// switch the board uses, not a second guess at it here.
|
|
730
|
-
|
|
739
|
+
//
|
|
740
|
+
// task 1004487 — the status is NOT the whole answer, and the paragraph
|
|
741
|
+
// above assumed it was. A dependency added after this task was promoted
|
|
742
|
+
// leaves the row at `ready` and never at `blocked`: ADR 0015 rules that the
|
|
743
|
+
// status stays put and the READERS change, which is what core_264 did for
|
|
744
|
+
// the claimable feed. So the button also asks the dependency question the
|
|
745
|
+
// feed asks, from the `dependencies[]` the record already carries (an edge
|
|
746
|
+
// is met iff its target shipped — the DEPS_NOT_SHIPPED rule verbatim), and
|
|
747
|
+
// when the answer is no it says so in words rather than offering a control
|
|
748
|
+
// that cannot work, exactly as the retire refusals below do.
|
|
749
|
+
const unmetDeps = deps.filter((d) => d.status !== 'shipped');
|
|
750
|
+
if (t.status === 'ready' && unmetDeps.length === 0) {
|
|
731
751
|
lifecycle = `<button class="td-btn td-btn--primary" type="button" id="claim-btn">Claim it</button>`;
|
|
752
|
+
} else if (t.status === 'ready') {
|
|
753
|
+
const names = unmetDeps.map((d) => `${d.id}`).join(', ');
|
|
754
|
+
lifecycle = `<p class="rec-actnote">This task is waiting on ${unmetDeps.length === 1 ? 'task' : 'tasks'} ${escapeHtml(names)}. It cannot be claimed until ${unmetDeps.length === 1 ? 'that one ships' : 'those ship'} — see Blocked by above.</p>`;
|
|
732
755
|
} else if (t.status === 'confirmed' && (isSupervisor || ownsLastClaim)) {
|
|
733
756
|
lifecycle = `<button class="td-btn td-btn--primary" type="button" id="ship-btn">Ship it</button>`;
|
|
734
757
|
} else if (t.status === 'completed') {
|
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
// THE BUCKETS ARE THE CLIENT'S, VERBATIM. goals-lib.js `readinessOf` is the
|
|
20
20
|
// definition of record and this SQL mirrors it exactly — done (shipped |
|
|
21
21
|
// confirmed | completed) · cut (abandoned | dropped, excluded from the total the
|
|
22
|
-
// way migration 017 keeps them out of version_progress) · active ·
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
22
|
+
// way migration 017 keeps them out of version_progress) · active · blocked
|
|
23
|
+
// (unclaimed with >=1 unshipped dependency, whatever the status column says —
|
|
24
|
+
// task 1004487) · ready · backlog. A divergence here would show a person
|
|
25
|
+
// different numbers depending on which surface they opened, so
|
|
26
|
+
// tests/goal_sort_staleness.mjs pins the two against each other.
|
|
26
27
|
//
|
|
27
28
|
// LAST ACTIVITY is `max(updated_at, shipped_at)` — the same "any task activity
|
|
28
29
|
// counts" rule the client uses (task 1002965, the owner's call), now expressed
|
|
@@ -43,8 +44,8 @@ const { DONE_STATUSES, CUT_STATUSES } = require('./task-status');
|
|
|
43
44
|
// Returns [{ goal_id, done, active, ready, blocked, backlog, cut, total,
|
|
44
45
|
// last_activity_at, disciplines[], next_ready_id, next_ready_title }].
|
|
45
46
|
//
|
|
46
|
-
// `blocked` needs the dependency edges, so the CTE resolves each
|
|
47
|
-
// unshipped-dependency
|
|
47
|
+
// `blocked` needs the dependency edges, so the CTE resolves each task's
|
|
48
|
+
// unshipped-dependency flag once rather than per row. deps.pool honors the #538
|
|
48
49
|
// test seam (NODE_ENV==='test' only), like every other helper in this module.
|
|
49
50
|
// VISIBILITY. A rollup is a COUNT, and counts on a private goal are not the
|
|
50
51
|
// secret — its prose is (ADR 0112's leak rule: a non-member must not learn a
|
|
@@ -78,8 +79,13 @@ async function goalRollups({ versionId = null, viewerId = null, viewerIsArchon =
|
|
|
78
79
|
WHEN s.status = ANY($2::text[]) THEN 'done'
|
|
79
80
|
WHEN s.status = ANY($3::text[]) THEN 'cut'
|
|
80
81
|
WHEN s.status = 'active' THEN 'active'
|
|
82
|
+
-- task 1004487 — ahead of the 'ready' arm, and no longer
|
|
83
|
+
-- backlog-only: a dependency added after promotion leaves the
|
|
84
|
+
-- row at 'ready' (ADR 0015 demotes the readers, never the
|
|
85
|
+
-- status), so counting it ready advertises work POST /claims
|
|
86
|
+
-- refuses — and next_ready below reads this very bucket.
|
|
87
|
+
WHEN s.has_unshipped_dep THEN 'blocked'
|
|
81
88
|
WHEN s.status = 'ready' THEN 'ready'
|
|
82
|
-
WHEN s.status = 'backlog' AND s.has_unshipped_dep THEN 'blocked'
|
|
83
89
|
ELSE 'backlog'
|
|
84
90
|
END AS bucket
|
|
85
91
|
FROM scoped s
|
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
// tests/goal_sort_staleness.mjs pins). "Done" there means "no longer in the
|
|
28
28
|
// queue", which deliberately includes the mid-ship states. It is a progress
|
|
29
29
|
// ring, not a ledger, and must never be used to decide whether a goal may
|
|
30
|
-
// close.
|
|
30
|
+
// close. Note too that the bucketing is NOT a pure function of the status
|
|
31
|
+
// column: `blocked` there means "has an unshipped dependency", which a
|
|
32
|
+
// `ready` row can be (ADR 0015 / task 1004487), so neither set below is
|
|
33
|
+
// enough on its own to bucket a task.
|
|
31
34
|
//
|
|
32
35
|
// NO DEPENDENCIES, deliberately. This file requires nothing — not even
|
|
33
36
|
// `module-api` — so a CLI (`scripts/gds/sequence.js`) can read the vocabulary
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"stub": { "fixtures": { "me__cross-project": "me-cross-project-private" } },
|
|
6
6
|
"modes": ["dark"],
|
|
7
7
|
"modeQuery": false,
|
|
8
|
-
"ignoreRequests": ["/api/(gds|bongos)/me/cross-project$"],
|
|
8
|
+
"ignoreRequests": ["/api/(gds|bongos)/me$", "/api/(gds|bongos)/me/cross-project$"],
|
|
9
9
|
"ignoreConsole": ["/api/(gds|bongos)/me/cross-project", "\\b401\\b", "Unauthorized"],
|
|
10
10
|
"ignoreControls": [".vh"],
|
|
11
11
|
"states": {
|
|
@@ -24,14 +24,16 @@
|
|
|
24
24
|
/* The account page's own layout, in the hub's manage-view voice (a lowercase
|
|
25
25
|
light heading, hairline sections with the title left and the controls right,
|
|
26
26
|
hairline fields, the choice cells with the dot). No :root block — the pack's
|
|
27
|
-
injected theme owns the tokens; everything below reads them.
|
|
27
|
+
injected theme owns the tokens; everything below reads them. The bar and
|
|
28
|
+
the bands are cosmos.css's own rules, not restyled here. */
|
|
29
|
+
/* the skip link un-hides itself on focus, reversing everything .vh sets (the hub's rule) */
|
|
30
|
+
.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;
|
|
31
|
+
font-size:14px;font-weight:800;text-decoration:none;color:var(--ink);background:var(--bg-card);
|
|
32
|
+
border:2px solid var(--ctl-line);border-radius:12px;padding:10px 16px;}
|
|
28
33
|
.acct{position:relative;z-index:1;max-width:1060px;margin:0 auto;padding:clamp(6px,1.5vw,18px) clamp(16px,4vw,40px) 56px;}
|
|
29
34
|
.acct h1{font-family:var(--font-display);font-weight:300;font-size:clamp(28px,3.4vw,44px);letter-spacing:-.035em;line-height:1.04;text-transform:lowercase;color:var(--ink);margin:18px 0 0;}
|
|
30
35
|
.acct .lede{margin:8px 0 18px;max-width:62ch;font-size:15.5px;line-height:1.55;color:var(--ink-soft);}
|
|
31
36
|
.acct .loading{font-size:15px;color:var(--ink-soft);}
|
|
32
|
-
.aWho{display:flex;flex-wrap:wrap;align-items:center;gap:4px 14px;margin:0 0 26px;font-size:14px;color:var(--ink-soft);}
|
|
33
|
-
.aWho b{font-weight:500;color:var(--ink);overflow-wrap:anywhere;}
|
|
34
|
-
.aWho a{display:inline-flex;align-items:center;min-height:24px;color:var(--ink);text-decoration:underline;}
|
|
35
37
|
.aDoor{border-top:1px solid var(--rule);padding:30px 0 10px;max-width:62ch;}
|
|
36
38
|
.aDoor p{margin:0 0 18px;font-size:15.5px;line-height:1.55;color:var(--ink-soft);}
|
|
37
39
|
.aDoor .gh{width:18px;height:18px;fill:currentColor;}
|
|
@@ -70,12 +72,6 @@
|
|
|
70
72
|
.aSec>h2,.aSec>.aSide,.aSec>.aBody{grid-column:1;grid-row:auto;}
|
|
71
73
|
.aSec>.aSide{max-width:none;}
|
|
72
74
|
}
|
|
73
|
-
@media (max-width:420px){
|
|
74
|
-
.top{padding:16px;}
|
|
75
|
-
.top .mark{font-size:19px;}
|
|
76
|
-
.top-right{gap:8px;}
|
|
77
|
-
.top .btn-space{padding:7px 12px;}
|
|
78
|
-
}
|
|
79
75
|
@media (max-width:560px){
|
|
80
76
|
.aLinks{grid-template-columns:1fr;}
|
|
81
77
|
.aBand [role="radiogroup"]{grid-template-columns:1fr;}
|
|
@@ -85,14 +81,55 @@
|
|
|
85
81
|
</style>
|
|
86
82
|
</head>
|
|
87
83
|
<body>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
<!-- THE BAR and its two bands are the hub's own (projects.html, task 1004455):
|
|
85
|
+
same markup, same cosmos.css rules, so /projects → /account keeps the chrome.
|
|
86
|
+
No map search (it only filters the map), and the band links name /projects
|
|
87
|
+
in full because this page has no views of its own. -->
|
|
88
|
+
<a class="skip vh" href="#acct">Skip to content</a>
|
|
89
|
+
<header class="shell bar">
|
|
90
|
+
<div class="barLeft">
|
|
91
|
+
<a class="barMark" id="barMark" href="/">
|
|
92
|
+
<svg viewBox="0 0 26 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.4"
|
|
93
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
94
|
+
<ellipse cx="8.4" cy="5.6" rx="6.6" ry="2.9"/>
|
|
95
|
+
<path d="M1.8 5.6v6.6a6.6 2.9 0 0 0 13.2 0V5.6"/>
|
|
96
|
+
<ellipse cx="19.6" cy="8.4" rx="4.7" ry="2.1"/>
|
|
97
|
+
<path d="M14.9 8.4v4.6a4.7 2.1 0 0 0 9.4 0V8.4"/>
|
|
98
|
+
</svg>
|
|
99
|
+
<span>{{productName}}</span>
|
|
100
|
+
</a>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="barRight">
|
|
103
|
+
<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>
|
|
104
|
+
<!-- the identity chip IS the profile trigger; paintBar fills it from GET /me.
|
|
105
|
+
Signed out it stays hidden and the sign-in door takes its slot. -->
|
|
106
|
+
<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>
|
|
107
|
+
<a class="signinDoor" id="signinTop" hidden href="/api/bongos/auth/web/start?return=/account">Sign in</a>
|
|
93
108
|
</div>
|
|
94
109
|
</header>
|
|
95
110
|
|
|
111
|
+
<div class="menuBand" id="menuProjects" data-menu="projects" hidden>
|
|
112
|
+
<div class="bandIn">
|
|
113
|
+
<ul class="bandRow">
|
|
114
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=map">Sky</a><span class="bandC">the sky of every project</span></li>
|
|
115
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=map&sub=table">Table</a><span class="bandC">the same projects as rows</span></li>
|
|
116
|
+
<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>
|
|
117
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=new">New project</a><span class="bandC">five questions and a home</span></li>
|
|
118
|
+
</ul>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="menuBand" id="menuProfile" data-menu="profile" hidden>
|
|
122
|
+
<div class="bandIn bandProfile">
|
|
123
|
+
<p class="bandWho" id="bandWho"><img id="bandWhoImg" alt="" hidden><span class="whoName" id="bandWhoLogin"></span></p>
|
|
124
|
+
<ul class="bandRow">
|
|
125
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=mine">My projects</a></li>
|
|
126
|
+
<li class="bandCell"><a class="bandT" href="/account" aria-current="page">Account</a></li>
|
|
127
|
+
<!-- the honest hover target; the delegated click upgrades it to the POST -->
|
|
128
|
+
<li class="bandCell"><a class="bandT signout" href="/api/bongos/auth/logout">Sign out</a></li>
|
|
129
|
+
</ul>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
96
133
|
<main class="acct" id="acct">
|
|
97
134
|
<h1>your account</h1>
|
|
98
135
|
<p class="lede">Who you are across every project on {{productName}}, and who can find you. It is one account, so you change these once, here — each project’s own settings stay in that project’s hall.</p>
|
|
@@ -108,10 +145,6 @@
|
|
|
108
145
|
</a>
|
|
109
146
|
</section>
|
|
110
147
|
|
|
111
|
-
<!-- who is signed in, and the way out: in the page rather than the bar, so
|
|
112
|
-
the bar keeps the footer doors' two pills and fits a phone on one line -->
|
|
113
|
-
<p class="aWho" id="aWho" hidden><span>Signed in as <b id="whoName"></b></span><a href="/api/bongos/auth/logout" id="signOut">sign out</a></p>
|
|
114
|
-
|
|
115
148
|
<p class="loading" id="loading">Reading your account…</p>
|
|
116
149
|
<div class="alert" id="pageAlert" role="alert"></div>
|
|
117
150
|
|
|
@@ -214,7 +247,7 @@
|
|
|
214
247
|
</div>
|
|
215
248
|
</main>
|
|
216
249
|
|
|
217
|
-
<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">Terms</a><a href="/contact">Contact</a></nav></footer>
|
|
250
|
+
<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">Terms</a><a href="/contact">Contact</a><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a></nav></footer>
|
|
218
251
|
|
|
219
252
|
<script>
|
|
220
253
|
(function () {
|
|
@@ -265,7 +298,7 @@
|
|
|
265
298
|
function showDoor() {
|
|
266
299
|
$('loading').hidden = true;
|
|
267
300
|
$('acctBody').hidden = true;
|
|
268
|
-
|
|
301
|
+
paintBar(null);
|
|
269
302
|
$('door').hidden = false;
|
|
270
303
|
}
|
|
271
304
|
function showAlert(text) {
|
|
@@ -283,8 +316,6 @@
|
|
|
283
316
|
$('loading').hidden = true;
|
|
284
317
|
$('door').hidden = true;
|
|
285
318
|
$('acctBody').hidden = false;
|
|
286
|
-
$('whoName').textContent = acct.handle || acct.display_name || 'your account';
|
|
287
|
-
$('aWho').hidden = false;
|
|
288
319
|
}, function () { showAlert('Couldn’t reach the server — check your connection and refresh.'); });
|
|
289
320
|
}
|
|
290
321
|
|
|
@@ -492,8 +523,86 @@
|
|
|
492
523
|
: 'Cross-project record shown on your public profile.');
|
|
493
524
|
});
|
|
494
525
|
|
|
495
|
-
// ----
|
|
496
|
-
|
|
526
|
+
// ---- the bar: who is signed in, from the hub's own identity read. Signed
|
|
527
|
+
// out there is no chip and no Your-projects item; the sign-in door takes the slot ----
|
|
528
|
+
function paintBar(me) {
|
|
529
|
+
var login = me ? (me.github_login || 'signed in') : '';
|
|
530
|
+
var avatar = me && me.avatar_url;
|
|
531
|
+
$('who').hidden = !me;
|
|
532
|
+
$('signinTop').hidden = !!me;
|
|
533
|
+
$('mineCell').hidden = !me;
|
|
534
|
+
[['whoImg', 'whoLogin'], ['bandWhoImg', 'bandWhoLogin']].forEach(function (ids) {
|
|
535
|
+
$(ids[0]).hidden = !avatar;
|
|
536
|
+
if (avatar) $(ids[0]).src = avatar;
|
|
537
|
+
$(ids[1]).textContent = login;
|
|
538
|
+
});
|
|
539
|
+
if (me) $('who').setAttribute('aria-label', 'Account, ' + login);
|
|
540
|
+
else closeMenus();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ---- the bands: the hub's disclosure (projects.html "THE BANDS") without
|
|
544
|
+
// the map search. Escape closes and returns focus to the trigger, a click
|
|
545
|
+
// outside closes, the arrows walk the items, opening one closes the other ----
|
|
546
|
+
function openBand() { return document.querySelector('.menuBand:not([hidden])'); }
|
|
547
|
+
function closeMenus() {
|
|
548
|
+
var open = openBand();
|
|
549
|
+
if (!open) return;
|
|
550
|
+
var trig = document.querySelector('.trig[aria-expanded="true"]');
|
|
551
|
+
var inside = open.contains(document.activeElement);
|
|
552
|
+
open.hidden = true;
|
|
553
|
+
if (trig) trig.setAttribute('aria-expanded', 'false');
|
|
554
|
+
if (inside && trig) trig.focus();
|
|
555
|
+
}
|
|
556
|
+
function openMenu(trig, byKeyboard) {
|
|
557
|
+
closeMenus();
|
|
558
|
+
trig.setAttribute('aria-expanded', 'true');
|
|
559
|
+
var band = $(trig.getAttribute('aria-controls'));
|
|
560
|
+
band.hidden = false;
|
|
561
|
+
// Enter/Space on the trigger lands on the first item; a pointer stays put
|
|
562
|
+
if (byKeyboard) { var first = band.querySelector('.bandCell:not([hidden]) a'); if (first) first.focus(); }
|
|
563
|
+
}
|
|
564
|
+
document.querySelectorAll('.trig').forEach(function (t) {
|
|
565
|
+
t.addEventListener('click', function (ev) {
|
|
566
|
+
if (t.getAttribute('aria-expanded') === 'true') closeMenus();
|
|
567
|
+
else openMenu(t, ev.detail === 0);
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
document.addEventListener('click', function (ev) {
|
|
571
|
+
if (!openBand()) return;
|
|
572
|
+
if (ev.target.closest && (ev.target.closest('.menuBand') || ev.target.closest('.trig'))) return;
|
|
573
|
+
closeMenus();
|
|
574
|
+
});
|
|
575
|
+
document.addEventListener('keydown', function (ev) {
|
|
576
|
+
var open = openBand();
|
|
577
|
+
if (!open) return;
|
|
578
|
+
if (ev.key === 'Escape') { closeMenus(); return; }
|
|
579
|
+
if (ev.key !== 'ArrowDown' && ev.key !== 'ArrowUp') return;
|
|
580
|
+
var items = Array.prototype.filter.call(open.querySelectorAll('a'), function (a) { return !a.closest('[hidden]'); });
|
|
581
|
+
if (!items.length) return;
|
|
582
|
+
var at = items.indexOf(document.activeElement);
|
|
583
|
+
ev.preventDefault();
|
|
584
|
+
items[(at + (ev.key === 'ArrowDown' ? 1 : items.length - 1) + items.length) % items.length].focus();
|
|
585
|
+
});
|
|
586
|
+
document.querySelectorAll('.menuBand').forEach(function (b) {
|
|
587
|
+
b.addEventListener('click', function (ev) {
|
|
588
|
+
if (ev.target.closest && ev.target.closest('a')) closeMenus();
|
|
589
|
+
});
|
|
590
|
+
// Tab out of the band closes it; a null relatedTarget is the window losing
|
|
591
|
+
// focus or a click on nothing, which the document click covers
|
|
592
|
+
b.addEventListener('focusout', function (ev) {
|
|
593
|
+
var to = ev.relatedTarget;
|
|
594
|
+
if (!to || b.contains(to) || (to.closest && to.closest('.trig'))) return;
|
|
595
|
+
closeMenus();
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// ---- sign out: the hub's own logout (projects.html doLogout), delegated on
|
|
600
|
+
// the profile band's plain anchor ----
|
|
601
|
+
$('menuProfile').addEventListener('click', function (ev) {
|
|
602
|
+
if (!ev.target.closest) return;
|
|
603
|
+
// Account is this page: a reload would only throw away what was typed
|
|
604
|
+
if (ev.target.closest('[aria-current="page"]')) { ev.preventDefault(); return; }
|
|
605
|
+
if (!ev.target.closest('a.signout')) return;
|
|
497
606
|
ev.preventDefault();
|
|
498
607
|
var finish = function () {
|
|
499
608
|
var apex = location.hostname.split('.').slice(-2).join('.');
|
|
@@ -506,7 +615,15 @@
|
|
|
506
615
|
fetch(API + '/auth/logout', { method: 'POST', credentials: 'same-origin' }).then(finish, finish);
|
|
507
616
|
});
|
|
508
617
|
|
|
509
|
-
|
|
618
|
+
var whoRead = request('GET', '/me').then(function (res) {
|
|
619
|
+
var d = res.ok && res.data;
|
|
620
|
+
paintBar(d ? (d.builder || d.me || d) : null);
|
|
621
|
+
}, function () { paintBar(null); });
|
|
622
|
+
// The identity read failing is not a sign-out. If the account itself was
|
|
623
|
+
// read, the chip names it from that answer, so the way out is never lost.
|
|
624
|
+
load().then(function () { return whoRead; }).then(function () {
|
|
625
|
+
if (acct && $('who').hidden) paintBar({ github_login: acct.handle || acct.display_name });
|
|
626
|
+
});
|
|
510
627
|
})();
|
|
511
628
|
</script>
|
|
512
629
|
</body>
|
|
@@ -30,6 +30,36 @@
|
|
|
30
30
|
{ "name": "hiding the record saves hide_stats alone", "continue": true,
|
|
31
31
|
"steps": [["click", "#aRecord .aOpt:nth-child(2)"], ["click", "#recordSave"], ["wait", 400]],
|
|
32
32
|
"expect": { "text": [["#recordMsg", "Cross-project record hidden — removed from your profile and directories immediately."]], "checked": "input[name=record][value=hide]" } },
|
|
33
|
+
{ "name": "the hub's bar: the chip names the person, both bands closed, the page starts under the bar (task 1004455)", "auth": true, "width": 1440,
|
|
34
|
+
"steps": [["wait", 400]],
|
|
35
|
+
"expect": { "expanded": false, "top": [["#acct", 64]], "visible": ["#projTrig", "#who"], "hidden": ["#signinTop", "#menuProjects", "#menuProfile"], "text": [["#whoLogin", "builder"]], "count": [["#mapQ", 0]] } },
|
|
36
|
+
{ "name": "Enter opens Projects and lands on the first item; the band overlays and the page does not move", "continue": true,
|
|
37
|
+
"steps": [["focus", "#projTrig"], ["press", "Enter"], ["wait", 150]],
|
|
38
|
+
"expect": { "open": "#menuProjects", "activeText": "Sky", "top": [["#acct", 64]], "href": [["#menuProjects .bandCell:first-child a", "/projects?view=map"]] } },
|
|
39
|
+
{ "name": "ArrowDown walks to Table", "continue": true,
|
|
40
|
+
"steps": [["press", "ArrowDown"]],
|
|
41
|
+
"expect": { "activeText": "Table" } },
|
|
42
|
+
{ "name": "ArrowUp wraps to the last item", "continue": true,
|
|
43
|
+
"steps": [["press", "ArrowUp"], ["press", "ArrowUp"]],
|
|
44
|
+
"expect": { "activeText": "New project" } },
|
|
45
|
+
{ "name": "Escape closes and returns focus to the trigger", "continue": true,
|
|
46
|
+
"steps": [["press", "Escape"], ["wait", 100]],
|
|
47
|
+
"expect": { "closed": "#menuProjects", "activeId": "projTrig", "expanded": false } },
|
|
48
|
+
{ "name": "opening the profile band closes the Projects band; Account is the current place", "continue": true,
|
|
49
|
+
"steps": [["click", "#projTrig"], ["wait", 120], ["click", "#who"], ["wait", 120]],
|
|
50
|
+
"expect": { "open": "#menuProfile", "closed": "#menuProjects", "expandedIds": ["who"], "text": [["#bandWhoLogin", "builder"], ["#menuProfile [aria-current=\"page\"]", "Account"]], "count": [["#bandWho img", 1], ["[aria-current=\"page\"]", 1]], "href": [["#menuProfile a.signout", "/api/bongos/auth/logout"], ["#menuProfile .bandCell:first-child a", "/projects?view=mine"]] } },
|
|
51
|
+
{ "name": "a click outside closes", "continue": true,
|
|
52
|
+
"steps": [["clickAt", 700, 600], ["wait", 120]],
|
|
53
|
+
"expect": { "closed": "#menuProfile", "expanded": false } },
|
|
54
|
+
{ "name": "Tab out of the band closes it", "continue": true,
|
|
55
|
+
"steps": [["click", "#projTrig"], ["wait", 120], ["focus", "#menuProjects .bandCell:last-child a"], ["press", "Tab"], ["wait", 120]],
|
|
56
|
+
"expect": { "closed": "#menuProjects" } },
|
|
57
|
+
{ "name": "signed out: no chip, the sign-in door in its slot, returning to /account", "auth": false, "width": 1440,
|
|
58
|
+
"steps": [["wait", 300]],
|
|
59
|
+
"expect": { "hidden": ["#who"], "visible": ["#signinTop", "#projTrig"], "href": [["#signinTop", "/api/bongos/auth/web/start?return=/account"]] } },
|
|
60
|
+
{ "name": "signed out the Projects band is Sky / Table / New project (no Your projects)", "continue": true,
|
|
61
|
+
"steps": [["click", "#projTrig"], ["wait", 120]],
|
|
62
|
+
"expect": { "open": "#menuProjects", "count": [["#menuProjects .bandCell:not([hidden])", 3]], "hidden": ["#mineCell"] } },
|
|
33
63
|
{ "name": "phone width: the bar stays one line and nothing scrolls sideways", "auth": true, "width": 320,
|
|
34
64
|
"steps": [["wait", 400]],
|
|
35
65
|
"expect": { "noOverflow": true, "visible": ["#acctBody"] } }
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_": "The account page's states for modules/ui-design/kit/render.js (task 1004259, ADR 0337 D4.3): signed out it is the sign-in door and nothing else; signed in it is the four sections, painted from the stub's one account (fixtures/me__cross-project.json — public, recruiters following the profile setting, a bio and two links). Dark only, like every apex page (ADR 0204). The private, hidden, not-yet-published account renders from account-private.states.json.",
|
|
2
|
+
"_": "The account page's states for modules/ui-design/kit/render.js (task 1004259, ADR 0337 D4.3): signed out it is the sign-in door and nothing else; signed in it is the hub's bar (task 1004455: the identity chip from GET /me, the Projects band and the profile band with Account current) over the four sections, painted from the stub's one account (fixtures/me__cross-project.json — public, recruiters following the profile setting, a bio and two links). Dark only, like every apex page (ADR 0204). The private, hidden, not-yet-published account renders from account-private.states.json.",
|
|
3
3
|
"page": "/account",
|
|
4
4
|
"surface": "public-landing",
|
|
5
5
|
"modes": ["dark"],
|
|
6
6
|
"modeQuery": false,
|
|
7
|
-
"ignoreRequests": ["/api/(gds|bongos)/me/cross-project$"],
|
|
8
|
-
"ignoreConsole": ["/api/(gds|bongos)/me
|
|
7
|
+
"ignoreRequests": ["/api/(gds|bongos)/me$", "/api/(gds|bongos)/me/cross-project$"],
|
|
8
|
+
"ignoreConsole": ["/api/(gds|bongos)/me", "\\b401\\b", "Unauthorized"],
|
|
9
9
|
"ignoreControls": [".vh"],
|
|
10
10
|
"states": {
|
|
11
|
-
"out": { "auth": false, "actions": [["wait", 300]], "expect": { "visible": ["#door", "#signin"], "hidden": ["#acctBody", "#
|
|
12
|
-
"in": { "auth": true, "actions": [["wait", 400]], "expect": { "visible": ["#acctBody", "#fName", "#fBio", "#fHandle", "#recWhat", "#recordLine", "#
|
|
11
|
+
"out": { "auth": false, "actions": [["wait", 300]], "expect": { "visible": ["#door", "#signin", "#signinTop", "#projTrig"], "hidden": ["#acctBody", "#who", "#loading"] } },
|
|
12
|
+
"in": { "auth": true, "actions": [["wait", 400]], "expect": { "visible": ["#acctBody", "#fName", "#fBio", "#fHandle", "#recWhat", "#recordLine", "#who"], "text": [["#whoLogin", "builder"], ["#recordLine", "Your record: 3 projects · 1000 credits · 19 works shipped · 10 karma."]], "hidden": ["#door", "#loading", "#signinTop", "#menuProjects", "#menuProfile"], "value": [["#fHandle", "builder"], ["#fName", "A Builder"], ["#fLinkX", "https://x.com/builder"]] } },
|
|
13
|
+
"in-projects-band": { "auth": true, "actions": [["wait", 400], ["click", "#projTrig"], ["wait", 300]], "expect": { "visible": ["#menuProjects", "#mineCell"], "hidden": ["#menuProfile"] } },
|
|
14
|
+
"in-profile-band": { "auth": true, "actions": [["wait", 400], ["click", "#who"], ["wait", 300]], "expect": { "visible": ["#menuProfile"], "hidden": ["#menuProjects"], "text": [["#bandWhoLogin", "builder"], ["#menuProfile [aria-current=\"page\"]", "Account"]] } },
|
|
13
15
|
"in-privacy": { "auth": true, "actions": [["wait", 400], ["click", "#aPrivacy .aOpt:nth-child(2)"], ["wait", 150]], "expect": { "checked": "input[name=visibility][value=private]", "text": [["#recWhat", "They will see your name, your picture, your roles, and whether you have been active in the last 30 days — never your credits, works shipped, karma, or the projects you build on. This is the one thing a recruiter can see while your profile page is private. Turning this off removes you from both recruiting surfaces — the recruiting directory, and a recruiter’s look-up of one builder — straight away. · For now this follows your profile page setting."]] } }
|
|
14
16
|
},
|
|
15
17
|
"reducedMotion": { "auth": true, "actions": [["wait", 300]] }
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.68",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.68",
|
|
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.20.
|
|
3
|
+
"version": "1.20.68",
|
|
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
|
@@ -8525,5 +8525,17 @@
|
|
|
8525
8525
|
"id": "1004485",
|
|
8526
8526
|
"text": "Goals can now be moved from one version to another, taking their tasks and done-when criteria with them and keeping every task number. Used first to move the two goals that did not make V2 into V3."
|
|
8527
8527
|
}
|
|
8528
|
+
],
|
|
8529
|
+
"1.20.67": [
|
|
8530
|
+
{
|
|
8531
|
+
"id": "1004487",
|
|
8532
|
+
"text": "The hall no longer offers a Claim button on work the system will refuse. If a task gets a new prerequisite after it was opened up for claiming, its page and its goal card now say it is waiting on that other task — and name whi"
|
|
8533
|
+
}
|
|
8534
|
+
],
|
|
8535
|
+
"1.20.68": [
|
|
8536
|
+
{
|
|
8537
|
+
"id": "1004455",
|
|
8538
|
+
"text": "The account page on cloudbongos.com now has the same top bar as the projects page — the Projects menu and your profile menu (My projects, Account, Sign out) — so moving between the two no longer swaps the whole header."
|
|
8539
|
+
}
|
|
8528
8540
|
]
|
|
8529
8541
|
}
|
|
@@ -58,7 +58,11 @@ const SNAPSHOTS = [
|
|
|
58
58
|
['goals/25/conflicts', 'goals__conflicts'],
|
|
59
59
|
['goals/25/requests', 'goals__requests'],
|
|
60
60
|
['tasks/claimable', 'tasks__claimable'],
|
|
61
|
-
|
|
61
|
+
// &include=deps (task 1004487) — the goal page's task cards read `blocked_by[]`
|
|
62
|
+
// to tell a claimable `ready` row from one whose dependencies have not shipped.
|
|
63
|
+
// Without it every row refreshes with no dependency data and the harness can
|
|
64
|
+
// only ever demo the claimable half of that fork.
|
|
65
|
+
['tasks?status=ready&include=deps', 'tasks', trimList('tasks', 60)],
|
|
62
66
|
['tasks/2166', 'tasks__detail'],
|
|
63
67
|
['tasks/peer-votes/feed?limit=25', 'tasks__peer-votes__feed'],
|
|
64
68
|
['analytics/builder/95?days=90', 'analytics__builder'],
|
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.20.
|
|
78
|
+
const CORE_VERSION = '1.20.68'; // 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');
|
|
@@ -38,11 +38,32 @@ await test('task.js constructs the shared claim action', () => {
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
await test('task.js renders a Claim button only when the task is ready', () => {
|
|
41
|
-
const m = taskJs.match(/if \(t\.status === 'ready'\) \{\s*lifecycle = `([^`]*)`/);
|
|
41
|
+
const m = taskJs.match(/if \(t\.status === 'ready' && unmetDeps\.length === 0\) \{\s*lifecycle = `([^`]*)`/);
|
|
42
42
|
assert.ok(m, 'no ready-status branch found in the lifecycle chain');
|
|
43
43
|
assert.match(m[1], /id="claim-btn"/);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
// Task 1004487 — `ready` alone was never the claim gate's question. A dependency
|
|
47
|
+
// added after promotion leaves the row at `ready` (ADR 0015: the status is
|
|
48
|
+
// deliberately never demoted), and POST /claims refuses it with
|
|
49
|
+
// DEPS_NOT_SHIPPED. core_264 taught the claimable FEED that; this page was the
|
|
50
|
+
// surface it did not reach.
|
|
51
|
+
await test('a dep-blocked ready task gets NO Claim button, and is told why', () => {
|
|
52
|
+
assert.match(taskJs, /const unmetDeps = deps\.filter\(\(d\) => d\.status !== 'shipped'\);/,
|
|
53
|
+
'the unmet-dependency predicate must mirror the gate: met iff the target shipped');
|
|
54
|
+
const refusal = taskJs.match(/\} else if \(t\.status === 'ready'\) \{\s*([\s\S]{0,600}?)\n {4}\} else if/);
|
|
55
|
+
assert.ok(refusal, 'no dep-blocked ready branch after the claimable one');
|
|
56
|
+
assert.doesNotMatch(refusal[1], /claim-btn/, 'a task the gate will refuse must not be offered the button');
|
|
57
|
+
assert.match(refusal[1], /rec-actnote/, 'the refusal is pre-answered in words, like the retire refusals');
|
|
58
|
+
assert.match(refusal[1], /waiting on/, 'and it names what it is waiting on');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await test("the lifecycle rail's first beat does not call a dep-blocked task 'waiting for a builder'", () => {
|
|
62
|
+
assert.match(taskJs, /const waitingOnDep = \(t\.dependencies \|\| \[\]\)\.some\(\(d\) => d\.status !== 'shipped'\);/);
|
|
63
|
+
assert.match(taskJs, /t\.status === 'ready' && waitingOnDep\)\s*\n?\s*\? CLAIM_CAPTION\.blocked/,
|
|
64
|
+
"a dep-blocked ready row borrows the 'blocked' caption — waiting on another task");
|
|
65
|
+
});
|
|
66
|
+
|
|
46
67
|
await test('the Claim button click routes through claimAction.claim, not a raw POST', () => {
|
|
47
68
|
const m = taskJs.match(/const claimBtn = \$\('#claim-btn'\);[\s\S]*?\n {4}\}/);
|
|
48
69
|
assert.ok(m, 'claim-btn wiring block not found');
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// tests/dep_blocked_ready_cards.mjs — task 1004487.
|
|
2
|
+
//
|
|
3
|
+
// THE BUG. ADR 0015 rules that a dependency added AFTER a task was promoted
|
|
4
|
+
// leaves the row at `ready`: the status answers "was this promoted?", not "is
|
|
5
|
+
// this workable right now?", so nothing demotes it. `core_264` (task 1004190)
|
|
6
|
+
// taught the claimable FEED to ask the dependency graph instead of the status
|
|
7
|
+
// column. The Goals page's task cards were a surface it did not reach — they
|
|
8
|
+
// read `t.status === 'ready'` alone, so a dep-blocked row got a bulk-claim
|
|
9
|
+
// checkbox and a Claim button for work `POST /claims` refuses with
|
|
10
|
+
// DEPS_NOT_SHIPPED.
|
|
11
|
+
//
|
|
12
|
+
// The predicate itself (`hasUnmetDeps` / `readinessOf`) is proven BEHAVIOURALLY
|
|
13
|
+
// in tests/goals_lib.mjs, and the server-side mirror in
|
|
14
|
+
// tests/goal_sort_staleness.mjs. What is left to pin, and what this file pins,
|
|
15
|
+
// is that the card actually CALLS it — and that the data it needs is asked for,
|
|
16
|
+
// because the predicate is silent (correctly) when `blocked_by[]` is absent, so
|
|
17
|
+
// a dropped `?include=deps` would reopen the bug with every test still green.
|
|
18
|
+
//
|
|
19
|
+
// goals-render.js is a browser IIFE reaching for `window` at eval time, so it is
|
|
20
|
+
// read as text — the idiom tests/claim_action.mjs and claim_from_task_and_home.mjs
|
|
21
|
+
// already use for these files.
|
|
22
|
+
//
|
|
23
|
+
// Run: node tests/dep_blocked_ready_cards.mjs
|
|
24
|
+
|
|
25
|
+
import { strict as assert } from 'node:assert';
|
|
26
|
+
import { readFileSync } from 'node:fs';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
import { dirname, join } from 'node:path';
|
|
29
|
+
import { makeRunner } from './helpers.mjs';
|
|
30
|
+
|
|
31
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const repoRoot = join(__dirname, '..');
|
|
33
|
+
const read = (...p) => readFileSync(join(repoRoot, ...p), 'utf8');
|
|
34
|
+
const { test, summary } = makeRunner();
|
|
35
|
+
|
|
36
|
+
const renderJs = read('modules', 'hall-ui', 'public', 'goals-render.js');
|
|
37
|
+
|
|
38
|
+
await test('the goal page ASKS for the dependency data its cards now read', () => {
|
|
39
|
+
// Without include=deps every task arrives with no blocked_by[], hasUnmetDeps
|
|
40
|
+
// answers false for all of them, and the gate below is a no-op that still passes.
|
|
41
|
+
assert.match(renderJs, /\$\{API\}\/tasks\?goal_id=\$\{encodeURIComponent\(goalId\)\}&include=deps/,
|
|
42
|
+
'loadGoalTasks must request ?include=deps');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await test('a dep-blocked ready card is not offered the claim affordances', () => {
|
|
46
|
+
assert.match(renderJs, /const isReadyNow = t\.status === 'ready' && !hasUnmetDeps\(t\);/,
|
|
47
|
+
"the card's claimable predicate must ask the dependency question, not the status alone");
|
|
48
|
+
// The checkbox and the Claim button both hang off isReady, so gating that one
|
|
49
|
+
// name is what removes both.
|
|
50
|
+
assert.match(renderJs, /const isReady = isReadyNow && !!state\.me;/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await test('the bulk selection prunes a tick that is no longer claimable', () => {
|
|
54
|
+
// A tick that survives the prune rides into a bulk claim the server refuses,
|
|
55
|
+
// which is the same lie one layer down.
|
|
56
|
+
assert.match(renderJs, /state\.work\.todo\.filter\(\(t\) => t\.status === 'ready' && !hasUnmetDeps\(t\)\)/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await test('the card takes its lead tag from the lib, never a status echo of its own', () => {
|
|
60
|
+
// Hiding the checkbox without changing the word would leave the row reading
|
|
61
|
+
// "ready" with no affordance and no reason — worse than the bug. The tag is
|
|
62
|
+
// pure, so it lives in goals-lib.js and is proven BEHAVIOURALLY in
|
|
63
|
+
// tests/goals_lib.mjs; what this pins is that the card calls it and that the
|
|
64
|
+
// hand-rolled status span it replaced has not grown back beside it.
|
|
65
|
+
assert.match(renderJs, /: taskStatusTagHtml\(t\)\);/);
|
|
66
|
+
assert.doesNotMatch(renderJs, /<span class="task__tag" title="Task status">/,
|
|
67
|
+
'goals-render.js must not re-grow its own status tag');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
summary();
|
|
@@ -171,9 +171,14 @@ await test('the SQL rollup buckets MIRROR goals-lib readinessOf exactly', () =>
|
|
|
171
171
|
assert.match(lib, /DONE_STATUSES = new Set\(\['completed', 'confirmed', 'shipped'\]\)/);
|
|
172
172
|
assert.match(vocab, /const CUT_STATUSES = \['abandoned', 'dropped'\]/);
|
|
173
173
|
assert.match(lib, /CUT_STATUSES = new Set\(\['abandoned', 'dropped'\]\)/);
|
|
174
|
-
// blocked =
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
// blocked = >=1 unshipped dependency on an unclaimed task, both sides — and
|
|
175
|
+
// on BOTH sides the arm sits AHEAD of 'ready' (task 1004487), because a dep
|
|
176
|
+
// added after promotion leaves the row at 'ready' and a status-first
|
|
177
|
+
// bucketing would then count un-claimable work as ready. The ORDER is the
|
|
178
|
+
// assertion, not just the presence of the clause.
|
|
179
|
+
assert.match(rollup, /WHEN s\.has_unshipped_dep\s+THEN 'blocked'\s*\n\s*WHEN s\.status = 'ready'\s+THEN 'ready'/);
|
|
180
|
+
assert.match(lib, /const hasUnmetDeps = \(t\) => \(\(t && t\.blocked_by\) \|\| \[\]\)\.some\(\(d\) => !d\.satisfied\);/);
|
|
181
|
+
assert.match(lib, /if \(hasUnmetDeps\(t\)\) return 'blocked';\s*\n\s*if \(s === 'ready'\) return 'ready';/);
|
|
177
182
|
// cut is excluded from the total on both sides (migration 017's rule)
|
|
178
183
|
assert.match(rollup, /FILTER \(WHERE b\.bucket <> 'cut'\)::int\s+AS total/);
|
|
179
184
|
assert.match(read('modules/hall-ui/public/goals-page.js'), /total: row\.total \|\| 0/);
|