@xtrable-ltd/nanoesis 0.1.7 → 0.1.8

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 (26) hide show
  1. package/dist/adapter-azure-blob.js +1 -1
  2. package/dist/{chunk-Y7PUOM5I.js → chunk-J6VYOB47.js} +168 -1
  3. package/dist/editor-api.d.ts +47 -2
  4. package/dist/editor-api.js +113 -3
  5. package/dist/index.d.ts +96 -7
  6. package/dist/index.js +7 -3
  7. package/editor/assets/{TemplatesPane-CtH_G1zX.js → TemplatesPane-4IAoeX4-.js} +7 -7
  8. package/editor/assets/{cssMode-Bp_d9uU6.js → cssMode-BhSmGQp_.js} +1 -1
  9. package/editor/assets/{freemarker2-Bjkifjqk.js → freemarker2-Z1jVSRUs.js} +1 -1
  10. package/editor/assets/{handlebars-BRat5rme.js → handlebars-C3kew8-P.js} +1 -1
  11. package/editor/assets/{html-BABGfo5F.js → html-RVg2mWQY.js} +1 -1
  12. package/editor/assets/{htmlMode-iA96VUY3.js → htmlMode-ljHXud-Y.js} +1 -1
  13. package/editor/assets/{index-B11qMUOV.js → index-CLoI_HF2.js} +71 -70
  14. package/editor/assets/{index-BRPY7tEn.css → index-R39CtpUa.css} +1 -1
  15. package/editor/assets/{javascript-Py4Moqda.js → javascript-CAr3NHzi.js} +1 -1
  16. package/editor/assets/{jsonMode-ChrNU-Y4.js → jsonMode-Cq4fxtNe.js} +1 -1
  17. package/editor/assets/{liquid-CB6iNJlZ.js → liquid-DeSAzZOT.js} +1 -1
  18. package/editor/assets/{mdx-DOQaWGy9.js → mdx-C4Ynnq4H.js} +1 -1
  19. package/editor/assets/{python-QNihkXeJ.js → python-rTSPH-tA.js} +1 -1
  20. package/editor/assets/{razor-bRuM_KbJ.js → razor-B-0Do2A9.js} +1 -1
  21. package/editor/assets/{tsMode-CuAeEMPg.js → tsMode-m4eOcwoz.js} +1 -1
  22. package/editor/assets/{typescript-Dw4AxvYT.js → typescript-CdvinFng.js} +1 -1
  23. package/editor/assets/{xml-mN7rLnTp.js → xml-DuAYX2gv.js} +1 -1
  24. package/editor/assets/{yaml-tN2lKdvN.js → yaml-BWplD8Hf.js} +1 -1
  25. package/editor/index.html +2 -2
  26. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  contentTypeFor
3
- } from "./chunk-Y7PUOM5I.js";
3
+ } from "./chunk-J6VYOB47.js";
4
4
 
5
5
  // ../../adapters/azure-blob/src/container.ts
6
6
  var InMemoryBlobContainer = class {
@@ -2560,6 +2560,171 @@ function canEdit(principal) {
2560
2560
  return hasRole(principal, "author") || hasRole(principal, "developer");
2561
2561
  }
2562
2562
 
2563
+ // ../engine/src/diagnose/registry.ts
2564
+ function createDiagnosticRegistry() {
2565
+ const diagnostics = /* @__PURE__ */ new Map();
2566
+ const repairs = /* @__PURE__ */ new Map();
2567
+ const runAll = async (deps) => {
2568
+ const out = [];
2569
+ for (const diagnostic of diagnostics.values()) {
2570
+ try {
2571
+ const findings = await diagnostic.run(deps);
2572
+ out.push(...findings);
2573
+ } catch (error) {
2574
+ out.push(crashFinding(diagnostic.id, error));
2575
+ }
2576
+ }
2577
+ return out;
2578
+ };
2579
+ return {
2580
+ add(diagnostic) {
2581
+ if (diagnostics.has(diagnostic.id)) {
2582
+ throw new Error(`Diagnostic id "${diagnostic.id}" is already registered`);
2583
+ }
2584
+ diagnostics.set(diagnostic.id, diagnostic);
2585
+ },
2586
+ addRepair(repair) {
2587
+ if (repairs.has(repair.id)) {
2588
+ throw new Error(`Repair id "${repair.id}" is already registered`);
2589
+ }
2590
+ repairs.set(repair.id, repair);
2591
+ },
2592
+ runAll,
2593
+ async runRepair(id, deps) {
2594
+ const repair = repairs.get(id);
2595
+ if (repair === void 0) throw new Error(`Unknown repair id: ${id}`);
2596
+ await repair.run(deps);
2597
+ return runAll(deps);
2598
+ }
2599
+ };
2600
+ }
2601
+ function crashFinding(diagnosticId, error) {
2602
+ const message = error instanceof Error ? error.message : String(error);
2603
+ return {
2604
+ id: `${diagnosticId}.crashed`,
2605
+ source: "connectivity",
2606
+ severity: "error",
2607
+ title: `Diagnostic "${diagnosticId}" crashed`,
2608
+ detail: `The diagnostic itself threw an error: ${message}`
2609
+ };
2610
+ }
2611
+
2612
+ // ../engine/src/diagnose/port-probes.ts
2613
+ var PROBE_PATH = "__nanoesis-diagnose-probe.tmp";
2614
+ var PROBE_CONTENT = "__nanoesis-diagnose-probe__";
2615
+ var DIAGNOSTIC_ID = "connectivity.working-store-roundtrip";
2616
+ var workingStoreRoundTripDiagnostic = {
2617
+ id: DIAGNOSTIC_ID,
2618
+ async run({ store }) {
2619
+ const encoder = new TextEncoder();
2620
+ const decoder = new TextDecoder();
2621
+ try {
2622
+ await store.write(PROBE_PATH, encoder.encode(PROBE_CONTENT));
2623
+ } catch (error) {
2624
+ return [
2625
+ finding(
2626
+ "write-failed",
2627
+ "Working store write failed",
2628
+ `The diagnostics probe could not write a temporary file (${PROBE_PATH}). The store's adapter reported: ${describe(error)}`
2629
+ )
2630
+ ];
2631
+ }
2632
+ try {
2633
+ const bytes = await store.readBytes(PROBE_PATH);
2634
+ if (decoder.decode(bytes) !== PROBE_CONTENT) {
2635
+ await silently(() => store.delete(PROBE_PATH));
2636
+ return [
2637
+ finding(
2638
+ "read-mismatch",
2639
+ "Working store wrote different bytes than were read back",
2640
+ `The probe wrote ${PROBE_CONTENT.length} bytes but readBytes returned a different value. The store's writes are not durably persisting through reads.`
2641
+ )
2642
+ ];
2643
+ }
2644
+ } catch (error) {
2645
+ await silently(() => store.delete(PROBE_PATH));
2646
+ return [
2647
+ finding(
2648
+ "read-failed",
2649
+ "Working store could not read its own probe write",
2650
+ `The diagnostics probe wrote ${PROBE_PATH} but reading it back failed: ${describe(error)}. The write may have been silently dropped, or the store is partitioned (writes and reads going to different backends).`
2651
+ )
2652
+ ];
2653
+ }
2654
+ try {
2655
+ const entries = await store.list("");
2656
+ if (!entries.some((entry) => entry.name === PROBE_PATH)) {
2657
+ await silently(() => store.delete(PROBE_PATH));
2658
+ return [
2659
+ finding(
2660
+ "list-misses-probe",
2661
+ "Working store cannot enumerate its own write",
2662
+ `The probe at ${PROBE_PATH} read back successfully, but does not appear when the store lists its root. The store's enumeration is out of sync with what it stores \u2014 typically a content-index drift.`
2663
+ )
2664
+ ];
2665
+ }
2666
+ } catch (error) {
2667
+ await silently(() => store.delete(PROBE_PATH));
2668
+ return [
2669
+ finding(
2670
+ "list-failed",
2671
+ "Working store list threw",
2672
+ `Enumeration of the store root failed: ${describe(error)}.`
2673
+ )
2674
+ ];
2675
+ }
2676
+ try {
2677
+ await store.delete(PROBE_PATH);
2678
+ } catch (error) {
2679
+ return [
2680
+ finding(
2681
+ "delete-failed",
2682
+ "Working store delete threw",
2683
+ `The probe could not be cleaned up: ${describe(error)}. A file named ${PROBE_PATH} may remain in the store.`
2684
+ )
2685
+ ];
2686
+ }
2687
+ try {
2688
+ if (await store.exists(PROBE_PATH)) {
2689
+ return [
2690
+ finding(
2691
+ "delete-ineffective",
2692
+ "Working store delete did not remove the probe",
2693
+ `After deleting ${PROBE_PATH} the store still reports it exists. Deletes are not taking effect \u2014 typically a soft-delete configuration that hides items instead of removing them.`
2694
+ )
2695
+ ];
2696
+ }
2697
+ } catch (error) {
2698
+ return [
2699
+ finding(
2700
+ "exists-failed",
2701
+ "Working store exists() threw on the probe",
2702
+ `After the probe was deleted, exists() threw rather than returning false: ${describe(error)}.`
2703
+ )
2704
+ ];
2705
+ }
2706
+ return [];
2707
+ }
2708
+ };
2709
+ function finding(suffix, title, detail) {
2710
+ return {
2711
+ id: `${DIAGNOSTIC_ID}.${suffix}`,
2712
+ source: "connectivity",
2713
+ severity: "error",
2714
+ title,
2715
+ detail
2716
+ };
2717
+ }
2718
+ function describe(error) {
2719
+ return error instanceof Error ? error.message : String(error);
2720
+ }
2721
+ async function silently(work) {
2722
+ try {
2723
+ await work();
2724
+ } catch {
2725
+ }
2726
+ }
2727
+
2563
2728
  export {
2564
2729
  slugify,
2565
2730
  humanize,
@@ -2622,5 +2787,7 @@ export {
2622
2787
  publishSite,
2623
2788
  InMemoryArtifactSink,
2624
2789
  hasRole,
2625
- canEdit
2790
+ canEdit,
2791
+ createDiagnosticRegistry,
2792
+ workingStoreRoundTripDiagnostic
2626
2793
  };
@@ -1,4 +1,4 @@
1
- import { WorkingStore, IdentityProvider, PublishResult, ReconcileResult, AuthEndpoints, UserAdminEndpoints, AuthorOption, UserSummary, AuthorDirectory } from '@nanoesis/engine';
1
+ import { WorkingStore, IdentityProvider, PublishResult, ReconcileResult, AuthEndpoints, UserAdminEndpoints, AuthorOption, DiagnosticRegistry, UserSummary, AuthorDirectory, DiagnosticCheck, Repair } from '@nanoesis/engine';
2
2
 
3
3
  /**
4
4
  * Editor white-labelling storage (DESIGN §11, Phase B). This is the *editor app's*
@@ -139,6 +139,16 @@ interface ApiDeps {
139
139
  * store and feeds the *same* users to the compiler's `authorDirectory` at publish.
140
140
  */
141
141
  readonly authors?: () => Promise<readonly AuthorOption[]>;
142
+ /**
143
+ * Optional diagnostics + self-heal registry. When present, the router mounts the
144
+ * admin-only `GET /api/admin/diagnostics` (run all probes, return findings) and
145
+ * `POST /api/admin/diagnostics/repair` (run a named repair, return refreshed findings).
146
+ * The registry is port-agnostic: it depends only on the engine's port contracts, so
147
+ * adopters using their own adapters get the same diagnostics with no per-host wiring.
148
+ * Hosts typically supply {@link buildDefaultDiagnostics}; omit on a deployment that
149
+ * has no admin operator (an embedded read-only host).
150
+ */
151
+ readonly diagnostics?: DiagnosticRegistry;
142
152
  }
143
153
  /**
144
154
  * Dispatch one API request. The editor is private, so every route requires an editing
@@ -241,4 +251,39 @@ declare function readMcpResource(uri: string): {
241
251
  */
242
252
  declare const SCAFFOLD_FILES: Readonly<Record<string, string>>;
243
253
 
244
- 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 };
254
+ /**
255
+ * The default {@link DiagnosticRegistry} the host mounts behind `/api/admin/diagnostics`.
256
+ * Combines the engine's port-agnostic round-trip probe (works against any adapter) with
257
+ * the scaffold-aware content checks that need to know what a "home template" is. The
258
+ * editor-api layer is the right home for the latter: the engine has no opinion on what
259
+ * paths matter, while the scaffold is bundled here as pure data (so MCP and every host
260
+ * see the same set; see scaffold.ts).
261
+ *
262
+ * Returned registry is mutable — a host that needs site-specific checks can call `add` /
263
+ * `addRepair` before mounting it.
264
+ */
265
+ declare function buildDefaultDiagnostics(): DiagnosticRegistry;
266
+ /**
267
+ * `templates/home.html` is the locked path the editor refuses to rename, move, or delete,
268
+ * and the scaffold guarantees it on first run. A missing home template means the site has
269
+ * no home page and no template to fall back to; restoring the bundled scaffold version is
270
+ * a one-click fix (the page is just the user's content again, the template is what was
271
+ * deleted out from under them — typically by a direct blob-store edit).
272
+ */
273
+ declare const homeTemplateMissingDiagnostic: DiagnosticCheck;
274
+ /**
275
+ * Restore `templates/home.html` from the bundled scaffold. Idempotent: if a customised
276
+ * home template already exists this is a no-op, so a stale UI re-firing the repair never
277
+ * silently overwrites real work. The home template is the only file the scaffold owns
278
+ * as "the locked, blessed copy", so reset semantics are correct here.
279
+ */
280
+ declare const recreateHomeTemplateRepair: Repair;
281
+ /**
282
+ * A site with zero `isPublished: true` items publishes nothing — a common confused-user
283
+ * state. We surface it as a warning (the publish still completes, just empty), not an
284
+ * error, because the situation is legitimate during early authoring. No auto-repair: the
285
+ * user picks what to publish; we only point out that nothing is.
286
+ */
287
+ declare const noPublishedContentDiagnostic: DiagnosticCheck;
288
+
289
+ export { type ApiDeps, type ApiRequest, type ApiResponse, type BrandingLogo, type BrandingLogoMeta, type BrandingState, type BrandingStore, FileBrandingStore, InMemoryBrandingStore, MCP_RESOURCES, MCP_TOOLS, type McpCallOptions, type McpResourceDef, type McpToolDef, type McpToolResult, SCAFFOLD_FILES, authorDirectory, authorOptions, buildDefaultDiagnostics, callMcpTool, handleApi, homeTemplateMissingDiagnostic, noPublishedContentDiagnostic, readMcpResource, recreateHomeTemplateRepair };
@@ -2,13 +2,15 @@ import {
2
2
  buildAuthoringReference,
3
3
  canEdit,
4
4
  contentTypeFor,
5
+ createDiagnosticRegistry,
5
6
  deriveFields,
6
7
  hasRole,
7
8
  loadComponents,
8
9
  loadTemplate,
9
10
  renderReferenceMarkdown,
10
- validateSite
11
- } from "./chunk-Y7PUOM5I.js";
11
+ validateSite,
12
+ workingStoreRoundTripDiagnostic
13
+ } from "./chunk-J6VYOB47.js";
12
14
 
13
15
  // ../editor-api/src/scaffold.ts
14
16
  var HOME_HTML = `<!doctype html>
@@ -227,6 +229,31 @@ async function handleUserAdminRoute(deps, req, caller) {
227
229
  }
228
230
  return json(404, { ok: false, error: `Unknown API route: ${req.path}` });
229
231
  }
232
+ async function handleDiagnosticsRoute(deps, req, caller) {
233
+ const registry = deps.diagnostics;
234
+ if (registry === void 0) {
235
+ return json(404, { ok: false, error: "diagnostics are not available on this host" });
236
+ }
237
+ if (!hasRole(caller, "admin")) {
238
+ return json(403, { ok: false, error: "admin role required" });
239
+ }
240
+ if (req.path === "/api/admin/diagnostics") {
241
+ if (req.method !== "GET") return json(405, { ok: false, error: "method not allowed" });
242
+ const findings = await registry.runAll({ store: deps.store });
243
+ return json(200, { findings });
244
+ }
245
+ if (req.method !== "POST") return json(405, { ok: false, error: "method not allowed" });
246
+ const body = await parseJsonBody(req);
247
+ if (!isObject(body) || typeof body["id"] !== "string") {
248
+ return json(400, { ok: false, error: "JSON body with repair id required" });
249
+ }
250
+ try {
251
+ const findings = await registry.runRepair(body["id"], { store: deps.store });
252
+ return json(200, { findings });
253
+ } catch (error) {
254
+ return json(400, { ok: false, error: error instanceof Error ? error.message : String(error) });
255
+ }
256
+ }
230
257
  async function handleBrandingRoute(deps, req) {
231
258
  if (req.path !== "/api/branding" && req.path !== "/api/branding/logo") return void 0;
232
259
  const store = deps.branding;
@@ -292,6 +319,9 @@ async function dispatchApi(deps, req) {
292
319
  if (req.path === "/api/users" || req.path.startsWith("/api/users/")) {
293
320
  return handleUserAdminRoute(deps, req, principal);
294
321
  }
322
+ if (req.path === "/api/admin/diagnostics" || req.path === "/api/admin/diagnostics/repair") {
323
+ return handleDiagnosticsRoute(deps, req, principal);
324
+ }
295
325
  const get = (key) => req.query.get(key) ?? "";
296
326
  switch (req.path) {
297
327
  case "/api/list": {
@@ -610,6 +640,82 @@ function readMcpResource(uri) {
610
640
  return void 0;
611
641
  }
612
642
 
643
+ // ../editor-api/src/diagnose.ts
644
+ function buildDefaultDiagnostics() {
645
+ const registry = createDiagnosticRegistry();
646
+ registry.add(workingStoreRoundTripDiagnostic);
647
+ registry.add(homeTemplateMissingDiagnostic);
648
+ registry.add(noPublishedContentDiagnostic);
649
+ registry.addRepair(recreateHomeTemplateRepair);
650
+ return registry;
651
+ }
652
+ var HOME_PATH = "templates/home.html";
653
+ var homeTemplateMissingDiagnostic = {
654
+ id: "content.home-template",
655
+ async run({ store }) {
656
+ if (await store.exists(HOME_PATH)) return [];
657
+ return [
658
+ {
659
+ id: "content.home-template.missing",
660
+ source: "content",
661
+ severity: "error",
662
+ title: "Home template is missing",
663
+ detail: `The locked path ${HOME_PATH} does not exist. Without it, the editor cannot render the home page and publishing will fail to compile the index. Recreate it from the bundled scaffold to restore the working state.`,
664
+ repair: { id: "content.home-template.recreate", label: "Recreate home template" }
665
+ }
666
+ ];
667
+ }
668
+ };
669
+ var recreateHomeTemplateRepair = {
670
+ id: "content.home-template.recreate",
671
+ async run({ store }) {
672
+ if (await store.exists(HOME_PATH)) return;
673
+ const bytes = new TextEncoder().encode(SCAFFOLD_FILES[HOME_PATH]);
674
+ await store.write(HOME_PATH, bytes);
675
+ }
676
+ };
677
+ var noPublishedContentDiagnostic = {
678
+ id: "content.no-published-content",
679
+ async run({ store }) {
680
+ const published = await anyPublishedContentItem(store);
681
+ if (published) return [];
682
+ return [
683
+ {
684
+ id: "content.no-published-content",
685
+ source: "content",
686
+ severity: "warning",
687
+ title: "Nothing is published",
688
+ detail: "No content item has isPublished: true, so a publish would produce no pages. Open a content item and toggle Published, then publish again."
689
+ }
690
+ ];
691
+ }
692
+ };
693
+ async function anyPublishedContentItem(store, dir = "content") {
694
+ let entries;
695
+ try {
696
+ entries = await store.list(dir);
697
+ } catch {
698
+ return false;
699
+ }
700
+ for (const entry of entries) {
701
+ const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
702
+ if (entry.kind === "dir") {
703
+ if (await anyPublishedContentItem(store, path)) return true;
704
+ continue;
705
+ }
706
+ if (!entry.name.endsWith(".json")) continue;
707
+ try {
708
+ const text = await store.readText(path);
709
+ const data = JSON.parse(text);
710
+ if (typeof data === "object" && data !== null && data.isPublished === true) {
711
+ return true;
712
+ }
713
+ } catch {
714
+ }
715
+ }
716
+ return false;
717
+ }
718
+
613
719
  // ../editor-api/src/branding.ts
614
720
  import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
615
721
  import { dirname, join } from "path";
@@ -714,7 +820,11 @@ export {
714
820
  SCAFFOLD_FILES,
715
821
  authorDirectory,
716
822
  authorOptions,
823
+ buildDefaultDiagnostics,
717
824
  callMcpTool,
718
825
  handleApi,
719
- readMcpResource
826
+ homeTemplateMissingDiagnostic,
827
+ noPublishedContentDiagnostic,
828
+ readMcpResource,
829
+ recreateHomeTemplateRepair
720
830
  };
package/dist/index.d.ts CHANGED
@@ -919,18 +919,18 @@ declare function analyzeTemplate(source: string, components?: ComponentMap): Tem
919
919
  * are returned as data (not thrown), so a host can present the clear list a user
920
920
  * needs (CLAUDE.md §2: "3 things would break: …").
921
921
  */
922
- type Severity = 'error' | 'warning';
923
- interface Diagnostic {
924
- readonly severity: Severity;
922
+ type Severity$1 = 'error' | 'warning';
923
+ interface Diagnostic$1 {
924
+ readonly severity: Severity$1;
925
925
  readonly code: string;
926
926
  readonly message: string;
927
927
  /** Content path of the item/dir the diagnostic concerns, when applicable. */
928
928
  readonly path?: string;
929
929
  }
930
930
  interface ValidationResult {
931
- readonly diagnostics: readonly Diagnostic[];
932
- readonly errors: readonly Diagnostic[];
933
- readonly warnings: readonly Diagnostic[];
931
+ readonly diagnostics: readonly Diagnostic$1[];
932
+ readonly errors: readonly Diagnostic$1[];
933
+ readonly warnings: readonly Diagnostic$1[];
934
934
  /** True when there are no errors, i.e. a publish may proceed. */
935
935
  readonly ok: boolean;
936
936
  }
@@ -1302,4 +1302,93 @@ declare function escapeJsonStringContent(value: string): string;
1302
1302
  */
1303
1303
  declare function sanitizeUrl(url: string): string;
1304
1304
 
1305
- 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 };
1305
+ /**
1306
+ * The diagnostics + self-heal feature (CLAUDE.md §2: expected failures are data, not
1307
+ * exceptions; we generalise the validation gate's shape from "is this content
1308
+ * publishable?" to "is this whole installation healthy?"). A diagnostic is a port-agnostic
1309
+ * probe that returns {@link Finding}s; a repair is an idempotent fix that runs through the
1310
+ * same ports. Both layers know nothing of any specific host or adapter: when an adapter
1311
+ * fails a round-trip probe, the surfacing detail is whatever that adapter put in its
1312
+ * thrown error's message. Adapter authors who want richer detail write richer errors,
1313
+ * not a new interface on the engine.
1314
+ */
1315
+ type Severity = 'error' | 'warning' | 'info';
1316
+ /**
1317
+ * What broad area of the system a finding concerns; UI groups by this. Deliberately
1318
+ * coarse and stable — adding adapter-specific categories ("kv", "cloudflare", …) would
1319
+ * couple the engine to specific cloud concepts, which is what we are avoiding.
1320
+ */
1321
+ type Source = 'content' | 'index' | 'auth' | 'connectivity';
1322
+ /**
1323
+ * One thing the diagnostics gate found wrong (or noteworthy). Shape mirrors the
1324
+ * validation gate's {@link Diagnostic} type so a future merger is one step.
1325
+ */
1326
+ interface Finding {
1327
+ /** Stable across runs of the same diagnostic, so the UI can dedupe and target repairs. */
1328
+ readonly id: string;
1329
+ readonly source: Source;
1330
+ readonly severity: Severity;
1331
+ /** Short human-readable headline ("Home template is missing"). */
1332
+ readonly title: string;
1333
+ /** Full prose explaining what's wrong and where it was detected. */
1334
+ readonly detail: string;
1335
+ /** Present iff the engine can fix this in-place. Manual fixes go in `detail` text. */
1336
+ readonly repair?: {
1337
+ readonly id: string;
1338
+ readonly label: string;
1339
+ };
1340
+ }
1341
+ /**
1342
+ * The ports a diagnostic is allowed to depend on. Just the working store today, the
1343
+ * port the editor's content API already binds (DESIGN §11c). Round-trip probes exercise
1344
+ * `write`/`list`/`exists`/`readBytes`/`delete`; content checks just read. Anything beyond
1345
+ * those (a `PurgeService`, a `BlobStore`, …) is added here as a new optional field, never
1346
+ * a host- or adapter-specific surface.
1347
+ */
1348
+ interface DiagnoseDeps {
1349
+ readonly store: WorkingStore;
1350
+ }
1351
+ /**
1352
+ * One probe. `id` is stable and used to namespace the finding ids it emits. `run` returns
1353
+ * the {@link Finding}s it generated (empty array when there's nothing to report).
1354
+ */
1355
+ interface Diagnostic {
1356
+ readonly id: string;
1357
+ run(deps: DiagnoseDeps): Promise<readonly Finding[]>;
1358
+ }
1359
+ /**
1360
+ * One in-place fix. Idempotent: calling `run` twice does not double-write. The host
1361
+ * re-runs the diagnostic set after a repair to refresh the UI, so repairs don't have to
1362
+ * return findings themselves.
1363
+ */
1364
+ interface Repair {
1365
+ readonly id: string;
1366
+ run(deps: DiagnoseDeps): Promise<void>;
1367
+ }
1368
+
1369
+ /**
1370
+ * The diagnostic registry: an in-process collection of {@link Diagnostic}s and
1371
+ * {@link Repair}s the host's `/api/admin/diagnostics` route runs. A buggy diagnostic does
1372
+ * not take down the page — its thrown error is caught and surfaced as a finding with
1373
+ * `id` "<diagnostic-id>.crashed", so the user sees the failure exactly the way they see
1374
+ * every other finding. (CLAUDE.md §2 spirit: failures are data, not exceptions, even when
1375
+ * the failure is the probe itself.)
1376
+ *
1377
+ * Ids are unique per registry; registering a duplicate throws, since that is a
1378
+ * programmer error, not user-visible data.
1379
+ */
1380
+ interface DiagnosticRegistry {
1381
+ /** Register a diagnostic. Throws if its id is already in the registry. */
1382
+ add(diagnostic: Diagnostic): void;
1383
+ /** Register a repair. Throws if its id is already in the registry. */
1384
+ addRepair(repair: Repair): void;
1385
+ /** Run every registered diagnostic in order; concatenate their findings. */
1386
+ runAll(deps: DiagnoseDeps): Promise<readonly Finding[]>;
1387
+ /** Run the named repair, then return a fresh `runAll` so the UI re-renders. */
1388
+ runRepair(id: string, deps: DiagnoseDeps): Promise<readonly Finding[]>;
1389
+ }
1390
+ declare function createDiagnosticRegistry(): DiagnosticRegistry;
1391
+
1392
+ declare const workingStoreRoundTripDiagnostic: Diagnostic;
1393
+
1394
+ 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 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 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 Repair, type ResetPasswordRequest, type ResolveContext, type Role, type RssOptions, type Scope, type Severity$1 as 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, createDiagnosticRegistry, 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, workingStoreRoundTripDiagnostic };
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  compileTemplate,
23
23
  contentHash,
24
24
  contentTypeFor,
25
+ createDiagnosticRegistry,
25
26
  deriveFields,
26
27
  emptyIndex,
27
28
  escapeHtmlAttribute,
@@ -60,8 +61,9 @@ import {
60
61
  urlForItem,
61
62
  validateSite,
62
63
  valueKindOf,
63
- wholeValueToken
64
- } from "./chunk-Y7PUOM5I.js";
64
+ wholeValueToken,
65
+ workingStoreRoundTripDiagnostic
66
+ } from "./chunk-J6VYOB47.js";
65
67
  export {
66
68
  ContentParseError,
67
69
  DEFAULT_DIRS,
@@ -86,6 +88,7 @@ export {
86
88
  compileTemplate,
87
89
  contentHash,
88
90
  contentTypeFor,
91
+ createDiagnosticRegistry,
89
92
  deriveFields,
90
93
  emptyIndex,
91
94
  escapeHtmlAttribute,
@@ -124,5 +127,6 @@ export {
124
127
  urlForItem,
125
128
  validateSite,
126
129
  valueKindOf,
127
- wholeValueToken
130
+ wholeValueToken,
131
+ workingStoreRoundTripDiagnostic
128
132
  };