Skip to content

feat(artifact-format): pure structural artifact file format codec - #174

Open
stonexer wants to merge 7 commits into
mainfrom
fm/artifact-codec-v1
Open

feat(artifact-format): pure structural artifact file format codec#174
stonexer wants to merge 7 commits into
mainfrom
fm/artifact-codec-v1

Conversation

@stonexer

@stonexer stonexer commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Intent

Stage 3 of a captain-directed rewrite: narrow @loopany/artifact-format from the v1 domain-aware format library into a PURE STRUCTURAL CODEC with zero domain knowledge, implementing a test suite that was authored and captain-approved in stage 2 on this same branch.

BACKGROUND THE DIFF DOES NOT SHOW. The platform design (Graph Engineering v3) was rewritten. The v1 package survives but its role narrows. This branch has three commits by design and that layering is deliberate, not accidental: (1) a tests-first commit authoring the target suite while src was untouched (a red suite was the expected and directed state), (2) a fix to one off-by-one in my own depth-boundary test case, (3) the implementation that turns the suite green. Do not flag the tests-before-implementation ordering as a mistake; it was the directive.

THE EIGHT DESIGN DECISIONS BELOW WERE EXPLICITLY REVIEWED AND APPROVED BY THE CAPTAIN before implementation, from a written test catalog. They are settled. Do not relitigate them:

  • D1: checkTimestamp(value, path) returns an ArtifactIssue or null, rather than a boolean predicate, so it composes directly into a server seam's issue accumulation loop.
  • D2: serializeArtifact's keyOrder option applies to the TOP-LEVEL mapping only; nested mappings are always pure lexicographic. Rationale: a caller's presentation intent for the head must not reach down and reorder a nested value that merely shares a key name.
  • D3: keyOrder entries absent from the data are skipped (never invented/defaulted); a key listed twice keeps its first position; an empty array behaves exactly like omitting the option.
  • D4: issue accumulation carries real paths (nested.third, list[1]) and reports ALL offending values in one error. This is a deliberate strengthening over v1, which threw on the first host object with a generic path; 'reports all its problems in one error' is unachievable without paths.
  • D5: the SCHEMA_VIOLATION error code is retained for structural value rejection (the error-code system was directed to be kept; the name still reads correctly for 'the structural schema').
  • D6: updateArtifactFrontMatter, splitArtifact, bodyFormatOf and ARTIFACT_FORMAT_VERSION are all KEPT. The directive removed them nowhere and each is structural with no domain knowledge.
  • D7: the v1 test files (parse/serialize/render tests) were DELETED rather than left alongside the new suite, because they encode the superseded domain contract and keeping them would make a red run unreadable. They remain in history on branch fm/artifact-format-a1. This is intentional deletion of coverage that no longer describes the product, not lost coverage: every structural behavior still in scope is re-authored in the new suite.
  • D8: the export-surface test also asserts marked and sanitize-html are absent from package.json, because 'the render module is stripped' is only true when its dependencies go too.

WHAT CHANGED AND WHY:

  • Domain schema OUT. type/status/source/externalId/sourceUrl/attachments, the required-type rule, and the source+externalId pairing rule are all removed, along with the ArtifactCoreFields type. Structural validation is now exactly two rules: the head parses to a YAML mapping, and a declared format is a known enum. EVERY other key is preserved verbatim. Pass-through is the CORRECT behavior at this level and not a fallback or a leniency: an unknown key is not a key the codec failed to understand, it is a key that is none of the codec's business. Object kinds (task/doc/loop) and their closed front-matter key sets now live in a server-side seam module that this library must NOT know about. A reviewer should NOT read the removal of validation as a security or robustness regression - it is the point of the stage, and rejection moved, it did not disappear.
  • Key order parametrized. CORE_FIELD_ORDER removed; no key is privileged any more. Default is pure lexicographic at every depth; serializeArtifact(doc, {keyOrder}) puts named keys first. Determinism contract unchanged: equal data yields identical bytes, independent of insertion order and of host locale (code-unit comparison, never localeCompare).
  • Render module DELETED along with marked and sanitize-html. The body is opaque text to this library; a consumer that displays a body renders it under its own policy. yaml is now the entire dependency budget. The earlier two-independent-sanitization-defenses design is therefore intentionally gone from this package, not weakened in place.
  • SUPPORTED_BODY_FORMATS becomes [markdown, html], validated as an enum only, with no rendering semantics attached to either value.
  • Issue accumulation reworked per D4. Host objects (Date/Map/Set/class instance/RegExp/function/symbol/bigint) are still rejected rather than silently flattened to {}.
    Kept unchanged: the error-code system, safeParseArtifact result semantics for batch ingress, the five hostile-input ceilings (document bytes, front-matter bytes, depth, node count, alias/billion-laughs) each failing loud rather than clipping, byte-exact body preservation, and strict YAML 1.2 core-schema parsing (no timestamp coercion, no YAML 1.1 booleans, duplicate keys and unresolved tags rejected).

METHOD NOTE: before submitting the stage-2 suite for captain review I wrote a throwaway implementation of the target, confirmed the suite reached 158/158 against it, and then discarded it uncommitted - so the suite was known achievable rather than merely red. That verification pass is what caught the depth-boundary off-by-one fixed in commit 2.

VERIFIED BEFORE VALIDATION: pnpm -r typecheck green; full repo pnpm test green (845 server + 360 daemon + 158 artifact-format); a clean tsc build emits dist with no render module and the emitted JS smoke-tests end to end (correct export set, default lexicographic order, keyOrder honored, checkTimestamp both ways). The package remains unwired into server or UI by design - this branch ships the library only.

What Changed

  • Adds packages/artifact-format (@loopany/artifact-format, private): a pure structural codec for artifact files — YAML front matter plus a byte-exact opaque body. parseArtifact/serializeArtifact are deterministic inverses; structural validation is exactly two rules (the head resolves to a YAML mapping, a declared format is one of markdown/html) and every other key passes through verbatim, with object kinds and their key sets left to a future server-side seam. Parsing is strict YAML 1.2 core schema (no timestamp coercion, no YAML 1.1 booleans, duplicate keys and unresolved tags rejected); key order is pure lexicographic by code unit at every depth, with an optional keyOrder that reorders the top-level mapping only. yaml is the whole dependency budget — the package renders nothing.
  • Rounds out the surface and the hostile-input behavior: typed error codes plus safeParseArtifact for batch ingress, path-carrying issue accumulation that reports every offending value in one error (host objects such as Date/Map/class instances are rejected, never flattened), checkTimestamp returning an issue or null, and splitArtifact/updateArtifactFrontMatter/bodyFormatOf/ARTIFACT_FORMAT_VERSION. The five ceilings (document bytes, front-matter bytes, depth, node count, alias expansion) fail loud rather than clip, and are now applied on the serialize path as well as on parse, so the library cannot emit a file it would refuse to read back; updateArtifactFrontMatter takes the caller's limits, and checkTimestamp rejects impossible calendar dates like 2026-02-30 that Date.parse silently rolls over.
  • Wires the package into the monorepo without wiring it into the product: root tsconfig.json project reference, the Dockerfile install layer now copies its manifest (pnpm silently skips an importer whose package.json is absent), root pnpm test runs its suite, and README.md/CONTRIBUTING.md/AGENTS.md describe the third package. The package README owns the format contract, error codes and ceilings. 173 tests across 5 files cover round-trip, determinism, structure, limits and the exported surface — the last pinned against test/target.ts so an export cannot appear or vanish unnoticed. Nothing in server or the UI imports it yet, by design.

Risk Assessment

✅ Low: The round-1 warnings are fixed durably at the correct shared boundary (a single guardShape walk both directions call) with the round-trip law now provably unconditional, each behavior change is pinned by a new test, and the public export surface, build output and captain-approved design decisions are all untouched.

Testing

Ran the package's stage-2 suite (173 tests, 5 files) and its typecheck green, then produced the real product-level evidence by building the package with tsc and driving the emitted dist from a standalone Node consumer script resolved through the package's exports map - that transcript shows the 15-export surface with the render module and marked/sanitize-html gone, arbitrary front-matter keys passing through verbatim while format/mapping rejection still fires, keyOrder behaving per D2/D3 (nested same-name key untouched, absent key skipped, duplicate keeping first position, [] equivalent to omission), D4 issue accumulation reporting all eight host-object offenders in one error with real paths, all five ceilings failing loud on serialize as well as parse, strict YAML 1.2 behavior, and checkTimestamp/splitArtifact/updateArtifactFrontMatter/bodyFormatOf/safeParseArtifact intact. I also proved the locale-independence claim by serializing under tr_TR vs en_US for byte-identical output where localeCompare would reorder, and ran the Dockerfile's own frozen-lockfile install to confirm the new workspace package is wired correctly. No visual artifact applies - this change ships a headless library with no UI surface and is deliberately unwired from server or UI. Everything passed and the transient dist build output was removed, leaving the worktree clean.

Evidence: Consumer smoke transcript against the emitted dist (30 checks + locale determinism)
consumer resolves: @loopany/artifact-format -> ./dist/index.js
node v22.22.0

── 1. The shipped surface — render module and its deps are gone
  ✓ runtime exports are exactly the 15 codec entry points  ARTIFACT_FORMAT_VERSION, ArtifactFormatError, DEFAULT_LIMITS, SUPPORTED_BODY_FORMATS, bodyFormatOf, checkTimestamp, isArtifactFormatError, parseArtifact, resolveLimits, safeParseArtifact, serializeArtifact, splitArtifact, toResult, updateArtifactFrontMatter, validateFrontMatter
  ✓ v1 domain/render exports are absent  CORE_FIELD_ORDER, renderMarkdown, renderArtifactBody
  ✓ dist emits no render module  errors.js index.js parse.js schema.js serialize.js shape.js types.js
  ✓ dependency budget is exactly `yaml`  dependencies: {"yaml":"^2.8.1"}
  ✓ SUPPORTED_BODY_FORMATS is [markdown, html]; ARTIFACT_FORMAT_VERSION kept  formats=["markdown","html"] version=1

── 2. Zero domain knowledge — arbitrary keys pass through verbatim
  ✓ a head with NO `type` and no v1 domain fields parses (v1 rejected this)  keys: attachments, createdAt, kind, nested, severity, state, wildcard
  ✓ every unknown key survives parse → serialize → parse verbatim  wildcard = "none of the codec's business"
  ✓ body is byte-exact and opaque (blank line + inline `---` preserved)  "\n# Sync floods on a worktree drop\n" …
  ✓ rejection did not disappear: an unknown `format` is still refused  UNSUPPORTED_FORMAT: unsupported body format "pdf"; this version supports only "markdown", "html"
  ✓ a non-mapping head is still refused  FRONT_MATTER_NOT_MAPPING
  ✓ bodyFormatOf defaults to markdown, honors an explicit html  markdown (implicit) / html (declared)

── 3. Key order is the caller's — D2/D3
  ✓ default is pure lexicographic at EVERY depth  top: attachments createdAt kind nested severity state wildcard | nested: first, third (input order was third, first)
  ✓ keyOrder puts named keys first, rest lexicographic  kind state attachments createdAt nested severity wildcard
  ✓ keyOrder is TOP-LEVEL only — a nested key of the same name is untouched  "\nnested:\n  first: 1\n  third: 3\na: 1\n"
  ✓ a keyOrder key absent from the data is skipped, never invented as null  "---\nb: 2\na: 1\n---\n"
  ✓ a key listed twice keeps its FIRST position; [] === omitting the option  dup → c,a,b   |   [] → lexicographic
  ✓ determinism: insertion order does not change the bytes  "---\na: 2\nm:\n  b: 2\n  y: 1\nz: 1\n---\nx"

── 4. Issue accumulation with real paths — D4
  ✓ ALL offending host values reported in ONE error, each with its path  big: unrepresentable bigint value
      fn: unrepresentable function value
      list[1]: unrepresentable Date value
      map: unrepresentable Map value
      nested.third: unrepresentable symbol value
      re: unrepresentable RegExp value
      set: unrepresentable Set value
      when: unrepresentable Date value

── 5. Hostile-input ceilings — loud in BOTH directions
  ✓ DOCUMENT_TOO_LARGE on parse  document is 212 bytes, over the 64-byte ceiling
  ✓ FRONT_MATTER_TOO_LARGE on parse  front matter is 204 bytes, over the 32-byte ceiling
  ✓ FRONT_MATTER_TOO_DEEP on parse (17 levels vs the default 16)  front matter nests deeper than 16 levels
  ✓ FRONT_MATTER_TOO_MANY_NODES on parse  front matter has more than 10 nodes
  ✓ alias / billion-laughs guard trips on parse  INVALID_YAML: front matter could not be materialized: Excessive alias count indicates a resource exhaustion attack
  ✓ the SAME ceilings are enforced on SERIALIZE (b2b910c) — never writes an unreadable file  DOCUMENT_TOO_LARGE, FRONT_MATTER_TOO_LARGE, FRONT_MATTER_TOO_DEEP, FRONT_MATTER_TOO_MANY_NODES
  ✓ a document read under raised limits can be edited and written back  20-level head round-trips under raised ceilings

── 6. Strict YAML 1.2 core schema
  ✓ no !!timestamp coercion — a date stays a string  typeof d = string ("2026-07-29")
  ✓ no YAML 1.1 booleans — `no` stays the string "no"  {"n":"no","y":"yes","t":true}
  ✓ duplicate keys rejected  INVALID_YAML
  ✓ unresolved tags rejected  INVALID_YAML

── 7. Kept helpers — D1, D6, safeParse
  ✓ checkTimestamp: null for a valid instant, Issue for the rest (D1)  createdAt: must be an RFC 3339 date-time with an explicit offset (e.g. 2026-07-29T09:15:00Z)
      updatedAt: must be an RFC 3339 date-time with an explicit offset (e.g. 2026-07-29T09:15:00Z)
      closedAt: must be an RFC 3339 date-time with an explicit offset (e.g. 2026-07-29T09:15:00Z); got number
      impossible: must be an RFC 3339 date-time with an explicit offset (e.g. 2026-07-29T09:15:00Z)
  ✓ checkTimestamp is applied to NOTHING by the codec itself  createdAt: yesterday parses fine — the seam's problem, not the codec's
  ✓ splitArtifact routes without paying for a YAML parse (D6)  startLine=2, body="\n# Sync floods on a "…
  ✓ updateArtifactFrontMatter edits the head, body untouched by construction (D6)  state fixing→verifying, severity dropped, body identical
  ✓ safeParseArtifact keeps a batch alive on one bad file  ok, MISSING_FRONT_MATTER, UNSUPPORTED_FORMAT

── 8. A real editing session, printed

  │ ---
  │ kind: defect
  │ state: verifying
  │ attachments:
  │   - manifest-trace.json
  │ createdAt: 2026-07-01T00:00:00Z
  │ nested:
  │   first: 1
  │   third: 3
  │ severity: p1
  │ verifiedAt: 2026-08-03T21:00:00Z
  │ wildcard: none of the codec's business
  │ ---
  │ 
  │ # Sync floods on a worktree drop
  │ 
  │ A body containing --- a delimiter-looking line stays body.
  │ 
  ✓ re-parses to the same document (round-trip law)  frontMatter deep-equal, body byte-identical

ALL CONSUMER CHECKS PASSED

── 9. Host-locale independence (code-unit ordering, never localeCompare)
LC_ALL=en_US.UTF-8  Intl=en-US
"---\nIr: 3\nIstanbul: 1\na-b: 5\naB: 6\nir: 2\nizmir: 4\n---\n"
localeCompare would give: ["a-b","aB","ir","Ir","Istanbul","izmir"]
LC_ALL=tr_TR.UTF-8  Intl=tr-TR
"---\nIr: 3\nIstanbul: 1\na-b: 5\naB: 6\nir: 2\nizmir: 4\n---\n"
localeCompare would give: ["a-b","aB","Ir","Istanbul","ir","izmir"]
Evidence: Consumer smoke script (imports dist/index.js via the package exports map)
/**
 * End-to-end consumer smoke test for @loopany/artifact-format.
 *
 * Imports the EMITTED dist (what a downstream package would resolve through
 * package.json "exports"), not the TypeScript sources, and exercises the
 * stage-3 contract the way a consuming developer would experience it.
 *
 * Usage: node consumer-smoke.mjs <path-to-packages/artifact-format>
 */
import { readFileSync, readdirSync } from "node:fs";
import { pathToFileURL } from "node:url";
import { resolve } from "node:path";
import assert from "node:assert/strict";

const pkgDir = resolve(process.argv[2]);
const pkgJson = JSON.parse(readFileSync(resolve(pkgDir, "package.json"), "utf8"));
const entry = resolve(pkgDir, pkgJson.exports["."].default);
const codec = await import(pathToFileURL(entry).href);

let failures = 0;
const section = (t) => console.log(`\n\x1b[1m── ${t}\x1b[0m`);
function check(label, fn) {
  try {
    const detail = fn();
    console.log(`  \x1b[32m✓\x1b[0m ${label}${detail ? `  \x1b[2m${detail}\x1b[0m` : ""}`);
  } catch (err) {
    failures += 1;
    console.log(`  \x1b[31m✗\x1b[0m ${label}\n      ${err.message.split("\n").join("\n      ")}`);
  }
}
/** Run fn, expect an ArtifactFormatError, return it. */
function rejects(fn) {
  try {
    fn();
  } catch (err) {
    assert.equal(err.name, "ArtifactFormatError", `threw ${err.name}: ${err.message}`);
    return err;
  }
  throw new Error("expected a throw, got none");
}

console.log(`consumer resolves: ${pkgJson.name} -> ${pkgJson.exports["."].default}`);
console.log(`node ${process.version}`);

// ─────────────────────────────────────────────────────────────────────────────
section("1. The shipped surface — render module and its deps are gone");

check("runtime exports are exactly the 15 codec entry points", () => {
  const actual = Object.keys(codec).sort();
  assert.deepEqual(actual, [
    "ARTIFACT_FORMAT_VERSION", "ArtifactFormatError", "DEFAULT_LIMITS", "SUPPORTED_BODY_FORMATS",
    "bodyFormatOf", "checkTimestamp", "isArtifactFormatError", "parseArtifact", "resolveLimits",
    "safeParseArtifact", "serializeArtifact", "splitArtifact", "toResult",
    "updateArtifactFrontMatter", "validateFrontMatter",
  ]);
  return actual.join(", ");
});

check("v1 domain/render exports are absent", () => {
  for (const gone of ["CORE_FIELD_ORDER", "renderMarkdown", "renderArtifactBody"]) {
    assert.equal(codec[gone], undefined, `${gone} is still exported`);
  }
  return "CORE_FIELD_ORDER, renderMarkdown, renderArtifactBody";
});

check("dist emits no render module", () => {
  const files = readdirSync(resolve(pkgDir, "dist")).filter((f) => f.endsWith(".js"));
  assert.ok(!files.some((f) => /render/i.test(f)), `found ${files}`);
  return files.join(" ");
});

check("dependency budget is exactly `yaml`", () => {
  const deps = Object.keys(pkgJson.dependencies ?? {});
  assert.deepEqual(deps, ["yaml"]);
  for (const gone of ["marked", "sanitize-html"]) {
    assert.equal(pkgJson.dependencies?.[gone], undefined);
    assert.equal(pkgJson.devDependencies?.[gone], undefined);
  }
  return `dependencies: ${JSON.stringify(pkgJson.dependencies)}`;
});

check("SUPPORTED_BODY_FORMATS is [markdown, html]; ARTIFACT_FORMAT_VERSION kept", () => {
  assert.deepEqual([...codec.SUPPORTED_BODY_FORMATS], ["markdown", "html"]);
  assert.equal(codec.ARTIFACT_FORMAT_VERSION, 1);
  return `formats=${JSON.stringify(codec.SUPPORTED_BODY_FORMATS)} version=${codec.ARTIFACT_FORMAT_VERSION}`;
});

// ─────────────────────────────────────────────────────────────────────────────
section("2. Zero domain knowledge — arbitrary keys pass through verbatim");

const DOC = [
  "---",
  "attachments:",
  "  - manifest-trace.json",
  "createdAt: 2026-07-01T00:00:00Z",
  "kind: defect",
  "nested:",
  "  third: 3",
  "  first: 1",
  "severity: p1",
  "state: fixing",
  "wildcard: none of the codec's business",
  "---",
  "",
  "# Sync floods on a worktree drop",
  "",
  "A body containing --- a delimiter-looking line stays body.",
  "",
].join("\n");

check("a head with NO `type` and no v1 domain fields parses (v1 rejected this)", () => {
  const doc = codec.parseArtifact(DOC);
  assert.equal(doc.frontMatter.type, undefined);
  return `keys: ${Object.keys(doc.frontMatter).join(", ")}`;
});

check("every unknown key survives parse → serialize → parse verbatim", () => {
  const doc = codec.parseArtifact(DOC);
  const back = codec.parseArtifact(codec.serializeArtifact(doc));
  assert.deepEqual(back.frontMatter, doc.frontMatter);
  assert.equal(back.body, doc.body);
  return `wildcard = ${JSON.stringify(back.frontMatter.wildcard)}`;
});

check("body is byte-exact and opaque (blank line + inline `---` preserved)", () => {
  const doc = codec.parseArtifact(DOC);
  assert.equal(doc.body, DOC.slice(DOC.indexOf("---\n", 4) + 4));
  assert.ok(doc.body.startsWith("\n# Sync floods"));
  assert.ok(doc.body.includes("--- a delimiter-looking line"));
  return JSON.stringify(doc.body.slice(0, 34)) + " …";
});

check("rejection did not disappear: an unknown `format` is still refused", () => {
  const err = rejects(() => codec.parseArtifact("---\nformat: pdf\n---\nbody\n"));
  assert.equal(err.code, "UNSUPPORTED_FORMAT");
  return `${err.code}: ${err.message}`;
});

check("a non-mapping head is still refused", () => {
  const err = rejects(() => codec.parseArtifact("---\n- a\n- b\n---\nbody\n"));
  assert.equal(err.code, "FRONT_MATTER_NOT_MAPPING");
  return err.code;
});

check("bodyFormatOf defaults to markdown, honors an explicit html", () => {
  assert.equal(codec.bodyFormatOf(codec.parseArtifact(DOC).frontMatter), "markdown");
  assert.equal(codec.bodyFormatOf({ format: "html" }), "html");
  return "markdown (implicit) / html (declared)";
});

// ─────────────────────────────────────────────────────────────────────────────
section("3. Key order is the caller's — D2/D3");

check("default is pure lexicographic at EVERY depth", () => {
  const out = codec.serializeArtifact(codec.parseArtifact(DOC));
  const head = out.slice(4, out.indexOf("\n---", 4));
  const top = head.split("\n").filter((l) => /^\S/.test(l)).map((l) => l.split(":")[0]);
  assert.deepEqual(top, [...top].sort());
  assert.ok(head.includes("nested:\n  first: 1\n  third: 3"), head);
  return `top: ${top.join(" ")} | nested: first, third (input order was third, first)`;
});

check("keyOrder puts named keys first, rest lexicographic", () => {
  const out = codec.serializeArtifact(codec.parseArtifact(DOC), { keyOrder: ["kind", "state"] });
  const top = out.slice(4, out.indexOf("\n---", 4)).split("\n").filter((l) => /^\S/.test(l)).map((l) => l.split(":")[0]);
  assert.deepEqual(top.slice(0, 2), ["kind", "state"]);
  assert.deepEqual(top.slice(2), [...top.slice(2)].sort());
  return top.join(" ");
});

check("keyOrder is TOP-LEVEL only — a nested key of the same name is untouched", () => {
  const doc = { frontMatter: { a: 1, nested: { third: 3, first: 1 } }, body: "b\n" };
  const out = codec.serializeArtifact(doc, { keyOrder: ["third", "nested"] });
  assert.ok(out.includes("nested:\n  first: 1\n  third: 3"), out);
  assert.ok(out.indexOf("nested:") < out.indexOf("a: 1"), out);
  return JSON.stringify(out.split("---")[1]);
});

check("a keyOrder key absent from the data is skipped, never invented as null", () => {
  const out = codec.serializeArtifact({ frontMatter: { b: 2, a: 1 }, body: "" }, { keyOrder: ["zzz", "b"] });
  assert.ok(!out.includes("zzz"), out);
  assert.equal(out, "---\nb: 2\na: 1\n---\n");
  return JSON.stringify(out);
});

check("a key listed twice keeps its FIRST position; [] === omitting the option", () => {
  const fm = { frontMatter: { a: 1, b: 2, c: 3 }, body: "" };
  assert.equal(codec.serialize

... [1027 bytes truncated] ...

ew Date(0),
        map: new Map(),
        set: new Set(),
        re: /x/,
        fn: () => {},
        big: 1n,
        nested: { third: Symbol("s") },
        list: ["ok", new Date(0)],
      },
      body: "",
    }),
  );
  assert.equal(err.code, "SCHEMA_VIOLATION");
  const paths = err.issues.map((i) => i.path).sort();
  assert.deepEqual(paths, ["big", "fn", "list[1]", "map", "nested.third", "re", "set", "when"]);
  return err.issues.map((i) => `${i.path}: ${i.message}`).join("\n      ");
});

// ─────────────────────────────────────────────────────────────────────────────
section("5. Hostile-input ceilings — loud in BOTH directions");

const big = (n) => "x".repeat(n);
const deep = (n) => {
  let v = "leaf";
  for (let i = 0; i < n; i += 1) v = { child: v };
  return v;
};

check("DOCUMENT_TOO_LARGE on parse", () => {
  const err = rejects(() => codec.parseArtifact(`---\na: ${big(200)}\n---\n`, { limits: { maxDocumentBytes: 64 } }));
  assert.equal(err.code, "DOCUMENT_TOO_LARGE");
  return err.message;
});
check("FRONT_MATTER_TOO_LARGE on parse", () => {
  const err = rejects(() => codec.parseArtifact(`---\na: ${big(200)}\n---\n`, { limits: { maxFrontMatterBytes: 32 } }));
  assert.equal(err.code, "FRONT_MATTER_TOO_LARGE");
  return err.message;
});
check("FRONT_MATTER_TOO_DEEP on parse (17 levels vs the default 16)", () => {
  const err = rejects(() => codec.parseArtifact(codec.serializeArtifact({ frontMatter: { root: deep(16) }, body: "" }, { limits: { maxFrontMatterDepth: 64 } })));
  assert.equal(err.code, "FRONT_MATTER_TOO_DEEP");
  return err.message;
});
check("FRONT_MATTER_TOO_MANY_NODES on parse", () => {
  const head = Array.from({ length: 40 }, (_, i) => `k${i}: ${i}`).join("\n");
  const err = rejects(() => codec.parseArtifact(`---\n${head}\n---\n`, { limits: { maxFrontMatterNodes: 10 } }));
  assert.equal(err.code, "FRONT_MATTER_TOO_MANY_NODES");
  return err.message;
});
check("alias / billion-laughs guard trips on parse", () => {
  const bomb = ["---", "a: &a [x,x,x,x,x,x,x,x,x,x]", "b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a,*a]", "c: [*b,*b,*b,*b,*b,*b,*b,*b,*b,*b]", "---", ""].join("\n");
  const err = rejects(() => codec.parseArtifact(bomb));
  return `${err.code}: ${err.message.split("\n")[0]}`;
});
check("the SAME ceilings are enforced on SERIALIZE (b2b910c) — never writes an unreadable file", () => {
  const codes = [
    rejects(() => codec.serializeArtifact({ frontMatter: { a: big(200) }, body: "" }, { limits: { maxDocumentBytes: 64 } })).code,
    rejects(() => codec.serializeArtifact({ frontMatter: { a: big(200) }, body: "" }, { limits: { maxFrontMatterBytes: 32 } })).code,
    rejects(() => codec.serializeArtifact({ frontMatter: { root: deep(16) }, body: "" })).code,
    rejects(() => codec.serializeArtifact({ frontMatter: Object.fromEntries(Array.from({ length: 40 }, (_, i) => [`k${i}`, i])), body: "" }, { limits: { maxFrontMatterNodes: 10 } })).code,
  ];
  assert.deepEqual(codes, ["DOCUMENT_TOO_LARGE", "FRONT_MATTER_TOO_LARGE", "FRONT_MATTER_TOO_DEEP", "FRONT_MATTER_TOO_MANY_NODES"]);
  return codes.join(", ");
});
check("a document read under raised limits can be edited and written back", () => {
  const raised = { limits: { maxFrontMatterDepth: 32 } };
  const text = codec.serializeArtifact({ frontMatter: { root: deep(20) }, body: "b\n" }, raised);
  const doc = codec.parseArtifact(text, raised);
  const next = codec.updateArtifactFrontMatter(doc, { state: "verifying" }, raised);
  assert.equal(codec.parseArtifact(codec.serializeArtifact(next, raised), raised).frontMatter.state, "verifying");
  return "20-level head round-trips under raised ceilings";
});

// ─────────────────────────────────────────────────────────────────────────────
section("6. Strict YAML 1.2 core schema");

check("no !!timestamp coercion — a date stays a string", () => {
  const v = codec.parseArtifact("---\nd: 2026-07-29\n---\n").frontMatter.d;
  assert.equal(typeof v, "string");
  return `typeof d = ${typeof v} (${JSON.stringify(v)})`;
});
check("no YAML 1.1 booleans — `no` stays the string \"no\"", () => {
  const fm = codec.parseArtifact("---\nn: no\ny: yes\nt: true\n---\n").frontMatter;
  assert.deepEqual(fm, { n: "no", y: "yes", t: true });
  return JSON.stringify(fm);
});
check("duplicate keys rejected", () => rejects(() => codec.parseArtifact("---\na: 1\na: 2\n---\n")).code);
check("unresolved tags rejected", () => rejects(() => codec.parseArtifact("---\na: !!python/object x\n---\n")).code);

// ─────────────────────────────────────────────────────────────────────────────
section("7. Kept helpers — D1, D6, safeParse");

check("checkTimestamp: null for a valid instant, Issue for the rest (D1)", () => {
  assert.equal(codec.checkTimestamp("2026-07-29T09:15:00Z", "createdAt"), null);
  const rows = [
    ["2026-07-29T09:15:00", "createdAt"],   // no offset
    ["yesterday", "updatedAt"],
    [42, "closedAt"],
    ["2026-02-30T00:00:00Z", "impossible"], // b2b910c: Date.parse would roll to Mar 2
  ].map(([v, p]) => codec.checkTimestamp(v, p));
  assert.ok(rows.every((r) => r && typeof r.path === "string" && typeof r.message === "string"));
  return rows.map((r) => `${r.path}: ${r.message}`).join("\n      ");
});

check("checkTimestamp is applied to NOTHING by the codec itself", () => {
  const doc = codec.parseArtifact("---\ncreatedAt: yesterday\n---\nbody\n");
  assert.equal(doc.frontMatter.createdAt, "yesterday");
  return "createdAt: yesterday parses fine — the seam's problem, not the codec's";
});

check("splitArtifact routes without paying for a YAML parse (D6)", () => {
  const s = codec.splitArtifact(DOC);
  assert.equal(s.frontMatterStartLine, 2);
  assert.ok(s.frontMatterText.includes("kind: defect"));
  assert.ok(s.body.startsWith("\n# Sync floods"));
  return `startLine=${s.frontMatterStartLine}, body=${JSON.stringify(s.body.slice(0, 20))}…`;
});

check("updateArtifactFrontMatter edits the head, body untouched by construction (D6)", () => {
  const doc = codec.parseArtifact(DOC);
  const next = codec.updateArtifactFrontMatter(doc, { state: "verifying", severity: undefined });
  assert.equal(next.body, doc.body);
  assert.equal(next.frontMatter.state, "verifying");
  assert.equal("severity" in next.frontMatter, false);
  return "state fixing→verifying, severity dropped, body identical";
});

check("safeParseArtifact keeps a batch alive on one bad file", () => {
  const batch = [DOC, "no front matter here\n", "---\nformat: pdf\n---\n"];
  const rows = batch.map((t) => codec.safeParseArtifact(t)).map((r) => (r.ok ? "ok" : r.error.code));
  assert.deepEqual(rows, ["ok", "MISSING_FRONT_MATTER", "UNSUPPORTED_FORMAT"]);
  return rows.join(", ");
});

// ─────────────────────────────────────────────────────────────────────────────
section("8. A real editing session, printed");

{
  const doc = codec.parseArtifact(DOC);
  const next = codec.updateArtifactFrontMatter(doc, { state: "verifying", verifiedAt: "2026-08-03T21:00:00Z" });
  const out = codec.serializeArtifact(next, { keyOrder: ["kind", "state"] });
  console.log("\n" + out.split("\n").map((l) => "  │ " + l).join("\n"));
  check("re-parses to the same document (round-trip law)", () => {
    assert.deepEqual(codec.parseArtifact(out).frontMatter, next.frontMatter);
    assert.equal(codec.parseArtifact(out).body, doc.body);
    return "frontMatter deep-equal, body byte-identical";
  });
}

console.log(failures === 0 ? "\n\x1b[32mALL CONSUMER CHECKS PASSED\x1b[0m" : `\n\x1b[31m${failures} CHECK(S) FAILED\x1b[0m`);
process.exit(failures === 0 ? 0 : 1);
Evidence: Host-locale determinism: identical bytes under tr_TR vs en_US where localeCompare differs
LC_ALL=en_US.UTF-8 Intl=en-US
"---\nIr: 3\nIstanbul: 1\na-b: 5\naB: 6\nir: 2\nizmir: 4\n---\n"
localeCompare would give: ["a-b","aB","ir","Ir","Istanbul","izmir"]

LC_ALL=tr_TR.UTF-8 Intl=tr-TR
"---\nIr: 3\nIstanbul: 1\na-b: 5\naB: 6\nir: 2\nizmir: 4\n---\n"
localeCompare would give: ["a-b","aB","Ir","Istanbul","ir","izmir"]

--- serialized bytes identical across locales? YES
Evidence: Zero domain knowledge + rejection moved, not removed (excerpt)
── 2. Zero domain knowledge — arbitrary keys pass through verbatim
✓ a head with NO `type` and no v1 domain fields parses (v1 rejected this) keys: attachments, createdAt, kind, nested, severity, state, wildcard
✓ every unknown key survives parse → serialize → parse verbatim wildcard = "none of the codec's business"
✓ body is byte-exact and opaque (blank line + inline `---` preserved)
✓ rejection did not disappear: an unknown `format` is still refused UNSUPPORTED_FORMAT: unsupported body format "pdf"; this version supports only "markdown", "html"
✓ a non-mapping head is still refused FRONT_MATTER_NOT_MAPPING

── 4. Issue accumulation with real paths — D4
✓ ALL offending host values reported in ONE error, each with its path
big: unrepresentable bigint value
fn: unrepresentable function value
list[1]: unrepresentable Date value
map: unrepresentable Map value
nested.third: unrepresentable symbol value
re: unrepresentable RegExp value
set: unrepresentable Set value
when: unrepresentable Date value

── 5. Hostile-input ceilings — loud in BOTH directions
✓ the SAME ceilings are enforced on SERIALIZE — never writes an unreadable file DOCUMENT_TOO_LARGE, FRONT_MATTER_TOO_LARGE, FRONT_MATTER_TOO_DEEP, FRONT_MATTER_TOO_MANY_NODES
Evidence: A real editing session: parse → updateArtifactFrontMatter → serialize({keyOrder}) → re-parse
│ ---
│ kind: defect
│ state: verifying
│ attachments:
│ - manifest-trace.json
│ createdAt: 2026-07-01T00:00:00Z
│ nested:
│ first: 1
│ third: 3
│ severity: p1
│ verifiedAt: 2026-08-03T21:00:00Z
│ wildcard: none of the codec's business
│ ---
│
│ # Sync floods on a worktree drop
│
│ A body containing --- a delimiter-looking line stays body.
│
✓ re-parses to the same document (round-trip law) frontMatter deep-equal, body byte-identical

ALL CONSUMER CHECKS PASSED

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ⚠️ packages/artifact-format/src/serialize.ts:156 - serializeArtifact enforces only maxFrontMatterDepth. canonicalize() checks depth (serialize.ts:93) and nothing else, and no other ceiling name appears anywhere in serialize.ts, so maxDocumentBytes, maxFrontMatterBytes and maxFrontMatterNodes are parse-side only. Failing sequence: serializeArtifact({frontMatter:{kind:'note'}, body:'y'.repeat(510241024)}) succeeds, and parseArtifact of that exact output throws DOCUMENT_TOO_LARGE; likewise serializeArtifact({frontMatter:{list:Array.from({length:6000},()=>1)}, body:''}) succeeds and re-parses as FRONT_MATTER_TOO_MANY_NODES, and a single 70KB string value re-parses as FRONT_MATTER_TOO_LARGE. Such a document cannot have come from parseArtifact (it would have been rejected), so this is reachable only from caller-built front matter - which is the normal write path for the server seam this library is being narrowed for, and the result is a file written to disk that the same library can never read back. This contradicts the inline claim on serialize.ts:158 ('Serializing never emits a file this library would refuse to read back') and the unconditional round-trip guarantee in README.md:107. Either apply the same resolved limits on the serialize side (byte-measure the emitted head and whole document, and count nodes during the canonicalize walk that already visits every node), or narrow both claims to say the ceilings are enforced on read only.
  • ⚠️ packages/artifact-format/src/schema.ts:56 - checkTimestamp accepts calendar dates that do not exist. The guard is !RFC3339.test(value) || Number.isNaN(Date.parse(value)), but V8 does not reject out-of-range days: verified with node that Date.parse('2026-02-30T00:00:00Z') returns 1772409600000 (silently rolled over to 2026-03-02), so checkTimestamp('2026-02-30T00:00:00Z', 'at') returns null, i.e. 'this is a well-formed instant'. Same for '2026-04-31T...'. A seam that accepts the value and later re-derives it from Date gets a different day than the file says - precisely the silent-ambiguity class the RFC3339 comment on schema.ts:20 says is a bug in a timestamp the engine schedules on. Fix inside the existing check by round-tripping the parsed instant against the source fields (e.g. compare new Date(parsed).toISOString() day/month against the matched groups) rather than trusting Date.parse for range validation. No existing case in codec.surface.test.ts is affected: neither the valid nor the invalid list contains a rolled-over date.
  • ℹ️ packages/artifact-format/src/serialize.ts:203 - updateArtifactFrontMatter validates through canonicalFrontMatter(merged, resolveLimits(undefined)), so it always uses DEFAULT_LIMITS and takes no options of its own. A caller that parses with {limits:{maxFrontMatterDepth:64}} (the pattern codec.limits.test.ts:86 exercises) and then edits a legitimately 20-level-deep head gets a spurious FRONT_MATTER_TOO_DEEP from the update, even though both parseArtifact and serializeArtifact accept that same document under the caller's own limits. Accept an optional ParseOptions parameter and thread it into resolveLimits so the three entry points agree on the effective ceilings.
  • ℹ️ packages/artifact-format/src/serialize.ts:193 - updateArtifactFrontMatter builds its merged head on Object.assign(Object.create(null), ...) and returns that object directly as the public frontMatter (serialize.ts:205). The null prototype is load-bearing while merging (it is what makes a 'proto' patch key land as an own property), but leaking it to the caller means the returned ArtifactDocument behaves differently from every other one the library hands out: doc.frontMatter.hasOwnProperty(k) throws TypeError, and any implicit string coercion of the object throws 'Cannot convert object to primitive value'. Restoring the ordinary prototype after the merge (Object.setPrototypeOf(merged, Object.prototype)) keeps the already-created own 'proto' data property intact while making the update path's return value interchangeable with the parse path's.
  • ℹ️ packages/artifact-format/tsconfig.json:18 - The package tsconfig includes only src//* and excludes src//*.test.ts, and test/ (target.ts, assert.ts) is outside the include entirely, so pnpm typecheck covers none of the package's test code - roughly half its lines. That exclusion was correct at stage 2, when target.ts deliberately described a surface src did not implement yet, but stage 3 makes the surface real, so the exclusion now only hides type drift in the very files that pin the public surface. Consider a separate tsconfig.test.json (or widening include and keeping rootDir/exclude only for the emitting build) so the suite and target.ts typecheck without shipping into dist.

🔧 Fix: enforce codec ceilings on serialize, reject impossible dates
2 infos still open:

  • ℹ️ packages/artifact-format/src/serialize.ts:243 - updateArtifactFrontMatter now accepts options and threads them into resolveLimits, but it only runs canonicalFrontMatter - which enforces representability and (inside canonicalize, serialize.ts:93) the depth ceiling. It never calls guardShape, so maxFrontMatterNodes is not applied on the edit path: updateArtifactFrontMatter(doc, {list: Array.from({length:6000},()=>1)}, {limits:{maxFrontMatterNodes:10}}) returns normally. The new limits case at codec.limits.test.ts:206 pins that depth IS honoured here, which makes the node gap the odd one out - a caller passing limits gets one of the two shape ceilings silently ignored. Not a correctness hole (the subsequent serializeArtifact still refuses to emit it, so no unreadable file can result), only an eagerness/option-fidelity gap in the function whose doc comment at serialize.ts:212 promises validation happens at the edit rather than later at the save. One line closes it: guardShape(canonicalFrontMatter(merged, limits), limits), reusing the tree canonicalFrontMatter already returns and currently discards.
  • ℹ️ packages/artifact-format/src/serialize.ts:185 - The two byte ceilings are checked in the opposite order from parse. parse.ts checks maxDocumentBytes first (parse.ts:44) and maxFrontMatterBytes second (parse.ts:123); serialize checks the front-matter ceiling at serialize.ts:186 and the document ceiling at serialize.ts:195. For a document that violates both - reachable with stock defaults, e.g. a single 5MB string value in the head - parse reports DOCUMENT_TOO_LARGE while serialize reports FRONT_MATTER_TOO_LARGE for the same bytes. Both are loud and both use the shared vocabulary, so this does not weaken the round-trip law; it just means the code a caller sees depends on the direction. Computing the document total from the already-known parts (4 + fmBytes + 4 + Buffer.byteLength(doc.body)) lets the document check run first, exactly mirroring parse, and as a side benefit avoids materializing and flattening the oversized text at serialize.ts:193 before deciding to reject it.
✅ **Test** - passed

✅ No issues found.

  • pnpm --filter @loopany/artifact-format test - 173 tests / 5 files green (codec.surface, codec.structure, codec.roundtrip, codec.determinism, codec.limits)
  • pnpm --filter @loopany/artifact-format typecheck - tsc --noEmit plus tsc -p tsconfig.test.json
  • pnpm --filter @loopany/artifact-format build - clean tsc emit; verified dist contains errors/index/parse/schema/serialize/shape/types only, no render module
  • node consumer-smoke.mjs &lt;pkgDir&gt; - end-to-end consumer script importing the emitted dist/index.js via the package exports map; 30 checks covering export surface, removed v1 exports, dependency budget, domain pass-through, format/mapping rejection, keyOrder D2/D3, determinism, D4 path-carrying issue accumulation, all five ceilings on parse AND serialize, strict YAML 1.2, checkTimestamp D1 incl. 2026-02-30, splitArtifact/updateArtifactFrontMatter/bodyFormatOf/safeParseArtifact - exit 0
  • LC_ALL=tr_TR.UTF-8 node locale-check.mjs vs LC_ALL=en_US.UTF-8 - byte-identical serialization where localeCompare orders differently (code-unit ordering proven)
  • pnpm install --frozen-lockfile --ignore-scripts - the Dockerfile install-layer command; lockfile up to date across all 4 workspace projects
  • grep -rn &#39;marked|sanitize-html|renderMarkdown|CORE_FIELD_ORDER|ArtifactCoreFields&#39; packages/artifact-format - only surviving hits are the deliberate REMOVED_* assertion lists in test/target.ts
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…tter + Markdown)

The canonical form of a Graph-Engineering-v3 artifact: ONE YAML front-matter
block (the machine head) plus a Markdown body. Sanitized HTML is a projection
of that, never storage. New pure package `@loopany/artifact-format` — no I/O,
no server internals, not yet wired into server or UI.

- Parse: strict split (only the FIRST closing `---` closes the head, so a `---`
  in the body is inert, not an injection point) + YAML 1.2 CORE schema (no
  !!timestamp coercion, no YAML 1.1 booleans, duplicate keys and unresolved
  tags rejected, alias expansion capped). There is no lenient path: a file that
  opens a front-matter block and then malforms it is a typed ArtifactFormatError,
  never a document that silently becomes "all body". Size/depth/node ceilings
  fail loudly rather than clipping.
- Core schema validates type/status/title/format/source/externalId/sourceUrl/
  createdAt/updatedAt/attachments and PRESERVES every unknown field, so the type
  registry can add per-type fields without this library changing. An explicit
  unknown `format:` is an error, not a silent fallback to markdown.
- Serialize is a pure function of the data: core fields in declared order, all
  other keys lexicographic at every depth, body bytes exact. parse(serialize(x))
  deep-equals x and the bytes are stable under repetition.
- Render: Markdown (CommonMark + GFM) to sanitized, style-free semantic HTML.
  Two independent defenses — raw HTML is escaped (or dropped) before the HTML
  layer, and marked's output is then filtered through a strict sanitize-html
  allowlist — so a behavior change in either dependency cannot open a hole. GFM
  column alignment rides as data-align rather than a presentational attribute.
- 73 tests: round-trip determinism, unknown-field preservation, status
  read/update/re-serialize with a byte-identical body, a hostile-input suite
  (script tags, raw HTML, event handlers, javascript:/data:/protocol-relative
  URLs, front-matter injection, alias bombs, deep/huge YAML) and typed
  malformed-file behavior.

Wiring: root `pnpm test` + tsconfig references, and the Dockerfile now copies
every workspace manifest into the install layer.
…odec

Tests only — no implementation. Expresses the narrowed role: a pure format
codec with ZERO domain knowledge. A red suite is the expected state of this
stage; 48 of 158 already pass, and those are exactly the structural behaviors
that survive the rewrite unchanged.

What the suite pins:
- round-trip laws (inverse, byte-stable under repetition, fixed point) and
  body byte-exactness across 14 body shapes (leading blank line, trailing
  newlines, CRLF, lone CR, empty, delimiter-looking lines, non-ASCII).
- unknown-key pass-through as the CORRECT behavior rather than a fallback,
  including the keys v1 used to police (type/status/source/externalId/
  createdAt/attachments now carry no rules at all) and prototype-shadowing
  keys landing as own properties.
- determinism: insertion-order independence at every depth, code-unit rather
  than locale collation (with a guard proving the case discriminates), and the
  new parametrized `keyOrder` — listed keys first in order, rest lexicographic,
  default pure lexicographic, top-level only, absent keys skipped, duplicates
  collapsed, presentation-only.
- every hostile-input ceiling failing loud, never clipping.
- structural rejection: non-mapping heads, duplicate keys, host objects
  (Date/Map/Set/class/RegExp/function/symbol/bigint), with issue accumulation
  reporting every offending path in one error.
- `format` as an enum only — markdown and html both legal, absence means
  markdown, anything else UNSUPPORTED_FORMAT; an html body stays opaque bytes.
- the RFC 3339 helper surviving as an exported `checkTimestamp` applied to no
  key by the library, composing into a caller's own issue accumulation.
- safeParseArtifact result semantics for batch ingress.
- the ABSENCE of the render projection: export-surface pin plus a check that
  the markdown/sanitizer dependencies are gone.

The v1 test files (parse/serialize/render) are removed rather than left
alongside: they encode the superseded domain contract, so keeping them would
make a red result unreadable — a reviewer could not tell "not implemented yet"
from "deliberately deleted". They remain in history on fm/artifact-format-a1.

test/target.ts is the executable statement of the target surface and lives
outside src/ so the package tsconfig neither includes nor typechecks it.
The first draft asserted acceptance at maxFrontMatterDepth - 1 lists, which is
one past the ceiling: depth counts from the root mapping, so N nested lists put
the innermost scalar at N + 2. Pin the boundary from both directions so an
off-by-one in either fails.
Implements the stage-2 target suite; all 158 cases green. The package survives
but its role narrows: it now knows that the head is a YAML mapping and that the
body is bytes, and nothing else.

- Domain schema OUT. `type`/`status`/`source`/`externalId`/`sourceUrl`/
  `attachments`, the required-`type` rule and the source+externalId pairing are
  gone, along with `ArtifactCoreFields`. Structural validation is now exactly
  two rules: the head parses to a mapping, and a declared `format` is a known
  enum. Every other key is preserved verbatim — pass-through is the CORRECT
  behavior at this level, not a fallback, since an unknown key is not one the
  codec failed to understand but one that is none of its business. Object kinds
  and their closed key sets belong to the server seam.
- Key order is PARAMETRIZED. `CORE_FIELD_ORDER` is removed; no key is
  privileged. Default is pure lexicographic at every depth, and
  `serializeArtifact(doc, { keyOrder })` puts the named keys first. It applies
  to the top level only, so a caller's intent for the head cannot reach down and
  reorder a nested value that merely shares a key name; absent keys are skipped
  rather than invented, duplicates keep their first position.
- Render module DELETED, with `marked` and `sanitize-html`. The body is opaque
  text; a consumer that displays one renders it under its own policy. `yaml` is
  now the entire dependency budget.
- `SUPPORTED_BODY_FORMATS` becomes markdown + html, validated as an enum with no
  rendering semantics — which kinds may use html is server-side policy.
- The RFC 3339 checker survives as an exported `checkTimestamp(value, path)`,
  applied to NO key by the library and returning an issue-or-null so it composes
  straight into a seam's accumulation.
- Issues now ACCUMULATE with real paths (`nested.third`, `list[1]`): one pass
  reports every unrepresentable value instead of marching the caller through
  them one round trip at a time. Host objects (Date/Map/Set/class/RegExp/
  function/symbol/bigint) are still rejected rather than flattened to `{}`.

Kept unchanged: the error-code system, `safeParseArtifact` result semantics, the
five hostile-input ceilings, byte-exact bodies, and the deterministic
code-unit (never locale) ordering.

README rewritten for the codec contract; the AGENTS.md entry now warns against
adding a field rule here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant