Changelog
Catatan rilis Rekamedika
Riwayat rilis Rekamedika: fitur baru, perbaikan, peningkatan performa, dan pembaruan dokumentasi di setiap versi, diurutkan dari yang terbaru.
v0.1.9
DetailPerbaikan
- api: every admin console load dead-ended on "Sesi tidak dapat diperiksa".
platform.meanswered400 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 splitplatform.merides an unbatchedhttpLink, so that body is genuinely empty rather than{}; andrequirePlatformRoleread the raw input eagerly and unguarded, only to fill the audit row'stargetTenantId, which made tRPC's lazyreq.json()throw. The gate now tolerates an unreadable body with the same.catch(() => undefined)tenantProcedurealready puts on its own audit read, which is whyauth.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.tsdrives the real tRPC client link through the real fetch adapter into the real gate: red without the fix, green with it.
- api: every admin console load dead-ended on "Sesi tidak dapat diperiksa".
v0.1.8
DetailPerbaikan
- admin: completes the content-negotiation repair 0.1.7 landed for web, dashboard and tenant.
apps/adminwas left out because it carries noagentDiscoveryMiddleware— 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-HTMLAcceptfell straight through to the framework's 500. Verified before the change:curl -H 'Accept: application/json' https://admin.rekamedika.com/returned500 {"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.tsgains the matching guard, so removing the middleware now fails the same way removingcsrfMiddlewaredoes.
- admin: completes the content-negotiation repair 0.1.7 landed for web, dashboard and tenant.
v0.1.7
DetailFitur Baru
- factory: the release rail gated releases and nothing watched production. Between 2026-09-04 and 2026-09-11,
app.rekamedika.comreturned 500 on every SSR route for eight minutes,klinikfn24.comdid the same forty minutes later, andrekamedika-serverthrew on 941 of 2,260 invocations across five days — every one a cron, all Postgres28P01— and the owner was told none of it.src/health/adds a fourth report to the existing*/15tick: 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 withoutcome: "ok"— GraphQL reportederrors: 0for 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. RequiresZone Analytics:ReadonCLOUDFLARE_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
Acceptnames neither the wildcard nortext/htmlwith HTTP 500 and a fixed JSON body, so agents, crawlers, uptime probes and the Claude-in-Chrome webmcp bridge — which probes<origin>/mcpwithAccept: application/json, text/event-streamon 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 afternext()rather than checkingAcceptbefore it, because request middleware runs before route matching and cannot tell an unmatched path from a server route that negotiates its ownAccept(/llms.txt,/index.md,.well-known/*.json,blog/rss.xml).apps/adminand 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 — andreportUptimewrote that down asalerted, advancinglastAlertAt, so a lost alert suppressed its own retry for a full repeat interval.notify()now returns whether the message actually landed, Telegram'sokfield is the authority rather than the HTTP status, and every refusal is recorded asfactory-telegram-faultin Workers Logs. - factory:
UPTIME_TARGETSwatched 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.mdgains 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.
- factory: the release rail gated releases and nothing watched production. Between 2026-09-04 and 2026-09-11,
v0.1.6
DetailFitur Baru
- api: patient-record access was auditable in principle and unrecorded in practice.
auditedReadnow wraps the 28 "open one record" procedures across pasien / rme / gizi / farmasi / lab / radiologi / igd / dokumen / kasir / kamar-bersalin,auditedExportwraps document download and signing withaksi=export, andrequirePermission/requireRoles/requireAnyPermissionappend anaksi=deniedrow 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.createContextstampsactorTenantId/actorUserId/actorVia(session | platform | oauth | anonymous) after identity resolution and the logger appendstenant=<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 ofpengaturan_audit_log_entriesper tenant × actor and writesabnormal_access_eventsforunusual_volume(≥150 access),after_hours(≥20 access between 22:00–05:00 WIB),bulk_export(≥3 export) andrepeated_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/setUserEntitlementtake a complete set — everything absent fromactiveIdsgets 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.lockCandidatesnow 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-systemrenders 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.tsxdrops 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 whenVITE_FILES_URLis 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/agenthas never been deployed — fixed at full severity because enabling it is one variable. - api, server: a permission denial wrote its
deniedaudit row on the root connection from insidetenantGate'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.stepUpVerifymutation. better-auth's own/two-factor/verify-totpand/verify-backup-codeare mounted raw, and for a caller holding a live session the plugin setsisSignIn = falseand 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
userIdwas 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.setActivepromised that freezing a tenant stops all staff access, but the only request-path reader oftenants.isActivewas the join-code lookup. Session and OAuth resolution checkeddeletedAtalone andjanjiPublik.resolveTenantIdmatched 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:
deriveNoRkmMedisstopped being a function of NIK when encoding NIK into plaintextno_rkm_mediswas found to defeat the AES-GCM onpatients.nik, butcreatePatientLocallystill branched onnikDerivedand answered auq_patients_tenant_no_rkmcollision 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 structuredhono-faultline inapp.onError, the better-auth$contextwarm-up awaited inloadApp, and a 60 squery_timeouton 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
callApidiscarded the HTTP status and Cloudflare's error envelope. Every refused or thrown call now lands inrecordedas anapiFaultand in Workers Logs as onefactory-api-faultline 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.tsand 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.tsis the single declaration, kept dependency-free because a client component imports it.AUTH_ERROR_COPYgained 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.tsandgenerate-mcp-docs.tseach 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.tsanddocs-mdx.tsare pure and now carry the first tests this code has had; the generators drop to 76 and 72 lines. Additive-only as before — nothing underapps/server,apps/mcporpackages/apimoved. - security:
hono^4.13.5 (catalog + override),qs>=6.16.0 andsharp>=0.35.4 floors leavebun auditclean; Dependabot covers the bun workspaces; a CycloneDX 1.5 SBOM (1450 components) and the gitleaks record (0 leaks over 1814 commits) are underdocs/security, andscripts/harden/{cloudflare-apply,aws-apply,dr-drill}.shscript the live changes the session could not apply itself. - mcp, api: two stray blank lines in
apps/mcpand one out-of-order import inidentity-ingress.selftest.tshad leftbun x ultracite checkred onmain. - the Cloudflare MCP server is registered in
.mcp.json, and three route trees are regenerated for TanStack Start moving the config type offcreateStart.
Dokumentasi
- security: the run-4 audit artifacts — report, detail file, checklist and a schema-valid
findings.json— plus the dashboard rendering for the newexportanddeniedaudit 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.mdand an RFC 9116/.well-known/security.txton the marketing site. - security: the scored assessment workbook is tracked at last —
CLAUDE.mdand every artifact underdocs/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.
- api: patient-record access was auditable in principle and unrecorded in practice.
v0.1.5
DetailFitur 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_actionsaccepts an optionalfasyankesId. 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_createpublishes 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.meor 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_actionsfail closed on envelope errors. Identity permissions are copied into OAuth grant props so a catalog tool cannot outrunauth.me. - webmcp:
allOfon tool schemas is preserved, confirm-gate writes stay gated, and marketing duplicate tools are dropped. Union JSON Schema forpasien_createis 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.
- 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.
v0.1.4
DetailFitur 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_codestable and served viatrpc.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
seedTerminologyMastersand the terminology import pipeline to be fully idempotent across all 29,685 global catalog records and refreshed the clinical foreign-key projection tableicd10_codeswith the complete 18,543 ICD-10 dataset.
- 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
v0.1.3
DetailFitur 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_EXERCISEDand 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:
PageFormprovides the full-page counterpart toSheetForm: 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 andcount(*). 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:
/sdmand/pengaturanwere 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.
- 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
v0.1.2
DetailFitur 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_LANEon 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 takesigd_triageandigd_transportdown with it. Run CONF-1787261297481 passed 19/19 and the fence is retired. The hypothesis it was fencing is confirmed at the wire:OC000034was 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
buildCreateInputnever forwarded it whilepatientExtensionsemitted 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.
quantityUnitwas pinned to"tablet"and mapped to theTABcode inv3-orderableDrugForm. The real unit was on the same row the whole time. There is deliberately no"unit"fallback inUNIT_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
calis 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 ownunitcontradicting itscode. Thepackages/dbconstant 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.
assertKfaClassvalidated shape and class against a zod schema and never readkfa_codes, so93999999cleared both readiness and the publisher. Membership is what is checked, notactive— 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.tsassertedblocked.length === 0while 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.tscarried a PRIVATE COPY ofloadKfaDetail, 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-001reference and plain disease names reachedCondition.code.recordCpptConditionsINSERTED the tenant's typed code intoicd10_codesimmediately before writingdiagnoses, 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_codesholds ~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.
markLinkFailedhad zero callers across all four master lanes and their tables sat outsideSTATUS_SPECS, so the failure resurfaced later as a 422missing_referencenaming 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 —employeesnever writes upstream, andensureOrganizationalready has conscious handling - api: tenant purge died on
oauth_applications, the table migration 0048 renamed.USER_ID_KEYED_AUTH_TABLESis a list of STRINGS fed tosql.identifier, so the rename did not fail to compile andcheck-typesstayed 17/17 green; it surfaced only at runtime as 42P01, and because the sweep runs insideplatformProcedure's single audited transaction it took the whole purge down with it, across 11 backend selftests including the blast-radius proof.oauth_refresh_tokensjoins 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
reserveVerificationValuewrites a DETERMINISTIC primary key —base64url(SHA-256("reserve:" || identifier))— withforceAllowId: true, which bypasses theadvanced.database.generateId: falsesetting the schema relied on to guarantee better-auth never writes a non-uuid id.revokeUnprovenAccountAccesstakes that reservation on every email-OTP sign-in whose user row is stillemailVerified = 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 andemailVerifiedas 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.cataloggrows from 13 to 31 entries and the root consumescatalog:like every other workspace, so the four entries that had drifted between the root and the packages cannot drift again. RootCLAUDE.mdalready documented "31 entries" and theesbuild0.28.1 pin — this is the code catching up with a doc that had been describing it for some time.esbuildmoves off^0.25.12, which was a CEILING rather than a floor: an override is a pin, and that range was dragging wrangler's own0.28.1down to a bundler it never tested with - auth: better-auth 1.6.25 to 1.7.1.
oidcProviderandmcpcollapse into a single@better-auth/mcpplugin — 1.7 deleted both core plugins and@better-auth/mcpnow IS@better-auth/oauth-provider— so the duplicate-/oauth2/consentworkaround goes with them and there are no/mcp/*endpoints any more. Storage moves from three tables to seven:oauth_applicationsis RENAMED tooauth_clientsso 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, socontext.tshashes the presented bearer before lookup — matching raw would match nothing and read as "unauthenticated" against a green build - dashboard-ui:
@tanstack/react-tablev8 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.tsregisters the one feature set and re-exports the types already bound to it, so a call site keeps writingColumnDef<Row>;column-meta.tsstops being adeclare moduleaugmentation 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.tsleft 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 checkand 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
v0.1.1
DetailFitur 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 provelocalToFhirLocationor 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.tsis an O_EXCL lockfile with a heartbeat and takeover of a provably-dead holder, taken byskipUnlessStagingConfiguredso no selftest has to remember it, plus abunfig.tomlpreload 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 whilerequireSpecimenTyperefused everything else, and the conformance case put a SNOMED procedure code in theSpecimen.typeslot - 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.descriptionistextrather 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:
encounterhad 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_episodeswas renamed toepisodes_of_carerather than given a rival table, preserving every row, policy and FK, because a TB-SO episode living in a table calledhiv_episodesis both a misnomer and a PHI mislabel - satusehat: gizi assessments reach
finalon staging, which had never once been true in this repo —markGiziFinalsat 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 accepted437651000124103and999999999999alike. 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.authorandObservation.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 —
SheetFormreads the failing mutation and renders the card itself. Rules whose workbook row has an empty description resolve withguidance: nullrather 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 makesAllergyIntolerance.codemandatory, and untilallergy_intolerances.kfa_codeexisted every drug allergy went out ascode: {"coding": [], "text": "Alergi Amoksisilin"}— an empty repeating element, which is not a code - satusehat: patient demographic edits reach the national MPI —
pasien-update-service.tscarried no SATUSEHAT reference at all andPATCH /patient/:ihsIdhad no client caller, so a clinic could correct a misspelt name and the MPI kept the old one, silently, forever. The route is gated onpendaftaran.pasien.updaterather thanintegrasi.satusehat.update: ROLE-004 Petugas Pendaftaran, the exact role that corrects a name, holds only the former, and a 403 never enterspatchPatientDemographics, sompi_last_errorwould 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.tsdeclares one lane per(resourceType, localTable)builder-unit andpublish-lane-inventory.test.tsfails 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.
ChildStatusListmerged every local table of one resourceType into one group and numbered rows by array index, so a rejecteddental_educationProcedure rendered "Tindakan 2"; rows are named from their lane throughsatusehat-lane-labels.ts, joined to the server'sSTATUS_SPECSby a set-equality test in both directions. Theblocked[]list has one renderer shared by the IGD sheet and the encounter panel and prints the lane behind thelocalId— the three IGD Observation lanes are refused for the same reason and all reportresourceType: "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.tsemitted a literal{"reference":""}, a 400 for any dispense without an encounter, andhiv/fhir-risk.tsemitted"Encounter/undefined"; a falsemissing_terminologyblocker 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 ismedicationstatement(rule 10445). The placeholder terminology is purged —KPTL-0001, free-texttablet, and108252007mis-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 satisfiedchangedSinceSync,publishKunjunganwas 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 nosection, so Composition and DocumentReference re-PUT on every publish. Mechanical extraction of the official corpus showed two further payload faults —encounters.status_historywas a write-never column, so a discharged visit reached the national record asstatusHistory:[{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.requesterwas hardcoded null, aborting the whole visit Bundle under rules 10455 and 20013; MedicationDispense invertedwhenPrepared/whenHandedOverfor 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; andvalidateAddressCodesnever readadministrativeAreas.level, so it accepted province code11as avillageCode. DiagnosticReport codes route through the KPTL→LOINC companion, since staging bindsServiceRequest.codeandDiagnosticReport.codeto 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 danglingurn: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 whosesync_statuswasfailed - satusehat: MPI-sourced data can no longer overwrite what the clinic knows.
sync-patient.tsdropped redacted names butsync-patient-secondary.tswrote MPI values intopatient_addresses,patient_telecomsandpatient_contactsbehind 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-candidatesand 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:
describeFaultredactedfaultstringbut splicederrorcodein raw, so when Apigee flattensfault.detailto a string theSATUSEHAT_CLIENT_IDreached the error card, the copy-detail clipboard andsatusehat_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, becauselinkBodyForomittedsentReferences— 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 refusedPOST /Patientcould 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, andpostFhirthrows on any non-2xx, sopersistPatientnever ran. An adopted id that fails to persist now throws 502adopted_patient_unpersistedcarrying the IHS number so an operator can link it by hand - satusehat: the conformance harness reports statuses it actually measured.
conformance-run.tssynthesisedhttpStatus: ev.created ? 201 : 200and the prereq case emittedlinkPersisted: trueas 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 fromfhirFetchandstatusSourcedistinguishes observed-write / observed-read / upstream-error / local-refusal / none. Quota is survived rather than reported through:organization-contact-update.selftest.tsasserted "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.tsand 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. RebindingpostBundleAndLinkto a stub was silent across full runs andtsc -b --forcestill 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:checkResourceLanesmatched lanes byidentifier.value == localId, butlocalToFhirProceduredoes 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, asistemkey 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.viewis the narrow clinical read added instead, withuseCanAnymirroring the server'srequireAnyPermissionRestso 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_requestsis CASCADE, butmedication_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_idis 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 nopatient_idand noencounter_id, so an orphan would name no patient, no visit and no prescriber - db: the seed produces a database that can actually publish.
db:seedwas dead on any machine that had run the conformance harness, and the sweep never coveredclinical_medication_administrations. Practitioner IHS ids were written asDEMO-IHS-0001, which failsIHS_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.jsonto match — the >/changelogpage 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 patternrequireDispensePerformerenforces — 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:seedisonConflictDoNothing, so the repair reaches a fresh database only; existing environments need theirDEMO-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>", whilefarmasi.createOpnameresolves the system stock by prefix-matching the submitted item againstfarmasi_stok_menipis.obat— no label that picker could produce was ever such a prefix, sostokSistemalways fell back tostokFisik, 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.roleis the display jabatan — the schema says so, and all eight server-side consumers only ever SELECT it to render — but the picker submittedrole.slug, so a UI-created pegawai read "pendaftaran" in a column where every seeded row reads "Petugas Pendaftaran", andaturRoleechoed the stored value into a toast that told an Indonesian-speaking clerk "Role Budi kini rawat-inap." The picker now submits the role'snama; the RBAC grant is unchanged, since it runs throughrole_aksesandinviteUser - antrean: a walk-in issued from the Alur Kedatangan page did not appear on it.
TiketManualSheetinvalidated onlyantrean.list, which backs the operator console and the boards, while the page reads its roster, hero and lane ranks fromkedatangan.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:generatediffs the declared schema against the newest snapshot inmeta/, 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, becausedb:migratereads_journal.jsonrather than the snapshots and 47 of 47 applied cleanly everywhere; the cost was deferred onto whoever next randb:generate, which re-discovered 0045'ssource_dokumen_idand 0046'speriod_startNOT NULL as if they were new and re-emitted both verbatim, failing on any database already at 0046 withcolumn 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 whileROUTE_REGISTRATIONonly matched a directsatusehat.<method>("<path>"call, so they were live in the Hono route table and absent from the scan — exactly the mismatchbuildSatusehatDocumentexists to refuse. The fix is in the generator rather than inroutes-clinical.ts, because docs-gen must readapps/serverand 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.json45 → 56 paths andtrpc.json481 → 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, includingdocs/e2e-ai.md, which the rootCLAUDE.mdstill 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
checkjob 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-typesruns with--concurrency=2: the job had failed four consecutive runs with noerror TSline anywhere in the log, only a SIGKILL at 3m57s with 9 of 14 turbo tasks done, because turbo fanstsc -bover every workspace at once and the peak takes an ubuntu-latest runner down — leaving the secret scan,bun audit, the SBOM, the unit suite andbuildwith 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.tshas 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.tswithSATUSEHAT_BASE_URLpointed at.invalid, so suites that write to the live sandbox threw there, andbundle-scenario.selftest.tshadbackend-suitered on every push and paging the owner throughnotify-failure— the exact gate-that-can-only-fail class the rail-bringup postmortem is about. One sharedstaging-skip.tsreplaces the per-file copies, and the two layers are not redundant: the env pre-check skips when there is no sandbox to measure against, whileassertStagingTargetstill 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 assertedDocumentReference[0].context.relatedis 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, becausebun testshares one process and executes every file's top-levelmock.modulebefore running any test; registration happens once inclinical-shared-mock.testkit.tsnow. The measurement rule that fell out of it is worth more than the fix —bun testprints0 failwhile tests error out and never run, so trustRan Nand 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 owne2ejob 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 inantrean-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.jsonwas the sole proof behind several resources and named a*.gate.check.tsthat appears in no commit on any ref.audit-evidence-provenance.test.tsruns in thecheckjob and rejects any artefact asserting upstream results without aprovenanceblock naming a git-tracked instrument, resolved withgit ls-files --error-unmatchrather thanexistsSync— 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.tsasserts 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 onServiceRequest.category, not on.code, because a code is only ever attested for an (element, code) pair
Miscellaneous
- errors:
packages/errors/src/catalog.tssplits into acatalog-lookup.tssibling without weakening any rule — a four-line catalog addition pushed it to 503 lines, over the hard 500-line cap, and becausebun run checkis one&&chain that masked gates 3 through 7 entirely and no lane reported it
- 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
v0.1.0
DetailFitur 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-disabledandpayment-requiredwere in the error catalog but nothing produced them; every gate threw a bareFORBIDDEN, 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/enablereadsauth_two_factorimmediately 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-requestclient-side —fromHttpStatusfiltered on tRPC equivalence, which skipped every catalog entry that has none - api:
resolveErrorignored the RFC 9457typeand 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
v0.0.1
DetailFitur 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/uidesign 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