@mandujs/core 0.53.3 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +654 -654
- package/package.json +2 -2
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +394 -394
- package/src/a11y/types.ts +125 -125
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +122 -122
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +120 -120
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/bundler/__tests__/build-runner.ts +113 -113
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -10
- package/src/bundler/__tests__/hdr.test.ts +353 -353
- package/src/bundler/__tests__/hmr-client.test.ts +532 -532
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -519
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +179 -179
- package/src/bundler/build.ts +55 -55
- package/src/bundler/dev.ts +42 -42
- package/src/bundler/manifest-schema.ts +301 -301
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +263 -263
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +155 -155
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +201 -201
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +10 -10
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +340 -340
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/router.ts +11 -11
- package/src/client/runtime.ts +47 -47
- package/src/client/serialize.ts +404 -404
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +3 -3
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +8 -8
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +482 -482
- package/src/db/index.ts +138 -138
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +182 -182
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -254
- package/src/desktop/__tests__/window.test.ts +248 -248
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +320 -320
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +500 -500
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +450 -450
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/devtools/worker/worker-manager.ts +410 -410
- package/src/diagnose/__tests__/checks.test.ts +451 -451
- package/src/diagnose/checks.ts +832 -832
- package/src/diagnose/index.ts +17 -17
- package/src/diagnose/run.ts +93 -93
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +60 -60
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +454 -454
- package/src/filling/auth.ts +308 -308
- package/src/filling/cookie-codec.ts +299 -299
- package/src/filling/deps.ts +265 -265
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/index.ts +3 -3
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/index.ts +105 -105
- package/src/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -544
- package/src/kitchen/api/errors-grouping.ts +126 -0
- package/src/kitchen/kitchen-handler.ts +192 -63
- package/src/kitchen/kitchen-ui.ts +842 -464
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -32
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +823 -823
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +559 -559
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.ts +6 -6
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +300 -300
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/router.ts +105 -105
- package/src/runtime/server.ts +67 -67
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/watcher.ts +18 -18
package/src/design/extract.ts
CHANGED
|
@@ -1,285 +1,285 @@
|
|
|
1
|
-
import type * as __ManduNodeFsTypes0 from "node:fs";
|
|
2
|
-
/**
|
|
3
|
-
* DESIGN.md token extractor — scan project source for tokens that
|
|
4
|
-
* should be promoted into DESIGN.md (Issue #245 M4 §3.5 internal loop).
|
|
5
|
-
*
|
|
6
|
-
* Scope:
|
|
7
|
-
* - **color**: `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb(...)`, `rgba(...)`,
|
|
8
|
-
* `hsl(...)`, `oklch(...)` literals in TS/TSX/JSX/CSS/MDX sources.
|
|
9
|
-
* - **font-family**: `font-family: "X", sans-serif` declarations and
|
|
10
|
-
* Tailwind v4-style `--font-<slug>: ...;` tokens that don't yet
|
|
11
|
-
* appear in DESIGN.md typography.
|
|
12
|
-
* - **component**: identifier-style className combos that recur 3+
|
|
13
|
-
* times across files, suggesting a missing extraction.
|
|
14
|
-
*
|
|
15
|
-
* The extractor is **proposal-only**: it never edits anything.
|
|
16
|
-
* Callers (CLI / MCP `mandu.design.extract`) decide whether to flow
|
|
17
|
-
* the proposals into DESIGN.md via `patchDesignMd`. Confidence is a
|
|
18
|
-
* coarse 0..1 score driven by occurrence count; agents can filter on
|
|
19
|
-
* it before showing the user.
|
|
20
|
-
*
|
|
21
|
-
* Performance: the walker bounds itself to the conventional source
|
|
22
|
-
* roots and skips dotfile dirs / node_modules. Large monorepos can
|
|
23
|
-
* narrow further with the `scope` option.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import path from "node:path";
|
|
27
|
-
import { promises as fs } from "node:fs";
|
|
28
|
-
import type { DesignSpec } from "./types";
|
|
29
|
-
|
|
30
|
-
// ─── Public surface ───────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
export type ExtractKind = "color" | "typography" | "spacing" | "component";
|
|
33
|
-
|
|
34
|
-
export interface ExtractProposal {
|
|
35
|
-
kind: ExtractKind;
|
|
36
|
-
/** DESIGN.md section the proposal would land in. */
|
|
37
|
-
section: "color-palette" | "typography" | "layout" | "components";
|
|
38
|
-
/** Stable key to compare against existing tokens (slug or literal). */
|
|
39
|
-
key: string;
|
|
40
|
-
/** Suggested DESIGN.md value (`#FF8C42`, `Inter sans-serif`, …). */
|
|
41
|
-
value: string;
|
|
42
|
-
/** Total occurrences across the scanned tree. */
|
|
43
|
-
occurrences: number;
|
|
44
|
-
/** Up to 5 distinct file paths the literal/pattern was found in. */
|
|
45
|
-
files: string[];
|
|
46
|
-
/** Coarse 0..1 confidence — higher = more occurrences. */
|
|
47
|
-
confidence: number;
|
|
48
|
-
/** Optional human note ("seen in ButtonPrimary, ButtonGhost"). */
|
|
49
|
-
note?: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface ExtractOptions {
|
|
53
|
-
/** Glob-rooted scopes (relative). Defaults to `["src", "app"]`. */
|
|
54
|
-
scope?: readonly string[];
|
|
55
|
-
/** Filter the kinds emitted. Defaults to all four. */
|
|
56
|
-
kinds?: readonly ExtractKind[];
|
|
57
|
-
/** Minimum occurrence threshold for color/font/component. Default 3. */
|
|
58
|
-
minOccurrences?: number;
|
|
59
|
-
/**
|
|
60
|
-
* When provided, proposals already represented by an existing
|
|
61
|
-
* DesignSpec token are dropped (so agents only see "new" candidates).
|
|
62
|
-
*/
|
|
63
|
-
existing?: DesignSpec;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface ExtractResult {
|
|
67
|
-
proposals: ExtractProposal[];
|
|
68
|
-
/** Total source files scanned. */
|
|
69
|
-
scannedFiles: number;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Walk the project and collect proposals. Pure async — no caching, no
|
|
74
|
-
* side effects beyond reads.
|
|
75
|
-
*/
|
|
76
|
-
export async function extractDesignTokens(
|
|
77
|
-
rootDir: string,
|
|
78
|
-
options: ExtractOptions = {},
|
|
79
|
-
): Promise<ExtractResult> {
|
|
80
|
-
const scope = options.scope ?? ["src", "app"];
|
|
81
|
-
const kinds = new Set<ExtractKind>(options.kinds ?? ["color", "typography", "spacing", "component"]);
|
|
82
|
-
const minOccurrences = options.minOccurrences ?? 3;
|
|
83
|
-
|
|
84
|
-
const colorOccurrences = new Map<string, { files: Set<string>; count: number }>();
|
|
85
|
-
const fontFamilyOccurrences = new Map<string, { files: Set<string>; count: number }>();
|
|
86
|
-
const classnameComboOccurrences = new Map<string, { files: Set<string>; count: number }>();
|
|
87
|
-
|
|
88
|
-
let scannedFiles = 0;
|
|
89
|
-
for (const dir of scope) {
|
|
90
|
-
const root = path.join(rootDir, dir);
|
|
91
|
-
const files = await collectFiles(root);
|
|
92
|
-
scannedFiles += files.length;
|
|
93
|
-
for (const file of files) {
|
|
94
|
-
let content: string;
|
|
95
|
-
try {
|
|
96
|
-
content = await fs.readFile(file, "utf8");
|
|
97
|
-
} catch {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
const rel = path.relative(rootDir, file).replace(/\\/g, "/");
|
|
101
|
-
if (kinds.has("color")) collectColors(content, rel, colorOccurrences);
|
|
102
|
-
if (kinds.has("typography")) collectFontFamilies(content, rel, fontFamilyOccurrences);
|
|
103
|
-
if (kinds.has("component")) collectClassNameCombos(content, rel, classnameComboOccurrences);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const proposals: ExtractProposal[] = [];
|
|
108
|
-
if (kinds.has("color")) {
|
|
109
|
-
proposals.push(
|
|
110
|
-
...buildProposals({
|
|
111
|
-
section: "color-palette",
|
|
112
|
-
kind: "color",
|
|
113
|
-
keyer: (literal) => literal,
|
|
114
|
-
valuer: (literal) => literal,
|
|
115
|
-
seed: colorOccurrences,
|
|
116
|
-
minOccurrences,
|
|
117
|
-
existingKeys: collectExistingColorValues(options.existing),
|
|
118
|
-
}),
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
if (kinds.has("typography")) {
|
|
122
|
-
proposals.push(
|
|
123
|
-
...buildProposals({
|
|
124
|
-
section: "typography",
|
|
125
|
-
kind: "typography",
|
|
126
|
-
keyer: (literal) => literal,
|
|
127
|
-
valuer: (literal) => literal,
|
|
128
|
-
seed: fontFamilyOccurrences,
|
|
129
|
-
minOccurrences,
|
|
130
|
-
existingKeys: collectExistingFontFamilies(options.existing),
|
|
131
|
-
}),
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
if (kinds.has("component")) {
|
|
135
|
-
proposals.push(
|
|
136
|
-
...buildProposals({
|
|
137
|
-
section: "components",
|
|
138
|
-
kind: "component",
|
|
139
|
-
keyer: (literal) => literal,
|
|
140
|
-
valuer: (literal) => literal,
|
|
141
|
-
seed: classnameComboOccurrences,
|
|
142
|
-
minOccurrences,
|
|
143
|
-
existingKeys: new Set(),
|
|
144
|
-
}),
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
proposals.sort((a, b) => b.occurrences - a.occurrences);
|
|
149
|
-
return { proposals, scannedFiles };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ─── Walker ───────────────────────────────────────────────────────────
|
|
153
|
-
|
|
154
|
-
const SOURCE_RX = /\.(?:tsx?|jsx?|mdx?|css)$/;
|
|
155
|
-
|
|
156
|
-
async function collectFiles(root: string): Promise<string[]> {
|
|
157
|
-
const out: string[] = [];
|
|
158
|
-
async function walk(dir: string): Promise<void> {
|
|
159
|
-
let entries: __ManduNodeFsTypes0.Dirent[];
|
|
160
|
-
try {
|
|
161
|
-
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
162
|
-
} catch {
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
for (const entry of entries) {
|
|
166
|
-
const full = path.join(dir, entry.name);
|
|
167
|
-
if (entry.isDirectory()) {
|
|
168
|
-
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
169
|
-
await walk(full);
|
|
170
|
-
} else if (entry.isFile() && SOURCE_RX.test(entry.name)) {
|
|
171
|
-
out.push(full);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
await walk(root);
|
|
176
|
-
return out;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// ─── Collectors ───────────────────────────────────────────────────────
|
|
180
|
-
|
|
181
|
-
const COLOR_RX = /#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b|(?:rgba?|hsla?|oklch|hwb|lab|lch)\([^)]+\)/g;
|
|
182
|
-
|
|
183
|
-
function collectColors(
|
|
184
|
-
content: string,
|
|
185
|
-
rel: string,
|
|
186
|
-
bucket: Map<string, { files: Set<string>; count: number }>,
|
|
187
|
-
): void {
|
|
188
|
-
let m: RegExpExecArray | null;
|
|
189
|
-
while ((m = COLOR_RX.exec(content)) !== null) {
|
|
190
|
-
const literal = m[0]!.toLowerCase();
|
|
191
|
-
const entry = bucket.get(literal) ?? { files: new Set(), count: 0 };
|
|
192
|
-
entry.files.add(rel);
|
|
193
|
-
entry.count++;
|
|
194
|
-
bucket.set(literal, entry);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const FONT_FAMILY_RX = /font-family\s*:\s*([^;\n}]+)/gi;
|
|
199
|
-
|
|
200
|
-
function collectFontFamilies(
|
|
201
|
-
content: string,
|
|
202
|
-
rel: string,
|
|
203
|
-
bucket: Map<string, { files: Set<string>; count: number }>,
|
|
204
|
-
): void {
|
|
205
|
-
let m: RegExpExecArray | null;
|
|
206
|
-
while ((m = FONT_FAMILY_RX.exec(content)) !== null) {
|
|
207
|
-
const value = m[1]!.trim().replace(/[`'"]/g, "");
|
|
208
|
-
if (!value || value.length > 200) continue;
|
|
209
|
-
const entry = bucket.get(value) ?? { files: new Set(), count: 0 };
|
|
210
|
-
entry.files.add(rel);
|
|
211
|
-
entry.count++;
|
|
212
|
-
bucket.set(value, entry);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const CLASSNAME_RX = /className\s*=\s*["']([^"']{16,200})["']/g;
|
|
217
|
-
|
|
218
|
-
function collectClassNameCombos(
|
|
219
|
-
content: string,
|
|
220
|
-
rel: string,
|
|
221
|
-
bucket: Map<string, { files: Set<string>; count: number }>,
|
|
222
|
-
): void {
|
|
223
|
-
let m: RegExpExecArray | null;
|
|
224
|
-
while ((m = CLASSNAME_RX.exec(content)) !== null) {
|
|
225
|
-
const literal = m[1]!.trim().split(/\s+/).sort().join(" ");
|
|
226
|
-
if (!literal) continue;
|
|
227
|
-
// Skip combos that are mostly variant prefixes — they're rarely
|
|
228
|
-
// good extraction candidates.
|
|
229
|
-
const tokenCount = literal.split(/\s+/).length;
|
|
230
|
-
if (tokenCount < 3) continue;
|
|
231
|
-
const entry = bucket.get(literal) ?? { files: new Set(), count: 0 };
|
|
232
|
-
entry.files.add(rel);
|
|
233
|
-
entry.count++;
|
|
234
|
-
bucket.set(literal, entry);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ─── Proposal builder ────────────────────────────────────────────────
|
|
239
|
-
|
|
240
|
-
interface BuildProposalsArgs {
|
|
241
|
-
section: ExtractProposal["section"];
|
|
242
|
-
kind: ExtractKind;
|
|
243
|
-
keyer: (literal: string) => string;
|
|
244
|
-
valuer: (literal: string) => string;
|
|
245
|
-
seed: Map<string, { files: Set<string>; count: number }>;
|
|
246
|
-
minOccurrences: number;
|
|
247
|
-
existingKeys: Set<string>;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function buildProposals(args: BuildProposalsArgs): ExtractProposal[] {
|
|
251
|
-
const out: ExtractProposal[] = [];
|
|
252
|
-
for (const [literal, info] of args.seed) {
|
|
253
|
-
if (info.count < args.minOccurrences) continue;
|
|
254
|
-
const key = args.keyer(literal).toLowerCase();
|
|
255
|
-
if (args.existingKeys.has(key)) continue;
|
|
256
|
-
out.push({
|
|
257
|
-
kind: args.kind,
|
|
258
|
-
section: args.section,
|
|
259
|
-
key: literal,
|
|
260
|
-
value: args.valuer(literal),
|
|
261
|
-
occurrences: info.count,
|
|
262
|
-
files: [...info.files].slice(0, 5),
|
|
263
|
-
confidence: Math.min(1, info.count / 10),
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
return out;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function collectExistingColorValues(spec: DesignSpec | undefined): Set<string> {
|
|
270
|
-
const out = new Set<string>();
|
|
271
|
-
if (!spec) return out;
|
|
272
|
-
for (const t of spec.sections["color-palette"].tokens) {
|
|
273
|
-
if (t.value) out.add(t.value.toLowerCase());
|
|
274
|
-
}
|
|
275
|
-
return out;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function collectExistingFontFamilies(spec: DesignSpec | undefined): Set<string> {
|
|
279
|
-
const out = new Set<string>();
|
|
280
|
-
if (!spec) return out;
|
|
281
|
-
for (const t of spec.sections.typography.tokens) {
|
|
282
|
-
if (t.fontFamily) out.add(t.fontFamily.toLowerCase());
|
|
283
|
-
}
|
|
284
|
-
return out;
|
|
285
|
-
}
|
|
1
|
+
import type * as __ManduNodeFsTypes0 from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* DESIGN.md token extractor — scan project source for tokens that
|
|
4
|
+
* should be promoted into DESIGN.md (Issue #245 M4 §3.5 internal loop).
|
|
5
|
+
*
|
|
6
|
+
* Scope:
|
|
7
|
+
* - **color**: `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb(...)`, `rgba(...)`,
|
|
8
|
+
* `hsl(...)`, `oklch(...)` literals in TS/TSX/JSX/CSS/MDX sources.
|
|
9
|
+
* - **font-family**: `font-family: "X", sans-serif` declarations and
|
|
10
|
+
* Tailwind v4-style `--font-<slug>: ...;` tokens that don't yet
|
|
11
|
+
* appear in DESIGN.md typography.
|
|
12
|
+
* - **component**: identifier-style className combos that recur 3+
|
|
13
|
+
* times across files, suggesting a missing extraction.
|
|
14
|
+
*
|
|
15
|
+
* The extractor is **proposal-only**: it never edits anything.
|
|
16
|
+
* Callers (CLI / MCP `mandu.design.extract`) decide whether to flow
|
|
17
|
+
* the proposals into DESIGN.md via `patchDesignMd`. Confidence is a
|
|
18
|
+
* coarse 0..1 score driven by occurrence count; agents can filter on
|
|
19
|
+
* it before showing the user.
|
|
20
|
+
*
|
|
21
|
+
* Performance: the walker bounds itself to the conventional source
|
|
22
|
+
* roots and skips dotfile dirs / node_modules. Large monorepos can
|
|
23
|
+
* narrow further with the `scope` option.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { promises as fs } from "node:fs";
|
|
28
|
+
import type { DesignSpec } from "./types";
|
|
29
|
+
|
|
30
|
+
// ─── Public surface ───────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export type ExtractKind = "color" | "typography" | "spacing" | "component";
|
|
33
|
+
|
|
34
|
+
export interface ExtractProposal {
|
|
35
|
+
kind: ExtractKind;
|
|
36
|
+
/** DESIGN.md section the proposal would land in. */
|
|
37
|
+
section: "color-palette" | "typography" | "layout" | "components";
|
|
38
|
+
/** Stable key to compare against existing tokens (slug or literal). */
|
|
39
|
+
key: string;
|
|
40
|
+
/** Suggested DESIGN.md value (`#FF8C42`, `Inter sans-serif`, …). */
|
|
41
|
+
value: string;
|
|
42
|
+
/** Total occurrences across the scanned tree. */
|
|
43
|
+
occurrences: number;
|
|
44
|
+
/** Up to 5 distinct file paths the literal/pattern was found in. */
|
|
45
|
+
files: string[];
|
|
46
|
+
/** Coarse 0..1 confidence — higher = more occurrences. */
|
|
47
|
+
confidence: number;
|
|
48
|
+
/** Optional human note ("seen in ButtonPrimary, ButtonGhost"). */
|
|
49
|
+
note?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ExtractOptions {
|
|
53
|
+
/** Glob-rooted scopes (relative). Defaults to `["src", "app"]`. */
|
|
54
|
+
scope?: readonly string[];
|
|
55
|
+
/** Filter the kinds emitted. Defaults to all four. */
|
|
56
|
+
kinds?: readonly ExtractKind[];
|
|
57
|
+
/** Minimum occurrence threshold for color/font/component. Default 3. */
|
|
58
|
+
minOccurrences?: number;
|
|
59
|
+
/**
|
|
60
|
+
* When provided, proposals already represented by an existing
|
|
61
|
+
* DesignSpec token are dropped (so agents only see "new" candidates).
|
|
62
|
+
*/
|
|
63
|
+
existing?: DesignSpec;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ExtractResult {
|
|
67
|
+
proposals: ExtractProposal[];
|
|
68
|
+
/** Total source files scanned. */
|
|
69
|
+
scannedFiles: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Walk the project and collect proposals. Pure async — no caching, no
|
|
74
|
+
* side effects beyond reads.
|
|
75
|
+
*/
|
|
76
|
+
export async function extractDesignTokens(
|
|
77
|
+
rootDir: string,
|
|
78
|
+
options: ExtractOptions = {},
|
|
79
|
+
): Promise<ExtractResult> {
|
|
80
|
+
const scope = options.scope ?? ["src", "app"];
|
|
81
|
+
const kinds = new Set<ExtractKind>(options.kinds ?? ["color", "typography", "spacing", "component"]);
|
|
82
|
+
const minOccurrences = options.minOccurrences ?? 3;
|
|
83
|
+
|
|
84
|
+
const colorOccurrences = new Map<string, { files: Set<string>; count: number }>();
|
|
85
|
+
const fontFamilyOccurrences = new Map<string, { files: Set<string>; count: number }>();
|
|
86
|
+
const classnameComboOccurrences = new Map<string, { files: Set<string>; count: number }>();
|
|
87
|
+
|
|
88
|
+
let scannedFiles = 0;
|
|
89
|
+
for (const dir of scope) {
|
|
90
|
+
const root = path.join(rootDir, dir);
|
|
91
|
+
const files = await collectFiles(root);
|
|
92
|
+
scannedFiles += files.length;
|
|
93
|
+
for (const file of files) {
|
|
94
|
+
let content: string;
|
|
95
|
+
try {
|
|
96
|
+
content = await fs.readFile(file, "utf8");
|
|
97
|
+
} catch {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const rel = path.relative(rootDir, file).replace(/\\/g, "/");
|
|
101
|
+
if (kinds.has("color")) collectColors(content, rel, colorOccurrences);
|
|
102
|
+
if (kinds.has("typography")) collectFontFamilies(content, rel, fontFamilyOccurrences);
|
|
103
|
+
if (kinds.has("component")) collectClassNameCombos(content, rel, classnameComboOccurrences);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const proposals: ExtractProposal[] = [];
|
|
108
|
+
if (kinds.has("color")) {
|
|
109
|
+
proposals.push(
|
|
110
|
+
...buildProposals({
|
|
111
|
+
section: "color-palette",
|
|
112
|
+
kind: "color",
|
|
113
|
+
keyer: (literal) => literal,
|
|
114
|
+
valuer: (literal) => literal,
|
|
115
|
+
seed: colorOccurrences,
|
|
116
|
+
minOccurrences,
|
|
117
|
+
existingKeys: collectExistingColorValues(options.existing),
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (kinds.has("typography")) {
|
|
122
|
+
proposals.push(
|
|
123
|
+
...buildProposals({
|
|
124
|
+
section: "typography",
|
|
125
|
+
kind: "typography",
|
|
126
|
+
keyer: (literal) => literal,
|
|
127
|
+
valuer: (literal) => literal,
|
|
128
|
+
seed: fontFamilyOccurrences,
|
|
129
|
+
minOccurrences,
|
|
130
|
+
existingKeys: collectExistingFontFamilies(options.existing),
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (kinds.has("component")) {
|
|
135
|
+
proposals.push(
|
|
136
|
+
...buildProposals({
|
|
137
|
+
section: "components",
|
|
138
|
+
kind: "component",
|
|
139
|
+
keyer: (literal) => literal,
|
|
140
|
+
valuer: (literal) => literal,
|
|
141
|
+
seed: classnameComboOccurrences,
|
|
142
|
+
minOccurrences,
|
|
143
|
+
existingKeys: new Set(),
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
proposals.sort((a, b) => b.occurrences - a.occurrences);
|
|
149
|
+
return { proposals, scannedFiles };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─── Walker ───────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
const SOURCE_RX = /\.(?:tsx?|jsx?|mdx?|css)$/;
|
|
155
|
+
|
|
156
|
+
async function collectFiles(root: string): Promise<string[]> {
|
|
157
|
+
const out: string[] = [];
|
|
158
|
+
async function walk(dir: string): Promise<void> {
|
|
159
|
+
let entries: __ManduNodeFsTypes0.Dirent[];
|
|
160
|
+
try {
|
|
161
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
162
|
+
} catch {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
const full = path.join(dir, entry.name);
|
|
167
|
+
if (entry.isDirectory()) {
|
|
168
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
169
|
+
await walk(full);
|
|
170
|
+
} else if (entry.isFile() && SOURCE_RX.test(entry.name)) {
|
|
171
|
+
out.push(full);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
await walk(root);
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── Collectors ───────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
const COLOR_RX = /#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b|(?:rgba?|hsla?|oklch|hwb|lab|lch)\([^)]+\)/g;
|
|
182
|
+
|
|
183
|
+
function collectColors(
|
|
184
|
+
content: string,
|
|
185
|
+
rel: string,
|
|
186
|
+
bucket: Map<string, { files: Set<string>; count: number }>,
|
|
187
|
+
): void {
|
|
188
|
+
let m: RegExpExecArray | null;
|
|
189
|
+
while ((m = COLOR_RX.exec(content)) !== null) {
|
|
190
|
+
const literal = m[0]!.toLowerCase();
|
|
191
|
+
const entry = bucket.get(literal) ?? { files: new Set(), count: 0 };
|
|
192
|
+
entry.files.add(rel);
|
|
193
|
+
entry.count++;
|
|
194
|
+
bucket.set(literal, entry);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const FONT_FAMILY_RX = /font-family\s*:\s*([^;\n}]+)/gi;
|
|
199
|
+
|
|
200
|
+
function collectFontFamilies(
|
|
201
|
+
content: string,
|
|
202
|
+
rel: string,
|
|
203
|
+
bucket: Map<string, { files: Set<string>; count: number }>,
|
|
204
|
+
): void {
|
|
205
|
+
let m: RegExpExecArray | null;
|
|
206
|
+
while ((m = FONT_FAMILY_RX.exec(content)) !== null) {
|
|
207
|
+
const value = m[1]!.trim().replace(/[`'"]/g, "");
|
|
208
|
+
if (!value || value.length > 200) continue;
|
|
209
|
+
const entry = bucket.get(value) ?? { files: new Set(), count: 0 };
|
|
210
|
+
entry.files.add(rel);
|
|
211
|
+
entry.count++;
|
|
212
|
+
bucket.set(value, entry);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const CLASSNAME_RX = /className\s*=\s*["']([^"']{16,200})["']/g;
|
|
217
|
+
|
|
218
|
+
function collectClassNameCombos(
|
|
219
|
+
content: string,
|
|
220
|
+
rel: string,
|
|
221
|
+
bucket: Map<string, { files: Set<string>; count: number }>,
|
|
222
|
+
): void {
|
|
223
|
+
let m: RegExpExecArray | null;
|
|
224
|
+
while ((m = CLASSNAME_RX.exec(content)) !== null) {
|
|
225
|
+
const literal = m[1]!.trim().split(/\s+/).sort().join(" ");
|
|
226
|
+
if (!literal) continue;
|
|
227
|
+
// Skip combos that are mostly variant prefixes — they're rarely
|
|
228
|
+
// good extraction candidates.
|
|
229
|
+
const tokenCount = literal.split(/\s+/).length;
|
|
230
|
+
if (tokenCount < 3) continue;
|
|
231
|
+
const entry = bucket.get(literal) ?? { files: new Set(), count: 0 };
|
|
232
|
+
entry.files.add(rel);
|
|
233
|
+
entry.count++;
|
|
234
|
+
bucket.set(literal, entry);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ─── Proposal builder ────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
interface BuildProposalsArgs {
|
|
241
|
+
section: ExtractProposal["section"];
|
|
242
|
+
kind: ExtractKind;
|
|
243
|
+
keyer: (literal: string) => string;
|
|
244
|
+
valuer: (literal: string) => string;
|
|
245
|
+
seed: Map<string, { files: Set<string>; count: number }>;
|
|
246
|
+
minOccurrences: number;
|
|
247
|
+
existingKeys: Set<string>;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function buildProposals(args: BuildProposalsArgs): ExtractProposal[] {
|
|
251
|
+
const out: ExtractProposal[] = [];
|
|
252
|
+
for (const [literal, info] of args.seed) {
|
|
253
|
+
if (info.count < args.minOccurrences) continue;
|
|
254
|
+
const key = args.keyer(literal).toLowerCase();
|
|
255
|
+
if (args.existingKeys.has(key)) continue;
|
|
256
|
+
out.push({
|
|
257
|
+
kind: args.kind,
|
|
258
|
+
section: args.section,
|
|
259
|
+
key: literal,
|
|
260
|
+
value: args.valuer(literal),
|
|
261
|
+
occurrences: info.count,
|
|
262
|
+
files: [...info.files].slice(0, 5),
|
|
263
|
+
confidence: Math.min(1, info.count / 10),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function collectExistingColorValues(spec: DesignSpec | undefined): Set<string> {
|
|
270
|
+
const out = new Set<string>();
|
|
271
|
+
if (!spec) return out;
|
|
272
|
+
for (const t of spec.sections["color-palette"].tokens) {
|
|
273
|
+
if (t.value) out.add(t.value.toLowerCase());
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function collectExistingFontFamilies(spec: DesignSpec | undefined): Set<string> {
|
|
279
|
+
const out = new Set<string>();
|
|
280
|
+
if (!spec) return out;
|
|
281
|
+
for (const t of spec.sections.typography.tokens) {
|
|
282
|
+
if (t.fontFamily) out.add(t.fontFamily.toLowerCase());
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|