@design-ai/cli 4.57.0 → 4.58.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "design-ai",
3
- "version": "4.57.0",
3
+ "version": "4.58.0",
4
4
  "description": "Senior product designer for any AI coding agent. 20 skills, 17 commands, 4 review agents covering UI/UX, website improvement, design systems, motion, illustration, print, video, game UI, conversational, and spatial design. Korean market depth.",
5
5
  "author": {
6
6
  "name": "Sungjin Park",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  User-facing release notes for design-ai. Versions follow semver.
4
4
 
5
+ ## v4.58.0 — Retrieval Across Surfaces and Corpus Depth (2026-07)
6
+
7
+ Completes the retrieval integration started in v4.57.0 by bringing it to the last untouched surface (`route --explain`), refines recall quality so injected knowledge is real design guidance rather than index files, and closes the remaining dogfood-named corpus gaps. All additions are additive and backward-compatible; default output is unchanged unless a flag is passed.
8
+
9
+ ### Added
10
+ - `route --explain` now includes an advisory "Related knowledge" section — brief-relevant corpus knowledge recalled by the shared lexical scorer, excluding what the route already curates. This is advisory only: route ids, scores, and curated knowledge are unchanged, and default `route` output is byte-unchanged. `design_ai_route`'s existing `explain` parameter carries it to MCP automatically. Retrieval now spans every surface: `search --ranked`, `prompt`/`pack --with-recall`, `learn --recall`, and `route --explain`.
11
+ - `knowledge/patterns/async-control.md` — the async action in-flight lifecycle: pending states, double-submit prevention, duration-matched loading affordances, optimistic updates with rollback, debounce/throttle, cancellation and out-of-order handling, concurrency policy, timeouts, and the accessibility floor.
12
+ - `knowledge/i18n/korean-density-conventions.md` — Korean B2B/enterprise density: the density ladder, table-first layouts, label-left multi-column forms, dense navigation, typography at density, and where density must yield to accessibility.
13
+
14
+ ### Changed
15
+ - Recall injection surfaces (`prompt`/`pack --with-recall`, `learn --recall`, `route --explain`) now exclude generated index/meta files (`COVERAGE.md`, `INDEX.md`, `docs/reference/*`) so injected context is real design knowledge; raw `search --ranked` still returns those files when explicitly searched.
16
+ - Knowledge corpus is now 94 files (from 92).
17
+
18
+ ### Fixed
19
+ - `tools/audit/registry-smoke.py`'s learn-relevance assertion expected a stale token order (`keyboard, accessibility`) after the v4.57 shared-scorer change sorted matched tokens; aligned it with `package-smoke.py` and the real CLI output so the live post-publish registry smoke matches the published package.
20
+
21
+ ### Verified
22
+ - All 8 audits passed.
23
+ - `npm run release:check`.
24
+ - `npm run release:metadata`.
25
+ - `git diff --check`.
26
+ - Main-branch GitHub Actions (`Design-AI audit`, `Deploy doc site`) passed for the constituent commits.
27
+
28
+ ### Versions
29
+ - `package.json` + `.claude-plugin/plugin.json`: 4.57.0 → 4.58.0.
30
+ - `vscode-extension/package.json`: remains 0.4.1.
31
+
32
+ ### What this enables
33
+ - Agents routing a brief now see additional relevant knowledge beyond the route's curated set, and every retrieval surface injects real design guidance rather than index tables.
34
+ - The Korean-market corpus gains async-control and B2B density coverage that dogfood runs specifically named as missing.
35
+ - Publishing v4.58.0 realigns `main` with the published package so the live registry smoke verifies cleanly again.
36
+
5
37
  ## v4.57.0 — Local Retrieval Memory: Ranked Search and Optional Embeddings (2026-07)
6
38
 
7
39
  Ships the Phase 754 AI-learning retrieval work: a deterministic, zero-dependency local retrieval layer over the knowledge corpus and the local learning profile, plus an opt-in local embedding rerank backend. Defaults are unchanged — `search`, `route`, `prompt`, and `pack` behave exactly as before unless a new flag is passed — so this release is additive and backward-compatible. It also moves `refs/` source links behind generated reference pages and hardens the docs deploy against intermittent GitHub Pages failures.
package/README.ko.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Audit](https://img.shields.io/badge/audit-passing-brightgreen)](https://github.com/sungjin9288/design-ai/actions/workflows/audit.yml)
4
4
  [![Docs](https://img.shields.io/badge/docs-live-indigo)](https://sungjin9288.github.io/design-ai/ko/)
5
- [![Knowledge files](https://img.shields.io/badge/knowledge-92-blue)](knowledge/PRINCIPLES.md)
5
+ [![Knowledge files](https://img.shields.io/badge/knowledge-94-blue)](knowledge/PRINCIPLES.md)
6
6
  [![Examples](https://img.shields.io/badge/examples-221-blue)](examples/README.md)
7
7
  [![Skills](https://img.shields.io/badge/skills-20-blue)](skills/README.md)
8
8
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  > **모델이 아니에요. 파인튜닝도 아니에요.** 디자인 전문 지식을 구조화한 코퍼스 + 에이전트가 바로 실행할 수 있는 지시문이에요. 범용 LLM을 이번 세션에서만큼은 시니어 디자이너로 바꿔주는 셈이에요.
14
14
 
15
- > **배포 상태, 2026-07-02 확인:** 로컬 `npm run release:check`는 통과했고, GitHub Pages 문서는 live 상태이며, GitHub Release `v4.56.0`과 npm `@design-ai/cli@4.56.0` publish registry smoke가 확인됐어요. Homebrew formula는 `v4.56.0`에 pinning되어 있고, VS Code Marketplace에는 `sungjin.design-ai-vscode@0.4.1`이 공개되어 있어요. 자세한 내용은 [`docs/external-status.md`](docs/external-status.md)를 확인하세요.
15
+ > **배포 상태, 2026-07-03 확인:** 로컬 `npm run release:check`는 통과했고, GitHub Pages 문서는 live 상태이며, GitHub Release `v4.57.0`과 npm `@design-ai/cli@4.57.0`(`latest`) publish provenance와 함께 확인됐어요. Homebrew formula는 `v4.57.0`에 pinning되어 있고, VS Code Marketplace에는 `sungjin.design-ai-vscode@0.4.1`이 공개되어 있어요. v4.57.0 post-publish live registry smoke는 stale 어서션에 걸렸고(`main`에서 수정됨), 패키지는 pre-publish packed-tarball smoke로 검증됐어요. 자세한 내용은 [`docs/external-status.md`](docs/external-status.md)를 확인하세요.
16
16
 
17
17
  ## 한눈에 보는 커버리지
18
18
 
@@ -107,7 +107,7 @@ design-ai/
107
107
 
108
108
  ├── refs/ # Sparse-clone된 업스트림 소스 (gitignored)
109
109
 
110
- ├── knowledge/ # 92개 손으로 쓴 + 추출된 지식 파일
110
+ ├── knowledge/ # 94개 손으로 쓴 + 추출된 지식 파일
111
111
  │ ├── design-tokens/ # W3C DTCG, OKLCH, HCT
112
112
  │ ├── components/ # Ant + MUI + shadcn 합성
113
113
  │ ├── patterns/ # 인증, 가격, 랜딩 히어로, 브랜드, 이메일 등
@@ -171,7 +171,7 @@ design-ai는 한국 시장을 1순위로 만들어졌고, 글로벌 시장 패
171
171
 
172
172
  ## 상태
173
173
 
174
- 전체 단계 로그는 [`docs/ROADMAP.md`](docs/ROADMAP.md), 현재 완료 범위는 [`docs/PRODUCT-READINESS.md`](docs/PRODUCT-READINESS.md)에서 확인하세요. 현재 **v4.56.0**: public npm publish, provenance-backed GitHub Actions release, public registry smoke, Website Console MCP readiness, workspace learning restore/eval coverage, handoff bundle verification, 90%+ component coverage가 완료됐어요.
174
+ 전체 단계 로그는 [`docs/ROADMAP.md`](docs/ROADMAP.md), 현재 완료 범위는 [`docs/PRODUCT-READINESS.md`](docs/PRODUCT-READINESS.md)에서 확인하세요. 현재 **v4.57.0**: public npm publish, provenance-backed GitHub Actions release, public registry smoke, Website Console MCP readiness, workspace learning restore/eval coverage, handoff bundle verification, 90%+ component coverage가 완료됐어요.
175
175
 
176
176
  핵심 디자인 컨설팅 워크플로우는 로컬 릴리스 기준으로 준비되어 있어요. 웹사이트 개선 컨트롤 타워는 zero-dependency static Web App과 `website-improvement` route/skill/command로 제공되고, Site Profile, audit checklist, MCP readiness, refactor prompt, handoff evidence tracking, bundle export/verify/repair를 한 번에 다뤄요. 로컬 학습 선호도는 `design-ai learn`으로 관리해요 — profile bootstrap, feedback 캡처, 읽기 전용 signal registry, 반복 QA 신호에서 만드는 skill 제안, 그리고 backup/restore/curate/audit까지 전부 로컬에서만 동작하는 opt-in 기능이에요. AI 모델 학습이나 fine-tuning은 여전히 현재 배포 범위 밖이에요.
177
177
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Audit](https://img.shields.io/badge/audit-passing-brightgreen)](https://github.com/sungjin9288/design-ai/actions/workflows/audit.yml)
4
4
  [![Docs](https://img.shields.io/badge/docs-live-indigo)](https://sungjin9288.github.io/design-ai/)
5
- [![Knowledge files](https://img.shields.io/badge/knowledge-92-blue)](knowledge/PRINCIPLES.md)
5
+ [![Knowledge files](https://img.shields.io/badge/knowledge-94-blue)](knowledge/PRINCIPLES.md)
6
6
  [![Examples](https://img.shields.io/badge/examples-221-blue)](examples/README.md)
7
7
  [![Skills](https://img.shields.io/badge/skills-20-blue)](skills/README.md)
8
8
 
@@ -12,7 +12,7 @@ A model-agnostic design knowledge base + skill system. Drop it in front of any A
12
12
 
13
13
  > **Not a model. Not a fine-tune.** A structured corpus of design expertise + agent-ready instructions that turn a general-purpose LLM into an expert.
14
14
 
15
- > **Distribution status, checked 2026-07-02:** local `npm run release:check` passes, GitHub Pages docs are live, GitHub Release `v4.56.0` is published, `@design-ai/cli@4.56.0` is public on npm with registry smoke coverage, the Homebrew formula is pinned to `v4.56.0`, and `sungjin.design-ai-vscode@0.4.1` is public on the VS Code Marketplace. See [`docs/external-status.md`](docs/external-status.md).
15
+ > **Distribution status, checked 2026-07-03:** local `npm run release:check` passes, GitHub Pages docs are live, GitHub Release `v4.57.0` is published, `@design-ai/cli@4.57.0` is public on npm (`latest`) with provenance and registry smoke coverage, the Homebrew formula is pinned to `v4.57.0`, and `sungjin.design-ai-vscode@0.4.1` is public on the VS Code Marketplace. The v4.57.0 post-publish live registry smoke hit a stale assertion (fixed on `main`); the package is validated by the pre-publish packed-tarball smoke. See [`docs/external-status.md`](docs/external-status.md).
16
16
 
17
17
  ## Coverage at a glance
18
18
 
@@ -107,7 +107,7 @@ design-ai/
107
107
 
108
108
  ├── refs/ # Sparse-cloned upstream sources (gitignored)
109
109
 
110
- ├── knowledge/ # 92 hand-written + extracted knowledge files
110
+ ├── knowledge/ # 94 hand-written + extracted knowledge files
111
111
  │ ├── design-tokens/ # Token systems (W3C DTCG, OKLCH, HCT)
112
112
  │ ├── components/ # Component synthesis (Ant + MUI + shadcn)
113
113
  │ ├── patterns/ # Auth, pricing, landing hero, brand, email, ...
@@ -213,7 +213,7 @@ Refresh refs/ on demand: `./tools/extractors/run-all.sh`.
213
213
 
214
214
  ## Status
215
215
 
216
- See [`docs/ROADMAP.md`](docs/ROADMAP.md) for the full phase log and [`docs/PRODUCT-READINESS.md`](docs/PRODUCT-READINESS.md) for the current completion boundary. Currently at **v4.56.0**: public npm publish, provenance-backed GitHub Actions release, public registry smoke, Website Console MCP readiness, workspace learning restore/eval coverage, handoff bundle verification, and 90%+ component coverage are complete.
216
+ See [`docs/ROADMAP.md`](docs/ROADMAP.md) for the full phase log and [`docs/PRODUCT-READINESS.md`](docs/PRODUCT-READINESS.md) for the current completion boundary. Currently at **v4.57.0**: public npm publish, provenance-backed GitHub Actions release, public registry smoke, Website Console MCP readiness, workspace learning restore/eval coverage, handoff bundle verification, and 90%+ component coverage are complete.
217
217
 
218
218
  Core design consulting workflows are locally release-ready. The website improvement control tower ships as a zero-dependency static Web App plus a `website-improvement` route/skill/command, covering Site Profiles, audit checklists, MCP readiness, refactor prompts, handoff evidence tracking, and bundle export/verify/repair. Local learning preferences are available through `design-ai learn` — profile bootstrap, feedback capture, a read-only signals registry, skill-proposal generation from repeated QA signals, and full backup/restore/curate/audit tooling, all local-only and opt-in. AI model training or fine-tuning remains outside the shipped scope.
219
219
 
@@ -28,7 +28,7 @@ function printHelp() {
28
28
  console.log(" --stdin Read the task brief, or route eval JSON with --eval, from standard input");
29
29
  console.log(" --list List route ids without scoring a brief");
30
30
  console.log(" --limit N Maximum route recommendations to return, 1-10. Default: 3");
31
- console.log(" --explain Include route scoring and reference coverage details");
31
+ console.log(" --explain Include route scoring, reference coverage, and related knowledge");
32
32
  console.log(" --eval-template Generate a runnable route eval checkpoint JSON template");
33
33
  console.log(" --eval Run deterministic route-selection checkpoint cases");
34
34
  console.log(" --strict With --eval, exit non-zero on warning or failure");
@@ -78,6 +78,17 @@ function printRoute(route, index, explain = false) {
78
78
  for (const knowledge of route.knowledge) {
79
79
  console.log(` read: ${formatPath(knowledge)}`);
80
80
  }
81
+
82
+ if (explain) {
83
+ const related = route.relatedKnowledge || [];
84
+ if (related.length === 0) {
85
+ console.log(" related: (none)");
86
+ } else {
87
+ for (const item of related) {
88
+ console.log(` related: ${item.id} ${dim(`(score ${item.score.toFixed(2)})`)}`);
89
+ }
90
+ }
91
+ }
81
92
  }
82
93
 
83
94
  function printCatalogRoute(route, index) {
@@ -211,6 +222,7 @@ export async function runRoute(args) {
211
222
  brief,
212
223
  sourceRoot: DESIGN_AI_HOME,
213
224
  limit: parsed.limit,
225
+ explain: parsed.explain,
214
226
  });
215
227
 
216
228
  const payload = {
@@ -109,7 +109,7 @@ export function buildRecallContext({
109
109
  const candidateCount = collectCorpusDocuments({ designAiPath, dirs }).length;
110
110
 
111
111
  const hits = query
112
- ? rankedSearchCorpus({ query, designAiPath, dirs, limit: recallLimit }).hits
112
+ ? rankedSearchCorpus({ query, designAiPath, dirs, limit: recallLimit, excludeGeneratedIndex: true }).hits
113
113
  : [];
114
114
 
115
115
  const selected = hits.map((hit) => ({
@@ -150,7 +150,7 @@ export function buildLearnRecall({
150
150
 
151
151
  const corpusCandidateCount = collectCorpusDocuments({ designAiPath, dirs }).length;
152
152
  const corpusHits = normalizedQuery
153
- ? rankedSearchCorpus({ query: normalizedQuery, designAiPath, dirs, limit }).hits
153
+ ? rankedSearchCorpus({ query: normalizedQuery, designAiPath, dirs, limit, excludeGeneratedIndex: true }).hits
154
154
  : [];
155
155
  const corpusSelected = corpusHits.map((hit) => ({
156
156
  id: hit.relPath,
package/cli/lib/route.mjs CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  import path from "node:path";
8
8
 
9
9
  import { parseBriefSourceFlag } from "./brief.mjs";
10
+ import { rankedSearchCorpus } from "./search-ranked.mjs";
10
11
  import { suggestNearest, unknownOptionMessage } from "./suggest.mjs";
11
12
 
12
13
  function exists(p) {
@@ -35,6 +36,17 @@ const ROUTE_OPTIONS = [
35
36
  ];
36
37
  const ROUTE_EVAL_VERSION = 1;
37
38
  const ROUTE_EVAL_DEFAULT_LIMIT = 3;
39
+ // Advisory "Related knowledge" recall (docs/AI-LEARNING-PHASE2.md, "Phase A
40
+ // implementation review" Q3): under `--explain` only, surface the top corpus
41
+ // knowledge/ files recalled by the shipped deterministic lexical scorer that the
42
+ // route's curated `knowledge` list does NOT already point to. Purely additive —
43
+ // it never changes route selection, ids, scores, or the curated list. Restricted
44
+ // to knowledge/ so it surfaces design knowledge, not docs/QUICKSTART. The recall
45
+ // pulls RELATED_KNOWLEDGE_RECALL_LIMIT candidates then keeps the top
46
+ // RELATED_KNOWLEDGE_KEEP after excluding the curated set.
47
+ const RELATED_KNOWLEDGE_DIRS = ["knowledge"];
48
+ const RELATED_KNOWLEDGE_RECALL_LIMIT = 10;
49
+ const RELATED_KNOWLEDGE_KEEP = 3;
38
50
  const CONFIDENCE_ORDER = {
39
51
  low: 1,
40
52
  medium: 2,
@@ -456,6 +468,35 @@ function routeExplanation({ hits, command, skills, agents, knowledge, forced = f
456
468
  };
457
469
  }
458
470
 
471
+ // Advisory related-knowledge recall for a single route. REUSES rankedSearchCorpus
472
+ // (the shipped deterministic lexical scorer — score desc, id asc) scoped to
473
+ // knowledge/, excludes the route's curated knowledge relPaths, and keeps the top
474
+ // RELATED_KNOWLEDGE_KEEP remaining. Returns [] on empty brief or no hits.
475
+ function relatedKnowledgeFor({ brief, sourceRoot, curatedRelPaths }) {
476
+ const query = String(brief || "").trim();
477
+ if (!query) return [];
478
+
479
+ const curated = new Set(curatedRelPaths);
480
+ const { hits } = rankedSearchCorpus({
481
+ query,
482
+ dirs: RELATED_KNOWLEDGE_DIRS,
483
+ limit: RELATED_KNOWLEDGE_RECALL_LIMIT,
484
+ designAiPath: sourceRoot,
485
+ // Recall/injection surface: keep generated index/meta docs (COVERAGE.md,
486
+ // INDEX.md, docs/reference/*) out of the advisory related-knowledge list.
487
+ excludeGeneratedIndex: true,
488
+ });
489
+
490
+ return hits
491
+ .filter((hit) => !curated.has(hit.relPath))
492
+ .slice(0, RELATED_KNOWLEDGE_KEEP)
493
+ .map((hit) => ({
494
+ id: hit.relPath,
495
+ score: hit.score,
496
+ matchedTokens: hit.matchedTokens,
497
+ }));
498
+ }
499
+
459
500
  function routeToResult(route, sourceRoot, hits, options = {}) {
460
501
  const command = route.command
461
502
  ? { path: route.command, exists: exists(path.join(sourceRoot, route.command)) }
@@ -467,6 +508,17 @@ function routeToResult(route, sourceRoot, hits, options = {}) {
467
508
  const fallback = Boolean(options.fallback);
468
509
  const catalog = Boolean(options.catalog);
469
510
 
511
+ // Advisory related-knowledge is attached ONLY when explain is requested, so the
512
+ // default `route` JSON stays byte-unchanged. `knowledge` already merges
513
+ // COMMON_KNOWLEDGE + route.knowledge, so its relPaths are the dedupe set.
514
+ const relatedKnowledge = options.explain
515
+ ? relatedKnowledgeFor({
516
+ brief: options.brief || "",
517
+ sourceRoot,
518
+ curatedRelPaths: knowledge.map((entry) => entry.path),
519
+ })
520
+ : null;
521
+
470
522
  return {
471
523
  id: route.id,
472
524
  label: route.label,
@@ -479,6 +531,7 @@ function routeToResult(route, sourceRoot, hits, options = {}) {
479
531
  knowledge,
480
532
  keywords: route.keywords,
481
533
  explanation: routeExplanation({ hits, command, skills, agents, knowledge, forced, fallback, catalog }),
534
+ ...(relatedKnowledge ? { relatedKnowledge } : {}),
482
535
  ...(forced ? { forced: true } : {}),
483
536
  ...(fallback ? { fallback: true } : {}),
484
537
  };
@@ -501,18 +554,22 @@ export function routeById({ routeId, sourceRoot }) {
501
554
  };
502
555
  }
503
556
 
504
- function fallbackResult(sourceRoot) {
557
+ function fallbackResult(sourceRoot, options = {}) {
505
558
  const route = ROUTES.find((item) => item.id === "design-from-brief");
506
559
  return {
507
- ...routeToResult(route, sourceRoot, [], { fallback: true }),
560
+ ...routeToResult(route, sourceRoot, [], { fallback: true, ...options }),
508
561
  confidence: "low",
509
562
  };
510
563
  }
511
564
 
512
- export function routeBrief({ brief, sourceRoot, limit = 3 }) {
565
+ export function routeBrief({ brief, sourceRoot, limit = 3, explain = false }) {
513
566
  const normalized = brief.trim();
514
567
  if (!normalized) return [];
515
568
 
569
+ // Only compute advisory related-knowledge under --explain; keep the default
570
+ // routing (keyword scoring, ordering, selection) completely unchanged.
571
+ const resultOptions = explain ? { explain: true, brief: normalized } : {};
572
+
516
573
  const scored = ROUTES
517
574
  .map((route) => ({ route, hits: keywordHits(normalized, route.keywords) }))
518
575
  .filter((item) => item.hits.length > 0)
@@ -521,10 +578,10 @@ export function routeBrief({ brief, sourceRoot, limit = 3 }) {
521
578
  return a.route.label.localeCompare(b.route.label);
522
579
  })
523
580
  .slice(0, limit)
524
- .map((item) => routeToResult(item.route, sourceRoot, item.hits));
581
+ .map((item) => routeToResult(item.route, sourceRoot, item.hits, resultOptions));
525
582
 
526
583
  if (scored.length > 0) return scored;
527
- return [fallbackResult(sourceRoot)];
584
+ return [fallbackResult(sourceRoot, resultOptions)];
528
585
  }
529
586
 
530
587
  function isoTimestamp(now = new Date()) {
@@ -24,6 +24,22 @@ import { DEFAULT_SEARCH_DIRS } from "./search.mjs";
24
24
 
25
25
  const RANKED_PREVIEW_LEN = 120;
26
26
 
27
+ // Predicate for GENERATED index/meta docs that must be kept out of the RECALL /
28
+ // context-injection layer (pack/prompt --with-recall, learn --recall corpus side,
29
+ // route --explain related knowledge). These files rank on keyword density but are
30
+ // meta/index tables, not design knowledge worth injecting into an agent's context.
31
+ // Rule: true when the basename is `COVERAGE.md` or `INDEX.md` (generated coverage
32
+ // table / component index), OR the forward-slash-normalized path starts with
33
+ // `docs/reference/` (generated upstream-reference pages). Raw `search --ranked`
34
+ // does NOT apply this — a user explicitly searching may legitimately want an index.
35
+ export function isGeneratedIndexDoc(relPath) {
36
+ const normalized = String(relPath || "").replace(/\\/g, "/");
37
+ const basename = normalized.slice(normalized.lastIndexOf("/") + 1);
38
+ return basename === "COVERAGE.md"
39
+ || basename === "INDEX.md"
40
+ || normalized.startsWith("docs/reference/");
41
+ }
42
+
27
43
  // N = max(limit*5, 25): the number of top lexical candidates handed to the embedding
28
44
  // reranker. Documented constant (docs/AI-LEARNING-PHASE2.md, Phase B CLI wiring).
29
45
  export function embeddingCandidateCount(limit) {
@@ -66,12 +82,28 @@ export function rankedSearchCorpus({
66
82
  dirs = DEFAULT_SEARCH_DIRS,
67
83
  limit = 20,
68
84
  indexDir = defaultIndexDir(),
85
+ // Opt-in RECALL filter: when true, drop generated index/meta docs
86
+ // (isGeneratedIndexDoc) BEFORE applying `limit`, so the limit fills with real
87
+ // knowledge instead of index files. Default false keeps raw `search --ranked`
88
+ // byte-unchanged. Filtering before the limit preserves determinism (score desc,
89
+ // id asc) — the rank pass already orders fully, we only remove excluded ids.
90
+ excludeGeneratedIndex = false,
69
91
  } = {}) {
70
92
  const documents = collectCorpusDocuments({ designAiPath, dirs });
71
93
  const stats = buildLexicalStats(documents.map(({ id, text }) => ({ id, text })));
72
94
  const textById = new Map(documents.map((doc) => [doc.id, doc.text]));
73
95
 
74
- const hits = rankLexical(query, stats, { limit }).map((hit) => ({
96
+ // When excluding, rank the FULL candidate pool first (limit = documents.length)
97
+ // so that filtering out index docs cannot let a real-knowledge hit fall off the
98
+ // pre-limit cutoff; then re-apply `limit`. Ordering is unchanged (score desc, id asc).
99
+ const ranked = rankLexical(query, stats, {
100
+ limit: excludeGeneratedIndex ? documents.length : limit,
101
+ });
102
+ const filtered = excludeGeneratedIndex
103
+ ? ranked.filter((hit) => !isGeneratedIndexDoc(hit.id)).slice(0, limit)
104
+ : ranked;
105
+
106
+ const hits = filtered.map((hit) => ({
75
107
  relPath: hit.id,
76
108
  file: path.join(designAiPath, hit.id),
77
109
  score: hit.score,
package/docs/ROADMAP.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Roadmap
2
2
 
3
+ ## Phase 756 — Retrieval Across Surfaces and Corpus Depth Release (v4.58.0) ✓ ready
4
+
5
+ Groups the post-v4.57.0 retrieval completion and corpus work into one release: `route --explain` related-knowledge enrichment (retrieval now spans every surface), recall exclusion of generated index/meta files, the async-control and Korean B2B density knowledge files, and the registry-smoke assertion fix that realigns the live post-publish smoke with the shared scorer. Defaults are unchanged; the release is additive and backward-compatible.
6
+
7
+ ### Changed
8
+ - `route --explain` gains an advisory related-knowledge section (routing unchanged), completing retrieval across `search`/`prompt`/`pack`/`learn`/`route`; carried to MCP via the existing `design_ai_route` `explain` parameter.
9
+ - Recall injection excludes generated index/meta files so injected context is real design knowledge; raw `search --ranked` is unchanged.
10
+ - Added `knowledge/patterns/async-control.md` and `knowledge/i18n/korean-density-conventions.md` (corpus now 94 files), closing the remaining dogfood-named gaps.
11
+ - Fixed the stale `registry-smoke.py` learn-relevance token-order assertion so the live post-publish registry smoke matches the published package.
12
+
13
+ ### Verified
14
+ - All 8 audits passed.
15
+ - `npm run release:check` (unit tests, strict audits, whitespace, package contents, release metadata, release self-tests, packed-tarball smoke).
16
+ - `npm run release:metadata`.
17
+ - `git diff --check`.
18
+ - Main-branch GitHub Actions (`Design-AI audit`, `Deploy doc site`) passed for the constituent commits.
19
+
20
+ ### Versions
21
+ - `package.json` + `.claude-plugin/plugin.json`: 4.57.0 → 4.58.0.
22
+ - `vscode-extension/package.json`: remains 0.4.1.
23
+
24
+ ### What this enables
25
+ - Retrieval is uniform across every design-ai surface and injects real design knowledge, not index tables; the Korean corpus gains async-control and B2B density depth.
26
+ - Publishing v4.58.0 realigns `main` with the published package so the live registry smoke verifies cleanly.
27
+
28
+ ### What's still ahead
29
+ - Push the v4.58.0 tag, then verify npm publish (provenance), GitHub Release, public `npm run registry:smoke` (now skew-free), and docs deployment.
30
+ - Refresh the Homebrew formula SHA-256 and the [external-status.md](external-status.md) / README distribution status from published `4.57.0` to `4.58.0` after the tag tarball exists.
31
+
3
32
  ## Phase 755 — Local Retrieval Memory Release (v4.57.0) ✓ ready
4
33
 
5
34
  Groups the Phase 754 A+B retrieval work, the MCP ranked-search parameter, the `refs/` reference-page migration, and the docs-deploy retry hardening into one public package release. Scope and data boundaries are defined in [AI-LEARNING-PHASE2.md](AI-LEARNING-PHASE2.md); defaults are unchanged, so the release is additive and backward-compatible.
@@ -1,22 +1,22 @@
1
1
  # External Publication Status
2
2
 
3
- > Checked: 2026-07-02
3
+ > Checked: 2026-07-03
4
4
  > Scope: npm registry, GitHub Pages, Homebrew tap, VS Code Marketplace, Claude/Codex MCP
5
5
 
6
6
  ## Summary
7
7
 
8
- Local release readiness is verified, GitHub Pages docs are publicly reachable, GitHub Release `v4.56.0` is published, and the Homebrew tap formula points at the `v4.56.0` release source tarball with a verified SHA-256. npm is publicly published at `@design-ai/cli@4.56.0`; publish run `28569283984` succeeded with provenance and the public registry smoke passed. The VS Code extension `sungjin.design-ai-vscode` is published to the VS Code Marketplace at version `0.4.1`; the Gallery API is reachable. The public npm MCP entrypoint and the local Claude Code / Codex MCP client registrations were rechecked on 2026-07-02.
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.
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.56.0`; publish run `28569283984` succeeded and public registry smoke passed for `@design-ai/cli@4.56.0`. | `evidence/cli-logs/npm-registry-status.log`, `evidence/cli-logs/npm-registry-smoke.log`, `evidence/cli-logs/npm-publish-v4.56.0-success.log` |
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` |
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.56.0` release source tarball with SHA-256 `507d2519296497defcd486c0ffc2b5164967a0bc540ddc31bc89502350688212`; Ruby syntax and `brew style` passed | `evidence/cli-logs/homebrew-formula-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` |
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.56.0` | Published with release tarball asset `design-ai-cli-4.56.0.tgz` | `evidence/cli-logs/github-release-v4.56.0.log` |
19
- | MCP server | `@design-ai/cli@4.56.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.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` |
20
20
 
21
21
  ## Interpretation
22
22
 
@@ -12,7 +12,7 @@ generated_at: 2026-07-03
12
12
 
13
13
  | Layer | Count | Detail |
14
14
  | --- | --- | --- |
15
- | Knowledge files | 92 | 77 hand-written + 15 generated |
15
+ | Knowledge files | 94 | 79 hand-written + 15 generated |
16
16
  | Skills (PLAYBOOK + SKILL) | 20 | 20 with verification phase |
17
17
  | Worked examples | 221 | |
18
18
  | Extractors | 12 | |
@@ -30,12 +30,12 @@ generated_at: 2026-07-03
30
30
  | `conversational` | 5 | 5 | 0 |
31
31
  | `design-tokens` | 4 | 3 | 1 |
32
32
  | `game-ui` | 5 | 5 | 0 |
33
- | `i18n` | 6 | 6 | 0 |
33
+ | `i18n` | 7 | 7 | 0 |
34
34
  | `icons` | 1 | 0 | 1 |
35
35
  | `illustration` | 5 | 5 | 0 |
36
36
  | `layout` | 1 | 1 | 0 |
37
37
  | `motion` | 6 | 6 | 0 |
38
- | `patterns` | 30 | 24 | 6 |
38
+ | `patterns` | 31 | 25 | 6 |
39
39
  | `platforms` | 1 | 1 | 0 |
40
40
  | `print` | 6 | 6 | 0 |
41
41
  | `spatial` | 5 | 5 | 0 |
@@ -49,7 +49,7 @@ generated_at: 2026-07-03
49
49
 
50
50
  | File | Lines | Type | Title |
51
51
  | --- | --- | --- | --- |
52
- | [knowledge/COVERAGE.md](../knowledge/COVERAGE.md) | 741 | generated | Coverage report |
52
+ | [knowledge/COVERAGE.md](../knowledge/COVERAGE.md) | 743 | generated | Coverage report |
53
53
  | [knowledge/PRINCIPLES.md](../knowledge/PRINCIPLES.md) | 108 | hand-written | Design-AI principles |
54
54
 
55
55
  #### a11y
@@ -108,9 +108,10 @@ generated_at: 2026-07-03
108
108
  | File | Lines | Type | Title |
109
109
  | --- | --- | --- | --- |
110
110
  | [knowledge/i18n/korean-app-store-visual.md](../knowledge/i18n/korean-app-store-visual.md) | 223 | hand-written | Korean app store visual design |
111
+ | [knowledge/i18n/korean-density-conventions.md](../knowledge/i18n/korean-density-conventions.md) | 108 | hand-written | Korean B2B density conventions |
111
112
  | [knowledge/i18n/korean-document-style.md](../knowledge/i18n/korean-document-style.md) | 301 | hand-written | Korean document style |
112
113
  | [knowledge/i18n/korean-payments.md](../knowledge/i18n/korean-payments.md) | 211 | hand-written | Korean payments |
113
- | [knowledge/i18n/korean-product-conventions.md](../knowledge/i18n/korean-product-conventions.md) | 96 | hand-written | Korean product UX conventions |
114
+ | [knowledge/i18n/korean-product-conventions.md](../knowledge/i18n/korean-product-conventions.md) | 98 | hand-written | Korean product UX conventions |
114
115
  | [knowledge/i18n/korean-publishing.md](../knowledge/i18n/korean-publishing.md) | 139 | hand-written | Korean app store & publishing requirements |
115
116
  | [knowledge/i18n/korean-typography.md](../knowledge/i18n/korean-typography.md) | 123 | hand-written | Korean (Hangul) typography for product UI |
116
117
 
@@ -151,6 +152,7 @@ generated_at: 2026-07-03
151
152
 
152
153
  | File | Lines | Type | Title |
153
154
  | --- | --- | --- | --- |
155
+ | [knowledge/patterns/async-control.md](../knowledge/patterns/async-control.md) | 233 | hand-written | Async control patterns |
154
156
  | [knowledge/patterns/auth-flow-design.md](../knowledge/patterns/auth-flow-design.md) | 316 | hand-written | Authentication flow design |
155
157
  | [knowledge/patterns/b2b-onboarding-flows.md](../knowledge/patterns/b2b-onboarding-flows.md) | 182 | hand-written | B2B onboarding flows |
156
158
  | [knowledge/patterns/brand-identity.md](../knowledge/patterns/brand-identity.md) | 238 | hand-written | Brand identity |
@@ -162,8 +164,8 @@ generated_at: 2026-07-03
162
164
  | [knowledge/patterns/document-typography.md](../knowledge/patterns/document-typography.md) | 248 | hand-written | Document typography |
163
165
  | [knowledge/patterns/email-design.md](../knowledge/patterns/email-design.md) | 338 | hand-written | Email design |
164
166
  | [knowledge/patterns/empty-states.md](../knowledge/patterns/empty-states.md) | 263 | hand-written | Empty states |
165
- | [knowledge/patterns/error-states.md](../knowledge/patterns/error-states.md) | 318 | hand-written | Error states |
166
- | [knowledge/patterns/form-design.md](../knowledge/patterns/form-design.md) | 237 | hand-written | Form design patterns |
167
+ | [knowledge/patterns/error-states.md](../knowledge/patterns/error-states.md) | 319 | hand-written | Error states |
168
+ | [knowledge/patterns/form-design.md](../knowledge/patterns/form-design.md) | 239 | hand-written | Form design patterns |
167
169
  | [knowledge/patterns/information-architecture.md](../knowledge/patterns/information-architecture.md) | 341 | hand-written | Information architecture |
168
170
  | [knowledge/patterns/landing-hero-design.md](../knowledge/patterns/landing-hero-design.md) | 283 | hand-written | Landing hero design |
169
171
  | [knowledge/patterns/landing-page-patterns.md](../knowledge/patterns/landing-page-patterns.md) | 681 | generated | Landing page patterns |
@@ -0,0 +1,108 @@
1
+ <!-- hand-written -->
2
+ ---
3
+ title: Korean B2B density conventions
4
+ applies_to: [web, mobile, all-ui, korean]
5
+ version: 1.0.0
6
+ last_updated: 2026-07
7
+ stability: stable
8
+ ---
9
+
10
+ # Korean B2B density conventions
11
+
12
+ Korean enterprise software — ERP, HR, groupware, admin consoles, financial back-office — is visibly denser than its Western counterpart, and denser again than Korean *consumer* apps. A Western enterprise layout ported unchanged into a Korean B2B product reads as sparse, slow, and "not serious." This file is the density half of the Korean playbook; for consumer density and the broader Korean conventions, see [`knowledge/i18n/korean-product-conventions.md`](korean-product-conventions.md).
13
+
14
+ ## Why Korean B2B is denser
15
+
16
+ - **Information-per-screen is a value, not a cost.** Power users (accountants, HR admins, operators) work the same screens all day and want everything in view — more rows, more columns, fewer clicks to the next field. Scrolling and pagination read as friction, not breathing room.
17
+ - **The reference points are dense.** 더존, 영림원, SAP-Korea deployments, government e-gov portals, and bank back-office tools set the expectation. Naver Works / Kakao Work admin, Toss Business, and 뱅크샐러드 business consoles are the modern-but-still-dense benchmark.
18
+ - **Hangul carries more meaning per character.** A Korean label is often shorter than its English equivalent for the same content, so the same row holds more without feeling cramped.
19
+
20
+ This is a deliberate register, not clutter. The craft is being dense **and** legible — not dense **and** broken.
21
+
22
+ ## The density ladder
23
+
24
+ Pick a mode per surface and hold it. Do not mix comfortable and compact rows in one table.
25
+
26
+ | Mode | Row height (approx) | Use for |
27
+ | --- | --- | --- |
28
+ | **Comfortable** | 48–56px | Consumer-facing, onboarding, marketing-in-product, low-frequency admin |
29
+ | **Cozy** (default KR B2B) | 36–44px | Most enterprise tables, forms, lists — the Korean B2B baseline |
30
+ | **Compact** | 28–32px | Data grids for power users, financial ledgers, high-row-count monitoring |
31
+
32
+ Western enterprise defaults to Comfortable; **Korean B2B defaults to Cozy.** Porting in usually means dropping one rung: reduce vertical padding ~15–25% and expect 30–50% more rows per fold. Offer a density toggle (Comfortable / Cozy / Compact) on heavy data surfaces so individual operators can go denser; persist the choice per user.
33
+
34
+ ## Tables are the center of gravity
35
+
36
+ Korean B2B UIs are table-first. Most screens are a filter bar plus a table.
37
+
38
+ - **Many columns are normal.** 8–15 columns is routine; horizontal scroll with a **frozen first column** (and often a frozen header row) beats hiding columns behind a menu.
39
+ - **Compact rows, aligned numerals.** Right-align numbers and currency, use tabular figures, and keep row height in the Cozy/Compact band. See [`knowledge/patterns/money-and-amount.md`](../patterns/money-and-amount.md) for amount formatting.
40
+ - **Inline row actions.** Put edit / delete / detail as compact icon buttons or a trailing action cell, not behind a per-row overflow menu that costs an extra click.
41
+ - **Sticky context.** Freeze the header and the key identity column (사번, 거래처, 전표번호) so the operator never loses the row's identity while scrolling wide.
42
+ - **Totals and subtotals in view.** A pinned footer row with sums/counts is expected on financial and inventory tables — operators reconcile against it constantly.
43
+ - **Zebra or hairline rules, not heavy borders.** At Cozy/Compact density, 1px hairline separators or subtle zebra striping keep rows scannable without adding visual weight.
44
+
45
+ ## Forms at density
46
+
47
+ - **Label-left (horizontal) layout** is the Korean B2B default for data-entry forms — it packs more fields per vertical space than label-top and matches the ledger-like mental model. Label-top is for consumer/onboarding.
48
+ - **Multi-column forms.** Two or three field columns per row are normal for registration and master-data screens. Group by section with thin dividers, not big gaps.
49
+ - **Tight field spacing.** Reduce inter-field vertical rhythm versus consumer forms; keep it consistent so the grid reads cleanly. See [`knowledge/patterns/form-design.md`](../patterns/form-design.md).
50
+ - **Required/optional density.** Mark required with a compact indicator (asterisk or a small "필수" chip); don't spend a whole helper-text line per field unless the rule is non-obvious.
51
+ - **Keyboard-first.** Power users tab through fields fast. Preserve a logical tab order, support Enter-to-next where appropriate, and don't trap focus in date/select popovers.
52
+
53
+ ## Lists, trees, and navigation
54
+
55
+ - **Dense list rows** with secondary metadata inline (status chip, date, owner on one line) rather than stacked. See [`knowledge/patterns/list-and-feed.md`](../patterns/list-and-feed.md).
56
+ - **Tree navigation** (조직도, 계정과목, 메뉴 권한) is common and should stay compact — small row height, clear expand affordances, many levels visible at once.
57
+ - **Persistent side navigation** with dense, possibly two-level menus is expected over hamburger-hidden nav; enterprise users want the whole map visible. This is the opposite of the consumer preference noted in [`korean-product-conventions.md`](korean-product-conventions.md).
58
+
59
+ ## Typography at density
60
+
61
+ - **Base size can drop, but not below legibility.** 13–14px body is common in KR B2B tables (vs 16px consumer). Do not go below ~12px for Hangul primary content — Hangul syllable blocks lose legibility faster than Latin at small sizes.
62
+ - **Line-height stays generous relative to size.** Even dense Korean text needs ~1.4–1.5 line-height for the syllable blocks to breathe; do not crush leading to save space. See [`knowledge/i18n/korean-typography.md`](korean-typography.md).
63
+ - **Numerals: tabular, aligned.** Financial density depends on figures lining up.
64
+ - **Truncate with intent.** Ellipsize long values with a tooltip/title for the full string rather than wrapping and breaking row rhythm.
65
+
66
+ ## Density must not break accessibility
67
+
68
+ Density is the constraint; accessibility is the floor. When they conflict, accessibility wins.
69
+
70
+ - **Touch targets.** On touch/hybrid B2B (tablets on the floor, POS, hospital carts), keep interactive targets ≥ 44×44px *hit area* even when the visual row is compact — expand the clickable area beyond the visible cell rather than shrinking the target.
71
+ - **Contrast holds at every density.** Hairline separators and secondary text must still meet WCAG contrast; dense does not mean low-contrast gray-on-gray. See [`knowledge/a11y/contrast.md`](../a11y/contrast.md).
72
+ - **Focus visibility.** A visible focus ring is non-negotiable for keyboard-driven data entry; ensure it is not clipped by tight row borders. See [`knowledge/a11y/keyboard-and-focus.md`](../a11y/keyboard-and-focus.md).
73
+ - **Density toggle as an a11y affordance.** Offering Comfortable mode is itself an accessibility accommodation for users who need larger targets and text.
74
+
75
+ ## Tokens consumed
76
+
77
+ ```
78
+ --space-2xs, --space-xs (dense row padding, inter-field gaps)
79
+ --space-sm (section spacing at density)
80
+ --row-height-compact (or --space-based row sizing)
81
+ --font-size-sm (13–14px dense body)
82
+ --line-height-normal (~1.4–1.5 for Hangul)
83
+ --color-border-subtle (hairline separators)
84
+ --color-bg-subtle (zebra striping)
85
+ --color-text-secondary (inline metadata)
86
+ --color-text-tabular (aligned numerals)
87
+ ```
88
+
89
+ Define a **density scale** as tokens (comfortable / cozy / compact row heights + padding) so the toggle flips a token set, not ad-hoc per-component values.
90
+
91
+ ## Don't
92
+
93
+ - Don't ship Western enterprise comfortable-density defaults unchanged into a Korean B2B product — it reads as sparse and unserious.
94
+ - Don't hide columns behind menus to avoid width — Korean power users prefer horizontal scroll with frozen columns.
95
+ - Don't mix row-height modes within one table.
96
+ - Don't crush Hangul below ~12px or crush line-height to gain rows — legibility breaks before the space saved is worth it.
97
+ - Don't let density erode contrast, focus rings, or touch-target hit areas.
98
+ - Don't bury per-row actions behind an overflow menu on high-frequency screens — inline them.
99
+ - Don't apply consumer nav patterns (hamburger, hidden tabs) to enterprise — surface the full dense menu.
100
+
101
+ ## Cross-reference
102
+
103
+ - [`knowledge/i18n/korean-product-conventions.md`](korean-product-conventions.md) — consumer density and the broader Korean conventions this file specializes from
104
+ - [`knowledge/i18n/korean-typography.md`](korean-typography.md) — Hangul sizing and line-height at density
105
+ - [`knowledge/patterns/form-design.md`](../patterns/form-design.md) — label-left, multi-column, dense forms
106
+ - [`knowledge/patterns/list-and-feed.md`](../patterns/list-and-feed.md) — dense list rows and data grids
107
+ - [`knowledge/patterns/money-and-amount.md`](../patterns/money-and-amount.md) — tabular numerals and aligned amounts
108
+ - [`knowledge/a11y/contrast.md`](../a11y/contrast.md), [`knowledge/a11y/keyboard-and-focus.md`](../a11y/keyboard-and-focus.md) — the accessibility floor density must respect
@@ -58,6 +58,8 @@ This is not "cluttered design" — it matches reading pace and screen-time habit
58
58
 
59
59
  Reference: see how Coupang, Naver, KakaoTalk, Toss differ from Amazon, Google, WhatsApp, Robinhood.
60
60
 
61
+ For enterprise / B2B density (denser again — ERP, HR, groupware, admin, financial back-office), see [`knowledge/i18n/korean-density-conventions.md`](korean-density-conventions.md).
62
+
61
63
  ## Color associations
62
64
 
63
65
  Different from Western color symbolism:
@@ -0,0 +1,233 @@
1
+ <!-- hand-written -->
2
+ ---
3
+ title: Async control patterns
4
+ applies_to: [web, mobile, all-ui]
5
+ version: 1.0.0
6
+ last_updated: 2026-07
7
+ stability: stable
8
+ ---
9
+
10
+ # Async control patterns
11
+
12
+ Every action that talks to a server has an in-flight life. This file is about that life — the moment between the tap and the answer — not about the error screen at the end (see [`patterns/error-states.md`](error-states.md) for that). Get the in-flight state wrong and you get the two most common async bugs in product UIs: double submits and stuck spinners.
13
+
14
+ ## The async action lifecycle
15
+
16
+ Every async action moves through the same four states. Model them explicitly; do not infer "loading" from the absence of data.
17
+
18
+ ```
19
+ idle ──trigger──▶ pending ──┬──▶ success ──▶ idle
20
+ ▲ │
21
+ └─────────────────────────┴──▶ error ────▶ idle (retryable)
22
+ ```
23
+
24
+ | State | The control shows | The user can |
25
+ | --- | --- | --- |
26
+ | **idle** | Its normal, enabled label | Trigger the action |
27
+ | **pending** | Busy affordance + disabled | Wait, or cancel (if cancellable) |
28
+ | **success** | Brief confirmation, then idle | Continue |
29
+ | **error** | Recovery affordance | Retry or recover — see `error-states.md` |
30
+
31
+ Rule: a control never sits in `pending` forever. Every request has a timeout that forces it to `error`.
32
+
33
+ ## Action controls during pending
34
+
35
+ The trigger control (button, menu item, toggle) is where most async bugs live.
36
+
37
+ | Do | Don't |
38
+ | --- | --- |
39
+ | Disable the control while its own action is in flight | Leave it clickable and hope |
40
+ | Show a busy affordance **inside or beside** the control | Replace the whole screen with a spinner for a single button |
41
+ | Keep the label readable — swap to a present-progressive verb | Blank the label to just a spinner (the user forgets what they clicked) |
42
+ | Restore the control on success **and** error | Leave it disabled after a failed request |
43
+
44
+ Label swap, Korean and English:
45
+
46
+ | Idle | Pending | Done |
47
+ | --- | --- | --- |
48
+ | 저장 | 저장 중… | 저장됨 |
49
+ | 불러오기 | 불러오는 중… | — |
50
+ | 결제하기 | 결제 처리 중… | 결제 완료 |
51
+ | 제출 | 제출 중… | — |
52
+
53
+ ```
54
+ ┌─────────────────┐ ┌─────────────────┐
55
+ │ 저장 │ → │ ◌ 저장 중… │ (disabled, spinner leads label)
56
+ └─────────────────┘ └─────────────────┘
57
+ ```
58
+
59
+ ## Double-submit prevention
60
+
61
+ The single most important async rule: **a control that started an action cannot start it again until that action resolves.** Disabling on `pending` handles the fast double-tap. Also guard the handler itself, because disabled is a UI state that a determined tap (or a slow render) can beat.
62
+
63
+ ```tsx
64
+ function SubmitButton({ onSubmit }) {
65
+ const [status, setStatus] = useState("idle"); // idle | pending | error
66
+
67
+ async function handleClick() {
68
+ if (status === "pending") return; // guard: not just the disabled attr
69
+ setStatus("pending");
70
+ try {
71
+ await onSubmit();
72
+ setStatus("idle");
73
+ } catch (err) {
74
+ setStatus("error"); // hand off to error-states.md
75
+ }
76
+ }
77
+
78
+ return (
79
+ <Button onClick={handleClick} disabled={status === "pending"} aria-busy={status === "pending"}>
80
+ {status === "pending" ? "저장 중…" : "저장"}
81
+ </Button>
82
+ );
83
+ }
84
+ ```
85
+
86
+ For money and irreversible actions (payment, transfer, delete), double-submit guarding is a correctness requirement, not a nicety. Pair the client guard with a server-side idempotency key — cite [`patterns/money-and-amount.md`](money-and-amount.md).
87
+
88
+ ## Choosing a loading affordance by duration
89
+
90
+ Match the indicator to how long the wait actually is. Guessing wrong is worse than no indicator.
91
+
92
+ | Expected wait | Affordance | Why |
93
+ | --- | --- | --- |
94
+ | < 100ms | **Nothing** | Faster than perception; a flashed spinner reads as a glitch |
95
+ | 100ms – 1s | **In-place busy** (button spinner, subtle) | Confirms the tap registered |
96
+ | 1s – 10s | **Skeleton** (for content) or **progress** (for known-length work) | Shows shape/position; feels faster than a spinner |
97
+ | > 10s | **Progress + cancel + background option** | Let the user leave and be notified |
98
+ | Unknown length | **Indeterminate spinner with a timeout** | Never an infinite bar |
99
+
100
+ Spinner vs skeleton: a spinner says "something is happening"; a skeleton says "*this* is what's coming and where." Prefer skeletons for content regions (lists, cards, detail panes) — see [`patterns/list-and-feed.md`](list-and-feed.md). Reserve spinners for actions and small inline waits.
101
+
102
+ Anti-flicker: if you show a delayed spinner (only after 100–300ms), also hold it for a minimum (~300ms) once shown, so a request that resolves at 310ms doesn't flash. Delay-in, min-hold-out.
103
+
104
+ ## Optimistic updates
105
+
106
+ Apply the change in the UI immediately, before the server confirms, then reconcile.
107
+
108
+ Use optimistic when: the action almost always succeeds, is cheap to reverse, and the wait would break flow — likes, toggles, reordering, adding a to-do, marking read.
109
+
110
+ Do **not** use optimistic when: the action is money, irreversible, or the server is the source of truth for the result (payment, booking a seat, submitting an exam). There, show real `pending` and wait.
111
+
112
+ ```
113
+ tap ─▶ update UI now ─▶ fire request ─┬─ ok: keep, drop the "syncing" mark
114
+ └─ fail: roll back to prior value + show why
115
+ ```
116
+
117
+ Rules:
118
+ - Keep the pre-action value so you can roll back exactly.
119
+ - Mark optimistic items as unconfirmed (subtle) until the server agrees, so a rollback isn't a jarring surprise.
120
+ - On rollback, tell the user what reverted and why — a silent snap-back looks like a bug.
121
+
122
+ ## Debounce and throttle for async-triggering input
123
+
124
+ Input that fires requests as the user types or drags must be rate-limited, or it floods the server and races itself.
125
+
126
+ | Technique | Fires | Use for |
127
+ | --- | --- | --- |
128
+ | **Debounce** (wait for a pause) | Once, after input stops for N ms | Search-as-you-type (250–350ms), autosave (500ms–2s) |
129
+ | **Throttle** (at most every N ms) | On a steady cadence | Drag/scroll-driven fetches, live position updates |
130
+ | **Leading + trailing** | Immediately, then once more at the end | Feels responsive but still settles |
131
+
132
+ Debounce is about *when to fire*; cancellation (next section) is about *what to do with requests already in flight*. You need both.
133
+
134
+ ## Cancellation and out-of-order responses
135
+
136
+ Two requests fired in order can return out of order. Without cancellation, an older, slower response overwrites a newer one — the classic "I typed 서울, it shows 서" bug.
137
+
138
+ - Cancel the previous in-flight request when a new one supersedes it (`AbortController` on the web).
139
+ - Cancel in-flight requests when the component unmounts or the user navigates away.
140
+ - If you cannot cancel, **guard on apply**: tag each request and ignore any response that isn't the latest.
141
+
142
+ ```tsx
143
+ useEffect(() => {
144
+ const controller = new AbortController();
145
+ fetchResults(query, { signal: controller.signal })
146
+ .then(setResults)
147
+ .catch(ignoreAbort);
148
+ return () => controller.abort(); // supersede on new query / unmount
149
+ }, [query]);
150
+ ```
151
+
152
+ ## Concurrency policy
153
+
154
+ When the same action can be triggered while one is running, decide the policy on purpose:
155
+
156
+ | Policy | Behavior | Use for |
157
+ | --- | --- | --- |
158
+ | **Block** | Ignore new triggers until the current resolves | Submits, payments (with double-submit guard) |
159
+ | **Latest-wins** | Cancel the running one, keep only the newest | Search, filters, autosave |
160
+ | **Queue** | Run in order, one at a time | Ordered writes that must all land |
161
+ | **Parallel** | Let all run | Independent reads |
162
+
163
+ Default to **block** for writes and **latest-wins** for reads unless you have a reason otherwise.
164
+
165
+ ## Timeouts and the stuck-spinner failure
166
+
167
+ A spinner with no timeout is a trap: on a dropped connection it spins forever and the user is stuck.
168
+
169
+ - Give every request a client timeout (e.g. 10–30s by operation).
170
+ - On timeout, transition to `error` with a retry — hand off to [`patterns/error-states.md`](error-states.md) (network/server section, retry with backoff).
171
+ - For genuinely long work (video processing, exports), don't hold a spinner — move to a **background job** pattern: acknowledge, let the user leave, notify on completion.
172
+
173
+ ## Perceived performance
174
+
175
+ The felt speed of an async UI is mostly about what you show during the wait:
176
+
177
+ - **Optimistic UI** — instant for reversible actions (above).
178
+ - **Skeletons** — for content; show structure, not a void.
179
+ - **Stale-while-revalidate** — show the last known data immediately, refresh in the background, swap in quietly. Mark it subtly as refreshing so a change isn't startling.
180
+ - **Progressive reveal** — render what's ready (shell, then data) instead of blocking on the slowest piece.
181
+
182
+ ## Tokens consumed
183
+
184
+ ```
185
+ --color-accent (spinner, progress fill)
186
+ --color-text-secondary (pending label, "저장 중…")
187
+ --color-text-tertiary (unconfirmed / optimistic mark, "동기화 중")
188
+ --color-bg-subtle (skeleton base)
189
+ --color-bg-elevated (skeleton shimmer highlight)
190
+ --radius-md
191
+ --space-xs, --space-sm
192
+ --duration-fast (spinner delay-in / min-hold)
193
+ --easing-standard
194
+ ```
195
+
196
+ ## Accessibility
197
+
198
+ - Set `aria-busy="true"` on a control or region while its action is pending; clear it when resolved.
199
+ - Announce state changes politely: a visually-hidden `role="status"` (`aria-live="polite"`) region saying "저장 중" then "저장됨". Do not use `assertive` for routine progress — reserve that for errors (see `error-states.md`).
200
+ - Keep the control's accessible name stable and meaningful during pending — the label swap ("저장" → "저장 중…") is fine; an icon-only spinner with no name is not.
201
+ - On success that removes the control (e.g. a submit that navigates), move focus deliberately to the next logical element; never drop focus to `<body>`.
202
+ - Respect `prefers-reduced-motion`: swap spinners/shimmer for a static or fade indicator.
203
+ - Disabled-while-pending must still be perceivable — don't rely on color alone; the busy affordance carries the meaning.
204
+
205
+ ## Korean considerations
206
+
207
+ Per [`knowledge/i18n/korean-product-conventions.md`](../i18n/korean-product-conventions.md):
208
+ - Progress labels use the present-progressive `~중…`: "저장 중…", "불러오는 중…", "처리 중…", "결제 처리 중…".
209
+ - Keep pending labels short — Korean verbs + `중…` stay compact, so the button width rarely jumps.
210
+ - For payment and transfer flows, show explicit real `pending` (never optimistic) and a clear "결제 처리 중…" state; cite [`knowledge/i18n/korean-payments.md`](../i18n/korean-payments.md) for the surrounding flow and failure messages.
211
+ - Dense Korean B2B layouts favor in-place busy affordances (inline spinner, row-level skeleton) over full-page blocking overlays — cite [`knowledge/i18n/korean-product-conventions.md`](../i18n/korean-product-conventions.md) on density.
212
+
213
+ ## Don't
214
+
215
+ - Don't leave a control clickable during its own async action — the double-submit bug.
216
+ - Don't guard double-submit with the disabled attribute alone; guard the handler too.
217
+ - Don't show a spinner for a sub-100ms wait — it reads as a flicker.
218
+ - Don't show a spinner that can spin forever — every request has a timeout.
219
+ - Don't blank a button to a bare spinner; keep a readable pending label.
220
+ - Don't use optimistic updates for money, payments, or irreversible actions.
221
+ - Don't let an older response overwrite a newer one — cancel or guard-on-apply.
222
+ - Don't hold a full-screen spinner for long work — move it to a background job with notify.
223
+ - Don't roll back an optimistic change silently — say what reverted and why.
224
+ - Don't announce routine progress with `aria-live="assertive"` — that's for errors.
225
+
226
+ ## Cross-reference
227
+
228
+ - [`knowledge/patterns/error-states.md`](error-states.md) — where the `error` state goes: recovery, retry-with-backoff, network/server messages
229
+ - [`knowledge/patterns/form-design.md`](form-design.md) — submit buttons, validation-in-flight, form-level pending
230
+ - [`knowledge/patterns/list-and-feed.md`](list-and-feed.md) — skeletons, pull-to-refresh, infinite scroll loading
231
+ - [`knowledge/patterns/empty-states.md`](empty-states.md) — the resolved-but-no-data end state, distinct from loading
232
+ - [`knowledge/patterns/money-and-amount.md`](money-and-amount.md) — why money actions use real pending + idempotency, never optimistic
233
+ - [`knowledge/i18n/korean-payments.md`](../i18n/korean-payments.md) — payment in-flight and failure states
@@ -312,6 +312,7 @@ function PageContent() {
312
312
 
313
313
  ## Cross-reference
314
314
 
315
+ - [`knowledge/patterns/async-control.md`](async-control.md) — the in-flight state before the error: pending, retry-on-timeout, double-submit
315
316
  - [`knowledge/patterns/empty-states.md`](empty-states.md) — when there's no data, but it's not an error
316
317
  - [`knowledge/patterns/form-design.md`](form-design.md) — validation errors at the field level
317
318
  - [`knowledge/i18n/korean-payments.md`](../i18n/korean-payments.md) — payment-specific error messages
@@ -232,6 +232,8 @@ Required affordances:
232
232
 
233
233
  ## Cross-reference
234
234
 
235
+ - [`knowledge/patterns/async-control.md`](async-control.md) — submit-in-flight, double-submit prevention, pending button states
236
+
235
237
  - [knowledge/patterns/ux-guidelines.md](ux-guidelines.md) — broader UX rules
236
238
  - [knowledge/i18n/korean-product-conventions.md](../i18n/korean-product-conventions.md) — Korean form expectations (phone, address, terms)
237
239
  - [knowledge/a11y/keyboard-and-focus.md](../a11y/keyboard-and-focus.md) — keyboard navigation contract
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design-ai/cli",
3
- "version": "4.57.0",
3
+ "version": "4.58.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",
@@ -4785,7 +4785,7 @@ def assert_learning_query_human(raw: str, *, context: str, cmd: list[str]) -> No
4785
4785
  "Limit: 2",
4786
4786
  "Explain: selection score, matched tokens, and reason",
4787
4787
  "[accessibility] Prioritize keyboard accessibility details for Button component API specs",
4788
- "matched keyboard, accessibility",
4788
+ "matched accessibility, keyboard",
4789
4789
  "reason brief-match",
4790
4790
  ):
4791
4791
  require_registry_smoke(
@@ -8378,7 +8378,7 @@ def run_self_test() -> None:
8378
8378
  "",
8379
8379
  "1. [accessibility] Prioritize keyboard accessibility details for Button component API specs",
8380
8380
  " learn-relevant · 2026-05-22T00:00:01.000Z",
8381
- " score 4 · matched keyboard, accessibility · reason brief-match",
8381
+ " score 2.114533 · matched accessibility, keyboard · reason brief-match",
8382
8382
  ]),
8383
8383
  context="registry smoke self-test",
8384
8384
  cmd=learn_query_human_cmd,
@@ -8392,7 +8392,7 @@ def run_self_test() -> None:
8392
8392
  "Limit: 2",
8393
8393
  "Explain: selection score, matched tokens, and reason",
8394
8394
  "[accessibility] Prioritize keyboard accessibility details for Button component API specs",
8395
- "matched keyboard, accessibility",
8395
+ "matched accessibility, keyboard",
8396
8396
  ]),
8397
8397
  context="registry smoke self-test",
8398
8398
  cmd=learn_query_human_cmd,
@@ -2246,6 +2246,7 @@ def passing_route_explain_human_output() -> str:
2246
2246
  f" agent: ✓ {EXPECTED_ROUTE_AGENT}",
2247
2247
  " read: ✓ knowledge/PRINCIPLES.md",
2248
2248
  f" read: ✓ {EXPECTED_ROUTE_KNOWLEDGE}",
2249
+ " related: knowledge/patterns/design-system-qa.md (score 10.13)",
2249
2250
  "",
2250
2251
  ])
2251
2252
 
@@ -3714,6 +3715,8 @@ def assert_route_explain_human_output(raw: str, *, context: str, cmd: list[str])
3714
3715
  f"agent: ✓ {EXPECTED_ROUTE_AGENT}",
3715
3716
  "read: ✓ knowledge/PRINCIPLES.md",
3716
3717
  f"read: ✓ {EXPECTED_ROUTE_KNOWLEDGE}",
3718
+ # Advisory related-knowledge recall is surfaced only under --explain.
3719
+ "related: knowledge/",
3717
3720
  ),
3718
3721
  context=context,
3719
3722
  label="route explain human output",