@hobocode/thought-layer 0.7.0 → 0.8.6

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/tl.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // bin/tl.ts
4
- import { readFileSync as readFileSync4 } from "fs";
4
+ import { readFileSync as readFileSync6 } from "fs";
5
5
 
6
6
  // core/scoring.ts
7
7
  var CONFIDENCE_GOAL = 0.85;
@@ -84,6 +84,7 @@ var KNOWN_STATE_KEYS = [
84
84
  "prd",
85
85
  "naming",
86
86
  "brand",
87
+ "governance",
87
88
  "kit"
88
89
  ];
89
90
  function emptyState() {
@@ -99,6 +100,7 @@ function emptyState() {
99
100
  prd: null,
100
101
  naming: null,
101
102
  brand: null,
103
+ governance: null,
102
104
  kit: null
103
105
  };
104
106
  }
@@ -115,12 +117,12 @@ function parseProgress(text) {
115
117
  const rawFormat = payload["format"];
116
118
  const formatNewer = typeof rawFormat === "number" && rawFormat > PROGRESS_FORMAT;
117
119
  const s = payload["state"] && typeof payload["state"] === "object" ? payload["state"] : {};
118
- const obj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
120
+ const obj3 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
119
121
  const state = {
120
122
  ...s,
121
123
  version: 2,
122
- answers: obj(s["answers"]),
123
- feedback: obj(s["feedback"]),
124
+ answers: obj3(s["answers"]),
125
+ feedback: obj3(s["feedback"]),
124
126
  bizModel: s["bizModel"] ?? null,
125
127
  grill: s["grill"] ?? null,
126
128
  assets: s["assets"] ?? null,
@@ -129,6 +131,7 @@ function parseProgress(text) {
129
131
  prd: s["prd"] ?? null,
130
132
  naming: s["naming"] ?? null,
131
133
  brand: s["brand"] ?? null,
134
+ governance: s["governance"] ?? null,
132
135
  kit: s["kit"] ?? null
133
136
  };
134
137
  return {
@@ -153,6 +156,7 @@ function buildProgress(state, writer, exportedAt) {
153
156
  prd,
154
157
  naming,
155
158
  brand,
159
+ governance,
156
160
  kit,
157
161
  version: _v,
158
162
  exportedAt: _ea,
@@ -177,6 +181,7 @@ function buildProgress(state, writer, exportedAt) {
177
181
  prd: prd ?? null,
178
182
  naming: naming ?? null,
179
183
  brand: brand ?? null,
184
+ governance: governance ?? null,
180
185
  kit: kit ?? null,
181
186
  ...rest
182
187
  }
@@ -259,7 +264,7 @@ function summarizeState(state) {
259
264
  if (s === "green" || s === "yellow" || s === "red") byStatus[s]++;
260
265
  else byStatus.ungraded++;
261
266
  }
262
- const artifacts = ["bizModel", "grill", "assets", "research", "swot", "prd", "naming", "brand"].filter((k) => state[k] != null);
267
+ const artifacts = ["bizModel", "grill", "assets", "research", "swot", "prd", "naming", "brand", "governance"].filter((k) => state[k] != null);
263
268
  const kit = state.kit && typeof state.kit === "object" ? state.kit : null;
264
269
  return {
265
270
  answered,
@@ -329,11 +334,11 @@ function saveStateFile(state, opts) {
329
334
  }
330
335
 
331
336
  // core/state-ops.ts
332
- var ARTIFACT_KEYS = ["bizModel", "grill", "assets", "research", "swot", "prd", "naming", "brand"];
337
+ var ARTIFACT_KEYS = ["bizModel", "grill", "assets", "research", "swot", "prd", "naming", "brand", "governance"];
333
338
  var END_STATES = ["open", "pass", "setAside"];
334
339
  function applyStateOp(p, ctx) {
335
340
  const { ts, exportedAt } = ctx;
336
- const fail2 = (message) => ({ ok: false, message, details: {} });
341
+ const fail4 = (message) => ({ ok: false, message, details: {} });
337
342
  try {
338
343
  if (p.op === "list") {
339
344
  const files = listStateFiles(p.path);
@@ -342,8 +347,8 @@ function applyStateOp(p, ctx) {
342
347
  }
343
348
  const rows = files.map((f) => {
344
349
  try {
345
- const sum = summarizeState(loadStateFile(f.path).state);
346
- return { name: f.name, path: f.path, answered: sum.answered, artifacts: sum.artifacts };
350
+ const sum2 = summarizeState(loadStateFile(f.path).state);
351
+ return { name: f.name, path: f.path, answered: sum2.answered, artifacts: sum2.artifacts };
347
352
  } catch {
348
353
  return { name: f.name, path: f.path, answered: 0, artifacts: [], unreadable: true };
349
354
  }
@@ -356,24 +361,24 @@ Pick one with --path .thought-layer/<name>.json, or set ${STATE_ENV} for the ses
356
361
  const loaded = loadStateFile(p.path);
357
362
  const save = (next) => saveStateFile(next, { target: p.path, ts, exportedAt }).path;
358
363
  if (p.op === "read" || p.op === "export") {
359
- const sum = summarizeState(loaded.state);
364
+ const sum2 = summarizeState(loaded.state);
360
365
  let message;
361
366
  if (loaded.exists) {
362
- message = `Loaded ${loaded.path}: ${sum.answered}/${sum.totalAnswerable} answered (${sum.byStatus.green} green, ${sum.byStatus.yellow} yellow, ${sum.byStatus.red} red), artifacts: ${sum.artifacts.join(", ") || "none"}. Resume at ${sum.cursor ? `stage ${sum.cursor.backboneStage ?? "?"} (${sum.cursor.phase ?? "?"})` : "the beginning"}.`;
367
+ message = `Loaded ${loaded.path}: ${sum2.answered}/${sum2.totalAnswerable} answered (${sum2.byStatus.green} green, ${sum2.byStatus.yellow} yellow, ${sum2.byStatus.red} red), artifacts: ${sum2.artifacts.join(", ") || "none"}. Resume at ${sum2.cursor ? `stage ${sum2.cursor.backboneStage ?? "?"} (${sum2.cursor.phase ?? "?"})` : "the beginning"}.`;
363
368
  } else {
364
369
  const others = listStateFiles().filter((f) => f.path !== loaded.path);
365
370
  const hint = others.length ? ` Other state files here: ${others.map((f) => f.name).join(", ")} (pick one with --path, or 'tl list').` : "";
366
371
  message = `No state file yet at ${loaded.path}.${hint} Start a fresh run; it will be created on first write.`;
367
372
  }
368
- return { ok: true, message, details: { path: loaded.path, exists: loaded.exists, summary: sum, state: loaded.state } };
373
+ return { ok: true, message, details: { path: loaded.path, exists: loaded.exists, summary: sum2, state: loaded.state } };
369
374
  }
370
375
  if (p.op === "answer") {
371
- if (!p.qId || typeof p.value !== "string") return fail2("answer needs a qId and a string value.");
376
+ if (!p.qId || typeof p.value !== "string") return fail4("answer needs a qId and a string value.");
372
377
  const path = save(setAnswer(loaded.state, p.qId, p.value, ts));
373
378
  return { ok: true, message: `Recorded answer for "${p.qId}" and saved ${path}.`, details: { path, qId: p.qId } };
374
379
  }
375
380
  if (p.op === "feedback") {
376
- if (!p.qId || !p.personas?.length) return fail2("feedback needs a qId and at least one persona.");
381
+ if (!p.qId || !p.personas?.length) return fail4("feedback needs a qId and at least one persona.");
377
382
  const endState = END_STATES.includes(p.endState || "") ? p.endState : "open";
378
383
  const mode = p.mode || (p.personas.length > 1 ? "panel" : p.personas[0].persona);
379
384
  const entry = buildFeedbackEntry({ mode, personas: p.personas, endState, round: p.round, ts });
@@ -390,24 +395,24 @@ Pick one with --path .thought-layer/<name>.json, or set ${STATE_ENV} for the ses
390
395
  }
391
396
  if (p.op === "artifact") {
392
397
  const key = p.artifact;
393
- if (!ARTIFACT_KEYS.includes(key)) return fail2(`artifact needs a key in: ${ARTIFACT_KEYS.join(", ")}.`);
394
- if (p.value == null) return fail2("artifact needs a value object.");
398
+ if (!ARTIFACT_KEYS.includes(key)) return fail4(`artifact needs a key in: ${ARTIFACT_KEYS.join(", ")}.`);
399
+ if (p.value == null) return fail4("artifact needs a value object.");
395
400
  const path = save(setArtifact(loaded.state, key, normalizeArtifactValue(key, p.value), ts));
396
401
  return { ok: true, message: `Stored ${key} artifact and saved ${path}.`, details: { path, artifact: key } };
397
402
  }
398
403
  if (p.op === "cursor") {
399
- if (!p.cursor) return fail2("cursor needs a cursor object.");
404
+ if (!p.cursor) return fail4("cursor needs a cursor object.");
400
405
  const path = save(setCursor(loaded.state, p.cursor, ts));
401
406
  return { ok: true, message: `Saved resume cursor (stage ${p.cursor.backboneStage ?? "?"}, ${p.cursor.phase ?? "?"}) to ${path}.`, details: { path } };
402
407
  }
403
408
  if (p.op === "park") {
404
- if (!p.key || !p.note) return fail2("park needs a key and a note.");
409
+ if (!p.key || !p.note) return fail4("park needs a key and a note.");
405
410
  const path = save(parkNote(loaded.state, p.key, p.note, ts));
406
411
  return { ok: true, message: `Parked a note under "${p.key}" and saved ${path}.`, details: { path, key: p.key } };
407
412
  }
408
- return fail2(`Unknown op "${p.op}". Use read, answer, feedback, artifact, cursor, park, or export.`);
413
+ return fail4(`Unknown op "${p.op}". Use read, answer, feedback, artifact, cursor, park, or export.`);
409
414
  } catch (e) {
410
- return fail2(`tl_state error: ${e.message}`);
415
+ return fail4(`tl_state error: ${e.message}`);
411
416
  }
412
417
  }
413
418
 
@@ -419,26 +424,26 @@ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, resolve
419
424
  var esc = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
420
425
  var fam = (f) => f.trim().replace(/\s+/g, "+");
421
426
  function extractScaffoldSpec(state) {
422
- const obj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
423
- const str2 = (v) => typeof v === "string" ? v : "";
424
- const brand = obj(state.brand);
425
- const guide = obj(brand["guide"]);
427
+ const obj3 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
428
+ const str4 = (v) => typeof v === "string" ? v : "";
429
+ const brand = obj3(state.brand);
430
+ const guide = obj3(brand["guide"]);
426
431
  const answers = state.answers || {};
427
432
  const palette = Array.isArray(guide["palette"]) ? guide["palette"] : [];
428
433
  const role = (re, fb) => {
429
434
  const m = palette.find((p) => p?.name && new RegExp(re, "i").test(p.name));
430
435
  return m?.hex || fb;
431
436
  };
432
- const typography = obj(guide["typography"]);
433
- const fontOf = (slot, fb) => str2(obj(slot)["family"]) || fb;
434
- const voice = obj(guide["voice"]);
437
+ const typography = obj3(guide["typography"]);
438
+ const fontOf = (slot, fb) => str4(obj3(slot)["family"]) || fb;
439
+ const voice = obj3(guide["voice"]);
435
440
  const logos = Array.isArray(brand["logos"]) ? brand["logos"] : [];
436
441
  const chosen = logos.find((l) => l?.id === brand["chosenLogoId"]) || logos[0];
437
442
  return {
438
- brandName: str2(guide["brandName"]) || "Your Product",
439
- tagline: str2(guide["tagline"]),
440
- pitch: str2(answers["pitch"]) || str2(answers["what-statement"]),
441
- positioning: str2(guide["positioning"]),
443
+ brandName: str4(guide["brandName"]) || "Your Product",
444
+ tagline: str4(guide["tagline"]),
445
+ pitch: str4(answers["pitch"]) || str4(answers["what-statement"]),
446
+ positioning: str4(guide["positioning"]),
442
447
  personality: Array.isArray(guide["personality"]) ? guide["personality"].filter((x) => typeof x === "string") : [],
443
448
  palette: {
444
449
  primary: role("primary", palette[0]?.hex || "#1f3a5f"),
@@ -449,9 +454,9 @@ function extractScaffoldSpec(state) {
449
454
  },
450
455
  displayFont: fontOf(typography["display"], "Inter"),
451
456
  bodyFont: fontOf(typography["body"], "Inter"),
452
- voiceTone: str2(voice["tone"]),
457
+ voiceTone: str4(voice["tone"]),
453
458
  logoSvg: chosen?.svg || void 0,
454
- pricing: str2(answers["pricing-model"]) || void 0
459
+ pricing: str4(answers["pricing-model"]) || void 0
455
460
  };
456
461
  }
457
462
  function indexHtml(spec, opts) {
@@ -748,11 +753,11 @@ function dedupeSortEnv(envVars) {
748
753
  function normalizeDatabase(raw) {
749
754
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
750
755
  const r = raw;
751
- const str2 = (v, fb) => typeof v === "string" && v.trim() ? v.trim() : fb;
756
+ const str4 = (v, fb) => typeof v === "string" && v.trim() ? v.trim() : fb;
752
757
  return {
753
- provider: str2(r["provider"], "neon"),
754
- schemaFile: str2(r["schemaFile"], "schema.sql"),
755
- envVar: str2(r["envVar"], "DATABASE_URL")
758
+ provider: str4(r["provider"], "neon"),
759
+ schemaFile: str4(r["schemaFile"], "schema.sql"),
760
+ envVar: str4(r["envVar"], "DATABASE_URL")
756
761
  };
757
762
  }
758
763
  function normalizeEnvVars(raw) {
@@ -783,15 +788,15 @@ function normalizeBackendMeta(raw) {
783
788
  const database = normalizeDatabase(r["database"]);
784
789
  const hasFunctionsDir = typeof r["functionsDir"] === "string" && r["functionsDir"].trim().length > 0;
785
790
  if (kind === null && envVars.length === 0 && database === null && !hasFunctionsDir) return null;
786
- const str2 = (v, fb) => typeof v === "string" && v.trim() ? v.trim() : fb;
791
+ const str4 = (v, fb) => typeof v === "string" && v.trim() ? v.trim() : fb;
787
792
  return {
788
793
  backendKind: kind ?? "serverless",
789
- functionsDir: str2(r["functionsDir"], "netlify/functions"),
790
- runtime: str2(r["runtime"], "nodejs20.x"),
791
- nodeVersion: str2(r["nodeVersion"], "20"),
794
+ functionsDir: str4(r["functionsDir"], "netlify/functions"),
795
+ runtime: str4(r["runtime"], "nodejs20.x"),
796
+ nodeVersion: str4(r["nodeVersion"], "20"),
792
797
  envVars,
793
798
  database,
794
- guide: str2(r["guide"], "BACKEND.md")
799
+ guide: str4(r["guide"], "BACKEND.md")
795
800
  };
796
801
  }
797
802
  var SECRET_NAME_RE = /(KEY|SECRET|TOKEN|PASSWORD|PASSWD|DATABASE_URL|DB_URL|CONN|DSN|CREDENTIAL|PRIVATE|AUTH)/;
@@ -1605,6 +1610,7 @@ function mergeProgressStates(ours, theirs, opts) {
1605
1610
  prd: artifact("prd"),
1606
1611
  naming: artifact("naming"),
1607
1612
  brand: artifact("brand"),
1613
+ governance: artifact("governance"),
1608
1614
  kit: mergeKit(ours.kit, theirs.kit, oursNewer)
1609
1615
  };
1610
1616
  for (const src of oursNewer ? [theirs, ours] : [ours, theirs]) {
@@ -1712,6 +1718,10 @@ deploy.json
1712
1718
  dist/
1713
1719
  .netlify/
1714
1720
  node_modules/
1721
+ # Delivered artifacts (tl artifacts) ARE intentionally tracked, even though the
1722
+ # same filenames are ignored elsewhere in the tree.
1723
+ !artifacts/
1724
+ !artifacts/**
1715
1725
  `;
1716
1726
  var README = `# Thought Layer sessions
1717
1727
 
@@ -1872,8 +1882,8 @@ function syncList(opts) {
1872
1882
  if (!files.length) return ok(`No sessions yet in ${cloneDir}. Create one with tl sync save --name <name>.`, { cloneDir, sessions: [] });
1873
1883
  const rows = files.map((f) => {
1874
1884
  try {
1875
- const sum = summarizeState(loadStateFile(f.path).state);
1876
- return { name: f.name, path: f.path, answered: sum.answered, artifacts: sum.artifacts };
1885
+ const sum2 = summarizeState(loadStateFile(f.path).state);
1886
+ return { name: f.name, path: f.path, answered: sum2.answered, artifacts: sum2.artifacts };
1877
1887
  } catch {
1878
1888
  return { name: f.name, path: f.path, answered: 0, artifacts: [], unreadable: true };
1879
1889
  }
@@ -1993,6 +2003,1341 @@ function syncStatus(opts) {
1993
2003
  );
1994
2004
  }
1995
2005
 
2006
+ // core/artifacts-io.ts
2007
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync6 } from "fs";
2008
+ import { basename, dirname as dirname5, join as join6 } from "path";
2009
+
2010
+ // core/model.ts
2011
+ function num2(v, fallback = 0) {
2012
+ const n = Number(v);
2013
+ return Number.isFinite(n) ? n : fallback;
2014
+ }
2015
+ function sum(arr) {
2016
+ return arr.reduce((a, b) => a + b, 0);
2017
+ }
2018
+ function computeProjection(assumptions) {
2019
+ if (!assumptions?.parties?.length) return null;
2020
+ const horizon = Math.min(Math.max(assumptions.horizonMonths || 36, 6), 120);
2021
+ const parties = assumptions.parties;
2022
+ const fixedCosts = assumptions.fixedCosts || [];
2023
+ const oneTimeCosts = assumptions.oneTimeCosts || [];
2024
+ const counts = {};
2025
+ parties.forEach((p) => {
2026
+ counts[p.id] = num2(p.startingCount);
2027
+ });
2028
+ const rows = [];
2029
+ let cumulative = 0;
2030
+ let breakEvenMonth = null;
2031
+ let cumBreakEvenMonth = null;
2032
+ for (let m = 1; m <= horizon; m++) {
2033
+ const row = {
2034
+ month: m,
2035
+ parties: {},
2036
+ revenue: 0,
2037
+ variableCost: 0,
2038
+ cacSpend: 0,
2039
+ fixedCost: 0,
2040
+ oneTimeCost: 0,
2041
+ totalCost: 0,
2042
+ grossProfit: 0,
2043
+ netProfit: 0,
2044
+ cumulative: 0
2045
+ };
2046
+ for (const p of parties) {
2047
+ const prev = counts[p.id] ?? 0;
2048
+ const newUnits = num2(p.monthlyNewBase) * Math.pow(1 + num2(p.monthlyNewGrowthPct) / 100, m - 1);
2049
+ const churned = prev * (num2(p.monthlyChurnPct) / 100);
2050
+ const count = Math.max(0, prev + newUnits - churned);
2051
+ counts[p.id] = count;
2052
+ const revenue = count * num2(p.revenuePerUnitPerMonth);
2053
+ const varCost = count * num2(p.variableCostPerUnitPerMonth);
2054
+ const cac = newUnits * num2(p.cacPerUnit);
2055
+ row.parties[p.id] = { count, newUnits, churned, revenue, varCost, cac };
2056
+ row.revenue += revenue;
2057
+ row.variableCost += varCost;
2058
+ row.cacSpend += cac;
2059
+ }
2060
+ for (const f of fixedCosts) {
2061
+ if (m >= num2(f.startMonth, 1)) row.fixedCost += num2(f.monthlyAmount);
2062
+ }
2063
+ for (const o of oneTimeCosts) {
2064
+ if (num2(o.month, 1) === m) row.oneTimeCost += num2(o.amount);
2065
+ }
2066
+ row.totalCost = row.variableCost + row.cacSpend + row.fixedCost + row.oneTimeCost;
2067
+ row.grossProfit = row.revenue - row.variableCost;
2068
+ row.netProfit = row.revenue - row.totalCost;
2069
+ cumulative += row.netProfit;
2070
+ row.cumulative = cumulative;
2071
+ if (breakEvenMonth === null && row.netProfit > 0) breakEvenMonth = m;
2072
+ if (cumBreakEvenMonth === null && cumulative > 0) cumBreakEvenMonth = m;
2073
+ rows.push(row);
2074
+ }
2075
+ const last = rows[rows.length - 1];
2076
+ if (!last) return null;
2077
+ const year1 = rows.slice(0, 12);
2078
+ return {
2079
+ rows,
2080
+ summary: {
2081
+ horizon,
2082
+ breakEvenMonth,
2083
+ cumBreakEvenMonth,
2084
+ year1Revenue: sum(year1.map((r) => r.revenue)),
2085
+ year1Net: sum(year1.map((r) => r.netProfit)),
2086
+ totalRevenue: sum(rows.map((r) => r.revenue)),
2087
+ totalNet: cumulative,
2088
+ maxDrawdown: Math.min(...rows.map((r) => r.cumulative), 0),
2089
+ endingMRR: last.revenue,
2090
+ endingCounts: Object.fromEntries(parties.map((p) => [p.id, last.parties[p.id]?.count || 0]))
2091
+ }
2092
+ };
2093
+ }
2094
+ function fmtMoney(n, currency = "USD") {
2095
+ try {
2096
+ return new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: 0 }).format(n);
2097
+ } catch {
2098
+ return `$${Math.round(n).toLocaleString()}`;
2099
+ }
2100
+ }
2101
+
2102
+ // core/artifacts.ts
2103
+ var obj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
2104
+ var str2 = (v) => typeof v === "string" ? v : "";
2105
+ var esc2 = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2106
+ var fam2 = (f) => String(f).trim().replace(/\s+/g, "+");
2107
+ function brandTokens(guide) {
2108
+ if (!guide || !Array.isArray(guide.palette) || !guide.palette.length) return null;
2109
+ const role = (re, fb) => guide.palette.find((p) => p?.name && new RegExp(re, "i").test(p.name))?.hex || fb;
2110
+ return {
2111
+ primary: role("primary", guide.palette[0]?.hex || "#1f3a5f"),
2112
+ accent: role("accent|secondary", guide.palette[1]?.hex || "#e8743b"),
2113
+ ink: role("ink|text|dark|black", "#16202b"),
2114
+ surface: role("surface|background|light|paper|off.?white|cream", "#f7f8fa"),
2115
+ muted: role("muted|gray|grey|neutral|border", "#8a9099"),
2116
+ display: guide.typography?.display?.family || "Inter",
2117
+ body: guide.typography?.body?.family || "Inter"
2118
+ };
2119
+ }
2120
+ var SVG_FONTS = `<style><![CDATA[@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=IBM+Plex+Mono:wght@400;500&display=swap');.s{font-family:'Inter',system-ui,sans-serif}.d{font-family:'Inter',system-ui,sans-serif}.m{font-family:'IBM Plex Mono',ui-monospace,monospace}]]></style>`;
2121
+ function svgFontBlock(t) {
2122
+ if (!t) return { style: SVG_FONTS, disp: "d" };
2123
+ const style = `<style><![CDATA[@import url('https://fonts.googleapis.com/css2?family=${fam2(t.display)}:wght@600;700;800&family=${fam2(t.body)}:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap');.s{font-family:'${t.body}',system-ui,sans-serif}.d{font-family:'${t.display}','${t.body}',sans-serif}.m{font-family:'IBM Plex Mono',ui-monospace,monospace}]]></style>`;
2124
+ return { style, disp: "d" };
2125
+ }
2126
+ function wrapText(text, maxWidthPx, fontSizePx, avg = 0.54) {
2127
+ const maxChars = Math.max(6, Math.floor(maxWidthPx / (fontSizePx * avg)));
2128
+ const out = [];
2129
+ let line = "";
2130
+ for (let word of String(text).trim().split(/\s+/)) {
2131
+ while (word.length > maxChars) {
2132
+ if (line) {
2133
+ out.push(line);
2134
+ line = "";
2135
+ }
2136
+ out.push(word.slice(0, maxChars - 1) + "-");
2137
+ word = word.slice(maxChars - 1);
2138
+ }
2139
+ const test = line ? line + " " + word : word;
2140
+ if (test.length > maxChars && line) {
2141
+ out.push(line);
2142
+ line = word;
2143
+ } else line = test;
2144
+ }
2145
+ if (line) out.push(line);
2146
+ return out.length ? out : [""];
2147
+ }
2148
+ function glossaryMarkdown(grill) {
2149
+ const terms = grill?.glossary || [];
2150
+ return `# Domain Glossary
2151
+
2152
+ ${terms.map((g) => `- **${g.term}**: ${g.definition}`).join("\n")}
2153
+ `;
2154
+ }
2155
+ var REQ_CATS = ["persona", "journey", "ux", "functional", "business-rule", "data", "integration", "non-functional", "metric"];
2156
+ function requirementsMarkdown(grill) {
2157
+ const reqs = (grill?.requirements || []).map((r) => ({
2158
+ id: r.id || "",
2159
+ category: r.category || "functional",
2160
+ text: r.text ?? r.statement ?? ""
2161
+ }));
2162
+ let md = "# Requirements\n";
2163
+ for (const cat of REQ_CATS) {
2164
+ const inCat = reqs.filter((r) => r.category === cat);
2165
+ if (!inCat.length) continue;
2166
+ md += `
2167
+ ## ${cat.replace("-", " ").replace(/\b\w/g, (c) => c.toUpperCase())}
2168
+
2169
+ `;
2170
+ md += inCat.map((r) => `- **${r.id}** ${r.text}`).join("\n") + "\n";
2171
+ }
2172
+ return md;
2173
+ }
2174
+ function swotMarkdown(swot) {
2175
+ if (!swot) return "";
2176
+ const quads = [
2177
+ ["strengths", "Strengths"],
2178
+ ["weaknesses", "Weaknesses"],
2179
+ ["opportunities", "Opportunities"],
2180
+ ["threats", "Threats"]
2181
+ ];
2182
+ let md = "# SWOT Analysis\n";
2183
+ for (const [k, label] of quads) {
2184
+ const items = (swot[k] || []).filter((x) => x && x.trim());
2185
+ md += `
2186
+ ## ${label}
2187
+
2188
+ ${items.length ? items.map((i) => `- ${i}`).join("\n") : "_(none)_"}
2189
+ `;
2190
+ }
2191
+ return md;
2192
+ }
2193
+ function brandGuideMarkdown(guide) {
2194
+ if (!guide) return "";
2195
+ const pal = (guide.palette || []).map((p) => `- **${p.name}** \`${p.hex}\` (${p.role})`).join("\n");
2196
+ const msg = (guide.messaging || []).map((m) => `- ${m}`).join("\n");
2197
+ const dos = (guide.voice?.dos || []).map((d) => `- ${d}`).join("\n");
2198
+ const donts = (guide.voice?.donts || []).map((d) => `- ${d}`).join("\n");
2199
+ const disp = guide.typography?.display;
2200
+ const body = guide.typography?.body;
2201
+ return `# Brand Style Guide: ${guide.brandName || ""}
2202
+
2203
+ > ${guide.tagline || ""}
2204
+
2205
+ **Positioning.** ${guide.positioning || ""}
2206
+
2207
+ **Personality.** ${(guide.personality || []).join(", ")}
2208
+
2209
+ ## Voice and Tone
2210
+ ${guide.voice?.tone || ""}
2211
+
2212
+ **Do**
2213
+ ${dos || "- (none)"}
2214
+
2215
+ **Don't**
2216
+ ${donts || "- (none)"}
2217
+
2218
+ ## Color Palette
2219
+ ${pal || "- (none)"}
2220
+
2221
+ ## Typography
2222
+ - **Display:** ${disp?.family || ""} (${disp?.weights || ""}). Usage: ${disp?.usage || ""}
2223
+ - **Body:** ${body?.family || ""} (${body?.weights || ""}). Usage: ${body?.usage || ""}
2224
+
2225
+ ## Logo Direction
2226
+ ${guide.logoDirection || ""}
2227
+
2228
+ ## Imagery and Iconography
2229
+ ${guide.imagery || ""}
2230
+
2231
+ ## Messaging Examples
2232
+ ${msg || "- (none)"}
2233
+ `;
2234
+ }
2235
+ function collectTodos(feedback) {
2236
+ const out = [];
2237
+ for (const [qId, v] of Object.entries(feedback || {})) {
2238
+ const todos = v?.todos;
2239
+ if (todos?.length) out.push({ qId, summaries: todos.map((t) => t.summary || "to-do") });
2240
+ }
2241
+ return out;
2242
+ }
2243
+ var SEO_DEPLOY_SECTION = `## Discoverability and SEO (required)
2244
+ Build the product so both people and AI assistants can find and trust it. Implement all of the following:
2245
+ - Structured data: add schema.org JSON-LD as one linked @graph. Always include an Organization and a Person (the founder), cross-referenced (Organization.founder and Person.worksFor) and each with a sameAs array pointing to real profiles. Add a WebSite node and the type that matches the page (WebPage, SoftwareApplication, or Product). Add FAQPage wherever there is an FAQ, HowTo for any step-by-step process, BreadcrumbList on every real sub-page, and Article or BlogPosting on real article pages. Never add a schema type that does not match visible on-page content.
2246
+ - llms.txt: create /llms.txt at the site root with an H1 title, a one-paragraph summary in a blockquote, sections that link the key pages, and a short FAQ, so AI crawlers can extract what the site is and does.
2247
+ - sitemap.xml listing every real page, and robots.txt that allows crawling and points to the sitemap.
2248
+ - A canonical link on every page.
2249
+ - Open Graph and Twitter Card meta on every page, plus a 1200x630 social image.
2250
+ - Semantic, accessible HTML: landmark elements, headings in order, alt text on images, labelled form controls, and a visible focus style.
2251
+ - An SEO README at SEO.md that documents every item above: where each lives and how to update it later (especially the Organization and Person sameAs links and the social image).
2252
+
2253
+ ## Deployment (Netlify recommended)
2254
+ Netlify is the simplest way to ship this. Include what it needs and give the founder a paste-ready deploy path:
2255
+ - Add a netlify.toml with the build command and publish directory. For a single-page app, add a SPA redirect (/* to /index.html with status 200).
2256
+ - Document two deploy options in SEO.md or a DEPLOY.md:
2257
+ 1. Continuous deploy: push the repo to GitHub and connect it at app.netlify.com, so every push to the main branch deploys automatically.
2258
+ 2. CLI deploy: run the build, then "npx netlify-cli deploy --prod --dir <publish-directory>".
2259
+ - Make sure llms.txt, robots.txt, sitemap.xml, and the social image sit in the publish directory so they are served at the site root.
2260
+ `;
2261
+ function buildKitPrompt(grill, prdMarkdown, assumptions, brand, feedback = {}) {
2262
+ const projection = assumptions ? computeProjection(assumptions) : null;
2263
+ let bizSummary = "";
2264
+ if (projection && assumptions) {
2265
+ const s = projection.summary;
2266
+ bizSummary = `Business model: ${(assumptions.parties || []).map((p) => `${p.name} (${p.role})`).join(", ")}. Monthly break-even at month ${s.breakEvenMonth ?? "beyond horizon"}; cumulative break-even at month ${s.cumBreakEvenMonth ?? "beyond horizon"}; year-1 revenue about ${fmtMoney(s.year1Revenue, assumptions.currency)}; max cash drawdown about ${fmtMoney(Math.abs(s.maxDrawdown), assumptions.currency)}.`;
2267
+ }
2268
+ const groups = collectTodos(feedback);
2269
+ const todoBlock = groups.length ? `
2270
+ ## Open validation to-dos (the founder set these aside; treat as known gaps, not blockers)
2271
+ ${groups.map((g) => `- ${g.qId}: ${g.summaries.join("; ")}`).join("\n")}
2272
+ ` : "";
2273
+ const hasGuide = !!brand?.guide;
2274
+ return `# Build This Product
2275
+
2276
+ You are an expert full-stack engineering agent. Build version 1 of the product specified below. Work iteratively: scaffold, implement the critical user journeys first, then the remaining requirements. Ask nothing; every decision you need is in this document, and where genuinely unspecified, choose the simplest option consistent with the PRD and note it in a DECISIONS.md.
2277
+
2278
+ ## Ground rules
2279
+ - Honor the Domain Glossary exactly: use its terms for entities, fields, and UI labels (ubiquitous language).
2280
+ - Every requirement has an ID (R-1, R-2, ...). Track them in a TRACEABILITY.md mapping requirement to implementation to test.
2281
+ - Respect the "Out of Scope" list absolutely. Do not build excluded features.
2282
+ - Mobile and desktop must both work.${hasGuide ? "\n- Apply the brand identity in the Brand section below to all UI, copy, color, and type, and to any generated assets." : ""}
2283
+ ${bizSummary ? `
2284
+ ## Business context
2285
+ ${bizSummary}
2286
+ ` : ""}${hasGuide ? `
2287
+ ## Brand identity (apply consistently)
2288
+ ${brandGuideMarkdown(brand.guide)}
2289
+ (The kit also includes Logo.svg and a rendered LookBook.html.)
2290
+ ` : ""}${todoBlock}
2291
+ ${SEO_DEPLOY_SECTION}
2292
+ ---
2293
+
2294
+ ${prdMarkdown || "(PRD not yet composed; generate the PRD first)"}
2295
+
2296
+ ---
2297
+
2298
+ ${requirementsMarkdown(grill)}
2299
+
2300
+ ---
2301
+
2302
+ ${glossaryMarkdown(grill)}
2303
+ `;
2304
+ }
2305
+ function swotInfographicSvg(swot, brand) {
2306
+ const t = brandTokens(brand?.guide);
2307
+ const { style: fontStyle, disp } = svgFontBlock(t);
2308
+ const surface = t?.surface || "#f7f8fa";
2309
+ const ink = t?.ink || "#0f1729";
2310
+ const W = 1e3, spineX = W / 2, R = 34, LH = 19, FS = 13.5;
2311
+ const headerH = 104, footerH = 42, gapRow = 26;
2312
+ const PANEL = { L: { x: 28, w: 444 }, R: { x: 528, w: 444 } };
2313
+ const textW = 360;
2314
+ const quads = [
2315
+ { key: "strengths", label: "STRENGTHS", color: "#059669", icon: "up", side: -1, row: 0 },
2316
+ { key: "weaknesses", label: "WEAKNESSES", color: "#dc2626", icon: "down", side: -1, row: 1 },
2317
+ { key: "opportunities", label: "OPPORTUNITIES", color: "#4f46e5", icon: "search", side: 1, row: 0 },
2318
+ { key: "threats", label: "THREATS", color: "#d97706", icon: "warn", side: 1, row: 1 }
2319
+ ].map((q) => ({
2320
+ ...q,
2321
+ items: (swot?.[q.key] || []).map((x) => String(x || "").trim()).filter(Boolean).slice(0, 8).map((s) => wrapText(s, textW, FS))
2322
+ }));
2323
+ const bulletsH = (q) => q.items.length ? q.items.reduce((a, lines) => a + lines.length * LH + 10, 0) : 26;
2324
+ const quadH = (q) => 94 + bulletsH(q) + 14;
2325
+ const rowH = [Math.max(quadH(quads[0]), quadH(quads[2])), Math.max(quadH(quads[1]), quadH(quads[3]))];
2326
+ const rowY = [headerH + gapRow, 0];
2327
+ rowY[1] = rowY[0] + rowH[0] + gapRow;
2328
+ const footerTop = rowY[1] + rowH[1] + gapRow;
2329
+ const H = footerTop + footerH;
2330
+ const icon = (type, cx, cy) => {
2331
+ const P = (d, w = 5) => `<path d="${d}" fill="none" stroke="#ffffff" stroke-width="${w}" stroke-linecap="round" stroke-linejoin="round"/>`;
2332
+ if (type === "up") return P(`M${cx} ${cy + 13} L${cx} ${cy - 13} M${cx - 9} ${cy - 4} L${cx} ${cy - 13} L${cx + 9} ${cy - 4}`);
2333
+ if (type === "down") return P(`M${cx} ${cy - 13} L${cx} ${cy + 13} M${cx - 9} ${cy + 4} L${cx} ${cy + 13} L${cx + 9} ${cy + 4}`);
2334
+ if (type === "search") return `<circle cx="${cx - 3}" cy="${cy - 3}" r="9.5" fill="none" stroke="#ffffff" stroke-width="4.5"/>` + P(`M${cx + 4} ${cy + 4} L${cx + 13} ${cy + 13}`);
2335
+ return P(`M${cx} ${cy - 15} L${cx + 15} ${cy + 12} L${cx - 15} ${cy + 12} Z`, 4.5) + `<rect x="${cx - 2.5}" y="${cy - 6}" width="5" height="11" rx="2.5" fill="#ffffff"/><circle cx="${cx}" cy="${cy + 9}" r="2.7" fill="#ffffff"/>`;
2336
+ };
2337
+ const quadrant = (q) => {
2338
+ const pan = q.side < 0 ? PANEL.L : PANEL.R;
2339
+ const top = rowY[q.row], rh = rowH[q.row];
2340
+ const my = top + 40;
2341
+ const mx = spineX + q.side * 70;
2342
+ const innerEdge = mx - q.side * R;
2343
+ const textX = pan.x + 42, dotX = pan.x + 26;
2344
+ let b = `<rect x="${pan.x}" y="${top + 10}" width="${pan.w}" height="${rh - 10}" rx="18" fill="${q.color}" opacity="0.06"/>`;
2345
+ b += `<rect x="${pan.x}" y="${top + 10}" width="${pan.w}" height="${rh - 10}" rx="18" fill="none" stroke="${q.color}" stroke-opacity="0.18"/>`;
2346
+ b += `<line x1="${spineX}" y1="${my}" x2="${innerEdge}" y2="${my}" stroke="${q.color}" stroke-width="2.5"/>`;
2347
+ b += `<circle cx="${spineX}" cy="${my}" r="4.5" fill="${q.color}"/>`;
2348
+ b += `<circle cx="${mx}" cy="${my}" r="${R + 6}" fill="${q.color}" opacity="0.15"/>`;
2349
+ b += `<circle cx="${mx}" cy="${my}" r="${R}" fill="${q.color}"/>`;
2350
+ b += icon(q.icon, mx, my);
2351
+ const tabW = Math.round(q.label.length * 8.6) + 30, tabH = 34, tabY = my - tabH / 2;
2352
+ const tabX = q.side < 0 ? mx - R - 12 - tabW : mx + R + 12;
2353
+ b += `<rect x="${tabX}" y="${tabY}" width="${tabW}" height="${tabH}" rx="8" fill="${q.color}"/>`;
2354
+ b += `<text class="${disp}" x="${tabX + tabW / 2}" y="${tabY + 22}" text-anchor="middle" font-size="13.5" font-weight="700" letter-spacing="1.5" fill="#ffffff">${esc2(q.label)}</text>`;
2355
+ let y = top + 94;
2356
+ if (!q.items.length) return b + `<text class="s" x="${textX}" y="${y}" font-size="13" fill="#9ca3af">(none yet)</text>`;
2357
+ for (const lines of q.items) {
2358
+ b += `<circle cx="${dotX}" cy="${y - 4}" r="3" fill="${q.color}"/>`;
2359
+ b += `<text class="s" x="${textX}" y="${y}" font-size="13.5" fill="#374151">` + lines.map((ln, i) => `<tspan x="${textX}"${i ? ` dy="${LH}"` : ""}>${esc2(ln)}</tspan>`).join("") + `</text>`;
2360
+ y += lines.length * LH + 10;
2361
+ }
2362
+ return b;
2363
+ };
2364
+ let body = `<rect width="${W}" height="${H}" fill="${surface}"/>`;
2365
+ body += `<line x1="${spineX}" y1="${headerH}" x2="${spineX}" y2="${footerTop}" stroke="#cfd5de" stroke-width="2"/>`;
2366
+ body += quads.map(quadrant).join("");
2367
+ body += `<rect x="0" y="0" width="${W}" height="${headerH}" fill="${ink}"/>`;
2368
+ body += `<text class="m" x="40" y="38" font-size="11" letter-spacing="3" fill="#ffffff" fill-opacity="0.5">THE THOUGHT LAYER</text>`;
2369
+ body += `<text class="m" x="${W - 40}" y="38" font-size="11" letter-spacing="3" fill="#ffffff" fill-opacity="0.5" text-anchor="end">STRATEGY</text>`;
2370
+ body += `<text class="${disp}" x="${spineX}" y="74" font-size="46" font-weight="700" letter-spacing="6" fill="#ffffff" text-anchor="middle">SWOT ANALYSIS</text>`;
2371
+ body += `<rect x="0" y="${footerTop}" width="${W}" height="${footerH}" fill="${ink}"/>`;
2372
+ body += `<text class="m" x="${spineX}" y="${footerTop + 26}" font-size="11" fill="#ffffff" fill-opacity="0.55" text-anchor="middle">The Thought Layer, generated locally</text>`;
2373
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">${fontStyle}${body}</svg>`;
2374
+ }
2375
+ function bizModelInfographicSvg(assumptions, brand) {
2376
+ const projection = computeProjection(assumptions);
2377
+ if (!projection || !assumptions) return null;
2378
+ const s = projection.summary;
2379
+ const cur = assumptions.currency || "USD";
2380
+ const t = brandTokens(brand?.guide);
2381
+ const { style: fontStyle, disp } = svgFontBlock(t);
2382
+ const surface = t?.surface || "#f7f8fa";
2383
+ const ink = t?.ink || "#0f1729";
2384
+ const accent = t?.accent || "#4f46e5";
2385
+ const W = 1e3, spineX = W / 2, M = 28, LH = 19, FS = 13.5;
2386
+ const headerH = 104, footerH = 42;
2387
+ const PANEL = { L: { x: 28, w: 444 }, R: { x: 528, w: 444 } };
2388
+ const metrics = [
2389
+ ["Year 1 revenue", fmtMoney(s.year1Revenue, cur)],
2390
+ ["Monthly break-even", s.breakEvenMonth ? `Month ${s.breakEvenMonth}` : "Beyond horizon"],
2391
+ ["Max cash drawdown", fmtMoney(s.maxDrawdown, cur)],
2392
+ [`MRR at month ${s.horizon}`, fmtMoney(s.endingMRR, cur)]
2393
+ ];
2394
+ const milestones = assumptions.milestones || [];
2395
+ const partyLines = (assumptions.parties || []).slice(0, 7).map((p) => wrapText(`${p.name} (${p.role}): ${fmtMoney(Number(p.revenuePerUnitPerMonth) || 0, cur)}/unit/mo, CAC ${fmtMoney(Number(p.cacPerUnit) || 0, cur)}`, PANEL.L.w - 60, FS));
2396
+ const msLines = [...milestones].sort((a, b) => (a.month || 0) - (b.month || 0)).slice(0, 9).map((m) => ({ month: m.month ?? 0, lines: wrapText(m.label || "", PANEL.R.w - 92, FS) }));
2397
+ const narrLines = assumptions.narrative ? wrapText(assumptions.narrative, W - 96, 12.5) : [];
2398
+ const cardsY = headerH + 26, cardH = 84, cardGap = 18;
2399
+ const cardW = (W - M * 2 - cardGap * 3) / 4;
2400
+ const panelTop = cardsY + cardH + 34;
2401
+ const listTop = panelTop + 60;
2402
+ let ly = listTop;
2403
+ partyLines.forEach((lines) => {
2404
+ ly += lines.length * LH + 10;
2405
+ });
2406
+ let ry = listTop;
2407
+ msLines.forEach((m) => {
2408
+ ry += m.lines.length * LH + 10;
2409
+ });
2410
+ const colBottom = Math.max(ly, ry, listTop + 26);
2411
+ const panelBottom = colBottom + 6;
2412
+ const narrTop = panelBottom + 30;
2413
+ const narrPanelH = narrLines.length * 17 + 36;
2414
+ const footerTop = narrLines.length ? narrTop + narrPanelH + 14 : panelBottom + 24;
2415
+ const H = footerTop + footerH;
2416
+ const tab = (label, x, y2) => {
2417
+ const w = Math.round(label.length * 8.6) + 28, h = 32;
2418
+ return `<rect x="${x}" y="${y2}" width="${w}" height="${h}" rx="8" fill="${accent}"/><text class="${disp}" x="${x + w / 2}" y="${y2 + 21}" text-anchor="middle" font-size="13" font-weight="700" letter-spacing="1.5" fill="#ffffff">${esc2(label)}</text>`;
2419
+ };
2420
+ let body = `<rect width="${W}" height="${H}" fill="${surface}"/>`;
2421
+ metrics.forEach((m, i) => {
2422
+ const x = M + i * (cardW + cardGap);
2423
+ body += `<rect x="${x}" y="${cardsY}" width="${cardW}" height="${cardH}" rx="14" fill="${accent}" opacity="0.08"/>`;
2424
+ body += `<rect x="${x}" y="${cardsY}" width="${cardW}" height="${cardH}" rx="14" fill="none" stroke="${accent}" stroke-opacity="0.2"/>`;
2425
+ body += `<text class="s" x="${x + 18}" y="${cardsY + 30}" font-size="11.5" fill="#6b7280">${esc2(m[0])}</text>`;
2426
+ body += `<text class="${disp}" x="${x + 18}" y="${cardsY + 62}" font-size="22" font-weight="700" fill="${accent}">${esc2(m[1])}</text>`;
2427
+ });
2428
+ body += `<rect x="${PANEL.L.x}" y="${panelTop}" width="${PANEL.L.w}" height="${panelBottom - panelTop}" rx="18" fill="${accent}" opacity="0.05"/>`;
2429
+ body += `<rect x="${PANEL.R.x}" y="${panelTop}" width="${PANEL.R.w}" height="${panelBottom - panelTop}" rx="18" fill="${accent}" opacity="0.05"/>`;
2430
+ body += `<line x1="${spineX}" y1="${panelTop}" x2="${spineX}" y2="${panelBottom}" stroke="#cfd5de" stroke-width="2"/>`;
2431
+ body += tab("PARTIES", PANEL.L.x + 22, panelTop + 16);
2432
+ body += tab("MILESTONES", PANEL.R.x + 22, panelTop + 16);
2433
+ let y = listTop;
2434
+ if (!partyLines.length) body += `<text class="s" x="${PANEL.L.x + 24}" y="${y}" font-size="13" fill="#9ca3af">(no parties)</text>`;
2435
+ partyLines.forEach((lines) => {
2436
+ body += `<circle cx="${PANEL.L.x + 26}" cy="${y - 4}" r="3" fill="${accent}"/>`;
2437
+ body += `<text class="s" x="${PANEL.L.x + 40}" y="${y}" font-size="13.5" fill="#374151">` + lines.map((ln, i) => `<tspan x="${PANEL.L.x + 40}"${i ? ` dy="${LH}"` : ""}>${esc2(ln)}</tspan>`).join("") + `</text>`;
2438
+ y += lines.length * LH + 10;
2439
+ });
2440
+ let my = listTop;
2441
+ if (!msLines.length) body += `<text class="s" x="${PANEL.R.x + 24}" y="${my}" font-size="13" fill="#9ca3af">(no milestones)</text>`;
2442
+ msLines.forEach((m) => {
2443
+ body += `<text class="m" x="${PANEL.R.x + 24}" y="${my}" font-size="11" font-weight="500" fill="${accent}">M${esc2(m.month)}</text>`;
2444
+ body += `<text class="s" x="${PANEL.R.x + 60}" y="${my}" font-size="13.5" fill="#374151">` + m.lines.map((ln, i) => `<tspan x="${PANEL.R.x + 60}"${i ? ` dy="${LH}"` : ""}>${esc2(ln)}</tspan>`).join("") + `</text>`;
2445
+ my += m.lines.length * LH + 10;
2446
+ });
2447
+ if (narrLines.length) {
2448
+ body += `<rect x="${M}" y="${narrTop}" width="${W - M * 2}" height="${narrPanelH}" rx="14" fill="${accent}" opacity="0.05"/>`;
2449
+ body += `<text class="m" x="${M + 18}" y="${narrTop + 22}" font-size="10.5" letter-spacing="1.5" fill="#9ca3af">NOTES</text>`;
2450
+ body += `<text class="s" x="${M + 18}" y="${narrTop + 42}" font-size="12.5" fill="#6b7280">` + narrLines.map((ln, i) => `<tspan x="${M + 18}"${i ? ` dy="17"` : ""}>${esc2(ln)}</tspan>`).join("") + `</text>`;
2451
+ }
2452
+ body += `<rect x="0" y="0" width="${W}" height="${headerH}" fill="${ink}"/>`;
2453
+ body += `<text class="m" x="40" y="38" font-size="11" letter-spacing="3" fill="#ffffff" fill-opacity="0.5">THE THOUGHT LAYER</text>`;
2454
+ body += `<text class="m" x="${W - 40}" y="38" font-size="11" letter-spacing="3" fill="#ffffff" fill-opacity="0.5" text-anchor="end">FINANCIALS</text>`;
2455
+ body += `<text class="${disp}" x="${spineX}" y="74" font-size="44" font-weight="700" letter-spacing="4" fill="#ffffff" text-anchor="middle">BUSINESS MODEL</text>`;
2456
+ body += `<rect x="0" y="${footerTop}" width="${W}" height="${footerH}" fill="${ink}"/>`;
2457
+ body += `<text class="m" x="${spineX}" y="${footerTop + 26}" font-size="11" fill="#ffffff" fill-opacity="0.55" text-anchor="middle">The Thought Layer, figures are your assumptions, computed locally</text>`;
2458
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">${fontStyle}${body}</svg>`;
2459
+ }
2460
+ function brandLookbookHtml(guide, logoSvg) {
2461
+ if (!guide) return "";
2462
+ const role = (re, fb) => (guide.palette || []).find((p) => p?.name && new RegExp(re, "i").test(p.name))?.hex || fb;
2463
+ const primary = role("primary", guide.palette?.[0]?.hex || "#1a1a2e");
2464
+ const accent = role("accent|secondary", guide.palette?.[1]?.hex || "#e94f37");
2465
+ const ink = role("ink|text|dark|black", "#14141b");
2466
+ const surface = role("surface|background|light|paper|off.?white|cream", "#fbfaf7");
2467
+ const muted = role("muted|gray|grey|neutral|border", "#8a8a99");
2468
+ const disp = guide.typography?.display?.family || "Georgia";
2469
+ const body = guide.typography?.body?.family || "system-ui";
2470
+ const fontsUrl = `https://fonts.googleapis.com/css2?family=${fam2(disp)}:wght@400;600;700&family=${fam2(body)}:wght@400;500;600&display=swap`;
2471
+ const name = esc2(guide.brandName || "Your Brand");
2472
+ const tagline = esc2(guide.tagline || "");
2473
+ const messaging = guide.messaging || [];
2474
+ const hero = esc2(messaging[0] || guide.positioning || guide.brandName || "");
2475
+ const lockup = logoSvg || `<span class="wordmark">${name}</span>`;
2476
+ const initials = (guide.brandName || "B").split(/\s+/).map((w) => w[0] || "").join("").slice(0, 2).toUpperCase();
2477
+ const swatches = (guide.palette || []).map((p) => `<div class="sw"><div class="chip" style="background:${esc2(p.hex)}"></div><div class="swmeta"><strong>${esc2(p.name)}</strong><code>${esc2(p.hex)}</code><span>${esc2(p.role)}</span></div></div>`).join("");
2478
+ const traits = (guide.personality || []).map((tr) => `<span class="pill">${esc2(tr)}</span>`).join("");
2479
+ const dos = (guide.voice?.dos || []).map((d) => `<li>${esc2(d)}</li>`).join("");
2480
+ const donts = (guide.voice?.donts || []).map((d) => `<li>${esc2(d)}</li>`).join("");
2481
+ return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
2482
+ <title>${name} Brand Look Book</title><link href="${fontsUrl}" rel="stylesheet"><style>
2483
+ :root{--p:${primary};--a:${accent};--ink:${ink};--su:${surface};--mu:${muted};--disp:'${disp}',Georgia,serif;--body:'${body}',system-ui,sans-serif}
2484
+ *{margin:0;padding:0;box-sizing:border-box}body{background:var(--su);color:var(--ink);font-family:var(--body);line-height:1.6}
2485
+ .wrap{max-width:960px;margin:0 auto;padding:0 28px}
2486
+ section{padding:60px 0;border-bottom:1px solid color-mix(in srgb,var(--mu) 30%,transparent)}
2487
+ .eyebrow{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--mu);font-weight:600;margin-bottom:16px}
2488
+ h2{font-family:var(--disp);font-size:30px;font-weight:700}
2489
+ .wordmark{font-family:var(--disp);font-size:44px;font-weight:700;color:var(--su)}
2490
+ .cover{background:var(--p);color:var(--su);padding:84px 0}
2491
+ .cover .lockup{display:inline-block;margin-bottom:26px}.cover .lockup svg{height:64px;width:auto}
2492
+ .cover h1{font-family:var(--disp);font-size:clamp(32px,6vw,58px);font-weight:700;line-height:1.08;margin:12px 0}
2493
+ .cover p{font-size:18px;opacity:.85;max-width:640px}
2494
+ .pill{display:inline-block;background:color-mix(in srgb,var(--a) 16%,transparent);color:#fff;border:1px solid color-mix(in srgb,#fff 30%,transparent);border-radius:999px;padding:6px 14px;font-size:13px;font-weight:600;margin:0 8px 8px 0}
2495
+ .swatches{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:16px}
2496
+ .sw{border:1px solid color-mix(in srgb,var(--mu) 28%,transparent);border-radius:12px;overflow:hidden;background:#fff}
2497
+ .chip{height:80px}.swmeta{padding:12px 14px;font-size:13px;display:flex;flex-direction:column;gap:2px}
2498
+ .swmeta code,.swmeta span{color:var(--mu);font-size:12px}
2499
+ .big{font-family:var(--disp);font-size:62px;font-weight:700;line-height:1.05}
2500
+ .row{font-family:var(--disp);font-size:24px;margin-top:6px;color:var(--a)}
2501
+ .bodyspec{font-size:16px;max-width:640px;margin-top:16px}
2502
+ .voice{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:8px}
2503
+ .col{border:1px solid color-mix(in srgb,var(--mu) 28%,transparent);border-radius:12px;padding:18px 20px;background:#fff}
2504
+ .col h3{font-size:13px;text-transform:uppercase;letter-spacing:1px;margin-bottom:10px}
2505
+ .col ul{list-style:none}.col li{padding:5px 0 5px 22px;position:relative;font-size:14px}
2506
+ .col li:before{position:absolute;left:0}.do li:before{content:"+";color:var(--a);font-weight:700}.dont li:before{content:"x";color:var(--mu)}
2507
+ .apps{display:grid;grid-template-columns:1fr 1fr;gap:20px}
2508
+ .mock{border:1px solid color-mix(in srgb,var(--mu) 28%,transparent);border-radius:14px;overflow:hidden;background:#fff}
2509
+ .cap{font-size:12px;color:var(--mu);padding:8px 14px;border-top:1px solid color-mix(in srgb,var(--mu) 22%,transparent)}
2510
+ .appbar{display:flex;align-items:center;justify-content:space-between;padding:13px 18px;background:var(--su);border-bottom:1px solid color-mix(in srgb,var(--mu) 22%,transparent)}
2511
+ .appbar .lk svg{height:24px}.appbar .lk .wordmark{font-size:18px;color:var(--ink)}
2512
+ .appbar nav{font-size:12px;color:var(--mu);display:flex;gap:14px}
2513
+ .appbody{height:118px;background:repeating-linear-gradient(0deg,transparent,transparent 22px,color-mix(in srgb,var(--mu) 9%,transparent) 23px)}
2514
+ .card{aspect-ratio:1.75;background:var(--p);color:var(--su);padding:22px;display:flex;flex-direction:column;justify-content:space-between}
2515
+ .card .lk svg{height:26px}.card .nm{font-family:var(--disp);font-size:22px;font-weight:700}.card .tg{font-size:12px;opacity:.8}
2516
+ .slide{padding:26px;background:var(--su)}.slide .kicker{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--a);font-weight:600}
2517
+ .slide h4{font-family:var(--disp);font-size:25px;font-weight:700;margin-top:6px;line-height:1.15}.slide .bar{width:46px;height:4px;background:var(--a);margin-top:14px;border-radius:2px}
2518
+ .avatar{height:158px;display:flex;align-items:center;justify-content:center}.circle{width:84px;height:84px;border-radius:50%;background:var(--a);color:#fff;display:flex;align-items:center;justify-content:center;font-family:var(--disp);font-weight:700;font-size:30px}
2519
+ .foot{padding:36px 0;color:var(--mu);font-size:12px}
2520
+ @media(max-width:680px){.voice,.apps{grid-template-columns:1fr}}
2521
+ </style></head><body>
2522
+ <section class="cover"><div class="wrap">
2523
+ <div class="lockup">${lockup}</div>
2524
+ <div class="eyebrow" style="color:rgba(255,255,255,.7)">Brand Look Book</div>
2525
+ <h1>${hero}</h1>${tagline ? `<p>${tagline}</p>` : ""}
2526
+ <div style="margin-top:20px">${traits}</div>
2527
+ </div></section>
2528
+ <section><div class="wrap"><div class="eyebrow">Positioning</div><h2>${name}</h2>
2529
+ <p style="font-size:18px;max-width:680px;margin-top:8px">${esc2(guide.positioning || "")}</p></div></section>
2530
+ <section><div class="wrap"><div class="eyebrow">Color</div><h2>Palette</h2><div class="swatches" style="margin-top:20px">${swatches}</div></div></section>
2531
+ <section><div class="wrap"><div class="eyebrow">Type</div><h2>Typography</h2>
2532
+ <div class="big" style="margin-top:18px">${name}</div><div class="row">${esc2(disp)} for display</div>
2533
+ <p class="bodyspec">Set in ${esc2(body)} for body and UI. ${esc2(messaging[1] || "The quick brown fox jumps over the lazy dog 0123456789.")}</p></div></section>
2534
+ <section><div class="wrap"><div class="eyebrow">Voice</div><h2>How the brand sounds</h2>
2535
+ <p style="max-width:680px;margin-top:8px">${esc2(guide.voice?.tone || "")}</p>
2536
+ <div class="voice"><div class="col do"><h3>Do</h3><ul>${dos || "<li>(none)</li>"}</ul></div><div class="col dont"><h3>Don't</h3><ul>${donts || "<li>(none)</li>"}</ul></div></div></div></section>
2537
+ <section><div class="wrap"><div class="eyebrow">In the wild</div><h2>The identity applied</h2>
2538
+ <div class="apps" style="margin-top:20px">
2539
+ <div class="mock"><div class="appbar"><span class="lk">${lockup}</span><nav><span>Home</span><span>Pricing</span><span>Sign in</span></nav></div><div class="appbody"></div><div class="cap">App and website header</div></div>
2540
+ <div class="mock"><div class="card"><span class="lk" style="filter:brightness(0) invert(1)">${lockup}</span><div><div class="nm">${name}</div><div class="tg">${tagline}</div></div></div><div class="cap">Business card</div></div>
2541
+ <div class="mock"><div class="slide"><div class="kicker">${esc2((guide.personality || [])[0] || "Brand")}</div><h4>${hero}</h4><div class="bar"></div></div><div class="cap">Slide title</div></div>
2542
+ <div class="mock"><div class="avatar"><div class="circle">${esc2(initials)}</div></div><div class="cap">Social avatar</div></div>
2543
+ </div></div></section>
2544
+ <div class="wrap foot">${name} brand look book, generated locally by The Thought Layer. Type: ${esc2(disp)} / ${esc2(body)}.</div>
2545
+ </body></html>`;
2546
+ }
2547
+ var kindOf = (path) => path.endsWith(".md") ? "markdown" : path.endsWith(".svg") ? "svg" : path.endsWith(".html") ? "html" : path.endsWith(".json") ? "json" : "text";
2548
+ function buildArtifactSet(state, opts) {
2549
+ const files = {};
2550
+ const sources = {};
2551
+ const add = (path, content, source) => {
2552
+ if (content && content.trim()) {
2553
+ files[path] = content;
2554
+ sources[path] = source;
2555
+ }
2556
+ };
2557
+ const brand = state.brand && typeof state.brand === "object" ? state.brand : null;
2558
+ const grill = state.grill && typeof state.grill === "object" ? state.grill : null;
2559
+ const swot = state.swot && typeof state.swot === "object" ? state.swot : null;
2560
+ const prd = obj(state.prd);
2561
+ const bizModel = obj(state.bizModel);
2562
+ const research = obj(state.research);
2563
+ const assumptions = bizModel["assumptions"] || null;
2564
+ const prdMarkdown = str2(prd["markdown"]);
2565
+ add("PRD.md", prdMarkdown, "prd");
2566
+ if (grill?.requirements?.length) add("Requirements.md", requirementsMarkdown(grill), "grill");
2567
+ if (grill?.glossary?.length) add("DomainGlossary.md", glossaryMarkdown(grill), "grill");
2568
+ add("BuildPrompt.md", buildKitPrompt(grill, prdMarkdown, assumptions, brand, state.feedback), "prd+grill+bizModel+brand");
2569
+ const swotHasItems = !!swot && Object.values(swot).some((v) => Array.isArray(v) && v.some((x) => x && String(x).trim()));
2570
+ if (swotHasItems) {
2571
+ add("SWOT.md", swotMarkdown(swot), "swot");
2572
+ add("SWOT.svg", swotInfographicSvg(swot, brand), "swot");
2573
+ }
2574
+ const bizSvg = bizModelInfographicSvg(assumptions, brand);
2575
+ if (bizSvg) add("BusinessModel.svg", bizSvg, "bizModel");
2576
+ if (research["brief"]) {
2577
+ add("MarketResearch.md", `# Market Research
2578
+
2579
+ _${str2(research["description"])}_
2580
+
2581
+ ${str2(research["brief"])}`, "research");
2582
+ }
2583
+ const governance = obj(state.governance);
2584
+ if (str2(governance["report"]).trim()) {
2585
+ add("Compliance.md", str2(governance["report"]), "governance");
2586
+ }
2587
+ if (brand?.guide) {
2588
+ add("Brand/BrandStyleGuide.md", brandGuideMarkdown(brand.guide), "brand");
2589
+ const chosen = (brand.logos || []).find((l) => l.id === brand.chosenLogoId) || (brand.logos || [])[0];
2590
+ if (chosen?.svg) add("Brand/Logo.svg", chosen.svg, "brand");
2591
+ add("Brand/LookBook.html", brandLookbookHtml(brand.guide, chosen?.svg), "brand");
2592
+ }
2593
+ try {
2594
+ const spec = extractScaffoldSpec(state);
2595
+ const site = buildStarterSite(spec, { domain: opts.domain, founderName: opts.founderName });
2596
+ for (const [name, content] of Object.entries(site.files)) add(`LandingPage/${name}`, content, "scaffold");
2597
+ } catch {
2598
+ }
2599
+ const brandName = str2(obj(brand?.guide)["brandName"]) || "Your Product";
2600
+ add("README.md", readmeIndex(files, brandName), "index");
2601
+ const manifest = {
2602
+ app: "thought-layer",
2603
+ kind: "artifacts",
2604
+ version: 1,
2605
+ generatedAt: opts.generatedAt,
2606
+ brandName,
2607
+ files: Object.keys(files).sort().map((path) => ({
2608
+ path,
2609
+ bytes: Buffer.byteLength(files[path], "utf8"),
2610
+ kind: kindOf(path),
2611
+ source: sources[path] || "index"
2612
+ }))
2613
+ };
2614
+ return { files, manifest };
2615
+ }
2616
+ function readmeIndex(files, brandName) {
2617
+ const has = (p) => p in files;
2618
+ const lines = [];
2619
+ const row = (p, desc) => {
2620
+ if (has(p)) lines.push(`- **${p}**: ${desc}`);
2621
+ };
2622
+ row("PRD.md", "Complete product requirements document");
2623
+ row("Requirements.md", "Numbered, testable requirements by category");
2624
+ row("DomainGlossary.md", "Ubiquitous language for the domain");
2625
+ row("BuildPrompt.md", "Paste into an AI coding agent (Claude Code, Cursor) to build version 1");
2626
+ row("BusinessModel.svg", "The numbers as a one-page infographic");
2627
+ row("SWOT.md", "Strengths, weaknesses, opportunities, threats");
2628
+ row("SWOT.svg", "The SWOT as a poster infographic");
2629
+ row("MarketResearch.md", "The market research brief");
2630
+ row("Compliance.md", "Governance, licensing, and tax requirements to review with your legal and tax advisors");
2631
+ row("Brand/BrandStyleGuide.md", "Brand voice, palette, and typography");
2632
+ row("Brand/Logo.svg", "The chosen logo (vector, editable)");
2633
+ row("Brand/LookBook.html", "The identity applied; open in any browser");
2634
+ row("LandingPage/index.html", "A deployable landing page; drag onto app.netlify.com/drop");
2635
+ return `# ${brandName}: Thought Layer artifacts
2636
+
2637
+ Everything The Thought Layer built for this idea, delivered to your own repo.
2638
+
2639
+ ${lines.join("\n")}
2640
+ `;
2641
+ }
2642
+
2643
+ // core/artifacts-io.ts
2644
+ var ok2 = (message, details = {}) => ({ ok: true, message, details });
2645
+ var fail2 = (message, details = {}) => ({ ok: false, message, details });
2646
+ var STATE_DIR_ARTIFACTS = ["build.json", "deploy.json", "TRACEABILITY.md", "DECISIONS.md"];
2647
+ var ROOT_ARTIFACTS = ["BACKEND.md", "schema.sql", "netlify.toml", ".env.example"];
2648
+ var kindOf2 = (path) => path.endsWith(".md") ? "markdown" : path.endsWith(".svg") ? "svg" : path.endsWith(".html") ? "html" : path.endsWith(".json") ? "json" : "text";
2649
+ function repoOwnerName(repo) {
2650
+ const m = String(repo || "").trim().match(/(?:github\.com[:/])?([\w.-]+)\/([\w.-]+?)(?:\.git)?$/);
2651
+ if (!m) return null;
2652
+ if (m[1] === "." || m[1] === "..") return null;
2653
+ return `${m[1]}/${m[2]}`;
2654
+ }
2655
+ function runArtifacts(opts, ctx) {
2656
+ try {
2657
+ const cfg = loadConfig();
2658
+ const { cloneDir, ws } = resolveWorkspace(opts, cfg);
2659
+ if (!isGitRepo(cloneDir)) {
2660
+ return fail2(`No sessions workspace at ${cloneDir}. Run tl sync init --repo <owner/name> first, then save a session.`, { cloneDir });
2661
+ }
2662
+ const slug = slugify(opts.name || ws?.activeSession?.replace(/\.json$/, "") || "");
2663
+ if (!slug) return fail2("Name the session whose artifacts to deliver: tl artifacts --name <name>.");
2664
+ const sessionPath = join6(cloneDir, STATE_DIR, `${slug}.json`);
2665
+ const useExplicit = !!(opts.path && opts.path.trim() || (process.env["THOUGHT_LAYER_STATE"] || "").trim());
2666
+ const loadTarget = useExplicit ? opts.path : existsSync5(sessionPath) ? sessionPath : opts.path;
2667
+ const loaded = loadStateFile(loadTarget);
2668
+ if (!loaded.exists) {
2669
+ return fail2(`No session "${slug}" found (looked at ${loaded.path}). Save it first with tl sync save --name ${slug}.`, { cloneDir });
2670
+ }
2671
+ const { files, manifest } = buildArtifactSet(loaded.state, {
2672
+ generatedAt: ctx.generatedAt,
2673
+ domain: opts.domain,
2674
+ founderName: opts.founderName
2675
+ });
2676
+ const srcStateDir = dirname5(loaded.path);
2677
+ const srcRoot = basename(srcStateDir) === STATE_DIR ? dirname5(srcStateDir) : srcStateDir;
2678
+ const extra = [];
2679
+ const copyIfPresent = (fromDir, fname) => {
2680
+ const from = join6(fromDir, fname);
2681
+ if (!existsSync5(from)) return;
2682
+ try {
2683
+ const content = readFileSync4(from, "utf8");
2684
+ const rel = `Deploy/${fname}`;
2685
+ files[rel] = content;
2686
+ extra.push({ path: rel, bytes: Buffer.byteLength(content, "utf8"), kind: kindOf2(rel), source: "build/deploy" });
2687
+ } catch {
2688
+ }
2689
+ };
2690
+ STATE_DIR_ARTIFACTS.forEach((f) => copyIfPresent(srcStateDir, f));
2691
+ ROOT_ARTIFACTS.forEach((f) => copyIfPresent(srcRoot, f));
2692
+ manifest.files = [...manifest.files, ...extra].sort((a, b) => a.path.localeCompare(b.path));
2693
+ const baseRel = join6("artifacts", slug);
2694
+ const baseAbs = join6(cloneDir, baseRel);
2695
+ for (const [rel, content] of Object.entries(files)) {
2696
+ const dest = join6(baseAbs, rel);
2697
+ mkdirSync5(dirname5(dest), { recursive: true });
2698
+ writeFileSync6(dest, content);
2699
+ }
2700
+ writeFileSync6(join6(baseAbs, "artifacts.json"), JSON.stringify(manifest, null, 2) + "\n");
2701
+ const fileCount = Object.keys(files).length + 1;
2702
+ if (opts.noDeliver) {
2703
+ return ok2(
2704
+ `Generated ${fileCount} artifact file(s) into ${baseAbs} (local only; not committed).`,
2705
+ { cloneDir, dir: baseAbs, session: slug, files: Object.keys(files), count: fileCount, committed: false, pushed: false }
2706
+ );
2707
+ }
2708
+ git(cloneDir, ["add", "-f", "--", baseRel]);
2709
+ const msg = opts.message || `Deliver artifacts for ${slug}`;
2710
+ const committed = git(cloneDir, ["commit", "-m", msg]).status === 0;
2711
+ let pushed = false;
2712
+ let pushNote = "";
2713
+ if (committed && !opts.noPush) {
2714
+ const p = git(cloneDir, ["push"]);
2715
+ pushed = p.status === 0;
2716
+ if (!pushed) pushNote = ` Could not push (${(p.err || "").split("\n")[0] || "see git output"}); commit is local, run tl sync push when ready.`;
2717
+ }
2718
+ const branch = git(cloneDir, ["rev-parse", "--abbrev-ref", "HEAD"]).out.trim() || ws?.defaultBranch || "main";
2719
+ const ownerName = repoOwnerName(ws?.repo || "");
2720
+ const githubBase = ownerName ? `https://github.com/${ownerName}/blob/${branch}/${baseRel}` : null;
2721
+ const urls = {};
2722
+ if (githubBase) for (const rel of Object.keys(files)) urls[rel] = `${githubBase}/${rel.split("/").map(encodeURIComponent).join("/")}`;
2723
+ return ok2(
2724
+ `Delivered ${fileCount} artifact file(s) for "${slug}" to ${baseRel} in ${cloneDir}.${committed ? opts.noPush ? " Committed locally (no push)." : pushed ? " Committed and pushed." : pushNote : " Nothing changed since the last delivery."}${githubBase ? `
2725
+ View on GitHub: ${githubBase}` : ""}`,
2726
+ { cloneDir, dir: baseRel, session: slug, files: Object.keys(files), count: fileCount, committed, pushed, branch, repo: ownerName, githubBase, urls }
2727
+ );
2728
+ } catch (e) {
2729
+ return fail2(`tl_artifacts error: ${e.message}`);
2730
+ }
2731
+ }
2732
+
2733
+ // core/notion-io.ts
2734
+ import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync7 } from "fs";
2735
+ import { homedir as homedir2 } from "os";
2736
+ import { dirname as dirname6, join as join7 } from "path";
2737
+
2738
+ // core/notion.ts
2739
+ var NOTION_FREE_FILE_LIMIT = 5 * 1024 * 1024;
2740
+ var RICH_TEXT_MAX = 2e3;
2741
+ var CHILDREN_MAX = 100;
2742
+ var obj2 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
2743
+ var str3 = (v) => typeof v === "string" ? v : "";
2744
+ var rtSeg = (content, ann, link) => ({
2745
+ type: "text",
2746
+ text: { content, ...link ? { link: { url: link } } : {} },
2747
+ ...ann ? { annotations: ann } : {}
2748
+ });
2749
+ function chunkRichText(text, ann, link) {
2750
+ const s = String(text ?? "");
2751
+ if (s.length <= RICH_TEXT_MAX) return [rtSeg(s, ann, link)];
2752
+ const out = [];
2753
+ for (let i = 0; i < s.length; i += RICH_TEXT_MAX) out.push(rtSeg(s.slice(i, i + RICH_TEXT_MAX), ann, link));
2754
+ return out;
2755
+ }
2756
+ function inline(text) {
2757
+ const out = [];
2758
+ const re = /\*\*([^*]+)\*\*|`([^`]+)`/g;
2759
+ let last = 0;
2760
+ let m;
2761
+ while (m = re.exec(text)) {
2762
+ if (m.index > last) out.push(...chunkRichText(text.slice(last, m.index)));
2763
+ if (m[1] != null) out.push(...chunkRichText(m[1], { bold: true }));
2764
+ else if (m[2] != null) out.push(...chunkRichText(m[2], { code: true }));
2765
+ last = re.lastIndex;
2766
+ }
2767
+ if (last < text.length) out.push(...chunkRichText(text.slice(last)));
2768
+ return out.length ? out : [rtSeg("")];
2769
+ }
2770
+ var para = (text) => ({ object: "block", type: "paragraph", paragraph: { rich_text: inline(text) } });
2771
+ var heading = (level, text) => {
2772
+ const t = `heading_${level}`;
2773
+ return { object: "block", type: t, [t]: { rich_text: inline(text) } };
2774
+ };
2775
+ var bullet = (text) => ({ object: "block", type: "bulleted_list_item", bulleted_list_item: { rich_text: inline(text) } });
2776
+ var numbered = (text) => ({ object: "block", type: "numbered_list_item", numbered_list_item: { rich_text: inline(text) } });
2777
+ var quote = (text) => ({ object: "block", type: "quote", quote: { rich_text: inline(text) } });
2778
+ var callout = (text, emoji = "\u{1F4A1}") => ({ object: "block", type: "callout", callout: { rich_text: inline(text), icon: { type: "emoji", emoji } } });
2779
+ var divider = () => ({ object: "block", type: "divider", divider: {} });
2780
+ var codeBlock = (text, language = "plain text") => ({ object: "block", type: "code", code: { rich_text: chunkRichText(text), language } });
2781
+ function table(headers, rows) {
2782
+ const toRow = (cells) => ({ object: "block", type: "table_row", table_row: { cells: cells.map((c) => chunkRichText(c)) } });
2783
+ return {
2784
+ object: "block",
2785
+ type: "table",
2786
+ table: { table_width: headers.length, has_column_header: true, has_row_header: false, children: [toRow(headers), ...rows.map(toRow)] }
2787
+ };
2788
+ }
2789
+ function markdownToBlocks(md) {
2790
+ const lines = String(md ?? "").split("\n");
2791
+ const out = [];
2792
+ let i = 0;
2793
+ while (i < lines.length) {
2794
+ const raw = lines[i] ?? "";
2795
+ const t = raw.trim();
2796
+ if (t.startsWith("```")) {
2797
+ const buf = [];
2798
+ i++;
2799
+ while (i < lines.length && !(lines[i] ?? "").trim().startsWith("```")) {
2800
+ buf.push(lines[i] ?? "");
2801
+ i++;
2802
+ }
2803
+ i++;
2804
+ out.push(codeBlock(buf.join("\n")));
2805
+ continue;
2806
+ }
2807
+ if (!t) {
2808
+ i++;
2809
+ continue;
2810
+ }
2811
+ if (t === "---" || t === "***" || t === "___") {
2812
+ out.push(divider());
2813
+ i++;
2814
+ continue;
2815
+ }
2816
+ const mh = t.match(/^(#{1,6})\s+(.*)$/);
2817
+ if (mh) {
2818
+ out.push(heading(Math.min(3, mh[1].length), mh[2]));
2819
+ i++;
2820
+ continue;
2821
+ }
2822
+ if (t.startsWith("> ")) {
2823
+ out.push(quote(t.slice(2)));
2824
+ i++;
2825
+ continue;
2826
+ }
2827
+ const mb = t.match(/^[-*]\s+(.*)$/);
2828
+ if (mb) {
2829
+ out.push(bullet(mb[1]));
2830
+ i++;
2831
+ continue;
2832
+ }
2833
+ const mn = t.match(/^\d+\.\s+(.*)$/);
2834
+ if (mn) {
2835
+ out.push(numbered(mn[1]));
2836
+ i++;
2837
+ continue;
2838
+ }
2839
+ out.push(para(t));
2840
+ i++;
2841
+ }
2842
+ return out;
2843
+ }
2844
+ function richTextToMarkdown(rt) {
2845
+ if (!rt || !rt.length) return "";
2846
+ const sig = (s) => {
2847
+ const a = s.annotations || {};
2848
+ return `${a.bold ? 1 : 0}${a.italic ? 1 : 0}${a.code ? 1 : 0}|${s.text.link?.url || ""}`;
2849
+ };
2850
+ const merged = [];
2851
+ for (const s of rt) {
2852
+ const prev = merged[merged.length - 1];
2853
+ if (prev && sig(prev) === sig(s)) prev.text.content += s.text.content;
2854
+ else merged.push({ type: "text", text: { content: s.text.content, ...s.text.link ? { link: s.text.link } : {} }, ...s.annotations ? { annotations: { ...s.annotations } } : {} });
2855
+ }
2856
+ return merged.map((seg) => {
2857
+ const a = seg.annotations || {};
2858
+ let t = seg.text.content;
2859
+ if (a.code) t = "`" + t + "`";
2860
+ else {
2861
+ if (a.bold) t = `**${t}**`;
2862
+ if (a.italic) t = `*${t}*`;
2863
+ }
2864
+ const link = seg.text.link?.url;
2865
+ return link ? `[${t}](${link})` : t;
2866
+ }).join("");
2867
+ }
2868
+ function richTextToPlain(rt) {
2869
+ return (rt || []).map((s) => s.text.content).join("");
2870
+ }
2871
+ function tableToMarkdown(tbl) {
2872
+ const rows = (tbl["children"] || []).map(
2873
+ (r) => (obj2(r["table_row"])["cells"] || []).map(
2874
+ (c) => richTextToPlain(c).replace(/\|/g, "\\|").replace(/\n+/g, " ").trim()
2875
+ )
2876
+ );
2877
+ if (!rows.length) return "";
2878
+ const width = Math.max(...rows.map((r) => r.length));
2879
+ const pad = (r) => {
2880
+ const c = r.slice();
2881
+ while (c.length < width) c.push("");
2882
+ return c;
2883
+ };
2884
+ const line = (cells) => `| ${cells.join(" | ")} |`;
2885
+ const [header, ...body] = rows;
2886
+ const out = [line(pad(header)), line(pad(header).map(() => "---"))];
2887
+ for (const r of body) out.push(line(pad(r)));
2888
+ return out.join("\n");
2889
+ }
2890
+ function blocksToMarkdown(blocks) {
2891
+ const isList = (t) => t === "bulleted_list_item" || t === "numbered_list_item";
2892
+ const parts = [];
2893
+ let prevType = "";
2894
+ let numbered2 = 0;
2895
+ for (const b of blocks || []) {
2896
+ const type = str3(b["type"]);
2897
+ const data = obj2(b[type]);
2898
+ const rt = data["rich_text"] || [];
2899
+ if (type !== "numbered_list_item") numbered2 = 0;
2900
+ let rendered;
2901
+ switch (type) {
2902
+ case "heading_1":
2903
+ rendered = `# ${richTextToMarkdown(rt)}`;
2904
+ break;
2905
+ case "heading_2":
2906
+ rendered = `## ${richTextToMarkdown(rt)}`;
2907
+ break;
2908
+ case "heading_3":
2909
+ rendered = `### ${richTextToMarkdown(rt)}`;
2910
+ break;
2911
+ case "bulleted_list_item":
2912
+ rendered = `- ${richTextToMarkdown(rt)}`;
2913
+ break;
2914
+ case "numbered_list_item":
2915
+ rendered = `${++numbered2}. ${richTextToMarkdown(rt)}`;
2916
+ break;
2917
+ case "quote":
2918
+ rendered = `> ${richTextToMarkdown(rt)}`;
2919
+ break;
2920
+ case "callout": {
2921
+ const emoji = str3(obj2(data["icon"])["emoji"]);
2922
+ rendered = `> ${emoji ? `${emoji} ` : ""}${richTextToMarkdown(rt)}`;
2923
+ break;
2924
+ }
2925
+ case "divider":
2926
+ rendered = "---";
2927
+ break;
2928
+ case "code": {
2929
+ const lang = str3(data["language"]);
2930
+ rendered = "```" + (lang && lang !== "plain text" ? lang : "") + "\n" + richTextToPlain(rt) + "\n```";
2931
+ break;
2932
+ }
2933
+ case "table":
2934
+ rendered = tableToMarkdown(data);
2935
+ break;
2936
+ case "bookmark":
2937
+ rendered = str3(data["url"]);
2938
+ break;
2939
+ case "image": {
2940
+ const u = str3(obj2(data["external"])["url"]);
2941
+ rendered = u ? `![](${u})` : "";
2942
+ break;
2943
+ }
2944
+ case "paragraph":
2945
+ rendered = richTextToMarkdown(rt);
2946
+ break;
2947
+ default:
2948
+ rendered = richTextToMarkdown(rt);
2949
+ }
2950
+ if (!rendered && type !== "divider") continue;
2951
+ const sep = parts.length === 0 ? "" : isList(type) && isList(prevType) ? "\n" : "\n\n";
2952
+ parts.push(sep + rendered);
2953
+ prevType = type;
2954
+ }
2955
+ return parts.join("");
2956
+ }
2957
+ function chunkChildren(blocks, size = CHILDREN_MAX) {
2958
+ const out = [];
2959
+ for (let i = 0; i < blocks.length; i += size) out.push(blocks.slice(i, i + size));
2960
+ return out.length ? out : [[]];
2961
+ }
2962
+ function artifactCategory(path) {
2963
+ if (path.startsWith("Brand/")) return "Brand";
2964
+ if (path.startsWith("Deploy/")) return "Deploy";
2965
+ if (path.startsWith("LandingPage/")) return "Landing";
2966
+ if (path.endsWith(".svg")) return "Infographic";
2967
+ if (path === "BuildPrompt.md") return "Build prompt";
2968
+ return "Doc";
2969
+ }
2970
+ var WIKI_AREAS = [
2971
+ { key: "big-idea", title: "The Big Idea", emoji: "\u{1F4A1}" },
2972
+ { key: "business-model", title: "Business Model", emoji: "\u{1F4B0}" },
2973
+ { key: "brand", title: "Brand", emoji: "\u{1F3A8}" },
2974
+ { key: "market-research", title: "Market Research", emoji: "\u{1F4CA}" },
2975
+ { key: "strategy", title: "Strategy", emoji: "\u{1F4C8}" },
2976
+ { key: "product", title: "Product (PRD)", emoji: "\u{1F4CB}" },
2977
+ { key: "compliance", title: "Compliance & Tax", emoji: "\u2696\uFE0F" },
2978
+ { key: "decision-science", title: "Decision Science", emoji: "\u{1F9ED}" },
2979
+ { key: "library", title: "Library", emoji: "\u{1F4DA}" }
2980
+ ];
2981
+ function buildWikiPlan(state, opts = {}) {
2982
+ const answers = obj2(state.answers);
2983
+ const brand = state.brand && typeof state.brand === "object" ? state.brand : null;
2984
+ const guide = brand?.guide || null;
2985
+ const grill = state.grill && typeof state.grill === "object" ? state.grill : null;
2986
+ const swot = state.swot && typeof state.swot === "object" ? state.swot : null;
2987
+ const prd = obj2(state.prd);
2988
+ const bizModel = obj2(state.bizModel);
2989
+ const research = obj2(state.research);
2990
+ const assumptions = bizModel["assumptions"] || null;
2991
+ const brandName = str3(guide?.brandName) || "Your Product";
2992
+ const oneLiner = str3(answers["what-statement"]) || str3(answers["pitch"]) || str3(guide?.positioning);
2993
+ const overview = [];
2994
+ if (oneLiner) overview.push(callout(oneLiner, "\u{1F4A1}"));
2995
+ overview.push(para("This private workspace was generated by The Thought Layer. Each section below is a page; the Artifacts database links the files delivered to your repo."));
2996
+ const areaBlocks = {};
2997
+ {
2998
+ const b = [];
2999
+ if (oneLiner) b.push(heading(2, "What it is"), callout(oneLiner, "\u{1F3AF}"));
3000
+ if (guide?.positioning) b.push(heading(2, "Who it is for"), para(guide.positioning));
3001
+ const press = str3(answers["press-release"]);
3002
+ if (press) b.push(heading(2, "The press release"), ...markdownToBlocks(press));
3003
+ areaBlocks["big-idea"] = b;
3004
+ }
3005
+ {
3006
+ const b = [];
3007
+ const proj = computeProjection(assumptions);
3008
+ if (proj && assumptions) {
3009
+ const s = proj.summary;
3010
+ const cur = assumptions.currency || "USD";
3011
+ b.push(heading(2, "The numbers"));
3012
+ b.push(callout(
3013
+ `Year 1 revenue ${fmtMoney(s.year1Revenue, cur)}. Monthly break-even ${s.breakEvenMonth ? `month ${s.breakEvenMonth}` : "beyond horizon"}. Max cash drawdown ${fmtMoney(s.maxDrawdown, cur)}. MRR at month ${s.horizon} is ${fmtMoney(s.endingMRR, cur)}.`,
3014
+ "\u{1F4B0}"
3015
+ ));
3016
+ const rows = (assumptions.parties || []).slice(0, 12).map((p) => [
3017
+ str3(p.name) || p.id,
3018
+ str3(p.role),
3019
+ String(p.startingCount ?? ""),
3020
+ String(p.monthlyNewBase ?? ""),
3021
+ `${p.monthlyChurnPct ?? 0}%`,
3022
+ fmtMoney(Number(p.revenuePerUnitPerMonth) || 0, cur),
3023
+ fmtMoney(Number(p.cacPerUnit) || 0, cur)
3024
+ ]);
3025
+ b.push(heading(2, "Parties"), table(["Party", "Role", "Start", "New/mo", "Churn", "Rev/unit/mo", "CAC"], rows));
3026
+ if (assumptions.narrative) b.push(heading(2, "Notes"), para(assumptions.narrative));
3027
+ }
3028
+ areaBlocks["business-model"] = b;
3029
+ }
3030
+ {
3031
+ const b = [];
3032
+ if (guide) {
3033
+ b.push(...markdownToBlocks(brandGuideMarkdown(guide)));
3034
+ const pal = (guide.palette || []).filter((p) => p?.hex);
3035
+ if (pal.length) b.push(heading(2, "Palette"), table(["Color", "Hex", "Role"], pal.map((p) => [str3(p.name), str3(p.hex), str3(p.role)])));
3036
+ }
3037
+ areaBlocks["brand"] = b;
3038
+ }
3039
+ {
3040
+ const b = [];
3041
+ const brief = str3(research["brief"]);
3042
+ if (brief) {
3043
+ const desc = str3(research["description"]);
3044
+ if (desc) b.push(callout(desc, "\u{1F4CA}"));
3045
+ b.push(...markdownToBlocks(brief));
3046
+ }
3047
+ areaBlocks["market-research"] = b;
3048
+ }
3049
+ {
3050
+ const b = [];
3051
+ const hasSwot = !!swot && Object.values(swot).some((v) => Array.isArray(v) && v.some((x) => x && String(x).trim()));
3052
+ if (hasSwot) b.push(...markdownToBlocks(swotMarkdown(swot)));
3053
+ areaBlocks["strategy"] = b;
3054
+ }
3055
+ {
3056
+ const b = [];
3057
+ const prdMd = str3(prd["markdown"]);
3058
+ if (prdMd) b.push(...markdownToBlocks(prdMd));
3059
+ if (grill?.requirements?.length) {
3060
+ b.push(divider());
3061
+ b.push(...markdownToBlocks(requirementsMarkdown(grill)));
3062
+ }
3063
+ if (grill?.glossary?.length) {
3064
+ b.push(divider());
3065
+ b.push(...markdownToBlocks(glossaryMarkdown(grill)));
3066
+ }
3067
+ areaBlocks["product"] = b;
3068
+ }
3069
+ {
3070
+ const b = [];
3071
+ const report = str3(obj2(state.governance)["report"]);
3072
+ if (report.trim()) b.push(...markdownToBlocks(report));
3073
+ areaBlocks["compliance"] = b;
3074
+ }
3075
+ {
3076
+ const b = [];
3077
+ const dq = Object.keys(answers).filter((k) => /^(dq|decision)/i.test(k) && str3(answers[k]).trim());
3078
+ if (dq.length) {
3079
+ b.push(heading(2, "Decision records"));
3080
+ for (const k of dq) b.push(bullet(`**${k}**: ${str3(answers[k])}`));
3081
+ }
3082
+ areaBlocks["decision-science"] = b;
3083
+ }
3084
+ areaBlocks["library"] = [];
3085
+ const areas = WIKI_AREAS.map((a) => ({ ...a, blocks: areaBlocks[a.key] || [] })).filter((a) => a.blocks.length > 0);
3086
+ const urls = opts.urls || {};
3087
+ const files = opts.manifest?.files || [];
3088
+ const artifacts = files.filter((f) => !(f.path.startsWith("LandingPage/") && f.path !== "LandingPage/index.html")).map((f) => ({ name: f.path, path: f.path, category: artifactCategory(f.path), bytes: f.bytes, ...urls[f.path] ? { url: urls[f.path] } : {} }));
3089
+ return { title: `${brandName} workspace`, icon: "\u{1F680}", overview, areas, artifacts };
3090
+ }
3091
+ function wikiPlanToMarkdown(plan) {
3092
+ return {
3093
+ title: plan.title,
3094
+ icon: plan.icon,
3095
+ overview: blocksToMarkdown(plan.overview),
3096
+ areas: plan.areas.map((a) => ({ key: a.key, title: a.title, emoji: a.emoji, markdown: blocksToMarkdown(a.blocks) })),
3097
+ artifacts: plan.artifacts
3098
+ };
3099
+ }
3100
+
3101
+ // core/notion-io.ts
3102
+ var NOTION_API = "https://api.notion.com/v1";
3103
+ var NOTION_VERSION = "2022-06-28";
3104
+ var MIN_INTERVAL_MS = 350;
3105
+ var TOKEN_ENVS = ["THOUGHT_LAYER_NOTION_TOKEN", "NOTION_TOKEN"];
3106
+ var ok3 = (message, details = {}) => ({ ok: true, message, details });
3107
+ var fail3 = (message, details = {}) => ({ ok: false, message, details });
3108
+ function notionConfigPath() {
3109
+ return process.env["THOUGHT_LAYER_NOTION_CONFIG"] || join7(homedir2(), ".thought-layer", "notion.json");
3110
+ }
3111
+ function loadNotionConfig() {
3112
+ const p = notionConfigPath();
3113
+ if (!existsSync6(p)) return { schema: 1, sessions: {} };
3114
+ try {
3115
+ const raw = JSON.parse(readFileSync5(p, "utf8"));
3116
+ const sessions = raw["sessions"] && typeof raw["sessions"] === "object" ? raw["sessions"] : {};
3117
+ return { schema: 1, sessions };
3118
+ } catch {
3119
+ return { schema: 1, sessions: {} };
3120
+ }
3121
+ }
3122
+ function saveNotionConfig(cfg) {
3123
+ const p = notionConfigPath();
3124
+ mkdirSync6(dirname6(p), { recursive: true });
3125
+ writeFileSync7(p, JSON.stringify(cfg, null, 2) + "\n");
3126
+ }
3127
+ function pageIdFromInput(s) {
3128
+ const t = String(s || "").trim();
3129
+ const dashed = t.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/);
3130
+ if (dashed) return dashed[0].toLowerCase();
3131
+ const runs = t.match(/[0-9a-fA-F]{32}/g);
3132
+ if (!runs || !runs.length) return null;
3133
+ const id = runs[runs.length - 1].toLowerCase();
3134
+ return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
3135
+ }
3136
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
3137
+ var Notion = class {
3138
+ constructor(token) {
3139
+ this.token = token;
3140
+ }
3141
+ token;
3142
+ last = 0;
3143
+ async call(method, path, body) {
3144
+ for (let attempt = 0; ; attempt++) {
3145
+ const wait = MIN_INTERVAL_MS - (Date.now() - this.last);
3146
+ if (wait > 0) await sleep(wait);
3147
+ this.last = Date.now();
3148
+ const res = await fetch(`${NOTION_API}${path}`, {
3149
+ method,
3150
+ headers: {
3151
+ Authorization: `Bearer ${this.token}`,
3152
+ "Notion-Version": NOTION_VERSION,
3153
+ "Content-Type": "application/json"
3154
+ },
3155
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
3156
+ });
3157
+ if (res.status === 429 && attempt < 4) {
3158
+ const retry = Number(res.headers.get("Retry-After")) || Math.pow(2, attempt);
3159
+ await sleep(retry * 1e3);
3160
+ continue;
3161
+ }
3162
+ let json = {};
3163
+ try {
3164
+ json = await res.json();
3165
+ } catch {
3166
+ }
3167
+ return { status: res.status, json };
3168
+ }
3169
+ }
3170
+ async pageExists(id) {
3171
+ const r = await this.call("GET", `/pages/${id}`);
3172
+ return r.status === 200 && r.json["archived"] !== true;
3173
+ }
3174
+ async createPage(parentPageId, title, emoji) {
3175
+ const r = await this.call("POST", "/pages", {
3176
+ parent: { page_id: parentPageId },
3177
+ ...emoji ? { icon: { type: "emoji", emoji } } : {},
3178
+ properties: { title: { title: [{ text: { content: title } }] } }
3179
+ });
3180
+ if (r.status !== 200) throw new Error(notionErr("create page", r));
3181
+ return String(r.json["id"]);
3182
+ }
3183
+ // Replace a page's content: delete every existing child, then append the new
3184
+ // blocks in <=100-block batches.
3185
+ async replaceChildren(pageId, blocks) {
3186
+ let cursor;
3187
+ do {
3188
+ const q = cursor ? `?start_cursor=${cursor}&page_size=100` : "?page_size=100";
3189
+ const r = await this.call("GET", `/blocks/${pageId}/children${q}`);
3190
+ if (r.status !== 200) break;
3191
+ for (const b of r.json["results"] || []) await this.call("DELETE", `/blocks/${b.id}`);
3192
+ cursor = r.json["has_more"] ? String(r.json["next_cursor"]) : void 0;
3193
+ } while (cursor);
3194
+ for (const batch of chunkChildren(blocks)) {
3195
+ if (!batch.length) continue;
3196
+ const r = await this.call("PATCH", `/blocks/${pageId}/children`, { children: batch });
3197
+ if (r.status !== 200) throw new Error(notionErr("append blocks", r));
3198
+ }
3199
+ }
3200
+ async createArtifactsDb(parentPageId, artifacts) {
3201
+ const cats = Array.from(new Set(artifacts.map((a) => a.category)));
3202
+ const r = await this.call("POST", "/databases", {
3203
+ parent: { type: "page_id", page_id: parentPageId },
3204
+ title: [{ text: { content: "Artifacts" } }],
3205
+ icon: { type: "emoji", emoji: "\u{1F4CE}" },
3206
+ properties: {
3207
+ Name: { title: {} },
3208
+ Category: { select: { options: cats.map((c) => ({ name: c })) } },
3209
+ Size: { rich_text: {} },
3210
+ Link: { url: {} }
3211
+ }
3212
+ });
3213
+ if (r.status !== 200) throw new Error(notionErr("create database", r));
3214
+ return String(r.json["id"]);
3215
+ }
3216
+ async addArtifactRow(dbId, a) {
3217
+ await this.call("POST", "/pages", {
3218
+ parent: { database_id: dbId },
3219
+ properties: {
3220
+ Name: { title: [{ text: { content: a.name } }] },
3221
+ Category: { select: { name: a.category } },
3222
+ Size: { rich_text: [{ text: { content: humanSize(a.bytes) } }] },
3223
+ ...a.url ? { Link: { url: a.url } } : {}
3224
+ }
3225
+ });
3226
+ }
3227
+ };
3228
+ function notionErr(what, r) {
3229
+ const msg = String(r.json["message"] || "").slice(0, 200);
3230
+ return `Notion ${what} failed (${r.status})${msg ? `: ${msg}` : ""}`;
3231
+ }
3232
+ function humanSize(bytes) {
3233
+ if (bytes < 1024) return `${bytes} B`;
3234
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
3235
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
3236
+ }
3237
+ async function runWiki(opts) {
3238
+ try {
3239
+ const cfg = loadConfig();
3240
+ const { cloneDir, ws } = resolveWorkspace(opts, cfg);
3241
+ if (!isGitRepo(cloneDir)) {
3242
+ return fail3(`No sessions workspace at ${cloneDir}. Run tl sync init then tl artifacts before building the wiki.`, { cloneDir });
3243
+ }
3244
+ const slug = slugify(opts.name || ws?.activeSession?.replace(/\.json$/, "") || "");
3245
+ if (!slug) return fail3("Name the session to publish: tl wiki --name <name>.");
3246
+ const sessionPath = join7(cloneDir, STATE_DIR, `${slug}.json`);
3247
+ const useExplicit = !!(opts.path && opts.path.trim() || (process.env["THOUGHT_LAYER_STATE"] || "").trim());
3248
+ const loaded = loadStateFile(useExplicit ? opts.path : existsSync6(sessionPath) ? sessionPath : opts.path);
3249
+ if (!loaded.exists) return fail3(`No session "${slug}" found (looked at ${loaded.path}). Save it first with tl sync save --name ${slug}.`, { cloneDir });
3250
+ const artifactsDir = join7(cloneDir, "artifacts", slug);
3251
+ let manifest = null;
3252
+ const manifestPath = join7(artifactsDir, "artifacts.json");
3253
+ if (existsSync6(manifestPath)) {
3254
+ try {
3255
+ manifest = JSON.parse(readFileSync5(manifestPath, "utf8"));
3256
+ } catch {
3257
+ manifest = null;
3258
+ }
3259
+ }
3260
+ const branch = git(cloneDir, ["rev-parse", "--abbrev-ref", "HEAD"]).out.trim() || ws?.defaultBranch || "main";
3261
+ const ownerName = repoOwnerName(ws?.repo || "");
3262
+ const urls = {};
3263
+ if (ownerName && manifest) {
3264
+ const base = `https://github.com/${ownerName}/blob/${branch}/artifacts/${slug}`;
3265
+ for (const f of manifest.files) urls[f.path] = `${base}/${f.path.split("/").map(encodeURIComponent).join("/")}`;
3266
+ }
3267
+ const plan = buildWikiPlan(loaded.state, { manifest, urls });
3268
+ const blockCount = plan.overview.length + plan.areas.reduce((n, a) => n + a.blocks.length, 0);
3269
+ if (opts.dryRun) {
3270
+ const areaList = plan.areas.map((a) => `${a.emoji} ${a.title} (${a.blocks.length} blocks)`).join(", ");
3271
+ return ok3(
3272
+ `Dry run for "${plan.title}": ${plan.areas.length} area page(s), ${blockCount} blocks total, ${plan.artifacts.length} artifact(s) in the database.
3273
+ Areas: ${areaList || "(none with content yet)"}.${manifest ? "" : "\nNo delivered artifacts found; run tl artifacts first to populate the Artifacts database with GitHub links."}`,
3274
+ { title: plan.title, areas: plan.areas.map((a) => ({ key: a.key, blocks: a.blocks.length })), blockCount, artifacts: plan.artifacts.length, delivered: !!manifest }
3275
+ );
3276
+ }
3277
+ if (opts.emitPlan) {
3278
+ const pm = wikiPlanToMarkdown(plan);
3279
+ const areaList = pm.areas.map((a) => `${a.emoji} ${a.title}`).join(", ");
3280
+ return ok3(
3281
+ `Wiki plan for "${plan.title}" ready (no Notion call, no token needed): a root page, ${pm.areas.length} child page(s), ${blockCount} blocks, ${pm.artifacts.length} artifact(s). Create them with your connected Notion MCP: a root page "${plan.title}", one child page per area (each area's markdown is in the plan), and an Artifacts database with the listed files.${manifest ? "" : " No delivered artifacts were found, so the Artifacts list is empty; run tl artifacts first to add GitHub links."}
3282
+ Areas: ${areaList || "(none with content yet)"}.`,
3283
+ { plan: pm, delivered: !!manifest }
3284
+ );
3285
+ }
3286
+ const token = TOKEN_ENVS.map((e) => process.env[e]).find((v) => v && v.trim())?.trim() || "";
3287
+ if (!token) {
3288
+ return fail3(
3289
+ "No Notion token. Create an internal integration at https://www.notion.so/my-integrations, copy its secret, and set THOUGHT_LAYER_NOTION_TOKEN. Then share a Notion page with the integration and pass it as --parent-page.",
3290
+ { needs: "token" }
3291
+ );
3292
+ }
3293
+ const ncfg = loadNotionConfig();
3294
+ const entry = ncfg.sessions[slug] || {};
3295
+ const notion = new Notion(token);
3296
+ let rootId = opts.replace ? "" : entry.rootPageId || "";
3297
+ if (rootId && !await notion.pageExists(rootId)) rootId = "";
3298
+ if (!rootId) {
3299
+ const parentInput = opts.parentPage || process.env["THOUGHT_LAYER_NOTION_PARENT"] || "";
3300
+ const parentId = pageIdFromInput(parentInput);
3301
+ if (!parentId) {
3302
+ return fail3(
3303
+ "No Notion parent page. Share a page with your integration in Notion (Share, then add your integration), then pass it as --parent-page <id or url>.",
3304
+ { needs: "parent-page" }
3305
+ );
3306
+ }
3307
+ if (!await notion.pageExists(parentId)) {
3308
+ return fail3(`Notion cannot see the parent page ${parentId}. In Notion, open the page, click Share, and add your integration so it has access.`, { needs: "share" });
3309
+ }
3310
+ rootId = await notion.createPage(parentId, plan.title, plan.icon);
3311
+ entry.areaPageIds = {};
3312
+ entry.artifactsDbId = void 0;
3313
+ }
3314
+ await notion.replaceChildren(rootId, plan.overview);
3315
+ const areaIds = entry.areaPageIds || {};
3316
+ for (const area of plan.areas) {
3317
+ let pid = areaIds[area.key] || "";
3318
+ if (pid && !await notion.pageExists(pid)) pid = "";
3319
+ if (!pid) pid = await notion.createPage(rootId, `${area.emoji} ${area.title}`, area.emoji);
3320
+ areaIds[area.key] = pid;
3321
+ await notion.replaceChildren(pid, area.blocks);
3322
+ }
3323
+ let dbId = entry.artifactsDbId || "";
3324
+ if (plan.artifacts.length && (!dbId || opts.replace)) {
3325
+ dbId = await notion.createArtifactsDb(rootId, plan.artifacts);
3326
+ for (const a of plan.artifacts) await notion.addArtifactRow(dbId, a);
3327
+ }
3328
+ ncfg.sessions[slug] = { rootPageId: rootId, areaPageIds: areaIds, artifactsDbId: dbId || void 0, updatedAt: Date.now() };
3329
+ saveNotionConfig(ncfg);
3330
+ const rootUrl = `https://www.notion.so/${rootId.replace(/-/g, "")}`;
3331
+ return ok3(
3332
+ `Built the "${plan.title}" wiki in Notion: ${plan.areas.length} area page(s), ${blockCount} blocks, ${plan.artifacts.length} artifact(s) listed.${manifest ? "" : " No delivered artifacts were found, so the Artifacts database links are empty; run tl artifacts first."}
3333
+ Open it: ${rootUrl}`,
3334
+ { title: plan.title, rootPageId: rootId, rootUrl, areas: plan.areas.length, blockCount, artifacts: plan.artifacts.length }
3335
+ );
3336
+ } catch (e) {
3337
+ return fail3(`tl_wiki error: ${e.message}`);
3338
+ }
3339
+ }
3340
+
1996
3341
  // bin/tl.ts
1997
3342
  var HELP = `tl - read/write a portable Thought Layer state file (default: .thought-layer/state.json)
1998
3343
 
@@ -2003,6 +3348,11 @@ var HELP = `tl - read/write a portable Thought Layer state file (default: .thoug
2003
3348
  [--static-only] [--provision-db] [--apply-schema] when build.json has a backend: ships functions+env by default; flags opt out or add Neon provision/schema
2004
3349
  tl sync <init|save|list|open|pull|push|status> store/sync your session files in your own private GitHub repo
2005
3350
  [--repo owner/name] [--name x] [--dir p] [--workspace w] [--message m] [--no-push]
3351
+ tl artifacts [--name x] [--workspace w] deliver the full asset bundle (PRD, brand, infographics, landing, deploy rules) to your sessions repo
3352
+ [--no-push] [--no-deliver] [--domain x.com] [--founder "Name"]
3353
+ tl wiki [--parent-page id|url] [--name x] build/refresh a private Notion wiki from the session + delivered artifacts
3354
+ [--workspace w] [--replace] [--dry-run] token path: set THOUGHT_LAYER_NOTION_TOKEN and share a Notion page with your integration
3355
+ [--emit-plan] or emit the wiki as per-area markdown to build via a connected Notion MCP (no token)
2006
3356
  tl export [path] handoff check
2007
3357
  tl answer <qId> <value> [path] record an answer
2008
3358
  tl feedback --data '<json>' record a panel verdict ({qId,mode,personas,endState,round})
@@ -2037,7 +3387,7 @@ function parseArgs(argv) {
2037
3387
  function readData(flags) {
2038
3388
  const d = flags["data"];
2039
3389
  if (d === void 0) return void 0;
2040
- const raw = d === "-" || d === true ? readFileSync4(0, "utf8") : String(d);
3390
+ const raw = d === "-" || d === true ? readFileSync6(0, "utf8") : String(d);
2041
3391
  try {
2042
3392
  return JSON.parse(raw);
2043
3393
  } catch {
@@ -2131,6 +3481,42 @@ function main() {
2131
3481
  });
2132
3482
  return;
2133
3483
  }
3484
+ if (args[0] === "artifacts") {
3485
+ const r2 = runArtifacts(
3486
+ {
3487
+ path: typeof flags["path"] === "string" ? flags["path"] : void 0,
3488
+ name: typeof flags["name"] === "string" ? flags["name"] : void 0,
3489
+ workspace: typeof flags["workspace"] === "string" ? flags["workspace"] : void 0,
3490
+ dir: typeof flags["dir"] === "string" ? flags["dir"] : void 0,
3491
+ message: typeof flags["message"] === "string" ? flags["message"] : void 0,
3492
+ noPush: flags["no-push"] === true,
3493
+ noDeliver: flags["no-deliver"] === true,
3494
+ domain: typeof flags["domain"] === "string" ? flags["domain"] : void 0,
3495
+ founderName: typeof flags["founder"] === "string" ? flags["founder"] : void 0
3496
+ },
3497
+ { generatedAt: (/* @__PURE__ */ new Date()).toISOString() }
3498
+ );
3499
+ if (flags["json"]) console.log(JSON.stringify(r2.details, null, 2));
3500
+ else console.log(r2.message);
3501
+ process.exit(r2.ok ? 0 : 1);
3502
+ }
3503
+ if (args[0] === "wiki") {
3504
+ runWiki({
3505
+ path: typeof flags["path"] === "string" ? flags["path"] : void 0,
3506
+ name: typeof flags["name"] === "string" ? flags["name"] : void 0,
3507
+ workspace: typeof flags["workspace"] === "string" ? flags["workspace"] : void 0,
3508
+ dir: typeof flags["dir"] === "string" ? flags["dir"] : void 0,
3509
+ parentPage: typeof flags["parent-page"] === "string" ? flags["parent-page"] : void 0,
3510
+ replace: flags["replace"] === true,
3511
+ dryRun: flags["dry-run"] === true,
3512
+ emitPlan: flags["emit-plan"] === true
3513
+ }).then((r2) => {
3514
+ if (flags["json"]) console.log(JSON.stringify(r2.details, null, 2));
3515
+ else console.log(r2.message);
3516
+ process.exit(r2.ok ? 0 : 1);
3517
+ });
3518
+ return;
3519
+ }
2134
3520
  let payload;
2135
3521
  try {
2136
3522
  payload = buildOp(args, flags);