@xtrable-ltd/nanoesis 0.1.12 → 0.1.14

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 (29) hide show
  1. package/dist/adapter-azure-blob.js +1 -1
  2. package/dist/{chunk-XO3CT6GL.js → chunk-CBLBQP27.js} +275 -220
  3. package/dist/{chunk-EN5SMEWJ.js → chunk-PKCYTOHW.js} +6 -6
  4. package/dist/editor-api.js +2 -2
  5. package/dist/index.d.ts +50 -12
  6. package/dist/index.js +1 -1
  7. package/dist/mcp.js +3 -3
  8. package/editor/assets/MigrationsPane-DSiYzhDN.js +4 -0
  9. package/editor/assets/{TemplatesPane-B5hn_v0Z.js → TemplatesPane-DacDItlh.js} +172 -168
  10. package/editor/assets/{cssMode-BbIf5k6I.js → cssMode-CiAvvnrb.js} +1 -1
  11. package/editor/assets/{freemarker2-DoW0pSYV.js → freemarker2-C7G3JwfA.js} +1 -1
  12. package/editor/assets/{handlebars-DLlET-qc.js → handlebars-DsJPq-J8.js} +1 -1
  13. package/editor/assets/{html-4khbqrhe.js → html-i_wgtIMk.js} +1 -1
  14. package/editor/assets/{htmlMode-DblHkZ-k.js → htmlMode-Bx_dmHBj.js} +1 -1
  15. package/editor/assets/index-CLUiygGJ.js +138 -0
  16. package/editor/assets/{javascript-CgPO2Hmj.js → javascript-C3n1eUNo.js} +1 -1
  17. package/editor/assets/{jsonMode-BrWh2436.js → jsonMode-BpC5c5px.js} +1 -1
  18. package/editor/assets/{liquid-BsQJXwPT.js → liquid-B3_Yp2NE.js} +1 -1
  19. package/editor/assets/{mdx-AO8t67gx.js → mdx-nOh_vCzd.js} +1 -1
  20. package/editor/assets/{python-3w4sZj5c.js → python-BZXwPif_.js} +1 -1
  21. package/editor/assets/{razor-BFsvo06w.js → razor-BYwO2G_q.js} +1 -1
  22. package/editor/assets/{tsMode-QrC4ERjp.js → tsMode-CmNOLqvJ.js} +1 -1
  23. package/editor/assets/{typescript-BXJ3QLad.js → typescript-C9KU4rft.js} +1 -1
  24. package/editor/assets/{xml-CxKYn1FP.js → xml-CFRBv758.js} +1 -1
  25. package/editor/assets/{yaml-BmWLvF7Q.js → yaml-Co0yIk6T.js} +1 -1
  26. package/editor/index.html +1 -1
  27. package/package.json +1 -1
  28. package/editor/assets/MigrationsPane-BYGqWBAA.js +0 -4
  29. package/editor/assets/index-Do1drqEQ.js +0 -138
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  contentTypeFor
3
- } from "./chunk-XO3CT6GL.js";
3
+ } from "./chunk-CBLBQP27.js";
4
4
 
5
5
  // ../../adapters/azure-blob/src/container.ts
6
6
  var InMemoryBlobContainer = class {
@@ -43,6 +43,15 @@ var ContentParseError = class extends Error {
43
43
  this.name = "ContentParseError";
44
44
  }
45
45
  };
46
+ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
47
+ "template",
48
+ "title",
49
+ "isPublished",
50
+ "fields",
51
+ "created",
52
+ "published",
53
+ "updated"
54
+ ]);
46
55
  function isPlainObject(value) {
47
56
  return typeof value === "object" && value !== null && !Array.isArray(value);
48
57
  }
@@ -72,6 +81,9 @@ function parseOptionalString(raw, key) {
72
81
  return value;
73
82
  }
74
83
  function parseContentItem(raw) {
84
+ return parseContentItemWithDiagnostics(raw).item;
85
+ }
86
+ function parseContentItemWithDiagnostics(raw) {
75
87
  if (!isPlainObject(raw)) {
76
88
  throw new ContentParseError("A content item must be a JSON object.");
77
89
  }
@@ -100,7 +112,8 @@ function parseContentItem(raw) {
100
112
  const created = parseOptionalString(raw, "created");
101
113
  const published = parseOptionalString(raw, "published");
102
114
  const updated = parseOptionalString(raw, "updated");
103
- return {
115
+ const unknownTopLevelKeys = Object.keys(raw).filter((key) => !KNOWN_TOP_LEVEL_KEYS.has(key)).sort();
116
+ const item = {
104
117
  title: raw.title,
105
118
  isPublished,
106
119
  fields,
@@ -109,6 +122,7 @@ function parseContentItem(raw) {
109
122
  ...published !== void 0 && { published },
110
123
  ...updated !== void 0 && { updated }
111
124
  };
125
+ return { item, diagnostics: { unknownTopLevelKeys } };
112
126
  }
113
127
 
114
128
  // ../engine/src/content/sort.ts
@@ -154,6 +168,218 @@ function copy(bytes) {
154
168
  return bytes.slice();
155
169
  }
156
170
 
171
+ // ../engine/src/url/redirects.ts
172
+ var DEFAULT_STATUS = 301;
173
+ var OUTPUT_PATH = "_redirects";
174
+ function isRule(value) {
175
+ if (typeof value !== "object" || value === null) return false;
176
+ const record = value;
177
+ return typeof record.from === "string" && typeof record.to === "string";
178
+ }
179
+ function parseRedirects(raw) {
180
+ if (!Array.isArray(raw)) return [];
181
+ const rules = [];
182
+ for (const entry of raw) {
183
+ if (!isRule(entry)) continue;
184
+ const status = entry.status;
185
+ rules.push(
186
+ typeof status === "number" && Number.isInteger(status) ? { from: entry.from, to: entry.to, status } : { from: entry.from, to: entry.to }
187
+ );
188
+ }
189
+ return rules;
190
+ }
191
+ function resolveTarget(from, exact) {
192
+ const seen = /* @__PURE__ */ new Set([from]);
193
+ let target = exact.get(from).to;
194
+ while (exact.has(target) && !seen.has(target)) {
195
+ seen.add(target);
196
+ target = exact.get(target).to;
197
+ }
198
+ return target;
199
+ }
200
+ function buildRedirects(rules, liveUrls) {
201
+ const exact = /* @__PURE__ */ new Map();
202
+ const wildcard = /* @__PURE__ */ new Map();
203
+ for (const rule of rules) {
204
+ (rule.from.includes("*") ? wildcard : exact).set(rule.from, rule);
205
+ }
206
+ const resolved = [];
207
+ for (const [from, rule] of exact) {
208
+ if (liveUrls.has(from)) continue;
209
+ const to = resolveTarget(from, exact);
210
+ if (to === from) continue;
211
+ resolved.push(rule.status !== void 0 ? { from, to, status: rule.status } : { from, to });
212
+ }
213
+ resolved.push(...wildcard.values());
214
+ if (resolved.length === 0) return void 0;
215
+ resolved.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
216
+ const contents = resolved.map((rule) => `${rule.from} ${rule.to} ${rule.status ?? DEFAULT_STATUS}
217
+ `).join("");
218
+ return { path: OUTPUT_PATH, contents };
219
+ }
220
+
221
+ // ../engine/src/content/loader.ts
222
+ var SORT_FILE = "_sort.json";
223
+ var REDIRECTS_FILE = "_redirects.json";
224
+ var SITE_CONFIG_FILE = "_site.json";
225
+ var ASSETS_DIR = "assets";
226
+ var ITEM_EXT = ".json";
227
+ var DEFAULT_DIRS = {
228
+ content: "content",
229
+ templates: "templates",
230
+ components: "components",
231
+ /** Static passthrough, copied verbatim to the published root (DESIGN §8). The
232
+ * engine never reads it; named here so hosts/editor share one source of truth. */
233
+ public: "public"
234
+ };
235
+ function join(...parts) {
236
+ return parts.filter((part) => part !== "").join("/");
237
+ }
238
+ function stripBom(text) {
239
+ return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
240
+ }
241
+ async function loadContentTree(source, contentDir = DEFAULT_DIRS.content) {
242
+ return loadDir(source, contentDir, "", "");
243
+ }
244
+ async function loadDir(source, dirPath, slug, treePath) {
245
+ const [entries, sort] = await Promise.all([source.list(dirPath), readSort(source, dirPath)]);
246
+ const childMap = /* @__PURE__ */ new Map();
247
+ for (const entry of entries) {
248
+ if (entry.kind === "file") {
249
+ if (entry.name === SORT_FILE || entry.name === REDIRECTS_FILE || entry.name === SITE_CONFIG_FILE || !entry.name.endsWith(ITEM_EXT))
250
+ continue;
251
+ const childSlug = entry.name.slice(0, -ITEM_EXT.length);
252
+ childMap.set(
253
+ childSlug,
254
+ await loadItem(source, join(dirPath, entry.name), childSlug, treePath)
255
+ );
256
+ } else {
257
+ if (entry.name === ASSETS_DIR) continue;
258
+ const childPath = join(treePath, entry.name);
259
+ childMap.set(
260
+ entry.name,
261
+ await loadDir(source, join(dirPath, entry.name), entry.name, childPath)
262
+ );
263
+ }
264
+ }
265
+ const name = sort.name ?? (slug === "" ? "" : humanize(slug));
266
+ return {
267
+ kind: "dir",
268
+ slug,
269
+ name,
270
+ path: treePath,
271
+ children: orderChildren(childMap, sort.order),
272
+ ...sort.collection !== void 0 && { collection: sort.collection },
273
+ ...sort.defaultTemplate !== void 0 && { defaultTemplate: sort.defaultTemplate }
274
+ };
275
+ }
276
+ async function loadItem(source, filePath, slug, parentPath2) {
277
+ const raw = await source.readText(filePath);
278
+ try {
279
+ const { item, diagnostics } = parseContentItemWithDiagnostics(JSON.parse(raw));
280
+ return {
281
+ kind: "item",
282
+ slug,
283
+ path: join(parentPath2, slug),
284
+ item,
285
+ ...diagnostics.unknownTopLevelKeys.length > 0 && { parseDiagnostics: diagnostics }
286
+ };
287
+ } catch (error) {
288
+ throw new Error(`Failed to load ${filePath}: ${error.message}`);
289
+ }
290
+ }
291
+ async function readSort(source, dirPath) {
292
+ const path = join(dirPath, SORT_FILE);
293
+ if (!await source.exists(path)) return { order: [] };
294
+ try {
295
+ return parseSortFile(JSON.parse(await source.readText(path)));
296
+ } catch {
297
+ return { order: [] };
298
+ }
299
+ }
300
+ async function loadRedirects(source, contentDir = DEFAULT_DIRS.content) {
301
+ const path = join(contentDir, REDIRECTS_FILE);
302
+ if (!await source.exists(path)) return [];
303
+ try {
304
+ return parseRedirects(JSON.parse(stripBom(await source.readText(path))));
305
+ } catch {
306
+ return [];
307
+ }
308
+ }
309
+ async function loadSiteConfig(source, contentDir = DEFAULT_DIRS.content) {
310
+ const path = join(contentDir, SITE_CONFIG_FILE);
311
+ if (!await source.exists(path)) return {};
312
+ try {
313
+ const raw = JSON.parse(stripBom(await source.readText(path)));
314
+ const baseUrl = typeof raw.baseUrl === "string" ? raw.baseUrl.trim() : "";
315
+ return baseUrl !== "" ? { baseUrl } : {};
316
+ } catch {
317
+ return {};
318
+ }
319
+ }
320
+ function orderChildren(map, order) {
321
+ const out = [];
322
+ const used = /* @__PURE__ */ new Set();
323
+ for (const slug of order) {
324
+ const node = map.get(slug);
325
+ if (node !== void 0) {
326
+ out.push(node);
327
+ used.add(slug);
328
+ }
329
+ }
330
+ for (const [slug, node] of map) {
331
+ if (!used.has(slug)) out.push(node);
332
+ }
333
+ return out;
334
+ }
335
+ function loadComponents(source, componentsDir = DEFAULT_DIRS.components) {
336
+ return loadComponentFiles(source, componentsDir, "html");
337
+ }
338
+ function loadComponentStyles(source, componentsDir = DEFAULT_DIRS.components) {
339
+ return loadComponentFiles(source, componentsDir, "css");
340
+ }
341
+ function loadComponentScripts(source, componentsDir = DEFAULT_DIRS.components) {
342
+ return loadComponentFiles(source, componentsDir, "js");
343
+ }
344
+ async function loadComponentFiles(source, componentsDir, extension) {
345
+ const suffix = `.${extension}`;
346
+ const map = /* @__PURE__ */ new Map();
347
+ const walk = async (dir) => {
348
+ if (!await source.exists(dir)) return;
349
+ for (const entry of await source.list(dir)) {
350
+ const full = join(dir, entry.name);
351
+ if (entry.kind === "dir") {
352
+ await walk(full);
353
+ } else if (entry.name.endsWith(suffix)) {
354
+ const key = entry.name.slice(0, -suffix.length).toLowerCase();
355
+ map.set(key, await source.readText(full));
356
+ }
357
+ }
358
+ };
359
+ await walk(componentsDir);
360
+ return map;
361
+ }
362
+ async function loadTemplate(source, name, templatesDir = DEFAULT_DIRS.templates) {
363
+ return source.readText(join(templatesDir, `${name}.html`));
364
+ }
365
+ function loadTemplateStyle(source, name, templatesDir = DEFAULT_DIRS.templates) {
366
+ return tryReadText(source, join(templatesDir, `${name}.css`));
367
+ }
368
+ function loadTemplateScript(source, name, templatesDir = DEFAULT_DIRS.templates) {
369
+ return tryReadText(source, join(templatesDir, `${name}.js`));
370
+ }
371
+ var DOCUMENT_SHELL = "document";
372
+ function loadDocumentShell(source, templatesDir = DEFAULT_DIRS.templates) {
373
+ return tryReadText(source, join(templatesDir, `${DOCUMENT_SHELL}.html`));
374
+ }
375
+ async function tryReadText(source, path) {
376
+ try {
377
+ return await source.readText(path);
378
+ } catch {
379
+ return void 0;
380
+ }
381
+ }
382
+
157
383
  // ../engine/src/html/dom.ts
158
384
  import { parse, parseFragment, serialize } from "parse5";
159
385
  function isElement(node) {
@@ -776,10 +1002,15 @@ var IndexedStore = class {
776
1002
  }
777
1003
  }
778
1004
  this.index = nextIndex;
1005
+ let parseDiagnostics;
1006
+ if (isContentItemPath(target)) {
1007
+ parseDiagnostics = contentParseDiagnosticsFor(bytes);
1008
+ }
779
1009
  return {
780
1010
  ...stamped !== void 0 && { stamped },
781
1011
  ...stampIncomplete === true && { stampIncomplete: true },
782
- ...schemaDelta !== void 0 && { schemaDelta }
1012
+ ...schemaDelta !== void 0 && { schemaDelta },
1013
+ ...parseDiagnostics !== void 0 && { parseDiagnostics }
783
1014
  };
784
1015
  });
785
1016
  }
@@ -895,8 +1126,23 @@ function stampTargetOf(target) {
895
1126
  if (dir === void 0) return void 0;
896
1127
  const stem = target.slice(dir.length + 1, -".html".length);
897
1128
  if (stem === "" || stem.includes("@v")) return void 0;
1129
+ if (dir === "templates" && stem === DOCUMENT_SHELL) return void 0;
898
1130
  return { dir, name: stem };
899
1131
  }
1132
+ function isContentItemPath(target) {
1133
+ if (!target.startsWith("content/") || !target.endsWith(".json")) return false;
1134
+ const tail = target.slice(target.lastIndexOf("/") + 1);
1135
+ return tail !== "_sort.json" && tail !== "_redirects.json" && tail !== "_site.json";
1136
+ }
1137
+ function contentParseDiagnosticsFor(bytes) {
1138
+ try {
1139
+ const text = new TextDecoder().decode(bytes);
1140
+ const { diagnostics } = parseContentItemWithDiagnostics(JSON.parse(text));
1141
+ return diagnostics.unknownTopLevelKeys.length > 0 ? diagnostics : void 0;
1142
+ } catch {
1143
+ return void 0;
1144
+ }
1145
+ }
900
1146
  function guarded(key) {
901
1147
  if (key === "" || key.startsWith(RESERVED_PREFIX)) {
902
1148
  throw new Error(`Refusing to mutate a reserved key: ${key === "" ? "(root)" : key}`);
@@ -973,212 +1219,6 @@ var InMemoryContentSource = class {
973
1219
  }
974
1220
  };
975
1221
 
976
- // ../engine/src/url/redirects.ts
977
- var DEFAULT_STATUS = 301;
978
- var OUTPUT_PATH = "_redirects";
979
- function isRule(value) {
980
- if (typeof value !== "object" || value === null) return false;
981
- const record = value;
982
- return typeof record.from === "string" && typeof record.to === "string";
983
- }
984
- function parseRedirects(raw) {
985
- if (!Array.isArray(raw)) return [];
986
- const rules = [];
987
- for (const entry of raw) {
988
- if (!isRule(entry)) continue;
989
- const status = entry.status;
990
- rules.push(
991
- typeof status === "number" && Number.isInteger(status) ? { from: entry.from, to: entry.to, status } : { from: entry.from, to: entry.to }
992
- );
993
- }
994
- return rules;
995
- }
996
- function resolveTarget(from, exact) {
997
- const seen = /* @__PURE__ */ new Set([from]);
998
- let target = exact.get(from).to;
999
- while (exact.has(target) && !seen.has(target)) {
1000
- seen.add(target);
1001
- target = exact.get(target).to;
1002
- }
1003
- return target;
1004
- }
1005
- function buildRedirects(rules, liveUrls) {
1006
- const exact = /* @__PURE__ */ new Map();
1007
- const wildcard = /* @__PURE__ */ new Map();
1008
- for (const rule of rules) {
1009
- (rule.from.includes("*") ? wildcard : exact).set(rule.from, rule);
1010
- }
1011
- const resolved = [];
1012
- for (const [from, rule] of exact) {
1013
- if (liveUrls.has(from)) continue;
1014
- const to = resolveTarget(from, exact);
1015
- if (to === from) continue;
1016
- resolved.push(rule.status !== void 0 ? { from, to, status: rule.status } : { from, to });
1017
- }
1018
- resolved.push(...wildcard.values());
1019
- if (resolved.length === 0) return void 0;
1020
- resolved.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
1021
- const contents = resolved.map((rule) => `${rule.from} ${rule.to} ${rule.status ?? DEFAULT_STATUS}
1022
- `).join("");
1023
- return { path: OUTPUT_PATH, contents };
1024
- }
1025
-
1026
- // ../engine/src/content/loader.ts
1027
- var SORT_FILE = "_sort.json";
1028
- var REDIRECTS_FILE = "_redirects.json";
1029
- var SITE_CONFIG_FILE = "_site.json";
1030
- var ASSETS_DIR = "assets";
1031
- var ITEM_EXT = ".json";
1032
- var DEFAULT_DIRS = {
1033
- content: "content",
1034
- templates: "templates",
1035
- components: "components",
1036
- /** Static passthrough, copied verbatim to the published root (DESIGN §8). The
1037
- * engine never reads it; named here so hosts/editor share one source of truth. */
1038
- public: "public"
1039
- };
1040
- function join(...parts) {
1041
- return parts.filter((part) => part !== "").join("/");
1042
- }
1043
- function stripBom(text) {
1044
- return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
1045
- }
1046
- async function loadContentTree(source, contentDir = DEFAULT_DIRS.content) {
1047
- return loadDir(source, contentDir, "", "");
1048
- }
1049
- async function loadDir(source, dirPath, slug, treePath) {
1050
- const [entries, sort] = await Promise.all([source.list(dirPath), readSort(source, dirPath)]);
1051
- const childMap = /* @__PURE__ */ new Map();
1052
- for (const entry of entries) {
1053
- if (entry.kind === "file") {
1054
- if (entry.name === SORT_FILE || entry.name === REDIRECTS_FILE || entry.name === SITE_CONFIG_FILE || !entry.name.endsWith(ITEM_EXT))
1055
- continue;
1056
- const childSlug = entry.name.slice(0, -ITEM_EXT.length);
1057
- childMap.set(
1058
- childSlug,
1059
- await loadItem(source, join(dirPath, entry.name), childSlug, treePath)
1060
- );
1061
- } else {
1062
- if (entry.name === ASSETS_DIR) continue;
1063
- const childPath = join(treePath, entry.name);
1064
- childMap.set(
1065
- entry.name,
1066
- await loadDir(source, join(dirPath, entry.name), entry.name, childPath)
1067
- );
1068
- }
1069
- }
1070
- const name = sort.name ?? (slug === "" ? "" : humanize(slug));
1071
- return {
1072
- kind: "dir",
1073
- slug,
1074
- name,
1075
- path: treePath,
1076
- children: orderChildren(childMap, sort.order),
1077
- ...sort.collection !== void 0 && { collection: sort.collection },
1078
- ...sort.defaultTemplate !== void 0 && { defaultTemplate: sort.defaultTemplate }
1079
- };
1080
- }
1081
- async function loadItem(source, filePath, slug, parentPath2) {
1082
- const raw = await source.readText(filePath);
1083
- try {
1084
- const item = parseContentItem(JSON.parse(raw));
1085
- return { kind: "item", slug, path: join(parentPath2, slug), item };
1086
- } catch (error) {
1087
- throw new Error(`Failed to load ${filePath}: ${error.message}`);
1088
- }
1089
- }
1090
- async function readSort(source, dirPath) {
1091
- const path = join(dirPath, SORT_FILE);
1092
- if (!await source.exists(path)) return { order: [] };
1093
- try {
1094
- return parseSortFile(JSON.parse(await source.readText(path)));
1095
- } catch {
1096
- return { order: [] };
1097
- }
1098
- }
1099
- async function loadRedirects(source, contentDir = DEFAULT_DIRS.content) {
1100
- const path = join(contentDir, REDIRECTS_FILE);
1101
- if (!await source.exists(path)) return [];
1102
- try {
1103
- return parseRedirects(JSON.parse(stripBom(await source.readText(path))));
1104
- } catch {
1105
- return [];
1106
- }
1107
- }
1108
- async function loadSiteConfig(source, contentDir = DEFAULT_DIRS.content) {
1109
- const path = join(contentDir, SITE_CONFIG_FILE);
1110
- if (!await source.exists(path)) return {};
1111
- try {
1112
- const raw = JSON.parse(stripBom(await source.readText(path)));
1113
- const baseUrl = typeof raw.baseUrl === "string" ? raw.baseUrl.trim() : "";
1114
- return baseUrl !== "" ? { baseUrl } : {};
1115
- } catch {
1116
- return {};
1117
- }
1118
- }
1119
- function orderChildren(map, order) {
1120
- const out = [];
1121
- const used = /* @__PURE__ */ new Set();
1122
- for (const slug of order) {
1123
- const node = map.get(slug);
1124
- if (node !== void 0) {
1125
- out.push(node);
1126
- used.add(slug);
1127
- }
1128
- }
1129
- for (const [slug, node] of map) {
1130
- if (!used.has(slug)) out.push(node);
1131
- }
1132
- return out;
1133
- }
1134
- function loadComponents(source, componentsDir = DEFAULT_DIRS.components) {
1135
- return loadComponentFiles(source, componentsDir, "html");
1136
- }
1137
- function loadComponentStyles(source, componentsDir = DEFAULT_DIRS.components) {
1138
- return loadComponentFiles(source, componentsDir, "css");
1139
- }
1140
- function loadComponentScripts(source, componentsDir = DEFAULT_DIRS.components) {
1141
- return loadComponentFiles(source, componentsDir, "js");
1142
- }
1143
- async function loadComponentFiles(source, componentsDir, extension) {
1144
- const suffix = `.${extension}`;
1145
- const map = /* @__PURE__ */ new Map();
1146
- const walk = async (dir) => {
1147
- if (!await source.exists(dir)) return;
1148
- for (const entry of await source.list(dir)) {
1149
- const full = join(dir, entry.name);
1150
- if (entry.kind === "dir") {
1151
- await walk(full);
1152
- } else if (entry.name.endsWith(suffix)) {
1153
- const key = entry.name.slice(0, -suffix.length).toLowerCase();
1154
- map.set(key, await source.readText(full));
1155
- }
1156
- }
1157
- };
1158
- await walk(componentsDir);
1159
- return map;
1160
- }
1161
- async function loadTemplate(source, name, templatesDir = DEFAULT_DIRS.templates) {
1162
- return source.readText(join(templatesDir, `${name}.html`));
1163
- }
1164
- function loadTemplateStyle(source, name, templatesDir = DEFAULT_DIRS.templates) {
1165
- return tryReadText(source, join(templatesDir, `${name}.css`));
1166
- }
1167
- function loadTemplateScript(source, name, templatesDir = DEFAULT_DIRS.templates) {
1168
- return tryReadText(source, join(templatesDir, `${name}.js`));
1169
- }
1170
- var DOCUMENT_SHELL = "document";
1171
- function loadDocumentShell(source, templatesDir = DEFAULT_DIRS.templates) {
1172
- return tryReadText(source, join(templatesDir, `${DOCUMENT_SHELL}.html`));
1173
- }
1174
- async function tryReadText(source, path) {
1175
- try {
1176
- return await source.readText(path);
1177
- } catch {
1178
- return void 0;
1179
- }
1180
- }
1181
-
1182
1222
  // ../engine/src/url/references.ts
1183
1223
  var REFERENCE_PREFIX = "ref:";
1184
1224
  function referenceTarget(value) {
@@ -2598,6 +2638,12 @@ function guardrailsSection() {
2598
2638
  {
2599
2639
  name: "richtext values are HTML, shorttext/text are plain text",
2600
2640
  summary: "For a `richtext` field, write the value as HTML (`<p>...</p>` etc). For `shorttext` or `text`, write plain text \u2014 any HTML you include is escaped at render time. If the gate emits a `content.type-drift` warning, a field's type changed destructively (commonly richtext \u2192 shorttext) and the old HTML content now renders as escaped text."
2641
+ },
2642
+ {
2643
+ name: "Content fields nest under `fields`, not at the top level",
2644
+ summary: "A content/*.json item has system keys at the top level (`template`, `title`, `isPublished`, `created`, `published`, `updated`) and ALL template-specific values under `fields`. Writing `{ template, title, body, tagline }` at the top level silently drops `body`/`tagline` because the parser is tolerant of unknown keys; the page publishes with empty placeholders. After write_file, check the response for `parseDiagnostics.unknownTopLevelKeys` and the gate for `content.unknown-top-level-key` \u2014 both list what was dropped.",
2645
+ detail: 'System keys are case-sensitive: `IsPublished` is not the same as `isPublished` (the typo silently defaults to false), and `Fields` is not `fields`. The same drop-and-flag applies. The published HTML for a tolerated-drift item will render with empty `<div class="prose"></div>` etc., which is the smoking-gun symptom.',
2646
+ example: '<!-- WRONG (body and tagline silently dropped) -->\n{ "template": "about", "title": "Hi", "body": "...", "tagline": "..." }\n\n<!-- RIGHT -->\n{ "template": "about", "title": "Hi", "fields": { "title": "Hi", "body": "...", "tagline": "..." } }'
2601
2647
  }
2602
2648
  ]
2603
2649
  };
@@ -2780,6 +2826,15 @@ async function validateSite(source) {
2780
2826
  const owners = urlOwners.get(urlForItem(node.path)) ?? [];
2781
2827
  owners.push(node.path);
2782
2828
  urlOwners.set(urlForItem(node.path), owners);
2829
+ const dropped = node.parseDiagnostics?.unknownTopLevelKeys ?? [];
2830
+ if (dropped.length > 0) {
2831
+ add(
2832
+ "warning",
2833
+ "content.unknown-top-level-key",
2834
+ `"${node.path}" has top-level key${dropped.length === 1 ? "" : "s"} the parser does not recognise: ${dropped.map((k) => `"${k}"`).join(", ")}. They were dropped \u2014 content values belong under "fields" (and system keys are case-sensitive: "isPublished", not "IsPublished").`,
2835
+ node.path
2836
+ );
2837
+ }
2783
2838
  if (templateName === void 0) {
2784
2839
  add(
2785
2840
  "error",
@@ -3253,6 +3308,18 @@ export {
3253
3308
  parseContentItem,
3254
3309
  parseSortFile,
3255
3310
  InMemoryBlobStore,
3311
+ parseRedirects,
3312
+ buildRedirects,
3313
+ DEFAULT_DIRS,
3314
+ loadContentTree,
3315
+ loadRedirects,
3316
+ loadSiteConfig,
3317
+ loadComponents,
3318
+ loadComponentStyles,
3319
+ loadComponentScripts,
3320
+ loadTemplate,
3321
+ DOCUMENT_SHELL,
3322
+ loadDocumentShell,
3256
3323
  FIELD_TYPES,
3257
3324
  isFieldType,
3258
3325
  valueKindOf,
@@ -3276,18 +3343,6 @@ export {
3276
3343
  reconcileIndex,
3277
3344
  IndexedStore,
3278
3345
  InMemoryContentSource,
3279
- parseRedirects,
3280
- buildRedirects,
3281
- DEFAULT_DIRS,
3282
- loadContentTree,
3283
- loadRedirects,
3284
- loadSiteConfig,
3285
- loadComponents,
3286
- loadComponentStyles,
3287
- loadComponentScripts,
3288
- loadTemplate,
3289
- DOCUMENT_SHELL,
3290
- loadDocumentShell,
3291
3346
  analyzeTemplate,
3292
3347
  pendingMigrations,
3293
3348
  bestFitSnapshot,
@@ -15,7 +15,7 @@ import {
15
15
  renderReferenceMarkdown,
16
16
  validateSite,
17
17
  workingStoreRoundTripDiagnostic
18
- } from "./chunk-XO3CT6GL.js";
18
+ } from "./chunk-CBLBQP27.js";
19
19
 
20
20
  // ../editor-api/src/scaffold.ts
21
21
  var HOME_HTML = `<!doctype html>
@@ -500,7 +500,10 @@ async function dispatchApi(deps, req) {
500
500
  ok: true,
501
501
  ...result.stamped !== void 0 && { stamped: result.stamped },
502
502
  ...result.stampIncomplete === true && { stampIncomplete: true },
503
- ...result.schemaDelta !== void 0 && { schemaDelta: result.schemaDelta }
503
+ ...result.schemaDelta !== void 0 && { schemaDelta: result.schemaDelta },
504
+ ...result.parseDiagnostics !== void 0 && {
505
+ parseDiagnostics: result.parseDiagnostics
506
+ }
504
507
  });
505
508
  } catch (error) {
506
509
  return json(500, { ok: false, error: String(error) });
@@ -567,9 +570,6 @@ async function dispatchApi(deps, req) {
567
570
  if (deps.reconcile === void 0) {
568
571
  return json(404, { ok: false, error: "reconcile is not available on this host" });
569
572
  }
570
- if (!hasRole(principal, "admin")) {
571
- return json(403, { ok: false, error: "admin role required" });
572
- }
573
573
  const result = await deps.reconcile();
574
574
  return json(200, {
575
575
  ok: true,
@@ -694,7 +694,7 @@ var TOOL_SPECS = [
694
694
  },
695
695
  {
696
696
  name: "write_file",
697
- description: "Create or overwrite a text file. content/ requires the author role; templates/, components/, and public/ require the developer role. Read the nanoesis://reference resource first for the authoring syntax. For template/component paths the response includes `schemaDelta` (added/removed/typeChanged fields) \u2014 if `typeChanged` contains `destructive: true` or `removed` is non-empty, an auto-stamp has been written and authored content may need migration, so surface to the user before further edits. See the reference's 'Authoring guardrails for LLMs' section for the common pitfalls (notably: `data-*` annotations bind to the element, not the token inside it \u2014 moving a token out of its annotated container silently strips them).",
697
+ description: "Create or overwrite a text file. content/ requires the author role; templates/, components/, and public/ require the developer role. Read the nanoesis://reference resource first for the authoring syntax. For template/component paths the response includes `schemaDelta` (added/removed/typeChanged fields) \u2014 if `typeChanged` contains `destructive: true` or `removed` is non-empty, an auto-stamp has been written and authored content may need migration, so surface to the user before further edits. For content/*.json writes the response may include `parseDiagnostics.unknownTopLevelKeys` \u2014 top-level keys the parser dropped because they are not system keys; content values belong under `fields` (system keys are case-sensitive: `isPublished`, not `IsPublished`). See the reference's 'Authoring guardrails for LLMs' section for the common pitfalls (notably: `data-*` annotations bind to the element, not the token inside it \u2014 moving a token out of its annotated container silently strips them).",
698
698
  inputSchema: {
699
699
  type: "object",
700
700
  properties: {
@@ -18,8 +18,8 @@ import {
18
18
  recreateHomeTemplateRepair,
19
19
  templateSnapshotIntegrityDiagnostic,
20
20
  templateSuffixConflictDiagnostic
21
- } from "./chunk-EN5SMEWJ.js";
22
- import "./chunk-XO3CT6GL.js";
21
+ } from "./chunk-PKCYTOHW.js";
22
+ import "./chunk-CBLBQP27.js";
23
23
  export {
24
24
  FileBrandingStore,
25
25
  InMemoryBrandingStore,