@bongos/core 1.21.124 → 1.21.126

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.
Files changed (44) hide show
  1. package/.bongos-core.json +91 -41
  2. package/bin/bongos.js +9 -0
  3. package/docs/api/openapi.json +4 -4
  4. package/docs/copy-inventory.md +128 -126
  5. package/docs/copy-registry.json +188 -170
  6. package/docs/module-api-changelog.md +4 -0
  7. package/docs/page-inventory.json +2 -1
  8. package/docs/page-readings.json +152 -143
  9. package/modules/hall-ui/public/goal-forest-lib.js +36 -0
  10. package/modules/hall-ui/public/goal-forest-render.js +50 -10
  11. package/modules/hall-ui/public/goals-page.js +19 -22
  12. package/modules/hall-ui/public/goals-render.js +6 -16
  13. package/modules/hall-ui/public/goals.css +13 -4
  14. package/modules/hall-ui/public/goals.html +22 -24
  15. package/modules/hall-ui/public/goals.states.json +19 -1
  16. package/modules/hall-ui/records/goal-dependency-forest.md +4 -3
  17. package/modules/onboarding/howto-guide.js +259 -0
  18. package/modules/platform-identity/invite-authz.js +1 -1
  19. package/modules/provisioning/catalog-bridge.js +1 -1
  20. package/modules/provisioning/provisioning.js +1 -1
  21. package/modules/provisioning/route-helpers.js +50 -0
  22. package/modules/provisioning/routes/disconnect.js +6 -5
  23. package/modules/provisioning/routes/env-manifest.js +11 -27
  24. package/modules/provisioning/routes/provisioning.js +58 -75
  25. package/package-lock.json +2 -2
  26. package/package.json +1 -1
  27. package/release-notes.json +24 -0
  28. package/scripts/gds/core-pin-sync.js +233 -0
  29. package/scripts/gds/module-assess-tests.js +40 -11
  30. package/scripts/gds/module-assess-version.js +10 -2
  31. package/scripts/gds/module-test-runner/README.md +44 -0
  32. package/scripts/gds/module-test-runner/module-tests.yml +87 -0
  33. package/scripts/gds/module-test-runner.js +358 -0
  34. package/src/module-api.js +1 -1
  35. package/tests/core_pin_sync.mjs +237 -0
  36. package/tests/goal_forest_lib.mjs +28 -0
  37. package/tests/goal_forest_page.mjs +106 -11
  38. package/tests/howto_guide.mjs +96 -0
  39. package/tests/module_assess_price_parity.mjs +2 -2
  40. package/tests/module_assess_score.mjs +13 -3
  41. package/tests/module_test_runner.mjs +294 -0
  42. package/tests/project_catalog_routes.mjs +1 -1
  43. package/tests/provisioning_module_selection.mjs +1 -1
  44. package/tests/provisioning_project_owner_gate.mjs +271 -0
@@ -5,12 +5,12 @@
5
5
  // header), so a new self-contained feature is its own file — the same
6
6
  // separation goal-map.js already keeps from goals-render.js, one file down.
7
7
  //
8
- // NOT a band column, unlike Completion/Members/Conflicts on the same detail
9
- // page: a node-link picture needs real width, and this is bounded small BY
10
- // DESIGN (ADR 0227 §3's own finding — the tasks a forest omits are exactly
11
- // the ones with nothing to connect them to), so there is no cap-and-"view
12
- // all" split here. What renders is always the whole forest returned by GET
13
- // /goals/:id/dependency-forest. The model + layout are goal-forest-lib.js
8
+ // Two surfaces (task 1004738): the band's compact Dependencies card, in the
9
+ // slot Conflicts used to hold — a summary, the tasks to ship first, the
10
+ // conflicts count — and the full picture behind it at #/goal/N/dependencies,
11
+ // where the forest needs real width. The full view always draws the whole
12
+ // forest returned by GET /goals/:id/dependency-forest (ADR 0227 §3: what it
13
+ // omits is exactly what has nothing to connect it). The model + layout are goal-forest-lib.js
14
14
  // (pure); this file only draws what it returns, with createElementNS +
15
15
  // textContent — the atlas / goal-map.js idiom — so a task title never
16
16
  // becomes markup.
@@ -18,8 +18,9 @@
18
18
  // A factory, the goals-render.js precedent, because `state` keeps changing
19
19
  // after load: `createGoalForestRender(ctx)` takes the SAME `{ API, state,
20
20
  // sameId, getJSON }` slice of ctx goals-page.js already builds, and returns
21
- // `{ loadDependencyForest, renderDependencyForest }` for goals-render.js to
22
- // call from renderDetail() and for tests/goal_forest_page.mjs to call directly.
21
+ // `{ loadDependencyForest, renderDependencyForest, renderDependencyCard }` for
22
+ // goals-render.js to call (the card again whenever the conflicts land) and for
23
+ // tests/goal_forest_page.mjs to call directly.
23
24
  (function () {
24
25
  'use strict';
25
26
 
@@ -103,7 +104,44 @@
103
104
  return svg;
104
105
  }
105
106
 
107
+ const SHIP_FIRST = 3;
108
+ const taskHref = (id) => `#/task/${encodeURIComponent(id)}`;
109
+ // The band card. Reads state.conflictPairs too, so goals-render.js calls it
110
+ // again when the conflicts land; null there (loading, or a failed read)
111
+ // draws no conflicts line rather than a guess.
112
+ function renderDependencyCard() {
113
+ const lede = $('#goal-deps-card-lede');
114
+ const body = $('#goal-deps-card-body');
115
+ if (!lede || !body) return;
116
+ const a = state.forest && !state.forestError ? lib.analyse(state.forest) : null;
117
+ const n = a ? a.order.length : 0;
118
+ const pill = $('#goal-deps-card-count');
119
+ if (pill) { pill.hidden = !n; pill.textContent = n ? String(n) : ''; }
120
+ if (state.forestError) lede.textContent = 'Couldn’t load this goal’s task dependencies right now.';
121
+ else lede.textContent = a ? lib.brief(a) : 'Reading…';
122
+ const first = a ? lib.shipFirst(a, SHIP_FIRST) : [];
123
+ const pairs = Array.isArray(state.conflictPairs) ? state.conflictPairs : null;
124
+ body.innerHTML = (first.length
125
+ ? `<p class="gf-brief__h">Ship these first</p><ul class="gf-brief">${first.map((r) => `<li class="gf-brief__row">
126
+ <a class="lb-name gf-brief__task" href="${taskHref(r.id)}" title="#${escapeHtml(r.id)} ${escapeHtml(r.title)}">#${escapeHtml(r.id)} ${escapeHtml(r.title)}</a>
127
+ <span class="gf-brief__n">unblocks ${r.unblocks}</span></li>`).join('')}</ul>`
128
+ : '')
129
+ + (pairs
130
+ ? `<p class="gf-brief__conflicts${pairs.length ? ' gf-brief__conflicts--live' : ''}">${pairs.length
131
+ ? `${escapeHtml(lib.plural(pairs.length, 'file conflict'))} between open tasks.`
132
+ : 'No file conflicts between open tasks.'}</p>`
133
+ : '');
134
+ const foot = $('#goal-deps-card-foot');
135
+ if (!foot) return;
136
+ foot.hidden = !((n || (pairs && pairs.length)) && state.current != null);
137
+ foot.innerHTML = foot.hidden ? ''
138
+ : `<a class="pill-link" href="#/goal/${encodeURIComponent(state.current)}/dependencies">See the full picture →</a>`;
139
+ }
140
+
106
141
  function renderDependencyForest() {
142
+ renderDependencyCard();
143
+ const back = $('#goal-deps-back');
144
+ if (back && state.current != null) back.setAttribute('href', `#/goal/${encodeURIComponent(state.current)}`);
107
145
  const lede = $('#goal-deps-lede');
108
146
  const body = $('#goal-deps-body');
109
147
  if (!lede || !body) return;
@@ -130,8 +168,10 @@
130
168
  setCount('#goal-deps-count', a.order.length);
131
169
  lede.innerHTML = `${escapeHtml(lib.describe(a))} An arrow runs from the task that has to ship first to the task that waits on it. `
132
170
  + 'Every task here, and every dependency, is also in the table below.';
171
+ // The picture is drawn only while its own view is open: on the goal page
172
+ // the card is all that shows, and a forest drawn hidden cannot measure its titles.
173
+ if (state.view !== 'dependencies') { body.innerHTML = ''; return; }
133
174
  const rows = lib.tableRows(a);
134
- const taskHref = (id) => `#/task/${encodeURIComponent(id)}`;
135
175
  const depList = (list) => (list.length
136
176
  ? `<ul class="gf-deps">${list.map((x) => `<li><a href="${taskHref(x.id)}">#${escapeHtml(x.id)} ${escapeHtml(x.title)}</a></li>`).join('')}</ul>`
137
177
  : '<span class="gf-none">—</span>');
@@ -178,6 +218,6 @@
178
218
  }
179
219
  }
180
220
 
181
- return { loadDependencyForest, renderDependencyForest };
221
+ return { loadDependencyForest, renderDependencyForest, renderDependencyCard };
182
222
  };
183
223
  })();
@@ -58,8 +58,8 @@
58
58
  me: null,
59
59
  goals: [], // list rows
60
60
  details: new Map(), // goal_id -> {goal, members, criteria}
61
- current: null, // open goal id (detail/conflicts view) or null (list)
62
- view: 'list', // task 1828 — 'list' | 'detail' | 'conflicts'
61
+ current: null, // open goal id (detail or a full view) or null (list)
62
+ view: 'list', // task 1828 — 'list' | 'detail' | 'dependencies' | 'members' | 'completion'
63
63
  inviteOpen: false, // task 1762 — the invite form's open/closed state
64
64
  directory: null, // task 1877 — GET /builders/directory, fetched once and cached
65
65
  // task 1828 — per-table column sort. by=null → natural order (todo: server
@@ -562,10 +562,8 @@
562
562
  // full view at #/goal/N/<view> (the shared pattern behind Conflicts, Members,
563
563
  // and Completion). Hidden when nothing is truncated.
564
564
 
565
- // task 1828 — the band column shows the first CONFLICT_CAP; the foot routes
566
- // to the dedicated full-width conflicts view. Count + amber accent on the card.
567
-
568
- // task 1828 — the standalone #/goal/N/conflicts view: the whole set, no cap.
565
+ // task 1004738 — the conflict set renders on the #/goal/N/dependencies view;
566
+ // the band's Dependencies card carries its count.
569
567
 
570
568
  /* ----- Completion (band column + #/goal/N/completion full view) ----- */
571
569
 
@@ -727,16 +725,16 @@
727
725
  setWorkTab('todo');
728
726
  workTabs?.setCount('todo', null);
729
727
  workTabs?.setCount('done', null);
730
- ['#goal-conflicts-count', '#goal-conflicts-view-count',
728
+ ['#goal-conflicts-view-count',
731
729
  '#goal-members-count', '#goal-members-view-count', '#goal-completion-count'].forEach((sel) => {
732
730
  const el = $(sel);
733
731
  if (el) { el.hidden = true; el.textContent = ''; }
734
732
  });
735
- ['#goal-todo-pager', '#goal-done-pager', '#goal-conflicts-foot', '#goal-members-foot', '#goal-completion-foot'].forEach((sel) => {
733
+ ['#goal-todo-pager', '#goal-done-pager', '#goal-members-foot', '#goal-completion-foot'].forEach((sel) => {
736
734
  const el = $(sel);
737
735
  if (el) el.hidden = true;
738
736
  });
739
- ['#goal-conflicts-card', '#goal-conflicts-view-card'].forEach((sel) => $(sel)?.classList.remove('goal-conflicts-card--live'));
737
+ $('#goal-conflicts-view-card')?.classList.remove('goal-conflicts-card--live');
740
738
  reflectSortHeaders('done');
741
739
  // task 1002425 — To do is a card list now; Completed stays a table. Show a
742
740
  // placeholder in the card list + skeleton rows in the Completed table while
@@ -749,25 +747,23 @@
749
747
  if (todoCount) todoCount.textContent = '';
750
748
  const bulkBar = $('#goal-bulk-toolbar');
751
749
  if (bulkBar) bulkBar.hidden = true;
752
- const conflicts = $('#goal-conflicts');
753
- if (conflicts) conflicts.innerHTML = '<span class="kit-skel"></span>';
754
750
  const conflictsFull = $('#goal-conflicts-full');
755
751
  if (conflictsFull) conflictsFull.innerHTML = '<span class="kit-skel"></span>';
756
752
  }
757
753
 
758
754
  // task 1828 — views sharing this page: the list, a goal's detail, and the
759
- // goal's three full views (conflicts / members / completion). `view` defaults
760
- // from goalId (null → list).
761
- const SUB_VIEWS = ['conflicts', 'members', 'completion'];
755
+ // goal's three full views (dependencies / members / completion). `view`
756
+ // defaults from goalId (null → list).
757
+ const SUB_VIEWS = ['dependencies', 'members', 'completion'];
762
758
  const VIEW_SECTION = {
763
759
  list: '#goals-list-view',
764
760
  detail: '#goal-detail-view',
765
- conflicts: '#goal-conflicts-view',
761
+ dependencies: '#goal-dependencies-view',
766
762
  members: '#goal-members-view',
767
763
  completion: '#goal-completion-view',
768
764
  };
769
765
  const SUB_SUBTITLE = {
770
- conflicts: 'Goal conflicts — every open task pair whose declared files overlap.',
766
+ dependencies: 'Goal dependencies — which tasks wait on which, and which open tasks touch the same files.',
771
767
  members: 'Goal members — who’s on this goal and what they’ve contributed to it.',
772
768
  completion: 'Goal completion — every done-when criterion, with its full detail.',
773
769
  };
@@ -787,7 +783,7 @@
787
783
  if (sub) {
788
784
  sub.textContent = view === 'list'
789
785
  ? 'Shared goals: progress, members, and the work remaining.'
790
- : (SUB_SUBTITLE[view] || 'Goal detail — completion, members, conflicts, and the work queue.');
786
+ : (SUB_SUBTITLE[view] || 'Goal detail — completion, members, dependencies, and the work queue.');
791
787
  }
792
788
  window.scrollTo({ top: 0 });
793
789
  }
@@ -804,14 +800,15 @@
804
800
  if (state.current == null) renderList();
805
801
  return;
806
802
  }
807
- // task 1828 — a goal's dedicated full views (conflicts / members /
803
+ // task 1828 — a goal's dedicated full views (dependencies / members /
808
804
  // completion). renderDetail renders all three full views + kicks
809
805
  // loadGoalTasks, so a cold load straight to one still fills in once data
810
- // lands (member stats / work / conflicts refresh async).
811
- const mSub = /^#\/goal\/(\d+)\/(conflicts|members|completion)$/.exec(hash);
806
+ // lands (member stats / work / conflicts refresh async). /conflicts is the
807
+ // pre-1004738 name, still linked from the attention inbox.
808
+ const mSub = /^#\/goal\/(\d+)\/(dependencies|conflicts|members|completion)$/.exec(hash);
812
809
  if (mSub) {
813
810
  const id = mSub[1];
814
- const view = mSub[2];
811
+ const view = mSub[2] === 'conflicts' ? 'dependencies' : mSub[2];
815
812
  try {
816
813
  await loadDetail(id);
817
814
  setView(id, view);
@@ -999,7 +996,7 @@
999
996
  }
1000
997
  $('#goal-back')?.addEventListener('click', (e) => { e.preventDefault(); window.location.hash = ''; });
1001
998
  // task 1828 — "← Back to goal" from any of the full views.
1002
- ['#goal-conflicts-back', '#goal-members-back', '#goal-completion-back'].forEach((sel) => {
999
+ ['#goal-deps-back', '#goal-members-back', '#goal-completion-back'].forEach((sel) => {
1003
1000
  $(sel)?.addEventListener('click', (e) => {
1004
1001
  e.preventDefault();
1005
1002
  if (state.current != null) window.location.hash = `#/goal/${state.current}`;
@@ -34,10 +34,9 @@
34
34
  // still draws its criteria (state by satisfied/open, no UAT controls).
35
35
  const uatUi = window.OTBCriterionUat || { isAwaiting: () => false, badgesHtml: () => '', actionHtml: () => '' };
36
36
 
37
- const CONFLICT_CAP = 3;
38
- // task 1828 — the band's Completion / Members / Conflicts columns each cap
39
- // their list and offer a "View all →" into a dedicated full view (these grow:
40
- // more members, more criteria). Conflicts uses CONFLICT_CAP above.
37
+ // task 1828 — the band's Completion / Members columns each cap their list and
38
+ // offer a "View all →" into a dedicated full view (these grow: more members,
39
+ // more criteria). The third column is the Dependencies card (task 1004738).
41
40
  const MEMBER_CAP = 5;
42
41
  const CRITERIA_CAP = 4;
43
42
  // task 1932/1956 → kit (task 2171): the goal-card grid + the goal-detail Work
@@ -620,24 +619,15 @@
620
619
  foot.innerHTML = '';
621
620
  }
622
621
  }
622
+ // task 1004738 — the whole conflict set lives on the dependencies view; the
623
+ // band's Dependencies card carries only its count.
623
624
  function renderConflicts() {
624
- const pairs = state.conflictPairs;
625
- const card = $('#goal-conflicts-card');
626
- fillConflicts($('#goal-conflicts'), pairs == null ? null : pairs.slice(0, CONFLICT_CAP));
627
- const total = pairs ? pairs.length : 0;
628
- renderViewAllFoot('#goal-conflicts-foot', total, CONFLICT_CAP, 'conflicts', 'conflicts');
629
- setCount('#goal-conflicts-count', total);
630
- card?.classList.toggle('goal-conflicts-card--live', total > 0);
631
- renderConflictsFull();
632
- }
633
- function renderConflictsFull() {
634
625
  const pairs = state.conflictPairs;
635
626
  const total = pairs ? pairs.length : 0;
636
627
  fillConflicts($('#goal-conflicts-full'), pairs);
637
628
  setCount('#goal-conflicts-view-count', total);
638
629
  $('#goal-conflicts-view-card')?.classList.toggle('goal-conflicts-card--live', total > 0);
639
- const back = $('#goal-conflicts-back');
640
- if (back && state.current != null) back.setAttribute('href', `#/goal/${state.current}`);
630
+ forestRender.renderDependencyCard();
641
631
  }
642
632
  // task 1003480 — the per-goal task dependency forest is drawn by its own
643
633
  // module now (goal-forest-render.js, carved out to keep this file under the
@@ -87,10 +87,8 @@
87
87
  .goal-band__col + .goal-band__col { border-top: none; border-left: var(--line) solid var(--rule-soft); }
88
88
  }
89
89
 
90
- /* Live conflicts: the band COLUMN can't own the card border, so it gets a
91
- subtle amber wash; the standalone full-view card keeps the amber border.
92
- (Count pills on these headers are the shared .count-pill.) */
93
- .goal-conflicts-card--live { background: color-mix(in srgb, var(--warn) 6%, transparent); }
90
+ /* Live conflicts: the conflicts card on the dependencies view wears the amber
91
+ border (task 1004738 moved it there from the band). */
94
92
  .goal-conflicts-card--live .count-pill { background: var(--warn-weak); color: var(--warn); }
95
93
  #goal-conflicts-view-card.goal-conflicts-card--live { background: var(--bg-card); border-color: color-mix(in srgb, var(--warn) 42%, transparent); }
96
94
 
@@ -554,3 +552,14 @@
554
552
  .gf-table .gf-deps { margin: 0; padding: 0; list-style: none; min-width: 12rem; }
555
553
  .gf-table .gf-deps li { font-size: 12.5px; line-height: 1.4; }
556
554
  .gf-none { color: var(--ink-faint); }
555
+
556
+ /* task 1004738 — the band's compact Dependencies card: a third of the width,
557
+ so each "ship first" title holds one line and ellipsises (the whole title is
558
+ its tooltip and the full view's table). */
559
+ .gf-brief__h { margin: 10px 0 2px; font-size: 12px; color: var(--ink-faint); }
560
+ .gf-brief { list-style: none; margin: 0; padding: 0; }
561
+ .gf-brief__row { display: flex; align-items: center; gap: 10px; font-size: 13px; }
562
+ .gf-brief__task { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
563
+ .gf-brief__n { flex-shrink: 0; font-size: 12px; color: var(--ink-faint); }
564
+ .gf-brief__conflicts { margin: 10px 0 0; font-size: 13px; color: var(--ink-soft); }
565
+ .gf-brief__conflicts--live { color: var(--warn); font-weight: 600; }
@@ -112,28 +112,18 @@
112
112
  <ul class="member-list" id="goal-members"></ul>
113
113
  <div class="goal-col__foot" id="goal-members-foot" hidden></div>
114
114
  </div>
115
- <div class="goal-band__col" id="goal-conflicts-card">
116
- <h2 class="scroll__h">Conflicts<span class="count-pill" id="goal-conflicts-count" hidden></span></h2>
117
- <p class="scroll__lede">Open tasks whose declared files overlap — sequence these before two builders collide.</p>
118
- <div id="goal-conflicts"></div>
119
- <!-- task 1828 — replaces the show-all pager: click into a dedicated
120
- full-width conflicts view instead of expanding this column. -->
121
- <div class="goal-col__foot" id="goal-conflicts-foot" hidden></div>
115
+ <!-- task 1004738 — the compact Dependencies card takes the slot
116
+ Conflicts held: a summary, the tasks to ship first and the
117
+ conflicts count. The forest, its table and the conflict list
118
+ are one click away on #/goal/N/dependencies. -->
119
+ <div class="goal-band__col" id="goal-deps-card">
120
+ <h2 class="scroll__h">Dependencies<span class="count-pill" id="goal-deps-card-count" hidden></span></h2>
121
+ <p class="scroll__lede" id="goal-deps-card-lede">Reading…</p>
122
+ <div id="goal-deps-card-body"></div>
123
+ <div class="goal-col__foot" id="goal-deps-card-foot" hidden></div>
122
124
  </div>
123
125
  </div>
124
126
 
125
- <!-- task 1003480 (ADR 0227 §6 item 4 + §10.5): the per-goal task
126
- dependency forest. Its own full-width section, not a band
127
- column — a node-link picture needs real width, and (unlike
128
- Completion/Members/Conflicts) this is bounded small BY DESIGN
129
- (ADR 0227 §3's own finding), so there is no "view all" split:
130
- what renders here is the whole forest, every time. -->
131
- <div class="scroll" style="margin-top:20px" id="goal-deps-scroll">
132
- <h2 class="scroll__h" id="goal-deps-h">Task dependencies<span class="count-pill" id="goal-deps-count" hidden></span></h2>
133
- <p class="scroll__lede" id="goal-deps-lede">Reading…</p>
134
- <div id="goal-deps-body"></div>
135
- </div>
136
-
137
127
  <div class="scroll" style="margin-top:20px">
138
128
  <div class="goal-work__head">
139
129
  <h2 class="scroll__h">Tasks</h2>
@@ -188,11 +178,19 @@
188
178
  </div>
189
179
  </section>
190
180
 
191
- <!-- CONFLICTS full view (hash-routed: #/goal/N/conflicts) — the whole
192
- conflict set, opened by the band's "View all" link (task 1828). -->
193
- <section id="goal-conflicts-view" hidden aria-labelledby="goal-conflicts-view-h">
194
- <p class="card-foot" style="margin:0 0 14px"><a href="#" id="goal-conflicts-back">← Back to goal</a></p>
195
- <div class="scroll" style="margin:0" id="goal-conflicts-view-card">
181
+ <!-- DEPENDENCIES full view (hash-routed: #/goal/N/dependencies; the old
182
+ #/goal/N/conflicts lands here too) — task 1004738. The per-goal task
183
+ dependency forest (task 1003480, ADR 0227 §6 item 4 + §10.5) gets
184
+ the page's full width a node-link picture needs, then the whole
185
+ conflict set (task 1828). -->
186
+ <section id="goal-dependencies-view" hidden aria-labelledby="goal-deps-h">
187
+ <p class="card-foot" style="margin:0 0 14px"><a href="#" id="goal-deps-back">← Back to goal</a></p>
188
+ <div class="scroll" style="margin:0" id="goal-deps-scroll">
189
+ <h2 class="scroll__h" id="goal-deps-h">Task dependencies<span class="count-pill" id="goal-deps-count" hidden></span></h2>
190
+ <p class="scroll__lede" id="goal-deps-lede">Reading…</p>
191
+ <div id="goal-deps-body"></div>
192
+ </div>
193
+ <div class="scroll" style="margin-top:20px" id="goal-conflicts-view-card">
196
194
  <h2 class="scroll__h" id="goal-conflicts-view-h">Conflicts<span class="count-pill" id="goal-conflicts-view-count" hidden></span></h2>
197
195
  <p class="scroll__lede">Open tasks whose declared files overlap — sequence these before two builders collide.</p>
198
196
  <div id="goal-conflicts-full"></div>
@@ -122,7 +122,25 @@
122
122
  ],
123
123
  "expect": {
124
124
  "visible": [
125
- "#goal-detail-view"
125
+ "#goal-detail-view",
126
+ "#goal-deps-card"
127
+ ]
128
+ }
129
+ },
130
+ "dependencies": {
131
+ "auth": true,
132
+ "url": "/builders/goals#/goal/25/dependencies",
133
+ "actions": [
134
+ [
135
+ "wait",
136
+ 1200
137
+ ]
138
+ ],
139
+ "expect": {
140
+ "visible": [
141
+ "#goal-dependencies-view",
142
+ "#goal-deps-scroll",
143
+ "#goal-conflicts-view-card"
126
144
  ]
127
145
  }
128
146
  },
@@ -1,9 +1,10 @@
1
- # The per-goal dependency forest — on the Goals page (tasks 1003480, 1004200)
1
+ # The per-goal dependency forest — on the Goals page (tasks 1003480, 1004200, 1004738)
2
2
 
3
3
  **THE PER-GOAL DEPENDENCY FOREST landed on the Goals page (task 1003480, goal 1000089; ADR 0227 §6 item 4 and its §10.5 amendment)** — the drill-down the goal map's own header pointed at (the map is the project sky since task 1004232): a task-level picture of which of THIS goal's tasks depend on which, from `GET /goals/:id/dependency-forest`. `goal-forest-lib.js` was the goal map's sibling — the same layered-layout idea (a task sits one column right of what it depends on, deterministic) at a smaller scale, since a per-goal forest is small BY MEASUREMENT (ADR 0227 §10.5: largest 127 tasks, median 9) rather than needing the goal map's cluster-and-shelf packing. Five things worth knowing:
4
4
 
5
- - **Its own full-width section, not a band column.** The existing Completion / Members / Conflicts band (task 1828) is three narrow columns; a node-link picture needs real width, so "Task dependencies" sits as its own `.scroll` block between the band and Tasks, drawn with `createElementNS` + `textContent` (the atlas idiom, which the retired goal-map.js shared — a task title never becomes markup) inside `.gf-scroll`, which names BOTH overflow axes (task 1004130).
6
- - **No cap, no "view all", unlike its band neighbours.** Completion/Members/Conflicts are per-item lists that can genuinely grow long, so they cap-and-link into a dedicated full view. A dependency forest cannot — ADR 0227 §3's own finding is that the tasks it omits are precisely the ones with nothing to connect them to, so what the server returns is already the honest whole picture, every time.
5
+ - **A compact card on the goal page, the whole picture one click away (task 1004738, owner's call).** The forest first shipped as its own full-width block between the band and Tasks — ~840px for a five-task forest, with a table repeating every task — and the owner called it way too long. Now the band's third column (Conflicts' old slot) is a **Dependencies card**: `brief` (one line), up to three `shipFirst` tasks (open, blocked by nothing open, ranked by how many open tasks wait on them down the chain), the conflicts count, and "See the full picture →". The forest, its table and the whole conflict list live on **`#/goal/N/dependencies`**; `#/goal/N/conflicts` still lands there because the attention inbox links it. Conflicts folded in rather than kept beside it: both answer "what order do we build in".
6
+ - **The full view is never capped.** ADR 0227 §3's finding still holds — the tasks a forest omits are precisely the ones with nothing to connect them to — so the full view draws everything the server returns; only the card summarises. The figure is drawn with `createElementNS` + `textContent` (the atlas idiom — a task title never becomes markup) inside `.gf-scroll`, which names BOTH overflow axes (task 1004130), and it is drawn only while its view is open (`state.view === 'dependencies'`): the goal page shows just the card, so drawing there would be thrown away, and a forest drawn hidden cannot measure its titles.
7
+ - **The card reads two feeds.** It renders from `state.forest` AND `state.conflictPairs`, so `renderConflicts` in `goals-render.js` calls `renderDependencyCard` again when the conflicts land; a null `conflictPairs` (loading, or a failed read) draws no conflicts line rather than a zero.
7
8
  - **The states are three, and distinct on purpose:** "Reading…" while the request is in flight, "No task in this goal depends on another task in this goal" for a real empty forest (never a blank canvas), and "Couldn't load…" only for an actual failure — a redirecting-to-auth error is swallowed silently instead, the `loadGoalTasks` precedent, because the page is already navigating away.
8
9
  - **`loadDependencyForest`/`renderDependencyForest` are exported from `createGoalsRender`'s return** specifically so `tests/goal_forest_page.mjs` can call them directly against a minimal DOM stub, without needing the much larger fixture a full `renderDetail()` boot would need for members/completion/invites this task never touches.
9
10
  - **The harness fixture is a single generic `goals__dependency-forest.json`** (task ids real, drawn from goal 25's own rows in `tasks.json`) — every goal id in the harness sees the same forest, which is fine for looking at the picture and not meant to describe any goal's real dependencies.