From bb3dc5b493cc795b979d1e05929feefeed8f540b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Nov 2026 14:05:15 +0000 Subject: [PATCH] Plans leave the tree; the public-tree test now scans everything The working plans and hand-offs have moved, with their history, to the private planning repository CONTRIBUTING.md describes. The public-tree test no longer skips plans/ and asserts the directory stays gone. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016NSWj6mJotpoJ4FontE4Kg --- plans/001-sdk-adoption-campaign.md | 495 ---------------- plans/002-adopt-sdk-gh-v1.md | 100 ---- plans/003-adopt-sdk-k6i-agility-slack-bot.md | 86 --- plans/004-adopt-sdk-agility-rooms.md | 164 ------ plans/005-ergonomics-and-hardening.md | 539 ------------------ plans/handoffs/alpha-3-gh-v1.md | 49 -- .../handoffs/alpha-3-k6i-agility-slack-bot.md | 45 -- plans/handoffs/gh-v1.md | 71 --- plans/handoffs/k6i-agility-slack-bot.md | 55 -- test/unit/publicTree.test.ts | 12 +- 10 files changed, 9 insertions(+), 1607 deletions(-) delete mode 100644 plans/001-sdk-adoption-campaign.md delete mode 100644 plans/002-adopt-sdk-gh-v1.md delete mode 100644 plans/003-adopt-sdk-k6i-agility-slack-bot.md delete mode 100644 plans/004-adopt-sdk-agility-rooms.md delete mode 100644 plans/005-ergonomics-and-hardening.md delete mode 100644 plans/handoffs/alpha-3-gh-v1.md delete mode 100644 plans/handoffs/alpha-3-k6i-agility-slack-bot.md delete mode 100644 plans/handoffs/gh-v1.md delete mode 100644 plans/handoffs/k6i-agility-slack-bot.md diff --git a/plans/001-sdk-adoption-campaign.md b/plans/001-sdk-adoption-campaign.md deleted file mode 100644 index 34ae036..0000000 --- a/plans/001-sdk-adoption-campaign.md +++ /dev/null @@ -1,495 +0,0 @@ -# Plan 001: TypeScript SDK campaign and adoption - -A parallel campaign, run from a cloud agent session on this repo, alongside -daily work. Goal: take `@versionone/agility-sdk` from a verified starter (layers, -api/asset + rest-1 + meta.v1 wire modules, schema-driven translation, 49 unit -tests green) to a package that three surfaces depend on: - -| Surface | Repo | Kind | Pace | -|---|---|---|---| -| CLI | `versionone/gh-v1` | Node CLI | first | -| Messaging | `digital-ai/k6i-agility-slack-bot` | Slack app (Node 24, plain ESM JS) | first | -| Rooms | `digital-ai/agility-rooms` | React/Vite SPA, same-origin cookie auth | slow, later | - -The SDK's goal is one abstraction around every part of changing an asset — -its attributes, its relations, its operations, and the files attached to -it (attachments, embedded images) — so no consumer talks to an endpoint -directly for any of them. gh-v1 (`gh v1 attach`) and agility-rooms both -handle attachments today, each on its own. - -Specs (`specs/`) hold invariants; plans (`plans/`) hold sequencing and are -allowed to go stale once done. - -Reference: the Agility API documentation is public at -https://docs.digital.ai/agility-api/ (as of 2026-11-04). Cite it in specs -and handoffs in place of the older GitHub Pages copy. It is not reachable -from the cloud environment (allowed 2026-11-04); `llms-full.txt` there is -the whole site as one text file, the fastest way to check a claim. Live -behaviour is still settled by the integration tier. - -Findings from the testing instance worth carrying into later milestones: - -- api/asset answers a body it cannot parse with **400** and one message - in `Exceptions` (changed 2026-11; the docs' errors page says so). The - transport reads that shape into `ApiError.message`. -- A value containing an em dash (—) fails as a `filter` literal and in - `where` (`Invalid QueryFilter2 token …`), while `é` passes; the same - value as a `with` variable (`Name=$n`) works. Services that build filters - from user text (A5 `resolve`, gh-v1's `--filter`, the bot's search) should - pass values through `with`, not splice them into the filter string. - Possibly a server bug worth a defect. -- A failed query's slot is **dropped** from `queryResult.results` (later - ones shift up); the docs promise only that empty matches keep theirs. - `send` now restores the slot as `null`. Worth a line in the docs' - Queries page. Consumers that index results positionally after a partial - failure (the bot's `results[2]`, rooms' `getApiResults`) are exposed - until they move to the SDK. -- Number roots differ from gh-v1's hand list: Epic, Theme and TestSet are - Workitems now (resolved by the Workitem query); Milestone, Okr - (KeyResult, OkrObjective), OkrSession, Environment, Grant and - ExternalActionInvocation carry numbers too. Grant has no Name and - ExternalActionInvocation neither Name nor AssetState. `resolve` derives - all of this from the metamodel, under test. -- Number matching is case-insensitive on the server. -- **ChangeDate as a `with` variable never compares equal** — not even the - value just read back — while the same value as a filter literal does. - The guard therefore splices ChangeDate as a literal (after a strict - timestamp check); gh-v1 already did, which is why its guard works. A - likely server defect, alongside the em-dash one. -- **`Moment=` in a filter is an as-of selector**, not a comparison: an - earlier Moment of the asset matches and returns the asset as it was - then, and an update scoped by it lands on the current asset. Never use - Moment as a compare-and-set stamp. -- Preview projects even a scalar update in add/remove form - (`Estimate: { add: ["2"], remove: [] }`); sent back as a real update, it - applies. -- A StoryStatus create requires `RollupState` (`"Active"` and `64` both - accepted). -- An api/asset update that gives a multi-value relation as a plain list - **adds** to it (like rest-1's `act: "add"`); it does not replace the list - (slack bot, 2026-11-07). -- **Agility refuses to Delete a closed asset** - (`Violation'AssetCannotDeleteClosed`); reactivate first. The batch - service's reverse does this (`reversible` reports `reactivated`), and the - guard sends a second command on one asset as a second pass. - -## What the consumers do today (surveyed 2026-11-04) - -**k6i-agility-slack-bot.** Every Agility call goes through one seam, -`lib/v1api.js` (axios, Bearer token, a 401 interceptor that calls -`authenticationFailureHandler` to restart setup). It uses five endpoints: -`query.v1` (most reads, query bodies in `lib/queries.js`), `api/asset` -(bulk, `execute` QuickClose/Reactivate), `rest-1.v1` (create and update with -the `Attributes` envelope via `lib/transformDataToAsset.js`; `findAssets` -reads with `find`/`findin`/`page`), and `loc-2.v1`. Its `AGENTS.md` already -plans for this: the Bolt migration's Phase 2 rewrites the seam as -`services/v1.js` on native `fetch` "so it can migrate into `@versionone/agility-sdk`". -It hand-decodes string scalars in 3 places (`"Team.HasProcess" === "True"`, -`IsKanban.toLowerCase() === "true"`) — exactly what spec 000 §2a removes. - -**agility-rooms.** `src/utils/axios.tsx` builds a same-origin axios instance -(`X-Requested-With`, cookie auth, an `axios-auth-refresh` interceptor that -opens the SSO login popup on 401/403 and retries, a response interceptor -that turns api/asset `commandFailures` into a localized snackbar and a -thrown error). `src/utils/bulkApiService.tsx` wraps query/create/modify/ -remove. 67 files import axios; `api/asset` appears 96 times, `query.v1` 29, -`meta.v1` 20, `rest-1.v1` 5, `loc-2.v1` 3, plus a few `*.mvc` routes. It -hand-decodes string booleans in 48 places (including `getApiResults` -patching `IsReadOnly`), and `src/metaAssetStore/` keeps its own per-type -field knowledge that overlaps the SDK's `schema` service. - -Re-surveyed 2026-11-07 at `main` `0874590` (read-only): - -- **HTTP.** One default axios instance (`src/utils/axios.tsx`) plus - `getAxiosInstance(...)` variants, a bare `meta` instance and a plain - `axios.create` in `src/navigation/routes.tsx`; several skip the - auth-refresh and snackbar interceptors. 88 production files import axios. - Cookie auth only. On 401/403 an SSO popup (`Account.mvc/Login`), then - replay. Cancellation by `AbortController` in a few places; no retries. - The commandFailures snackbar runs only when the URL is exactly - `/api/asset`; 9 calls written `api/asset` (no slash) bypass it. -- **Endpoints.** api/asset (111 call sites, 57 files), query.v1 (16), - rest-1.v1 reads (4) **and writes** (5 `Data/{T}/{id}` posts with - `{ Attributes: { X: { value, act } } }`, plus 2 `?op=Copy`), meta.v1, - loc-2.v1, `loc.v1`, `config.v1`, `attachment.img`, `embedded.img`, - `EmbeddedImage.mvc/Upload`, about 15 other `*.mvc` routes and about 20 - custom `api/*` routes (reports, tags, watch, rooms2config, LLM). -- **Writes.** `metaAssetSlice` updates multi-value fields as - `{ X: { [value]: "add" | "remove" } }`, sends a cleared relation as - `"NULL"`, updates optimistically and reverts on error. Execute ops with - and without `args` (Convert). No change comments, no ChangeDate checks. -- **Reads.** 34 explicit pages, but some board and `ID=$Oids` reads are - unbounded. Positional `results[n]` with index constants - (`metaAssetStore/constants.ts`). No `group` queries. -- **Decoding.** 46 `"True"`/`"False"` comparisons; `NULL_OID` referenced - 118 times; `massageApiAssetResponseObject` unwraps single relations by - meta. meta.v1 is loaded by `store/metaDefinition/useMetaDefinitionStore.ts` - (per type, in-flight dedupe, ETag revalidation), not by `metaAssetStore/`. -- **Tests.** Jest (222 files) mocks modules; no MSW. The AI domain's - injected `IHttpClient` / `FakeHttpClient` is the cleanest seam for a swap. - Playwright (79 specs) runs against a live server, including attachment CRUD. - -**gh-v1** (surveyed 2026-11-04 at `41fe333`). A `gh` extension in plain -CommonJS Node (≥20), no runtime dependencies, `node --test` with 99 passing -tests. One seam, `lib/api.js` (native `fetch`, `Authorization` header from -config), required by 22 modules. It is already the SDK's design in JS form: - -- `query`/`queries` over api/asset with a default page on every query; - `asset` throws on `commandFailures` with the failures kept structured - (`err.failures`); `attempt` tolerates them and reports each by - `sourceCommandIndex` — this *is* gap 4 below, already proven. -- `rest` flattens rest-1's `Attributes` and refuses a read without a page - (what `src/wire/rest1.ts` already does); `meta` and `localize` (loc-2.v1); - `blob` get/post for attachment.img; `raw` for the `gh v1 api` passthrough. -- `lib/resolve.js` — numbers → assets: one bounded `Workitem` query for - every number, then one rest-1 `BaseAsset` read with type-qualified wheres - for the non-workitem numbered types; plus room and project-path lookup - with interactive disambiguation (`lib/prompt.js`). -- `lib/batch.js` — the receipt pipeline behind `gh v1 bulk`/`apply`: - preview (projection), hydrate, before-snapshot, **`guarded()`** (each - projected update/execute rewritten as a query-scoped command filtered on - `ID` and the snapshot's `ChangeDate` — a per-request compare-and-set, the - "guard" spec 000 §6 means), send, and a best-effort reverse batch. -- `lib/guard.js` is CLI policy for `gh v1 api` (endpoint allowlist, - unbounded-request detection, secret redaction) — not the §6 guard. -- It also hand-decodes string booleans (`isTrue` in `api.js`, `meta.js`, - `resolve.js`, `board.js`) and keeps its own `momentless`/`oidOf`. - -So gh-v1 is the *source* for A2, A5 and parts of A3, and the most natural -first consumer: same runtime, same `fetch`, no framework migration in -flight. - -## Gaps the consumers expose in the SDK - -Found by reading the code above against `src/`: - -1. **Browser auth.** The transport has no `credentials` option and no way - to send same-origin cookies; rooms needs `credentials: "include"` (or - `same-origin`) and `X-Requested-With`. (The 2.x SDK had `withImplicitAuth` - for this.) `defaultHeaders` covers the header; credentials is missing. -2. **Auth-failure hook.** Both consumers react to 401 (slack: re-run setup; - rooms: SSO popup, then retry). Transport needs an `onUnauthorized` - callback that can resolve "retry once" or "fail" — without breaking §6 - (a retried api/asset write is still never automatic). -3. **Cancellation.** No caller `AbortSignal` on `RequestOptions`; rooms - cancels in-flight requests on navigation. -4. **Command failures.** api/asset returns 200 with `commandFailures`. - Rooms treats any failure as an error; slack ignores them. The SDK should - return failures typed (it does) and offer `throwOnFailure` producing an - `ApiError` subclass carrying `sourceCommandIndex` per failure. -5. **Missing wire modules** named in spec 000 but not built: `loc2` (both - consumers), `queryV1` (slack's main read path), `attachment`/ - `EmbeddedImage` (rooms). `queryV1` is built (2026-11-06) and kept on - purpose even though api/asset can run the same queries: query.v1 is - read-only by construction and answers with rows alone, so it is the safe, - simple place to explore a query (and its requests may be retried). -6. **rest-1 writes — not needed.** Settled from the docs - (apis/asset-bulk/updating-assets, creating-assets): api/asset carries a - change comment as `ChangeComment` inside an update's `update` node, or - at the root of a create command; `execute` commands cannot carry one. - So the bot's `update(oid, data, comment)` moves to api/asset and the SDK - grows no rest-1 write path. -7. **Services.** `resolve` (number → asset), `guard` (ChangeDate - compare-and-set), `batch` (preview/snapshot/receipt/reverse) — port from - gh-v1's `resolve.js` and `batch.js`, minus the prompting and file I/O, - which stay in the CLI. -8. **Distribution.** No CI in this repo, no publish target, version 0.1.0. - Source is browser-safe already (no `node:` imports, no `Buffer`). -9. **CommonJS consumers — done.** `test/unit/cjs.test.ts` builds the CJS - bundle, installs it as `node_modules/@versionone/agility-sdk`, and has a plain - `.cjs` script `require` and use it. The exports map now gives each - condition its own types (`.d.cts` for require), which a CJS caller - type-checking with NodeNext (gh-v1 after G2) needs. - -## Track A — SDK (this repo, this session) - -Each milestone is one commit series on `proposed_type_script_api_version`, -`npm run verify` green, spec updated in the same commit where an invariant -moves. - -- **A1 Transport for real callers** — gaps 1–3: `credentials`, - `onUnauthorized` (retry-once semantics, idempotency rules unchanged), - `signal`. Unit tests with the fake fetch. Unblocks rooms and slack auth. -- **A2 Command-failure ergonomics** — gap 4: `CommandFailureError`, - `send(..., { throwOnFailure })`, `sourceCommandIndex` preserved. -- **A3 loc2 wire module** — gap 5, smallest consumer-shared endpoint. -- **A4 Slack-shaped reads** — done: a `queryV1` wire module and - `client.queryV1.query`/`queries`, rows translated through the same - schema as api/asset (the integration tier proves both endpoints give the - same rows). The bot keeps query.v1 for its reads. (No `rest1` writes: - gap 6 is settled.) -- **A5 Services port** — gap 7. `services/resolve` first (gh-v1's - `resolveAssets`; slack's `queryForAssetByNumber` needs it too), then - `services/guard` (`guarded()`), then `services/batch` (preview, describe, - snapshot, reverseOf — a receipt as data; writing it to disk stays in - gh-v1). gh-v1's tests for these move with them, rewritten against recorded - responses. -- **A6 CI and packaging** — done: CI runs `npm run verify` on Linux (Node - 20/22/24) and Windows (Node 24); `@versionone/agility-sdk` is on npmjs - (`1.0.0-alpha.0`, `alpha` dist-tag); `publish.yml` publishes later - versions from a `v*` tag through npm trusted publishing, gated by the - GitHub environment `npm` (an approval). Consumers install from npmjs. -- **A7 Attachments and embedded images** — gap 5's `attachment`/ - `EmbeddedImage`, as part of the asset-update abstraction, not a side - channel. Two consumers to serve: gh-v1's `gh v1 attach` (list, get, add, - put) and agility-rooms' attachment handling (survey it first: where it - uploads, which `*.mvc` routes it uses, how rich text embeds images). - Shape, from the docs and gh-v1's working code: - - wire: `attachment.img/` (and the embedded-image endpoint) — bytes - in and out, addressed by the numeric id, not the OID token; a body as - `Uint8Array`, `Blob` or `FormData` so it works in Node and the browser; - the endpoint's statuses (404 missing, 413 too large) as typed errors. - - service: `client.attachments.list(asset)` (metadata only — never - select `Content` across many: it Base64s every blob into one - response), `get(id)` → bytes + content type, `add(asset, bytes, - { filename, contentType, name, description })` as the docs prescribe - (create the Attachment with empty `Content`, then POST the bytes; on a - failed upload, report the created-but-empty asset rather than hide it), - `put(id, bytes)` to replace. - - batches: an Attachment create inside `assets.send`/`batch.run` stays an - ordinary create; the bytes are a second step the service owns, and a - receipt's reverse deletes the asset. - Integration tests on v1sdktesting with `sdk-integration` fixtures, - deleted by OID. - - Verified live (2026-11-07): an Attachment create **requires** `Content` - (empty is allowed; absent is `Violation'Required'…'Content'`); Base64 - `Content` in the create stores the bytes in one request; `attachment.img` - GET answers the bytes with their type and `Content-Disposition`, 404 when - missing, and POST replaces them. `EmbeddedImage.mvc/Upload` accepts token - auth and answers ``. - **An EmbeddedImage cannot be deleted** (`Unknown Operation - 'EmbeddedImage.Delete'`), so its live test is opt-in - (`SDK_TEST_EMBEDDED_IMAGES=1`) and leaves one image per run. - - What rooms does today (survey 2026-11-07), which A7 must cover: - - **Upload** is one api/asset create with the bytes inline: `{ AssetType: - "Attachment", Asset, ContentType, Filename, Name, Content: }` - (`MultiValueRelations/Attachments.tsx`). The UI caps a file at 4 MB and - has no 413 handling. So `add` needs both modes: inline Base64 for small - files (one request, what rooms does) and create-then-POST-bytes for - large ones (what the docs and gh-v1 do), chosen by size. - - **Preview** of an image re-reads `Content` for one row and builds a - `data:` URL; **download** opens `attachment.img/` from the app root. - So `get` returns bytes and type, and the service exposes the - `attachment.img` URL for a browser to open. - - **Delete** is `execute: "Delete"` on the Attachment. - - **Embedded images** (TinyMCE paste) post multipart to - `EmbeddedImage.mvc/Upload` with fields `image` and `oidToken`; the - answer is HTML with ``. Stored HTML - references `embedded.img/`, rewritten to app-root URLs. So: - `embeddedImages.upload(asset, blob)` → `{ id, url }`, parsing that - response, and a helper that normalises `embedded.img` URLs. - -## Track B — k6i-agility-slack-bot (fast, on `bolt-migration`) - -Target branch: `bolt-migration`, not `main`. There, Bolt Phase 2 has already -replaced the axios `lib/v1api.js` with `services/v1.js` — -`createV1Client({ url, accessToken })` on native `fetch`, throwing -`AuthFailureError` on 401 (caught by `withV1()`, auth Flow 4) — and its -tests (`test/v1-service.test.js`) stub global `fetch` exactly as gh-v1's do. -So the SDK swap is internal to `createV1Client`; listeners do not move. -The 401 mapping is a `catch (e) { if (e.status === 401) throw new -AuthFailureError(...) }` around SDK calls, so **B does not need A1**. - -Calls the client still makes on `bolt-migration`: `query.v1` (most reads, -bodies in `lib/queries.js`), api/asset (`bulk`, `createWithNumber`, -`exec`), rest-1 writes (`create`/`update` with `?comment=` through -`lib/rest1DataTransformer.js`), rest-1 `findAssets`, loc-2. - -- **B0 Pre-SDK migrations in the bot's own session** (S1–S4 under - *Consumer migrations* below). They shrink what the SDK must support: - once S2 pages every read, B1's reads move to `client.queryV1` as they - are; A4's rest-1-writes question is closed (gap 6). -- **B1 Client swap.** `createV1Client` keeps its surface; inside, one - `AgilityApiClient({ baseUrl: url, accessToken })`. `query` → - `client.queryV1.queries`; `bulk`/`exec` → `client.assets.*`; - `fetchLocalizations` → `loc2` (A3). - `test/v1-service.test.js` stays the parity proof. -- **B2 Delete the decodes.** Rows arrive translated: `"True"` comparisons, - `IsKanban.toLowerCase()`, `toMomentless` and positional - `results[0][0]`/`results[2]` handling go. -- **B3 Writes.** `create`/`update` → `assets.send` (api/asset), the change - comment as `ChangeComment` in the `update` node (gap 6); - `rest1DataTransformer.js` is deleted. -- **B4 Number resolution.** `queryForAssetByNumber` → `resolve` (A5). - -Gate per step: `npm test`, then a sandbox-workspace smoke (asset mention, -`/agility find`, backlog-item status change, webhook notification). - -## Track C — gh-v1 (fast, first consumer) - -Its 99 tests stub global `fetch`, so the swap can be proven without a live -instance: same requests out, same rows in. - -- **C1 Seam swap.** Re-implement `lib/api.js`'s exported surface (`query`, - `queries`, `asset`, `attempt`, `rest`, `meta`, `localize`, `raw`, `blob`, - `permalink`) on the SDK, imported from a vendored copy of its ESM build - (gh installs extensions without npm; see plan 002, *Dependency*). Callers - unchanged; all tests green. Needs A2, A3 (loc2). -- **C2 Delete the decodes.** With rows translated by the schema, `isTrue` - and `oidOf` drop out of `meta.js`, `resolve.js`, `board.js`; `momentless` - comes from the SDK. -- **C3 Invert the services.** A5 is done, so `resolve.js` and `batch.js` - keep only prompting, console rendering, permalinks and receipt files: - `resolveAssets` → `client.resolve.numbers` (its `missing` drives the - "not found — skipped" lines); `run()` → `client.batch.run` with - `confirm` as the prompt and the receipt written as returned; - `--reverse` → `client.batch.reversible` (+ `--force`). Two deliberate - differences to carry over: the SDK refuses an unguardable batch unless - `guard: false` (gh-v1 sent it as written with a note — keep that UX by - checking `unguardable()` first), and its number roots are wider than - gh-v1's list. -- **C4 Attachments on the SDK.** Once A7 lands, `gh v1 attach` moves onto - `client.attachments` and `blob` leaves `lib/api.js`. `raw` stays CLI-local: - it is the `gh v1 api` passthrough. - -`lib/guard.js` stays in gh-v1: it is policy for a terminal passthrough, not -SDK behaviour. - -## Track D — agility-rooms (slow) - -Open questions from the 2026-11-07 survey: rooms still writes through -rest-1 (5 attribute posts, 2 `?op=Copy`) and reads `rest-1.v1/New/{T}` -templates, which gap 6's "no rest-1 writes" did not foresee; decide whether -those move to api/asset (Copy as an execute?) or the SDK grows a narrow -rest-1 write path. Its commandFailures policy should apply to every -api/asset call, not only URLs spelled `/api/asset`. - -Strangler, starting below the call sites so nothing user-visible moves: - -- **D1 SDK under the existing axios.** Implement a `Transport` that - delegates to rooms' current axios instance (keeps the SSO refresh - interceptor and snackbar). Point `bulkApiService.tsx` at - `client.assets.*`. Rows arrive translated, so `getApiResults`' `IsReadOnly` - patch goes. This is the whole first PR. (A2 is done: - `CommandFailureError` carries what the snackbar needs.) -- **D2 Domain by domain.** Start with `src/domains/ai` (it already has - adapters, architecture tests, and the injected `IHttpClient` / - `FakeHttpClient` seam), then retire string-boolean decodes per feature - (46 at the 2026-11-07 survey). Attachments and embedded images move to - `client.attachments` / `client.embeddedImages` (A7). Jest + Playwright - gate each PR. -- **D3 Native transport.** Swap the axios-backed Transport for - `createFetchTransport` with `credentials` + `onUnauthorized` (A1 is - done). -- **D4 Schema convergence.** Evaluate replacing parts of - `src/metaAssetStore/` field knowledge with the SDK `schema` service. - -## Consumer migrations (run in each repo's own session) - -Modernisation the consumers should adopt *independently of* the SDK, run -from the sessions already focused on those repos, not from this one. Each -is sized to one PR and has a paste-ready prompt in `plans/handoffs/`. They -are ordered so that, by the time the SDK swap (Tracks B/C/D) arrives, it is -a change of one file. - -**gh-v1** (`plans/handoffs/gh-v1.md`) - -- **G0 Runs anywhere.** The `gh-v1` bin is committed without its executable - bit (`100644`), so installing the extension fails on macOS/Linux; and - config comes only from `~/.v1config/v1api/config.json`. Fix the mode and - accept `V1_URL` + `V1_ACCESS_TOKEN` from the environment, so cloud - sessions and CI can run it. -- **G1 CommonJS → ESM.** gh-v1 is the one consumer still on `require` - (`"type"` unset, `module.exports` in every `lib/` file, the `gh-v1` bin - script). Convert to `"type": "module"`, `import`/`export`, `node:` - specifiers, `import.meta`-based paths where `__dirname` is used. Its - tests stub global `fetch`, so ESM conversion is provable offline. Do this - before C1 so the seam swap is written once, in its final syntax. -- **G2 Type checking without a build.** `jsconfig.json` with `checkJs`, - `module`/`moduleResolution: NodeNext`, `target: ES2022`, and JSDoc on - `lib/api.js`'s surface — so SDK types flow into the CLI with no compile - step. `tsc --noEmit -p jsconfig.json` as a test-time check. -- **G3 One page policy.** Lift the scattered `page: { size: 20, start: 0 }` - defaults into one constant, matching the SDK's refusal of unpaged reads. - -**k6i-agility-slack-bot**, on `bolt-migration` (`plans/handoffs/k6i-agility-slack-bot.md`) - -The runtime is already ESM (`"type": "module"`, no `require`), so "move to -ESM" here means finishing the edges: - -- **S1 Finish ESM tooling.** `jsconfig.json` still says `module: commonJS`, - `target: es6`; set `NodeNext`/`ES2022` and turn on `checkJs` so editors - and `tsc --noEmit` see the real module system (and, later, SDK types). -- **S2 Page every read.** Reads stay on query.v1 (read-only, simplest - responses). Add an explicit `page` to every body in `lib/queries.js`, - as the SDK's `queryV1` refuses an unpaged query. -- **S3 Close Bolt Phase 2.** Drop `botkit`, `botbuilder-*`, - `botkit-plugin-cms` and `axios` from `package.json` once nothing imports - them; delete `lib/v1api.js` and the `plugins/` Botkit modules that - `listeners/` replaced. (AGENTS.md: "axios is dropped then.") -- **S4 Record real responses.** Replace hand-written fixtures in - `test/v1-service.test.js` and `test/queries.test.js` with responses - captured from the local dev instance (spec 000 §2a: fakes send what the - server sends — api/asset spells `"True"`, not `true`). - -**agility-rooms** (slow; brief: `plans/004-adopt-sdk-agility-rooms.md`) - -- **R1** Narrow the axios surface: route the axios importers (88 production - files at the 2026-11-07 survey) through `utils/bulkApiService.tsx` and a - small set of helpers, so D1 swaps one module. - -## Operating model for the campaign - -- **Home:** this cloud session on `proposed_type_script_api_version`. One - milestone per request ("do A1"), each ending with verify green, a commit, - and a push. Consumer work happens on a feature branch in each consumer repo - (proposed: `sdk-adoption`), never on their default branches, PRs only on - request. -- **Order here:** A2 → A3 → C1 → C2 → B1 → B2 → A5 → C3 → B3/B4 → A6 → A1 → - A7 → C4 → D1 → D2+. A7 starts with the rooms attachment survey, so - rooms' needs shape the module before gh-v1 moves onto it. gh-v1 first: no framework migration, and stubbed-fetch tests - prove parity offline. A1 (browser credentials, onUnauthorized) waits for - rooms; the bot maps `ApiError.status === 401` itself. -- **In parallel, elsewhere:** G0–G3 in the gh-v1 session, S1–S4 in the - slack-bot session (on `bolt-migration`). C1 starts after G1 merges; B1 - after S2 and S3. -- **Hand-offs:** this session writes the prompt (`plans/handoffs/*.md`); - you paste it into the repo's session. Results come back as merged - commits that this session reads before starting the dependent milestone. -- **Status:** this file's checklist below is the source of truth; update it - in the commit that finishes a milestone. - -## Decisions needed - -- ~~Publish target~~ — decided 2026-11-07: public npmjs, as - `@versionone/agility-sdk` (renamed from `@versionone/sdk` on 2026-11-07, - before any publish: the scope already has `ctm-pipeline-builder-sdk`, and - the product is Agility; the `@versionone` org already publishes there), starting - at `1.0.0-alpha.0` on the `alpha` dist-tag so `latest` is never a - prerelease. Not `v1sdk` 3.x: the 2.x package (`v1sdk`, last published - 2022) stays as it is, because a new major of the same name would imply a - compatibility this SDK does not aim for. Publishing needs an `@versionone` - org owner to set up a token or trusted publisher. -- Dependency audit before the first publish — done 2026-11-07: no runtime - dependencies, and the built bundle imports nothing. `npm audit` found 13 - issues, all in dev tooling (vitest/vite, tinypool, tsup/esbuild, - rimraf/glob); fixed by in-range updates plus vitest 3 → 5 and tsup 8 → 9. - `npm audit` is clean; unit and integration tiers pass on the new tools. -- ~~Live instance for the integration tier from a cloud session~~ — done - 2026-11-04: `V1_URL`/`V1_ACCESS_TOKEN` point at the API-testing instance - `https://www14.v1host.com/v1sdktesting`; `npm run test:integration` is - green there (custom fields skip: hosted). - -## Checklist - -- [x] A1 transport: credentials, onUnauthorized (idempotent requests resent once; writes never), signal (`RequestOptions.signal`, `client.withSignal`) -- [x] A2 command-failure error (`CommandFailureError`, `throwOnFailure`, reads always throw) -- [x] A3 loc2 (`loc2.localize`, `client.localize`; GET, POST past 2000 encoded chars, culture via Accept-Language) -- [x] A4 slack-shaped reads — `queryV1` wire module, `client.queryV1.query`/`queries` (read-only, idempotent, same row translation as api/asset) -- [x] A5 services port from gh-v1 — `resolve` (`client.resolve.numbers`/`.ref`), `guard` (`client.assets.sendGuarded`), `batch` (`client.batch.run`/`.reversible`) -- [x] A7 attachments and embedded images — `attachment` wire module (`attachment.img` bytes, `EmbeddedImage.mvc/Upload`, URL helpers, browser-safe Base64), `client.attachments` (list/info/get/add/put/remove/url; inline up to `INLINE_LIMIT`, two-step above) and `client.embeddedImages` (upload/url/normalize) -- [x] A6 CI + prerelease publish — `@versionone/agility-sdk@1.0.0-alpha.0` on npmjs (2026-11-07; dist-tags `alpha` and, as npm sets on a first publish, `latest`), MIT. CI on Linux and Windows; `.github/workflows/publish.yml` publishes later versions from a `v*` tag once trusted publishing is linked on npmjs. The first version was published by hand, so it has no provenance -- [x] G0–G3 gh-v1 migrations — versionone/gh-v1#29–#32 -- [x] S1–S4 slack-bot migrations — digital-ai/k6i-agility-slack-bot#21–#24 (on `bolt-migration`) -- [x] B1 slack bot client swap — digital-ai/k6i-agility-slack-bot#29 -- [x] B2 slack bot decodes deleted; reads through `client.queryV1` with a schema — digital-ai/k6i-agility-slack-bot#30 -- [x] B3 slack bot writes on `client.assets.send` (rest-1 writes gone) — digital-ai/k6i-agility-slack-bot#32 -- [x] B4 slack bot number lookups on `client.resolve.numbers` — digital-ai/k6i-agility-slack-bot#34 (SDK #44 adopted in #33: pin `785b4c9`, a `TypeCache` per instance) -- [x] C0 gh-v1 surveyed (2026-11-04) -- [x] C1 gh-v1 seam swap — versionone/gh-v1#33, with the vendored SDK's hashes, CI and Node 22 floor in #35 -- [x] C2 gh-v1 rows translated through the schema; `isTrue`/`oidOf` gone — versionone/gh-v1#36 -- [x] C3 gh-v1 resolve and batch on `client.resolve`/`client.batch` — versionone/gh-v1#37 (with the SDK fixes from #43) -- [x] C4 gh-v1 `gh v1 attach` on `client.attachments`, `blob` removed — versionone/gh-v1#38 (re-vendored at `d51fb16`; small files now upload inline); `raw` stays CLI-local -- [ ] D1–D4 agility-rooms — brief: `plans/004-adopt-sdk-agility-rooms.md` (first: decide rooms' rest-1 writes and Copy) diff --git a/plans/002-adopt-sdk-gh-v1.md b/plans/002-adopt-sdk-gh-v1.md deleted file mode 100644 index 83f9112..0000000 --- a/plans/002-adopt-sdk-gh-v1.md +++ /dev/null @@ -1,100 +0,0 @@ -# Plan 002: gh-v1 adopts `@versionone/agility-sdk` (Track C) - -Brief for a session on `versionone/gh-v1`. Source plan: -`plans/001-sdk-adoption-campaign.md`, Track C. - -**Status (2026-11-07): done.** C1–C4 merged (versionone/gh-v1#33, #35–#38); -gh-v1 then moved to the renamed, published package (#39, #40). gh-v1 -reaches Agility only through the vendored SDK, apart from the `gh v1 api` -passthrough. The rest of this brief is the record of how. - -## Goal - -gh-v1 talks to Agility only through `@versionone/agility-sdk`. CLI behaviour and -output stay the same; the code that duplicates the SDK goes. - -## Where things stand (checked 2026-11-06, gh-v1 `2c58c7e`) - -- G0–G3 are merged: executable bin, `V1_URL`/`V1_ACCESS_TOKEN` config, ESM - (`"type": "module"`), `jsconfig.json` + `npm run typecheck`, one paging - module (`lib/paging.js`). -- The bin `gh-v1` is now a `sh` launcher that runs `gh-v1.js` with the real - node (bypassing Volta's shim, which mangles arguments on Windows). The - swap does not touch either; keep `test/launcher.test.js` green. -- One seam: `lib/api.js` exports `api(cfg)` returning `query`, `queries`, - `asset`, `attempt`, `rest`, `meta`, `localize`, `raw`, `blob`, - `permalink`, typed by the `Api` JSDoc typedef. Its tests under `test/` - stub global `fetch`. -- String-boolean and OID helpers (`isTrue`, `oidOf`, `momentless`) are used - in `lib/api.js`, `meta.js`, `resolve.js`, `board.js`, `batch.js`, and - `commands/apply.js`, `bulk.js`, `board.js`. - -## Dependency: vendored - -gh installs a script extension with a plain `git clone` and never runs npm, -so gh-v1 cannot rely on `node_modules` at run time. The SDK is still a -runtime dependency, but its code ships inside the repo: - -- `vendor/versionone-sdk/` holds the SDK's ESM build (`index.js`, which - imports nothing) and types (`index.d.ts`), plus `SOURCE.json` naming - the version they came from and each file's SHA-256. `lib/api.js` - imports from there. -- `package.json` lists `@versionone/agility-sdk` under `devDependencies`, - pinned to an exact published version (`1.0.0-alpha.0` today; before the - first publish it was a git commit), so the lockfile records exactly what - was vendored. -- To move the pin: change it in `package.json`, then - `npm install && npm run vendor:sdk`, and commit the copy. - `test/vendor.test.js` fails if the copy, `SOURCE.json` and the pin - disagree. -- The SDK is on npmjs (plan 001, A6); that changed only where the pin - points. The copy stays, because gh never runs npm. - -## Steps, one PR each, on branch `sdk-adoption` - -1. **C1 Seam swap** (`lib/api.js`) — done in versionone/gh-v1#33. Keep `api(cfg)` and the `Api` typedef. - Inside, build one `AgilityApiClient({ baseUrl: cfg.v1Url, accessToken })`. - Map: `query`/`queries` → `client.assets.query`/`queries`; - `asset` → `client.assets.send(cmds, { throwOnFailure: true })` - (a `CommandFailureError` carries `failures` by index — keep `err.failures`); - `attempt` → `client.assets.send` (failures returned, not thrown); - `rest` → `client.data.list`/`get`; `meta` → `client.meta.*`; - `localize` → `client.localize`. `raw`, `blob`, `permalink` stay local. - Rows now arrive translated (`true`, numbers, single relations as one - Ref), so update the fixtures in tests that assert raw strings, and add - the meta fixture the schema needs (`test/meta-fixture.js`, or seed - `ClientOptions.metamodel`). -2. **C2 Delete the decodes.** Remove `isTrue`/`oidOf` call sites listed - above; import `momentless` from the SDK. -3. **C3 Invert the services.** `resolveAssets` → `client.resolve.numbers` - (its `missing` drives the "not found — skipped" lines). `batch.run` → - `client.batch.run`, with `confirm` as the prompt and the receipt written - as returned. `--reverse` → `client.batch.reversible` (`--force` passes - `force`). Keep the CLI's UX for an unguardable batch by checking - `unguardable()` first, then sending with `guard: false` and the existing - note. Prompting, rendering, permalinks and receipt files stay in gh-v1. -4. **C4 Attachments on the SDK** (after plan 001's A7). `gh v1 attach` - (`list`, `get`, `add`, `put`) moves onto `client.attachments`; `blob` - leaves `lib/api.js`. File I/O, `--out`, the content-type guess and the - CLI's messages stay in gh-v1. `raw` (the `gh v1 api` passthrough) and - `lib/guard.js` stay: they are CLI policy, not SDK behaviour. - -## Rules - -- Requests out must match today's, apart from what translation changes. - Add a test that asserts the request body where it differs, and say why. -- Every query keeps an explicit page (the SDK refuses one without). -- Live Agility calls go only to the API/SDK testing instance, - `https://www14.v1host.com/v1sdktesting` (`V1_URL`). Writes are allowed - there and nowhere else: name fixtures `sdk-integration …` and delete them - by exact OID when done. Every query sets an explicit page size and stays - well under 20 s of server work. - -## Done when - -- `npm test` and `npm run typecheck` pass on each PR. -- `lib/api.js` has no `fetch` call except in `raw`/`blob`. -- No `isTrue`/`oidOf`/local `momentless` remain in `lib/`. -- `lib/resolve.js` and `lib/batch.js` keep only prompting, rendering and - file I/O. -- Plan 001's checklist line `C1–C4 gh-v1` is ticked, with the PR links. diff --git a/plans/003-adopt-sdk-k6i-agility-slack-bot.md b/plans/003-adopt-sdk-k6i-agility-slack-bot.md deleted file mode 100644 index 94a2196..0000000 --- a/plans/003-adopt-sdk-k6i-agility-slack-bot.md +++ /dev/null @@ -1,86 +0,0 @@ -# Plan 003: k6i-agility-slack-bot adopts `@versionone/agility-sdk` (Track B) - -Brief for a session on `digital-ai/k6i-agility-slack-bot`. Source plan: -`plans/001-sdk-adoption-campaign.md`, Track B. Work on `bolt-migration`, -never `main`. - -**Status (2026-11-07): done.** S1–S4 (digital-ai/k6i-agility-slack-bot#21–#24) -and B1–B4 (#29, #30, #32, #34) merged; the bot installs -`@versionone/agility-sdk@1.0.0-alpha.0` from npmjs (#41). Its SDK requests -(group rows, a shared TypeCache, a registry release, number roots, Template) -are all answered. The rest of this brief is the record of how. - -## Goal - -`services/v1.js`'s `createV1Client` runs on `@versionone/agility-sdk`, with the same -exported surface, so listeners and features do not change. The bot's -hand-decoding and rest-1 write path go. - -## Where things stand (checked 2026-11-06, `bolt-migration` `4d77693`) - -- `createV1Client({ url, accessToken })` uses native `fetch` and throws - `AuthFailureError` on 401. Its methods: `query` (query.v1), `bulk`, - `createWithNumber`, `exec`/`quickClose`/`reactivate` (api/asset), - `create`/`update` (rest-1 writes via `lib/rest1DataTransformer.js`), - `findAssets` (rest-1 `find`/`findin`, `page` up to 200), - `fetchLocalizations` (loc-2), and `queryFor*` helpers built on - `lib/queries.js`. `test/v1-service.test.js` stubs global `fetch`. -- **S1–S4 were not done yet** (then; all merged since): `jsconfig.json` still says `commonJS`/`es6`; - query bodies have no explicit page; `axios`, `botkit*`, `botbuilder-*` are - still dependencies and `lib/v1api.js` still exists. These come first. - -## Dependency - -`"@versionone/agility-sdk": "1.0.0-alpha.0"` from npmjs, an exact pin -(until the first publish it was a git commit, which needed `git` in the -image and wrote a `git+ssh` URL into the lockfile). The bot is ESM, so -`import { AgilityApiClient } from "@versionone/agility-sdk"`. - -## Steps, one PR each - -0. **S1–S4 first**, as written in `plans/handoffs/k6i-agility-slack-bot.md`. - B1 needs at least S2 (every read paged) and S3 (axios and Botkit gone). - If the bot's own session already merged them, skip. -1. **B1 Client swap** (`services/v1.js`). Inside `createV1Client`, build one - `new AgilityApiClient({ baseUrl: url, accessToken })`. `query` → - `client.queryV1.queries` (the bot's reads stay on query.v1: read-only, - simplest responses; a refused query throws with the server's message); `bulk`/`exec`/`createWithNumber` → - `client.assets.send`; `fetchLocalizations` → `client.localize`; - `findAssets` → `client.data.list("BaseAsset", { find, findin, where, - sort, page, deleted: false })`. Map `ApiError` with `status === 401` - to `AuthFailureError` in one place. The SDK's `onUnauthorized` is not - needed. `test/v1-service.test.js` stays the parity proof. -2. **B2 Delete the decodes.** Rows arrive translated: drop the - `"Team.HasProcess" === "True"` and `IsKanban.toLowerCase() === "true"` - comparisons, local `toMomentless` (use the SDK's `momentless`), and the - `results[1]`/`results[2]` guessing in `queryForPrimaryWorkitemByOid` and - `queryForTeamRoomContext`. -3. **B3 Writes on api/asset.** `create`/`update` → `client.assets.send`, - with the change comment as `ChangeComment` inside the `update` node (or - at the root of a create). `execute` cannot carry a comment. Delete - `lib/rest1DataTransformer.js` and its test. -4. **B4 Number resolution.** `queryForAssetByNumber` → - `client.resolve.numbers([n])`; a miss is `missing`, not an empty row. - -## Rules - -- Every api/asset query keeps an explicit `page` (the SDK refuses one - without). Values from user text go in `with` variables, not spliced into - `filter`/`where` (an em dash breaks a literal). -- Fixtures send what the server sends (S4): `"True"`, not `true`. -- Live Agility calls go only to the API/SDK testing instance, - `https://www14.v1host.com/v1sdktesting` (`V1_URL`). Writes are allowed - there and nowhere else: name fixtures `sdk-integration …` and delete them - by exact OID when done. Every query sets an explicit page size and stays - well under 20 s of server work. - -## Done when - -- `npm test` passes on each PR; S1's `npm run typecheck` passes. -- `services/v1.js` makes no `fetch` call of its own; reads go through - `client.queryV1` (or `client.data` for `find`), and nothing writes through - rest-1. -- No string-boolean comparisons or local `toMomentless` remain. -- Sandbox-workspace smoke passes: asset mention, `/agility find`, - backlog-item status change, webhook notification. -- Plan 001's checklist lines `S1–S4` and `B1–B4` are ticked, with PR links. diff --git a/plans/004-adopt-sdk-agility-rooms.md b/plans/004-adopt-sdk-agility-rooms.md deleted file mode 100644 index 7260a82..0000000 --- a/plans/004-adopt-sdk-agility-rooms.md +++ /dev/null @@ -1,164 +0,0 @@ -# Plan 004: agility-rooms adopts `@versionone/agility-sdk` (Track D) - -Started 2026-11-14 on `1.0.0-alpha.3` (plan 005's breaking batch), as -session `session_016NSWj5rd2UBmAD3hyYSyT2` from the SDK session. - -Brief for a session on `digital-ai/agility-rooms`, a React/Vite SPA that -runs inside Agility (same-origin, cookie auth). Source plan: -`plans/001-sdk-adoption-campaign.md`, Track D. gh-v1 (plan 002) and the -slack bot (plan 003) have finished their adoptions; this is the last -consumer, and the slowest: a strangler that starts below the call sites so -nothing user-visible moves. - -## Goal - -Rooms talks to the Agility data endpoints (api/asset, query.v1, rest-1, -meta.v1, loc-2, attachments, embedded images) only through -`@versionone/agility-sdk`. Rows arrive translated, so its hand-decoding -goes. UI behaviour stays the same. Rooms' own routes (`api/*` reports, -tags, watch, rooms2config, LLM; `*.mvc` pages) are out of scope unless the -SDK already covers them. - -## Where things stand (surveyed 2026-11-07 at `main` `0874590`, read-only) - -Full survey: plan 001, *What the consumers do today*, the agility-rooms -re-survey paragraph. In short: - -- **HTTP.** `src/utils/axios.tsx` builds the default instance (`baseURL` - the app root, `X-Requested-With`, cookie auth; on 401/403 an SSO popup - via `Account.mvc/Login`, then axios-auth-refresh replays). A response - interceptor turns api/asset `commandFailures` into a localized snackbar - and throws — but only when the URL is exactly `/api/asset`; 9 calls - written `api/asset` bypass it. `getAxiosInstance(...)` variants, a bare - `meta` instance and a plain `axios.create` in `src/navigation/routes.tsx` - skip some interceptors. 88 production files import axios. -- **Endpoints.** api/asset (111 call sites in 57 files), query.v1 (16), - rest-1 reads (4) **and writes** (5 `Data/{T}/{id}` attribute posts, 2 - `?op=Copy`, plus `New/{T}?ctx=` templates), meta.v1, loc-2, `loc.v1`, - `config.v1`, attachments (`MultiValueRelations/Attachments.tsx`: inline - Base64 create, `attachment.img` download), embedded images - (`useImageUpload.ts` → `EmbeddedImage.mvc/Upload`). -- **Reads.** Positional `results[n]` with index constants - (`metaAssetStore/constants.ts`); some board and `ID=$Oids` reads are - unbounded (the SDK refuses an unpaged query). -- **Decoding.** 46 `"True"`/`"False"` comparisons; `NULL_OID` used 118 - times; `massageApiAssetResponseObject` unwraps single relations. meta.v1 - is loaded by `store/metaDefinition/useMetaDefinitionStore.ts`. -- **Tests.** Jest (222 files, module mocks, no MSW); the AI domain's - injected `IHttpClient` / `FakeHttpClient` is the cleanest seam. - Playwright (79 specs) runs against a live server. - -## What the SDK offers rooms (1.0.0-alpha.3, on npmjs as `latest`) - -- `AgilityApiClient` with a pluggable `Transport` (D1 wraps rooms' axios), - or `createFetchTransport` with `credentials: "same-origin"` and - `onUnauthorized` (resend an idempotent request after re-auth; a write is - never resent), `signal` / `client.withSignal` for cancellation. -- One query body, the client picks the endpoint: `client.query` / - `queries` (query.v1, read-only, retried like a GET; rows translated - through the schema; a failed query throws), `client.get(oid, select)`, - `client.queryPage(q)` (rows, total, more), `client.pages` / `client.all` - (page by page up to a limit), `client.me()`. A literal `from` completes - `select` from the type's attributes and types the rows by what was - selected; `Custom_` list types read as `CustomList`; custom and - `ReciprocalOf_` attributes are accepted on every type. -- `client.assets.send` answers an `Outcome` (`created`, `modified`, - `operated`, `failures` with each `violation` parsed — rule, attribute - token, loc-2 tag for the snackbar — and `queryResults`), never the - envelope; `throwOnFailure` throws a `CommandFailureError` carrying the - same Outcome. `client.assets.sendGuarded`, `client.batch`. -- Every error is an `ApiError` with `kind` (`auth`, `forbidden`, - `notFound`, `invalid`, `transient`, `aborted`, `commandFailure`, - `protocol`, `usage`): the one switch the interceptor needs. -- `client.data` (rest-1 reads, query spelling), `client.localize`, - `client.schema` / `TypeCache`, `client.resolve`, `client.attachments` - (list/page/info/get/add/put/remove; inline Base64 up to 1 MB as rooms - does, two-step above), `client.embeddedImages` (upload/normalize), - `client.urls` (asset page, attachment, embedded image). -- Browser-safe: no `node:` imports, no Buffer. Install - `npm install @versionone/agility-sdk@1.0.0-alpha.3` (an exact pin; it is - an alpha). `CHANGELOG.md` in the SDK repo lists each change with its - migration line; the generated model is meta.v1 26.3.1.13. - -## First: a decision for Jim - -Rooms writes through rest-1 (attribute posts with `act`, `?op=Copy`) and -reads `New/{T}` templates, which plan 001 assumed no consumer needed. -Before D1, find out (live, on v1sdktesting) whether api/asset covers each: -an update for the attribute posts (likely), an `execute: "Copy"` or similar -for Copy, and whether `New/{T}` has an api/asset equivalent. Report what you -find to the SDK session and Jim, with a recommendation: move them to -api/asset, or have the SDK grow a narrow rest-1 write path. - -## Steps, one PR each against `main`, from its own branch - -1. **R1 Narrow the axios surface.** Route the axios importers through - `utils/bulkApiService.tsx` and a small set of helpers, so D1 swaps one - module. Fix the `api/asset` vs `/api/asset` interceptor gap on the way. -2. **D1 SDK under the existing axios.** A `Transport` that delegates to - rooms' axios instance (keeps the SSO refresh and the snackbar); point - `bulkApiService.tsx` at `client.assets.*`. Rows arrive translated, so - `getApiResults`' `IsReadOnly` patch goes. -3. **D2 Domain by domain.** Start with `src/domains/ai`, then retire the - string-boolean and `NULL_OID` decodes per feature; attachments onto - `client.attachments`, embedded images onto `client.embeddedImages`; - positional `results[n]` onto named or `queries` results; page every read. -4. **D3 Native transport.** Replace the axios-backed Transport with - `createFetchTransport({ credentials: "same-origin", onUnauthorized })`, - the SSO popup as the hook. -5. **D4 Schema convergence.** Evaluate replacing parts of - `src/metaAssetStore/` and `useMetaDefinitionStore` with the SDK's - `schema` / `TypeCache`. - -## D2: the add-asset flow (recorded 2026-11-14 at Jim's request) - -What the server gives, source-checked in Core and verified live: -`New/{T}?ctx=` takes exactly one OID (a comma list is a 400 "Invalid OID -token") and runs the type's `IAssetInitializer`. `StoryInitializer` -picks a strategy by the context's type — Epic, Scope, Theme, Issue, -Request, Retrospective, Room, Idea. From a Room it sets Scope (the -room's open DefaultScope, else the room's Scope when it is inside the -room's scope context, else the first of that context), Team when the -room has one, and Customer from DefaultCustomer; Defect behaves the -same. Live: `New/Story?ctx=TeamRoom:57394` answered Scope:2204 and -Team:4094. So `client.data.template(type, { ctx: room })` is the -server's defaults for an add dialog, and the SDK's single-ctx signature -matches the endpoint. Core's multi-context initializer (which also -derives Team from a StoryStatus) is reachable only through Core's own -add-asset controller, not the API. - -What rooms does today: `AddAsset.tsx` recomputes the room defaults by -hand (DefaultScope, Team from the team store), without Customer and -without Core's scope-context fallback; the board passes -`prefilledAttributeValues` (column Status, row value, Timebox for the -backlog column) through the URL; nothing from the filter drawer reaches -the dialog. - -Proposed D2 shape (waits for Jim): `client.data.template(type, { ctx: -room })` for the server's defaults, plus a rooms-side layer where every -active filter can offer an initial value, with the board column and row -as today. The API gap behind that layer is in plan 005. - -## Rules - -- Jest and the existing Playwright gates pass on every PR. No behaviour or - UI change unless a step says so. -- Live Agility calls go only to `https://www14.v1host.com/v1sdktesting` - (`V1_URL`, token in `V1_ACCESS_TOKEN`; Node's fetch needs - `NODE_USE_ENV_PROXY=1`). Writes are allowed there and nowhere else: name - assets `sdk-integration …` and delete them by exact OID. Every query is - small and explicitly paged. Point Playwright at nothing else. -- An EmbeddedImage cannot be deleted; upload one only when a test needs it. -- If the SDK lacks something, don't work around it in rooms: tell the SDK - session (`session_016NSWj6mJotpoJ4FontE4Kg`, `send_message`) with what - you need and why; the friction you hit writing real rooms code is the - next findings pass of plan 005. Don't request reviewers; Jim picks. - -## Done when - -- Rooms reaches the data endpoints only through the SDK; no file outside - the transport adapter imports axios for them. -- No `"True"`/`"False"` comparisons or `NULL_OID` checks on SDK rows remain. -- Attachments and embedded images go through the SDK. -- Plan 001's checklist line `D1–D4 agility-rooms` is ticked, with PR links - (tell the SDK session as each merges). diff --git a/plans/005-ergonomics-and-hardening.md b/plans/005-ergonomics-and-hardening.md deleted file mode 100644 index 38e598a..0000000 --- a/plans/005-ergonomics-and-hardening.md +++ /dev/null @@ -1,539 +0,0 @@ -# Plan 005: Ergonomics and hardening before agility-rooms adopts the SDK - -Refined 2026-11-12 against this repo at `2841655` (`1.0.0-alpha.2`). The -first draft was written in a brainstorming session without the code; this -version checks every item against `src/`, `test/`, `specs/000-layers.md` -and plans 001–004, keeps what is still open, marks what is already done, -and ranks the findings by how much friction each causes. The consumer -repos (gh-v1, k6i-agility-slack-bot, agility-rooms) are not re-read here; -what they need comes from plan 001's surveys, plans 002–004, and the SDK -fixes their adoptions forced (#42–#45, #50, #52). - -## Context, corrected - -- **What exists.** One client (`AgilityApiClient`) over a pluggable - `Transport`; wire modules for api/asset, query.v1, rest-1 (reads only), - meta.v1, loc-2.v1 and attachments; services for schema (lazy, shareable - `TypeCache`), resolve (numbers → assets), guard (ChangeDate - compare-and-set), batch (receipt and reverse), attachments and embedded - images. 165 generated asset types (`AssetTypeName`, - `ConcreteAssetTypeName`, `AssetTypeMap`). ~8,000 lines of source, 131 - unit tests, 33 integration tests on `v1sdktesting`, CI on Linux and - Windows, staged publishing to npmjs. Spec 000 holds the invariants. -- **Who depends on it.** gh-v1 (vendored ESM build, hash-checked, pinned - `1.0.0-alpha.0`+) and the slack bot (exact pin) are done; agility-rooms - (plan 004) has not started, and its first step is a decision on rooms' - rest-1 writes, `?op=Copy` and `New/{T}` templates. -- **What adoption already taught.** gh-v1's C2–C3 forced six fixes - (aliased-only batches, `BatchRecordError`, `names` before confirm, - `NULL` relations, Rank as number); the bot's B2/B4 forced group-row - translation, `TypeCache`, absent-root memory and the `Template` state. - Each was a real gap the fakes had not shown, which is the argument for - doing this pass before rooms' 111 api/asset call sites move. - -## Goals (unchanged) - -- Common tasks simple, hard tasks possible. -- Types that guide a person or an agent to correct use. -- Close the gaps the slack bot, the CLI and rooms expose. - -## Review 1: Ergonomics — findings, ranked by friction - -Read-only pass, done. Each finding names the code and the fix. **B** marks -a breaking change; those go out together (see Sequencing). - -1. **`assets.send` returns the api/asset envelope** (`AssetApiResponse`: - `assetsCreated.oidTokens`, `queryResult.results`, `requestId`…) while - `batch.run` returns `Outcome` in model terms. Spec 000 §2 says a caller - never sees an envelope; the client's own `send` hands one over, and a - caller must know `outcomeOf` exists. **B:** `send` returns `Outcome` - (with the envelope reachable for the passthrough case). -2. **Nothing typed guides a query.** `QueryBody.from` and `select` are - `string`; `query(…)` is a caller's assertion, not a derivation, - so a typo in `select` compiles and `row.Name` is typed as if the - whole asset were selected. `AssetTypeName`/`AssetTypeMap` are exported - and unused. Fix: `from: AssetTypeName | (string & {})` (completion - without closing the door on an OID or alias), rows typed from - `AssetTypeMap[F]`, and first-hop `select` completion from the type's - keys; the same for `CreateBody` (required attributes are already - non-optional in the generated types). Not breaking if done as - overloads; verify with gh-v1's JSDoc consumers. -3. **A caller chooses the endpoint.** `client.assets` (api/asset), - `client.queryV1`, `client.data` (rest-1), `client.meta`: the namespaces - are the API's history, and the same read is written three ways with - three parameter spellings — api/asset `select`/`filter`/`with` vs - rest-1 `sel`/`where`/`with` (and `where` means a filter list there, a - map on api/asset). **B:** one `QueryBody` everywhere (the rest-1 wire - module translates to `sel`); `client.query`/`queries` as the default - read path (query.v1: read-only, retryable), `client.assets` for - batches, `client.history` for rest-1's Hist. `client.localize` is a bare - method beside namespaces; fold URL builders into one place (finding 9). -4. **Errors are typed, not classified.** `ApiError` carries `status`, - `code?`, `details`; codes exist for the transport (ABORTED, TIMEOUT, - NETWORK_ERROR, BAD_JSON), queries (QUERY_REJECTED, BAD_RESPONSE), - attachments (NOT_FOUND, TOO_LARGE) and `CommandFailure` (status 200, - which surprises), but nothing says "auth", "permission", "not found", - "invalid" or "transient": the bot switches on `status === 401` itself, - `schema.load` on `404`. Programmer mistakes (an unpaged query, an empty - batch, a bad OID) are plain `Error`/`TypeError`. Validation failures are - text tokens (`Violation'Required'…'Content'`) nobody parses. Fix: a - `kind` on every SDK error (`auth | forbidden | notFound | invalid | - transient | conflict | usage`), a `UsageError` class for mistakes the - SDK catches before any request, and a parser for the violation tokens - (attribute, rule) that loc-2 can label. Messages already say how to fix - most things; keep that bar. -5. **Totals and paging stop at the wire.** `page` is required and spelled - the same everywhere (good). But `client.assets.query` returns bare rows; - the total and "more?" live only in `assetApi.queryPage` (not on the - client, and it needs the schema passed as `lookup`), `rest1.list`'s - `ReadResult`, and `attachments.page`. Fix: `client.queryPage` on the - client, and `client.pages(q, { limit })` as an async iterator with a - hard cap, so no consumer writes its own loop. -6. **Common tasks take hand-assembled calls.** Counted against the code: - - one asset by OID with chosen fields: a query with `from: "Story:1"` - and a mandatory `page` (or rest-1 `data.get`). Add `client.get(oid, - select)` → one translated row or null. - - create a story in a status by name: query StoryStatus, query Scope, - then create — three calls and knowledge of list types. See Review 2.1. - - comment on an asset: one batch (Conversation + Expression under an - alias) but the shape (`Author`, `BelongsTo`, `Content`, `Mentions`) - is folklore; and `Author` needs the current Member, which nothing - returns. Add `client.me()` and a conversations service (Review 2.3). - - a link to an asset's page: none; gh-v1 kept `permalink` local. -7. **Two APIs ship from one entry.** `src/index.ts` exports every wire - module (`assetApi`, `rest1`, …), `select`, `AttributeLookup`, - `Schema.ensureFor`: the low-level API beside the model one. It is - needed (gh-v1's `gh v1 api` passthrough, custom transports, the bot's - fixtures) but it doubles what an editor completes. Decide (Open - Questions): keep both, move the wire layer to a `./wire` subpath - export and mark it advanced. -8. **Naming, smaller.** `attachments.remove` beside `execute: "Delete"`; - `info`/`get`/`list`/`page` on attachments vs `query`/`queries` on - assets; `resolve.ref` vs `resolve.numbers`; `batch.reversible` is a - builder, `batch.run` a sender. Settle one verb set (`get`, `list`, - `page`, `create`, `update`, `remove`, `run`) in the breaking batch and - keep the old names as deprecated aliases for one alpha. -9. **Three URL builders, two homes.** `attachments.url`, - `embeddedImages.url`, and (missing) the asset page. One `client.urls` - namespace: `asset(oid)`, `attachment(id)`, `embeddedImage(id)`. -10. **What is fine and should not move.** `page` shape; `Ref` as the one - relation shape; `with` variables for user text; reads that throw on a - failed query; guard/receipt semantics; `TypeCache` sharing; the - doc-comment density on public types. Spec 000 stays the gate. - -## Review 2: Capabilities consumers need - -Checked against meta.v1's actual document (`metamodel.json`) and the code. - -1. **Asset type metadata — half there.** `schema.load(type)` already gives - each attribute's `AttributeType`, `IsRequired`, `IsReadOnly`, - `IsMultivalue`, `RelatedAsset`, `DisplayByAttribute`, and the type's - `Operations`. Missing: allowed values for a list relation (`Status` → - rows of `StoryStatus`, which is a `List`: `Name`, `Order`, `Inactive`, - `SelectedInSchemes`; the right rows depend on the project's scheme), - and what an operation takes (meta names only a `Validator` attribute - per operation, `CheckQuickClose`; argument shapes are not in meta and - must be documented by hand). Build `schema.describe(type)` (attributes - grouped: required, writable, relations with their value type, custom) - and `lists.values(type, attribute, { scope })` that resolves the scheme. - Consumers: slack (Gap 1), CLI (Refinement 3), rooms forms. -2. **Type discovery — only the 7 MB read.** `meta.metamodel()` is the - single listing; `AssetTypeName` is compile-time only. Rooms loads - per type with ETag revalidation; the transport has no `If-None-Match`. - Build `schema.types()` (token, DisplayName, Abstract, Base) from a - seeded metamodel, a `download:metamodel`-style cache helper with ETag - support in the fetch transport, and document seeding the client with - the committed `metamodel.json` for apps that can ship it. -3. **Conversations and expressions — types only.** Generated types exist; - the batch service knows a Conversation has no `Delete` (the server - removes it with its last Expression) and `Expression` has one. No - service; issue #37 (2017) is the same pain in the old SDK. Build - `conversations`: `post(asset, content, { mentions, inReplyTo })`, - `thread(asset, page)`, `reply(expression, content)`, `remove(expression)`, - with `client.me()` for `Author`. Verify live what `AuthoredAt` and - `Mentions` accept. Consumer: slack (thread sync, summary comments); - rooms shows conversations today. -4. **Search — building blocks exist.** `find`/`findin` on `QueryBody` and - rest-1 `ReadParams`; spec §5 records full-text index lag. Add - `client.search(text, { types, scope, page })` over `find` with a - documented caveat (a just-created asset is not indexed yet), and a - duplicate-candidate helper that ranks by name similarity client-side. - Consumer: slack (Gap 8). -5. **Webhooks — nothing.** The bot handles its webhook notification - itself. Survey its handler and the Agility webhook/subscription - payloads live before designing; then a `webhooks.parse(body)` that - yields `{ event, oid, type, changes }` and a verified fixture per - event. Smallest consumer count; last. -6. **Asset links — nothing.** Review 1.9. Take gh-v1's `permalink` - implementation (`assetdetail.v1?oid=`), verify it opens on - `v1sdktesting`. Three consumers; first. - -## Review 3: Agent-friendliness - -1. **Doc comments: present and dense** on every public type and method, - with live-verified behaviours dated. Keep; add `@example` blocks to - the dozen entry points agents reach first (`query`, `send`, - `sendGuarded`, `batch.run`, `resolve.numbers`, `attachments.add`). -2. **Examples: one, in the README.** Add `examples/` (runnable against - `V1_URL`/`V1_ACCESS_TOKEN`, each under a minute, fixtures named - `sdk-integration …` and deleted): paged query with sub-selects, create - with relations, guarded update, batch with receipt and reverse, - attachments, a comment, browser cookie auth with `onUnauthorized`, a - server with one `TypeCache` per instance, error handling by `kind`. -3. **Failure messages: mostly specific.** Gaps are in Review 1.4 (kinds, - violation tokens). The CommandFailureError message already reads like - the server's words, root cause first; keep. -4. **A guide for agents.** The consumer repos use `AGENTS.md`; this repo - has none, and `.claude/` holds only settings. Write `AGENTS.md` - (how to read spec 000, the two test tiers, where writes may go, - "values travel as `with` variables") and a consumer-facing skill - (`skills/agility-sdk/SKILL.md`, publishable) that teaches the model - API, the error kinds and the five examples above. Consider a generated - compact API summary (from `dist/index.d.ts`) for context windows. - -## Review 4: Quality and release - -1. **Coverage, per module** (unit / integration tests): assetApi 15/6, - batch 14/6, attachment 14/2, guard 8/4, queryV1 8/4, resolve 7/2, - translate 7, select 8, loc2 6/2, fetchTransport 6 + auth 8, schema 5/2, - rest1 5/3 (history: one each), meta 4/0, typeCache 3, oid 4, cjs 2, - layers 3, generate-types 4. Thinnest: rest-1, meta, the client's own - composition (`client.ts` is exercised only through other suites), and - the transport's retry/5xx path live. Add: a client suite (ensureFor - before every read, `withSignal`, option spellings), rest-1 history and - `needTotal`, meta error shapes, and recorded responses for the awkward - cases spec §2a lists (dropped result slots, `NULL` relations, group - rows, state `200`). Add `scripts/record-fixture.mjs` so a fixture is - captured, not typed. Run the integration tier on a schedule (weekly, - environment-gated secrets) so live drift shows up before a consumer - does. -2. **Versioning policy — write it down.** Today: alphas on the `alpha` - dist-tag, README says "may change between them", no CHANGELOG. Policy: - every alpha has a `CHANGELOG.md` entry with a migration line per - breaking change; `1.0.0` after rooms D1–D2 are merged; semver after - that; a renamed export keeps its old name as a `@deprecated` alias for - one minor; the breaking batch below is the last planned one before - `1.0.0`. -3. **Upgrading consumers.** gh-v1 re-vendors on a pin change - (`npm run vendor:sdk`, hash-checked); the bot and rooms pin exact - versions. The breaking batch therefore ships as one alpha (`alpha.3`) - with a handoff per consumer, and rooms starts D1 on it, never on - `alpha.2`. -4. **Publishing — done.** npmjs under `@versionone/agility-sdk`, trusted - publishing from a `v*` tag, staged and approved with 2FA (plan 001 - A6, #53–#54). Nothing to decide. - -## Sequencing - -1. **Findings pass** — this file. Read-only, done 2026-11-12. -2. **Breaking batch → `1.0.0-alpha.3`** — done and published 2026-11-12 - (`alpha` dist-tag; tag `v1.0.0-alpha.3` at `071f0d5`), one release - before rooms D1: Review 1.1 (Outcome from `send`), 1.3 (one query - shape; `client.query` the default read path, rest-1 behind `data`), - 1.4 (error kinds, `UsageError`, parsed violations; spec 000 §7), 1.5 - (`queryPage`/`pages`/`all` on the client), 1.6 (`get`, `me`), 1.9 and - 2.6 (`urls`), 1.2 (typed `from`/`select`). 1.8's verb review kept the - existing names: the only renames are the read path and the URL - builders, each with a deprecated alias for this alpha. `CHANGELOG.md` - carries the migration lines; handoffs: `plans/handoffs/alpha-3-gh-v1.md` - and `plans/handoffs/alpha-3-k6i-agility-slack-bot.md`, started - 2026-11-14 as sessions `session_016NSWj9RUo5RDw3AvSzQbUd` (gh-v1) and - `session_016NSWj8xjTv47UXheToZpGg` (slack bot, against `main`: - `bolt-migration` is merged). -3. **Rooms D1 on `alpha.3`.** Plan 004's first decision (rest-1 writes, - Copy, `New/{T}`) and the live friction list from writing rooms' top - flows (board load, status change, attachment, conversation) come back - here as a second findings pass. -4. **Capabilities, by consumer count:** `urls.asset` (in step 2) → - `schema.describe` and `lists.values` (slack, CLI, rooms) → conversations - (slack, rooms) → search (slack) → ETag and `schema.types()` (rooms) → - webhooks (slack; survey first). -5. **Agent and docs track, alongside:** `AGENTS.md`, examples, the skill, - `@example` blocks, CHANGELOG discipline. -6. **Quality track, alongside:** client suite, rest-1/meta tests, fixture - recorder, scheduled integration run. - -## Findings from the adoption sessions (the second pass) - -**agility-rooms, 2026-11-14** (live on v1sdktesting, alpha.3; plan 004's -first decision). Rooms' rest-1 writes are 6 attribute posts, 2 -`?op=Copy`, 1 `New/Timebox?ctx=` template read, and 2 plain reads. - -1. **Attribute posts: covered.** `update` with `{ add }` / `{ remove }` - on a multi-value relation and a scalar set or `null` clear do what the - posts do; `client.get` reads it back. Rooms moves them to - `client.assets.send` in D2. No SDK change. -2. **Copy: api/asset discards the result.** `execute: "Copy"` performs - the copy but the envelope reports only `assetsOperatedOn: [source]`; - `assetsCreated` is empty and `args: { Name }` is ignored, so the new - OID is unobservable. rest-1 `?op=Copy` answers `{ id: "Story:…:…" }`. - Querying "newest by CreatedBy" is a race, and CreateDate is stamped in - instance-local time, so a since-now filter misses it. **Requested:** a - narrow rest-1 operation path, `client.data.operate(oid, "Copy")` - answering `{ oid }` momentless (or `client.copy(oid)`): the one POST - whose return value api/asset drops, not a general rest-1 write path. -3. **`New/{T}?ctx=`: no api/asset equivalent.** The template carries - server-computed defaults nothing else exposes (next sprint name, - dates after the schedule's last timebox, Schedule, State, Owner); - `assets.preview` echoes the command and meta.v1 carries no defaults. - **Requested:** `client.data.template(type, { ctx })` (GET, idempotent), - answered like `data.get` (Refs, translated scalars, `act` wrappers - dropped). One rooms site uses it. -4. **Reads: covered** by `client.data.list` / `data.get` / `client.get`. - Observation: a rest-1 row spells a nested select twice (`Parent: { - _oid, Name }` and `"Parent.Name"`), which `client.get` does not. That - is `fromAssetDocument`'s documented fold; worth deciding whether the - dotted key should go once rooms is on `client.get` (Review 1.3). - -Decided 2026-11-14 (Jim): grow the two narrow rest-1 paths. Done in -alpha.4 as `client.data.operate` / `client.copy` and -`client.data.template`. Read in Core (`versionone/Core`, attached to the -SDK session read-only that day): rest-1's `New` is -`NewAssetQueryProcessor` → `Services.GetNewAsset(assetType, ctx)`, which -builds a `CandidateAsset` and runs the type's registered -`IAssetInitializer.InitializeNewAsset(asset, context, …)` (Meta.xml's -``; `TimeboxInitializer` is what names the sprint and dates -it); a type without one gets only its primary relation to the context's -type set, which is why a Story in a Scope gets just `Scope`. api/asset's -`ctx` on a create applies the same initializer but does not echo the -values. The docs page is `apis/rest-1/new-asset-templates` in -`digital-ai/docs-agility-api`. - -**Fields for a form, 2026-11-14 (Jim):** required, visible and important -attributes given an asset's Scope. Three sources, now one call, -`client.fields.describe(type, scope)`: meta's `IsRequired` (writable -attributes), `api/visibleFields?scopeOid=…&assetTypeToken=…` (both keys -repeat; one entry per type; custom fields once configured; a global -type ignores the scope and mixing global and non-global is 400 — Core's -`DisplayedFieldsController`, docs `apis/field-visibility/visible-fields`), -and `Scope.Scheme.ImportantFields` (a multi-value Text of attribute -tokens — selecting it with a sub-select, as if a relation, crashes the -server with a NullReferenceException) with `Scheme.IsRequiredAtClose`. -`api/visibleFieldsByAsset?oidToken=` answers 500 for an asset of a -global type (a Member) on v1sdktesting; `api/customFields` is a POST. -Not yet covered: the "required at close" enforcement details -(`ImportantEnforcementController`) and `api/customFields`; the testing -instance's schemes name no important fields, so that path is proven on -shape only. - -**Totals (Jim, 2026-11-14):** api/asset reports the total of a paged -query (`needTotal`) beside the rows; query.v1's response is a bare list -of row lists with nowhere to put it. That was a reason consumers routed -reads through api/asset. The client keeps both: `query` on query.v1 for -rows, `queryPage` on api/asset for a total; `pages`/`all` stop at a short -page and need no total. The client's doc comment says so. - -**gh-v1, 2026-11-14** — on alpha.3 in versionone/gh-v1#64 (175 tests, -typecheck, live smoke; CLI output unchanged). Nothing in gh-v1 caught -`TypeError` or read more than one page, so `UsageError` and `client.all` -had nothing to replace there. Seven findings: - -1. **`parseViolation.tag` on the token spelling** gave the token - (`Link.Asset`), which is not a loc-2 tag. Fixed: null there. -2. **`urls.asset` takes only an OID**; the CLI links by Number - (`assetdetail.v1?Number=S-1`), the readable form every command prints, - so it keeps its own builder. Done in alpha.4: `urls.asset` takes - either (docs `apis/asset-permalinks`: both forms 302 to the detail - page; a signed-out reader lands there after login). -3. **The npm package shipped no CHANGELOG**; migration lines had to be - read off the deprecation notes in `index.d.ts`. Fixed: `files` includes - it. -4. **A rejected query surfaces differently** on the new read path: an - `ApiError` kind `invalid`, code `QUERY_REJECTED`, `details` the whole - error document, where api/asset gave a `CommandFailureError` whose - message was the token. A caller that prints `details` now prints a - JSON blob; `message` already holds the server's sentences. Add the - migration line (alpha.4) and consider trimming `details` to the - exceptions. -5. **`queryV1` is the deprecated name while `client.query` is what goes - to query.v1**, and the deprecation note on `assets.query` says "read - through query.v1", which reads as pointing at `queryV1`. Sharpen the - notes (alpha.4). -6. **`CommandFailureDetail.echoed`** rides on every failure, so a caller - that serialises failures gets the echoed body; gh-v1 re-maps. Note it; - maybe make it lazy or optional later. -7. **"Retried like a GET" needs `retries > 0`**, which defaults to 0; the - doc comment on `client.query` promises a retry the default config never - makes. Say so in the comment, or default `retries` to 1 for idempotent - requests (alpha.4, a behaviour change to call out). - -**k6i-agility-slack-bot, 2026-11-14** — on alpha.3 in -digital-ai/k6i-agility-slack-bot#63 against `main` (175 tests, typecheck, -live smoke: mention resolve, find, createWithNumber, update, -QuickClose/Reactivate, a Required violation, a UsageError; fixtures -deleted by OID). The deprecated aliases carried it; the only breaking -surprise was envelope → Outcome in writes (7 tests, all where expected). - -1. **CHANGELOG not in the tarball** — same as gh-v1's 3; fixed. -2. **Asset URL by Number** — same as gh-v1's 2, now two consumers: cards - and find results link `assetdetail.v1?Number=S-01022`, what the user - typed. First item of alpha.4. -3. **A UsageError reads like a server failure** to a wrapper that formats - every ApiError as "Agility failed: " (status 0). - The bot special-cases `instanceof UsageError`. Add a doc line: `kind: - "usage"` means nothing was sent; and consider a `sent: boolean` or - excluding status 0 from such formats in the README example. -4. **Violation localization works** end to end (`client.localize([tag])` - → "Project"); not yet wired into a reply. -5. **`outcomeOf` tolerates a partial envelope** (a fake sending only - `queryResult`); the bot's fakes depend on it. Make it a stated - contract (it is: every field is read with a default) and a test. -6. **Outcome has no `requestId`** — added in alpha.4. -7. **Testing-instance data:** `v1sdktesting` has no StoryStatus rows, so a - Story status change cannot be exercised live there (spec §5 already - notes the empty instance). Seed a few, or test on a Defect/Epic status. - -**agility-rooms R1, 2026-11-14** — digital-ai/agility-rooms#1464 -(`r1/narrow-axios-surface`, against `main`): every Agility data-endpoint -call now goes through one helper module per endpoint under `src/utils` -(api/asset, query.v1, rest-1 with `executeCopy` and -`getNewAssetTemplate`, meta with ETag, loc-2, embedded images); the -`/api/asset` interceptor gap is fixed. D1 keeps `executeCopy` and -`getNewAssetTemplate` on rest-1 through the transport adapter until -alpha.4 is on npm, then moves them to `client.copy` / -`client.data.template`. Flagged early for D1: rooms re-parses the -metamodel only when meta.v1's ETag changes; `client.meta.metamodel()` -cannot surface that header today, so rooms either keeps its own meta -instance for that one read or the SDK grows `meta.metamodel({ -ifNoneMatch })` answering `{ metamodel, etag }` and null on 304 (Review -2.2's ETag item, now concrete; alpha.5). - -**agility-rooms D1, 2026-11-14** — digital-ai/agility-rooms#1465 -(`d1/sdk-under-axios`, stacked on R1 #1464), SDK pinned `1.0.0-alpha.4`. -`src/utils/agilityTransport.ts` is a `Transport` over rooms' axios -instance (paths, body, responseType, signal and onResponse mapped; axios -failures become ApiErrors with the server's status and words); -`agilityClient.ts` holds the client. Moved: `executeCopy` → -`client.copy`, `getNewAssetTemplate` → `client.data.template`, -`fetchLocalizations` → `client.localize`, `uploadEmbeddedImage` → -`client.embeddedImages.upload`. api/asset and query.v1 stay raw through -the transport until D2 (rooms' 118 `NULL_OID` sites decode by hand, and -the SDK's reads turn `NULL` refs into null even without a lookup). -meta.v1 stays on rooms' own instance until D3 (alpha.5's conditional -reads). Three findings, none blocking: - -1. **No untranslated path through the SDK.** The only way to get a raw - envelope is `Transport.request` with a caller-spelled path. A - `translate: false` (or `lookup: null` meaning "leave the rows as - sent") on `assetApi.send` / `queryV1.queries` would let an adopter - route through the wire modules first and translate second. Candidate - for alpha.6; weigh against spec 000 §2a ("a consumer never sees the - untranslated shape") — it is a strangler's need, not a steady state. -2. **A custom Transport reimplements `messageOf`** (the server's error - words) because it is not exported. Export it, or an - `apiErrorFromResponse(status, data)` helper, so custom transports' - errors match the fetch transport's. Small; alpha.6. -3. **`ClientConfig.baseUrl` is required even with a custom Transport**, - though only `urls.*` read it. A doc line. - -The instance's database began refusing logins at about 04:20Z that day -("Cannot open database v1sdktesting requested by the login"), which the -transport reports as kind `transient` (batch upgrades that night). Back -by 07:11Z; the D1 transport smoke then passed on every call (me, -translated and raw reads, create, copy, template, localize, config.v1, -loc.v1, a rejected query as invalid/QUERY_REJECTED, a 404 as notFound, -an abort as aborted), fixtures deleted by OID; result on #1465. - -**Initial values from a view — an API gap, recorded 2026-11-14 at Jim's -request.** Core's gadgets treat the user's current view as the context -for a new asset: `FilterBubbleContext` asks every filter applicator on a -gadget for `ProvideInitialValues(querySettings)`, folds in the config -filters, and the boards add the clicked column (Status) and row (the -group-by value); the set travels as `FilterAttributeValues` on the -add-dialog URL. Jim's framing: the boundary between filters and context -is a matter of perspective, not mechanics. This lives entirely in -VersionOne.Web; `New/{T}` — and so `client.data.template` — knows one -OID context and nothing of a view. An API-first client therefore has no -server-side way to ask "the initial values for a new {T} given this view -(these filters, this column, this row)". Options, not decided: (a) the -consumer keeps a client-side layer where each active filter offers an -initial value, over the server's OID template (rooms' proposed D2 -shape); (b) Core grows an API surface for the filter context — `New/{T}` -with a filter body, or an api/asset preview that accepts a view — which -the SDK would then wrap. Source-checked by the rooms session with Core -attached read-only. - -**alpha.5, started 2026-11-14** with the ETag-aware schema reads rooms -asked for. Live: meta.v1 sends one strong ETag for the whole schema -(release plus a change stamp), identical on the document, a type and an -attribute; `If-None-Match` with the exact tag is a 304 with no body (a -weak form is not matched); Cache-Control is public, max-age under an -hour. Core sets the same kind of tag on loc-2 (`BaseLocalizationHandler`), -a later item. Shipped: `client.meta.read` / `readType` (`{ value, etag }` -or null), `client.meta.etag()`, `TypeCache.etag`, -`client.schema.revalidate()`, and `RequestOptions.acceptNotModified` on -the transport. The three leftovers followed the same day: `retries` -defaults to 1 for idempotent requests (a 429 counts as transient, with -Retry-After honoured), a rejected query's `details` are the exceptions -alone, and `echoed` is readable but not enumerable on a failure. The -live tier passed in full on 2026-11-14 at 07:20Z, after the instance's -batch upgrades (40 passed, 3 skipped: custom fields on a hosted -instance, embedded images opt-in). Published 2026-11-14 (`latest` and -`alpha`; tag `v1.0.0-alpha.5` at `9f7e0db`), smoke-tested from a fresh -consumer: etag, a 304 on readType, revalidate unchanged, a rejected -query's trimmed details, echoed readable but not serialised. The -instance came back from its batch upgrades on 26.3.2.13 (the ETag's -release part moved from 26.3.1.13), so the committed metamodel is one -patch release behind again: refresh it after the public-repo hygiene PR -lands, since that PR changes how the pull is normalised. - -**alpha.4 (2026-11-14)** — published the same day (`latest` and -`alpha`; tag `v1.0.0-alpha.4` at `f77e8d5`), smoke-tested from a fresh -consumer against v1sdktesting (copy, template, fields, Number URL, -requestId, CHANGELOG in the tarball) — carries all of the above that was marked -alpha.4, plus the two rest-1 paths and `client.fields`. Still open from -the lists: trimming `QUERY_REJECTED` details, `echoed` on every failure, -defaulting `retries` for idempotent requests. - -## Open questions, with answers where the code gives one - -- **Least polished today:** rest-1 (reads only, its own parameter - spelling, thinnest tests), meta/schema discovery (no listing short of - the whole document, no ETag), the client's composition (untested on its - own), and the three absent capabilities (conversations, webhooks, - links). -- **Low-level and high-level, or one?** Both already ship from one entry. - Recommendation: keep both; the model API is the main entry, the wire - layer moves to `@versionone/agility-sdk/wire` and is documented as the - passthrough and transport-author surface. Decide in step 2. -- **Auth and configuration across consumers:** `ClientConfig` takes - `accessToken` (Bearer; gh-v1 from env or `~/.v1config`, the bot per - workspace), `authorization` (a ready header; Basic on dev instances), or - cookie `credentials` with `onUnauthorized` and `unauthorizedStatuses` - (rooms, SSO popup). A server building a client per user shares one - `TypeCache` per instance. Not covered: OAuth refresh (the bot's Flow 4 - stays in the bot) and ETag caching. Enough for the three consumers; - document it in one place. -- **Who else** (answered 2026-11-12): the Agility MCP server is Python, so - not a consumer; this SDK is the reference for updated Python bindings - (its layers, one-shape model, error kinds and guard are the parts worth - carrying over). Nearer-term TypeScript surfaces: a Microsoft Teams app - and a VS Code extension, both shaped like the slack bot (token per - user, one `TypeCache` per instance) and the CLI respectively; a Claude - mod is possible. Also ossum-ui (named in `src/types/oid.ts`), Core's - gulp `v1api` helper, and any customer integration now the package is - public. -- **An API console is a surface too**, and a demanding one: it would - exercise the schema service, the error kinds and the typed query shape - directly, and surface gaps faster than an app with a fixed set of - queries. Two pieces exist: `src/wire/select.ts` resolves select paths - hop by hop against meta (dotted paths, downcasts, filters, aggregates), - which is the seed of a type-ahead for `select`; and api/asset's - `previewOnly` with `sendYaml` parses a batch server-side. The `filter` - grammar is not parsed client-side at all. A browser-safe parser for both - grammars (`V1Query.g` as the docs publish it), driven by meta, with - completion of attributes, operators and list values, is a candidate - module (`services/grammar`, say) that the console, rooms' query builders - and the typed-query work would share. Add it to Review 2 as 2.7 once a - first consumer commits to it. -- **The committed metamodel was behind the hosted instance** — refreshed - 2026-11-12 to meta.v1 26.3.1.13 (from `v1sdktesting`; the diff was - three Workflow types and four attributes, nothing removed). The - download script now strips instance-specific definitions and reports - them, so the pull is reviewable from a hosted instance as well as from - a dev instance on a blank database (Core's gulp task). Refreshing is a - step of each release; the README names the version. Custom list types (`Custom_`, Base List) and custom - attributes (`Custom_…`, and `ReciprocalOf_…` on a custom relation's - target) are instance-specific by design and handled without generation. -- **Still open from #51:** an exact guard stamp (Moment is not one; - ChangeDate as a literal is). Not blocking; the guard works. diff --git a/plans/handoffs/alpha-3-gh-v1.md b/plans/handoffs/alpha-3-gh-v1.md deleted file mode 100644 index 01fc367..0000000 --- a/plans/handoffs/alpha-3-gh-v1.md +++ /dev/null @@ -1,49 +0,0 @@ -# Handoff: gh-v1 moves to `@versionone/agility-sdk@1.0.0-alpha.3` - -Paste everything below the line into the session working on -`versionone/gh-v1`. Source: `plans/005-ergonomics-and-hardening.md` and -`CHANGELOG.md` in `versionone/VersionOne.SDK.JavaScript`. - ---- - -Context: the SDK's breaking batch (`1.0.0-alpha.3`) changes what -`client.assets.send` returns, how errors are classified, and where reads -live. gh-v1 vendors the SDK, so this is one PR: move the pin, re-vendor, -adapt `lib/api.js`, keep every test green and the CLI's behaviour and -output unchanged. `npm test` and `npm run typecheck` gate it. - -1. **Pin and vendor.** `package.json` devDependency - `@versionone/agility-sdk` → `1.0.0-alpha.3`; `npm install && npm run - vendor:sdk`; commit `vendor/versionone-sdk/` and `SOURCE.json` - (`test/vendor.test.js` checks the hashes). -2. **`asset` / `attempt` read an Outcome.** `client.assets.send` now - answers `{ created, modified, operated, failures, queryResults, - durationSeconds }` instead of the api/asset envelope. Wherever - `lib/api.js` or its callers read `assetsCreated.oidTokens`, - `commandFailures`, `queryResult.results`, read `created`, `failures`, - `queryResults`. `attempt` reports each failure by `index` as before; - each failure also carries `violation` (`rule`, `attribute`, `tag`) if - the message was a `Violation'…` token — use it for the "not found — - skipped" and required-attribute lines if that reads better. If a - caller needs the raw envelope (the `gh v1 api` passthrough does not; - it is `raw`), the wire module `assetApi.send` still returns it. -3. **Reads.** `client.assets.query` / `queries` are deprecated (removed - next alpha): move `query` / `queries` to `client.query` / - `client.queries`. These read through query.v1 (read-only, retried like - a GET); rows translate identically (the SDK's integration tier proves - it). A command that needs the total of a query uses - `client.queryPage`; one that walks a whole result uses `client.all(q, - { limit })`, which replaces any paging loop in `lib/paging.js` that - reads more than one page. -4. **Errors.** Every SDK error is an `ApiError` with `kind`. Where gh-v1 - catches `TypeError` from `resolve` or `parseOid`, catch `UsageError` - (or `kind === "usage"`). Where it branches on `status === 401`, `kind - === "auth"` is equivalent and also covers the transport's own codes. -5. **Permalinks.** `client.urls.asset(oid)` builds the same - `assetdetail.v1?oid=` URL gh-v1's `permalink` does; keep `permalink` - as the CLI's name but let it call the SDK. -6. **rest-1 reads** (`rest` in `lib/api.js`): `client.data.list` takes - `select`/`filter` now; `sel`/`where` work this alpha and go next. - -Report back with the PR link, and anything in the SDK that got in the -way: that list is the next findings pass. diff --git a/plans/handoffs/alpha-3-k6i-agility-slack-bot.md b/plans/handoffs/alpha-3-k6i-agility-slack-bot.md deleted file mode 100644 index 8bd73ae..0000000 --- a/plans/handoffs/alpha-3-k6i-agility-slack-bot.md +++ /dev/null @@ -1,45 +0,0 @@ -# Handoff: the slack bot moves to `@versionone/agility-sdk@1.0.0-alpha.3` - -Paste everything below the line into the session working on -`digital-ai/k6i-agility-slack-bot` (`bolt-migration` was merged into -`main` on 2026-11-14; `main` is the target). Source: -`plans/005-ergonomics-and-hardening.md` and `CHANGELOG.md` in -`versionone/VersionOne.SDK.JavaScript`. - ---- - -Context: the SDK's breaking batch (`1.0.0-alpha.3`) changes what -`client.assets.send` returns, how errors are classified, and where reads -live. The bot pins an exact version, so this is one PR on -`bolt-migration`: move the pin, adapt `services/v1.js`, keep -`test/v1-service.test.js` as the parity proof and the sandbox smoke green -(asset mention, `/agility find`, backlog-item status change, webhook -notification). - -1. **Pin.** `"@versionone/agility-sdk": "1.0.0-alpha.3"`. -2. **`bulk` / `exec` / `createWithNumber` / `create` / `update` read an - Outcome.** `client.assets.send` answers `{ created, modified, - operated, failures, queryResults, durationSeconds }`, not the envelope: - `assetsCreated.oidTokens` → `created` (momentless OIDs already), - `commandFailures` → `failures` (by `index`, with `violation` parsed: - `rule`, `attribute` such as `Story.Scope`, and `tag`, which - `client.localize([tag])` turns into the attribute's label — the right - text for a "Scope is required" reply). -3. **Reads.** `client.queryV1.query` / `queries` are deprecated (removed - next alpha): rename to `client.query` / `client.queries`; same - endpoint, same rows, nothing else changes. `findAssets` on - `client.data.list`: `where` → `filter` (`where` still works this - alpha). A `/agility find` that wants the total can use - `client.queryPage`. -4. **Errors.** `AuthFailureError` is thrown where the bot sees `status - === 401`; `err.kind === "auth"` is the same test and also names the - transport's cases. A caller mistake (an unpaged query body, a bad - number) is now a `UsageError`, not a `TypeError`. -5. **Links.** Cards and messages that build an asset URL use - `client.urls.asset(oid)`. -6. **New, for later features:** `client.me()` (the bot's member, for a - comment's `Author` once conversations land), `client.get(oid, select)`, - `client.all(q, { limit })`. - -Report back with the PR link and anything in the SDK that got in the -way: that list is the next findings pass. diff --git a/plans/handoffs/gh-v1.md b/plans/handoffs/gh-v1.md deleted file mode 100644 index 0c84838..0000000 --- a/plans/handoffs/gh-v1.md +++ /dev/null @@ -1,71 +0,0 @@ -# Handoff: gh-v1 migrations G0–G3 - -Paste everything below the line into the session working on -`versionone/gh-v1`. Source: `plans/001-sdk-adoption-campaign.md` in -`versionone/VersionOne.SDK.JavaScript` (branch -`proposed_type_script_api_version`), section *Consumer migrations*. - ---- - -Context: gh-v1 will become the first consumer of `@versionone/agility-sdk` (the -TypeScript rewrite of the Agility JS SDK). Before that swap, gh-v1 should -adopt these migrations so the swap later touches only `lib/api.js`. Do them -as separate PRs, in order, each with `npm test` green (99 tests -today) and no change to existing CLI behaviour or output. - -**G0 — Runs anywhere, configurable from the environment.** Do this first; it -is small. -- The bin script `gh-v1` is committed as mode `100644`, so - `gh extension install versionone/gh-v1` (or `gh extension install .`) - gives "permission denied" on macOS and Linux; Windows does not notice. - Fix with `git update-index --chmod=+x gh-v1` and commit. -- `lib/config.js` reads the instance only from - `~/.v1config/v1api/config.json` (or `V1_CONFIG_FILE`) and prompts when it - is missing. Add: when `V1_URL` and `V1_ACCESS_TOKEN` are both set, use - them as the profile (no file, no prompt), so a cloud or CI environment can - configure gh-v1 from environment variables. A profile in the file named by - `--instance`/`V1_INSTANCE` still wins when one is asked for explicitly. - Document both in the README and add a test. - -**G1 — CommonJS to ESM.** -- `package.json`: `"type": "module"`. Convert every `require`/`module.exports` - in `gh-v1` (the bin script), `lib/`, `lib/commands/` and `test/` to - `import`/`export`, keeping `node:` specifiers. -- Known snags, found by survey: - - `test/apply.test.js` (around line 104) replaces `lib/commands/bulk` via - `require.cache`. ESM has no module cache to patch. Prefer making the - dependency injectable (pass the bulk runner into the apply command, with - the real one as default) over `mock.module`, which needs - `--experimental-test-module-mocks`. - - `test/prompt.test.js` builds a child-process script that `require`s - `lib/prompt.js` by `__dirname` path; switch to an `import()` of a - `file://` URL built from `import.meta.url`. - - Commands that are loaded by name in `lib/cli.js` become dynamic - `import()`s; keep loading lazy so startup stays fast. -- Check `gh extension` dispatch still works on Windows (the README has a - section on argument mangling there): the bin script keeps its - `#!/usr/bin/env node` shebang. - -**G2 — Type checking without a build step.** -- Add `jsconfig.json`: `checkJs: true`, `module` and `moduleResolution` - `NodeNext`, `target: ES2022`, `strict: false` to start, `noEmit: true`. -- JSDoc types on the object returned by `api()` in `lib/api.js` (`query`, - `queries`, `asset`, `attempt`, `rest`, `meta`, `localize`, `raw`, - `blob`, `permalink`) and on `resolveAssets`. That surface is the contract - the SDK swap must preserve. -- Add `typescript` as a devDependency and an `npm run typecheck` - (`tsc -p jsconfig.json`); fix what it finds or `// @ts-expect-error` - with a reason. Runtime dependencies stay at zero. - -**G3 — One paging policy.** -- `page: { ... }` literals appear in ~16 files under `lib/`. Introduce one - module-level default (and named sizes where a command deliberately uses a - different one) so every read is visibly bounded from one place. No - change in the pages actually sent; the tests that assert request bodies - should not change. - -API reference: https://docs.digital.ai/agility-api/ (the whole site as -text: https://docs.digital.ai/agility-api/llms-full.txt). - -Do not start the SDK swap itself (that is milestone C1, run from the SDK -session after G1 merges). Report back with the PR links. diff --git a/plans/handoffs/k6i-agility-slack-bot.md b/plans/handoffs/k6i-agility-slack-bot.md deleted file mode 100644 index f33851d..0000000 --- a/plans/handoffs/k6i-agility-slack-bot.md +++ /dev/null @@ -1,55 +0,0 @@ -# Handoff: k6i-agility-slack-bot migrations S1–S4 - -Paste everything below the line into the session working on -`digital-ai/k6i-agility-slack-bot`. Source: `plans/001-sdk-adoption-campaign.md` -in `versionone/VersionOne.SDK.JavaScript` (branch -`proposed_type_script_api_version`), section *Consumer migrations*. - ---- - -Context: after the Bolt migration, `services/v1.js` (`createV1Client`) will -be re-implemented on `@versionone/agility-sdk`, the TypeScript rewrite of the -Agility JS SDK, keeping its exported surface. These four migrations come -first, so that swap is one file. Work on (or branch from) `bolt-migration`, -not `main`. One PR each, `npm test` green, and follow AGENTS.md and -`docs/auth-flows.md` as usual. - -**S1 — Finish ESM tooling.** The runtime is already ESM (`"type": "module"`, -no `require`). `jsconfig.json` still declares `module: commonJS`, -`target: es6`. Set `module`/`moduleResolution` to `NodeNext`, `target` to -`ES2022`, add `checkJs: true` and `noEmit: true`, and an -`npm run typecheck` (`tsc -p jsconfig.json`; `typescript` is already a -devDependency). Start non-strict; fix or annotate what it finds. - -**S2 — Page every read.** Reads stay on `query.v1`: it is read-only and -answers with rows alone, and the SDK has a `queryV1` module for it. Add an -explicit `page: { size, start }` to every body in `lib/queries.js` (the -SDK refuses an unpaged query; no read should be unbounded), choosing sizes -that match what each caller actually shows. Update -`test/v1-service.test.js` to assert the pages sent. - -**S3 — Close Bolt Phase 2.** AGENTS.md says axios is dropped in Phase 2. -Once nothing imports them, remove `axios`, `botkit`, -`botbuilder-adapter-slack`, `botbuilder-storage-mongodb`, -`botkit-plugin-cms` from `package.json`, and delete `lib/v1api.js` and -the Botkit-era `plugins/` modules that `listeners/` replaced. Keep -`bot.js` only if something still runs it; otherwise `main` should point at -`app.js`. If anything blocks removal, list it rather than forcing it. - -**S4 — Fixtures from real responses.** Replace the hand-written payloads -in `test/v1-service.test.js` and `test/queries.test.js` with responses -captured from the local dev instance (`http://localhost/VersionOne.Web/`), -committed under `test/fixtures/`. A fake must send what the server sends. - -Settled, for when writes move (milestone B3, not part of S1–S4): api/asset -accepts a change comment as `ChangeComment` inside an update command's -`update` node (or at the root of a create), so `update()`'s `?comment=` -has an api/asset equivalent; `execute` commands (QuickClose, Reactivate) -cannot carry one. Reference: -https://docs.digital.ai/agility-api/apis/asset-bulk/updating-assets - -API reference for all of this: https://docs.digital.ai/agility-api/ (the -whole site as text: https://docs.digital.ai/agility-api/llms-full.txt). - -Do not add `@versionone/agility-sdk` yet (milestone B1, run from the SDK session -after S2 and S3 merge). Report back with the PR links. diff --git a/test/unit/publicTree.test.ts b/test/unit/publicTree.test.ts index 6557afa..98f627a 100644 --- a/test/unit/publicTree.test.ts +++ b/test/unit/publicTree.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { readdirSync, readFileSync, statSync } from "node:fs"; +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { join, relative } from "node:path"; /** @@ -11,8 +11,8 @@ import { join, relative } from "node:path"; * test keeps out. */ const root = join(__dirname, "..", ".."); -/** Not scanned: tooling output, dependencies, and `plans/` while it is on its way out of the tree. */ -const SKIPPED = new Set([".git", "node_modules", "dist", "coverage", "plans"]); +/** Not scanned: tooling output and dependencies. */ +const SKIPPED = new Set([".git", "node_modules", "dist", "coverage"]); const SHAPES: { what: string; pattern: RegExp }[] = [ { what: "a Claude session id", pattern: /\bsession_[A-Za-z0-9]{20,}\b/g }, { what: "a GitHub repository other than this one", pattern: /(? { expect(found).toEqual([]); }); }); + +describe("the tree holds no working plans", () => { + it("has no plans/ directory: plans and hand-offs live in the private planning repository (CONTRIBUTING.md)", () => { + expect(existsSync(join(root, "plans"))).toBe(false); + }); +});