@xtrable-ltd/nanoesis 0.1.31 → 0.1.33

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 (30) hide show
  1. package/dist/adapter-azure-blob.d.ts +41 -4
  2. package/dist/adapter-azure-blob.js +90 -4
  3. package/dist/{chunk-BCWZRKMF.js → chunk-C3QPGXG5.js} +14 -4
  4. package/dist/{chunk-GFQT7BYP.js → chunk-P6NDWIKK.js} +201 -83
  5. package/dist/editor-api.d.ts +11 -5
  6. package/dist/editor-api.js +2 -2
  7. package/dist/index.d.ts +102 -2
  8. package/dist/index.js +9 -1
  9. package/dist/mcp.js +3 -3
  10. package/editor/assets/{MigrationsPane-Drxje7Nq.js → MigrationsPane-BuaKhsie.js} +1 -1
  11. package/editor/assets/{TemplatesPane-CxO1IknP.js → TemplatesPane-Bg_b0htA.js} +7 -7
  12. package/editor/assets/{cssMode-BPla51Av.js → cssMode-Bcwjp8T3.js} +1 -1
  13. package/editor/assets/{freemarker2-Bob6Bse-.js → freemarker2-Cgqj3dEx.js} +1 -1
  14. package/editor/assets/{handlebars-DsQdZgzp.js → handlebars-CLf2-RZf.js} +1 -1
  15. package/editor/assets/{html-BNBtLdPe.js → html-BrWGWRh6.js} +1 -1
  16. package/editor/assets/{htmlMode-6kyy4G1O.js → htmlMode-D9skx8gp.js} +1 -1
  17. package/editor/assets/index-CY5Kehuu.js +142 -0
  18. package/editor/assets/{javascript-Dx8xuybD.js → javascript-KaABDboM.js} +1 -1
  19. package/editor/assets/{jsonMode-DWRGUqMk.js → jsonMode-0usLs2ME.js} +1 -1
  20. package/editor/assets/{liquid-DlIa11aQ.js → liquid-DYna-Clp.js} +1 -1
  21. package/editor/assets/{mdx-DR0q2TJm.js → mdx-f7LAIls6.js} +1 -1
  22. package/editor/assets/{python-Cbswpzux.js → python-Bh21_mvq.js} +1 -1
  23. package/editor/assets/{razor-IYEYh5Ox.js → razor-D_530Ymg.js} +1 -1
  24. package/editor/assets/{tsMode-DQrbmWSC.js → tsMode-CTRVjt9n.js} +1 -1
  25. package/editor/assets/{typescript-DiBU2jVJ.js → typescript-D084tkOf.js} +1 -1
  26. package/editor/assets/{xml-CCxs0QRL.js → xml-BFulX1C6.js} +1 -1
  27. package/editor/assets/{yaml-C1tH0_iR.js → yaml-CgZsYmUt.js} +1 -1
  28. package/editor/index.html +1 -1
  29. package/package.json +1 -1
  30. package/editor/assets/index-BG-8SSzq.js +0 -142
@@ -144,24 +144,51 @@ function parseSortFile(raw) {
144
144
  }
145
145
 
146
146
  // ../engine/src/store/blob-store.ts
147
+ function isConditionalBlobStore(store) {
148
+ const candidate = store;
149
+ return typeof candidate.getVersioned === "function" && typeof candidate.putConditional === "function";
150
+ }
147
151
  var InMemoryBlobStore = class {
148
152
  blobs = /* @__PURE__ */ new Map();
153
+ /** Per-key version, bumped on every write, so {@link putConditional} can detect a
154
+ * concurrent overwrite the same way an ETag does on a real store. */
155
+ versions = /* @__PURE__ */ new Map();
156
+ nextVersion = 1;
149
157
  constructor(initial) {
150
158
  if (initial === void 0) return;
151
159
  const encoder = new TextEncoder();
152
160
  for (const [key, value] of Object.entries(initial)) {
153
- this.blobs.set(key, copy(typeof value === "string" ? encoder.encode(value) : value));
161
+ this.set(key, copy(typeof value === "string" ? encoder.encode(value) : value));
154
162
  }
155
163
  }
164
+ set(key, bytes) {
165
+ this.blobs.set(key, bytes);
166
+ const version = this.nextVersion;
167
+ this.nextVersion += 1;
168
+ this.versions.set(key, version);
169
+ return version;
170
+ }
156
171
  async get(key) {
157
172
  const value = this.blobs.get(key);
158
173
  return value === void 0 ? void 0 : copy(value);
159
174
  }
160
175
  async put(key, bytes) {
161
- this.blobs.set(key, copy(bytes));
176
+ this.set(key, copy(bytes));
162
177
  }
163
178
  async delete(key) {
164
179
  this.blobs.delete(key);
180
+ this.versions.delete(key);
181
+ }
182
+ async getVersioned(key) {
183
+ const value = this.blobs.get(key);
184
+ if (value === void 0) return void 0;
185
+ return { bytes: copy(value), version: String(this.versions.get(key)) };
186
+ }
187
+ async putConditional(key, bytes, expected) {
188
+ const current = this.versions.get(key);
189
+ const currentTag = current === void 0 ? null : String(current);
190
+ if (currentTag !== expected) return void 0;
191
+ return String(this.set(key, copy(bytes)));
165
192
  }
166
193
  };
167
194
  function copy(bytes) {
@@ -218,6 +245,59 @@ function buildRedirects(rules, liveUrls) {
218
245
  return { path: OUTPUT_PATH, contents };
219
246
  }
220
247
 
248
+ // ../engine/src/content/source.ts
249
+ var NotFoundError = class extends Error {
250
+ constructor(path) {
251
+ super(`No such file in content source: ${path}`);
252
+ this.path = path;
253
+ this.name = "NotFoundError";
254
+ }
255
+ path;
256
+ };
257
+ function normalizePath(path) {
258
+ return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
259
+ }
260
+ var InMemoryContentSource = class {
261
+ files;
262
+ constructor(files) {
263
+ this.files = new Map(Object.entries(files).map(([key, value]) => [normalizePath(key), value]));
264
+ }
265
+ async readText(path) {
266
+ const value = this.files.get(normalizePath(path));
267
+ if (value === void 0) throw new NotFoundError(path);
268
+ return typeof value === "string" ? value : new TextDecoder().decode(value);
269
+ }
270
+ async readBytes(path) {
271
+ const value = this.files.get(normalizePath(path));
272
+ if (value === void 0) throw new NotFoundError(path);
273
+ return typeof value === "string" ? new TextEncoder().encode(value) : value;
274
+ }
275
+ async exists(path) {
276
+ const target = normalizePath(path);
277
+ if (target === "") return true;
278
+ if (this.files.has(target)) return true;
279
+ const prefix = `${target}/`;
280
+ for (const key of this.files.keys()) {
281
+ if (key.startsWith(prefix)) return true;
282
+ }
283
+ return false;
284
+ }
285
+ async list(dir) {
286
+ const base = normalizePath(dir);
287
+ const prefix = base === "" ? "" : `${base}/`;
288
+ const entries = /* @__PURE__ */ new Map();
289
+ for (const key of this.files.keys()) {
290
+ if (prefix !== "" && !key.startsWith(prefix)) continue;
291
+ const rest = key.slice(prefix.length);
292
+ if (rest === "") continue;
293
+ const slash = rest.indexOf("/");
294
+ if (slash === -1) entries.set(rest, "file");
295
+ else entries.set(rest.slice(0, slash), "dir");
296
+ }
297
+ return [...entries].map(([name, kind]) => ({ name, kind })).sort((a, b) => a.name.localeCompare(b.name));
298
+ }
299
+ };
300
+
221
301
  // ../engine/src/content/loader.ts
222
302
  var SORT_FILE = "_sort.json";
223
303
  var REDIRECTS_FILE = "_redirects.json";
@@ -249,10 +329,8 @@ async function loadDir(source, dirPath, slug, treePath) {
249
329
  if (entry.name === SORT_FILE || entry.name === REDIRECTS_FILE || entry.name === SITE_CONFIG_FILE || !entry.name.endsWith(ITEM_EXT))
250
330
  continue;
251
331
  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
- );
332
+ const item = await loadItemIfPresent(source, join(dirPath, entry.name), childSlug, treePath);
333
+ if (item !== void 0) childMap.set(childSlug, item);
256
334
  } else {
257
335
  if (entry.name === ASSETS_DIR) continue;
258
336
  const childPath = join(treePath, entry.name);
@@ -273,6 +351,14 @@ async function loadDir(source, dirPath, slug, treePath) {
273
351
  ...sort.defaultTemplate !== void 0 && { defaultTemplate: sort.defaultTemplate }
274
352
  };
275
353
  }
354
+ async function loadItemIfPresent(source, filePath, slug, parentPath2) {
355
+ try {
356
+ return await loadItem(source, filePath, slug, parentPath2);
357
+ } catch (error) {
358
+ if (error instanceof NotFoundError) return void 0;
359
+ throw error;
360
+ }
361
+ }
276
362
  async function loadItem(source, filePath, slug, parentPath2) {
277
363
  const raw = await source.readText(filePath);
278
364
  try {
@@ -352,7 +438,11 @@ async function loadComponentFiles(source, componentsDir, extension) {
352
438
  await walk(full);
353
439
  } else if (entry.name.endsWith(suffix)) {
354
440
  const key = entry.name.slice(0, -suffix.length).toLowerCase();
355
- map.set(key, await source.readText(full));
441
+ try {
442
+ map.set(key, await source.readText(full));
443
+ } catch (error) {
444
+ if (!(error instanceof NotFoundError)) throw error;
445
+ }
356
446
  }
357
447
  }
358
448
  };
@@ -810,6 +900,9 @@ function emptyIndex() {
810
900
  async function loadIndex(store) {
811
901
  const live = parseIndex(await store.get(INDEX_KEY));
812
902
  if (live !== void 0) return live;
903
+ return recoverFromBackups(store);
904
+ }
905
+ async function recoverFromBackups(store) {
813
906
  let best;
814
907
  for (let slot = 0; slot < BACKUP_RING_SIZE; slot += 1) {
815
908
  const candidate = parseIndex(await store.get(backupKey(slot)));
@@ -819,6 +912,18 @@ async function loadIndex(store) {
819
912
  }
820
913
  return best ?? emptyIndex();
821
914
  }
915
+ async function loadIndexVersioned(store) {
916
+ const live = await store.getVersioned(INDEX_KEY);
917
+ if (live === void 0) return { index: await recoverFromBackups(store), version: null };
918
+ const parsed = parseIndex(live.bytes);
919
+ return { index: parsed ?? await recoverFromBackups(store), version: live.version };
920
+ }
921
+ async function saveIndexConditional(store, prev, nextKeys) {
922
+ await store.put(backupKey(prev.index.version % BACKUP_RING_SIZE), serialize2(prev.index));
923
+ const next = freezeIndex(prev.index.version + 1, nextKeys);
924
+ const version = await store.putConditional(INDEX_KEY, serialize2(next), prev.version);
925
+ return version === void 0 ? void 0 : { index: next, version };
926
+ }
822
927
  async function saveIndex(store, prev, nextKeys) {
823
928
  await store.put(backupKey(prev.version % BACKUP_RING_SIZE), serialize2(prev));
824
929
  const next = freezeIndex(prev.version + 1, nextKeys);
@@ -916,6 +1021,47 @@ var IndexedStore = class {
916
1021
  }
917
1022
  })();
918
1023
  }
1024
+ /**
1025
+ * Apply a key-set change to the index and persist it (DESIGN §11d). `mutate` receives the
1026
+ * current key set and adds/removes in place; it must be a pure function of that set (it can
1027
+ * be re-run on a fresh read), because on a store with the optimistic-concurrency capability
1028
+ * this runs as a compare-and-set retry loop: read the live index + version, apply `mutate`,
1029
+ * write only if the version is unchanged, and retry on a conflict. This is what stops a
1030
+ * second host instance's blind overwrite from evicting a key this one just added
1031
+ * (NANOESIS-MCP-ISSUES Issue 1). A store without the capability keeps the single-writer
1032
+ * path, correct under the in-process {@link mutationLock} alone. Either way an unchanged key
1033
+ * set writes nothing (overwriting an existing item stays a single blob `put`).
1034
+ *
1035
+ * Callers do their blob writes/deletes first (content-first, §11d) and pass only the
1036
+ * resulting index delta here, so a failed blob op never advances the index.
1037
+ */
1038
+ async commitIndex(mutate) {
1039
+ const store = this.store;
1040
+ if (isConditionalBlobStore(store)) {
1041
+ for (let attempt = 0; attempt < MAX_INDEX_CAS_ATTEMPTS; attempt += 1) {
1042
+ const current = await loadIndexVersioned(store);
1043
+ const keys2 = new Set(current.index.keys);
1044
+ mutate(keys2);
1045
+ if (sameKeySet(keys2, current.index.keys)) {
1046
+ this.index = current.index;
1047
+ return;
1048
+ }
1049
+ const saved = await saveIndexConditional(store, current, [...keys2]);
1050
+ if (saved !== void 0) {
1051
+ this.index = saved.index;
1052
+ return;
1053
+ }
1054
+ }
1055
+ throw new Error(
1056
+ "content index: too many concurrent modifications; retry the operation (the index could not be saved after repeated conflicts)"
1057
+ );
1058
+ }
1059
+ const index = await this.loaded();
1060
+ const keys = new Set(index.keys);
1061
+ mutate(keys);
1062
+ if (sameKeySet(keys, index.keys)) return;
1063
+ this.index = await saveIndex(this.store, index, [...keys]);
1064
+ }
919
1065
  async list(dir) {
920
1066
  return childrenOf((await this.loaded()).keys, dir);
921
1067
  }
@@ -924,7 +1070,7 @@ var IndexedStore = class {
924
1070
  }
925
1071
  async readBytes(path) {
926
1072
  const bytes = await this.store.get(path);
927
- if (bytes === void 0) throw new Error(`No such file in content source: ${path}`);
1073
+ if (bytes === void 0) throw new NotFoundError(path);
928
1074
  return bytes;
929
1075
  }
930
1076
  async exists(path) {
@@ -965,8 +1111,8 @@ var IndexedStore = class {
965
1111
  schemaDelta = computeSchemaDelta(oldFields, newFields);
966
1112
  const destructive = schemaDelta.removed.length > 0 || schemaDelta.typeChanged.some((change) => change.destructive);
967
1113
  if (destructive) {
968
- const index2 = await this.loaded();
969
- const siblings = index2.keys.filter((k) => k.startsWith(`${stampCandidate.dir}/`) && k.endsWith(".html")).map((k) => k.slice(stampCandidate.dir.length + 1, -".html".length));
1114
+ const index = await this.loaded();
1115
+ const siblings = index.keys.filter((k) => k.startsWith(`${stampCandidate.dir}/`) && k.endsWith(".html")).map((k) => k.slice(stampCandidate.dir.length + 1, -".html".length));
970
1116
  const version = nextVersionNumber(siblings);
971
1117
  stampDecision = {
972
1118
  snapshotPath: `${stampCandidate.dir}/${stampCandidate.name}@v${version}.html`,
@@ -976,22 +1122,13 @@ var IndexedStore = class {
976
1122
  }
977
1123
  }
978
1124
  await this.store.put(target, bytes);
979
- const index = await this.loaded();
980
- let nextIndex = index;
981
- if (!nextIndex.keys.includes(target)) {
982
- nextIndex = await saveIndex(this.store, nextIndex, [...nextIndex.keys, target]);
983
- }
984
1125
  let stamped;
985
1126
  let stampIncomplete;
1127
+ let snapshotToIndex;
986
1128
  if (stampDecision !== void 0) {
987
1129
  try {
988
1130
  await this.store.put(stampDecision.snapshotPath, stampDecision.bytes);
989
- if (!nextIndex.keys.includes(stampDecision.snapshotPath)) {
990
- nextIndex = await saveIndex(this.store, nextIndex, [
991
- ...nextIndex.keys,
992
- stampDecision.snapshotPath
993
- ]);
994
- }
1131
+ snapshotToIndex = stampDecision.snapshotPath;
995
1132
  stamped = {
996
1133
  name: stampCandidate.name,
997
1134
  version: stampDecision.version,
@@ -1001,7 +1138,10 @@ var IndexedStore = class {
1001
1138
  stampIncomplete = true;
1002
1139
  }
1003
1140
  }
1004
- this.index = nextIndex;
1141
+ await this.commitIndex((keys) => {
1142
+ keys.add(target);
1143
+ if (snapshotToIndex !== void 0) keys.add(snapshotToIndex);
1144
+ });
1005
1145
  let parseDiagnostics;
1006
1146
  if (isContentItemPath(target)) {
1007
1147
  parseDiagnostics = contentParseDiagnosticsFor(bytes);
@@ -1037,11 +1177,7 @@ var IndexedStore = class {
1037
1177
  const version = nextVersionNumber(siblings);
1038
1178
  const snapshotPath = `${dir}/${name}@v${version}.html`;
1039
1179
  await this.store.put(snapshotPath, bytes);
1040
- if (!index.keys.includes(snapshotPath)) {
1041
- this.index = await saveIndex(this.store, index, [...index.keys, snapshotPath]);
1042
- } else {
1043
- this.index = index;
1044
- }
1180
+ await this.commitIndex((keys) => keys.add(snapshotPath));
1045
1181
  return { name, version, snapshotPath };
1046
1182
  });
1047
1183
  }
@@ -1061,8 +1197,11 @@ var IndexedStore = class {
1061
1197
  return;
1062
1198
  }
1063
1199
  await Promise.all(removed.map((k) => this.store.delete(k)));
1064
- const remaining = index.keys.filter((k) => k !== target && !k.startsWith(prefix));
1065
- this.index = await saveIndex(this.store, index, remaining);
1200
+ await this.commitIndex((keys) => {
1201
+ for (const k of [...keys]) {
1202
+ if (k === target || k.startsWith(prefix)) keys.delete(k);
1203
+ }
1204
+ });
1066
1205
  });
1067
1206
  }
1068
1207
  /**
@@ -1099,8 +1238,10 @@ var IndexedStore = class {
1099
1238
  await this.store.delete(move.from);
1100
1239
  }
1101
1240
  const movedFrom = new Set(moves.map((move) => move.from));
1102
- const next = index.keys.filter((k) => !movedFrom.has(k)).concat(moves.map((m) => m.to));
1103
- this.index = await saveIndex(this.store, index, next);
1241
+ await this.commitIndex((keys) => {
1242
+ for (const from2 of movedFrom) keys.delete(from2);
1243
+ for (const move of moves) keys.add(move.to);
1244
+ });
1104
1245
  return { ok: true };
1105
1246
  });
1106
1247
  }
@@ -1143,6 +1284,12 @@ function contentParseDiagnosticsFor(bytes) {
1143
1284
  return void 0;
1144
1285
  }
1145
1286
  }
1287
+ var MAX_INDEX_CAS_ATTEMPTS = 50;
1288
+ function sameKeySet(set, sorted) {
1289
+ if (set.size !== sorted.length) return false;
1290
+ for (const key of sorted) if (!set.has(key)) return false;
1291
+ return true;
1292
+ }
1146
1293
  function guarded(key) {
1147
1294
  if (key === "" || key.startsWith(RESERVED_PREFIX)) {
1148
1295
  throw new Error(`Refusing to mutate a reserved key: ${key === "" ? "(root)" : key}`);
@@ -1174,51 +1321,6 @@ function pathExists(keys, path) {
1174
1321
  return keys.some((key) => key.startsWith(prefix));
1175
1322
  }
1176
1323
 
1177
- // ../engine/src/content/source.ts
1178
- function normalizePath(path) {
1179
- return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
1180
- }
1181
- var InMemoryContentSource = class {
1182
- files;
1183
- constructor(files) {
1184
- this.files = new Map(Object.entries(files).map(([key, value]) => [normalizePath(key), value]));
1185
- }
1186
- async readText(path) {
1187
- const value = this.files.get(normalizePath(path));
1188
- if (value === void 0) throw new Error(`No such file in content source: ${path}`);
1189
- return typeof value === "string" ? value : new TextDecoder().decode(value);
1190
- }
1191
- async readBytes(path) {
1192
- const value = this.files.get(normalizePath(path));
1193
- if (value === void 0) throw new Error(`No such file in content source: ${path}`);
1194
- return typeof value === "string" ? new TextEncoder().encode(value) : value;
1195
- }
1196
- async exists(path) {
1197
- const target = normalizePath(path);
1198
- if (target === "") return true;
1199
- if (this.files.has(target)) return true;
1200
- const prefix = `${target}/`;
1201
- for (const key of this.files.keys()) {
1202
- if (key.startsWith(prefix)) return true;
1203
- }
1204
- return false;
1205
- }
1206
- async list(dir) {
1207
- const base = normalizePath(dir);
1208
- const prefix = base === "" ? "" : `${base}/`;
1209
- const entries = /* @__PURE__ */ new Map();
1210
- for (const key of this.files.keys()) {
1211
- if (prefix !== "" && !key.startsWith(prefix)) continue;
1212
- const rest = key.slice(prefix.length);
1213
- if (rest === "") continue;
1214
- const slash = rest.indexOf("/");
1215
- if (slash === -1) entries.set(rest, "file");
1216
- else entries.set(rest.slice(0, slash), "dir");
1217
- }
1218
- return [...entries].map(([name, kind]) => ({ name, kind })).sort((a, b) => a.name.localeCompare(b.name));
1219
- }
1220
- };
1221
-
1222
1324
  // ../engine/src/url/references.ts
1223
1325
  var REFERENCE_PREFIX = "ref:";
1224
1326
  function referenceTarget(value) {
@@ -2802,6 +2904,9 @@ async function validateSite(source) {
2802
2904
  }
2803
2905
  const shellRequiredFields = shellAnalysis?.requiredFields ?? [];
2804
2906
  const shellConstraints = shellAnalysis?.constraints ?? /* @__PURE__ */ new Map();
2907
+ const shellFieldNames = new Set(
2908
+ documentShell !== void 0 ? deriveFields(documentShell, components).map((f) => f.name) : []
2909
+ );
2805
2910
  const diagnostics = [];
2806
2911
  const add = (severity, code, message, path) => {
2807
2912
  diagnostics.push({ severity, code, message, ...path !== void 0 && { path } });
@@ -2970,7 +3075,9 @@ async function validateSite(source) {
2970
3075
  if (!isVersionedTemplateName(templateName)) {
2971
3076
  const expectedFields = expectedFieldsFor(templateName);
2972
3077
  const itemFieldNames = Object.keys(node.item.fields);
2973
- const orphans = itemFieldNames.filter((name) => !expectedFields.has(name));
3078
+ const orphans = itemFieldNames.filter(
3079
+ (name) => !expectedFields.has(name) && !shellFieldNames.has(name)
3080
+ );
2974
3081
  if (orphans.length > 0) {
2975
3082
  add(
2976
3083
  "warning",
@@ -3212,13 +3319,20 @@ async function publishSite(source, sink, options = {}) {
3212
3319
  );
3213
3320
  const byPath = /* @__PURE__ */ new Map();
3214
3321
  for (const artifact of [...stamped, ...passthrough]) byPath.set(artifact.path, artifact);
3322
+ const everything = [...byPath.values()];
3323
+ const pages = everything.filter((artifact) => artifact.path.endsWith(".html"));
3324
+ const resources = everything.filter((artifact) => !artifact.path.endsWith(".html"));
3215
3325
  const uploadTotal = byPath.size;
3216
3326
  let uploaded = 0;
3217
- await mapWithConcurrency([...byPath.values()], writeConcurrency, async (artifact) => {
3218
- await sink.write(artifact.path, artifact.contents, cacheControlFor(artifact));
3219
- uploaded += 1;
3220
- onProgress?.({ phase: "upload", written: uploaded, total: uploadTotal });
3221
- });
3327
+ const writeGroup = async (group) => {
3328
+ await mapWithConcurrency(group, writeConcurrency, async (artifact) => {
3329
+ await sink.write(artifact.path, artifact.contents, cacheControlFor(artifact));
3330
+ uploaded += 1;
3331
+ onProgress?.({ phase: "upload", written: uploaded, total: uploadTotal });
3332
+ });
3333
+ };
3334
+ await writeGroup(resources);
3335
+ await writeGroup(pages);
3222
3336
  if (purge !== noopPurgeService) onProgress?.({ phase: "purge" });
3223
3337
  await purge.purgeAll();
3224
3338
  onProgress?.({ phase: "done", written: uploadTotal, summary });
@@ -3472,9 +3586,12 @@ export {
3472
3586
  ContentParseError,
3473
3587
  parseContentItem,
3474
3588
  parseSortFile,
3589
+ isConditionalBlobStore,
3475
3590
  InMemoryBlobStore,
3476
3591
  parseRedirects,
3477
3592
  buildRedirects,
3593
+ NotFoundError,
3594
+ InMemoryContentSource,
3478
3595
  DEFAULT_DIRS,
3479
3596
  loadContentTree,
3480
3597
  loadRedirects,
@@ -3504,10 +3621,11 @@ export {
3504
3621
  RESERVED_PREFIX,
3505
3622
  emptyIndex,
3506
3623
  loadIndex,
3624
+ loadIndexVersioned,
3625
+ saveIndexConditional,
3507
3626
  saveIndex,
3508
3627
  reconcileIndex,
3509
3628
  IndexedStore,
3510
- InMemoryContentSource,
3511
3629
  analyzeTemplate,
3512
3630
  pendingMigrations,
3513
3631
  bestFitSnapshot,
@@ -222,7 +222,12 @@ interface EditorConfig {
222
222
  readonly prebuild?: PreBuildHook | (() => Promise<PreBuildHook | undefined>);
223
223
  /** Admin diagnostics/self-heal; defaults to {@link buildDefaultDiagnostics}. */
224
224
  readonly diagnostics?: DiagnosticRegistry;
225
- /** Clear the website before republishing (default true). Needs `website.wipe`. */
225
+ /**
226
+ * Whether a publish clears pages the new build no longer emits (default true). A
227
+ * `prune`-capable website does this as a post-publish orphan sweep (overwrite-in-place,
228
+ * zero downtime); a wipe-only website clears up front instead. Set false to never delete
229
+ * (overwrite-only, stale pages linger). Names `wipeBeforePublish` for back-compat.
230
+ */
226
231
  readonly wipeBeforePublish?: boolean;
227
232
  }
228
233
  /** A wired editor: mount {@link handleApi} at `/api/*` and call {@link publish} to go live. */
@@ -244,12 +249,13 @@ interface Editor {
244
249
  * Wire a complete nanoesis editor from a storage pair, a login, and a few optional
245
250
  * capabilities. This is the whole integration surface (DESIGN §11c): everything that used
246
251
  * to be hand-assembled per host, the content index over the store, the publish source/sink,
247
- * index reconcile, the wipe-before-publish, and the {@link ApiDeps} bag, is built here, so
252
+ * index reconcile, the orphan sweep, and the {@link ApiDeps} bag, is built here, so
248
253
  * internals (`IndexedStore`, `ArtifactSink`, reconcile plumbing) never reach the adopter.
249
254
  *
250
- * `publish()` validates **before** it wipes: an invalid site returns its errors and never
251
- * touches the live files, so a failed publish can never blank the website (the footgun the
252
- * old "host wipes, then publishes" wiring carried).
255
+ * `publish()` validates **first**: an invalid site returns its errors and never touches the
256
+ * live files. A `prune`-capable website then overwrites every artifact in place and sweeps
257
+ * the orphans afterwards, so a publish never blanks the live site, not even for the upload
258
+ * window (the footgun the old "host wipes, then publishes" wiring carried).
253
259
  */
254
260
  declare function createEditor(config: EditorConfig): Editor;
255
261
  /**
@@ -21,8 +21,8 @@ import {
21
21
  serveEditorAsset,
22
22
  templateSnapshotIntegrityDiagnostic,
23
23
  templateSuffixConflictDiagnostic
24
- } from "./chunk-BCWZRKMF.js";
25
- import "./chunk-GFQT7BYP.js";
24
+ } from "./chunk-C3QPGXG5.js";
25
+ import "./chunk-P6NDWIKK.js";
26
26
  export {
27
27
  FileBrandingStore,
28
28
  InMemoryBrandingStore,
package/dist/index.d.ts CHANGED
@@ -158,6 +158,37 @@ interface BlobStore {
158
158
  /** Remove `key`. Deleting an absent key is a no-op (idempotent). */
159
159
  delete(key: string): Promise<void>;
160
160
  }
161
+ /**
162
+ * Optional optimistic-concurrency capability a {@link BlobStore} may also implement
163
+ * (DESIGN §11d). It exists for exactly one blob: the content index, which is mutated by
164
+ * read-modify-write. With a single writer the in-process mutex on `IndexedStore` is enough,
165
+ * but a horizontally-scaled host (e.g. Azure Consumption spinning a second instance under
166
+ * load) runs two writers against one index blob, and a blind overwrite there loses updates,
167
+ * one instance's `saveIndex` evicts a key the other just added, though its content blob is
168
+ * fine (the "files exist but missing from the index" churn, NANOESIS-MCP-ISSUES Issue 1).
169
+ *
170
+ * When a store implements this, `IndexedStore` performs a compare-and-set on the index:
171
+ * read the current version, re-apply its key delta, write only if the version is unchanged,
172
+ * and retry on a conflict. A store without it (a `Map`, a folder) keeps the blind path,
173
+ * single-process hosts neither need nor pay for the capability. It is therefore an *optional*
174
+ * add-on, like {@link Storage.wipe}: the minimal adopter surface stays get/put/delete.
175
+ */
176
+ interface ConditionalBlobStore {
177
+ /** The bytes at `key` plus an opaque version tag (e.g. an ETag), or `undefined` if absent. */
178
+ getVersioned(key: string): Promise<{
179
+ bytes: Uint8Array;
180
+ version: string;
181
+ } | undefined>;
182
+ /**
183
+ * Write `key` only if its current version matches `expected` (or, when `expected` is
184
+ * `null`, only if the key does not yet exist). Resolves to the new version on success, or
185
+ * `undefined` when the precondition failed because another writer got there first, the
186
+ * signal for the caller to re-read and retry. A non-precondition failure still rejects.
187
+ */
188
+ putConditional(key: string, bytes: Uint8Array, expected: string | null): Promise<string | undefined>;
189
+ }
190
+ /** Whether a {@link BlobStore} also offers the optimistic-concurrency capability. */
191
+ declare function isConditionalBlobStore(store: BlobStore): store is BlobStore & ConditionalBlobStore;
161
192
  /**
162
193
  * The single storage contract an adopter implements (DESIGN §11c): a {@link BlobStore}
163
194
  * (get/put/delete) plus an optional {@link wipe}. The same shape backs both the editor's
@@ -173,6 +204,17 @@ interface Storage extends BlobStore {
173
204
  * (and so cannot clear itself) omits it; the publish then only overwrites.
174
205
  */
175
206
  wipe?(): Promise<void>;
207
+ /**
208
+ * Optional: delete every key **not** in `keep`, leaving the kept keys untouched. The
209
+ * zero-downtime alternative to {@link wipe} for a published website: the host overwrites
210
+ * every artifact in place (each key flips old→new atomically, so no URL is ever missing),
211
+ * then calls `prune` with the just-written path set to sweep only the orphans, pages that
212
+ * no longer exist. Unlike `wipe` (clear *before* the upload, blanking the live site for the
213
+ * whole write), `prune` runs *after* a successful upload and only removes what the new
214
+ * publish did not write. A store that cannot enumerate omits it; the host falls back to
215
+ * `wipe`, or to overwrite-only when neither is available.
216
+ */
217
+ prune?(keep: ReadonlySet<string>): Promise<void>;
176
218
  }
177
219
  /**
178
220
  * In-memory {@link BlobStore} backed by a plain map, the test double the engine's
@@ -180,12 +222,22 @@ interface Storage extends BlobStore {
180
222
  * interchangeable). It copies bytes in and out, so a caller can never mutate stored
181
223
  * data by holding on to a buffer.
182
224
  */
183
- declare class InMemoryBlobStore implements BlobStore {
225
+ declare class InMemoryBlobStore implements BlobStore, ConditionalBlobStore {
184
226
  private readonly blobs;
227
+ /** Per-key version, bumped on every write, so {@link putConditional} can detect a
228
+ * concurrent overwrite the same way an ETag does on a real store. */
229
+ private readonly versions;
230
+ private nextVersion;
185
231
  constructor(initial?: Readonly<Record<string, Uint8Array | string>>);
232
+ private set;
186
233
  get(key: string): Promise<Uint8Array | undefined>;
187
234
  put(key: string, bytes: Uint8Array): Promise<void>;
188
235
  delete(key: string): Promise<void>;
236
+ getVersioned(key: string): Promise<{
237
+ bytes: Uint8Array;
238
+ version: string;
239
+ } | undefined>;
240
+ putConditional(key: string, bytes: Uint8Array, expected: string | null): Promise<string | undefined>;
189
241
  }
190
242
 
191
243
  /**
@@ -195,6 +247,17 @@ declare class InMemoryBlobStore implements BlobStore {
195
247
  * relative to the site root (e.g. "content/blog/post.json").
196
248
  */
197
249
  type EntryKind = 'file' | 'dir';
250
+ /**
251
+ * Thrown by a {@link ContentSource} read when the path does not exist, as opposed to a
252
+ * real I/O failure (a network error, a permission error). Loaders catch *this* to tell
253
+ * index drift (a key the index lists but whose blob is gone) apart from an error they must
254
+ * not swallow: a listed-but-missing item is skipped, a genuine read failure still
255
+ * propagates. Carries the requested path for diagnostics.
256
+ */
257
+ declare class NotFoundError extends Error {
258
+ readonly path: string;
259
+ constructor(path: string);
260
+ }
198
261
  interface DirEntry {
199
262
  readonly name: string;
200
263
  readonly kind: EntryKind;
@@ -874,6 +937,28 @@ declare function emptyIndex(): ContentIndex;
874
937
  * but cannot be enumerated until something rewrites the index).
875
938
  */
876
939
  declare function loadIndex(store: BlobStore): Promise<ContentIndex>;
940
+ /** The index paired with the live index blob's version tag, for a compare-and-set save
941
+ * (DESIGN §11d). `version` is `null` when no live index blob exists yet, so the first save
942
+ * creates it (`putConditional` with `expected: null`). */
943
+ interface VersionedIndex {
944
+ readonly index: ContentIndex;
945
+ readonly version: string | null;
946
+ }
947
+ /**
948
+ * Load the index together with the version tag needed to write it back safely under
949
+ * optimistic concurrency. A present-but-corrupt live blob recovers from the backup ring but
950
+ * keeps the live version, so the next {@link saveIndexConditional} overwrites the corrupt
951
+ * copy rather than failing forever; an absent live blob recovers read-only with `version:
952
+ * null`.
953
+ */
954
+ declare function loadIndexVersioned(store: BlobStore & ConditionalBlobStore): Promise<VersionedIndex>;
955
+ /**
956
+ * Compare-and-set save (DESIGN §11d): back up the index being replaced (best-effort,
957
+ * unconditional), then write the new index *only if* the live version still matches the one
958
+ * read in `prev`. Returns the new {@link VersionedIndex} on success, or `undefined` when a
959
+ * concurrent writer won the race, the caller re-reads and re-applies its delta, then retries.
960
+ */
961
+ declare function saveIndexConditional(store: BlobStore & ConditionalBlobStore, prev: VersionedIndex, nextKeys: readonly string[]): Promise<VersionedIndex | undefined>;
877
962
  /**
878
963
  * Write a new index whose keys are `nextKeys`, backing up the one it replaces first
879
964
  * (DESIGN §11d). The previous index goes into its ring slot (`version % ring`, oldest
@@ -943,6 +1028,21 @@ declare class IndexedStore implements WorkingStore {
943
1028
  /** The index, loaded once on first need and cached (mutations replace the cached copy). */
944
1029
  private loaded;
945
1030
  private serializeMutation;
1031
+ /**
1032
+ * Apply a key-set change to the index and persist it (DESIGN §11d). `mutate` receives the
1033
+ * current key set and adds/removes in place; it must be a pure function of that set (it can
1034
+ * be re-run on a fresh read), because on a store with the optimistic-concurrency capability
1035
+ * this runs as a compare-and-set retry loop: read the live index + version, apply `mutate`,
1036
+ * write only if the version is unchanged, and retry on a conflict. This is what stops a
1037
+ * second host instance's blind overwrite from evicting a key this one just added
1038
+ * (NANOESIS-MCP-ISSUES Issue 1). A store without the capability keeps the single-writer
1039
+ * path, correct under the in-process {@link mutationLock} alone. Either way an unchanged key
1040
+ * set writes nothing (overwriting an existing item stays a single blob `put`).
1041
+ *
1042
+ * Callers do their blob writes/deletes first (content-first, §11d) and pass only the
1043
+ * resulting index delta here, so a failed blob op never advances the index.
1044
+ */
1045
+ private commitIndex;
946
1046
  list(dir: string): Promise<readonly DirEntry[]>;
947
1047
  readText(path: string): Promise<string>;
948
1048
  readBytes(path: string): Promise<Uint8Array>;
@@ -1870,4 +1970,4 @@ declare function createDiagnosticRegistry(): DiagnosticRegistry;
1870
1970
 
1871
1971
  declare const workingStoreRoundTripDiagnostic: Diagnostic;
1872
1972
 
1873
- export { type Artifact, type ArtifactSink, type AuthEndpoints, type AuthResult, type AuthorDirectory, type AuthorEntry, type AuthorOption, type AuthorRef, type AuthoringReference, type BlobStore, type BoundItem, CACHE_IMMUTABLE, CACHE_REVALIDATE, type ChangePasswordRequest, type ChangePasswordSuccess, type CollectionConfig, type CollectionQuery, type CompileInput, type CompilePageOptions, type CompileSiteOptions, type CompiledPage, type ComponentMap, type ContentIndex, type ContentItem, ContentParseError, type ContentSource, type CreateTokenSuccess, type CreateUserRequest, DEFAULT_DIRS, DOCUMENT_SHELL, type DerivedField, type DiagnoseDeps, type Severity as DiagnoseSeverity, type Source as DiagnoseSource, type Diagnostic$1 as Diagnostic, type Diagnostic as DiagnosticCheck, type DiagnosticRegistry, type DirEntry, type DirNode, type EncodeRequest, type EncodedImage, type EncodedVariant, type EntryKind, FIELD_TYPES, type FieldPrimitive, type FieldRecord, type FieldType, type FieldTypeDef, type FieldValue, type Finding, type IdentityProvider, type ImageEncoder, type ImageFormat, type ImageInfo, InMemoryArtifactSink, InMemoryBlobStore, InMemoryContentSource, IndexedStore, type ItemNode, type LengthConstraints, type LoginRequest, type LoginSuccess, type MediaResolver, type MigrationResolution, type PageEntry, type PendingMigrationItem, type PendingMigrations, type PreBuildHook, type Principal, type ProgressReporter, type ProgressSummary, type PublishOptions, type PublishPlan, type PublishProgress, type PublishResource, type PublishResult, type PublishSummary, type PurgeService, RESERVED_PREFIX, type ReconcileResult, type RedirectRule, type ReferenceContext, type ReferenceEntry, type ReferenceSection, type RefreshSuccess, type RenameResult, type Repair, type RepairArgs, type ResetPasswordRequest, type ResolveContext, type Role, type RssOptions, type SchemaDelta, type SchemaFieldRef, type Scope, type Severity$1 as Severity, type SiteConfig, type SortFile, type StampDecision, type StampRecord, type Storage, type TemplateAnalysis, type TemplateKind, type TokenContext, type TokenRef, type TreeNode, type TypeChange, type UpdateUserRequest, type UserAdminEndpoints, type UserSummary, type ValidationResult, type ValueKind, type WorkingStore, analyzeTemplate, applyMigration, baseTemplateName, bestFitSnapshot, buildAuthoringReference, buildContentIndex, buildPictureMarkup, buildRedirects, buildResolveContext, buildRss, buildSitemap, cacheControlFor, canEdit, compilePage, compileSite, compileTemplate, computeSchemaDelta, contentHash, contentTypeFor, createDiagnosticRegistry, deriveFields, detectStamp, emptyIndex, escapeHtmlAttribute, escapeHtmlText, escapeJsonStringContent, findTokens, hasRole, humanize, inferControl, isDestructiveTypeChange, isFieldType, isReservedVersionedPath, isVersionedTemplateName, joinAuthors, loadComponentScripts, loadComponentStyles, loadComponents, loadContentTree, loadDocumentShell, loadIndex, loadRedirects, loadSiteConfig, loadTemplate, nextVersionNumber, noopPurgeService, outputPathForItem, parseContentItem, parseRedirects, parseSortFile, pendingMigrations, planPublish, processImage, publishSite, reconcileIndex, renderAuthors, renderReferenceMarkdown, sanitizeUrl, saveIndex, slugify, snapshotName, textContent, toAuthorRefs, urlForItem, validateSite, valueKindOf, versionNumber, wholeValueToken, workingStoreRoundTripDiagnostic };
1973
+ export { type Artifact, type ArtifactSink, type AuthEndpoints, type AuthResult, type AuthorDirectory, type AuthorEntry, type AuthorOption, type AuthorRef, type AuthoringReference, type BlobStore, type BoundItem, CACHE_IMMUTABLE, CACHE_REVALIDATE, type ChangePasswordRequest, type ChangePasswordSuccess, type CollectionConfig, type CollectionQuery, type CompileInput, type CompilePageOptions, type CompileSiteOptions, type CompiledPage, type ComponentMap, type ConditionalBlobStore, type ContentIndex, type ContentItem, ContentParseError, type ContentSource, type CreateTokenSuccess, type CreateUserRequest, DEFAULT_DIRS, DOCUMENT_SHELL, type DerivedField, type DiagnoseDeps, type Severity as DiagnoseSeverity, type Source as DiagnoseSource, type Diagnostic$1 as Diagnostic, type Diagnostic as DiagnosticCheck, type DiagnosticRegistry, type DirEntry, type DirNode, type EncodeRequest, type EncodedImage, type EncodedVariant, type EntryKind, FIELD_TYPES, type FieldPrimitive, type FieldRecord, type FieldType, type FieldTypeDef, type FieldValue, type Finding, type IdentityProvider, type ImageEncoder, type ImageFormat, type ImageInfo, InMemoryArtifactSink, InMemoryBlobStore, InMemoryContentSource, IndexedStore, type ItemNode, type LengthConstraints, type LoginRequest, type LoginSuccess, type MediaResolver, type MigrationResolution, NotFoundError, type PageEntry, type PendingMigrationItem, type PendingMigrations, type PreBuildHook, type Principal, type ProgressReporter, type ProgressSummary, type PublishOptions, type PublishPlan, type PublishProgress, type PublishResource, type PublishResult, type PublishSummary, type PurgeService, RESERVED_PREFIX, type ReconcileResult, type RedirectRule, type ReferenceContext, type ReferenceEntry, type ReferenceSection, type RefreshSuccess, type RenameResult, type Repair, type RepairArgs, type ResetPasswordRequest, type ResolveContext, type Role, type RssOptions, type SchemaDelta, type SchemaFieldRef, type Scope, type Severity$1 as Severity, type SiteConfig, type SortFile, type StampDecision, type StampRecord, type Storage, type TemplateAnalysis, type TemplateKind, type TokenContext, type TokenRef, type TreeNode, type TypeChange, type UpdateUserRequest, type UserAdminEndpoints, type UserSummary, type ValidationResult, type ValueKind, type VersionedIndex, type WorkingStore, analyzeTemplate, applyMigration, baseTemplateName, bestFitSnapshot, buildAuthoringReference, buildContentIndex, buildPictureMarkup, buildRedirects, buildResolveContext, buildRss, buildSitemap, cacheControlFor, canEdit, compilePage, compileSite, compileTemplate, computeSchemaDelta, contentHash, contentTypeFor, createDiagnosticRegistry, deriveFields, detectStamp, emptyIndex, escapeHtmlAttribute, escapeHtmlText, escapeJsonStringContent, findTokens, hasRole, humanize, inferControl, isConditionalBlobStore, isDestructiveTypeChange, isFieldType, isReservedVersionedPath, isVersionedTemplateName, joinAuthors, loadComponentScripts, loadComponentStyles, loadComponents, loadContentTree, loadDocumentShell, loadIndex, loadIndexVersioned, loadRedirects, loadSiteConfig, loadTemplate, nextVersionNumber, noopPurgeService, outputPathForItem, parseContentItem, parseRedirects, parseSortFile, pendingMigrations, planPublish, processImage, publishSite, reconcileIndex, renderAuthors, renderReferenceMarkdown, sanitizeUrl, saveIndex, saveIndexConditional, slugify, snapshotName, textContent, toAuthorRefs, urlForItem, validateSite, valueKindOf, versionNumber, wholeValueToken, workingStoreRoundTripDiagnostic };