@reqlan/analytical 0.6.3 → 0.8.1

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 (90) hide show
  1. package/README.md +30 -0
  2. package/out/analysis/fuzzy-search-analyser.d.ts +17 -0
  3. package/out/analysis/fuzzy-search-analyser.js +25 -0
  4. package/out/analysis/fuzzy-search-analyser.js.map +1 -0
  5. package/out/analysis/fuzzy-search-worker-client.d.ts +39 -0
  6. package/out/analysis/fuzzy-search-worker-client.js +178 -0
  7. package/out/analysis/fuzzy-search-worker-client.js.map +1 -0
  8. package/out/analysis/fuzzy-search-worker.d.ts +32 -0
  9. package/out/analysis/fuzzy-search-worker.js +81 -0
  10. package/out/analysis/fuzzy-search-worker.js.map +1 -0
  11. package/out/analysis/fuzzy-search.d.ts +38 -0
  12. package/out/analysis/fuzzy-search.js +217 -0
  13. package/out/analysis/fuzzy-search.js.map +1 -0
  14. package/out/analysis/git-dates-analyser.js +12 -3
  15. package/out/analysis/git-dates-analyser.js.map +1 -1
  16. package/out/analysis-api.d.ts +16 -2
  17. package/out/analysis-api.js +36 -3
  18. package/out/analysis-api.js.map +1 -1
  19. package/out/core/barrel-page.d.ts +46 -0
  20. package/out/core/barrel-page.js +162 -0
  21. package/out/core/barrel-page.js.map +1 -0
  22. package/out/core/filter-specials.d.ts +37 -0
  23. package/out/core/filter-specials.js +113 -0
  24. package/out/core/filter-specials.js.map +1 -0
  25. package/out/core/types.d.ts +27 -1
  26. package/out/core/types.js +1 -0
  27. package/out/core/types.js.map +1 -1
  28. package/out/create-runtime.js +2 -0
  29. package/out/create-runtime.js.map +1 -1
  30. package/out/export/build-export-snapshot.d.ts +2 -2
  31. package/out/export/build-export-snapshot.js +43 -16
  32. package/out/export/build-export-snapshot.js.map +1 -1
  33. package/out/export/export-csv.d.ts +4 -0
  34. package/out/export/export-csv.js +13 -0
  35. package/out/export/export-csv.js.map +1 -0
  36. package/out/export/export-html.d.ts +2 -2
  37. package/out/export/export-html.js +7 -3
  38. package/out/export/export-html.js.map +1 -1
  39. package/out/export/export-json.d.ts +6 -0
  40. package/out/export/export-json.js +28 -0
  41. package/out/export/export-json.js.map +1 -0
  42. package/out/export/export-markdown.d.ts +4 -0
  43. package/out/export/export-markdown.js +23 -0
  44. package/out/export/export-markdown.js.map +1 -0
  45. package/out/export/html-export-assets.d.ts +1 -1
  46. package/out/export/html-export-assets.js +663 -20
  47. package/out/export/html-export-assets.js.map +1 -1
  48. package/out/export/html-export-template.js +160 -61
  49. package/out/export/html-export-template.js.map +1 -1
  50. package/out/export/html-export-utils.d.ts +33 -3
  51. package/out/export/html-export-utils.js +121 -13
  52. package/out/export/html-export-utils.js.map +1 -1
  53. package/out/export/types.d.ts +20 -1
  54. package/out/export/write-csv-export.d.ts +5 -0
  55. package/out/export/write-csv-export.js +132 -0
  56. package/out/export/write-csv-export.js.map +1 -0
  57. package/out/export/write-html-export.d.ts +2 -2
  58. package/out/export/write-html-export.js +61 -29
  59. package/out/export/write-html-export.js.map +1 -1
  60. package/out/export/write-json-export.d.ts +3 -0
  61. package/out/export/write-json-export.js +92 -0
  62. package/out/export/write-json-export.js.map +1 -0
  63. package/out/export/write-markdown-export.d.ts +5 -0
  64. package/out/export/write-markdown-export.js +177 -0
  65. package/out/export/write-markdown-export.js.map +1 -0
  66. package/out/graph/physics-core-source.d.ts +5 -0
  67. package/out/graph/physics-core-source.js +8 -0
  68. package/out/graph/physics-core-source.js.map +1 -0
  69. package/out/index-store/idea-extractor.d.ts +6 -1
  70. package/out/index-store/idea-extractor.js +56 -11
  71. package/out/index-store/idea-extractor.js.map +1 -1
  72. package/out/index-store/index-diagnostics-store.js +1 -2
  73. package/out/index-store/index-diagnostics-store.js.map +1 -1
  74. package/out/index-store/sqlite-store.d.ts +16 -3
  75. package/out/index-store/sqlite-store.js +107 -9
  76. package/out/index-store/sqlite-store.js.map +1 -1
  77. package/out/index-store/webview-graph-queries.d.ts +13 -2
  78. package/out/index-store/webview-graph-queries.js +98 -18
  79. package/out/index-store/webview-graph-queries.js.map +1 -1
  80. package/out/index-store/webview-table-queries.js +1 -0
  81. package/out/index-store/webview-table-queries.js.map +1 -1
  82. package/out/index-store/workspace-index-file.js +22 -1
  83. package/out/index-store/workspace-index-file.js.map +1 -1
  84. package/out/index-store/workspace-index.d.ts +2 -1
  85. package/out/index-store/workspace-index.js +4 -1
  86. package/out/index-store/workspace-index.js.map +1 -1
  87. package/out/index.d.ts +17 -2
  88. package/out/index.js +10 -1
  89. package/out/index.js.map +1 -1
  90. package/package.json +7 -3
@@ -1 +1 @@
1
- {"version":3,"file":"html-export-assets.js","sourceRoot":"","sources":["../../src/export/html-export-assets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,2FAA2F;AAC3F,SAAS,sBAAsB;IAC3B,OAAO,YAAY,CAAC,IAAI,GAAG,CAAC,0BAA0B,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;SAC5E,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0V5B,CAAC;AAEF,MAAM,CAAC,MAAM,MAAM,GAAG;EACpB,sBAAsB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA45BzB,CAAC;AAEF,MAAM,UAAU,sBAAsB,CAAC,SAAkB;IACrD,OAAO,wCAAwC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,KAAK,CAAC;AAC3G,CAAC"}
1
+ {"version":3,"file":"html-export-assets.js","sourceRoot":"","sources":["../../src/export/html-export-assets.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,41 @@
1
- import { escapeHtml, fileByIdea, filePageEnabled, formatAttributeValue, formatDate, hrefFor, ideaStatus, ideaTags, pageHref, relatedClusters, renderAttributeValueHtml, renderDefinitionList, renderIdeaSummaryHtml, renderMetric, renderOptionalStatusCell, renderOptionalStatusParagraph, renderOptionalTagsCell, renderOptionalTagsParagraph, renderTextWithRefsHtml, resolveExportFilePage, slugAttributeKey, stringifyJson } from './html-export-utils.js';
1
+ import { escapeHtml, exportIdeaAnchorId, fileByIdea, filePageEnabled, formatAttributeValue, formatDate, hrefFor, ideaStatus, ideaTags, pageHref, relatedClusters, renderAttributeValueHtml, listFilterHref, renderDefinitionList, renderFilePathCell, renderIdeaSummaryHtml, renderLinkedStatusCell, renderLinkedTagsCell, renderMetric, renderOptionalStatusCell, renderOptionalTagsCell, renderPrintIdeaAttributesHtml, renderTextWithRefsHtml, resolveExportFilePage, slugAttributeKey, statusOrTagFacetHref, stringifyJson } from './html-export-utils.js';
2
+ import { FILTER_EMPTY, FILTER_NOT_PRESENT, filterDisplayLabel, isFilterEmpty, isFilterNotPresent, isFilterUnspecified } from '../core/filter-specials.js';
3
+ function renderMultiFilterSelect(attr, countsByValue, emptyLabel) {
4
+ const valueSet = new Set(Object.keys(countsByValue));
5
+ valueSet.add(FILTER_NOT_PRESENT);
6
+ valueSet.add(FILTER_EMPTY);
7
+ const options = [...valueSet].map(value => ({
8
+ value,
9
+ label: filterDisplayLabel(value),
10
+ special: isFilterNotPresent(value) || isFilterEmpty(value) || isFilterUnspecified(value),
11
+ kind: isFilterNotPresent(value)
12
+ ? 'not-present'
13
+ : isFilterEmpty(value)
14
+ ? 'empty'
15
+ : isFilterUnspecified(value)
16
+ ? 'unspecified'
17
+ : 'concrete',
18
+ count: countsByValue[value] ?? 0
19
+ }));
20
+ options.sort((left, right) => {
21
+ const rank = (kind) => kind === 'not-present' ? 0 : kind === 'empty' ? 1 : kind === 'unspecified' ? 2 : 3;
22
+ return rank(left.kind) - rank(right.kind) || left.label.localeCompare(right.label);
23
+ });
24
+ return `
25
+ <div
26
+ class="scd is-loading"
27
+ data-graph-${attr}-scd
28
+ data-placeholder="${escapeHtml(emptyLabel)}"
29
+ data-label="${attr === 'status' ? 'Status' : 'Tags'}"
30
+ data-options="${escapeHtml(JSON.stringify(options))}"
31
+ >
32
+ <button type="button" class="scd-trigger" aria-haspopup="listbox" aria-expanded="false" aria-busy="true">
33
+ <span class="scd-trigger-label">${attr === 'status' ? 'Status…' : 'Tags…'}</span>
34
+ <span class="scd-chevron" aria-hidden="true"></span>
35
+ </button>
36
+ </div>
37
+ `;
38
+ }
2
39
  function exportHref(snapshot, currentPath, targetPath) {
3
40
  return hrefFor(currentPath, targetPath, snapshot.urlBase);
4
41
  }
@@ -7,8 +44,13 @@ function exportPageHref(snapshot, currentPath, pageInfo) {
7
44
  }
8
45
  export function renderHomePage(snapshot) {
9
46
  const currentPath = snapshot.manifest.home.path;
10
- const highlightClusters = snapshot.clusters.slice(0, 8).map(cluster => `
11
- <a class="entity-card" href="${escapeHtml(exportPageHref(snapshot, currentPath, cluster.page))}">
47
+ const clustersIndexHref = exportPageHref(snapshot, currentPath, snapshot.manifest.clustersIndex);
48
+ const highlightClusters = snapshot.clusters.slice(0, 8).map(cluster => {
49
+ const href = snapshot.pageOptions.includeClusterPages
50
+ ? exportPageHref(snapshot, currentPath, cluster.page)
51
+ : listFilterHref(snapshot, currentPath, snapshot.manifest.clustersIndex, { kind: cluster.kind, cluster: cluster.label });
52
+ return `
53
+ <a class="entity-card" href="${escapeHtml(href)}">
12
54
  <div class="pill-row">
13
55
  <span class="pill">${escapeHtml(cluster.kind)}</span>
14
56
  <span class="pill">${cluster.counts.ideas} ideas</span>
@@ -16,7 +58,8 @@ export function renderHomePage(snapshot) {
16
58
  <h3>${escapeHtml(cluster.label)}</h3>
17
59
  <p class="subtle">${escapeHtml(cluster.description)}</p>
18
60
  </a>
19
- `).join('');
61
+ `;
62
+ }).join('');
20
63
  const body = `
21
64
  <header class="hero">
22
65
  <div class="panel">
@@ -37,17 +80,17 @@ export function renderHomePage(snapshot) {
37
80
  ${renderMetric('References', String(snapshot.counts.edges), exportPageHref(snapshot, currentPath, snapshot.manifest.graph))}
38
81
  ${renderMetric('Files', String(snapshot.counts.files), exportPageHref(snapshot, currentPath, snapshot.manifest.filesIndex))}
39
82
  ${renderMetric('Code files', String(snapshot.codeFiles.length), exportPageHref(snapshot, currentPath, snapshot.manifest.codeFilesIndex))}
40
- ${renderMetric('Clusters', String(snapshot.counts.clusters), exportPageHref(snapshot, currentPath, snapshot.manifest.clustersIndex))}
83
+ ${renderMetric('Clusters', String(snapshot.counts.clusters), clustersIndexHref)}
41
84
  ${renderMetric('Attributes', String(snapshot.attributes.length), exportPageHref(snapshot, currentPath, snapshot.manifest.attributesIndex))}
42
85
  </section>
43
86
  <section class="split">
44
87
  <div class="panel">
45
88
  <h2>Status Rollup</h2>
46
- <div class="scroll-window">${renderDefinitionList(snapshot.byStatus)}</div>
89
+ <div class="scroll-window">${renderDefinitionList(snapshot.byStatus, key => statusOrTagFacetHref(snapshot, currentPath, 'status', key))}</div>
47
90
  </div>
48
91
  <div class="panel">
49
92
  <h2>Tags</h2>
50
- <div class="scroll-window">${renderDefinitionList(snapshot.byTag)}</div>
93
+ <div class="scroll-window">${renderDefinitionList(snapshot.byTag, key => statusOrTagFacetHref(snapshot, currentPath, 'tag', key))}</div>
51
94
  </div>
52
95
  </section>
53
96
  <section class="panel">
@@ -75,9 +118,9 @@ export function renderIdeasIndexPage(snapshot) {
75
118
  const rows = snapshot.ideas.map(idea => `
76
119
  <tr data-filter-row="ideas" data-filter-text="${escapeHtml([idea.name, idea.summary, ideaStatus(idea) ?? '', ideaTags(idea).join(' '), idea.fileUri].join(' '))}">
77
120
  <td><strong>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</strong></td>
78
- <td>${escapeHtml(idea.fileUri)}</td>
79
- <td>${renderOptionalStatusCell(idea)}</td>
80
- <td>${renderOptionalTagsCell(idea)}</td>
121
+ <td>${renderFilePathCell(snapshot, currentPath, idea.fileUri)}</td>
122
+ <td>${renderLinkedStatusCell(snapshot, currentPath, idea)}</td>
123
+ <td>${renderLinkedTagsCell(snapshot, currentPath, idea)}</td>
81
124
  <td>${renderIdeaSummaryHtml(snapshot, currentPath, idea, '—')}</td>
82
125
  <td>${idea.references.inbound.length}/${idea.references.outbound.length}</td>
83
126
  </tr>
@@ -104,12 +147,12 @@ export function renderIdeasIndexPage(snapshot) {
104
147
  <table>
105
148
  <thead>
106
149
  <tr>
107
- <th>Name</th>
108
- <th>Path</th>
109
- <th>Status</th>
110
- <th>Tags</th>
111
- <th>Summary</th>
112
- <th>Refs in/out</th>
150
+ <th data-filter-key="name">Name</th>
151
+ <th data-filter-key="path">Path</th>
152
+ <th data-filter-key="status">Status</th>
153
+ <th data-filter-key="tags">Tags</th>
154
+ <th data-filter-key="summary">Summary</th>
155
+ <th data-filter-key="refs">Refs in/out</th>
113
156
  </tr>
114
157
  </thead>
115
158
  <tbody>${rows}</tbody>
@@ -126,7 +169,10 @@ export function renderFilesIndexPage(snapshot) {
126
169
  <td>${escapeHtml(file.directory || '.')}</td>
127
170
  <td>${file.ideas.length}</td>
128
171
  <td>${file.edgeCount}</td>
129
- <td>${escapeHtml(Object.keys(file.tags).join(', ') || '—')}</td>
172
+ <td>${Object.keys(file.tags).map(tag => {
173
+ const href = statusOrTagFacetHref(snapshot, currentPath, 'tag', tag);
174
+ return `<a href="${escapeHtml(href)}">${escapeHtml(tag)}</a>`;
175
+ }).join(', ') || '—'}</td>
130
176
  </tr>
131
177
  `).join('');
132
178
  return renderShell({
@@ -150,7 +196,13 @@ export function renderFilesIndexPage(snapshot) {
150
196
  </div>
151
197
  <table>
152
198
  <thead>
153
- <tr><th>File</th><th>Directory</th><th>Ideas</th><th>References</th><th>Tags</th></tr>
199
+ <tr>
200
+ <th data-filter-key="file">File</th>
201
+ <th data-filter-key="directory">Directory</th>
202
+ <th data-filter-key="ideas">Ideas</th>
203
+ <th data-filter-key="references">References</th>
204
+ <th data-filter-key="tags">Tags</th>
205
+ </tr>
154
206
  </thead>
155
207
  <tbody>${rows}</tbody>
156
208
  </table>
@@ -189,7 +241,12 @@ export function renderCodeFilesIndexPage(snapshot) {
189
241
  </div>
190
242
  <table>
191
243
  <thead>
192
- <tr><th>File</th><th>Directory</th><th>Referenced by</th><th>Labels</th></tr>
244
+ <tr>
245
+ <th data-filter-key="file">File</th>
246
+ <th data-filter-key="directory">Directory</th>
247
+ <th data-filter-key="referenced-by">Referenced by</th>
248
+ <th data-filter-key="labels">Labels</th>
249
+ </tr>
193
250
  </thead>
194
251
  <tbody>${rows || '<tr><td colspan="4" class="subtle">No outbound code file references.</td></tr>'}</tbody>
195
252
  </table>
@@ -202,7 +259,7 @@ export function renderClustersIndexPage(snapshot) {
202
259
  const rows = snapshot.clusters.map(cluster => `
203
260
  <tr data-filter-row="clusters" data-filter-text="${escapeHtml([cluster.label, cluster.kind, cluster.description].join(' '))}">
204
261
  <td><strong>${snapshot.pageOptions.includeClusterPages ? `<a href="${escapeHtml(exportPageHref(snapshot, currentPath, cluster.page))}">${escapeHtml(cluster.label)}</a>` : escapeHtml(cluster.label)}</strong></td>
205
- <td>${escapeHtml(cluster.kind)}</td>
262
+ <td data-sort-value="${escapeHtml(cluster.kind)}"><a href="${escapeHtml(listFilterHref(snapshot, currentPath, snapshot.manifest.clustersIndex, { kind: cluster.kind }))}">${escapeHtml(cluster.kind)}</a></td>
206
263
  <td>${cluster.counts.ideas}</td>
207
264
  <td>${cluster.counts.files}</td>
208
265
  <td>${escapeHtml(cluster.description)}</td>
@@ -229,7 +286,13 @@ export function renderClustersIndexPage(snapshot) {
229
286
  </div>
230
287
  <table>
231
288
  <thead>
232
- <tr><th>Cluster</th><th>Kind</th><th>Ideas</th><th>Files</th><th>Description</th></tr>
289
+ <tr>
290
+ <th data-filter-key="cluster">Cluster</th>
291
+ <th data-filter-key="kind">Kind</th>
292
+ <th data-filter-key="ideas">Ideas</th>
293
+ <th data-filter-key="files">Files</th>
294
+ <th data-filter-key="description">Description</th>
295
+ </tr>
233
296
  </thead>
234
297
  <tbody>${rows}</tbody>
235
298
  </table>
@@ -277,7 +340,7 @@ export function renderAttributesIndexPage(snapshot) {
277
340
  </div>
278
341
  <table>
279
342
  <thead>
280
- <tr><th>Attribute</th><th>Ideas</th><th>Values</th><th>Value summary</th></tr>
343
+ <tr><th data-filter-key="attribute">Attribute</th><th data-filter-key="ideas">Ideas</th><th data-filter-key="values">Values</th><th data-filter-key="value-summary">Value summary</th></tr>
281
344
  </thead>
282
345
  <tbody>${rows || '<tr><td colspan="4" class="subtle">No attributes declared.</td></tr>'}</tbody>
283
346
  </table>
@@ -295,9 +358,10 @@ export function renderAttributeDetailPage(snapshot, attribute) {
295
358
  const percent = Math.round((entry.count / totalIdeas) * 1000) / 10;
296
359
  const width = Math.max(entry.count > 0 ? 1.5 : 0, Math.min(100, (entry.count / totalIdeas) * 100));
297
360
  const filterText = [entry.value, String(entry.count), `${percent}%`, ...entry.ideaIds].join(' ');
361
+ const valueHref = listFilterHref(snapshot, currentPath, attribute.page, { value: entry.value });
298
362
  return `
299
363
  <tr data-filter-row="attributeValues" data-filter-text="${escapeHtml(filterText)}">
300
- <td>${escapeHtml(entry.value)}</td>
364
+ <td data-sort-value="${escapeHtml(entry.value)}"><a href="${escapeHtml(valueHref)}">${escapeHtml(entry.value)}</a></td>
301
365
  <td>
302
366
  <div class="distribution-track" title="${escapeHtml(`${entry.count} of ${attribute.ideaCount} ideas (${percent}%)`)}" role="img" aria-label="${escapeHtml(`${entry.value}: ${entry.count} ideas, ${percent}%`)}">
303
367
  <span class="distribution-fill" style="width: ${width}%"></span>
@@ -313,9 +377,9 @@ export function renderAttributeDetailPage(snapshot, attribute) {
313
377
  return `
314
378
  <tr data-filter-row="attributeIdeas" data-filter-text="${escapeHtml(filterText)}">
315
379
  <td>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</td>
316
- <td>${escapeHtml(value)}</td>
317
- <td>${renderOptionalStatusCell(idea)}</td>
318
- <td>${renderOptionalTagsCell(idea)}</td>
380
+ <td data-sort-value="${escapeHtml(value)}"><a href="${escapeHtml(listFilterHref(snapshot, currentPath, attribute.page, { value }))}">${escapeHtml(value)}</a></td>
381
+ <td>${renderLinkedStatusCell(snapshot, currentPath, idea)}</td>
382
+ <td>${renderLinkedTagsCell(snapshot, currentPath, idea)}</td>
319
383
  <td>${escapeHtml(idea.summary)}</td>
320
384
  </tr>`;
321
385
  }).join('');
@@ -348,7 +412,7 @@ export function renderAttributeDetailPage(snapshot, attribute) {
348
412
  </div>
349
413
  <div class="scroll-window">
350
414
  <table>
351
- <thead><tr><th>Value</th><th>Distribution</th><th>Ideas</th><th>Share</th></tr></thead>
415
+ <thead><tr><th data-filter-key="value">Value</th><th data-filter-key="distribution">Distribution</th><th data-filter-key="ideas">Ideas</th><th data-filter-key="share">Share</th></tr></thead>
352
416
  <tbody>${valueRows || '<tr><td colspan="4" class="subtle">No values.</td></tr>'}</tbody>
353
417
  </table>
354
418
  </div>
@@ -361,7 +425,7 @@ export function renderAttributeDetailPage(snapshot, attribute) {
361
425
  </div>
362
426
  </div>
363
427
  <table>
364
- <thead><tr><th>Idea</th><th>Value</th><th>Status</th><th>Tags</th><th>Summary</th></tr></thead>
428
+ <thead><tr><th data-filter-key="idea">Idea</th><th data-filter-key="value">Value</th><th data-filter-key="status">Status</th><th data-filter-key="tags">Tags</th><th data-filter-key="summary">Summary</th></tr></thead>
365
429
  <tbody>${ideaRows || '<tr><td colspan="5" class="subtle">No ideas declare this attribute.</td></tr>'}</tbody>
366
430
  </table>
367
431
  </section>
@@ -371,8 +435,8 @@ export function renderAttributeDetailPage(snapshot, attribute) {
371
435
  export function renderGraphPage(snapshot) {
372
436
  const currentPath = snapshot.manifest.graph.path;
373
437
  const graph = enrichGraphUrls(snapshot, snapshot.graphs.workspace, currentPath);
374
- const statuses = Object.keys(snapshot.byStatus).sort();
375
- const tags = Object.keys(snapshot.byTag).sort();
438
+ const statuses = snapshot.byStatus;
439
+ const tags = snapshot.byTag;
376
440
  const clusters = snapshot.clusters.slice(0, 16).map(cluster => snapshot.pageOptions.includeClusterPages
377
441
  ? `<a class="pill" href="${escapeHtml(exportPageHref(snapshot, currentPath, cluster.page))}">${escapeHtml(cluster.label)}</a>`
378
442
  : `<span class="pill">${escapeHtml(cluster.label)}</span>`).join('');
@@ -398,23 +462,24 @@ export function renderGraphPage(snapshot) {
398
462
  <div class="graph-controls-bar" data-graph-controls="graph-data">
399
463
  <input class="searchbar graph-filter" type="search" placeholder="Search graph nodes" data-graph-search />
400
464
  <input class="searchbar graph-filter" type="search" placeholder="Path filter" data-graph-path />
401
- <select class="graph-filter" data-graph-status>
402
- <option value="">All statuses</option>
403
- ${statuses.map(status => `<option value="${escapeHtml(status)}">${escapeHtml(status)}</option>`).join('')}
404
- </select>
405
- <select class="graph-filter" data-graph-tag>
406
- <option value="">All tags</option>
407
- ${tags.map(tag => `<option value="${escapeHtml(tag)}">${escapeHtml(tag)}</option>`).join('')}
408
- </select>
465
+ ${renderMultiFilterSelect('status', statuses, 'Filter by status')}
466
+ ${renderMultiFilterSelect('tag', tags, 'Filter by tag')}
409
467
  <button type="button" class="graph-action" data-graph-toggle-external="true">Hide external</button>
410
468
  <button type="button" class="graph-action" data-graph-toggle-ideasets="true">Hide ideasets</button>
469
+ <button type="button" class="graph-action is-active" data-graph-toggle-wildcard aria-pressed="true" title="Show edges expanded from path+idea wildcard references">Wildcard refs</button>
470
+ <button type="button" class="graph-action is-active" data-graph-toggle-labels data-label-mode="auto" aria-pressed="mixed">Labels: auto</button>
411
471
  <button type="button" class="graph-action" data-graph-toggle-physics aria-pressed="false">Live physics</button>
412
472
  <button type="button" class="graph-action" data-graph-fit>Fit</button>
413
473
  <button type="button" class="graph-action" data-graph-reset>Reset</button>
414
474
  <span class="graph-status" data-graph-status-text></span>
415
475
  </div>
416
476
  <div class="pill-row">${clusters}</div>
417
- <div class="graph-root" data-graph-json="graph-data"></div>
477
+ <div class="graph-root is-booting" data-graph-json="graph-data">
478
+ <div class="graph-boot" data-graph-boot role="status" aria-live="polite" aria-busy="true">
479
+ <span class="graph-boot-spinner" aria-hidden="true"></span>
480
+ <p>Initialising graph…</p>
481
+ </div>
482
+ </div>
418
483
  <script id="graph-data" type="application/json">${stringifyJson(graph)}</script>
419
484
  </section>
420
485
  `
@@ -487,11 +552,17 @@ export function renderIdeaDetailPage(snapshot, idea) {
487
552
  <section class="graph-shell print-break-avoid" id="graph">
488
553
  <div class="toolbar"><h2>Local graph</h2></div>
489
554
  <div class="graph-controls-bar" data-graph-controls="graph-data">
555
+ <button type="button" class="graph-action is-active" data-graph-toggle-labels data-label-mode="auto" aria-pressed="mixed">Labels: auto</button>
490
556
  <button type="button" class="graph-action" data-graph-toggle-physics aria-pressed="false">Live physics</button>
491
557
  <button type="button" class="graph-action" data-graph-fit>Fit</button>
492
558
  <span class="graph-status" data-graph-status-text></span>
493
559
  </div>
494
- <div class="graph-root" data-graph-json="graph-data"></div>
560
+ <div class="graph-root is-booting" data-graph-json="graph-data">
561
+ <div class="graph-boot" data-graph-boot role="status" aria-live="polite" aria-busy="true">
562
+ <span class="graph-boot-spinner" aria-hidden="true"></span>
563
+ <p>Initialising graph…</p>
564
+ </div>
565
+ </div>
495
566
  <script id="graph-data" type="application/json">${stringifyJson(graph)}</script>
496
567
  </section>
497
568
  `
@@ -533,13 +604,13 @@ export function renderFileDetailPage(snapshot, file) {
533
604
  </div>
534
605
  <div class="scroll-window">
535
606
  <table>
536
- <thead><tr><th>Idea</th><th>Status</th><th>Tags</th><th>Summary</th></tr></thead>
607
+ <thead><tr><th data-filter-key="idea">Idea</th><th data-filter-key="status">Status</th><th data-filter-key="tags">Tags</th><th data-filter-key="summary">Summary</th></tr></thead>
537
608
  <tbody>
538
609
  ${file.ideas.map(idea => `
539
610
  <tr data-filter-row="fileIdeas" data-filter-text="${escapeHtml([idea.name, idea.summary, ideaStatus(idea) ?? '', ideaTags(idea).join(' ')].join(' '))}">
540
611
  <td>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</td>
541
- <td>${renderOptionalStatusCell(idea)}</td>
542
- <td>${renderOptionalTagsCell(idea)}</td>
612
+ <td>${renderLinkedStatusCell(snapshot, currentPath, idea)}</td>
613
+ <td>${renderLinkedTagsCell(snapshot, currentPath, idea)}</td>
543
614
  <td>${escapeHtml(idea.summary)}</td>
544
615
  </tr>
545
616
  `).join('')}
@@ -559,11 +630,17 @@ export function renderFileDetailPage(snapshot, file) {
559
630
  <section class="graph-shell print-break-avoid">
560
631
  <div class="toolbar"><h2>Local graph</h2></div>
561
632
  <div class="graph-controls-bar" data-graph-controls="graph-data">
633
+ <button type="button" class="graph-action is-active" data-graph-toggle-labels data-label-mode="auto" aria-pressed="mixed">Labels: auto</button>
562
634
  <button type="button" class="graph-action" data-graph-toggle-physics aria-pressed="false">Live physics</button>
563
635
  <button type="button" class="graph-action" data-graph-fit>Fit</button>
564
636
  <span class="graph-status" data-graph-status-text></span>
565
637
  </div>
566
- <div class="graph-root" data-graph-json="graph-data"></div>
638
+ <div class="graph-root is-booting" data-graph-json="graph-data">
639
+ <div class="graph-boot" data-graph-boot role="status" aria-live="polite" aria-busy="true">
640
+ <span class="graph-boot-spinner" aria-hidden="true"></span>
641
+ <p>Initialising graph…</p>
642
+ </div>
643
+ </div>
567
644
  <script id="graph-data" type="application/json">${stringifyJson(graph)}</script>
568
645
  </section>
569
646
  `
@@ -603,13 +680,13 @@ export function renderCodeFileDetailPage(snapshot, file) {
603
680
  </div>
604
681
  <div class="scroll-window">
605
682
  <table>
606
- <thead><tr><th>Idea</th><th>Status</th><th>Tags</th><th>Summary</th></tr></thead>
683
+ <thead><tr><th data-filter-key="idea">Idea</th><th data-filter-key="status">Status</th><th data-filter-key="tags">Tags</th><th data-filter-key="summary">Summary</th></tr></thead>
607
684
  <tbody>
608
685
  ${referencingIdeas.map(idea => `
609
686
  <tr data-filter-row="codeFileIdeas" data-filter-text="${escapeHtml([idea.name, idea.summary, ideaStatus(idea) ?? '', ideaTags(idea).join(' ')].join(' '))}">
610
687
  <td>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</td>
611
- <td>${renderOptionalStatusCell(idea)}</td>
612
- <td>${renderOptionalTagsCell(idea)}</td>
688
+ <td>${renderLinkedStatusCell(snapshot, currentPath, idea)}</td>
689
+ <td>${renderLinkedTagsCell(snapshot, currentPath, idea)}</td>
613
690
  <td>${escapeHtml(idea.summary)}</td>
614
691
  </tr>
615
692
  `).join('') || '<tr><td colspan="4" class="subtle">No referencing ideas.</td></tr>'}
@@ -662,13 +739,13 @@ export function renderClusterDetailPage(snapshot, cluster) {
662
739
  </div>
663
740
  <div class="scroll-window">
664
741
  <table>
665
- <thead><tr><th>Idea</th><th>File</th><th>Status</th><th>Summary</th></tr></thead>
742
+ <thead><tr><th data-filter-key="idea">Idea</th><th data-filter-key="path">File</th><th data-filter-key="status">Status</th><th data-filter-key="summary">Summary</th></tr></thead>
666
743
  <tbody>
667
744
  ${members.map(idea => `
668
745
  <tr data-filter-row="clusterIdeas" data-filter-text="${escapeHtml([idea.name, idea.fileUri, ideaStatus(idea) ?? '', idea.summary].join(' '))}">
669
746
  <td>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</td>
670
- <td>${escapeHtml(idea.fileUri)}</td>
671
- <td>${renderOptionalStatusCell(idea)}</td>
747
+ <td>${renderFilePathCell(snapshot, currentPath, idea.fileUri)}</td>
748
+ <td>${renderLinkedStatusCell(snapshot, currentPath, idea)}</td>
672
749
  <td>${escapeHtml(idea.summary)}</td>
673
750
  </tr>
674
751
  `).join('')}
@@ -679,11 +756,17 @@ export function renderClusterDetailPage(snapshot, cluster) {
679
756
  <section class="graph-shell print-break-avoid">
680
757
  <div class="toolbar"><h2>Cluster graph</h2></div>
681
758
  <div class="graph-controls-bar" data-graph-controls="graph-data">
759
+ <button type="button" class="graph-action is-active" data-graph-toggle-labels data-label-mode="auto" aria-pressed="mixed">Labels: auto</button>
682
760
  <button type="button" class="graph-action" data-graph-toggle-physics aria-pressed="false">Live physics</button>
683
761
  <button type="button" class="graph-action" data-graph-fit>Fit</button>
684
762
  <span class="graph-status" data-graph-status-text></span>
685
763
  </div>
686
- <div class="graph-root" data-graph-json="graph-data"></div>
764
+ <div class="graph-root is-booting" data-graph-json="graph-data">
765
+ <div class="graph-boot" data-graph-boot role="status" aria-live="polite" aria-busy="true">
766
+ <span class="graph-boot-spinner" aria-hidden="true"></span>
767
+ <p>Initialising graph…</p>
768
+ </div>
769
+ </div>
687
770
  <script id="graph-data" type="application/json">${stringifyJson(graph)}</script>
688
771
  </section>
689
772
  `
@@ -691,15 +774,18 @@ export function renderClusterDetailPage(snapshot, cluster) {
691
774
  }
692
775
  export function renderPrintHomePage(snapshot) {
693
776
  const currentPath = snapshot.manifest.printHome.path;
694
- const cards = snapshot.ideas.map(idea => `
695
- <article class="print-card print-break-avoid">
696
- <h3>${snapshot.pageOptions.includeIdeaPages ? `<a href="${escapeHtml(exportHref(snapshot, currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a>` : escapeHtml(idea.name)}</h3>
777
+ const samePageAnchors = { samePageIdeaAnchors: true };
778
+ const cards = snapshot.ideas.map(idea => {
779
+ const anchorId = exportIdeaAnchorId(idea);
780
+ return `
781
+ <article class="print-card print-break-avoid" id="${escapeHtml(anchorId)}">
782
+ <h3><a href="#${escapeHtml(anchorId)}">${escapeHtml(idea.name)}</a></h3>
697
783
  <p class="subtle">${escapeHtml(idea.fileUri)}</p>
698
- <p>${renderIdeaSummaryHtml(snapshot, currentPath, idea, '—')}</p>
699
- ${renderOptionalStatusParagraph(idea)}
700
- ${renderOptionalTagsParagraph(idea)}
784
+ <p>${renderIdeaSummaryHtml(snapshot, currentPath, idea, '—', samePageAnchors)}</p>
785
+ ${renderPrintIdeaAttributesHtml(snapshot, currentPath, idea, samePageAnchors)}
701
786
  </article>
702
- `).join('');
787
+ `;
788
+ }).join('');
703
789
  return renderShell({
704
790
  currentPath,
705
791
  activeNav: 'print',
@@ -710,6 +796,12 @@ export function renderPrintHomePage(snapshot) {
710
796
  <p class="eyebrow">Printable report</p>
711
797
  <h1>${escapeHtml(snapshot.title)}</h1>
712
798
  <p class="subtle">Generated ${escapeHtml(formatDate(snapshot.generatedAt))}</p>
799
+ <div class="toolbar">
800
+ <p class="subtle">${snapshot.ideas.length} ideas</p>
801
+ <div class="actions">
802
+ <button type="button" class="print-button hide-on-print" onclick="window.print()">Print</button>
803
+ </div>
804
+ </div>
713
805
  </header>
714
806
  <section class="grid">
715
807
  ${renderMetric('Ideas', String(snapshot.counts.ideas))}
@@ -735,13 +827,18 @@ export function renderPrintIdeaPage(snapshot, idea) {
735
827
  body: `
736
828
  <header class="page-header">
737
829
  <p class="eyebrow">Printable idea sheet</p>
738
- <h1>${escapeHtml(idea.name)}</h1>
830
+ <h1 id="${escapeHtml(exportIdeaAnchorId(idea))}">${escapeHtml(idea.name)}</h1>
739
831
  <p class="subtle">${escapeHtml(idea.fileUri)}:${idea.lineStart + 1}</p>
832
+ <div class="toolbar">
833
+ <div></div>
834
+ <div class="actions">
835
+ <button type="button" class="print-button hide-on-print" onclick="window.print()">Print</button>
836
+ </div>
837
+ </div>
740
838
  </header>
741
839
  <section class="print-card print-break-avoid">
742
840
  <p class="idea-summary">${renderIdeaSummaryHtml(snapshot, currentPath, idea)}</p>
743
- ${renderOptionalStatusParagraph(idea)}
744
- ${renderOptionalTagsParagraph(idea)}
841
+ ${renderPrintIdeaAttributesHtml(snapshot, currentPath, idea)}
745
842
  ${snapshot.pageOptions.includeIdeaPages ? `<p><strong>Interactive page:</strong> <a href="${escapeHtml(exportHref(snapshot, currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a></p>` : ''}
746
843
  </section>
747
844
  `
@@ -915,6 +1012,8 @@ function enrichGraphUrls(snapshot, graph, _currentPath) {
915
1012
  else {
916
1013
  delete node.status;
917
1014
  }
1015
+ node.statusKey = idea.statusKey;
1016
+ node.tagsKeys = idea.tagsKeys;
918
1017
  node.attributes = idea.attributes;
919
1018
  const attributeKeys = Object.keys(idea.attributes).sort();
920
1019
  if (attributeKeys.length > 0) {