diff --git a/CLAUDE.md b/CLAUDE.md index 2959443..57993e5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,317 +1,58 @@ -# @cosyte/transform: Project Guide for Claude - -**▶ The long form lives in [`documentation/agent-notes.md`](documentation/agent-notes.md).** Every -trap below is a one-line imperative with a pointer into that file, where the incident, the -measurement and the rationale are written out **verbatim**. Relocated 2026-08-04, not deleted -(umbrella ADR 0023, amendment 2026-08-04). **When a one-liner here and a paragraph there disagree, -the notes are the measurement.** Re-measure before you soften either. - -## Project - -**`@cosyte/transform`**: a developer-focused **HL7 v2 → FHIR R4 transformation** library for -Node.js/TypeScript, published under the Cosyte brand. Open-source (MIT). **Not a parser.** It is the -Cosyte **transformation tier**, one layer _above_ the parser suite: a **consumer** that takes an -already-parsed `@cosyte/hl7` message and produces a validated `@cosyte/fhir` model. Grounded on the -official **HL7 Version 2 to FHIR** Implementation Guide (`hl7.fhir.uv.v2mappings`, STU Edition 1). - -**North star:** a developer parses an `ORU^R01` or `ADT^A01` with `@cosyte/hl7` and gets back valid -FHIR R4, without reading the v2 spec, without hand-writing a ConceptMap, and **without ever being -handed a confident wrong FHIR value**. The borrowed disciplines (not the parser shape): the -**fail-safe rule** (ambiguity → a typed, value-free diagnostic, never a guessed value), a stable -`OperationOutcome`-shaped diagnostic channel, immutable output, conservative emit validated against -`@cosyte/fhir`. Two architecture ADRs govern the tier: `documentation/decisions/0001` (may depend on -the parser tier; third-party runtime deps stay zero) and `0002` (terminology is a separate -`@cosyte/terminology` sibling; BYO ConceptMap). - -**A silently mis-transformed message is the same harm as a mis-parsed one.** Everything below marked -as a trap is clinical-safety content. - -## Status - -- **Phases 1-6 shipped**: datatype converters + diagnostic channel, ADT/ORU/ORM-OML/RXO/VXU/SIU/MDM - message graphs, and the IG value-ConceptMap translation layer. Full per-phase inventory: - `documentation/agent-notes.md#shipped-phase-history-phases-16`. -- **`AL1` is now built, `IAM` is still not.** An AL1 in any message becomes an `AllergyIntolerance` - wired to the bundle Patient, so bundles carry a resource type they did not before and the - completeness diagnostic no longer flags an AL1 it emits. **▶ `AllergyIntolerance.reaction.severity` - IS DELIBERATELY NEVER POPULATED** and neither is a `category` the IG's own map has no target for: - the guide publishes TWO maps over Table 0127 with different unmapped sets, so `MA` yields a type - and no category and that is the answer, not a gap to fill. Why, and the fixed `clinicalStatus`, - the alternate-codes extension, the withheld cases and the withdrawn AL1-6: - `documentation/agent-notes.md#shipped-phase-history-phases-16`. -- **`DG1`, `PR1` and `IN1` are now built too: `Condition`, `Procedure`, `Coverage`, all wired to the - bundle Patient**, with each Condition referenced back from `Encounter.diagnosis`. **▶ - `Coverage.status` IS NEVER ASSERTED**: the IN1 map publishes no row for it, so it ships - value-absent with a `data-absent-reason` of `unknown` and a - `TRANSFORM_REQUIRED_ELEMENT_UNKNOWN`, and its emit-schema entry deliberately carries NO required - binding so that shape satisfies the cardinality. **▶ `Coverage.payor` DECIDES WHETHER THE COVERAGE - EXISTS**: IN1-4 is its only source, so an unnamed insurer withholds the whole resource, and a named - one is a `display` with no literal reference because no Organization is built. **▶ - `Procedure.status` IS THE `unknown` THE MAP'S OWN ROW DIRECTS**, never `completed`. `DG1-21` - grounds exactly one status (`D` to `entered-in-error`) and `Condition.clinicalStatus` has no row at - all, so a Condition here can fail R4's `con-3`. Every deferred row of the three maps is declared - with a diagnostic rather than dropped, and the completeness baselines were SUPERSEDED, never - recaptured. **▶ THE `PV1-20` DECLARATION IS GUARDED ON THE MESSAGE CARRYING AN `IN1`**: it is the - one declaration sourced from a segment this reading does not otherwise touch, and ungated it added - an issue to a message carrying none of the three, which must produce exactly what it produced - before. All of it: `documentation/agent-notes.md#shipped-phase-history-phases-16`. -- **Phase 7 (FHIR→v2) shipped NARROWLY, and the narrowness is the point**: `toV2Patient` and - `toV2Observation` emit a **complete** v2 message (`ADT^` + PID, `ORU^` + OBX) - from the subset of the IG segment maps whose **inverse is one-to-one**. The **trigger is a required - argument on every entry point** and is never inferred: no FHIR resource carries one. - **▶ THE IG PUBLISHES NO FHIR-TO-V2 MAP**, so a many-to-one forward row has no usable inverse and is - refused, never resolved to its most likely source code; and **round-trip is asserted only as - "parses back", never as "equals"**. **▶ AND ABSENT IS NOT THE SAME AS SILENT**: a v2-REQUIRED field - the resource gives no source for (PID-3, PID-5, OBX-11) stays absent and RAISES - `TRANSFORM_V2_REQUIRED_FIELD_ABSENT`, and a conversion that grounds no field at all raises - `TRANSFORM_NO_V2_MESSAGE_EMITTED` instead of returning an empty success. **The usage cells behind - those rows are asserted, NOT extracted** (the pass that wrote them had no network egress), so - re-extract before trusting or widening them. The `Patient` + `Encounter` visit-carrying ADT is - **deferred, not dropped**, and **▶ THE UPSTREAM REASON FOR THE DEFERRAL IS GONE WHILE THE SHAPE IS - STILL NOT BUILT**: the parser exported no ADT assembly entry point when it was deferred, and - `@cosyte/hl7` `0.0.10` exports `buildAdt` (measured by a call that compiles and runs, in - `test/upstream-capabilities.test.ts`). Nothing else about the deferral changed, so do not read - "the builder exists" as "the shape is ready": hand-assembling PID + PV1 here would still invert - the tier split, and the IG grounding is still owed. Every measurement, the refusal set, and the - deferral: `documentation/agent-notes.md#the-reverse-direction-and-what-it-does-not-claim`; the - dependency refresh that took the measurement: `documentation/hl7-refresh/record.md`. Phase **8 - (profiles)** and deeper terminology remain deferred. -- **Never quote a version here.** This line read "not yet published to npm" for several releases - after first publish, which is part of why a `VERSION` constant stuck at `"0.0.0"` shipped unnoticed. - Derive it: `npm view @cosyte/transform version`. -- **▶ PUBLISHED AND INSTALLABLE, BOTH PEERS INCLUDED** (read 2026-09-25): `@cosyte/fhir` is on the - registry, and `npm install @cosyte/transform @cosyte/hl7 @cosyte/fhir` resolves and imports all - three. The `E404` on the absent peer and the `E403` that kept it absent are dated history in the - notes. Derive the current state, never recall it. -- **▶ THE "NAME-SIMILARITY" READING IS RETRACTED. DO NOT RENAME ANYTHING**, not the package, not the - scope, not an export. `FHIR-NPM-NAME` is a label, not a diagnosis; the error never asked for a - rename. **And this repo's older wording (`npm 404, a human-gated publish`) IS FLAGGED STALE**: the - registry refused at policy and **there was no approval button to press.** It is quoted, dated and - disputed in the notes; **relocating a disputed claim must not launder it into fact.** Derive, - never recall: `npm view @cosyte/fhir version`. **Visibility and publish state are independent**; - never infer one from the other. Why: - `documentation/agent-notes.md#publish-state-and-the-stale-claim-inside-it`. -- **Consumes two Cosyte siblings** (`@cosyte/hl7`, `@cosyte/fhir`) as **peer dependencies**, and for - dev/test **both are plain registry devDependencies** resolved through `pnpm-lock.yaml`. **▶ DO NOT - RE-VENDOR EITHER**: no `pnpm pack` tarball of a sibling goes back under `vendor/`, because a second - copy of a library in this tree is a version no dependency route watches, and the PHI scan refuses - a tracked file there. **Third-party runtime deps: zero.** - -## Tech Stack (the shared `@cosyte/*` standard) - -This repo inherits the canonical toolchain by depending on the published `@cosyte/*` config packages, -not by copying files. The source of truth is the meta-repo's `documentation/conventions.md`: this is -a summary. - -- **Language:** TypeScript (strict, full rigor set incl. `noUncheckedIndexedAccess`) via - `@cosyte/tsconfig`. **Target ES2023**, `NodeNext`. TypeScript 5.9.x, exact-pinned. -- **Build:** dual ESM + CJS + `.d.ts` via `tsup` (`@cosyte/tsup-config`); `attw` is a publish gate - (per-condition types: `.d.ts` for `import`, `.d.cts` for `require`). The `attw` script is - **`scripts/attw.mjs`, not the bare CLI**: see the guardrail below. -- **Node:** **>= 22** (CI matrix 22 + 24). -- **Package manager:** `pnpm@10`. -- **Lint/format:** **ESLint 10** + unified `typescript-eslint` (type-checked) via - `@cosyte/eslint-config`; Prettier via `@cosyte/prettier-config`. Lint at `--max-warnings=0`. -- **Testing:** **Vitest 4** + v8 coverage (`@cosyte/vitest-config`), per-directory >= 90 gates on - `src/datatypes`, `src/diagnostics`, `src/terminology` and `src/messages`. Property + fuzz - (`fast-check`) over **two** boundaries: `test/datatypes/boundary.property.test.ts` and - `test/messages/property.test.ts`, asserting never-throw, only registered value-free issues, no dangling - `urn:uuid:` reference, and an emit gate against `@cosyte/fhir.validateResource`. -- **CI/CD:** thin callers of the reusable `cosyte/.github` workflows, the two gate workflows - (`no-internal-refs`, `no-emdash`) included; the scanners they name stay in this repo. A called - job's check run is ` / `, so the contexts read - `no-internal-refs / public-surface`, `no-emdash / tracked-files` and `no-emdash / messages`. - **The checks BIND**: ruleset `ci-required-checks`, id `19914044`. -- **Runtime deps:** **Zero third-party.** `@cosyte/hl7` + `@cosyte/fhir` are peer deps (ADR 0001). -- **License:** MIT. - -## Branch protection - -**▶ EVERY TRAP HERE, WITH ITS MEASUREMENT AND PROVENANCE: -`documentation/agent-notes.md#branch-protection-and-the-limits-of-this-claim`. Read it before you -touch the ruleset, a required context, or a workflow that emits one**: the one ruleset (`19914044`) -and never a second, contexts pinned to `integration_id: 15368`, names read off real check runs and -not off a workflow's `name:`, what must never be required, the required job that gates only the -steps it still runs, the gate that can leave the job, the thin coverage backstop, the structurally -`BLOCKED` release PR and its escape, and that **nothing here can observe its own ruleset**. - -## Dependency watching - -Weekly `npm` + `github-actions` via `.github/dependabot.yml`. **Both siblings are registry -devDependencies, so the `npm` entry watches the versions the tests actually exercise**; security -update PRs are a separate repo setting: `documentation/agent-notes.md#dependency-watching`. - -## Engineering Guardrails - -- No `any`. No unjustified `as` casts. Use `unknown` and narrow. -- JSDoc (with `@example`) on every public export: the JSDoc lint rule is an **error** on public - exports, so this is enforced, not optional. -- Immutable output: produced nodes are `@cosyte/fhir` immutables; input v2 composites are never mutated. -- No `console.*` in library code. Return `{ value, issues }`; never throw on ambiguity. -- Short, testable functions over big mapping blobs. -- **The fail-safe rule (the whole point):** on any ambiguity (a naked timestamp, an unresolvable - assigning authority, an unmapped code, a non-UCUM unit, a value that would fail R4 validation), - emit a typed, value-free diagnostic and **refuse to produce a confident wrong FHIR value**. Never - silently default, pad a truncation, guess a timezone, synthesize an identifier system, or coerce an - unmapped code to a neighbor. -- **Grounded on the IG, never invented** (umbrella ADR 0018 applied to mappings). Every segment/ - field/datatype/table mapping is verified firsthand against the published HL7 v2-to-FHIR IG's - ConceptMaps and cited in the source; a mapping the IG has no target for is flagged, not guessed. -- **Value-free diagnostics.** A `TransformIssue` carries a stable code, severity, the v2 location - (segment/field/component index), and the FHIR path, **never a value**. Messages are static/per-code. -- Stable codes are a public API: `ISSUE_CODES` + `FATAL_CODES` are `key === value`; renaming/removing - one is a **breaking change**; new codes are additions only. -- Coverage: per-directory >= 90% (lines/branches/functions/statements), enforced by - `pnpm test:coverage`. - -### The `attw` gate - -Full narrative, every measurement: `documentation/agent-notes.md#the-attw-guardrail-in-full`. - -- **▶ `attw` SAYS "does not contain types" AND EXITS 0, SO THE `attw` SCRIPT IS A WRAPPER, NOT THE - BARE CLI.** `getExitCode.js` returns 0 before the problem list is read; no `--profile`, - `--ignore-rules` or config setting reaches that early return. For a package that ships types, that - sentence means **a broken publish reported as a pass**. -- **The race only supplies the condition**: every `tsup` build has a **~1.6–2.0 s** window with no - `.d.ts` on disk, reproduced with zero concurrency. **So the answer is not a lock, a lease or a - build queue:** the gate must be able to say its own inputs were missing, whatever removed them. -- **`scripts/attw.mjs` carries two nets that catch different things**: a path preflight (catches the - build window and _names_ the missing file) and a post-check on the untyped sentence (catches - declarations on disk but excluded from the tarball). **Do not collapse them into one.** -- **The post-check reads a string, so what would hide that string is refused by option NAME, - wholesale, not by value**: `--quiet`, `--format`, `--config-path`, and `.attw.json` settings. - A harmless value is refused anyway; that is the deliberate trade. -- **Do not reduce the wrapper to the bare CLI**: it reds 10 of `test/scripts/attw-gate.test.ts`'s 13 - tests, which is how the suite was checked for bite rather than assumed to have it. -- **A green `attw` has never meant a consumer can install the peer**: measured, `attw` never - resolves `@cosyte/fhir` at all. And a **complete but stale `dist/`** passes both nets. -- **This is a per-repo script and the prose does NOT port with the code.** Re-measure every number on - the package you port it to. Derive who still runs the bare CLI: - `rg -l --glob '**/package.json' '"attw":' /workspace`. - -### The PHI scanner - -Measurements, the grid, the refuters, and the `--staged` ARGV traps (`--diff-filter` keeps `T`/`U`, -`--no-renames`, STATUS not mode, re-measure the stride): -`documentation/agent-notes.md#the-phi-scanner-guardrail-in-full`. **No counts.** - -- **▶ THE CLAIM IS EXACTLY: it refuses (exit 2) every entry it ENUMERATES, and every path NAMED - DIRECTLY, that is not a regular file.** "Follows nothing" is looser and **two refuter passes - measured it FALSE**; do not tighten it back, and never close it by following. **`lstat` answers - for the FINAL COMPONENT ONLY**: touch `buildTargetsForPaths` and **re-measure, never re-assert**. - **A refusal never echoes the link target**, and that binds the prose: a _shape_, never an example. -- **▶ THERE ARE THREE ROUTES, NOT TWO** (`all`, `--staged`, ``), all running the content - passes. **Enumerate all three before calling anything additive.** -- **▶ SCOPE IS THE TRACKED CORPUS, RECONCILED AGAINST `git ls-files` EVERY RUN**, because - **the `fixtures` root HAD NEVER EXISTED ON ANY COMMIT** and went unopened on every run ever made while - the run printed clean. **A count cannot detect that**, nor can an existence check: an EMPTIED root - opens nothing. **Roots stay DISJOINT** or nested files report twice. The `*.md` walk skip is - gone (additive); `src/**.ts` was the **`--staged`** bound, now widened. **Not one rule.** -- **▶ WIDEN BY UNION AND PROVE THE GRID: every base `1` still `1`.** One cell is not: - **`phi-scan package.json` on the npm publisher mailbox**, declared with `EMAIL` (a **path AND an - address**). **Every allow-list entry is ROUTE-BLIND** and clears on `--staged`; every tag but - `EMAIL` is FILE-blind. **Named, never scrubbed.** -- **An exemption is a LITERAL PATH, never a predicate, and reaches the ALL route only**: the list is - empty, and `` reads whatever it is handed. -- **▶ `--allow-fixture` IS RECORDED AND REFUSED, NEVER HONORED**: unlogged, rejected before a target - is read; logged, still `2` after reporting every target it did not withdraw. **Exit 0 in NO mode.** - Only the allow-list can clear one: `documentation/agent-notes.md#the-bypass-recorded-and-refused`. -- **Exit `2` is every failure to complete; `1` is HITS FOUND. A regular-file root is `2` HERE, - derived from this contract; siblings differ, never port one.** A non-directory root refuses first, - because **`existsSync` FOLLOWS**: a dangling one printed clean over an off-disk corpus. An - **absent** root is fine. -- **Enumerating buys the SSN/email floor and NOTHING else**, so the HL7 v2 pass ships **in addition - to** it, never instead: the floor finds **zero** in this repo's `PID|` fixtures. **No standalone - `.hl7` ships: every message is a `.ts` literal.** -- **Four residuals are disclosed, NOT closed**, named in the notes; the reconciliation is **path - sets, not bytes**; **the enumerate-then-read race precondition HAPPENED.** -- **Throwaway repos under `os.tmpdir()`; never write a violator here. The scanner's own test file is - IN the corpus**: payloads assemble at runtime, never as literals. - -### The agent-instruction contract gate - -Full narrative, every measurement: -`documentation/agent-notes.md#the-agent-instruction-contract-gate-in-full`. - -- **▶ THIS FILE AND `documentation/agent-notes.md` ARE A CHECKED CONTRACT**: - `pnpm check:agent-notes`. Refuses a missing archive, an empty section, a dead `#anchor`, an - unresolvable file pointer here, an archive `##` nothing here points at. Exit `2` = could not - decide, `1` = violations. -- **▶ EXISTENCE IS NOT OBSERVATION**, and **a count cannot detect it**: a count counts the roots - that DID exist. It reconciles what it OPENED against `git ls-files`. **Never re-add a - `tracked.has()` pre-check before a read**: that made every branch unreachable, at zero firings, - while this line sold it as protection. -- **▶ EACH SPACE IS ITS OWN HYPHEN in an anchor slug; runs do NOT collapse.** Collapsing passed a - dead pointer and reddened a working one; our spaced-em-dash headings are that shape. -- **It proves a heading is POINTED AT, never that the one-liner says what the section says**: the - deliberate-omission trap has no identifier to grep for. **Enumerate those by hand.** -- **Two routes, not removable by one edit**: the suite in `ci / verify` (which inherits the two - levers above) and the command the `no-internal-refs` job runs, `pnpm ci:no-internal-refs` (which - inherits neither). `verify.sh` runs only the first. **That command is a single program on - purpose**: the shared gate word-splits it, so a `&&` between two gates is passed to the first one - as an argument and the second never runs. - -## Standing disciplines (every change) - -Mirrors the three disciplines in the meta-repo's `documentation/conventions.md`, and they bind here too: - -1. **Documentation follows code**. A change to the public surface/stack/status isn't done until the - docs are: this repo's docs content (`README.md`, `docs-content/`), the meta-repo - `documentation/repos/transform.md` (bump its "last verified" date), and the `ecosystem-map.md` - status table. -2. **Version + changelog**: a Changeset (`patch` on the `0.0.x` ladder) per meaningful change. - **The changeset summary IS the changelog entry** and `CHANGELOG.md` is generated output above - `## Released before this file was generated`: `.changeset/config.json` names a `changelog` - generator, so the release writes the version heading and the entry itself. **Do not hand-edit - `CHANGELOG.md`**, and do not reintroduce a hand-maintained `[Unreleased]` heading: one stood - there unrolled for the whole published history of this package, which is how a shipped tarball - came to describe its own contents as unreleased. **The Prettier pass stays ON here** (no - `"prettier": false`), derived from this repo having no `.prettierignore` and a `format:check` - that globs root markdown, **not copied from a sibling**: a sibling whose `.prettierignore` lists - `*.md` needs it off, and leaving it on there rewrote already-published text. **Re-measure both - arms if the `version` script changes**: with the pass off, canonical output here rests entirely - on `CHANGELOG.md` staying inside that script's `prettier --write` argument list. - `test/scripts/changelog-generation.test.ts` pins all of it. Renaming a stable warning code is a - **breaking change**. -3. **Crew + knowledgebase loop**: if this library's public API or issue codes change, flag/update - the matching `crew` healthcare skills (`terminology-mapping`, `fhir-resource-design`) + the KB - product doc. -4. **No internal project bookkeeping on a public surface** (founder directive, 2026-07-27). What a - consumer reads (`README.md`, `docs-content/`, the npm `description`, a release body, the JSDoc - their editor renders, the message text their log prints) says what the software does and what - changed. Item identifiers (`TRANSFORM-6`), phase and wave language, roadmap section numbers, ADR - numbers, meta-repo paths and "how this got built" commentary belong in the changeset, - `CHANGELOG.md`, the commit, the PR and the roadmap. It is a **translation** at the boundary, not a - deletion: when you strip a label off the front of a line, **repair the head**. Gated by - `pnpm check:no-internal-refs`. Full rationale and every measurement: - `documentation/agent-notes.md#no-internal-project-bookkeeping-on-a-public-surface-in-full`. - The traps: - - **The gate keys on known project prefixes**, so a new programme prefix has to be added by hand; - and it catches identifiers, not English sentences about our process: **the reviewer owns half - the rule.** - - **▶ NEVER RE-KEY THE RULE ON THE `WORD-N` SHAPE.** This repo's whole domain vocabulary is - written that way (`MSH-9`, `PID-3`, `OBX-5`, `OBR-25`, `SCH-8`, `TXA-19`) and none of it is a - violation. Never "resync" the prefix list with a sibling's copy without re-reading why this one - keeps `SYNTH` and the `HL7-\d{3,4}` exclusion. - - **Case sensitivity is load-bearing**: `FHIR-core`, `FHIR-required`, `FHIR-core-fixed` are live - here, and a case-insensitive rule calls every one of them a violation. - - **Three source surfaces, three answers:** `/** */` doc comments are **gated** (they render in a - consumer's editor); string literals are **gated** (they reach a consumer as diagnostic text); - `//` and `/* */` comments are **not gated and identifiers are welcome in them**, because the - convention says source comments are a place identifiers belong. - - **▶ DO NOT JUSTIFY THAT BOUNDARY FROM WHAT REACHES `dist/`**: two attempts to do so in a - sibling repo were both false. Measured here, `dist/*.map` ships every tracked source byte in - `sourcesContent`, so **all of `src/` is in the tarball anyway**. The line is what a consumer is - **shown**, not what lands on their disk. - - **Removing a doc comment to satisfy the gate is a REGRESSION, not a fix**: JSDoc with - `@example` on every public export is a hard guardrail above, and neither lint nor coverage will - catch its loss. - - The gate cannot read `dist/` (untracked build output): it gates the **source** of the published - text, not the published text. -5. **No em dash, anywhere** (founder directive, 2026-07-24), **commit messages and the PR title and - body included**. `pnpm check:no-emdash` gates every tracked file and filename; - `.github/workflows/no-emdash.yml` also gates the PR title, body and commit range. It landed - **with** its sweep: a gate before the sweep reds `main`, a sweep before the gate grows it back. - **Count the bytes in Python, never `grep`** (the container `grep` reads `0` here), and - **`no-emdash / messages` must never be required** (Dependabot pastes upstream release notes into - a PR body). Counts, exemptions, traps: `documentation/agent-notes.md#no-em-dash-anywhere`. +# @cosyte/transform + +The HL7 v2 to FHIR R4 transformation library: it takes a message parsed by `@cosyte/hl7` and +returns FHIR R4 resources validated with `@cosyte/fhir`, mapped from the HL7 Version 2 to FHIR +Implementation Guide (the IG), plus a narrow FHIR to v2 path back. It sits above the parser +libraries and never parses bytes; we publish it to npm (MIT) with both siblings as peers and no +third-party runtime dependencies. + +## Gate + +From the repo root, in this order: the steps of `.github/workflows/ci.yml` (the shared +`cosyte/.github` ladder, then its examples job), `.github/workflows/no-emdash.yml` and +`.github/workflows/no-internal-refs.yml`. + +```bash +pnpm install --frozen-lockfile +pnpm typecheck +pnpm lint +pnpm format:check +pnpm phi-scan +pnpm test +pnpm test:coverage +pnpm build +pnpm attw +pnpm typecheck:examples +pnpm lint:examples +pnpm examples +pnpm phi-scan:examples +pnpm check:no-emdash +pnpm ci:no-internal-refs +``` + +`package.json` pins pnpm 10, and scripts and tests call a bare `pnpm`: where the one on PATH is +another major, put corepack's first (`corepack enable --install-directory pnpm`, then prepend +`` to PATH). CI also runs checks with no script here: an ESM and a CJS import of the built +`dist/`, actionlint, CodeQL (`.github/workflows/codeql.yml`), and the shared pre-publish job that +packs the tarball and installs it into an empty directory. What the required checks prove and do +not: `documentation/agent-notes.md#branch-protection-and-the-limits-of-this-claim`. + +## Hazard + +This code sits in live patient-data flows: a mapping defect does not crash, it corrupts patient data +or sends it to the wrong patient or system in flight, and the receiver cannot tell. So every mapping +cites the IG row it implements, and anything left ambiguous becomes a typed, value-free diagnostic, +never a guessed value; an absent, flagged element is usually a cited decision, not a gap to fill. + +## Rules, each from a recorded failure linked on its line + +- Adding a segment must not change what a message without it produces, bundle or issues: an ungated PV1-20 declaration did during #46, and `test/messages/to-fhir.test.ts` now pins it (documentation/agent-notes.md#shipped-phase-history-phases-16). +- On the FHIR to v2 path, a v2-required field with no source stays absent and raises `TRANSFORM_V2_REQUIRED_FIELD_ABSENT`, never a default: the first cut in #38 returned no issue at all (documentation/agent-notes.md#the-reverse-direction-and-what-it-does-not-claim). +- Never write a version or publish state by hand, in prose or code; derive it with `npm view @cosyte/transform version`: `VERSION` shipped as `"0.0.0"` on every release until #22, and the README called a published package unpublished until #17 (documentation/agent-notes.md#publish-state-and-the-stale-claim-inside-it). +- Never hand-edit `CHANGELOG.md`: the release writes it from each `.changeset/` summary, and a hand-kept `[Unreleased]` heading shipped published code labelled unreleased until #28. +- `pnpm attw` runs `scripts/attw.mjs`; never swap in the bare `attw` CLI, which exits 0 on a tarball that carries no types (#19; documentation/agent-notes.md#the-attw-guardrail-in-full). +- `scripts/phi-scan.ts` must refuse, never pass, when it has not read every file it should: a root that never existed, a symlink and an honored `--allow-fixture` each printed clean over unread files (#35, #21, #44; documentation/agent-notes.md#the-phi-scanner-guardrail-in-full). +- `README.md`, `docs-content/`, JSDoc and diagnostic message strings carry no item IDs, phase names, ADR numbers or private repo paths: #11 swept them off every consumer surface and `pnpm check:no-internal-refs` keeps them off (documentation/agent-notes.md#no-internal-project-bookkeeping-on-a-public-surface-in-full). +- `pnpm check:agent-notes` checks this file against `documentation/agent-notes.md`: keep `@cosyte/transform` in the title above and a pointer here to every `##` section there, since a copy of this file was once rewritten to name another package (#29; documentation/agent-notes.md#the-agent-instruction-contract-gate-in-full). +- Take `@cosyte/hl7` and `@cosyte/fhir` from the registry as devDependencies, never as a tarball under `vendor/`: the vendored parser sat nine releases behind with nothing watching it until #39 (documentation/agent-notes.md#dependency-watching). +- Count dashes with `pnpm check:no-emdash` (em dash only) or Python over raw bytes, never `grep`: `grep` printed nothing for the pattern and the count read 0, which is how the first census came up short (#33; documentation/agent-notes.md#no-em-dash-anywhere).