npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mark2form/contracts

v0.13.0

Published

De enige contractbron van het Mark2Form-platform: zod-schema's en types die apps, packages en de twee websites delen.

Readme

@mark2form/contracts

De contractbron van het Mark2Form-platform: de enige plek waar de types, zod-schema's, protocollen, claimsets, event-namen, staffelvormen en gedeelde configsecties staan die apps, packages en de twee websites met elkaar delen. Nul runtime-deps behalve zod, en geen import uit een ander package — de afhankelijkheidsrichting van het platform loopt hiernaartoe, nooit hiervandaan.

Structuurplan §3 principe 3: "Types, schema's, event-namen, protocollen en prijzen staan één keer, in packages/contracts of packages/catalog; wie ze nodig heeft importeert ze. Een kopie is een bug."

Wat hier wel en niet hoort

Wel: vorm (schema's + types), de defaults en clamps die BEIDE kanten van een grens moeten delen, en pure lezers daarop.

Niet: app-gedrag, I/O, env-lezing, database, en niets dat een apps/-pad importeert. Een functie die een EditorSnapshot leest of sessionStorage aanraakt hoort in de app.

Deze grens is niet cosmetisch. Elke drift die het structuurplan in Bijlage A noemt ontstond doordat twee kanten bijna hetzelfde deden: de embed-JWT-claim po bestond alleen in het portaal, STUDIO_MAX_PRESETS stond op 6 en op 8, en VISION_DAILY_BUDGET_DEFAULT stond twee keer met aan beide kanten een commentaarregel die de ander opdroeg mee te veranderen. Niets faalde.

product-module en events

product-module is de ProductModule-interface uit §8.5: wat het portaal en de runtime van een product moeten weten zónder de engine te laden — naam, slug, configfamilie, modi, uitvoerformaten, staffels, btw-regel, accent, tabs en het tenant-configschema. Fase 2 vult alleen beschrijving (daarvoor staat ProductModuleFase2 klaar); defaults, render, pipeline en quality zijn fase 5 en dragen tot die tijd unknown. De consumenten zijn packages/products/* (die de module leveren) en apps/portal en apps/runtime (die alleen de vorm kennen).

De vormen Vertaling, StaffelTrap, ModusBeschrijving en UitvoerformaatBeschrijving lijken op Tekst, Staffel, Modus en Uitvoerformaat uit @mark2form/catalog en zijn dat met opzet niet: dit package importeert uit geen enkel ander package en catalog heeft nul dependencies, dus die twee kunnen geen gedeeld type hebben. De uitweg is structureel typen — hier staat de vorm met string waar catalog een eigen unie heeft (Taal, ModusId, UitvoerformaatId), zodat catalog-data er zonder omweg in past. Het bewijs dat ze niet uit elkaar lopen wordt geleverd door packages/products/neon, het enige package dat van allebei afhangt, met één satisfies ProductModuleBeschrijving. Verbreed de types hier dus nooit naar Record<Taal, string> of ModusId: dat zou de catalog-unies overtypen, en overtypen is de bug die §3 principe 3 bedoelt.

events is de event-enum: EVENT_TYPES in de volgorde van de CHECK-constraint events_event_type_check, plus EventType, isEventType, EventTypeSchema en de groepering (EVENT_GROEPEN, groepVanEvent) die het portaal telt als impressie, interactie, upload, trace of bestelling. Daarnaast staat hier de publieke zeef: PUBLIEKE_EVENT_TYPES (wat POST /api/event van een embed accepteert) naast SERVER_ONLY_EVENT_TYPES (de vier types die omzet- en conversiecijfers aansturen: payment_completed, invoice_created, demo_requested, demo_activated). Beide lijsten zijn met de hand geschreven en de test eist dat hun unie exact EVENT_TYPES is — een nieuw type dat niemand classificeert laat de suite falen in plaats van stilletjes publiek postbaar te worden. De constraint is leidend: whitelist en TS-union kunnen ernaast zitten zonder dat er iets kapotgaat, de database niet. Drie types zitten opvallend NIET in een groep — trace_failure (wel een trace-gebeurtenis, geen geslaagde omzetting), export (een eigen funnel-stap, zou naast submission dubbeltellen) en config_saved (een handeling van de klant, geen bezoekersmeting). De redenen staan per type in de bron.

endpoints

endpoints is de machine-leesbare tabel van §5.3: alle 25 endpoints van /api/v1 met hun id, pad, methoden, klasse (public-browser / server-to-server / proxied), auth, het OUDE pad dat als alias mee moet en een toelichting. zetWildcardCors(klasse) is de enige plek waar de CORS-regel uit die klasse volgt — §5.3 zegt dat negen huidige routes access-control-allow-origin: * zetten en dat alleen de public-browser-rijen hem houden; hier is dat data in plaats van negen keer hetzelfde met de hand. Zes van die negen houden hun wildcard, drie raken hem kwijt (embed-session-refresh, embed-meta, designs-create), en de test bevriest die uitkomst met een handgeschreven verwachtingstabel. aliasPermanent is precies één keer waar: embed-legacy-loader, want GET /api/embed/<slug>.js staat in klant-HTML die wij niet kunnen wijzigen.

Er staat bewust géén status in. De tijdelijke kopie in apps/portal draagt per rij ook de bouwstand (gebouwd / wacht-op-l1 / fase-3); dat is de stand van één deployment op één moment en geen afspraak tussen twee kanten. Een contract dat zijn eigen bouwstand meepubliceert dwingt een versiebump af zonder dat er iets aan het contract verandert, en vertelt een consument iets dat voor hém niet waar hoeft te zijn. De app houdt die kolom in een eigen overlay. Om dezelfde reden ontbreekt routebestand() (:id → [id]): dat is de mapnaamconventie van Next, geen eigenschap van het endpoint.

Het design-id-headerbesluit hoort hier ook: DESIGN_ID_HEADER is x-m2f-design-id, want §5.2 hernoemt het protocol naar m2f:* en een merknaam in een header is een merknaam in code. DESIGN_ID_HEADER_LEGACY is x-upgraded-design-id — wat de runtime vandaag stuurt. Het portaal accepteert beide tot de runtime in zijn eigen PR overstapt; zijn allebei gezet, dan wint de nieuwe. Wie de nieuwe naam toelaat noemt hem ook in access-control-allow-headers, anders faalt het cart-relais stil op een preflight.

embed-protocol

embed-protocol is het embed-protocol v1 uit §5.2 (issue #49): de vijf berichttypen van de handshake als { m2f, legacy }-paren in EMBED_BERICHTEN (m2f:ready/upgraded:ready t/m m2f:request-close/upgraded:request_close), het legacy-containervoorvoegsel upgraded-, het custom element <mark2form-configurator>, data-m2f met alias data-upg, de hoogteklem 200–5000 px, de init-timeout van 1500 ms en de headers van het cart-relais (x-m2f-signature/x-upgraded-signature, x-m2f-event/x-upgraded-event, eventwaarde design.submitted, user-agent UpgradedPortalRelay/1.0).

De upgraded:*-namen zijn hier geen merkstring maar protocol: ze staan in klant-HTML en tenant-webhook-verifiers die wij niet kunnen wijzigen, en §5.2 houdt ze als alias gedurende de hele v1-levensduur — de nieuwe naam komt ernaast, nooit in de plaats. Elke legacy-waarde is byte-gelijk nagelezen in de code die hem vandaag spreekt; de bronnen staan per constante in het docblok, en de test bevriest ze letterlijk. Het design-id in dezelfde relay-headers (DESIGN_ID_HEADER(_LEGACY)) stond al in endpoints en blijft daar.

tenant-config-schema en branding

tenant-config-schema is het volledige zod-model van software_configs.config, met per veld een .meta() die zegt wat het portaal moet tonen en waar de waarde landt. Drie afwijkingen tussen dit schema en wat er LIVE staat zijn in 0.5.0 rechtgetrokken (issue #38, sectie A):

| Veld | Was | Is | |---|---|---| | general.tubeDiametersMm | z.array(z.number()) | z.array(z.string()) — de opslagvorm (string_list, migratie 0011/0012). tubeDiametersMmAlsGetallen() is de ENE omzetting naar millimeters | | branding | {brandLogoUrl, primaryColor, secondaryColor} | de vorm die gelezen wordt (hieronder); de camelCase-sleutels blijven als leeslocatie met portalField: false | | production.profile | ontbrak | staat erin, letterlijk zoals migratie 0037 hem definieert — mergeConfigSave kan het pad nu schrijven |

tenantConfigVelden() is de introspectie: per veld pad, sectie, type, zodType, het schema, de uitgepelde kern en de meta. unwrapVeld, vindVeldMeta, veldVorm, enumOpties en afgeleidVeldType staan er los naast. Ze stonden tot 0.5.0 als private helpers in apps/runtime/scripts/generate-config-schema.ts én in het portaal: twee kopieën van dezelfde aannames over zods interne def. De lezer laat bewust niets weg (portalField: false en normalisator-secties komen mee) en daalt niet af in arrays — die rijen zijn data. Wie een rij-editor bouwt leest NeonColorSchema en RoomSchema, die sinds 0.5.0 per subveld een label dragen plus de rij-typen hex en image.

Veldlabels zijn Nederlands en blijven string. labelVertaling, helpVertaling en (op een optie) labelVertaling staan ernaast, met Vertaling uit product-module; veldLabel, veldHelp en optieLabel lezen ze en zeggen erbij in wélke taal de tekst staat en of het de terugval is. Dat laatste is wat een scherm in zijn lang-attribuut hoort te zetten. Er is nog niets vertaald, en dat hoor je te zien.

branding is de branding van één grant, in de vorm die GELEZEN wordt — { logo_url, primary_color, brand_name, offer_email, powered_by }. Die vorm komt uit extractBranding (config-model), readBrandingFromGrant (runtime) en de mailsjablonen; het portaalscherm schrijft hem al, want anders ziet de bezoeker het logo niet. primary_color is bewust NIET nullable: beide lezers vallen terug op BRANDING_DEFAULT_PRIMARY_COLOR, dus een lezer heeft altijd een kleur. powered_by is nieuw en staat standaard AAN — de vermelding staat er vandaag, dus een ontbrekende sleutel moet true betekenen. De opgeslagen vorm is BrandingSchema.partial() en dus geen tweede sleutellijst.

DesignOrigin / ProductKind en de leesalias

Eigenaarsbesluit Pascal, 21-9-2026 (issue #74):

Neon Studio            de CATEGORIE: hier beheer je je instellingen
├── Neon Logo          product, eigen staffel
└── Neon Tekst         product, eigen staffel

DesignOrigin (en zijn alias ProductKind) draagt daarom 'logo' | 'tekst' in plaats van 'logo' | 'studio'. Dat zijn nu dezelfde twee waarden als OutboxModus, zodat de tarief-afbeelding tussen runtime en factuur kan verdwijnen; één test legt de lijsten naast elkaar.

'studio' staat nog in bewaarde gegevens — revisies, autosaves en elke #design=-deel-link die ooit de deur uit ging. Daarvoor is er één lezer, en die gaat maar één kant op:

import { leesDesignOrigin, designOriginOfLogo } from "@mark2form/contracts";

leesDesignOrigin("studio");      // "tekst"
leesDesignOrigin("tekst");       // "tekst"
leesDesignOrigin("van alles");   // null
designOriginOfLogo(undefined);   // "logo"  — ontbreekt → logo (G1)

Er bestaat géén tekst → studio, en 'studio' zit niet in DesignOrigin: een schrijver krijgt de waarde genormaliseerd of hij compileert niet. Zet de lezer op de PARSE-grens waar bewaarde gegevens binnenkomen — decodeShareUrl, loadDesign, de snapshot-lezer van de editor, leesEmbedStand — en nergens anders. Niet bij het herafleiden van een bestaande design_revision: die waarde zit in de gehashte inhoud, dus normaliseren levert daar een andere contentHash dan de opgeslagene. Of de alias ooit weg kan staat bij DESIGN_ORIGIN_LEESALIASSEN in neon-domain.ts.

outbox en usage-download

outbox zijn de domein-events van §5.4: de zeven typen (download.completed, order.created, order.status_changed, proof.decided, payment.completed, review.approved, subscription.changed), elk met zijn eigen payload-schema, samen in OutboxEventSchema — een discriminatedUnion op type, zodat een payload nooit onder het verkeerde type kan hangen. Alles wat geld raakt of waar een ander systeem op moet reageren gaat als rij de tabel outbox in (id, type, key unique, payload, created_at, processed_at, attempts); het PORTAAL schrijft ze, een worker-taak verwerkt ze (grootboek, mail, Stripe, webhooks naar de maker). Omdat key uniek is, levert dezelfde gebeurtenis twee keer melden één rij en dus één factuurregel — precies waarom §5.4 een afzender opdraagt bij géén 2xx te blijven proberen en het event nooit te laten vallen.

De idempotentiesleutel van download.completed staat hier als invoerstring en niet als hash: downloadIdempotencyInvoer(payload, maand) geeft bedrijf_id|modus|design_revision_hash|maand, en billing en de runtime halen daar zelf een SHA-256 overheen met node:crypto. Dat is geen halve implementatie maar de grens op de goede plek — dit package heeft alleen zod als dependency (§5.1), en wat de twee kanten moeten delen is de VOLGORDE en de scheidingstekens, niet de hashfunctie. Welke vier velden erin zitten volgt letterlijk uit §9.1 ("per modus, per kalendermaand; herdownload van dezelfde ontwerprevisie in dezelfde maand telt niet opnieuw, ook niet in een ander formaat"), dus formaat, bron, grant_id, design_id en downloaded_at zitten er bewust NIET in. De maand komt uit amsterdamMaand — nieuw in amsterdam-dag, want de factuur is Nederlands en de maandgrens ligt in Europe/Amsterdam (§9.1).

usage-download is het contract van POST /api/v1/usage/download (§5.3, fase 3 punt 3): de runtime meldt, het portaal antwoordt. UsageDownloadRequestSchema is DownloadCompletedSchema min bedrijf_id en bron — afgeleid, niet overgetypt. Die twee horen niet in de body: het bedrijf volgt uit de grant (en die opzoeking is meteen de plek waar het portaal controleert of deze aanroep die grant mag melden), en de bron volgt uit de auth van de aanroeper. Daarmee is ook duidelijk wie de sleutel van §5.4 samenstelt: de aanroeper kent bedrijf_id niet, dus zet hij hooguit een eigen retry-sleutel in de header IDEMPOTENCY_KEY_HEADER (idempotency-key) terwijl het portaal de echte sleutel uitrekent zodra het de grant heeft opgezocht. Het antwoord ({ ok: true, key, duplicate }) geeft hem terug; een duplicaat is een 2xx, want een tweede melding is de normale uitkomst van een geslaagde retry en geen fout.

webhooks

Het bericht dat het platform naar een https-adres van de maker post (WENS-14): de envelop { id, type, versie, aangemaakt_op, data } met als eerste event bestelling.aangemaakt (versie 1) en het testbericht ping. data draagt alleen wat de maker in zijn portaal ook al ziet — ordernummer, soort, omschrijving, naam en adres van de klant, het herrekende bedrag in centen, de configurator en een portaal_url — en bewust niet de ruwe payload, bestanden of downloadlinks: het gaat naar een adres dat de maker zelf kiest (AVG, zie het kopblok van src/webhooks.ts).

Elke bezorging draagt drie headers: Mark2Form-Signature (t=<unix>,v1=<hex hmac-sha256(geheim, t + "." + ruwe body)>, zoals bij Stripe), Mark2Form-Event en Mark2Form-Delivery. De maker controleert met dezelfde functie die wij gebruiken om te ondertekenen:

import { verifieerHandtekening, WEBHOOK_HEADERS } from "@mark2form/contracts/webhooks";

const ruw = await request.text();
const uit = await verifieerHandtekening({
  geheim: process.env.MARK2FORM_WEBHOOK_GEHEIM!,
  ruweBody: ruw,
  header: request.headers.get(WEBHOOK_HEADERS.handtekening),
});
if (!uit.geldig) return new Response("ongeldig", { status: 400 });

De functies gebruiken WebCrypto (globalThis.crypto.subtle) en zijn daarom async; zo draaien ze ook op een edge-runtime, Deno en Bun, zonder dat dit package een tweede dependency krijgt. Tolerantie: vijf minuten (WEBHOOK_TOLERANTIE_SECONDEN), vergelijking in constante tijd.

account-status

Het account-statusmodel van een Mark2Form-klant (K02): negen standen in de volgorde van de reis — account_aangemaakt, email_bevestigd, software_gekozen, proefgebruik, betaling_vereist, actief, betaling_open, gepauzeerd, opgezegd — plus de acht statussen die Stripe voor een subscription kent (STRIPE_ABONNEMENT_STATUSSEN, letterlijk). accountStatusUitStripe is de pure afleiding daartussen; een onbekende waarde geeft null, nooit een gok. De vier standen vóór een abonnement leidt hij bewust niet af: die hangen aan auth en portaalgegevens, en horen in het portaal — met deze woorden. De Mark2Form-webhook (@mark2form/billing/stripe-m2f) schrijft de Stripe-status letterlijk in subscriptions.status (migratie 0077), zodat het portaal hem met deze functie kan lezen.

De registratie van het logodossier (admin W01)

Acht submodules voor de Mark2Form-admin (uitvoeringsopdracht, contracten C01–C11). Het ontwerp en de keuzes staan in docs/uitvoering/mark2form-admin/contracten-w01.md; hier alleen wat een consument moet weten.

| Ingang | Wat | |---|---| | registratie-basis | gebrande ID's (RunId, ArtifactId, …), UtcTijdstipSchema (Z of +00:00), Sha256HexSchema, ActorSchema, PROCESSING_ENGINES (de acht van InspectorEngine plus orkestratie, met een typeslot), coördinatenruimten, MeetwaardeSchema, TimingMetingSchema | | canonical-json | canoniekeJson en canoniekeJsonSha256 — canonical-json@1, byte-gelijk aan de bestaande sorteer-serialisaties voor elke waarde die ze aanneemt; gooit op NaN, Date, undefined in een array, __proto__ | | engine-release | ReleaseManifestSchema (strikt), manifestIdVan (manifest:<sha256>), ReleaseBindingSchema, de levensloop van een release, registratie en toewijzing | | processing-run | run en poging met hun statusmachines, capture_status, cacheherkomst, en leesRunCorrelatie voor events.metadata.run_id | | processing-step | StepExecutionSchema, stapcodes, verliesboekhouding, afhankelijkheden | | artifact-reference | ArtifactSchema met precies één eigenaar, beschikbaarheid, bewaring en exportbinding | | quality-case | verbeterzaken, vermoede tegenover bewezen oorzaak, annotaties met ruimte | | experiment | experiment, heruitvoerbaarheid en afgeleide vergelijkbaarheid |

Twee regels die overal gelden: onbekend blijft onbekend (een historisch record krijgt unknown_legacy, nooit "de huidige"; er staat nergens een .default()), en kleine gemarkeerde unies zijn strikt (een actor, eigenaar of binding met een veld te veel wordt geweigerd, niet gesnoeid).

De bevoegdheden van het beheer (admin W18a)

admin-capability legt de zeven capabilities van C10 vast (admin.read, customer.manage, diagnostics.run, quality.review, release.manage, billing.adjust, data.erase), de bundels die er rollen van maken (beheerder = alles, support, onderzoeker, releasebeheer, lezer), capabilitiesVanBundels (de vereniging, in vaste volgorde, een onbekende bundel geeft niets) en AUDIT_RESULTATEN (aangevraagd voor de vooraf-regel van een duurzame audit, plus gelukt | geweigerd | mislukt). Dezelfde lijsten staan als CHECK in de concept-migraties 0097 en 0099; admin-capability.test.ts legt ze naast elkaar. Wie wat mag beslist het portaal (eisCapability), niet dit package.

Ontwerprevisie en exportbewijs (admin W07)

export-bewijs legt C06 vast: RevisieVastleggingSchema (een servergevalideerde revisie met bron-sha256, bronrun — registered, niet_gekoppeld of unknown_legacy, nooit een browserclaim —, fysieke maat, productiekeuzes en de build die valideerde), ExportPogingSchema (één keer maken, met per bestand de sha256, het artifact, de opslagstand, of het geleverd is en de controle met haar profiel), PreviewBindingSchema met vergelijkPreviewMetExport (zelfde, andere of onbekend), de rijvertalingen voor de concept-tabellen design_revisions, export_pogingen en export_poging_bestanden, en EXPORT_MEDIATYPEN (de ene tabel mediatype per uitvoerformaat). Een reconstructie is altijd een eigen poging met een eigen bestand; zij heet nooit het historische bestand. De artifactsoort design_revision (eigenaar: de revisie) draagt de canonieke inhoud achter de hash.

Consumeren

In de monorepo — gewoon een workspace-dependency, en je leest de TS-bron:

{ "dependencies": { "@mark2form/contracts": "workspace:*" } }

Buiten de monorepo (het Astro-portaal, de marketingsites) — de gepubliceerde versie van npm, gebouwd met tsup (ESM + .d.ts):

npm install @mark2form/contracts

Dat verschil zie je niet in de code: package.json wijst in de werkkopie naar ./src/... en via publishConfig in de tarball naar ./dist/.... Niemand hoeft in de monorepo eerst te bouwen, en niemand buiten de monorepo krijgt ongebouwde TypeScript.

Smal importeren mag — elke submodule heeft een eigen ingang, zodat een consument die alleen het health-schema wil niet het hele neon-domein in zijn bundel krijgt:

import { HealthSchema } from "@mark2form/contracts/health";
import { OpsOverzichtSchema } from "@mark2form/contracts/ops-overzicht";
import { STUDIO_MAX_PRESETS } from "@mark2form/contracts/config/studio";
import { amsterdamMidnight } from "@mark2form/contracts/amsterdam-dag";
import { BrandingSchema, DEFAULT_BRANDING } from "@mark2form/contracts/branding";
import { tenantConfigVelden } from "@mark2form/contracts/tenant-config-schema";
import { EmbedJwtClaimsSchema, type EmbedJwtClaims } from "@mark2form/contracts/embed-jwt";
import { groepVanEvent } from "@mark2form/contracts/events";
import { ENDPOINTS, zetWildcardCors } from "@mark2form/contracts/endpoints";
import type { ProductModuleBeschrijving } from "@mark2form/contracts/product-module";
import { downloadIdempotencyInvoer } from "@mark2form/contracts/outbox";
import { UsageDownloadRequestSchema } from "@mark2form/contracts/usage-download";
import { verifieerHandtekening, WebhookBerichtSchema } from "@mark2form/contracts/webhooks";

De volledige lijst staat in het exports-blok van package.json; . levert alles.

Versiebeleid

Semver via Changesets (§5.1):

| Wijziging | Bump | |---|---| | een veld, schema, constante of submodule erbij | minor | | een veld weg, hernoemd, of van type veranderd; een strengere regel op iets dat bestond | major | | tekst, commentaar, een test | patch |

De schema's zijn daarom additief-vriendelijk: z.looseObject laat onbekende sleutels staan in plaats van ze te snoeien, zodat een consument op een oudere contractversie een nieuwer antwoord gewoon kan lezen. Runtime, portaal en cockpit worden niet op hetzelfde moment gedeployed.

CONTRACT_VERSION is de versie van dít package als constante. Hij wordt gelijkgehouden door scripts/contract-versie-sync.mjs, dat als tweede helft van pnpm version-packages draait; src/__tests__/tenant-config-contract.test.ts legt de twee naast elkaar en faalt zodra iemand dat overslaat.

Publiceren

Je publiceert niets met de hand. De flow is:

  1. Een PR die dit package raakt draagt een changeset (pnpm changeset).
  2. Na de merge naar main opent de release-workflow de PR "chore(release): versies bijwerken": die bumpt package.json, schrijft de CHANGELOG.md en synchroniseert CONTRACT_VERSION.
  3. Pascal mergt die PR. Dán pas bouwt en publiceert de workflow naar npm (@mark2form, public).

Wie een versie nodig heeft die nog niet gepubliceerd is, wacht dus op stap 3 — dat is met opzet: een contract dat op npm staat kan niet meer teruggenomen worden.

Wijzigen

Een contractwijziging raakt per definitie twee kanten. De spelregels (§5.1):

  • alleen via een PR met changeset, goedgekeurd door de contract-eigenaar (CODEOWNERS wijst packages/contracts aan als Pascal-review, §10.4);
  • elke wijziging heeft een test in beide consumerende apps — niet alleen hier;
  • geen enkele import uit een ander package erbij; pnpm boundaries bewaakt dat.

Tests

pnpm --filter @mark2form/contracts test        # vitest
pnpm --filter @mark2form/contracts typecheck   # tsc --noEmit
pnpm --filter @mark2form/contracts build       # tsup → dist/

De proef die het meest oplevert is de bevroren sleutellijst voor PRODUCTION_FIELDS: die bevriest de 35 mapper-velden en de 23 portaalvelden van deze kant. Het portaal draagt de tegenhanger, gelegd naast de config_schema-snapshot uit zijn migratieketen. Zodra het portaal dit package als dependency neemt zijn dat twee tests op één bron en kan de drift van september niet meer ontstaan.