@askewly/design 0.4.5 → 0.5.1

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/dist/data.js ADDED
@@ -0,0 +1,90 @@
1
+ // M110 앞단 — 레포 자산 조회 통합 (`data recall`).
2
+ // design-intake.md §5단계와 askewly-design SKILL §2.7 이 약속해 온 `askewly data recall`
3
+ // 이 유령 명령이었다(이 기기 어디에도 `data` 커맨드가 없었다) — 그 약속을 실재하게 만든다.
4
+ //
5
+ // 번들된 두 축(terms · recipes) + knowledge 축을 한 번에 훑는다. methodology/ 는
6
+ // build-data.mjs 가 애초에 번들하지 않으므로(knowledge/*.md 만 읽는다) 여기서도 다루지
7
+ // 않는다 — 새 데이터 로더를 만들지 않고 기존 load.ts 만 재사용한다는 이 step 의 원칙.
8
+ // methodology 를 포함한 5축 통합 회수가 필요하면 `python ~/projects/second-brain/bin/data.py recall`
9
+ // 로 보완한다(design-intake.md 가 그 경계를 함께 적는다).
10
+ import { loadKnowledge, loadRecipes, loadTerms, searchKnowledge, searchTerms, } from "./load.js";
11
+ export const RECALL_AXES = ["terms", "recipes", "knowledge"];
12
+ function summarize(text, max = 120) {
13
+ if (!text)
14
+ return "";
15
+ const oneLine = text.replace(/\s+/g, " ").trim();
16
+ return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
17
+ }
18
+ /** Case-insensitive substring search over id, name, pattern_group, body (low weight). */
19
+ function searchRecipes(recipes, query) {
20
+ const needle = query.trim().toLowerCase();
21
+ if (!needle)
22
+ return [];
23
+ const scored = [];
24
+ for (const recipe of recipes) {
25
+ const id = recipe.id.toLowerCase();
26
+ const name = (recipe.name ?? "").toLowerCase();
27
+ const group = (recipe.pattern_group ?? "").toLowerCase();
28
+ const body = recipe.body?.toLowerCase() ?? "";
29
+ let score = 0;
30
+ if (id === needle || name === needle)
31
+ score = 100;
32
+ else if (id.includes(needle) || name.includes(needle))
33
+ score = 60;
34
+ else if (group.includes(needle))
35
+ score = 40;
36
+ else if (body.includes(needle))
37
+ score = 20;
38
+ if (score > 0)
39
+ scored.push({ recipe, score });
40
+ }
41
+ return scored.sort((a, b) => b.score - a.score || a.recipe.id.localeCompare(b.recipe.id)).map((s) => s.recipe);
42
+ }
43
+ function termHit(term) {
44
+ return {
45
+ axis: "terms",
46
+ id: term.id,
47
+ path: `docs/ui-vocabulary/terms.yml#${term.id}`,
48
+ summary: summarize(term.one_liner ?? term.en?.name ?? term.id),
49
+ };
50
+ }
51
+ function recipeHit(recipe) {
52
+ return {
53
+ axis: "recipes",
54
+ id: recipe.id,
55
+ path: recipe.source_path,
56
+ summary: summarize(`${recipe.name ? `${recipe.name} (${recipe.pattern_group ?? "-"})` : recipe.id}${recipe.status ? ` [${recipe.status}]` : ""}`),
57
+ status: recipe.status,
58
+ };
59
+ }
60
+ function knowledgeHit(doc) {
61
+ return {
62
+ axis: "knowledge",
63
+ id: doc.id,
64
+ path: doc.source_path,
65
+ summary: summarize(doc.description),
66
+ };
67
+ }
68
+ /**
69
+ * Search terms + recipes + knowledge in one pass and return path + one-line
70
+ * summary per hit. This is an index, not the source text — the caller must
71
+ * still open `path` and read the body before judging.
72
+ */
73
+ export function recall(query, limit) {
74
+ const terms = searchTerms(loadTerms(), query).map(termHit);
75
+ const recipes = searchRecipes(loadRecipes(), query).map(recipeHit);
76
+ const knowledge = searchKnowledge(loadKnowledge(), query).map(knowledgeHit);
77
+ // Interleave by axis (round-robin) so a query that happens to match many
78
+ // terms doesn't crowd recipes/knowledge out of a small --limit window.
79
+ const merged = [];
80
+ const maxLen = Math.max(terms.length, recipes.length, knowledge.length);
81
+ for (let i = 0; i < maxLen; i++) {
82
+ if (terms[i])
83
+ merged.push(terms[i]);
84
+ if (recipes[i])
85
+ merged.push(recipes[i]);
86
+ if (knowledge[i])
87
+ merged.push(knowledge[i]);
88
+ }
89
+ return merged.slice(0, Math.max(0, limit));
90
+ }
package/dist/index.js CHANGED
@@ -5,8 +5,10 @@ import { Command } from "commander";
5
5
  import { addRecipe, initProject, InjectError } from "./inject.js";
6
6
  import { runKickstart, KickstartError } from "./kickstart.js";
7
7
  import { DEFAULT_TYPOGRAPHY_THRESHOLD } from "./typography.js";
8
+ import { MASCOT_POSES, renderMascot } from "./mascot.js";
8
9
  import { verifyDir } from "./verify.js";
9
10
  import { loadKnowledge, loadRecipes, loadTerms, loadTokens, loadTokensCss, searchKnowledge, searchTerms, tokensForTier, } from "./load.js";
11
+ import { recall, RECALL_AXES } from "./data.js";
10
12
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
11
13
  const program = new Command();
12
14
  program
@@ -316,4 +318,49 @@ recipes
316
318
  console.log("");
317
319
  console.log(recipe.body);
318
320
  });
321
+ // 어스큐리 수달 — 브랜드 마스코트. 사람 눈 전용 표면이라 --json 이 없다(기계 소비 출력에
322
+ // 캐릭터를 섞지 않는다 — docs/design-system/mascot.md §3). 색은 TTY 에서만 기본 ON.
323
+ program
324
+ .command("mascot")
325
+ .argument("[pose]", `pose: ${MASCOT_POSES.join(" | ")}`, "lying")
326
+ .option("--no-color", "print without ANSI color (piped output is plain by default)")
327
+ .description("print the Askewly otter mascot (lavender half-block pixel art)")
328
+ .action((pose, options) => {
329
+ if (!MASCOT_POSES.includes(pose)) {
330
+ fail(`unknown pose "${pose}" — expected one of: ${MASCOT_POSES.join(", ")}`);
331
+ }
332
+ const color = options.color && Boolean(process.stdout.isTTY);
333
+ console.log(renderMascot(pose, { color }));
334
+ });
335
+ const data = program
336
+ .command("data")
337
+ .description("query bundled repo assets across axes (terms, recipes, knowledge) in one pass");
338
+ data
339
+ .command("recall")
340
+ .argument("<query>", "text to match across terms / recipes / knowledge")
341
+ .option("--limit <n>", "max results", "5")
342
+ .option("--json", "machine-readable output")
343
+ .description("search terms + recipes + knowledge in one pass — an index of paths, not the source text")
344
+ .action((query, options) => {
345
+ const limit = Number(options.limit) || 5;
346
+ const hits = recall(query, limit);
347
+ if (options.json) {
348
+ console.log(JSON.stringify({
349
+ note: "index only — read `path` before judging",
350
+ axes: RECALL_AXES,
351
+ query,
352
+ hits,
353
+ }, null, 2));
354
+ return;
355
+ }
356
+ console.log("색인이지 본문이 아니다 — 후보 경로를 받은 뒤 판단은 본문을 읽고 한다.");
357
+ if (hits.length === 0) {
358
+ console.log(`no matches for "${query}" across axes: ${RECALL_AXES.join(", ")} (methodology/ is not bundled here — try: python ~/projects/second-brain/bin/data.py recall "${query}")`);
359
+ return;
360
+ }
361
+ for (const hit of hits) {
362
+ console.log(`[${hit.axis}] ${hit.id} — ${hit.path}`);
363
+ console.log(` ${hit.summary}`);
364
+ }
365
+ });
319
366
  program.parse();
package/dist/inject.js CHANGED
@@ -45,6 +45,19 @@ export function extractCodeSnippet(recipe) {
45
45
  if (recipe.code_source) {
46
46
  return recipe.code_source.trimEnd() + "\n";
47
47
  }
48
+ // 영상 자산은 코드가 **있는데도** 배급하지 않는다 — Remotion 런타임 위에서만 돌아서,
49
+ // 여기서 써 주면 remotion 없는 프로젝트가 설치 직후 깨진다. 「없다」가 아니라 「여기로
50
+ // 가라」로 답한다 (M95 step-8 · docs/design-system/video-contract.md §6).
51
+ if (recipe.surface_refs?.includes("video")) {
52
+ throw new InjectError([
53
+ `recipe "${recipe.id}" is a video asset — not distributed through add.`,
54
+ " It runs only inside Remotion (useCurrentFrame), so writing it here would break this project.",
55
+ " Read the recipe and the element source, then rebuild it in your own Remotion project:",
56
+ ` https://ui.askewly.com/llms/${recipe.source_path}`,
57
+ ...(recipe.code_asset ? [` https://ui.askewly.com/llms/${recipe.code_asset}`] : []),
58
+ " Medium contract: https://ui.askewly.com/llms/docs/design-system/video-contract.md",
59
+ ].join("\n"));
60
+ }
48
61
  throw new InjectError(`recipe "${recipe.id}" has no fenced code block under ## Code`);
49
62
  }
50
63
  /** Extracts list items of a `## <title>` section (used for Checks / Anti-patterns notes). */
package/dist/mascot.js ADDED
@@ -0,0 +1,43 @@
1
+ // 어스큐리 수달 — Askewly 브랜드 마스코트 (M83, 2026-08-28).
2
+ // 정본은 레포 docs/design-system/mascot.md + mascot/*.map.txt (픽셀맵). 아래 문자열은
3
+ // 파생 사본이다 — 손편집 금지, 맵을 고치고 재렌더해서 옮긴다.
4
+ // 사용 규칙(정본 §3): 라벤더 #B298DC 단색 고정 · 등폭 전제 · 기계 소비 출력 금지.
5
+ export const MASCOT_POSES = ["lying", "standing", "sleepy", "mini"];
6
+ const ART = {
7
+ lying: ` ▄██▄ ▄██▄
8
+ █ ██ █ ██
9
+ ▄██████████████████▄
10
+ █▄ █████████████████████
11
+ ▀█▄ ████ ████ █████████
12
+ ▀██▄████ ████ █████████
13
+ ▀███████████████████▀
14
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀`,
15
+ standing: ` ▄██▄ ▄██▄
16
+ █ ██ █ ██
17
+ ▄██████████████▄
18
+ ███ ████ █████
19
+ ███ ████ █████
20
+ ████████████████
21
+ ████████████████
22
+ ▀████▀▀▀████▀▀██
23
+ ████ ████ ▀███▄▄▄`,
24
+ sleepy: ` ▄██▄ ▄██▄
25
+ █ ██ █ ██
26
+ ▄██████████████████▄
27
+ █▄ █████████████████████
28
+ ▀█▄ █████████████████████
29
+ ▀██▄███ ██ ████████
30
+ ▀███████████████████▀
31
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀`,
32
+ mini: ` ▄██▄▄▄▄██▄
33
+ ██████████
34
+ ██ ██ ██
35
+ ▀████████▀`,
36
+ };
37
+ /** askewly.lavender #B298DC — ANSI 24-bit. */
38
+ const LAVENDER = "";
39
+ const RESET = "";
40
+ export function renderMascot(pose, options) {
41
+ const art = ART[pose];
42
+ return options.color ? `${LAVENDER}${art}${RESET}` : art;
43
+ }
package/package.json CHANGED
@@ -1,50 +1,50 @@
1
- {
2
- "name": "@askewly/design",
3
- "version": "0.4.5",
4
- "description": "Query, inject, and verify the Askewly Design system (tokens, UI vocabulary, recipes) from your terminal or coding agent.",
5
- "license": "MIT",
6
- "homepage": "https://ui.askewly.com",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/lumatic2/ui-dictionary.git",
10
- "directory": "packages/cli"
11
- },
12
- "keywords": [
13
- "design-system",
14
- "design-tokens",
15
- "linter",
16
- "ui",
17
- "coding-agent"
18
- ],
19
- "type": "module",
20
- "bin": {
21
- "askewly-design": "./dist/index.js"
22
- },
23
- "files": [
24
- "dist",
25
- "data",
26
- "README.md",
27
- "LICENSE"
28
- ],
29
- "publishConfig": {
30
- "access": "public"
31
- },
32
- "engines": {
33
- "node": ">=18"
34
- },
35
- "scripts": {
36
- "build:data": "node scripts/build-data.mjs",
37
- "build": "npm run build:data && tsc",
38
- "test": "vitest run",
39
- "prepack": "npm run build"
40
- },
41
- "dependencies": {
42
- "commander": "^13.1.0"
43
- },
44
- "devDependencies": {
45
- "@types/node": "^22.10.0",
46
- "typescript": "^5.7.0",
47
- "vitest": "^3.0.0",
48
- "yaml": "^2.6.0"
49
- }
50
- }
1
+ {
2
+ "name": "@askewly/design",
3
+ "version": "0.5.1",
4
+ "description": "Query, inject, and verify the Askewly Design system (tokens, UI vocabulary, recipes) from your terminal or coding agent.",
5
+ "license": "MIT",
6
+ "homepage": "https://ui.askewly.com",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/lumatic2/ui-dictionary.git",
10
+ "directory": "packages/cli"
11
+ },
12
+ "keywords": [
13
+ "design-system",
14
+ "design-tokens",
15
+ "linter",
16
+ "ui",
17
+ "coding-agent"
18
+ ],
19
+ "type": "module",
20
+ "bin": {
21
+ "askewly-design": "./dist/index.js"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "data",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "scripts": {
36
+ "build:data": "node scripts/build-data.mjs",
37
+ "build": "npm run build:data && tsc",
38
+ "test": "vitest run",
39
+ "prepack": "npm run build"
40
+ },
41
+ "dependencies": {
42
+ "commander": "^13.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^22.10.0",
46
+ "typescript": "^5.7.0",
47
+ "vitest": "^3.0.0",
48
+ "yaml": "^2.6.0"
49
+ }
50
+ }