@bongos/core 1.19.462 → 1.19.463

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/docs/file-map.md CHANGED
@@ -127,7 +127,7 @@
127
127
  │ ├── sessions/ ← CORE-DOMAIN carve (BV1.R82, default-on): the session_records corpus + data plane (upload, cross-builder search/deep-dive), the BFG read-the-room scorer, session-pulse/stale-claim helpers. Registers no port — reached via HTTP POST to its own /sessions route; resolves the economy `reward` port for the cost-plus token reward. CLAUDE.md, module.json, routes/sessions.js, bfg.js, pulse.js.
128
128
  │ ├── autonomy/ ← CORE-DOMAIN carve (BV1.R83, default-on): the MAS autonomy surfaces — the $0 deterministic autonomy gate (System 3, inert until armed), the conductor/architect CLIs, and the in-process routine timers (incl. the weekly peer-vote-tally poller). Registers no port; resolves the lifecycle `tallyPeerVotes` doorway. CLAUDE.md, module.json, routes/autonomy.js, autonomy.js.
129
129
  │ ├── status-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R84, default-on): the public status dashboard at status.<apex>, moved from public-status/. NO routes/db/port — declares contributes.webSurfaces:[{host:"status.",dir:"public"}], served by the loader web-surface seam (moduleWebSurfaces → serve-internal). CLAUDE.md, module.json, public/{index.html,status.js,style.css,cursors/,snapshots/}.
130
- │ ├── hall-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R85, default-on): the authenticated builders' hall at builders.<apex>, moved from public-builders/ (~13k LOC, the largest). NO routes/db/port — declares contributes.webSurfaces:[{host:"builders.",dir:"public"}] for discovery, but UNLIKE status-ui keeps its DEDICATED serving block in serve-internal (the /builders shim + canonical redirects + citizen/Archon page gates + asset-stamping + hall-widget injection), partitioned out of the generic web-surface loop and served from BUILDERS_DIR. CLAUDE.md, module.json, public/{index.html,builders.js,style.css,work,watch,harbor,gate,pair,settings,ranks,sessions,primer,diagrams,atlas,...}.{html,js},cursors/,chime.wav. Three ARCHETYPE sheets sit beside the per-page ones: `record.css` (the task + idea records, task 1003305), `room.css` (the READING ROOMS — the primer + the diagrams index, task 1003306: the reading measure and rhythm, the table of contents, the progress hairline and the framed figure diagram-viewer.js upgrades on both pages) and `oversight.css` (the OVERSIGHT family — government, watch, harbor, gate, task 1003469: the row, the figure band inside a panel, the group head, the inline form, the grant toggle and the shared confirm modal; named oversight because the vocabulary wall bans "governance" on the government surface). A page sheet then holds only what one page has and the others do not — `primer.css` the acknowledge gate, `diagrams.css` the legend strip, `gate.css` the hold reasons and file chips, `watch.css` the severities, the report reader and the grade trend.
130
+ │ ├── hall-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R85, default-on): the authenticated builders' hall at builders.<apex>, moved from public-builders/ (~13k LOC, the largest). NO routes/db/port — declares contributes.webSurfaces:[{host:"builders.",dir:"public"}] for discovery, but UNLIKE status-ui keeps its DEDICATED serving block in serve-internal (the /builders shim + canonical redirects + citizen/Archon page gates + asset-stamping + hall-widget injection), partitioned out of the generic web-surface loop and served from BUILDERS_DIR. CLAUDE.md, module.json, public/{index.html,builders.js,style.css,work,watch,harbor,gate,pair,settings,ranks,sessions,primer,diagrams,atlas,...}.{html,js},cursors/,chime.wav. Three ARCHETYPE sheets sit beside the per-page ones: `record.css` (the task + idea records, task 1003305), `room.css` (the READING ROOMS — the primer + the diagrams index, task 1003306: the reading measure and rhythm, the table of contents, the progress hairline and the framed figure diagram-viewer.js upgrades on both pages) and `oversight.css` (the OVERSIGHT family — government, watch, harbor, gate, task 1003469: the row, the figure band inside a panel, the group head, the inline form, the grant toggle and the shared confirm modal; named oversight because the vocabulary wall bans "governance" on the government surface). A page sheet then holds only what one page has and the others do not — `primer.css` the acknowledge gate, `diagrams.css` the legend strip, `gate.css` the hold reasons and file chips, `watch.css` the severities, the report reader and the grade trend, `people.css` the directory's identity cell (task 1003470: `/scouting` is the LEGACY DOOR to `/people` and ships the same body and script — `scouting.html` loads `people.js`; there is no `scouting.js`).
131
131
  │ ├── lifecycle/ ← CORE-DOMAIN carve (BV1.R86, default-on): the build state machine — tasks · claims · versions · done-when · goals · dependencies · claim→ship→grade→credit · github-push + merge-lock + conflict-resolve + publish-reconciler. The LAST, most-coupled carve; registers the `lifecycle` kernel port (createTask, classifyTaskKind, tallyPeerVotes, work-tracking reads). CLAUDE.md, module.json, routes/{tasks,claims,versions,done-when,goals,dependencies,gate-approvals,analytics}.js, lifecycle.js, github-push.js, merge-lock.js, conflict-resolve.js, publish-reconciler.js, task-visuals.js + task-visual-db.js + routes/visuals.js (the optional ship-time visual, task 1003109 — the route file is NOT named task-*.js on purpose; see the module CLAUDE.md).
132
132
  │ ├── copy-desk/ ← CORE-DOMAIN module (task 1003113 / R02 of goal 1000074, default-on): the COPY half of the design contract — ADR 0081 gave design TOKENS a repo-owned source of truth, text had none. Owns the FLAG (anyone in any role marks a string or a whole surface as needing an artist's attention, with a reason required in BOTH the route and a CHECK constraint) and the QUEUE an artist reads it from. READS R01's committed registry (docs/copy-registry.json, generated by scripts/gds/copy-inventory.js) and never writes it. NO LIVE CMS is the hard non-goal: no column here could hold replacement text, and copy_no_cms.mjs pins that. Key is `copy-desk`, not `copy` — the branding contract already owns a `copy` block, so the bare word reds the "core carries no module code" fitness check. CLAUDE.md, module.json, registry.js, flags.js, routes/copy-desk.js, migrations/copy_desk_001_flags.sql, tests/{copy_flags,copy_queue,copy_no_cms}.mjs. Its hall surface lives with hall-ui (public/copy-desk.{html,js,css}), per the one-key-per-web-surface precedent.
133
133
  │ ├── ui-design/ ← CORE-DOMAIN module (task 1003324 / ADR 0197, default-on): the design capability every builder on every instance gets — ships METHOD, never a world. Absorbs ADR 0081's layer: adapters/{claude-design,figma}/index.js (moved from src/ui/adapters/), scripts/{design-sync,figma-design-sync,validate-design}.js (moved from scripts/gds/, which keeps thin shims), docs/design-contract.md (moved from docs/). Declaration-only: contributes.skills design (the /design playbook, new) + design-sync + figma-design-sync; no routes/port/migrations, so disabling it touches no served surface. CLAUDE.md separates the platform FLOORS (the Fifteen Rule, the dark twins, no page :root, the 24px floor, AA in both modes, the kill switch) from the instance's WORLD (its packs + DESIGN.md). kit/ (task 1003320) is the look-before-you-ship kit: lib.js (the floors, the states/probes contracts, the action + expectation vocabularies, the WCAG maths, the stub spawner), serve.js (any surface through the doorway's real transforms + fixtures/), render.js (states × 1440/390/320 × dark/light, audited), probe.js (PASS/FAIL contracts), check-mock.js (platform rules + the instance's DESIGN.md taste bans), contrast.js + png.js; a page's <page>.states.json / <page>.probes.json sit BESIDE the surface (modules/public-landing/public/, modules/hall-ui/public/). Recipe: docs/recipes/ui-look-before-you-ship.md. skills/ (task 1003321 / ADR 0198) is the vendoring policy: policy.json (licence allowlist, verdicts that may land, sidecar fields, preamble markers), PREAMBLE.md (the platform preamble every vendored SKILL.md opens with), README.md (the roster — both origins scanned dangerous on 2026-08-28, landed as rebuild specs; nothing vendored — and the landing recipe); a landable copy goes under skills/vendor/<name>/ with PROVENANCE.md + LICENSE and is materialised into an instance's .claude/skills/ by claude-materialize.js step 1b. skills/<name>/ (task 1003325 / ADR 0220) are the in-house rebuilds from the specs, first-party and counted: design-taste-frontend (the default taste skill), design-taste-frontend-v1 (the earlier generation, kept distinct), high-end-visual-design (the polish pass), redesign-existing-projects (the in-place upgrade); each opens with the same preamble and carries a "Rebuilt from" section instead of a sidecar. impeccable/ (task 1003326 / ADR 0221) is the design-craft method as ONE skill with sub-commands: SKILL.md (the router, the menu, the craft floor, the bounded finish) + reference/<sub-command>.md (eighteen playbooks, one loaded per request); no hooks, no bypass agent, no telemetry, no live server. styles/ (task 1003323 / ADR 0219) is the style library: <name>/pack.json (the theme.ui overlay, exactly the fifteen) + DESIGN.md (the look's derived recipes, thirteen in lockstep with the pack) + mock.html (the specimen); chrome-world (pinned to the neutral pack), expedition, grove; STYLE=<name> on the kit lays a look over the instance's pack through GDS_BRANDING_FILE; tests/ui_design_styles.mjs runs the world/cosmos/hall token suites once per alternate (UI_DESIGN_PACK). config/design-tokens.* + design-sources.json STAY in config/ as the host boundary; public-landing keeps its own key (first customer, not owner).
@@ -1361,5 +1361,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1361
1361
  landed since 1.19.460 with no explicit bump. run 33571982271. (task 1002620)
1362
1362
  1.19.462 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1363
1363
  landed since 1.19.461 with no explicit bump. run 33576366095. (task 1002620)
1364
+ 1.19.463 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1365
+ landed since 1.19.462 with no explicit bump. run 33577660347. (task 1002620)
1364
1366
  ---------------------------------------------------------------------------
1365
1367
  ```
@@ -26,7 +26,9 @@
26
26
  const circ = 2 * Math.PI * r;
27
27
  const dash = (pct * circ).toFixed(1);
28
28
  const gap = (circ - pct * circ).toFixed(1);
29
- const stroke = achieved ? 'var(--ok)' : 'var(--accent)';
29
+ // The arc is ink, not the accent: the accent lights what is pressable or
30
+ // live, and a progress arc is neither (task 1003470; the mock's .ring__arc).
31
+ const stroke = achieved ? 'var(--ok)' : 'var(--ink)';
30
32
  return `<svg class="goal-ring" width="${size}" height="${size}" viewBox="0 0 42 42" role="img" aria-label="${satisfied} of ${total} criteria satisfied">
31
33
  <circle cx="21" cy="21" r="${r}" fill="none" stroke="var(--rule-soft)" stroke-width="4"></circle>
32
34
  <circle cx="21" cy="21" r="${r}" fill="none" stroke="${stroke}" stroke-width="4" stroke-linecap="round" stroke-dasharray="${dash} ${gap}" transform="rotate(-90 21 21)"></circle>
@@ -60,15 +62,17 @@
60
62
  const EMPTY_ROLLUP = { done: 0, active: 0, ready: 0, blocked: 0, backlog: 0, cut: 0, total: 0, nextReady: null };
61
63
 
62
64
  // task 1002439 — a goal row's readiness rollup line (only the non-zero buckets).
65
+ // The dot colours are classes in goals.css (.gr-mark--*), shared with the
66
+ // readiness band above the list, so the two cannot drift (task 1003470).
63
67
  const ROLL_DOTS = [
64
- ['ready', 'ready', 'var(--ok)'],
65
- ['active', 'in progress', 'var(--info)'],
66
- ['blocked', 'blocked', 'var(--terracotta)'],
67
- ['backlog', 'backlog', 'var(--ink-faint)'],
68
+ ['ready', 'ready'],
69
+ ['active', 'in progress'],
70
+ ['blocked', 'blocked'],
71
+ ['backlog', 'backlog'],
68
72
  ];
69
73
  function goalRollLine(r) {
70
74
  const parts = ROLL_DOTS.filter(([k]) => r[k] > 0)
71
- .map(([k, label, c]) => `<span class="gr-d"><i style="background:${c}"></i><b>${r[k]}</b> ${label}</span>`);
75
+ .map(([k, label]) => `<span class="gr-d"><i class="gr-mark gr-mark--${k}" aria-hidden="true"></i><b>${r[k]}</b> ${label}</span>`);
72
76
  return parts.length ? `<div class="gr-roll">${parts.join('')}</div>` : '';
73
77
  }
74
78
 
@@ -856,8 +856,9 @@
856
856
  // Kit shimmer placeholders while the goal list loads (was one "…" div).
857
857
  const grid = $('#goal-grid');
858
858
  if (grid) {
859
- grid.innerHTML = Array.from({ length: 8 }, () =>
860
- '<div class="goal-card goal-card--skel" aria-hidden="true"><span class="kit-skel"></span><span class="kit-skel"></span></div>').join('');
859
+ // task 1003470 — row-shaped, since the list opens on rows.
860
+ grid.innerHTML = `<div class="gr-list">${Array.from({ length: 6 }, () =>
861
+ '<div class="gr-row gr-row--skel" aria-hidden="true"><span class="gr-skel__ring"></span><div><span class="kit-skel"></span><span class="kit-skel"></span></div><span></span></div>').join('')}</div>`;
861
862
  }
862
863
  try {
863
864
  state.me = await getJSON(`${API}/me`).catch(() => null);
@@ -927,12 +928,23 @@
927
928
  // options are filled after boot's fetch (populateOwnerFilter), the
928
929
  // populateVersionFilter pattern. No URL riding (url defaults off here and this
929
930
  // page is deliberately url:false — it owns the #/goal/N hash route).
931
+ // task 1003470 — three lenses stay in the open: the version, the state and
932
+ // the order. The other five are real but rarely turned, and twelve controls in
933
+ // two rows were the loudest thing on the page; they sit behind the kit's
934
+ // Filters disclosure, which counts and chips whatever is active so a hidden
935
+ // filter can never narrow the board silently. Control ids are unchanged.
930
936
  makeFilterBar({
931
937
  mount: $('#goals-filters'),
932
938
  search: { id: 'f-goals-search', label: 'Search', placeholder: 'goal # or name…' },
933
939
  primary: [
934
940
  { id: 'f-goal-version', label: 'Version', options: [{ value: '', label: '— any —' }] },
935
941
  { id: 'f-goal-status', label: 'Status', options: [{ value: '', label: '— any —' }, 'open', 'achieved', 'archived'] },
942
+ { id: 'f-goal-order',
943
+ label: 'Sort',
944
+ value: GOAL_SORTS[savedGoalSort] ? savedGoalSort : undefined,
945
+ options: Object.entries(GOAL_SORTS).map(([value, { label }]) => ({ value, label })) },
946
+ ],
947
+ overflow: [
936
948
  { id: 'f-goal-mine', label: 'Membership', options: [{ value: '', label: 'All goals' }, { value: 'mine', label: 'My goals' }] },
937
949
  { id: 'f-goal-owner', label: 'Owner', options: [{ value: '', label: '— any —' }] },
938
950
  // task 1003276 — filled from GET /work-categories after boot
@@ -946,19 +958,22 @@
946
958
  { id: 'f-goal-role', label: 'Craft', options: [ // ADR 0174: "role" means a craft
947
959
  { value: '', label: '— any —' }, 'engineer', 'artist', 'ideator', 'ui'] },
948
960
  { id: 'f-goal-stale', label: 'Age', options: window.OTBTaskLib.STALE_CHOICES },
949
- { id: 'f-goal-order',
950
- label: 'Sort',
951
- value: GOAL_SORTS[savedGoalSort] ? savedGoalSort : undefined,
952
- options: Object.entries(GOAL_SORTS).map(([value, { label }]) => ({ value, label })) },
953
- // task 1003136 — list|cards toggle; renderList forks its group renderer
954
- // on this. Persisted via GOAL_VIEW_PREF, never the URL.
955
- { id: 'f-goal-view',
956
- label: 'View',
957
- value: savedGoalView === 'cards' ? 'cards' : undefined,
958
- options: [{ value: 'list', label: 'list' }, { value: 'cards', label: 'cards' }] },
959
961
  ],
960
962
  onChange: onListFilterChange,
961
963
  });
964
+ // task 1003470 — rows|cards is a VIEW, not a filter: it left the filter bar
965
+ // for the archetype's segmented toggle on the count line. renderList still
966
+ // reads #f-goal-view (task 1003136's fork), now a hidden input the two buttons
967
+ // write, so the fork and the otb.goals.view pref are unchanged.
968
+ const viewInput = $('#f-goal-view');
969
+ const viewBtns = Array.from(document.querySelectorAll('#goals-view .viewtoggle__btn'));
970
+ function setGoalView(view, opts) {
971
+ if (viewInput) viewInput.value = view;
972
+ viewBtns.forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.view === view)));
973
+ if (!opts || opts.fire !== false) onListFilterChange();
974
+ }
975
+ viewBtns.forEach((b) => b.addEventListener('click', () => setGoalView(b.dataset.view)));
976
+ if (savedGoalView === 'cards') setGoalView('cards', { fire: false });
962
977
  // task 2171 — stamp + wire the kit pager bars (grid + both Work tables); the
963
978
  // bar ids and localStorage prefs are exactly what the bespoke pagers used.
964
979
  const pagerMounts = [
@@ -127,6 +127,15 @@
127
127
  const { label, ms } = stamp(g);
128
128
  return ` · ${escapeHtml(label)} ${escapeHtml(ms ? fmtDate(new Date(ms).toISOString()) : '—')}`;
129
129
  }
130
+ // The check drawn, not typed: the world's icons are strokes, never glyphs.
131
+ const CHECK_PATH = '<path d="M3.4 8.4 6.6 11.5 12.6 4.9"/>';
132
+ const checkSvg = (cls) => `<svg class="${cls}" viewBox="0 0 16 16" aria-hidden="true">${CHECK_PATH}</svg>`;
133
+ // task 1003470 — a goal's facts, computed ONCE for the row and the card so the
134
+ // two views cannot drift (pinned by tests/goals_card_view.mjs). Three things a
135
+ // row says, each in one language: the ring (the goal's own done-when,
136
+ // satisfied over total), the facts line (state pill, scope + version chips,
137
+ // the category), and the right column — ONE next action, or a one-line state.
138
+ // The task readiness counts live on the roll line and nowhere else.
130
139
  function goalRenderFacts(g, ringSize) {
131
140
  const d = state.details.get(String(g.id));
132
141
  const crit = d ? (d.criteria || []) : [];
@@ -134,90 +143,83 @@
134
143
  const r = rollupOf(g.id);
135
144
  const achieved = g.status === 'achieved';
136
145
  const ringHtml = crit.length ? ring(sat, crit.length, achieved, ringSize)
137
- : `<span class="gr-crit-none" aria-hidden="true">—</span>`;
146
+ : `<span class="gr-crit-none" role="img" aria-label="No done-when criteria yet" title="No done-when criteria yet"></span>`;
138
147
  const goalHref = `#/goal/${escapeHtml(String(g.id))}`;
139
148
  let right;
140
149
  if (achieved) {
141
- right = `<span class="gr-met">✓ achieved</span>`;
150
+ right = `<span class="gr-state gr-state--ok">${checkSvg('gr-state__mark')}achieved</span>`;
142
151
  } else if (crit.length && sat >= crit.length) {
143
- right = `<span class="gr-met">Criteria met — awaiting review</span>`;
152
+ right = `<span class="gr-state gr-state--ok">${checkSvg('gr-state__mark')}criteria met · awaiting review</span>`;
144
153
  } else if (r.nextReady) {
145
- right = `<div class="gr-next"><span class="gr-next__t">${escapeHtml(r.nextReady.title || 'Next task')}</span>`
146
- + `<a class="gr-claim" href="#/task/${escapeHtml(String(r.nextReady.id))}">Claim next</a></div>`;
154
+ // The one place the accent lands on the list: the row's live door.
155
+ right = `<div class="gr-next"><span class="gr-next__t" title="${escapeHtml(r.nextReady.title || '')}">${escapeHtml(r.nextReady.title || 'Next task')}</span>`
156
+ + `<a class="btn-accent gr-claim" href="#/task/${escapeHtml(String(r.nextReady.id))}">claim next</a></div>`;
147
157
  } else if (r.blocked > 0) {
148
- right = `<a class="gr-drill" href="${goalHref}">See what's blocking <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>`;
158
+ right = `<a class="btn-ghost gr-drill" href="${goalHref}">what's blocking</a>`;
149
159
  } else {
150
- right = `<span class="gr-quiet">No tasks yet</span>`;
160
+ right = `<span class="gr-quiet">no tasks yet</span>`;
151
161
  }
152
- const metaHtml = `#${escapeHtml(String(g.id))} · ${escapeHtml(g.version_id || '')}${(achieved || g.status === 'open') ? '' : ' · ' + escapeHtml(g.status)}${goalStampHtml(g)}`;
153
- // task 1003307 — the archetype's fact line: the goal's STATE as a pill, then
154
- // one chip per scope module and one for the version. The mock argues rows
155
- // over cards precisely because a goal carries this many facts; the row was
156
- // showing two of them as one run of grey text.
162
+ // The sort's date, only while a date sort is on (task 1002965) — it rides
163
+ // under the right column's state, not in a meta run under the title.
157
164
  const stampHtml = goalStampHtml(g);
165
+ const whenHtml = stampHtml ? `<span class="gr-when">${stampHtml.replace(/^ · /, '')}</span>` : '';
158
166
  const idHtml = `<span class="gr-title__id">#${escapeHtml(String(g.id))}</span>`;
159
167
  const chips = [];
160
168
  for (const m of (Array.isArray(g.scope_modules) ? g.scope_modules : []).slice(0, 3)) {
161
169
  chips.push(`<span class="fact-chip">${escapeHtml(String(m))}</span>`);
162
170
  }
163
171
  if (g.version_id) chips.push(`<span class="fact-chip">${escapeHtml(g.version_id)}</span>`);
172
+ // task 1003276 — the category chip is a fact like the others, so it sits in
173
+ // the facts line (a label a person scans, never a door — ADR 0192).
174
+ const catHtml = categoryChip(g.category);
164
175
  const factsHtml = `<div class="gr-facts">`
165
176
  + `<span class="fact-pill fact-pill--${escapeHtml(g.status || 'open')}">${escapeHtml(g.status || 'open')}</span>`
166
177
  + chips.join('')
178
+ + catHtml
167
179
  + `</div>`;
168
- // task 1003276 — the category chip. Computed here with the rest of the facts
169
- // so the row and the card (task 1003136) cannot drift on it.
170
- const catHtml = categoryChip(g.category);
171
- return { r, achieved, ringHtml, goalHref, right, metaHtml, catHtml, idHtml, stampHtml, factsHtml };
180
+ return { r, achieved, ringHtml, goalHref, right, catHtml, idHtml, stampHtml, whenHtml, factsHtml };
172
181
  }
173
182
  function goalRowHtml(g) {
174
- const { r, achieved, ringHtml, goalHref, right, catHtml, idHtml, stampHtml, factsHtml } = goalRenderFacts(g, 44);
175
- // NOTE: a rollup row nests real links (title → goal, Claim → task overlay,
183
+ const { r, achieved, ringHtml, goalHref, right, idHtml, whenHtml, factsHtml } = goalRenderFacts(g, 44);
184
+ // NOTE: a rollup row nests real links (title → goal, claim → the task page,
176
185
  // drill → goal), so the row itself is a <div>, not an <a> — nested anchors
177
186
  // are invalid. Only the explicit links navigate.
178
- // task 1003307 — the archetype's row (mock .goal): the id rides INSIDE the
179
- // title, mono and quiet, so the name starts at the same x on every row; the
180
- // subtitle is a real line; the state and the scope are pills and chips
181
- // rather than a grey run-on. The sort stamp is all that is left of .gr-meta,
182
- // and it renders only while a date sort is active.
187
+ // The archetype's row (mock .goal): the id rides INSIDE the title, mono and
188
+ // quiet; the subtitle is a real line; the facts are pills and chips; the
189
+ // right column is one action or one state, with the sort date under it.
183
190
  return `<div class="gr-row" data-goal-id="${escapeHtml(String(g.id))}">
184
191
  <div class="gr-ringcol">${ringHtml}</div>
185
192
  <div class="gr-main">
186
193
  <a class="gr-title" href="${goalHref}">${idHtml}${escapeHtml(g.title || 'Untitled goal')}</a>
187
194
  ${subtitleLine(g)}
188
195
  ${factsHtml}
189
- ${stampHtml ? `<div class="gr-meta">${stampHtml.replace(/^ · /, '')}${catHtml}</div>` : (catHtml ? `<div class="gr-meta">${catHtml}</div>` : '')}
190
196
  ${achieved ? '' : goalRollLine(r)}
191
197
  </div>
192
- <div class="gr-right">${right}</div>
198
+ <div class="gr-right">${right}${whenHtml}</div>
193
199
  </div>`;
194
200
  }
201
+ // The card is the row folded (goals.css says why it draws no box). It keeps
202
+ // the subtitle as a tooltip — task 1003224's shape — because a 280px column
203
+ // has no line to give it.
195
204
  function goalCardHtml(g) {
196
- const { r, achieved, ringHtml, goalHref, right, metaHtml, catHtml } = goalRenderFacts(g, 36);
205
+ const { r, achieved, ringHtml, goalHref, right, idHtml, whenHtml, factsHtml } = goalRenderFacts(g, 36);
197
206
  return `<div class="goal-card" data-goal-id="${escapeHtml(String(g.id))}">
198
207
  <div class="goal-card__head">
199
208
  <div class="gr-ringcol">${ringHtml}</div>
200
- <a class="goal-card__title gr-title" href="${goalHref}"${subtitleAttr(g)}>${escapeHtml(g.title || 'Untitled goal')}</a>
209
+ <a class="goal-card__title gr-title" href="${goalHref}"${subtitleAttr(g)}>${idHtml}${escapeHtml(g.title || 'Untitled goal')}</a>
201
210
  </div>
202
- <div class="goal-card__meta">${metaHtml}${catHtml}</div>
211
+ ${factsHtml}
203
212
  ${achieved ? '' : goalRollLine(r)}
204
- <div class="goal-card__foot">${right}</div>
213
+ <div class="goal-card__foot">${right}${whenHtml}</div>
205
214
  </div>`;
206
215
  }
207
216
  function renderList() {
208
217
  const grid = $('#goal-grid');
209
218
  if (!grid) return;
210
219
  const rows = filteredGoals();
211
- const count = $('#goals-count');
212
- if (count) count.textContent = `${rows.length} goal${rows.length === 1 ? '' : 's'}`;
213
220
  const pagerMount = $('#goals-pager-mount');
214
221
  if (pagerMount) pagerMount.hidden = true;
215
222
 
216
- if (!rows.length) {
217
- grid.innerHTML = emptyStateHtml('No goals match these filters.');
218
- return;
219
- }
220
-
221
223
  const isBlocked = (g) => {
222
224
  const r = rollupOf(g.id);
223
225
  return g.status === 'open' && r.ready === 0 && r.active === 0 && r.blocked > 0;
@@ -231,26 +233,55 @@
231
233
  const blocked = sortGoals(openRows.filter(isBlocked));
232
234
  const active = sortGoals(openRows.filter((g) => !isBlocked(g)));
233
235
 
234
- let R = 0, A = 0, B = 0;
235
- openRows.forEach((g) => { const r = rollupOf(g.id); R += r.ready; A += r.active; B += r.blocked; });
236
- const summary = `<div class="gr-summary">
237
- <span class="gr-sm"><span class="gr-sm__dot" style="background:var(--ok)"></span><b>${R}</b> ready to claim</span>
238
- <span class="gr-sm"><span class="gr-sm__dot" style="background:var(--info)"></span><b>${A}</b> in progress</span>
239
- <span class="gr-sm"><span class="gr-sm__dot" style="background:var(--terracotta)"></span><b>${B}</b> blocked</span>
240
- </div>`;
236
+ // task 1003470 — the count line reads the groups (the mock's count-line).
237
+ const count = $('#goals-count');
238
+ if (count) {
239
+ const parts = [`${rows.length} goal${rows.length === 1 ? '' : 's'}`];
240
+ if (rows.length) parts.push(`${openRows.length} open`, `${achieved.length} achieved`, `${archived.length} archived`);
241
+ count.textContent = parts.join(' · ');
242
+ }
243
+ if (!rows.length) {
244
+ grid.innerHTML = emptyStateHtml('No goals match these filters.');
245
+ return;
246
+ }
247
+
248
+ // The readiness band: the four figures the mock puts on the panel, in the
249
+ // family's figure band, over the OPEN goals in view. The dots are the roll
250
+ // line's own, so the band and the rows read as one language.
251
+ let R = 0, A = 0, B = 0, sat = 0, crit = 0;
252
+ openRows.forEach((g) => {
253
+ const r = rollupOf(g.id); R += r.ready; A += r.active; B += r.blocked;
254
+ const d = state.details.get(String(g.id));
255
+ const c = d ? (d.criteria || []) : [];
256
+ crit += c.length; sat += c.filter((x) => x.satisfied).length;
257
+ });
258
+ const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
259
+ const fig = (label, value, foot, mark) => `<div class="ov-fig">`
260
+ + `<span class="ov-fig__l">${mark ? `<i class="gr-mark gr-mark--${mark}" aria-hidden="true"></i>` : ''}${label}</span>`
261
+ + `<span class="ov-fig__v">${value}</span>`
262
+ + (foot ? `<span class="ov-fig__s">${foot}</span>` : '')
263
+ + `</div>`;
264
+ const summary = `<div class="ov-figs gr-figs">`
265
+ + fig('Ready to claim', R, `across ${plural(active.length, 'active goal')}`, 'ready')
266
+ + fig('In progress', A, 'claimed right now', 'active')
267
+ + fig('Blocked', B, 'waiting on upstream tasks', 'blocked')
268
+ + fig('Criteria met', `${sat} / ${crit}`, `on ${plural(openRows.length, 'open goal')}`)
269
+ + `</div>`;
241
270
 
242
- const sec = (label, n) => `<div class="gr-sec"><b>${label}</b><span class="gr-sec__n">${n}</span></div>`;
243
- // task 1003136 — the list|cards toggle swaps how the open groups render;
271
+ // The groups: the family head (name in the label voice, count in mono, one
272
+ // rule under) over the rows, the cards, or the pills.
273
+ const group = (label, arr, body) => `<div class="ov-group"><div class="ov-group__head"><span class="ov-group__name">${label}</span><span class="ov-group__n">${arr.length}</span></div>${body}</div>`;
274
+ // task 1003136 — the rows|cards toggle swaps how the open groups render;
244
275
  // grouping, filters, and sort run above the fork so both views share them.
245
276
  // Achieved/archived pills stay pills (already the compact form). The inner
246
277
  // div is class-only .goal-grid on purpose: task 1002439's block override
247
- // targets #goal-grid.goal-grid, so the original card-grid CSS applies here.
278
+ // targets #goal-grid.goal-grid, so the card-grid CSS applies here.
248
279
  const cardsView = ($('#f-goal-view')?.value || 'list') === 'cards';
249
280
  const listOf = cardsView
250
281
  ? (arr) => `<div class="goal-grid">${arr.map(goalCardHtml).join('')}</div>`
251
282
  : (arr) => `<div class="gr-list">${arr.map(goalRowHtml).join('')}</div>`;
252
283
  // task 1002965 — an achieved/archived goal is a compact pill, so its sort
253
- // date rides as its own muted span (the row's `gr-meta` equivalent). The
284
+ // date rides as its own muted span (the row's gr-when equivalent). The
254
285
  // pills row already wraps, so the extra text can't widen the page.
255
286
  const pillWhen = (g) => {
256
287
  const { stamp } = currentGoalSort();
@@ -258,13 +289,13 @@
258
289
  const { label, ms } = stamp(g);
259
290
  return `<span class="gr-pill__when">${escapeHtml(label)} ${escapeHtml(ms ? fmtDate(new Date(ms).toISOString()) : '—')}</span>`;
260
291
  };
261
- const pill = (g) => `<a class="gr-pill" href="#/goal/${escapeHtml(String(g.id))}"><span class="gr-pill__ok">✓</span><b>${escapeHtml(g.title || 'Untitled')}</b><span class="gr-pill__id">#${escapeHtml(String(g.id))}</span>${pillWhen(g)}</a>`;
292
+ const pill = (g) => `<a class="gr-pill" href="#/goal/${escapeHtml(String(g.id))}">${checkSvg('gr-pill__mark')}<b>${escapeHtml(g.title || 'Untitled')}</b><span class="gr-pill__id">#${escapeHtml(String(g.id))}</span>${pillWhen(g)}</a>`;
262
293
 
263
294
  grid.innerHTML = summary
264
- + (active.length ? sec('Active', active.length) + listOf(active) : '')
265
- + (blocked.length ? sec('Blocked', blocked.length) + listOf(blocked) : '')
266
- + (achieved.length ? sec('Achieved', achieved.length) + `<div class="gr-pills">${achieved.map(pill).join('')}</div>` : '')
267
- + (archived.length ? sec('Archived', archived.length) + `<div class="gr-pills gr-pills--muted">${archived.map(pill).join('')}</div>` : '');
295
+ + (active.length ? group('Active', active, listOf(active)) : '')
296
+ + (blocked.length ? group('Blocked', blocked, listOf(blocked)) : '')
297
+ + (achieved.length ? group('Achieved', achieved, `<div class="gr-pills">${achieved.map(pill).join('')}</div>`) : '')
298
+ + (archived.length ? group('Archived', archived, `<div class="gr-pills gr-pills--muted">${archived.map(pill).join('')}</div>`) : '');
268
299
  }
269
300
  function todoSearchQuery() {
270
301
  const el = $('#f-goal-search');