vinta-ai-workflows 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/README.md +24 -1
  3. package/package.json +1 -1
  4. package/schemas/vinta-ai-workflows-config.v1.schema.json +43 -0
  5. package/skills/vinta-analyze-codebase/SKILL.md +15 -1
  6. package/skills/vinta-bootstrap-ai-tools/SKILL.md +40 -11
  7. package/skills/vinta-derive-skills/SKILL.md +46 -5
  8. package/skills/vinta-derive-skills/resources/foundation-skills/deslop-comments/SKILL.md +147 -21
  9. package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/resources/ai-models.yaml +0 -2
  10. package/skills/vinta-derive-skills/resources/plan-execution/partials/pr-context.md +5 -3
  11. package/skills/vinta-derive-skills/resources/plan-execution/shell/amend-plan-template.md +2 -0
  12. package/skills/vinta-derive-skills/resources/prs-context-template.md +25 -0
  13. package/skills/vinta-derive-skills/resources/write-unit-test-packs/runners/jest.md +49 -0
  14. package/skills/vinta-derive-skills/resources/write-unit-test-packs/runners/pytest.md +36 -0
  15. package/skills/vinta-derive-skills/resources/write-unit-test-packs/runners/vitest.md +43 -0
  16. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/django.md +36 -0
  17. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/fastapi.md +34 -0
  18. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/flask.md +31 -0
  19. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/medplum-test.globalSetup.ts +54 -0
  20. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/medplum-test.setup.ts +39 -0
  21. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/medplum.md +47 -0
  22. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/nextjs.md +27 -0
  23. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/prisma.md +31 -0
  24. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/python-package.md +28 -0
  25. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/react-router.md +25 -0
  26. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/react.md +29 -0
  27. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/tanstack-start.md +32 -0
  28. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/typescript-package.md +32 -0
  29. package/skills/vinta-derive-skills/resources/write-unit-test-template.md +44 -0
@@ -1,11 +1,15 @@
1
1
  ---
2
2
  name: deslop-comments
3
- description: Rewrite code comments and docs touched during the current conversation into Simple English, stripping AI-slop / AI-lingo vocabulary and negative framing. Use when the user says "deslop these comments", "clean up the comments we just wrote", "rewrite this in plain English", or after a review flags comments as convoluted/AI-sounding. Comment-only - never changes function names, APIs, or behavior.
3
+ description: Rewrite code comments and docs touched during the current conversation into Simple English, stripping AI-slop / AI-lingo vocabulary and negative framing, and delete the ones that should not be there at all. Use when the user says "deslop these comments", "clean up the comments we just wrote", "rewrite this in plain English", or after a review flags comments as convoluted/AI-sounding. Comment-only - never changes function names, APIs, or behavior.
4
4
  ---
5
5
 
6
6
  # Deslop comments
7
7
 
8
- Rewrites comments and doc blocks into short, direct Simple English. This is a comment-only pass: no renames, no logic changes, no behavior changes.
8
+ Rewrites comments and doc blocks into short, direct Simple English, and deletes the ones that should not be there. This is a comment-only pass: no renames, no logic changes, no behavior changes.
9
+
10
+ Prefer fewer and shorter comments. Deleting one is a normal result, not a last resort. Default to one line, and add more only when the extra lines say something the code cannot.
11
+
12
+ Some comments only need rewording. Others should not exist at all, whatever the wording - a list of call sites, another module's internals, a plan that never shipped. Delete those; making them shorter does not help.
9
13
 
10
14
  ## Scope
11
15
 
@@ -15,10 +19,10 @@ Do not expand scope to a file just because the agent `Read` it or reviewed it in
15
19
 
16
20
  ## What counts as slop
17
21
 
18
- Rewrite a comment if it has any of:
22
+ Rules 1-5 are usually reworded. Rules 6-12 are usually deleted.
19
23
 
20
- 1. **Dense, multi-clause sentences** that try to explain everything at once instead of one idea per sentence.
21
- 2. **Negative / before-state framing** - describing removed code, or leading with "not X" when X was never the point. Keep "not X, it's Y" only for a genuine edge case, a non-obvious gotcha, or bug-fix rationale worth flagging; otherwise state the current behavior directly.
24
+ 1. **Dense, multi-clause sentences** that try to explain everything at once instead of one idea per sentence. Split a sentence that adds a second thought.
25
+ 2. **Negative / before-state framing** - describing removed code, or leading with "not X" when X was never the point. Keep "not X, it's Y" only for a genuine edge case or a non-obvious gotcha.
22
26
  3. **AI buzzwords** in place of plain software engineering vocabulary. Common offenders and their replacements:
23
27
  - `gate` / `gates` / `gating` → "check", "decides whether to show...", "authentication check", "feature check"
24
28
  - `guard` → "check" / "prevent"
@@ -39,10 +43,35 @@ Rewrite a comment if it has any of:
39
43
  - `canonical` → "shared" / "standard"
40
44
  - `churn` → "unnecessary changes"
41
45
  - `invariant` → "rule" / "condition" / "constraint" / or something like "state that must stay true"
46
+ - `gloss` → "explanation" / "note"
47
+ - `roll-call` → "list of call sites"
48
+ - `signal` (meaning a hint) → "sign", or drop the word and say what to check
49
+ - `the tell` → "to tell them apart, ask whether..."
50
+ - `unpick` → "work out" / "read twice"
51
+ - `papering over` → "hiding", or name the real fix ("fix the type instead")
52
+ - `strictly worse` → "worse", and say why
53
+ - `earns its place` → say what to do instead: "delete it unless it explains X"
54
+ - "asks nicely" (for something that isn't enforced) → name the mechanism: "a comment does not fail when the code changes; a test does"
55
+ - "prefers X over Y" → "uses X, or Y when there is no X"
56
+ - `failure` for a wording problem → name the problem: "this is the part to fix"
42
57
 
43
58
  Do not apply this as a blind blacklist. Keep the original word when it is the precise domain term, especially for security, type-system, React, database, FHIR, or JavaScript runtime concepts. For example, `opaque` is a precise security/API term for an id, token, or string whose structure and meaning a caller must not depend on ("recipient is an opaque viewer-supplied string; never log it"); it is not on the replacement list above, so leave it — "generic" would drop that contract.
44
- 4. **Undocumented return shapes.** A function returning an object or union should say what each field/variant means, not just that it "returns a result."
45
- 5. **Any other AI-slop or AI-lingo words, framing, structure.**
59
+ 4. **Essay voice.** Write plain statements a reader can check, not persuasion. No sayings ("most do not clear that bar"), no "it looks like X but is really Y", no treating code as if it wants things ("the ideas want to be separate sentences"), and no metaphor where an instruction fits ("earn its place" → "delete it").
60
+ 5. **Undocumented return shapes.** A function returning an object or union should say what each field or variant means, not just that it "returns a result". TypeScript covers most of this, so raise a missing type in the conversation as a code fix instead of describing the shape in a comment. This is the one rule that can make a comment longer, so apply it only where the field names do not already answer it: `{ ready, missingSteps }` needs no explanation, `{ status, mode }` does.
61
+ 6. **Where-used lists.** Naming the call sites of a helper, type, or constant, or the features that use it. Call sites move, get added, and get deleted, and the comment does not follow them - so it is wrong the first time someone adds a second caller, and the editor already answers the question with "find all references". Say what the thing is and why it exists. Keeping a *reason* that happens to involve other code is fine ("shared, so the count and the list cannot disagree"); listing *which* code is not ("shared by the list, the count and the latest scan").
62
+ 7. **Another module's internals, explained from here.** A comment on A that explains how B works behind its own API. Nobody editing B will think to update it, so it goes stale silently. Say what this code needs, and let B's own comment explain B. The same applies downward: do not explain a caller's behaviour from inside the thing being called.
63
+ 8. **One decision explained in several places.** Three sites explaining the same design decision give three wordings that drift apart, and a reader cannot tell which is current. Which way to fix it depends on the code:
64
+ - **The code is duplicated too** (two apps with the same database setup). Repeat the comment in full at each copy. Each file has to be readable on its own, so do not shorten one copy to a link pointing at the other.
65
+ - **One decision shows up in several places.** Say a mapper leaves `documentUrl` out of the rows it returns, the row type leaves it out to match, and the app fills it in later. That is one decision seen from three files. Write the reason once at the place that makes it - the mapper that leaves the field out - and at the other two say only what a reader there needs. See the `documentUrl` example below.
66
+
67
+ A link is fine for extra depth ("more info in `x.ts`"). A link is not enough when the reader needs it to understand the line in front of them - inline that sentence instead.
68
+ 9. **Restating the code.** A comment that is just explaining what is already written in code: a visible fallback chain, the steps of a loop, or a parameter's name and its type in prose ("`reportsByOrderId` holds the report for each order, keyed by the order's id" for `reportsByOrderId: Map<string, DiagnosticReport>`). Comment the part that is *not* on screen: why this order, why this default, what breaks otherwise.
69
+ 10. **A comment standing in for a name.** When the comment exists to explain what a condition or a function means, the name is the fix: `shouldSendErrorToSentry(error)` instead of a note above a negated two-part condition. Renaming is out of scope for this pass, so report it and leave the comment alone - see "When the fix is not a comment edit".
70
+ 11. **Plans, deferred work, and requests.** "Once X is supported we can drop this", "temporary until Y", "as requested in review". None of it is verifiable from the code, most of it never happens, and it outlives the conversation that produced it. Real follow-up work belongs in a tracked ticket, and the comment should link that ticket or say nothing.
71
+ 12. **Paths not taken.** "Tried X, settled on Y", "started with a Map, an array was simpler". If X was never committed it was a draft, and drafts are not part of the code's history. If X was committed, git and the PR record it better than a comment can. Cut the alternative and keep what the code has to do now.
72
+
73
+ One thing this is not: a fact about an external system, even when you found it by trying the other thing first. `PUT Binary/<id>` returning 404 is Medplum's behaviour, so write it that way - a property of the API, with no author and no alternative in it.
74
+ 13. **Any other AI-slop or AI-lingo words, framing, structure.**
46
75
 
47
76
  ## What to leave alone
48
77
 
@@ -52,17 +81,115 @@ Rewrite a comment if it has any of:
52
81
  - Genuine security markers or contracts written as caps-negatives (`NEVER log PHI`, `Does NOT upsert`) - these are useful warnings, not slop.
53
82
  - User-facing UI copy (JSX text nodes rendered to the browser) - that's product copy, not a code comment.
54
83
  - Already-clear one-liners that don't exhibit any of the problems above. Don't rewrite for the sake of rewriting.
84
+ - **The reason a non-obvious line exists** - why this order, why this default, what an external system does, what breaks without the line. This pass removes noise, not knowledge, so keep the reason and shorten the wording instead.
85
+ - **A comment repeated because the code is repeated.** Two apps with the same setup step each keep their own copy. See rule 8.
55
86
 
56
87
  ## Process
57
88
 
58
89
  1. Build the file list per Scope above.
59
- 2. For each file, read every comment/doc block (`//`, `/* */`, `/** */`, `#`, `%`, depending on the language) and check it against "What counts as slop." Skip anything covered by "What to leave alone."
60
- 3. Rewrite in place with `Edit`. Prefer one idea per sentence. When a function's return type is a union or object with meaningful fields, spell out what each branch/field means using the comment body or JSDoc (or similar) if it's a project pattern.
61
- 4. After editing, confirm the diff is comment-only: `git diff -- <files>` should show no code-line changes, only comment text.
62
- 5. Run the project's lint/typecheck on touched files (e.g. `pnpm biome check <files>`, `pnpm --filter <pkg> typecheck`) to confirm nothing broke.
63
- 6. Summarize what changed in 1-2 sentences - don't restate every line edited.
90
+ 2. Read every comment/doc block in each file (`//`, `/* */`, `/** */`, `#`, `%`, depending on the language) and check it against "What counts as slop". Skip anything under "What to leave alone".
91
+ 3. Check every comment next to changed code for claims that are no longer true, including comments this task did not write. A comment goes wrong when the code under it moves, and nothing fails: no test, no type error, no lint rule. This is the one check a reader cannot make from the comment alone.
92
+ 4. Decide **delete or reword** before editing. Rules 1-5 are rewordings. Rules 6-12 are usually deletions - cut the comment, or cut the offending part and keep the rest.
93
+ 5. Find the duplicates across the whole file set before editing anything. Rule 8 needs the full set to decide which copy stays; one file at a time leaves the same reason in three places, each slightly reworded.
94
+ 6. Rewrite in place with `Edit`. One idea per sentence.
95
+ 7. Grep the edited files for every word listed in rule 3, including comments this pass just wrote. Reading for them does not work: they get written and then skimmed past, in the same file that lists them. A hit is not automatically wrong - rule 3 keeps the precise domain term, `signal` is right for `AbortSignal`, and a function genuinely named `isGated` matches - so check each one. This covers rule 3 only; the other rules need judgement.
96
+ 8. Confirm the diff is comment-only: `git diff -- <files>` should show no code-line changes.
97
+ 9. Run the project's lint/typecheck on touched files (e.g. `pnpm biome check <files>`, `pnpm --filter <pkg> typecheck`).
98
+ 10. Summarize in 1-2 sentences, with the comment-line count before and after. List separately, since neither is a style note: any comment that was **factually wrong**, and anything left in place per the next section.
99
+
100
+ ## When the fix is not a comment edit
101
+
102
+ This pass edits comments and nothing else. When the right fix is a test, a rename, or splitting a function, leave the comment alone and report it, naming the file and what has to exist first: "`use-lab-results.ts:44` - this note can go once a regression test covers the sort flip. Left in place; that test does not exist yet." Never delete a comment whose knowledge has nowhere else to live yet.
103
+
104
+ ## Examples: what a comment says
105
+
106
+ Bad (rule 6, a list of today's three callers):
107
+ ```ts
108
+ /** Shared by the list, the count and the latest scan, so the tile cannot count a row the list omits. */
109
+ const LAB_ORDER_SEARCH = { category: LAB_ORDER_CATEGORY }
110
+ ```
111
+
112
+ Good (keeps the reason, drops the list):
113
+ ```ts
114
+ /** Narrows a `ServiceRequest` search to lab orders. Every read here shares it, so none can disagree. */
115
+ const LAB_ORDER_SEARCH = { category: LAB_ORDER_CATEGORY }
116
+ ```
117
+
118
+ Bad (rule 8, first case - this file cannot be read without opening another app):
119
+ ```ts
120
+ // Remove sslmode from the URL so it does not conflict with the ssl object above.
121
+ // More info on shl-server/src/db.ts
122
+ ```
123
+
124
+ Good (the same explanation in full at each copy):
125
+ ```ts
126
+ // Remove sslmode from the URL so it does not conflict with the ssl object above: node-postgres
127
+ // rejects the connection when both are set.
128
+ ```
129
+
130
+ Bad (rule 8, second case - one decision about `documentUrl`, repeated in all three files):
131
+ ```ts
132
+ // types.ts
133
+ /** A mapped lab row. The mapper knows whether a document exists; only the app knows its url. */
134
+ export interface LabResultRow extends Omit<LabResultView, 'documentUrl'> { ... }
135
+
136
+ // mappers.ts
137
+ /** Maps lab orders to rows. No `documentUrl`: the mapper knows a document exists; only the app knows its url. */
138
+ export function mapLabResults(...)
139
+
140
+ // use-lab-results.ts
141
+ /** Adds `documentUrl`. The mapper knows whether a document exists; only the app knows its url. */
142
+ function withDocumentUrl(row: LabResultRow): LabResultView
143
+ ```
144
+
145
+ Good (the reason sits with the mapper, since that is what leaves the field out; the other two say only what a reader there needs):
146
+ ```ts
147
+ // types.ts
148
+ /** A mapped lab row, before the app fills in `documentUrl`. */
149
+ export interface LabResultRow extends Omit<LabResultView, 'documentUrl'> { ... }
150
+
151
+ // mappers.ts
152
+ /** Maps lab orders to rows. Leaves out `documentUrl` because the url depends on how the app serves binaries; each row carries `reportId` instead. */
153
+ export function mapLabResults(...)
154
+
155
+ // use-lab-results.ts
156
+ /** Serves the row's PDF through this app's `/api/binary` route. */
157
+ function withDocumentUrl(row: LabResultRow): LabResultView
158
+ ```
159
+
160
+ Bad (rule 2 - explains why a check that used to be here is gone; the comment never mentions the removal, so it reads as rationale):
161
+ ```ts
162
+ // Report only unexpected failures to Sentry, tagged with the resource. A missing session is
163
+ // routine, since cookies expire, so it stays out of Sentry. A not-provisioned patient is
164
+ // reported like any other unexpected error: login already rejects that state, so a signed-in
165
+ // patient who hits it had their membership revoked mid-session, which is worth seeing.
166
+ if (!isPatientSessionUnauthorizedError(error)) { ... }
167
+ ```
168
+
169
+ Good (what the line does now; the deleted check is not a reader's problem):
170
+ ```ts
171
+ // Report only unexpected failures to Sentry
172
+ if (!isPatientSessionUnauthorizedError(error)) { ... }
173
+ ```
64
174
 
65
- ## Example
175
+ Bad (rule 9, restating a fallback chain that is on screen):
176
+ ```ts
177
+ /**
178
+ * The badge for a report: its interpretation when it carries one ("Abnormal", flagged), otherwise its
179
+ * workflow status ("Preliminary"), otherwise a bare "Result". A workflow status never flags - only an
180
+ * out-of-range interpretation does.
181
+ */
182
+ function reportBadge(report: DiagnosticReport): LabBadge {
183
+ return interpretation ?? (statusMeta ? { ...statusMeta, flagged: false } : UNKNOWN_RESULT_BADGE)
184
+ }
185
+ ```
186
+
187
+ Good (the order is visible in the code; the flagging rule is not):
188
+ ```ts
189
+ /** The badge for a report. Only an out-of-range interpretation sets `flagged`. */
190
+ ```
191
+
192
+ ## Examples: how a comment is written
66
193
 
67
194
  Bad (dense, negative framing, buzzwords):
68
195
  ```
@@ -135,14 +262,13 @@ export async function getIntakeReadiness(headers: Headers): Promise<IntakeReadin
135
262
  Good:
136
263
  ```
137
264
  /**
138
- * Returns whether the patient can finalize their intake: every required step
139
- * must pass its schema, and one of the payment steps must have a saved
140
- * response. `completeIntake` enforces the same rule on the server, so a
141
- * client cannot complete a partial intake by skipping this check. A
142
- * Save-for-later draft counts as missing, which sends the patient back to
143
- * the earliest unfinished step (e.g. Demographics) instead of skipping past
144
- * it. In the returned object, `ready` tells whether the intake can be
145
- * completed, and `missingSteps` lists the keys of the steps still missing.
265
+ * Returns whether the patient can finalize their intake: every required step must pass its schema,
266
+ * and one of the payment steps must have a saved response.
267
+ *
268
+ * A Save-for-later draft counts as missing, so the patient goes back to the earliest unfinished step
269
+ * instead of past it. `completeIntake` applies the same rule on the server.
146
270
  */
147
271
  export async function getIntakeReadiness(headers: Headers): Promise<IntakeReadiness> { ... }
148
272
  ```
273
+
274
+ `ready` and `missingSteps` are not explained, because their names already say it - see rule 5.
@@ -51,7 +51,6 @@ tiers:
51
51
  models:
52
52
  anthropic:
53
53
  - id: claude-haiku-4-5
54
- note: with iteration; step up to claude-sonnet-5 if phase touches >3 files
55
54
  - id: claude-sonnet-5
56
55
  note: step-up target when phase touches >3 files
57
56
  openai:
@@ -84,7 +83,6 @@ tiers:
84
83
  models:
85
84
  anthropic:
86
85
  - id: claude-opus-4-8
87
- note: "[1m] 1M-context variant for large codebases"
88
86
  openai:
89
87
  - id: gpt-5.6-sol
90
88
  note: Frontier model for complex professional work
@@ -41,9 +41,11 @@ Two project-level signals decide the actual behavior:
41
41
 
42
42
  4. **Write the prs-context file** at {{PRS_CONTEXT_FILE_PATH}}, following [resources/prs-context-template.md](../../prs-context-template.md). Frontmatter: `plan_id`, `feature_name`, `phase_id`, `phase_title`, `branch`, `base`, `created_at`, `status: pending`, empty `pr_url`. **`base` is the branch the PR opens against — resolve it per the commit strategy, never default it to `<BASE_BRANCH>` blindly:** for stacked branches only the first executed phase bases on `<BASE_BRANCH>`; every subsequent phase bases on the **previous phase's branch** (see the PR-base rule under the {{BRANCH_PUSH_HEADING}} step above). For a single plan-level PR (modular / one-PR strategies) `base = <BASE_BRANCH>`. Body sections: `# Title` (single-line PR title), `# Description` (Markdown body — uses the project's PR template structure from step 2 when one exists), `# Comments` (YAML list of `{file, start_line, end_line?, side, body}` — empty list when comments are off).
43
43
 
44
- 5. **Confirm `.vinta-ai-workflows/prs-context/` is in `.gitignore`.** [vinta-install-ai-tools-setup](../../../vinta-install-ai-tools-setup/SKILL.md) runs the multi-vendor setup script which appends `.vinta-ai-workflows/prs-context/` on its first invocation. If an older bootstrap missed it, append it now.
44
+ 5. **Deslop the prose.** Run the `deslop-comments` skill ([deslop-comments](../deslop-comments/SKILL.md)) over the file you just wrote, passing {{PRS_CONTEXT_FILE_PATH}} as the explicit scope. `# Title`, the `# Description` body, and every `# Comments` `body` must read as Simple English: one idea per sentence, current behavior stated directly instead of "not X" framing, and no AI-slop vocabulary (`gate`, `guard`, `backstop`, `surface`, `leverage`, `plumb`, `canonical`, …). Keep precise domain terms, and keep the project's PR-template structure intact — section headings, `<!-- HTML comment -->` placeholders, and checklist lines stay exactly as step 2 wrote them. This pass rewrites prose only: it never touches the frontmatter, the section layout, or which file and lines a comment targets. Do it before `open-pr.sh` runs, since the published PR body and comments come straight from this file.
45
45
 
46
- 6. **Run `open-pr.sh`** (only when policy = agents create PRs). Detect a usable CLI (`gh` for GitHub, `glab` for GitLab) plus the script's other deps (`yq`, `jq`):
46
+ 6. **Confirm `.vinta-ai-workflows/prs-context/` is in `.gitignore`.** [vinta-install-ai-tools-setup](../../../vinta-install-ai-tools-setup/SKILL.md) runs the multi-vendor setup script which appends `.vinta-ai-workflows/prs-context/` on its first invocation. If an older bootstrap missed it, append it now.
47
+
48
+ 7. **Run `open-pr.sh`** (only when policy = agents create PRs). Detect a usable CLI (`gh` for GitHub, `glab` for GitLab) plus the script's other deps (`yq`, `jq`):
47
49
 
48
50
  ```bash
49
51
  bash ai-tools/skills/open-pr-from-context/scripts/open-pr.sh {{PRS_CONTEXT_FILE_PATH}}
@@ -57,5 +59,5 @@ Two project-level signals decide the actual behavior:
57
59
 
58
60
  When policy = "branches only": **don't run the script.** File stays `status: pending`.
59
61
 
60
- 7. **Skill wrapper** — [open-pr-from-context](../open-pr-from-context/SKILL.md) is available for ad-hoc invocation (after the run, on a different machine, etc.). The orchestrator can call the script directly here; the skill is for humans.
62
+ 8. **Skill wrapper** — [open-pr-from-context](../open-pr-from-context/SKILL.md) is available for ad-hoc invocation (after the run, on a different machine, etc.). The orchestrator can call the script directly here; the skill is for humans.
61
63
  <!-- block-end: PR_CONTEXT -->
@@ -289,6 +289,8 @@ For the rewritten branch, look for `.vinta-ai-workflows/prs-context/{feature-keb
289
289
  - Inline comments may now reference SHAs that no longer exist. They'll appear as "outdated" in the PR UI.
290
290
  - If the new diff has materially different comment-worthy spots, regenerate the `# Comments` block, set `status: pending`, and re-run [open-pr.sh](../open-pr-from-context/scripts/open-pr.sh) on the file. The script reuses the existing PR, posts new comments. Old "outdated" comments stay visible in the PR for audit; that's the platform's behavior.
291
291
 
292
+ Whenever you rewrite any of `# Title`, `# Description`, or `# Comments`, finish by running the `deslop-comments` skill ([deslop-comments](../deslop-comments/SKILL.md)) over the file with the file path as the explicit scope, so the rewritten text stays in Simple English same as [integrate-phase]({{INTEGRATE_PHASE_LINK}}) does when it first writes the file. Structure, frontmatter, and comment line targets stay untouched.
293
+
292
294
  When rewriting the `# Description` body, **honor `project.pr_template_paths`** from `.vinta-ai-workflows.yaml` — same rule as [integrate-phase]({{INTEGRATE_PHASE_LINK}})'s **Open PR via context file** step: follow the project's PR template structure, fill new sections with phase-specific content from the rewritten body, leave un-fillable placeholders untouched. If the prior file used a different template than the project now declares, prefer the current `pr_template_paths` choice — surface the change to the user when the body shape shifts visibly.
293
295
 
294
296
  Always include in the publish-log block at the bottom of the file:
@@ -12,6 +12,10 @@ status: pending # `pending` until published; `published
12
12
  pr_url: # set by open-pr-from-context after publishing
13
13
  ---
14
14
 
15
+ Everything below the frontmatter is prose a human reads on the PR, so write it in
16
+ **Simple English** — see [Write it in Simple English](#write-it-in-simple-english)
17
+ at the bottom of this file.
18
+
15
19
  # Title
16
20
 
17
21
  <single-line PR title — follow project commit style; keep ≤72 chars>
@@ -89,3 +93,24 @@ Plan sections have **names** (`Goals + Non-goals`, `Guiding Decisions`, `Data Mo
89
93
  - Boilerplate matching nearby files.
90
94
  - Standard patterns documented in AGENTS.md.
91
95
  - Test bodies whose names already describe the assertion.
96
+
97
+ ## Write it in Simple English
98
+
99
+ The title, the description, and every comment `body` are read by a human reviewer.
100
+ Write them the way the `deslop-comments` skill
101
+ (`ai-tools/skills/deslop-comments/SKILL.md`) asks for:
102
+
103
+ - One idea per sentence. Split the dense multi-clause sentence instead of nesting it.
104
+ - Say what the code does now. Lead with "not X" only when the reader would
105
+ otherwise get it wrong — a real gotcha, an edge case, or the reason behind a fix.
106
+ - Plain engineering words, not AI buzzwords: "check" instead of `gate` / `guard`,
107
+ "use" instead of `leverage` / `utilize`, "show" or "return" instead of `surface`,
108
+ "pass" or "connect" instead of `plumb` / `wire`, "created" instead of `mint`,
109
+ "rule" or "constraint" instead of `invariant`. Keep a word when it is the precise
110
+ domain term (`opaque` for a token, `idempotent` for a handler, framework and
111
+ database vocabulary).
112
+ - Keep proper nouns verbatim: plan and phase names, the plan's section names,
113
+ branch names, flag names, file paths, identifiers from the code.
114
+
115
+ `implement-plan` runs `deslop-comments` over this file before publishing it, so
116
+ anything that slips through gets rewritten there — but write it clean the first time.
@@ -0,0 +1,49 @@
1
+ # Jest runner pack
2
+
3
+ Test-runner idioms for Jest. Stack-agnostic — framework-client and domain-mock specifics live in the **stack pack**, loaded alongside this one. The Testing Library / DOM notes apply only when the project renders UI. Read with the skill's universal rules. (Jest and Vitest share most idioms; the differences are noted.)
4
+
5
+ ## Structure & style
6
+
7
+ - **`describe` / `it` (or `test`) with the behavior in the name.** `it("returns zero for an empty cart")`. The string is the spec.
8
+ - **Assert full values with the right matcher.** `expect(result).toEqual(expected)` for deep value equality, `toBe` for primitives/identity, `toStrictEqual` when `undefined` fields / class identity matter. Avoid `toBeTruthy()` / `toBeDefined()` as the *only* assertion. For arrays assert contents (and order when it matters), not just `.toHaveLength(n)`.
9
+ - Error paths: `expect(fn).toThrow(Message)` — assert the error, not just that it threw.
10
+
11
+ ## Table-drive instead of looping
12
+
13
+ - Use `it.each([...])` / `describe.each` — each row is its own test with a literal expected value. Never loop inside one test recomputing the expectation.
14
+
15
+ ## Test data
16
+
17
+ - **Prefer small factory/builder helpers over inline object literals** when the project has them — `makeUser({ email })` returns a valid object with only the relevant field overridden. Keep overrides minimal.
18
+ - No factory convention → a local `base` object with per-test spread beats copy-pasted full literals.
19
+
20
+ ## Mocking & externals
21
+
22
+ - **Mock only genuine externals, at the boundary.** `jest.mock("./module")`, `jest.fn()` for callbacks, `jest.spyOn(obj, "method")` for one method. Don't mock the unit under test or pure helpers.
23
+ - **Reset between tests.** Set `clearMocks: true` (and consider `restoreMocks: true`) in the Jest config, or `afterEach(() => jest.restoreAllMocks())`. Stale mock state between tests is a top flake source.
24
+ - **HTTP:** use **MSW** (`setupServer`) to intercept at the network layer and return canned responses — the real client path runs. Never let a real request out. Prefer MSW over hand-mocking `fetch`/`axios`.
25
+ - **Complex APIs (e.g. LLM endpoints):** when hand-writing responses is impractical, record real traffic once and replay it from a committed cassette — [PollyJS](https://github.com/Netflix/pollyjs) or `nock`'s `nock.back`. Scrub secrets/PII from the recording, commit it, and never re-hit the live API in CI. Re-record deliberately when the contract changes.
26
+ - **Time:** `jest.useFakeTimers()` + `jest.setSystemTime(...)`, `jest.useRealTimers()` in teardown. Don't assert on `Date.now()` live.
27
+ - Prefer dependency injection over `jest.mock` when practical — fewer module-graph surprises.
28
+
29
+ ## UI / component tests
30
+
31
+ - When the project renders UI, the component-testing idioms (Testing Library queries, `user-event`, asserting output not internals) live in the **React stack pack** — follow it there. Pure-logic units don't need it.
32
+
33
+ ## Isolation
34
+
35
+ - Keep test files independent; don't share mutable module-level state. Clean up any DOM/global you touched in `afterEach`.
36
+ - DB-backed tests: wrap each in a transaction rolled back in `afterEach`, or reset to a known seed. Never leave rows behind.
37
+
38
+ ## Jest-specific notes
39
+
40
+ - `jest.mock(...)` is **hoisted** above imports; a mock factory can't reference outer variables unless they're prefixed `mock`. Return values from inside the factory.
41
+ - Automock is off by default — keep it off; explicit mocks are clearer.
42
+ - `jest.mock("module")` replaces the whole module for the file; use `jest.requireActual` to keep the real parts you don't mean to stub.
43
+
44
+ ## Pitfalls
45
+
46
+ - Asserting on a mock's return you just configured — tests the mock, not the code.
47
+ - Forgetting `await` on async expectations (`await expect(p).resolves...`) — the assertion never runs; the test passes hollow.
48
+ - Snapshot sprawl: giant `toMatchSnapshot()` blobs get blindly re-recorded. Assert the fields that matter by value.
49
+ - `toEqual` ignores `undefined` properties — use `toStrictEqual` when their presence/absence is part of the contract.
@@ -0,0 +1,36 @@
1
+ # pytest runner pack
2
+
3
+ Test-runner idioms for pytest. Stack-agnostic — DB isolation and framework-client/mock specifics live in the **stack pack** (Django / FastAPI / Flask / …), loaded alongside this one. Read with the skill's universal rules.
4
+
5
+ ## Structure & style
6
+
7
+ - **Plain functions + fixtures, not `unittest.TestCase`.** `def test_x():` with fixtures is the idiomatic form. Use a plain `class TestThing:` only when it genuinely groups (shared parametrization, a natural noun) — never subclass `TestCase` in new pytest code.
8
+ - **Fixtures over `setUp`/`tearDown`.** Fixtures compose, scope (`function`/`module`/`session`), and `yield` for teardown. Put shared ones in `conftest.py`.
9
+ - **Plain `assert`, not `self.assertEqual`.** pytest rewrites `assert` to show both sides. `assert result == expected`. Unordered collections: compare `sorted(...)` or sets. Approximate floats: `pytest.approx`.
10
+
11
+ ## Parametrize instead of looping
12
+
13
+ - `@pytest.mark.parametrize("input,expected", [...])` — each row is its own test with a **literal** expected value. Add `ids=` when the repr is opaque. Never loop inside one test recomputing the expectation.
14
+
15
+ ## Test data
16
+
17
+ - **Prefer `factory_boy` factories over inline dicts** when the project has them — `UserFactory(email="x@y.z")` spells out only the relevant field and stays correct as the model grows. `Factory.build()` (no persistence) vs `Factory.create()` (persisted). Override only the fields the assertion depends on.
18
+ - No factories → a small local constructor/helper beats hand-assembled deep dicts repeated across tests.
19
+
20
+ ## Mocking & externals
21
+
22
+ - **`mocker` (`pytest-mock`)** — `mocker.patch("module.where_its_used.thing")`, patching where the name is *looked up*, not where it's defined. Auto-undone at teardown.
23
+ - **HTTP:** `responses` (requests) / `respx` (httpx) — assert the outbound request, return a canned response. `pytest-socket` (`--disable-socket`) is a good CI guard against stray real calls.
24
+ - **Complex APIs (e.g. LLM endpoints):** when hand-writing responses is impractical, record real traffic once and replay it from a committed cassette — `vcrpy` (`@pytest.mark.vcr`) or `betamax`. Scrub secrets/PII from the cassette, commit it, and never re-hit the live API in CI. Re-record deliberately when the contract changes.
25
+ - **Time/randomness:** `freezegun` (`@freeze_time`) or inject a clock; seed or patch `random`.
26
+ - Don't patch your own pure functions or the unit under test — that tests the mock.
27
+
28
+ ## DB isolation
29
+
30
+ - Wrap each DB-touching test in a transaction rolled back at teardown, via a fixture — the exact mechanism is stack-specific (see the stack pack). Never `DELETE FROM` in setup as a cleanup crutch, and never depend on test ordering.
31
+
32
+ ## Pitfalls
33
+
34
+ - Patching the definition site instead of the SUT's namespace (`from lib import thing` → patch `sut.thing`) — the patch silently does nothing.
35
+ - `assert mock.called` as the only assertion — asserts an interaction, not a result.
36
+ - Comparing objects without value equality — give them a dataclass `__eq__` or assert the fields you mean.
@@ -0,0 +1,43 @@
1
+ # Vitest runner pack
2
+
3
+ Test-runner idioms for Vitest. Stack-agnostic — framework-client and domain-mock specifics (e.g. Medplum's `MockClient`) live in the **stack pack**, loaded alongside this one. The Testing Library / DOM notes apply only when the project renders UI. Read with the skill's universal rules.
4
+
5
+ ## Structure & style
6
+
7
+ - **`describe` / `it` (or `test`) with the behavior in the name.** `it("returns zero for an empty cart")`. The string is the spec; the failure line should read as a sentence.
8
+ - **Assert full values with the right matcher.** `expect(result).toEqual(expected)` for deep value equality (objects/arrays), `toBe` for primitives/identity, `toStrictEqual` when `undefined` fields and class identity matter. Avoid `toBeTruthy()` / `toBeDefined()` as the *only* assertion — a wrong-but-truthy value passes. For arrays assert contents (and order when it matters), not just `.toHaveLength(n)`.
9
+ - Use `expect(fn).toThrow(Message)` for error paths; assert the error, not just that it threw.
10
+
11
+ ## Table-drive instead of looping
12
+
13
+ - Use `it.each([...])` / `describe.each` to cover many cases — each row is its own test with a literal expected value. Never `for`-loop inside one test recomputing the expectation.
14
+
15
+ ## Test data
16
+
17
+ - **Prefer small factory/builder helpers over inline object literals** when the project has them — `makeUser({ email })` returns a valid object with only the relevant field overridden, and survives new required fields on the type. Keep overrides minimal: set only what the assertion depends on.
18
+ - When there's no factory convention, a local `const base = {...}` with per-test spread (`{ ...base, qty: 2 }`) beats copy-pasting full literals across tests.
19
+
20
+ ## Mocking & externals
21
+
22
+ - **Mock only genuine externals, at the boundary.** `vi.mock("./module")` for a whole module, `vi.fn()` for a passed-in callback, `vi.spyOn(obj, "method")` for one method. Don't mock the unit under test or pure helpers that run fine.
23
+ - **Reset between tests.** Set `test: { clearMocks: true }` (or `restoreMocks: true`) in the Vitest config, or `afterEach(() => vi.restoreAllMocks())` — stale mock state leaking between tests is a top flake source.
24
+ - **HTTP:** use **MSW** (`setupServer`) to intercept at the network layer and return canned responses — the code under test runs its real fetch/axios path. Never let a real request out. Prefer MSW over mocking `fetch` directly (which couples to the client).
25
+ - **Complex APIs (e.g. LLM endpoints):** when hand-writing responses is impractical, record real traffic once and replay it from a committed cassette — [PollyJS](https://github.com/Netflix/pollyjs) or `nock`'s `nock.back`. Scrub secrets/PII from the recording, commit it, and never re-hit the live API in CI. Re-record deliberately when the contract changes.
26
+ - **Time:** `vi.useFakeTimers()` + `vi.setSystemTime(...)`, and `vi.useRealTimers()` in teardown. Don't assert on `Date.now()` live.
27
+ - **Modules with side effects on import:** prefer dependency injection over `vi.mock` when you can — fewer module-graph surprises.
28
+
29
+ ## UI / component tests
30
+
31
+ - When the project renders UI, the component-testing idioms (Testing Library queries, `user-event`, asserting output not internals) live in the **React stack pack** — follow it there. Pure-logic units don't need it.
32
+
33
+ ## Isolation
34
+
35
+ - Vitest isolates test files by default; keep it. Don't share mutable module-level state between tests. Clean up any DOM/global you touched in `afterEach`.
36
+ - For DB-backed integration tests, wrap each test in a transaction rolled back in `afterEach`, or reset to a known seed — never leave rows behind.
37
+
38
+ ## Pitfalls
39
+
40
+ - Asserting on a mock's return you just configured — tests the mock, not the code.
41
+ - `vi.mock` factory hoisting surprises: `vi.mock` is hoisted above imports; reference values via the factory, not outer closures, or use `vi.hoisted`.
42
+ - Forgetting `await` on async expectations (`expect(promise).resolves...`) — the assertion never runs and the test passes hollow.
43
+ - Snapshot sprawl: giant `toMatchSnapshot()` blobs get blindly re-recorded. Assert the fields that matter by value.
@@ -0,0 +1,36 @@
1
+ # Django stack pack
2
+
3
+ Django-specific testing — DB isolation, ORM data, settings, the test client, external boundaries. Loaded **only when the project is Django**, alongside the runner pack (pytest or unittest). Read with the skill's universal rules.
4
+
5
+ ## DB isolation & base class
6
+
7
+ - **`TestCase` (or pytest-django's `db` fixture) wraps each test in a transaction rolled back at teardown** — the default, fast path, zero manual cleanup.
8
+ - **`TransactionTestCase` / `@pytest.mark.django_db(transaction=True)`** truncates instead of rolling back — use **only** when the code commits its own transactions, tests `on_commit` hooks, or uses `select_for_update`. Markedly slower; not the default.
9
+ - **`SimpleTestCase`** forbids DB access — use it to prove a unit is DB-free.
10
+ - Never `Model.objects.all().delete()` in setup as a cleanup crutch; rely on rollback.
11
+
12
+ ## Test data
13
+
14
+ - **Prefer `factory_boy` or `model_bakery` recipes over fixture files or inline `Model.objects.create(...)` walls** — `UserFactory(is_staff=True)` builds a valid instance spelling out only the relevant field. Fixture files drift from the schema silently.
15
+ - With `TestCase`, put shared read-only data in **`setUpTestData(cls)`** (runs once per class inside the outer transaction), not `setUp` (per test). Only the fields the assertion needs.
16
+
17
+ ## Assertions
18
+
19
+ - `self.assertEqual(response.json(), {...})` / `assertEqual(obj.status, "paid")` — full value, not `assertTrue(obj)` or a lone `Model.objects.count()`.
20
+ - Use the specific ones for signal: `assertQuerySetEqual`, `assertContains`/`assertNotContains` (body + status), `assertRaisesMessage`, `assertNumQueries` (guard a hot path).
21
+ - **`obj.refresh_from_db()` before asserting persisted state** — the in-memory instance hides DB defaults, signals, `auto_now`.
22
+
23
+ ## Externals
24
+
25
+ - **`unittest.mock.patch`** where the name is *used* (`patch("myapp.services.stripe_client")`), not where defined.
26
+ - **Email:** assert on `django.core.mail.outbox` (the test runner's `locmem` backend), don't mock `send`.
27
+ - **HTTP:** `responses` / `respx` — never a real outbound call.
28
+ - **Settings/time:** `@override_settings(...)` / `self.settings(...)`; `freezegun` or an injected clock.
29
+ - **Test client:** `self.client` exercises the real URL/middleware/view stack — good. Mock only the external service the view calls, not the view.
30
+
31
+ ## Pitfalls
32
+
33
+ - `TransactionTestCase` "to be safe" — slower for no reason. Default to `TestCase`.
34
+ - `on_commit` hooks never fire under `TestCase` (the transaction rolls back) — use `TestCase.captureOnCommitCallbacks(execute=True)` or `TransactionTestCase`.
35
+ - A lone `assertEqual(Model.objects.count(), 1)` — asserts a count, not that the right object with the right fields exists.
36
+ - Asserting on `obj` after `save()` without `refresh_from_db()`.
@@ -0,0 +1,34 @@
1
+ # FastAPI stack pack
2
+
3
+ FastAPI-specific testing — the test client, dependency overrides, async, DB isolation. Loaded **only when the project is FastAPI**, alongside the runner pack (usually pytest). Read with the skill's universal rules.
4
+
5
+ ## Test client & dependencies
6
+
7
+ - **`TestClient(app)`** (Starlette, sync) exercises the real routing/validation/response stack. For async-native tests use **`httpx.AsyncClient(transport=ASGITransport(app=app))`**.
8
+ - **Override dependencies with `app.dependency_overrides[dep] = fake`**, not by monkeypatching internals — inject a test DB session, a stubbed auth principal, a fake external client. Clear overrides in teardown (a fixture that yields then pops).
9
+ - Don't mock the endpoint function; drive it through the client and override only what crosses the boundary.
10
+
11
+ ## Async
12
+
13
+ - Use `pytest-asyncio` (`@pytest.mark.asyncio`) or `anyio`. Await the real coroutine; don't wrap sync mocks around async code. Mock async collaborators with `AsyncMock`.
14
+
15
+ ## DB isolation
16
+
17
+ - Inject the DB session via a dependency and override it with a **session bound to a transaction rolled back in teardown** (SQLAlchemy: connection → `begin()` → session → rollback). Each test starts clean; no manual delete.
18
+ - Prefer a real (SQLite/Postgres) test DB over mocking the ORM — mocking the session tests the mock.
19
+
20
+ ## Externals
21
+
22
+ - **HTTP:** `respx` (httpx) / `responses` (requests) — never a real outbound call.
23
+ - **Settings:** override via a `Settings` dependency or env fixture, not by mutating a global.
24
+ - **Time/randomness:** `freezegun` or an injected clock; seed `random`.
25
+
26
+ ## Assertions
27
+
28
+ - Assert `response.status_code` **and** `response.json()` by full value — not just the status. Validate against the response model's shape, not a lone key.
29
+
30
+ ## Pitfalls
31
+
32
+ - Forgetting to clear `app.dependency_overrides` — leaks a fake into the next test.
33
+ - Mixing sync `TestClient` with async DB drivers — use `AsyncClient` + async session instead.
34
+ - Asserting only the status code while the body is wrong.
@@ -0,0 +1,31 @@
1
+ # Flask stack pack
2
+
3
+ Flask-specific testing — the test client, app/request context, DB isolation. Loaded **only when the project is Flask**, alongside the runner pack (usually pytest). Read with the skill's universal rules.
4
+
5
+ ## App, client & context
6
+
7
+ - Build the app from an **app factory** with a test config (`create_app({"TESTING": True, ...})`) so each test gets a clean instance; expose it as a fixture.
8
+ - **`app.test_client()`** exercises the real routing/blueprint/error-handler stack — drive endpoints through it, don't call view functions directly.
9
+ - Code that touches `current_app` / `g` / `session` needs an active context — use `with app.app_context():` / `with app.test_request_context():` in the fixture. Don't fake the context.
10
+
11
+ ## DB isolation
12
+
13
+ - **SQLAlchemy:** bind the session to a connection-level transaction rolled back in teardown (connection → `begin()` → scoped session → `rollback` + `close`). Each test starts clean; no manual delete.
14
+ - Prefer a real test DB over mocking the ORM.
15
+
16
+ ## Externals
17
+
18
+ - **Auth:** log in through the client (post to the login route or set the session via `client.session_transaction()`), not by patching the auth check.
19
+ - **HTTP:** `responses` / `respx` — never a real outbound call.
20
+ - **Extensions (mail, tasks):** use the extension's test/suppress mode (`mail.record_messages()`, eager Celery) and assert on what it captured, rather than mocking send.
21
+ - **Time/randomness:** `freezegun` or an injected clock; seed `random`.
22
+
23
+ ## Assertions
24
+
25
+ - Assert `response.status_code` **and** the body (`response.get_json()` / rendered content) by full value. For JSON APIs assert the whole payload, not one key.
26
+
27
+ ## Pitfalls
28
+
29
+ - Working outside an app/request context and getting `RuntimeError: working outside of ...` — push the right context in the fixture.
30
+ - Reusing one app instance with mutable global state across tests — the factory per test avoids it.
31
+ - Asserting only the status code while the body is wrong.
@@ -0,0 +1,54 @@
1
+ // @ts-nocheck
2
+ // Bundled verbatim from vintasoftware/medplum-snippet-catalog → TestSetup/test.globalSetup.ts
3
+ // Source: https://github.com/vintasoftware/medplum-snippet-catalog/tree/main/TestSetup
4
+ // Retrieved: 2026-07-31. The catalog is canonical — if it has advanced past this copy, prefer it.
5
+ // Install as (e.g.) `src/test.globalSetup.ts` and register in Vitest config `test.globalSetup`.
6
+
7
+ // Assert that the versions of @medplum/core, @medplum/definitions, @medplum/mock and
8
+ // @medplum/fhirtypes match.
9
+ import type { IndexedStructureDefinition } from '@medplum/core';
10
+ import type { Bundle, SearchParameter } from '@medplum/fhirtypes';
11
+ import type { TestProject } from 'vitest/node';
12
+
13
+ export default async function setup(project: TestProject): Promise<void> {
14
+ // @medplum/core v5 references WebSocket at module-load time. This file runs in the Vitest
15
+ // main process, where WebSocket is only guaranteed as a global on Node >= 22.4. It has to be
16
+ // polyfilled *before* @medplum/core is loaded, which is why the imports below are dynamic and
17
+ // only the (erased) type imports sit at the top of the file.
18
+ if (typeof globalThis.WebSocket === 'undefined') {
19
+ try {
20
+ const ws = await import('ws');
21
+ (globalThis as Record<string, unknown>).WebSocket = ws.default ?? ws.WebSocket;
22
+ } catch {
23
+ throw new Error(
24
+ '@medplum/core needs a global WebSocket. Upgrade to Node >= 22.4, or add "ws" as a dev dependency.'
25
+ );
26
+ }
27
+ }
28
+
29
+ const { globalSchema, indexSearchParameterBundle, indexStructureDefinitionBundle } = await import('@medplum/core');
30
+ const { readJson, SEARCH_PARAMETER_BUNDLE_FILES } = await import('@medplum/definitions');
31
+
32
+ // Initialize FHIR
33
+ indexStructureDefinitionBundle(readJson('fhir/r4/profiles-types.json') as Bundle);
34
+ indexStructureDefinitionBundle(readJson('fhir/r4/profiles-resources.json') as Bundle);
35
+ indexStructureDefinitionBundle(readJson('fhir/r4/profiles-medplum.json') as Bundle);
36
+ for (const filename of SEARCH_PARAMETER_BUNDLE_FILES) {
37
+ indexSearchParameterBundle(readJson(filename) as Bundle<SearchParameter>);
38
+ }
39
+
40
+ // `globalSchema.types` is plain JSON-serializable data, so it can be handed to every test
41
+ // worker instead of each one re-reading and re-indexing the search parameter bundles.
42
+ // See test.setup.ts for the consuming side.
43
+ project.provide('globalSchemaTypes', globalSchema.types);
44
+ }
45
+
46
+ export function teardown(): void {
47
+ // Nothing to do.
48
+ }
49
+
50
+ declare module 'vitest' {
51
+ export interface ProvidedContext {
52
+ globalSchemaTypes: IndexedStructureDefinition['types'];
53
+ }
54
+ }