@majeanson/lac 3.5.0 → 3.5.2

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/dist/index.mjs CHANGED
@@ -135,7 +135,7 @@ function mergeDefs$1(...defs) {
135
135
  for (const def of defs) Object.assign(mergedDescriptors, Object.getOwnPropertyDescriptors(def));
136
136
  return Object.defineProperties({}, mergedDescriptors);
137
137
  }
138
- function esc$14(str) {
138
+ function esc$18(str) {
139
139
  return JSON.stringify(str);
140
140
  }
141
141
  function slugify$1(input) {
@@ -1539,7 +1539,7 @@ const $ZodObjectJIT$1 = /* @__PURE__ */ $constructor$1("$ZodObjectJIT", (inst, d
1539
1539
  ]);
1540
1540
  const normalized = _normalized.value;
1541
1541
  const parseStr = (key) => {
1542
- const k = esc$14(key);
1542
+ const k = esc$18(key);
1543
1543
  return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;
1544
1544
  };
1545
1545
  doc.write(`const input = payload.value;`);
@@ -1549,7 +1549,7 @@ const $ZodObjectJIT$1 = /* @__PURE__ */ $constructor$1("$ZodObjectJIT", (inst, d
1549
1549
  doc.write(`const newResult = {};`);
1550
1550
  for (const key of normalized.keys) {
1551
1551
  const id = ids[key];
1552
- const k = esc$14(key);
1552
+ const k = esc$18(key);
1553
1553
  const isOptionalOut = shape[key]?._zod?.optout === "optional";
1554
1554
  doc.write(`const ${id} = ${parseStr(key)};`);
1555
1555
  if (isOptionalOut) doc.write(`
@@ -4115,7 +4115,7 @@ function mergeDefs(...defs) {
4115
4115
  for (const def of defs) Object.assign(mergedDescriptors, Object.getOwnPropertyDescriptors(def));
4116
4116
  return Object.defineProperties({}, mergedDescriptors);
4117
4117
  }
4118
- function esc$13(str) {
4118
+ function esc$17(str) {
4119
4119
  return JSON.stringify(str);
4120
4120
  }
4121
4121
  function slugify(input) {
@@ -5505,7 +5505,7 @@ const $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def)
5505
5505
  ]);
5506
5506
  const normalized = _normalized.value;
5507
5507
  const parseStr = (key) => {
5508
- const k = esc$13(key);
5508
+ const k = esc$17(key);
5509
5509
  return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;
5510
5510
  };
5511
5511
  doc.write(`const input = payload.value;`);
@@ -5515,7 +5515,7 @@ const $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def)
5515
5515
  doc.write(`const newResult = {};`);
5516
5516
  for (const key of normalized.keys) {
5517
5517
  const id = ids[key];
5518
- const k = esc$13(key);
5518
+ const k = esc$17(key);
5519
5519
  const isOptionalOut = shape[key]?._zod?.optout === "optional";
5520
5520
  doc.write(`const ${id} = ${parseStr(key)};`);
5521
5521
  if (isOptionalOut) doc.write(`
@@ -10470,10 +10470,10 @@ const doctorCommand = new Command("doctor").description("Check workspace health
10470
10470
  });
10471
10471
  //#endregion
10472
10472
  //#region src/lib/htmlGenerator.ts
10473
- function esc$12(s) {
10473
+ function esc$16(s) {
10474
10474
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
10475
10475
  }
10476
- function generateHtmlWiki(features, projectName, viewLabel, viewName) {
10476
+ function generateHtmlWiki(features, projectName, viewLabel, viewName, renderMode) {
10477
10477
  const dataJson = JSON.stringify(features).replace(/<\/script>/gi, "<\\/script>");
10478
10478
  features.filter((f) => f.status === "active").length, features.filter((f) => f.status === "frozen").length, features.filter((f) => f.status === "draft").length, features.filter((f) => f.status === "deprecated").length;
10479
10479
  const domains = [...new Set(features.map((f) => f.domain).filter(Boolean))].sort();
@@ -10482,7 +10482,7 @@ function generateHtmlWiki(features, projectName, viewLabel, viewName) {
10482
10482
  <head>
10483
10483
  <meta charset="UTF-8">
10484
10484
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
10485
- <title>${esc$12(projectName)}${viewLabel ? ` · ${esc$12(viewLabel)}` : ""} — LAC Wiki</title>
10485
+ <title>${esc$16(projectName)}${viewLabel ? ` · ${esc$16(viewLabel)}` : ""} — LAC Wiki</title>
10486
10486
  <style>
10487
10487
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
10488
10488
 
@@ -11003,7 +11003,7 @@ body { background: var(--bg); color: var(--text); font-family: var(--sans); font
11003
11003
  <div class="topbar">
11004
11004
  <span class="topbar-logo">◈ lac</span>
11005
11005
  <span class="topbar-sep">|</span>
11006
- <span class="topbar-project">${esc$12(projectName)}${viewLabel ? ` <span style="opacity:.55;font-weight:400">· ${esc$12(viewLabel)} view</span>` : ""}</span>
11006
+ <span class="topbar-project">${esc$16(projectName)}${viewLabel ? ` <span style="opacity:.55;font-weight:400">· ${esc$16(viewLabel)} view</span>` : ""}</span>
11007
11007
  <span class="topbar-count">${features.length} features · ${domains.length} domains</span>
11008
11008
  </div>
11009
11009
  <div class="body-row">
@@ -11082,7 +11082,7 @@ function statusColor(s) {
11082
11082
 
11083
11083
  // ── Tree building ────────────────────────────────────────────────────────────
11084
11084
 
11085
- const VIEW = '${viewName || ""}';
11085
+ const VIEW = '${renderMode || viewName || ""}';
11086
11086
  const byKey = new Map(FEATURES.map(f => [f.featureKey, f]));
11087
11087
 
11088
11088
  function getChildren(key) {
@@ -11457,7 +11457,7 @@ window.setSortMode = setSortMode;
11457
11457
  }
11458
11458
  //#endregion
11459
11459
  //#region src/lib/rawHtmlGenerator.ts
11460
- function esc$11(s) {
11460
+ function esc$15(s) {
11461
11461
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
11462
11462
  }
11463
11463
  function generateRawHtml(features, projectName, viewLabel, viewName) {
@@ -11468,7 +11468,7 @@ function generateRawHtml(features, projectName, viewLabel, viewName) {
11468
11468
  <head>
11469
11469
  <meta charset="UTF-8">
11470
11470
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
11471
- <title>${esc$11(projectName)}${viewLabel ? ` · ${esc$11(viewLabel)}` : ""} — LAC Raw</title>
11471
+ <title>${esc$15(projectName)}${viewLabel ? ` · ${esc$15(viewLabel)}` : ""} — LAC Raw</title>
11472
11472
  <style>
11473
11473
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
11474
11474
 
@@ -12000,7 +12000,7 @@ body { background: var(--bg); color: var(--text); font-family: var(--sans); font
12000
12000
  <div class="topbar">
12001
12001
  <span class="topbar-logo">◈ lac</span>
12002
12002
  <span class="topbar-sep">|</span>
12003
- <span class="topbar-project">${esc$11(projectName)}${viewLabel ? ` <span style="opacity:.55;font-weight:400">· ${esc$11(viewLabel)} view</span>` : ""}</span>
12003
+ <span class="topbar-project">${esc$15(projectName)}${viewLabel ? ` <span style="opacity:.55;font-weight:400">· ${esc$15(viewLabel)} view</span>` : ""}</span>
12004
12004
  <span class="topbar-badge">raw</span>
12005
12005
  <span class="topbar-count">${features.length} features · ${domains.length} domains</span>
12006
12006
  </div>
@@ -12451,7 +12451,7 @@ async function generateSite(features, outDir, viewLabel, viewName) {
12451
12451
  }
12452
12452
  //#endregion
12453
12453
  //#region src/lib/postcardGenerator.ts
12454
- function esc$10(s) {
12454
+ function esc$14(s) {
12455
12455
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
12456
12456
  }
12457
12457
  function renderInline$1(s) {
@@ -12459,7 +12459,7 @@ function renderInline$1(s) {
12459
12459
  }
12460
12460
  /** Minimal markdown → HTML for postcard body text. */
12461
12461
  function md(text) {
12462
- const lines = esc$10(text).split("\n");
12462
+ const lines = esc$14(text).split("\n");
12463
12463
  const result = [];
12464
12464
  let inList = false;
12465
12465
  for (const line of lines) {
@@ -12521,14 +12521,14 @@ function generatePostcard(feature, projectName) {
12521
12521
  <li class="decision-item">
12522
12522
  <div class="decision-header">
12523
12523
  <span class="decision-index">${i + 1}</span>
12524
- <span class="decision-text">${esc$10(d.decision)}</span>
12525
- ${d.date ? `<span class="decision-date">${esc$10(d.date)}</span>` : ""}
12524
+ <span class="decision-text">${esc$14(d.decision)}</span>
12525
+ ${d.date ? `<span class="decision-date">${esc$14(d.date)}</span>` : ""}
12526
12526
  </div>
12527
- <div class="decision-rationale">${esc$10(d.rationale)}</div>
12527
+ <div class="decision-rationale">${esc$14(d.rationale)}</div>
12528
12528
  ${d.alternativesConsidered && d.alternativesConsidered.length > 0 ? `
12529
12529
  <div class="decision-alts">
12530
12530
  <span class="alts-label">Alternatives considered:</span>
12531
- ${d.alternativesConsidered.map((a) => `<span class="alt-pill">${esc$10(a)}</span>`).join("")}
12531
+ ${d.alternativesConsidered.map((a) => `<span class="alt-pill">${esc$14(a)}</span>`).join("")}
12532
12532
  </div>
12533
12533
  ` : ""}
12534
12534
  </li>
@@ -12544,12 +12544,12 @@ function generatePostcard(feature, projectName) {
12544
12544
  <section class="section">
12545
12545
  <h2 class="section-label">Known Limitations</h2>
12546
12546
  <ul class="limitations-list">
12547
- ${feature.knownLimitations.map((l) => `<li>${esc$10(l)}</li>`).join("")}
12547
+ ${feature.knownLimitations.map((l) => `<li>${esc$14(l)}</li>`).join("")}
12548
12548
  </ul>
12549
12549
  </section>` : "";
12550
12550
  const tagsHtml = feature.tags && feature.tags.length > 0 ? `
12551
12551
  <div class="tags-row">
12552
- ${feature.tags.map((t) => `<span class="tag">${esc$10(t)}</span>`).join("")}
12552
+ ${feature.tags.map((t) => `<span class="tag">${esc$14(t)}</span>`).join("")}
12553
12553
  </div>` : "";
12554
12554
  const priorityHtml = feature.priority != null ? `<span class="meta-pill priority-pill">P${feature.priority}</span>` : "";
12555
12555
  return `<!DOCTYPE html>
@@ -12557,7 +12557,7 @@ function generatePostcard(feature, projectName) {
12557
12557
  <head>
12558
12558
  <meta charset="UTF-8">
12559
12559
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
12560
- <title>${esc$10(feature.title)} — ${esc$10(projectName)}</title>
12560
+ <title>${esc$14(feature.title)} — ${esc$14(projectName)}</title>
12561
12561
  <style>
12562
12562
  :root {
12563
12563
  --bg: #12100e;
@@ -12915,7 +12915,7 @@ function generatePostcard(feature, projectName) {
12915
12915
  <header class="topbar">
12916
12916
  <span class="topbar-logo">◈ lac</span>
12917
12917
  <span class="topbar-project">
12918
- <strong>${esc$10(projectName)}</strong> / ${esc$10(feature.featureKey)}
12918
+ <strong>${esc$14(projectName)}</strong> / ${esc$14(feature.featureKey)}
12919
12919
  </span>
12920
12920
  <span class="topbar-date">${today}</span>
12921
12921
  </header>
@@ -12923,12 +12923,12 @@ function generatePostcard(feature, projectName) {
12923
12923
  <!-- Hero -->
12924
12924
  <div class="hero">
12925
12925
  <div class="badges">
12926
- <span class="badge-status">${esc$10(statusLabel)}</span>
12927
- ${feature.domain ? `<span class="badge-domain">${esc$10(feature.domain)}</span>` : ""}
12926
+ <span class="badge-status">${esc$14(statusLabel)}</span>
12927
+ ${feature.domain ? `<span class="badge-domain">${esc$14(feature.domain)}</span>` : ""}
12928
12928
  ${priorityHtml}
12929
- ${feature.owner ? `<span class="meta-pill">${esc$10(feature.owner)}</span>` : ""}
12929
+ ${feature.owner ? `<span class="meta-pill">${esc$14(feature.owner)}</span>` : ""}
12930
12930
  </div>
12931
- <h1 class="feature-title">${esc$10(feature.title)}</h1>
12931
+ <h1 class="feature-title">${esc$14(feature.title)}</h1>
12932
12932
  </div>
12933
12933
 
12934
12934
  <!-- Body -->
@@ -12951,7 +12951,7 @@ function generatePostcard(feature, projectName) {
12951
12951
  <footer class="postcard-footer">
12952
12952
  <span class="footer-brand">generated via life-as-code</span>
12953
12953
  <span class="footer-sep">·</span>
12954
- <span class="footer-key">${esc$10(feature.featureKey)}</span>
12954
+ <span class="footer-key">${esc$14(feature.featureKey)}</span>
12955
12955
  </footer>
12956
12956
 
12957
12957
  </article>
@@ -12961,7 +12961,7 @@ function generatePostcard(feature, projectName) {
12961
12961
  }
12962
12962
  //#endregion
12963
12963
  //#region src/lib/printGenerator.ts
12964
- function esc$9(s) {
12964
+ function esc$13(s) {
12965
12965
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
12966
12966
  }
12967
12967
  /**
@@ -12977,11 +12977,11 @@ function mdToHtml(text) {
12977
12977
  while (i < lines.length) {
12978
12978
  const line = lines[i] ?? "";
12979
12979
  if (line.startsWith("```")) {
12980
- const lang = esc$9(line.slice(3).trim());
12980
+ const lang = esc$13(line.slice(3).trim());
12981
12981
  const codeLines = [];
12982
12982
  i++;
12983
12983
  while (i < lines.length && !(lines[i] ?? "").startsWith("```")) {
12984
- codeLines.push(esc$9(lines[i] ?? ""));
12984
+ codeLines.push(esc$13(lines[i] ?? ""));
12985
12985
  i++;
12986
12986
  }
12987
12987
  if (i < lines.length) i++;
@@ -12990,24 +12990,24 @@ function mdToHtml(text) {
12990
12990
  continue;
12991
12991
  }
12992
12992
  if (line.startsWith("### ")) {
12993
- out.push(`<h3>${renderInline(esc$9(line.slice(4)))}</h3>`);
12993
+ out.push(`<h3>${renderInline(esc$13(line.slice(4)))}</h3>`);
12994
12994
  i++;
12995
12995
  continue;
12996
12996
  }
12997
12997
  if (line.startsWith("## ")) {
12998
- out.push(`<h2>${renderInline(esc$9(line.slice(3)))}</h2>`);
12998
+ out.push(`<h2>${renderInline(esc$13(line.slice(3)))}</h2>`);
12999
12999
  i++;
13000
13000
  continue;
13001
13001
  }
13002
13002
  if (line.startsWith("# ")) {
13003
- out.push(`<h1>${renderInline(esc$9(line.slice(2)))}</h1>`);
13003
+ out.push(`<h1>${renderInline(esc$13(line.slice(2)))}</h1>`);
13004
13004
  i++;
13005
13005
  continue;
13006
13006
  }
13007
13007
  if (/^[-*] /.test(line)) {
13008
13008
  const items = [];
13009
13009
  while (i < lines.length && /^[-*] /.test(lines[i] ?? "")) {
13010
- items.push(`<li>${renderInline(esc$9((lines[i] ?? "").slice(2)))}</li>`);
13010
+ items.push(`<li>${renderInline(esc$13((lines[i] ?? "").slice(2)))}</li>`);
13011
13011
  i++;
13012
13012
  }
13013
13013
  out.push(`<ul>${items.join("")}</ul>`);
@@ -13016,7 +13016,7 @@ function mdToHtml(text) {
13016
13016
  if (/^[1-9]\d*\. /.test(line)) {
13017
13017
  const items = [];
13018
13018
  while (i < lines.length && /^[1-9]\d*\. /.test(lines[i] ?? "")) {
13019
- items.push(`<li>${renderInline(esc$9((lines[i] ?? "").replace(/^[1-9]\d*\. /, "")))}</li>`);
13019
+ items.push(`<li>${renderInline(esc$13((lines[i] ?? "").replace(/^[1-9]\d*\. /, "")))}</li>`);
13020
13020
  i++;
13021
13021
  }
13022
13022
  out.push(`<ol>${items.join("")}</ol>`);
@@ -13031,7 +13031,7 @@ function mdToHtml(text) {
13031
13031
  paraLines.push(lines[i] ?? "");
13032
13032
  i++;
13033
13033
  }
13034
- if (paraLines.length > 0) out.push(`<p>${renderInline(esc$9(paraLines.join(" ")))}</p>`);
13034
+ if (paraLines.length > 0) out.push(`<p>${renderInline(esc$13(paraLines.join(" ")))}</p>`);
13035
13035
  }
13036
13036
  return out.join("\n");
13037
13037
  }
@@ -13491,8 +13491,8 @@ function buildCoverPage(features, projectName, viewLabel, today) {
13491
13491
  <hr class="page-rule">
13492
13492
  <div class="cover">
13493
13493
  <div class="cover-eyebrow">◈ life-as-code · Feature Documentation</div>
13494
- <div class="cover-title">${esc$9(projectName)}</div>
13495
- <div class="cover-subtitle">${esc$9(viewLabel)}</div>
13494
+ <div class="cover-title">${esc$13(projectName)}</div>
13495
+ <div class="cover-subtitle">${esc$13(viewLabel)}</div>
13496
13496
  <div class="cover-meta">
13497
13497
  <div class="cover-meta-item">
13498
13498
  <strong>Generated</strong>
@@ -13514,7 +13514,7 @@ function buildCoverPage(features, projectName, viewLabel, today) {
13514
13514
  </div>
13515
13515
  </div>
13516
13516
  <div class="page-footer">
13517
- <span class="footer-left">${esc$9(projectName)}</span>
13517
+ <span class="footer-left">${esc$13(projectName)}</span>
13518
13518
  <span class="footer-right">${today}</span>
13519
13519
  </div>
13520
13520
  </div>`;
@@ -13531,14 +13531,14 @@ function buildTocPage(features, projectName, today) {
13531
13531
  return `
13532
13532
  <li class="toc-item">
13533
13533
  <span class="toc-num">${idx + 1}.</span>
13534
- <span class="toc-key">${esc$9(f.featureKey)}</span>
13535
- <span class="toc-feature-title">${esc$9(f.title)}</span>
13536
- <span class="toc-badge" style="color:${color};background:${color}1a">${esc$9(label)}</span>
13534
+ <span class="toc-key">${esc$13(f.featureKey)}</span>
13535
+ <span class="toc-feature-title">${esc$13(f.title)}</span>
13536
+ <span class="toc-badge" style="color:${color};background:${color}1a">${esc$13(label)}</span>
13537
13537
  </li>`;
13538
13538
  }).join("")}
13539
13539
  </ol>
13540
13540
  <div class="page-footer">
13541
- <span class="footer-left">${esc$9(projectName)}</span>
13541
+ <span class="footer-left">${esc$13(projectName)}</span>
13542
13542
  <span class="footer-right">${today}</span>
13543
13543
  </div>
13544
13544
  </div>`;
@@ -13554,11 +13554,11 @@ function buildFeaturePage(feature, projectName, today) {
13554
13554
  <li class="decision-item">
13555
13555
  <div class="decision-header">
13556
13556
  <span class="decision-num">${i + 1}.</span>
13557
- <span class="decision-text">${esc$9(d.decision)}</span>
13558
- ${d.date ? `<span class="decision-date">${esc$9(d.date)}</span>` : ""}
13557
+ <span class="decision-text">${esc$13(d.decision)}</span>
13558
+ ${d.date ? `<span class="decision-date">${esc$13(d.date)}</span>` : ""}
13559
13559
  </div>
13560
- <div class="decision-rationale">${esc$9(d.rationale)}</div>
13561
- ${d.alternativesConsidered && d.alternativesConsidered.length > 0 ? `<div class="decision-alts">Alternatives considered: ${d.alternativesConsidered.map((a) => esc$9(a)).join(", ")}</div>` : ""}
13560
+ <div class="decision-rationale">${esc$13(d.rationale)}</div>
13561
+ ${d.alternativesConsidered && d.alternativesConsidered.length > 0 ? `<div class="decision-alts">Alternatives considered: ${d.alternativesConsidered.map((a) => esc$13(a)).join(", ")}</div>` : ""}
13562
13562
  </li>`).join("")}
13563
13563
  </ol>
13564
13564
  </div>` : "";
@@ -13581,26 +13581,26 @@ function buildFeaturePage(feature, projectName, today) {
13581
13581
  <div class="section">
13582
13582
  <div class="section-label">Known Limitations</div>
13583
13583
  <ul class="limitations-list">
13584
- ${feature.knownLimitations.map((l) => `<li>${esc$9(l)}</li>`).join("")}
13584
+ ${feature.knownLimitations.map((l) => `<li>${esc$13(l)}</li>`).join("")}
13585
13585
  </ul>
13586
13586
  </div>` : "";
13587
13587
  const tagsHtml = feature.tags && feature.tags.length > 0 ? `
13588
13588
  <div class="section">
13589
13589
  <div class="section-label">Tags</div>
13590
13590
  <div class="tags-row">
13591
- ${feature.tags.map((t) => `<span class="tag">${esc$9(t)}</span>`).join("")}
13591
+ ${feature.tags.map((t) => `<span class="tag">${esc$13(t)}</span>`).join("")}
13592
13592
  </div>
13593
13593
  </div>` : "";
13594
13594
  return `
13595
13595
  <div class="page">
13596
13596
  <hr class="page-rule">
13597
13597
  <div class="feature-eyebrow">
13598
- <span class="feature-key">${esc$9(feature.featureKey)}</span>
13599
- <span class="feature-badge" style="color:${color};background:${color}1a">${esc$9(label)}</span>
13600
- ${feature.domain ? `<span class="feature-domain">${esc$9(feature.domain)}</span>` : ""}
13598
+ <span class="feature-key">${esc$13(feature.featureKey)}</span>
13599
+ <span class="feature-badge" style="color:${color};background:${color}1a">${esc$13(label)}</span>
13600
+ ${feature.domain ? `<span class="feature-domain">${esc$13(feature.domain)}</span>` : ""}
13601
13601
  ${feature.priority != null ? `<span class="feature-domain">P${feature.priority}</span>` : ""}
13602
13602
  </div>
13603
- <h1 class="feature-h1">${esc$9(feature.title)}</h1>
13603
+ <h1 class="feature-h1">${esc$13(feature.title)}</h1>
13604
13604
 
13605
13605
  <div class="section">
13606
13606
  <div class="section-label">Problem</div>
@@ -13615,7 +13615,7 @@ function buildFeaturePage(feature, projectName, today) {
13615
13615
  ${tagsHtml}
13616
13616
 
13617
13617
  <div class="page-footer">
13618
- <span class="footer-left">${esc$9(projectName)}</span>
13618
+ <span class="footer-left">${esc$13(projectName)}</span>
13619
13619
  <span class="footer-right">${today}</span>
13620
13620
  </div>
13621
13621
  </div>`;
@@ -13637,7 +13637,7 @@ function generatePrint(features, projectName, viewLabel) {
13637
13637
  <head>
13638
13638
  <meta charset="UTF-8">
13639
13639
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
13640
- <title>${esc$9(projectName)} — ${esc$9(resolvedLabel)}</title>
13640
+ <title>${esc$13(projectName)} — ${esc$13(resolvedLabel)}</title>
13641
13641
  <style>
13642
13642
  ${buildCss(today)}
13643
13643
  </style>
@@ -13653,7 +13653,7 @@ ${featurePages}
13653
13653
  }
13654
13654
  //#endregion
13655
13655
  //#region src/lib/resumeGenerator.ts
13656
- function esc$8(s) {
13656
+ function esc$12(s) {
13657
13657
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
13658
13658
  }
13659
13659
  function today$3() {
@@ -13691,17 +13691,17 @@ function renderStatusBadge() {
13691
13691
  return `<span class="badge badge-frozen">frozen</span>`;
13692
13692
  }
13693
13693
  function renderTagPills(tags) {
13694
- return tags.map((t) => `<span class="pill">${esc$8(t)}</span>`).join("");
13694
+ return tags.map((t) => `<span class="pill">${esc$12(t)}</span>`).join("");
13695
13695
  }
13696
13696
  function renderNpmPills(pkgs) {
13697
- return pkgs.map((p) => `<span class="npm-pill">${esc$8(p)}</span>`).join("");
13697
+ return pkgs.map((p) => `<span class="npm-pill">${esc$12(p)}</span>`).join("");
13698
13698
  }
13699
13699
  function renderDecisions(decisions) {
13700
13700
  return `
13701
13701
  <div class="section-block">
13702
13702
  <span class="section-label">Key decisions:</span>
13703
13703
  <ul class="decision-list">
13704
- ${decisions.slice(0, 3).map((d) => `<li>${esc$8(d.decision)}</li>`).join("\n ")}
13704
+ ${decisions.slice(0, 3).map((d) => `<li>${esc$12(d.decision)}</li>`).join("\n ")}
13705
13705
  ${decisions.length > 3 ? `<li class="more-decisions">+${decisions.length - 3} more decision${decisions.length - 3 === 1 ? "" : "s"}</li>` : ""}
13706
13706
  </ul>
13707
13707
  </div>`;
@@ -13712,12 +13712,12 @@ function renderFeatureCard(f) {
13712
13712
  const successCriteria = f.successCriteria ? `
13713
13713
  <div class="section-block outcome">
13714
13714
  <span class="section-label accent">Outcome:</span>
13715
- <span class="outcome-text">${esc$8(f.successCriteria)}</span>
13715
+ <span class="outcome-text">${esc$12(f.successCriteria)}</span>
13716
13716
  </div>` : "";
13717
13717
  const spawnReason = f.lineage?.spawnReason ? `
13718
13718
  <div class="section-block spawn-note">
13719
13719
  <span class="section-label">Spawned from:</span>
13720
- <span class="spawn-text">${esc$8(f.lineage.spawnReason)}</span>
13720
+ <span class="spawn-text">${esc$12(f.lineage.spawnReason)}</span>
13721
13721
  </div>` : "";
13722
13722
  const npmPackages = f.npmPackages && f.npmPackages.length > 0 ? `
13723
13723
  <div class="section-block">
@@ -13728,8 +13728,8 @@ function renderFeatureCard(f) {
13728
13728
  <article class="feature-card">
13729
13729
  <header class="card-header">
13730
13730
  <div class="card-title-row">
13731
- <h3 class="card-title">${esc$8(f.title)}</h3>
13732
- <code class="feature-key">${esc$8(f.featureKey)}</code>
13731
+ <h3 class="card-title">${esc$12(f.title)}</h3>
13732
+ <code class="feature-key">${esc$12(f.featureKey)}</code>
13733
13733
  </div>
13734
13734
  <div class="card-meta">
13735
13735
  ${renderStatusBadge()}
@@ -13737,7 +13737,7 @@ function renderFeatureCard(f) {
13737
13737
  </div>
13738
13738
  </header>
13739
13739
 
13740
- <blockquote class="problem-block">${esc$8(f.problem)}</blockquote>
13740
+ <blockquote class="problem-block">${esc$12(f.problem)}</blockquote>
13741
13741
  ${decisions}
13742
13742
  ${successCriteria}
13743
13743
  ${spawnReason}
@@ -13749,7 +13749,7 @@ function renderDomainGroup(domain, features) {
13749
13749
  return `
13750
13750
  <section class="domain-group">
13751
13751
  <h2 class="domain-heading">
13752
- <span class="domain-label">${esc$8(domain.toUpperCase())}</span>
13752
+ <span class="domain-label">${esc$12(domain.toUpperCase())}</span>
13753
13753
  <span class="domain-count">${features.length} feature${features.length === 1 ? "" : "s"}</span>
13754
13754
  </h2>
13755
13755
  ${cards}
@@ -13767,7 +13767,7 @@ function renderEmptyState(projectName) {
13767
13767
  have been met.
13768
13768
  </p>
13769
13769
  <p class="empty-hint">
13770
- Run <code>lac advance ${esc$8(projectName)} --status frozen</code> when a feature ships.
13770
+ Run <code>lac advance ${esc$12(projectName)} --status frozen</code> when a feature ships.
13771
13771
  </p>
13772
13772
  </div>
13773
13773
  </main>`;
@@ -13813,7 +13813,7 @@ function generateResume(features, projectName) {
13813
13813
  <head>
13814
13814
  <meta charset="UTF-8">
13815
13815
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
13816
- <title>${esc$8(projectName)} — Feature Portfolio</title>
13816
+ <title>${esc$12(projectName)} — Feature Portfolio</title>
13817
13817
  <style>
13818
13818
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
13819
13819
 
@@ -14258,7 +14258,7 @@ function generateResume(features, projectName) {
14258
14258
  <header class="page-header">
14259
14259
  <div class="header-eyebrow">feature portfolio</div>
14260
14260
  <h1 class="header-title">
14261
- <span class="header-diamond">◈</span>${esc$8(projectName)}
14261
+ <span class="header-diamond">◈</span>${esc$12(projectName)}
14262
14262
  </h1>
14263
14263
  <p class="header-subtitle">
14264
14264
  ${frozen.length} shipped feature${frozen.length === 1 ? "" : "s"}
@@ -17032,7 +17032,7 @@ const FIELDS = [
17032
17032
  }
17033
17033
  ];
17034
17034
  const TOTAL_FIELDS = FIELDS.length;
17035
- function esc$7(s) {
17035
+ function esc$11(s) {
17036
17036
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
17037
17037
  }
17038
17038
  function isFilled(f, field) {
@@ -17109,7 +17109,7 @@ function renderColumnHeaders() {
17109
17109
  ${FIELDS.map((fd) => `
17110
17110
  <th class="col-header" scope="col">
17111
17111
  <div class="col-label-wrap">
17112
- <span class="col-label">${esc$7(fd.label)}</span>
17112
+ <span class="col-label">${esc$11(fd.label)}</span>
17113
17113
  </div>
17114
17114
  </th>`).join("")}
17115
17115
  <th class="pct-header" scope="col">done</th>
@@ -17137,16 +17137,16 @@ function renderFeatureRow(f, idx) {
17137
17137
  data-feature-idx="${idx}"
17138
17138
  data-col-idx="${colIdx}"
17139
17139
  role="gridcell"
17140
- aria-label="${esc$7(fd.key)}: ${filled ? "filled" : "empty"}"
17140
+ aria-label="${esc$11(fd.key)}: ${filled ? "filled" : "empty"}"
17141
17141
  ><span class="cell-inner"></span></td>`;
17142
17142
  }).join("");
17143
17143
  return `
17144
17144
  <tr class="feature-row" data-feature-idx="${idx}">
17145
17145
  <th class="row-header" scope="row">
17146
17146
  <div class="row-header-inner">
17147
- <span class="row-key">${esc$7(f.featureKey)}</span>
17148
- <span class="row-title">${esc$7(f.title)}</span>
17149
- ${domain ? `<span class="row-domain">${esc$7(domain)}</span>` : ""}
17147
+ <span class="row-key">${esc$11(f.featureKey)}</span>
17148
+ <span class="row-title">${esc$11(f.title)}</span>
17149
+ ${domain ? `<span class="row-domain">${esc$11(domain)}</span>` : ""}
17150
17150
  </div>
17151
17151
  </th>
17152
17152
  ${cells}
@@ -17181,7 +17181,7 @@ function generateHeatmap(features, projectName) {
17181
17181
  <head>
17182
17182
  <meta charset="UTF-8">
17183
17183
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
17184
- <title>${esc$7(projectName)} — LAC Heatmap</title>
17184
+ <title>${esc$11(projectName)} — LAC Heatmap</title>
17185
17185
  <style>
17186
17186
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
17187
17187
 
@@ -17636,7 +17636,7 @@ function generateHeatmap(features, projectName) {
17636
17636
  <span>heatmap</span>
17637
17637
  </div>
17638
17638
  <div class="topbar-right">
17639
- <span class="topbar-project">${esc$7(projectName)}</span>
17639
+ <span class="topbar-project">${esc$11(projectName)}</span>
17640
17640
  <span class="topbar-count">${features.length} feature${features.length === 1 ? "" : "s"}</span>
17641
17641
  </div>
17642
17642
  </div>
@@ -17665,13 +17665,13 @@ function generateHeatmap(features, projectName) {
17665
17665
  <div class="footer-block">
17666
17666
  <div class="footer-block-title">Most missed fields</div>
17667
17667
  <ul class="footer-list">
17668
- ${missed.length > 0 ? missed.map((m) => `<li>${esc$7(m)}</li>`).join("\n ") : "<li>All fields well covered</li>"}
17668
+ ${missed.length > 0 ? missed.map((m) => `<li>${esc$11(m)}</li>`).join("\n ") : "<li>All fields well covered</li>"}
17669
17669
  </ul>
17670
17670
  </div>
17671
17671
  <div class="footer-block">
17672
17672
  <div class="footer-block-title">Most complete features</div>
17673
17673
  <ul class="footer-list">
17674
- ${stars.length > 0 ? stars.map((s) => `<li>${esc$7(s)}</li>`).join("\n ") : "<li>No features yet</li>"}
17674
+ ${stars.length > 0 ? stars.map((s) => `<li>${esc$11(s)}</li>`).join("\n ") : "<li>No features yet</li>"}
17675
17675
  </ul>
17676
17676
  </div>
17677
17677
  </div>
@@ -17770,7 +17770,7 @@ function generateHeatmap(features, projectName) {
17770
17770
  }
17771
17771
  //#endregion
17772
17772
  //#region src/lib/diffGenerator.ts
17773
- function esc$6(s) {
17773
+ function esc$10(s) {
17774
17774
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
17775
17775
  }
17776
17776
  const SCALAR_FIELDS = [
@@ -17828,42 +17828,42 @@ function diffFeatures(a, b) {
17828
17828
  return diffs;
17829
17829
  }
17830
17830
  function statusBadge(status) {
17831
- return `<span class="badge badge-${esc$6(status)}">${esc$6(status)}</span>`;
17831
+ return `<span class="badge badge-${esc$10(status)}">${esc$10(status)}</span>`;
17832
17832
  }
17833
17833
  function renderAdded(f) {
17834
17834
  return `<div class="feature-card added">
17835
17835
  <div class="card-header">
17836
17836
  <span class="card-sign added-sign">+</span>
17837
- <span class="feature-key">${esc$6(f.featureKey)}</span>
17837
+ <span class="feature-key">${esc$10(f.featureKey)}</span>
17838
17838
  ${statusBadge(f.status)}
17839
- ${f.domain ? `<span class="domain-badge">${esc$6(f.domain)}</span>` : ""}
17840
- <span class="feature-title">${esc$6(f.title)}</span>
17839
+ ${f.domain ? `<span class="domain-badge">${esc$10(f.domain)}</span>` : ""}
17840
+ <span class="feature-title">${esc$10(f.title)}</span>
17841
17841
  </div>
17842
- <div class="card-problem">${esc$6(f.problem)}</div>
17843
- ${f.tags && f.tags.length ? `<div class="card-tags">${f.tags.map((t) => `<span class="tag">${esc$6(t)}</span>`).join("")}</div>` : ""}
17842
+ <div class="card-problem">${esc$10(f.problem)}</div>
17843
+ ${f.tags && f.tags.length ? `<div class="card-tags">${f.tags.map((t) => `<span class="tag">${esc$10(t)}</span>`).join("")}</div>` : ""}
17844
17844
  </div>`;
17845
17845
  }
17846
17846
  function renderRemoved(f) {
17847
17847
  return `<div class="feature-card removed">
17848
17848
  <div class="card-header">
17849
17849
  <span class="card-sign removed-sign">−</span>
17850
- <span class="feature-key">${esc$6(f.featureKey)}</span>
17850
+ <span class="feature-key">${esc$10(f.featureKey)}</span>
17851
17851
  ${statusBadge(f.status)}
17852
- ${f.domain ? `<span class="domain-badge">${esc$6(f.domain)}</span>` : ""}
17853
- <span class="feature-title">${esc$6(f.title)}</span>
17852
+ ${f.domain ? `<span class="domain-badge">${esc$10(f.domain)}</span>` : ""}
17853
+ <span class="feature-title">${esc$10(f.title)}</span>
17854
17854
  </div>
17855
- <div class="card-problem">${esc$6(f.problem)}</div>
17856
- ${f.tags && f.tags.length ? `<div class="card-tags">${f.tags.map((t) => `<span class="tag">${esc$6(t)}</span>`).join("")}</div>` : ""}
17855
+ <div class="card-problem">${esc$10(f.problem)}</div>
17856
+ ${f.tags && f.tags.length ? `<div class="card-tags">${f.tags.map((t) => `<span class="tag">${esc$10(t)}</span>`).join("")}</div>` : ""}
17857
17857
  </div>`;
17858
17858
  }
17859
17859
  function renderChanged(a, b, diffs) {
17860
17860
  const rows = diffs.map((d) => {
17861
17861
  const oldEmpty = d.oldVal === "";
17862
17862
  const newEmpty = d.newVal === "";
17863
- const oldDisplay = oldEmpty ? "<span class=\"diff-empty\">— not set</span>" : `<pre class="diff-val">${esc$6(d.oldVal.length > 400 ? d.oldVal.slice(0, 400) + "…" : d.oldVal)}</pre>`;
17864
- const newDisplay = newEmpty ? "<span class=\"diff-empty\">— not set</span>" : `<pre class="diff-val">${esc$6(d.newVal.length > 400 ? d.newVal.slice(0, 400) + "…" : d.newVal)}</pre>`;
17863
+ const oldDisplay = oldEmpty ? "<span class=\"diff-empty\">— not set</span>" : `<pre class="diff-val">${esc$10(d.oldVal.length > 400 ? d.oldVal.slice(0, 400) + "…" : d.oldVal)}</pre>`;
17864
+ const newDisplay = newEmpty ? "<span class=\"diff-empty\">— not set</span>" : `<pre class="diff-val">${esc$10(d.newVal.length > 400 ? d.newVal.slice(0, 400) + "…" : d.newVal)}</pre>`;
17865
17865
  return `<tr>
17866
- <td class="diff-field"><code>${esc$6(d.field)}</code></td>
17866
+ <td class="diff-field"><code>${esc$10(d.field)}</code></td>
17867
17867
  <td class="diff-old">${oldDisplay}</td>
17868
17868
  <td class="diff-new">${newDisplay}</td>
17869
17869
  </tr>`;
@@ -17871,10 +17871,10 @@ function renderChanged(a, b, diffs) {
17871
17871
  return `<div class="feature-card changed">
17872
17872
  <div class="card-header">
17873
17873
  <span class="card-sign changed-sign">~</span>
17874
- <span class="feature-key">${esc$6(b.featureKey)}</span>
17874
+ <span class="feature-key">${esc$10(b.featureKey)}</span>
17875
17875
  ${statusBadge(b.status)}
17876
- ${b.domain ? `<span class="domain-badge">${esc$6(b.domain)}</span>` : ""}
17877
- <span class="feature-title">${esc$6(b.title)}</span>
17876
+ ${b.domain ? `<span class="domain-badge">${esc$10(b.domain)}</span>` : ""}
17877
+ <span class="feature-title">${esc$10(b.title)}</span>
17878
17878
  <span class="diff-count">${diffs.length} field${diffs.length === 1 ? "" : "s"} changed</span>
17879
17879
  </div>
17880
17880
  <div class="diff-table-wrap">
@@ -17913,7 +17913,7 @@ function generateDiff(featuresA, featuresB, nameA, nameB) {
17913
17913
  <head>
17914
17914
  <meta charset="UTF-8">
17915
17915
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
17916
- <title>Diff: ${esc$6(nameA)} → ${esc$6(nameB)}</title>
17916
+ <title>Diff: ${esc$10(nameA)} → ${esc$10(nameB)}</title>
17917
17917
  <style>
17918
17918
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
17919
17919
 
@@ -18203,9 +18203,9 @@ body {
18203
18203
  <span class="topbar-logo">◈ lac</span>
18204
18204
  <span class="topbar-sep">|</span>
18205
18205
  <div class="topbar-dirs">
18206
- <span>${esc$6(nameA)}</span>
18206
+ <span>${esc$10(nameA)}</span>
18207
18207
  <span class="topbar-arrow">→</span>
18208
- <span>${esc$6(nameB)}</span>
18208
+ <span>${esc$10(nameB)}</span>
18209
18209
  </div>
18210
18210
  <div class="topbar-right">${date} · ${totalChanges} change${totalChanges === 1 ? "" : "s"}</div>
18211
18211
  </div>
@@ -18257,7 +18257,7 @@ body {
18257
18257
  ` : ""}
18258
18258
 
18259
18259
  ${totalChanges === 0 ? `
18260
- <div class="empty-state">No differences found between ${esc$6(nameA)} and ${esc$6(nameB)}</div>
18260
+ <div class="empty-state">No differences found between ${esc$10(nameA)} and ${esc$10(nameB)}</div>
18261
18261
  ` : ""}
18262
18262
 
18263
18263
  </div>
@@ -18266,7 +18266,7 @@ body {
18266
18266
  }
18267
18267
  //#endregion
18268
18268
  //#region src/lib/treemapGenerator.ts
18269
- function esc$5(s) {
18269
+ function esc$9(s) {
18270
18270
  return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
18271
18271
  }
18272
18272
  function generateTreemap(features, projectName) {
@@ -18274,13 +18274,13 @@ function generateTreemap(features, projectName) {
18274
18274
  const count = features.length;
18275
18275
  const domainSet = /* @__PURE__ */ new Set();
18276
18276
  for (const f of features) if (f.domain) domainSet.add(f.domain);
18277
- const domainChips = Array.from(domainSet).sort().map((d) => `<button class="domain-chip" data-domain="${esc$5(d)}">${esc$5(d)}</button>`).join("\n ");
18277
+ const domainChips = Array.from(domainSet).sort().map((d) => `<button class="domain-chip" data-domain="${esc$9(d)}">${esc$9(d)}</button>`).join("\n ");
18278
18278
  return `<!DOCTYPE html>
18279
18279
  <html lang="en">
18280
18280
  <head>
18281
18281
  <meta charset="UTF-8">
18282
18282
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
18283
- <title>${esc$5(projectName)} · LAC Treemap</title>
18283
+ <title>${esc$9(projectName)} · LAC Treemap</title>
18284
18284
  <style>
18285
18285
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
18286
18286
  :root{
@@ -18484,7 +18484,7 @@ html,body{
18484
18484
  <div id="topbar">
18485
18485
  <span id="topbar-brand">◈ lac · treemap</span>
18486
18486
  <span id="topbar-sep">|</span>
18487
- <span id="topbar-project">${esc$5(projectName)}</span>
18487
+ <span id="topbar-project">${esc$9(projectName)}</span>
18488
18488
  <span id="topbar-count">${count} feature${count === 1 ? "" : "s"}</span>
18489
18489
  <div id="domain-chips">
18490
18490
  <button id="chip-all">All</button>
@@ -18802,7 +18802,7 @@ buildTiles();
18802
18802
  }
18803
18803
  //#endregion
18804
18804
  //#region src/lib/kanbanGenerator.ts
18805
- function esc$4(s) {
18805
+ function esc$8(s) {
18806
18806
  return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
18807
18807
  }
18808
18808
  function generateKanban(features, projectName) {
@@ -18810,13 +18810,13 @@ function generateKanban(features, projectName) {
18810
18810
  const count = features.length;
18811
18811
  const domainSet = /* @__PURE__ */ new Set();
18812
18812
  for (const f of features) if (f.domain) domainSet.add(f.domain);
18813
- const domainOptions = Array.from(domainSet).sort().map((d) => `<option value="${esc$4(d)}">${esc$4(d)}</option>`).join("\n ");
18813
+ const domainOptions = Array.from(domainSet).sort().map((d) => `<option value="${esc$8(d)}">${esc$8(d)}</option>`).join("\n ");
18814
18814
  return `<!DOCTYPE html>
18815
18815
  <html lang="en">
18816
18816
  <head>
18817
18817
  <meta charset="UTF-8">
18818
18818
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
18819
- <title>${esc$4(projectName)} · LAC Kanban</title>
18819
+ <title>${esc$8(projectName)} · LAC Kanban</title>
18820
18820
  <style>
18821
18821
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
18822
18822
  :root{
@@ -19050,7 +19050,7 @@ select.ctrl-select:focus{border-color:var(--accent);}
19050
19050
  <div id="topbar">
19051
19051
  <span id="topbar-brand">◈ lac · kanban</span>
19052
19052
  <span id="topbar-sep">|</span>
19053
- <span id="topbar-project">${esc$4(projectName)}</span>
19053
+ <span id="topbar-project">${esc$8(projectName)}</span>
19054
19054
  <span id="topbar-count">${count} feature${count === 1 ? "" : "s"}</span>
19055
19055
  <div id="topbar-controls">
19056
19056
  <span class="ctrl-label">Domain</span>
@@ -19329,7 +19329,7 @@ buildBoard();
19329
19329
  }
19330
19330
  //#endregion
19331
19331
  //#region src/lib/healthGenerator.ts
19332
- function esc$3(s) {
19332
+ function esc$7(s) {
19333
19333
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
19334
19334
  }
19335
19335
  function today$2() {
@@ -19520,7 +19520,7 @@ function renderStatusBar(features) {
19520
19520
  <div class="section-card">
19521
19521
  <h3 class="section-heading">Status Breakdown</h3>
19522
19522
  <div class="status-bar">${segments.map((s) => `<div class="status-segment" style="width:${(s.count / n * 100).toFixed(1)}%;background:${s.color}" title="${s.key}: ${s.count}"></div>`).join("")}</div>
19523
- <div class="status-legend">${segments.map((s) => `<span class="status-legend-item"><span class="status-dot" style="background:${s.color}"></span>${esc$3(s.key)} <strong>${s.count}</strong></span>`).join("")}</div>
19523
+ <div class="status-legend">${segments.map((s) => `<span class="status-legend-item"><span class="status-dot" style="background:${s.color}"></span>${esc$7(s.key)} <strong>${s.count}</strong></span>`).join("")}</div>
19524
19524
  </div>`;
19525
19525
  }
19526
19526
  function renderDomainMiniChart(grouped) {
@@ -19534,7 +19534,7 @@ function renderDomainMiniChart(grouped) {
19534
19534
  const pct = maxCount > 0 ? (fs.length / maxCount * 100).toFixed(1) : "0";
19535
19535
  return `
19536
19536
  <div class="domain-row">
19537
- <span class="domain-name">${esc$3(domain)}</span>
19537
+ <span class="domain-name">${esc$7(domain)}</span>
19538
19538
  <span class="domain-count-label">${fs.length}</span>
19539
19539
  <div class="domain-mini-bar-wrap">
19540
19540
  <div class="domain-mini-bar" style="width:${pct}%"></div>
@@ -19554,7 +19554,7 @@ function renderFieldCoverageBars(features) {
19554
19554
  const color = barColor(pct);
19555
19555
  return `
19556
19556
  <div class="coverage-row">
19557
- <span class="coverage-label">${esc$3(label)}</span>
19557
+ <span class="coverage-label">${esc$7(label)}</span>
19558
19558
  <div class="coverage-bar-wrap">
19559
19559
  <div class="coverage-bar" style="width:${pct}%;background:${color}"></div>
19560
19560
  </div>
@@ -19569,7 +19569,7 @@ function renderTechDebt(features) {
19569
19569
  const noDomain = features.filter((f) => !f.domain);
19570
19570
  function debtList(items, emptyText) {
19571
19571
  if (items.length === 0) return `<div class="debt-empty">&#10003; ${emptyText}</div>`;
19572
- return `<ul class="debt-list">${items.map((f) => `<li><code class="debt-key">${esc$3(f.featureKey)}</code> <span class="debt-title">${esc$3(f.title)}</span></li>`).join("")}</ul>`;
19572
+ return `<ul class="debt-list">${items.map((f) => `<li><code class="debt-key">${esc$7(f.featureKey)}</code> <span class="debt-title">${esc$7(f.title)}</span></li>`).join("")}</ul>`;
19573
19573
  }
19574
19574
  return `
19575
19575
  <div class="section-card debt-card">
@@ -19599,7 +19599,7 @@ function generateHealth(features, projectName) {
19599
19599
  <head>
19600
19600
  <meta charset="UTF-8">
19601
19601
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
19602
- <title>${esc$3(projectName)} — LAC Health</title>
19602
+ <title>${esc$7(projectName)} — LAC Health</title>
19603
19603
  <style>
19604
19604
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
19605
19605
 
@@ -19982,7 +19982,7 @@ body {
19982
19982
  <div class="topbar">
19983
19983
  <span class="topbar-logo">&#9672; lac &middot; health</span>
19984
19984
  <span class="topbar-sep">|</span>
19985
- <span class="topbar-project">${esc$3(projectName)}</span>
19985
+ <span class="topbar-project">${esc$7(projectName)}</span>
19986
19986
  <span class="topbar-date">${today$2()}</span>
19987
19987
  </div>
19988
19988
 
@@ -20041,7 +20041,7 @@ body {
20041
20041
  }
20042
20042
  //#endregion
20043
20043
  //#region src/lib/embedGenerator.ts
20044
- function esc$2(s) {
20044
+ function esc$6(s) {
20045
20045
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
20046
20046
  }
20047
20047
  function today$1() {
@@ -20098,7 +20098,7 @@ function generateEmbed(features, projectName) {
20098
20098
  const extraDomains = domains.length > MAX_DOMAINS_SHOWN ? domains.length - MAX_DOMAINS_SHOWN : 0;
20099
20099
  const domainDots = shownDomains.map((d, i) => `<span class="domain-dot-item">
20100
20100
  <span class="domain-dot" style="background:${domainDotColor(i)}"></span>
20101
- <span class="domain-dot-name">${esc$2(d)}</span>
20101
+ <span class="domain-dot-name">${esc$6(d)}</span>
20102
20102
  </span>`).join("");
20103
20103
  const extraBadge = extraDomains > 0 ? `<span class="domain-extra">+${extraDomains} more</span>` : "";
20104
20104
  const iframeCode = `<iframe src="lac-embed.html" width="380" height="320" frameborder="0" style="border-radius:8px;"></iframe>`;
@@ -20107,7 +20107,7 @@ function generateEmbed(features, projectName) {
20107
20107
  <head>
20108
20108
  <meta charset="UTF-8">
20109
20109
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
20110
- <title>${esc$2(projectName)} — LAC Embed</title>
20110
+ <title>${esc$6(projectName)} — LAC Embed</title>
20111
20111
  <style>
20112
20112
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
20113
20113
 
@@ -20388,7 +20388,7 @@ html, body {
20388
20388
  <div class="widget">
20389
20389
 
20390
20390
  <div class="widget-header">
20391
- <div class="widget-brand">&#9672; ${esc$2(projectName)}</div>
20391
+ <div class="widget-brand">&#9672; ${esc$6(projectName)}</div>
20392
20392
  <div class="widget-sub">life-as-code feature map</div>
20393
20393
  </div>
20394
20394
 
@@ -20424,7 +20424,7 @@ html, body {
20424
20424
  <span class="footer-sep">&middot;</span>
20425
20425
  <span class="footer-item">${domainCount} domain${domainCount === 1 ? "" : "s"}</span>
20426
20426
  <span class="footer-sep">&middot;</span>
20427
- <span class="footer-item">Updated: ${esc$2(updated)}</span>
20427
+ <span class="footer-item">Updated: ${esc$6(updated)}</span>
20428
20428
  </div>
20429
20429
 
20430
20430
  <div class="embed-area">
@@ -20433,7 +20433,7 @@ html, body {
20433
20433
  <span>embed code</span>
20434
20434
  </button>
20435
20435
  <div class="embed-code-wrap" id="embed-code-wrap">
20436
- <pre class="embed-pre" id="embed-pre">${esc$2(iframeCode)}</pre>
20436
+ <pre class="embed-pre" id="embed-pre">${esc$6(iframeCode)}</pre>
20437
20437
  <div class="embed-copy-row">
20438
20438
  <button class="embed-copy-btn" id="embed-copy-btn">copy</button>
20439
20439
  </div>
@@ -20483,7 +20483,7 @@ html, body {
20483
20483
  }
20484
20484
  //#endregion
20485
20485
  //#region src/lib/decisionLogGenerator.ts
20486
- function esc$1(s) {
20486
+ function esc$5(s) {
20487
20487
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
20488
20488
  }
20489
20489
  function today() {
@@ -20536,7 +20536,7 @@ function mostDecidedFeature(entries) {
20536
20536
  if (entries.length === 0) return "—";
20537
20537
  let best = entries[0];
20538
20538
  for (const e of entries) if (e.decisions.length > best.decisions.length) best = e;
20539
- return `${esc$1(best.featureKey)}: ${best.decisions.length}`;
20539
+ return `${esc$5(best.featureKey)}: ${best.decisions.length}`;
20540
20540
  }
20541
20541
  function mostCommonTagAmongDecisionHeavy(entries) {
20542
20542
  if (entries.length === 0) return "—";
@@ -20547,7 +20547,7 @@ function mostCommonTagAmongDecisionHeavy(entries) {
20547
20547
  for (const e of heavy) for (const t of e.tags) freq.set(t, (freq.get(t) ?? 0) + 1);
20548
20548
  if (freq.size === 0) return "—";
20549
20549
  const top = [...freq.entries()].sort((a, b) => b[1] - a[1])[0];
20550
- return top ? esc$1(top[0]) : "—";
20550
+ return top ? esc$5(top[0]) : "—";
20551
20551
  }
20552
20552
  function renderSidebar(features) {
20553
20553
  return `
@@ -20563,19 +20563,19 @@ function renderSidebar(features) {
20563
20563
  const dimClass = hasDecisions ? "" : " nav-item-dim";
20564
20564
  const countBadge = hasDecisions ? `<span class="nav-dec-count">${f.decisions.length}</span>` : "";
20565
20565
  return `
20566
- <a class="nav-item${dimClass}" href="#${esc$1(f.featureKey)}" data-key="${esc$1(f.featureKey)}"
20567
- data-title="${esc$1(f.title.toLowerCase())}" data-domain="${esc$1(domain.toLowerCase())}">
20566
+ <a class="nav-item${dimClass}" href="#${esc$5(f.featureKey)}" data-key="${esc$5(f.featureKey)}"
20567
+ data-title="${esc$5(f.title.toLowerCase())}" data-domain="${esc$5(domain.toLowerCase())}">
20568
20568
  <span class="nav-dot" style="background:${statusColor(f.status)};opacity:${hasDecisions ? "1" : "0.35"}"></span>
20569
- <span class="nav-item-key">${esc$1(f.featureKey)}</span>
20570
- <span class="nav-item-title">${esc$1(f.title)}</span>
20569
+ <span class="nav-item-key">${esc$5(f.featureKey)}</span>
20570
+ <span class="nav-item-title">${esc$5(f.title)}</span>
20571
20571
  ${countBadge}
20572
20572
  </a>`;
20573
20573
  }).join("");
20574
20574
  return `
20575
- <div class="nav-group" data-domain="${esc$1(domain.toLowerCase())}">
20575
+ <div class="nav-group" data-domain="${esc$5(domain.toLowerCase())}">
20576
20576
  <div class="nav-domain">
20577
20577
  <span class="nav-domain-arrow">&#9660;</span>
20578
- <span class="nav-domain-name">${esc$1(domain)}</span>
20578
+ <span class="nav-domain-name">${esc$5(domain)}</span>
20579
20579
  <span class="nav-domain-count">${fs.length}</span>
20580
20580
  </div>
20581
20581
  <div class="nav-group-items">
@@ -20588,14 +20588,14 @@ function renderSidebar(features) {
20588
20588
  }
20589
20589
  function renderDecisionSection(entry) {
20590
20590
  const decItems = entry.decisions.map((d, i) => {
20591
- const rationale = d.rationale ? `<div class="dec-field"><span class="dec-field-label">Rationale</span><p class="dec-field-body">${esc$1(d.rationale)}</p></div>` : "";
20592
- const alternativesHtml = d.alternativesConsidered && d.alternativesConsidered.length > 0 ? `<div class="dec-field"><span class="dec-field-label">Alternatives considered</span><ul class="dec-alts">${d.alternativesConsidered.map((a) => `<li>${esc$1(a)}</li>`).join("")}</ul></div>` : "";
20593
- const date = d.date ? `<div class="dec-field dec-field-inline"><span class="dec-field-label">Date</span><span class="dec-field-value">${esc$1(d.date)}</span></div>` : "";
20591
+ const rationale = d.rationale ? `<div class="dec-field"><span class="dec-field-label">Rationale</span><p class="dec-field-body">${esc$5(d.rationale)}</p></div>` : "";
20592
+ const alternativesHtml = d.alternativesConsidered && d.alternativesConsidered.length > 0 ? `<div class="dec-field"><span class="dec-field-label">Alternatives considered</span><ul class="dec-alts">${d.alternativesConsidered.map((a) => `<li>${esc$5(a)}</li>`).join("")}</ul></div>` : "";
20593
+ const date = d.date ? `<div class="dec-field dec-field-inline"><span class="dec-field-label">Date</span><span class="dec-field-value">${esc$5(d.date)}</span></div>` : "";
20594
20594
  return `
20595
20595
  <div class="dec-item" data-index="${i}">
20596
20596
  <h4 class="dec-title">
20597
20597
  <span class="dec-num">${i + 1}</span>
20598
- <span class="dec-text">${esc$1(d.decision)}</span>
20598
+ <span class="dec-text">${esc$5(d.decision)}</span>
20599
20599
  </h4>
20600
20600
  ${rationale}
20601
20601
  ${alternativesHtml}
@@ -20603,20 +20603,20 @@ function renderDecisionSection(entry) {
20603
20603
  </div>`;
20604
20604
  }).join("\n");
20605
20605
  return `
20606
- <section class="feature-section" id="${esc$1(entry.featureKey)}"
20607
- data-domain="${esc$1(entry.domain.toLowerCase())}"
20608
- data-key="${esc$1(entry.featureKey.toLowerCase())}"
20606
+ <section class="feature-section" id="${esc$5(entry.featureKey)}"
20607
+ data-domain="${esc$5(entry.domain.toLowerCase())}"
20608
+ data-key="${esc$5(entry.featureKey.toLowerCase())}"
20609
20609
  data-date="${entry.decisions.map((d) => d.date ?? "").filter(Boolean).sort().reverse()[0] ?? ""}">
20610
20610
  <header class="feature-section-header">
20611
20611
  <div class="feature-section-title-row">
20612
- <h2 class="feature-section-title">${esc$1(entry.title)}</h2>
20613
- <code class="feature-section-key">${esc$1(entry.featureKey)}</code>
20612
+ <h2 class="feature-section-title">${esc$5(entry.title)}</h2>
20613
+ <code class="feature-section-key">${esc$5(entry.featureKey)}</code>
20614
20614
  </div>
20615
20615
  <div class="feature-section-meta">
20616
- <span class="domain-chip">${esc$1(entry.domain)}</span>
20616
+ <span class="domain-chip">${esc$5(entry.domain)}</span>
20617
20617
  <span class="status-badge"
20618
20618
  style="color:${statusColor(entry.status)};background:${statusBg(entry.status)};border-color:${statusColor(entry.status)}40">
20619
- ${esc$1(entry.status)}
20619
+ ${esc$5(entry.status)}
20620
20620
  </span>
20621
20621
  <span class="dec-count-badge">${entry.decisions.length} decision${entry.decisions.length === 1 ? "" : "s"}</span>
20622
20622
  </div>
@@ -20657,7 +20657,7 @@ function generateDecisionLog(features, projectName) {
20657
20657
  <head>
20658
20658
  <meta charset="UTF-8">
20659
20659
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
20660
- <title>${esc$1(projectName)} — Decision Log</title>
20660
+ <title>${esc$5(projectName)} — Decision Log</title>
20661
20661
  <style>
20662
20662
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
20663
20663
 
@@ -21100,7 +21100,7 @@ mark {
21100
21100
  <div class="topbar">
21101
21101
  <span class="topbar-logo">&#9672; lac &middot; decisions</span>
21102
21102
  <span class="topbar-sep">|</span>
21103
- <span class="topbar-project">${esc$1(projectName)}</span>
21103
+ <span class="topbar-project">${esc$5(projectName)}</span>
21104
21104
  <span class="topbar-count">${totalDecisions} total decisions</span>
21105
21105
  <div class="sort-controls">
21106
21106
  <span style="font-family:var(--mono);font-size:10px;color:var(--text-soft);margin-right:4px;">sort:</span>
@@ -23026,6 +23026,34 @@ const ALL_HUB_ENTRIES = [
23026
23026
  description: "Field-by-field dump of every feature.json with sidebar navigation",
23027
23027
  icon: "🔩",
23028
23028
  primary: false
23029
+ },
23030
+ {
23031
+ file: "lac-radar.html",
23032
+ label: "Maturity Radar",
23033
+ description: "SVG polar chart — 5 quality dimensions scored per domain",
23034
+ icon: "🎯",
23035
+ primary: false
23036
+ },
23037
+ {
23038
+ file: "lac-successboard.html",
23039
+ label: "Success Board",
23040
+ description: "successCriteria + acceptanceCriteria organized by delivery status",
23041
+ icon: "✅",
23042
+ primary: false
23043
+ },
23044
+ {
23045
+ file: "lac-pitch.html",
23046
+ label: "Pitch Deck",
23047
+ description: "Full-screen keyboard-navigable slide deck — present your product story",
23048
+ icon: "🎤",
23049
+ primary: true
23050
+ },
23051
+ {
23052
+ file: "lac-timeline.html",
23053
+ label: "Feature Timeline",
23054
+ description: "Horizontal swim-lane timeline built from statusHistory — velocity at a glance",
23055
+ icon: "📆",
23056
+ primary: false
23029
23057
  }
23030
23058
  ];
23031
23059
  function generateHub(projectName, stats, entries, generatedAt = (/* @__PURE__ */ new Date()).toISOString(), prefix) {
@@ -23229,6 +23257,1552 @@ function lacGo(file){location.href=location.href.replace(/[^\\/]*$/,'')+file}
23229
23257
  </html>`;
23230
23258
  }
23231
23259
  //#endregion
23260
+ //#region src/lib/radarGenerator.ts
23261
+ function hasText$1(v, min = 10) {
23262
+ return typeof v === "string" && v.trim().length >= min;
23263
+ }
23264
+ function arrLen(v) {
23265
+ return Array.isArray(v) ? v.length : 0;
23266
+ }
23267
+ function esc$4(s) {
23268
+ return String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
23269
+ }
23270
+ const METRICS = [
23271
+ {
23272
+ key: "docs",
23273
+ label: "Documentation",
23274
+ color: "#5b82cc",
23275
+ desc: "Avg fill of problem, analysis, implementation",
23276
+ score(fs) {
23277
+ if (!fs.length) return 0;
23278
+ return fs.reduce((s, f) => {
23279
+ let n = 0;
23280
+ if (hasText$1(f["problem"])) n++;
23281
+ if (hasText$1(f["analysis"])) n++;
23282
+ if (hasText$1(f["implementation"])) n++;
23283
+ return s + n / 3;
23284
+ }, 0) / fs.length;
23285
+ }
23286
+ },
23287
+ {
23288
+ key: "decisions",
23289
+ label: "Decision Quality",
23290
+ color: "#9b7ecc",
23291
+ desc: "% features with 2+ documented decisions",
23292
+ score(fs) {
23293
+ if (!fs.length) return 0;
23294
+ return fs.filter((f) => arrLen(f["decisions"]) >= 2).length / fs.length;
23295
+ }
23296
+ },
23297
+ {
23298
+ key: "guide",
23299
+ label: "User Guide",
23300
+ color: "#4aad72",
23301
+ desc: "% features with userGuide written",
23302
+ score(fs) {
23303
+ if (!fs.length) return 0;
23304
+ return fs.filter((f) => hasText$1(f["userGuide"], 1)).length / fs.length;
23305
+ }
23306
+ },
23307
+ {
23308
+ key: "code",
23309
+ label: "Code Reference",
23310
+ color: "#d4a853",
23311
+ desc: "% features with componentFile linked",
23312
+ score(fs) {
23313
+ if (!fs.length) return 0;
23314
+ return fs.filter((f) => hasText$1(f["componentFile"], 1)).length / fs.length;
23315
+ }
23316
+ },
23317
+ {
23318
+ key: "shipped",
23319
+ label: "Ship Rate",
23320
+ color: "#e07b54",
23321
+ desc: "% features frozen / shipped",
23322
+ score(fs) {
23323
+ if (!fs.length) return 0;
23324
+ return fs.filter((f) => f["status"] === "frozen").length / fs.length;
23325
+ }
23326
+ }
23327
+ ];
23328
+ function generateRadar(features, projectName) {
23329
+ const domains = [...new Set(features.map((f) => f["domain"] || "misc"))].sort();
23330
+ const byDomain = /* @__PURE__ */ new Map();
23331
+ for (const f of features) {
23332
+ const d = f["domain"] || "misc";
23333
+ if (!byDomain.has(d)) byDomain.set(d, []);
23334
+ byDomain.get(d).push(f);
23335
+ }
23336
+ const N = domains.length;
23337
+ const CX = 240, CY = 220, R = 170;
23338
+ const PAD_LABEL = 28;
23339
+ const scores = METRICS.map((m) => domains.map((d) => m.score(byDomain.get(d) ?? [])));
23340
+ function polar(i, r) {
23341
+ const angle = i / N * 2 * Math.PI - Math.PI / 2;
23342
+ return {
23343
+ x: CX + r * Math.cos(angle),
23344
+ y: CY + r * Math.sin(angle)
23345
+ };
23346
+ }
23347
+ function polygonPts(scoreRow) {
23348
+ return scoreRow.map((s, i) => {
23349
+ const p = polar(i, s * R);
23350
+ return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
23351
+ }).join(" ");
23352
+ }
23353
+ const rings = [
23354
+ .25,
23355
+ .5,
23356
+ .75,
23357
+ 1
23358
+ ].map((pct) => {
23359
+ return `<polygon points="${domains.map((_, i) => {
23360
+ const p = polar(i, pct * R);
23361
+ return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
23362
+ }).join(" ")}" fill="none" stroke="#2a2724" stroke-width="${pct === 1 ? 1.5 : .8}"/>`;
23363
+ }).join("\n");
23364
+ const spokes = domains.map((_, i) => {
23365
+ const p = polar(i, R);
23366
+ return `<line x1="${CX}" y1="${CY}" x2="${p.x.toFixed(1)}" y2="${p.y.toFixed(1)}" stroke="#2a2724" stroke-width="0.8"/>`;
23367
+ }).join("\n");
23368
+ const labels = domains.map((d, i) => {
23369
+ const p = polar(i, R + PAD_LABEL);
23370
+ const anchor = p.x < CX - 5 ? "end" : p.x > CX + 5 ? "start" : "middle";
23371
+ const label = d.replace(/-/g, "‑");
23372
+ return `<text x="${p.x.toFixed(1)}" y="${(p.y + 4).toFixed(1)}" text-anchor="${anchor}" class="domain-label">${esc$4(label)}</text>`;
23373
+ }).join("\n");
23374
+ const metricPolygons = METRICS.map((m, mi) => {
23375
+ const pts = polygonPts(scores[mi]);
23376
+ return `<polygon id="poly-${m.key}" points="${pts}" fill="${m.color}" fill-opacity="0.12" stroke="${m.color}" stroke-width="1.8" stroke-linejoin="round" class="metric-poly" data-metric="${m.key}"/>`;
23377
+ }).join("\n");
23378
+ const ringLabels = [
23379
+ 25,
23380
+ 50,
23381
+ 75,
23382
+ 100
23383
+ ].map((pct) => {
23384
+ const p = polar(0, pct / 100 * R);
23385
+ return `<text x="${(CX - 6).toFixed(1)}" y="${(p.y + 3).toFixed(1)}" text-anchor="end" class="ring-label">${pct}%</text>`;
23386
+ }).join("\n");
23387
+ const tableRows = domains.map((d, di) => {
23388
+ const domFeats = byDomain.get(d) ?? [];
23389
+ const cells = METRICS.map((m, mi) => {
23390
+ const pct = Math.round(scores[mi][di] * 100);
23391
+ return `<td style="color:${pct >= 70 ? "#4aad72" : pct >= 40 ? "#c4a255" : "#cc5b5b"};font-family:var(--mono);text-align:center">${pct}%</td>`;
23392
+ });
23393
+ return `<tr>
23394
+ <td><strong>${esc$4(d)}</strong></td>
23395
+ <td style="color:var(--text-soft);text-align:center">${domFeats.length}</td>
23396
+ ${cells.join("")}
23397
+ </tr>`;
23398
+ }).join("\n");
23399
+ const compositeByDomain = domains.map((_, di) => Math.round(METRICS.reduce((s, _, mi) => s + scores[mi][di], 0) / METRICS.length * 100));
23400
+ const dataJson = JSON.stringify(domains.map((d, di) => ({
23401
+ domain: d,
23402
+ count: (byDomain.get(d) ?? []).length,
23403
+ composite: compositeByDomain[di],
23404
+ scores: Object.fromEntries(METRICS.map((m, mi) => [m.key, Math.round(scores[mi][di] * 100)]))
23405
+ }))).replace(/<\/script>/gi, "<\\/script>");
23406
+ const svgWidth = 480, svgHeight = 440;
23407
+ return `<!DOCTYPE html>
23408
+ <html lang="en">
23409
+ <head>
23410
+ <meta charset="UTF-8">
23411
+ <meta name="viewport" content="width=device-width,initial-scale=1">
23412
+ <title>${esc$4(projectName)} — Domain Maturity Radar</title>
23413
+ <style>
23414
+ :root {
23415
+ --bg: #12100e;
23416
+ --bg-card: #1a1714;
23417
+ --bg-hover: #201d1a;
23418
+ --border: #2a2724;
23419
+ --border-soft: #221f1c;
23420
+ --text: #e8e0d4;
23421
+ --text-soft: #8a7f74;
23422
+ --accent: #d4a853;
23423
+ --mono: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
23424
+ }
23425
+ * { box-sizing: border-box; margin: 0; padding: 0; }
23426
+ html, body { height: 100%; background: var(--bg); color: var(--text); font-family: system-ui, -apple-system, sans-serif; }
23427
+ body { display: flex; flex-direction: column; min-height: 100vh; }
23428
+
23429
+ .topbar {
23430
+ display: flex; align-items: center; gap: 10px; padding: 0 20px; height: 48px;
23431
+ background: #0e0c0a; border-bottom: 1px solid var(--border); flex-shrink: 0;
23432
+ font-size: 13px;
23433
+ }
23434
+ .topbar-logo { color: var(--accent); font-weight: 700; font-family: var(--mono); }
23435
+ .topbar-sep { color: var(--border); }
23436
+ .topbar-project { color: var(--text); }
23437
+ .topbar-count { margin-left: auto; color: var(--text-soft); font-size: 12px; font-family: var(--mono); }
23438
+
23439
+ .main { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 32px 24px; gap: 32px; }
23440
+
23441
+ h1 { font-size: 20px; font-weight: 600; color: var(--text); }
23442
+ .subtitle { font-size: 13px; color: var(--text-soft); margin-top: 4px; text-align: center; }
23443
+
23444
+ .radar-wrap {
23445
+ display: flex; gap: 40px; align-items: flex-start; flex-wrap: wrap; justify-content: center;
23446
+ }
23447
+
23448
+ svg.radar { overflow: visible; }
23449
+ .domain-label { font-size: 11px; fill: var(--text); font-family: var(--mono); }
23450
+ .ring-label { font-size: 9px; fill: var(--text-soft); font-family: var(--mono); }
23451
+ .metric-poly { cursor: pointer; transition: fill-opacity 0.2s, stroke-width 0.2s; }
23452
+ .metric-poly:hover { fill-opacity: 0.35; stroke-width: 2.5; }
23453
+ .metric-poly.dimmed { fill-opacity: 0.04; stroke-opacity: 0.25; }
23454
+
23455
+ .legend {
23456
+ display: flex; flex-direction: column; gap: 10px; padding: 20px;
23457
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
23458
+ min-width: 200px;
23459
+ }
23460
+ .legend-title { font-size: 11px; color: var(--text-soft); font-family: var(--mono); text-transform: uppercase; letter-spacing: .08em; margin-bottom: 4px; }
23461
+ .legend-item {
23462
+ display: flex; align-items: flex-start; gap: 10px; cursor: pointer;
23463
+ padding: 6px 8px; border-radius: 4px; transition: background 0.15s;
23464
+ }
23465
+ .legend-item:hover { background: var(--bg-hover); }
23466
+ .legend-item.dimmed { opacity: 0.35; }
23467
+ .legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; margin-top: 2px; }
23468
+ .legend-text { display: flex; flex-direction: column; }
23469
+ .legend-label { font-size: 13px; font-weight: 500; color: var(--text); }
23470
+ .legend-desc { font-size: 11px; color: var(--text-soft); margin-top: 1px; }
23471
+
23472
+ .table-wrap {
23473
+ width: 100%; max-width: 760px;
23474
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
23475
+ overflow: hidden;
23476
+ }
23477
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
23478
+ th {
23479
+ padding: 10px 14px; text-align: left; font-weight: 500; font-size: 11px;
23480
+ color: var(--text-soft); border-bottom: 1px solid var(--border);
23481
+ text-transform: uppercase; letter-spacing: .06em; font-family: var(--mono);
23482
+ }
23483
+ th.metric-header { cursor: pointer; }
23484
+ th.metric-header:hover { color: var(--text); }
23485
+ td { padding: 10px 14px; border-bottom: 1px solid var(--border-soft); }
23486
+ tr:last-child td { border-bottom: none; }
23487
+ tr:hover td { background: var(--bg-hover); }
23488
+
23489
+ .tooltip {
23490
+ position: fixed; pointer-events: none; z-index: 999;
23491
+ background: #1e1b18; border: 1px solid var(--border); border-radius: 6px;
23492
+ padding: 10px 14px; font-size: 12px; color: var(--text); max-width: 220px;
23493
+ display: none; box-shadow: 0 8px 24px rgba(0,0,0,.5);
23494
+ }
23495
+ .tooltip.visible { display: block; }
23496
+ .tooltip-domain { font-weight: 600; margin-bottom: 6px; color: var(--accent); font-family: var(--mono); font-size: 11px; }
23497
+ .tooltip-row { display: flex; justify-content: space-between; gap: 16px; margin: 2px 0; }
23498
+ .tooltip-label { color: var(--text-soft); }
23499
+ .tooltip-val { font-family: var(--mono); }
23500
+ </style>
23501
+ </head>
23502
+ <body>
23503
+ <div class="topbar">
23504
+ <span class="topbar-logo">◈ lac</span>
23505
+ <span class="topbar-sep">|</span>
23506
+ <span class="topbar-project">${esc$4(projectName)}</span>
23507
+ <span class="topbar-count">${features.length} features · ${domains.length} domains · Domain Maturity Radar</span>
23508
+ </div>
23509
+
23510
+ <div class="main">
23511
+ <div style="text-align:center">
23512
+ <h1>Domain Maturity Radar</h1>
23513
+ <p class="subtitle">5 quality dimensions scored per domain — hover a legend item to isolate a metric</p>
23514
+ </div>
23515
+
23516
+ <div class="radar-wrap">
23517
+ <svg class="radar" width="${svgWidth}" height="${svgHeight}" viewBox="0 0 ${svgWidth} ${svgHeight}">
23518
+ <!-- Guide rings -->
23519
+ ${rings}
23520
+ <!-- Spokes -->
23521
+ ${spokes}
23522
+ <!-- Ring labels -->
23523
+ ${ringLabels}
23524
+ <!-- Metric polygons -->
23525
+ ${metricPolygons}
23526
+ <!-- Domain labels -->
23527
+ ${labels}
23528
+ <!-- Center dot -->
23529
+ <circle cx="${CX}" cy="${CY}" r="3" fill="var(--border)"/>
23530
+ </svg>
23531
+
23532
+ <div class="legend">
23533
+ <div class="legend-title">Metric</div>
23534
+ ${METRICS.map((m) => `
23535
+ <div class="legend-item" data-metric="${m.key}" onclick="toggleMetric('${m.key}')">
23536
+ <span class="legend-dot" style="background:${m.color}"></span>
23537
+ <span class="legend-text">
23538
+ <span class="legend-label">${esc$4(m.label)}</span>
23539
+ <span class="legend-desc">${esc$4(m.desc)}</span>
23540
+ </span>
23541
+ </div>`).join("")}
23542
+ </div>
23543
+ </div>
23544
+
23545
+ <div class="table-wrap">
23546
+ <table>
23547
+ <thead>
23548
+ <tr>
23549
+ <th>Domain</th>
23550
+ <th style="text-align:center">Features</th>
23551
+ ${METRICS.map((m) => `<th class="metric-header" style="text-align:center;color:${m.color}" title="${esc$4(m.desc)}">${esc$4(m.label)}</th>`).join("")}
23552
+ </tr>
23553
+ </thead>
23554
+ <tbody id="table-body">
23555
+ ${tableRows}
23556
+ </tbody>
23557
+ </table>
23558
+ </div>
23559
+ </div>
23560
+
23561
+ <div class="tooltip" id="tooltip"></div>
23562
+
23563
+ <script>
23564
+ const DATA = ${dataJson};
23565
+ const byDomain = new Map(DATA.map(d => [d.domain, d]));
23566
+ const METRIC_COLORS = {${METRICS.map((m) => `'${m.key}': '${m.color}'`).join(", ")}};
23567
+
23568
+ let activeMetrics = new Set(${JSON.stringify(METRICS.map((m) => m.key))});
23569
+
23570
+ function toggleMetric(key) {
23571
+ if (activeMetrics.size === ${METRICS.length} && activeMetrics.has(key)) {
23572
+ // Solo this metric
23573
+ activeMetrics.clear();
23574
+ activeMetrics.add(key);
23575
+ } else if (activeMetrics.size === 1 && activeMetrics.has(key)) {
23576
+ // Re-activate all
23577
+ ${JSON.stringify(METRICS.map((m) => m.key))}.forEach(k => activeMetrics.add(k));
23578
+ } else {
23579
+ if (activeMetrics.has(key)) activeMetrics.delete(key);
23580
+ else activeMetrics.add(key);
23581
+ }
23582
+ updateVisibility();
23583
+ }
23584
+
23585
+ function updateVisibility() {
23586
+ document.querySelectorAll('.metric-poly').forEach(el => {
23587
+ const k = el.dataset.metric;
23588
+ el.classList.toggle('dimmed', !activeMetrics.has(k));
23589
+ });
23590
+ document.querySelectorAll('.legend-item').forEach(el => {
23591
+ const k = el.dataset.metric;
23592
+ el.classList.toggle('dimmed', !activeMetrics.has(k));
23593
+ });
23594
+ }
23595
+
23596
+ // Domain hover tooltip via SVG polygon hit-test approximation
23597
+ // Attach mousemove to SVG, find nearest domain spoke
23598
+ const svg = document.querySelector('svg.radar');
23599
+ const tooltip = document.getElementById('tooltip');
23600
+ const CX = ${CX}, CY = ${CY};
23601
+ const domainAngles = ${JSON.stringify(domains.map((_, i) => i / N * 360 - 90))};
23602
+ const domainNames = ${JSON.stringify(domains)};
23603
+
23604
+ svg.addEventListener('mousemove', e => {
23605
+ const rect = svg.getBoundingClientRect();
23606
+ const x = (e.clientX - rect.left) * (${svgWidth} / rect.width) - CX;
23607
+ const y = (e.clientY - rect.top) * (${svgHeight} / rect.height) - CY;
23608
+ const dist = Math.sqrt(x*x + y*y);
23609
+ if (dist < 15 || dist > ${R + 40}) { tooltip.classList.remove('visible'); return; }
23610
+
23611
+ let angle = Math.atan2(y, x) * 180 / Math.PI + 90;
23612
+ if (angle < 0) angle += 360;
23613
+
23614
+ // Find nearest domain spoke
23615
+ const step = 360 / domainNames.length;
23616
+ const idx = Math.round(angle / step) % domainNames.length;
23617
+ const domain = domainNames[idx];
23618
+ const d = byDomain.get(domain);
23619
+ if (!d) { tooltip.classList.remove('visible'); return; }
23620
+
23621
+ const metricsHtml = Object.entries(d.scores).map(([k, v]) => {
23622
+ const color = v >= 70 ? '#4aad72' : v >= 40 ? '#c4a255' : '#cc5b5b';
23623
+ return '<div class="tooltip-row"><span class="tooltip-label">' + k + '</span><span class="tooltip-val" style="color:' + color + '">' + v + '%</span></div>';
23624
+ }).join('');
23625
+
23626
+ tooltip.innerHTML = '<div class="tooltip-domain">' + domain + '</div>' +
23627
+ '<div class="tooltip-row"><span class="tooltip-label">features</span><span class="tooltip-val">' + d.count + '</span></div>' +
23628
+ metricsHtml;
23629
+ tooltip.style.left = (e.clientX + 14) + 'px';
23630
+ tooltip.style.top = (e.clientY - 10) + 'px';
23631
+ tooltip.classList.add('visible');
23632
+ });
23633
+
23634
+ svg.addEventListener('mouseleave', () => tooltip.classList.remove('visible'));
23635
+ <\/script>
23636
+ </body>
23637
+ </html>`;
23638
+ }
23639
+ //#endregion
23640
+ //#region src/lib/successboardGenerator.ts
23641
+ function esc$3(s) {
23642
+ return String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
23643
+ }
23644
+ function first(s, chars = 160) {
23645
+ const str = typeof s === "string" ? s.trim() : "";
23646
+ return str.length > chars ? str.slice(0, chars - 1) + "…" : str;
23647
+ }
23648
+ const STATUS_COLOR$2 = {
23649
+ frozen: "#5b82cc",
23650
+ active: "#4aad72",
23651
+ draft: "#c4a255",
23652
+ deprecated: "#664444"
23653
+ };
23654
+ const STATUS_LABEL = {
23655
+ frozen: "🔒 Achieved",
23656
+ active: "🟡 In Progress",
23657
+ draft: "⚪ Planned",
23658
+ deprecated: "❌ Deprecated"
23659
+ };
23660
+ const COLUMN_ORDER = [
23661
+ "frozen",
23662
+ "active",
23663
+ "draft"
23664
+ ];
23665
+ function generateSuccessboard(features, projectName) {
23666
+ const hasCriteria = (f) => typeof f["successCriteria"] === "string" && f["successCriteria"].trim().length > 0 || Array.isArray(f["acceptanceCriteria"]) && f["acceptanceCriteria"].length > 0;
23667
+ const withCriteria = features.filter(hasCriteria);
23668
+ const withoutCriteria = features.filter((f) => !hasCriteria(f));
23669
+ const domains = [...new Set(features.map((f) => f["domain"] || "misc"))].sort();
23670
+ const byStatus = /* @__PURE__ */ new Map();
23671
+ for (const status of COLUMN_ORDER) byStatus.set(status, []);
23672
+ for (const f of withCriteria) {
23673
+ const s = f["status"] || "draft";
23674
+ if (!byStatus.has(s)) byStatus.set(s, []);
23675
+ byStatus.get(s).push(f);
23676
+ }
23677
+ const frozenCount = features.filter((f) => f["status"] === "frozen").length;
23678
+ const criteriaPct = features.length ? Math.round(withCriteria.length / features.length * 100) : 0;
23679
+ const frozenWithCriteria = withCriteria.filter((f) => f["status"] === "frozen").length;
23680
+ function renderAC(f) {
23681
+ const ac = f["acceptanceCriteria"];
23682
+ if (!Array.isArray(ac) || ac.length === 0) return "";
23683
+ const isFrozen = f["status"] === "frozen";
23684
+ return `<ul class="ac-list">` + ac.map((item) => {
23685
+ return `<li class="ac-item${isFrozen ? " ac-done" : ""}">
23686
+ <span class="ac-check">${isFrozen ? "✓" : "○"}</span>
23687
+ <span class="ac-text">${esc$3(typeof item === "string" ? item : String(item))}</span>
23688
+ </li>`;
23689
+ }).join("") + `</ul>`;
23690
+ }
23691
+ function renderCard(f) {
23692
+ const status = f["status"] || "draft";
23693
+ const sc = typeof f["successCriteria"] === "string" ? f["successCriteria"].trim() : "";
23694
+ const domain = f["domain"] || "";
23695
+ const priority = f["priority"] != null ? `P${f["priority"]}` : "";
23696
+ const acHtml = renderAC(f);
23697
+ const key = f["featureKey"] || "";
23698
+ return `<div class="card" data-domain="${esc$3(domain)}" data-status="${esc$3(status)}" onclick="window.open('lac-wiki.html#${esc$3(key)}','_self')">
23699
+ <div class="card-header">
23700
+ ${domain ? `<span class="badge badge-domain">${esc$3(domain)}</span>` : ""}
23701
+ ${priority ? `<span class="badge badge-priority">${esc$3(priority)}</span>` : ""}
23702
+ </div>
23703
+ <div class="card-title">${esc$3(first(f["title"], 80))}</div>
23704
+ ${sc ? `<blockquote class="card-sc">${esc$3(first(sc, 200))}</blockquote>` : ""}
23705
+ ${acHtml}
23706
+ <div class="card-key">${esc$3(key)}</div>
23707
+ </div>`;
23708
+ }
23709
+ const columnHtml = COLUMN_ORDER.map((status) => {
23710
+ const colFeatures = (byStatus.get(status) ?? []).sort((a, b) => (a["priority"] ?? 99) - (b["priority"] ?? 99));
23711
+ return `<div class="column">
23712
+ <div class="col-header" style="border-left:3px solid ${STATUS_COLOR$2[status] ?? "#888"}">
23713
+ <span class="col-title">${STATUS_LABEL[status] ?? status}</span>
23714
+ <span class="col-count">${colFeatures.length}</span>
23715
+ </div>
23716
+ <div class="col-cards" id="col-${status}">
23717
+ ${colFeatures.length === 0 ? `<div class="col-empty">No features yet</div>` : colFeatures.map(renderCard).join("\n")}
23718
+ </div>
23719
+ </div>`;
23720
+ }).join("\n");
23721
+ const domainPills = domains.map((d) => {
23722
+ const count = withCriteria.filter((f) => f["domain"] === d).length;
23723
+ return `<span class="domain-pill" data-domain="${esc$3(d)}" onclick="toggleDomain('${esc$3(d)}')">${esc$3(d)} <span class="pill-count">${count}</span></span>`;
23724
+ }).join("");
23725
+ const missingCards = withoutCriteria.slice(0, 12).map((f) => {
23726
+ const status = f["status"] || "draft";
23727
+ return `<div class="missing-card" onclick="window.open('lac-wiki.html#${esc$3(f["featureKey"] || "")}','_self')">
23728
+ <span class="missing-status" style="color:${STATUS_COLOR$2[status] ?? "#888"}">●</span>
23729
+ <span class="missing-title">${esc$3(first(f["title"], 60))}</span>
23730
+ <span class="missing-domain">${esc$3(f["domain"] || "")}</span>
23731
+ <span class="missing-hint">+ add successCriteria</span>
23732
+ </div>`;
23733
+ }).join("");
23734
+ const dataJson = JSON.stringify({
23735
+ total: features.length,
23736
+ withCriteria: withCriteria.length,
23737
+ without: withoutCriteria.length,
23738
+ frozenWithCriteria,
23739
+ criteriaPct
23740
+ }).replace(/<\/script>/gi, "<\\/script>");
23741
+ return `<!DOCTYPE html>
23742
+ <html lang="en">
23743
+ <head>
23744
+ <meta charset="UTF-8">
23745
+ <meta name="viewport" content="width=device-width,initial-scale=1">
23746
+ <title>${esc$3(projectName)} — Success Board</title>
23747
+ <style>
23748
+ :root {
23749
+ --bg: #12100e; --bg-card: #1a1714; --bg-hover: #201d1a;
23750
+ --border: #2a2724; --border-soft: #221f1c;
23751
+ --text: #e8e0d4; --text-soft: #8a7f74; --accent: #d4a853;
23752
+ --mono: 'SF Mono','Fira Code','Cascadia Code',monospace;
23753
+ }
23754
+ * { box-sizing: border-box; margin: 0; padding: 0; }
23755
+ html, body { height: 100%; background: var(--bg); color: var(--text); font-family: system-ui,-apple-system,sans-serif; }
23756
+
23757
+ .topbar {
23758
+ display: flex; align-items: center; gap: 10px; padding: 0 20px; height: 48px;
23759
+ background: #0e0c0a; border-bottom: 1px solid var(--border); flex-shrink: 0; font-size: 13px;
23760
+ }
23761
+ .topbar-logo { color: var(--accent); font-weight: 700; font-family: var(--mono); }
23762
+ .topbar-sep { color: var(--border); }
23763
+ .topbar-count { margin-left: auto; color: var(--text-soft); font-size: 12px; font-family: var(--mono); }
23764
+
23765
+ .hero {
23766
+ padding: 28px 24px 0;
23767
+ display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; gap: 20px;
23768
+ }
23769
+ .hero-text h1 { font-size: 22px; font-weight: 600; }
23770
+ .hero-text p { font-size: 13px; color: var(--text-soft); margin-top: 4px; }
23771
+
23772
+ .stats-row { display: flex; gap: 16px; flex-wrap: wrap; }
23773
+ .stat-card {
23774
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
23775
+ padding: 14px 18px; text-align: center; min-width: 90px;
23776
+ }
23777
+ .stat-num { font-size: 28px; font-weight: 700; font-family: var(--mono); color: var(--accent); }
23778
+ .stat-label { font-size: 11px; color: var(--text-soft); margin-top: 2px; }
23779
+
23780
+ .progress-bar-wrap { padding: 16px 24px 0; }
23781
+ .progress-bar-label { font-size: 12px; color: var(--text-soft); margin-bottom: 6px; font-family: var(--mono); }
23782
+ .progress-bar { height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
23783
+ .progress-fill { height: 100%; background: linear-gradient(90deg, #5b82cc, #4aad72); border-radius: 3px; transition: width .6s ease; }
23784
+
23785
+ .filters { padding: 16px 24px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; border-bottom: 1px solid var(--border); }
23786
+ .filter-label { font-size: 11px; color: var(--text-soft); font-family: var(--mono); }
23787
+ .domain-pill {
23788
+ font-size: 11px; padding: 4px 10px; border-radius: 12px; cursor: pointer;
23789
+ background: var(--bg-card); border: 1px solid var(--border); color: var(--text-soft);
23790
+ transition: all .15s; user-select: none;
23791
+ }
23792
+ .domain-pill:hover { border-color: var(--accent); color: var(--text); }
23793
+ .domain-pill.active { background: var(--accent); color: #12100e; border-color: var(--accent); font-weight: 600; }
23794
+ .pill-count { opacity: .6; }
23795
+
23796
+ .board { display: flex; gap: 0; flex: 1; overflow: hidden; height: calc(100vh - 260px); min-height: 400px; }
23797
+ .column { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); min-width: 260px; }
23798
+ .column:last-child { border-right: none; }
23799
+ .col-header { padding: 14px 16px; display: flex; align-items: center; justify-content: space-between; background: var(--bg-card); border-bottom: 1px solid var(--border); flex-shrink: 0; margin: 0; padding-left: 13px; }
23800
+ .col-title { font-size: 13px; font-weight: 600; }
23801
+ .col-count { font-size: 12px; color: var(--text-soft); background: var(--border); border-radius: 10px; padding: 1px 8px; font-family: var(--mono); }
23802
+ .col-cards { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 10px; }
23803
+ .col-empty { padding: 24px 0; text-align: center; color: var(--text-soft); font-size: 13px; font-style: italic; }
23804
+
23805
+ .card {
23806
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
23807
+ padding: 14px 14px 12px; cursor: pointer; transition: border-color .15s, background .15s;
23808
+ }
23809
+ .card:hover { border-color: var(--accent); background: var(--bg-hover); }
23810
+ .card-header { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
23811
+ .badge { font-size: 10px; padding: 2px 8px; border-radius: 10px; font-family: var(--mono); }
23812
+ .badge-domain { background: #2a2724; color: var(--text-soft); border: 1px solid var(--border); }
23813
+ .badge-priority { background: #1e1b12; color: var(--accent); border: 1px solid #3a2f10; }
23814
+ .card-title { font-size: 14px; font-weight: 600; line-height: 1.35; margin-bottom: 8px; }
23815
+ .card-sc {
23816
+ font-size: 12px; color: var(--text-soft); font-style: italic; line-height: 1.5;
23817
+ border-left: 2px solid var(--accent); padding-left: 8px; margin: 6px 0 8px;
23818
+ }
23819
+ .ac-list { list-style: none; display: flex; flex-direction: column; gap: 4px; margin: 6px 0; }
23820
+ .ac-item { display: flex; align-items: flex-start; gap: 6px; font-size: 12px; }
23821
+ .ac-check { color: var(--text-soft); flex-shrink: 0; font-size: 11px; margin-top: 1px; font-family: var(--mono); }
23822
+ .ac-done .ac-check { color: #4aad72; }
23823
+ .ac-text { color: var(--text-soft); line-height: 1.4; }
23824
+ .ac-done .ac-text { color: var(--text); text-decoration: line-through; opacity: .6; }
23825
+ .card-key { font-size: 10px; font-family: var(--mono); color: #4a4540; margin-top: 8px; }
23826
+
23827
+ .missing-section { padding: 20px 24px; border-top: 1px solid var(--border); }
23828
+ .missing-title { font-size: 13px; font-weight: 600; color: var(--text-soft); margin-bottom: 12px; }
23829
+ .missing-grid { display: flex; flex-direction: column; gap: 6px; }
23830
+ .missing-card {
23831
+ display: flex; align-items: center; gap: 10px; padding: 8px 12px;
23832
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: 6px;
23833
+ cursor: pointer; font-size: 12px; transition: border-color .15s;
23834
+ }
23835
+ .missing-card:hover { border-color: var(--accent); }
23836
+ .missing-status { flex-shrink: 0; }
23837
+ .missing-title-text, .missing-title { flex: 1; }
23838
+ .missing-domain { color: var(--text-soft); font-family: var(--mono); font-size: 10px; }
23839
+ .missing-hint { color: #4aad72; font-size: 10px; opacity: .6; }
23840
+ </style>
23841
+ </head>
23842
+ <body>
23843
+ <div class="topbar">
23844
+ <span class="topbar-logo">◈ lac</span>
23845
+ <span class="topbar-sep">|</span>
23846
+ <span class="topbar-project">${esc$3(projectName)} — Success Board</span>
23847
+ <span class="topbar-count">${withCriteria.length}/${features.length} features have criteria</span>
23848
+ </div>
23849
+
23850
+ <div class="hero">
23851
+ <div class="hero-text">
23852
+ <h1>What does "done" look like?</h1>
23853
+ <p>Success criteria and acceptance criteria — organized by delivery status</p>
23854
+ </div>
23855
+ <div class="stats-row">
23856
+ <div class="stat-card">
23857
+ <div class="stat-num">${frozenWithCriteria}</div>
23858
+ <div class="stat-label">criteria met</div>
23859
+ </div>
23860
+ <div class="stat-card">
23861
+ <div class="stat-num">${withCriteria.filter((f) => f["status"] === "active").length}</div>
23862
+ <div class="stat-label">in progress</div>
23863
+ </div>
23864
+ <div class="stat-card">
23865
+ <div class="stat-num">${criteriaPct}%</div>
23866
+ <div class="stat-label">coverage</div>
23867
+ </div>
23868
+ <div class="stat-card">
23869
+ <div class="stat-num">${frozenCount}</div>
23870
+ <div class="stat-label">shipped total</div>
23871
+ </div>
23872
+ </div>
23873
+ </div>
23874
+
23875
+ <div class="progress-bar-wrap">
23876
+ <div class="progress-bar-label">${criteriaPct}% of features have defined success criteria</div>
23877
+ <div class="progress-bar"><div class="progress-fill" style="width:${criteriaPct}%"></div></div>
23878
+ </div>
23879
+
23880
+ <div class="filters">
23881
+ <span class="filter-label">Domain:</span>
23882
+ <span class="domain-pill active" data-domain="__all__" onclick="toggleDomain('__all__')">All <span class="pill-count">${withCriteria.length}</span></span>
23883
+ ${domainPills}
23884
+ </div>
23885
+
23886
+ <div class="board">
23887
+ ${columnHtml}
23888
+ </div>
23889
+
23890
+ ${withoutCriteria.length > 0 ? `
23891
+ <div class="missing-section">
23892
+ <div class="missing-title">⚠ ${withoutCriteria.length} feature${withoutCriteria.length !== 1 ? "s" : ""} missing success criteria${withoutCriteria.length > 12 ? ` (showing 12 of ${withoutCriteria.length})` : ""}</div>
23893
+ <div class="missing-grid">${missingCards}</div>
23894
+ </div>` : ""}
23895
+
23896
+ <script>
23897
+ const DATA = ${dataJson};
23898
+ let activeDomain = '__all__';
23899
+
23900
+ function toggleDomain(d) {
23901
+ activeDomain = d;
23902
+ document.querySelectorAll('.domain-pill').forEach(el => {
23903
+ el.classList.toggle('active', el.dataset.domain === d);
23904
+ });
23905
+ document.querySelectorAll('.card').forEach(el => {
23906
+ const match = d === '__all__' || el.dataset.domain === d;
23907
+ el.style.display = match ? '' : 'none';
23908
+ });
23909
+ // Update column counts
23910
+ ['frozen','active','draft'].forEach(status => {
23911
+ const col = document.getElementById('col-' + status);
23912
+ if (!col) return;
23913
+ const visible = col.querySelectorAll('.card:not([style*="none"])').length;
23914
+ const hdr = col.closest('.column').querySelector('.col-count');
23915
+ if (hdr) hdr.textContent = visible;
23916
+ });
23917
+ }
23918
+ <\/script>
23919
+ </body>
23920
+ </html>`;
23921
+ }
23922
+ //#endregion
23923
+ //#region src/lib/pitchGenerator.ts
23924
+ function esc$2(s) {
23925
+ return String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
23926
+ }
23927
+ function firstSentence(s) {
23928
+ if (typeof s !== "string") return "";
23929
+ const match = s.match(/^[^.!?]*[.!?]/);
23930
+ return match ? match[0].trim() : s.trim().slice(0, 120);
23931
+ }
23932
+ function hasText(v, min = 1) {
23933
+ return typeof v === "string" && v.trim().length >= min;
23934
+ }
23935
+ const DOMAIN_HUE = {
23936
+ "app-shell": 215,
23937
+ "auth": 195,
23938
+ "recording": 10,
23939
+ "editing": 35,
23940
+ "sessions": 265,
23941
+ "versioning": 155,
23942
+ "collaboration": 320,
23943
+ "band": 55,
23944
+ "render": 180,
23945
+ "storage": 240
23946
+ };
23947
+ function domainBg(domain) {
23948
+ return `radial-gradient(ellipse at 30% 40%, hsl(${DOMAIN_HUE[domain] ?? 215},28%,12%) 0%, #0d0b09 70%)`;
23949
+ }
23950
+ const STATUS_COLOR$1 = {
23951
+ frozen: "#5b82cc",
23952
+ active: "#4aad72",
23953
+ draft: "#c4a255",
23954
+ deprecated: "#664444"
23955
+ };
23956
+ function generatePitch(features, projectName) {
23957
+ const domains = [...new Set(features.map((f) => f["domain"] || "misc"))].sort();
23958
+ const byDomain = /* @__PURE__ */ new Map();
23959
+ for (const f of features) {
23960
+ const d = f["domain"] || "misc";
23961
+ if (!byDomain.has(d)) byDomain.set(d, []);
23962
+ byDomain.get(d).push(f);
23963
+ }
23964
+ const frozen = features.filter((f) => f["status"] === "frozen");
23965
+ const active = features.filter((f) => f["status"] === "active");
23966
+ const draft = features.filter((f) => f["status"] === "draft");
23967
+ const frozenWithGuide = frozen.filter((f) => hasText(f["userGuide"], 20)).sort((a, b) => (a["priority"] ?? 99) - (b["priority"] ?? 99));
23968
+ const allDecisions = [];
23969
+ for (const f of features) {
23970
+ const decs = f["decisions"];
23971
+ if (!Array.isArray(decs)) continue;
23972
+ for (const d of decs) {
23973
+ const obj = d;
23974
+ const rationale = typeof obj["rationale"] === "string" ? obj["rationale"] : "";
23975
+ if (rationale.length > 40) allDecisions.push({
23976
+ decision: String(obj["decision"] ?? ""),
23977
+ rationale,
23978
+ domain: f["domain"] || "",
23979
+ feature: f["title"] || ""
23980
+ });
23981
+ }
23982
+ }
23983
+ allDecisions.sort((a, b) => b.rationale.length - a.rationale.length);
23984
+ const topDecisions = allDecisions.slice(0, 3);
23985
+ const taglineSource = frozen.sort((a, b) => (a["priority"] ?? 99) - (b["priority"] ?? 99))[0];
23986
+ const tagline = taglineSource ? firstSentence(taglineSource["problem"]) : "";
23987
+ const slides = [];
23988
+ slides.push({
23989
+ type: "cover",
23990
+ title: projectName,
23991
+ tagline: tagline || `${features.length} features across ${domains.length} domains`,
23992
+ stats: {
23993
+ total: features.length,
23994
+ frozen: frozen.length,
23995
+ active: active.length,
23996
+ draft: draft.length,
23997
+ domains: domains.length
23998
+ }
23999
+ });
24000
+ slides.push({
24001
+ type: "overview",
24002
+ title: "At a Glance",
24003
+ stats: {
24004
+ frozen: frozen.length,
24005
+ active: active.length,
24006
+ draft: draft.length
24007
+ },
24008
+ domains
24009
+ });
24010
+ for (const domain of domains) {
24011
+ const domFeats = (byDomain.get(domain) ?? []).sort((a, b) => (a["priority"] ?? 99) - (b["priority"] ?? 99));
24012
+ slides.push({
24013
+ type: "domain",
24014
+ domain,
24015
+ title: domain.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
24016
+ features: domFeats.slice(0, 8).map((f) => ({
24017
+ title: f["title"],
24018
+ status: f["status"],
24019
+ key: f["featureKey"]
24020
+ })),
24021
+ total: domFeats.length,
24022
+ frozen: domFeats.filter((f) => f["status"] === "frozen").length
24023
+ });
24024
+ }
24025
+ for (const f of frozenWithGuide.slice(0, 12)) {
24026
+ const decs = Array.isArray(f["decisions"]) ? f["decisions"] : [];
24027
+ const topDec = decs[0] ? String(decs[0]["rationale"] ?? "") : "";
24028
+ slides.push({
24029
+ type: "feature",
24030
+ domain: f["domain"] || "",
24031
+ title: f["title"],
24032
+ userGuide: f["userGuide"],
24033
+ keyDecision: firstSentence(topDec),
24034
+ key: f["featureKey"]
24035
+ });
24036
+ }
24037
+ if (topDecisions.length > 0) slides.push({
24038
+ type: "decisions",
24039
+ title: "What We Decided",
24040
+ decisions: topDecisions.map((d) => ({
24041
+ decision: d.decision.slice(0, 100),
24042
+ rationale: firstSentence(d.rationale),
24043
+ domain: d.domain,
24044
+ feature: d.feature
24045
+ }))
24046
+ });
24047
+ if (active.length > 0) slides.push({
24048
+ type: "roadmap",
24049
+ title: "What's Next",
24050
+ features: active.sort((a, b) => (a["priority"] ?? 99) - (b["priority"] ?? 99)).slice(0, 8).map((f) => ({
24051
+ title: f["title"],
24052
+ domain: f["domain"],
24053
+ priority: f["priority"],
24054
+ problem: firstSentence(f["problem"])
24055
+ }))
24056
+ });
24057
+ slides.push({
24058
+ type: "outro",
24059
+ stats: {
24060
+ frozen: frozen.length,
24061
+ domains: domains.length,
24062
+ total: features.length
24063
+ }
24064
+ });
24065
+ const slidesJson = JSON.stringify(slides).replace(/<\/script>/gi, "<\\/script>");
24066
+ const domainBgMap = {};
24067
+ for (const d of domains) domainBgMap[d] = domainBg(d);
24068
+ const domainBgJson = JSON.stringify(domainBgMap).replace(/<\/script>/gi, "<\\/script>");
24069
+ const statusColorJson = JSON.stringify(STATUS_COLOR$1).replace(/<\/script>/gi, "<\\/script>");
24070
+ return `<!DOCTYPE html>
24071
+ <html lang="en">
24072
+ <head>
24073
+ <meta charset="UTF-8">
24074
+ <meta name="viewport" content="width=device-width,initial-scale=1">
24075
+ <title>${esc$2(projectName)} — Pitch Deck</title>
24076
+ <style>
24077
+ * { box-sizing: border-box; margin: 0; padding: 0; }
24078
+ html, body { width: 100%; height: 100%; overflow: hidden; background: #0d0b09; color: #e8e0d4; font-family: system-ui, -apple-system, sans-serif; }
24079
+
24080
+ .deck { position: fixed; inset: 0; }
24081
+
24082
+ /* Slide */
24083
+ .slide {
24084
+ position: absolute; inset: 0;
24085
+ display: flex; flex-direction: column;
24086
+ justify-content: center; align-items: center;
24087
+ padding: 48px 64px;
24088
+ transform: translateX(100%);
24089
+ transition: transform 0.42s cubic-bezier(0.4, 0, 0.2, 1);
24090
+ will-change: transform;
24091
+ }
24092
+ .slide.active { transform: translateX(0); }
24093
+ .slide.prev { transform: translateX(-100%); }
24094
+
24095
+ /* Cover */
24096
+ .cover { background: radial-gradient(ellipse at 30% 30%, #1a1508 0%, #0d0b09 65%); }
24097
+ .cover-eyebrow { font-size: 13px; color: #d4a853; font-family: monospace; letter-spacing: .15em; text-transform: uppercase; margin-bottom: 20px; }
24098
+ .cover-title { font-size: clamp(36px, 6vw, 72px); font-weight: 800; text-align: center; line-height: 1.1; letter-spacing: -.02em; margin-bottom: 20px; }
24099
+ .cover-tagline { font-size: clamp(14px, 2vw, 20px); color: #8a7f74; text-align: center; max-width: 640px; line-height: 1.55; margin-bottom: 40px; }
24100
+ .cover-pills { display: flex; gap: 14px; flex-wrap: wrap; justify-content: center; }
24101
+ .cover-pill { font-size: 13px; padding: 6px 16px; border-radius: 20px; border: 1px solid #2a2724; color: #8a7f74; font-family: monospace; }
24102
+ .cover-pill span { color: #d4a853; font-weight: 700; }
24103
+
24104
+ /* Overview */
24105
+ .overview { background: #0d0b09; }
24106
+ .slide-title { font-size: clamp(24px, 4vw, 48px); font-weight: 700; text-align: center; margin-bottom: 40px; letter-spacing: -.01em; }
24107
+ .overview-stats { display: flex; gap: 40px; margin-bottom: 48px; flex-wrap: wrap; justify-content: center; }
24108
+ .stat-block { text-align: center; }
24109
+ .stat-num { font-size: clamp(48px, 7vw, 88px); font-weight: 800; font-family: monospace; }
24110
+ .stat-label { font-size: 14px; color: #8a7f74; margin-top: 4px; }
24111
+ .domain-cloud { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; max-width: 700px; }
24112
+ .domain-chip { font-size: 13px; padding: 6px 14px; border-radius: 14px; background: #1a1714; border: 1px solid #2a2724; color: #8a7f74; }
24113
+
24114
+ /* Domain slide */
24115
+ .domain-slide { text-align: left; align-items: flex-start; }
24116
+ .domain-eyebrow { font-size: 11px; color: #d4a853; font-family: monospace; letter-spacing: .15em; text-transform: uppercase; margin-bottom: 16px; }
24117
+ .domain-title { font-size: clamp(32px, 5vw, 64px); font-weight: 800; letter-spacing: -.02em; margin-bottom: 32px; }
24118
+ .domain-features { display: flex; flex-direction: column; gap: 10px; max-width: 640px; }
24119
+ .domain-feat { display: flex; align-items: center; gap: 12px; }
24120
+ .feat-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
24121
+ .feat-title { font-size: clamp(13px, 1.8vw, 18px); color: #c8c0b4; line-height: 1.3; }
24122
+ .domain-stat { margin-top: 32px; font-size: 13px; color: #8a7f74; font-family: monospace; }
24123
+
24124
+ /* Feature slide */
24125
+ .feature-slide { align-items: flex-start; text-align: left; }
24126
+ .feature-domain { font-size: 11px; color: #d4a853; font-family: monospace; letter-spacing: .15em; text-transform: uppercase; margin-bottom: 16px; }
24127
+ .feature-title { font-size: clamp(24px, 4vw, 48px); font-weight: 700; line-height: 1.2; letter-spacing: -.01em; margin-bottom: 24px; max-width: 800px; }
24128
+ .feature-guide-label { font-size: 11px; color: #4aad72; font-family: monospace; text-transform: uppercase; letter-spacing: .1em; margin-bottom: 10px; }
24129
+ .feature-guide { font-size: clamp(15px, 2vw, 20px); color: #c8c0b4; line-height: 1.65; max-width: 680px; margin-bottom: 28px; }
24130
+ .feature-decision { font-size: 13px; color: #8a7f74; border-left: 2px solid #d4a853; padding-left: 12px; max-width: 600px; line-height: 1.5; }
24131
+ .feature-key { font-size: 10px; color: #3a3530; font-family: monospace; position: absolute; bottom: 60px; right: 64px; }
24132
+
24133
+ /* Decisions slide */
24134
+ .decisions-grid { display: flex; gap: 24px; flex-wrap: wrap; justify-content: center; max-width: 960px; }
24135
+ .decision-card {
24136
+ flex: 1; min-width: 240px; max-width: 280px;
24137
+ background: #1a1714; border: 1px solid #2a2724; border-radius: 12px;
24138
+ padding: 20px 18px;
24139
+ }
24140
+ .decision-meta { font-size: 10px; color: #8a7f74; font-family: monospace; margin-bottom: 10px; }
24141
+ .decision-text { font-size: 14px; font-weight: 600; color: #e8e0d4; line-height: 1.4; margin-bottom: 10px; }
24142
+ .decision-rationale { font-size: 12px; color: #8a7f74; line-height: 1.55; }
24143
+
24144
+ /* Roadmap */
24145
+ .roadmap-grid { display: flex; gap: 14px; flex-wrap: wrap; justify-content: center; max-width: 960px; }
24146
+ .roadmap-card {
24147
+ background: #1a1714; border: 1px solid #2a2724; border-radius: 10px;
24148
+ padding: 16px 16px 14px; min-width: 200px; max-width: 240px; flex: 1;
24149
+ }
24150
+ .roadmap-domain { font-size: 10px; color: #d4a853; font-family: monospace; margin-bottom: 6px; }
24151
+ .roadmap-title { font-size: 13px; font-weight: 600; line-height: 1.35; margin-bottom: 6px; }
24152
+ .roadmap-problem { font-size: 11px; color: #8a7f74; line-height: 1.4; }
24153
+
24154
+ /* Outro */
24155
+ .outro { background: radial-gradient(ellipse at 70% 60%, #0f1008 0%, #0d0b09 65%); }
24156
+ .outro-title { font-size: clamp(28px, 5vw, 56px); font-weight: 800; margin-bottom: 12px; letter-spacing: -.01em; }
24157
+ .outro-sub { font-size: 16px; color: #8a7f74; margin-bottom: 40px; }
24158
+ .outro-stats { display: flex; gap: 32px; flex-wrap: wrap; justify-content: center; margin-bottom: 40px; }
24159
+ .outro-stat { text-align: center; }
24160
+ .outro-num { font-size: 40px; font-weight: 800; font-family: monospace; color: #d4a853; }
24161
+ .outro-label { font-size: 12px; color: #8a7f74; }
24162
+ .lac-badge { font-size: 12px; color: #4a4540; font-family: monospace; }
24163
+
24164
+ /* Nav */
24165
+ .nav { position: fixed; bottom: 0; left: 0; right: 0; display: flex; align-items: center; padding: 0 32px; height: 52px; background: rgba(13,11,9,.85); backdrop-filter: blur(8px); z-index: 100; gap: 16px; }
24166
+ .progress-track { flex: 1; height: 2px; background: #2a2724; border-radius: 1px; overflow: hidden; }
24167
+ .progress-bar { height: 100%; background: #d4a853; border-radius: 1px; transition: width .3s ease; }
24168
+ .slide-counter { font-size: 12px; color: #8a7f74; font-family: monospace; min-width: 48px; text-align: right; }
24169
+ .nav-btn { width: 32px; height: 32px; border-radius: 6px; border: 1px solid #2a2724; background: transparent; color: #8a7f74; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; transition: all .15s; }
24170
+ .nav-btn:hover { border-color: #d4a853; color: #d4a853; }
24171
+ .key-hint { font-size: 10px; color: #3a3530; font-family: monospace; }
24172
+
24173
+ /* Grid overview */
24174
+ .grid-overlay {
24175
+ position: fixed; inset: 0; background: rgba(13,11,9,.96); z-index: 200;
24176
+ display: none; overflow-y: auto; padding: 24px;
24177
+ }
24178
+ .grid-overlay.visible { display: block; }
24179
+ .grid-title { font-size: 14px; color: #8a7f74; font-family: monospace; margin-bottom: 16px; letter-spacing: .05em; }
24180
+ .grid-slides { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; }
24181
+ .grid-thumb {
24182
+ aspect-ratio: 16/9; border: 1px solid #2a2724; border-radius: 6px; background: #1a1714;
24183
+ cursor: pointer; display: flex; flex-direction: column; justify-content: center; align-items: center;
24184
+ padding: 10px; text-align: center; transition: border-color .15s; overflow: hidden;
24185
+ }
24186
+ .grid-thumb:hover { border-color: #d4a853; }
24187
+ .grid-thumb.active-thumb { border-color: #d4a853; border-width: 2px; }
24188
+ .grid-num { font-size: 9px; color: #4a4540; font-family: monospace; margin-bottom: 4px; }
24189
+ .grid-thumb-title { font-size: 10px; color: #8a7f74; line-height: 1.3; }
24190
+ .grid-thumb-type { font-size: 9px; color: #4a4540; font-family: monospace; margin-top: 3px; }
24191
+ </style>
24192
+ </head>
24193
+ <body>
24194
+ <div class="deck" id="deck"></div>
24195
+
24196
+ <div class="nav">
24197
+ <button class="nav-btn" onclick="prev()" title="Previous (←)">‹</button>
24198
+ <div class="progress-track"><div class="progress-bar" id="prog"></div></div>
24199
+ <div class="slide-counter" id="counter">1 / ${slides.length}</div>
24200
+ <button class="nav-btn" onclick="next()" title="Next (→ or Space)">›</button>
24201
+ <span class="key-hint">G=grid P=notes</span>
24202
+ </div>
24203
+
24204
+ <div class="grid-overlay" id="grid-overlay">
24205
+ <div class="grid-title">◈ SLIDE OVERVIEW — click to jump · G or Esc to close</div>
24206
+ <div class="grid-slides" id="grid-slides"></div>
24207
+ </div>
24208
+
24209
+ <script>
24210
+ const SLIDES = ${slidesJson};
24211
+ const DOMAIN_BG = ${domainBgJson};
24212
+ const STATUS_COLOR = ${statusColorJson};
24213
+
24214
+ let current = 0;
24215
+ let presenterMode = false;
24216
+ let gridVisible = false;
24217
+
24218
+ function esc(s) {
24219
+ if (s == null) return '';
24220
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
24221
+ }
24222
+
24223
+ function renderSlide(slide, idx) {
24224
+ const isActive = idx === current;
24225
+ const isPrev = idx < current;
24226
+ const cls = isActive ? 'active' : isPrev ? 'prev' : '';
24227
+
24228
+ if (slide.type === 'cover') {
24229
+ const s = slide.stats;
24230
+ return \`<div class="slide cover \${cls}" data-idx="\${idx}">
24231
+ <div class="cover-eyebrow">◈ life-as-code</div>
24232
+ <div class="cover-title">\${esc(slide.title)}</div>
24233
+ \${slide.tagline ? \`<div class="cover-tagline">\${esc(slide.tagline)}</div>\` : ''}
24234
+ <div class="cover-pills">
24235
+ <div class="cover-pill"><span>\${s.frozen}</span> shipped</div>
24236
+ <div class="cover-pill"><span>\${s.active}</span> active</div>
24237
+ <div class="cover-pill"><span>\${s.draft}</span> planned</div>
24238
+ <div class="cover-pill"><span>\${s.domains}</span> domains</div>
24239
+ <div class="cover-pill"><span>\${s.total}</span> features</div>
24240
+ </div>
24241
+ </div>\`;
24242
+ }
24243
+
24244
+ if (slide.type === 'overview') {
24245
+ const s = slide.stats;
24246
+ return \`<div class="slide \${cls}" data-idx="\${idx}" style="background:#0d0b09">
24247
+ <div class="slide-title">\${esc(slide.title)}</div>
24248
+ <div class="overview-stats">
24249
+ <div class="stat-block"><div class="stat-num" style="color:#5b82cc">\${s.frozen}</div><div class="stat-label">shipped features</div></div>
24250
+ <div class="stat-block"><div class="stat-num" style="color:#4aad72">\${s.active}</div><div class="stat-label">in progress</div></div>
24251
+ <div class="stat-block"><div class="stat-num" style="color:#c4a255">\${s.draft}</div><div class="stat-label">planned</div></div>
24252
+ </div>
24253
+ <div class="domain-cloud">
24254
+ \${slide.domains.map(d => \`<span class="domain-chip">\${esc(d)}</span>\`).join('')}
24255
+ </div>
24256
+ </div>\`;
24257
+ }
24258
+
24259
+ if (slide.type === 'domain') {
24260
+ const bg = DOMAIN_BG[slide.domain] || 'radial-gradient(ellipse at 30% 40%, #181410 0%, #0d0b09 70%)';
24261
+ return \`<div class="slide domain-slide \${cls}" data-idx="\${idx}" style="background:\${bg}">
24262
+ <div class="domain-eyebrow">domain</div>
24263
+ <div class="domain-title">\${esc(slide.title)}</div>
24264
+ <div class="domain-features">
24265
+ \${slide.features.map(f => \`<div class="domain-feat">
24266
+ <span class="feat-dot" style="background:\${STATUS_COLOR[f.status]||'#888'}"></span>
24267
+ <span class="feat-title">\${esc(f.title)}</span>
24268
+ </div>\`).join('')}
24269
+ </div>
24270
+ <div class="domain-stat">\${slide.total} feature\${slide.total!==1?'s':''} · \${slide.frozen} shipped</div>
24271
+ </div>\`;
24272
+ }
24273
+
24274
+ if (slide.type === 'feature') {
24275
+ const bg = DOMAIN_BG[slide.domain] || 'radial-gradient(ellipse at 30% 40%, #181410 0%, #0d0b09 70%)';
24276
+ return \`<div class="slide feature-slide \${cls}" data-idx="\${idx}" style="background:\${bg}">
24277
+ <div class="feature-domain">\${esc(slide.domain)}</div>
24278
+ <div class="feature-title">\${esc(slide.title)}</div>
24279
+ <div class="feature-guide-label">what you can do</div>
24280
+ <div class="feature-guide">\${esc(String(slide.userGuide||''))}</div>
24281
+ \${slide.keyDecision && presenterMode ? \`<div class="feature-decision">💡 \${esc(slide.keyDecision)}</div>\` : ''}
24282
+ <div class="feature-key">\${esc(slide.key)}</div>
24283
+ </div>\`;
24284
+ }
24285
+
24286
+ if (slide.type === 'decisions') {
24287
+ return \`<div class="slide \${cls}" data-idx="\${idx}" style="background:#0d0b09">
24288
+ <div class="slide-title">\${esc(slide.title)}</div>
24289
+ <div class="decisions-grid">
24290
+ \${slide.decisions.map(d => \`<div class="decision-card">
24291
+ <div class="decision-meta">\${esc(d.domain)} · \${esc(d.feature.slice(0,40))}</div>
24292
+ <div class="decision-text">\${esc(d.decision)}</div>
24293
+ <div class="decision-rationale">\${esc(d.rationale)}</div>
24294
+ </div>\`).join('')}
24295
+ </div>
24296
+ </div>\`;
24297
+ }
24298
+
24299
+ if (slide.type === 'roadmap') {
24300
+ return \`<div class="slide \${cls}" data-idx="\${idx}" style="background:#0d0b09">
24301
+ <div class="slide-title">\${esc(slide.title)}</div>
24302
+ <div class="roadmap-grid">
24303
+ \${slide.features.map(f => \`<div class="roadmap-card">
24304
+ \${f.domain ? \`<div class="roadmap-domain">\${esc(f.domain)}</div>\` : ''}
24305
+ <div class="roadmap-title">\${esc(f.title)}</div>
24306
+ \${f.problem ? \`<div class="roadmap-problem">\${esc(f.problem)}</div>\` : ''}
24307
+ </div>\`).join('')}
24308
+ </div>
24309
+ </div>\`;
24310
+ }
24311
+
24312
+ if (slide.type === 'outro') {
24313
+ const s = slide.stats;
24314
+ return \`<div class="slide outro \${cls}" data-idx="\${idx}">
24315
+ <div class="outro-title">\${esc(SLIDES[0].title)}</div>
24316
+ <div class="outro-sub">Built with life-as-code</div>
24317
+ <div class="outro-stats">
24318
+ <div class="outro-stat"><div class="outro-num">\${s.frozen}</div><div class="outro-label">features shipped</div></div>
24319
+ <div class="outro-stat"><div class="outro-num">\${s.domains}</div><div class="outro-label">domains</div></div>
24320
+ <div class="outro-stat"><div class="outro-num">\${s.total}</div><div class="outro-label">total features</div></div>
24321
+ </div>
24322
+ <div class="lac-badge">◈ lac · /lac/</div>
24323
+ </div>\`;
24324
+ }
24325
+
24326
+ return \`<div class="slide \${cls}" data-idx="\${idx}" style="background:#0d0b09">
24327
+ <div class="slide-title">\${esc(slide.title||slide.type)}</div>
24328
+ </div>\`;
24329
+ }
24330
+
24331
+ function render() {
24332
+ const deck = document.getElementById('deck');
24333
+ const start = Math.max(0, current - 1);
24334
+ const end = Math.min(SLIDES.length - 1, current + 1);
24335
+
24336
+ // Remove stale slides
24337
+ deck.querySelectorAll('.slide').forEach(el => {
24338
+ const idx = parseInt(el.dataset.idx, 10);
24339
+ if (idx < start || idx > end) el.remove();
24340
+ });
24341
+
24342
+ // Render/update visible slides
24343
+ for (let i = start; i <= end; i++) {
24344
+ const existing = deck.querySelector(\`.slide[data-idx="\${i}"]\`);
24345
+ const html = renderSlide(SLIDES[i], i);
24346
+ if (existing) {
24347
+ const cls = i === current ? 'active' : i < current ? 'prev' : '';
24348
+ existing.className = existing.className.replace(/\\b(active|prev)\\b/g, '').trim() + (cls ? ' ' + cls : '');
24349
+ } else {
24350
+ deck.insertAdjacentHTML('beforeend', html);
24351
+ }
24352
+ }
24353
+
24354
+ // Progress
24355
+ const pct = SLIDES.length > 1 ? (current / (SLIDES.length - 1)) * 100 : 100;
24356
+ document.getElementById('prog').style.width = pct + '%';
24357
+ document.getElementById('counter').textContent = (current + 1) + ' / ' + SLIDES.length;
24358
+
24359
+ // Grid thumbs
24360
+ document.querySelectorAll('.grid-thumb').forEach(el => {
24361
+ const idx = parseInt(el.dataset.idx, 10);
24362
+ el.classList.toggle('active-thumb', idx === current);
24363
+ });
24364
+ }
24365
+
24366
+ function next() { if (current < SLIDES.length - 1) { current++; render(); } }
24367
+ function prev() { if (current > 0) { current--; render(); } }
24368
+
24369
+ function toggleGrid() {
24370
+ gridVisible = !gridVisible;
24371
+ const overlay = document.getElementById('grid-overlay');
24372
+ overlay.classList.toggle('visible', gridVisible);
24373
+ if (gridVisible && !document.getElementById('grid-slides').children.length) {
24374
+ document.getElementById('grid-slides').innerHTML = SLIDES.map((s, i) =>
24375
+ \`<div class="grid-thumb\${i===current?' active-thumb':''}" data-idx="\${i}" onclick="jumpTo(\${i})">
24376
+ <div class="grid-num">\${i+1}</div>
24377
+ <div class="grid-thumb-title">\${esc(s.title||s.type)}</div>
24378
+ <div class="grid-thumb-type">\${esc(s.type)}</div>
24379
+ </div>\`
24380
+ ).join('');
24381
+ }
24382
+ }
24383
+
24384
+ function jumpTo(idx) {
24385
+ current = idx;
24386
+ gridVisible = false;
24387
+ document.getElementById('grid-overlay').classList.remove('visible');
24388
+ render();
24389
+ }
24390
+
24391
+ // Keyboard
24392
+ document.addEventListener('keydown', e => {
24393
+ if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); next(); }
24394
+ else if (e.key === 'ArrowLeft') { e.preventDefault(); prev(); }
24395
+ else if (e.key === 'g' || e.key === 'G') toggleGrid();
24396
+ else if (e.key === 'Escape' && gridVisible) { gridVisible = false; document.getElementById('grid-overlay').classList.remove('visible'); }
24397
+ else if (e.key === 'p' || e.key === 'P') { presenterMode = !presenterMode; render(); }
24398
+ });
24399
+
24400
+ // Touch swipe
24401
+ let tx = 0;
24402
+ document.addEventListener('touchstart', e => { tx = e.touches[0].clientX; }, { passive: true });
24403
+ document.addEventListener('touchend', e => {
24404
+ const dx = e.changedTouches[0].clientX - tx;
24405
+ if (Math.abs(dx) > 50) dx < 0 ? next() : prev();
24406
+ });
24407
+
24408
+ // Init
24409
+ render();
24410
+ <\/script>
24411
+ </body>
24412
+ </html>`;
24413
+ }
24414
+ //#endregion
24415
+ //#region src/lib/timelineGenerator.ts
24416
+ function esc$1(s) {
24417
+ return String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
24418
+ }
24419
+ const STATUS_COLOR = {
24420
+ frozen: "#5b82cc",
24421
+ active: "#4aad72",
24422
+ draft: "#c4a255",
24423
+ deprecated: "#664444"
24424
+ };
24425
+ const DOMAIN_ORDER = [
24426
+ "app-shell",
24427
+ "auth",
24428
+ "recording",
24429
+ "editing",
24430
+ "sessions",
24431
+ "versioning",
24432
+ "collaboration",
24433
+ "band",
24434
+ "render",
24435
+ "storage"
24436
+ ];
24437
+ function getFeatureDates(f) {
24438
+ const history = f["statusHistory"];
24439
+ const transitions = [];
24440
+ if (Array.isArray(history)) {
24441
+ for (const entry of history) {
24442
+ const e = entry;
24443
+ const dateStr = e["date"];
24444
+ if (dateStr && typeof dateStr === "string") {
24445
+ const d = new Date(dateStr);
24446
+ if (!isNaN(d.getTime())) transitions.push({
24447
+ from: String(e["from"] ?? ""),
24448
+ to: String(e["to"] ?? ""),
24449
+ date: d
24450
+ });
24451
+ }
24452
+ }
24453
+ transitions.sort((a, b) => a.date.getTime() - b.date.getTime());
24454
+ }
24455
+ if (transitions.length === 0) return {
24456
+ start: null,
24457
+ end: null,
24458
+ transitions
24459
+ };
24460
+ const start = transitions[0].date;
24461
+ const lastStatus = f["status"] || "draft";
24462
+ return {
24463
+ start,
24464
+ end: lastStatus === "frozen" || lastStatus === "deprecated" ? transitions[transitions.length - 1].date : /* @__PURE__ */ new Date(),
24465
+ transitions
24466
+ };
24467
+ }
24468
+ function generateTimeline(features, projectName) {
24469
+ const today = /* @__PURE__ */ new Date();
24470
+ const allDates = [];
24471
+ for (const f of features) {
24472
+ const { start, end } = getFeatureDates(f);
24473
+ if (start) allDates.push(start);
24474
+ if (end) allDates.push(end);
24475
+ }
24476
+ const minDate = allDates.length > 0 ? new Date(Math.min(...allDates.map((d) => d.getTime()))) : new Date(today.getFullYear() - 1, 0, 1);
24477
+ const maxDate = today;
24478
+ const padded = (maxDate.getTime() - minDate.getTime()) * .04;
24479
+ const domainStart = new Date(minDate.getTime() - padded);
24480
+ const domainEnd = new Date(maxDate.getTime() + padded);
24481
+ const totalMs = domainEnd.getTime() - domainStart.getTime();
24482
+ function toPct(d) {
24483
+ return (d.getTime() - domainStart.getTime()) / totalMs * 100;
24484
+ }
24485
+ const domains = [...DOMAIN_ORDER.filter((d) => features.some((f) => f["domain"] === d)), ...[...new Set(features.map((f) => f["domain"] || "misc"))].filter((d) => !DOMAIN_ORDER.includes(d)).sort()];
24486
+ const byDomain = /* @__PURE__ */ new Map();
24487
+ for (const f of features) {
24488
+ const d = f["domain"] || "misc";
24489
+ if (!byDomain.has(d)) byDomain.set(d, []);
24490
+ byDomain.get(d).push(f);
24491
+ }
24492
+ const ticks = [];
24493
+ const cursor = new Date(domainStart.getFullYear(), domainStart.getMonth(), 1);
24494
+ while (cursor <= domainEnd) {
24495
+ ticks.push({
24496
+ date: new Date(cursor),
24497
+ label: cursor.toLocaleString("en", {
24498
+ month: "short",
24499
+ year: "2-digit"
24500
+ })
24501
+ });
24502
+ cursor.setMonth(cursor.getMonth() + 1);
24503
+ }
24504
+ const featData = features.map((f) => {
24505
+ const { start, end, transitions } = getFeatureDates(f);
24506
+ return {
24507
+ key: String(f["featureKey"] ?? ""),
24508
+ title: String(f["title"] ?? ""),
24509
+ status: String(f["status"] ?? "draft"),
24510
+ domain: String(f["domain"] ?? "misc"),
24511
+ startPct: start ? toPct(start) : null,
24512
+ endPct: end ? toPct(end) : null,
24513
+ hasHistory: transitions.length > 0,
24514
+ transitions: transitions.map((t) => ({
24515
+ from: t.from,
24516
+ to: t.to,
24517
+ date: t.date.toISOString().slice(0, 10)
24518
+ }))
24519
+ };
24520
+ });
24521
+ const dataJson = JSON.stringify({
24522
+ projectName,
24523
+ today: today.toISOString().slice(0, 10),
24524
+ todayPct: toPct(today),
24525
+ domains,
24526
+ features: featData,
24527
+ ticks: ticks.map((t) => ({
24528
+ label: t.label,
24529
+ pct: toPct(t.date)
24530
+ }))
24531
+ }).replace(/<\/script>/gi, "<\\/script>");
24532
+ const statusColorJson = JSON.stringify(STATUS_COLOR).replace(/<\/script>/gi, "<\\/script>");
24533
+ const featuresWithHistory = features.filter((f) => getFeatureDates(f).transitions.length > 0).length;
24534
+ const featuresWithoutHistory = features.length - featuresWithHistory;
24535
+ return `<!DOCTYPE html>
24536
+ <html lang="en">
24537
+ <head>
24538
+ <meta charset="UTF-8">
24539
+ <meta name="viewport" content="width=device-width,initial-scale=1">
24540
+ <title>${esc$1(projectName)} — Feature Timeline</title>
24541
+ <style>
24542
+ :root {
24543
+ --bg: #12100e; --bg-card: #1a1714; --bg-hover: #201d1a;
24544
+ --border: #2a2724; --border-soft: #221f1c;
24545
+ --text: #e8e0d4; --text-soft: #8a7f74; --accent: #d4a853;
24546
+ --lane-h: 52px; --label-w: 120px;
24547
+ --mono: 'SF Mono','Fira Code','Cascadia Code',monospace;
24548
+ }
24549
+ * { box-sizing: border-box; margin: 0; padding: 0; }
24550
+ html, body { height: 100%; background: var(--bg); color: var(--text); font-family: system-ui,-apple-system,sans-serif; overflow: hidden; display: flex; flex-direction: column; }
24551
+
24552
+ .topbar {
24553
+ display: flex; align-items: center; gap: 10px; padding: 0 20px; height: 48px;
24554
+ background: #0e0c0a; border-bottom: 1px solid var(--border); flex-shrink: 0; font-size: 13px;
24555
+ }
24556
+ .topbar-logo { color: var(--accent); font-weight: 700; font-family: var(--mono); }
24557
+ .topbar-sep { color: var(--border); }
24558
+ .topbar-count { margin-left: auto; color: var(--text-soft); font-size: 12px; font-family: var(--mono); }
24559
+
24560
+ .controls {
24561
+ display: flex; align-items: center; gap: 12px; padding: 10px 20px;
24562
+ background: var(--bg-card); border-bottom: 1px solid var(--border); flex-shrink: 0;
24563
+ font-size: 12px;
24564
+ }
24565
+ .ctrl-label { color: var(--text-soft); font-family: var(--mono); }
24566
+ .ctrl-btn {
24567
+ padding: 4px 12px; border-radius: 4px; border: 1px solid var(--border);
24568
+ background: transparent; color: var(--text-soft); cursor: pointer; font-size: 12px; font-family: var(--mono);
24569
+ transition: all .15s;
24570
+ }
24571
+ .ctrl-btn:hover { border-color: var(--accent); color: var(--accent); }
24572
+ .ctrl-sep { color: var(--border); }
24573
+ .legend { display: flex; gap: 14px; margin-left: auto; }
24574
+ .leg-item { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--text-soft); }
24575
+ .leg-dot { width: 10px; height: 10px; border-radius: 2px; }
24576
+ .zoom-info { font-size: 11px; color: var(--text-soft); font-family: var(--mono); }
24577
+
24578
+ .timeline-wrap { flex: 1; overflow: hidden; position: relative; display: flex; flex-direction: column; }
24579
+
24580
+ /* Tick labels row */
24581
+ .tick-row {
24582
+ display: flex; height: 32px; flex-shrink: 0;
24583
+ padding-left: var(--label-w); position: relative; overflow: hidden;
24584
+ border-bottom: 1px solid var(--border);
24585
+ }
24586
+ .tick-track { position: absolute; left: var(--label-w); right: 0; top: 0; bottom: 0; }
24587
+
24588
+ /* Lanes */
24589
+ .lanes-scroll { flex: 1; overflow-y: auto; overflow-x: hidden; position: relative; }
24590
+ .lanes-inner { position: relative; }
24591
+ .lane { display: flex; height: var(--lane-h); border-bottom: 1px solid var(--border-soft); }
24592
+ .lane:hover { background: rgba(255,255,255,.012); }
24593
+ .lane-label {
24594
+ width: var(--label-w); flex-shrink: 0; padding: 0 12px 0 16px;
24595
+ display: flex; align-items: center; border-right: 1px solid var(--border);
24596
+ position: sticky; left: 0; background: var(--bg); z-index: 2;
24597
+ }
24598
+ .lane-label-text { font-size: 11px; color: var(--text-soft); font-family: var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
24599
+ .lane-track { flex: 1; position: relative; }
24600
+
24601
+ /* Feature pill */
24602
+ .feat-pill {
24603
+ position: absolute; top: 8px; height: 34px;
24604
+ border-radius: 5px; cursor: pointer;
24605
+ display: flex; align-items: center; padding: 0 6px;
24606
+ transition: filter .15s, z-index .15s;
24607
+ border: 1px solid rgba(255,255,255,.08);
24608
+ min-width: 6px; overflow: hidden;
24609
+ }
24610
+ .feat-pill:hover { filter: brightness(1.3); z-index: 10; }
24611
+ .feat-pill-text { font-size: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: rgba(255,255,255,.85); font-family: var(--mono); }
24612
+
24613
+ /* Dot for features without history */
24614
+ .feat-dot {
24615
+ position: absolute; top: 50%; width: 8px; height: 8px;
24616
+ border-radius: 50%; transform: translate(-50%, -50%);
24617
+ cursor: pointer; border: 1px solid rgba(255,255,255,.2);
24618
+ }
24619
+
24620
+ /* Today line */
24621
+ .today-line { position: absolute; top: 0; bottom: 0; width: 1.5px; background: rgba(212,168,83,.5); pointer-events: none; z-index: 5; }
24622
+ .today-label { position: absolute; top: 4px; font-size: 9px; color: var(--accent); font-family: var(--mono); transform: translateX(-50%); white-space: nowrap; }
24623
+
24624
+ /* Tooltip */
24625
+ .tooltip {
24626
+ position: fixed; pointer-events: none; z-index: 999;
24627
+ background: #1e1b18; border: 1px solid var(--border); border-radius: 8px;
24628
+ padding: 12px 14px; font-size: 12px; color: var(--text); max-width: 260px;
24629
+ display: none; box-shadow: 0 8px 24px rgba(0,0,0,.5);
24630
+ }
24631
+ .tooltip.visible { display: block; }
24632
+ .tooltip-title { font-weight: 600; margin-bottom: 6px; font-size: 13px; line-height: 1.3; }
24633
+ .tooltip-row { display: flex; justify-content: space-between; gap: 20px; margin: 2px 0; font-size: 11px; }
24634
+ .tooltip-label { color: var(--text-soft); }
24635
+ .tooltip-val { font-family: var(--mono); }
24636
+ .tooltip-hist { margin-top: 8px; border-top: 1px solid var(--border); padding-top: 8px; }
24637
+ .tooltip-trans { font-size: 10px; color: var(--text-soft); font-family: var(--mono); margin: 1px 0; }
24638
+ </style>
24639
+ </head>
24640
+ <body>
24641
+ <div class="topbar">
24642
+ <span class="topbar-logo">◈ lac</span>
24643
+ <span class="topbar-sep">|</span>
24644
+ <span class="topbar-project">${esc$1(projectName)} — Feature Timeline</span>
24645
+ <span class="topbar-count">${features.length} features · ${featuresWithHistory} with history · ${featuresWithoutHistory} undated</span>
24646
+ </div>
24647
+
24648
+ <div class="controls">
24649
+ <span class="ctrl-label">Zoom:</span>
24650
+ <button class="ctrl-btn" onclick="zoom(0.7)">−</button>
24651
+ <span class="zoom-info" id="zoom-info">100%</span>
24652
+ <button class="ctrl-btn" onclick="zoom(1.4)">+</button>
24653
+ <button class="ctrl-btn" onclick="resetZoom()">Reset</button>
24654
+ <span class="ctrl-sep">|</span>
24655
+ <span class="ctrl-label">Sort:</span>
24656
+ <button class="ctrl-btn" onclick="setSortMode('domain')">Domain</button>
24657
+ <button class="ctrl-btn" onclick="setSortMode('start')">Start date</button>
24658
+ <button class="ctrl-btn" onclick="setSortMode('status')">Status</button>
24659
+ <div class="legend">
24660
+ ${Object.entries(STATUS_COLOR).map(([s, c]) => `<div class="leg-item"><div class="leg-dot" style="background:${c}"></div>${s}</div>`).join("")}
24661
+ <div class="leg-item"><div class="leg-dot" style="background:#3a3530;border:1px solid #5a5550"></div>no history</div>
24662
+ </div>
24663
+ </div>
24664
+
24665
+ <div class="timeline-wrap">
24666
+ <div class="tick-row">
24667
+ <div class="tick-track" id="tick-track"></div>
24668
+ </div>
24669
+ <div class="lanes-scroll">
24670
+ <div class="lanes-inner" id="lanes"></div>
24671
+ </div>
24672
+ </div>
24673
+
24674
+ <div class="tooltip" id="tooltip"></div>
24675
+
24676
+ <script>
24677
+ const DATA = ${dataJson};
24678
+ const STATUS_COLOR = ${statusColorJson};
24679
+
24680
+ let zoomLevel = 1;
24681
+ let sortMode = 'domain';
24682
+ const tooltip = document.getElementById('tooltip');
24683
+
24684
+ // Scale: zoomLevel * 100% width for the track
24685
+ function trackWidth() { return Math.round(zoomLevel * 100) + '%'; }
24686
+
24687
+ function pctToPx(pct) {
24688
+ const track = document.getElementById('lanes');
24689
+ return (pct / 100) * track.offsetWidth;
24690
+ }
24691
+
24692
+ function render() {
24693
+ document.getElementById('zoom-info').textContent = Math.round(zoomLevel * 100) + '%';
24694
+ renderTicks();
24695
+ renderLanes();
24696
+ }
24697
+
24698
+ function renderTicks() {
24699
+ const track = document.getElementById('tick-track');
24700
+ track.style.width = trackWidth();
24701
+ track.innerHTML = DATA.ticks.map(t =>
24702
+ t.pct >= 0 && t.pct <= 100
24703
+ ? \`<div style="position:absolute;left:\${t.pct}%;top:0;bottom:0;border-left:1px solid #2a2724;padding-top:8px">
24704
+ <span style="font-size:9px;color:#6a6055;font-family:var(--mono);padding-left:4px">\${t.label}</span>
24705
+ </div>\`
24706
+ : ''
24707
+ ).join('') + \`<div class="today-line" style="left:\${DATA.todayPct}%">
24708
+ <div class="today-label" style="left:50%">today</div>
24709
+ </div>\`;
24710
+ }
24711
+
24712
+ function renderLanes() {
24713
+ const lanesEl = document.getElementById('lanes');
24714
+ // Sort domains
24715
+ let domainOrder = [...DATA.domains];
24716
+ if (sortMode === 'status') {
24717
+ // sort domains by avg frozen ratio descending
24718
+ }
24719
+
24720
+ lanesEl.innerHTML = domainOrder.map(domain => {
24721
+ const domFeats = DATA.features.filter(f => f.domain === domain);
24722
+ // Sort features within domain
24723
+ let sorted = [...domFeats];
24724
+ if (sortMode === 'start') sorted.sort((a,b) => (a.startPct??101) - (b.startPct??101));
24725
+ else if (sortMode === 'status') sorted.sort((a,b) => a.status.localeCompare(b.status));
24726
+ else sorted.sort((a,b) => (a.startPct??101) - (b.startPct??101));
24727
+
24728
+ const laneH = Math.max(${DOMAIN_ORDER.length > 0 ? "Math.ceil(sorted.length / 1) * 44 + 8" : "52"}, 52);
24729
+
24730
+ const pills = sorted.map((f, fi) => {
24731
+ const color = STATUS_COLOR[f.status] || '#444';
24732
+ const bgAlpha = f.hasHistory ? '55' : '22';
24733
+ const top = 8 + Math.floor(fi * 0) ; // stack vertically — one row per feature lane
24734
+ if (f.startPct !== null && f.endPct !== null) {
24735
+ const left = Math.max(0, f.startPct);
24736
+ const width = Math.max(0.4, f.endPct - f.startPct);
24737
+ return \`<div class="feat-pill"
24738
+ style="left:\${left}%;width:\${width}%;background:\${color}\${bgAlpha};top:8px"
24739
+ data-key="\${f.key}"
24740
+ onmousemove="showTooltip(event, '\${f.key}')"
24741
+ onmouseleave="hideTooltip()"
24742
+ onclick="window.open('lac-wiki.html#\${f.key}','_self')">
24743
+ \${width > 2 ? \`<span class="feat-pill-text">\${f.title}</span>\` : ''}
24744
+ </div>\`;
24745
+ } else {
24746
+ // dot at end of timeline
24747
+ return \`<div class="feat-dot"
24748
+ style="left:\${DATA.todayPct}%;background:\${color}44;border-color:\${color}88"
24749
+ data-key="\${f.key}"
24750
+ onmousemove="showTooltip(event, '\${f.key}')"
24751
+ onmouseleave="hideTooltip()"
24752
+ onclick="window.open('lac-wiki.html#\${f.key}','_self')">
24753
+ </div>\`;
24754
+ }
24755
+ }).join('');
24756
+
24757
+ return \`<div class="lane" style="height:var(--lane-h)">
24758
+ <div class="lane-label"><span class="lane-label-text" title="\${domain}">\${domain}</span></div>
24759
+ <div class="lane-track" style="width:\${trackWidth()}">
24760
+ \${pills}
24761
+ <div class="today-line" style="left:\${DATA.todayPct}%"></div>
24762
+ </div>
24763
+ </div>\`;
24764
+ }).join('');
24765
+ }
24766
+
24767
+ const featByKey = new Map(DATA.features.map(f => [f.key, f]));
24768
+
24769
+ function showTooltip(e, key) {
24770
+ const f = featByKey.get(key);
24771
+ if (!f) return;
24772
+ const color = STATUS_COLOR[f.status] || '#888';
24773
+ const histHtml = f.transitions.length > 0
24774
+ ? '<div class="tooltip-hist">' + f.transitions.map(t =>
24775
+ \`<div class="tooltip-trans">\${t.date} · \${t.from || '–'} → \${t.to}</div>\`
24776
+ ).join('') + '</div>'
24777
+ : '<div class="tooltip-row"><span class="tooltip-label">history</span><span class="tooltip-val" style="color:#4a4540">not recorded</span></div>';
24778
+
24779
+ tooltip.innerHTML =
24780
+ \`<div class="tooltip-title">\${f.title}</div>\` +
24781
+ \`<div class="tooltip-row"><span class="tooltip-label">status</span><span class="tooltip-val" style="color:\${color}">\${f.status}</span></div>\` +
24782
+ \`<div class="tooltip-row"><span class="tooltip-label">domain</span><span class="tooltip-val">\${f.domain}</span></div>\` +
24783
+ \`<div class="tooltip-row"><span class="tooltip-label">key</span><span class="tooltip-val">\${f.key}</span></div>\` +
24784
+ histHtml;
24785
+
24786
+ tooltip.style.left = (e.clientX + 14) + 'px';
24787
+ tooltip.style.top = (e.clientY - 10) + 'px';
24788
+ tooltip.classList.add('visible');
24789
+ }
24790
+ function hideTooltip() { tooltip.classList.remove('visible'); }
24791
+
24792
+ function zoom(factor) {
24793
+ zoomLevel = Math.max(0.5, Math.min(8, zoomLevel * factor));
24794
+ render();
24795
+ }
24796
+ function resetZoom() { zoomLevel = 1; render(); }
24797
+ function setSortMode(mode) { sortMode = mode; render(); }
24798
+
24799
+ render();
24800
+ window.addEventListener('resize', render);
24801
+ <\/script>
24802
+ </body>
24803
+ </html>`;
24804
+ }
24805
+ //#endregion
23232
24806
  //#region src/lib/views.ts
23233
24807
  /** Fields always shown at summary density regardless of view */
23234
24808
  const SUMMARY_FIELDS = new Set([
@@ -23679,7 +25253,7 @@ function buildReconstructionPrompt(features, projectName, promptDir) {
23679
25253
  lines.push("");
23680
25254
  return lines.join("\n");
23681
25255
  }
23682
- const exportCommand = new Command("export").description("Export feature.json as JSON, Markdown, or generate a static HTML view").option("--out <path>", "Output file or directory path").option("--html [dir]", "Scan <dir> (default: cwd) and emit a single self-contained HTML wiki").option("--raw [dir]", "Raw field-by-field HTML dump with sidebar navigation").option("--print [dir]", "Print-ready HTML document (A4, all features, @media print CSS)").option("--postcard", "Beautiful single-feature shareable card (nearest feature.json)").option("--resume [dir]", "Portfolio page from all frozen features").option("--slide [dir]", "Full-screen HTML slideshow, one slide per feature").option("--graph [dir]", "Interactive force-directed feature lineage graph").option("--heatmap [dir]", "Completeness heatmap — fields × features grid").option("--quiz [dir]", "Flashcard-style quiz to test knowledge of your feature set").option("--story [dir]", "Long-form narrative document — product case study from feature data").option("--treemap [dir]", "Rectangular treemap — features sized by decisions × completeness, grouped by domain").option("--kanban [dir]", "Kanban board — Active / Frozen / Draft columns with sortable, filterable cards").option("--health [dir]", "Project health scorecard — completeness, coverage, tech debt, and health score").option("--embed [dir]", "Compact embeddable stats widget (iframe-ready)").option("--decisions [dir]", "Consolidated ADR — all decisions from all features, searchable by domain").option("--guide [dir]", "User guide — one page per feature that has a non-empty userGuide field").option("--hub [dir]", "Hub landing page linking to all generated views → lac-hub.html").option("--all [dir]", "Generate all HTML views + hub index.html → --out dir (default: ./lac-output)").option("--prefix <prefix>", "URL prefix for hub links (no leading slash), e.g. lac → hrefs become /lac/lac-guide.html").option("--diff <dir-b>", "Compare cwd workspace against <dir-b> and show added/removed/changed").option("--site <dir>", "Generate a multi-page static site → --out dir (default: ./lac-site)").option("--prompt [dir]", "AI reconstruction prompt for all features (stdout or --out file)").option("--markdown", "Single feature as Markdown (nearest feature.json)").option("--changelog [dir]", "Structured changelog grouped by month — from revisions[] across all features").option("--since <date>", "Filter --changelog and --release-notes to entries after this date (YYYY-MM-DD)").option("--release-notes [dir]", "User-facing release notes — features that went frozen since --since date or --release version").option("--release <version>", "Filter --release-notes to features matching this releaseVersion (e.g. 3.5.0)").option("--sprint [dir]", "Sprint planning view — draft+active features sorted by priority, summary density").option("--api-surface [dir]", "Aggregated publicInterface[] reference across all features → lac-api-surface.html").option("--dependency-map [dir]", "Runtime dependency graph from externalDependencies[] → lac-depmap.html").option("--tags <tags>", "Comma-separated tags to filter by (OR logic) — applies to all multi-feature modes").option("--sort <mode>", "Sort order for multi-feature modes: key (default) | build-order (parents before children)").option("--view <name>", `Audience view — built-in (${VIEW_NAMES.join(", ")}) or custom name from lac.config.json views`).option("--density <level>", "Content density: summary | standard | verbose (default: standard)").addHelpText("after", `
25256
+ const exportCommand = new Command("export").description("Export feature.json as JSON, Markdown, or generate a static HTML view").option("--out <path>", "Output file or directory path").option("--html [dir]", "Scan <dir> (default: cwd) and emit a single self-contained HTML wiki").option("--raw [dir]", "Raw field-by-field HTML dump with sidebar navigation").option("--print [dir]", "Print-ready HTML document (A4, all features, @media print CSS)").option("--postcard", "Beautiful single-feature shareable card (nearest feature.json)").option("--resume [dir]", "Portfolio page from all frozen features").option("--slide [dir]", "Full-screen HTML slideshow, one slide per feature").option("--graph [dir]", "Interactive force-directed feature lineage graph").option("--heatmap [dir]", "Completeness heatmap — fields × features grid").option("--quiz [dir]", "Flashcard-style quiz to test knowledge of your feature set").option("--story [dir]", "Long-form narrative document — product case study from feature data").option("--treemap [dir]", "Rectangular treemap — features sized by decisions × completeness, grouped by domain").option("--kanban [dir]", "Kanban board — Active / Frozen / Draft columns with sortable, filterable cards").option("--health [dir]", "Project health scorecard — completeness, coverage, tech debt, and health score").option("--embed [dir]", "Compact embeddable stats widget (iframe-ready)").option("--decisions [dir]", "Consolidated ADR — all decisions from all features, searchable by domain").option("--guide [dir]", "User guide — one page per feature that has a non-empty userGuide field").option("--hub [dir]", "Hub landing page linking to all generated views → lac-hub.html").option("--all [dir]", "Generate all HTML views + hub index.html → --out dir (default: ./lac-output)").option("--prefix <prefix>", "URL prefix for hub links (no leading slash), e.g. lac → hrefs become /lac/lac-guide.html").option("--diff <dir-b>", "Compare cwd workspace against <dir-b> and show added/removed/changed").option("--site <dir>", "Generate a multi-page static site → --out dir (default: ./lac-site)").option("--prompt [dir]", "AI reconstruction prompt for all features (stdout or --out file)").option("--markdown", "Single feature as Markdown (nearest feature.json)").option("--changelog [dir]", "Structured changelog grouped by month — from revisions[] across all features").option("--since <date>", "Filter --changelog and --release-notes to entries after this date (YYYY-MM-DD)").option("--release-notes [dir]", "User-facing release notes — features that went frozen since --since date or --release version").option("--release <version>", "Filter --release-notes to features matching this releaseVersion (e.g. 3.5.0)").option("--sprint [dir]", "Sprint planning view — draft+active features sorted by priority, summary density").option("--api-surface [dir]", "Aggregated publicInterface[] reference across all features → lac-api-surface.html").option("--dependency-map [dir]", "Runtime dependency graph from externalDependencies[] → lac-depmap.html").option("--radar [dir]", "Domain maturity radar — SVG polar chart across 5 quality dimensions → lac-radar.html").option("--successboard [dir]", "Success criteria board — achieved/in-progress/planned by successCriteria → lac-successboard.html").option("--pitch [dir]", "Demo slide deck — keyboard-navigable fullscreen presentation → lac-pitch.html").option("--timeline [dir]", "Feature velocity timeline — swim-lane history from statusHistory → lac-timeline.html").option("--tags <tags>", "Comma-separated tags to filter by (OR logic) — applies to all multi-feature modes").option("--sort <mode>", "Sort order for multi-feature modes: key (default) | build-order (parents before children)").option("--view <name>", `Audience view — built-in (${VIEW_NAMES.join(", ")}) or custom name from lac.config.json views`).option("--density <level>", "Content density: summary | standard | verbose (default: standard)").addHelpText("after", `
23683
25257
  Examples:
23684
25258
  lac export --html HTML wiki (cwd) → lac-wiki.html
23685
25259
  lac export --raw Raw field dump → lac-raw.html
@@ -23717,6 +25291,7 @@ Views (--view):
23717
25291
  tech Complete technical record — all fields including history and revisions`).action(async (options) => {
23718
25292
  const config = loadConfig(process$1.cwd());
23719
25293
  let activeView = options.view ? resolveView(options.view, config.views) : void 0;
25294
+ const activeViewRenderMode = options.view && config.views[options.view] ? config.views[options.view].extends : void 0;
23720
25295
  if (options.view && !activeView) {
23721
25296
  const customNames = Object.keys(config.views);
23722
25297
  const allNames = [...VIEW_NAMES, ...customNames];
@@ -23782,7 +25357,7 @@ Views (--view):
23782
25357
  }
23783
25358
  const projectName = basename(htmlDir);
23784
25359
  const densityFeatures = withDensity(features);
23785
- const html = generateHtmlWiki(activeView ? densityFeatures.map((f) => applyViewForHtml(f.feature, activeView)) : densityFeatures.map((f) => f.feature), projectName, activeView?.label, activeView?.name);
25360
+ const html = generateHtmlWiki(activeView ? densityFeatures.map((f) => applyViewForHtml(f.feature, activeView)) : densityFeatures.map((f) => f.feature), projectName, activeView?.label, activeView?.name, activeViewRenderMode);
23786
25361
  const outFile = options.out ? resolve(options.out) : resolve(process$1.cwd(), "lac-wiki.html");
23787
25362
  try {
23788
25363
  await writeFile(outFile, html, "utf-8");
@@ -24223,6 +25798,80 @@ Views (--view):
24223
25798
  }
24224
25799
  return;
24225
25800
  }
25801
+ if (options.radar !== void 0) {
25802
+ const dir = typeof options.radar === "string" ? resolve(options.radar) : resolve(process$1.cwd());
25803
+ const features = await scanAndFilter(dir);
25804
+ if (features.length === 0) {
25805
+ process$1.stdout.write(`No valid feature.json files found in "${dir}".\n`);
25806
+ process$1.exit(0);
25807
+ }
25808
+ const html = generateRadar(features.map((f) => f.feature), basename(dir));
25809
+ const outFile = options.out ? resolve(options.out) : resolve(process$1.cwd(), "lac-radar.html");
25810
+ try {
25811
+ await writeFile(outFile, html, "utf-8");
25812
+ process$1.stdout.write(`✓ Radar (${features.length} features, ${new Set(features.map((f) => f.feature.domain)).size} domains) → ${options.out ?? "lac-radar.html"}\n`);
25813
+ } catch (err) {
25814
+ process$1.stderr.write(`Error writing "${outFile}": ${err instanceof Error ? err.message : String(err)}\n`);
25815
+ process$1.exit(1);
25816
+ }
25817
+ return;
25818
+ }
25819
+ if (options.successboard !== void 0) {
25820
+ const dir = typeof options.successboard === "string" ? resolve(options.successboard) : resolve(process$1.cwd());
25821
+ const features = await scanAndFilter(dir);
25822
+ if (features.length === 0) {
25823
+ process$1.stdout.write(`No valid feature.json files found in "${dir}".\n`);
25824
+ process$1.exit(0);
25825
+ }
25826
+ const fs = features.map((f) => f.feature);
25827
+ const html = generateSuccessboard(fs, basename(dir));
25828
+ const withCriteria = fs.filter((f) => f["successCriteria"] || Array.isArray(f["acceptanceCriteria"])).length;
25829
+ const outFile = options.out ? resolve(options.out) : resolve(process$1.cwd(), "lac-successboard.html");
25830
+ try {
25831
+ await writeFile(outFile, html, "utf-8");
25832
+ process$1.stdout.write(`✓ Success board (${withCriteria}/${features.length} features with criteria) → ${options.out ?? "lac-successboard.html"}\n`);
25833
+ } catch (err) {
25834
+ process$1.stderr.write(`Error writing "${outFile}": ${err instanceof Error ? err.message : String(err)}\n`);
25835
+ process$1.exit(1);
25836
+ }
25837
+ return;
25838
+ }
25839
+ if (options.pitch !== void 0) {
25840
+ const dir = typeof options.pitch === "string" ? resolve(options.pitch) : resolve(process$1.cwd());
25841
+ const features = await scanAndFilter(dir);
25842
+ if (features.length === 0) {
25843
+ process$1.stdout.write(`No valid feature.json files found in "${dir}".\n`);
25844
+ process$1.exit(0);
25845
+ }
25846
+ const html = generatePitch(features.map((f) => f.feature), basename(dir));
25847
+ const outFile = options.out ? resolve(options.out) : resolve(process$1.cwd(), "lac-pitch.html");
25848
+ try {
25849
+ await writeFile(outFile, html, "utf-8");
25850
+ process$1.stdout.write(`✓ Pitch deck (${features.length} features) → ${options.out ?? "lac-pitch.html"}\n`);
25851
+ } catch (err) {
25852
+ process$1.stderr.write(`Error writing "${outFile}": ${err instanceof Error ? err.message : String(err)}\n`);
25853
+ process$1.exit(1);
25854
+ }
25855
+ return;
25856
+ }
25857
+ if (options.timeline !== void 0) {
25858
+ const dir = typeof options.timeline === "string" ? resolve(options.timeline) : resolve(process$1.cwd());
25859
+ const features = await scanAndFilter(dir);
25860
+ if (features.length === 0) {
25861
+ process$1.stdout.write(`No valid feature.json files found in "${dir}".\n`);
25862
+ process$1.exit(0);
25863
+ }
25864
+ const html = generateTimeline(features.map((f) => f.feature), basename(dir));
25865
+ const outFile = options.out ? resolve(options.out) : resolve(process$1.cwd(), "lac-timeline.html");
25866
+ try {
25867
+ await writeFile(outFile, html, "utf-8");
25868
+ process$1.stdout.write(`✓ Timeline (${features.length} features) → ${options.out ?? "lac-timeline.html"}\n`);
25869
+ } catch (err) {
25870
+ process$1.stderr.write(`Error writing "${outFile}": ${err instanceof Error ? err.message : String(err)}\n`);
25871
+ process$1.exit(1);
25872
+ }
25873
+ return;
25874
+ }
24226
25875
  if (options.all !== void 0) {
24227
25876
  const dir = typeof options.all === "string" ? resolve(options.all) : resolve(process$1.cwd());
24228
25877
  const outDir = resolve(options.out ?? "./lac-output");
@@ -24264,6 +25913,10 @@ Views (--view):
24264
25913
  await write("lac-sprint.html", generateSprint(fs.filter((f) => f.status === "draft" || f.status === "active"), projectName));
24265
25914
  await write("lac-api-surface.html", generateApiSurface(fs, projectName));
24266
25915
  await write("lac-depmap.html", generateDependencyMap(fs, projectName));
25916
+ await write("lac-radar.html", generateRadar(fs, projectName));
25917
+ await write("lac-successboard.html", generateSuccessboard(fs, projectName));
25918
+ await write("lac-pitch.html", generatePitch(fs, projectName));
25919
+ await write("lac-timeline.html", generateTimeline(fs, projectName));
24267
25920
  const stats = {
24268
25921
  total: fs.length,
24269
25922
  frozen: fs.filter((f) => f.status === "frozen").length,
@@ -24312,7 +25965,9 @@ Views (--view):
24312
25965
  filterStatus: resolved.filterStatus,
24313
25966
  sortBy: resolved.sortBy
24314
25967
  });
24315
- await write(filename, generateHtmlWiki(viewFeatures.map((f) => applyViewForHtml(f, resolved)), projectName, label, viewName));
25968
+ const viewHtmlFeatures = viewFeatures.map((f) => applyViewForHtml(f, resolved));
25969
+ const renderMode = viewDef.extends;
25970
+ await write(filename, generateHtmlWiki(viewHtmlFeatures, projectName, label, viewName, renderMode));
24316
25971
  customEntries.push({
24317
25972
  file: filename,
24318
25973
  label,
@@ -24322,7 +25977,7 @@ Views (--view):
24322
25977
  });
24323
25978
  }
24324
25979
  await write("index.html", generateHub(projectName, stats, [...ALL_HUB_ENTRIES, ...customEntries], (/* @__PURE__ */ new Date()).toISOString(), options.prefix));
24325
- const totalFiles = 15 + customEntries.length + 1;
25980
+ const totalFiles = 19 + customEntries.length + 1;
24326
25981
  process$1.stdout.write(`Done — ${features.length} features, ${totalFiles} files written to ${outDir}\n`);
24327
25982
  return;
24328
25983
  }