@hobocode/thought-layer 0.6.1 → 0.8.5

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 readFileSync3 } from "fs";
4
+ import { readFileSync as readFileSync6 } from "fs";
5
5
 
6
6
  // core/scoring.ts
7
7
  var CONFIDENCE_GOAL = 0.85;
@@ -72,6 +72,21 @@ var KIT_WRITTEN_QIDS = [
72
72
  // core/progress.ts
73
73
  var APP = "thought-layer";
74
74
  var PROGRESS_FORMAT = 2;
75
+ var KNOWN_STATE_KEYS = [
76
+ "version",
77
+ "answers",
78
+ "feedback",
79
+ "bizModel",
80
+ "grill",
81
+ "assets",
82
+ "research",
83
+ "swot",
84
+ "prd",
85
+ "naming",
86
+ "brand",
87
+ "governance",
88
+ "kit"
89
+ ];
75
90
  function emptyState() {
76
91
  return {
77
92
  version: 2,
@@ -85,6 +100,7 @@ function emptyState() {
85
100
  prd: null,
86
101
  naming: null,
87
102
  brand: null,
103
+ governance: null,
88
104
  kit: null
89
105
  };
90
106
  }
@@ -101,12 +117,12 @@ function parseProgress(text) {
101
117
  const rawFormat = payload["format"];
102
118
  const formatNewer = typeof rawFormat === "number" && rawFormat > PROGRESS_FORMAT;
103
119
  const s = payload["state"] && typeof payload["state"] === "object" ? payload["state"] : {};
104
- const obj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
120
+ const obj3 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
105
121
  const state = {
106
122
  ...s,
107
123
  version: 2,
108
- answers: obj(s["answers"]),
109
- feedback: obj(s["feedback"]),
124
+ answers: obj3(s["answers"]),
125
+ feedback: obj3(s["feedback"]),
110
126
  bizModel: s["bizModel"] ?? null,
111
127
  grill: s["grill"] ?? null,
112
128
  assets: s["assets"] ?? null,
@@ -115,6 +131,7 @@ function parseProgress(text) {
115
131
  prd: s["prd"] ?? null,
116
132
  naming: s["naming"] ?? null,
117
133
  brand: s["brand"] ?? null,
134
+ governance: s["governance"] ?? null,
118
135
  kit: s["kit"] ?? null
119
136
  };
120
137
  return {
@@ -139,6 +156,7 @@ function buildProgress(state, writer, exportedAt) {
139
156
  prd,
140
157
  naming,
141
158
  brand,
159
+ governance,
142
160
  kit,
143
161
  version: _v,
144
162
  exportedAt: _ea,
@@ -163,6 +181,7 @@ function buildProgress(state, writer, exportedAt) {
163
181
  prd: prd ?? null,
164
182
  naming: naming ?? null,
165
183
  brand: brand ?? null,
184
+ governance: governance ?? null,
166
185
  kit: kit ?? null,
167
186
  ...rest
168
187
  }
@@ -245,7 +264,7 @@ function summarizeState(state) {
245
264
  if (s === "green" || s === "yellow" || s === "red") byStatus[s]++;
246
265
  else byStatus.ungraded++;
247
266
  }
248
- 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);
249
268
  const kit = state.kit && typeof state.kit === "object" ? state.kit : null;
250
269
  return {
251
270
  answered,
@@ -315,11 +334,11 @@ function saveStateFile(state, opts) {
315
334
  }
316
335
 
317
336
  // core/state-ops.ts
318
- var ARTIFACT_KEYS = ["bizModel", "grill", "assets", "research", "swot", "prd", "naming", "brand"];
337
+ var ARTIFACT_KEYS = ["bizModel", "grill", "assets", "research", "swot", "prd", "naming", "brand", "governance"];
319
338
  var END_STATES = ["open", "pass", "setAside"];
320
339
  function applyStateOp(p, ctx) {
321
340
  const { ts, exportedAt } = ctx;
322
- const fail = (message) => ({ ok: false, message, details: {} });
341
+ const fail4 = (message) => ({ ok: false, message, details: {} });
323
342
  try {
324
343
  if (p.op === "list") {
325
344
  const files = listStateFiles(p.path);
@@ -328,8 +347,8 @@ function applyStateOp(p, ctx) {
328
347
  }
329
348
  const rows = files.map((f) => {
330
349
  try {
331
- const sum = summarizeState(loadStateFile(f.path).state);
332
- 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 };
333
352
  } catch {
334
353
  return { name: f.name, path: f.path, answered: 0, artifacts: [], unreadable: true };
335
354
  }
@@ -342,24 +361,24 @@ Pick one with --path .thought-layer/<name>.json, or set ${STATE_ENV} for the ses
342
361
  const loaded = loadStateFile(p.path);
343
362
  const save = (next) => saveStateFile(next, { target: p.path, ts, exportedAt }).path;
344
363
  if (p.op === "read" || p.op === "export") {
345
- const sum = summarizeState(loaded.state);
364
+ const sum2 = summarizeState(loaded.state);
346
365
  let message;
347
366
  if (loaded.exists) {
348
- 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"}.`;
349
368
  } else {
350
369
  const others = listStateFiles().filter((f) => f.path !== loaded.path);
351
370
  const hint = others.length ? ` Other state files here: ${others.map((f) => f.name).join(", ")} (pick one with --path, or 'tl list').` : "";
352
371
  message = `No state file yet at ${loaded.path}.${hint} Start a fresh run; it will be created on first write.`;
353
372
  }
354
- 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 } };
355
374
  }
356
375
  if (p.op === "answer") {
357
- if (!p.qId || typeof p.value !== "string") return fail("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.");
358
377
  const path = save(setAnswer(loaded.state, p.qId, p.value, ts));
359
378
  return { ok: true, message: `Recorded answer for "${p.qId}" and saved ${path}.`, details: { path, qId: p.qId } };
360
379
  }
361
380
  if (p.op === "feedback") {
362
- if (!p.qId || !p.personas?.length) return fail("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.");
363
382
  const endState = END_STATES.includes(p.endState || "") ? p.endState : "open";
364
383
  const mode = p.mode || (p.personas.length > 1 ? "panel" : p.personas[0].persona);
365
384
  const entry = buildFeedbackEntry({ mode, personas: p.personas, endState, round: p.round, ts });
@@ -376,24 +395,24 @@ Pick one with --path .thought-layer/<name>.json, or set ${STATE_ENV} for the ses
376
395
  }
377
396
  if (p.op === "artifact") {
378
397
  const key = p.artifact;
379
- if (!ARTIFACT_KEYS.includes(key)) return fail(`artifact needs a key in: ${ARTIFACT_KEYS.join(", ")}.`);
380
- if (p.value == null) return fail("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.");
381
400
  const path = save(setArtifact(loaded.state, key, normalizeArtifactValue(key, p.value), ts));
382
401
  return { ok: true, message: `Stored ${key} artifact and saved ${path}.`, details: { path, artifact: key } };
383
402
  }
384
403
  if (p.op === "cursor") {
385
- if (!p.cursor) return fail("cursor needs a cursor object.");
404
+ if (!p.cursor) return fail4("cursor needs a cursor object.");
386
405
  const path = save(setCursor(loaded.state, p.cursor, ts));
387
406
  return { ok: true, message: `Saved resume cursor (stage ${p.cursor.backboneStage ?? "?"}, ${p.cursor.phase ?? "?"}) to ${path}.`, details: { path } };
388
407
  }
389
408
  if (p.op === "park") {
390
- if (!p.key || !p.note) return fail("park needs a key and a note.");
409
+ if (!p.key || !p.note) return fail4("park needs a key and a note.");
391
410
  const path = save(parkNote(loaded.state, p.key, p.note, ts));
392
411
  return { ok: true, message: `Parked a note under "${p.key}" and saved ${path}.`, details: { path, key: p.key } };
393
412
  }
394
- return fail(`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.`);
395
414
  } catch (e) {
396
- return fail(`tl_state error: ${e.message}`);
415
+ return fail4(`tl_state error: ${e.message}`);
397
416
  }
398
417
  }
399
418
 
@@ -405,26 +424,26 @@ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, resolve
405
424
  var esc = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
406
425
  var fam = (f) => f.trim().replace(/\s+/g, "+");
407
426
  function extractScaffoldSpec(state) {
408
- const obj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
409
- const str = (v) => typeof v === "string" ? v : "";
410
- const brand = obj(state.brand);
411
- 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"]);
412
431
  const answers = state.answers || {};
413
432
  const palette = Array.isArray(guide["palette"]) ? guide["palette"] : [];
414
433
  const role = (re, fb) => {
415
434
  const m = palette.find((p) => p?.name && new RegExp(re, "i").test(p.name));
416
435
  return m?.hex || fb;
417
436
  };
418
- const typography = obj(guide["typography"]);
419
- const fontOf = (slot, fb) => str(obj(slot)["family"]) || fb;
420
- 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"]);
421
440
  const logos = Array.isArray(brand["logos"]) ? brand["logos"] : [];
422
441
  const chosen = logos.find((l) => l?.id === brand["chosenLogoId"]) || logos[0];
423
442
  return {
424
- brandName: str(guide["brandName"]) || "Your Product",
425
- tagline: str(guide["tagline"]),
426
- pitch: str(answers["pitch"]) || str(answers["what-statement"]),
427
- positioning: str(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"]),
428
447
  personality: Array.isArray(guide["personality"]) ? guide["personality"].filter((x) => typeof x === "string") : [],
429
448
  palette: {
430
449
  primary: role("primary", palette[0]?.hex || "#1f3a5f"),
@@ -435,9 +454,9 @@ function extractScaffoldSpec(state) {
435
454
  },
436
455
  displayFont: fontOf(typography["display"], "Inter"),
437
456
  bodyFont: fontOf(typography["body"], "Inter"),
438
- voiceTone: str(voice["tone"]),
457
+ voiceTone: str4(voice["tone"]),
439
458
  logoSvg: chosen?.svg || void 0,
440
- pricing: str(answers["pricing-model"]) || void 0
459
+ pricing: str4(answers["pricing-model"]) || void 0
441
460
  };
442
461
  }
443
462
  function indexHtml(spec, opts) {
@@ -734,11 +753,11 @@ function dedupeSortEnv(envVars) {
734
753
  function normalizeDatabase(raw) {
735
754
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
736
755
  const r = raw;
737
- const str = (v, fb) => typeof v === "string" && v.trim() ? v.trim() : fb;
756
+ const str4 = (v, fb) => typeof v === "string" && v.trim() ? v.trim() : fb;
738
757
  return {
739
- provider: str(r["provider"], "neon"),
740
- schemaFile: str(r["schemaFile"], "schema.sql"),
741
- envVar: str(r["envVar"], "DATABASE_URL")
758
+ provider: str4(r["provider"], "neon"),
759
+ schemaFile: str4(r["schemaFile"], "schema.sql"),
760
+ envVar: str4(r["envVar"], "DATABASE_URL")
742
761
  };
743
762
  }
744
763
  function normalizeEnvVars(raw) {
@@ -769,15 +788,15 @@ function normalizeBackendMeta(raw) {
769
788
  const database = normalizeDatabase(r["database"]);
770
789
  const hasFunctionsDir = typeof r["functionsDir"] === "string" && r["functionsDir"].trim().length > 0;
771
790
  if (kind === null && envVars.length === 0 && database === null && !hasFunctionsDir) return null;
772
- const str = (v, fb) => typeof v === "string" && v.trim() ? v.trim() : fb;
791
+ const str4 = (v, fb) => typeof v === "string" && v.trim() ? v.trim() : fb;
773
792
  return {
774
793
  backendKind: kind ?? "serverless",
775
- functionsDir: str(r["functionsDir"], "netlify/functions"),
776
- runtime: str(r["runtime"], "nodejs20.x"),
777
- nodeVersion: str(r["nodeVersion"], "20"),
794
+ functionsDir: str4(r["functionsDir"], "netlify/functions"),
795
+ runtime: str4(r["runtime"], "nodejs20.x"),
796
+ nodeVersion: str4(r["nodeVersion"], "20"),
778
797
  envVars,
779
798
  database,
780
- guide: str(r["guide"], "BACKEND.md")
799
+ guide: str4(r["guide"], "BACKEND.md")
781
800
  };
782
801
  }
783
802
  var SECRET_NAME_RE = /(KEY|SECRET|TOKEN|PASSWORD|PASSWD|DATABASE_URL|DB_URL|CONN|DSN|CREDENTIAL|PRIVATE|AUTH)/;
@@ -1172,10 +1191,10 @@ Static deploy: the front end is live. This build also declares a backend that th
1172
1191
  return `${lead} To run the backend, follow ${guide}: provision Neon Postgres, set ${envList} in your host environment, then run netlify deploy with the functions present.`;
1173
1192
  })() : "";
1174
1193
  const token = process.env.NETLIFY_AUTH_TOKEN || process.env.NETLIFY_TOKEN || "";
1175
- const writeRecord = (rec) => {
1194
+ const writeRecord = (rec2) => {
1176
1195
  const recPath = join4(dirname3(stateFile), "deploy.json");
1177
1196
  mkdirSync3(dirname3(recPath), { recursive: true });
1178
- writeFileSync4(recPath, JSON.stringify(rec, null, 2) + "\n");
1197
+ writeFileSync4(recPath, JSON.stringify(rec2, null, 2) + "\n");
1179
1198
  return recPath;
1180
1199
  };
1181
1200
  if (opts.dryRun) {
@@ -1447,6 +1466,1747 @@ ${notes.join("\n")}` : ""}` + (!url ? `
1447
1466
  };
1448
1467
  }
1449
1468
 
1469
+ // core/sync-io.ts
1470
+ import { spawnSync as spawnSync3 } from "child_process";
1471
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, readdirSync as readdirSync3, writeFileSync as writeFileSync5 } from "fs";
1472
+ import { homedir } from "os";
1473
+ import { dirname as dirname4, isAbsolute as isAbsolute3, join as join5, resolve as resolve4 } from "path";
1474
+
1475
+ // core/sync.ts
1476
+ function slugify(name) {
1477
+ return String(name || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/g, "");
1478
+ }
1479
+ function emptySyncConfig() {
1480
+ return { schema: 1, workspaces: [] };
1481
+ }
1482
+ var str = (v, fb = "") => typeof v === "string" ? v : fb;
1483
+ function parseSyncConfig(text) {
1484
+ let raw;
1485
+ try {
1486
+ raw = JSON.parse(text);
1487
+ } catch {
1488
+ return emptySyncConfig();
1489
+ }
1490
+ const list = Array.isArray(raw["workspaces"]) ? raw["workspaces"] : [];
1491
+ const workspaces = [];
1492
+ for (const w of list) {
1493
+ if (!w || typeof w !== "object") continue;
1494
+ const r = w;
1495
+ const cloneDir = str(r["cloneDir"]).trim();
1496
+ if (!cloneDir) continue;
1497
+ const ws = {
1498
+ name: str(r["name"]).trim() || cloneDir,
1499
+ repo: str(r["repo"]).trim(),
1500
+ defaultBranch: str(r["defaultBranch"]).trim() || "main",
1501
+ cloneDir
1502
+ };
1503
+ const active = str(r["activeSession"]).trim();
1504
+ if (active) ws.activeSession = active;
1505
+ workspaces.push(ws);
1506
+ }
1507
+ const cfg = { schema: 1, workspaces };
1508
+ const aw = str(raw["activeWorkspace"]).trim();
1509
+ if (aw) cfg.activeWorkspace = aw;
1510
+ return cfg;
1511
+ }
1512
+ function serializeSyncConfig(cfg) {
1513
+ return JSON.stringify({ schema: 1, activeWorkspace: cfg.activeWorkspace, workspaces: cfg.workspaces }, null, 2) + "\n";
1514
+ }
1515
+ function selectWorkspace(cfg, name) {
1516
+ if (name) return cfg.workspaces.find((w) => w.name === name) || null;
1517
+ if (cfg.activeWorkspace) {
1518
+ const w = cfg.workspaces.find((ws) => ws.name === cfg.activeWorkspace);
1519
+ if (w) return w;
1520
+ }
1521
+ return cfg.workspaces.length === 1 ? cfg.workspaces[0] : null;
1522
+ }
1523
+ function defaultSessionsDir(home) {
1524
+ return `${home}/.thought-layer/sessions`;
1525
+ }
1526
+ function parseGitStatus(out) {
1527
+ const lines = String(out || "").split("\n").filter((l) => l.length > 0);
1528
+ let branch = null;
1529
+ let ahead = 0;
1530
+ let behind = 0;
1531
+ const files = [];
1532
+ for (const line of lines) {
1533
+ if (line.startsWith("## ")) {
1534
+ const header = line.slice(3);
1535
+ branch = header.split(/\.\.\.| /)[0] || null;
1536
+ const a = header.match(/ahead (\d+)/);
1537
+ const b = header.match(/behind (\d+)/);
1538
+ if (a) ahead = Number(a[1]);
1539
+ if (b) behind = Number(b[1]);
1540
+ } else {
1541
+ files.push(line.slice(3).trim());
1542
+ }
1543
+ }
1544
+ return { branch, ahead, behind, dirty: files.length > 0, files };
1545
+ }
1546
+
1547
+ // core/merge.ts
1548
+ var num = (v) => typeof v === "number" && !Number.isNaN(v) ? v : 0;
1549
+ var genAt = (v) => v && typeof v === "object" ? num(v["generatedAt"]) : 0;
1550
+ var jsonEq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
1551
+ var rec = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
1552
+ function mergeProgressStates(ours, theirs, opts) {
1553
+ const coarse = [];
1554
+ const oursNewer = opts.oursTs > opts.theirsTs;
1555
+ const answers = {};
1556
+ const oa = rec(ours.answers);
1557
+ const ta = rec(theirs.answers);
1558
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(oa), ...Object.keys(ta)])) {
1559
+ const inO = k in oa;
1560
+ const inT = k in ta;
1561
+ if (inO && !inT) answers[k] = oa[k];
1562
+ else if (!inO && inT) answers[k] = ta[k];
1563
+ else if (jsonEq(oa[k], ta[k])) answers[k] = oa[k];
1564
+ else {
1565
+ answers[k] = oursNewer ? oa[k] : ta[k];
1566
+ coarse.push(`answers.${k}`);
1567
+ }
1568
+ }
1569
+ const feedback = {};
1570
+ const of = rec(ours.feedback);
1571
+ const tf = rec(theirs.feedback);
1572
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(of), ...Object.keys(tf)])) {
1573
+ const o = of[k];
1574
+ const t = tf[k];
1575
+ if (o != null && t == null) feedback[k] = o;
1576
+ else if (o == null && t != null) feedback[k] = t;
1577
+ else if (jsonEq(o, t)) feedback[k] = o;
1578
+ else {
1579
+ const oR = num(rec(o)["round"]);
1580
+ const tR = num(rec(t)["round"]);
1581
+ const oTs = num(rec(o)["ts"]);
1582
+ const tTs = num(rec(t)["ts"]);
1583
+ const pickOurs = oR !== tR ? oR > tR : oTs !== tTs ? oTs > tTs : oursNewer;
1584
+ feedback[k] = pickOurs ? o : t;
1585
+ coarse.push(`feedback.${k}`);
1586
+ }
1587
+ }
1588
+ const artifact = (key) => {
1589
+ const o = ours[key];
1590
+ const t = theirs[key];
1591
+ if (o == null && t == null) return null;
1592
+ if (o != null && t == null) return o;
1593
+ if (o == null && t != null) return t;
1594
+ if (jsonEq(o, t)) return o;
1595
+ const og = genAt(o);
1596
+ const tg = genAt(t);
1597
+ const pickOurs = og !== tg ? og > tg : oursNewer;
1598
+ coarse.push(key);
1599
+ return pickOurs ? o : t;
1600
+ };
1601
+ const merged = {
1602
+ version: 2,
1603
+ answers,
1604
+ feedback,
1605
+ bizModel: artifact("bizModel"),
1606
+ grill: artifact("grill"),
1607
+ assets: artifact("assets"),
1608
+ research: artifact("research"),
1609
+ swot: artifact("swot"),
1610
+ prd: artifact("prd"),
1611
+ naming: artifact("naming"),
1612
+ brand: artifact("brand"),
1613
+ governance: artifact("governance"),
1614
+ kit: mergeKit(ours.kit, theirs.kit, oursNewer)
1615
+ };
1616
+ for (const src of oursNewer ? [theirs, ours] : [ours, theirs]) {
1617
+ for (const k of Object.keys(src)) {
1618
+ if (!KNOWN_STATE_KEYS.includes(k)) merged[k] = src[k];
1619
+ }
1620
+ }
1621
+ return { state: merged, coarse: Array.from(new Set(coarse)).sort() };
1622
+ }
1623
+ function mergeKit(o, t, oursNewer) {
1624
+ if (!o && !t) return null;
1625
+ if (o && !t) return o;
1626
+ if (!o && t) return t;
1627
+ const oo = o;
1628
+ const tt = t;
1629
+ const modulesRun = Array.from(/* @__PURE__ */ new Set([...oo.modulesRun || [], ...tt.modulesRun || []]));
1630
+ const parked = {};
1631
+ for (const src of [oo.parked || {}, tt.parked || {}]) {
1632
+ for (const [k, v] of Object.entries(src)) {
1633
+ parked[k] = Array.from(/* @__PURE__ */ new Set([...parked[k] || [], ...Array.isArray(v) ? v : []]));
1634
+ }
1635
+ }
1636
+ const newer = oursNewer ? oo : tt;
1637
+ const older = oursNewer ? tt : oo;
1638
+ const panelMeta = { ...older.panelMeta || {}, ...newer.panelMeta || {} };
1639
+ const kit = {
1640
+ schema: Math.max(num(oo.schema) || 1, num(tt.schema) || 1),
1641
+ updatedAt: Math.max(num(oo.updatedAt), num(tt.updatedAt))
1642
+ };
1643
+ if (modulesRun.length) kit.modulesRun = modulesRun;
1644
+ if (Object.keys(parked).length) kit.parked = parked;
1645
+ const cursor = newer.cursor ?? older.cursor;
1646
+ if (cursor) kit.cursor = cursor;
1647
+ if (Object.keys(panelMeta).length) kit.panelMeta = panelMeta;
1648
+ return kit;
1649
+ }
1650
+
1651
+ // core/sync-io.ts
1652
+ function hasGit() {
1653
+ try {
1654
+ return spawnSync3("git", ["--version"], { encoding: "utf8", timeout: 15e3 }).status === 0;
1655
+ } catch {
1656
+ return false;
1657
+ }
1658
+ }
1659
+ function hasGh() {
1660
+ try {
1661
+ return spawnSync3("gh", ["--version"], { encoding: "utf8", timeout: 15e3 }).status === 0;
1662
+ } catch {
1663
+ return false;
1664
+ }
1665
+ }
1666
+ function ghAuthed() {
1667
+ try {
1668
+ return spawnSync3("gh", ["auth", "status"], { encoding: "utf8", timeout: 2e4 }).status === 0;
1669
+ } catch {
1670
+ return false;
1671
+ }
1672
+ }
1673
+ function syncConfigPath() {
1674
+ return process.env["THOUGHT_LAYER_SYNC_CONFIG"] || join5(homedir(), ".thought-layer", "sync.json");
1675
+ }
1676
+ function loadConfig() {
1677
+ const p = syncConfigPath();
1678
+ if (!existsSync4(p)) return emptySyncConfig();
1679
+ try {
1680
+ return parseSyncConfig(readFileSync3(p, "utf8"));
1681
+ } catch {
1682
+ return emptySyncConfig();
1683
+ }
1684
+ }
1685
+ function saveConfig(cfg) {
1686
+ const p = syncConfigPath();
1687
+ mkdirSync4(dirname4(p), { recursive: true });
1688
+ writeFileSync5(p, serializeSyncConfig(cfg));
1689
+ }
1690
+ function git(dir, args, timeout = 12e4) {
1691
+ const r = spawnSync3("git", dir ? ["-C", dir, ...args] : args, { encoding: "utf8", timeout });
1692
+ return { status: r.status ?? 1, out: r.stdout || "", err: r.stderr || "" };
1693
+ }
1694
+ function isGitRepo(dir) {
1695
+ return existsSync4(join5(dir, ".git")) && git(dir, ["rev-parse", "--is-inside-work-tree"]).status === 0;
1696
+ }
1697
+ function dirNonEmpty(dir) {
1698
+ try {
1699
+ return existsSync4(dir) && readdirSync3(dir).length > 0;
1700
+ } catch {
1701
+ return false;
1702
+ }
1703
+ }
1704
+ var absDir = (d, cwd = process.cwd()) => isAbsolute3(d) ? d : resolve4(cwd, d);
1705
+ var GITATTRIBUTES = `# The kit reconciles session JSON itself; never let git textually merge it
1706
+ # (which would corrupt the envelope). -merge keeps our copy in the working tree
1707
+ # on a conflict; the kit then rebuilds the merged result from the clean blobs.
1708
+ .thought-layer/*.json -merge
1709
+ `;
1710
+ var GITIGNORE = `# A Thought Layer sessions repo holds session state only. Built product
1711
+ # artifacts and secrets never sync here.
1712
+ build.json
1713
+ deploy.json
1714
+ *.local
1715
+ .env
1716
+ .env.*
1717
+ !.env.example
1718
+ dist/
1719
+ .netlify/
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/**
1725
+ `;
1726
+ var README = `# Thought Layer sessions
1727
+
1728
+ This private repo is the home for Thought Layer session files. Each session is one
1729
+ file under \`.thought-layer/<name>.json\` (the portable validation and design state).
1730
+ Use the kit to work with them:
1731
+
1732
+ tl sync open --name <session> pull and resume a session
1733
+ tl sync save --name <session> snapshot the current state, commit, and push
1734
+ tl sync list list the sessions in this repo
1735
+
1736
+ Collaboration is handled by GitHub: add a collaborator to this repo in its GitHub
1737
+ settings, and they can clone it and run the kit against the same sessions.
1738
+ The kit reconciles concurrent edits itself (newest wins per field, conflicts are
1739
+ reported), so git never has to merge the JSON by hand.
1740
+ `;
1741
+ function writeCloneScaffold(cloneDir) {
1742
+ mkdirSync4(join5(cloneDir, STATE_DIR), { recursive: true });
1743
+ const put = (name, body) => {
1744
+ const p = join5(cloneDir, name);
1745
+ if (!existsSync4(p)) writeFileSync5(p, body);
1746
+ };
1747
+ put(".gitattributes", GITATTRIBUTES);
1748
+ put(".gitignore", GITIGNORE);
1749
+ put("README.md", README);
1750
+ }
1751
+ var ok = (message, details = {}) => ({ ok: true, message, details });
1752
+ var fail = (message, details = {}) => ({ ok: false, message, details });
1753
+ var collaboratorPointer = (repo) => `To collaborate, add people to ${repo} in its GitHub settings (Settings, Collaborators). They clone it and run the kit; the kit never changes GitHub permissions.`;
1754
+ async function runSync(opts, ctx) {
1755
+ if (!hasGit()) {
1756
+ return fail("git is not installed. Install git, then re-run. The sync feature stores your session files in your own private GitHub repo.", { needs: "git" });
1757
+ }
1758
+ try {
1759
+ switch (opts.op) {
1760
+ case "init":
1761
+ return syncInit(opts);
1762
+ case "save":
1763
+ return syncSave(opts, ctx);
1764
+ case "list":
1765
+ return syncList(opts);
1766
+ case "open":
1767
+ return syncOpen(opts, ctx);
1768
+ case "pull":
1769
+ return syncPull(opts, ctx);
1770
+ case "push":
1771
+ return syncPush(opts);
1772
+ case "status":
1773
+ return syncStatus(opts);
1774
+ default:
1775
+ return fail(`Unknown sync op "${opts.op}". Use one of: init, save, list, open, pull, push, status.`);
1776
+ }
1777
+ } catch (e) {
1778
+ return fail(`Sync ${opts.op} failed: ${e.message}`);
1779
+ }
1780
+ }
1781
+ function resolveWorkspace(opts, cfg) {
1782
+ if (opts.dir && opts.dir.trim()) return { cloneDir: absDir(opts.dir), ws: null };
1783
+ const env = process.env["THOUGHT_LAYER_SESSIONS_DIR"];
1784
+ if (env && env.trim()) return { cloneDir: absDir(env), ws: null };
1785
+ const ws = selectWorkspace(cfg, opts.workspace);
1786
+ if (ws) return { cloneDir: ws.cloneDir, ws };
1787
+ return { cloneDir: defaultSessionsDir(homedir()), ws: null };
1788
+ }
1789
+ function syncInit(opts) {
1790
+ const repo = (opts.repo || "").trim();
1791
+ if (!repo) return fail("Pass the private repo to use: tl sync init --repo <owner/name or url> [--name <label>] [--dir <path>].");
1792
+ const home = homedir();
1793
+ const label = (opts.name || "").trim();
1794
+ const cloneDir = opts.dir ? absDir(opts.dir) : !label || slugify(label) === "personal" ? defaultSessionsDir(home) : join5(home, ".thought-layer", `sessions-${slugify(label)}`);
1795
+ if (dirNonEmpty(cloneDir)) {
1796
+ if (isGitRepo(cloneDir)) return fail(`${cloneDir} is already a git repo. It looks initialized; use tl sync list or pick another --dir.`, { cloneDir });
1797
+ return fail(`${cloneDir} already exists and is not empty. Pick another --dir or remove it first.`, { cloneDir });
1798
+ }
1799
+ const isOwnerName = /^[\w.-]+\/[\w.-]+$/.test(repo);
1800
+ const useGh = isOwnerName && hasGh() && ghAuthed();
1801
+ mkdirSync4(dirname4(cloneDir), { recursive: true });
1802
+ let cloned = false;
1803
+ if (useGh) {
1804
+ cloned = spawnSync3("gh", ["repo", "clone", repo, cloneDir], { encoding: "utf8", timeout: 18e4 }).status === 0;
1805
+ if (!cloned) {
1806
+ const created = spawnSync3("gh", ["repo", "create", repo, "--private"], { encoding: "utf8", timeout: 18e4 });
1807
+ if (created.status !== 0) {
1808
+ return fail(`Could not clone or create ${repo} with gh. Create the private repo on GitHub yourself, then re-run. gh said: ${(created.stderr || "").slice(0, 300)}`);
1809
+ }
1810
+ cloned = spawnSync3("gh", ["repo", "clone", repo, cloneDir], { encoding: "utf8", timeout: 18e4 }).status === 0;
1811
+ }
1812
+ } else {
1813
+ const url = isOwnerName ? `https://github.com/${repo}.git` : repo;
1814
+ cloned = git(null, ["clone", url, cloneDir], 18e4).status === 0;
1815
+ if (!cloned) {
1816
+ return fail(
1817
+ isOwnerName && !hasGh() ? `Could not clone https://github.com/${repo}.git. Create the private repo on GitHub first (or install gh and run gh auth login so the kit can create it), then re-run.` : `Could not clone ${repo}. Check the repo path or URL and your git access, then re-run.`,
1818
+ { repo, cloneDir }
1819
+ );
1820
+ }
1821
+ }
1822
+ writeCloneScaffold(cloneDir);
1823
+ git(cloneDir, ["add", "-A"]);
1824
+ const committed = git(cloneDir, ["commit", "-m", "Initialize Thought Layer sessions"]).status === 0;
1825
+ let branch = git(cloneDir, ["rev-parse", "--abbrev-ref", "HEAD"]).out.trim() || "main";
1826
+ if (committed && branch === "HEAD") {
1827
+ git(cloneDir, ["branch", "-M", "main"]);
1828
+ branch = "main";
1829
+ }
1830
+ let pushed = false;
1831
+ if (committed) pushed = git(cloneDir, ["push", "-u", "origin", branch]).status === 0;
1832
+ const cfg = loadConfig();
1833
+ const wsName = label || "personal";
1834
+ const ws = { name: wsName, repo, defaultBranch: branch, cloneDir };
1835
+ cfg.workspaces = [...cfg.workspaces.filter((w) => w.cloneDir !== cloneDir && w.name !== wsName), ws];
1836
+ cfg.activeWorkspace = wsName;
1837
+ saveConfig(cfg);
1838
+ return ok(
1839
+ `Initialized the "${wsName}" sessions workspace at ${cloneDir} (repo ${repo}).${committed ? pushed ? " Pushed the initial commit." : " Committed locally; push it once your git access is set." : " The repo already had content; left it as is."}
1840
+ ${collaboratorPointer(repo)}
1841
+ Save your first session with: tl sync save --name <name>${label ? ` --workspace ${wsName}` : ""}.`,
1842
+ { cloneDir, repo, workspace: wsName, branch, committed, pushed }
1843
+ );
1844
+ }
1845
+ function syncSave(opts, ctx) {
1846
+ const cfg = loadConfig();
1847
+ const { cloneDir, ws } = resolveWorkspace(opts, cfg);
1848
+ if (!isGitRepo(cloneDir)) return fail(`No sessions workspace at ${cloneDir}. Run tl sync init --repo <owner/name> first.`, { cloneDir });
1849
+ const slug = slugify(opts.name || ws?.activeSession?.replace(/\.json$/, "") || "");
1850
+ if (!slug) return fail("Name the session: tl sync save --name <name> (for example photobooth, peptide, blogging).");
1851
+ const targetPath = join5(cloneDir, STATE_DIR, `${slug}.json`);
1852
+ const existed = existsSync4(targetPath);
1853
+ const useExplicit = !!(opts.path && opts.path.trim() || (process.env["THOUGHT_LAYER_STATE"] || "").trim());
1854
+ const loadTarget = useExplicit ? opts.path : existed ? targetPath : opts.path;
1855
+ const source = loadStateFile(loadTarget).state;
1856
+ saveStateFile(source, { target: targetPath, ts: ctx.ts, exportedAt: ctx.exportedAt });
1857
+ git(cloneDir, ["add", "-A"]);
1858
+ const msg = opts.message || `${existed ? "Update" : "Save"} session ${slug}`;
1859
+ const commit = git(cloneDir, ["commit", "-m", msg]);
1860
+ const committed = commit.status === 0;
1861
+ let pushed = false;
1862
+ let pushNote = "";
1863
+ if (committed && !opts.noPush) {
1864
+ const p = git(cloneDir, ["push"]);
1865
+ pushed = p.status === 0;
1866
+ if (!pushed) pushNote = ` Could not push (${(p.err || "").split("\n")[0] || "see git output"}); commit is local, run tl sync push when ready.`;
1867
+ }
1868
+ if (ws) {
1869
+ ws.activeSession = `${slug}.json`;
1870
+ saveConfig(cfg);
1871
+ }
1872
+ return ok(
1873
+ `${existed ? "Updated" : "Saved"} session ${slug} in ${cloneDir}.${committed ? opts.noPush ? " Committed locally (no push)." : pushed ? " Committed and pushed." : pushNote : " Nothing changed since the last save."}`,
1874
+ { cloneDir, session: `${slug}.json`, path: targetPath, committed, pushed }
1875
+ );
1876
+ }
1877
+ function syncList(opts) {
1878
+ const cfg = loadConfig();
1879
+ const { cloneDir } = resolveWorkspace(opts, cfg);
1880
+ if (!isGitRepo(cloneDir)) return fail(`No sessions workspace at ${cloneDir}. Run tl sync init --repo <owner/name> first.`, { cloneDir });
1881
+ const files = listStateFiles(cloneDir);
1882
+ if (!files.length) return ok(`No sessions yet in ${cloneDir}. Create one with tl sync save --name <name>.`, { cloneDir, sessions: [] });
1883
+ const rows = files.map((f) => {
1884
+ try {
1885
+ const sum2 = summarizeState(loadStateFile(f.path).state);
1886
+ return { name: f.name, path: f.path, answered: sum2.answered, artifacts: sum2.artifacts };
1887
+ } catch {
1888
+ return { name: f.name, path: f.path, answered: 0, artifacts: [], unreadable: true };
1889
+ }
1890
+ });
1891
+ const lines = rows.map((r) => ` ${r.name}: ${r.answered} answered${r.artifacts.length ? `, artifacts ${r.artifacts.join(", ")}` : ""}${"unreadable" in r ? " (unreadable)" : ""}`).join("\n");
1892
+ return ok(`${files.length} session(s) in ${cloneDir}:
1893
+ ${lines}
1894
+ Open one with tl sync open --name <name>.`, { cloneDir, sessions: rows });
1895
+ }
1896
+ function syncOpen(opts, ctx) {
1897
+ const cfg = loadConfig();
1898
+ const { cloneDir, ws } = resolveWorkspace(opts, cfg);
1899
+ if (!isGitRepo(cloneDir)) return fail(`No sessions workspace at ${cloneDir}. Run tl sync init --repo <owner/name> first.`, { cloneDir });
1900
+ const slug = slugify(opts.name || "");
1901
+ if (!slug) return fail("Name the session to open: tl sync open --name <name>. List them with tl sync list.");
1902
+ const pullResult = pullAndReconcile(cloneDir, ctx);
1903
+ const targetPath = join5(cloneDir, STATE_DIR, `${slug}.json`);
1904
+ if (!existsSync4(targetPath)) {
1905
+ return fail(`No session "${slug}" in ${cloneDir} after pulling. List them with tl sync list, or create it with tl sync save --name ${slug}.`, { cloneDir });
1906
+ }
1907
+ if (ws) {
1908
+ ws.activeSession = `${slug}.json`;
1909
+ saveConfig(cfg);
1910
+ }
1911
+ return ok(
1912
+ `Opened session ${slug} (pulled latest${pullResult.merged ? `, reconciled local and remote edits${pullResult.coarse.length ? ` (review: ${pullResult.coarse.join(", ")})` : ""}` : ""}).
1913
+ Work on it by pointing the kit at this file:
1914
+ export THOUGHT_LAYER_STATE="${targetPath}"
1915
+ or pass --path "${targetPath}" to tl read/answer/artifact. Save with tl sync save --name ${slug}.`,
1916
+ { cloneDir, session: `${slug}.json`, path: targetPath, merged: pullResult.merged, coarse: pullResult.coarse }
1917
+ );
1918
+ }
1919
+ function pullAndReconcile(cloneDir, ctx) {
1920
+ const branch = git(cloneDir, ["rev-parse", "--abbrev-ref", "HEAD"]).out.trim() || "main";
1921
+ if (git(cloneDir, ["fetch", "origin", branch], 12e4).status !== 0) {
1922
+ return { merged: false, coarse: [], note: "no remote branch to fetch yet" };
1923
+ }
1924
+ const local = git(cloneDir, ["rev-parse", "HEAD"]).out.trim();
1925
+ const remote = git(cloneDir, ["rev-parse", `origin/${branch}`]).out.trim();
1926
+ if (!remote || local === remote) return { merged: false, coarse: [], note: "up to date" };
1927
+ const base = git(cloneDir, ["merge-base", local, remote]).out.trim();
1928
+ if (base === remote) return { merged: false, coarse: [], note: "local is ahead; nothing to pull" };
1929
+ if (base === local) {
1930
+ git(cloneDir, ["merge", "--ff-only", `origin/${branch}`], 12e4);
1931
+ return { merged: false, coarse: [], note: "fast-forward" };
1932
+ }
1933
+ const changed = git(cloneDir, ["diff", "--name-only", local, remote]).out.split("\n").map((s) => s.trim()).filter(Boolean);
1934
+ git(cloneDir, ["merge", "--no-ff", "--no-commit", `origin/${branch}`], 12e4);
1935
+ const coarseAll = [];
1936
+ let reconciled = 0;
1937
+ for (const rel of changed) {
1938
+ if (rel.startsWith(`${STATE_DIR}/`) && rel.endsWith(".json")) {
1939
+ const ours = readShow(cloneDir, local, rel);
1940
+ const theirs = readShow(cloneDir, remote, rel);
1941
+ if (ours && theirs) {
1942
+ try {
1943
+ const op = parseProgress(ours);
1944
+ const tp = parseProgress(theirs);
1945
+ const { state, coarse } = mergeProgressStates(op.state, tp.state, { oursTs: op.writer?.ts ?? 0, theirsTs: tp.writer?.ts ?? 0 });
1946
+ writeFileSync5(join5(cloneDir, rel), serializeProgress(buildProgress(state, { kind: "kit", ts: ctx.ts }, ctx.exportedAt)));
1947
+ coarseAll.push(...coarse);
1948
+ reconciled++;
1949
+ } catch {
1950
+ if (theirs) writeFileSync5(join5(cloneDir, rel), theirs);
1951
+ }
1952
+ } else if (theirs && !ours) {
1953
+ writeFileSync5(join5(cloneDir, rel), theirs);
1954
+ reconciled++;
1955
+ }
1956
+ } else {
1957
+ const theirs = readShow(cloneDir, remote, rel);
1958
+ if (theirs !== null) writeFileSync5(join5(cloneDir, rel), theirs);
1959
+ }
1960
+ git(cloneDir, ["add", "--", rel]);
1961
+ }
1962
+ git(cloneDir, ["add", "-A"]);
1963
+ git(cloneDir, ["commit", "--no-edit", "-m", "Reconcile sessions (kit merge)"]);
1964
+ return { merged: reconciled > 0, coarse: Array.from(new Set(coarseAll)).sort(), note: `reconciled ${reconciled} session file(s)` };
1965
+ }
1966
+ function readShow(cloneDir, ref, rel) {
1967
+ const r = git(cloneDir, ["show", `${ref}:${rel}`]);
1968
+ return r.status === 0 ? r.out : null;
1969
+ }
1970
+ function syncPull(opts, ctx) {
1971
+ const cfg = loadConfig();
1972
+ const { cloneDir } = resolveWorkspace(opts, cfg);
1973
+ if (!isGitRepo(cloneDir)) return fail(`No sessions workspace at ${cloneDir}. Run tl sync init first.`, { cloneDir });
1974
+ const r = pullAndReconcile(cloneDir, ctx);
1975
+ const push = git(cloneDir, ["push"]);
1976
+ return ok(
1977
+ `Pulled ${cloneDir}.${r.merged ? ` Reconciled local and remote edits${r.coarse.length ? ` (a coarse tie-break dropped one side for: ${r.coarse.join(", ")})` : ""}.` : " Already up to date or fast-forwarded."}${r.merged && push.status === 0 ? " Pushed the reconciliation." : ""}`,
1978
+ { cloneDir, merged: r.merged, coarse: r.coarse }
1979
+ );
1980
+ }
1981
+ function syncPush(opts) {
1982
+ const cfg = loadConfig();
1983
+ const { cloneDir } = resolveWorkspace(opts, cfg);
1984
+ if (!isGitRepo(cloneDir)) return fail(`No sessions workspace at ${cloneDir}. Run tl sync init first.`, { cloneDir });
1985
+ git(cloneDir, ["add", "-A"]);
1986
+ const committed = git(cloneDir, ["commit", "-m", opts.message || "Sync sessions"]).status === 0;
1987
+ const p = git(cloneDir, ["push"]);
1988
+ if (p.status !== 0) return fail(`Push failed: ${(p.err || "").split("\n")[0] || "see git output"}. Pull first (tl sync pull), then push.`, { cloneDir });
1989
+ return ok(`Pushed ${cloneDir}.${committed ? " Committed pending changes." : " Nothing new to commit; pushed any local commits."}`, { cloneDir, committed });
1990
+ }
1991
+ function syncStatus(opts) {
1992
+ const cfg = loadConfig();
1993
+ const { cloneDir, ws } = resolveWorkspace(opts, cfg);
1994
+ if (!isGitRepo(cloneDir)) {
1995
+ const known = cfg.workspaces.length ? ` Known workspaces: ${cfg.workspaces.map((w) => w.name).join(", ")}.` : "";
1996
+ return fail(`No sessions workspace at ${cloneDir}. Run tl sync init --repo <owner/name> first.${known}`, { cloneDir, workspaces: cfg.workspaces });
1997
+ }
1998
+ const st = parseGitStatus(git(cloneDir, ["status", "--porcelain=v1", "--branch"]).out);
1999
+ const sessions = listStateFiles(cloneDir).length;
2000
+ return ok(
2001
+ `Workspace ${ws?.name || "(by dir)"} at ${cloneDir}: ${sessions} session(s), branch ${st.branch || "?"}, ${st.ahead} ahead, ${st.behind} behind, ${st.dirty ? `${st.files.length} uncommitted change(s)` : "clean"}.${ws?.activeSession ? ` Active session: ${ws.activeSession}.` : ""}`,
2002
+ { cloneDir, workspace: ws?.name || null, sessions, ...st }
2003
+ );
2004
+ }
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 chunkChildren(blocks, size = CHILDREN_MAX) {
2845
+ const out = [];
2846
+ for (let i = 0; i < blocks.length; i += size) out.push(blocks.slice(i, i + size));
2847
+ return out.length ? out : [[]];
2848
+ }
2849
+ function artifactCategory(path) {
2850
+ if (path.startsWith("Brand/")) return "Brand";
2851
+ if (path.startsWith("Deploy/")) return "Deploy";
2852
+ if (path.startsWith("LandingPage/")) return "Landing";
2853
+ if (path.endsWith(".svg")) return "Infographic";
2854
+ if (path === "BuildPrompt.md") return "Build prompt";
2855
+ return "Doc";
2856
+ }
2857
+ var WIKI_AREAS = [
2858
+ { key: "big-idea", title: "The Big Idea", emoji: "\u{1F4A1}" },
2859
+ { key: "business-model", title: "Business Model", emoji: "\u{1F4B0}" },
2860
+ { key: "brand", title: "Brand", emoji: "\u{1F3A8}" },
2861
+ { key: "market-research", title: "Market Research", emoji: "\u{1F4CA}" },
2862
+ { key: "strategy", title: "Strategy", emoji: "\u{1F4C8}" },
2863
+ { key: "product", title: "Product (PRD)", emoji: "\u{1F4CB}" },
2864
+ { key: "compliance", title: "Compliance & Tax", emoji: "\u2696\uFE0F" },
2865
+ { key: "decision-science", title: "Decision Science", emoji: "\u{1F9ED}" },
2866
+ { key: "library", title: "Library", emoji: "\u{1F4DA}" }
2867
+ ];
2868
+ function buildWikiPlan(state, opts = {}) {
2869
+ const answers = obj2(state.answers);
2870
+ const brand = state.brand && typeof state.brand === "object" ? state.brand : null;
2871
+ const guide = brand?.guide || null;
2872
+ const grill = state.grill && typeof state.grill === "object" ? state.grill : null;
2873
+ const swot = state.swot && typeof state.swot === "object" ? state.swot : null;
2874
+ const prd = obj2(state.prd);
2875
+ const bizModel = obj2(state.bizModel);
2876
+ const research = obj2(state.research);
2877
+ const assumptions = bizModel["assumptions"] || null;
2878
+ const brandName = str3(guide?.brandName) || "Your Product";
2879
+ const oneLiner = str3(answers["what-statement"]) || str3(answers["pitch"]) || str3(guide?.positioning);
2880
+ const overview = [];
2881
+ if (oneLiner) overview.push(callout(oneLiner, "\u{1F4A1}"));
2882
+ 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."));
2883
+ const areaBlocks = {};
2884
+ {
2885
+ const b = [];
2886
+ if (oneLiner) b.push(heading(2, "What it is"), callout(oneLiner, "\u{1F3AF}"));
2887
+ if (guide?.positioning) b.push(heading(2, "Who it is for"), para(guide.positioning));
2888
+ const press = str3(answers["press-release"]);
2889
+ if (press) b.push(heading(2, "The press release"), ...markdownToBlocks(press));
2890
+ areaBlocks["big-idea"] = b;
2891
+ }
2892
+ {
2893
+ const b = [];
2894
+ const proj = computeProjection(assumptions);
2895
+ if (proj && assumptions) {
2896
+ const s = proj.summary;
2897
+ const cur = assumptions.currency || "USD";
2898
+ b.push(heading(2, "The numbers"));
2899
+ b.push(callout(
2900
+ `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)}.`,
2901
+ "\u{1F4B0}"
2902
+ ));
2903
+ const rows = (assumptions.parties || []).slice(0, 12).map((p) => [
2904
+ str3(p.name) || p.id,
2905
+ str3(p.role),
2906
+ String(p.startingCount ?? ""),
2907
+ String(p.monthlyNewBase ?? ""),
2908
+ `${p.monthlyChurnPct ?? 0}%`,
2909
+ fmtMoney(Number(p.revenuePerUnitPerMonth) || 0, cur),
2910
+ fmtMoney(Number(p.cacPerUnit) || 0, cur)
2911
+ ]);
2912
+ b.push(heading(2, "Parties"), table(["Party", "Role", "Start", "New/mo", "Churn", "Rev/unit/mo", "CAC"], rows));
2913
+ if (assumptions.narrative) b.push(heading(2, "Notes"), para(assumptions.narrative));
2914
+ }
2915
+ areaBlocks["business-model"] = b;
2916
+ }
2917
+ {
2918
+ const b = [];
2919
+ if (guide) {
2920
+ b.push(...markdownToBlocks(brandGuideMarkdown(guide)));
2921
+ const pal = (guide.palette || []).filter((p) => p?.hex);
2922
+ if (pal.length) b.push(heading(2, "Palette"), table(["Color", "Hex", "Role"], pal.map((p) => [str3(p.name), str3(p.hex), str3(p.role)])));
2923
+ }
2924
+ areaBlocks["brand"] = b;
2925
+ }
2926
+ {
2927
+ const b = [];
2928
+ const brief = str3(research["brief"]);
2929
+ if (brief) {
2930
+ const desc = str3(research["description"]);
2931
+ if (desc) b.push(callout(desc, "\u{1F4CA}"));
2932
+ b.push(...markdownToBlocks(brief));
2933
+ }
2934
+ areaBlocks["market-research"] = b;
2935
+ }
2936
+ {
2937
+ const b = [];
2938
+ const hasSwot = !!swot && Object.values(swot).some((v) => Array.isArray(v) && v.some((x) => x && String(x).trim()));
2939
+ if (hasSwot) b.push(...markdownToBlocks(swotMarkdown(swot)));
2940
+ areaBlocks["strategy"] = b;
2941
+ }
2942
+ {
2943
+ const b = [];
2944
+ const prdMd = str3(prd["markdown"]);
2945
+ if (prdMd) b.push(...markdownToBlocks(prdMd));
2946
+ if (grill?.requirements?.length) {
2947
+ b.push(divider());
2948
+ b.push(...markdownToBlocks(requirementsMarkdown(grill)));
2949
+ }
2950
+ if (grill?.glossary?.length) {
2951
+ b.push(divider());
2952
+ b.push(...markdownToBlocks(glossaryMarkdown(grill)));
2953
+ }
2954
+ areaBlocks["product"] = b;
2955
+ }
2956
+ {
2957
+ const b = [];
2958
+ const report = str3(obj2(state.governance)["report"]);
2959
+ if (report.trim()) b.push(...markdownToBlocks(report));
2960
+ areaBlocks["compliance"] = b;
2961
+ }
2962
+ {
2963
+ const b = [];
2964
+ const dq = Object.keys(answers).filter((k) => /^(dq|decision)/i.test(k) && str3(answers[k]).trim());
2965
+ if (dq.length) {
2966
+ b.push(heading(2, "Decision records"));
2967
+ for (const k of dq) b.push(bullet(`**${k}**: ${str3(answers[k])}`));
2968
+ }
2969
+ areaBlocks["decision-science"] = b;
2970
+ }
2971
+ areaBlocks["library"] = [];
2972
+ const areas = WIKI_AREAS.map((a) => ({ ...a, blocks: areaBlocks[a.key] || [] })).filter((a) => a.blocks.length > 0);
2973
+ const urls = opts.urls || {};
2974
+ const files = opts.manifest?.files || [];
2975
+ 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] } : {} }));
2976
+ return { title: `${brandName} workspace`, icon: "\u{1F680}", overview, areas, artifacts };
2977
+ }
2978
+
2979
+ // core/notion-io.ts
2980
+ var NOTION_API = "https://api.notion.com/v1";
2981
+ var NOTION_VERSION = "2022-06-28";
2982
+ var MIN_INTERVAL_MS = 350;
2983
+ var TOKEN_ENVS = ["THOUGHT_LAYER_NOTION_TOKEN", "NOTION_TOKEN"];
2984
+ var ok3 = (message, details = {}) => ({ ok: true, message, details });
2985
+ var fail3 = (message, details = {}) => ({ ok: false, message, details });
2986
+ function notionConfigPath() {
2987
+ return process.env["THOUGHT_LAYER_NOTION_CONFIG"] || join7(homedir2(), ".thought-layer", "notion.json");
2988
+ }
2989
+ function loadNotionConfig() {
2990
+ const p = notionConfigPath();
2991
+ if (!existsSync6(p)) return { schema: 1, sessions: {} };
2992
+ try {
2993
+ const raw = JSON.parse(readFileSync5(p, "utf8"));
2994
+ const sessions = raw["sessions"] && typeof raw["sessions"] === "object" ? raw["sessions"] : {};
2995
+ return { schema: 1, sessions };
2996
+ } catch {
2997
+ return { schema: 1, sessions: {} };
2998
+ }
2999
+ }
3000
+ function saveNotionConfig(cfg) {
3001
+ const p = notionConfigPath();
3002
+ mkdirSync6(dirname6(p), { recursive: true });
3003
+ writeFileSync7(p, JSON.stringify(cfg, null, 2) + "\n");
3004
+ }
3005
+ function pageIdFromInput(s) {
3006
+ const t = String(s || "").trim();
3007
+ 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}/);
3008
+ if (dashed) return dashed[0].toLowerCase();
3009
+ const runs = t.match(/[0-9a-fA-F]{32}/g);
3010
+ if (!runs || !runs.length) return null;
3011
+ const id = runs[runs.length - 1].toLowerCase();
3012
+ return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
3013
+ }
3014
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
3015
+ var Notion = class {
3016
+ constructor(token) {
3017
+ this.token = token;
3018
+ }
3019
+ token;
3020
+ last = 0;
3021
+ async call(method, path, body) {
3022
+ for (let attempt = 0; ; attempt++) {
3023
+ const wait = MIN_INTERVAL_MS - (Date.now() - this.last);
3024
+ if (wait > 0) await sleep(wait);
3025
+ this.last = Date.now();
3026
+ const res = await fetch(`${NOTION_API}${path}`, {
3027
+ method,
3028
+ headers: {
3029
+ Authorization: `Bearer ${this.token}`,
3030
+ "Notion-Version": NOTION_VERSION,
3031
+ "Content-Type": "application/json"
3032
+ },
3033
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
3034
+ });
3035
+ if (res.status === 429 && attempt < 4) {
3036
+ const retry = Number(res.headers.get("Retry-After")) || Math.pow(2, attempt);
3037
+ await sleep(retry * 1e3);
3038
+ continue;
3039
+ }
3040
+ let json = {};
3041
+ try {
3042
+ json = await res.json();
3043
+ } catch {
3044
+ }
3045
+ return { status: res.status, json };
3046
+ }
3047
+ }
3048
+ async pageExists(id) {
3049
+ const r = await this.call("GET", `/pages/${id}`);
3050
+ return r.status === 200 && r.json["archived"] !== true;
3051
+ }
3052
+ async createPage(parentPageId, title, emoji) {
3053
+ const r = await this.call("POST", "/pages", {
3054
+ parent: { page_id: parentPageId },
3055
+ ...emoji ? { icon: { type: "emoji", emoji } } : {},
3056
+ properties: { title: { title: [{ text: { content: title } }] } }
3057
+ });
3058
+ if (r.status !== 200) throw new Error(notionErr("create page", r));
3059
+ return String(r.json["id"]);
3060
+ }
3061
+ // Replace a page's content: delete every existing child, then append the new
3062
+ // blocks in <=100-block batches.
3063
+ async replaceChildren(pageId, blocks) {
3064
+ let cursor;
3065
+ do {
3066
+ const q = cursor ? `?start_cursor=${cursor}&page_size=100` : "?page_size=100";
3067
+ const r = await this.call("GET", `/blocks/${pageId}/children${q}`);
3068
+ if (r.status !== 200) break;
3069
+ for (const b of r.json["results"] || []) await this.call("DELETE", `/blocks/${b.id}`);
3070
+ cursor = r.json["has_more"] ? String(r.json["next_cursor"]) : void 0;
3071
+ } while (cursor);
3072
+ for (const batch of chunkChildren(blocks)) {
3073
+ if (!batch.length) continue;
3074
+ const r = await this.call("PATCH", `/blocks/${pageId}/children`, { children: batch });
3075
+ if (r.status !== 200) throw new Error(notionErr("append blocks", r));
3076
+ }
3077
+ }
3078
+ async createArtifactsDb(parentPageId, artifacts) {
3079
+ const cats = Array.from(new Set(artifacts.map((a) => a.category)));
3080
+ const r = await this.call("POST", "/databases", {
3081
+ parent: { type: "page_id", page_id: parentPageId },
3082
+ title: [{ text: { content: "Artifacts" } }],
3083
+ icon: { type: "emoji", emoji: "\u{1F4CE}" },
3084
+ properties: {
3085
+ Name: { title: {} },
3086
+ Category: { select: { options: cats.map((c) => ({ name: c })) } },
3087
+ Size: { rich_text: {} },
3088
+ Link: { url: {} }
3089
+ }
3090
+ });
3091
+ if (r.status !== 200) throw new Error(notionErr("create database", r));
3092
+ return String(r.json["id"]);
3093
+ }
3094
+ async addArtifactRow(dbId, a) {
3095
+ await this.call("POST", "/pages", {
3096
+ parent: { database_id: dbId },
3097
+ properties: {
3098
+ Name: { title: [{ text: { content: a.name } }] },
3099
+ Category: { select: { name: a.category } },
3100
+ Size: { rich_text: [{ text: { content: humanSize(a.bytes) } }] },
3101
+ ...a.url ? { Link: { url: a.url } } : {}
3102
+ }
3103
+ });
3104
+ }
3105
+ };
3106
+ function notionErr(what, r) {
3107
+ const msg = String(r.json["message"] || "").slice(0, 200);
3108
+ return `Notion ${what} failed (${r.status})${msg ? `: ${msg}` : ""}`;
3109
+ }
3110
+ function humanSize(bytes) {
3111
+ if (bytes < 1024) return `${bytes} B`;
3112
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
3113
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
3114
+ }
3115
+ async function runWiki(opts) {
3116
+ try {
3117
+ const cfg = loadConfig();
3118
+ const { cloneDir, ws } = resolveWorkspace(opts, cfg);
3119
+ if (!isGitRepo(cloneDir)) {
3120
+ return fail3(`No sessions workspace at ${cloneDir}. Run tl sync init then tl artifacts before building the wiki.`, { cloneDir });
3121
+ }
3122
+ const slug = slugify(opts.name || ws?.activeSession?.replace(/\.json$/, "") || "");
3123
+ if (!slug) return fail3("Name the session to publish: tl wiki --name <name>.");
3124
+ const sessionPath = join7(cloneDir, STATE_DIR, `${slug}.json`);
3125
+ const useExplicit = !!(opts.path && opts.path.trim() || (process.env["THOUGHT_LAYER_STATE"] || "").trim());
3126
+ const loaded = loadStateFile(useExplicit ? opts.path : existsSync6(sessionPath) ? sessionPath : opts.path);
3127
+ if (!loaded.exists) return fail3(`No session "${slug}" found (looked at ${loaded.path}). Save it first with tl sync save --name ${slug}.`, { cloneDir });
3128
+ const artifactsDir = join7(cloneDir, "artifacts", slug);
3129
+ let manifest = null;
3130
+ const manifestPath = join7(artifactsDir, "artifacts.json");
3131
+ if (existsSync6(manifestPath)) {
3132
+ try {
3133
+ manifest = JSON.parse(readFileSync5(manifestPath, "utf8"));
3134
+ } catch {
3135
+ manifest = null;
3136
+ }
3137
+ }
3138
+ const branch = git(cloneDir, ["rev-parse", "--abbrev-ref", "HEAD"]).out.trim() || ws?.defaultBranch || "main";
3139
+ const ownerName = repoOwnerName(ws?.repo || "");
3140
+ const urls = {};
3141
+ if (ownerName && manifest) {
3142
+ const base = `https://github.com/${ownerName}/blob/${branch}/artifacts/${slug}`;
3143
+ for (const f of manifest.files) urls[f.path] = `${base}/${f.path.split("/").map(encodeURIComponent).join("/")}`;
3144
+ }
3145
+ const plan = buildWikiPlan(loaded.state, { manifest, urls });
3146
+ const blockCount = plan.overview.length + plan.areas.reduce((n, a) => n + a.blocks.length, 0);
3147
+ if (opts.dryRun) {
3148
+ const areaList = plan.areas.map((a) => `${a.emoji} ${a.title} (${a.blocks.length} blocks)`).join(", ");
3149
+ return ok3(
3150
+ `Dry run for "${plan.title}": ${plan.areas.length} area page(s), ${blockCount} blocks total, ${plan.artifacts.length} artifact(s) in the database.
3151
+ Areas: ${areaList || "(none with content yet)"}.${manifest ? "" : "\nNo delivered artifacts found; run tl artifacts first to populate the Artifacts database with GitHub links."}`,
3152
+ { title: plan.title, areas: plan.areas.map((a) => ({ key: a.key, blocks: a.blocks.length })), blockCount, artifacts: plan.artifacts.length, delivered: !!manifest }
3153
+ );
3154
+ }
3155
+ const token = TOKEN_ENVS.map((e) => process.env[e]).find((v) => v && v.trim())?.trim() || "";
3156
+ if (!token) {
3157
+ return fail3(
3158
+ "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.",
3159
+ { needs: "token" }
3160
+ );
3161
+ }
3162
+ const ncfg = loadNotionConfig();
3163
+ const entry = ncfg.sessions[slug] || {};
3164
+ const notion = new Notion(token);
3165
+ let rootId = opts.replace ? "" : entry.rootPageId || "";
3166
+ if (rootId && !await notion.pageExists(rootId)) rootId = "";
3167
+ if (!rootId) {
3168
+ const parentInput = opts.parentPage || process.env["THOUGHT_LAYER_NOTION_PARENT"] || "";
3169
+ const parentId = pageIdFromInput(parentInput);
3170
+ if (!parentId) {
3171
+ return fail3(
3172
+ "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>.",
3173
+ { needs: "parent-page" }
3174
+ );
3175
+ }
3176
+ if (!await notion.pageExists(parentId)) {
3177
+ 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" });
3178
+ }
3179
+ rootId = await notion.createPage(parentId, plan.title, plan.icon);
3180
+ entry.areaPageIds = {};
3181
+ entry.artifactsDbId = void 0;
3182
+ }
3183
+ await notion.replaceChildren(rootId, plan.overview);
3184
+ const areaIds = entry.areaPageIds || {};
3185
+ for (const area of plan.areas) {
3186
+ let pid = areaIds[area.key] || "";
3187
+ if (pid && !await notion.pageExists(pid)) pid = "";
3188
+ if (!pid) pid = await notion.createPage(rootId, `${area.emoji} ${area.title}`, area.emoji);
3189
+ areaIds[area.key] = pid;
3190
+ await notion.replaceChildren(pid, area.blocks);
3191
+ }
3192
+ let dbId = entry.artifactsDbId || "";
3193
+ if (plan.artifacts.length && (!dbId || opts.replace)) {
3194
+ dbId = await notion.createArtifactsDb(rootId, plan.artifacts);
3195
+ for (const a of plan.artifacts) await notion.addArtifactRow(dbId, a);
3196
+ }
3197
+ ncfg.sessions[slug] = { rootPageId: rootId, areaPageIds: areaIds, artifactsDbId: dbId || void 0, updatedAt: Date.now() };
3198
+ saveNotionConfig(ncfg);
3199
+ const rootUrl = `https://www.notion.so/${rootId.replace(/-/g, "")}`;
3200
+ return ok3(
3201
+ `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."}
3202
+ Open it: ${rootUrl}`,
3203
+ { title: plan.title, rootPageId: rootId, rootUrl, areas: plan.areas.length, blockCount, artifacts: plan.artifacts.length }
3204
+ );
3205
+ } catch (e) {
3206
+ return fail3(`tl_wiki error: ${e.message}`);
3207
+ }
3208
+ }
3209
+
1450
3210
  // bin/tl.ts
1451
3211
  var HELP = `tl - read/write a portable Thought Layer state file (default: .thought-layer/state.json)
1452
3212
 
@@ -1455,6 +3215,12 @@ var HELP = `tl - read/write a portable Thought Layer state file (default: .thoug
1455
3215
  tl scaffold [--out dist] [--domain x.com] [--founder "Name"] deterministic deployable static site from the spec + brand
1456
3216
  tl deploy [--dry-run] [--anonymous] [--name x] [--site id] take build.json's publish dir live to a user-owned Netlify URL
1457
3217
  [--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
3218
+ tl sync <init|save|list|open|pull|push|status> store/sync your session files in your own private GitHub repo
3219
+ [--repo owner/name] [--name x] [--dir p] [--workspace w] [--message m] [--no-push]
3220
+ tl artifacts [--name x] [--workspace w] deliver the full asset bundle (PRD, brand, infographics, landing, deploy rules) to your sessions repo
3221
+ [--no-push] [--no-deliver] [--domain x.com] [--founder "Name"]
3222
+ tl wiki [--parent-page id|url] [--name x] build/refresh a private Notion wiki from the session + delivered artifacts
3223
+ [--workspace w] [--replace] [--dry-run] (set THOUGHT_LAYER_NOTION_TOKEN; share a Notion page with your integration)
1458
3224
  tl export [path] handoff check
1459
3225
  tl answer <qId> <value> [path] record an answer
1460
3226
  tl feedback --data '<json>' record a panel verdict ({qId,mode,personas,endState,round})
@@ -1489,7 +3255,7 @@ function parseArgs(argv) {
1489
3255
  function readData(flags) {
1490
3256
  const d = flags["data"];
1491
3257
  if (d === void 0) return void 0;
1492
- const raw = d === "-" || d === true ? readFileSync3(0, "utf8") : String(d);
3258
+ const raw = d === "-" || d === true ? readFileSync6(0, "utf8") : String(d);
1493
3259
  try {
1494
3260
  return JSON.parse(raw);
1495
3261
  } catch {
@@ -1563,6 +3329,61 @@ function main() {
1563
3329
  });
1564
3330
  return;
1565
3331
  }
3332
+ if (args[0] === "sync") {
3333
+ runSync(
3334
+ {
3335
+ op: typeof args[1] === "string" ? args[1] : "status",
3336
+ name: typeof flags["name"] === "string" ? flags["name"] : void 0,
3337
+ repo: typeof flags["repo"] === "string" ? flags["repo"] : void 0,
3338
+ dir: typeof flags["dir"] === "string" ? flags["dir"] : void 0,
3339
+ workspace: typeof flags["workspace"] === "string" ? flags["workspace"] : void 0,
3340
+ message: typeof flags["message"] === "string" ? flags["message"] : void 0,
3341
+ noPush: flags["no-push"] === true,
3342
+ path: typeof flags["path"] === "string" ? flags["path"] : void 0
3343
+ },
3344
+ { ts: Date.now(), exportedAt: (/* @__PURE__ */ new Date()).toISOString() }
3345
+ ).then((r2) => {
3346
+ if (flags["json"]) console.log(JSON.stringify(r2.details, null, 2));
3347
+ else console.log(r2.message);
3348
+ process.exit(r2.ok ? 0 : 1);
3349
+ });
3350
+ return;
3351
+ }
3352
+ if (args[0] === "artifacts") {
3353
+ const r2 = runArtifacts(
3354
+ {
3355
+ path: typeof flags["path"] === "string" ? flags["path"] : void 0,
3356
+ name: typeof flags["name"] === "string" ? flags["name"] : void 0,
3357
+ workspace: typeof flags["workspace"] === "string" ? flags["workspace"] : void 0,
3358
+ dir: typeof flags["dir"] === "string" ? flags["dir"] : void 0,
3359
+ message: typeof flags["message"] === "string" ? flags["message"] : void 0,
3360
+ noPush: flags["no-push"] === true,
3361
+ noDeliver: flags["no-deliver"] === true,
3362
+ domain: typeof flags["domain"] === "string" ? flags["domain"] : void 0,
3363
+ founderName: typeof flags["founder"] === "string" ? flags["founder"] : void 0
3364
+ },
3365
+ { generatedAt: (/* @__PURE__ */ new Date()).toISOString() }
3366
+ );
3367
+ if (flags["json"]) console.log(JSON.stringify(r2.details, null, 2));
3368
+ else console.log(r2.message);
3369
+ process.exit(r2.ok ? 0 : 1);
3370
+ }
3371
+ if (args[0] === "wiki") {
3372
+ runWiki({
3373
+ path: typeof flags["path"] === "string" ? flags["path"] : void 0,
3374
+ name: typeof flags["name"] === "string" ? flags["name"] : void 0,
3375
+ workspace: typeof flags["workspace"] === "string" ? flags["workspace"] : void 0,
3376
+ dir: typeof flags["dir"] === "string" ? flags["dir"] : void 0,
3377
+ parentPage: typeof flags["parent-page"] === "string" ? flags["parent-page"] : void 0,
3378
+ replace: flags["replace"] === true,
3379
+ dryRun: flags["dry-run"] === true
3380
+ }).then((r2) => {
3381
+ if (flags["json"]) console.log(JSON.stringify(r2.details, null, 2));
3382
+ else console.log(r2.message);
3383
+ process.exit(r2.ok ? 0 : 1);
3384
+ });
3385
+ return;
3386
+ }
1566
3387
  let payload;
1567
3388
  try {
1568
3389
  payload = buildOp(args, flags);