@xtrable-ltd/nanoesis 0.1.31 → 0.1.32

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.d.ts +39 -3
  2. package/dist/adapter-azure-blob.js +81 -4
  3. package/dist/{chunk-BCWZRKMF.js → chunk-6Y3I6SYT.js} +8 -2
  4. package/dist/{chunk-GFQT7BYP.js → chunk-D5G56CA4.js} +189 -78
  5. package/dist/editor-api.js +2 -2
  6. package/dist/index.d.ts +91 -2
  7. package/dist/index.js +9 -1
  8. package/dist/mcp.js +3 -3
  9. package/editor/assets/{MigrationsPane-Drxje7Nq.js → MigrationsPane-BuaKhsie.js} +1 -1
  10. package/editor/assets/{TemplatesPane-CxO1IknP.js → TemplatesPane-Bg_b0htA.js} +7 -7
  11. package/editor/assets/{cssMode-BPla51Av.js → cssMode-Bcwjp8T3.js} +1 -1
  12. package/editor/assets/{freemarker2-Bob6Bse-.js → freemarker2-Cgqj3dEx.js} +1 -1
  13. package/editor/assets/{handlebars-DsQdZgzp.js → handlebars-CLf2-RZf.js} +1 -1
  14. package/editor/assets/{html-BNBtLdPe.js → html-BrWGWRh6.js} +1 -1
  15. package/editor/assets/{htmlMode-6kyy4G1O.js → htmlMode-D9skx8gp.js} +1 -1
  16. package/editor/assets/index-CY5Kehuu.js +142 -0
  17. package/editor/assets/{javascript-Dx8xuybD.js → javascript-KaABDboM.js} +1 -1
  18. package/editor/assets/{jsonMode-DWRGUqMk.js → jsonMode-0usLs2ME.js} +1 -1
  19. package/editor/assets/{liquid-DlIa11aQ.js → liquid-DYna-Clp.js} +1 -1
  20. package/editor/assets/{mdx-DR0q2TJm.js → mdx-f7LAIls6.js} +1 -1
  21. package/editor/assets/{python-Cbswpzux.js → python-Bh21_mvq.js} +1 -1
  22. package/editor/assets/{razor-IYEYh5Ox.js → razor-D_530Ymg.js} +1 -1
  23. package/editor/assets/{tsMode-DQrbmWSC.js → tsMode-CTRVjt9n.js} +1 -1
  24. package/editor/assets/{typescript-DiBU2jVJ.js → typescript-D084tkOf.js} +1 -1
  25. package/editor/assets/{xml-CCxs0QRL.js → xml-BFulX1C6.js} +1 -1
  26. package/editor/assets/{yaml-C1tH0_iR.js → yaml-CgZsYmUt.js} +1 -1
  27. package/editor/index.html +1 -1
  28. package/package.json +1 -1
  29. 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",
@@ -3472,9 +3579,12 @@ export {
3472
3579
  ContentParseError,
3473
3580
  parseContentItem,
3474
3581
  parseSortFile,
3582
+ isConditionalBlobStore,
3475
3583
  InMemoryBlobStore,
3476
3584
  parseRedirects,
3477
3585
  buildRedirects,
3586
+ NotFoundError,
3587
+ InMemoryContentSource,
3478
3588
  DEFAULT_DIRS,
3479
3589
  loadContentTree,
3480
3590
  loadRedirects,
@@ -3504,10 +3614,11 @@ export {
3504
3614
  RESERVED_PREFIX,
3505
3615
  emptyIndex,
3506
3616
  loadIndex,
3617
+ loadIndexVersioned,
3618
+ saveIndexConditional,
3507
3619
  saveIndex,
3508
3620
  reconcileIndex,
3509
3621
  IndexedStore,
3510
- InMemoryContentSource,
3511
3622
  analyzeTemplate,
3512
3623
  pendingMigrations,
3513
3624
  bestFitSnapshot,
@@ -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-6Y3I6SYT.js";
25
+ import "./chunk-D5G56CA4.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
@@ -180,12 +211,22 @@ interface Storage extends BlobStore {
180
211
  * interchangeable). It copies bytes in and out, so a caller can never mutate stored
181
212
  * data by holding on to a buffer.
182
213
  */
183
- declare class InMemoryBlobStore implements BlobStore {
214
+ declare class InMemoryBlobStore implements BlobStore, ConditionalBlobStore {
184
215
  private readonly blobs;
216
+ /** Per-key version, bumped on every write, so {@link putConditional} can detect a
217
+ * concurrent overwrite the same way an ETag does on a real store. */
218
+ private readonly versions;
219
+ private nextVersion;
185
220
  constructor(initial?: Readonly<Record<string, Uint8Array | string>>);
221
+ private set;
186
222
  get(key: string): Promise<Uint8Array | undefined>;
187
223
  put(key: string, bytes: Uint8Array): Promise<void>;
188
224
  delete(key: string): Promise<void>;
225
+ getVersioned(key: string): Promise<{
226
+ bytes: Uint8Array;
227
+ version: string;
228
+ } | undefined>;
229
+ putConditional(key: string, bytes: Uint8Array, expected: string | null): Promise<string | undefined>;
189
230
  }
190
231
 
191
232
  /**
@@ -195,6 +236,17 @@ declare class InMemoryBlobStore implements BlobStore {
195
236
  * relative to the site root (e.g. "content/blog/post.json").
196
237
  */
197
238
  type EntryKind = 'file' | 'dir';
239
+ /**
240
+ * Thrown by a {@link ContentSource} read when the path does not exist, as opposed to a
241
+ * real I/O failure (a network error, a permission error). Loaders catch *this* to tell
242
+ * index drift (a key the index lists but whose blob is gone) apart from an error they must
243
+ * not swallow: a listed-but-missing item is skipped, a genuine read failure still
244
+ * propagates. Carries the requested path for diagnostics.
245
+ */
246
+ declare class NotFoundError extends Error {
247
+ readonly path: string;
248
+ constructor(path: string);
249
+ }
198
250
  interface DirEntry {
199
251
  readonly name: string;
200
252
  readonly kind: EntryKind;
@@ -874,6 +926,28 @@ declare function emptyIndex(): ContentIndex;
874
926
  * but cannot be enumerated until something rewrites the index).
875
927
  */
876
928
  declare function loadIndex(store: BlobStore): Promise<ContentIndex>;
929
+ /** The index paired with the live index blob's version tag, for a compare-and-set save
930
+ * (DESIGN §11d). `version` is `null` when no live index blob exists yet, so the first save
931
+ * creates it (`putConditional` with `expected: null`). */
932
+ interface VersionedIndex {
933
+ readonly index: ContentIndex;
934
+ readonly version: string | null;
935
+ }
936
+ /**
937
+ * Load the index together with the version tag needed to write it back safely under
938
+ * optimistic concurrency. A present-but-corrupt live blob recovers from the backup ring but
939
+ * keeps the live version, so the next {@link saveIndexConditional} overwrites the corrupt
940
+ * copy rather than failing forever; an absent live blob recovers read-only with `version:
941
+ * null`.
942
+ */
943
+ declare function loadIndexVersioned(store: BlobStore & ConditionalBlobStore): Promise<VersionedIndex>;
944
+ /**
945
+ * Compare-and-set save (DESIGN §11d): back up the index being replaced (best-effort,
946
+ * unconditional), then write the new index *only if* the live version still matches the one
947
+ * read in `prev`. Returns the new {@link VersionedIndex} on success, or `undefined` when a
948
+ * concurrent writer won the race, the caller re-reads and re-applies its delta, then retries.
949
+ */
950
+ declare function saveIndexConditional(store: BlobStore & ConditionalBlobStore, prev: VersionedIndex, nextKeys: readonly string[]): Promise<VersionedIndex | undefined>;
877
951
  /**
878
952
  * Write a new index whose keys are `nextKeys`, backing up the one it replaces first
879
953
  * (DESIGN §11d). The previous index goes into its ring slot (`version % ring`, oldest
@@ -943,6 +1017,21 @@ declare class IndexedStore implements WorkingStore {
943
1017
  /** The index, loaded once on first need and cached (mutations replace the cached copy). */
944
1018
  private loaded;
945
1019
  private serializeMutation;
1020
+ /**
1021
+ * Apply a key-set change to the index and persist it (DESIGN §11d). `mutate` receives the
1022
+ * current key set and adds/removes in place; it must be a pure function of that set (it can
1023
+ * be re-run on a fresh read), because on a store with the optimistic-concurrency capability
1024
+ * this runs as a compare-and-set retry loop: read the live index + version, apply `mutate`,
1025
+ * write only if the version is unchanged, and retry on a conflict. This is what stops a
1026
+ * second host instance's blind overwrite from evicting a key this one just added
1027
+ * (NANOESIS-MCP-ISSUES Issue 1). A store without the capability keeps the single-writer
1028
+ * path, correct under the in-process {@link mutationLock} alone. Either way an unchanged key
1029
+ * set writes nothing (overwriting an existing item stays a single blob `put`).
1030
+ *
1031
+ * Callers do their blob writes/deletes first (content-first, §11d) and pass only the
1032
+ * resulting index delta here, so a failed blob op never advances the index.
1033
+ */
1034
+ private commitIndex;
946
1035
  list(dir: string): Promise<readonly DirEntry[]>;
947
1036
  readText(path: string): Promise<string>;
948
1037
  readBytes(path: string): Promise<Uint8Array>;
@@ -1870,4 +1959,4 @@ declare function createDiagnosticRegistry(): DiagnosticRegistry;
1870
1959
 
1871
1960
  declare const workingStoreRoundTripDiagnostic: Diagnostic;
1872
1961
 
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 };
1962
+ 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 };
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  InMemoryBlobStore,
10
10
  InMemoryContentSource,
11
11
  IndexedStore,
12
+ NotFoundError,
12
13
  RESERVED_PREFIX,
13
14
  analyzeTemplate,
14
15
  applyMigration,
@@ -40,6 +41,7 @@ import {
40
41
  hasRole,
41
42
  humanize,
42
43
  inferControl,
44
+ isConditionalBlobStore,
43
45
  isDestructiveTypeChange,
44
46
  isFieldType,
45
47
  isReservedVersionedPath,
@@ -51,6 +53,7 @@ import {
51
53
  loadContentTree,
52
54
  loadDocumentShell,
53
55
  loadIndex,
56
+ loadIndexVersioned,
54
57
  loadRedirects,
55
58
  loadSiteConfig,
56
59
  loadTemplate,
@@ -69,6 +72,7 @@ import {
69
72
  renderReferenceMarkdown,
70
73
  sanitizeUrl,
71
74
  saveIndex,
75
+ saveIndexConditional,
72
76
  slugify,
73
77
  snapshotName,
74
78
  textContent,
@@ -79,7 +83,7 @@ import {
79
83
  versionNumber,
80
84
  wholeValueToken,
81
85
  workingStoreRoundTripDiagnostic
82
- } from "./chunk-GFQT7BYP.js";
86
+ } from "./chunk-D5G56CA4.js";
83
87
  export {
84
88
  CACHE_IMMUTABLE,
85
89
  CACHE_REVALIDATE,
@@ -91,6 +95,7 @@ export {
91
95
  InMemoryBlobStore,
92
96
  InMemoryContentSource,
93
97
  IndexedStore,
98
+ NotFoundError,
94
99
  RESERVED_PREFIX,
95
100
  analyzeTemplate,
96
101
  applyMigration,
@@ -122,6 +127,7 @@ export {
122
127
  hasRole,
123
128
  humanize,
124
129
  inferControl,
130
+ isConditionalBlobStore,
125
131
  isDestructiveTypeChange,
126
132
  isFieldType,
127
133
  isReservedVersionedPath,
@@ -133,6 +139,7 @@ export {
133
139
  loadContentTree,
134
140
  loadDocumentShell,
135
141
  loadIndex,
142
+ loadIndexVersioned,
136
143
  loadRedirects,
137
144
  loadSiteConfig,
138
145
  loadTemplate,
@@ -151,6 +158,7 @@ export {
151
158
  renderReferenceMarkdown,
152
159
  sanitizeUrl,
153
160
  saveIndex,
161
+ saveIndexConditional,
154
162
  slugify,
155
163
  snapshotName,
156
164
  textContent,
package/dist/mcp.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  MCP_TOOLS,
4
4
  callMcpTool,
5
5
  readMcpResource
6
- } from "./chunk-BCWZRKMF.js";
7
- import "./chunk-GFQT7BYP.js";
6
+ } from "./chunk-6Y3I6SYT.js";
7
+ import "./chunk-D5G56CA4.js";
8
8
 
9
9
  // ../../hosts/host-mcp/src/http.ts
10
10
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
@@ -56,7 +56,7 @@ function createMcpServer(deps, identity) {
56
56
  }
57
57
 
58
58
  // ../../hosts/host-mcp/src/http.ts
59
- var DEFAULT_VERSION = true ? "0.1.31" : "0.0.0-workspace";
59
+ var DEFAULT_VERSION = true ? "0.1.32" : "0.0.0-workspace";
60
60
  async function handleMcpRequest(deps, request, opts) {
61
61
  const server = createMcpServer(deps, {
62
62
  name: opts?.name ?? "nanoesis",