@signalsafe/api-philosophy
v0.1.3
Published
ApiPhilosophy: shared strict OpenAPI contracts, generation and browser boundaries
Readme
ApiPhilosophy
@signalsafe/api-philosophy 0.1.3 is the coordinated shared-contract release approved on 2026-10-21. It extracts the common PhoneMe/DeliveryPlus Hey API pipeline and supplies browser request/response boundaries. Backend Pydantic models and current router exports remain authoritative. Generated application contracts, credentials, transport lifecycles, executable refinements and compatibility adapters stay in their applications.
Public entry points
@signalsafe/api-philosophy/tooling exports generateContracts, parseOpenApiDocument and compareArtifacts. Supply current OpenAPI 3.1, an output directory and an optional asynchronous formatter. A project owns backend export, conditional-route configuration and atomic promotion after successful generation. implicitObjectPolicy explicitly selects preservation (default), stripping or forbidding undeclared extras; declared additionalProperties always wins. stripPathPrefix is only the host's URL adaptation. Unsupported validation keywords, formats, unresolved references, parameter encodings and request protocols stop generation with locations. trimMode explicitly chooses ECMAScript or Python whitespace rules for a registered trim extension; arbitrary executable extensions/formats cannot silently pass through generation.
@signalsafe/api-philosophy/runtime exports createOperationRegistry, validateOutgoingRequest, validateResponse, decodeResponse, finiteJsonSchema, matchesSchema and their concrete contract types. Import this entry point in browsers. Its dependency graph contains Zod and runtime helpers, never Node APIs, TypeScript or Hey API.
Preflight checks finite JSON before host normalization, generated declared body/path/query/application-header schemas, then the final normalized body and URL. Query callbacks mutate URLSearchParams; changed entries are serialized once and PreparedRequest.path must be used for dispatch, cache identity, deadlines and response lookup. No-op callbacks retain the original URL encoding. Repeated entries and order survive serialization; optional entries may be explicitly removed. Scalar/array parameter callbacks parse backend-compatible wire values and must not substitute unrelated values. Host callbacks own body normalization, serialized scalar/array/deep-object parameter compatibility, unknown-query policy and executable refinements. No package-level scalar coercion, default insertion or extra stripping changes the dispatched body. Validation errors must stop dispatch and mutation. Standard unrelated headers are allowed. Scalar duplicate queries use the last value; form arrays can use repeated values or CSV. Flat deepObject query maps are supported; nested bracket encodings are rejected. HttpOnly cookies cannot be inspected by JavaScript: the host must explicitly declare their browser-managed names, and backend cookie validation remains authoritative.
Response lookup is method/path plus exact status, falling back only to explicitly declared NXX then default. It checks the selected variant's media type (case insensitive, ignoring parameters), declared application response headers, finite JSON and the selected schema. Multiple media variants are explicit. Empty responses must really be empty; binary/text download protocols return bytes for host-specific decoding. Pass the same metadata/body through validateResponse on cache reads. Applications keep cancellation, deadlines, retries, credentials, diagnostics, HTTP classification and conflict recovery around these checks.
Supported validation and compatibility
Generated Zod models preserve required/optional versus nullable fields, wire aliases, declared extra policy, numeric bounds and integer semantics without imposing an undeclared JavaScript safe-integer bound, string bounds in Unicode code points, patterns, array bounds/uniqueness, property names and object cardinality, supported unions including exclusive oneOf, and UUID/date/time/URI/email formats. Empty JSON schemas and Pydantic recursive JSON aliases mean finite JSON, never arbitrary JavaScript. Cycles, non-JSON values and numeric overflow are rejected. Domain models with genuine recursive structure still use generated model validators; arbitrary executable Python rules need tested application refinements. No claim is made that JSON Schema expresses all Python behavior. Structured enum/const values and validation siblings beside references/compositions currently fail generation; place supported constraints inside alternatives. Non-JSON string constraints require a protocol adapter rather than being discarded by byte decoding.
PhoneMe explicitly configures Python whitespace for its established trim extension and implicit object stripping. Optional named fields represented as JavaScript undefined are explicitly projected to missing wire fields; required fields and extension values still reject undefined. Its own adapter uses the exported native-Pydantic operation/model inventory for aliases, defaults, legacy scalar coercion and ignored extras. Compatibility controllers keep JSON-schema acceptance before normalization. Historical fixture fields recording old generator defects remain unchanged; shared acceptance tests require backend/wire authority instead. DeliveryPlus tests cover every Python-normalized fixture, while AdventureStories tests compare both its recorded strict-predicate wire expectations and normalized backend outputs. DeliveryPlus raw coercion and domain refinements must remain in its adoption adapter, not be inferred from normalized fixtures.
Maintaining the implementation
The public entry points are src/runtime.ts (browser validation) and src/tooling.ts (generation and drift orchestration). Internal modules own a single concern:
| Directory/module | Responsibility |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------- |
| src/generation/openapi.ts | Parse and inspect the current OpenAPI contract |
| src/generation/document-preparation.ts | Clone, name and promote inline declarations |
| src/generation/resolvers.ts and schema-policy.ts | Explicit Hey API object/string policies and semantic supplements |
| src/generation/*-transform.ts | Separate expression, field and component AST transformations |
| src/generation/operations-source.ts | Emit method/status-specific request and response registries |
| src/schema/ | Schema definitions, inspection, matching, structural input and supplemental issues |
| src/transport/ | Typed contracts, route/status lookup and serialized parameter validation |
| src/json.ts and refinements.ts | Finite JSON and shared generated-validator refinements |
Keep public signatures stable and change the owning module rather than adding host-specific branches to the pipeline. Tests execute emitted contracts, including the specification-derived semantic matrix and all three host fixture sets. npm run check:architecture rejects dependency cycles, modules over 250 lines and functions over 120 lines; CI runs it alongside types, lint, formatting, tests and packed-consumer verification. These limits prevent another monolithic generator without replacing design review.
Builds remove obsolete dist output before compilation. The package check requires the packed archive to match the current source inventory exactly, so deleted modules cannot ship accidentally. Internal files are packaged to support their imports but are not new public package subpaths.
Verification
The local pilot pins a scoped js-yaml 4.3.2 override for the generator's exact older parser dependency to close YAML denial-of-service advisories without changing generator or peer versions. Tests retain aliases, registered custom tags and malformed-input diagnostics, and verify the patched merge-work limit. npm overrides apply at this repository's installation root; they do not propagate to consuming hosts. Any future adoption must verify its own resolved dependency graph before release.
npm run test:coverage runs the same suites with source maps and writes coverage/lcov.info plus a summary for authored src and scripts. It restores the normal build even when a suite fails, so coverage maps cannot enter a release archive. The recorded baseline is measurement, not a new coverage threshold. CI imports LCOV for TypeScript and also analyzes the authored workflow files; OpenAPI fixtures remain test inputs rather than production source. Sonar CI analysis requires Automatic Analysis to be disabled for this project.
Node 24 and one of the tested Zod peers listed below are required. npm ci, npm test, npm run typecheck, npm run lint, npm run check:architecture, npm run format:check, npm run check:package and git diff --check are the local gates. Tests generate real contracts for all three recorded OpenAPI exports (PhoneMe 151 operations, DeliveryPlus workspace 173 and administration 66, AdventureStories 55), assert valid/invalid semantic edges, retained historical acceptance fixtures, deterministic drift failures and browser-runtime isolation. fixtures/provenance.json records source paths, original source SHA-256 checksums (sha256) and formatted snapshot checksums (fixtureSha256). Formatting changes snapshot bytes, never fixture values. Compatibility coverage includes PhoneMe's 10 extraction cases and 10 legacy normalization cases, all 1,192 accepted Python-normalized DeliveryPlus cases from 2,397 recorded inputs, and all 883 AdventureStories wire expectations plus 207 accepted Python-normalized outputs. Fixture snapshots do not replace each host's fresh-backend export/drift gate.
npm run build then npm pack --ignore-scripts makes the local artifact. PhoneMe temporarily verifies and installs that packed artifact through scripts/bootstrap-api-philosophy.mjs; it does not declare a file/link release dependency. The intended registry dependency is exactly @signalsafe/api-philosophy: 0.1.3. The coordinated public release was approved on 2026-10-21; registry availability must still be verified. Once available, add that exact dependency and lockfile to hosts and remove packed bootstrap. The package CI validates source/build/tests/packing; PhoneMe CI bootstraps the verified local artifact after npm ci and keeps backend, drift, reverse contracts, frontend quality and consumer tests.
Subsequent adoption of the same version
DeliveryPlus: replace common generator resolvers/comparison with generateContracts/compareArtifacts after the existing fresh exports for both applications. Keep its implicit strip policy explicit. Integrate runtime preflight after its documented body/query normalization (including case/empty sort direction and the already accepted UUID representations), retaining local TreeSpec/simulator/domain refinements, auth/cookies, pagination and binary adapters. Declare browser-managed HttpOnly cookie ownership. Validate exact status/media before callbacks/cache and on cache reads. Run its complete raw and normalized fixtures, mounted-route regressions, current-backend drift, types/lint/builds and cross-app boundary checker. Do not copy a separate generator.
AdventureStories: run its current route export and conditional inventory through the same package version. Its existing generated predicates/ExternalValue decoders stay authoritative until a typed adapter passes all existing strict predicates and complete host tests. Keep UUID/date semantics, account/auth headers, backend normalization and domain refinements. Replace only the common preflight/media/status machinery, preserving stronger decoders and binary adapters. Regenerate through the host's hooks, then run fresh drift, strictness fixtures, types/lint/builds and boundary gates. Do not mutate either application during the PhoneMe pilot.
Review and release
Independent review is required before release. Review the complete local package and affected host integration against their preserved starting state; earlier approval does not automatically cover subsequent changes. No repository remote, commit, publication or deployment is part of this pilot. Licensing and registry ownership must be confirmed before any publication; the local artifact is currently UNLICENSED.
Generated scalar/collection/enum validators include semantic checks against their original schema, including constraints without a scalar type and constraints alongside literal alternatives. Boolean false item/property/name schemas reject values at every nesting level. Conditional object keywords without an explicit type fail with a component/operation schema location: the current emitter would incorrectly narrow their legitimate scalar alternatives. Declare type: object only when that is the actual backend contract, or express the alternatives explicitly. Generated request and status-specific response registries exercise the same validators. Tests execute emitted modules, rather than treating the internal evaluator as evidence of generator parity.
Nullable enums without an explicit type also fail generation with their location. The upstream emitter drops their null alternative. Explicit nullable type alternatives are supported and tested. Supplements target omitted semantics; concrete scalar bounds already enforced by the emitter retain their ordinary Zod checks instead of acquiring duplicate refinements.
References reuse the completed component validator; supplements are not duplicated around pure references. A declared local default retains an output check because a Zod default can bypass the referenced parse.
Request paths represent HTTP targets. Raw URL fragments are rejected before normalization because fetch never sends them; encode a literal hash in path/query data as %23.
Conditional schema matrix
Untyped items constrains array elements while retaining non-array JSON values. Type and enum/const constraints intersect using JSON kinds: null is never an object, arrays are distinct from objects, and integers are a subset of numbers. Impossible intersections reject every value. Boolean schemas work at roots, fields, collection positions, references and union/intersection branches; object-only upstream scanners receive equivalent forms without changing the recorded validation authority or ordinary extra-field policies. False root models have TypeScript never types.
Required properties remain required even if their declarations or referenced models contain defaults. Pre-parse checks prevent defaults from hiding missing required input. Declared optional defaults preserve existing generated normalized output; an invalid default cannot escape output validation, and request preflight never silently inserts a generated default into the dispatched body. Empty or duplicate type alternatives fail with their schema location.
Defaults are an explicitly supported generated-output policy, never an instruction to send a missing HTTP body. A required body must remain present after host normalization and refinement, even if its parser accepts undefined through a default. Hosts may explicitly create a body; required bodies removed by a refinement reject before dispatch. Optional bodies remain absent, and null is accepted only when declared. Invalid defaults (missing required fields, wrong scalar types, violated bounds or forbidden extras) fail generation at the concrete /default schema location, including referenced and nested defaults. This also prevents optional parsers from swallowing a failed default and silently dropping it. Valid object defaults receive full output constraint checks. Defaults containing undeclared extras also fail generation under an implicit strip/forbid policy; declared open maps remain open. This prevents defaults from bypassing object parsing without silently changing their normalized output.
The executable matrix uses explicit specification-derived valid/invalid expectations across roots, required/optional fields, arrays, typed extension maps, named maps, references, anyOf, oneOf and direct/referenced allOf. It executes generated validators and method/status-specific request/response registries and compiles the emitted registry in strict TypeScript. Separate tests assert normalized output for preserve/strip/forbid object policies and explicit overrides, and fail unsupported shape families at every tested schema location. These supplement the unchanged three-project backend compatibility fixtures.
Repository style
Use four spaces per indentation level and a tab width of four. .editorconfig configures editors; .oxfmtrc.json is the formatter authority. npm run format and CI's npm run format:check cover all supported text formats across source, tests, scripts, configuration, workflows, documentation, local skills, lockfile and imported fixtures. Dependencies and generated build output are excluded; the TypeScript compiler emits dist with four-space indentation. Markdown and YAML retain the formatter's language-specific list alignment.
Lint enforces strict equality, const bindings where possible, no var declarations, type-only imports, no duplicate imports, no non-null assertions and braces around all branches and loops, alongside the existing correctness and unsafe-type rules. Warnings and unused disable directives fail the check. Strict TypeScript and the architecture check add separate type, module-size, function-size and cycle gates. These checks enforce concrete rules; they do not replace maintainability review.
Package-owned component-reference syntax, child/composition traversal groups and generated artifact filenames have named constants. JSON Schema vocabulary, JavaScript typeof operands, emitted-code text and independent test expectations remain explicit strings: turning every such string into an enum would obscure the protocol without removing duplicated behavior.
Explicit date-time compatibility
SchemaOptions.dateTimePolicy accepts the named policies exported as DATE_TIME_POLICY from @signalsafe/api-philosophy/runtime. The omitted policy remains zod-local, preserving the existing PhoneMe and DeliveryPlus pilot behavior. offset-required preserves AdventureStories's stricter authored timestamp contract: four-digit years from 0001, real calendar dates, seconds, a mandatory Z/z or colon-separated offset, and uppercase or lowercase T/t. Fractions and offsets remain verbatim; no timestamp normalization occurs.
Pass schemaOptions: { dateTimePolicy: DATE_TIME_POLICY.offsetRequired } to generateContracts, parseOpenApiDocument and direct matchesSchema calls. missingConstraints accepts the same options as its fifth argument (the fourth remains the diagnostic path). Generated validators and request/response parameter and body registries carry this policy; callers do not need a second runtime configuration. The emitter replaces its earlier ISO parser so lowercase text reaches the configured check. Defaults are inspected under the same policy before emission. Arbitrary SchemaOptions.formats callbacks remain runtime-only and generation rejects them with a diagnostic pointing to the supported named policy; executable functions are never serialized.
The cross-language fixture records actual ApiAuthSessionResponse JSON-mode Pydantic acceptance/output separately from browser wire acceptance. Python accepts some timezone-less, compact-offset and minute-precision inputs that the established browser contract rejects; normalized backend output is checked independently. Python may uppercase markers and truncate fractions, while browser validation preserves its input. These intentional differences do not authorize a global policy change. Independent approval is still required before repinning a host to this behavior change.
DeliveryPlus generator compatibility
A recursive union is emitted as typed finite JSON only when it contains the full unconstrained null/boolean/string/integer/number/self-array/self-map basis. Additional supported scalar alternatives (including formatted or constrained strings) are redundant in that union and preserve the same accepted values. A constrained basis, transforming extension, default-bearing extra alternative or unsupported recursive branch must fail with its component schema location.
Operation validator references case the complete operation name and role suffix once, matching Hey API declarations. Generation verifies every registry validator reference against the emitted declarations before returning output. Backend operation identifiers and trailing-slash ownership remain unchanged.
The runtime peer supports exactly the tested Zod versions 4.5.4 || 4.6.5 || 4.6.7.
Development is pinned to patched Zod 4.6.7; CI runs the complete suite, strict
public consumer and packaging checks on all three versions. Other peer versions have not
been declared compatible. The explicit date-time policy and omitted-policy
behavior remain unchanged.
Release preparation
Release 0.1.3 combines the installed Step 5 features and verified audit fixes. Public distribution was approved on 2026-10-21 and the package remains UNLICENSED; see RELEASING.md for approval, peer-matrix, archive, and host verification requirements.
Step 5 validation capabilities
A response media entry explicitly marked "x-api-philosophy-response-kind": "text" opts into generated string constraints and strict UTF-8 decoding. The media type must be text/* with a string schema; references resolve before validation. Invalid bytes, unresolved/cyclic references, incompatible media and binary-formatted schemas fail explicitly. A BOM remains part of the decoded string for schema validation. Existing unmarked text and binary downloads keep their byte contract. Cached text must pass the same response headers and string validator as network text.
Standard JSON Schema not applies to raw input before defaults or stripping and to emitted validators through structural constraints. Boolean operands, sibling constraints, nesting, references and double negation are covered by emitted-validator and request/response registry cases; unsupported negated keywords still fail generation at their source location.
Constrained recursive JSON unions may declare one non-transforming branch per JSON kind, with arrays and object values explicitly referencing that union. Scalar, array-size/uniqueness and object-size constraints apply at every depth. The generated validator proves finite JSON, checks the complete declared schema and returns the original value. Defaults, transforming extensions and other unsupported shapes retain their existing emitters or fail explicitly; they never silently become unconstrained JSON.
sameJsonValue from ./runtime compares finite JSON while ignoring object-key ordering and preserving scalar types and array order. Inherited properties, sparse arrays, cycles and other non-JSON values are rejected. This supports comparison with JSONB-loaded values without weakening request or response validators.
See the current release documentation for release-specific compatibility and verification boundaries.
