Skip to content

v1.1.0 — Charts, server rendering, and an autonomous agent surface - #18

Merged
Nizoka merged 5 commits into
mainfrom
release/v1.1.0
Jul 26, 2026
Merged

v1.1.0 — Charts, server rendering, and an autonomous agent surface#18
Nizoka merged 5 commits into
mainfrom
release/v1.1.0

Conversation

@Nizoka

@Nizoka Nizoka commented Jul 26, 2026

Copy link
Copy Markdown
Owner

v1.1.0 — Charts, server rendering, and an autonomous agent surface

Branch: release/v1.1.0main
Type: Minor release. No API removed or changed; two install-time floors raised.
pdfnative: ^1.6.0 (peer + dev), was ^1.5.0
Node: >=22, was >=20

Summary

Tracks the pdfnative 1.6.0
engine release, and closes the two gaps that were costing adoption: there was no
first-class way to serve a PDF from a modern React server, and no way for an AI
agent to check its environment, discover the API, or verify its own output.

Four themes:

  1. Engine 1.6.0 authoring surface<Chart>, the only authoring
    capability 1.6.0 adds, with full DocSpec parity and schema coverage.
  2. Server renderingrenderToResponse / renderSpecToResponse returning a
    web-standard Response, streaming by default.
  3. Document-level layout sugar + lintingwatermark, header, footer,
    attachments, tagged as first-class props; and lintDocument/lintSpec,
    whose rules include eight that pre-empt engine-level render failures.
  4. The agent automation contractErrorCode, capabilityManifest(),
    doctor(), validateSpec(), multi-subject schema(), and the governance
    contract exported as runtime capability. Backed by a new single-source
    registry with compile-time anti-drift locks.

Install-time floors (no API break, but read this)

Neither is a source-breaking change; both are install-time requirements.

  • pdfnative peer ^1.5.0^1.6.0. <Chart> compiles to a chart
    block that does not exist before 1.6.0; a 1.5 engine would receive an unknown
    block and silently drop or mis-render it. A loud install error beats a quiet
    wrong PDF. The alternative (^1.5.0 || ^1.6.0 plus a capability guard on every
    chart path) trades a build-time error for a runtime surprise.
  • Node >=20>=22. Inherited, not invented: pdfnative@1.6.0 requires
    Node ≥ 22, so any compliant install is already there. CI matrix is now 22/24.

Changes

New: src/registry.ts — the anti-drift mechanism

Four single-source tables (BLOCK_REGISTRY, COMPONENT_REGISTRY,
CLIENT_COMPONENT_REGISTRY, LINT_RULES) that spec/schema.ts, spec/validate.ts and manifest.ts all
derive from rather than restate. Pure data; imports nothing at runtime, which
is what keeps schema emission free of the engine.

Two independent locks:

  • Compile-time — Assert<Equals<RegisteredBlockKind, BlockSpecKind>> and the
    HostTag twin; plus satisfies Record<BlockGroupId, …> on BLOCK_SCHEMAS.
  • Test-time — tests/registry.test.ts pins the exact ordered contents;
    tests/agent.test.tsx asserts every manifest name resolves to a real export.

Verified destructively: removing the chart entry produces two independent
compile errors (registry.ts TS2344, schema.ts TS2353) and fails
tests/registry.test.ts. If a future change leaves only one half failing, the
lock has become decorative.

src/core-bridge/index.ts

  • Type re-exports for ChartBlock/ChartSeries/ChartType, and for the layout
    sugar (PageTemplate, WatermarkOptions/WatermarkText/WatermarkImage,
    PdfAttachment/PdfAttachmentRelationship, EncryptionOptions).
  • One new runtime import: estimateChartHeight, used solely as a capability
    probe
    by doctor() — it first exists in 1.6.0. Probing beats parsing a
    version string: it survives bundling into a browser build (the trap
    pdfnative-cli hit when tsup flattened its require). Deliberately not
    re-exported from the public barrel.

src/components.tsx

  • <Chart> — props mirror ChartBlock one-for-one.
  • <Document> gains watermark (accepts a plain string as shorthand for
    { text: { text } }), header, footer, attachments, tagged.

These are props, not child components, because they are document-level page
furniture; a component would mean a host tag with no corresponding pdfnative
block, which golden rule 2 forbids. <Document> already has precedent
(outline, pageLabels, metadata).

src/reconciler/serialize.ts + nodes.ts

  • HostTag gains 'chart'; toBlock gains the chart case.
  • New resolveLayout() folds the sugar props into layout under the engine's
    keys, with an explicit layout always winning — matching prepare()'s
    precedence in render.ts.
  • Critical invariant: with no sugar and no layout, resolveLayout returns
    undefined, never {}. An empty object would change the serialized bytes of
    every existing document. Pinned by three assertions in
    tests/layout-sugar.test.tsx.
  • PdfStructureError moves to src/errors.ts but is re-exported from here,
    so the original import path and class identity are preserved.

New: src/response.ts

renderToResponse(node, options?)Promise<Response>. Streams via a
ReadableStream over the existing renderToStream generator (with a cancel
hook so the generator cleans up on client disconnect); buffered: true uses
renderToBytes and sets Content-Length. RFC 6266 Content-Disposition
including filename* for non-ASCII. async, so options.fonts is honoured.

Stays on the root barrel; the client components moved to a ./client subpath
instead, which is where the 'use client' directive belongs. No 'use client'
here — this is server code, and marking it would break every server usage.

New: src/lint.ts

lintDocument(node, options?)LintReport. Runs on the compiled
DocumentParams, so JSX and DocSpec share one implementation for free
(lintSpec is a two-line delegate, and a test asserts they agree).

Eighteen rules (10 error, 7 warning, 1 info). Eight pre-empt failures the engine
raises by throwing mid-render; L_ATTACHMENTS_NEED_PDFA3 exists because writing
samples/layout/watermark-header-footer.tsx hit exactly that throw, and
L_CHART_EMPTY because the architecture review found two more.

Pure by design: no console output, no throwing, overflow opt-in because it
costs a layout pass.

New: src/errors.ts, src/manifest.ts, src/doctor.ts, src/governance.ts

  • ErrorCode (E_STRUCTURE, E_INPUT, E_UNSUPPORTED, E_ENV, E_POLICY,
    E_RUNTIME), PdfReactError with .code and .toJSON(), and
    toErrorEnvelope(unknown) so a caller only ever handles one shape.
  • capabilityManifest() — derived wholly from the registries.
  • doctor() — every check wrapped; reports rather than raises. It cannot reach
    the completely absent peer case (a static re-export fails at module
    resolution first), which the docs now state plainly.
  • governance.tsaiGovernancePolicy, agentRulesText, validateIssueDraft.
    The regex tables are duplicated from scripts/verify-issue.mjs because
    that script must stay zero-dependency and runnable in an unbuilt checkout;
    tests/governance.test.ts parses its source and asserts both tables are
    literally identical. Duplication with a proof, not with a comment.

src/spec/ (DocSpec parity)

  • ChartSpec = ['chart', ChartSpecBody] — a body object like table/img/
    field, since the payload is nested (series[].values, axis.yMin) and named
    keys measurably reduce generation errors.
  • Five new top-level DocSpec fields mirroring the layout sugar.
  • schema.ts refactored: $defs.block.oneOf assembled from the registry, with
    arity and descriptions sourced there too (removed from the builders, so they
    cannot disagree). Seven subjects; docSpecSchema()/docSpecSchemaId() retained
    and delegating, pinned by a toEqual test.
  • New spec/validate.tsvalidateSpec(unknown), zero-dependency structural
    validation with path-anchored V_* findings. Unknown top-level fields are a
    warning, preserving forward compatibility.

Samples & tests

  • 7 new samples: charts/charts.tsx, layout/watermark-header-footer.tsx,
    server/next-route-handler.tsx, quality/lint.tsx, agent/agent-loop.ts,
    agent/manifest.ts, agent/error-envelope.tsx. All added to
    samples/README.md (with new "Server" and "Quality" sections) and all executed
    end to end, not just type-checked.
  • 8 new test files: registry, chart, layout-sugar, response, lint,
    agent, schema, compile-snapshot. governance and version extended.
  • 79 → 226 tests, 8 → 16 files, including a golden compile snapshot.

Docs & governance

  • New guides: docs/CHARTS.md, docs/SERVER.md, docs/LINTING.md,
    docs/AGENT_CONTRACT.md, and docs/RECIPES.md — the counterpart to golden
    rule 7, with working code for extractText, fillForm/flattenForm,
    openPdf({ password }), merge/split and re-encryption.
  • docs/KNOWLEDGE_BASE.md — new §9 "Agent automation contract"; §3 module map,
    §5 serialization rules and §6 test map updated.
  • README.md, llms.txt, AGENTS.md, CLAUDE.md, ROADMAP.md,
    CHANGELOG.md, CITATION.cff, .github/ai-governance.json all updated.
  • AGENTS.md gains an "adding a block kind" checklist that now routes through
    the registry, and a "recommended agent loop" section.
  • CI — Node matrix 20/22/24 → 22/24, and a new advisory governance step that
    validates any staged draft. ai-governance.json declared advisory_in_ci: true
    but no workflow had ever run it.
  • package.jsonfiles now includes llms.txt (it was never shipped), and
    keywords extended for discovery.

Validation

npm run typecheck:all   clean (src + tests + samples)
npm run lint            clean, zero warnings
npm test                226 passed / 226, 16 files
npm run test:coverage   94.77 stmts · 86.04 branches · 97.76 funcs · 95.80 lines
                        (thresholds 85/80/85/85 — unchanged, not lowered)
npm run build           root ESM/CJS + client ESM/CJS + four .d.ts; postbuild verifies
                        the node: prefix, the client-only directive, and tree-shaking
npm audit --omit=dev    0 vulnerabilities (runtime tree)
npm pack --dry-run      llms.txt present in the tarball

Additionally verified by hand:

  • CJS require and ESM import smoke tests against the built artifacts,
    covering all new exports; doctor().ok === true, manifest reports 14 block
    kinds, schema()['$id'] carries 1.1.0.
  • Every new sample executed and confirmed to write a valid PDF.
  • The registry lock verified destructively (see above).
  • The corrected annotation recipe in docs/RECIPES.md executed end to end.

Adversarial review

Five independent reviews were run against this branch across three rounds —
architecture, documentation accuracy, engine-1.6.0 gap analysis, ecosystem
state-of-the-art, and a final go/no-go verification. Every confirmed finding is
fixed. Rounds are listed newest first, because each caught claims the previous
round's fixes had asserted but not completed.

Round 3 — go/no-go

Finding Fix
CHANGELOG.md still said "Six rules pre-empt" and filed L_MAX_BLOCKS_EXCEEDED under "renders successfully but is wrong" — the last live instance of the very defect round 2 was named after. The round-2 sweep missed it because its regex read six pre-empt and the text reads Six rules pre-empt Paragraph corrected. The gate itself was the real defect, so AGENTS.md now documents the sweep, with this miss as the cautionary example
The ./client subpath — new public API — was absent from CHANGELOG.md, from the user-facing release note, from capabilityManifest(), and from every sample. So were the two other user-visible packaging fixes of round 2 (the node: prefix restoration, the tree-shaking win) ### Added entry; a release-note section; contract.entry / contract.clientEntry / contract.reactServerCondition and clientComponents[].importFrom on the manifest; both samples/client/* switched to src/client.js with the real-world import shown in the header
The encryption security notice never reached the release note. The CHANGELOG tells readers to re-render anything shipped with layout.encryption; the artifact users actually read did not Dedicated ## Security section, placed above the highlights
"Two independent adversarial reviews" in the release note, when there were five Corrected, with the two rejected findings recorded
docs/KNOWLEDGE_BASE.md §3 had no src/client.ts, §6 had no tests/compile-snapshot.test.tsx, and §304's enumeration listed six under the word "eight" All three fixed
"Three tables" survived in docs/KNOWLEDGE_BASE.md and AGENTS.md; src/registry.ts and .github/copilot-instructions.md still said the bundle strips the 'use client' directive All four corrected
publish.yml verified only the four dist/index.* artifacts and never resolved the exports map — both workflows imported by file path, so a wrong types target or a dropped condition would have shipped unseen Now packs a tarball, installs it into a throwaway project, resolves . and ./client in both conditions, and renders a real PDF from the installed package
postbuild.mjs failed open — its headline tree-shaking guard degraded to a console.log and exit 0 if esbuild was absent Fails closed, with an explicit POSTBUILD_SKIP_SHAKE_CHECK=1 opt-out

Two round-3 findings were rejected after verification: the reported
upload-artifact version inconsistency is shared verbatim by pdfnative-cli
and pdfnative-mcp, so "fixing" it would have created the ecosystem divergence
it claimed to remove; and a reported total test-suite failure turned out to be
an audit tool perturbing node_modulesnpm ci from the committed lockfile
restores 224/224, which is also what CI does.

Round 2

Finding Fix
The published bundle emitted import('fs/promises') without the node: prefix. Deno and Cloudflare nodejs_compat refuse to resolve the bare form, so a wrangler or Vite-browser build failed to compile — against four documents advertising Edge/Deno/Bun/Workers. Root cause is a rollup pass inside tsup that survives platform, target, external and banner alike (all four measured) scripts/postbuild.mjs restores the prefix and fails the build if the expected shape is absent; a bundler-resolution step in ci.yml compiles both artifacts the way a non-Node bundler would
import { version } pulled the entire React reconciler into a consumer's bundle — 10 137 bytes for a string constant, and react-reconciler forced to resolve. A single-file bundle makes sideEffects: false inoperative /* @__PURE__ */ on ReactReconciler(hostConfig), HostTransitionContext, HOST_CONTEXT, LINT_RULE_CODES and BY_KIND. Now 3 216 bytes, no reconciler; postbuild fails the build if it regresses
'use client' never reached dist/, so RSC users needed a hand-written wrapper — while README.md claimed the directive was carried New pdfnative-react/client subpath export, built separately with the directive applied and verified by postbuild. The root bundle is asserted not to carry it
L_MAX_BLOCKS could not fire on the engine's default ceiling. It checked only an explicit layout.maxBlocks, but the engine applies DEFAULT_MAX_BLOCKS = 100 000 unconditionally and throws — so a large generated document linted clean and then crashed layout?.maxBlocks ?? 100_000; test at 100 001 blocks
"Six rules pre-empt an engine throw" was wrong — it is eight. L_TAGGED_ENCRYPTED (pdf-document.ts:169) and L_MAX_BLOCKS_EXCEEDED (:146) both throw; the docs listed them as safe. Repeated in 7 files Verified against each engine throw site and corrected in 7 of 8 sites — round 3 caught the eighth (CHANGELOG.md), which the sweep's own regex had missed
schema('manifest') described 10 of the manifest's 13 properties — missing clientComponents, errorClasses, schemaSubjects, two of which were added for agent honesty. No test covered it Completed, plus a test comparing Object.keys(capabilityManifest()) to the schema's properties and required
ChartProps had no compile-time tie to ChartBlock, while docs/CHARTS.md promises Charts-v2 fields "arrive as new ChartProps" ChartPropsCoversChartBlock assert; verified destructively
toBlock had no exhaustiveness guard — a new HostTag without a case compiled cleanly and failed at render, while the DocSpec side had a never guard since 1.0 const exhaustive: never; verified destructively
The doctor() claim retracted in round 1 was still live in five documents, including llms.txt and AGENT_CONTRACT.md — the two an agent loads first Corrected in all five
.nvmrc pinned lts/iron (Node 20) against engines: >=22; CONTRIBUTING.md and publish.yml said 20 too — a leftover from this PR's own bump All set to 22
ci.yml, codeql.yml and scorecard.yml were a generation behind the three sibling repos: unpinned actions (while publish.yml in the same repo is SHA-pinned), codeql-action@v3 vs v4, no concurrency, no timeout-minutes, and scorecard.yml job permissions that drop contents/actions to none — job-level permissions replace, not merge, so checkout gets a 403 All three aligned on pdfnative-cli, React deltas re-applied
3 high-severity dev advisories shipping through a green CI js-yaml/postcss overrides; runtime audit is now blocking (npm audit --omit=dev is clean — the prod tree is one dependency), dev audit advisory with the reason stated
Two engine fixes affecting documents this package authored were undocumented: pre-1.6.0 encrypted files left outline titles, link URIs and metadata in clear text, and AES-256 output was not ISO 32000-2 compliant New ### Security section in the CHANGELOG and a callout in docs/RECIPES.md
The colour-emoji module grew 221 → 1167 glyphs (~0.25 MB → 4.0 MB) on an upgrade this package's own peer floor forces — and this is the only package in the ecosystem targeting a browser bundle Font-weight table in README.md with measured sizes and the --codepoints escape hatch
.github/instructions/components.instructions.md had the same stale-procedure defect its two siblings were rewritten for in round 1; spec.instructions.md claimed "the first five steps are compiler-enforced" when the real set is 1, 3, 4, 5, 6, 7 Both corrected
No golden test on the compiled model — the strongest assertion on output was byteLength > 100 tests/compile-snapshot.test.tsx: a committed snapshot of a document using every block and every document-level prop
Sample header miscounts and a wrong run command (.ts for a .tsx file) Corrected

One round-2 finding was rejected after verification: a reviewer disputed the
coverage figures. Re-measured — the documented numbers were correct.

Round 1

Finding Fix
validateSpec — the "never throws" untrusted-input gate — overflowed the stack on a ~44 kB deeply nested payload Nesting bounded at 64 levels, new V_TOO_DEEP code, regression test at depth 5000
schema('toString') resolved through Object.prototype and returned a string Object.hasOwn guard; test covers five prototype keys
schema('lint-report') handed out a live reference to LINT_RULES; mutating the returned schema changed every subsequent lint severity process-wide Fresh copy; regression test
L_CHART_VALUES missed an undefined value (.find() returns undefined for a found undefined) .some(); test
L_MAX_BLOCKS reported "within 10% of the ceiling" when 5× over it, as a warning New L_MAX_BLOCKS_EXCEEDED error; both tested
L_HEADING_HIERARCHY never flagged a document whose first heading was h2/h3 Guard removed; test
Two engine throws had no lint rule (empty series, empty values) New L_CHART_EMPTY; test
capabilityManifest() claimed to describe "everything" while omitting 24 of 73 exports All added, plus clientComponents/errorClasses; a test now locks both directions
schema.ts hardcoded every kind discriminator, so the registry and the schema could disagree (proved: registry h1–h4, schema h1–h3, typecheck green) blockDefs() overwrites the discriminator from the registry; registry.test.ts asserts it
KNOWN_FIELDS in validate.ts had no lock satisfies readonly (keyof DocSpec)[] plus an Assert<Equals<…>>
LINT_RULES was not locked in either direction — a declared-but-unimplemented rule would ship into the schema and the manifest New EMITTED_LINT_RULES + equality test
Content-Disposition filename* emitted ' ( ) ! *, which are not RFC 8187 attr-char; a raw apostrophe mis-parses the ext-value Percent-escaped; test
docs/RECIPES.md annotation example was wrong on both arguments and could not run Rewritten against the real API (createModifier(openPdf(bytes)), buildAnnotationBody, save()) and executed
.github/copilot-instructions.md and .github/instructions/spec.instructions.md still described pre-1.1.0 architecture — no chart, no registry.ts — so an agent following them would fail the repo's own compile-time lock Both rewritten, including the 10-step block checklist
doctor()'s headline claim ("works when the peer is missing") was false — a static re-export means the module graph fails first Corrected in src/doctor.ts; round 2 found five documents still carrying it and finished the job
docs/SERVER.md documented a Server Action, but RSC-layer imports fail at module load (react-server has no createContext) Replaced with the real constraint; round 2 replaced the manual wrapper advice with the ./client subpath
Hand-maintained counts wrong in six places Recounted; round 2 found five sites still stale, including the user-facing release note

Findings acknowledged but not acted on, with reasons:

  • The two install-time floors as a minor. One reviewer argues ^1.6.0 + Node
    >=22 warrant a major. Neither is source-breaking, both are documented at the
    top of the release notes, and the alternative for the peer (^1.5.0 || ^1.6.0
    plus a capability guard on every chart path) trades a build-time error for a
    runtime surprise. Recorded here so a reviewer can overrule it.
  • Two defects in sibling repositories. pdfnative-cli declares
    engines.node: ">=20" while depending on pdfnative@^1.6.0, which requires 22
    — and its CI matrix tests Node 20. And pdfnative/docs/guides/react.md still
    describes a pre-1.0 version of this wrapper. Both were verified; both are out
    of scope for this PR by explicit decision, and neither is being reported from
    here.
  • PDF/UA round-trip test, validateSpec fuzzing, raised coverage thresholds,
    eslint-plugin-react-hooks, Cache-Control/ETag on renderToResponse,
    cause on PdfReactError.
    All reasonable; all tracked for 1.2.0 rather
    than widening this release further.

Backward compatibility

Change Impact
Schema $id now /1.1.0/ By design — the versioned $id is the drift-detection contract
params.layout populated by sugar Only when a sugar prop is used; undefined invariant preserved and tested
PdfStructureError extends PdfReactError instanceof (both classes and Error) and .name unchanged
PdfStructureError moved to errors.ts Same class object re-exported from the old path; identity asserted in tests
docSpecSchema() / docSpecSchemaId() Retained; toEqual test against schema('doc-spec')
files += llms.txt Tarball grows ~9 kB; no API impact
peer ^1.6.0, Node >=22 Install-time only — the two friction points, headlined above

Out of scope (by design)

pdfnative 1.6.0 also shipped extractText, readFormFields/fillForm/
flattenForm, openPdf({ password }), streamMergedPdfs/streamSplitPdf/
streamExtractPages, and MergeOptions.encrypt. None are re-exported: they
operate on existing bytes, and this package authors documents (golden rule 7).
docs/RECIPES.md shows how to call each of them on the bytes we produce.

Also dropped, with reasons recorded in ROADMAP.md:

  • <Outline> / <Bookmark> sugar — outline="auto" already covers the common
    case; permanent public surface for a marginal gain.
  • Automatic dev-mode lint warnings — would make render behaviour depend on
    NODE_ENV and emit unrequested output; also would make lintDocument impure,
    ruling out its best use (a test assertion).

Self-review checklist

  • 1. All runtime pdfnative imports still go through core-bridge;
    types.ts remains the one type-only exception; pdfnative is still a peer.
  • 2. No CSS layout model introduced. <Chart> maps 1:1 onto the engine's
    chart block; the layout sugar is <Document> props, not new host tags.
    <Section> is still the only composite.
  • 3. react-reconciler contract untouched — no change to host-config.ts
    or reconciler/render.ts.
  • 4. Strict TypeScript, no any; lint clean with zero warnings.
  • 5. 'use client' unchanged on hooks.ts/viewer.tsx; none added to
    src/spec/; response.ts is explicitly server-side.
  • 6. DocSpec ↔ JSX parity holds — every new capability reaches both
    surfaces, with compileSpec toEqual compileDocument tests for charts
    and the layout sugar. src/version.ts bumped; package.json and
    CITATION.cff in sync (pinned by test).
  • 7. Authoring only — nothing byte-level re-exported;
    docs/RECIPES.md added as the documented alternative.
  • 8. This PR is a draft. No issue, PR, comment, branch push, release
    or publish was performed autonomously. A human reviews and submits it
    under their own identity.

Compliance report

Field Value
no_new_runtime_dependency_confirmed dependencies is still exactly ["react-reconciler"], asserted by tests/version.test.ts
reproduction_command npm run typecheck:all && npm run lint && npm run test:coverage && npm run build && npm pack --dry-run
reproduction_result All green on a clean npm ci; 226/226 tests; coverage above thresholds on all four axes; runtime npm audit clean; both export subpaths resolved from a real packed tarball
duplicate_search_performed N/A — release PR, not an issue report
affected_packages pdfnative-react only. Upstream pdfnative docs still reference pdfnative-react v1.0.0 in docs/guides/react.md, llms.txt, AGENTS.md and README.md — a companion PR there would be worthwhile, and is not included here.
identity_reminder_shown ✅ This draft must be reviewed and submitted by a human under their own GitHub identity. You share responsibility for its content.

Nizoka added 4 commits July 25, 2026 22:50
Tracks the pdfnative 1.6.0 engine release and closes the two gaps costing
adoption: no first-class way to serve a PDF from a modern React server, and
no way for an AI agent to check its environment or verify its own output.

Engine 1.6.0 surface:
- <Chart> — bar, barH, line, pie, donut as pure PDF path operators. The only
  authoring capability 1.6.0 adds. Full DocSpec parity via ['chart', body].

Adoption:
- renderToResponse / renderSpecToResponse — web-standard Response, streaming
  by default. Next.js route handlers, Remix, Hono, Edge, Deno, Bun, Workers.
- <Document> layout sugar: watermark, header, footer, attachments, tagged.
  These PdfLayoutOptions fields already worked as an undocumented pass-through;
  they are now typed, schema-covered, sampled and tested.
- lintDocument / lintSpec — 16 rules with stable L_* codes. Five pre-empt
  constraints the engine otherwise enforces by throwing mid-render.

Agent surface:
- ErrorCode taxonomy + PdfReactError.toJSON() + toErrorEnvelope.
- capabilityManifest(), doctor(), validateSpec(), schema(subject).
- Governance (aiGovernancePolicy, agentRulesText, validateIssueDraft) shipped
  as runtime capability; llms.txt now included in the tarball.

Anti-drift: new src/registry.ts holds the block grammar, component list and
lint rules as single-source tables. The JSON Schema, validateSpec and the
capability manifest derive from them, with compile-time Assert<Equals<...>>
locks making omission a build error. Verified destructively.

Install-time floors raised (no API break): pdfnative peer ^1.5.0 -> ^1.6.0
because <Chart> needs a block type 1.5 does not have; Node >=20 -> >=22,
inherited from the engine.

79 -> 205 tests. Coverage 94.7/85.7/97.2/96.0 (thresholds 85/80/85/85).
Two independent reviews (architecture, documentation accuracy) were run on the
v1.1.0 branch. Both found real defects. All confirmed findings are fixed.

Security / robustness:
- validateSpec — the "never throws" gate for untrusted input — overflowed the
  call stack on a ~44 kB deeply nested payload. Nesting is now bounded at 64
  levels with a new V_TOO_DEEP code; regression test at depth 5000.
- schema('toString') resolved through Object.prototype and returned a string.
  This is the one API designed to be called with a model-generated string.
  Now guarded with Object.hasOwn; five prototype keys tested.
- schema('lint-report') handed out a live reference to LINT_RULES, so any
  consumer that annotated a returned schema silently changed every subsequent
  lint severity process-wide. Now a fresh copy.

Linter correctness:
- L_CHART_VALUES missed an undefined value (.find returns undefined for a
  *found* undefined). Now .some().
- L_MAX_BLOCKS claimed "within 10% of the ceiling" when 5x over it, as a
  warning. Split out L_MAX_BLOCKS_EXCEEDED (error).
- L_HEADING_HIERARCHY never flagged a first heading of h2/h3.
- New L_CHART_EMPTY: two engine throws (empty series, empty values) had no rule.

Anti-drift — the release's central claim, which was oversold:
- schema.ts hardcoded every kind discriminator, so the registry and the schema
  could disagree while both claimed one source. blockDefs() now derives it.
- KNOWN_FIELDS had no lock; now satisfies + Assert<Equals<>>.
- LINT_RULES was unlocked in both directions; EMITTED_LINT_RULES + a test close
  the direction the type system cannot.

Honesty of the agent surface:
- capabilityManifest() claimed to describe "everything" while omitting 24 of 73
  exports. All added, plus clientComponents and errorClasses; a test now locks
  both directions.
- doctor()'s claim that it diagnoses a missing peer was false — a static
  re-export means the module graph fails first. Corrected in code and docs.
- docs/SERVER.md documented a Server Action, but RSC-layer imports fail at
  module load. Replaced with the real constraint and the wrapper pattern.

Documentation:
- The annotation recipe was wrong on both arguments and could not run.
  Rewritten against the real API and executed.
- .github/copilot-instructions.md and instructions/spec.instructions.md still
  described pre-1.1.0 architecture; an agent following either would fail the
  repo's own compile-time lock.
- RFC 8187: filename* emitted characters that are not attr-char.
- Six hand-maintained counts recounted against the code.

205 -> 219 tests. Coverage 95.4/86.9/97.8/96.3.
Third review round — five independent audits (architecture, documentation,
engine-1.6.0 gap, ecosystem state of the art, final doc pass). Every confirmed
finding is fixed. Three of them were defects I introduced; one was a shipped-
broken feature.

Packaging (the serious one):
- dist emitted `import('fs/promises')` without the `node:` prefix, so Deno and
  Cloudflare nodejs_compat could not resolve it — the edge runtimes four
  documents advertise. Cause is a rollup pass inside tsup that survives
  platform, target, external and banner alike; all four were measured.
  scripts/postbuild.mjs restores the prefix and fails the build if the expected
  shape is absent, and ci.yml now bundles both artifacts the way a non-Node
  bundler would.
- New `pdfnative-react/client` subpath, built separately with 'use client'
  applied — the directive never survived into the single-file bundle, so RSC
  users needed a hand-written wrapper while the README claimed otherwise.
- `import { version }` pulled the whole React reconciler (10 137 bytes for a
  string constant). /* @__PURE__ */ on five top-level side effects; now 3 216
  bytes with no reconciler, and postbuild fails on regression.

Correctness:
- L_MAX_BLOCKS could not fire on the engine's default ceiling: it checked only
  an explicit layout.maxBlocks, while the engine applies DEFAULT_MAX_BLOCKS
  unconditionally and throws.
- "Six rules pre-empt an engine throw" was wrong — it is eight. L_TAGGED_ENCRYPTED
  and L_MAX_BLOCKS_EXCEEDED both throw; the docs listed them as safe.
- schema('manifest') described 10 of the manifest's 13 properties, with no test.

Anti-drift, completed:
- ChartProps is now compile-locked to ChartBlock (Charts v2 will add fields).
- toBlock has a `never` exhaustiveness guard; the DocSpec side had one since 1.0.
- Both verified destructively.
- tests/compile-snapshot.test.tsx: a golden snapshot of the compiled model.

Ecosystem:
- ci.yml, codeql.yml and scorecard.yml aligned on pdfnative-cli — SHA-pinned
  actions, codeql v4, concurrency, timeouts. scorecard.yml job permissions were
  dropping contents/actions to none (job-level replaces workflow-level), which
  likely broke that workflow silently.
- Runtime audit is now blocking and clean; dev audit advisory with the reason
  stated. .nvmrc, CONTRIBUTING and publish.yml no longer say Node 20.

Documentation:
- The doctor() claim retracted last round was still live in five documents.
- Security section: pre-1.6.0 encrypted files left outline titles, link URIs and
  metadata in clear text, and AES-256 output was not ISO 32000-2 compliant.
- Font-weight table — the colour-emoji module is now 4.0 MB, and this is the
  only package in the ecosystem targeting a browser bundle.

219 -> 224 tests. Coverage 94.8/86.0/97.8/95.8 (thresholds 85/80/85/85).
… drift gate

Final ship-readiness round. Five independent reviews across three rounds; this
one was a go/no-go rather than a defect hunt. Two findings were rejected after
verification, and the rest are fixed.

The blocker:
- CHANGELOG.md still said "Six rules pre-empt an engine throw" and filed
  L_MAX_BLOCKS_EXCEEDED under "renders successfully but is wrong". It was the
  last live instance of the defect the previous round was named after.
  The real defect was the gate: the round-2 sweep searched `six pre-empt` while
  the text read `Six rules pre-empt`. AGENTS.md now documents the drift sweep
  with this miss as its cautionary example — a gate is only as good as its regex.

The ./client subpath shipped under-documented. It is new public API and was
absent from the CHANGELOG, from the user-facing release note, from
capabilityManifest(), and from every sample:
- CHANGELOG gains a Packaging section covering the subpath plus the two other
  user-visible fixes of the last round (the node: prefix restoration that
  unblocks Deno/Workers, and the tree-shaking win).
- The release note gains a section for it, and — more importantly — a Security
  section. The CHANGELOG told readers to re-render anything encrypted with a
  pre-1.6.0 engine; that instruction never reached the artifact users read.
- capabilityManifest() gains contract.entry, contract.clientEntry and
  contract.reactServerCondition, plus clientComponents[].importFrom. An agent
  reading only the manifest previously had no way to discover the entry point.
- Both samples/client/* now import from src/client.js and show the real-world
  specifier in their header.

Gates that did not guard:
- publish.yml verified only the four dist/index.* artifacts and imported by file
  path, so the exports map was never resolved by any workflow — a wrong `types`
  target would have shipped unseen. It now packs a tarball, installs it into a
  throwaway project, resolves both subpaths in both conditions, and renders a
  real PDF from the installed package.
- postbuild.mjs failed *open*: its tree-shaking guard degraded to a console.log
  and exit 0 if esbuild was absent. Now fails closed, with an explicit opt-out.

Documentation drift: "three tables" (four since CLIENT_COMPONENT_REGISTRY) in
KNOWLEDGE_BASE and AGENTS; "the bundle strips the directive" in registry.ts and
copilot-instructions.md; KB §3 missing src/client.ts, §6 missing
compile-snapshot, §304 enumerating six under the word "eight".

Rejected after verification:
- The reported upload-artifact version inconsistency is shared verbatim by
  pdfnative-cli and pdfnative-mcp. Aligning unilaterally would have created the
  ecosystem divergence it claimed to remove.
- A reported total test-suite failure was an audit tool perturbing node_modules
  (install marker at 18:55, vitest/jsdom mtimes at 20:06). `npm ci` from the
  committed lockfile restores 224/224 — which is what CI does.

224 -> 226 tests. Coverage 94.8/86.0/97.8/95.8. Both export subpaths verified
from a real packed tarball, types included.
@Nizoka Nizoka self-assigned this Jul 26, 2026
@Nizoka Nizoka added bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request labels Jul 26, 2026
Comment thread src/governance.ts Fixed
CodeQL js/polynomial-redos on `DEPENDENCY_PATTERNS`. The block pattern was
`"dependencies"\s*:\s*\{[^}]*[\w-]+[^}]*\}`. Because `[\w-]` is a subset of
`[^}]`, the engine had ambiguous ways to split the input and degraded to
quadratic backtracking on a string that never closes the brace.

Measured on `"dependencies":{` plus n hyphens:

    n=100   3.5 ms      n=400    31.9 ms
    n=200   4.7 ms      n=800   224.7 ms      n=2000  > 2 minutes

This is reachable: `validateIssueDraft` is a public export that takes untrusted
markdown, and the same table backs `scripts/verify-issue.mjs`, which CI runs
over draft files.

Replaced with `"dependencies"\s*:\s*\{\s*"` — one unbounded quantifier with a
deterministic follow, so linear. It is also stricter: an empty
`"dependencies": {}` quoted in prose no longer trips the policy check, which
was a false positive before.

Both copies of the table updated identically. The parity test now compares the
patterns rather than the surrounding comments, so each file can explain the
duplication in its own terms without reading as a policy divergence.

226 -> 228 tests: one for the corrected semantics, one that fails if the
quadratic behaviour ever returns.
@Nizoka
Nizoka merged commit 4ac6f1b into main Jul 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants