@mandujs/core 0.54.31 → 0.55.0-beta.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/README.md +654 -654
- package/package.json +106 -210
- package/scripts/postinstall-lock.ts +153 -153
- 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 +15 -15
- package/src/a11y/types.ts +125 -125
- package/src/agent/__tests__/apply.test.ts +224 -0
- package/src/agent/__tests__/context.test.ts +97 -98
- package/src/agent/apply.ts +1031 -0
- package/src/agent/context.ts +552 -552
- package/src/agent/index.ts +4 -3
- package/src/agent/plan.ts +161 -243
- package/src/agent/repair.ts +239 -162
- package/src/agent/sync.ts +197 -198
- package/src/agent/types.ts +242 -94
- package/src/agent/verify.ts +55 -55
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +15 -5
- package/src/auth/__tests__/reset.test.ts +2 -2
- 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 +14 -2
- 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/brain/doctor/analyzer.ts +7 -7
- package/src/bundler/__snapshots__/build.test.ts.snap +5 -5
- package/src/bundler/__tests__/build-runner.ts +166 -166
- package/src/bundler/__tests__/client-boundary-transform.test.ts +524 -524
- package/src/bundler/__tests__/cold-start.test.ts +60 -60
- package/src/bundler/__tests__/css.test.ts +49 -20
- 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 +24 -24
- package/src/bundler/__tests__/generation.test.ts +447 -0
- package/src/bundler/__tests__/hdr.test.ts +24 -18
- package/src/bundler/__tests__/hmr-client.test.ts +62 -24
- package/src/bundler/__tests__/jsx-runtime-shim.test.ts +179 -140
- package/src/bundler/__tests__/manifest-schema.test.ts +305 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +42 -42
- 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/analyzer.ts +15 -15
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +898 -915
- package/src/bundler/build.ts +113 -24
- package/src/bundler/client-boundary-transform.ts +977 -977
- package/src/bundler/css.ts +65 -44
- package/src/bundler/dev.ts +105 -73
- package/src/bundler/fast-refresh-preamble.ts +47 -47
- package/src/bundler/generation.ts +602 -0
- package/src/bundler/index.ts +3 -3
- package/src/bundler/manifest-schema.ts +55 -40
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
- 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 +16 -15
- 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/prerender.ts +29 -9
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +1 -1
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +58 -56
- 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__/props-serialization.test.ts +37 -37
- 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 +2 -2
- package/src/client/index.ts +1 -1
- package/src/client/island.ts +79 -79
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/props-serialization.ts +233 -233
- package/src/client/runtime-entry.ts +598 -598
- package/src/client/runtime.ts +1 -1
- package/src/client/serialize.ts +50 -50
- 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-compat.ts +3 -0
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +1 -1
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +9 -9
- 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/generate-types.ts +1 -1
- package/src/content/index.ts +2 -2
- 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/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +9 -9
- package/src/db/index.ts +2 -2
- 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 +82 -82
- 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__/smoke.test.ts +2 -2
- package/src/desktop/__tests__/webview-fallback.test.ts +12 -32
- package/src/desktop/__tests__/window.test.ts +20 -99
- 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/webview-fallback.ts +17 -4
- package/src/desktop/window.ts +17 -8
- 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/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 +16 -16
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +1 -1
- 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/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/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/diagnose/__tests__/checks.test.ts +136 -136
- package/src/diagnose/checks.ts +185 -185
- package/src/diagnose/index.ts +17 -17
- package/src/diagnose/run.ts +10 -10
- 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/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 +5 -1
- package/src/filling/auth.ts +308 -308
- package/src/filling/body-parse.test.ts +60 -60
- package/src/filling/cookie-codec.ts +5 -3
- package/src/filling/deps.ts +265 -265
- package/src/filling/head-method.test.ts +154 -154
- 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/generate.ts +30 -30
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.test.ts +48 -48
- package/src/generator/templates.ts +219 -219
- 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/check.ts +39 -24
- package/src/guard/config-guard.ts +13 -13
- 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/fs-routes-policy.ts +51 -51
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/index.ts +11 -11
- 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/__tests__/id.test.ts +3 -3
- package/src/id/index.ts +105 -105
- package/src/index.ts +9 -48
- package/src/internal/client-boundary.ts +266 -266
- package/src/internal/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -779
- package/src/kitchen/api/bundle-inspector.ts +179 -0
- package/src/kitchen/api/errors-grouping.ts +126 -126
- package/src/kitchen/api/file-api.ts +11 -11
- package/src/kitchen/kitchen-handler.ts +36 -0
- package/src/kitchen/kitchen-ui.ts +456 -0
- 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/perf/user-marks.ts +1 -1
- 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 +7 -7
- package/src/resource/__tests__/schema.test.ts +14 -14
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +165 -165
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +146 -146
- 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/generator-schema.ts +11 -11
- package/src/resource/generators/slot.ts +72 -72
- package/src/resource/schema.ts +21 -21
- package/src/router/client-entry.test.ts +227 -227
- package/src/router/client-entry.ts +218 -218
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.test.ts +532 -532
- package/src/router/fs-routes.ts +53 -53
- package/src/router/fs-scanner.ts +216 -216
- package/src/router/fs-types.ts +19 -19
- package/src/router/route-source-analyzer.ts +521 -521
- 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__/devtools-adapter.test.ts +68 -68
- 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__/inline-client-hydration.test.ts +237 -237
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
- package/src/runtime/__tests__/page-render-response.test.ts +517 -517
- package/src/runtime/__tests__/request-middleware.test.ts +70 -70
- package/src/runtime/__tests__/searchparams-page-props.test.ts +81 -81
- 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/devtools-adapter.ts +68 -68
- package/src/runtime/escape.ts +34 -34
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/fast-refresh-types.ts +1 -1
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +30 -11
- 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/observability-lifecycle.ts +290 -290
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/page-render-response.ts +287 -274
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/rate-limit.ts +1 -1
- package/src/runtime/redirect.ts +1 -1
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/request-middleware.ts +31 -31
- package/src/runtime/router.test.ts +4 -4
- package/src/runtime/router.ts +10 -10
- package/src/runtime/server.ts +39 -20
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/ssr.ts +43 -17
- package/src/runtime/static-files.ts +369 -289
- package/src/runtime/streaming-ssr.ts +219 -180
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/__tests__/scheduler.test.ts +12 -11
- package/src/scheduler/index.ts +11 -5
- 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/spec/schema.ts +35 -35
- 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/index.ts +9 -3
- package/src/testing/lcov.ts +192 -0
- 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/__tests__/watcher.test.ts +59 -59
- package/src/watcher/watcher.ts +61 -61
|
@@ -1,165 +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
|
-
}
|
|
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/diff.ts
CHANGED
|
@@ -1,138 +1,138 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DESIGN.md upstream diff (Issue #245 M4 §3.5 external loop).
|
|
3
|
-
*
|
|
4
|
-
* Compare a local DESIGN.md against an upstream source (typically a
|
|
5
|
-
* brand spec from awesome-design-md). The diff is computed at the
|
|
6
|
-
* **structured-token level** — added / changed / removed entries per
|
|
7
|
-
* section — so agents can patch only the sections the user wants to
|
|
8
|
-
* sync without touching free-form prose.
|
|
9
|
-
*
|
|
10
|
-
* The diff is intentionally narrow: it ignores prose changes inside
|
|
11
|
-
* `rawBody`, comment shifts, and heading wording differences. It
|
|
12
|
-
* answers one question — "did the upstream catalog gain, change, or
|
|
13
|
-
* drop a token compared to my local file?".
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type { ColorToken, DesignSpec, ShadowToken, SpacingToken, TypographyToken } from "./types";
|
|
17
|
-
|
|
18
|
-
export interface DiffEntryAddedRemoved<T> {
|
|
19
|
-
kind: "added" | "removed";
|
|
20
|
-
name: string;
|
|
21
|
-
token: T;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface DiffEntryChanged<T> {
|
|
25
|
-
kind: "changed";
|
|
26
|
-
name: string;
|
|
27
|
-
before: T;
|
|
28
|
-
after: T;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export type DiffEntry<T> =
|
|
32
|
-
| DiffEntryAddedRemoved<T>
|
|
33
|
-
| DiffEntryChanged<T>;
|
|
34
|
-
|
|
35
|
-
export interface DesignSpecDiff {
|
|
36
|
-
/** Per-section diff arrays (only structured sections). */
|
|
37
|
-
colorPalette: DiffEntry<ColorToken>[];
|
|
38
|
-
typography: DiffEntry<TypographyToken>[];
|
|
39
|
-
layout: DiffEntry<SpacingToken>[];
|
|
40
|
-
shadows: DiffEntry<ShadowToken>[];
|
|
41
|
-
/** Total count of differences across all sections — convenience. */
|
|
42
|
-
totalChanges: number;
|
|
43
|
-
/** Sections whose `present` flag flipped between local and upstream. */
|
|
44
|
-
sectionPresenceChanged: string[];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Compute the diff. Pure — accepts already-parsed specs so callers
|
|
49
|
-
* own fetching / caching of the upstream source.
|
|
50
|
-
*/
|
|
51
|
-
export function diffDesignSpecs(local: DesignSpec, upstream: DesignSpec): DesignSpecDiff {
|
|
52
|
-
const colorPalette = diffByName(
|
|
53
|
-
local.sections["color-palette"].tokens,
|
|
54
|
-
upstream.sections["color-palette"].tokens,
|
|
55
|
-
(a, b) => a.value === b.value && a.role === b.role,
|
|
56
|
-
);
|
|
57
|
-
const typography = diffByName(
|
|
58
|
-
local.sections.typography.tokens,
|
|
59
|
-
upstream.sections.typography.tokens,
|
|
60
|
-
(a, b) =>
|
|
61
|
-
a.fontFamily === b.fontFamily &&
|
|
62
|
-
a.size === b.size &&
|
|
63
|
-
a.weight === b.weight &&
|
|
64
|
-
a.lineHeight === b.lineHeight,
|
|
65
|
-
);
|
|
66
|
-
const layout = diffByName(
|
|
67
|
-
local.sections.layout.tokens,
|
|
68
|
-
upstream.sections.layout.tokens,
|
|
69
|
-
(a, b) => a.value === b.value,
|
|
70
|
-
);
|
|
71
|
-
const shadows = diffByName(
|
|
72
|
-
local.sections.shadows.tokens,
|
|
73
|
-
upstream.sections.shadows.tokens,
|
|
74
|
-
(a, b) => a.value === b.value,
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
const sectionPresenceChanged: string[] = [];
|
|
78
|
-
for (const id of [
|
|
79
|
-
"theme",
|
|
80
|
-
"color-palette",
|
|
81
|
-
"typography",
|
|
82
|
-
"components",
|
|
83
|
-
"layout",
|
|
84
|
-
"shadows",
|
|
85
|
-
"dos-donts",
|
|
86
|
-
"responsive",
|
|
87
|
-
"agent-prompts",
|
|
88
|
-
] as const) {
|
|
89
|
-
if (local.sections[id].present !== upstream.sections[id].present) {
|
|
90
|
-
sectionPresenceChanged.push(id);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const totalChanges = colorPalette.length + typography.length + layout.length + shadows.length;
|
|
95
|
-
return { colorPalette, typography, layout, shadows, totalChanges, sectionPresenceChanged };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
interface NamedToken {
|
|
99
|
-
name: string;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function diffByName<T extends NamedToken>(
|
|
103
|
-
local: readonly T[],
|
|
104
|
-
upstream: readonly T[],
|
|
105
|
-
same: (a: T, b: T) => boolean,
|
|
106
|
-
): DiffEntry<T>[] {
|
|
107
|
-
const localByKey = new Map<string, T>();
|
|
108
|
-
for (const t of local) localByKey.set(slug(t.name), t);
|
|
109
|
-
const upstreamByKey = new Map<string, T>();
|
|
110
|
-
for (const t of upstream) upstreamByKey.set(slug(t.name), t);
|
|
111
|
-
|
|
112
|
-
const entries: DiffEntry<T>[] = [];
|
|
113
|
-
for (const [key, upstreamToken] of upstreamByKey) {
|
|
114
|
-
const localToken = localByKey.get(key);
|
|
115
|
-
if (!localToken) {
|
|
116
|
-
entries.push({ kind: "added", name: upstreamToken.name, token: upstreamToken });
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
if (!same(localToken, upstreamToken)) {
|
|
120
|
-
entries.push({ kind: "changed", name: upstreamToken.name, before: localToken, after: upstreamToken });
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
for (const [key, localToken] of localByKey) {
|
|
124
|
-
if (!upstreamByKey.has(key)) {
|
|
125
|
-
entries.push({ kind: "removed", name: localToken.name, token: localToken });
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return entries;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function slug(name: string): string {
|
|
132
|
-
return name
|
|
133
|
-
.normalize("NFKD")
|
|
134
|
-
.replace(/[^\w\s-]/g, "")
|
|
135
|
-
.trim()
|
|
136
|
-
.replace(/\s+/g, "-")
|
|
137
|
-
.toLowerCase();
|
|
138
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* DESIGN.md upstream diff (Issue #245 M4 §3.5 external loop).
|
|
3
|
+
*
|
|
4
|
+
* Compare a local DESIGN.md against an upstream source (typically a
|
|
5
|
+
* brand spec from awesome-design-md). The diff is computed at the
|
|
6
|
+
* **structured-token level** — added / changed / removed entries per
|
|
7
|
+
* section — so agents can patch only the sections the user wants to
|
|
8
|
+
* sync without touching free-form prose.
|
|
9
|
+
*
|
|
10
|
+
* The diff is intentionally narrow: it ignores prose changes inside
|
|
11
|
+
* `rawBody`, comment shifts, and heading wording differences. It
|
|
12
|
+
* answers one question — "did the upstream catalog gain, change, or
|
|
13
|
+
* drop a token compared to my local file?".
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ColorToken, DesignSpec, ShadowToken, SpacingToken, TypographyToken } from "./types";
|
|
17
|
+
|
|
18
|
+
export interface DiffEntryAddedRemoved<T> {
|
|
19
|
+
kind: "added" | "removed";
|
|
20
|
+
name: string;
|
|
21
|
+
token: T;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DiffEntryChanged<T> {
|
|
25
|
+
kind: "changed";
|
|
26
|
+
name: string;
|
|
27
|
+
before: T;
|
|
28
|
+
after: T;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type DiffEntry<T> =
|
|
32
|
+
| DiffEntryAddedRemoved<T>
|
|
33
|
+
| DiffEntryChanged<T>;
|
|
34
|
+
|
|
35
|
+
export interface DesignSpecDiff {
|
|
36
|
+
/** Per-section diff arrays (only structured sections). */
|
|
37
|
+
colorPalette: DiffEntry<ColorToken>[];
|
|
38
|
+
typography: DiffEntry<TypographyToken>[];
|
|
39
|
+
layout: DiffEntry<SpacingToken>[];
|
|
40
|
+
shadows: DiffEntry<ShadowToken>[];
|
|
41
|
+
/** Total count of differences across all sections — convenience. */
|
|
42
|
+
totalChanges: number;
|
|
43
|
+
/** Sections whose `present` flag flipped between local and upstream. */
|
|
44
|
+
sectionPresenceChanged: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compute the diff. Pure — accepts already-parsed specs so callers
|
|
49
|
+
* own fetching / caching of the upstream source.
|
|
50
|
+
*/
|
|
51
|
+
export function diffDesignSpecs(local: DesignSpec, upstream: DesignSpec): DesignSpecDiff {
|
|
52
|
+
const colorPalette = diffByName(
|
|
53
|
+
local.sections["color-palette"].tokens,
|
|
54
|
+
upstream.sections["color-palette"].tokens,
|
|
55
|
+
(a, b) => a.value === b.value && a.role === b.role,
|
|
56
|
+
);
|
|
57
|
+
const typography = diffByName(
|
|
58
|
+
local.sections.typography.tokens,
|
|
59
|
+
upstream.sections.typography.tokens,
|
|
60
|
+
(a, b) =>
|
|
61
|
+
a.fontFamily === b.fontFamily &&
|
|
62
|
+
a.size === b.size &&
|
|
63
|
+
a.weight === b.weight &&
|
|
64
|
+
a.lineHeight === b.lineHeight,
|
|
65
|
+
);
|
|
66
|
+
const layout = diffByName(
|
|
67
|
+
local.sections.layout.tokens,
|
|
68
|
+
upstream.sections.layout.tokens,
|
|
69
|
+
(a, b) => a.value === b.value,
|
|
70
|
+
);
|
|
71
|
+
const shadows = diffByName(
|
|
72
|
+
local.sections.shadows.tokens,
|
|
73
|
+
upstream.sections.shadows.tokens,
|
|
74
|
+
(a, b) => a.value === b.value,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const sectionPresenceChanged: string[] = [];
|
|
78
|
+
for (const id of [
|
|
79
|
+
"theme",
|
|
80
|
+
"color-palette",
|
|
81
|
+
"typography",
|
|
82
|
+
"components",
|
|
83
|
+
"layout",
|
|
84
|
+
"shadows",
|
|
85
|
+
"dos-donts",
|
|
86
|
+
"responsive",
|
|
87
|
+
"agent-prompts",
|
|
88
|
+
] as const) {
|
|
89
|
+
if (local.sections[id].present !== upstream.sections[id].present) {
|
|
90
|
+
sectionPresenceChanged.push(id);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const totalChanges = colorPalette.length + typography.length + layout.length + shadows.length;
|
|
95
|
+
return { colorPalette, typography, layout, shadows, totalChanges, sectionPresenceChanged };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface NamedToken {
|
|
99
|
+
name: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function diffByName<T extends NamedToken>(
|
|
103
|
+
local: readonly T[],
|
|
104
|
+
upstream: readonly T[],
|
|
105
|
+
same: (a: T, b: T) => boolean,
|
|
106
|
+
): DiffEntry<T>[] {
|
|
107
|
+
const localByKey = new Map<string, T>();
|
|
108
|
+
for (const t of local) localByKey.set(slug(t.name), t);
|
|
109
|
+
const upstreamByKey = new Map<string, T>();
|
|
110
|
+
for (const t of upstream) upstreamByKey.set(slug(t.name), t);
|
|
111
|
+
|
|
112
|
+
const entries: DiffEntry<T>[] = [];
|
|
113
|
+
for (const [key, upstreamToken] of upstreamByKey) {
|
|
114
|
+
const localToken = localByKey.get(key);
|
|
115
|
+
if (!localToken) {
|
|
116
|
+
entries.push({ kind: "added", name: upstreamToken.name, token: upstreamToken });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!same(localToken, upstreamToken)) {
|
|
120
|
+
entries.push({ kind: "changed", name: upstreamToken.name, before: localToken, after: upstreamToken });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const [key, localToken] of localByKey) {
|
|
124
|
+
if (!upstreamByKey.has(key)) {
|
|
125
|
+
entries.push({ kind: "removed", name: localToken.name, token: localToken });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return entries;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function slug(name: string): string {
|
|
132
|
+
return name
|
|
133
|
+
.normalize("NFKD")
|
|
134
|
+
.replace(/[^\w\s-]/g, "")
|
|
135
|
+
.trim()
|
|
136
|
+
.replace(/\s+/g, "-")
|
|
137
|
+
.toLowerCase();
|
|
138
|
+
}
|