Lewati ke konten

Changelog

Catatan rilis Rekamedika

Riwayat rilis Rekamedika: fitur baru, perbaikan, peningkatan performa, dan pembaruan dokumentasi di setiap versi, diurutkan dari yang terbaru.

Versi saat iniv0.1.9
  1. v0.1.9

    TerbaruDetail

    Perbaikan

    • api: every admin console load dead-ended on "Sesi tidak dapat diperiksa". platform.me answered 400 BAD_REQUEST "Unexpected end of JSON input" in 1 ms for the platform owner with a valid session. Three facts lined up: the dashboard-ui client forces every query onto POST (so PHI never reaches a URL), which sends an input-less query with no body at all; since the 2026-09-11 identity split platform.me rides an unbatched httpLink, so that body is genuinely empty rather than {}; and requirePlatformRole read the raw input eagerly and unguarded, only to fill the audit row's targetTenantId, which made tRPC's lazy req.json() throw. The gate now tolerates an unreadable body with the same .catch(() => undefined) tenantProcedure already puts on its own audit read, which is why auth.me, with the identical wire shape, never failed. A procedure that declares .input() still gets its own parser's BAD_REQUEST, so no bad input is masked. platform-gate.test.ts drives the real tRPC client link through the real fetch adapter into the real gate: red without the fix, green with it.
  2. v0.1.8

    Detail

    Perbaikan

    • admin: completes the content-negotiation repair 0.1.7 landed for web, dashboard and tenant. apps/admin was left out because it carries no agentDiscoveryMiddleware — an internal console has no markdown twins and no agent-discovery story — so it never had the branch the other apps repaired first, and every non-HTML Accept fell straight through to the framework's 500. Verified before the change: curl -H 'Accept: application/json' https://admin.rekamedika.com/ returned 500 {"error":"Only HTML requests are supported here"}, which is exactly the shape crawlers, agents, uptime probes and the Claude-in-Chrome webmcp bridge send. start.selftest.ts gains the matching guard, so removing the middleware now fails the same way removing csrfMiddleware does.
  3. v0.1.7

    Detail

    Fitur Baru

    • factory: the release rail gated releases and nothing watched production. Between 2026-09-04 and 2026-09-11, app.rekamedika.com returned 500 on every SSR route for eight minutes, klinikfn24.com did the same forty minutes later, and rekamedika-server threw on 941 of 2,260 invocations across five days — every one a cron, all Postgres 28P01 — and the owner was told none of it. src/health/ adds a fourth report to the existing */15 tick: edge 5xx per host, Worker exceptions by rate, CPU kills, downstream stalls, cron failures, skipped crons, and every deploy and rollback. The load-bearing finding is that a Worker which *returns* HTTP 500 is recorded with outcome: "ok" — GraphQL reported errors: 0 for the dashboard across the entire week containing a total outage — so HTTP status and Worker outcome are read from two separate sources and never collapsed into one number. Requires Zone Analytics:Read on CLOUDFLARE_API_TOKEN; without it the watch says on Telegram that it is blind rather than going quiet.

    Perbaikan

    • web, dashboard, tenant: TanStack Start answers any request whose Accept names neither the wildcard nor text/html with HTTP 500 and a fixed JSON body, so agents, crawlers, uptime probes and the Claude-in-Chrome webmcp bridge — which probes <origin>/mcp with Accept: application/json, text/event-stream on every site it visits — all saw a server error where a 406 belongs, and every one landed in the Worker's error metrics. Detection matches the framework's sentinel body after next() rather than checking Accept before it, because request middleware runs before route matching and cannot tell an unmatched path from a server route that negotiates its own Accept (/llms.txt, /index.md, .well-known/*.json, blog/rss.xml). apps/admin and the separate docs repository are not covered and still return 500.
    • factory: every Telegram send reported success whenever the HTTP request completed. A 429, a revoked token, a wrong chat id, and Telegram's own {"ok":false} under HTTP 200 were all indistinguishable from delivery — and reportUptime wrote that down as alerted, advancing lastAlertAt, so a lost alert suppressed its own retry for a full repeat interval. notify() now returns whether the message actually landed, Telegram's ok field is the authority rather than the HTTP status, and every refusal is recorded as factory-telegram-fault in Workers Logs.
    • factory: UPTIME_TARGETS watched two URLs of a ten-Worker estate, which is why two of the three outages above were silent. It now covers nine hosts, each verified returning 200 on 2026-09-12, and the alert's "suspect Hyperdrive credentials" line is conditional on the failing target actually reaching Postgres — an OG-image Worker outage no longer points the responder at a database incident document.

    Dokumentasi

    • factory: apps/factory/CLAUDE.md gains the health watch — its invariant, its three data sources and the three traps already sprung once during its construction (analytics sampling, blindness ageing into a false alarm, and the window/threshold contract). docs/incident-2026-08-07-postgres-unreachable.md §7.6 described a two-target Postgres probe that no longer exists; both of its open follow-ups are now marked closed or partly closed, with what closed them.
  4. v0.1.6

    Detail

    Fitur Baru

    • api: patient-record access was auditable in principle and unrecorded in practice. auditedRead now wraps the 28 "open one record" procedures across pasien / rme / gizi / farmasi / lab / radiologi / igd / dokumen / kasir / kamar-bersalin, auditedExport wraps document download and signing with aksi=export, and requirePermission / requireRoles / requireAnyPermission append an aksi=denied row on every FORBIDDEN. The denied row is written on the root connection specifically so it survives the rolled-back tenant transaction that produced it.
    • server: the --> request line said what was called but never who called it, so a log could show an export without naming the exporter. createContext stamps actorTenantId / actorUserId / actorVia (session | platform | oauth | anonymous) after identity resolution and the logger appends tenant=<id> user=<id> via=<kind>. Ids and the credential kind only — never an email, and nothing at all when the route never resolved an identity, so the existing --> greps still match. /satusehat/* shares the resolver and is covered by the same line.
    • server: audit rows nobody reads are not detection. A new 20 * * * * cron scans the last 24 h of pengaturan_audit_log_entries per tenant × actor and writes abnormal_access_events for unusual_volume (≥150 access), after_hours (≥20 access between 22:00–05:00 WIB), bulk_export (≥3 export) and repeated_denied (≥10 denied). Thresholds are env-overridable (ANOMALY_*), one idempotent row per WIB window day, and the alert mail to platform and tenant admins is PHI-free.
    • api, dashboard-ui: the entitlement lock screen was last-writer-wins by construction. setFasyankesEntitlement / setUserEntitlement take a complete set — everything absent from activeIds gets locked — so a branch created after the admin's page loaded was locked without anyone choosing to, and no per-row check could see it because every id in the payload was valid, owned and within the limit. lockCandidates now returns an order-independent fingerprint of the rows it handed over; the mutation recomputes it under the advisory lock and answers CONFLICT when the set has moved.
    • ui: stacking order, animation duration, icon size and control height were the four things the mirror spec left every component to hardcode, which is how a dropdown ends up under a sheet with no way to find out but to open both. They are tokens now — --z-base…--z-tooltip, --duration-*, --icon-*, --control-height-* — and 31 new primitives (accordion, banner, breadcrumb, multi-select, pagination, stepper, timeline and the rest) replace the shapes that were being hand-rolled per page. /dev/design-system renders all of them at once, which is the only way a token change gets reviewed as a system.
    • dashboard-ui: six clinical display primitives (PatientIdentity, IdentifierDisplay, AllergyIndicator, ClinicalStatus, VitalSignDisplay, PatientSummaryCard) replace per-screen copies of the same identity line. patient-identity-header.tsx drops from 115 lines to 43 by calling them.

    Perbaikan

    • agent, ui: every TenantAgent registered the Agents SDK browser tools with no URL allowlist and no approval step, in the same turn that holds decrypted NIK and mother's name, and the chat rendered assistant markdown with allowedImagePrefixes: ["*"] and no CSP — the zero-click exfiltration channel publicly exploited in ChatGPT, Bard, Copilot and M365 EchoLeak. Browser tools are off this PHI surface, image and link prefixes are first-party only and fail closed when VITE_FILES_URL is unset, and the public GeneralAgent now needs a credential and has a budget instead of sharing the paid tenants' account-wide Workers AI cap. Latent, not live: apps/agent has never been deployed — fixed at full severity because enabling it is one variable.
    • api, server: a permission denial wrote its denied audit row on the root connection from inside tenantGate's still-open transaction — a second checkout from the same 10-slot pool. Past the pool size every holder waited for a connection only another holder could release: 12 concurrent denied calls deadlocked for the full 10 s connect timeout, returned INTERNAL_SERVER_ERROR to co-located requests, and wrote zero rows.
    • api: the step-up TOTP lockout (5 failures / 15 min) lived only inside the tRPC auth.stepUpVerify mutation. better-auth's own /two-factor/verify-totp and /verify-backup-code are mounted raw, and for a caller holding a live session the plugin sets isSignIn = false and skips its attempt counter entirely — a clean 401/200 TOTP oracle bounded only by an in-memory 3-per-10s-per-IP bucket.
    • api, dashboard: an unauthenticated join request stored an arbitrary NIK with no proof, and on approval the system silently reused any employee row whose NIK matched and whose userId was null, mailing the invite to the applicant's own address and mirroring the victim's NIP and name — with no email comparison at any step. Approval now states what it is about to claim and requires confirmation.
    • api: platform.tenants.setActive promised that freezing a tenant stops all staff access, but the only request-path reader of tenants.isActive was the join-code lookup. Session and OAuth resolution checked deletedAt alone and janjiPublik.resolveTenantId matched on tenant code alone, so a frozen tenant kept signing in, calling every procedure and MCP tool, and accepting public bookings that persist visitor PII. Soft-deleted patients could also still open episodes.
    • api: deriveNoRkmMedis stopped being a function of NIK when encoding NIK into plaintext no_rkm_medis was found to defeat the AES-GCM on patients.nik, but createPatientLocally still branched on nikDerived and answered a uq_patients_tenant_no_rkm collision with a terminal CONFLICT reading "No. RM bentukan dari NIK ini bentrok dengan pasien lain". The message asserted something untrue since the encryption fix, and it refused a registration the existing retry loop would have completed on its next iteration. The selftest was asserting the old 10-char NIK-derived shape, so it would have gone red against correct code.
    • server: the fault-diagnostics and auth warm-up work left uncommitted by the session that resolved the Hyperdrive credential outage — describeFault / logServerFault, the structured hono-fault line in app.onError, the better-auth $context warm-up awaited in loadApp, and a 60 s query_timeout on the pg pool so a torn-down socket can no longer hold a request open silently.
    • factory: R-2026-0825-76 held with "REM GAGAL" after all six 1 % deployments and nothing could say whether the token, the body or the network was at fault, because callApi discarded the HTTP status and Cloudflare's error envelope. Every refused or thrown call now lands in recorded as an apiFault and in Workers Logs as one factory-api-fault line carrying method, url, status, code and message.

    Miscellaneous

    • api, dashboard-ui: the OTP TTL, attempt cap and digit count were declared identically in auth-instance.ts, admin-auth-instance.ts and a third time in the login card's copy — three copies of a number the server enforces, and the two commonest errors on that screen are produced by exactly the limits it never named. packages/api/src/auth-otp.ts is the single declaration, kept dependency-free because a client component imports it. AUTH_ERROR_COPY gained the passkey codes, missing because the maintenance note named only the two-factor path and the passkey plugin had moved to its own package.
    • docs-gen: generate-api-pages.ts and generate-mcp-docs.ts each mixed router introspection, page-tree shaping and filesystem writes, so the shaping could not be tested without booting the router. api-nav.ts, mcp-pages.ts and docs-mdx.ts are pure and now carry the first tests this code has had; the generators drop to 76 and 72 lines. Additive-only as before — nothing under apps/server, apps/mcp or packages/api moved.
    • security: hono ^4.13.5 (catalog + override), qs >=6.16.0 and sharp >=0.35.4 floors leave bun audit clean; Dependabot covers the bun workspaces; a CycloneDX 1.5 SBOM (1450 components) and the gitleaks record (0 leaks over 1814 commits) are under docs/security, and scripts/harden/{cloudflare-apply,aws-apply,dr-drill}.sh script the live changes the session could not apply itself.
    • mcp, api: two stray blank lines in apps/mcp and one out-of-order import in identity-ingress.selftest.ts had left bun x ultracite check red on main.
    • the Cloudflare MCP server is registered in .mcp.json, and three route trees are regenerated for TanStack Start moving the config type off createStart.

    Dokumentasi

    • security: the run-4 audit artifacts — report, detail file, checklist and a schema-valid findings.json — plus the dashboard rendering for the new export and denied audit verbs. Two candidates were rejected or downgraded on evidence rather than kept for the count.
    • security: seven governance policies under docs/policies (information security, PDP with ROPA, NDA template and register, environment/data SOP, DRP/BCP at RPO 24 h / RTO 4 h, PIC + DPO appointment, incident escalation), a one-page network architecture, SECURITY.md and an RFC 9116 /.well-known/security.txt on the marketing site.
    • security: the scored assessment workbook is tracked at last — CLAUDE.md and every artifact under docs/security/ had pointed at it while it existed only on one machine.
    • security: the spec and plan for taking SATUSEHAT hardening to 3/3.
    • the spec and plan for Artifacts CI as a GitHub Actions replacement.
  5. v0.1.5

    Detail

    Fitur Baru

    • mcp: the original ten MCP tools and the remaining in-scope tRPC procedures live in one agent catalog with an envelope, visibility rules, and exact input schemas. list_actions accepts an optional fasyankesId. Marketing WebMCP tools stay off the OAuth catalog so an operator grant cannot see rekamedika.com primitives.
    • webmcp: dashboard and marketing register in-page UI primitives (native fill, portaled select, confirm-gate writes) and derive discovery cards from the catalog. Well-known agent, MCP, and OAuth documents are served on both apps; pasien_create publishes the real registration union instead of a flattened subset.
    • api: tenant agent-auth tables and an MFA-reset HTTP guard sit next to the existing session authority. Document upload is a sign-PUT-commit flow with partial file pickers. KPI reads split by permission instead of a single bundle.
    • dashboard-ui: a retryable session-failure screen replaces the infinite loader when auth.me or get-session faults. Sign-out is awaitable, visible, and revokes the server session before clearing the client. Gate-blocking identity reads and login actions have a deadline; resend is paced to the server's one-per-minute window.

    Perbaikan

    • auth: OTP send uses a rolling per-address window with distinct minute and daily refusal copy, so one clinic NAT no longer shares a 3/min bucket. Every better-auth request carries a timeout; 408 and 429 are never retried.
    • dashboard: a failed session asks for a retry instead of looking unsigned-in, and the session you are using is no longer offered for revocation. Admin and agent share the same failure and sign-out contract.
    • mcp: A2A tasks and WebMCP list_actions fail closed on envelope errors. Identity permissions are copied into OAuth grant props so a catalog tool cannot outrun auth.me.
    • webmcp: allOf on tool schemas is preserved, confirm-gate writes stay gated, and marketing duplicate tools are dropped. Union JSON Schema for pasien_create is passed through rather than collapsed.
    • satusehat: usage events strip query strings and mask 16-digit NIK runs so identifier searches never land in the billing log.
    • kasir: bills are unique per episode so two cashiers cannot open a second running bill for the same visit.

    Dokumentasi

    • auth: the login and session reliability design and implementation plan land under docs/superpowers/.
    • security: three audit runs plus the final gates checklist are checked in under docs/security/audits/.

    Miscellaneous

    • Independent identity reads (platform session and tenant session) resolve together instead of sequentially on every tRPC call.
    • better-* agent skills refreshed; break, explain-interface, and variant skills added.
  6. v0.1.4

    Detail

    Fitur Baru

    • terminology: normalized and globally seeded all six clinical ICD workbooks (18,543 ICD-10 e-klaim, 4,626 ICD-9-CM e-klaim, 4,497 ICD-MM maternal mortality, 476 ICD-PM perinatal mortality, 1,142 ICD-O-3 morphology with reconstructed slash notation and search aliases, and 401 ICD-O topography anatomical site codes). All catalogs are stored in the tenant-independent icd_catalog_codes table and served via trpc.terminology.icdCatalog.
    • dashboard: CPPT working diagnosis comboboxes, diagnosis amendment sheets, nutrition care diagnosis selectors with E40–E67 prefix filters, HIV clinical condition & visit pickers, and E-Klaim INA-CBG primary diagnosis selectors now read directly from the global ICD catalog with debounced search and asynchronous pagination.

    Perbaikan

    • rme: CPPT note creation now resolves ICD-10 code selections and authoritative display titles server-side from the global catalog rather than trusting client-supplied strings or requiring tenant-curated entries in Pengaturan Terminologi.
    • db: updated seedTerminologyMasters and the terminology import pipeline to be fully idempotent across all 29,685 global catalog records and refreshed the clinical foreign-key projection table icd10_codes with the complete 18,543 ICD-10 dataset.
  7. v0.1.3

    Detail

    Fitur Baru

    • satusehat: the 27-resource programme now carries the production builders, writers, readers, durable link/refusal state and correction or retraction paths implemented across its 53 audited units. The work covers patient, practitioner, organization and location identity; Encounter clinical children; medication and e-MAR; diagnostics; nutrition; documents; HIV; and the dependency ordering needed to publish related resources in one bundle. The committed walk and status artefacts retain the exact remaining staging gaps instead of collapsing INCOMPLETE, NOT_EXERCISED and quota-limited read-backs into a green summary.
    • satusehat: pharmacy, HIV and penunjang workflows now expose the identity and status of the resource that actually failed. Prescription documents carry their national number and DocumentReference verdict; shared HIV Observation builders retain lane attribution without leaking raw payloads; lab, radiology, dental, IGD and gizi seeds pass through the same writers used by operators rather than direct fixture inserts.
    • dashboard: SDM and Pengaturan are consolidated into permission-filtered, URL-driven tab systems. Legacy child routes redirect with replace, row details and create/edit flows are addressable full pages, browser Back restores the originating table state, and any role holding one reachable sub-tab permission can enter the parent module.
    • dashboard-ui: PageForm provides the full-page counterpart to SheetForm: a shared field registry, validity summary, inline error channel, pending-state controls and an optional sticky aside for the new SDM and Pengaturan workflows.

    Perbaikan

    • satusehat: the resource-link log now filters by sync status, local table and resource type, pages beyond the first hundred rows, orders ties by id, and uses one predicate for rows and count(*). The duplicate unreachable status reader is removed, so the route guarded and rendered by the product is the same route whose failure channel is tested.
    • satusehat: the branch closes the measured writer defects found during its walk runs: invented or wrong terminology, missing Encounter and practitioner references, stale child-resource identity, retractions that violated FHIR invariants, stubbed upstream responses persisted as real IHS IDs, direct seed paths that bypassed writers, and failure states that disappeared before an operator could act on them. Each correction remains paired with its focused regression or selftest and the status evidence that motivated it.
    • farmasi: the prescription DocumentReference ID was added to the batched status request but the SQL predicate still admitted only medication request and dispense links, so a real rejected prescription document always appeared as null. The query now keeps medication and document table/type identities paired and the pharmacy projection receives the persisted upstream error.
    • dashboard: /sdm and /pengaturan were still locked by one legacy permission even though their new tab models used any-of visibility. The rail and root page gate now derive from the complete tab permission lists, so cuti-only, mapping-only and audit-only roles can reach the content they own.

    Continuous Integration

    • check: the check chain now runs its linter, file-size and page-grammar guards, Wrangler/environment and workspace-dependency checks, three static migration invariants, and citation validation independently. Backend CI runs the standalone API, server and database selftests against migrated, seeded Postgres and MinIO, pins Bun 1.3.13, and preserves the single documented Hono audit waiver rather than hiding unexpected advisories behind it.

    Miscellaneous

    • graphify: Claude and Codex Stop hooks fingerprint non-Graphify changes, coalesce concurrent requests and update the knowledge graph asynchronously. The generated graph, report, labels, manifest and AST cache are refreshed only after the authored source settles.
    • mcp: the repository now carries an explicit empty MCP server map, making the repository-level default deterministic without embedding credentials or machine-local endpoints.
    • tooling: the repository vendors the unlazy completion gates and security-audit workflow used by its agents, alongside the schema migrations, generated Drizzle snapshots and rule catalogues required by the SATUSEHAT implementation.

    Dokumentasi

    • satusehat: executable P0–P9 plans, the 53-unit status register, dated revalidation evidence and the walk artefacts document both landed behavior and decisions deliberately left to staging or the owner. Citation checks pin dated claims to their measured commit so later line movement cannot silently rewrite the evidence.
    • dashboard: the Pencil design-system specification and the updated dashboard route, SDM, Pengaturan and layout contracts describe the URL, permission and full-page-form cutover that the implementation now enforces.
  8. v0.1.2

    Detail

    Fitur Baru

    • satusehat: a re-runnable revalidation sweep for all 27 resource types, replacing a matrix that was read off the code rather than measured from it. The driver is staging-gated and idempotent, the wire-artefact assembler records what actually went over the wire, and a request-ceiling guard stops a sweep from spending the shared sandbox quota it does not own. The first honest run accepted 36 of 37 artefacts; the 21 August sweep, recorded with its run id and per-check results rather than a summary, closed the last one
    • satusehat: the IGD referral lane is live. It shipped fenced behind SATUSEHAT_IGD_REFERRAL_LANE on 2026-08-17 with a single condition for flipping the default — a 201 in hand, not a corpus citation — because IGD bundles are atomic and one refused lane takes igd_triage and igd_transport down with it. Run CONF-1787261297481 passed 19/19 and the fence is retired. The hypothesis it was fencing is confirmed at the wire: OC000034 was always the right code, its bare system URI was the defect. The old lane note had concluded the opposite by comparing two dental codes that both went up under the long URI — an invalid comparison, now rewritten as PROVEN with its evidence attached

    Perbaikan

    • satusehat: every foreign national was recorded as an Indonesian citizen. The registration form captures WNI/WNA into a notNull column, but buildCreateInput never forwarded it while patientExtensions emitted the extension unconditionally with a hardcoded "wni" default. The chain was not broken; it was sending the wrong value
    • farmasi: every syrup, injection and ointment dispensed was published as a tablet. quantityUnit was pinned to "tablet" and mapped to the TAB code in v3-orderableDrugForm. The real unit was on the same row the whole time. There is deliberately no "unit" fallback in UNIT_CODINGS — publishing with no dosage-form coding is honest about not knowing, claiming tablet is not
    • satusehat: enteral caloric density published 1000x too small. UCUM cal is the gram-calorie, not the kilocalorie, so a 1.5 kcal/mL formula order went out as 1.5 cal/mL with the Quantity's own unit contradicting its code. The packages/db constant is corrected in the same commit — it is the SSOT that would have re-planted this in the next writer
    • satusehat: a well-formed but fictional KFA code published as a real drug with itself as its only active ingredient. assertKfaClass validated shape and class against a zod schema and never read kfa_codes, so 93999999 cleared both readiness and the publisher. Membership is what is checked, not active — a withdrawn drug is still the real drug of a historical prescription. Three fixtures are corrected alongside, and they are the EVIDENCE for this gap rather than a rebuttal of it: medication-bundle-order.selftest.ts asserted blocked.length === 0 while using an invented code, and was only ever green because the publisher never checked
    • satusehat: the same hole a second time, in the e-MAR lane. clinical-publish-medication-administration.ts carried a PRIVATE COPY of loadKfaDetail, so the same fictional code was refused when a pharmacist dispensed it and published when a nurse administered it — same drug, same code, two different answers. No compounding exemption here, unlike the dispensing lane, and the difference is deliberate: this lane has no branch that suppresses the code, so anything reaching it publishes
    • rme: free text, a TRM-001 reference and plain disease names reached Condition.code. recordCpptConditions INSERTED the tenant's typed code into icd10_codes immediately before writing diagnoses, so its NOT NULL restrict foreign key could never refuse anything — the writer manufactured its own FK target. Shape is what is checked, not membership: icd10_codes holds ~47 sample rows, so a membership test would kill nearly every CPPT. The letter class is the full A-Z rather than A-T/V-Z, pinned by its own test, because U00-U49 provisional assignments are genuinely in use (U07.1, COVID-19)
    • satusehat: a refused master publish vanished without trace. markLinkFailed had zero callers across all four master lanes and their tables sat outside STATUS_SPECS, so the failure resurfaced later as a 422 missing_reference naming the Encounter rather than the Location that never landed. Two lanes are deliberately left untouched with the reason stated rather than patched into a lie — employees never writes upstream, and ensureOrganization already has conscious handling
    • api: tenant purge died on oauth_applications, the table migration 0048 renamed. USER_ID_KEYED_AUTH_TABLES is a list of STRINGS fed to sql.identifier, so the rename did not fail to compile and check-types stayed 17/17 green; it surfaced only at runtime as 42P01, and because the sweep runs inside platformProcedure's single audited transaction it took the whole purge down with it, across 11 backend selftests including the blast-radius proof. oauth_refresh_tokens joins the list in the same pass: through 1.6 a refresh token was two extra columns on the access-token row and was deleted with it, but 1.7 gives it its own table, so without the line a purge leaves a live refresh token pointing at a deleted user — an orphaned credential that still mints access
    • auth: better-auth 1.7 locked every new user out of sign-in, and this is the one defect found by this consolidation rather than by the Band A plan. 1.7's reserveVerificationValue writes a DETERMINISTIC primary key — base64url(SHA-256("reserve:" || identifier)) — with forceAllowId: true, which bypasses the advanced.database.generateId: false setting the schema relied on to guarantee better-auth never writes a non-uuid id. revokeUnprovenAccountAccess takes that reservation on every email-OTP sign-in whose user row is still emailVerified = false, so against a uuid column it died with 22P02. The product is passkey-first with email OTP as the only other way in, and a new account starts unverified. Measured with one user row and emailVerified as the only variable: true returns 200 with three cookies, false returns 22P02. Four other OTP selftests stayed green because they happen to verify their users, which is exactly why it reached this far

    Miscellaneous

    • deps: workspaces.catalog grows from 13 to 31 entries and the root consumes catalog: like every other workspace, so the four entries that had drifted between the root and the packages cannot drift again. Root CLAUDE.md already documented "31 entries" and the esbuild 0.28.1 pin — this is the code catching up with a doc that had been describing it for some time. esbuild moves off ^0.25.12, which was a CEILING rather than a floor: an override is a pin, and that range was dragging wrangler's own 0.28.1 down to a bundler it never tested with
    • auth: better-auth 1.6.25 to 1.7.1. oidcProvider and mcp collapse into a single @better-auth/mcp plugin — 1.7 deleted both core plugins and @better-auth/mcp now IS @better-auth/oauth-provider — so the duplicate-/oauth2/consent workaround goes with them and there are no /mcp/* endpoints any more. Storage moves from three tables to seven: oauth_applications is RENAMED to oauth_clients so no client registration is lost, refresh tokens get their own table, and a consent row's EXISTENCE is now the grant. Tokens and client secrets are stored hashed, so context.ts hashes the presented bearer before lookup — matching raw would match nothing and read as "unauthenticated" against a green build
    • dashboard-ui: @tanstack/react-table v8 to v9. v9 parameterises every table type by its feature set (ColumnDef<TFeatures, TData, TValue>), which would have rewritten ~90 call sites. data-table/table-features.ts registers the one feature set and re-exports the types already bound to it, so a call site keeps writing ColumnDef<Row>; column-meta.ts stops being a declare module augmentation and becomes the plain interface v9 reads off the feature set
    • satusehat: the unattested-code baseline tightens from 74 to 72. Both removals are the ratchet closing, not loosening: moving the shape predicates into gizi-terminology.ts left 169741004 and 410177006 attested, and the gate fails on a stale baseline entry precisely so a code that stops being unattested cannot keep its grace
    • check: the citation gate is wired into bun run check and into CI. It was written for Fase 1 and then connected to nothing — not the check chain, not a hook, not CI — so when a Fase 2 commit moved three symbols the chassis went red and stayed red for a whole phase with nobody watching. A stale-citation strip path is closed in the same pass, along with the duplicated stale citations it was hiding

    Dokumentasi

    • the 27-resource revalidation matrix, its Fase 1 design and plan, and the audit chassis behind them, with four pre-flight rulings applied. Several corrections are to this repo's own prose rather than to code: six false claims in the matrix, two cross-references pointing at empty sections, an inflated blocker count, and a false exhaustiveness claim reconciled against the section that contradicted it
    • audit: G62 is corrected — the danger is a suite that is green, not one that is red. The classified gap list is recorded as the Fase 2 gate, and cells whose reason Band A closed are re-derived rather than left asserting a state that no longer holds
  9. v0.1.1

    Detail

    Fitur Baru

    • satusehat: a staging conformance harness driven by the production builders and publishers rather than pure mappers or hand-written payloads, with a GET read-back after every POST, writing docs/audits/conformance-matrix.json. The audit it replaces hand-wrote both payloads in its dry run, so it could never prove localToFhirLocation or the production Encounter builder at all; the first honest baseline from production code paths was 9 PASS / 6 FAIL / 12 NO_WRITER
    • satusehat: a staging lease, because the sandbox quota is per-credential and not per-process — three measurement rounds were lost outright to QuotaViolation, one running strictly serially, so per-agent pacing could never have fixed it. staging-lease.ts is an O_EXCL lockfile with a heartbeat and takeover of a provably-dead holder, taken by skipUnlessStagingConfigured so no selftest has to remember it, plus a bunfig.toml preload that refuses any sandbox request from a process holding no lease. The three sweeps after it ran 287, 75 and 331 upstream calls with zero quota violations
    • satusehat: every FHIR resource type in the integration publishes — the harness closed at 35 resources, 35 PASS, 0 NO_WRITER. The penunjang chain (ServiceRequest, Specimen, DiagnosticReport, ImagingStudy) POSTs and reads back with the lab release chain and order sequencing behind it, and KFA/KPTL is modelled structurally (base_code, modifier_path, has_wildcard) instead of as an opaque string. Specimen was the last NO_WRITER and had three independent blockers, so fixing any one alone changed nothing: no UI field sent the specimen block, the catalogue offered a single option while requireSpecimenType refused everything else, and the conformance case put a SNOMED procedure code in the Specimen.type slot
    • satusehat: writers for the asuhan resources — CarePlan, FamilyMemberHistory, QuestionnaireResponse and the general Condition — with the six mandatory fields staging enforces, each verified individually. Rule 10328 (CarePlan.description) was missed by the audit because 10330 and 10382 mask it until both are supplied. CarePlan.description is text rather than varchar(512), since CPPT assessment and plan are unbounded and the display column would have truncated the backfill
    • satusehat: MedicationAdministration end to end with an e-MAR sheet — the audit's top patient-safety gap, since there was no legal record of drug administration in the product or the national record. The mapper was rewritten rather than patched (the old one emitted no dose, no route and no request); dose is a coded Quantity and rate a full Ratio, so 500 mL over 8 h keeps the ordered volume a nurse titrates against instead of collapsing to "62.5 mL/h". MedicationStatement lands as a reconciliation opening on what the pharmacy actually dispensed, carrying each drug's status — a bare drug name for a course the patient quit three months ago is more dangerous than the em-dash it replaced
    • satusehat: Medication publishes as a standalone resource for Rawat Jalan, Rawat Inap and IGD, and contained only for Farmasi. It had never once appeared in the transaction log, and because contained is valid FHIR staging never returned a 400 and nothing looked broken locally — what differed was the content of the national record
    • satusehat: Immunization (including the kader report variant) and EpisodeOfCare publish, with episode closure via PATCH. Immunization corrected our own ground-truth doc: encounter had been inferred optional from a 5-of-11 frequency in the official examples, and staging enforces it (rule 10293) — frequency in the corpus is not the enforced profile. hiv_episodes was renamed to episodes_of_care rather than given a rival table, preserving every row, policy and FK, because a TB-SO episode living in a table called hiv_episodes is both a misnomer and a PHI mislabel
    • satusehat: gizi assessments reach final on staging, which had never once been true in this repo — markGiziFinal sat after a loop that always threw, so the "Terpublikasi" StatCard had never left 0. Its Questionnaire canonicals were invented (rule 10169) and are now the real Q0014 / Q0024 / Q0025, each traced by node path to the official IGD collection
    • satusehat: terminology attestation refuses a code that fails its own system's check digit — LOINC mod-10 or SNOMED Verhoeff — at write time and again in the mappers, resource-scoped so one bad diet code blocks its row rather than the whole visit. The old validator was a /^\d{6,18}$/ shape check that accepted 437651000124103 and 999999999999 alike. It came up red on 20 constants already in the repo: 437421000124108 "Renal diet" is the valid Diabetic-diet code with its check digit edited from 5 to 8. All 20 were refused and none replaced, because the official corpus attests no code for those concepts and coding IMD as "nutrition education" would be a real code on the wrong concept
    • errors: a typed catalogue of the 657 official Kemenkes rule numbers, keyed on (rule, path) rather than the number alone — the numbers are not unique (558 distinct across 657 entries), and rule 10382 means both CarePlan.author and Observation.referenceRange.low.code, so a number-keyed lookup would answer a missing author with UCUM unit guidance. Waves 9 and 10 ran in parallel and each built its own catalogue; the two had already diverged, and the dashboard copy — the one that actually rendered — did not normalise invisible characters, so the word joiner in rule 10435's path silently dropped one of its two published code-system bindings. 900 duplicate lines deleted and the card re-pointed at @rekamedika/errors
    • dashboard-ui: the SATUSEHAT error card arrives by construction on 116 of 116 sheet mounts, up from 5 — SheetForm reads the failing mutation and renders the card itself. Rules whose workbook row has an empty description resolve with guidance: null rather than being suppressed, so the card never claims a real rule number is unknown and sends support hunting for the wrong remedy
    • satusehat: the operator surfaces the integration needs — an MPI candidate flow for patients without a NIK, practitioner resolve scoped to employees, an Organization/Location master-data panel and a KFA picker sourced from kfa_codes — plus the product wiring that lets a chain complete rather than a resource merely map: radiology acquisition and result capture, Farmasi prescribing-doctor and kunjungan pickers, and allergy capture from a seeded KFA allergen catalogue. Rule 10078 makes AllergyIntolerance.code mandatory, and until allergy_intolerances.kfa_code existed every drug allergy went out as code: {"coding": [], "text": "Alergi Amoksisilin"} — an empty repeating element, which is not a code
    • satusehat: patient demographic edits reach the national MPI — pasien-update-service.ts carried no SATUSEHAT reference at all and PATCH /patient/:ihsId had no client caller, so a clinic could correct a misspelt name and the MPI kept the old one, silently, forever. The route is gated on pendaftaran.pasien.update rather than integrasi.satusehat.update: ROLE-004 Petugas Pendaftaran, the exact role that corrects a name, holds only the former, and a 403 never enters patchPatientDemographics, so mpi_last_error would never be written and the divergence would go silent again one layer up
    • satusehat: amendment (PUT) legs are measured end to end across fifteen resource types. A 200 on the PUT used to be the whole proof, which cannot tell an accepted edit from an ignored one; every instrument now asserts the new value is present upstream, the old value is gone, and a republish with nothing changed emits zero upstream writes. Coverage is a checked inventory rather than a claim — publish-lanes-data.ts declares one lane per (resourceType, localTable) builder-unit and publish-lane-inventory.test.ts fails when a builder appears that no lane declares, which is what turned "Observation COMPLETE" into 1 of that resource's 13 builder-units
    • dashboard: the publish-status surfaces say which row failed, not just that something did. ChildStatusList merged every local table of one resourceType into one group and numbered rows by array index, so a rejected dental_education Procedure rendered "Tindakan 2"; rows are named from their lane through satusehat-lane-labels.ts, joined to the server's STATUS_SPECS by a set-equality test in both directions. The blocked[] list has one renderer shared by the IGD sheet and the encounter panel and prints the lane behind the localId — the three IGD Observation lanes are refused for the same reason and all report resourceType: "Observation", so three identical "Hasil pemeriksaan: Patient IHS belum tersedia" lines were the only trace they left anywhere

    Perbaikan

    • satusehat: the live defects the standing audit never saw — fhir-clinical-dispense.ts emitted a literal {"reference":""}, a 400 for any dispense without an encounter, and hiv/fhir-risk.ts emitted "Encounter/undefined"; a false missing_terminology blocker rejected every encounter publish containing a UI-created prescription; an unresolved Composition DPJP author blocked 100% of resumes; FamilyMemberHistory was coded outside SNOMED (rule 10707) and MedicationStatement used a hyphenated slug where the registered one is medicationstatement (rule 10445). The placeholder terminology is purged — KPTL-0001, free-text tablet, and 108252007 mis-systemed both as a v3-RoleCode relationship and as a DICOM modality
    • satusehat: updates go out as standalone PUTs. Staging rejects a PUT entry inside a transaction Bundle in every form, while a standalone PUT /Encounter/{id} is accepted and genuinely updates — so once every published encounter satisfied changedSinceSync, publishKunjungan was assembling a Bundle staging refuses wholesale and taking down every child in the visit with it. Re-publishing an unchanged encounter also converges now: the drift check compared against the upstream echo, which carries no section, so Composition and DocumentReference re-PUT on every publish. Mechanical extraction of the official corpus showed two further payload faults — encounters.status_history was a write-never column, so a discharged visit reached the national record as statusHistory:[{finished}], and outpatient encounters emitted no ServiceClass extension though 48 of 54 official payloads carry one
    • satusehat: the staging 400s blocking publication are cleared. MedicationRequest.requester was hardcoded null, aborting the whole visit Bundle under rules 10455 and 20013; MedicationDispense inverted whenPrepared/whenHandedOver for any hand-over time in the past (all three existing tests hand-wrote both timestamps already correctly ordered, so nothing in the repo could see it); Patient create omitted the mandatory address whenever the patient had no primary address row; and validateAddressCodes never read administrativeAreas.level, so it accepted province code 11 as a villageCode. DiagnosticReport codes route through the KPTL→LOINC companion, since staging binds ServiceRequest.code and DiagnosticReport.code to different value sets and the same code is accepted on the order and rejected on the report
    • satusehat: readiness refuses before sending, and by blast radius rather than fatally in every case. The reported harm was an already-published visit that could never receive its diagnoses because one employee row lacked an IHS. Withheld rows are seeded into blocked[] before planning, never after assembly, since a planned row has already registered a Bundle sibling and removing its entry later would leave a dangling urn:uuid. An Encounter with no diagnosis now fails before it is sent: staging accepts the create and rejects the update (rule 10457), so nothing failed until a visit was closed — and IGD routinely has no published Condition, while readiness was answering {ready:true, blockers:[]} for the exact encounter staging rejects and the rail rendered "Terkirim" over a link whose sync_status was failed
    • satusehat: MPI-sourced data can no longer overwrite what the clinic knows. sync-patient.ts dropped redacted names but sync-patient-secondary.ts wrote MPI values into patient_addresses, patient_telecoms and patient_contacts behind a truthiness check only, and a mask is perfectly truthy — every one of those blocks is DELETE-then-INSERT, so a masked value did not merely fail to enrich, it replaced a real phone number, address or emergency contact outright. If any value in a collection is redacted the whole collection is now skipped. POST /patient, PATCH /patient/:ihsId, search-mpi-candidates and every Practitioner route carried no permission check, so any authenticated user of any role could create or amend a record in the national MPI
    • satusehat: describeFault redacted faultstring but spliced errorcode in raw, so when Apigee flattens fault.detail to a string the SATUSEHAT_CLIENT_ID reached the error card, the copy-detail clipboard and satusehat_resource_links.last_error
    • satusehat: two ways a national record could be left in a state nothing local knew about. A Composition whose post-create read-back failed kept literal urn:uuid: section entries forever while all three drift signals reported healthy, because linkBodyFor omitted sentReferences — the in-code comment claiming a re-publish repairs it was simply false, and the code, the comment and the affected links are all fixed. Separately, a refused POST /Patient could mint a national patient record and then throw it away: staging answers HTTP 400 with a *Patient* body carrying an IHS number that resolves upstream, and postFhir throws on any non-2xx, so persistPatient never ran. An adopted id that fails to persist now throws 502 adopted_patient_unpersisted carrying the IHS number so an operator can link it by hand
    • satusehat: the conformance harness reports statuses it actually measured. conformance-run.ts synthesised httpStatus: ev.created ? 201 : 200 and the prereq case emitted linkPersisted: true as a literal while calling neither the resolver nor the persister its buildPath advertised — one row was outright wrong and 34 more were misdescribed. A wire seam emits the real status from fhirFetch and statusSource distinguishes observed-write / observed-read / upstream-error / local-refusal / none. Quota is survived rather than reported through: organization-contact-update.selftest.ts asserted "every failure is a rate-limit refusal", which a run where the sandbox refuses every unit satisfies perfectly — the last such run scanned 155 units, updated 90, had 64 refused and reported ALL PASS, leaving 64 organizations on the old telephone number
    • satusehat: the encounter publish could be unwired without a single test going red. Twelve folds in clinical-kunjungan.ts and its siblings were deleted one at a time against the full suite; ten are killed by a test now and the two that stay silent are recorded rather than claimed. Rebinding postBundleAndLink to a stub was silent across full runs and tsc -b --force still exited 0, meaning no transaction Bundle would be POSTed at all and nothing in the repo would notice. The Bundle lane checker was lying in the other direction, reporting "Procedure lane → 0 matching entries" against a Bundle staging had accepted: checkResourceLanes matched lanes by identifier.value == localId, but localToFhirProcedure does not and must not stamp one, since the official corpus records Procedure as carrying no identifier across all 39 examples
    • rbac: the clinician who captured the data could not see whether it reached SATUSEHAT. Every per-encounter status surface pre-gated on integrasi.satusehat.view, a sistem key held only by admin, auditor and Petugas Integrasi, so a dokter or perawat opening a patient saw nothing; widening that key was rejected because it also unlocks the tenant's SATUSEHAT credentials and live upstream Practitioner PII. rme.satusehat-status.view is the narrow clinical read added instead, with useCanAny mirroring the server's requireAnyPermissionRest so a screen and its endpoint answer the same question
    • db: an encounter that had ever had a dose charted or a drug dispensed could not be deleted. encounters → medication_requests is CASCADE, but medication_requests → administrations and → dispenses were both RESTRICT, so the cascade hit the restrict and the statement aborted as a raw Postgres 23503, stranding the tenant-purge and soft-delete paths. administrations.medication_request_id is now ON DELETE SET NULL — the dose keeps patient, drug code, coded dose, performer, status and effective period, and only the pointer at a row that no longer exists is cleared. Dispenses cascade instead, because that table has no patient_id and no encounter_id, so an orphan would name no patient, no visit and no prescriber
    • db: the seed produces a database that can actually publish. db:seed was dead on any machine that had run the conformance harness, and the sweep never covered clinical_medication_administrations. Practitioner IHS ids were written as DEMO-IHS-0001, which fails IHS_REFERENCE_ID (^[A-Za-z0-9]{8,12}# Changelog All notable changes to Rekamedika are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). > **Maintained by hand since 2026-07-21.** Entries were previously generated > from [Conventional Commits](https://www.conventionalcommits.org) by > release-please, which has been removed. Add a new section yourself when you > cut a release, and bump .release-please-manifest.json to match — the > /changelog page and the version badge on the marketing site are built from > these two files (apps/web/vite-plugins/app-release.ts). Conventional > Commits are still enforced by commitlint, they just no longer drive the > version automatically. ) on both length and character class — the pattern requireDispensePerformer enforces — so on a correctly seeded database every clinical employee was invisible to the pharmacist picker and a hyphenated id could never have published either. Local runs never saw it because a long-lived dev database has hand-made employees. db:seed is onConflictDoNothing, so the repair reaches a fresh database only; existing environments need their DEMO-IHS-*` rows updated by hand
    • farmasi: every stok-opname adjustment made through the UI silently recorded a zero variance. The "Item" picker was fed from the curated Terminologi master, whose labels are "<kode> - <display>", while farmasi.createOpname resolves the system stock by prefix-matching the submitted item against farmasi_stok_menipis.obat — no label that picker could produce was ever such a prefix, so stokSistem always fell back to stokFisik, the selisih was always 0, and the dual validation the sheet advertises ("Selisih terhadap stok sistem memicu validasi ganda") could not be triggered at all. The field now reads the pharmacy's own vocabulary (useObatItemOptions, the distinct item names already on the low-stock snapshot and opname history), the same idiom the Depo picker beside it already used
    • sdm: the pegawai role picker wrote an RBAC slug into the free-text display column. sdm_pegawai_pegawai.role is the display jabatan — the schema says so, and all eight server-side consumers only ever SELECT it to render — but the picker submitted role.slug, so a UI-created pegawai read "pendaftaran" in a column where every seeded row reads "Petugas Pendaftaran", and aturRole echoed the stored value into a toast that told an Indonesian-speaking clerk "Role Budi kini rawat-inap." The picker now submits the role's nama; the RBAC grant is unchanged, since it runs through role_akses and inviteUser
    • antrean: a walk-in issued from the Alur Kedatangan page did not appear on it. TiketManualSheet invalidated only antrean.list, which backs the operator console and the boards, while the page reads its roster, hero and lane ranks from kedatangan.alurHariIni — so the freshly issued ticket stayed invisible on the very page that issued it until an unrelated refetch happened to land
    • db: the drizzle snapshot chain had drifted three migrations behind the schema. db:generate diffs the declared schema against the newest snapshot in meta/, and drizzle-kit only writes a snapshot for migrations it generates itself — 0044/0045/0046 were hand-written, so the newest stayed at 0043. Nothing was red, because db:migrate reads _journal.json rather than the snapshots and 47 of 47 applied cleanly everywhere; the cost was deferred onto whoever next ran db:generate, which re-discovered 0045's source_dokumen_id and 0046's period_start NOT NULL as if they were new and re-emitted both verbatim, failing on any database already at 0046 with column already exists. 0047 keeps the snapshot that diff produced and drops its SQL, since the DDL needs no re-run in either direction
    • docs-gen: scan helper-registered SATUSEHAT routes, and report every gap at once. Eight publish endpoints were wrapped in a publishRoute(...) helper while ROUTE_REGISTRATION only matched a direct satusehat.<method>("<path>" call, so they were live in the Hono route table and absent from the scan — exactly the mismatch buildSatusehatDocument exists to refuse. The fix is in the generator rather than in routes-clinical.ts, because docs-gen must read apps/server and never require changes in it, and the check now collects all missing routes and throws once with the list rather than inside the loop. With the scan fixed the in-repo OpenAPI copies were regenerated for the first time in twenty waves: purely additive, satusehat.json 45 → 56 paths and trpc.json 481 → 510

    Dikembalikan

    • restore 118 design specs deleted by an over-broad git add -A. Every earlier wave staged an explicit file list that excluded the documentation deletions sitting in the working tree from unrelated sessions; one wave swept them all in, including docs/e2e-ai.md, which the root CLAUDE.md still links to as the specification for the optional Midscene vision suite. Whether those docs should go is a decision for whoever deleted them, not a side effect of a staging command; the wave's own additions are untouched

    Continuous Integration

    • ci: the check job actually checks. The root type-check gate reported "12 of 12" while silently skipping six workspaces, including the entire tRPC surface, and now covers 17 of 17. check-types runs with --concurrency=2: the job had failed four consecutive runs with no error TS line anywhere in the log, only a SIGKILL at 3m57s with 9 of 14 turbo tasks done, because turbo fans tsc -b over every workspace at once and the peak takes an ubuntu-latest runner down — leaving the secret scan, bun audit, the SBOM, the unit suite and build with zero signal behind a plain "failure". With the cap the job reached the error the kill had been hiding since the initial commit: apps/web/src/routeTree.gen.ts has been gitignored while all five sibling apps commit theirs, and every other error in that log cascaded from the missing route tree
    • satusehat: the staging-touching selftests skip on a non-staging target instead of failing. CI globs every *.selftest.ts with SATUSEHAT_BASE_URL pointed at .invalid, so suites that write to the live sandbox threw there, and bundle-scenario.selftest.ts had backend-suite red on every push and paging the owner through notify-failure — the exact gate-that-can-only-fail class the rail-bringup postmortem is about. One shared staging-skip.ts replaces the per-file copies, and the two layers are not redundant: the env pre-check skips when there is no sandbox to measure against, while assertStagingTarget still refuses — never skips — a tenant whose stored credentials point elsewhere, since that means a write suite was aimed at production
    • test: the committed gates that proved nothing. Four were red and owned by nobody, including scenario-modules, which asserted DocumentReference[0].context.related is a Composition — something the Farmasi module never produces, since its only document is a prescription whose related is a MedicationRequest by design. Two more were green on every laptop and red on every CI run, both asserting against ambient environment state instead of a fixture they own. Eleven builder tests failed only when run together, because bun test shares one process and executes every file's top-level mock.module before running any test; registration happens once in clinical-shared-mock.testkit.ts now. The measurement rule that fell out of it is worth more than the fix — bun test prints 0 fail while tests error out and never run, so trust Ran N and the exit code, never the fail count
    • test: the 13 standing e2e failures are closed, and the suite is green. They predate this release — 12 of the 13 fail identically on e77c5556, where CI's own e2e job was already red at 41 failed / 224 passed — and the split is what matters: four were real product defects (the three above plus the queue-roster refresh), and the rest were assertions that had drifted from deliberate product changes nobody re-ran the suite after. Two renamed chart headings on /laporan, a row click that became a route navigation instead of a read-only sheet, a branch switcher that moved from the navbar to the sidebar, a cancel toast that dropped its Task ID marker (still set server-side, still proven in antrean-rbac.selftest.ts), staff pickers that migrated from free text to live rosters. One was a plain locator bug: the rawat-inap round-trip reached its delete step through .first(), and since a discharged row need not sort first, it was one assertion away from deleting a different patient's admission and reporting success. Nothing was fixed by weakening an assertion
    • test: wire evidence must name a driver a reader can re-run. docs/audits/gate3-production-walk.json was the sole proof behind several resources and named a *.gate.check.ts that appears in no commit on any ref. audit-evidence-provenance.test.ts runs in the check job and rejects any artefact asserting upstream results without a provenance block naming a git-tracked instrument, resolved with git ls-files --error-unmatch rather than existsSync — the latter would have been green on the authoring agent's machine, the exact asymmetry that let the untracked driver through

    Dokumentasi

    • four ground-truth references extracted from the official Kemenkes Postman collections (~900 payloads) — the identifier map, the required-field shapes, the diagnostic value set and the flow contract — each correcting the standing audit, alongside a production-mapper baseline measured from the code rather than read off it
    • the conformance figures are recorded in a form that is comparable across waves. The harness headline folded EpisodeOfCare and Immunization into the core number even though conformance.ts asserts five times that they are reported separately; the comparable figure is core-27 at 26 PASS / 0 FAIL / 1 NO_WRITER. The end-to-end gate answers a stricter question than the harness, so gate 5 records 3 of 27 resource types and, counted properly for the first time, 14 of 53 core builder-units — the figure keeps falling because the standard keeps tightening, not because the code regresses, since the old headline counted resource types and one proven builder could carry "Observation COMPLETE" for its other twelve. docs/audits/satusehat-gizi-terminology.md §4 is corrected in the same spirit: 409063005 is attested on ServiceRequest.category, not on .code, because a code is only ever attested for an (element, code) pair

    Miscellaneous

    • errors: packages/errors/src/catalog.ts splits into a catalog-lookup.ts sibling without weakening any rule — a four-line catalog addition pushed it to 503 lines, over the hard 500-line cap, and because bun run check is one && chain that masked gates 3 through 7 entirely and no lane reported it
  10. v0.1.0

    Detail

    Fitur Baru

    • ui: copyable error report on every error surface — boundaries, route errors, toasts, inline banners, empty states, and the agent chat all offer one "Salin detail error" action that puts a full diagnostic report on the clipboard: code, HTTP status, endpoint, page URL and route, the signed-in operator (name, email, user id, tenant, facility, roles), the last ten things they clicked, and browser/timezone. Labels only, never input values — a rejected patient form cannot put a NIK on the clipboard
    • api: gate refusals are now distinguishable. insufficient-scope, feature-disabled and payment-required were in the error catalog but nothing produced them; every gate threw a bare FORBIDDEN, so "ask your admin for access", "this feature is off for your facility" and "your subscription lapsed" all rendered as one generic denial

    Perbaikan

    • server: disable Hyperdrive query caching — it served a stale empty result to /two-factor/verify-totp, breaking MFA enrolment in production with "TOTP not enabled" for every user. /two-factor/enable reads auth_two_factor immediately before inserting the row, and Hyperdrive cached that read. tRPC was never affected (its resolvers run in a transaction, which Hyperdrive does not cache); better-auth's adapter queries are not
    • api: a 423 account lockout and a 406 both resolved to bad-request client-side — fromHttpStatus filtered on tRPC equivalence, which skipped every catalog entry that has none
    • api: resolveError ignored the RFC 9457 type and recomputed the slug from the coarse tRPC code, flattening every precise error back to its generic parent
    • api: map the remaining REST dialect codes (missing_encounter, validation_failed, invalid_date, missing_field, invalid_terminology) onto canonical slugs
    • dashboard: the MFA screen no longer shows better-auth's raw English "TOTP not enabled" — it says, in Indonesian, that the enrolment secret expired and the page needs reloading for a fresh QR

    Dokumentasi

    • Hyperdrive caching must stay disabled — recorded in the deployment runbook and beside the binding in apps/server/wrangler.jsonc, since caching is on by default and returns whenever the config is recreated
  11. v0.0.1

    Detail

    Fitur Baru

    • web: marketing site (landing, about, contact, legal, register) on TanStack Start, deployed as a Cloudflare Worker at rekamedika.com
    • web: editorial blog with tag filtering, reading time, table of contents, RSS, and per-page markdown twins
    • dashboard: product surfaces for registration, encounters, laboratory results, and FHIR Practitioner profiles
    • server: Hono/tRPC API worker with SATUSEHAT and BPJS integration paths
    • db: Drizzle/Postgres schema for the encounter spine, laboratory results, and the SDM (HR) lifecycle — contracts, leave, appraisals, offboarding
    • ui: shared @rekamedika/ui design system — Mintlify-inspired monochrome surfaces, single Mint Green accent, Inter type scale, pill controls, and square cards
    • mcp: remote MCP worker exposing the operator's AI agent tools
    • og: Takumi OG-image worker rendering per-page social cards at og.rekamedika.com

    Dokumentasi

    • Design system spec (DESIGN.md), deployment runbook, and security audit checked into the repository