Files
indiekit-server/scripts/patch-micropub-ai-block-resync.mjs
svemagie 3f367610f5 fix: force one-time ai: block resync for posts with stale files
The v3 patch bug allowed Micropub to update MongoDB with aiTextLevel/
aiCodeLevel values but write post files without the ai: frontmatter
block (supportsAiDisclosure was always false). Re-saving with the same
values correctly returned "no properties changed" — but the file on disk
remained stale.

New patch (patch-micropub-ai-block-resync.mjs) adds _aiBlockVersion to
each post document in MongoDB. On update, if a post has AI fields but
_aiBlockVersion != "v4", the no-change check is bypassed and the file
is force-rewritten exactly once. Subsequent no-change saves behave
normally.

Also adds AI transparency section to README documenting the full
implementation, patch chain, v4 root cause, and re-save instructions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 09:03:22 +01:00

116 lines
4.2 KiB
JavaScript

/**
* Patch @indiekit/endpoint-micropub/lib/post-data.js to detect stale AI block files.
*
* Problem: The v3 patch bug (supportsAiDisclosure always false) caused Indiekit to update
* MongoDB with AI field values (aiTextLevel, aiCodeLevel, etc.) but write the post file
* WITHOUT the ai: frontmatter block. Now when the user re-saves with the same AI values,
* Indiekit's isDeepStrictEqual check says "no properties changed" and skips the file write.
* The file remains stale (missing ai: block) even though MongoDB has the right data.
*
* Fix: Store an `_aiBlockVersion` field in MongoDB alongside each post. On update, if the
* stored version doesn't match the current patch version AND the post has AI fields, bypass
* the no-change check and force a file re-write. This triggers exactly once per affected
* post, then every subsequent no-change save correctly skips the write.
*/
import { access, readFile, writeFile } from "node:fs/promises";
const AI_BLOCK_VERSION = "v4";
const candidates = [
"node_modules/@indiekit/endpoint-micropub/lib/post-data.js",
"node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/post-data.js",
];
const marker = "AI block version resync patch";
// --- Old: simple destructuring that ignores _aiBlockVersion ---
const oldDestructure = `let { path: _originalPath, properties } = await this.read(application, url);`;
const newDestructure = `let { path: _originalPath, properties, _aiBlockVersion: storedAiBlockVersion } = await this.read(application, url); // AI block version resync patch`;
// --- Old: early return when no properties changed ---
const oldNoChange = ` // Return if no changes to template properties detected
const newProperties = getPostTemplateProperties(properties);
oldProperties = getPostTemplateProperties(oldProperties);
if (isDeepStrictEqual(newProperties, oldProperties)) {
return;
}`;
const newNoChange = ` // Return if no changes to template properties detected
const newProperties = getPostTemplateProperties(properties);
oldProperties = getPostTemplateProperties(oldProperties);
if (isDeepStrictEqual(newProperties, oldProperties)) {
// AI block version resync patch: if post has AI fields and the file was written by an
// older patch version (or never written with the ai: block), force a one-time re-write.
const hasAiFields =
newProperties.aiTextLevel !== undefined ||
newProperties.aiCodeLevel !== undefined;
const currentAiBlockVersion = "${AI_BLOCK_VERSION}";
if (!hasAiFields || storedAiBlockVersion === currentAiBlockVersion) {
return;
}
// Fall through: force re-write to fix stale ai: block
}`;
// --- Old: postData construction without _aiBlockVersion ---
const oldPostData = ` // Update data in posts collection
const postData = { _originalPath, path, properties };`;
const newPostData = ` // Update data in posts collection
const postData = { _originalPath, path, properties, _aiBlockVersion: "${AI_BLOCK_VERSION}" }; // AI block version resync patch`;
async function exists(filePath) {
try {
await access(filePath);
return true;
} catch {
return false;
}
}
let checked = 0;
let patched = 0;
for (const filePath of candidates) {
if (!(await exists(filePath))) {
continue;
}
checked += 1;
const source = await readFile(filePath, "utf8");
if (source.includes(marker)) {
continue;
}
if (
!source.includes(oldDestructure) ||
!source.includes(oldNoChange) ||
!source.includes(oldPostData)
) {
console.warn(
`[postinstall] Skipping micropub AI block resync patch for ${filePath}: upstream format changed`,
);
continue;
}
const updated = source
.replace(oldDestructure, newDestructure)
.replace(oldNoChange, newNoChange)
.replace(oldPostData, newPostData);
await writeFile(filePath, updated, "utf8");
patched += 1;
}
if (checked === 0) {
console.log("[postinstall] No endpoint-micropub post-data.js found");
} else if (patched === 0) {
console.log("[postinstall] micropub AI block resync patch already applied");
} else {
console.log(
`[postinstall] Patched micropub AI block resync in ${patched} file(s)`,
);
}