@design-ai/cli 4.58.0 → 4.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/SDK.md ADDED
@@ -0,0 +1,249 @@
1
+ # Agent SDK reference
2
+
3
+ > Status: shipped (Phase A + Phase B) — 8 read-only verbs plus the opt-in `learn.*` local-write namespace, semver-stable surface
4
+
5
+ `@design-ai/cli/sdk` lets an external Node.js program — an agent runtime, a build script, a custom tool — use design-ai's deterministic design capabilities as importable functions, without shelling out to the CLI or spawning the MCP server. It is a thin, curated adapter over the same `cli/lib` functions the CLI and MCP server already call, so a capability that ships in the CLI is instantly available to an SDK consumer.
6
+
7
+ See [`AGENT-SDK.md`](AGENT-SDK.md) for the full design rationale, phased plan, and open questions. This page is the public reference for the shipped Phase A (read-only) and Phase B (local-write) surface.
8
+
9
+ ## Install and import
10
+
11
+ The SDK ships inside the `@design-ai/cli` package (no separate install):
12
+
13
+ ```bash
14
+ npm install @design-ai/cli
15
+ ```
16
+
17
+ ```js
18
+ import { route, prompt, pack, search, recall, check, routes, version } from "@design-ai/cli/sdk";
19
+ ```
20
+
21
+ Only the `./sdk` subpath is exported — `import "@design-ai/cli"` (the bare package root) is intentionally not exported, so importing the SDK is always an explicit `@design-ai/cli/sdk` import. `cli/lib/*` is internal and unstable; do not import it directly.
22
+
23
+ ### TypeScript
24
+
25
+ Hand-written type declarations ship with the package at `cli/sdk/index.d.ts` and are wired through the `exports` `types` condition, so TypeScript and editors resolve them automatically for `@design-ai/cli/sdk` — no `@types` install, and no build step on our side (the declarations are maintained by hand to preserve the zero-toolchain stance). Use `moduleResolution: node16`/`nodenext` (or `bundler`) so the subpath `types` condition is honored. All exported types (`RouteResult`, `PromptPlan`, `Pack`, `SearchHit`, `RankedSearchHit`, `RecallResult`, `CheckReport`, option interfaces, …) are importable:
26
+
27
+ ```ts
28
+ import { route, type RouteResult, type SearchOptions } from "@design-ai/cli/sdk";
29
+ ```
30
+
31
+ A `node --test` guard (`cli/sdk/types.test.mjs`) asserts the declaration file stays in exact sync with the runtime exports.
32
+
33
+ ## Phase A: read-only
34
+
35
+ Every verb below is a pure, read-only adapter:
36
+
37
+ - It validates its inputs and throws a plain `Error` with a clear message on bad input (e.g. a non-string brief).
38
+ - It resolves the package root the same way the CLI does.
39
+ - It calls the corresponding `cli/lib` function and returns a plain JSON-serializable object — the same shape the CLI's `--json` mode emits.
40
+ - It performs no file writes, no network calls, and no learning-usage sidecar writes. This is a deliberate difference from the CLI: `prompt`/`pack`'s `withLearning` option in the SDK only **reads** the local learning profile to build context — it never records a usage event, unlike the CLI's `--with-learning` flag. There is no SDK equivalent of `check --learn` (capture) in Phase A.
41
+ - Given the same inputs, it returns the same outputs (determinism), matching the CLI.
42
+
43
+ ## Verbs
44
+
45
+ ### `route(brief, opts)`
46
+
47
+ Recommend the best design-ai route(s), commands, skills, and knowledge files for a task brief.
48
+
49
+ ```js
50
+ route(brief: string, opts?: { limit?: number, explain?: boolean }): RouteResult[]
51
+ ```
52
+
53
+ - `limit` — maximum route recommendations, 1-10. Default: `3`.
54
+ - `explain` — include route scoring, reference coverage, and related-knowledge detail. Default: `false`.
55
+
56
+ Returns an array of `RouteResult` objects (`id`, `label`, `score`, `confidence`, `matchedKeywords`, `command`, `skills`, `agents`, `knowledge`, `keywords`, `explanation`, plus `relatedKnowledge` when `explain: true`) — the same shape as the `routes` array in `design-ai route --json`.
57
+
58
+ ### `prompt(brief, opts)`
59
+
60
+ Build a ready-to-use agent prompt plan from a task brief.
61
+
62
+ ```js
63
+ prompt(brief: string, opts?: {
64
+ routeId?: string,
65
+ withLearning?: boolean,
66
+ learningCategory?: string,
67
+ learningLimit?: number,
68
+ withRecall?: boolean,
69
+ recallLimit?: number,
70
+ }): PromptPlan
71
+ ```
72
+
73
+ - `routeId` — force a route id instead of scoring the brief.
74
+ - `withLearning` — include brief-relevant local learning preferences (reads `DESIGN_AI_LEARNING_FILE`; **never** records usage, unlike the CLI's `--with-learning`).
75
+ - `learningCategory`, `learningLimit` (1-100) — scope/limit the learning context; require `withLearning`.
76
+ - `withRecall` — include brief-relevant shipped corpus knowledge.
77
+ - `recallLimit` (1-20) — limit recalled corpus files; requires `withRecall`.
78
+
79
+ Returns a `PromptPlan` object (`brief`, `version`, `route`, `slashCommand`, `referenceExamples`, `filesToRead`, `checklist`, `qualityCommand`, `prompt`, plus `learningContext`/`recall` when requested) — the same shape as `design-ai prompt --json`, minus `learningUsage` (Phase A never writes it).
80
+
81
+ ### `pack(brief, opts)`
82
+
83
+ Build a ready-to-use prompt plus a bounded context-file bundle from a task brief.
84
+
85
+ ```js
86
+ pack(brief: string, opts?: {
87
+ routeId?: string,
88
+ maxBytes?: number,
89
+ withLearning?: boolean,
90
+ learningCategory?: string,
91
+ learningLimit?: number,
92
+ withRecall?: boolean,
93
+ recallLimit?: number,
94
+ }): Pack
95
+ ```
96
+
97
+ - `maxBytes` — maximum context bytes to include, 1000-1,000,000. Default: `120000`.
98
+ - The remaining options mirror `prompt`'s learning/recall options.
99
+
100
+ Returns a `Pack` object (`brief`, `version`, `maxBytes`, `usedBytes`, `summary`, `warnings`, `plan`, `files`, `markdown`) — the same shape as `design-ai pack --json`, minus `learningUsage`.
101
+
102
+ ### `search(query, opts)`
103
+
104
+ Search the local design-ai markdown corpus.
105
+
106
+ ```js
107
+ search(query: string, opts?: { dir?: string, limit?: number, ranked?: boolean }): SearchHit[]
108
+ ```
109
+
110
+ - `dir` — restrict to one corpus directory: `knowledge`, `examples`, `skills`, `docs`, `agents`, or `commands`.
111
+ - `limit` — maximum hits, 1-500. Default: `20`.
112
+ - `ranked` — rank results with the deterministic lexical (BM25-style) scorer instead of returning raw line hits. Default: `false`.
113
+
114
+ Returns `SearchHit[]`. Unranked hits: `{ file, relPath, lineNumber, preview }`. Ranked hits: `{ file, relPath, score, matchedTokens, preview }` — the same shapes as `design-ai search --json` and `design-ai search --ranked --json`.
115
+
116
+ ### `recall(query, opts)`
117
+
118
+ Recall brief-relevant shipped corpus knowledge and local learning-profile entries for a query — a combined read-only view.
119
+
120
+ ```js
121
+ recall(query: string, opts?: { limit?: number, category?: string }): { corpus: object, learning: object }
122
+ ```
123
+
124
+ - `limit` — applies to both the corpus and learning lists, 1-20. Default: `5`.
125
+ - `category` — scopes only the learning list.
126
+
127
+ Returns `{ corpus: { candidateCount, selectedCount, selected }, learning: { mode, candidateCount, selectedCount, selected } }` — the same shape as `design-ai learn --recall --json`.
128
+
129
+ ### `check(artifact, opts)`
130
+
131
+ Check a generated design Markdown artifact for grounding, accessibility, responsive, unresolved-marker, and route-specific requirements.
132
+
133
+ ```js
134
+ check(artifact: string, opts?: { routeId?: string, strict?: boolean }): CheckReport
135
+ ```
136
+
137
+ - `routeId` — add route-specific checks (e.g. `component-spec`, `palette-from-brand`). Must be a known route id.
138
+ - `strict` — accepted for CLI-flag parity but has no effect on the returned report; the SDK never sets a process exit code. Inspect `report.status` yourself.
139
+
140
+ Returns a `CheckReport` object (`filePath`, `status`, `passes`, `warnings`, `failures`, `total`, `score`, `results`, plus `routeId` when passed) — the same shape as `design-ai check --json` (minus any `--learn` capture, which Phase A does not support).
141
+
142
+ ### `routes()`
143
+
144
+ List the full route catalog (every route id with its static metadata), independent of any brief.
145
+
146
+ ```js
147
+ routes(): { version: string, routes: RouteResult[] }
148
+ ```
149
+
150
+ Returns the same shape as `design-ai route --list --json`.
151
+
152
+ ### `version()`
153
+
154
+ Report the CLI package version and the plugin/corpus version.
155
+
156
+ ```js
157
+ version(): { cli: string, corpus: string }
158
+ ```
159
+
160
+ ## Stability contract
161
+
162
+ `@design-ai/cli/sdk` is **semver-stable**:
163
+
164
+ - Additive changes (new optional fields, new opt-in options) are minor version bumps.
165
+ - Signature or return-shape changes are major version bumps.
166
+ - The 8 read-only function exports (`route`, `prompt`, `pack`, `search`, `recall`, `check`, `routes`, `version`) plus the frozen `learn` namespace object (`learn.remember`, `learn.feedback`, `learn.captureFromCheck`) and their return-shape key sets are pinned by an SDK contract test (`cli/sdk/index.test.mjs`) — this is the semver anchor. If a name or a top-level key drifts unintentionally, that test fails.
167
+ - `cli/lib/*` remains internal and unstable. Only `@design-ai/cli/sdk` is a supported import path.
168
+ - Determinism: the same inputs always produce the same outputs, with no randomness or time-dependence added by the adapter layer.
169
+
170
+ ## Phase B — local writes
171
+
172
+ Phase A (above) is read-only by design: the 8 verbs never write a file. Phase B adds a single namespace export, `learn`, grouping the three explicit, opt-in **LOCAL-WRITE** verbs. This is the only place the SDK writes files — every Phase A verb stays read-only and unchanged.
173
+
174
+ ```js
175
+ import { learn } from "@design-ai/cli/sdk";
176
+
177
+ learn.remember(text, opts?) // opts: { category?: string }
178
+ learn.feedback(text, opts?) // opts: { outcome?: string, category?: string }
179
+ learn.captureFromCheck(artifact, opts?) // opts: { routeId?: string }
180
+ ```
181
+
182
+ **Write boundary rationale:** an SDK consumer should never be surprised by a file write. Phase A's 8 verbs are safe to call from any context — a build script, a read path, a CI check — with zero side effects. `learn.*` is the deliberate, narrow exception: three verbs, one destination, no ambiguity about what gets written or where.
183
+
184
+ All three verbs write **only** the local learning profile — `DESIGN_AI_LEARNING_FILE` if set, otherwise the CLI's `defaultLearningFile()` default path — and never touch the network. There is no `filePath` option and no `now`/timestamp option on any of them: target a specific profile the same way the CLI does, by setting the `DESIGN_AI_LEARNING_FILE` environment variable before calling. The underlying library functions supply their own defaults (the env var and `new Date()`).
185
+
186
+ ### `learn.remember(text, opts)`
187
+
188
+ Record a local learning-profile preference. Adapter over `rememberLearning` from `cli/lib/learn-profile.mjs`.
189
+
190
+ ```js
191
+ learn.remember(text: string, opts?: { category?: string }): RememberResult
192
+ ```
193
+
194
+ - `text` — required, non-empty string.
195
+ - `category` — one of the learning-profile categories (e.g. `preference`, `brand`, `workflow`, `constraint`, `accessibility`, `korean`, `other`); the library normalizes and validates it. Default: `"preference"`.
196
+
197
+ Returns a `RememberResult`: `{ file, entry, profile }`, where `entry` is `{ id, category, text, source, createdAt }` (`source` is always `"sdk"`) and `profile` is the full updated learning profile `{ version, updatedAt, entries }`.
198
+
199
+ ### `learn.feedback(text, opts)`
200
+
201
+ Record feedback (keep/avoid/improve) as a local learning-profile entry. Adapter over `recordLearningFeedback` from `cli/lib/learn-profile.mjs`.
202
+
203
+ ```js
204
+ learn.feedback(text: string, opts?: { outcome?: string, category?: string }): RememberResult
205
+ ```
206
+
207
+ - `text` — required, non-empty string.
208
+ - `outcome` — any string; the library normalizes it (`normalizeFeedbackOutcome`) to one of `keep` / `avoid` / `improve` and folds it into both the stored entry's `text` (e.g. `"Avoid in future outputs: …"`) and its `source` (e.g. `"feedback:avoid"`). Default: `"improve"`.
209
+ - `category` — same category enum as `remember`. Default: `"workflow"`.
210
+
211
+ Returns the same `RememberResult` shape as `learn.remember`.
212
+
213
+ ### `learn.captureFromCheck(artifact, opts)`
214
+
215
+ Check a Markdown artifact, then capture its non-pass results as local learning-profile entries — the SDK equivalent of the CLI's `check --learn --yes`. Adapter over `checkArtifactContent` + `buildCheckLearningCapture` from `cli/lib/check.mjs`.
216
+
217
+ ```js
218
+ learn.captureFromCheck(artifact: string, opts?: { routeId?: string }): CaptureResult
219
+ ```
220
+
221
+ - `artifact` — required, non-empty Markdown string.
222
+ - `routeId` — add route-specific check requirements before capturing; must be a known route id (validated with the same `assertKnownRouteId` helper the read-only `check` adapter uses).
223
+
224
+ Returns a `CaptureResult` — the `captureLearningEntries` return shape:
225
+
226
+ ```
227
+ {
228
+ file: string, // the learning profile file written to
229
+ dryRun: false, // always false; captureFromCheck always applies
230
+ applied: true, // always true when dryRun is false
231
+ source: string, // "check:<routeId>" or "check:artifact"
232
+ candidateCount: number, // non-pass check results considered
233
+ addedCount: number, // entries newly written
234
+ skippedCount: number, // entries skipped as duplicates of existing entries
235
+ count: number, // total entries in the profile after this call
236
+ entries: LearningProfileEntry[], // the entries that were added
237
+ skipped: Array<{ category, text, source, reason }>, // reason is "duplicate-entry-text"
238
+ }
239
+ ```
240
+
241
+ Calling `captureFromCheck` twice with the same artifact adds nothing the second time — every candidate is skipped with `reason: "duplicate-entry-text"`.
242
+
243
+ ### Validation
244
+
245
+ All three verbs use the same `cli/sdk/validate.mjs` helpers as Phase A: `requireNonEmptyString` for `text`/`artifact`, `requireOptions` for the options bag, and `optionalString` for string options. `routeId` is validated with `assertKnownRouteId`, exactly like the read-only `check` adapter. Bad input throws a plain `Error` with a clear message — no process exit code, no silent fallback.
246
+
247
+ ### What stays unchanged
248
+
249
+ `check()` (the Phase A read-only verb) is untouched: it still never writes, and it has no capture option. Capture is exclusively reached through `learn.captureFromCheck`, which is a separate, explicitly-named write verb — this keeps the read/write boundary visible at the call site rather than hidden behind an option flag on a read-only function.
@@ -1,22 +1,22 @@
1
1
  # External Publication Status
2
2
 
3
- > Checked: 2026-07-03
3
+ > Checked: 2026-07-04
4
4
  > Scope: npm registry, GitHub Pages, Homebrew tap, VS Code Marketplace, Claude/Codex MCP
5
5
 
6
6
  ## Summary
7
7
 
8
- npm is publicly published at `@design-ai/cli@4.57.0` (npm dist-tag `latest` = `4.57.0`); publish run `28663430171` from tag `v4.57.0` succeeded with provenance and all pre-publish gates green (8 audits, CLI unit tests, release self-tests, package contents, and the packed-tarball installed-package smoke on the 4.57.0 tarball). The automated post-publish live registry smoke step failed on a stale learn-relevance token-order assertion in `tools/audit/registry-smoke.py` a verification-tool bug fixed on `main` in `710656a`, not a package defect; the published package is validated by the pre-publish packed-tarball smoke. GitHub Release `v4.57.0` is published, and the Homebrew tap formula points at the `v4.57.0` release source tarball with a verified SHA-256. The VS Code extension `sungjin.design-ai-vscode` remains published at `0.4.1`. GitHub Pages docs are publicly reachable.
8
+ npm is publicly published at `@design-ai/cli@4.59.0` (npm dist-tag `latest` = `4.59.0`); publish run from tag `v4.59.0` succeeded with provenance and all pre-publish gates green. This release adds the read-only Agent SDK surface (`@design-ai/cli/sdk`, eight adapter verbs) additive only, so CLI output is unchanged. The live `npm run registry:smoke` passes cleanly against published `@design-ai/cli@4.59.0` ("Registry smoke passed"), covering the retrieval surfaces (index/ranked/embeddings/recall) and route enrichment. GitHub Release `v4.59.0` is published, and the Homebrew tap formula points at the `v4.59.0` release source tarball with a verified SHA-256. The VS Code extension `sungjin.design-ai-vscode` remains published at `0.4.1`. GitHub Pages docs are publicly reachable.
9
9
 
10
10
  ## Results
11
11
 
12
12
  | Surface | Checked target | Result | Evidence |
13
13
  |---|---|---|---|
14
- | npm registry | `@design-ai/cli` | Published latest is `4.57.0`; publish run `28663430171` (tag `v4.57.0`) succeeded with provenance. Pre-publish packed-tarball smoke passed on the 4.57.0 tarball; the post-publish live registry smoke failed on a stale assertion (fixed on `main` `710656a`). | publish run `28663430171`; `npm view @design-ai/cli version` → `4.57.0` |
14
+ | npm registry | `@design-ai/cli` | Published latest is `4.59.0` (tag `v4.59.0`, provenance). Pre-publish packed-tarball smoke (incl. SDK import) and live `npm run registry:smoke` both pass for `@design-ai/cli@4.59.0`. | `npm view @design-ai/cli version` → `4.59.0`; live registry smoke "Registry smoke passed" |
15
15
  | GitHub Pages | `https://sungjin9288.github.io/design-ai/` | Published and reachable: HTTP `200`, design-ai MkDocs page rendered | `evidence/cli-logs/github-pages-status.log` |
16
- | Homebrew tap | `Formula/design-ai.rb` | Formula pinned to `v4.57.0` release source tarball with SHA-256 `c1fed279ed7bc2daf42473bd9a68cf3fa6df6823fe3390b2f2ccee8625407559` (recomputed from the published tag tarball) | `Formula/design-ai.rb` |
16
+ | Homebrew tap | `Formula/design-ai.rb` | Formula pinned to `v4.59.0` release source tarball with SHA-256 `e711203fb96fee5f6e7f25d809bc42108553bc8108ddf3e8cd181f0a76f6e0f5` (recomputed from the published tag tarball) | `Formula/design-ai.rb` |
17
17
  | VS Code Marketplace | `sungjin.design-ai-vscode` | Published: run `28431571256` published `v0.4.1`, and the Marketplace Gallery API returned visible version `0.4.1` on 2026-07-02. | `evidence/cli-logs/vscode-marketplace-status.log`, `evidence/cli-logs/vscode-marketplace-secret-status.log`, `evidence/cli-logs/vscode-extension-vsce-package.log`, `evidence/cli-logs/vscode-publish-workflow-status.log` |
18
- | GitHub Release | `v4.57.0` | Published (not draft/prerelease) for tag `v4.57.0` at commit `4be46d7` | `gh release view v4.57.0` |
19
- | MCP server | `@design-ai/cli@4.57.0` / local clone | Public npm `design-ai-mcp` responds to initialize and tools/list with 10 tools; local Codex and Claude Code both report `design-ai` MCP as configured and connected. | `evidence/cli-logs/npm-registry-smoke.log`, `evidence/cli-logs/design-ai-mcp-client-status.log` |
18
+ | GitHub Release | `v4.59.0` | Published for tag `v4.59.0` at commit `9f53345` | `gh release view v4.59.0` |
19
+ | MCP server | `@design-ai/cli@4.59.0` / local clone | Public npm `design-ai-mcp` responds to initialize and tools/list with 10 tools; local Codex and Claude Code both report `design-ai` MCP as configured and connected. | `evidence/cli-logs/npm-registry-smoke.log`, `evidence/cli-logs/design-ai-mcp-client-status.log` |
20
20
 
21
21
  ## Interpretation
22
22
 
package/package.json CHANGED
@@ -1,17 +1,24 @@
1
1
  {
2
2
  "name": "@design-ai/cli",
3
- "version": "4.58.0",
3
+ "version": "4.60.0",
4
4
  "description": "Senior product designer for any AI coding agent. Installs design-ai (20 skills, 17 commands, 4 agents) into Claude Code globally. Korean market depth plus website improvement control tower.",
5
5
  "bin": {
6
6
  "design-ai": "cli/bin/design-ai.mjs",
7
7
  "design-ai-mcp": "cli/bin/design-ai-mcp.mjs"
8
8
  },
9
9
  "type": "module",
10
+ "exports": {
11
+ "./sdk": {
12
+ "types": "./cli/sdk/index.d.ts",
13
+ "default": "./cli/sdk/index.mjs"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
10
17
  "engines": {
11
18
  "node": ">=18"
12
19
  },
13
20
  "scripts": {
14
- "test": "node --test cli/lib/*.test.mjs",
21
+ "test": "node --test cli/lib/*.test.mjs cli/sdk/*.test.mjs",
15
22
  "audit": "python3 -B tools/audit/run-all.py",
16
23
  "audit:strict": "python3 -B tools/audit/run-all.py --strict",
17
24
  "audit:runner:self-test": "python3 -B tools/audit/run-all.py --self-test",
@@ -763,6 +763,85 @@ def assert_version_json_smoke(cmd: list[str], *, env: dict[str, str], cwd: Path
763
763
  assert_version_json(result.stdout, context=context, cmd=cmd)
764
764
 
765
765
 
766
+ # Agent SDK packed-tarball smoke (docs/AGENT-SDK.md, docs/SDK.md): imports
767
+ # `@design-ai/cli/sdk` from the installed package (proving the package.json
768
+ # "exports" map resolves) and exercises route/search(ranked)/recall, asserting
769
+ # each call is deterministic (same input -> same JSON-serialized output) and
770
+ # that Phase A's read-only contract holds (prompt/pack never include
771
+ # learningUsage). Also exercises Phase B's `learn.remember` against a temp
772
+ # DESIGN_AI_LEARNING_FILE and asserts it actually wrote the profile. This is a
773
+ # Node script, not a design-ai CLI invocation, so it is run directly rather
774
+ # than through npm_exec_cmd/design-ai bin helpers.
775
+ SDK_SMOKE_SCRIPT = """
776
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
777
+ import { tmpdir } from "node:os";
778
+ import path from "node:path";
779
+ import { check, learn, pack, prompt, recall, route, routes, search, version } from "@design-ai/cli/sdk";
780
+
781
+ const brief = "Spec a Button component API with variants, props, and keyboard accessibility";
782
+
783
+ const r1 = route(brief, { limit: 1 });
784
+ const r2 = route(brief, { limit: 1 });
785
+ if (JSON.stringify(r1) !== JSON.stringify(r2)) throw new Error("route() not deterministic");
786
+ if (r1[0].id !== "component-spec") throw new Error(`unexpected route id: ${r1[0].id}`);
787
+
788
+ const s1 = search("accessibility", { ranked: true, limit: 3 });
789
+ const s2 = search("accessibility", { ranked: true, limit: 3 });
790
+ if (JSON.stringify(s1) !== JSON.stringify(s2)) throw new Error("search() not deterministic");
791
+ if (s1.length === 0) throw new Error("search() returned no hits");
792
+
793
+ const rec1 = recall("keyboard accessibility button", { limit: 2 });
794
+ const rec2 = recall("keyboard accessibility button", { limit: 2 });
795
+ if (JSON.stringify(rec1) !== JSON.stringify(rec2)) throw new Error("recall() not deterministic");
796
+ if (!rec1.corpus || !rec1.learning) throw new Error("recall() missing corpus/learning");
797
+
798
+ const v = version();
799
+ if (typeof v.cli !== "string" || typeof v.corpus !== "string") throw new Error("version() bad shape");
800
+
801
+ const rc = routes();
802
+ if (!Array.isArray(rc.routes) || rc.routes.length === 0) throw new Error("routes() bad shape");
803
+
804
+ const p = prompt(brief, { routeId: "component-spec" });
805
+ if (Object.hasOwn(p, "learningUsage")) throw new Error("prompt() must not include learningUsage in Phase A");
806
+
807
+ const pk = pack(brief, { routeId: "component-spec", maxBytes: 20000 });
808
+ if (Object.hasOwn(pk, "learningUsage")) throw new Error("pack() must not include learningUsage in Phase A");
809
+
810
+ const ck = check(
811
+ "# T\\n\\nAnatomy variants API contrast 4.5:1 keyboard focus screen reader aria- responsive mobile desktop.",
812
+ { routeId: "component-spec" },
813
+ );
814
+ if (typeof ck.status !== "string") throw new Error("check() bad shape");
815
+
816
+ // Phase B: learn.remember must write only DESIGN_AI_LEARNING_FILE.
817
+ const smokeDir = mkdtempSync(path.join(tmpdir(), "design-ai-sdk-learn-smoke-"));
818
+ const learningFile = path.join(smokeDir, "learning.json");
819
+ process.env.DESIGN_AI_LEARNING_FILE = learningFile;
820
+ try {
821
+ const result = learn.remember("Package smoke: prefer 8px spacing grid.", { category: "preference" });
822
+ if (result.entry.source !== "sdk") throw new Error("learn.remember() entry.source must be 'sdk'");
823
+ if (!existsSync(learningFile)) throw new Error("learn.remember() did not write DESIGN_AI_LEARNING_FILE");
824
+ const onDisk = JSON.parse(readFileSync(learningFile, "utf8"));
825
+ if (!Array.isArray(onDisk.entries) || onDisk.entries.length !== 1) {
826
+ throw new Error("learn.remember() did not persist exactly one entry");
827
+ }
828
+ } finally {
829
+ rmSync(smokeDir, { recursive: true, force: true });
830
+ delete process.env.DESIGN_AI_LEARNING_FILE;
831
+ }
832
+
833
+ console.log("design-ai sdk smoke passed");
834
+ """.strip()
835
+
836
+
837
+ def assert_sdk_smoke(*, cwd: Path, env: dict[str, str], context: str) -> None:
838
+ result = run_plain(["node", "--input-type=module", "-e", SDK_SMOKE_SCRIPT], cwd=cwd, env=env)
839
+ if result.returncode != 0:
840
+ raise SystemExit(f"{context} failed (exit {result.returncode})")
841
+ if "design-ai sdk smoke passed" not in result.stdout:
842
+ raise SystemExit(f"{context}: expected success marker missing from output")
843
+
844
+
766
845
  def assert_workspace_json_smoke(cmd: list[str], *, env: dict[str, str], cwd: Path | None = None, context: str) -> None:
767
846
  result = run_plain(cmd, cwd=cwd, env=env)
768
847
  assert_workspace_json(result.stdout, context=context, cmd=cmd)
@@ -20239,6 +20318,11 @@ def smoke_tarball(tarball: Path) -> None:
20239
20318
  env=smoke_env,
20240
20319
  context="package smoke installed bin version JSON",
20241
20320
  )
20321
+ assert_sdk_smoke(
20322
+ cwd=install_root,
20323
+ env=smoke_env,
20324
+ context="package smoke installed Agent SDK (@design-ai/cli/sdk)",
20325
+ )
20242
20326
  assert_design_ai_mcp_protocol_smoke(
20243
20327
  [str(mcp_bin_path)],
20244
20328
  cwd=install_root,
@@ -4637,7 +4637,7 @@ def assert_learning_relevance_context(payload: dict[str, object], *, context: st
4637
4637
  message="learning selection explanation should mark the relevant entry as a brief match",
4638
4638
  )
4639
4639
  require_registry_smoke(
4640
- isinstance(selected_entry.get("score"), int) and selected_entry.get("score") > 0,
4640
+ type(selected_entry.get("score")) in (int, float) and selected_entry.get("score") > 0,
4641
4641
  context=context,
4642
4642
  cmd=cmd,
4643
4643
  message="learning selection explanation should include a positive relevance score",
@@ -4759,7 +4759,7 @@ def assert_learning_query_json(
4759
4759
  require_registry_smoke(
4760
4760
  selected[0].get("id") == "learn-relevant"
4761
4761
  and selected[0].get("reason") == "brief-match"
4762
- and isinstance(selected[0].get("score"), int)
4762
+ and type(selected[0].get("score")) in (int, float)
4763
4763
  and selected[0].get("score") > 0,
4764
4764
  context=context,
4765
4765
  cmd=cmd,