@xtrable-ltd/nanoesis 0.1.9 → 0.1.11
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.
- package/dist/adapter-azure-blob.d.ts +7 -1
- package/dist/adapter-azure-blob.js +2 -2
- package/dist/{chunk-J6VYOB47.js → chunk-XO3CT6GL.js} +1699 -1167
- package/dist/editor-api.d.ts +39 -2
- package/dist/editor-api.js +396 -6
- package/dist/index.d.ts +491 -222
- package/dist/index.js +25 -1
- package/editor/assets/MigrationsPane-BAHPPSXP.css +1 -0
- package/editor/assets/MigrationsPane-BYGqWBAA.js +4 -0
- package/editor/assets/{TemplatesPane-CHzfB00-.js → TemplatesPane-B5hn_v0Z.js} +208 -202
- package/editor/assets/{TemplatesPane-B4_sg2u5.css → TemplatesPane-D0gGehUt.css} +1 -1
- package/editor/assets/{cssMode-BahdJh1A.js → cssMode-BbIf5k6I.js} +1 -1
- package/editor/assets/{freemarker2-2FC3twUE.js → freemarker2-DoW0pSYV.js} +1 -1
- package/editor/assets/{handlebars-pMjPHNx1.js → handlebars-DLlET-qc.js} +1 -1
- package/editor/assets/{html-KTToTG0n.js → html-4khbqrhe.js} +1 -1
- package/editor/assets/{htmlMode-ufik94dZ.js → htmlMode-DblHkZ-k.js} +1 -1
- package/editor/assets/index-CkESQLMV.css +7 -0
- package/editor/assets/index-Do1drqEQ.js +138 -0
- package/editor/assets/{javascript-CD4kAZXr.js → javascript-CgPO2Hmj.js} +1 -1
- package/editor/assets/{jsonMode-ClHucayn.js → jsonMode-BrWh2436.js} +1 -1
- package/editor/assets/{liquid-B-uYib60.js → liquid-BsQJXwPT.js} +1 -1
- package/editor/assets/{mdx-BOc9oMkZ.js → mdx-AO8t67gx.js} +1 -1
- package/editor/assets/{python-BipLFHGs.js → python-3w4sZj5c.js} +1 -1
- package/editor/assets/{razor-C0di_gwM.js → razor-BFsvo06w.js} +1 -1
- package/editor/assets/{tsMode-B7fenrcD.js → tsMode-QrC4ERjp.js} +1 -1
- package/editor/assets/{typescript-CDg7c2A-.js → typescript-BXJ3QLad.js} +1 -1
- package/editor/assets/{xml-DTAdn5Pw.js → xml-CxKYn1FP.js} +1 -1
- package/editor/assets/{yaml-B9-OjY0Z.js → yaml-BmWLvF7Q.js} +1 -1
- package/editor/index.html +2 -2
- package/package.json +1 -1
- package/editor/assets/index-BsRGVHEP.css +0 -7
- package/editor/assets/index-CPKtfzWD.js +0 -134
package/dist/editor-api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WorkingStore, IdentityProvider, PublishResult, ReconcileResult, AuthEndpoints, UserAdminEndpoints, AuthorOption, DiagnosticRegistry, UserSummary, AuthorDirectory,
|
|
1
|
+
import { WorkingStore, IdentityProvider, PublishResult, ReconcileResult, AuthEndpoints, UserAdminEndpoints, AuthorOption, DiagnosticRegistry, UserSummary, AuthorDirectory, Repair, DiagnosticCheck } from '@nanoesis/engine';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Editor white-labelling storage (DESIGN §11, Phase B). This is the *editor app's*
|
|
@@ -285,5 +285,42 @@ declare const recreateHomeTemplateRepair: Repair;
|
|
|
285
285
|
* user picks what to publish; we only point out that nothing is.
|
|
286
286
|
*/
|
|
287
287
|
declare const noPublishedContentDiagnostic: DiagnosticCheck;
|
|
288
|
+
/**
|
|
289
|
+
* Walk every published content item; for any pinned to a snapshot, check whether the
|
|
290
|
+
* snapshot file exists and parses (PROPOSAL §16.1, §16.2). Emits one finding per
|
|
291
|
+
* affected item — the missing-snapshot case (publish-blocking error) and the
|
|
292
|
+
* corrupt-snapshot case (publish-blocking error, distinct headline so the user knows
|
|
293
|
+
* "the file is there but the bytes are bad" vs "the file is gone"). Both repair via
|
|
294
|
+
* `templates.rebind-item-to-current`.
|
|
295
|
+
*/
|
|
296
|
+
declare const templateSnapshotIntegrityDiagnostic: DiagnosticCheck;
|
|
297
|
+
/**
|
|
298
|
+
* Rebind an item's `template` field from `<name>@v<N>` to `<name>` (current). Used for
|
|
299
|
+
* the missing/corrupt snapshot repair: the item's pin is lost (the snapshot was the
|
|
300
|
+
* intent), but the item itself stays, and shows up in the pending-migrations list so
|
|
301
|
+
* its fields can be reconciled with current. Idempotent: a write of the same JSON is
|
|
302
|
+
* a no-op semantically.
|
|
303
|
+
*/
|
|
304
|
+
declare const rebindItemToCurrentRepair: Repair;
|
|
305
|
+
/**
|
|
306
|
+
* Snapshot exists but its current sibling is missing (PROPOSAL §16.4). Someone deleted
|
|
307
|
+
* `templates/article.html` and left `templates/article@v1.html` behind. The Health page
|
|
308
|
+
* surfaces this with the `copy-snapshot-to-current` repair — promotes the snapshot to
|
|
309
|
+
* be the current.
|
|
310
|
+
*/
|
|
311
|
+
declare const templateSuffixConflictDiagnostic: DiagnosticCheck;
|
|
312
|
+
/**
|
|
313
|
+
* Copy a snapshot's bytes to the current path (PROPOSAL §16.4 repair). The snapshot
|
|
314
|
+
* itself remains in place; the user can retire it manually later. Idempotent: if the
|
|
315
|
+
* current already exists, the repair is a no-op (so a stale UI re-firing the repair
|
|
316
|
+
* never silently overwrites real work).
|
|
317
|
+
*/
|
|
318
|
+
declare const copySnapshotToCurrentRepair: Repair;
|
|
319
|
+
/**
|
|
320
|
+
* Count items whose current-template binding has orphan fields (PROPOSAL §10). Read-only
|
|
321
|
+
* summary — points the user at the Migrations workspace (Phase 2). No auto-repair, since
|
|
322
|
+
* resolution is per-item (drop / rename / keep) and needs human choice.
|
|
323
|
+
*/
|
|
324
|
+
declare const pendingMigrationsDiagnostic: DiagnosticCheck;
|
|
288
325
|
|
|
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 };
|
|
326
|
+
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, copySnapshotToCurrentRepair, handleApi, homeTemplateMissingDiagnostic, noPublishedContentDiagnostic, pendingMigrationsDiagnostic, readMcpResource, rebindItemToCurrentRepair, recreateHomeTemplateRepair, templateSnapshotIntegrityDiagnostic, templateSuffixConflictDiagnostic };
|
package/dist/editor-api.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
|
+
analyzeTemplate,
|
|
3
|
+
applyMigration,
|
|
4
|
+
baseTemplateName,
|
|
2
5
|
buildAuthoringReference,
|
|
3
6
|
canEdit,
|
|
4
7
|
contentTypeFor,
|
|
5
8
|
createDiagnosticRegistry,
|
|
6
9
|
deriveFields,
|
|
7
10
|
hasRole,
|
|
11
|
+
isVersionedTemplateName,
|
|
8
12
|
loadComponents,
|
|
9
13
|
loadTemplate,
|
|
14
|
+
pendingMigrations,
|
|
10
15
|
renderReferenceMarkdown,
|
|
11
16
|
validateSite,
|
|
12
17
|
workingStoreRoundTripDiagnostic
|
|
13
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-XO3CT6GL.js";
|
|
14
19
|
|
|
15
20
|
// ../editor-api/src/scaffold.ts
|
|
16
21
|
var HOME_HTML = `<!doctype html>
|
|
@@ -254,6 +259,128 @@ async function handleDiagnosticsRoute(deps, req, caller) {
|
|
|
254
259
|
return json(400, { ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
255
260
|
}
|
|
256
261
|
}
|
|
262
|
+
async function handleMigrationsRoute(deps, req, caller) {
|
|
263
|
+
if (req.path === "/api/migrations") {
|
|
264
|
+
if (req.method !== "GET") return json(405, { ok: false, error: "method not allowed" });
|
|
265
|
+
const result = await pendingMigrations(deps.store);
|
|
266
|
+
return json(200, {
|
|
267
|
+
items: result.items,
|
|
268
|
+
byTemplate: Object.fromEntries(result.byTemplate)
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (req.path === "/api/migrations/preview") {
|
|
272
|
+
if (req.method !== "GET") return json(405, { ok: false, error: "method not allowed" });
|
|
273
|
+
const itemPath2 = req.query.get("path") ?? "";
|
|
274
|
+
if (itemPath2 === "") return json(400, { ok: false, error: "path query param required" });
|
|
275
|
+
return previewMigration(deps, itemPath2);
|
|
276
|
+
}
|
|
277
|
+
if (req.method !== "POST") return json(405, { ok: false, error: "method not allowed" });
|
|
278
|
+
void caller;
|
|
279
|
+
const body = await parseJsonBody(req);
|
|
280
|
+
if (!isObject(body) || typeof body["path"] !== "string" || !isObject(body["resolution"])) {
|
|
281
|
+
return json(400, { ok: false, error: "JSON body { path, resolution } required" });
|
|
282
|
+
}
|
|
283
|
+
const itemPath = body["path"];
|
|
284
|
+
const denied = denyWrite(caller, itemPath);
|
|
285
|
+
if (denied) return denied;
|
|
286
|
+
try {
|
|
287
|
+
await applyMigration(deps.store, itemPath, normalizeResolution(body["resolution"]));
|
|
288
|
+
const refreshed = await pendingMigrations(deps.store);
|
|
289
|
+
return json(200, {
|
|
290
|
+
ok: true,
|
|
291
|
+
items: refreshed.items,
|
|
292
|
+
byTemplate: Object.fromEntries(refreshed.byTemplate)
|
|
293
|
+
});
|
|
294
|
+
} catch (error) {
|
|
295
|
+
return json(400, { ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async function previewMigration(deps, itemPath) {
|
|
299
|
+
let itemJson;
|
|
300
|
+
try {
|
|
301
|
+
itemJson = JSON.parse(await deps.store.readText(itemPath));
|
|
302
|
+
} catch {
|
|
303
|
+
return json(404, { ok: false, error: "item not found" });
|
|
304
|
+
}
|
|
305
|
+
const tplName = typeof itemJson["template"] === "string" ? itemJson["template"] : "";
|
|
306
|
+
if (tplName === "") return json(400, { ok: false, error: "item has no template binding" });
|
|
307
|
+
const baseTpl = tplName.replace(/@v\d+$/, "");
|
|
308
|
+
const components = await loadComponents(deps.store).catch(() => /* @__PURE__ */ new Map());
|
|
309
|
+
const currentHtml = await safeReadTemplate(deps.store, baseTpl);
|
|
310
|
+
const currentFields = currentHtml === null ? [] : deriveFields(currentHtml, components).map((f) => f.name);
|
|
311
|
+
let leftTpl = null;
|
|
312
|
+
let leftHtml = null;
|
|
313
|
+
if (tplName !== baseTpl) {
|
|
314
|
+
leftTpl = tplName;
|
|
315
|
+
leftHtml = await safeReadTemplate(deps.store, tplName);
|
|
316
|
+
} else {
|
|
317
|
+
const { bestFitSnapshot: pickBestFit } = await import("./index.js");
|
|
318
|
+
const itemFields2 = Object.keys(
|
|
319
|
+
typeof itemJson["fields"] === "object" && itemJson["fields"] !== null ? itemJson["fields"] : {}
|
|
320
|
+
);
|
|
321
|
+
const best = await pickBestFit(deps.store, baseTpl, itemFields2);
|
|
322
|
+
if (best !== null) {
|
|
323
|
+
leftTpl = best;
|
|
324
|
+
leftHtml = await safeReadTemplate(deps.store, best);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const measureTpl = tplName === baseTpl ? baseTpl : tplName;
|
|
328
|
+
const measureHtml = tplName === baseTpl ? currentHtml : leftHtml;
|
|
329
|
+
const measureFields = measureHtml === null ? [] : deriveFields(measureHtml, components).map((f) => f.name);
|
|
330
|
+
const itemFields = typeof itemJson["fields"] === "object" && itemJson["fields"] !== null ? itemJson["fields"] : {};
|
|
331
|
+
const orphans = Object.keys(itemFields).filter((name) => !measureFields.includes(name)).map((name) => ({ name, value: itemFields[name] }));
|
|
332
|
+
void measureTpl;
|
|
333
|
+
return json(200, {
|
|
334
|
+
left: leftHtml === null ? null : { template: leftTpl, html: leftHtml },
|
|
335
|
+
right: { template: baseTpl, html: currentHtml ?? "" },
|
|
336
|
+
orphans,
|
|
337
|
+
missing: [],
|
|
338
|
+
currentTemplateFields: currentFields
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
async function safeReadTemplate(store, name) {
|
|
342
|
+
try {
|
|
343
|
+
return await store.readText(`templates/${name}.html`);
|
|
344
|
+
} catch {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function normalizeResolution(raw) {
|
|
349
|
+
const drop = Array.isArray(raw["drop"]) ? raw["drop"].filter((v) => typeof v === "string") : [];
|
|
350
|
+
const keep = Array.isArray(raw["keep"]) ? raw["keep"].filter((v) => typeof v === "string") : [];
|
|
351
|
+
const rename2 = {};
|
|
352
|
+
if (isObject(raw["rename"])) {
|
|
353
|
+
for (const [from, to] of Object.entries(raw["rename"])) {
|
|
354
|
+
if (typeof to === "string") rename2[from] = to;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const fill = {};
|
|
358
|
+
if (isObject(raw["fill"])) {
|
|
359
|
+
for (const [name, value] of Object.entries(raw["fill"])) {
|
|
360
|
+
if (typeof value === "string") fill[name] = value;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return { drop, rename: rename2, keep, fill };
|
|
364
|
+
}
|
|
365
|
+
async function handleStampRoute(deps, req, caller) {
|
|
366
|
+
if (req.method !== "POST") return json(405, { ok: false, error: "method not allowed" });
|
|
367
|
+
if (!hasRole(caller, "developer")) {
|
|
368
|
+
return json(403, { ok: false, error: "developer role required" });
|
|
369
|
+
}
|
|
370
|
+
const body = await parseJsonBody(req);
|
|
371
|
+
if (!isObject(body) || typeof body["name"] !== "string" || body["kind"] !== "template" && body["kind"] !== "component") {
|
|
372
|
+
return json(400, {
|
|
373
|
+
ok: false,
|
|
374
|
+
error: 'JSON body { name, kind: "template" | "component" } required'
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
const record = await deps.store.stamp(body["name"], body["kind"]);
|
|
379
|
+
return json(200, record);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
return json(400, { ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
257
384
|
async function handleBrandingRoute(deps, req) {
|
|
258
385
|
if (req.path !== "/api/branding" && req.path !== "/api/branding/logo") return void 0;
|
|
259
386
|
const store = deps.branding;
|
|
@@ -322,6 +449,12 @@ async function dispatchApi(deps, req) {
|
|
|
322
449
|
if (req.path === "/api/admin/diagnostics" || req.path === "/api/admin/diagnostics/repair") {
|
|
323
450
|
return handleDiagnosticsRoute(deps, req, principal);
|
|
324
451
|
}
|
|
452
|
+
if (req.path === "/api/migrations" || req.path === "/api/migrations/preview" || req.path === "/api/migrations/apply") {
|
|
453
|
+
return handleMigrationsRoute(deps, req, principal);
|
|
454
|
+
}
|
|
455
|
+
if (req.path === "/api/templates/stamp") {
|
|
456
|
+
return handleStampRoute(deps, req, principal);
|
|
457
|
+
}
|
|
325
458
|
const get = (key) => req.query.get(key) ?? "";
|
|
326
459
|
switch (req.path) {
|
|
327
460
|
case "/api/list": {
|
|
@@ -362,8 +495,13 @@ async function dispatchApi(deps, req) {
|
|
|
362
495
|
const denied = denyWrite(principal, get("path"));
|
|
363
496
|
if (denied) return denied;
|
|
364
497
|
try {
|
|
365
|
-
await deps.store.write(get("path"), await req.body());
|
|
366
|
-
return json(200, {
|
|
498
|
+
const result = await deps.store.write(get("path"), await req.body());
|
|
499
|
+
return json(200, {
|
|
500
|
+
ok: true,
|
|
501
|
+
...result.stamped !== void 0 && { stamped: result.stamped },
|
|
502
|
+
...result.stampIncomplete === true && { stampIncomplete: true },
|
|
503
|
+
...result.schemaDelta !== void 0 && { schemaDelta: result.schemaDelta }
|
|
504
|
+
});
|
|
367
505
|
} catch (error) {
|
|
368
506
|
return json(500, { ok: false, error: String(error) });
|
|
369
507
|
}
|
|
@@ -556,7 +694,7 @@ var TOOL_SPECS = [
|
|
|
556
694
|
},
|
|
557
695
|
{
|
|
558
696
|
name: "write_file",
|
|
559
|
-
description: "Create or overwrite a text file. content/ requires the author role; templates/, components/, and public/ require the developer role. Read the nanoesis://reference resource first for the authoring syntax.",
|
|
697
|
+
description: "Create or overwrite a text file. content/ requires the author role; templates/, components/, and public/ require the developer role. Read the nanoesis://reference resource first for the authoring syntax. For template/component paths the response includes `schemaDelta` (added/removed/typeChanged fields) \u2014 if `typeChanged` contains `destructive: true` or `removed` is non-empty, an auto-stamp has been written and authored content may need migration, so surface to the user before further edits. See the reference's 'Authoring guardrails for LLMs' section for the common pitfalls (notably: `data-*` annotations bind to the element, not the token inside it \u2014 moving a token out of its annotated container silently strips them).",
|
|
560
698
|
inputSchema: {
|
|
561
699
|
type: "object",
|
|
562
700
|
properties: {
|
|
@@ -581,7 +719,7 @@ var TOOL_SPECS = [
|
|
|
581
719
|
},
|
|
582
720
|
{
|
|
583
721
|
name: "rename_path",
|
|
584
|
-
description: "Move or rename a file or folder subtree. Refuses to overwrite an existing destination. Requires write rights at both the source and the destination.",
|
|
722
|
+
description: "Move or rename a file or folder subtree. Refuses to overwrite an existing destination. Requires write rights at both the source and the destination. Renaming a template also requires updating every content item bound to the old name (the validation gate will catch unbound items on publish, but `list_pending_migrations` is the cheaper check).",
|
|
585
723
|
inputSchema: {
|
|
586
724
|
type: "object",
|
|
587
725
|
properties: {
|
|
@@ -610,8 +748,59 @@ var TOOL_SPECS = [
|
|
|
610
748
|
description: "Compile and publish the whole site. Runs the validation gate first; if anything would break, it reports the problems and writes nothing.",
|
|
611
749
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
612
750
|
toRequest: (_args, token) => request("POST", "/api/publish", {}, void 0, token)
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
name: "list_pending_migrations",
|
|
754
|
+
description: "List content items whose JSON fields don't match their bound template's schema (orphan fields or missing required fields). Use this after a destructive template edit (you'll see a `stamped` record on write_file) to see what needs migrating, or any time to check site-wide drift.",
|
|
755
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
756
|
+
toRequest: (_args, token) => request("GET", "/api/migrations", {}, void 0, token)
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
name: "preview_migration",
|
|
760
|
+
description: 'For one content item, return the current template HTML, the best-fit snapshot HTML (the "before"), the list of orphan fields with their values, and the names of fields the current template defines. Use this to plan how to migrate (drop, rename, or keep each orphan).',
|
|
761
|
+
inputSchema: {
|
|
762
|
+
type: "object",
|
|
763
|
+
properties: { path: pathArg },
|
|
764
|
+
required: ["path"],
|
|
765
|
+
additionalProperties: false
|
|
766
|
+
},
|
|
767
|
+
toRequest: (args, token) => request("GET", "/api/migrations/preview", { path: str(args, "path") }, void 0, token)
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
name: "apply_migration",
|
|
771
|
+
description: "Resolve a content item's schema drift in one go. `resolution` is a JSON object: `drop` (array of orphan field names to delete), `rename` (object mapping orphan field name \u2192 current template field name to copy the value into), `keep` (array of orphan field names to leave untouched), and `fill` (object mapping current template field name \u2192 value, for missing required fields). Returns the refreshed pending list.",
|
|
772
|
+
inputSchema: {
|
|
773
|
+
type: "object",
|
|
774
|
+
properties: {
|
|
775
|
+
path: pathArg,
|
|
776
|
+
resolution: {
|
|
777
|
+
type: "string",
|
|
778
|
+
description: 'A JSON object encoded as a string: { "drop": string[], "rename": { from: to }, "keep": string[], "fill": { name: value } }.'
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
required: ["path", "resolution"],
|
|
782
|
+
additionalProperties: false
|
|
783
|
+
},
|
|
784
|
+
toRequest: (args, token) => request(
|
|
785
|
+
"POST",
|
|
786
|
+
"/api/migrations/apply",
|
|
787
|
+
{},
|
|
788
|
+
JSON.stringify({
|
|
789
|
+
path: str(args, "path"),
|
|
790
|
+
resolution: parseResolutionArg(str(args, "resolution"))
|
|
791
|
+
}),
|
|
792
|
+
token
|
|
793
|
+
)
|
|
613
794
|
}
|
|
614
795
|
];
|
|
796
|
+
function parseResolutionArg(raw) {
|
|
797
|
+
if (raw === "") return {};
|
|
798
|
+
try {
|
|
799
|
+
return JSON.parse(raw);
|
|
800
|
+
} catch {
|
|
801
|
+
return {};
|
|
802
|
+
}
|
|
803
|
+
}
|
|
615
804
|
var MCP_TOOLS = TOOL_SPECS.map(
|
|
616
805
|
({ name, description, inputSchema }) => ({ name, description, inputSchema })
|
|
617
806
|
);
|
|
@@ -646,7 +835,12 @@ function buildDefaultDiagnostics() {
|
|
|
646
835
|
registry.add(workingStoreRoundTripDiagnostic);
|
|
647
836
|
registry.add(homeTemplateMissingDiagnostic);
|
|
648
837
|
registry.add(noPublishedContentDiagnostic);
|
|
838
|
+
registry.add(templateSnapshotIntegrityDiagnostic);
|
|
839
|
+
registry.add(templateSuffixConflictDiagnostic);
|
|
840
|
+
registry.add(pendingMigrationsDiagnostic);
|
|
649
841
|
registry.addRepair(recreateHomeTemplateRepair);
|
|
842
|
+
registry.addRepair(rebindItemToCurrentRepair);
|
|
843
|
+
registry.addRepair(copySnapshotToCurrentRepair);
|
|
650
844
|
return registry;
|
|
651
845
|
}
|
|
652
846
|
var HOME_PATH = "templates/home.html";
|
|
@@ -690,6 +884,197 @@ var noPublishedContentDiagnostic = {
|
|
|
690
884
|
];
|
|
691
885
|
}
|
|
692
886
|
};
|
|
887
|
+
var templateSnapshotIntegrityDiagnostic = {
|
|
888
|
+
id: "templates.snapshot-integrity",
|
|
889
|
+
async run({ store }) {
|
|
890
|
+
const findings = [];
|
|
891
|
+
const components = await loadComponents(store).catch(() => /* @__PURE__ */ new Map());
|
|
892
|
+
const items = await collectContentItems(store);
|
|
893
|
+
const snapshotCache = /* @__PURE__ */ new Map();
|
|
894
|
+
for (const item of items) {
|
|
895
|
+
if (!isVersionedTemplateName(item.template)) continue;
|
|
896
|
+
const kind = "template";
|
|
897
|
+
const path = `${kind === "template" ? "templates" : "components"}/${item.template}.html`;
|
|
898
|
+
let state = snapshotCache.get(path);
|
|
899
|
+
if (state === void 0) {
|
|
900
|
+
state = await classifySnapshot(store, path, components);
|
|
901
|
+
snapshotCache.set(path, state);
|
|
902
|
+
}
|
|
903
|
+
if (state === "ok") continue;
|
|
904
|
+
const isMissing = state === "missing";
|
|
905
|
+
findings.push({
|
|
906
|
+
id: `templates.${isMissing ? "missing" : "corrupt"}-snapshot:${item.path}`,
|
|
907
|
+
source: "templates",
|
|
908
|
+
severity: "error",
|
|
909
|
+
title: isMissing ? `Pinned snapshot missing: ${path}` : `Pinned snapshot will not parse: ${path}`,
|
|
910
|
+
detail: isMissing ? `${item.path} pins to ${item.template}, but ${path} does not exist. Restore the snapshot from git/backup, or rebind this item to the current template (its fields will need migration after).` : `${item.path} pins to ${item.template}, but ${path} cannot be parsed as a template. Restore valid bytes from git/backup, or rebind this item to the current template.`,
|
|
911
|
+
repair: {
|
|
912
|
+
id: "templates.rebind-item-to-current",
|
|
913
|
+
label: "Rebind item to current template",
|
|
914
|
+
args: { itemPath: item.path }
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
return findings;
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
var rebindItemToCurrentRepair = {
|
|
922
|
+
id: "templates.rebind-item-to-current",
|
|
923
|
+
async run({ store }, args) {
|
|
924
|
+
const itemPath = args["itemPath"];
|
|
925
|
+
if (typeof itemPath !== "string" || itemPath === "") {
|
|
926
|
+
throw new Error("rebind-item-to-current: missing itemPath arg");
|
|
927
|
+
}
|
|
928
|
+
const text = await store.readText(itemPath);
|
|
929
|
+
const data = JSON.parse(text);
|
|
930
|
+
if (typeof data !== "object" || data === null) {
|
|
931
|
+
throw new Error(`rebind-item-to-current: ${itemPath} is not a JSON object`);
|
|
932
|
+
}
|
|
933
|
+
const obj = data;
|
|
934
|
+
if (typeof obj.template !== "string" || !isVersionedTemplateName(obj.template)) {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
obj.template = baseTemplateName(obj.template);
|
|
938
|
+
const next = `${JSON.stringify(obj, null, 2)}
|
|
939
|
+
`;
|
|
940
|
+
await store.write(itemPath, new TextEncoder().encode(next));
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
var templateSuffixConflictDiagnostic = {
|
|
944
|
+
id: "templates.suffix-conflict",
|
|
945
|
+
async run({ store }) {
|
|
946
|
+
const findings = [];
|
|
947
|
+
for (const dir of ["templates", "components"]) {
|
|
948
|
+
let entries;
|
|
949
|
+
try {
|
|
950
|
+
entries = await store.list(dir);
|
|
951
|
+
} catch {
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
const seenBases = /* @__PURE__ */ new Set();
|
|
955
|
+
const snapshots = [];
|
|
956
|
+
for (const entry of entries) {
|
|
957
|
+
if (entry.kind !== "file" || !entry.name.endsWith(".html")) continue;
|
|
958
|
+
const stem = entry.name.slice(0, -".html".length);
|
|
959
|
+
if (isVersionedTemplateName(stem)) {
|
|
960
|
+
snapshots.push({ base: baseTemplateName(stem), snapshotPath: `${dir}/${entry.name}` });
|
|
961
|
+
} else {
|
|
962
|
+
seenBases.add(stem);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
for (const snap of snapshots) {
|
|
966
|
+
if (seenBases.has(snap.base)) continue;
|
|
967
|
+
const currentPath = `${dir}/${snap.base}.html`;
|
|
968
|
+
findings.push({
|
|
969
|
+
id: `templates.suffix-conflict:${snap.snapshotPath}`,
|
|
970
|
+
source: "templates",
|
|
971
|
+
severity: "warning",
|
|
972
|
+
title: `Snapshot has no current sibling: ${snap.snapshotPath}`,
|
|
973
|
+
detail: `${snap.snapshotPath} exists but ${currentPath} does not. Promote the snapshot to current with "Copy snapshot to current", or rename the snapshot off the @v<N> suffix if it was named this way by mistake.`,
|
|
974
|
+
repair: {
|
|
975
|
+
id: "templates.copy-snapshot-to-current",
|
|
976
|
+
label: "Copy snapshot to current",
|
|
977
|
+
args: { snapshotPath: snap.snapshotPath, currentPath }
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return findings;
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
var copySnapshotToCurrentRepair = {
|
|
986
|
+
id: "templates.copy-snapshot-to-current",
|
|
987
|
+
async run({ store }, args) {
|
|
988
|
+
const snapshotPath = args["snapshotPath"];
|
|
989
|
+
const currentPath = args["currentPath"];
|
|
990
|
+
if (typeof snapshotPath !== "string" || typeof currentPath !== "string") {
|
|
991
|
+
throw new Error("copy-snapshot-to-current: missing snapshotPath or currentPath args");
|
|
992
|
+
}
|
|
993
|
+
if (await store.exists(currentPath)) return;
|
|
994
|
+
const bytes = await store.readBytes(snapshotPath);
|
|
995
|
+
await store.write(currentPath, bytes);
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
var pendingMigrationsDiagnostic = {
|
|
999
|
+
id: "content.pending-migrations",
|
|
1000
|
+
async run({ store }) {
|
|
1001
|
+
const components = await loadComponents(store).catch(() => /* @__PURE__ */ new Map());
|
|
1002
|
+
const items = await collectContentItems(store);
|
|
1003
|
+
const templateFieldCache = /* @__PURE__ */ new Map();
|
|
1004
|
+
let pendingCount = 0;
|
|
1005
|
+
for (const item of items) {
|
|
1006
|
+
if (isVersionedTemplateName(item.template)) continue;
|
|
1007
|
+
let expected = templateFieldCache.get(item.template);
|
|
1008
|
+
if (expected === void 0) {
|
|
1009
|
+
const path = `templates/${item.template}.html`;
|
|
1010
|
+
try {
|
|
1011
|
+
const html = await store.readText(path);
|
|
1012
|
+
expected = new Set(deriveFields(html, components).map((f) => f.name));
|
|
1013
|
+
} catch {
|
|
1014
|
+
expected = /* @__PURE__ */ new Set();
|
|
1015
|
+
}
|
|
1016
|
+
templateFieldCache.set(item.template, expected);
|
|
1017
|
+
}
|
|
1018
|
+
const orphans = Object.keys(item.fields).filter((k) => !expected.has(k));
|
|
1019
|
+
if (orphans.length > 0) pendingCount += 1;
|
|
1020
|
+
}
|
|
1021
|
+
if (pendingCount === 0) return [];
|
|
1022
|
+
return [
|
|
1023
|
+
{
|
|
1024
|
+
id: "content.pending-migrations",
|
|
1025
|
+
source: "content",
|
|
1026
|
+
severity: "warning",
|
|
1027
|
+
title: `${pendingCount} item${pendingCount === 1 ? "" : "s"} pending migration`,
|
|
1028
|
+
detail: `${pendingCount} content item${pendingCount === 1 ? "" : "s"} ${pendingCount === 1 ? "has" : "have"} fields not rendered by ${pendingCount === 1 ? "its" : "their"} current template. The Migrations workspace (shipping in Phase 2 of versioning) will resolve these; for now, edit the item JSON to drop or rename the orphan fields. The site continues to publish \u2014 drift is a soft warning, not a publish blocker.`
|
|
1029
|
+
}
|
|
1030
|
+
];
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
async function collectContentItems(store, dir = "content") {
|
|
1034
|
+
const out = [];
|
|
1035
|
+
let entries;
|
|
1036
|
+
try {
|
|
1037
|
+
entries = await store.list(dir);
|
|
1038
|
+
} catch {
|
|
1039
|
+
return out;
|
|
1040
|
+
}
|
|
1041
|
+
for (const entry of entries) {
|
|
1042
|
+
const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
|
|
1043
|
+
if (entry.kind === "dir") {
|
|
1044
|
+
out.push(...await collectContentItems(store, path));
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
if (!entry.name.endsWith(".json") || entry.name.startsWith("_")) continue;
|
|
1048
|
+
let data;
|
|
1049
|
+
try {
|
|
1050
|
+
data = JSON.parse(await store.readText(path));
|
|
1051
|
+
} catch {
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
if (typeof data !== "object" || data === null) continue;
|
|
1055
|
+
const obj = data;
|
|
1056
|
+
const template = obj["template"];
|
|
1057
|
+
if (typeof template !== "string") continue;
|
|
1058
|
+
const fields = obj["fields"];
|
|
1059
|
+
out.push({
|
|
1060
|
+
path,
|
|
1061
|
+
template,
|
|
1062
|
+
fields: typeof fields === "object" && fields !== null ? fields : {}
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
return out;
|
|
1066
|
+
}
|
|
1067
|
+
async function classifySnapshot(store, path, components) {
|
|
1068
|
+
if (!await store.exists(path)) return "missing";
|
|
1069
|
+
try {
|
|
1070
|
+
const html = await store.readText(path);
|
|
1071
|
+
if (html.trim() === "") return "corrupt";
|
|
1072
|
+
analyzeTemplate(html, components);
|
|
1073
|
+
return "ok";
|
|
1074
|
+
} catch {
|
|
1075
|
+
return "corrupt";
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
693
1078
|
async function anyPublishedContentItem(store, dir = "content") {
|
|
694
1079
|
let entries;
|
|
695
1080
|
try {
|
|
@@ -822,9 +1207,14 @@ export {
|
|
|
822
1207
|
authorOptions,
|
|
823
1208
|
buildDefaultDiagnostics,
|
|
824
1209
|
callMcpTool,
|
|
1210
|
+
copySnapshotToCurrentRepair,
|
|
825
1211
|
handleApi,
|
|
826
1212
|
homeTemplateMissingDiagnostic,
|
|
827
1213
|
noPublishedContentDiagnostic,
|
|
1214
|
+
pendingMigrationsDiagnostic,
|
|
828
1215
|
readMcpResource,
|
|
829
|
-
|
|
1216
|
+
rebindItemToCurrentRepair,
|
|
1217
|
+
recreateHomeTemplateRepair,
|
|
1218
|
+
templateSnapshotIntegrityDiagnostic,
|
|
1219
|
+
templateSuffixConflictDiagnostic
|
|
830
1220
|
};
|