@xtrable-ltd/nanoesis 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/adapter-azure-blob.d.ts +7 -1
  2. package/dist/adapter-azure-blob.js +2 -2
  3. package/dist/{chunk-J6VYOB47.js → chunk-UZQ7UP2B.js} +1678 -1298
  4. package/dist/editor-api.d.ts +39 -2
  5. package/dist/editor-api.js +393 -4
  6. package/dist/index.d.ts +371 -165
  7. package/dist/index.js +21 -1
  8. package/editor/assets/MigrationsPane-BAHPPSXP.css +1 -0
  9. package/editor/assets/MigrationsPane-_FGonx4-.js +4 -0
  10. package/editor/assets/{TemplatesPane-B4_sg2u5.css → TemplatesPane-CiLiMCc8.css} +1 -1
  11. package/editor/assets/{TemplatesPane-4IAoeX4-.js → TemplatesPane-Z6Bn69Hb.js} +204 -202
  12. package/editor/assets/{cssMode-BhSmGQp_.js → cssMode-dkQrIPWx.js} +1 -1
  13. package/editor/assets/{freemarker2-Z1jVSRUs.js → freemarker2-DEqcFFWa.js} +1 -1
  14. package/editor/assets/{handlebars-C3kew8-P.js → handlebars-C6ojANWr.js} +1 -1
  15. package/editor/assets/{html-RVg2mWQY.js → html-BmiAmVUD.js} +1 -1
  16. package/editor/assets/{htmlMode-ljHXud-Y.js → htmlMode-BBmUqToI.js} +1 -1
  17. package/editor/assets/{index-R39CtpUa.css → index-DEz8GUII.css} +1 -1
  18. package/editor/assets/index-LtCzUHAw.js +138 -0
  19. package/editor/assets/{javascript-CAr3NHzi.js → javascript-Cxm2TfJy.js} +1 -1
  20. package/editor/assets/{jsonMode-Cq4fxtNe.js → jsonMode-CW5012Hx.js} +1 -1
  21. package/editor/assets/{liquid-DeSAzZOT.js → liquid-DrS7ilHv.js} +1 -1
  22. package/editor/assets/{mdx-C4Ynnq4H.js → mdx-CwdSU5o1.js} +1 -1
  23. package/editor/assets/{python-rTSPH-tA.js → python-CALCR0yC.js} +1 -1
  24. package/editor/assets/{razor-B-0Do2A9.js → razor-SVCo2LoM.js} +1 -1
  25. package/editor/assets/{tsMode-m4eOcwoz.js → tsMode-CzXfTR_Q.js} +1 -1
  26. package/editor/assets/{typescript-CdvinFng.js → typescript-CP0Ovrv7.js} +1 -1
  27. package/editor/assets/{xml-DuAYX2gv.js → xml-B2yqloTa.js} +1 -1
  28. package/editor/assets/{yaml-BWplD8Hf.js → yaml-DTLJhzgY.js} +1 -1
  29. package/editor/index.html +2 -2
  30. package/package.json +1 -1
  31. package/editor/assets/index-CLoI_HF2.js +0 -134
@@ -1,4 +1,4 @@
1
- import { WorkingStore, IdentityProvider, PublishResult, ReconcileResult, AuthEndpoints, UserAdminEndpoints, AuthorOption, DiagnosticRegistry, UserSummary, AuthorDirectory, DiagnosticCheck, Repair } from '@nanoesis/engine';
1
+ import { WorkingStore, IdentityProvider, PublishResult, ReconcileResult, AuthEndpoints, UserAdminEndpoints, AuthorOption, DiagnosticRegistry, UserSummary, AuthorDirectory, Repair, DiagnosticCheck } from '@nanoesis/engine';
2
2
 
3
3
  /**
4
4
  * Editor white-labelling storage (DESIGN §11, Phase B). This is the *editor app's*
@@ -285,5 +285,42 @@ declare const recreateHomeTemplateRepair: Repair;
285
285
  * user picks what to publish; we only point out that nothing is.
286
286
  */
287
287
  declare const noPublishedContentDiagnostic: DiagnosticCheck;
288
+ /**
289
+ * Walk every published content item; for any pinned to a snapshot, check whether the
290
+ * snapshot file exists and parses (PROPOSAL §16.1, §16.2). Emits one finding per
291
+ * affected item — the missing-snapshot case (publish-blocking error) and the
292
+ * corrupt-snapshot case (publish-blocking error, distinct headline so the user knows
293
+ * "the file is there but the bytes are bad" vs "the file is gone"). Both repair via
294
+ * `templates.rebind-item-to-current`.
295
+ */
296
+ declare const templateSnapshotIntegrityDiagnostic: DiagnosticCheck;
297
+ /**
298
+ * Rebind an item's `template` field from `<name>@v<N>` to `<name>` (current). Used for
299
+ * the missing/corrupt snapshot repair: the item's pin is lost (the snapshot was the
300
+ * intent), but the item itself stays, and shows up in the pending-migrations list so
301
+ * its fields can be reconciled with current. Idempotent: a write of the same JSON is
302
+ * a no-op semantically.
303
+ */
304
+ declare const rebindItemToCurrentRepair: Repair;
305
+ /**
306
+ * Snapshot exists but its current sibling is missing (PROPOSAL §16.4). Someone deleted
307
+ * `templates/article.html` and left `templates/article@v1.html` behind. The Health page
308
+ * surfaces this with the `copy-snapshot-to-current` repair — promotes the snapshot to
309
+ * be the current.
310
+ */
311
+ declare const templateSuffixConflictDiagnostic: DiagnosticCheck;
312
+ /**
313
+ * Copy a snapshot's bytes to the current path (PROPOSAL §16.4 repair). The snapshot
314
+ * itself remains in place; the user can retire it manually later. Idempotent: if the
315
+ * current already exists, the repair is a no-op (so a stale UI re-firing the repair
316
+ * never silently overwrites real work).
317
+ */
318
+ declare const copySnapshotToCurrentRepair: Repair;
319
+ /**
320
+ * Count items whose current-template binding has orphan fields (PROPOSAL §10). Read-only
321
+ * summary — points the user at the Migrations workspace (Phase 2). No auto-repair, since
322
+ * resolution is per-item (drop / rename / keep) and needs human choice.
323
+ */
324
+ declare const pendingMigrationsDiagnostic: DiagnosticCheck;
288
325
 
289
- export { type ApiDeps, type ApiRequest, type ApiResponse, type BrandingLogo, type BrandingLogoMeta, type BrandingState, type BrandingStore, FileBrandingStore, InMemoryBrandingStore, MCP_RESOURCES, MCP_TOOLS, type McpCallOptions, type McpResourceDef, type McpToolDef, type McpToolResult, SCAFFOLD_FILES, authorDirectory, authorOptions, buildDefaultDiagnostics, callMcpTool, handleApi, homeTemplateMissingDiagnostic, noPublishedContentDiagnostic, readMcpResource, recreateHomeTemplateRepair };
326
+ export { type ApiDeps, type ApiRequest, type ApiResponse, type BrandingLogo, type BrandingLogoMeta, type BrandingState, type BrandingStore, FileBrandingStore, InMemoryBrandingStore, MCP_RESOURCES, MCP_TOOLS, type McpCallOptions, type McpResourceDef, type McpToolDef, type McpToolResult, SCAFFOLD_FILES, authorDirectory, authorOptions, buildDefaultDiagnostics, callMcpTool, copySnapshotToCurrentRepair, handleApi, homeTemplateMissingDiagnostic, noPublishedContentDiagnostic, pendingMigrationsDiagnostic, readMcpResource, rebindItemToCurrentRepair, recreateHomeTemplateRepair, templateSnapshotIntegrityDiagnostic, templateSuffixConflictDiagnostic };
@@ -1,16 +1,21 @@
1
1
  import {
2
+ analyzeTemplate,
3
+ applyMigration,
4
+ baseTemplateName,
2
5
  buildAuthoringReference,
3
6
  canEdit,
4
7
  contentTypeFor,
5
8
  createDiagnosticRegistry,
6
9
  deriveFields,
7
10
  hasRole,
11
+ isVersionedTemplateName,
8
12
  loadComponents,
9
13
  loadTemplate,
14
+ pendingMigrations,
10
15
  renderReferenceMarkdown,
11
16
  validateSite,
12
17
  workingStoreRoundTripDiagnostic
13
- } from "./chunk-J6VYOB47.js";
18
+ } from "./chunk-UZQ7UP2B.js";
14
19
 
15
20
  // ../editor-api/src/scaffold.ts
16
21
  var HOME_HTML = `<!doctype html>
@@ -254,6 +259,128 @@ async function handleDiagnosticsRoute(deps, req, caller) {
254
259
  return json(400, { ok: false, error: error instanceof Error ? error.message : String(error) });
255
260
  }
256
261
  }
262
+ async function handleMigrationsRoute(deps, req, caller) {
263
+ if (req.path === "/api/migrations") {
264
+ if (req.method !== "GET") return json(405, { ok: false, error: "method not allowed" });
265
+ const result = await pendingMigrations(deps.store);
266
+ return json(200, {
267
+ items: result.items,
268
+ byTemplate: Object.fromEntries(result.byTemplate)
269
+ });
270
+ }
271
+ if (req.path === "/api/migrations/preview") {
272
+ if (req.method !== "GET") return json(405, { ok: false, error: "method not allowed" });
273
+ const itemPath2 = req.query.get("path") ?? "";
274
+ if (itemPath2 === "") return json(400, { ok: false, error: "path query param required" });
275
+ return previewMigration(deps, itemPath2);
276
+ }
277
+ if (req.method !== "POST") return json(405, { ok: false, error: "method not allowed" });
278
+ void caller;
279
+ const body = await parseJsonBody(req);
280
+ if (!isObject(body) || typeof body["path"] !== "string" || !isObject(body["resolution"])) {
281
+ return json(400, { ok: false, error: "JSON body { path, resolution } required" });
282
+ }
283
+ const itemPath = body["path"];
284
+ const denied = denyWrite(caller, itemPath);
285
+ if (denied) return denied;
286
+ try {
287
+ await applyMigration(deps.store, itemPath, normalizeResolution(body["resolution"]));
288
+ const refreshed = await pendingMigrations(deps.store);
289
+ return json(200, {
290
+ ok: true,
291
+ items: refreshed.items,
292
+ byTemplate: Object.fromEntries(refreshed.byTemplate)
293
+ });
294
+ } catch (error) {
295
+ return json(400, { ok: false, error: error instanceof Error ? error.message : String(error) });
296
+ }
297
+ }
298
+ async function previewMigration(deps, itemPath) {
299
+ let itemJson;
300
+ try {
301
+ itemJson = JSON.parse(await deps.store.readText(itemPath));
302
+ } catch {
303
+ return json(404, { ok: false, error: "item not found" });
304
+ }
305
+ const tplName = typeof itemJson["template"] === "string" ? itemJson["template"] : "";
306
+ if (tplName === "") return json(400, { ok: false, error: "item has no template binding" });
307
+ const baseTpl = tplName.replace(/@v\d+$/, "");
308
+ const components = await loadComponents(deps.store).catch(() => /* @__PURE__ */ new Map());
309
+ const currentHtml = await safeReadTemplate(deps.store, baseTpl);
310
+ const currentFields = currentHtml === null ? [] : deriveFields(currentHtml, components).map((f) => f.name);
311
+ let leftTpl = null;
312
+ let leftHtml = null;
313
+ if (tplName !== baseTpl) {
314
+ leftTpl = tplName;
315
+ leftHtml = await safeReadTemplate(deps.store, tplName);
316
+ } else {
317
+ const { bestFitSnapshot: pickBestFit } = await import("./index.js");
318
+ const itemFields2 = Object.keys(
319
+ typeof itemJson["fields"] === "object" && itemJson["fields"] !== null ? itemJson["fields"] : {}
320
+ );
321
+ const best = await pickBestFit(deps.store, baseTpl, itemFields2);
322
+ if (best !== null) {
323
+ leftTpl = best;
324
+ leftHtml = await safeReadTemplate(deps.store, best);
325
+ }
326
+ }
327
+ const measureTpl = tplName === baseTpl ? baseTpl : tplName;
328
+ const measureHtml = tplName === baseTpl ? currentHtml : leftHtml;
329
+ const measureFields = measureHtml === null ? [] : deriveFields(measureHtml, components).map((f) => f.name);
330
+ const itemFields = typeof itemJson["fields"] === "object" && itemJson["fields"] !== null ? itemJson["fields"] : {};
331
+ const orphans = Object.keys(itemFields).filter((name) => !measureFields.includes(name)).map((name) => ({ name, value: itemFields[name] }));
332
+ void measureTpl;
333
+ return json(200, {
334
+ left: leftHtml === null ? null : { template: leftTpl, html: leftHtml },
335
+ right: { template: baseTpl, html: currentHtml ?? "" },
336
+ orphans,
337
+ missing: [],
338
+ currentTemplateFields: currentFields
339
+ });
340
+ }
341
+ async function safeReadTemplate(store, name) {
342
+ try {
343
+ return await store.readText(`templates/${name}.html`);
344
+ } catch {
345
+ return null;
346
+ }
347
+ }
348
+ function normalizeResolution(raw) {
349
+ const drop = Array.isArray(raw["drop"]) ? raw["drop"].filter((v) => typeof v === "string") : [];
350
+ const keep = Array.isArray(raw["keep"]) ? raw["keep"].filter((v) => typeof v === "string") : [];
351
+ const rename2 = {};
352
+ if (isObject(raw["rename"])) {
353
+ for (const [from, to] of Object.entries(raw["rename"])) {
354
+ if (typeof to === "string") rename2[from] = to;
355
+ }
356
+ }
357
+ const fill = {};
358
+ if (isObject(raw["fill"])) {
359
+ for (const [name, value] of Object.entries(raw["fill"])) {
360
+ if (typeof value === "string") fill[name] = value;
361
+ }
362
+ }
363
+ return { drop, rename: rename2, keep, fill };
364
+ }
365
+ async function handleStampRoute(deps, req, caller) {
366
+ if (req.method !== "POST") return json(405, { ok: false, error: "method not allowed" });
367
+ if (!hasRole(caller, "developer")) {
368
+ return json(403, { ok: false, error: "developer role required" });
369
+ }
370
+ const body = await parseJsonBody(req);
371
+ if (!isObject(body) || typeof body["name"] !== "string" || body["kind"] !== "template" && body["kind"] !== "component") {
372
+ return json(400, {
373
+ ok: false,
374
+ error: 'JSON body { name, kind: "template" | "component" } required'
375
+ });
376
+ }
377
+ try {
378
+ const record = await deps.store.stamp(body["name"], body["kind"]);
379
+ return json(200, record);
380
+ } catch (error) {
381
+ return json(400, { ok: false, error: error instanceof Error ? error.message : String(error) });
382
+ }
383
+ }
257
384
  async function handleBrandingRoute(deps, req) {
258
385
  if (req.path !== "/api/branding" && req.path !== "/api/branding/logo") return void 0;
259
386
  const store = deps.branding;
@@ -322,6 +449,12 @@ async function dispatchApi(deps, req) {
322
449
  if (req.path === "/api/admin/diagnostics" || req.path === "/api/admin/diagnostics/repair") {
323
450
  return handleDiagnosticsRoute(deps, req, principal);
324
451
  }
452
+ if (req.path === "/api/migrations" || req.path === "/api/migrations/preview" || req.path === "/api/migrations/apply") {
453
+ return handleMigrationsRoute(deps, req, principal);
454
+ }
455
+ if (req.path === "/api/templates/stamp") {
456
+ return handleStampRoute(deps, req, principal);
457
+ }
325
458
  const get = (key) => req.query.get(key) ?? "";
326
459
  switch (req.path) {
327
460
  case "/api/list": {
@@ -362,8 +495,12 @@ async function dispatchApi(deps, req) {
362
495
  const denied = denyWrite(principal, get("path"));
363
496
  if (denied) return denied;
364
497
  try {
365
- await deps.store.write(get("path"), await req.body());
366
- return json(200, { ok: true });
498
+ const result = await deps.store.write(get("path"), await req.body());
499
+ return json(200, {
500
+ ok: true,
501
+ ...result.stamped !== void 0 && { stamped: result.stamped },
502
+ ...result.stampIncomplete === true && { stampIncomplete: true }
503
+ });
367
504
  } catch (error) {
368
505
  return json(500, { ok: false, error: String(error) });
369
506
  }
@@ -610,8 +747,59 @@ var TOOL_SPECS = [
610
747
  description: "Compile and publish the whole site. Runs the validation gate first; if anything would break, it reports the problems and writes nothing.",
611
748
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
612
749
  toRequest: (_args, token) => request("POST", "/api/publish", {}, void 0, token)
750
+ },
751
+ {
752
+ name: "list_pending_migrations",
753
+ description: "List content items whose JSON fields don't match their bound template's schema (orphan fields or missing required fields). Use this after a destructive template edit (you'll see a `stamped` record on write_file) to see what needs migrating, or any time to check site-wide drift.",
754
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
755
+ toRequest: (_args, token) => request("GET", "/api/migrations", {}, void 0, token)
756
+ },
757
+ {
758
+ name: "preview_migration",
759
+ description: 'For one content item, return the current template HTML, the best-fit snapshot HTML (the "before"), the list of orphan fields with their values, and the names of fields the current template defines. Use this to plan how to migrate (drop, rename, or keep each orphan).',
760
+ inputSchema: {
761
+ type: "object",
762
+ properties: { path: pathArg },
763
+ required: ["path"],
764
+ additionalProperties: false
765
+ },
766
+ toRequest: (args, token) => request("GET", "/api/migrations/preview", { path: str(args, "path") }, void 0, token)
767
+ },
768
+ {
769
+ name: "apply_migration",
770
+ description: "Resolve a content item's schema drift in one go. `resolution` is a JSON object: `drop` (array of orphan field names to delete), `rename` (object mapping orphan field name \u2192 current template field name to copy the value into), `keep` (array of orphan field names to leave untouched), and `fill` (object mapping current template field name \u2192 value, for missing required fields). Returns the refreshed pending list.",
771
+ inputSchema: {
772
+ type: "object",
773
+ properties: {
774
+ path: pathArg,
775
+ resolution: {
776
+ type: "string",
777
+ description: 'A JSON object encoded as a string: { "drop": string[], "rename": { from: to }, "keep": string[], "fill": { name: value } }.'
778
+ }
779
+ },
780
+ required: ["path", "resolution"],
781
+ additionalProperties: false
782
+ },
783
+ toRequest: (args, token) => request(
784
+ "POST",
785
+ "/api/migrations/apply",
786
+ {},
787
+ JSON.stringify({
788
+ path: str(args, "path"),
789
+ resolution: parseResolutionArg(str(args, "resolution"))
790
+ }),
791
+ token
792
+ )
613
793
  }
614
794
  ];
795
+ function parseResolutionArg(raw) {
796
+ if (raw === "") return {};
797
+ try {
798
+ return JSON.parse(raw);
799
+ } catch {
800
+ return {};
801
+ }
802
+ }
615
803
  var MCP_TOOLS = TOOL_SPECS.map(
616
804
  ({ name, description, inputSchema }) => ({ name, description, inputSchema })
617
805
  );
@@ -646,7 +834,12 @@ function buildDefaultDiagnostics() {
646
834
  registry.add(workingStoreRoundTripDiagnostic);
647
835
  registry.add(homeTemplateMissingDiagnostic);
648
836
  registry.add(noPublishedContentDiagnostic);
837
+ registry.add(templateSnapshotIntegrityDiagnostic);
838
+ registry.add(templateSuffixConflictDiagnostic);
839
+ registry.add(pendingMigrationsDiagnostic);
649
840
  registry.addRepair(recreateHomeTemplateRepair);
841
+ registry.addRepair(rebindItemToCurrentRepair);
842
+ registry.addRepair(copySnapshotToCurrentRepair);
650
843
  return registry;
651
844
  }
652
845
  var HOME_PATH = "templates/home.html";
@@ -690,6 +883,197 @@ var noPublishedContentDiagnostic = {
690
883
  ];
691
884
  }
692
885
  };
886
+ var templateSnapshotIntegrityDiagnostic = {
887
+ id: "templates.snapshot-integrity",
888
+ async run({ store }) {
889
+ const findings = [];
890
+ const components = await loadComponents(store).catch(() => /* @__PURE__ */ new Map());
891
+ const items = await collectContentItems(store);
892
+ const snapshotCache = /* @__PURE__ */ new Map();
893
+ for (const item of items) {
894
+ if (!isVersionedTemplateName(item.template)) continue;
895
+ const kind = "template";
896
+ const path = `${kind === "template" ? "templates" : "components"}/${item.template}.html`;
897
+ let state = snapshotCache.get(path);
898
+ if (state === void 0) {
899
+ state = await classifySnapshot(store, path, components);
900
+ snapshotCache.set(path, state);
901
+ }
902
+ if (state === "ok") continue;
903
+ const isMissing = state === "missing";
904
+ findings.push({
905
+ id: `templates.${isMissing ? "missing" : "corrupt"}-snapshot:${item.path}`,
906
+ source: "templates",
907
+ severity: "error",
908
+ title: isMissing ? `Pinned snapshot missing: ${path}` : `Pinned snapshot will not parse: ${path}`,
909
+ detail: isMissing ? `${item.path} pins to ${item.template}, but ${path} does not exist. Restore the snapshot from git/backup, or rebind this item to the current template (its fields will need migration after).` : `${item.path} pins to ${item.template}, but ${path} cannot be parsed as a template. Restore valid bytes from git/backup, or rebind this item to the current template.`,
910
+ repair: {
911
+ id: "templates.rebind-item-to-current",
912
+ label: "Rebind item to current template",
913
+ args: { itemPath: item.path }
914
+ }
915
+ });
916
+ }
917
+ return findings;
918
+ }
919
+ };
920
+ var rebindItemToCurrentRepair = {
921
+ id: "templates.rebind-item-to-current",
922
+ async run({ store }, args) {
923
+ const itemPath = args["itemPath"];
924
+ if (typeof itemPath !== "string" || itemPath === "") {
925
+ throw new Error("rebind-item-to-current: missing itemPath arg");
926
+ }
927
+ const text = await store.readText(itemPath);
928
+ const data = JSON.parse(text);
929
+ if (typeof data !== "object" || data === null) {
930
+ throw new Error(`rebind-item-to-current: ${itemPath} is not a JSON object`);
931
+ }
932
+ const obj = data;
933
+ if (typeof obj.template !== "string" || !isVersionedTemplateName(obj.template)) {
934
+ return;
935
+ }
936
+ obj.template = baseTemplateName(obj.template);
937
+ const next = `${JSON.stringify(obj, null, 2)}
938
+ `;
939
+ await store.write(itemPath, new TextEncoder().encode(next));
940
+ }
941
+ };
942
+ var templateSuffixConflictDiagnostic = {
943
+ id: "templates.suffix-conflict",
944
+ async run({ store }) {
945
+ const findings = [];
946
+ for (const dir of ["templates", "components"]) {
947
+ let entries;
948
+ try {
949
+ entries = await store.list(dir);
950
+ } catch {
951
+ continue;
952
+ }
953
+ const seenBases = /* @__PURE__ */ new Set();
954
+ const snapshots = [];
955
+ for (const entry of entries) {
956
+ if (entry.kind !== "file" || !entry.name.endsWith(".html")) continue;
957
+ const stem = entry.name.slice(0, -".html".length);
958
+ if (isVersionedTemplateName(stem)) {
959
+ snapshots.push({ base: baseTemplateName(stem), snapshotPath: `${dir}/${entry.name}` });
960
+ } else {
961
+ seenBases.add(stem);
962
+ }
963
+ }
964
+ for (const snap of snapshots) {
965
+ if (seenBases.has(snap.base)) continue;
966
+ const currentPath = `${dir}/${snap.base}.html`;
967
+ findings.push({
968
+ id: `templates.suffix-conflict:${snap.snapshotPath}`,
969
+ source: "templates",
970
+ severity: "warning",
971
+ title: `Snapshot has no current sibling: ${snap.snapshotPath}`,
972
+ detail: `${snap.snapshotPath} exists but ${currentPath} does not. Promote the snapshot to current with "Copy snapshot to current", or rename the snapshot off the @v<N> suffix if it was named this way by mistake.`,
973
+ repair: {
974
+ id: "templates.copy-snapshot-to-current",
975
+ label: "Copy snapshot to current",
976
+ args: { snapshotPath: snap.snapshotPath, currentPath }
977
+ }
978
+ });
979
+ }
980
+ }
981
+ return findings;
982
+ }
983
+ };
984
+ var copySnapshotToCurrentRepair = {
985
+ id: "templates.copy-snapshot-to-current",
986
+ async run({ store }, args) {
987
+ const snapshotPath = args["snapshotPath"];
988
+ const currentPath = args["currentPath"];
989
+ if (typeof snapshotPath !== "string" || typeof currentPath !== "string") {
990
+ throw new Error("copy-snapshot-to-current: missing snapshotPath or currentPath args");
991
+ }
992
+ if (await store.exists(currentPath)) return;
993
+ const bytes = await store.readBytes(snapshotPath);
994
+ await store.write(currentPath, bytes);
995
+ }
996
+ };
997
+ var pendingMigrationsDiagnostic = {
998
+ id: "content.pending-migrations",
999
+ async run({ store }) {
1000
+ const components = await loadComponents(store).catch(() => /* @__PURE__ */ new Map());
1001
+ const items = await collectContentItems(store);
1002
+ const templateFieldCache = /* @__PURE__ */ new Map();
1003
+ let pendingCount = 0;
1004
+ for (const item of items) {
1005
+ if (isVersionedTemplateName(item.template)) continue;
1006
+ let expected = templateFieldCache.get(item.template);
1007
+ if (expected === void 0) {
1008
+ const path = `templates/${item.template}.html`;
1009
+ try {
1010
+ const html = await store.readText(path);
1011
+ expected = new Set(deriveFields(html, components).map((f) => f.name));
1012
+ } catch {
1013
+ expected = /* @__PURE__ */ new Set();
1014
+ }
1015
+ templateFieldCache.set(item.template, expected);
1016
+ }
1017
+ const orphans = Object.keys(item.fields).filter((k) => !expected.has(k));
1018
+ if (orphans.length > 0) pendingCount += 1;
1019
+ }
1020
+ if (pendingCount === 0) return [];
1021
+ return [
1022
+ {
1023
+ id: "content.pending-migrations",
1024
+ source: "content",
1025
+ severity: "warning",
1026
+ title: `${pendingCount} item${pendingCount === 1 ? "" : "s"} pending migration`,
1027
+ detail: `${pendingCount} content item${pendingCount === 1 ? "" : "s"} ${pendingCount === 1 ? "has" : "have"} fields not rendered by ${pendingCount === 1 ? "its" : "their"} current template. The Migrations workspace (shipping in Phase 2 of versioning) will resolve these; for now, edit the item JSON to drop or rename the orphan fields. The site continues to publish \u2014 drift is a soft warning, not a publish blocker.`
1028
+ }
1029
+ ];
1030
+ }
1031
+ };
1032
+ async function collectContentItems(store, dir = "content") {
1033
+ const out = [];
1034
+ let entries;
1035
+ try {
1036
+ entries = await store.list(dir);
1037
+ } catch {
1038
+ return out;
1039
+ }
1040
+ for (const entry of entries) {
1041
+ const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
1042
+ if (entry.kind === "dir") {
1043
+ out.push(...await collectContentItems(store, path));
1044
+ continue;
1045
+ }
1046
+ if (!entry.name.endsWith(".json") || entry.name.startsWith("_")) continue;
1047
+ let data;
1048
+ try {
1049
+ data = JSON.parse(await store.readText(path));
1050
+ } catch {
1051
+ continue;
1052
+ }
1053
+ if (typeof data !== "object" || data === null) continue;
1054
+ const obj = data;
1055
+ const template = obj["template"];
1056
+ if (typeof template !== "string") continue;
1057
+ const fields = obj["fields"];
1058
+ out.push({
1059
+ path,
1060
+ template,
1061
+ fields: typeof fields === "object" && fields !== null ? fields : {}
1062
+ });
1063
+ }
1064
+ return out;
1065
+ }
1066
+ async function classifySnapshot(store, path, components) {
1067
+ if (!await store.exists(path)) return "missing";
1068
+ try {
1069
+ const html = await store.readText(path);
1070
+ if (html.trim() === "") return "corrupt";
1071
+ analyzeTemplate(html, components);
1072
+ return "ok";
1073
+ } catch {
1074
+ return "corrupt";
1075
+ }
1076
+ }
693
1077
  async function anyPublishedContentItem(store, dir = "content") {
694
1078
  let entries;
695
1079
  try {
@@ -822,9 +1206,14 @@ export {
822
1206
  authorOptions,
823
1207
  buildDefaultDiagnostics,
824
1208
  callMcpTool,
1209
+ copySnapshotToCurrentRepair,
825
1210
  handleApi,
826
1211
  homeTemplateMissingDiagnostic,
827
1212
  noPublishedContentDiagnostic,
1213
+ pendingMigrationsDiagnostic,
828
1214
  readMcpResource,
829
- recreateHomeTemplateRepair
1215
+ rebindItemToCurrentRepair,
1216
+ recreateHomeTemplateRepair,
1217
+ templateSnapshotIntegrityDiagnostic,
1218
+ templateSuffixConflictDiagnostic
830
1219
  };