@mandujs/core 0.53.2 → 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 -380
- package/src/diagnose/checks.ts +832 -720
- package/src/diagnose/index.ts +17 -16
- package/src/diagnose/run.ts +93 -91
- 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/ssr.ts +24 -31
- package/src/runtime/streaming-ssr.ts +32 -37
- 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/patch.ts
CHANGED
|
@@ -1,241 +1,241 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Section-safe DESIGN.md patcher (Issue #245 M4 §3.5).
|
|
3
|
-
*
|
|
4
|
-
* The patcher rewrites only the *body* of a target H2 section, leaving:
|
|
5
|
-
* - the H1 title and other H2 sections untouched (verbatim)
|
|
6
|
-
* - the target heading line itself untouched
|
|
7
|
-
* - any free-form prose between the heading and the first structured
|
|
8
|
-
* row preserved
|
|
9
|
-
*
|
|
10
|
-
* Operations are scoped to one `(section, key)` pair at a time so an
|
|
11
|
-
* agent can stream multiple patches without re-loading the file. The
|
|
12
|
-
* `dryRun` flag returns the would-be next source without writing it,
|
|
13
|
-
* so MCP tools can show the user a diff before committing.
|
|
14
|
-
*
|
|
15
|
-
* Token row rules per section:
|
|
16
|
-
* - color-palette / shadows / layout / typography: bullet rows of
|
|
17
|
-
* the form `- <Name> — <value>` (extra columns preserved verbatim
|
|
18
|
-
* for `update`).
|
|
19
|
-
* - components: H3 sub-headings with optional bullet body.
|
|
20
|
-
*
|
|
21
|
-
* Unsupported sections return `{ applied: false, reason: ... }` —
|
|
22
|
-
* they're free-form by design. Callers decide whether to surface the
|
|
23
|
-
* limitation or fall back to a hand-edit.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
export type PatchableSection =
|
|
28
|
-
| "color-palette"
|
|
29
|
-
| "typography"
|
|
30
|
-
| "layout"
|
|
31
|
-
| "shadows"
|
|
32
|
-
| "components";
|
|
33
|
-
|
|
34
|
-
export interface PatchOperation {
|
|
35
|
-
section: PatchableSection;
|
|
36
|
-
/** "add" creates the row; "update" replaces an existing matching row;
|
|
37
|
-
* "remove" deletes a matching row. */
|
|
38
|
-
operation: "add" | "update" | "remove";
|
|
39
|
-
/** Token name. Match is case-insensitive on the slug. */
|
|
40
|
-
key: string;
|
|
41
|
-
/** Required for `add` / `update`. Free-form value (`#FF8C42`,
|
|
42
|
-
* `Inter, sans-serif`, `0 1px 3px rgba(...)`). */
|
|
43
|
-
value?: string;
|
|
44
|
-
/** Optional functional role / usage hint (color/shadow). */
|
|
45
|
-
role?: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface PatchResult {
|
|
49
|
-
applied: boolean;
|
|
50
|
-
/** Reason the operation was a no-op or rejected. */
|
|
51
|
-
reason?: string;
|
|
52
|
-
/** Source after applying — same as input when not applied. */
|
|
53
|
-
next: string;
|
|
54
|
-
/** Old row (for `update` / `remove`). Undefined for clean adds. */
|
|
55
|
-
before?: string;
|
|
56
|
-
/** New row (for `add` / `update`). Undefined for `remove`. */
|
|
57
|
-
after?: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const HEADING_BY_SECTION: Record<PatchableSection, RegExp> = {
|
|
61
|
-
"color-palette": /^##\s+.*?(color|palette).*$/im,
|
|
62
|
-
typography: /^##\s+.*?(typograph|typeface|font|type scale).*$/im,
|
|
63
|
-
layout: /^##\s+.*?(layout|spacing|grid).*$/im,
|
|
64
|
-
shadows: /^##\s+.*?(shadow|elevation|depth).*$/im,
|
|
65
|
-
components: /^##\s+.*?(component|button|card|input).*$/im,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Apply a single patch. Pure — does not touch the filesystem.
|
|
70
|
-
*
|
|
71
|
-
* The result's `applied` flag tells the caller whether the source
|
|
72
|
-
* actually changed (e.g. `remove` of a non-existent key returns
|
|
73
|
-
* `applied: false` with `reason`).
|
|
74
|
-
*/
|
|
75
|
-
export function patchDesignMd(source: string, op: PatchOperation): PatchResult {
|
|
76
|
-
const headingRx = HEADING_BY_SECTION[op.section];
|
|
77
|
-
const headingMatch = headingRx.exec(source);
|
|
78
|
-
if (!headingMatch) {
|
|
79
|
-
return {
|
|
80
|
-
applied: false,
|
|
81
|
-
reason: `Section "${op.section}" not found in DESIGN.md`,
|
|
82
|
-
next: source,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const sectionStart = headingMatch.index + headingMatch[0].length;
|
|
87
|
-
const nextHeadingIdx = source.indexOf("\n## ", sectionStart);
|
|
88
|
-
const sectionEnd = nextHeadingIdx >= 0 ? nextHeadingIdx : source.length;
|
|
89
|
-
const sectionBody = source.slice(sectionStart, sectionEnd);
|
|
90
|
-
|
|
91
|
-
const updated = applyToBody(sectionBody, op);
|
|
92
|
-
if (!updated.applied) {
|
|
93
|
-
return { ...updated, next: source };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const next = source.slice(0, sectionStart) + updated.body + source.slice(sectionEnd);
|
|
97
|
-
return {
|
|
98
|
-
applied: true,
|
|
99
|
-
next,
|
|
100
|
-
before: updated.before,
|
|
101
|
-
after: updated.after,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
interface BodyApplyResult {
|
|
106
|
-
applied: boolean;
|
|
107
|
-
reason?: string;
|
|
108
|
-
body: string;
|
|
109
|
-
before?: string;
|
|
110
|
-
after?: string;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function applyToBody(body: string, op: PatchOperation): BodyApplyResult {
|
|
114
|
-
const lines = body.split(/\r?\n/);
|
|
115
|
-
const targetSlug = slug(op.key);
|
|
116
|
-
|
|
117
|
-
const matchIdx = lines.findIndex((line) => {
|
|
118
|
-
const row = parseTokenRow(line, op.section);
|
|
119
|
-
return row !== null && slug(row.name) === targetSlug;
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
if (op.operation === "remove") {
|
|
123
|
-
if (matchIdx < 0) {
|
|
124
|
-
return { applied: false, reason: `No row with name "${op.key}"`, body };
|
|
125
|
-
}
|
|
126
|
-
const before = lines[matchIdx]!;
|
|
127
|
-
lines.splice(matchIdx, 1);
|
|
128
|
-
return { applied: true, body: lines.join("\n"), before };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (op.operation === "update") {
|
|
132
|
-
if (matchIdx < 0) {
|
|
133
|
-
return { applied: false, reason: `No row with name "${op.key}"`, body };
|
|
134
|
-
}
|
|
135
|
-
if (op.value === undefined) {
|
|
136
|
-
return { applied: false, reason: "`value` is required for update", body };
|
|
137
|
-
}
|
|
138
|
-
const before = lines[matchIdx]!;
|
|
139
|
-
const after = renderRow(op);
|
|
140
|
-
lines[matchIdx] = after;
|
|
141
|
-
return { applied: true, body: lines.join("\n"), before, after };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// add — value is required for token rows, optional for component H3
|
|
145
|
-
if (op.section !== "components" && op.value === undefined) {
|
|
146
|
-
return { applied: false, reason: "`value` is required for add", body };
|
|
147
|
-
}
|
|
148
|
-
if (matchIdx >= 0) {
|
|
149
|
-
return {
|
|
150
|
-
applied: false,
|
|
151
|
-
reason: `Row "${op.key}" already exists — use update or remove first`,
|
|
152
|
-
body,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const after = renderRow(op);
|
|
157
|
-
// Find the last existing token row to anchor the insertion. When
|
|
158
|
-
// none exist, insert at the bottom of the section before any
|
|
159
|
-
// trailing whitespace.
|
|
160
|
-
let insertAt = lines.length;
|
|
161
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
162
|
-
if (parseTokenRow(lines[i]!, op.section) !== null) {
|
|
163
|
-
insertAt = i + 1;
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
lines.splice(insertAt, 0, after);
|
|
168
|
-
// Ensure the section body keeps its trailing blank line before the
|
|
169
|
-
// next H2 (or EOF) so subsequent patches don't crowd headers.
|
|
170
|
-
let next = lines.join("\n");
|
|
171
|
-
if (!next.endsWith("\n")) next = `${next}\n`;
|
|
172
|
-
return { applied: true, body: next, after };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
interface ParsedTokenRow {
|
|
176
|
-
name: string;
|
|
177
|
-
rest: string;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function parseTokenRow(line: string, section: PatchableSection): ParsedTokenRow | null {
|
|
181
|
-
if (section === "components") {
|
|
182
|
-
const h3 = /^###\s+(.+?)\s*$/.exec(line);
|
|
183
|
-
if (h3) return { name: h3[1]!, rest: "" };
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
const stripped = line.trim().replace(/^[-*+]\s*/, "");
|
|
187
|
-
if (!stripped || /^[#|]/.test(stripped)) return null;
|
|
188
|
-
const m = /^([^—:|]+?)\s*[—:–|]\s*(.+)$/.exec(stripped);
|
|
189
|
-
if (!m) return null;
|
|
190
|
-
return { name: m[1]!.replace(/[`*_]/g, "").trim(), rest: m[2]!.trim() };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function renderRow(op: PatchOperation): string {
|
|
194
|
-
if (op.section === "components") {
|
|
195
|
-
return `### ${op.key}`;
|
|
196
|
-
}
|
|
197
|
-
const value = op.value ?? "";
|
|
198
|
-
const role = op.role ? ` — ${op.role}` : "";
|
|
199
|
-
return `- ${op.key} — ${value}${role}`;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function slug(name: string): string {
|
|
203
|
-
return name
|
|
204
|
-
.normalize("NFKD")
|
|
205
|
-
.replace(/[^\w\s-]/g, "")
|
|
206
|
-
.trim()
|
|
207
|
-
.replace(/\s+/g, "-")
|
|
208
|
-
.toLowerCase();
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// ─── Multi-op sugar ───────────────────────────────────────────────────
|
|
212
|
-
|
|
213
|
-
export interface PatchBatchResult {
|
|
214
|
-
next: string;
|
|
215
|
-
results: PatchResult[];
|
|
216
|
-
appliedCount: number;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Apply a list of operations in order. Each operation runs against
|
|
221
|
-
* the cumulative source — later ops see earlier ones. Failures are
|
|
222
|
-
* surfaced per-entry but never abort the batch (so a partial success
|
|
223
|
-
* is observable).
|
|
224
|
-
*/
|
|
225
|
-
export function patchDesignMdBatch(
|
|
226
|
-
source: string,
|
|
227
|
-
ops: readonly PatchOperation[],
|
|
228
|
-
): PatchBatchResult {
|
|
229
|
-
let current = source;
|
|
230
|
-
const results: PatchResult[] = [];
|
|
231
|
-
let appliedCount = 0;
|
|
232
|
-
for (const op of ops) {
|
|
233
|
-
const r = patchDesignMd(current, op);
|
|
234
|
-
results.push(r);
|
|
235
|
-
if (r.applied) {
|
|
236
|
-
appliedCount++;
|
|
237
|
-
current = r.next;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return { next: current, results, appliedCount };
|
|
241
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Section-safe DESIGN.md patcher (Issue #245 M4 §3.5).
|
|
3
|
+
*
|
|
4
|
+
* The patcher rewrites only the *body* of a target H2 section, leaving:
|
|
5
|
+
* - the H1 title and other H2 sections untouched (verbatim)
|
|
6
|
+
* - the target heading line itself untouched
|
|
7
|
+
* - any free-form prose between the heading and the first structured
|
|
8
|
+
* row preserved
|
|
9
|
+
*
|
|
10
|
+
* Operations are scoped to one `(section, key)` pair at a time so an
|
|
11
|
+
* agent can stream multiple patches without re-loading the file. The
|
|
12
|
+
* `dryRun` flag returns the would-be next source without writing it,
|
|
13
|
+
* so MCP tools can show the user a diff before committing.
|
|
14
|
+
*
|
|
15
|
+
* Token row rules per section:
|
|
16
|
+
* - color-palette / shadows / layout / typography: bullet rows of
|
|
17
|
+
* the form `- <Name> — <value>` (extra columns preserved verbatim
|
|
18
|
+
* for `update`).
|
|
19
|
+
* - components: H3 sub-headings with optional bullet body.
|
|
20
|
+
*
|
|
21
|
+
* Unsupported sections return `{ applied: false, reason: ... }` —
|
|
22
|
+
* they're free-form by design. Callers decide whether to surface the
|
|
23
|
+
* limitation or fall back to a hand-edit.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
export type PatchableSection =
|
|
28
|
+
| "color-palette"
|
|
29
|
+
| "typography"
|
|
30
|
+
| "layout"
|
|
31
|
+
| "shadows"
|
|
32
|
+
| "components";
|
|
33
|
+
|
|
34
|
+
export interface PatchOperation {
|
|
35
|
+
section: PatchableSection;
|
|
36
|
+
/** "add" creates the row; "update" replaces an existing matching row;
|
|
37
|
+
* "remove" deletes a matching row. */
|
|
38
|
+
operation: "add" | "update" | "remove";
|
|
39
|
+
/** Token name. Match is case-insensitive on the slug. */
|
|
40
|
+
key: string;
|
|
41
|
+
/** Required for `add` / `update`. Free-form value (`#FF8C42`,
|
|
42
|
+
* `Inter, sans-serif`, `0 1px 3px rgba(...)`). */
|
|
43
|
+
value?: string;
|
|
44
|
+
/** Optional functional role / usage hint (color/shadow). */
|
|
45
|
+
role?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PatchResult {
|
|
49
|
+
applied: boolean;
|
|
50
|
+
/** Reason the operation was a no-op or rejected. */
|
|
51
|
+
reason?: string;
|
|
52
|
+
/** Source after applying — same as input when not applied. */
|
|
53
|
+
next: string;
|
|
54
|
+
/** Old row (for `update` / `remove`). Undefined for clean adds. */
|
|
55
|
+
before?: string;
|
|
56
|
+
/** New row (for `add` / `update`). Undefined for `remove`. */
|
|
57
|
+
after?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const HEADING_BY_SECTION: Record<PatchableSection, RegExp> = {
|
|
61
|
+
"color-palette": /^##\s+.*?(color|palette).*$/im,
|
|
62
|
+
typography: /^##\s+.*?(typograph|typeface|font|type scale).*$/im,
|
|
63
|
+
layout: /^##\s+.*?(layout|spacing|grid).*$/im,
|
|
64
|
+
shadows: /^##\s+.*?(shadow|elevation|depth).*$/im,
|
|
65
|
+
components: /^##\s+.*?(component|button|card|input).*$/im,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Apply a single patch. Pure — does not touch the filesystem.
|
|
70
|
+
*
|
|
71
|
+
* The result's `applied` flag tells the caller whether the source
|
|
72
|
+
* actually changed (e.g. `remove` of a non-existent key returns
|
|
73
|
+
* `applied: false` with `reason`).
|
|
74
|
+
*/
|
|
75
|
+
export function patchDesignMd(source: string, op: PatchOperation): PatchResult {
|
|
76
|
+
const headingRx = HEADING_BY_SECTION[op.section];
|
|
77
|
+
const headingMatch = headingRx.exec(source);
|
|
78
|
+
if (!headingMatch) {
|
|
79
|
+
return {
|
|
80
|
+
applied: false,
|
|
81
|
+
reason: `Section "${op.section}" not found in DESIGN.md`,
|
|
82
|
+
next: source,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const sectionStart = headingMatch.index + headingMatch[0].length;
|
|
87
|
+
const nextHeadingIdx = source.indexOf("\n## ", sectionStart);
|
|
88
|
+
const sectionEnd = nextHeadingIdx >= 0 ? nextHeadingIdx : source.length;
|
|
89
|
+
const sectionBody = source.slice(sectionStart, sectionEnd);
|
|
90
|
+
|
|
91
|
+
const updated = applyToBody(sectionBody, op);
|
|
92
|
+
if (!updated.applied) {
|
|
93
|
+
return { ...updated, next: source };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const next = source.slice(0, sectionStart) + updated.body + source.slice(sectionEnd);
|
|
97
|
+
return {
|
|
98
|
+
applied: true,
|
|
99
|
+
next,
|
|
100
|
+
before: updated.before,
|
|
101
|
+
after: updated.after,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface BodyApplyResult {
|
|
106
|
+
applied: boolean;
|
|
107
|
+
reason?: string;
|
|
108
|
+
body: string;
|
|
109
|
+
before?: string;
|
|
110
|
+
after?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function applyToBody(body: string, op: PatchOperation): BodyApplyResult {
|
|
114
|
+
const lines = body.split(/\r?\n/);
|
|
115
|
+
const targetSlug = slug(op.key);
|
|
116
|
+
|
|
117
|
+
const matchIdx = lines.findIndex((line) => {
|
|
118
|
+
const row = parseTokenRow(line, op.section);
|
|
119
|
+
return row !== null && slug(row.name) === targetSlug;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (op.operation === "remove") {
|
|
123
|
+
if (matchIdx < 0) {
|
|
124
|
+
return { applied: false, reason: `No row with name "${op.key}"`, body };
|
|
125
|
+
}
|
|
126
|
+
const before = lines[matchIdx]!;
|
|
127
|
+
lines.splice(matchIdx, 1);
|
|
128
|
+
return { applied: true, body: lines.join("\n"), before };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (op.operation === "update") {
|
|
132
|
+
if (matchIdx < 0) {
|
|
133
|
+
return { applied: false, reason: `No row with name "${op.key}"`, body };
|
|
134
|
+
}
|
|
135
|
+
if (op.value === undefined) {
|
|
136
|
+
return { applied: false, reason: "`value` is required for update", body };
|
|
137
|
+
}
|
|
138
|
+
const before = lines[matchIdx]!;
|
|
139
|
+
const after = renderRow(op);
|
|
140
|
+
lines[matchIdx] = after;
|
|
141
|
+
return { applied: true, body: lines.join("\n"), before, after };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// add — value is required for token rows, optional for component H3
|
|
145
|
+
if (op.section !== "components" && op.value === undefined) {
|
|
146
|
+
return { applied: false, reason: "`value` is required for add", body };
|
|
147
|
+
}
|
|
148
|
+
if (matchIdx >= 0) {
|
|
149
|
+
return {
|
|
150
|
+
applied: false,
|
|
151
|
+
reason: `Row "${op.key}" already exists — use update or remove first`,
|
|
152
|
+
body,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const after = renderRow(op);
|
|
157
|
+
// Find the last existing token row to anchor the insertion. When
|
|
158
|
+
// none exist, insert at the bottom of the section before any
|
|
159
|
+
// trailing whitespace.
|
|
160
|
+
let insertAt = lines.length;
|
|
161
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
162
|
+
if (parseTokenRow(lines[i]!, op.section) !== null) {
|
|
163
|
+
insertAt = i + 1;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
lines.splice(insertAt, 0, after);
|
|
168
|
+
// Ensure the section body keeps its trailing blank line before the
|
|
169
|
+
// next H2 (or EOF) so subsequent patches don't crowd headers.
|
|
170
|
+
let next = lines.join("\n");
|
|
171
|
+
if (!next.endsWith("\n")) next = `${next}\n`;
|
|
172
|
+
return { applied: true, body: next, after };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
interface ParsedTokenRow {
|
|
176
|
+
name: string;
|
|
177
|
+
rest: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseTokenRow(line: string, section: PatchableSection): ParsedTokenRow | null {
|
|
181
|
+
if (section === "components") {
|
|
182
|
+
const h3 = /^###\s+(.+?)\s*$/.exec(line);
|
|
183
|
+
if (h3) return { name: h3[1]!, rest: "" };
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const stripped = line.trim().replace(/^[-*+]\s*/, "");
|
|
187
|
+
if (!stripped || /^[#|]/.test(stripped)) return null;
|
|
188
|
+
const m = /^([^—:|]+?)\s*[—:–|]\s*(.+)$/.exec(stripped);
|
|
189
|
+
if (!m) return null;
|
|
190
|
+
return { name: m[1]!.replace(/[`*_]/g, "").trim(), rest: m[2]!.trim() };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function renderRow(op: PatchOperation): string {
|
|
194
|
+
if (op.section === "components") {
|
|
195
|
+
return `### ${op.key}`;
|
|
196
|
+
}
|
|
197
|
+
const value = op.value ?? "";
|
|
198
|
+
const role = op.role ? ` — ${op.role}` : "";
|
|
199
|
+
return `- ${op.key} — ${value}${role}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function slug(name: string): string {
|
|
203
|
+
return name
|
|
204
|
+
.normalize("NFKD")
|
|
205
|
+
.replace(/[^\w\s-]/g, "")
|
|
206
|
+
.trim()
|
|
207
|
+
.replace(/\s+/g, "-")
|
|
208
|
+
.toLowerCase();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─── Multi-op sugar ───────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
export interface PatchBatchResult {
|
|
214
|
+
next: string;
|
|
215
|
+
results: PatchResult[];
|
|
216
|
+
appliedCount: number;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Apply a list of operations in order. Each operation runs against
|
|
221
|
+
* the cumulative source — later ops see earlier ones. Failures are
|
|
222
|
+
* surfaced per-entry but never abort the batch (so a partial success
|
|
223
|
+
* is observable).
|
|
224
|
+
*/
|
|
225
|
+
export function patchDesignMdBatch(
|
|
226
|
+
source: string,
|
|
227
|
+
ops: readonly PatchOperation[],
|
|
228
|
+
): PatchBatchResult {
|
|
229
|
+
let current = source;
|
|
230
|
+
const results: PatchResult[] = [];
|
|
231
|
+
let appliedCount = 0;
|
|
232
|
+
for (const op of ops) {
|
|
233
|
+
const r = patchDesignMd(current, op);
|
|
234
|
+
results.push(r);
|
|
235
|
+
if (r.applied) {
|
|
236
|
+
appliedCount++;
|
|
237
|
+
current = r.next;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return { next: current, results, appliedCount };
|
|
241
|
+
}
|