scribe-cms 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -56,12 +56,19 @@ function addColumnIfMissing(db, table, column, ddlType) {
56
56
  db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddlType}`);
57
57
  }
58
58
  }
59
+ function readSchemaVersion(db) {
60
+ const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
61
+ return row ? Number.parseInt(row.value, 10) || 0 : 0;
62
+ }
59
63
  function migrate(db) {
64
+ const previousVersion = readSchemaVersion(db);
60
65
  for (const sql of MIGRATIONS) {
61
66
  db.exec(sql);
62
67
  }
63
- addColumnIfMissing(db, "revisions", "frontmatter_json", "TEXT");
64
- addColumnIfMissing(db, "revisions", "body", "TEXT");
68
+ if (previousVersion < 4) {
69
+ db.exec(`DROP TABLE IF EXISTS revisions`);
70
+ }
71
+ addColumnIfMissing(db, "translations", "snapshot_id", "INTEGER");
65
72
  db.prepare(
66
73
  `INSERT INTO meta(key, value) VALUES('schema_version', ?)
67
74
  ON CONFLICT(key) DO UPDATE SET value = excluded.value`
@@ -74,7 +81,7 @@ var SCHEMA_VERSION, MIGRATIONS;
74
81
  var init_sqlite = __esm({
75
82
  "src/storage/sqlite.ts"() {
76
83
  init_esm_shims();
77
- SCHEMA_VERSION = 3;
84
+ SCHEMA_VERSION = 4;
78
85
  MIGRATIONS = [
79
86
  `CREATE TABLE IF NOT EXISTS meta (
80
87
  key TEXT PRIMARY KEY,
@@ -94,20 +101,6 @@ var init_sqlite = __esm({
94
101
  )`,
95
102
  `CREATE INDEX IF NOT EXISTS idx_translations_type_locale
96
103
  ON translations(content_type, locale)`,
97
- `CREATE TABLE IF NOT EXISTS revisions (
98
- id INTEGER PRIMARY KEY AUTOINCREMENT,
99
- content_type TEXT NOT NULL,
100
- en_slug TEXT NOT NULL,
101
- locale TEXT,
102
- revision_kind TEXT NOT NULL,
103
- en_hash TEXT NOT NULL,
104
- body_hash TEXT NOT NULL,
105
- created_at TEXT NOT NULL,
106
- model TEXT,
107
- body_preview TEXT
108
- )`,
109
- `CREATE INDEX IF NOT EXISTS idx_revisions_lookup
110
- ON revisions(content_type, en_slug, locale, created_at DESC)`,
111
104
  `CREATE TABLE IF NOT EXISTS slug_aliases (
112
105
  content_type TEXT NOT NULL,
113
106
  canonical_en_slug TEXT NOT NULL,
@@ -124,7 +117,19 @@ var init_sqlite = __esm({
124
117
  locale_slug TEXT NOT NULL,
125
118
  captured_at TEXT NOT NULL,
126
119
  PRIMARY KEY (content_type, alias_en_slug, locale)
127
- )`
120
+ )`,
121
+ `CREATE TABLE IF NOT EXISTS en_snapshots (
122
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
123
+ content_type TEXT NOT NULL,
124
+ en_slug TEXT NOT NULL,
125
+ en_hash TEXT NOT NULL,
126
+ frontmatter_json TEXT NOT NULL,
127
+ body TEXT NOT NULL,
128
+ created_at TEXT NOT NULL,
129
+ UNIQUE (content_type, en_slug, en_hash)
130
+ )`,
131
+ `CREATE INDEX IF NOT EXISTS idx_en_snapshots_lookup
132
+ ON en_snapshots(content_type, en_slug, created_at DESC)`
128
133
  ];
129
134
  }
130
135
  });
@@ -598,18 +603,51 @@ init_sqlite();
598
603
 
599
604
  // src/storage/translations.ts
600
605
  init_esm_shims();
606
+ function getOrCreateEnSnapshot(db, input) {
607
+ db.prepare(
608
+ `INSERT OR IGNORE INTO en_snapshots (
609
+ content_type, en_slug, en_hash, frontmatter_json, body, created_at
610
+ ) VALUES (?, ?, ?, ?, ?, ?)`
611
+ ).run(
612
+ input.contentType,
613
+ input.enSlug,
614
+ input.enHash,
615
+ JSON.stringify(input.frontmatter),
616
+ input.body,
617
+ input.createdAt
618
+ );
619
+ const row = db.prepare(
620
+ `SELECT id FROM en_snapshots
621
+ WHERE content_type = ? AND en_slug = ? AND en_hash = ?`
622
+ ).get(input.contentType, input.enSlug, input.enHash);
623
+ return row.id;
624
+ }
625
+ function getEnSnapshot(db, snapshotId) {
626
+ return db.prepare(`SELECT * FROM en_snapshots WHERE id = ?`).get(snapshotId);
627
+ }
628
+ function listEnSnapshotsForEnSlug(db, contentType, enSlug) {
629
+ return db.prepare(
630
+ `SELECT s.*, GROUP_CONCAT(t.locale) AS locales
631
+ FROM en_snapshots s
632
+ LEFT JOIN translations t ON t.snapshot_id = s.id
633
+ WHERE s.content_type = ? AND s.en_slug = ?
634
+ GROUP BY s.id
635
+ ORDER BY s.created_at DESC, s.id DESC`
636
+ ).all(contentType, enSlug);
637
+ }
601
638
  function upsertTranslation(db, input) {
602
639
  db.prepare(
603
640
  `INSERT INTO translations (
604
- content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model
605
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
641
+ content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model, snapshot_id
642
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
606
643
  ON CONFLICT(content_type, en_slug, locale) DO UPDATE SET
607
644
  slug = excluded.slug,
608
645
  frontmatter_json = excluded.frontmatter_json,
609
646
  body = excluded.body,
610
647
  en_hash = excluded.en_hash,
611
648
  translated_at = excluded.translated_at,
612
- model = excluded.model`
649
+ model = excluded.model,
650
+ snapshot_id = excluded.snapshot_id`
613
651
  ).run(
614
652
  input.contentType,
615
653
  input.enSlug,
@@ -619,7 +657,8 @@ function upsertTranslation(db, input) {
619
657
  input.body,
620
658
  input.enHash,
621
659
  input.translatedAt,
622
- input.model
660
+ input.model,
661
+ input.snapshotId
623
662
  );
624
663
  }
625
664
  function getTranslation(db, contentType, enSlug, locale) {
@@ -641,41 +680,6 @@ function listTranslationsForLocale(db, contentType, locale) {
641
680
  function bulkLoadTranslations(db) {
642
681
  return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
643
682
  }
644
- function appendRevision(db, input) {
645
- const result = db.prepare(
646
- `INSERT INTO revisions (
647
- content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
648
- model, body_preview, frontmatter_json, body
649
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
650
- ).run(
651
- input.contentType,
652
- input.enSlug,
653
- input.locale,
654
- input.revisionKind,
655
- input.enHash,
656
- input.bodyHash,
657
- input.createdAt,
658
- input.model ?? null,
659
- input.bodyPreview ?? null,
660
- input.frontmatterJson ?? null,
661
- input.body ?? null
662
- );
663
- return Number(result.lastInsertRowid);
664
- }
665
- function listRevisions(db, contentType, enSlug, locale) {
666
- if (locale) {
667
- return db.prepare(
668
- `SELECT * FROM revisions
669
- WHERE content_type = ? AND en_slug = ? AND locale = ?
670
- ORDER BY created_at DESC, id DESC`
671
- ).all(contentType, enSlug, locale);
672
- }
673
- return db.prepare(
674
- `SELECT * FROM revisions
675
- WHERE content_type = ? AND en_slug = ?
676
- ORDER BY created_at DESC, id DESC`
677
- ).all(contentType, enSlug);
678
- }
679
683
 
680
684
  // src/loader/normalize-en.ts
681
685
  init_esm_shims();
@@ -1812,9 +1816,6 @@ function computePageEnHash(translatableFrontmatter, body) {
1812
1816
  const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
1813
1817
  return sha256(payload);
1814
1818
  }
1815
- function computeBodyHash(body) {
1816
- return sha256(body);
1817
- }
1818
1819
 
1819
1820
  // src/translate/worklist.ts
1820
1821
  init_sqlite();
@@ -1879,25 +1880,20 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
1879
1880
  // src/translate/page-translator.ts
1880
1881
  init_esm_shims();
1881
1882
 
1882
- // src/history/record-revision.ts
1883
+ // src/history/record-snapshot.ts
1883
1884
  init_esm_shims();
1884
1885
  init_sqlite();
1885
- function recordRevision(config, input) {
1886
- const db = openStore(config, "readwrite");
1887
- const id = appendRevision(db, {
1886
+ function recordEnSnapshot(config, input, db) {
1887
+ const ownDb = db ?? openStore(config, "readwrite");
1888
+ const id = getOrCreateEnSnapshot(ownDb, {
1888
1889
  contentType: input.contentType,
1889
1890
  enSlug: input.enSlug,
1890
- locale: input.locale,
1891
- revisionKind: input.revisionKind,
1892
1891
  enHash: input.enHash,
1893
- bodyHash: computeBodyHash(input.body),
1894
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1895
- model: input.model,
1896
- bodyPreview: input.body.slice(0, 200),
1897
- frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
1898
- body: input.body
1892
+ frontmatter: input.frontmatter,
1893
+ body: input.body,
1894
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1899
1895
  });
1900
- db.close();
1896
+ if (!db) ownDb.close();
1901
1897
  return id;
1902
1898
  }
1903
1899
 
@@ -2299,6 +2295,17 @@ async function translatePage(config, item, options = {}) {
2299
2295
  throw new Error(`Translation validation failed: ${validated.error}`);
2300
2296
  }
2301
2297
  const writeDb = openStore(config, "readwrite");
2298
+ const snapshotId = recordEnSnapshot(
2299
+ config,
2300
+ {
2301
+ contentType: type.id,
2302
+ enSlug: item.enSlug,
2303
+ enHash: currentEnHash,
2304
+ frontmatter: payload.frontmatter,
2305
+ body: payload.body
2306
+ },
2307
+ writeDb
2308
+ );
2302
2309
  upsertTranslation(writeDb, {
2303
2310
  contentType: type.id,
2304
2311
  enSlug: item.enSlug,
@@ -2308,19 +2315,10 @@ async function translatePage(config, item, options = {}) {
2308
2315
  body: result.parsed.body,
2309
2316
  enHash: currentEnHash,
2310
2317
  translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2311
- model: result.model
2318
+ model: result.model,
2319
+ snapshotId
2312
2320
  });
2313
2321
  writeDb.close();
2314
- recordRevision(config, {
2315
- contentType: type.id,
2316
- enSlug: item.enSlug,
2317
- locale: item.locale,
2318
- revisionKind: "translation",
2319
- enHash: currentEnHash,
2320
- body: result.parsed.body,
2321
- frontmatter: validated.frontmatter,
2322
- model: result.model
2323
- });
2324
2322
  const estimatedCostUsd = estimateTranslationCostUsd(
2325
2323
  normalizeGeminiDisplayName(result.model),
2326
2324
  result.usage.inputTokens,
@@ -2559,38 +2557,31 @@ function renderFrontmatterTable(frontmatter, schema) {
2559
2557
  <tbody>${rows || `<tr><td colspan="2" class="dim">\u2014</td></tr>`}</tbody>
2560
2558
  </table>`;
2561
2559
  }
2562
- function renderRevisionSnapshot(revision) {
2563
- if (revision.frontmatter_json && revision.body) {
2564
- let frontmatter = {};
2565
- try {
2566
- frontmatter = JSON.parse(revision.frontmatter_json);
2567
- } catch {
2568
- frontmatter = {};
2569
- }
2570
- const fmRows = flattenFrontmatter(frontmatter).map(
2571
- (row) => `<tr><td class="k">${escapeHtml(row.key)}</td><td class="v">${escapeHtml(row.value)}</td></tr>`
2572
- ).join("");
2573
- return `<table class="kv"><tbody>${fmRows}</tbody></table>
2574
- <pre class="code">${escapeHtml(revision.body)}</pre>`;
2575
- }
2576
- return `<p class="dim">${escapeHtml(revision.body_preview ?? "(no snapshot)")}</p>`;
2577
- }
2578
- function renderRevisionTimeline(revisions) {
2579
- if (revisions.length === 0) {
2580
- return `<p class="dim">No history.</p>`;
2581
- }
2582
- const items = revisions.map(
2583
- (row) => `<tr>
2584
- <td class="dim">${escapeHtml(row.created_at.slice(0, 19))}</td>
2585
- <td>${escapeHtml(row.revision_kind)}</td>
2586
- <td class="dim">${row.model ? escapeHtml(row.model) : "\u2014"}</td>
2587
- <td class="dim mono">${escapeHtml(row.en_hash.slice(0, 8))}</td>
2588
- <td><details><summary>view</summary>${renderRevisionSnapshot(row)}</details></td>
2589
- </tr>`
2560
+ function renderEnSnapshotPreview(snapshot) {
2561
+ let frontmatter = {};
2562
+ try {
2563
+ frontmatter = JSON.parse(snapshot.frontmatter_json);
2564
+ } catch {
2565
+ frontmatter = {};
2566
+ }
2567
+ const fmRows = flattenFrontmatter(frontmatter).map(
2568
+ (row) => `<tr><td class="k">${escapeHtml(row.key)}</td><td class="v">${escapeHtml(row.value)}</td></tr>`
2590
2569
  ).join("");
2591
- return `<table class="data"><thead><tr>
2592
- <th>When</th><th>Kind</th><th>Model</th><th>Hash</th><th></th>
2593
- </tr></thead><tbody>${items}</tbody></table>`;
2570
+ return `<table class="kv"><tbody>${fmRows}</tbody></table>
2571
+ <pre class="code">${escapeHtml(snapshot.body)}</pre>`;
2572
+ }
2573
+ function renderTranslationSnapshotPanel(snapshot, currentEnHash) {
2574
+ if (!snapshot) {
2575
+ return `<p class="dim">No EN snapshot linked to this translation.</p>`;
2576
+ }
2577
+ const staleNote = currentEnHash && currentEnHash !== snapshot.en_hash ? `<p class="dim">Current EN hash differs from snapshot (${escapeHtml(currentEnHash.slice(0, 12))} vs ${escapeHtml(snapshot.en_hash.slice(0, 12))}).</p>` : "";
2578
+ return `<dl class="meta">
2579
+ <dt>snapshot</dt><dd>#${snapshot.id}</dd>
2580
+ <dt>captured</dt><dd>${escapeHtml(snapshot.created_at.slice(0, 19))}</dd>
2581
+ <dt>en_hash</dt><dd class="mono">${escapeHtml(snapshot.en_hash.slice(0, 12))}</dd>
2582
+ </dl>
2583
+ ${staleNote}
2584
+ <details><summary>EN source at translation time</summary>${renderEnSnapshotPreview(snapshot)}</details>`;
2594
2585
  }
2595
2586
  function renderLayout(title, body, project, options = {}) {
2596
2587
  const typeLinks = project.listTypes().map((type) => {
@@ -2901,10 +2892,10 @@ async function startStudio(project, options = {}) {
2901
2892
  <dt>slug</dt><dd>${escapeHtml(translation.slug)}</dd>
2902
2893
  <dt>en_hash</dt><dd>${escapeHtml(currentEnHash?.slice(0, 12) ?? "\u2014")} / ${escapeHtml(storedEnHash?.slice(0, 12) ?? "\u2014")}</dd>
2903
2894
  </dl>`;
2904
- const revisions = listRevisions(db, typeId, enSlug, locale);
2895
+ const snapshot = translation.snapshot_id != null ? getEnSnapshot(db, translation.snapshot_id) : void 0;
2905
2896
  historyPanel = `<div class="section">
2906
- <div class="section-head">History</div>
2907
- <div class="section-body">${renderRevisionTimeline(revisions)}</div>
2897
+ <div class="section-head">EN snapshot</div>
2898
+ <div class="section-body">${renderTranslationSnapshotPanel(snapshot, currentEnHash)}</div>
2908
2899
  </div>`;
2909
2900
  } else {
2910
2901
  contentPanel = `<p class="dim" style="padding:12px">No translation for ${escapeHtml(locale)}.</p>`;
@@ -3369,7 +3360,7 @@ Commands:
3369
3360
  validate Validate EN files and sqlite consistency
3370
3361
  export-static Write raw MDX files for static hosting
3371
3362
  translate Translate stale/missing locale pages
3372
- history <type> <slug> Show revision timeline
3363
+ history <type> <slug> Show EN snapshot timeline
3373
3364
  studio Start read-only local studio
3374
3365
  version Print scribe-cms version
3375
3366
 
@@ -3475,11 +3466,13 @@ Translate flags:
3475
3466
  }
3476
3467
  const { openStore: openStore2 } = await Promise.resolve().then(() => (init_sqlite(), sqlite_exports));
3477
3468
  const db = openStore2(config, "readonly");
3478
- const rows = listRevisions(db, typeId, enSlug, locale);
3469
+ const rows = listEnSnapshotsForEnSlug(db, typeId, enSlug);
3479
3470
  db.close();
3480
3471
  for (const row of rows) {
3472
+ const locales = row.locales ?? "";
3473
+ if (locale && !locales.split(",").includes(locale)) continue;
3481
3474
  console.log(
3482
- `${row.created_at} ${row.revision_kind} locale=${row.locale ?? "en"} en_hash=${row.en_hash.slice(0, 8)} body_hash=${row.body_hash.slice(0, 8)}`
3475
+ `${row.created_at} snapshot=#${row.id} en_hash=${row.en_hash.slice(0, 8)} locales=${locales || "\u2014"}`
3483
3476
  );
3484
3477
  }
3485
3478
  break;