@mandujs/core 0.52.0 → 0.53.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/package.json +38 -2
- package/src/a11y/run-audit.ts +6 -6
- package/src/brain/adapters/oauth-flow.ts +1 -1
- package/src/brain/architecture/analyzer.ts +0 -2
- package/src/brain/credentials.ts +2 -2
- package/src/bundler/__tests__/build-runner.ts +13 -1
- package/src/bundler/__tests__/extended-watch.test.ts +3 -2
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -12
- package/src/bundler/build.test.ts +30 -33
- package/src/bundler/build.ts +2 -1
- package/src/bundler/dev.ts +23 -22
- package/src/bundler/prerender.ts +63 -0
- package/src/bundler/safe-build.test.ts +2 -1
- package/src/bundler/types.ts +5 -3
- package/src/change/history.ts +1 -1
- package/src/change/snapshot.ts +18 -9
- package/src/client/Link.tsx +9 -9
- package/src/client/globals.ts +3 -2
- package/src/client/hooks.ts +0 -4
- package/src/client/hydrate.ts +1 -1
- package/src/client/router.ts +12 -12
- package/src/client/rpc.ts +1 -1
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-sse.ts +5 -5
- package/src/config/mcp-status.ts +0 -1
- package/src/config/validate.ts +1 -1
- package/src/config/watcher.ts +1 -1
- package/src/content/collection.ts +8 -12
- package/src/content/content-layer.ts +5 -5
- package/src/content/data-store.ts +2 -2
- package/src/content/loader-context.ts +0 -1
- package/src/content/loaders/file.ts +1 -1
- package/src/content/loaders/glob.ts +4 -4
- package/src/content/loaders/types.ts +1 -1
- package/src/content/meta-store.ts +1 -1
- package/src/content/types.ts +1 -1
- package/src/contract/define.ts +11 -12
- package/src/contract/index.ts +3 -3
- package/src/contract/protection.ts +2 -2
- package/src/contract/types.ts +1 -1
- package/src/contract/validator.ts +0 -1
- package/src/db/__tests__/db.test.ts +4 -7
- package/src/db/migrations/index.ts +3 -0
- package/src/db/migrations/lock.ts +11 -11
- package/src/deploy/cache.ts +1 -0
- package/src/design/__tests__/agents-link.test.ts +109 -0
- package/src/design/__tests__/lint.test.ts +110 -0
- package/src/design/agents-link.ts +165 -0
- package/src/design/extract.ts +2 -1
- package/src/design/index.ts +16 -0
- package/src/design/lint.ts +209 -0
- package/src/design/patch.ts +0 -1
- package/src/devtools/ai/mcp-connector.ts +24 -24
- package/src/devtools/client/components/kitchen-root.tsx +1 -1
- package/src/devtools/client/components/panel/errors-panel.tsx +1 -1
- package/src/devtools/client/state-manager.ts +1 -1
- package/src/devtools/init.ts +2 -3
- package/src/devtools/server/source-context.ts +1 -1
- package/src/devtools/worker/redaction-worker.ts +8 -4
- package/src/devtools/worker/worker-manager.ts +4 -3
- package/src/error/classifier.ts +1 -1
- package/src/error/domains.ts +1 -1
- package/src/filling/sse.ts +5 -5
- package/src/generator/generate.ts +2 -2
- package/src/guard/analyzer.ts +1 -1
- package/src/guard/ast-analyzer.ts +7 -7
- package/src/guard/auto-correct.ts +2 -2
- package/src/guard/config-guard.ts +0 -2
- package/src/guard/contract-guard.ts +1 -1
- package/src/guard/decision-memory.ts +2 -2
- package/src/guard/design-inline-class.ts +2 -1
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -41
- package/src/guard/negotiation.ts +3 -3
- package/src/guard/reporter.ts +1 -4
- package/src/guard/semantic-slots.ts +2 -2
- package/src/guard/suggestions.ts +2 -2
- package/src/guard/watcher.ts +5 -4
- package/src/index.ts +5 -5
- package/src/intent/index.ts +1 -1
- package/src/kitchen/api/contract-api.ts +1 -1
- package/src/kitchen/api/file-api.ts +3 -3
- package/src/kitchen/kitchen-handler.ts +29 -21
- package/src/lockfile/validate.ts +0 -1
- package/src/openapi/generator.ts +2 -1
- package/src/openapi/openapi.test.ts +43 -14
- package/src/perf/user-marks.ts +1 -1
- package/src/plugins/registry.ts +1 -2
- package/src/plugins/types.ts +1 -1
- package/src/resource/ddl/diff.ts +0 -1
- package/src/resource/generator.ts +1 -1
- package/src/resource/generators/contract.ts +1 -1
- package/src/resource/schema.ts +2 -1
- package/src/router/fs-routes.ts +6 -6
- package/src/router/fs-scanner.ts +1 -1
- package/src/runtime/env.ts +1 -1
- package/src/runtime/image-handler.ts +2 -1
- package/src/runtime/middleware.ts +2 -1
- package/src/runtime/server.ts +67 -60
- package/src/runtime/ssr.ts +3 -3
- package/src/runtime/streaming-ssr.ts +25 -27
- package/src/scheduler/validate.ts +1 -1
- package/src/seo/integration/ssr.ts +2 -3
- package/src/seo/render/opengraph.ts +1 -1
- package/src/seo/resolve/index.ts +0 -4
- package/src/slot/corrector.ts +1 -1
- package/src/slot/validator.ts +4 -4
- package/src/watcher/reporter.ts +1 -1
- package/src/watcher/watcher.ts +19 -19
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #245 M5 — DESIGN.md lint tests.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from "bun:test";
|
|
6
|
+
import { lintDesignSpec } from "../lint";
|
|
7
|
+
import { parseDesignMd } from "../parser";
|
|
8
|
+
|
|
9
|
+
describe("lintDesignSpec — color-palette", () => {
|
|
10
|
+
it("warns when parser couldn't extract a value (rule: color-missing-value)", () => {
|
|
11
|
+
// Parser drops non-hex/rgb values during extraction, so the
|
|
12
|
+
// resulting token has no `.value` — the linter flags that.
|
|
13
|
+
const r = lintDesignSpec(parseDesignMd(`# T
|
|
14
|
+
## Color Palette
|
|
15
|
+
- Primary — #FF8C42 — brand
|
|
16
|
+
- Bad — notahex — wrong
|
|
17
|
+
`));
|
|
18
|
+
expect(
|
|
19
|
+
r.issues.find((i) => i.rule === "color-missing-value" && i.name === "Bad"),
|
|
20
|
+
).toBeDefined();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("warns on missing color value", () => {
|
|
24
|
+
const r = lintDesignSpec(parseDesignMd(`# T
|
|
25
|
+
## Color Palette
|
|
26
|
+
- Primary — see docs
|
|
27
|
+
`));
|
|
28
|
+
expect(r.issues.find((i) => i.rule === "color-missing-value")?.severity).toBe("warning");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("warns on slug collision", () => {
|
|
32
|
+
const r = lintDesignSpec(parseDesignMd(`# T
|
|
33
|
+
## Color Palette
|
|
34
|
+
- Primary — #FF0000
|
|
35
|
+
- primary — #00FF00
|
|
36
|
+
`));
|
|
37
|
+
expect(r.issues.find((i) => i.rule === "color-slug-collision")).toBeDefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("notes duplicate values across distinct names (info)", () => {
|
|
41
|
+
const r = lintDesignSpec(parseDesignMd(`# T
|
|
42
|
+
## Color Palette
|
|
43
|
+
- Brand — #FF8C42 — main
|
|
44
|
+
- Accent — #FF8C42 — secondary
|
|
45
|
+
`));
|
|
46
|
+
const dup = r.issues.find((i) => i.rule === "color-duplicate-value");
|
|
47
|
+
expect(dup?.severity).toBe("info");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("lintDesignSpec — typography / layout / shadows / components", () => {
|
|
52
|
+
it("warns on a typography token with neither family nor size", () => {
|
|
53
|
+
const r = lintDesignSpec(parseDesignMd(`# T
|
|
54
|
+
## Typography
|
|
55
|
+
- body: weight: 400
|
|
56
|
+
`));
|
|
57
|
+
expect(r.issues.find((i) => i.rule === "typography-empty-token")).toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("warns on layout slug collision", () => {
|
|
61
|
+
const r = lintDesignSpec(parseDesignMd(`# T
|
|
62
|
+
## Layout
|
|
63
|
+
- sm: 0.5rem
|
|
64
|
+
- SM: 0.5rem
|
|
65
|
+
`));
|
|
66
|
+
expect(r.issues.find((i) => i.rule === "spacing-slug-collision")).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("warns on shadow slug collision", () => {
|
|
70
|
+
const r = lintDesignSpec(parseDesignMd(`# T
|
|
71
|
+
## Depth & Elevation
|
|
72
|
+
- card — 0 1px 2px rgba(0,0,0,0.1)
|
|
73
|
+
- Card — 0 2px 4px rgba(0,0,0,0.2)
|
|
74
|
+
`));
|
|
75
|
+
expect(r.issues.find((i) => i.rule === "shadow-slug-collision")).toBeDefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("warns on duplicate component H3 names", () => {
|
|
79
|
+
const r = lintDesignSpec(parseDesignMd(`# T
|
|
80
|
+
## Components
|
|
81
|
+
|
|
82
|
+
### Button
|
|
83
|
+
|
|
84
|
+
### button
|
|
85
|
+
`));
|
|
86
|
+
expect(r.issues.find((i) => i.rule === "component-duplicate")).toBeDefined();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("lintDesignSpec — clean DESIGN.md", () => {
|
|
91
|
+
it("returns ok with empty issues for a well-formed spec", () => {
|
|
92
|
+
const r = lintDesignSpec(parseDesignMd(`# T
|
|
93
|
+
## Color Palette
|
|
94
|
+
- Primary — #FF8C42 — brand
|
|
95
|
+
- Accent — #1A1F36 — text
|
|
96
|
+
|
|
97
|
+
## Typography
|
|
98
|
+
- body: font-family: Inter, sans-serif; size: 16px
|
|
99
|
+
|
|
100
|
+
## Layout
|
|
101
|
+
- sm: 0.5rem
|
|
102
|
+
- md: 1rem
|
|
103
|
+
|
|
104
|
+
## Depth & Elevation
|
|
105
|
+
- card — 0 1px 3px rgba(0,0,0,0.1)
|
|
106
|
+
`));
|
|
107
|
+
expect(r.issues).toEqual([]);
|
|
108
|
+
expect(r.ok).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AGENTS.md / CLAUDE.md ↔ DESIGN.md linker (Issue #245 M5).
|
|
3
|
+
*
|
|
4
|
+
* Drops a markered `## Design System` section into the project's
|
|
5
|
+
* agent guide files so coding agents read DESIGN.md and use the
|
|
6
|
+
* Mandu MCP tools (M4) before touching UI. Idempotent — running it
|
|
7
|
+
* twice never duplicates the section.
|
|
8
|
+
*
|
|
9
|
+
* The linker writes the same payload to whichever of `AGENTS.md` /
|
|
10
|
+
* `CLAUDE.md` exists. When neither exists in `force: true` mode it
|
|
11
|
+
* creates `AGENTS.md` (the open standard) seeded with just the
|
|
12
|
+
* design block — agents that consume `CLAUDE.md` follow the
|
|
13
|
+
* cross-reference to `AGENTS.md` per Anthropic's convention.
|
|
14
|
+
*
|
|
15
|
+
* The injected section:
|
|
16
|
+
*
|
|
17
|
+
* - Names DESIGN.md as the canonical design source.
|
|
18
|
+
* - Lists the 8 MCP tools with one-line descriptions.
|
|
19
|
+
* - Spells out the §3.5 incremental loop as a 5-step prompt agents
|
|
20
|
+
* can follow verbatim.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { promises as fs } from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
|
|
26
|
+
export const DESIGN_LINK_MARKER_START =
|
|
27
|
+
"<!-- @mandu-design-link:start — managed by `mandu design link` / `init --design`, do not edit -->";
|
|
28
|
+
export const DESIGN_LINK_MARKER_END = "<!-- @mandu-design-link:end -->";
|
|
29
|
+
|
|
30
|
+
export interface LinkAgentsOptions {
|
|
31
|
+
/** Project root. */
|
|
32
|
+
rootDir: string;
|
|
33
|
+
/** Filenames to update. Defaults to `["AGENTS.md", "CLAUDE.md"]`. */
|
|
34
|
+
filenames?: readonly string[];
|
|
35
|
+
/**
|
|
36
|
+
* When true, create a fresh `AGENTS.md` containing just the design
|
|
37
|
+
* link block when none of `filenames` exists. Default false — the
|
|
38
|
+
* linker only updates existing files unless explicitly asked.
|
|
39
|
+
*/
|
|
40
|
+
createIfMissing?: boolean;
|
|
41
|
+
/** Override DESIGN.md filename. Defaults to `DESIGN.md`. */
|
|
42
|
+
designFilename?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface LinkAgentsResult {
|
|
46
|
+
files: Array<{
|
|
47
|
+
path: string;
|
|
48
|
+
/** "created" | "inserted" (markered block added) | "updated" (markered block replaced) | "unchanged" */
|
|
49
|
+
action: "created" | "inserted" | "updated" | "unchanged";
|
|
50
|
+
}>;
|
|
51
|
+
/** True when at least one file was written. */
|
|
52
|
+
changed: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Generate the markered block payload. Pure — used by tests too. */
|
|
56
|
+
export function buildAgentsDesignBlock(designFilename: string = "DESIGN.md"): string {
|
|
57
|
+
const lines = [
|
|
58
|
+
DESIGN_LINK_MARKER_START,
|
|
59
|
+
"",
|
|
60
|
+
"## Design System",
|
|
61
|
+
"",
|
|
62
|
+
`This project uses **${designFilename}** as the single source of truth for visual design (colors, typography, spacing, shadows, components, agent prompts).`,
|
|
63
|
+
"Mandu's MCP tools expose every part of it without grepping the codebase. Agents MUST read DESIGN.md *before* writing or editing UI.",
|
|
64
|
+
"",
|
|
65
|
+
"### Tools (call before / during UI work)",
|
|
66
|
+
"",
|
|
67
|
+
"| Phase | Tool | Use it for |",
|
|
68
|
+
"|---|---|---|",
|
|
69
|
+
"| Read | `mandu.design.get` | Section-by-section DESIGN.md (or `'all'` for full spec). |",
|
|
70
|
+
"| Read | `mandu.design.prompt` | §9 Agent Prompts — pre-warm context every session. |",
|
|
71
|
+
"| Read | `mandu.component.list` | Existing components in `src/client/shared/ui/` + `widgets/`. Don't re-implement. |",
|
|
72
|
+
"| Check | `mandu.design.check` | Lint a file BEFORE editing — surfaces forbidden inline classes. |",
|
|
73
|
+
"| Discover | `mandu.design.extract` | Find token candidates the project uses but DESIGN.md doesn't list. |",
|
|
74
|
+
"| Patch | `mandu.design.propose` | One-call: extract → dry-run patch → user reviews diff. |",
|
|
75
|
+
"| Patch | `mandu.design.patch` | Section-safe add/update/remove. Defaults to `dry_run: true`. |",
|
|
76
|
+
"| Sync | `mandu.design.diff_upstream` | Compare against awesome-design-md slugs (e.g. `'stripe'`). |",
|
|
77
|
+
"",
|
|
78
|
+
"### 5-step UI workflow",
|
|
79
|
+
"",
|
|
80
|
+
"1. **Pre-warm.** Call `mandu.design.prompt` (and `mandu.design.get` for the section you'll touch) so your edit honours existing tokens.",
|
|
81
|
+
"2. **Inventory.** Call `mandu.component.list` for the matching category. Re-use first; only add when nothing fits.",
|
|
82
|
+
"3. **Check.** Before editing a file, call `mandu.design.check { file }` to see if the file already violates DESIGN.md §7.",
|
|
83
|
+
"4. **Edit.** Write the change. Use existing tokens from §2 / components from §1.",
|
|
84
|
+
"5. **Propose tokens.** If you introduced a new color/font/spacing pattern, call `mandu.design.propose` and ask the user to apply the patch (default `dry_run: true`).",
|
|
85
|
+
"",
|
|
86
|
+
"### Hard rules",
|
|
87
|
+
"",
|
|
88
|
+
"- Never invent colors / fonts / spacing values without a `mandu.design.propose` round.",
|
|
89
|
+
"- Never bypass `mandu.design.check` on a file you're about to edit.",
|
|
90
|
+
"- DESIGN.md is the spec — when in doubt, update DESIGN.md *first*, then write code.",
|
|
91
|
+
"",
|
|
92
|
+
DESIGN_LINK_MARKER_END,
|
|
93
|
+
"",
|
|
94
|
+
];
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Update agent guide files to reference DESIGN.md and the MCP tools.
|
|
100
|
+
*
|
|
101
|
+
* Behaviour per file:
|
|
102
|
+
* - Marker present → replace the markered region (idempotent).
|
|
103
|
+
* - File exists, no marker → append the block at the end.
|
|
104
|
+
* - File missing → skip unless `createIfMissing: true`.
|
|
105
|
+
*/
|
|
106
|
+
export async function linkAgentsToDesignMd(
|
|
107
|
+
options: LinkAgentsOptions,
|
|
108
|
+
): Promise<LinkAgentsResult> {
|
|
109
|
+
const filenames = options.filenames ?? ["AGENTS.md", "CLAUDE.md"];
|
|
110
|
+
const block = buildAgentsDesignBlock(options.designFilename);
|
|
111
|
+
const files: LinkAgentsResult["files"] = [];
|
|
112
|
+
|
|
113
|
+
let anyExists = false;
|
|
114
|
+
for (const name of filenames) {
|
|
115
|
+
const full = path.join(options.rootDir, name);
|
|
116
|
+
let existing: string | null;
|
|
117
|
+
try {
|
|
118
|
+
existing = await fs.readFile(full, "utf8");
|
|
119
|
+
anyExists = true;
|
|
120
|
+
} catch {
|
|
121
|
+
existing = null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (existing === null) {
|
|
125
|
+
files.push({ path: full, action: "unchanged" });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const startIdx = existing.indexOf(DESIGN_LINK_MARKER_START);
|
|
130
|
+
const endIdx = existing.indexOf(DESIGN_LINK_MARKER_END);
|
|
131
|
+
let next: string;
|
|
132
|
+
let action: LinkAgentsResult["files"][number]["action"];
|
|
133
|
+
if (startIdx >= 0 && endIdx > startIdx) {
|
|
134
|
+
const before = existing.slice(0, startIdx);
|
|
135
|
+
const after = existing.slice(endIdx + DESIGN_LINK_MARKER_END.length);
|
|
136
|
+
const replacement = block.replace(/\n+$/, "");
|
|
137
|
+
next = `${before}${replacement}${after}`;
|
|
138
|
+
action = next === existing ? "unchanged" : "updated";
|
|
139
|
+
} else {
|
|
140
|
+
const sep = existing.endsWith("\n") ? "" : "\n";
|
|
141
|
+
next = `${existing}${sep}\n${block}`;
|
|
142
|
+
action = "inserted";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (action !== "unchanged") {
|
|
146
|
+
await fs.writeFile(full, next, "utf8");
|
|
147
|
+
}
|
|
148
|
+
files.push({ path: full, action });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!anyExists && options.createIfMissing) {
|
|
152
|
+
const target = path.join(options.rootDir, filenames[0] ?? "AGENTS.md");
|
|
153
|
+
const seed = `# Project Agent Guide\n\n${block}`;
|
|
154
|
+
await fs.writeFile(target, seed, "utf8");
|
|
155
|
+
// Replace the placeholder "unchanged" entry with the real outcome.
|
|
156
|
+
const idx = files.findIndex((f) => f.path === target);
|
|
157
|
+
if (idx >= 0) files[idx] = { path: target, action: "created" };
|
|
158
|
+
else files.push({ path: target, action: "created" });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
files,
|
|
163
|
+
changed: files.some((f) => f.action !== "unchanged"),
|
|
164
|
+
};
|
|
165
|
+
}
|
package/src/design/extract.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type * as __ManduNodeFsTypes0 from "node:fs";
|
|
1
2
|
/**
|
|
2
3
|
* DESIGN.md token extractor — scan project source for tokens that
|
|
3
4
|
* should be promoted into DESIGN.md (Issue #245 M4 §3.5 internal loop).
|
|
@@ -155,7 +156,7 @@ const SOURCE_RX = /\.(?:tsx?|jsx?|mdx?|css)$/;
|
|
|
155
156
|
async function collectFiles(root: string): Promise<string[]> {
|
|
156
157
|
const out: string[] = [];
|
|
157
158
|
async function walk(dir: string): Promise<void> {
|
|
158
|
-
let entries:
|
|
159
|
+
let entries: __ManduNodeFsTypes0.Dirent[];
|
|
159
160
|
try {
|
|
160
161
|
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
161
162
|
} catch {
|
package/src/design/index.ts
CHANGED
|
@@ -57,6 +57,22 @@ export {
|
|
|
57
57
|
type DiffEntry,
|
|
58
58
|
} from "./diff";
|
|
59
59
|
|
|
60
|
+
export {
|
|
61
|
+
buildAgentsDesignBlock,
|
|
62
|
+
linkAgentsToDesignMd,
|
|
63
|
+
DESIGN_LINK_MARKER_START,
|
|
64
|
+
DESIGN_LINK_MARKER_END,
|
|
65
|
+
type LinkAgentsOptions,
|
|
66
|
+
type LinkAgentsResult,
|
|
67
|
+
} from "./agents-link";
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
lintDesignSpec,
|
|
71
|
+
type LintIssue,
|
|
72
|
+
type LintResult,
|
|
73
|
+
type LintSeverity,
|
|
74
|
+
} from "./lint";
|
|
75
|
+
|
|
60
76
|
export type {
|
|
61
77
|
AgentPrompt,
|
|
62
78
|
AgentPromptsSection,
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DESIGN.md self-consistency lint (Issue #245 M5).
|
|
3
|
+
*
|
|
4
|
+
* Different from `validateDesignSpec` (M1), which reports section
|
|
5
|
+
* presence / shape. The linter checks the *content* for issues that
|
|
6
|
+
* compile fine but waste reader time:
|
|
7
|
+
*
|
|
8
|
+
* - color-palette: hex values that aren't 3/6/8 digits, duplicate
|
|
9
|
+
* hex values across distinct names, slug collisions across names
|
|
10
|
+
* - typography: tokens missing both `fontFamily` and `size`,
|
|
11
|
+
* duplicate display-name slugs
|
|
12
|
+
* - layout / shadows: tokens with no value, duplicate slugs
|
|
13
|
+
* - components: duplicate H3 names
|
|
14
|
+
* - dos-donts: rules under §7 with no `do` / `don't` mode marker
|
|
15
|
+
*
|
|
16
|
+
* The linter is conservative — every rule is opt-out via severity
|
|
17
|
+
* filtering at the call site. The tool returns `{ ok, issues[] }`
|
|
18
|
+
* so CLIs can produce exit codes and MCP tools can stream the issue
|
|
19
|
+
* list.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { slugifyTokenName } from "./tailwind-theme";
|
|
23
|
+
import type { DesignSpec } from "./types";
|
|
24
|
+
|
|
25
|
+
export type LintSeverity = "error" | "warning" | "info";
|
|
26
|
+
|
|
27
|
+
export interface LintIssue {
|
|
28
|
+
rule: string;
|
|
29
|
+
section:
|
|
30
|
+
| "color-palette"
|
|
31
|
+
| "typography"
|
|
32
|
+
| "layout"
|
|
33
|
+
| "shadows"
|
|
34
|
+
| "components"
|
|
35
|
+
| "dos-donts"
|
|
36
|
+
| "agent-prompts"
|
|
37
|
+
| "responsive";
|
|
38
|
+
severity: LintSeverity;
|
|
39
|
+
message: string;
|
|
40
|
+
/** Token / row name when the issue is per-row. */
|
|
41
|
+
name?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface LintResult {
|
|
45
|
+
ok: boolean;
|
|
46
|
+
issues: LintIssue[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const HEX_RX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
50
|
+
const COLOR_FN_RX = /^(rgba?|hsla?|oklch|hwb|lab|lch)\(/i;
|
|
51
|
+
|
|
52
|
+
export function lintDesignSpec(spec: DesignSpec): LintResult {
|
|
53
|
+
const issues: LintIssue[] = [];
|
|
54
|
+
|
|
55
|
+
// ─── color-palette ────────────────────────────────────────────────
|
|
56
|
+
const seenColorSlug = new Map<string, string>();
|
|
57
|
+
const seenColorValue = new Map<string, string>();
|
|
58
|
+
for (const token of spec.sections["color-palette"].tokens) {
|
|
59
|
+
if (!token.value) {
|
|
60
|
+
issues.push({
|
|
61
|
+
rule: "color-missing-value",
|
|
62
|
+
section: "color-palette",
|
|
63
|
+
severity: "warning",
|
|
64
|
+
message: `Color "${token.name}" has no parseable value — Tailwind sync will skip it.`,
|
|
65
|
+
name: token.name,
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!HEX_RX.test(token.value) && !COLOR_FN_RX.test(token.value)) {
|
|
70
|
+
issues.push({
|
|
71
|
+
rule: "color-malformed-value",
|
|
72
|
+
section: "color-palette",
|
|
73
|
+
severity: "error",
|
|
74
|
+
message: `Color "${token.name}" value "${token.value}" is not a recognised hex/rgb/hsl/oklch literal.`,
|
|
75
|
+
name: token.name,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const sl = slugifyTokenName(token.name);
|
|
79
|
+
const prevByName = seenColorSlug.get(sl);
|
|
80
|
+
if (prevByName) {
|
|
81
|
+
issues.push({
|
|
82
|
+
rule: "color-slug-collision",
|
|
83
|
+
section: "color-palette",
|
|
84
|
+
severity: "warning",
|
|
85
|
+
message: `Color "${token.name}" slugifies to the same id as "${prevByName}" — Tailwind sync keeps only the first.`,
|
|
86
|
+
name: token.name,
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
seenColorSlug.set(sl, token.name);
|
|
90
|
+
}
|
|
91
|
+
const prevByValue = seenColorValue.get(token.value.toLowerCase());
|
|
92
|
+
if (prevByValue && prevByValue !== token.name) {
|
|
93
|
+
issues.push({
|
|
94
|
+
rule: "color-duplicate-value",
|
|
95
|
+
section: "color-palette",
|
|
96
|
+
severity: "info",
|
|
97
|
+
message: `Color "${token.name}" shares value ${token.value} with "${prevByValue}" — consider a single canonical name.`,
|
|
98
|
+
name: token.name,
|
|
99
|
+
});
|
|
100
|
+
} else {
|
|
101
|
+
seenColorValue.set(token.value.toLowerCase(), token.name);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── typography ──────────────────────────────────────────────────
|
|
106
|
+
const seenTypoSlug = new Map<string, string>();
|
|
107
|
+
for (const token of spec.sections.typography.tokens) {
|
|
108
|
+
if (!token.fontFamily && !token.size) {
|
|
109
|
+
issues.push({
|
|
110
|
+
rule: "typography-empty-token",
|
|
111
|
+
section: "typography",
|
|
112
|
+
severity: "warning",
|
|
113
|
+
message: `Typography token "${token.name}" has neither fontFamily nor size — nothing to sync.`,
|
|
114
|
+
name: token.name,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const sl = slugifyTokenName(token.name);
|
|
118
|
+
const prev = seenTypoSlug.get(sl);
|
|
119
|
+
if (prev) {
|
|
120
|
+
issues.push({
|
|
121
|
+
rule: "typography-slug-collision",
|
|
122
|
+
section: "typography",
|
|
123
|
+
severity: "warning",
|
|
124
|
+
message: `Typography "${token.name}" collides with "${prev}" on slug "${sl}".`,
|
|
125
|
+
name: token.name,
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
seenTypoSlug.set(sl, token.name);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── layout (spacing) ────────────────────────────────────────────
|
|
133
|
+
const seenLayoutSlug = new Map<string, string>();
|
|
134
|
+
for (const token of spec.sections.layout.tokens) {
|
|
135
|
+
if (!token.value) {
|
|
136
|
+
issues.push({
|
|
137
|
+
rule: "spacing-missing-value",
|
|
138
|
+
section: "layout",
|
|
139
|
+
severity: "warning",
|
|
140
|
+
message: `Spacing "${token.name}" has no value.`,
|
|
141
|
+
name: token.name,
|
|
142
|
+
});
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const sl = slugifyTokenName(token.name);
|
|
146
|
+
const prev = seenLayoutSlug.get(sl);
|
|
147
|
+
if (prev) {
|
|
148
|
+
issues.push({
|
|
149
|
+
rule: "spacing-slug-collision",
|
|
150
|
+
section: "layout",
|
|
151
|
+
severity: "warning",
|
|
152
|
+
message: `Spacing "${token.name}" collides with "${prev}" on slug "${sl}".`,
|
|
153
|
+
name: token.name,
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
seenLayoutSlug.set(sl, token.name);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── shadows ─────────────────────────────────────────────────────
|
|
161
|
+
const seenShadowSlug = new Map<string, string>();
|
|
162
|
+
for (const token of spec.sections.shadows.tokens) {
|
|
163
|
+
if (!token.value) {
|
|
164
|
+
issues.push({
|
|
165
|
+
rule: "shadow-missing-value",
|
|
166
|
+
section: "shadows",
|
|
167
|
+
severity: "warning",
|
|
168
|
+
message: `Shadow "${token.name}" has no value.`,
|
|
169
|
+
name: token.name,
|
|
170
|
+
});
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const sl = slugifyTokenName(token.name);
|
|
174
|
+
const prev = seenShadowSlug.get(sl);
|
|
175
|
+
if (prev) {
|
|
176
|
+
issues.push({
|
|
177
|
+
rule: "shadow-slug-collision",
|
|
178
|
+
section: "shadows",
|
|
179
|
+
severity: "warning",
|
|
180
|
+
message: `Shadow "${token.name}" collides with "${prev}" on slug "${sl}".`,
|
|
181
|
+
name: token.name,
|
|
182
|
+
});
|
|
183
|
+
} else {
|
|
184
|
+
seenShadowSlug.set(sl, token.name);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ─── components ──────────────────────────────────────────────────
|
|
189
|
+
const seenComponentSlug = new Map<string, string>();
|
|
190
|
+
for (const token of spec.sections.components.tokens) {
|
|
191
|
+
const sl = slugifyTokenName(token.name);
|
|
192
|
+
const prev = seenComponentSlug.get(sl);
|
|
193
|
+
if (prev) {
|
|
194
|
+
issues.push({
|
|
195
|
+
rule: "component-duplicate",
|
|
196
|
+
section: "components",
|
|
197
|
+
severity: "warning",
|
|
198
|
+
message: `Component "${token.name}" appears twice (also as "${prev}").`,
|
|
199
|
+
name: token.name,
|
|
200
|
+
});
|
|
201
|
+
} else {
|
|
202
|
+
seenComponentSlug.set(sl, token.name);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// The most critical level of severity wins for the `ok` flag.
|
|
207
|
+
const ok = !issues.some((i) => i.severity === "error");
|
|
208
|
+
return { ok, issues };
|
|
209
|
+
}
|
package/src/design/patch.ts
CHANGED
|
@@ -165,26 +165,26 @@ export class MCPConnector {
|
|
|
165
165
|
try {
|
|
166
166
|
this.ws = new WebSocket(url);
|
|
167
167
|
|
|
168
|
-
this.ws.
|
|
169
|
-
clearTimeout(timeout);
|
|
170
|
-
this.setStatus('connected');
|
|
171
|
-
this.reconnectAttempts = 0;
|
|
172
|
-
resolve();
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
this.ws.
|
|
176
|
-
this.handleDisconnect();
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
this.ws.
|
|
180
|
-
clearTimeout(timeout);
|
|
181
|
-
this.handleConnectionError(new Error('WebSocket error'));
|
|
182
|
-
reject(new Error('WebSocket connection failed'));
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
this.ws.
|
|
186
|
-
this.handleMessage(event.data);
|
|
187
|
-
};
|
|
168
|
+
this.ws.addEventListener('open', () => {
|
|
169
|
+
clearTimeout(timeout);
|
|
170
|
+
this.setStatus('connected');
|
|
171
|
+
this.reconnectAttempts = 0;
|
|
172
|
+
resolve();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
this.ws.addEventListener('close', () => {
|
|
176
|
+
this.handleDisconnect();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
this.ws.addEventListener('error', () => {
|
|
180
|
+
clearTimeout(timeout);
|
|
181
|
+
this.handleConnectionError(new Error('WebSocket error'));
|
|
182
|
+
reject(new Error('WebSocket connection failed'));
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
this.ws.addEventListener('message', (event) => {
|
|
186
|
+
this.handleMessage(event.data);
|
|
187
|
+
});
|
|
188
188
|
} catch (error) {
|
|
189
189
|
clearTimeout(timeout);
|
|
190
190
|
this.handleConnectionError(
|
|
@@ -210,10 +210,10 @@ export class MCPConnector {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
// 대기 중인 요청 모두 reject
|
|
213
|
-
for (const [
|
|
214
|
-
clearTimeout(pending.timeout);
|
|
215
|
-
pending.reject(new Error('Connection closed'));
|
|
216
|
-
}
|
|
213
|
+
for (const [, pending] of this.pendingRequests) {
|
|
214
|
+
clearTimeout(pending.timeout);
|
|
215
|
+
pending.reject(new Error('Connection closed'));
|
|
216
|
+
}
|
|
217
217
|
this.pendingRequests.clear();
|
|
218
218
|
|
|
219
219
|
this.setStatus('disconnected');
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
|
9
9
|
import { createRoot, type Root } from 'react-dom/client';
|
|
10
|
-
import type { NormalizedError, DevToolsConfig
|
|
10
|
+
import type { NormalizedError, DevToolsConfig } from '../../types';
|
|
11
11
|
import { generateCSSVariables, testIds, zIndex } from '../../design-tokens';
|
|
12
12
|
import { getStateManager, type KitchenState } from '../state-manager';
|
|
13
13
|
import { getOrCreateHook } from '../../hook';
|
|
@@ -159,7 +159,7 @@ export interface ErrorsPanelProps {
|
|
|
159
159
|
export function ErrorsPanel({
|
|
160
160
|
errors,
|
|
161
161
|
onErrorClick,
|
|
162
|
-
onErrorIgnore,
|
|
162
|
+
onErrorIgnore: _onErrorIgnore,
|
|
163
163
|
onClearAll,
|
|
164
164
|
}: ErrorsPanelProps): React.ReactElement {
|
|
165
165
|
const formatTime = useCallback((timestamp: number): string => {
|
package/src/devtools/init.ts
CHANGED
|
@@ -17,10 +17,9 @@ import {
|
|
|
17
17
|
initializePersistence,
|
|
18
18
|
destroyPersistence,
|
|
19
19
|
getStateManager,
|
|
20
|
-
getPersistenceManager,
|
|
21
20
|
} from './client';
|
|
22
21
|
import { initializeWorkerManager, destroyWorkerManager } from './worker';
|
|
23
|
-
import { getContextBuilder, resetContextBuilder,
|
|
22
|
+
import { getContextBuilder, resetContextBuilder, destroyMCPConnector } from './ai';
|
|
24
23
|
|
|
25
24
|
// ============================================================================
|
|
26
25
|
// Types
|
|
@@ -134,7 +133,7 @@ export function initManduKitchen(config: DevToolsConfig = {}): KitchenInstance {
|
|
|
134
133
|
});
|
|
135
134
|
|
|
136
135
|
// 6. v1.1: AI Context Builder 초기화
|
|
137
|
-
const
|
|
136
|
+
const _contextBuilder = getContextBuilder({ config });
|
|
138
137
|
|
|
139
138
|
// 7. UI 마운트
|
|
140
139
|
mountKitchen(config);
|
|
@@ -424,7 +424,7 @@ export function createViteMiddleware(projectRoot: string) {
|
|
|
424
424
|
res.setHeader('Cache-Control', 'no-store');
|
|
425
425
|
res.statusCode = result.success ? 200 : 400;
|
|
426
426
|
res.end(JSON.stringify(result));
|
|
427
|
-
} catch
|
|
427
|
+
} catch {
|
|
428
428
|
res.statusCode = 500;
|
|
429
429
|
res.end(JSON.stringify({
|
|
430
430
|
success: false,
|