v1.1.0 — Charts, server rendering, and an autonomous agent surface - #18
Merged
Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v1.1.0 — Charts, server rendering, and an autonomous agent surface
Summary
Tracks the
pdfnative1.6.0engine 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:
<Chart>, the only authoringcapability 1.6.0 adds, with full
DocSpecparity and schema coverage.renderToResponse/renderSpecToResponsereturning aweb-standard
Response, streaming by default.watermark,header,footer,attachments,taggedas first-class props; andlintDocument/lintSpec,whose rules include eight that pre-empt engine-level render failures.
ErrorCode,capabilityManifest(),doctor(),validateSpec(), multi-subjectschema(), and the governancecontract 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.
pdfnativepeer^1.5.0→^1.6.0.<Chart>compiles to achartblock 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.0plus a capability guard on everychart path) trades a build-time error for a runtime surprise.
>=20→>=22. Inherited, not invented:pdfnative@1.6.0requiresNode ≥ 22, so any compliant install is already there. CI matrix is now 22/24.
Changes
New:
src/registry.ts— the anti-drift mechanismFour single-source tables (
BLOCK_REGISTRY,COMPONENT_REGISTRY,CLIENT_COMPONENT_REGISTRY,LINT_RULES) thatspec/schema.ts,spec/validate.tsandmanifest.tsallderive from rather than restate. Pure data; imports nothing at runtime, which
is what keeps schema emission free of the engine.
Two independent locks:
Assert<Equals<RegisteredBlockKind, BlockSpecKind>>and theHostTagtwin; plussatisfies Record<BlockGroupId, …>onBLOCK_SCHEMAS.tests/registry.test.tspins the exact ordered contents;tests/agent.test.tsxasserts every manifest name resolves to a real export.Verified destructively: removing the
chartentry produces two independentcompile errors (
registry.tsTS2344,schema.tsTS2353) and failstests/registry.test.ts. If a future change leaves only one half failing, thelock has become decorative.
src/core-bridge/index.tsChartBlock/ChartSeries/ChartType, and for the layoutsugar (
PageTemplate,WatermarkOptions/WatermarkText/WatermarkImage,PdfAttachment/PdfAttachmentRelationship,EncryptionOptions).estimateChartHeight, used solely as a capabilityprobe by
doctor()— it first exists in 1.6.0. Probing beats parsing aversion string: it survives bundling into a browser build (the trap
pdfnative-clihit when tsup flattened itsrequire). Deliberately notre-exported from the public barrel.
src/components.tsx<Chart>— props mirrorChartBlockone-for-one.<Document>gainswatermark(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.tsHostTaggains'chart';toBlockgains thechartcase.resolveLayout()folds the sugar props intolayoutunder the engine'skeys, with an explicit
layoutalways winning — matchingprepare()'sprecedence in
render.ts.layout,resolveLayoutreturnsundefined, never{}. An empty object would change the serialized bytes ofevery existing document. Pinned by three assertions in
tests/layout-sugar.test.tsx.PdfStructureErrormoves tosrc/errors.tsbut is re-exported from here,so the original import path and class identity are preserved.
New:
src/response.tsrenderToResponse(node, options?)→Promise<Response>. Streams via aReadableStreamover the existingrenderToStreamgenerator (with acancelhook so the generator cleans up on client disconnect);
buffered: trueusesrenderToBytesand setsContent-Length. RFC 6266Content-Dispositionincluding
filename*for non-ASCII.async, sooptions.fontsis honoured.Stays on the root barrel; the client components moved to a
./clientsubpathinstead, 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.tslintDocument(node, options?)→LintReport. Runs on the compiledDocumentParams, so JSX andDocSpecshare one implementation for free(
lintSpecis 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_PDFA3exists because writingsamples/layout/watermark-header-footer.tsxhit exactly that throw, andL_CHART_EMPTYbecause the architecture review found two more.Pure by design: no console output, no throwing,
overflowopt-in because itcosts a layout pass.
New:
src/errors.ts,src/manifest.ts,src/doctor.ts,src/governance.tsErrorCode(E_STRUCTURE,E_INPUT,E_UNSUPPORTED,E_ENV,E_POLICY,E_RUNTIME),PdfReactErrorwith.codeand.toJSON(), andtoErrorEnvelope(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 reachthe completely absent peer case (a static re-export fails at module
resolution first), which the docs now state plainly.
governance.ts—aiGovernancePolicy,agentRulesText,validateIssueDraft.The regex tables are duplicated from
scripts/verify-issue.mjsbecausethat script must stay zero-dependency and runnable in an unbuilt checkout;
tests/governance.test.tsparses its source and asserts both tables areliterally identical. Duplication with a proof, not with a comment.
src/spec/(DocSpec parity)ChartSpec=['chart', ChartSpecBody]— a body object liketable/img/field, since the payload is nested (series[].values,axis.yMin) and namedkeys measurably reduce generation errors.
DocSpecfields mirroring the layout sugar.schema.tsrefactored:$defs.block.oneOfassembled from the registry, witharity and descriptions sourced there too (removed from the builders, so they
cannot disagree). Seven subjects;
docSpecSchema()/docSpecSchemaId()retainedand delegating, pinned by a
toEqualtest.spec/validate.ts—validateSpec(unknown), zero-dependency structuralvalidation with path-anchored
V_*findings. Unknown top-level fields are awarning, preserving forward compatibility.
Samples & tests
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 tosamples/README.md(with new "Server" and "Quality" sections) and all executedend to end, not just type-checked.
registry,chart,layout-sugar,response,lint,agent,schema,compile-snapshot.governanceandversionextended.Docs & governance
docs/CHARTS.md,docs/SERVER.md,docs/LINTING.md,docs/AGENT_CONTRACT.md, anddocs/RECIPES.md— the counterpart to goldenrule 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.jsonall updated.AGENTS.mdgains an "adding a block kind" checklist that now routes throughthe registry, and a "recommended agent loop" section.
validates any staged draft.
ai-governance.jsondeclaredadvisory_in_ci: truebut no workflow had ever run it.
package.json—filesnow includesllms.txt(it was never shipped), andkeywords extended for discovery.
Validation
Additionally verified by hand:
requireand ESMimportsmoke tests against the built artifacts,covering all new exports;
doctor().ok === true, manifest reports 14 blockkinds,
schema()['$id']carries1.1.0.docs/RECIPES.mdexecuted 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
CHANGELOG.mdstill said "Six rules pre-empt" and filedL_MAX_BLOCKS_EXCEEDEDunder "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 readsix pre-emptand the text readsSix rules pre-emptAGENTS.mdnow documents the sweep, with this miss as the cautionary example./clientsubpath — new public API — was absent fromCHANGELOG.md, from the user-facing release note, fromcapabilityManifest(), and from every sample. So were the two other user-visible packaging fixes of round 2 (thenode:prefix restoration, the tree-shaking win)### Addedentry; a release-note section;contract.entry/contract.clientEntry/contract.reactServerConditionandclientComponents[].importFromon the manifest; bothsamples/client/*switched tosrc/client.jswith the real-world import shown in the headerlayout.encryption; the artifact users actually read did not## Securitysection, placed above the highlightsdocs/KNOWLEDGE_BASE.md§3 had nosrc/client.ts, §6 had notests/compile-snapshot.test.tsx, and §304's enumeration listed six under the word "eight"docs/KNOWLEDGE_BASE.mdandAGENTS.md;src/registry.tsand.github/copilot-instructions.mdstill said the bundle strips the'use client'directivepublish.ymlverified only the fourdist/index.*artifacts and never resolved theexportsmap — both workflows imported by file path, so a wrongtypestarget or a dropped condition would have shipped unseen.and./clientin both conditions, and renders a real PDF from the installed packagepostbuild.mjsfailed open — its headline tree-shaking guard degraded to aconsole.logand exit 0 ifesbuildwas absentPOSTBUILD_SKIP_SHAKE_CHECK=1opt-outTwo round-3 findings were rejected after verification: the reported
upload-artifactversion inconsistency is shared verbatim bypdfnative-cliand
pdfnative-mcp, so "fixing" it would have created the ecosystem divergenceit claimed to remove; and a reported total test-suite failure turned out to be
an audit tool perturbing
node_modules—npm cifrom the committed lockfilerestores 224/224, which is also what CI does.
Round 2
import('fs/promises')without thenode:prefix. Deno and Cloudflarenodejs_compatrefuse 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 survivesplatform,target,externalandbanneralike (all four measured)scripts/postbuild.mjsrestores the prefix and fails the build if the expected shape is absent; a bundler-resolution step inci.ymlcompiles both artifacts the way a non-Node bundler wouldimport { version }pulled the entire React reconciler into a consumer's bundle — 10 137 bytes for a string constant, andreact-reconcilerforced to resolve. A single-file bundle makessideEffects: falseinoperative/* @__PURE__ */onReactReconciler(hostConfig),HostTransitionContext,HOST_CONTEXT,LINT_RULE_CODESandBY_KIND. Now 3 216 bytes, no reconciler; postbuild fails the build if it regresses'use client'never reacheddist/, so RSC users needed a hand-written wrapper — whileREADME.mdclaimed the directive was carriedpdfnative-react/clientsubpath export, built separately with the directive applied and verified by postbuild. The root bundle is asserted not to carry itL_MAX_BLOCKScould not fire on the engine's default ceiling. It checked only an explicitlayout.maxBlocks, but the engine appliesDEFAULT_MAX_BLOCKS = 100 000unconditionally and throws — so a large generated document linted clean and then crashedlayout?.maxBlocks ?? 100_000; test at 100 001 blocksL_TAGGED_ENCRYPTED(pdf-document.ts:169) andL_MAX_BLOCKS_EXCEEDED(:146) both throw; the docs listed them as safe. Repeated in 7 filesCHANGELOG.md), which the sweep's own regex had missedschema('manifest')described 10 of the manifest's 13 properties — missingclientComponents,errorClasses,schemaSubjects, two of which were added for agent honesty. No test covered itObject.keys(capabilityManifest())to the schema's properties andrequiredChartPropshad no compile-time tie toChartBlock, whiledocs/CHARTS.mdpromises Charts-v2 fields "arrive as newChartProps"ChartPropsCoversChartBlockassert; verified destructivelytoBlockhad no exhaustiveness guard — a newHostTagwithout a case compiled cleanly and failed at render, while the DocSpec side had aneverguard since 1.0const exhaustive: never; verified destructivelydoctor()claim retracted in round 1 was still live in five documents, includingllms.txtandAGENT_CONTRACT.md— the two an agent loads first.nvmrcpinnedlts/iron(Node 20) againstengines: >=22;CONTRIBUTING.mdandpublish.ymlsaid 20 too — a leftover from this PR's own bumpci.yml,codeql.ymlandscorecard.ymlwere a generation behind the three sibling repos: unpinned actions (whilepublish.ymlin the same repo is SHA-pinned),codeql-action@v3vs v4, noconcurrency, notimeout-minutes, andscorecard.ymljob permissions that dropcontents/actionstonone— job-levelpermissionsreplace, not merge, socheckoutgets a 403pdfnative-cli, React deltas re-appliedjs-yaml/postcssoverrides; runtime audit is now blocking (npm audit --omit=devis clean — the prod tree is one dependency), dev audit advisory with the reason stated### Securitysection in the CHANGELOG and a callout indocs/RECIPES.mdREADME.mdwith measured sizes and the--codepointsescape hatch.github/instructions/components.instructions.mdhad the same stale-procedure defect its two siblings were rewritten for in round 1;spec.instructions.mdclaimed "the first five steps are compiler-enforced" when the real set is 1, 3, 4, 5, 6, 7byteLength > 100tests/compile-snapshot.test.tsx: a committed snapshot of a document using every block and every document-level prop.tsfor a.tsxfile)One round-2 finding was rejected after verification: a reviewer disputed the
coverage figures. Re-measured — the documented numbers were correct.
Round 1
validateSpec— the "never throws" untrusted-input gate — overflowed the stack on a ~44 kB deeply nested payloadV_TOO_DEEPcode, regression test at depth 5000schema('toString')resolved throughObject.prototypeand returned a stringObject.hasOwnguard; test covers five prototype keysschema('lint-report')handed out a live reference toLINT_RULES; mutating the returned schema changed every subsequent lint severity process-wideL_CHART_VALUESmissed anundefinedvalue (.find()returnsundefinedfor a foundundefined).some(); testL_MAX_BLOCKSreported "within 10% of the ceiling" when 5× over it, as a warningL_MAX_BLOCKS_EXCEEDEDerror; both testedL_HEADING_HIERARCHYnever flagged a document whose first heading was h2/h3L_CHART_EMPTY; testcapabilityManifest()claimed to describe "everything" while omitting 24 of 73 exportsclientComponents/errorClasses; a test now locks both directionsschema.tshardcoded every kind discriminator, so the registry and the schema could disagree (proved: registryh1–h4, schemah1–h3, typecheck green)blockDefs()overwrites the discriminator from the registry;registry.test.tsasserts itKNOWN_FIELDSinvalidate.tshad no locksatisfies readonly (keyof DocSpec)[]plus anAssert<Equals<…>>LINT_RULESwas not locked in either direction — a declared-but-unimplemented rule would ship into the schema and the manifestEMITTED_LINT_RULES+ equality testContent-Dispositionfilename*emitted' ( ) ! *, which are not RFC 8187attr-char; a raw apostrophe mis-parses the ext-valuedocs/RECIPES.mdannotation example was wrong on both arguments and could not runcreateModifier(openPdf(bytes)),buildAnnotationBody,save()) and executed.github/copilot-instructions.mdand.github/instructions/spec.instructions.mdstill described pre-1.1.0 architecture — nochart, noregistry.ts— so an agent following them would fail the repo's own compile-time lockdoctor()'s headline claim ("works when the peer is missing") was false — a static re-export means the module graph fails firstsrc/doctor.ts; round 2 found five documents still carrying it and finished the jobdocs/SERVER.mddocumented a Server Action, but RSC-layer imports fail at module load (react-serverhas nocreateContext)./clientsubpathFindings acknowledged but not acted on, with reasons:
^1.6.0+ Node>=22warrant a major. Neither is source-breaking, both are documented at thetop of the release notes, and the alternative for the peer (
^1.5.0 || ^1.6.0plus a capability guard on every chart path) trades a build-time error for a
runtime surprise. Recorded here so a reviewer can overrule it.
pdfnative-clideclaresengines.node: ">=20"while depending onpdfnative@^1.6.0, which requires 22— and its CI matrix tests Node 20. And
pdfnative/docs/guides/react.mdstilldescribes 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.
validateSpecfuzzing, raised coverage thresholds,eslint-plugin-react-hooks,Cache-Control/ETag onrenderToResponse,causeonPdfReactError. All reasonable; all tracked for 1.2.0 ratherthan widening this release further.
Backward compatibility
$idnow/1.1.0/$idis the drift-detection contractparams.layoutpopulated by sugarundefinedinvariant preserved and testedPdfStructureError extends PdfReactErrorinstanceof(both classes andError) and.nameunchangedPdfStructureErrormoved toerrors.tsdocSpecSchema()/docSpecSchemaId()toEqualtest againstschema('doc-spec')files+=llms.txt^1.6.0, Node>=22Out of scope (by design)
pdfnative 1.6.0 also shipped
extractText,readFormFields/fillForm/flattenForm,openPdf({ password }),streamMergedPdfs/streamSplitPdf/streamExtractPages, andMergeOptions.encrypt. None are re-exported: theyoperate on existing bytes, and this package authors documents (golden rule 7).
docs/RECIPES.mdshows 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 commoncase; permanent public surface for a marginal gain.
NODE_ENVand emit unrequested output; also would makelintDocumentimpure,ruling out its best use (a test assertion).
Self-review checklist
pdfnativeimports still go throughcore-bridge;types.tsremains the one type-only exception;pdfnativeis still a peer.<Chart>maps 1:1 onto the engine'schartblock; the layout sugar is<Document>props, not new host tags.<Section>is still the only composite.host-config.tsor
reconciler/render.ts.any; lint clean with zero warnings.'use client'unchanged onhooks.ts/viewer.tsx; none added tosrc/spec/;response.tsis explicitly server-side.DocSpec↔ JSX parity holds — every new capability reaches bothsurfaces, with
compileSpectoEqualcompileDocumenttests for chartsand the layout sugar.
src/version.tsbumped;package.jsonandCITATION.cffin sync (pinned by test).docs/RECIPES.mdadded as the documented alternative.or publish was performed autonomously. A human reviews and submits it
under their own identity.
Compliance report
no_new_runtime_dependency_confirmeddependenciesis still exactly["react-reconciler"], asserted bytests/version.test.tsreproduction_commandnpm run typecheck:all && npm run lint && npm run test:coverage && npm run build && npm pack --dry-runreproduction_resultnpm ci; 226/226 tests; coverage above thresholds on all four axes; runtimenpm auditclean; both export subpaths resolved from a real packed tarballduplicate_search_performedaffected_packagespdfnative-reactonly. Upstreampdfnativedocs still referencepdfnative-react v1.0.0indocs/guides/react.md,llms.txt,AGENTS.mdandREADME.md— a companion PR there would be worthwhile, and is not included here.identity_reminder_shown