mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 15:20:13 +00:00
fix(catalog): explicit section ownership, no body-sniffing heuristic
carriedSectionsFrom() decided whether a ## Usage section was generated by matching its first line against a list of historical opener phrases — so a hand-written Usage section that happened to open that way was classified as generated and silently deleted on regeneration. Ownership is now purely set membership: a section is generated iff its heading is one the template emits, and ambiguous 'usage' is no longer in that set (the template never emits it), so any ## Usage is carried. Exported carriedSectionsFrom behind an entrypoint guard and added two executable preservation fixtures. Flagged by Magi (#5).
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import { readdirSync, readFileSync } from "node:fs";
|
import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
import { dirname, join, resolve } from "node:path";
|
import { dirname, join, resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { describe, expect, it } from "vitest";
|
import { afterAll, describe, expect, it } from "vitest";
|
||||||
|
import { carriedSectionsFrom } from "../../../../scripts/generate-catalog-pages.ts";
|
||||||
|
|
||||||
const here = dirname(fileURLToPath(import.meta.url));
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
const repoRoot = resolve(here, "../../../..");
|
const repoRoot = resolve(here, "../../../..");
|
||||||
@@ -39,3 +41,77 @@ describe("catalog pages keep the required reader continuation", () => {
|
|||||||
expect(lastHeading).toBe("## Related topics");
|
expect(lastHeading).toBe("## Related topics");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The carry-forward path is the subtler half: a regeneration must preserve every
|
||||||
|
// hand-written section, including one whose heading (## Usage) the generator once
|
||||||
|
// emitted and whose first line looks like generated prose, and one appended below
|
||||||
|
// the generated footer marker.
|
||||||
|
describe("carriedSectionsFrom preserves hand-written sections", () => {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), "hf-catalog-carry-"));
|
||||||
|
afterAll(() => rmSync(dir, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const write = (name: string, body: string) => {
|
||||||
|
const p = join(dir, name);
|
||||||
|
writeFileSync(p, body, "utf-8");
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
|
it("keeps a custom Usage section that opens like generated prose", () => {
|
||||||
|
const page = write(
|
||||||
|
"usage.mdx",
|
||||||
|
[
|
||||||
|
"## Install",
|
||||||
|
"",
|
||||||
|
"```bash Terminal",
|
||||||
|
"npx hyperframes add sample",
|
||||||
|
"```",
|
||||||
|
"",
|
||||||
|
"## Usage",
|
||||||
|
"",
|
||||||
|
// deliberately opens with a phrase an old generated Usage used
|
||||||
|
"After installing, add the block — but here is our own hand-written guidance the author cares about.",
|
||||||
|
"",
|
||||||
|
"{/* hf:generated-footer */}",
|
||||||
|
"",
|
||||||
|
"Tagged `sample`.",
|
||||||
|
"",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
const carried = carriedSectionsFrom(page);
|
||||||
|
const joined = carried.sections.join("\n");
|
||||||
|
expect(joined).toContain("## Usage");
|
||||||
|
expect(joined).toContain("hand-written guidance the author cares about");
|
||||||
|
expect(carried.hasCustomUsage).toBe(true);
|
||||||
|
// The generated Install section is owned by the template and not carried.
|
||||||
|
expect(joined).not.toContain("## Install");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a hand-written section appended below the generated footer", () => {
|
||||||
|
const page = write(
|
||||||
|
"below-footer.mdx",
|
||||||
|
[
|
||||||
|
"## Install",
|
||||||
|
"",
|
||||||
|
"text",
|
||||||
|
"",
|
||||||
|
"{/* hf:generated-footer */}",
|
||||||
|
"",
|
||||||
|
"Tagged `sample`.",
|
||||||
|
"",
|
||||||
|
"## Related topics",
|
||||||
|
"",
|
||||||
|
"- [Browse the complete Catalog](/catalog)",
|
||||||
|
"",
|
||||||
|
"## Field notes",
|
||||||
|
"",
|
||||||
|
"A section a human added after the generated tail.",
|
||||||
|
"",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
const joined = carriedSectionsFrom(page).sections.join("\n");
|
||||||
|
expect(joined).toContain("## Field notes");
|
||||||
|
expect(joined).toContain("A section a human added after the generated tail.");
|
||||||
|
// The generator's own Related topics is not carried (it re-emits it).
|
||||||
|
expect(joined).not.toContain("## Related topics");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
||||||
import { join, resolve, dirname } from "node:path";
|
import { join, resolve, dirname } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
// Import from source — bun workspace linking doesn't resolve for scripts outside packages/.
|
// Import from source — bun workspace linking doesn't resolve for scripts outside packages/.
|
||||||
import {
|
import {
|
||||||
type FileTarget,
|
type FileTarget,
|
||||||
@@ -118,10 +118,12 @@ const GENERATED_HEADINGS = new Set([
|
|||||||
"ask an agent for it",
|
"ask an agent for it",
|
||||||
"make the texture move",
|
"make the texture move",
|
||||||
"every texture",
|
"every texture",
|
||||||
// headings earlier revisions emitted — dropped on purpose, never carried
|
// headings earlier revisions emitted — dropped on purpose, never carried.
|
||||||
|
// `usage` is deliberately NOT listed: the current template never emits it, and
|
||||||
|
// it is a heading a human might reasonably write, so ownership stays explicit
|
||||||
|
// (anything not in this set is hand-written) rather than sniffing the body.
|
||||||
"details",
|
"details",
|
||||||
"files",
|
"files",
|
||||||
"usage",
|
|
||||||
"source prompt",
|
"source prompt",
|
||||||
"agent usage",
|
"agent usage",
|
||||||
"animated texture",
|
"animated texture",
|
||||||
@@ -130,19 +132,6 @@ const GENERATED_HEADINGS = new Set([
|
|||||||
"related topics",
|
"related topics",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Openers of every "## Usage" body this generator has written. A Usage section
|
|
||||||
* that starts with none of them was rewritten by hand, so it is kept and the
|
|
||||||
* generated usage prose steps aside for it.
|
|
||||||
*/
|
|
||||||
const GENERATED_USAGE_OPENERS = [
|
|
||||||
/^After installing, add the block/,
|
|
||||||
/^Open `[^`]+` and paste its contents/,
|
|
||||||
/^Open `[^`]+` and copy what is inside/,
|
|
||||||
/^After `npx hyperframes add/,
|
|
||||||
/^It runs for /,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the start of the generated provenance footer (tags, credit, prompt).
|
* Marks the start of the generated provenance footer (tags, credit, prompt).
|
||||||
* That footer carries no heading of its own, so without this marker the section
|
* That footer carries no heading of its own, so without this marker the section
|
||||||
@@ -170,8 +159,10 @@ const RELATED_TOPICS: readonly string[] = [
|
|||||||
* Pull the hand-written `## sections` out of an already-generated page.
|
* Pull the hand-written `## sections` out of an already-generated page.
|
||||||
* Returns the raw lines, heading included, in their original order.
|
* Returns the raw lines, heading included, in their original order.
|
||||||
*/
|
*/
|
||||||
|
// Exported for the preservation fixture in
|
||||||
|
// packages/core/src/registry/catalogGeneratorInstructions.test.ts.
|
||||||
// fallow-ignore-next-line complexity
|
// fallow-ignore-next-line complexity
|
||||||
function carriedSectionsFrom(pagePath: string): CarriedContent {
|
export function carriedSectionsFrom(pagePath: string): CarriedContent {
|
||||||
const empty: CarriedContent = { sections: [], hasCustomUsage: false };
|
const empty: CarriedContent = { sections: [], hasCustomUsage: false };
|
||||||
if (!existsSync(pagePath)) return empty;
|
if (!existsSync(pagePath)) return empty;
|
||||||
let text: string;
|
let text: string;
|
||||||
@@ -192,12 +183,12 @@ function carriedSectionsFrom(pagePath: string): CarriedContent {
|
|||||||
while (buffer.length && buffer[0]!.trim() === "") buffer.shift();
|
while (buffer.length && buffer[0]!.trim() === "") buffer.shift();
|
||||||
while (buffer.length && buffer.at(-1)!.trim() === "") buffer.pop();
|
while (buffer.length && buffer.at(-1)!.trim() === "") buffer.pop();
|
||||||
|
|
||||||
|
// Ownership is explicit: a section is generated iff its heading is one the
|
||||||
|
// template emits (GENERATED_HEADINGS). Everything else is hand-written and
|
||||||
|
// carried verbatim — no content heuristic that could misread custom prose as
|
||||||
|
// generated and silently delete it on the next regeneration.
|
||||||
const key = heading.toLowerCase();
|
const key = heading.toLowerCase();
|
||||||
const handWritten =
|
if (!GENERATED_HEADINGS.has(key) && buffer.length) {
|
||||||
!GENERATED_HEADINGS.has(key) ||
|
|
||||||
(key === "usage" && !GENERATED_USAGE_OPENERS.some((re) => re.test(buffer[0] ?? "")));
|
|
||||||
|
|
||||||
if (handWritten && buffer.length) {
|
|
||||||
if (key === "usage") hasCustomUsage = true;
|
if (key === "usage") hasCustomUsage = true;
|
||||||
sections.push(`## ${heading}`, "", ...buffer, "");
|
sections.push(`## ${heading}`, "", ...buffer, "");
|
||||||
}
|
}
|
||||||
@@ -844,4 +835,7 @@ function main(): void {
|
|||||||
console.log("\nDone.");
|
console.log("\nDone.");
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
// Only regenerate when run directly, so the module can be imported by tests.
|
||||||
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user