Skip to content

Commit 32454a7

Browse files
authored
Merge pull request #41 from versionone/claude/admiring-ride-vsq4sa
SDK: transport for real callers, CI, and consumer adoption briefs
2 parents c731e43 + 227c5e4 commit 32454a7

14 files changed

Lines changed: 441 additions & 16 deletions

‎.claude/settings.json‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(npm run test:integration*)"
5+
]
6+
}
7+
}

‎.github/workflows/ci.yml‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [master, proposed_type_script_api_version]
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
verify:
13+
runs-on: ubuntu-latest
14+
strategy:
15+
matrix:
16+
node: [20, 22, 24]
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: actions/setup-node@v4
20+
with:
21+
node-version: ${{ matrix.node }}
22+
cache: npm
23+
- run: npm ci
24+
# Unit tier only: the integration tier needs a testing instance and writes fixtures.
25+
- run: npm run verify
26+
- name: Pack
27+
if: matrix.node == 24
28+
run: npm pack
29+
- uses: actions/upload-artifact@v4
30+
if: matrix.node == 24
31+
with:
32+
name: versionone-sdk-tarball
33+
path: versionone-sdk-*.tgz

‎README.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ npm test
2525
- `resolve.numbers` / `resolve.ref` - asset numbers and OID tokens to assets, in one request
2626
- `assets.sendGuarded` - a batch as a compare-and-set on each asset's ChangeDate; changed assets come back `skipped`
2727
- `batch.run` / `batch.reversible` - preview, snapshot, (confirm,) send guarded, and a receipt with a reverse batch
28-
- `createFetchTransport` - configurable HTTP transport
28+
- `createFetchTransport` - configurable HTTP transport: `credentials` for same-origin cookie auth, `onUnauthorized` to re-authenticate on a 401 (resends idempotent requests once), and a per-request `signal`; `client.withSignal(signal)` ties a whole client to one
2929
- `ApiError` - typed API error; `CommandFailureError` - an api/asset batch in which commands failed (`failures` by index, plus the `response`)
3030

3131
API reference: https://docs.digital.ai/agility-api/
@@ -38,6 +38,10 @@ API reference: https://docs.digital.ai/agility-api/
3838
4. Export from `src/index.ts`.
3939
5. Add tests under `test/`.
4040

41+
## Installing before a release
42+
43+
No registry package yet. Pin a commit: `npm install github:versionone/VersionOne.SDK.JavaScript#<sha>` builds `dist` on install (the `prepare` script). CI also uploads an `npm pack` tarball on every run.
44+
4145
## Before PR
4246

4347
- Run `npm run verify` to validate typecheck, build, and tests before opening a pull request.

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
},
3333
"scripts": {
3434
"build": "tsup",
35+
"prepare": "npm run build",
3536
"dev": "tsup --watch",
3637
"download:metamodel": "node scripts/download-metamodel.mjs",
3738
"clean": "rimraf dist",

‎plans/001-sdk-adoption-campaign.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,15 +351,15 @@ ESM" here means finishing the edges:
351351

352352
## Checklist
353353

354-
- [ ] A1 transport: credentials, onUnauthorized, signal
354+
- [x] A1 transport: credentials, onUnauthorized (idempotent requests resent once; writes never), signal (`RequestOptions.signal`, `client.withSignal`)
355355
- [x] A2 command-failure error (`CommandFailureError`, `throwOnFailure`, reads always throw)
356356
- [x] A3 loc2 (`loc2.localize`, `client.localize`; GET, POST past 2000 encoded chars, culture via Accept-Language)
357357
- [ ] A4 slack-shaped reads (queryV1 or api/asset)
358358
- [x] A5 services port from gh-v1 — `resolve` (`client.resolve.numbers`/`.ref`), `guard` (`client.assets.sendGuarded`), `batch` (`client.batch.run`/`.reversible`)
359-
- [ ] A6 CI + prerelease publish
359+
- [ ] A6 CI + prerelease publish — CI (`.github/workflows/ci.yml`: verify on Node 20/22/24, `npm pack` artifact) and git-SHA installs (`prepare`) done; publish waits on the registry decision
360360
- [ ] G0–G3 gh-v1 migrations (gh-v1 session)
361361
- [ ] S1–S4 slack-bot migrations (slack-bot session, `bolt-migration`)
362-
- [ ] B1–B4 slack bot SDK swap
362+
- [ ] B1–B4 slack bot SDK swap — brief: `plans/003-adopt-sdk-k6i-agility-slack-bot.md`
363363
- [x] C0 gh-v1 surveyed (2026-11-04)
364-
- [ ] C1–C4 gh-v1
364+
- [ ] C1–C4 gh-v1 — brief: `plans/002-adopt-sdk-gh-v1.md`
365365
- [ ] D1–D4 agility-rooms

‎plans/002-adopt-sdk-gh-v1.md‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Plan 002: gh-v1 adopts `@versionone/sdk` (Track C)
2+
3+
Brief for a session on `versionone/gh-v1`. Start once the SDK PR that adds
4+
this file has merged into `proposed_type_script_api_version`. Source plan:
5+
`plans/001-sdk-adoption-campaign.md`, Track C.
6+
7+
## Goal
8+
9+
gh-v1 talks to Agility only through `@versionone/sdk`. CLI behaviour and
10+
output stay the same; the code that duplicates the SDK goes.
11+
12+
## Where things stand (checked 2026-11-06, gh-v1 `2c58c7e`)
13+
14+
- G0–G3 are merged: executable bin, `V1_URL`/`V1_ACCESS_TOKEN` config, ESM
15+
(`"type": "module"`), `jsconfig.json` + `npm run typecheck`, one paging
16+
module (`lib/paging.js`).
17+
- The bin `gh-v1` is now a `sh` launcher that runs `gh-v1.js` with the real
18+
node (bypassing Volta's shim, which mangles arguments on Windows). The
19+
swap does not touch either; keep `test/launcher.test.js` green.
20+
- One seam: `lib/api.js` exports `api(cfg)` returning `query`, `queries`,
21+
`asset`, `attempt`, `rest`, `meta`, `localize`, `raw`, `blob`,
22+
`permalink`, typed by the `Api` JSDoc typedef. Its tests under `test/`
23+
stub global `fetch`.
24+
- String-boolean and OID helpers (`isTrue`, `oidOf`, `momentless`) are used
25+
in `lib/api.js`, `meta.js`, `resolve.js`, `board.js`, `batch.js`, and
26+
`commands/apply.js`, `bulk.js`, `board.js`.
27+
28+
## Dependency
29+
30+
No registry package yet. Add the first runtime dependency as a pinned commit:
31+
`"@versionone/sdk": "github:versionone/VersionOne.SDK.JavaScript#<merge sha>"`.
32+
The SDK's `prepare` script builds `dist` on install. gh-v1 is ESM now, so use
33+
`import` (the ESM build), not `require`.
34+
35+
## Steps, one PR each, on branch `sdk-adoption`
36+
37+
1. **C1 Seam swap** (`lib/api.js`). Keep `api(cfg)` and the `Api` typedef.
38+
Inside, build one `AgilityApiClient({ baseUrl: cfg.v1Url, accessToken })`.
39+
Map: `query`/`queries` → `client.assets.query`/`queries`;
40+
`asset` → `client.assets.send(cmds, { throwOnFailure: true })`
41+
(a `CommandFailureError` carries `failures` by index — keep `err.failures`);
42+
`attempt` → `client.assets.send` (failures returned, not thrown);
43+
`rest` → `client.data.list`/`get`; `meta` → `client.meta.*`;
44+
`localize` → `client.localize`. `raw`, `blob`, `permalink` stay local.
45+
Rows now arrive translated (`true`, numbers, single relations as one
46+
Ref), so update the fixtures in tests that assert raw strings, and add
47+
the meta fixture the schema needs (`test/meta-fixture.js`, or seed
48+
`ClientOptions.metamodel`).
49+
2. **C2 Delete the decodes.** Remove `isTrue`/`oidOf` call sites listed
50+
above; import `momentless` from the SDK.
51+
3. **C3 Invert the services.** `resolveAssets` → `client.resolve.numbers`
52+
(its `missing` drives the "not found — skipped" lines). `batch.run` →
53+
`client.batch.run`, with `confirm` as the prompt and the receipt written
54+
as returned. `--reverse` → `client.batch.reversible` (`--force` passes
55+
`force`). Keep the CLI's UX for an unguardable batch by checking
56+
`unguardable()` first, then sending with `guard: false` and the existing
57+
note. Prompting, rendering, permalinks and receipt files stay in gh-v1.
58+
4. **C4 Passthrough.** `raw`/`blob` stay as they are. `lib/guard.js` stays:
59+
it is CLI policy, not SDK behaviour.
60+
61+
## Rules
62+
63+
- Requests out must match today's, apart from what translation changes.
64+
Add a test that asserts the request body where it differs, and say why.
65+
- Every query keeps an explicit page (the SDK refuses one without).
66+
- Live Agility calls go only to the API/SDK testing instance,
67+
`https://www14.v1host.com/v1sdktesting` (`V1_URL`). Writes are allowed
68+
there and nowhere else: name fixtures `sdk-integration …` and delete them
69+
by exact OID when done. Every query sets an explicit page size and stays
70+
well under 20 s of server work.
71+
72+
## Done when
73+
74+
- `npm test` and `npm run typecheck` pass on each PR.
75+
- `lib/api.js` has no `fetch` call except in `raw`/`blob`.
76+
- No `isTrue`/`oidOf`/local `momentless` remain in `lib/`.
77+
- `lib/resolve.js` and `lib/batch.js` keep only prompting, rendering and
78+
file I/O.
79+
- Plan 001's checklist line `C1–C4 gh-v1` is ticked, with the PR links.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Plan 003: k6i-agility-slack-bot adopts `@versionone/sdk` (Track B)
2+
3+
Brief for a session on `digital-ai/k6i-agility-slack-bot`. Start once the
4+
SDK PR that adds this file has merged into `proposed_type_script_api_version`.
5+
Source plan: `plans/001-sdk-adoption-campaign.md`, Track B. Work on
6+
`bolt-migration` (branch `sdk-adoption` from it), never `main`.
7+
8+
## Goal
9+
10+
`services/v1.js`'s `createV1Client` runs on `@versionone/sdk`, with the same
11+
exported surface, so listeners and features do not change. The bot's
12+
hand-decoding and rest-1 write path go.
13+
14+
## Where things stand (checked 2026-11-06, `bolt-migration` `4d77693`)
15+
16+
- `createV1Client({ url, accessToken })` uses native `fetch` and throws
17+
`AuthFailureError` on 401. Its methods: `query` (query.v1), `bulk`,
18+
`createWithNumber`, `exec`/`quickClose`/`reactivate` (api/asset),
19+
`create`/`update` (rest-1 writes via `lib/rest1DataTransformer.js`),
20+
`findAssets` (rest-1 `find`/`findin`, `page` up to 200),
21+
`fetchLocalizations` (loc-2), and `queryFor*` helpers built on
22+
`lib/queries.js`. `test/v1-service.test.js` stubs global `fetch`.
23+
- **S1–S4 are not done yet**: `jsconfig.json` still says `commonJS`/`es6`;
24+
reads still post to `query.v1`; `axios`, `botkit*`, `botbuilder-*` are
25+
still dependencies and `lib/v1api.js` still exists. These come first.
26+
27+
## Dependency
28+
29+
No registry package yet. Pin a commit:
30+
`"@versionone/sdk": "github:versionone/VersionOne.SDK.JavaScript#<merge sha>"`.
31+
The SDK's `prepare` script builds `dist` on install. The bot is ESM, so
32+
`import { AgilityApiClient } from "@versionone/sdk"`.
33+
34+
## Steps, one PR each
35+
36+
0. **S1–S4 first**, as written in `plans/handoffs/k6i-agility-slack-bot.md`.
37+
B1 needs at least S2 (reads on api/asset) and S3 (axios and Botkit gone).
38+
If the bot's own session already merged them, skip.
39+
1. **B1 Client swap** (`services/v1.js`). Inside `createV1Client`, build one
40+
`new AgilityApiClient({ baseUrl: url, accessToken })`. `query` →
41+
`client.assets.queries` (positional; a failed query now throws instead
42+
of shifting later results); `bulk`/`exec`/`createWithNumber` →
43+
`client.assets.send`; `fetchLocalizations` → `client.localize`;
44+
`findAssets` → `client.data.list("BaseAsset", { find, findin, where,
45+
sort, page, deleted: false })`. Map `ApiError` with `status === 401`
46+
to `AuthFailureError` in one place. The SDK's `onUnauthorized` is not
47+
needed. `test/v1-service.test.js` stays the parity proof.
48+
2. **B2 Delete the decodes.** Rows arrive translated: drop the
49+
`"Team.HasProcess" === "True"` and `IsKanban.toLowerCase() === "true"`
50+
comparisons, local `toMomentless` (use the SDK's `momentless`), and the
51+
`results[1]`/`results[2]` guessing in `queryForPrimaryWorkitemByOid` and
52+
`queryForTeamRoomContext`.
53+
3. **B3 Writes on api/asset.** `create`/`update` → `client.assets.send`,
54+
with the change comment as `ChangeComment` inside the `update` node (or
55+
at the root of a create). `execute` cannot carry a comment. Delete
56+
`lib/rest1DataTransformer.js` and its test.
57+
4. **B4 Number resolution.** `queryForAssetByNumber` →
58+
`client.resolve.numbers([n])`; a miss is `missing`, not an empty row.
59+
60+
## Rules
61+
62+
- Every api/asset query keeps an explicit `page` (the SDK refuses one
63+
without). Values from user text go in `with` variables, not spliced into
64+
`filter`/`where` (an em dash breaks a literal).
65+
- Fixtures send what the server sends (S4): `"True"`, not `true`.
66+
- Live Agility calls go only to the API/SDK testing instance,
67+
`https://www14.v1host.com/v1sdktesting` (`V1_URL`). Writes are allowed
68+
there and nowhere else: name fixtures `sdk-integration …` and delete them
69+
by exact OID when done. Every query sets an explicit page size and stays
70+
well under 20 s of server work.
71+
72+
## Done when
73+
74+
- `npm test` passes on each PR; S1's `npm run typecheck` passes.
75+
- `services/v1.js` makes no `fetch` call of its own; nothing posts to
76+
`query.v1` or writes through rest-1.
77+
- No string-boolean comparisons or local `toMomentless` remain.
78+
- Sandbox-workspace smoke passes: asset mention, `/agility find`,
79+
backlog-item status change, webhook notification.
80+
- Plan 001's checklist lines `S1–S4` and `B1–B4` are ticked, with PR links.

‎specs/000-layers.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,13 @@ retries only idempotent requests (GET by default), only on a network
138138
failure or a 5xx, never a 4xx; a batch write is never retried unless the
139139
caller has made it idempotent with a guard and says so.
140140

141+
Re-authentication follows the same rule. On a 401 (or a status the caller
142+
lists) the transport calls `onUnauthorized` once per request; "retry" resends
143+
an idempotent request one more time, outside the `retries` count. A plain
144+
write is never resent: the hook still runs, so the session can be repaired,
145+
and the caller decides. A caller's abort (`signal`) ends the request as
146+
`ABORTED` and is never retried.
147+
141148
A failure never reads as an empty. api/asset answers 200 and commits per
142149
command, so a failed command is reported inside the envelope
143150
(`commandFailures`, each by `sourceCommandIndex`). `assetApi.send` returns

‎src/client.ts‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export interface ClientOptions {
2222
* queries first touch them.
2323
*/
2424
metamodel?: Metamodel;
25+
/** Share an existing schema (and its cache) instead of starting a new one. */
26+
schema?: Schema;
2527
}
2628

2729
/**
@@ -44,9 +46,27 @@ export class AgilityApiClient {
4446
accessToken: config.accessToken,
4547
timeoutMs: config.timeoutMs,
4648
retries: config.retries,
47-
defaultHeaders: config.defaultHeaders
49+
defaultHeaders: config.defaultHeaders,
50+
credentials: config.credentials,
51+
onUnauthorized: config.onUnauthorized,
52+
unauthorizedStatuses: config.unauthorizedStatuses
4853
});
49-
this.schema = new Schema(this.transport, opts.metamodel);
54+
this.config = config;
55+
this.schema = opts.schema ?? new Schema(this.transport, opts.metamodel);
56+
}
57+
58+
private readonly config: ClientConfig;
59+
60+
/**
61+
* The same client, with every request it sends tied to `signal`: abort it
62+
* (on navigation, say) and in-flight requests end with an ApiError coded
63+
* ABORTED. The schema and its cache are shared; a type load that other
64+
* callers are waiting on is not cancelled.
65+
*/
66+
withSignal(signal: AbortSignal): AgilityApiClient {
67+
const base = this.transport;
68+
const transport: Transport = { request: (options) => base.request({ ...options, signal: options.signal ?? signal }) };
69+
return new AgilityApiClient(this.config, { transport, schema: this.schema });
5070
}
5171

5272
/** meta.v1: the instance's schema, raw. (The translated, cached view is `schema`.) */

‎src/core/clientConfig.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
export interface ClientConfig {
1+
import type { FetchTransportConfig } from "./fetchTransport";
2+
3+
export interface ClientConfig extends Pick<FetchTransportConfig, "credentials" | "onUnauthorized" | "unauthorizedStatuses"> {
24
/** The instance base URL: "https://www7.v1host.com/V1Production". */
35
baseUrl: string;
46
/** A ready Authorization header value ("Basic …" for a dev instance). Wins over accessToken. */

0 commit comments

Comments
 (0)