Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .claude/skills/api-design-guardian/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
name: api-design-guardian
description: Use when writing or changing a public API or SDK surface — GraphQL schemas (.graphql/.graphqls), REST/OpenAPI endpoints, SDK request/response types, or any client-facing contract. Enforces b.well's API-design bar (both the positive "what good looks like" and the antipatterns) before the change reaches PR, and treats external contract changes as breaking by default.
---

# API Design Guardian

You enforce b.well's API/SDK design standard. APIs are contracts; a bad one costs months of client updates. Catch problems **before** the PR. Be direct — a senior architect who explains *why* and shows the fix.

The standard is in the substrate — read and cite it, don't restate it:
- **What good looks like:** `reference-architectures/good-api-sdk.md` (grounded in the real bwell-sdk review bar).
- **Gradeable criteria:** `rubrics/api-design-rubric.md` — cite the criterion anchor (e.g. `#rub-api-breaking`) in every finding.
(These live in `icanbwell/.github`; read them there if not in the current repo.)

## Lead with the positive bar

Before hunting smells, hold the change to the good-API bar (`reference-architectures/good-api-sdk.md`): **layer purity** (no Apollo/generated/vendor types in domain/request objects — map at the boundary), **vendor neutrality** (no vendor error text/codes leak through the surface), **consistency** across request families and across the Kotlin/TS/Swift SDKs (diff a new object against `ConsentRequest`; use the shared `BWellResult` wrapper), **fail-fast typed errors** (validate in the builder with `require`; a closed neutral error enum with an `unknown`/default branch), **DI over singletons**, **interfaces over abstract classes**.

## Highest-priority: contract stability

**Treat any change to an externally- or downstream-consumed field, type, enum, or identifier as breaking by default** (`#rub-api-breaking`). Require deprecate-and-add or a new version, plus a consumer-driven contract test in the same PR. Real cost: an external id changed slug→UUID and silently broke a client for 34 days (INC-329).

**Enum resilience is a hard check** (`#rub-api-enum-default`): flag any exhaustive `switch`/`when` over a server-driven enum with **no `default`/`unknown` branch** — that exact bug broke the Swift SDK build *twice* (EA-2401 / DCON-4580). Additive-only evolution otherwise.

## Antipatterns to catch (with the fix)

- **Boolean-flag params** (`skipDeleted: Boolean`) → a status/filter **enum** with a default; flags don't compose (`#rub-api-boolean-flags`).
- **Internal/generated types as responses** (`*Entity`/`*Model`/`*DTO`/Apollo types) → a purpose-built contract type; ties to layer purity (`#rub-api-internal-types`).
- **Unbounded lists** → cursor pagination (Relay `Connection`) or offset for simple cases; never return unbounded arrays (`#rub-api-pagination`).
- **Naming drift** — non-singular request names, `I`-prefix/`Impl` suffix, inconsistent booleans → match the family template and cross-SDK contract (`#rub-api-consistency`).
- **Gateway bypass** — client-facing access not through the federated graph; local type names colliding with federated canonicals (INE-1001) (`#rub-api-gateway`).

## Verify against reality

Behavior is confirmed against the live resolver/dev, the federated graph is the source of truth, and SDK releases are gated on the backing change being in prod (`#rub-api-verify`). Public API changes carry a Tech Design Review.

## When to intervene / not

Intervene on new/changed `.graphql(s)`, OpenAPI, SDK request/response types, or query params. Don't intervene when the user is only reading schemas, writing tests, or the change is internal-only (not client-facing). When you flag something, name the rubric anchor and show the corrected snippet.
102 changes: 102 additions & 0 deletions .claude/skills/cleanup-descope-e2e-data/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
name: cleanup-descope-e2e-data
description: Use when asked to clean up, delete, or purge leftover Descope E2E test data — the "E2E Test Clinic" tenants and their users that ui-tests-descope leaves behind (no auto-cleanup; the test stack's management key is IP-restricted, error E023017). Drives the Descope MCP tools.
---

# Clean up Descope E2E test data

## Overview

`make ui-tests-descope` signs up real tenants + users in the live Descope project
(`dev-kill-the-clipboard-scanner`) and **cannot clean up after itself** — the test stack's
management key is IP-restricted (`E023017`). This skill deletes that residue through the
**Descope MCP** tools (a different, working credential path).

E2E data is named with a stable prefix:
- **Tenants:** `E2E Test Clinic <unique>`
- **Users:** owner `E2E Test User <unique>` + invitee, both with `*@inbox.testmail.app` login IDs,
and (for fresh test data) belonging **only** to their E2E tenant.

**Core mechanism:** `DeleteTenant` with `cascade: true` deletes users attached *only* to that
tenant and merely detaches shared ones — so deleting an `E2E` tenant removes its users in one
call without ever touching a user who also belongs to a protected tenant.

## NEVER delete (hard guardrails)

- **Wrong project.** Only ever run against project `dev-kill-the-clipboard-scanner`
(id `P3EgKEuFdQFLSuhXYk7KNsvlroTd`). If `whoami` shows any other project — **especially
production** — STOP and tell the user. Never `selectProject` to a prod project to delete.
- **`test-tenant-001` and `test-tenant-002`** — the seeded local multi-tenant dev fixtures.
- **Any tenant whose name does not start with `E2E`** (case-sensitive prefix). When in doubt, skip it.

## Workflow

This is a **destructive, confirmation-gated** operation. Do the read/plan steps, show the user
exactly what will be deleted, get an explicit "yes", then elevate and delete.

### 1. Confirm the project (read-only)
```
session({action: "whoami"})
```
Verify `project` / `projectId` is `dev-kill-the-clipboard-scanner` / `P3EgKEuFdQFLSuhXYk7KNsvlroTd`.
If not → STOP, report to user.

### 2. Find E2E tenants (read-only, no elevation)
```
tenants_read({operation: "LoadAllTenants"})
```
Filter `data.tenants` to those whose `name` starts with `E2E`. There is no server-side prefix
filter — filter client-side. Exclude everything in the guardrails list above.

### 3. Enumerate each tenant's members (read-only) — for the report + leftover sweep
```
users_read({operation: "SearchUsers", args: {tenantIds: ["<tenantId>"], limit: 100}})
```
Collect `userId`, `email`, and `userTenants` for each. (`userTenants` with a single entry ⇒
`cascade` will delete that user; multiple entries ⇒ it will only be detached.)

### 4. Present the plan and get explicit confirmation
List each tenant (`name` + `id`) and its users (`email` + `userId`). Ask the user to confirm,
citing the exact targets. **Wait for an affirmative reply.** Do not elevate before this.

### 5. Elevate write mode (only after confirmation)
```
session({action: "elevate", args: {reason: "<verbatim user request>"}})
```

### 6. Delete each tenant (cascades to single-tenant users)
```
tenants_write({operation: "DeleteTenant", args: {id: "<tenantId>", cascade: true}})
```

### 7. Sweep leftover E2E users (only if step 3 found shared/multi-tenant users `cascade` left behind)
Re-run `SearchUsers` (e.g. `args: {text: "E2E", limit: 100}`), keep only users still present whose
`name`/`email` is unmistakably E2E test data, then:
```
users_write({operation: "DeleteUsers", args: {userIds: ["<userId>", ...]}})
```

### 8. Verify
Re-run `tenants_read({operation: "LoadAllTenants"})` and confirm no `E2E`-prefixed tenants remain
(and `test-tenant-001/002` are untouched).

## Quick reference

| Step | MCP call | Elevation? |
|------|----------|------------|
| Check project | `session(whoami)` | no |
| List tenants | `tenants_read(LoadAllTenants)` | no |
| List a tenant's users | `users_read(SearchUsers, {tenantIds:[id]})` | no |
| Unlock writes | `session(elevate, {reason})` | — |
| Delete tenant + its users | `tenants_write(DeleteTenant, {id, cascade:true})` | yes |
| Delete specific users | `users_write(DeleteUsers, {userIds:[...]})` | yes |

## Common mistakes

- **Skipping the project check.** `whoami` first, every time. Deleting from the wrong project is unrecoverable.
- **Substring instead of prefix match.** Match names that *start with* `E2E`, not just contain it.
- **Elevating before confirmation.** The MCP elevation contract requires discovery → prepare →
ask → elevate. Never elevate autonomously.
- **Expecting `DeleteTenant` to delete every user.** It only deletes users attached solely to that
tenant; shared users are detached. Use the step-7 sweep for any genuinely-E2E shared/orphaned users.
- **Forgetting verification.** Re-list tenants at the end to prove the cleanup worked.
136 changes: 136 additions & 0 deletions .claude/skills/codeable-concept-mapping/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
name: codeable-concept-mapping
description: >
Update the CodeableConcept field mapping generator script in the sensitive-data-tagger package.
Use when modifying the list of target FHIR resources, adjusting depth, adding/removing field types,
or changing the output format. Also use when the FHIR spec XSD files are updated.
Triggers on: "update mapping", "add resource to mapping", "regenerate codeable concept",
"update sensitive fields", "change mapping script".
---

# CodeableConcept Field Mapping Generator

## What It Does

`packages/sensitive-data-tagger/generatorScripts/generate_codeable_concept_mapping.py` generates a TypeScript file that maps FHIR resource types to their CodeableConcept and Coding field paths. These are fields that can carry sensitive clinical codes (CPT, HCPCS, ICD, SNOMED, LOINC, NDC, RxNorm).

## How It Works

1. Uses `fhir_xml_schema_parser.py` to parse the FHIR R4B (v4.3.0) XML spec (`xsd/definitions.xml/`)
2. For each target resource, recursively traverses properties up to `MAX_DEPTH` levels
3. Collects fields where type is `CodeableConcept` or `Coding`
4. Resolves reusable complex types (e.g., `Dosage.route` inside `MedicationRequest.dosageInstruction`)
5. Outputs a TypeScript file with `export const SensitiveCodeableConceptFieldMapping`

## Key Files

| File | Purpose |
| -------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `packages/sensitive-data-tagger/generatorScripts/generate_codeable_concept_mapping.py` | The generator script |
| `packages/sensitive-data-tagger/generatorScripts/fhir_xml_schema_parser.py` | FHIR XML schema parser (from fhir-server, do not edit) |
| `packages/sensitive-data-tagger/generatorScripts/xsd/` | FHIR R4B XSD spec files (from fhir-server, do not edit) |
| `packages/sensitive-data-tagger/src/sensitiveCodeableConceptFieldMapping.generated.ts` | Generated output (do not hand-edit) |

## Regenerating

```bash
cd packages/sensitive-data-tagger
make generate-codeable-concept-mapping
```

Requires: Docker (the Makefile runs the Python script inside a `python:3.12-alpine` container that auto-installs `lxml`). No local Python setup needed.

## Script Configuration

These are at the top of `generate_codeable_concept_mapping.py`:

### Target Resources

The `TARGET_RESOURCES` list defines which FHIR resources to include. To add a new resource, simply append it to the list. All resource names must match the FHIR R4 spec exactly (PascalCase).

### Depth

`MAX_DEPTH = 4` — controls how deep to traverse nested fields. Depth is counted from the resource root:

- Depth 1: `Condition.code`
- Depth 2: `Condition.stage.summary`
- Depth 3: `Claim.item.detail.productOrService`
- Depth 4: `Claim.item.detail.subDetail.productOrService`

### Skipped Types

The script skips: `Extension`, `ModifierExtension`, `Identifier`, `Reference`, `Resource`, `ResourceContainer`, and the `meta` field. These either cause infinite recursion or don't carry clinical codes.

### Coded Types

`CODED_TYPES = {'CodeableConcept', 'Coding'}` — fields matching these types are collected. CodeableConcept contains Coding inside it, so when a CodeableConcept is found, the script stops recursing (doesn't duplicate the inner `.coding` field).

## Output Format

```typescript
export const SensitiveCodeableConceptFieldMapping: Record<
string,
{ CodeableConcept?: string[]; Coding?: string[] }
> = {
Condition: {
CodeableConcept: [
'bodySite',
'category',
'clinicalStatus',
'code',
'evidence.code',
'severity',
'stage.summary',
'stage.type',
'verificationStatus',
],
},
Encounter: {
CodeableConcept: ['diagnosis.use', 'type' /* ... */],
Coding: ['class', 'classHistory.class'],
},
};
```

- **CodeableConcept fields**: contain `coding[]` array + `text` field
- **Coding fields**: standalone single codes (e.g., `Encounter.class`)
- Field paths are dot-separated relative to the resource (e.g., `dosageInstruction.route`)

## Reused Infrastructure

The script reuses patterns from `fhir-server/generatorScripts/generate_everything_operation_data.py`:

- `get_field_type_property()` — looks up a type's entity definition by name
- `handle_nested_fields()` — recursive traversal with skip patterns and cycle detection
- `primitive_types_dict` — from `FhirXmlSchemaParser.get_fhir_primitive_types()`
- `all_classes` — from `FhirXmlSchemaParser.generate_classes()`

## FHIR Binding Strength (Not in Output, but Useful Context)

Each CodeableConcept field has a binding strength in the FHIR spec:

- **required**: locked to a FHIR value set — CANNOT contain sensitive codes (e.g., `clinicalStatus`)
- **extensible**: should use value set, but CAN use other systems
- **preferred**: recommended codes, any system valid
- **example**: any code system — where sensitive codes primarily live (e.g., `Condition.code`)
- **unbound**: no binding defined — completely open

Only 6 fields across all 31 resources have `required` binding. The rest can potentially carry sensitive codes.

## Common Tasks

### Add a new FHIR resource

1. Add the resource name to `TARGET_RESOURCES` list (alphabetical order)
2. Run `make generate-codeable-concept-mapping`

### Update FHIR spec version

1. Copy updated XSD files from `fhir-server/generatorScripts/xsd/` to `generatorScripts/xsd/`
2. Copy updated `fhir_xml_schema_parser.py` from `fhir-server/generatorScripts/`
3. Run `make generate-codeable-concept-mapping`

### Change traversal depth

1. Edit `MAX_DEPTH` in the script
2. Run `make generate-codeable-concept-mapping`
Loading
Loading