Skip to content
Merged
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
32 changes: 16 additions & 16 deletions .agents/docs/architecture/auth-and-permissions.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .agents/docs/architecture/instrument-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { createRoot } from '/runtime/v1/react-dom@19.x/client.js';

These resolve differently on each tier and are covered in
`.agents/docs/architecture/runtime-and-vendor.md`. Sibling files use ordinary relative imports
(`'./styles.css'`, `'./StroopTask.tsx'`).
(`'./styles.css'`, `'./Component.tsx'`).

Three places produce instrument source:

Expand Down
8 changes: 4 additions & 4 deletions .agents/docs/architecture/runtime-and-vendor.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Six workspaces have `runtime` in the name and they do different jobs. This file tells them apart,
explains the three unrelated ways `runtime/v1` is spelled in an import, and documents `vendor/`.

**`runtime/v1/dist` is gitignored and nothing works until it is built.** Nine `tsconfig.json` files
**`runtime/v1/dist` is gitignored and nothing works until it is built.** Eight `tsconfig.json` files
map a path into it, so `pnpm lint` fails across most of the repo on a cold checkout. Build it with
`pnpm --filter @opendatacapture/runtime-v1 build`, or just `pnpm build` (turbo's `^build` ordering
gets there).
Expand Down Expand Up @@ -59,7 +59,7 @@ path**, which only resolves because they are copied byte-for-byte (see
### `packages/runtime-bundler`

Ships TypeScript source directly (`exports: "./src/index.ts"`, `bin` → `src/cli.ts` under tsx). The
CLI reads `runtime.config.js` from the working directory, validates it with `$UserConfigs`, and for
CLI reads `runtime.config.js` from the working directory, validates it with `$Config`, and for
each `include` entry resolves `<pkg>/package.json` through a `createRequire` rooted at the config
file. **An entry in `include` must therefore also be a dependency of the workspace holding the
config.**
Expand Down Expand Up @@ -120,7 +120,7 @@ The same artifact is addressed three ways. They are not interchangeable.

| Spelling | Resolved by | Declared in |
| ----------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/runtime/v1/<pkg>` | the browser, over HTTP | served by `vite-plugin-runtime` (dev) or the copied `dist/runtime/v1` (prod); type-checked via `paths` in eight `tsconfig.json` files; marked external by `packages/instrument-bundler/src/plugin.ts` |
| `/runtime/v1/<pkg>` | the browser, over HTTP | served by `vite-plugin-runtime` (dev) or the copied `dist/runtime/v1` (prod); type-checked via `paths` in seven `tsconfig.json` files; marked external by `packages/instrument-bundler/src/plugin.ts` |
| `#runtime/v1/*` | Node subpath imports | `apps/api/package.json` `imports` **and** `apps/api/tsconfig.json` `paths` |
| `@opendatacapture/runtime-v1` | ordinary package resolution | a `dependencies` entry, used to locate `dist` on disk |

Expand Down Expand Up @@ -195,6 +195,6 @@ The value of an entry in a wrapper's `exports` decides how it is built. See
| A vendor directory name `<name>@<ver>` ↔ its `package.json` `name` `<name>__<ver>` | wrong output URL; `vendor-pairing.test.ts` cannot find the paired wrapper |
| A wrapper's sibling-wrapper dependency ↔ its pinned real peer | duplicate library instances at runtime; caught by `vendor-pairing.test.ts` |
| `packages/runtime-core/package.json` `exports` ↔ what its three build stages emit | a subpath resolves to a file that no longer exists |
| The eight `tsconfig.json` files mapping `/runtime/v1/*` | that workspace stops type-checking instrument imports |
| Every `tsconfig.json` mapping `/runtime/v1/*` | that workspace stops type-checking instrument imports |
| `jsxImportSource: '/runtime/v1/react@19.x'` in `packages/instrument-library/tsconfig.json` and `apps/playground/src/components/Editor/setup.ts` ↔ the react a bundle is built against, which `packages/instrument-bundler/src/build.ts` derives per instrument | the editor or `tsc` type-checks against a different React than the bundle uses |
| `src/index.js` ↔ `src/index.d.ts` in `runtime-internal` and `runtime-meta` | hand-written declarations; nothing checks them against the implementation |
8 changes: 2 additions & 6 deletions .agents/docs/architecture/testing-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ Touching any of them invalidates the cache for every task in the repo.
`coverage.thresholds: { 100: true }`, so the run fails when statements, branches, functions or lines
drop below 100%. The scope is its `coverage.include` — `apps/{api,gateway,web}/src` and
`packages/*/src`; `runtime/**` and `apps/playground` are outside it — minus `coverage.exclude`,
which lists only non-code: test fixtures and helpers (`__tests__`, `__mocks__`,
`apps/web/src/testing`), the generated `route-tree.ts`, and the bare entrypoints.
which lists only non-code: test fixtures and helpers (`__tests__`, `apps/web/src/testing`), the
generated `route-tree.ts`, and the bare entrypoints.

- **A line no test can reach is dead code.** Prove it (the types, every caller, the library's
contract) and delete it, or restructure so the impossible state cannot be written — e.g. ts-pattern
Expand Down Expand Up @@ -186,9 +186,5 @@ Flag these rather than fixing them in passing — each one is load-bearing somew
pnpm location; `@opendatacapture/api#db:generate` is therefore declared `cache: false` so a cache
hit never restores an empty output set. Changing either half changes what a `db:generate` run
produces.
- `packages/instrument-bundler/vitest.config.ts` aliases `/runtime/v1` to
`packages/instrument-bundler/runtime/v1/dist`, a directory that does not exist. It is inert today:
the fixtures under `src/__tests__/repositories/` are read as raw strings and handed to the bundler,
never resolved by vitest. Do not rely on the alias.
- Root `test` is `env-cmd vitest` but `test:coverage` is `vitest --coverage` with no `env-cmd`, so
the two do not run under the same environment.
2 changes: 1 addition & 1 deletion .agents/docs/packages/libjs.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A collection of utility functions and types for Node.js and the browser.

**Status in Open Data Capture:** the most widely spread DNP package here — imported in ~40 files across `apps/api`, `apps/web`, `apps/playground`, `packages/demo`, `packages/instrument-utils`, `packages/react-core`, `packages/runtime-bundler`, `packages/schemas`, `packages/serve-instrument`, and `testing`. (`packages/instrument-stubs` and `packages/vite-plugin-runtime` also declare it, with no current imports.)
**Status in Open Data Capture:** the most widely spread DNP package here — imported in ~40 files across `apps/api`, `apps/web`, `apps/playground`, `packages/demo`, `packages/instrument-stubs`, `packages/instrument-utils`, `packages/react-core`, `packages/runtime-bundler`, `packages/schemas`, `packages/serve-instrument`, and `testing`.

Single root export; there are no subpaths.

Expand Down
4 changes: 2 additions & 2 deletions .agents/docs/packages/libui.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Generic UI components for DNP projects, built with React and Tailwind CSS.

**Status in Open Data Capture:** used extensively — ~194 files, concentrated in `apps/web` (105) and `apps/playground` (52), plus `packages/react-core` (31), `apps/gateway`, `apps/outreach`, and `packages/serve-instrument`. It is the primary source of UI components, hooks, i18n, and theming for every frontend surface. (`storybook` declares it as a dependency but imports it only through the components it renders.)
**Status in Open Data Capture:** used extensively — ~194 files, concentrated in `apps/web` (105) and `apps/playground` (52), plus `packages/react-core` (31), `apps/gateway`, `apps/outreach`, and `packages/serve-instrument`. It is the primary source of UI components, hooks, i18n, and theming for every frontend surface.

There is no root `.` export — always import from a subpath.

Expand Down Expand Up @@ -102,7 +102,7 @@ ls apps/web/node_modules/@douglasneuroinformatics/libui/src/hooks
cat apps/web/node_modules/@douglasneuroinformatics/libui/src/utils/index.ts
```

Also resolvable from `apps/gateway`, `apps/playground`, `apps/outreach`, `packages/react-core`, `packages/serve-instrument`, and `storybook`.
Also resolvable from `apps/gateway`, `apps/playground`, `apps/outreach`, `packages/react-core`, and `packages/serve-instrument`.

## Docs

Expand Down
12 changes: 5 additions & 7 deletions .agents/docs/playbooks/add-e2e-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ Mongo replica set.
(`testing/src/pages/auth/login.page.ts` is the example). Read
`testing/src/pages/_app/session/start-session.page.ts` for the shape.

4. **Register it in the `pageModels` map in `testing/src/support/fixtures.ts`**, keyed by the exact
route literal — e.g. `'/datahub/$subjectId/table': SubjectDataTablePage`. The map is
`satisfies { [K in RouteTo]?: any }`, so a key that is not a real route fails `tsc`. A page object
missing from the map is unreachable from a spec.
4. **Register it in the `pageModels` map in `testing/src/support/fixtures.ts`** when the spec
navigates to it with `getPageModel`, keyed by the exact route literal — e.g.
`'/datahub/$subjectId/assignments': SubjectAssignmentsPage`. The map is
`satisfies { [K in RouteTo]?: any }`, so a key that is not a real route fails `tsc`. A page the
spec reaches by clicking through the app needs no entry; construct it with `new XPage(page)`.

5. **Write the spec** at `testing/src/specs/<flow>.spec.ts`, importing from the fixtures module:

Expand Down Expand Up @@ -80,6 +81,3 @@ pnpm --filter @opendatacapture/testing test:e2e src/specs/<flow>.spec.ts # one
pnpm --filter @opendatacapture/testing test:dev # Playwright UI mode
pnpm test:e2e # whole suite, from repo root
```

Ignore the `test:chrome` script: its `--project='*Desktop Chrome'` matches none of the four project
names and errors immediately.
8 changes: 5 additions & 3 deletions .agents/docs/playbooks/add-env-var.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ below in order; each skipped one type-checks and compiles, then produces `undefi
`env-cmd -f ../../.env`, which is why only build-time reads need this step.

8. **If the browser needs it, three files in `apps/web` must agree:** `apps/web/.env.public` (the
manifest `@import-meta-env/unplugin` and the `inject`/`start` scripts read — values are substituted
into `dist/index.html` after the build, not baked in), the `ImportMetaEnv` interface in
`apps/web/src/vite-env.d.ts`, and the parsed `config` object in `apps/web/src/config.ts`.
manifest `@import-meta-env/unplugin`, the `start` script and the web Docker image's
`import-meta-env` read — values are substituted into `dist/index.html` after the build, not baked
in), the `ImportMetaEnv` interface in `apps/web/src/vite-env.d.ts`, and the parsed `config` object
in `apps/web/src/config.ts` (unless only the inline script in `apps/web/index.html` reads it, as
`PLAUSIBLE_*` are).

9. **If it must reach the production stack, add it to `docker-compose.yaml`** under the `environment:`
list of the service that reads it (`api`, `gateway`, `web`). A bare `- YOUR_KEY` forwards the value
Expand Down
7 changes: 3 additions & 4 deletions .agents/docs/playbooks/add-instrument.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ advice does not apply here; everything it says about the definition object does.
| `kind` in the definition | Directory | Read first |
| ------------------------ | -------------------------- | -------------------------------------------------------------- |
| `'FORM'` | `src/forms/` (plural) | `src/forms/DNP_HAPPINESS_QUESTIONNAIRE/index.ts` |
| `'INTERACTIVE'` | `src/interactive/` | `src/interactive/DNP_STROOP_TASK/` (multi-file JSX) |
| `'INTERACTIVE'` | `src/interactive/` | `src/interactive/DNP_BREAKOUT_TASK/index.ts` |
| `'SERIES'` | `src/series/` | `src/series/DNP_HAPPINESS_QUESTIONNAIRE_WITH_CONSENT/index.ts` |
| `'FILE'` | `src/file/` (**singular**) | `src/file/MRI_SCAN_SESSION/index.ts` |

2. **Add `index.ts` (or `index.tsx` for JSX) with a default export.** The bundler globs
`src/**/*/index.{js,jsx,ts,tsx}`, so the directory is the unit of work and `index.*` is the only
fixed filename. **The directory must be flat**: the CLI then globs `<dir>/*` and reads every entry
as a file, so a subdirectory fails the build with a `Cannot infer loader` error. Relative imports
need their extension (`'./StroopTask.tsx'`, `'./styles.css'`).
need their extension (`'./styles.css'`, `'./Component.tsx'`).

3. **Import everything from `/runtime/v1/...`**, never from `node_modules` or a workspace package:

Expand Down Expand Up @@ -68,8 +68,7 @@ advice does not apply here; everything it says about the definition object does.

then `await this.instrumentsService.create({ bundle: myNewForm })` inside `init`. **Order in
`init` is load-bearing**: a series must be created after every instrument it references (step 5).
Skipping this step compiles, builds, and produces an instrument that is never in a demo instance —
`DNP_STROOP_TASK` is the live example of that.
Skipping this step compiles, builds, and produces an instrument that is never in a demo instance.

7. **Add a unit test and an end-to-end test.** The unit test goes in the top-level
`packages/instrument-library/src/__tests__/`, never inside the instrument directory (§Tests in
Expand Down
1 change: 0 additions & 1 deletion .agents/docs/playbooks/add-web-data-hook.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ step type-checks and compiles, and fails at runtime or serves the wrong cached d

| `meta` key | Use when |
| --------------------------------- | --------------------------------------------- |
| `disableRetry` | An idempotent request must not be retried |
| `disableDefaultTimeout` | The server may legitimately take >10s (setup) |
| `disableDefaultErrorNotification` | The caller shows its own error message |
| `disableDefaultAuth` | The request must go out unauthenticated |
Expand Down
2 changes: 1 addition & 1 deletion .agents/docs/playbooks/promote-to-react-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ so nothing that passed lint in `apps/web` fails on style here; what breaks is re
becomes an injected component, an `href`, or data passed in: `InstrumentRenderer`'s
`NavigationBlocker?: NavigationBlockerComponent`, implemented by
`apps/web/src/components/NavigationBlocker.tsx`, is the pattern, and `packages/react-core/AGENTS.md`
carries the rule and its exceptions. Declaring `@tanstack/react-query` here instead would make the
carries the rule. Declaring `@tanstack/react-query` here instead would make the
import resolve and the typecheck pass; the throw arrives only at render, which nothing before the
Verify block's `pnpm test:e2e` reaches — and only where step 9's second consumer renders it.

Expand Down
15 changes: 7 additions & 8 deletions .agents/docs/playbooks/run-locally.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,15 @@ that never says the word storage. Step 4 is where you avoid it.
Done when `node --version` is v24.15.0 or newer and `pnpm --version` prints.

3. **Generate `.env` — once per clone.** `pnpm generate:env` runs `scripts/generate-env.sh`, which
writes `.env` wholesale from `.env.template`: it fills `PROJECT_ROOT`, mints `SECRET_KEY`,
`GATEWAY_API_KEY`, `STORAGE_ACCESS_KEY` and `STORAGE_SECRET_KEY` with `openssl rand -hex`, and
points `GATEWAY_DATABASE_URL` at a sqlite file under `apps/gateway/data` (creating the
directory). Never re-run it over a working `.env`: it discards every local override and mints a
fresh `SECRET_KEY`, which invalidates live sessions and permanently orphans any stored
instrument-repo credential — `InstrumentReposService` encrypts those under `sha256(SECRET_KEY)`.
writes `.env` wholesale from `.env.template`: it mints `SECRET_KEY`, `GATEWAY_API_KEY`,
`STORAGE_ACCESS_KEY` and `STORAGE_SECRET_KEY` with `openssl rand -hex`, and points
`GATEWAY_DATABASE_URL` at a sqlite file under `apps/gateway/data` (creating the directory). Never
re-run it over a working `.env`: it discards every local override and mints a fresh `SECRET_KEY`,
which invalidates live sessions and permanently orphans any stored instrument-repo credential —
`InstrumentReposService` encrypts those under `sha256(SECRET_KEY)`.
Adding one variable to an existing `.env` is `.agents/docs/playbooks/add-env-var.md`.

Done when `.env` exists, `PROJECT_ROOT` is the absolute repo root, and none of the four generated
keys is empty.
Done when `.env` exists and none of the four generated keys is empty.

4. **Set `STORAGE_ENABLED=false` in `.env`** unless an S3-compatible service is listening on
`STORAGE_ENDPOINT`. With it false, `StorageModule` provides `null` for the `S3Client`,
Expand Down
Loading
Loading