wowbagger 0.1.0-alpha.9 → 0.5.0

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 (46) hide show
  1. package/CHANGELOG.md +509 -0
  2. package/README.md +272 -136
  3. package/docs/adapter-contract.md +1 -1
  4. package/docs/host-contract.md +7 -1
  5. package/docs/mutation-contract.md +354 -82
  6. package/docs/work-claim-contract.md +466 -88
  7. package/package.json +2 -2
  8. package/schemas/core-capabilities-response.json +1 -1
  9. package/schemas/core-envelope.json +4 -3
  10. package/schemas/index.json +18 -0
  11. package/schemas/ledger-repair-proposal.json +170 -0
  12. package/schemas/ledger-repair-request.json +61 -0
  13. package/schemas/ledger-repair-response.json +90 -0
  14. package/schemas/report-config-v1.json +4 -0
  15. package/schemas/report-config-v2.json +5 -0
  16. package/skills/wowbagger/SKILL.md +242 -59
  17. package/src/adapter/core-probe.js +3 -4
  18. package/src/adapter/process-outcome.js +8 -1
  19. package/src/claim-capabilities.js +3 -3
  20. package/src/claim-coordinator.js +61 -12
  21. package/src/claim-journal.js +212 -7
  22. package/src/claim-prospective.js +1 -28
  23. package/src/claim-publication.js +412 -78
  24. package/src/claim-request.js +9 -0
  25. package/src/claim-store.js +9 -4
  26. package/src/cli.js +302 -56
  27. package/src/extensions.js +1 -0
  28. package/src/git-autocommit.js +106 -43
  29. package/src/git-reconciliation.js +74 -19
  30. package/src/git-worktrees.js +73 -0
  31. package/src/instrumentation.js +1 -0
  32. package/src/launch.js +2 -2
  33. package/src/ledger-repair.js +1170 -0
  34. package/src/mutation.js +73 -15
  35. package/src/reconciliation-classifier.js +117 -0
  36. package/src/report-evidence.js +158 -41
  37. package/src/report-graph.js +201 -73
  38. package/src/report-html.js +358 -155
  39. package/src/report-impact.js +106 -0
  40. package/src/report-selection.js +97 -0
  41. package/src/report-sequencing.js +4 -4
  42. package/src/report-svg.js +74 -20
  43. package/src/report-view.js +14 -1
  44. package/src/report.js +109 -23
  45. package/src/version-drift.js +98 -0
  46. package/src/worktree-identity.js +165 -0
@@ -66,6 +66,7 @@ export function buildGraphModel(model) {
66
66
  asOf: model.asOf,
67
67
  nodes,
68
68
  links: buildLinks([...model.items, ...model.terminalItems], new Set(nodes.map((node) => node.id))),
69
+ impactById: model.impactById ?? {},
69
70
  };
70
71
  }
71
72
 
@@ -174,12 +175,10 @@ export function graphStyleSource() {
174
175
  .join('');
175
176
  return `${bandRules}
176
177
  #graph-stage{position:relative;height:min(66vh,620px);border-radius:11px;background:#080b0f;overflow:hidden}
177
- .graph-filter{margin-bottom:13px}
178
- .graph-filter .facet-group{padding:0}
179
- .graph-filter-actions{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:10px}
180
- .graph-filter-buttons{display:flex;gap:8px}
181
- .graph-filter-actions button{padding:7px 10px}
182
- .graph-filter-actions .result-count{margin-right:auto}
178
+ #graph-node-count{margin:12px 0 0}
179
+ .graph-actions{display:flex;flex-wrap:wrap;gap:8px;margin:9px 0 0}
180
+ .graph-actions button{padding:6px 10px}
181
+ #graph-card .graph-actions button{border-color:#3b4c5e;background:#22303d;color:#e6edf3}
183
182
  #graph-empty{margin:12px 0 0;border-left:3px solid var(--navy);background:#eef1f4;padding:10px 13px}
184
183
  #graph-canvas{position:absolute;inset:0}
185
184
  #graph-labels{position:absolute;inset:0;pointer-events:none}
@@ -214,9 +213,25 @@ export function graphStyleSource() {
214
213
 
215
214
  // The roster is not a fallback bolted on for the WebGL-less reader; it is where
216
215
  // the graph's decision-relevant content lives. The 3D view adds shape and
217
- // adjacency to it and holds nothing the roster does not already say.
218
- function renderRoster(nodes) {
219
- return `<details class="graph-fallback"><summary>Every node, without the graph</summary><ol class="graph-roster">${nodes.map((node) => `<li class="graph-band-${escapeHtml(node.band)}" data-node-status="${escapeHtml(node.status)}"><p class="graph-head"><span class="graph-handle">${escapeHtml(node.handle)}</span><span class="graph-title">${escapeHtml(node.title)}</span><span class="graph-meta">${escapeHtml(BAND_LABELS[node.band] ?? node.band)} · ${escapeHtml(node.status)} · age ${escapeHtml(node.ageDays)}d · unblocks ${escapeHtml(node.leverage)}</span></p><p class="graph-why">${node.reasons.map((reason) => `<span class="graph-reason">${escapeHtml(reason.label)}</span>`).join('')}</p></li>`).join('')}</ol></details>`;
216
+ // adjacency to it and holds nothing the roster does not already say. Each row
217
+ // carries the same actions the graph card offers: canonical details through
218
+ // inspectItem, and labelled drilldowns from impactById. Only nonempty impact
219
+ // sets become buttons, so a terminal node offers details alone.
220
+ function renderGraphActions(node, impactById) {
221
+ const impact = impactById[node.id] ?? {};
222
+ const downstreamIds = Array.isArray(impact.downstreamIds) ? impact.downstreamIds : [];
223
+ const readyIds = Array.isArray(impact.readyIfDoneIds) ? impact.readyIfDoneIds : [];
224
+ let buttons = `<button type="button" data-inspect="${escapeHtml(node.id)}">Details</button>`;
225
+ if (downstreamIds.length > 0) {
226
+ buttons += `<button type="button" data-downstream="${escapeHtml(node.id)}">Downstream (${downstreamIds.length})</button>`;
227
+ }
228
+ if (readyIds.length > 0) {
229
+ buttons += `<button type="button" data-ready="${escapeHtml(node.id)}">Ready if done (${readyIds.length})</button>`;
230
+ }
231
+ return `<p class="graph-actions">${buttons}</p>`;
232
+ }
233
+ function renderRoster(nodes, impactById = {}) {
234
+ return `<details class="graph-fallback"><summary>Every node, without the graph</summary><ol class="graph-roster">${nodes.map((node) => `<li class="graph-band-${escapeHtml(node.band)}" data-node-id="${escapeHtml(node.id)}" data-node-status="${escapeHtml(node.status)}"><p class="graph-head"><span class="graph-handle">${escapeHtml(node.handle)}</span><span class="graph-title">${escapeHtml(node.title)}</span><span class="graph-meta">${escapeHtml(BAND_LABELS[node.band] ?? node.band)} · ${escapeHtml(node.status)} · age ${escapeHtml(node.ageDays)}d · unblocks ${escapeHtml(node.leverage)}</span></p><p class="graph-why">${node.reasons.map((reason) => `<span class="graph-reason">${escapeHtml(reason.label)}</span>`).join('')}</p>${renderGraphActions(node, impactById)}</li>`).join('')}</ol></details>`;
220
235
  }
221
236
 
222
237
  function renderLegend() {
@@ -225,20 +240,6 @@ function renderLegend() {
225
240
  .join('');
226
241
  return `<div id="graph-legend">${bands}<span class="graph-key graph-key-edge graph-band-depends">Unblocks (depends_on)</span><span class="graph-key graph-key-edge graph-key-edge-parent graph-band-parent">Parent to child</span></div>`;
227
242
  }
228
-
229
- // The filter offers the lifecycle statuses the ledger actually holds, each
230
- // carrying how many nodes answer to it, and starts with all of them selected:
231
- // the graph's own default is the whole ledger. Select all and Clear are the two
232
- // moves a chip strip cannot make on its own.
233
- function renderStatusFilter(nodes) {
234
- const statuses = [...new Set(nodes.map((node) => node.status))].sort();
235
- const chips = statuses.map((status) => {
236
- const count = nodes.filter((node) => node.status === status).length;
237
- return `<label class="chip"><input type="checkbox" class="graph-status" value="${escapeHtml(status)}" checked><span class="chip-text">${escapeHtml(status)}</span> <span class="chip-count">${count}</span></label>`;
238
- }).join('');
239
- return `<div id="graph-filter" class="graph-filter"><fieldset class="facet-group" data-group="graph-status"><legend>Status</legend><div class="chips">${chips}</div></fieldset><div class="graph-filter-actions"><p id="graph-node-count" class="result-count" role="status" aria-live="polite">Showing ${nodes.length} of ${nodes.length} ${nodes.length === 1 ? 'node' : 'nodes'}</p><span class="graph-filter-buttons"><button type="button" id="graph-status-all">Select all</button><button type="button" id="graph-status-clear">Clear</button></span></div></div>`;
240
- }
241
-
242
243
  // The empty state has to name what emptied the graph. A reader who cleared
243
244
  // every status did that, and selecting one undoes it. A named view that matched
244
245
  // nothing was never narrowed by anybody, and there is no status for that reader
@@ -250,36 +251,58 @@ function renderGraphEmptyState(nodes, view) {
250
251
  }
251
252
 
252
253
  export function graphSection(model, manifest, view = null) {
254
+ const nodes = model.nodes;
253
255
  return `<section id="graph" class="panel"><div class="section-heading"><div><p class="eyebrow">Dependencies</p><h2>Ledger graph</h2></div><p class="muted">Every item as a node, sized by how much it unblocks. Edges run from a prerequisite or parent to the item it releases.</p></div>
254
- ${renderStatusFilter(model.nodes)}
256
+ <p id="graph-node-count" class="result-count" role="status" aria-live="polite">Showing ${nodes.length} of ${nodes.length} ${nodes.length === 1 ? 'node' : 'nodes'}</p>
255
257
  <div id="graph-stage"><div id="graph-canvas"></div><div id="graph-labels" aria-hidden="true"></div><aside id="graph-card" hidden></aside><p id="graph-hint">Drag to orbit · scroll to zoom · hover or click a node</p></div>
256
258
  <p id="graph-nowebgl" hidden>This browser has no WebGL, so the graph cannot draw. Nothing is lost: every node, its status, its age, and the reasons that place it are listed below, and the graph only adds the shape of the dependencies between them.</p>
257
- ${renderGraphEmptyState(model.nodes, view)}
259
+ ${renderGraphEmptyState(nodes, view)}
258
260
  ${renderLegend()}
259
- ${renderRoster(model.nodes)}
261
+ ${renderRoster(nodes, model.impactById ?? {})}
260
262
  <p class="muted">Rendered by ${escapeHtml(manifest.package)} ${escapeHtml(manifest.version)}, vendored and checksummed in this repository and inlined here. This report fetches nothing.</p></section>`;
261
263
  }
262
264
 
263
265
  export function graphClientSource(model) {
264
266
  return `'use strict';
265
- var GRAPH_MODEL=${scriptJson(model)};
267
+ var GRAPH_MODEL=${scriptJson({ asOf: model.asOf, nodes: model.nodes, links: model.links })};
266
268
  var GRAPH_COLORS=${scriptJson(BAND_COLORS)};
267
269
  var graphStage=document.getElementById('graph-stage');
268
270
  var graphNotice=document.getElementById('graph-nowebgl');
269
271
  var graphEmpty=document.getElementById('graph-empty');
270
272
  var graphCount=document.getElementById('graph-node-count');
273
+ var graphEmptyCopy=graphEmpty?graphEmpty.textContent:'';
271
274
  var graphFallback=document.querySelector('.graph-fallback');
272
- var graphStatusInputs=Array.prototype.slice.call(document.querySelectorAll('.graph-status'));
273
- var graphRosterRows=Array.prototype.slice.call(document.querySelectorAll('[data-node-status]'));
274
- // The stage hands back the one function that redraws it. Without WebGL there is
275
- // no stage, and the filter still runs: it is the roster's filter too.
275
+ var graphSection=document.getElementById('section-dependencies');
276
+ var graphNav=document.getElementById('nav-dependencies');
277
+ var graphReducedMotion=false;
278
+ try{graphReducedMotion=!!(window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches);}catch(error){}
279
+ var graphStarted=false;
280
+ var graphDisposed=false;
281
+ var graphInstance=null;
282
+ var graphUnsubscribe=null;
283
+ var graphLabelRaf=0;
284
+ var graphRosterRows=Array.prototype.slice.call(document.querySelectorAll('[data-node-id]'));
285
+ var graphNodesById=Object.create(null);
286
+ GRAPH_MODEL.nodes.forEach(function(node){graphNodesById[node.id]=node;});
287
+ // The shared report scope owns selection: the controller installed by
288
+ // reportClientSource publishes matching items, and the graph draws the induced
289
+ // subgraph without recomputing readiness from an incomplete picture.
290
+ var graphReport=window.wowbaggerReport||null;
291
+ // The stage hands back the one function that redraws it. A scope that arrives
292
+ // before the stage exists waits here until the first sizing.
276
293
  var graphDraw=null;
294
+ var graphPendingVisible=null;
277
295
  var graphShownId=null;
296
+ // Probed once: every probe opens a WebGL context, and a retry loop that probes
297
+ // each frame exhausts the browser's context budget and evicts the graph's own.
298
+ var graphWebglProbe=null;
278
299
  function graphWebglAvailable(){
300
+ if(graphWebglProbe!==null)return graphWebglProbe;
279
301
  try{
280
302
  var probe=document.createElement('canvas');
281
- return !!(window.WebGLRenderingContext&&(probe.getContext('webgl2')||probe.getContext('webgl')));
282
- }catch(error){return false;}
303
+ graphWebglProbe=!!(window.WebGLRenderingContext&&(probe.getContext('webgl2')||probe.getContext('webgl')));
304
+ }catch(error){graphWebglProbe=false;}
305
+ return graphWebglProbe;
283
306
  }
284
307
  function graphBandColor(node){return GRAPH_COLORS[node.band]||'#94a3b8';}
285
308
  function graphRenderCard(card,node){
@@ -308,52 +331,158 @@ function graphRenderCard(card,node){
308
331
  why.append(chip);
309
332
  });
310
333
  card.append(handle,title,facts,why);
334
+ var actions=document.createElement('p');
335
+ actions.className='graph-actions';
336
+ var details=document.createElement('button');
337
+ details.type='button';
338
+ details.textContent='Details';
339
+ details.addEventListener('click',function(){graphInspect(node.id);});
340
+ actions.append(details);
341
+ var impact=graphImpactLists(node.id);
342
+ if(impact.downstreamIds.length>0){
343
+ var downstream=document.createElement('button');
344
+ downstream.type='button';
345
+ downstream.textContent='Downstream ('+impact.downstreamIds.length+')';
346
+ downstream.addEventListener('click',function(){graphShowImpact(node.id,'downstream');});
347
+ actions.append(downstream);
348
+ }
349
+ if(impact.readyIfDoneIds.length>0){
350
+ var unlocks=document.createElement('button');
351
+ unlocks.type='button';
352
+ unlocks.textContent='Ready if done ('+impact.readyIfDoneIds.length+')';
353
+ unlocks.addEventListener('click',function(){graphShowImpact(node.id,'ready');});
354
+ actions.append(unlocks);
355
+ }
356
+ card.append(actions);
311
357
  card.hidden=false;
312
358
  graphShownId=node.id;
313
359
  }
314
- function graphSelectedStatuses(){
315
- var chosen=Object.create(null);
316
- graphStatusInputs.forEach(function(input){
317
- if(input.checked)chosen[input.value]=true;
318
- });
319
- return chosen;
320
- }
321
- // One selection drives the stage, the roster, and the count together, so the
322
- // three can never disagree about which part of the ledger is on screen.
323
- function graphApplyFilter(){
324
- var chosen=graphSelectedStatuses();
360
+ // One scope drives the stage, the roster, and the count together, so the three
361
+ // can never disagree about which part of the ledger is on screen. Bands and
362
+ // reasons come from the complete-ledger model, so hiding a prerequisite never
363
+ // recomputes the items it holds back.
364
+ function graphApplyScope(items){
325
365
  var visible=Object.create(null);
326
366
  var shown=0;
327
- GRAPH_MODEL.nodes.forEach(function(node){
328
- if(chosen[node.status]!==true)return;
329
- visible[node.id]=true;
330
- shown+=1;
367
+ (items||[]).forEach(function(item){
368
+ if(item&&graphNodesById[item.id]!==undefined&&visible[item.id]!==true){
369
+ visible[item.id]=true;
370
+ shown+=1;
371
+ }
331
372
  });
332
373
  graphRosterRows.forEach(function(row){
333
- row.hidden=chosen[row.getAttribute('data-node-status')]!==true;
374
+ var id=row.getAttribute('data-node-id');
375
+ row.hidden=!(id!==null&&visible[id]===true);
334
376
  });
335
377
  var total=GRAPH_MODEL.nodes.length;
336
- graphCount.textContent='Showing '+shown+' of '+total+(total===1?' node':' nodes');
337
- graphEmpty.hidden=shown!==0;
378
+ if(graphCount)graphCount.textContent='Showing '+shown+' of '+total+(total===1?' node':' nodes');
379
+ if(graphEmpty){
380
+ if(total===0){graphEmpty.hidden=false;}
381
+ else if(shown===0){graphEmpty.textContent='No items match the current scope, so the graph has nothing to draw.';graphEmpty.hidden=false;}
382
+ else{graphEmpty.textContent=graphEmptyCopy;graphEmpty.hidden=true;}
383
+ }
338
384
  if(graphDraw)graphDraw(visible);
385
+ else graphPendingVisible=visible;
339
386
  }
340
- // The two moves a chip strip cannot make on its own. Clearing every status is a
341
- // legitimate request, and its honest answer is an empty graph that says so.
342
- function graphSetEveryStatus(checked){
343
- graphStatusInputs.forEach(function(input){input.checked=checked;});
344
- graphApplyFilter();
387
+ function graphMount(){return document.getElementById('graph-canvas');}
388
+ // Impact actions read the frozen controller map, never a recomputed graph: the
389
+ // labels carry the same downstream and ready-if-done sets the Items detail
390
+ // shows, with their distinct counts.
391
+ function graphImpactLists(id){
392
+ var entry=null;
393
+ try{entry=graphReport&&graphReport.impactById?graphReport.impactById[id]:null;}catch(error){entry=null;}
394
+ var downstream=entry&&Array.isArray(entry.downstreamIds)?entry.downstreamIds.slice():[];
395
+ var ready=entry&&Array.isArray(entry.readyIfDoneIds)?entry.readyIfDoneIds.slice():[];
396
+ return {downstreamIds:downstream,readyIfDoneIds:ready};
345
397
  }
346
- if(!window.ForceGraph3D||!graphWebglAvailable()){
347
- graphStage.hidden=true;
348
- graphNotice.hidden=false;
349
- graphFallback.open=true;
350
- }else{
398
+ function graphNodeHandle(id){var node=graphNodesById[id];return node?node.handle:id;}
399
+ function graphInspect(id){
400
+ try{if(graphReport&&typeof graphReport.inspectItem==='function')return graphReport.inspectItem(id);}catch(error){}
401
+ return false;
402
+ }
403
+ function graphShowImpact(id,kind){
404
+ if(!graphReport||typeof graphReport.showItems!=='function')return;
405
+ var lists=graphImpactLists(id);
406
+ var handle=graphNodeHandle(id);
407
+ if(kind==='downstream'&&lists.downstreamIds.length>0)graphReport.showItems({label:'Downstream of '+handle+' ('+lists.downstreamIds.length+')',itemIds:lists.downstreamIds});
408
+ if(kind==='ready'&&lists.readyIfDoneIds.length>0)graphReport.showItems({label:'Ready if '+handle+' is done ('+lists.readyIfDoneIds.length+')',itemIds:lists.readyIfDoneIds});
409
+ }
410
+ function graphWireActions(root){
411
+ Array.prototype.slice.call(root.querySelectorAll('[data-inspect]')).forEach(function(button){
412
+ button.addEventListener('click',function(){graphInspect(button.getAttribute('data-inspect'));});
413
+ });
414
+ Array.prototype.slice.call(root.querySelectorAll('[data-downstream]')).forEach(function(button){
415
+ button.addEventListener('click',function(){graphShowImpact(button.getAttribute('data-downstream'),'downstream');});
416
+ });
417
+ Array.prototype.slice.call(root.querySelectorAll('[data-ready]')).forEach(function(button){
418
+ button.addEventListener('click',function(){graphShowImpact(button.getAttribute('data-ready'),'ready');});
419
+ });
420
+ }
421
+ graphWireActions(document);
422
+ function graphSectionHidden(){return !!(graphSection&&graphSection.hidden===true);}
423
+ function graphMeasurable(){var mount=graphMount();return !!(mount&&mount.clientWidth>0&&mount.clientHeight>0);}
424
+ function graphCanDraw(){return !!(window.ForceGraph3D&&graphWebglAvailable());}
425
+ // The 1,744-item reference built one canvas while Dependencies was still
426
+ // hidden: a hidden container measures zero, so the first sizing waits for a
427
+ // visible section with measurable dimensions. Repeated visits reuse the one
428
+ // canvas instead of starting the layout again.
429
+ function graphEnsureStarted(){
430
+ if(graphStarted||graphDisposed)return;
431
+ if(!graphCanDraw()||GRAPH_MODEL.nodes.length===0)return;
432
+ if(graphSectionHidden())return;
433
+ if(!graphMeasurable()){
434
+ if(window.requestAnimationFrame)window.requestAnimationFrame(function(){graphEnsureStarted();});
435
+ return;
436
+ }
351
437
  graphStart();
438
+ graphStarted=true;
439
+ if(graphPendingVisible&&graphDraw)graphDraw(graphPendingVisible);
440
+ }
441
+ function graphNoteVisibility(){
442
+ if(!graphStarted||!graphInstance)return;
443
+ if(graphSectionHidden()){
444
+ if(graphInstance&&typeof graphInstance.pauseAnimation==='function')graphInstance.pauseAnimation();
445
+ return;
446
+ }
447
+ var mount=graphMount();
448
+ if(mount&&graphInstance&&typeof graphInstance.width==='function'){
449
+ try{graphInstance.width(mount.clientWidth).height(mount.clientHeight);}catch(error){}
450
+ }
451
+ if(graphInstance&&typeof graphInstance.resumeAnimation==='function')graphInstance.resumeAnimation();
452
+ }
453
+ // Disposal releases the layout, its label loop, and the scope subscription, so
454
+ // leaving the document does not leave the force layout ticking.
455
+ function graphDispose(){
456
+ if(graphDisposed)return;
457
+ graphDisposed=true;
458
+ if(typeof graphUnsubscribe==='function'){try{graphUnsubscribe();}catch(error){}graphUnsubscribe=null;}
459
+ if(window.cancelAnimationFrame&&graphLabelRaf){try{window.cancelAnimationFrame(graphLabelRaf);}catch(error){}graphLabelRaf=0;}
460
+ if(graphInstance&&typeof graphInstance._destructor==='function'){try{graphInstance._destructor();}catch(error){}}
461
+ graphInstance=null;
462
+ graphDraw=null;
463
+ }
464
+ if(!graphCanDraw()){
465
+ if(graphStage)graphStage.hidden=true;
466
+ if(graphNotice)graphNotice.hidden=false;
467
+ if(graphFallback)graphFallback.open=true;
468
+ }
469
+ if(graphReport&&typeof graphReport.subscribeScope==='function'){
470
+ graphUnsubscribe=graphReport.subscribeScope(function(items){graphApplyScope(items||[]);graphEnsureStarted();graphNoteVisibility();});
471
+ }else{
472
+ graphApplyScope(GRAPH_MODEL.nodes.map(function(node){return {id:node.id};}));
473
+ graphEnsureStarted();
474
+ }
475
+ if(graphNav)graphNav.addEventListener('click',function(){graphEnsureStarted();graphNoteVisibility();});
476
+ if(graphSection&&window.MutationObserver){
477
+ try{
478
+ new window.MutationObserver(function(){graphEnsureStarted();graphNoteVisibility();}).observe(graphSection,{attributes:true,attributeFilter:['hidden']});
479
+ }catch(error){}
480
+ }
481
+ if(window.addEventListener){
482
+ window.addEventListener('resize',function(){graphEnsureStarted();graphNoteVisibility();});
483
+ window.addEventListener('pagehide',function(){graphDispose();});
484
+ window.addEventListener('unload',function(){graphDispose();});
352
485
  }
353
- graphStatusInputs.forEach(function(input){input.addEventListener('change',graphApplyFilter);});
354
- document.getElementById('graph-status-all').addEventListener('click',function(){graphSetEveryStatus(true);});
355
- document.getElementById('graph-status-clear').addEventListener('click',function(){graphSetEveryStatus(false);});
356
- graphApplyFilter();
357
486
  function graphStart(){
358
487
  var mount=document.getElementById('graph-canvas');
359
488
  var labelLayer=document.getElementById('graph-labels');
@@ -381,9 +510,10 @@ function graphStart(){
381
510
  .linkDirectionalArrowLength(function(link){return link.kind==='parent'?0:8;})
382
511
  .linkDirectionalArrowRelPos(1)
383
512
  .onNodeHover(function(node){if(node)graphRenderCard(card,node);})
384
- .onNodeClick(function(node){if(node)graphRenderCard(card,node);})
513
+ .onNodeClick(function(node){if(node){graphRenderCard(card,node);graphInspect(node.id);}})
385
514
  .onBackgroundClick(function(){card.hidden=true;graphShownId=null;})
386
- .cooldownTicks(220);
515
+ .cooldownTicks(graphReducedMotion?0:220);
516
+ graphInstance=graph;
387
517
  graph.d3Force('charge').strength(-40);
388
518
  var labels=nodes.map(function(node){
389
519
  var element=document.createElement('span');
@@ -397,11 +527,9 @@ function graphStart(){
397
527
  graph.onEngineStop(function(){
398
528
  if(framed||drawn===0)return;
399
529
  framed=true;
400
- graph.zoomToFit(800,20);
401
- });
402
- window.addEventListener('resize',function(){
403
- graph.width(mount.clientWidth).height(mount.clientHeight);
530
+ graph.zoomToFit(graphReducedMotion?0:800,20);
404
531
  });
532
+ window.addEventListener('resize',function(){graphNoteVisibility();});
405
533
  // A hidden node's edges have nowhere to land, and its label would hang over
406
534
  // empty space, so the node, its links, and its label leave together. Handing
407
535
  // the graph new data reheats the layout, which is what re-forms the remaining
@@ -458,8 +586,8 @@ function graphStart(){
458
586
  placement.element.style.transform='translate('+placement.screen.x.toFixed(1)+'px,'+placement.screen.y.toFixed(1)+'px)';
459
587
  placement.element.style.opacity=(1-0.55*((placement.depth-nearest)/span)).toFixed(2);
460
588
  }
461
- window.requestAnimationFrame(graphPlaceLabels);
589
+ if(!graphDisposed)graphLabelRaf=window.requestAnimationFrame(graphPlaceLabels);
462
590
  }
463
- window.requestAnimationFrame(graphPlaceLabels);
591
+ graphLabelRaf=window.requestAnimationFrame(graphPlaceLabels);
464
592
  }`;
465
593
  }