|
| 1 | +/** |
| 2 | + * The site payload — everything metaobjects.dev publishes, assembled once and gated |
| 3 | + * on the way out. |
| 4 | + * |
| 5 | + * The site's strongest claim is that its code blocks are real `meta gen` output. This |
| 6 | + * module is where that claim is made true rather than asserted: every snippet is READ |
| 7 | + * from the artifact it claims to come from, and four gates run during assembly, each |
| 8 | + * throwing rather than degrading. A payload that builds is a payload whose claims held. |
| 9 | + * |
| 10 | + * subsequence a published excerpt must still be an in-order subsequence of the real |
| 11 | + * generated file, so a renamed symbol or a dropped export fails here |
| 12 | + * instead of shipping. Elisions are COMPUTED from the match, so the |
| 13 | + * page cannot imply contiguity it does not have. |
| 14 | + * drift fixture the `verify` transcript must still be a FAILING run. A fixture gone |
| 15 | + * green would publish a screenshot of an error that no longer happens. |
| 16 | + * requirements `meta verify` on the showcase must exit 0, so the requirements page's |
| 17 | + * "resolved, not trusted" claim is checked rather than repeated. |
| 18 | + * home paths the assembled JSON must carry no absolute user path. This repo is |
| 19 | + * public and the payload publishes to a public site. |
| 20 | + * |
| 21 | + * Deterministic by construction: no timestamps, no durations (normalizeTranscript |
| 22 | + * replaces them), and object keys in registry order — so a rebuild that changed nothing |
| 23 | + * is byte-identical and `--check` means something. |
| 24 | + */ |
| 25 | +import { readFileSync } from "node:fs"; |
| 26 | +import { resolve } from "node:path"; |
| 27 | +import { SNIPPETS, type SnippetSource } from "./snippets.js"; |
| 28 | +import { extractMarkedRegion } from "./markers.js"; |
| 29 | +import { splitLines, matchSubsequence, renderWithElisions } from "./subsequence.js"; |
| 30 | +import { loadVocabulary, highlightMetadata, type Vocabulary } from "./highlight-metadata.js"; |
| 31 | +import { highlightCode } from "./highlight-code.js"; |
| 32 | +import { captureTranscript, normalizeTranscript, HOME_PATH } from "./transcript.js"; |
| 33 | + |
| 34 | +export interface Snippet { |
| 35 | + lang: string; |
| 36 | + /** Highlighted HTML of what the page shows inline. */ |
| 37 | + inline: string; |
| 38 | + /** Highlighted HTML of the whole generated file, for expand-to-view. */ |
| 39 | + full: string | null; |
| 40 | + /** Lines in the full file — the page says "N lines" on the expander. */ |
| 41 | + lineCount: number | null; |
| 42 | +} |
| 43 | + |
| 44 | +export interface Registries { |
| 45 | + npm: string; pypi: string; nuget: string; maven: string; metamodel: string; |
| 46 | +} |
| 47 | + |
| 48 | +export interface SitePayload { |
| 49 | + registries: Registries; |
| 50 | + snippets: Record<string, Snippet>; |
| 51 | +} |
| 52 | + |
| 53 | +const REGISTRY_MANIFEST = "fixtures/registry-conformance/expected-registry.json"; |
| 54 | + |
| 55 | +/** |
| 56 | + * The showcase's prompt text lives in `templates/`, not `verify`'s default `prompts/`. |
| 57 | + * Omitting this yields ERR_PARTIAL_UNRESOLVED — a failure about where a file lives, |
| 58 | + * which would masquerade as the payload-drift signal these two captures are about. |
| 59 | + * The same flag is on the drift capture in snippets.ts, for the same reason. |
| 60 | + */ |
| 61 | +const PROMPTS_ARGS = ["--prompts", "templates"]; |
| 62 | + |
| 63 | +const read = (repoRoot: string, rel: string) => readFileSync(resolve(repoRoot, rel), "utf8"); |
| 64 | + |
| 65 | +/** First capture of `re` in a file, or a throw naming what was being looked for. */ |
| 66 | +function readVersion(repoRoot: string, rel: string, re: RegExp, what: string): string { |
| 67 | + const m = re.exec(read(repoRoot, rel)); |
| 68 | + const v = m?.[1]; |
| 69 | + if (v === undefined) throw new Error(`site payload: could not read the ${what} version from ${rel}`); |
| 70 | + return v; |
| 71 | +} |
| 72 | + |
| 73 | +/** |
| 74 | + * Five coordinates, never one string. The registries do NOT share a version — Maven |
| 75 | + * runs on its historical major 7 — so a single `version` field could only be right for |
| 76 | + * three of the four, and the page would state the wrong one somewhere. `metamodel` is a |
| 77 | + * separate contract again (ADR-0035 Amendment 2): it moves when the METADATA changes, |
| 78 | + * independently of any package line. |
| 79 | + */ |
| 80 | +function readRegistries(repoRoot: string): Registries { |
| 81 | + return { |
| 82 | + npm: readVersion(repoRoot, "server/typescript/packages/cli/package.json", |
| 83 | + /"version":\s*"([^"]+)"/, "npm"), |
| 84 | + pypi: readVersion(repoRoot, "server/python/pyproject.toml", |
| 85 | + /^version\s*=\s*"([^"]+)"/m, "PyPI"), |
| 86 | + nuget: readVersion(repoRoot, "server/csharp/Directory.Build.props", |
| 87 | + /<Version>([^<]+)<\/Version>/, "NuGet"), |
| 88 | + maven: readVersion(repoRoot, "server/java/pom.xml", |
| 89 | + /<version>([^<]+)<\/version>/, "Maven"), |
| 90 | + metamodel: readVersion(repoRoot, REGISTRY_MANIFEST, |
| 91 | + /"metamodelVersion":\s*"([^"]+)"/, "metamodel"), |
| 92 | + }; |
| 93 | +} |
| 94 | + |
| 95 | +/** |
| 96 | + * A hand-authored file, delimited in place. Already whole — nothing to expand to. |
| 97 | + * |
| 98 | + * Vocabulary is deliberately NOT gated here. highlightMetadata is tolerant by design: |
| 99 | + * no key allow-list can be complete (`attr.properties` is a chartered arbitrary bag, |
| 100 | + * and `attr.expression`/`attr.filter` carry their own grammars whose inner keys are not |
| 101 | + * registry attrs), so a throw would be a false-positive generator — measured at 8 false |
| 102 | + * failures against the real corpora. The vocabulary gate is the LOADER, which is |
| 103 | + * strictly stronger and has none: a marked region lives inside a real model, and |
| 104 | + * `assertRequirementsResolve` loads it. |
| 105 | + * |
| 106 | + * `lang` is "yaml" rather than the registry's value: a marker's content is metadata |
| 107 | + * YAML by construction (that is what highlightMetadata renders), and `Lang` — the |
| 108 | + * highlight-code language set — has no member for it, so the registry cannot say so. |
| 109 | + */ |
| 110 | +function markerSnippet(repoRoot: string, id: string, src: Extract<SnippetSource, { kind: "marker" }>, |
| 111 | + vocab: Vocabulary): Snippet { |
| 112 | + const region = extractMarkedRegion(read(repoRoot, src.file), id); |
| 113 | + return { lang: "yaml", inline: highlightMetadata(region, vocab), full: null, lineCount: null }; |
| 114 | +} |
| 115 | + |
| 116 | +/** |
| 117 | + * Machine-owned output, published as an excerpt. The excerpt is not trusted: it must be |
| 118 | + * an in-order subsequence of the real file, and where it skips, the elision marker is |
| 119 | + * computed from the match rather than authored — which is exactly how the landing page |
| 120 | + * came to claim "this exact model" while eliding three members. |
| 121 | + */ |
| 122 | +function excerptSnippet(repoRoot: string, id: string, |
| 123 | + src: Extract<SnippetSource, { kind: "excerpt" }>): Snippet { |
| 124 | + const inlineLines = splitLines(read(repoRoot, src.inline)); |
| 125 | + const fullText = read(repoRoot, src.full); |
| 126 | + const fullLines = splitLines(fullText); |
| 127 | + |
| 128 | + const m = matchSubsequence(inlineLines, fullLines); |
| 129 | + if (!m.ok) { |
| 130 | + throw new Error( |
| 131 | + `site payload: snippet "${id}" is stale.\n` + |
| 132 | + ` ${src.inline} line ${m.failedAt + 1} is not present, in order, in ${src.full}:\n` + |
| 133 | + ` ${m.line.trim()}\n` + |
| 134 | + ` The site would publish it as real generated output. Re-cut the excerpt.`); |
| 135 | + } |
| 136 | + |
| 137 | + const rendered = renderWithElisions(inlineLines, m.positions, fullLines.length); |
| 138 | + return { |
| 139 | + lang: src.lang, |
| 140 | + inline: highlightCode(rendered.join("\n"), src.lang), |
| 141 | + full: highlightCode(fullText, src.lang), |
| 142 | + lineCount: fullLines.length, |
| 143 | + }; |
| 144 | +} |
| 145 | + |
| 146 | +/** |
| 147 | + * Machine-owned output short enough to publish ENTIRE. The published text IS the file, |
| 148 | + * a stricter guarantee than any excerpt, so there is no subsequence gate and nothing to |
| 149 | + * expand to. |
| 150 | + */ |
| 151 | +function wholeSnippet(repoRoot: string, src: Extract<SnippetSource, { kind: "whole" }>): Snippet { |
| 152 | + return { |
| 153 | + lang: src.lang, |
| 154 | + inline: highlightCode(read(repoRoot, src.file), src.lang), |
| 155 | + full: null, |
| 156 | + lineCount: null, |
| 157 | + }; |
| 158 | +} |
| 159 | + |
| 160 | +/** |
| 161 | + * Live CLI output. The exit code is the gate: this transcript exists to show `verify` |
| 162 | + * CATCHING drift, so a fixture that started passing would leave the page showing an |
| 163 | + * error the tool no longer emits — true once, false now, and invisible in a diff |
| 164 | + * because the captured text would simply change. |
| 165 | + */ |
| 166 | +function transcriptSnippet(repoRoot: string, id: string, |
| 167 | + src: Extract<SnippetSource, { kind: "transcript" }>): Snippet { |
| 168 | + const { text, exitCode } = captureTranscript(src.argv, resolve(repoRoot, src.cwd)); |
| 169 | + if (exitCode === 0) { |
| 170 | + throw new Error( |
| 171 | + `site payload: the "${id}" fixture now PASSES (exit 0).\n` + |
| 172 | + ` It is published as a demonstration of \`meta ${src.argv.join(" ")}\` catching drift.\n` + |
| 173 | + ` A passing fixture means the page would show an error that no longer happens.`); |
| 174 | + } |
| 175 | + return { |
| 176 | + lang: "console", |
| 177 | + inline: highlightCode(normalizeTranscript(text, repoRoot), "console"), |
| 178 | + full: null, |
| 179 | + lineCount: null, |
| 180 | + }; |
| 181 | +} |
| 182 | + |
| 183 | +/** |
| 184 | + * The requirements page claims a requirement's `implementedBy` is RESOLVED, not |
| 185 | + * trusted. `meta verify` is what resolves it, so the claim is only true while this |
| 186 | + * exits 0 — and a dangling reference is an ERROR, so a broken link cannot hide in the |
| 187 | + * warning cap. |
| 188 | + */ |
| 189 | +function assertRequirementsResolve(repoRoot: string): void { |
| 190 | + const showcase = resolve(repoRoot, "examples/showcase"); |
| 191 | + const { text, exitCode } = captureTranscript(["verify", ...PROMPTS_ARGS], showcase); |
| 192 | + if (exitCode !== 0) { |
| 193 | + throw new Error( |
| 194 | + `site payload: \`meta verify\` fails on examples/showcase (exit ${exitCode}).\n` + |
| 195 | + ` The site publishes its requirement links as resolved; they are not.\n\n` + |
| 196 | + normalizeTranscript(text, repoRoot)); |
| 197 | + } |
| 198 | +} |
| 199 | + |
| 200 | +export function buildPayload(repoRoot: string): SitePayload { |
| 201 | + const vocab = loadVocabulary(resolve(repoRoot, REGISTRY_MANIFEST)); |
| 202 | + |
| 203 | + const snippets: Record<string, Snippet> = {}; |
| 204 | + for (const [id, src] of Object.entries(SNIPPETS)) { |
| 205 | + switch (src.kind) { |
| 206 | + case "marker": snippets[id] = markerSnippet(repoRoot, id, src, vocab); break; |
| 207 | + case "excerpt": snippets[id] = excerptSnippet(repoRoot, id, src); break; |
| 208 | + case "whole": snippets[id] = wholeSnippet(repoRoot, src); break; |
| 209 | + case "transcript": snippets[id] = transcriptSnippet(repoRoot, id, src); break; |
| 210 | + } |
| 211 | + } |
| 212 | + |
| 213 | + assertRequirementsResolve(repoRoot); |
| 214 | + |
| 215 | + const payload: SitePayload = { registries: readRegistries(repoRoot), snippets }; |
| 216 | + |
| 217 | + // Final sweep. HOME_PATH is IMPORTED, never respelled here: two spellings of one rule |
| 218 | + // is how the weaker one ends up being the one that runs. normalizeTranscript already |
| 219 | + // applies it to captured output; this catches a leak arriving by any other route — |
| 220 | + // an absolute path baked into a committed excerpt, say. |
| 221 | + const leak = HOME_PATH.exec(JSON.stringify(payload)); |
| 222 | + if (leak) { |
| 223 | + throw new Error( |
| 224 | + `site payload: absolute home path ${leak[0]} — this repository is public and the ` + |
| 225 | + `payload publishes to a public site. Refusing to emit it.`); |
| 226 | + } |
| 227 | + return payload; |
| 228 | +} |
0 commit comments