Skip to content

Commit 1b21f18

Browse files
authored
Merge branch 'main' into armorer/m1-moderate-remediation
2 parents 4bd3115 + 9c32733 commit 1b21f18

17 files changed

Lines changed: 1310 additions & 293 deletions

File tree

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
node-version: 24
1919
- run: npm ci --ignore-scripts
2020
- run: npm run build
21-
- uses: actions/upload-artifact@v4
21+
- uses: actions/upload-artifact@v7
2222
with:
2323
name: build-output
2424
path: packages/*/dist/
@@ -38,7 +38,7 @@ jobs:
3838
node-version: 24
3939
registry-url: "https://registry.npmjs.org"
4040
scope: "@script-development"
41-
- uses: actions/download-artifact@v4
41+
- uses: actions/download-artifact@v8
4242
with:
4343
name: build-output
4444
path: packages

.oxlintrc.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "./node_modules/oxlint/configuration_schema.json",
3+
"plugins": ["typescript", "unicorn", "oxc"],
4+
"categories": {
5+
"correctness": "error"
6+
},
7+
"rules": {},
8+
"env": {
9+
"builtin": true
10+
}
11+
}

CLAUDE.md

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Shared frontend service packages monorepo under the `@script-development` npm sc
77
- **Language:** TypeScript 5.9+ (strict mode, `verbatimModuleSyntax`)
88
- **Build:** tsdown (Rolldown/oxc) — dual ESM + CJS output
99
- **Test:** vitest 4 (100% coverage threshold) + Stryker (90% mutation threshold)
10-
- **Lint:** oxlint
10+
- **Lint:** oxlint (explicit config at `.oxlintrc.json`)
1111
- **Format:** oxfmt
1212
- **Package lint:** publint + attw (Are The Types Wrong)
1313
- **Publish:** OIDC Trusted Publishing to public npm registry (no stored tokens)
@@ -54,10 +54,63 @@ Shared frontend service packages monorepo under the `@script-development` npm sc
5454

5555
**Build before typecheck.** Cross-package type resolution requires built `.d.mts` files. The CI pipeline enforces this order.
5656

57+
## Lint Rules
58+
59+
Lint configuration lives at `.oxlintrc.json` (repo-root, no per-package overrides). The explicit config declares three defaults so rule additions/removals land as a deliberate diff rather than silent upstream drift when oxlint bumps:
60+
61+
- **Plugins:** `typescript`, `unicorn`, `oxc` — the three plugins enabled by oxlint's own defaults.
62+
- **Categories:** `correctness: "error"` — all 107 Correctness rules fail CI (was `warn`, so violations were silently tolerated pre-config).
63+
- **`perf`, `suspicious`, `pedantic`, `style`, `restriction`, `nursery`:** unset — library posture is Correctness-only, opt-in per-rule for anything else.
64+
65+
To add a rule, set it in the `rules` object (e.g. `"perf/no-accumulating-spread": "error"`). To disable a default, set it to `"off"`. To opt into a whole category, add it to `categories` (be deliberate — `pedantic` has false positives, `nursery` is unstable). See `npx oxlint --rules` for the full catalog with default-on/off markers.
66+
5767
## Adding a Package
5868

5969
1. Create `packages/{name}/` with `package.json`, `tsconfig.json`, `tsdown.config.ts`, `vitest.config.ts`
6070
2. Name it `@script-development/fs-{name}`
6171
3. Use `defineProject` from `vitest/config` in the vitest config
6272
4. Add 100% coverage threshold and 90% mutation threshold
6373
5. Bump version in the new package's `package.json` (manual — no changeset `.md` files)
74+
75+
## War Room ADR Projections
76+
77+
Distilled operational rules from cross-project Architecture Decision Records. Canonical source: [adrs.script.nl](https://adrs.script.nl). This section is maintained by the War Room — do not edit directly.
78+
Last synced: 2026-04-17
79+
80+
### Applicable
81+
82+
#### ADR-0013: Adapter-Store Pattern
83+
84+
- Published here as `fs-adapter-store`. This territory is the canonical home of the pattern.
85+
- Preserve the reactive adapter-store contract: `createAdapterStoreModule()` factory returning a module with `resourceAdapter` for CRUD plus typed `Adapted<T>` / `NewAdapted<T>` records.
86+
- Changes to the pattern's surface (function signatures, exported types) are breaking for every consumer — treat them as major version decisions and coordinate with consumer territories (kendo, BIO).
87+
88+
#### ADR-0015: ADR Governance
89+
90+
- War Room ADRs are canonical at `adrs.script.nl`. Projections (this section) are distilled into territory CLAUDE.md by the War Room.
91+
- Do not amend projections in this file directly. Propose amendments through the war room; the update propagates here.
92+
- fs-packages is a full territory under the war room (not exempt like BIO).
93+
94+
#### ADR-0017: Page Integration Tests
95+
96+
- Kendo, BIO, and Entreezuil mock only `@script-development/fs-http` when running page integration tests. fs-http is the mock target; its public API (`createHttpService`, middleware hooks, `isAxiosError`) is the contract consumers depend on.
97+
- Do not introduce breaking changes to fs-http's public API without coordinating with consumer territories' mock-server infrastructure.
98+
99+
### Not Applicable (Library Territory Rationale)
100+
101+
The following cross-project ADRs do not apply to fs-packages because it has no Laravel/PHP backend, no HTTP API surface, no database, and no app-UI:
102+
103+
- **ADR-0001** Audit Logging — Laravel/DB-backed; N/A.
104+
- **ADR-0002** Cascade Deletion & Selective Soft Deletes — Laravel/DB-backed; N/A.
105+
- **ADR-0009** Unified ResourceData Pattern — Laravel JSON response shape; N/A.
106+
- **ADR-0011** Action Class Architecture — PHP `final readonly` action classes; N/A.
107+
- **ADR-0012** FormRequest → DTO Flow — Laravel request pipeline; N/A.
108+
- **ADR-0014** Domain-Driven Frontend Structure — App-level vertical slices by business domain; fs-packages is horizontal library infrastructure, not an app. N/A.
109+
- **ADR-0016** Config Attribute Injection — PHP `#[Config]` attribute; N/A.
110+
- **ADR-0019** Explicit Model Hydration — Eloquent model hydration; N/A.
111+
112+
Kendo-only or territory-scoped ADRs (0003, 0004, 0006, 0008, 0018) do not apply cross-territory.
113+
114+
### Internal / War-Room-Only
115+
116+
ADR-0005 (Spy System), ADR-0007 (Soldiers + Briefings), ADR-0010 (Squad System) govern war room operations, not territory code. No projection required.

docs/packages/adapter-store.md

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,59 @@ try {
181181

182182
The store automatically persists state to the provided storage service. When the page reloads, stored data is available immediately while `retrieveAll()` fetches fresh data from the API. This provides a fast initial render without loading spinners.
183183

184+
## Syncing External Updates
185+
186+
Some resources are updated outside of the store's own CRUD calls — by another user over a WebSocket, by a background job, by an in-process event emitter. The `broadcast` config slot is the single, narrow bridge for feeding those updates into the store without going through HTTP.
187+
188+
```typescript
189+
import type { AdapterStoreBroadcast } from "@script-development/fs-adapter-store";
190+
191+
const broadcast: AdapterStoreBroadcast<User> = {
192+
subscribe: ({ onUpdate, onDelete }) => {
193+
eventSource.on("user.updated", onUpdate);
194+
eventSource.on("user.deleted", onDelete);
195+
return () => {
196+
eventSource.off("user.updated", onUpdate);
197+
eventSource.off("user.deleted", onDelete);
198+
};
199+
},
200+
};
201+
202+
const usersStore = createAdapterStoreModule<User>({
203+
domainName: "users",
204+
adapter: resourceAdapter,
205+
httpService: http,
206+
storageService: storage,
207+
loadingService: loading,
208+
broadcast,
209+
});
210+
```
211+
212+
The store calls `subscribe` exactly once at construction and wires the handlers straight into its internal mutation path. `onUpdate(item)` replaces or inserts; `onDelete(id)` removes. Both update reactive state, refresh adapted views, and persist to storage — identical to what `update()` / `delete()` do after a successful HTTP call.
213+
214+
::: tip Why isn't there a public `setById` / `applyUpdate` method?
215+
By design. Exposing a raw mutation method would let any caller bypass HTTP, which is almost always a bug (you'd end up with stale server state). The `broadcast` contract forces the bridge to be declared explicitly at store construction, scoped to one event source per store.
216+
:::
217+
218+
### Lifecycle
219+
220+
The `subscribe` call happens once, when the store is created. The unsubscribe return is retained internally and never exposed. In practice stores live for the app's lifetime, so teardown isn't needed — but if your event source has its own lifecycle (e.g., a channel you join and leave), manage that _outside_ the store. The store only cares about incoming events, not which channel they came from.
221+
222+
A common pattern is a small in-process emitter as a middleman: your transport layer (WebSocket, SSE, channel service, whatever) joins and leaves connections as views mount/unmount, and forwards incoming payloads onto an emitter that the store subscribes to. The store stays agnostic of transport and lifecycle.
223+
224+
### The Contract
225+
226+
```typescript
227+
type AdapterStoreBroadcast<T> = {
228+
subscribe: (handlers: {
229+
onUpdate: (item: T) => void;
230+
onDelete: (id: number) => void;
231+
}) => () => void; // unsubscribe
232+
};
233+
```
234+
235+
That's it. Any event source that can emit "updated" and "deleted" events for your resource type can implement this.
236+
184237
## Custom New Types
185238

186239
By default, `generateNew()` creates an object with all fields except `id`. You can customize this with a third type parameter:
@@ -219,23 +272,25 @@ import { EntryNotFoundError, MissingResponseDataError } from "@script-developmen
219272

220273
### `createAdapterStoreModule(config)`
221274

222-
| Parameter | Type | Description |
223-
| ----------------------- | ----------------------------------------------- | -------------------------------------------- |
224-
| `config.domainName` | `string` | Resource endpoint name (e.g., `"users"`) |
225-
| `config.adapter` | `Adapter` | CRUD adapter factory (use `resourceAdapter`) |
226-
| `config.httpService` | `Pick<HttpService, "getRequest">` | HTTP service for fetching |
227-
| `config.storageService` | `Pick<StorageService, "get" \| "put">` | Storage for persistence |
228-
| `config.loadingService` | `Pick<LoadingService, "ensureLoadingFinished">` | Loading service for sync |
275+
| Parameter | Type | Description |
276+
| ----------------------- | ----------------------------------------------- | ----------------------------------------------------------- |
277+
| `config.domainName` | `string` | Resource endpoint name (e.g., `"users"`) |
278+
| `config.adapter` | `Adapter` | CRUD adapter factory (use `resourceAdapter`) |
279+
| `config.httpService` | `Pick<HttpService, "getRequest">` | HTTP service for fetching |
280+
| `config.storageService` | `Pick<StorageService, "get" \| "put">` | Storage for persistence |
281+
| `config.loadingService` | `Pick<LoadingService, "ensureLoadingFinished">` | Loading service for sync |
282+
| `config.broadcast?` | `AdapterStoreBroadcast<T>` | Optional external-event bridge for server-initiated updates |
229283

230284
### Store Module Methods
231285

232-
| Method | Returns | Description |
233-
| ------------------- | ----------------------------------- | -------------------------------------- |
234-
| `getAll` | `ComputedRef<Adapted[]>` | Reactive list of all adapted resources |
235-
| `getById(id)` | `ComputedRef<Adapted \| undefined>` | Reactive lookup by ID |
236-
| `getOrFailById(id)` | `Promise<Adapted>` | Wait for loading, throw if not found |
237-
| `generateNew()` | `NewAdapted` | Create a new unsaved resource |
238-
| `retrieveAll()` | `Promise<void>` | Fetch all from API and update state |
286+
| Method | Returns | Description |
287+
| ------------------- | ----------------------------------- | ------------------------------------------ |
288+
| `getAll` | `ComputedRef<Adapted[]>` | Reactive list of all adapted resources |
289+
| `getById(id)` | `ComputedRef<Adapted \| undefined>` | Reactive lookup by ID |
290+
| `getOrFailById(id)` | `Promise<Adapted>` | Wait for loading, throw if not found |
291+
| `generateNew()` | `NewAdapted` | Create a new unsaved resource |
292+
| `retrieveById(id)` | `Promise<void>` | Fetch a single resource from the API by id |
293+
| `retrieveAll()` | `Promise<void>` | Fetch all from API and update state |
239294

240295
### Adapted Properties
241296

0 commit comments

Comments
 (0)