Make repo Face articles interpreter-owned

This commit is contained in:
Meta Vens 2026-05-22 16:48:29 +01:00
commit 44be152744
12 changed files with 369 additions and 74 deletions

View file

@ -498,7 +498,7 @@ function parseDslBlocks(text) {
const lines = text.split(/\r?\n/);
const blocks = [];
for (let index = 0; index < lines.length; index += 1) {
const kind = ["STATE NOTE", "SAY", "BIFROST TOPIC"].includes(lines[index].trim().toUpperCase())
const kind = ["STATE NOTE", "SAY", "ARTICLE", "BIFROST TOPIC"].includes(lines[index].trim().toUpperCase())
? lines[index].trim().toUpperCase()
: undefined;
if (!kind) {

View file

@ -0,0 +1,88 @@
import {
normalizeArticlePath,
renderRepoIdentityArticleMarkdown,
resolveArticleRepoRoot,
validateRenderedArticleMarkdown,
type RepoIdentityArticleIntent,
} from "../apps/worker/src/repo-face-article";
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
const nibuIdentity = {
displayName: "Nibu",
repoName: "AetheriaLore",
repoPath: "E:/Projects/AetheriaLore",
};
const aetheriaIntent: RepoIdentityArticleIntent = {
identity: "nibu",
site: "aetheria",
title: "Timeline Smearing And The Bodies It Steals",
description: "Nibu's bylined argument about adjacent-timeline survival pressure.",
author: "Nibu",
date: "2026-05-22",
tags: ["Aetheria", "Nibu", "glitchcraft"],
body: [
"---",
"title: Wrong",
"---",
"",
"## The rude part",
"",
"Resetting is not a save button. It is a change of which incarnation has to pay attention.",
].join("\n"),
};
const aetheriaPath = normalizeArticlePath(aetheriaIntent, nibuIdentity);
assert(
aetheriaPath === "Aetheria/Articles/nibu/2026-05-22-timeline-smearing-and-the-bodies-it-steals.md",
`unexpected Aetheria article path: ${aetheriaPath}`,
);
const markdown = renderRepoIdentityArticleMarkdown(aetheriaIntent, nibuIdentity);
validateRenderedArticleMarkdown(markdown, aetheriaIntent);
assert(markdown.startsWith("---\n"), "article markdown should start with deterministic frontmatter");
assert(markdown.includes('title: "Timeline Smearing And The Bodies It Steals"'), "title frontmatter missing");
assert(markdown.includes('description: "Nibu\'s bylined argument'), "description frontmatter missing");
assert(markdown.includes('author: "Nibu"'), "author frontmatter missing");
assert(markdown.includes("date: 2026-05-22"), "date frontmatter missing");
assert(markdown.includes(' - "glitchcraft"'), "tag frontmatter missing");
assert(!markdown.includes("title: Wrong"), "model-provided frontmatter should be stripped from body");
const aquaIdentity = {
displayName: "Aqua",
repoName: "AquaSynth",
repoPath: "E:/Projects/AquaSynth",
};
const gamecultIntent: RepoIdentityArticleIntent = {
identity: "aqua",
title: "A Tiny Fish Demands Better Patch Cards",
description: "Aqua's bylined note about readable synth progress artifacts.",
date: "2026-05-22",
tags: [],
body: "A patch card is a promise the ear can cash.",
};
const gamecultPath = normalizeArticlePath(gamecultIntent, aquaIdentity);
assert(
gamecultPath === "GameCult/Blog/2026-05-22-a-tiny-fish-demands-better-patch-cards.md",
`unexpected GameCult blog path: ${gamecultPath}`,
);
const gamecultRoot = resolveArticleRepoRoot(gamecultIntent, aquaIdentity, {
sourceRepoRoot: "E:/Projects",
storageRoot: "E:/Projects/VoidBot/.voidbot",
});
assert(gamecultRoot.replace(/\\/g, "/").endsWith("/gamecult-site"), `unexpected GameCult root: ${gamecultRoot}`);
let rejected = false;
try {
normalizeArticlePath({ ...gamecultIntent, path: "AquaSynth/Notes/bad.md" }, aquaIdentity);
} catch {
rejected = true;
}
assert(rejected, "GameCult article path outside GameCult/Blog should be rejected");
console.log("repo Face article contract smoke passed");

View file

@ -132,7 +132,7 @@ function evaluateInterpreter(input) {
failures.push(`interpreter chose ${parsed.decision}: ${parsed.reason ?? "no reason"}`);
}
if (parsed.decision === "route" && parsed.blocks.length === 0) {
failures.push("interpreter routed but emitted no STATE NOTE, SAY, or BIFROST TOPIC blocks");
failures.push("interpreter routed but emitted no STATE NOTE, SAY, ARTICLE, or BIFROST TOPIC blocks");
}
if (!parsed.blocks.some((block) => block.kind === "STATE NOTE")) {
failures.push("interpreter did not preserve any durable state note");
@ -147,6 +147,13 @@ function evaluateInterpreter(input) {
if (block.kind === "BIFROST TOPIC" && !String(block.fields.content ?? "").trim()) {
failures.push("BIFROST TOPIC block has no content");
}
if (block.kind === "ARTICLE") {
for (const key of ["title", "description", "body"]) {
if (!String(block.fields[key] ?? "").trim()) {
failures.push(`ARTICLE block has no ${key}`);
}
}
}
}
return failures;
}
@ -322,7 +329,7 @@ function parseDslBlocks(text) {
const lines = text.split(/\r?\n/);
const blocks = [];
for (let index = 0; index < lines.length; index += 1) {
const kind = ["STATE NOTE", "SAY", "BIFROST TOPIC"].includes(lines[index].trim().toUpperCase())
const kind = ["STATE NOTE", "SAY", "ARTICLE", "BIFROST TOPIC"].includes(lines[index].trim().toUpperCase())
? lines[index].trim().toUpperCase()
: undefined;
if (!kind) {