@xtrable-ltd/nanoesis 0.1.1 → 0.1.4

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 (27) hide show
  1. package/dist/adapter-azure-blob.js +1 -1
  2. package/dist/{chunk-G2UEZTYC.js → chunk-S23S4ZAK.js} +50 -2
  3. package/dist/editor-api.d.ts +38 -4
  4. package/dist/editor-api.js +165 -3
  5. package/dist/index.d.ts +86 -34
  6. package/dist/index.js +5 -1
  7. package/editor/assets/{TemplatesPane-5qsDAK_B.js → TemplatesPane-Cjwu8SOY.js} +7 -7
  8. package/editor/assets/{cssMode-CGp4MIjR.js → cssMode-CIqtqlOX.js} +1 -1
  9. package/editor/assets/{freemarker2-CJkwxmPv.js → freemarker2-03aLbgAQ.js} +1 -1
  10. package/editor/assets/{handlebars-CKb5i2nM.js → handlebars-CsCT0Kz-.js} +1 -1
  11. package/editor/assets/{html-DyMbQx0w.js → html-BMDaxhoV.js} +1 -1
  12. package/editor/assets/{htmlMode-DVPeqtn-.js → htmlMode-BmX-GJzB.js} +1 -1
  13. package/editor/assets/{index-CbuWEnUB.css → index-DSooOnUR.css} +1 -1
  14. package/editor/assets/index-xCwd0bS3.js +138 -0
  15. package/editor/assets/{javascript-Bp1Qh9wR.js → javascript-y8MJYJ58.js} +1 -1
  16. package/editor/assets/{jsonMode-FLEeVtx7.js → jsonMode-DPpxOVap.js} +1 -1
  17. package/editor/assets/{liquid-Bh8c534t.js → liquid-CGoDffSW.js} +1 -1
  18. package/editor/assets/{mdx-BUbo8M9l.js → mdx-CMvr2LP2.js} +1 -1
  19. package/editor/assets/{python-CuJlk8g3.js → python-Dg93CFNM.js} +1 -1
  20. package/editor/assets/{razor-CuQT_1Ku.js → razor-D8HipAex.js} +1 -1
  21. package/editor/assets/{tsMode-CT2HUNtN.js → tsMode-Drl3yYU0.js} +1 -1
  22. package/editor/assets/{typescript-CtMx97cn.js → typescript-Dcqumk7N.js} +1 -1
  23. package/editor/assets/{xml-CyfpINj_.js → xml-zoMMlERG.js} +1 -1
  24. package/editor/assets/{yaml-BBWmgfMA.js → yaml-BMaJnJHN.js} +1 -1
  25. package/editor/index.html +2 -2
  26. package/package.json +1 -1
  27. package/editor/assets/index-DJmSgobK.js +0 -129
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  contentTypeFor
3
- } from "./chunk-G2UEZTYC.js";
3
+ } from "./chunk-S23S4ZAK.js";
4
4
 
5
5
  // ../../adapters/azure-blob/src/container.ts
6
6
  var InMemoryBlobContainer = class {
@@ -182,6 +182,16 @@ async function saveIndex(store, prev, nextKeys) {
182
182
  await store.put(INDEX_KEY, serialize(next));
183
183
  return next;
184
184
  }
185
+ async function reconcileIndex(store, actualKeys) {
186
+ const prev = await loadIndex(store);
187
+ const keys = [...new Set(actualKeys.filter((key) => !key.startsWith(RESERVED_PREFIX)))].sort();
188
+ const prevKeys = new Set(prev.keys);
189
+ const nextKeys = new Set(keys);
190
+ const added = keys.filter((key) => !prevKeys.has(key));
191
+ const removed = prev.keys.filter((key) => !nextKeys.has(key));
192
+ if (added.length === 0 && removed.length === 0) return { index: prev, added, removed };
193
+ return { index: await saveIndex(store, prev, keys), added, removed };
194
+ }
185
195
  function freezeIndex(version, keys) {
186
196
  const sorted = [...new Set(keys)].sort();
187
197
  return { version, keys: sorted, checksum: checksumOf(version, sorted) };
@@ -312,6 +322,19 @@ var IndexedStore = class {
312
322
  this.index = await saveIndex(this.store, index, next);
313
323
  return { ok: true };
314
324
  }
325
+ /**
326
+ * Rebuild this store's index from the *actual* keys the underlying store holds (DESIGN
327
+ * §11d), recovering files that arrived by a path that bypassed the index. A
328
+ * {@link BlobStore} cannot enumerate itself, so the caller supplies the real key set
329
+ * from an adapter that can (e.g. `BlobContainer.list`). Unlike calling
330
+ * {@link reconcileIndex} on a fresh store, this also refreshes the cached in-memory
331
+ * index, so this live instance sees the recovered files immediately.
332
+ */
333
+ async reconcile(actualKeys) {
334
+ const result = await reconcileIndex(this.store, actualKeys);
335
+ this.index = result.index;
336
+ return result;
337
+ }
315
338
  };
316
339
  function guarded(key) {
317
340
  if (key === "" || key.startsWith(RESERVED_PREFIX)) {
@@ -2407,8 +2430,9 @@ var noopPurgeService = {
2407
2430
  var DEFAULT_WRITE_CONCURRENCY = 8;
2408
2431
  async function publishSite(source, sink, options = {}) {
2409
2432
  const validation = await validateSite(source);
2433
+ const summary = summarizeTree(await loadContentTree(source));
2410
2434
  if (!validation.ok) {
2411
- return { ok: false, validation, written: [] };
2435
+ return { ok: false, validation, written: [], summary };
2412
2436
  }
2413
2437
  const {
2414
2438
  purge = noopPurgeService,
@@ -2431,7 +2455,29 @@ async function publishSite(source, sink, options = {}) {
2431
2455
  ([path, contents]) => sink.write(path, contents)
2432
2456
  );
2433
2457
  await purge.purgeAll();
2434
- return { ok: true, validation, written: [...byPath.keys()].sort() };
2458
+ return { ok: true, validation, written: [...byPath.keys()].sort(), summary };
2459
+ }
2460
+ function summarizeTree(root) {
2461
+ let published = 0;
2462
+ let drafts = 0;
2463
+ const walk = (node) => {
2464
+ for (const child of node.children) {
2465
+ if (child.kind === "dir") {
2466
+ walk(child);
2467
+ } else if (child.item.isPublished) {
2468
+ published += 1;
2469
+ } else {
2470
+ drafts += 1;
2471
+ }
2472
+ }
2473
+ };
2474
+ walk(root);
2475
+ if (published > 0) return { published, drafts };
2476
+ return {
2477
+ published,
2478
+ drafts,
2479
+ firstPublishWarning: drafts > 0 ? `Your site has no published pages \u2014 ${drafts} draft${drafts === 1 ? "" : "s"} were skipped. Toggle Publish on at least one page to go live.` : "Your site has no published pages. Create a page and toggle Publish on to go live."
2480
+ };
2435
2481
  }
2436
2482
  async function collectPublic(source, publicDir) {
2437
2483
  if (!await source.exists(publicDir)) return [];
@@ -2485,9 +2531,11 @@ export {
2485
2531
  parseContentItem,
2486
2532
  parseSortFile,
2487
2533
  InMemoryBlobStore,
2534
+ RESERVED_PREFIX,
2488
2535
  emptyIndex,
2489
2536
  loadIndex,
2490
2537
  saveIndex,
2538
+ reconcileIndex,
2491
2539
  IndexedStore,
2492
2540
  InMemoryContentSource,
2493
2541
  parseRedirects,
@@ -1,4 +1,4 @@
1
- import { WorkingStore, IdentityProvider, PublishResult, AuthEndpoints, UserAdminEndpoints, AuthorOption, UserSummary, AuthorDirectory } from '@nanoesis/engine';
1
+ import { WorkingStore, IdentityProvider, PublishResult, ReconcileResult, AuthEndpoints, UserAdminEndpoints, AuthorOption, UserSummary, AuthorDirectory } from '@nanoesis/engine';
2
2
 
3
3
  /**
4
4
  * Editor white-labelling storage (DESIGN §11, Phase B). This is the *editor app's*
@@ -100,6 +100,15 @@ interface ApiDeps {
100
100
  readonly identity: IdentityProvider;
101
101
  /** Run the publish pipeline (host binds source/sink/purge). */
102
102
  readonly publish: () => Promise<PublishResult>;
103
+ /**
104
+ * Optional index-reconcile (DESIGN §11d): rebuild the working store's content index
105
+ * from the keys actually present, recovering files that arrived by a path that
106
+ * bypassed the index (the silent-bootstrap failure). When present, the router mounts
107
+ * the admin-gated `POST /api/reconcile`. The host binds it because enumerating the
108
+ * real key set needs the adapter's listing (`BlobContainer.list`), which the engine's
109
+ * `BlobStore` deliberately lacks. Omitted on stores that cannot enumerate.
110
+ */
111
+ readonly reconcile?: () => Promise<ReconcileResult>;
103
112
  /**
104
113
  * Optional credential-management surface (DESIGN §11). When present, the router
105
114
  * mounts `/api/login`, `/api/refresh`, `/api/logout`, and `/api/auth/state`,
@@ -131,8 +140,12 @@ interface ApiDeps {
131
140
  */
132
141
  readonly authors?: () => Promise<readonly AuthorOption[]>;
133
142
  }
134
- /** Dispatch one API request. The editor is private, so every route requires an editing
135
- * role (author/developer/admin); writes are further partitioned by path (DESIGN §11). */
143
+ /**
144
+ * Dispatch one API request. The editor is private, so every route requires an editing
145
+ * role (author/developer/admin); writes are further partitioned by path (DESIGN §11).
146
+ * Every response then passes through {@link withDefaultHeaders} so that no `/api/*`
147
+ * response is silently cached (DESIGN §13).
148
+ */
136
149
  declare function handleApi(deps: ApiDeps, req: ApiRequest): Promise<ApiResponse>;
137
150
 
138
151
  /** The byline picker's options, display name + stable handle, sorted by display name. */
@@ -195,4 +208,25 @@ declare function readMcpResource(uri: string): {
195
208
  readonly mimeType: string;
196
209
  } | undefined;
197
210
 
198
- export { type ApiDeps, type ApiRequest, type ApiResponse, type BrandingLogo, type BrandingLogoMeta, type BrandingState, type BrandingStore, FileBrandingStore, InMemoryBrandingStore, MCP_RESOURCES, MCP_TOOLS, type McpCallOptions, type McpResourceDef, type McpToolDef, type McpToolResult, authorDirectory, authorOptions, callMcpTool, handleApi, readMcpResource };
211
+ /**
212
+ * The first-run starter site (DESIGN §14): a minimal, self-contained set of files that
213
+ * turns an empty working store into a real, publishable site. It is the *blessed* way to
214
+ * get initial content in — written through the editor API (so the content index stays
215
+ * authoritative by construction, never the bypassing path that silently breaks a store,
216
+ * §11d) and bundled here as **pure data**, so the engine's purity is untouched and the
217
+ * scaffold works identically for every host and the MCP server with no per-host wiring.
218
+ *
219
+ * Deliberately minimal (the owner's call, distinct from the fuller `examples/starter`):
220
+ * one home page, a page template, the document shell, and a stylesheet. It is gate-clean
221
+ * by construction — no collection loops (nothing to point at yet), no custom components,
222
+ * no required fields left unset — so a fresh adopter can publish it immediately, and the
223
+ * home page is a **draft** (`isPublished: false`) so going live is a deliberate act.
224
+ */
225
+ /**
226
+ * The starter files, keyed by working-store path. Written through the store's `write`
227
+ * (so the index records each), in this order — templates and styles before the content
228
+ * that depends on them, though the store has no cross-file ordering requirement.
229
+ */
230
+ declare const SCAFFOLD_FILES: Readonly<Record<string, string>>;
231
+
232
+ export { type ApiDeps, type ApiRequest, type ApiResponse, type BrandingLogo, type BrandingLogoMeta, type BrandingState, type BrandingStore, FileBrandingStore, InMemoryBrandingStore, MCP_RESOURCES, MCP_TOOLS, type McpCallOptions, type McpResourceDef, type McpToolDef, type McpToolResult, SCAFFOLD_FILES, authorDirectory, authorOptions, callMcpTool, handleApi, readMcpResource };
@@ -8,7 +8,111 @@ import {
8
8
  loadTemplate,
9
9
  renderReferenceMarkdown,
10
10
  validateSite
11
- } from "./chunk-G2UEZTYC.js";
11
+ } from "./chunk-S23S4ZAK.js";
12
+
13
+ // ../editor-api/src/scaffold.ts
14
+ var DOCUMENT_HTML = `<!doctype html>
15
+ <html lang="en">
16
+ <head>
17
+ <meta charset="utf-8" />
18
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
19
+ <title>{title}</title>
20
+ <meta name="description" content="{meta_description}" />
21
+ <link rel="stylesheet" href="/styles.css" />
22
+ </head>
23
+ <body>
24
+ <header class="site-header">
25
+ <a class="brand" href="/">Your site</a>
26
+ </header>
27
+ <main>
28
+ <slot></slot>
29
+ </main>
30
+ <footer class="site-footer">
31
+ <p>Built with nanoesis \u2014 static HTML, no runtime.</p>
32
+ </footer>
33
+ </body>
34
+ </html>
35
+ `;
36
+ var HOME_HTML = `<section class="hero">
37
+ <h1>{title}</h1>
38
+ <p class="lead">{lead}</p>
39
+ </section>
40
+ <div class="prose" data-type="richtext">{body}</div>
41
+ `;
42
+ var HOME_JSON = `${JSON.stringify(
43
+ {
44
+ template: "home",
45
+ title: "Welcome",
46
+ isPublished: false,
47
+ fields: {
48
+ lead: "Edit this page in the nanoesis editor, then publish when you are ready.",
49
+ body: "<p>This is your new site. It compiles to plain static HTML \u2014 there is no server and no client framework, and nothing of the editor survives into production.</p>"
50
+ }
51
+ },
52
+ null,
53
+ 2
54
+ )}
55
+ `;
56
+ var STYLES_CSS = `:root {
57
+ --ink: #1a1a2e;
58
+ --muted: #555;
59
+ --accent: #4f46e5;
60
+ --max: 42rem;
61
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
62
+ line-height: 1.6;
63
+ color: var(--ink);
64
+ }
65
+
66
+ body {
67
+ margin: 0;
68
+ }
69
+
70
+ .site-header,
71
+ main,
72
+ .site-footer {
73
+ max-width: var(--max);
74
+ margin-inline: auto;
75
+ padding: 1.5rem 1.25rem;
76
+ }
77
+
78
+ .site-header .brand {
79
+ font-weight: 700;
80
+ text-decoration: none;
81
+ color: var(--ink);
82
+ }
83
+
84
+ .hero h1 {
85
+ font-size: 2.25rem;
86
+ letter-spacing: -0.01em;
87
+ margin: 0 0 0.5rem;
88
+ }
89
+
90
+ .lead {
91
+ font-size: 1.2rem;
92
+ color: var(--muted);
93
+ margin: 0 0 2rem;
94
+ }
95
+
96
+ .prose {
97
+ font-size: 1.05rem;
98
+ }
99
+
100
+ a {
101
+ color: var(--accent);
102
+ }
103
+
104
+ .site-footer {
105
+ color: var(--muted);
106
+ font-size: 0.9rem;
107
+ border-top: 1px solid #eee;
108
+ }
109
+ `;
110
+ var SCAFFOLD_FILES = {
111
+ "templates/document.html": DOCUMENT_HTML,
112
+ "templates/home.html": HOME_HTML,
113
+ "public/styles.css": STYLES_CSS,
114
+ "content/index.json": HOME_JSON
115
+ };
12
116
 
13
117
  // ../editor-api/src/api.ts
14
118
  var MAX_LOGO_BYTES = 1024 * 1024;
@@ -32,6 +136,14 @@ function json(status, data) {
32
136
  function methodNotAllowed() {
33
137
  return json(405, { ok: false, error: "POST only" });
34
138
  }
139
+ function withDefaultHeaders(response) {
140
+ const headers = { ...response.headers ?? {} };
141
+ if (headers["cache-control"] === void 0) {
142
+ headers["cache-control"] = "no-store";
143
+ if (headers["vary"] === void 0) headers["vary"] = "Authorization";
144
+ }
145
+ return { ...response, headers };
146
+ }
35
147
  var BEARER_PREFIX = /^Bearer\s+/i;
36
148
  function bearerToken(getHeader) {
37
149
  const raw = getHeader("authorization") ?? getHeader("Authorization");
@@ -200,6 +312,9 @@ async function handleBrandingRoute(deps, req) {
200
312
  return json(405, { ok: false, error: "method not allowed" });
201
313
  }
202
314
  async function handleApi(deps, req) {
315
+ return withDefaultHeaders(await dispatchApi(deps, req));
316
+ }
317
+ async function dispatchApi(deps, req) {
203
318
  const authResponse = await handleAuthRoute(deps, req);
204
319
  if (authResponse !== void 0) return authResponse;
205
320
  const brandingResponse = await handleBrandingRoute(deps, req);
@@ -280,8 +395,54 @@ async function handleApi(deps, req) {
280
395
  case "/api/publish": {
281
396
  if (req.method !== "POST") return methodNotAllowed();
282
397
  const result = await deps.publish();
283
- if (result.ok) return json(200, { ok: true, written: result.written.length });
284
- return json(422, { ok: false, errors: result.validation.errors.map((e) => e.message) });
398
+ if (result.ok) {
399
+ return json(200, {
400
+ ok: true,
401
+ written: result.written.length,
402
+ ...result.summary !== void 0 && { summary: result.summary }
403
+ });
404
+ }
405
+ return json(422, {
406
+ ok: false,
407
+ errors: result.validation.errors.map((e) => e.message),
408
+ ...result.summary !== void 0 && { summary: result.summary }
409
+ });
410
+ }
411
+ case "/api/scaffold": {
412
+ if (req.method !== "POST") return methodNotAllowed();
413
+ if (!hasRole(principal, "admin")) {
414
+ return json(403, { ok: false, error: "admin role required" });
415
+ }
416
+ if ((await deps.store.list("content")).length > 0) {
417
+ return json(409, { ok: false, error: "site already has content" });
418
+ }
419
+ const created = [];
420
+ const skipped = [];
421
+ for (const [path, content] of Object.entries(SCAFFOLD_FILES)) {
422
+ if (await deps.store.exists(path)) {
423
+ skipped.push(path);
424
+ continue;
425
+ }
426
+ await deps.store.write(path, new TextEncoder().encode(content));
427
+ created.push(path);
428
+ }
429
+ return json(200, { ok: true, created, skipped });
430
+ }
431
+ case "/api/reconcile": {
432
+ if (req.method !== "POST") return methodNotAllowed();
433
+ if (deps.reconcile === void 0) {
434
+ return json(404, { ok: false, error: "reconcile is not available on this host" });
435
+ }
436
+ if (!hasRole(principal, "admin")) {
437
+ return json(403, { ok: false, error: "admin role required" });
438
+ }
439
+ const result = await deps.reconcile();
440
+ return json(200, {
441
+ ok: true,
442
+ added: result.added.length,
443
+ removed: result.removed.length,
444
+ total: result.index.keys.length
445
+ });
285
446
  }
286
447
  case "/api/me/password": {
287
448
  if (req.method !== "POST") return methodNotAllowed();
@@ -584,6 +745,7 @@ export {
584
745
  InMemoryBrandingStore,
585
746
  MCP_RESOURCES,
586
747
  MCP_TOOLS,
748
+ SCAFFOLD_FILES,
587
749
  authorDirectory,
588
750
  authorOptions,
589
751
  callMcpTool,
package/dist/index.d.ts CHANGED
@@ -203,6 +203,62 @@ interface WorkingStore extends ContentSource {
203
203
  rename(from: string, to: string): Promise<RenameResult>;
204
204
  }
205
205
 
206
+ /**
207
+ * The content index (DESIGN §11d): the sorted set of every key in the working store,
208
+ * the directory listing a flat blob store cannot give cheaply, lifted into one object
209
+ * we own. Enumeration (the editor tree, compile) reads this, so the adopter's store
210
+ * stays get/put/delete with no `list`. Persisted under a reserved key with a fixed-size
211
+ * backup ring for integrity.
212
+ */
213
+ interface ContentIndex {
214
+ /** Monotonic, bumped on every save; orders the backup ring during recovery. */
215
+ readonly version: number;
216
+ /** Every key that exists, sorted and de-duplicated (deterministic, CLAUDE §2). */
217
+ readonly keys: readonly string[];
218
+ /** Checksum over version + keys, so a corrupt stored index is caught on read. */
219
+ readonly checksum: string;
220
+ }
221
+ /** Reserved key prefix for the index and its backups; excluded from enumeration. */
222
+ declare const RESERVED_PREFIX = ".nanoesis/";
223
+ /** The empty index of a fresh store: version 0, no keys. */
224
+ declare function emptyIndex(): ContentIndex;
225
+ /**
226
+ * Load the index, recovering through the backup ring if the live copy is missing or
227
+ * corrupt (DESIGN §11d): the highest valid version among the ring slots wins. Returns
228
+ * the empty index when nothing valid is found, a fresh store, or the accepted residual
229
+ * case where the live index and every backup are lost (the files still exist by key,
230
+ * but cannot be enumerated until something rewrites the index).
231
+ */
232
+ declare function loadIndex(store: BlobStore): Promise<ContentIndex>;
233
+ /**
234
+ * Write a new index whose keys are `nextKeys`, backing up the one it replaces first
235
+ * (DESIGN §11d). The previous index goes into its ring slot (`version % ring`, oldest
236
+ * overwritten), then the new index is written to the live key, a constant two writes
237
+ * per save regardless of ring size. Returns the new index (the caller's next `prev`).
238
+ */
239
+ declare function saveIndex(store: BlobStore, prev: ContentIndex, nextKeys: readonly string[]): Promise<ContentIndex>;
240
+ /** What a {@link reconcileIndex} did, for a maintenance UI/CLI and the startup signal. */
241
+ interface ReconcileResult {
242
+ /** The index after reconciling (the prior one unchanged when nothing differed). */
243
+ readonly index: ContentIndex;
244
+ /** Keys now in the index that the prior index lacked (orphaned blobs, recovered). */
245
+ readonly added: readonly string[];
246
+ /** Keys the prior index held that no longer exist in the store (stale entries dropped). */
247
+ readonly removed: readonly string[];
248
+ }
249
+ /**
250
+ * Rebuild the index from the store's *actual* keys (DESIGN §11d): the safety net for a
251
+ * store that got files by a path that bypassed the index, where the files are present
252
+ * by key but unenumerable. The engine cannot list a {@link BlobStore} (none is required
253
+ * to, §11c), so the caller supplies the real key set, an adapter that *can* enumerate
254
+ * (e.g. `BlobContainer.list`) provides it; the rebuild stays pure index logic here.
255
+ *
256
+ * The reserved namespace is never indexed (the index is not its own content). When the
257
+ * resulting key set matches the existing index this is a no-op, no write, no version
258
+ * bump, so reconciling a healthy store is cheap and idempotent.
259
+ */
260
+ declare function reconcileIndex(store: BlobStore, actualKeys: readonly string[]): Promise<ReconcileResult>;
261
+
206
262
  /**
207
263
  * A {@link ContentSource} backed by the content index (DESIGN §11d) over a
208
264
  * {@link BlobStore}: enumeration (`list`/`exists`) is synthesised from the index's key
@@ -251,41 +307,17 @@ declare class IndexedStore implements WorkingStore {
251
307
  * still throws.)
252
308
  */
253
309
  rename(from: string, to: string): Promise<RenameResult>;
310
+ /**
311
+ * Rebuild this store's index from the *actual* keys the underlying store holds (DESIGN
312
+ * §11d), recovering files that arrived by a path that bypassed the index. A
313
+ * {@link BlobStore} cannot enumerate itself, so the caller supplies the real key set
314
+ * from an adapter that can (e.g. `BlobContainer.list`). Unlike calling
315
+ * {@link reconcileIndex} on a fresh store, this also refreshes the cached in-memory
316
+ * index, so this live instance sees the recovered files immediately.
317
+ */
318
+ reconcile(actualKeys: readonly string[]): Promise<ReconcileResult>;
254
319
  }
255
320
 
256
- /**
257
- * The content index (DESIGN §11d): the sorted set of every key in the working store,
258
- * the directory listing a flat blob store cannot give cheaply, lifted into one object
259
- * we own. Enumeration (the editor tree, compile) reads this, so the adopter's store
260
- * stays get/put/delete with no `list`. Persisted under a reserved key with a fixed-size
261
- * backup ring for integrity.
262
- */
263
- interface ContentIndex {
264
- /** Monotonic, bumped on every save; orders the backup ring during recovery. */
265
- readonly version: number;
266
- /** Every key that exists, sorted and de-duplicated (deterministic, CLAUDE §2). */
267
- readonly keys: readonly string[];
268
- /** Checksum over version + keys, so a corrupt stored index is caught on read. */
269
- readonly checksum: string;
270
- }
271
- /** The empty index of a fresh store: version 0, no keys. */
272
- declare function emptyIndex(): ContentIndex;
273
- /**
274
- * Load the index, recovering through the backup ring if the live copy is missing or
275
- * corrupt (DESIGN §11d): the highest valid version among the ring slots wins. Returns
276
- * the empty index when nothing valid is found, a fresh store, or the accepted residual
277
- * case where the live index and every backup are lost (the files still exist by key,
278
- * but cannot be enumerated until something rewrites the index).
279
- */
280
- declare function loadIndex(store: BlobStore): Promise<ContentIndex>;
281
- /**
282
- * Write a new index whose keys are `nextKeys`, backing up the one it replaces first
283
- * (DESIGN §11d). The previous index goes into its ring slot (`version % ring`, oldest
284
- * overwritten), then the new index is written to the live key, a constant two writes
285
- * per save regardless of ring size. Returns the new index (the caller's next `prev`).
286
- */
287
- declare function saveIndex(store: BlobStore, prev: ContentIndex, nextKeys: readonly string[]): Promise<ContentIndex>;
288
-
289
321
  /**
290
322
  * The authors byline (DESIGN §6.11). An `authors` field stores an ordered list of
291
323
  * author refs; the compiler renders them as one grammatical line. Everything here
@@ -962,6 +994,24 @@ interface PublishOptions extends CompileSiteOptions {
962
994
  /** Max sink writes in flight at once (default {@link DEFAULT_WRITE_CONCURRENCY}). */
963
995
  readonly writeConcurrency?: number;
964
996
  }
997
+ /**
998
+ * What the publish actually did to *content*, as opposed to artifacts. Counts come from a
999
+ * walk of the content tree (the same definition the compiler uses, `item.isPublished`).
1000
+ * Surfaced so a host/UI can explain a publish whose written-file count looks surprising,
1001
+ * the silent first-publish footgun: a published site with no published pages.
1002
+ */
1003
+ interface PublishSummary {
1004
+ /** Content items with `isPublished: true` — the pages the publish included. */
1005
+ readonly published: number;
1006
+ /** Content items with `isPublished` false/absent — the pages the publish skipped. */
1007
+ readonly drafts: number;
1008
+ /**
1009
+ * A friendly one-liner when `published === 0`, so a UI can warn an operator who just
1010
+ * scaffolded a draft home and hit Publish that nothing went live. Absent when at least
1011
+ * one page is published.
1012
+ */
1013
+ readonly firstPublishWarning?: string;
1014
+ }
965
1015
  interface PublishResult {
966
1016
  /** True when the validation gate passed and the site was published. */
967
1017
  readonly ok: boolean;
@@ -969,6 +1019,8 @@ interface PublishResult {
969
1019
  readonly validation: ValidationResult;
970
1020
  /** Output paths written to the sink, sorted; empty when validation blocked the publish. */
971
1021
  readonly written: readonly string[];
1022
+ /** Per-content counts (the human-readable side of `written`); present when the tree loaded. */
1023
+ readonly summary?: PublishSummary;
972
1024
  }
973
1025
  /**
974
1026
  * Run the Phase-1 publish pipeline (DESIGN §11) through ports: **validate → compile →
@@ -1235,4 +1287,4 @@ declare function escapeJsonStringContent(value: string): string;
1235
1287
  */
1236
1288
  declare function sanitizeUrl(url: string): string;
1237
1289
 
1238
- export { type Artifact, type ArtifactSink, type AuthEndpoints, type AuthResult, type AuthorDirectory, type AuthorEntry, type AuthorOption, type AuthorRef, type AuthoringReference, type BlobStore, type BoundItem, 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 Diagnostic, 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 IdentityProvider, type ImageEncoder, type ImageFormat, type ImageInfo, InMemoryArtifactSink, InMemoryBlobStore, InMemoryContentSource, IndexedStore, type ItemNode, type LengthConstraints, type LoginRequest, type LoginSuccess, type MediaResolver, type PageEntry, type PreBuildHook, type Principal, type PublishOptions, type PublishResult, type PurgeService, type RedirectRule, type ReferenceContext, type ReferenceEntry, type ReferenceSection, type RefreshSuccess, type RenameResult, type ResetPasswordRequest, type ResolveContext, type Role, type RssOptions, type Scope, type Severity, type SiteConfig, type SortFile, type TemplateAnalysis, type TokenContext, type TokenRef, type TreeNode, type UpdateUserRequest, type UserAdminEndpoints, type UserSummary, type ValidationResult, type ValueKind, type WorkingStore, analyzeTemplate, buildAuthoringReference, buildContentIndex, buildPictureMarkup, buildRedirects, buildResolveContext, buildRss, buildSitemap, canEdit, compilePage, compileSite, compileTemplate, contentHash, contentTypeFor, deriveFields, emptyIndex, escapeHtmlAttribute, escapeHtmlText, escapeJsonStringContent, findTokens, hasRole, humanize, inferControl, isFieldType, joinAuthors, loadComponentScripts, loadComponentStyles, loadComponents, loadContentTree, loadDocumentShell, loadIndex, loadRedirects, loadSiteConfig, loadTemplate, noopPurgeService, outputPathForItem, parseContentItem, parseRedirects, parseSortFile, processImage, publishSite, renderAuthors, renderReferenceMarkdown, sanitizeUrl, saveIndex, slugify, textContent, toAuthorRefs, urlForItem, validateSite, valueKindOf, wholeValueToken };
1290
+ export { type Artifact, type ArtifactSink, type AuthEndpoints, type AuthResult, type AuthorDirectory, type AuthorEntry, type AuthorOption, type AuthorRef, type AuthoringReference, type BlobStore, type BoundItem, 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 Diagnostic, 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 IdentityProvider, type ImageEncoder, type ImageFormat, type ImageInfo, InMemoryArtifactSink, InMemoryBlobStore, InMemoryContentSource, IndexedStore, type ItemNode, type LengthConstraints, type LoginRequest, type LoginSuccess, type MediaResolver, type PageEntry, type PreBuildHook, type Principal, type PublishOptions, type PublishResult, type PublishSummary, type PurgeService, RESERVED_PREFIX, type ReconcileResult, type RedirectRule, type ReferenceContext, type ReferenceEntry, type ReferenceSection, type RefreshSuccess, type RenameResult, type ResetPasswordRequest, type ResolveContext, type Role, type RssOptions, type Scope, type Severity, type SiteConfig, type SortFile, type TemplateAnalysis, type TokenContext, type TokenRef, type TreeNode, type UpdateUserRequest, type UserAdminEndpoints, type UserSummary, type ValidationResult, type ValueKind, type WorkingStore, analyzeTemplate, buildAuthoringReference, buildContentIndex, buildPictureMarkup, buildRedirects, buildResolveContext, buildRss, buildSitemap, canEdit, compilePage, compileSite, compileTemplate, contentHash, contentTypeFor, deriveFields, emptyIndex, escapeHtmlAttribute, escapeHtmlText, escapeJsonStringContent, findTokens, hasRole, humanize, inferControl, isFieldType, joinAuthors, loadComponentScripts, loadComponentStyles, loadComponents, loadContentTree, loadDocumentShell, loadIndex, loadRedirects, loadSiteConfig, loadTemplate, noopPurgeService, outputPathForItem, parseContentItem, parseRedirects, parseSortFile, processImage, publishSite, reconcileIndex, renderAuthors, renderReferenceMarkdown, sanitizeUrl, saveIndex, slugify, textContent, toAuthorRefs, urlForItem, validateSite, valueKindOf, wholeValueToken };
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  InMemoryBlobStore,
8
8
  InMemoryContentSource,
9
9
  IndexedStore,
10
+ RESERVED_PREFIX,
10
11
  analyzeTemplate,
11
12
  buildAuthoringReference,
12
13
  buildContentIndex,
@@ -48,6 +49,7 @@ import {
48
49
  parseSortFile,
49
50
  processImage,
50
51
  publishSite,
52
+ reconcileIndex,
51
53
  renderAuthors,
52
54
  renderReferenceMarkdown,
53
55
  sanitizeUrl,
@@ -59,7 +61,7 @@ import {
59
61
  validateSite,
60
62
  valueKindOf,
61
63
  wholeValueToken
62
- } from "./chunk-G2UEZTYC.js";
64
+ } from "./chunk-S23S4ZAK.js";
63
65
  export {
64
66
  ContentParseError,
65
67
  DEFAULT_DIRS,
@@ -69,6 +71,7 @@ export {
69
71
  InMemoryBlobStore,
70
72
  InMemoryContentSource,
71
73
  IndexedStore,
74
+ RESERVED_PREFIX,
72
75
  analyzeTemplate,
73
76
  buildAuthoringReference,
74
77
  buildContentIndex,
@@ -110,6 +113,7 @@ export {
110
113
  parseSortFile,
111
114
  processImage,
112
115
  publishSite,
116
+ reconcileIndex,
113
117
  renderAuthors,
114
118
  renderReferenceMarkdown,
115
119
  sanitizeUrl,