// (first-sign-in gate, Plan B), client Task 2 - the bundled legal copy the first-sign-in // gate shows, keyed by kind AND version. The server (AkashicAgreements/get.ts, via // useAkashicAgreementsService.getOutstanding) returns {kind, version, uri} and deliberately NO text, // so that changing legal copy is not a Lambda deploy. The text ships here, in the client bundle, // instead. // // THIS FILE IS A COPY, NOT THE SOURCE OF TRUTH. The real documents live in the akashic-docs repo - // a SIBLING repo, not this one: // - akashic-docs/legal/akashic-terms-of-service.md -> kind 'terms-of-service' // - akashic-docs/legal/akashic-privacy-notice.md -> kind 'privacy-consent' // There is no build step or CI link between the two repos. If someone edits those documents without // bumping their `version` frontmatter, this bundled copy silently drifts from the source with nothing // going red. The only thing standing between that and a false evidentiary record is version // discipline, enforced by resolveCopy() below and pinned by this module's own drift test // (agreementCopy.test.ts). // // THE DOCUMENTS BELOW ARE AUTHORED AS BLOCKS. THERE IS STILL NO MARKDOWN AND STILL NO RENDERER - // DO NOT ADD ONE. That remains a ruling, not a style preference. // // Spencer, 2026-08-12 (, finding B6): the gate was rendering the `.md` SOURCE to the patient - // literal `## 1. Acceptance`, `**Effective date:**`, `_[to be set]_` - inside a legal instrument that an // acceptance is then recorded against. Measured: 4,758 characters carrying ATX headings and `**`/`_`, // against 0 ``, 0 `

`, 0 `
  • ` in the DOM. // // He was offered a markdown renderer and REJECTED it. What that ruling buys, and the reason it is worth // more than headings: **there is no transform between the evidentiary record of what the patient agreed // to and what the patient was shown.** A renderer - even a sanitised, pinned one - puts a PARSER in that // gap: structure is INFERRED from syntax, so a mis-parse silently changes what the document appears to // say. For a legal instrument that gap is the whole risk. // // Spencer, 2026-08-13: the ruling held, and it left the document as ONE undifferentiated // text node - 14px/400 throughout, a section heading typographically identical to a sentence of body text. // A consent surface nobody can navigate is a weak place to assert "the patient was shown the terms". // He chose **structured blocks**, and the distinction from what he rejected is the entire point: // - a RENDERER *infers* structure by parsing syntax at display time - rejected, and still rejected; // - a BLOCK is *authored* structure. There is no syntax, nothing to parse, and nothing to mis-parse. // `{ type: 'heading', text: '1. Acceptance' }` renders as one element carrying that exact string. // **So the ban is unchanged and now has a positive form: never parse the text; declare the block.** // // AND THE CANONICAL STRING SURVIVES - this is what made the change safe to make at all. `deriveBody()` // reconstitutes the whole document from its blocks, and `agreementCopyFormat.test.ts` asserts the result // is **byte-identical** to the v0.1-placeholder text that acceptances were already recorded against. // **No version bump, and therefore no three-repo coordinated deploy** - proven, not argued. If that test // ever reddens, the document's TEXT has changed and a version bump IS required. It is not a formatting // nit; it is the tripwire. // // REFORMATTING IS A VERSION BUMP, NOT AN EDIT IN PLACE - which is why this file moved to // `v0.1-placeholder` and the SERVER constants moved with it (infra `utils/akashicTermsAgreement.ts`, // `utils/akashicPrivacyConsent.ts`) along with the docs frontmatter both transcribe from. An acceptance // already recorded against `v0.0-placeholder` refers to the text as it stood then; silently changing the // text under a version string is the drift this module's whole design exists to prevent. // Any skew between those deploys fails CLOSED (see resolveCopy) and blocks first sign-in for everyone // until both land. That is correct, and it makes a version bump a COORDINATED deploy. // // THE HANDOVER TO ED: his reviewed `v1.0` must arrive PRE-FORMATTED, not as markdown. If it arrives as // markdown it renders exactly as broken as the placeholder did - the defect was the FORMAT, never the // placeholder. The requirement is stated in the source documents' own "Notes for Ed" sections. // // THE ONE PROPERTY THAT MATTERS: the version a user READS must equal the version RECORDED. If the // bundled copy here were stale relative to what the server reports as current, letting the gate offer // Accept would file a record saying the user agreed to text they were never shown - worse than // blocking them. resolveCopy() therefore returns copy ONLY on an EXACT version match, and `undefined` // on any mismatch - wrong version OR unknown kind. Do NOT add a "closest version" fallback, a // default, or a lenient comparison here. Task 3's gate relies on `undefined` meaning "refuse to offer // Accept, show an error instead" - see task-2-brief.md. import type { AgreementKind, PatientAgreementKind } from '../../services/AkashicAgreementsService.tsx'; /** * A stretch of a block's text with its formatting. Held BESIDE the text rather than inside it, so the * canonical string stays derivable from `text` alone and formatting can never leak markup into it. The * runs of a block always join to exactly its `text`. */ export type InlineRun = { text: string; bold?: true; italic?: true; href?: string }; /** * One authored unit of a legal document. * * `runs` carries formatting BESIDE the text, never inside it - the moment a block's text carries * syntax, something has to parse it, and a parser between the record and the screen is precisely what * Spencer rejected. Four block types cover both documents completely; a fifth belongs on a card with a * reason, not in this union because a sentence looked plain. * * `text` may contain newlines. Those are the SOURCE document's hard wraps and they are load-bearing for * `deriveBody` - but a `

    ` collapses them, so the paragraph reflows to the reader's width on screen. * Both facts are true at once: the canonical string keeps its wraps, the patient sees flowing text. */ export type AgreementBlock = | { type: 'title'; text: string } | { type: 'heading'; text: string; runs?: InlineRun[] } | { type: 'paragraph'; text: string; runs?: InlineRun[] } | { type: 'list'; items: string[]; itemRuns?: InlineRun[][]; ordered?: true }; export interface AgreementCopy { title: string; blocks: AgreementBlock[]; /** The canonical document text, DERIVED from `blocks`. Never authored directly - see `deriveBody`. */ body: string; } /** * The whole document as one canonical string, rebuilt from its blocks. * * THIS IS THE EVIDENTIARY ANCHOR, not a convenience. A version identifies a document; this function is * what says, unambiguously, which characters that version consists of. `agreementCopyFormat.test.ts` * asserts its output is byte-identical to the v0.1-placeholder text that live acceptances were already * recorded against - which is the proof that moving to blocks did **not** change the instrument and so * needed no version bump. * * THE SPACING RULE IS PART OF THE DOCUMENT, so it is stated rather than tuned: a section heading gets * TWO blank lines above it, everything else gets one. That is not a preference - it is the rule the * existing documents already followed, which is how they round-trip exactly. Changing it changes the * canonical text of every version at once and will redden the byte-identity test. That is the test doing * its job, not an obstacle to route around. * * A list item's ` - ` marker lives here, not in the item text, so `

  • ` can carry its own marker on * screen while the canonical string keeps the exact bullet the accepted text had. An ordered list's * number lives here for the same reason: the canonical string keeps the exact marker the accepted text * had, and formatting (`runs`) is ignored entirely - it decorates `text`, it is never part of it. */ // The server accepts the same links when a document is published; the two rules must not drift apart. const MAX_AGREEMENT_HREF_LENGTH = 2048; /** A link a legal document may carry: https or mailto, at most 2048 characters, as the server allows. */ export const isSafeAgreementHref = (href: string): boolean => { if (href.length > MAX_AGREEMENT_HREF_LENGTH) return false; try { return ['https:', 'mailto:'].includes(new URL(href).protocol); } catch { return false; } }; /** Runs that join to exactly `text` and link only somewhere safe; absent runs show `text` itself. */ export const runsShowText = (text: string, runs?: InlineRun[]): boolean => !runs?.length || ( runs.map((run) => run.text).join('') === text && runs.every((run) => run.href === undefined || isSafeAgreementHref(run.href)) ); /** * Whether the screen will show exactly the text `deriveBody` reads. `runs` are drawn in place of `text`, * so a served document whose runs say other words would pass the body check while showing a different * instrument; this is the check that closes that gap. */ export const blocksShowTheirText = (blocks: AgreementBlock[]): boolean => blocks.every((block) => { if (block.type === 'title') return true; if (block.type === 'list') { return (block.itemRuns?.length ?? 0) <= block.items.length && block.items.every((item, j) => runsShowText(item, block.itemRuns?.[j])); } return runsShowText(block.text, block.runs); }); export function deriveBody(blocks: AgreementBlock[]): string { const render = (b: AgreementBlock): string => (b.type === 'list' ? b.items.map((item, n) => (b.ordered ? ` ${n + 1}. ` : ' • ') + item).join('\n') : b.text); return blocks .map((b, i) => (i === 0 ? render(b) : (b.type === 'heading' ? '\n\n\n' : '\n\n') + render(b))) .join(''); } // The version each bundled document below was transcribed from - MUST match the `version` frontmatter // of the corresponding akashic-docs file at the time of transcription. Both are `v0.0-placeholder` // today: neither document has been legally reviewed, and both still carry visible placeholder markers // in their body text (e.g. "_[to be set]_", "_[Liability cap... to be set by counsel.]_") - shipped // as-is, on purpose. When Ed's reviewed copy lands as `v1.0`, this map (and the body text below it) is // what gets bumped; until it is, a caller reporting `v1.0` correctly gets `undefined` here, never a // stale placeholder silently standing in for it. // The staff Terms have no bundled copy: they are shown only as the server serves them, never as patient text. const AGREEMENT_VERSION: Record = { 'terms-of-service': 'v0.1-placeholder', 'privacy-consent': 'v0.1-placeholder', }; // Transcribed verbatim from akashic-docs/legal/akashic-terms-of-service.md (body of the document, // i.e. everything under "# Terms of Service"). Excludes the file's frontmatter and its // " Notes for Ed (delete before publishing)" section - internal drafting notes, not document text. const TERMS_OF_SERVICE_BLOCKS: AgreementBlock[] = [ { type: 'title', text: `Terms of Service` }, { type: 'paragraph', text: `Effective date: [to be set on first real version] Version: v0.1-placeholder` }, { type: 'heading', text: `1. Acceptance` }, { type: 'paragraph', text: `By creating an account, signing in, or using Akashic Health Records ("Akashic", "we", "us"), you agree to these Terms. If you do not agree, do not use the service.` }, { type: 'paragraph', text: `If you are using Akashic on behalf of an organization, you represent that you are authorized to accept these Terms for that organization.` }, { type: 'heading', text: `2. What Akashic is — and is not` }, { type: 'paragraph', text: `Akashic is a health record and coordination platform. It stores health information, and lets you and the people and organizations you choose view and work with it together.` }, { type: 'paragraph', text: `Akashic is not a healthcare provider. We do not practice medicine, provide medical advice, diagnose, or treat. Nothing in the service is a substitute for professional medical judgment. Any clinical information in your record is authored by the clinicians and organizations who entered it, not by us.` }, { type: 'paragraph', text: `Akashic is not for medical emergencies. If you are experiencing a medical emergency, call your local emergency number or go to the nearest emergency department. Do not use Akashic to seek emergency care, and do not rely on it to deliver an urgent message to a clinician.` }, { type: 'heading', text: `3. Accounts and eligibility` }, { type: 'paragraph', text: `You must provide accurate information when creating an account and keep it current. You are responsible for activity under your account and for keeping your credentials secure. Tell us promptly if you believe your account has been accessed without your permission.` }, { type: 'paragraph', text: `Accounts for minors, and accounts accessed by a parent, guardian, or other authorized person on someone else's behalf, are governed by the proxy and guardianship rules described in the service and by applicable law.` }, { type: 'heading', text: `4. Your record, and your control over it` }, { type: 'paragraph', text: `Your health record in Akashic belongs to you. Organizations and practitioners participate in it; they do not own it.` }, { type: 'paragraph', text: `You control which organizations and individuals may access your clinical information, and you may change or withdraw that access. Withdrawing access is not retroactive: it does not erase what was previously and lawfully shared, and it does not remove records an organization is independently required to keep.` }, { type: 'heading', text: `5. Organizations, practitioners, and their own obligations` }, { type: 'paragraph', text: `Organizations using Akashic are separately responsible for their own legal obligations to you, including their own privacy practices and record-keeping duties. Where we handle protected health information on an organization's behalf, we do so under a Business Associate Agreement with that organization.` }, { type: 'paragraph', text: `Information an organization or practitioner enters is theirs; we do not verify its clinical accuracy.` }, { type: 'heading', text: `6. Acceptable use` }, { type: 'paragraph', text: `You agree not to:` }, { type: 'list', items: [ `access or attempt to access information you are not authorized to see;`, `misrepresent your identity, role, or authority;`, `interfere with, disrupt, probe, or attempt to circumvent the security of the service;`, `use the service to violate any law, or any other person's privacy or rights;`, `copy, scrape, resell, or redistribute the service or another person's information.`, ] }, { type: 'heading', text: `7. Fees` }, { type: 'paragraph', text: `Access for patients is free. Organizations pay for the service as described in their subscription. Fees, limits, and included capacity are set out at the time of purchase and may change on notice.` }, { type: 'heading', text: `8. Availability and disclaimers` }, { type: 'paragraph', text: `We work to keep the service available, but we do not guarantee uninterrupted or error-free operation. Except as required by law, the service is provided "as is" and without warranties of any kind, express or implied.` }, { type: 'heading', text: `9. Limitation of liability` }, { type: 'paragraph', text: `To the fullest extent permitted by law, Akashic is not liable for indirect, incidental, special, consequential, or punitive damages, or for lost profits or lost data. [Liability cap and carve-outs to be set by counsel.]` }, { type: 'heading', text: `10. Termination` }, { type: 'paragraph', text: `You may stop using Akashic at any time. We may suspend or terminate access for breach of these Terms, for suspected unauthorized access, or as required by law. Your health record and its history are retained and handled as described in the Privacy Notice; termination of access is not deletion of the record.` }, { type: 'heading', text: `11. Changes to these Terms` }, { type: 'paragraph', text: `We may update these Terms. When we do, the updated version is given a new version number and you will be asked to accept it at your next sign-in.` }, { type: 'paragraph', text: `A version you accepted is never revoked — it is superseded. Every version you accepted, and when you accepted it, is retained as part of the record.` }, { type: 'heading', text: `12. Governing law` }, { type: 'paragraph', text: `[To be set by counsel.]` }, { type: 'heading', text: `13. Contact` }, { type: 'paragraph', text: `[Contact address, support email, and notice address to be set.]` }, ]; // Transcribed verbatim from akashic-docs/legal/akashic-privacy-notice.md (Part 1 - Privacy Notice and // Part 2 - Consent). Excludes the file's frontmatter, its " There is no in-person capture of this // document" note (a dated design-decision note, not document text), and its // " Notes for Ed (delete before publishing)" section. const PRIVACY_CONSENT_BLOCKS: AgreementBlock[] = [ { type: 'title', text: `Part 1 — Privacy Notice` }, { type: 'paragraph', text: `Version: v0.1-placeholder · Effective: [to be set]` }, { type: 'heading', text: `What this covers` }, { type: 'paragraph', text: `This explains how Akashic Health Records handles your health information, what you control, and what rights you have.` }, { type: 'heading', text: `What we collect` }, { type: 'list', items: [ `Information you give us: name, date of birth, contact details, and anything you enter or upload.`, `Information organizations give us: clinical information entered by practitioners and staff at the organizations participating in your care.`, `Information about your use of the service: sign-ins, actions taken on your record, and who accessed it — kept as an audit record.`, ] }, { type: 'heading', text: `How we use it` }, { type: 'paragraph', text: `To operate the service: to store your record, show it to you and to the people you have authorized, support scheduling and communication with your care team, and meet our legal and security obligations.` }, { type: 'paragraph', text: `We do not sell your health information. We do not use it for advertising.` }, { type: 'heading', text: `Who we share it with` }, { type: 'list', items: [ `Organizations and individuals you authorize — and only those. You choose them, and you can change your mind.`, `Service providers who help us run the platform, under contracts that require them to protect it.`, `When the law requires it, or to prevent a serious and imminent threat to health or safety.`, ] }, { type: 'paragraph', text: `Where we handle information on an organization's behalf, we do so under a Business Associate Agreement with that organization.` }, { type: 'heading', text: `Your rights` }, { type: 'list', items: [ `See and get a copy of your information.`, `Ask us to correct information you believe is wrong.`, `See who has accessed or changed your record.`, `Choose and change who may access it.`, `Withdraw an authorization you previously gave.`, `Ask for restrictions on how your information is used or shared.`, `Be told if there is a breach affecting your information.`, ] }, { type: 'paragraph', text: `To exercise any of these, use the controls in the service or contact us at [address to be set].` }, { type: 'heading', text: `How withdrawal works` }, { type: 'paragraph', text: `Withdrawing access stops future access. It is not retroactive — it does not un-share what was already lawfully shared, and it does not remove records an organization is independently required to keep. Your record and its history remain intact; what changes is who may reach it going forward.` }, { type: 'heading', text: `Retention` }, { type: 'paragraph', text: `We retain your record and its history for as long as your account is active and afterwards as required by law and by our agreements with participating organizations. History is retained deliberately — the ability to show what your record said at a point in time, and who changed it, is part of protecting you.` }, { type: 'heading', text: `Security` }, { type: 'paragraph', text: `We protect your information with technical, administrative, and physical safeguards, including encryption and access controls. No system is perfectly secure, and we will notify you as required by law if a breach affects your information.` }, { type: 'heading', text: `Changes to this notice` }, { type: 'paragraph', text: `We may update this notice. Updated versions get a new version number, and you will be asked to review at your next sign-in. A version you accepted is superseded, never revoked, and every version is retained.` }, { type: 'heading', text: `Questions or complaints` }, { type: 'paragraph', text: `Contact us at [to be set]. You may also complain to the U.S. Department of Health and Human Services. We will not retaliate against you for filing a complaint.` }, { type: 'heading', text: `Part 2 — Consent` }, { type: 'paragraph', text: `Version: v0.1-placeholder` }, { type: 'paragraph', text: `By accepting, I consent to Akashic Health Records:` }, { type: 'paragraph', text: ` 1. Creating and maintaining a health record for me, including information I provide and information entered by organizations and practitioners participating in my care. 2. Storing and processing that information in order to operate the service described above. 3. Making it available to the organizations and individuals I choose, for as long as I choose, and stopping when I withdraw that choice.` }, { type: 'paragraph', text: `I understand that:` }, { type: 'list', items: [ `This consent is separate from any authorization I give an individual organization. Consenting to Akashic does not give any organization access to my clinical information — I choose those separately.`, `I may withdraw this consent at any time. Withdrawal applies going forward and does not undo sharing that already occurred.`, `Akashic is not my healthcare provider and does not give medical advice.`, `My record and its history are retained as described in the Privacy Notice.`, ] }, { type: 'paragraph', text: `Accepted by: [patient] · [date] · version v0.1-placeholder` }, ]; const AGREEMENT_COPY: Record = { 'terms-of-service': { title: 'Akashic Health Records — Terms of Service', blocks: TERMS_OF_SERVICE_BLOCKS, body: deriveBody(TERMS_OF_SERVICE_BLOCKS), }, 'privacy-consent': { title: 'Akashic Health Records — Privacy Notice & Consent', blocks: PRIVACY_CONSENT_BLOCKS, body: deriveBody(PRIVACY_CONSENT_BLOCKS), }, }; // Exported only so agreementCopy.test.ts can pin it against a hardcoded literal without going through // resolveCopy() - a direct drift guard, not part of Task 3's contract. Do not build a fallback on it. export const BUNDLED_AGREEMENT_VERSION: Readonly> = AGREEMENT_VERSION; // kind + version -> copy, iff `version` is an EXACT match for the version this module bundles for that // kind. Any mismatch (wrong version, or a kind this module doesn't know) returns undefined - never a // fallback. See the header comment for why that's the one property this whole module exists to hold. export function resolveCopy(kind: AgreementKind, version: string): AgreementCopy | undefined { if (kind === 'staff-terms-of-service') return undefined; const bundledVersion = AGREEMENT_VERSION[kind]; if (bundledVersion === undefined || bundledVersion !== version) { return undefined; } return AGREEMENT_COPY[kind]; }