@vendoai/vendo 0.4.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/LICENSE +202 -0
- package/README.md +28 -0
- package/bin/vendo.mjs +4 -0
- package/dist/ai-sdk.d.ts +35 -0
- package/dist/ai-sdk.js +62 -0
- package/dist/auth-presets/auth-js.d.ts +14 -0
- package/dist/auth-presets/auth-js.js +91 -0
- package/dist/auth-presets/auth0.d.ts +23 -0
- package/dist/auth-presets/auth0.js +98 -0
- package/dist/auth-presets/clerk.d.ts +23 -0
- package/dist/auth-presets/clerk.js +65 -0
- package/dist/auth-presets/conformance.d.ts +42 -0
- package/dist/auth-presets/conformance.js +139 -0
- package/dist/auth-presets/identity.d.ts +56 -0
- package/dist/auth-presets/identity.js +152 -0
- package/dist/auth-presets/index.d.ts +14 -0
- package/dist/auth-presets/index.js +12 -0
- package/dist/auth-presets/jwt.d.ts +16 -0
- package/dist/auth-presets/jwt.js +54 -0
- package/dist/auth-presets/shared.d.ts +35 -0
- package/dist/auth-presets/shared.js +1 -0
- package/dist/auth-presets/supabase.d.ts +42 -0
- package/dist/auth-presets/supabase.js +229 -0
- package/dist/byo-approvals.d.ts +37 -0
- package/dist/byo-approvals.js +189 -0
- package/dist/capability-misses.d.ts +38 -0
- package/dist/capability-misses.js +172 -0
- package/dist/catalog.d.ts +19 -0
- package/dist/catalog.js +152 -0
- package/dist/cli/cloud/args.d.ts +3 -0
- package/dist/cli/cloud/args.js +32 -0
- package/dist/cli/cloud/auth.d.ts +17 -0
- package/dist/cli/cloud/auth.js +123 -0
- package/dist/cli/cloud/client.d.ts +28 -0
- package/dist/cli/cloud/client.js +133 -0
- package/dist/cli/cloud/command.d.ts +22 -0
- package/dist/cli/cloud/command.js +73 -0
- package/dist/cli/cloud/deploy.d.ts +26 -0
- package/dist/cli/cloud/deploy.js +187 -0
- package/dist/cli/cloud/device-login.d.ts +32 -0
- package/dist/cli/cloud/device-login.js +259 -0
- package/dist/cli/cloud/index.d.ts +5 -0
- package/dist/cli/cloud/index.js +59 -0
- package/dist/cli/cloud/keys.d.ts +2 -0
- package/dist/cli/cloud/keys.js +31 -0
- package/dist/cli/cloud/members.d.ts +3 -0
- package/dist/cli/cloud/members.js +25 -0
- package/dist/cli/cloud/output.d.ts +4 -0
- package/dist/cli/cloud/output.js +12 -0
- package/dist/cli/cloud/pending-claim.d.ts +29 -0
- package/dist/cli/cloud/pending-claim.js +38 -0
- package/dist/cli/cloud/read.d.ts +3 -0
- package/dist/cli/cloud/read.js +12 -0
- package/dist/cli/cloud/services.d.ts +2 -0
- package/dist/cli/cloud/services.js +9 -0
- package/dist/cli/cloud/session.d.ts +13 -0
- package/dist/cli/cloud/session.js +41 -0
- package/dist/cli/cloud-init.d.ts +60 -0
- package/dist/cli/cloud-init.js +149 -0
- package/dist/cli/dep-versions.d.ts +26 -0
- package/dist/cli/dep-versions.js +77 -0
- package/dist/cli/doctor-codes.d.ts +58 -0
- package/dist/cli/doctor-codes.js +60 -0
- package/dist/cli/doctor-live.d.ts +67 -0
- package/dist/cli/doctor-live.js +169 -0
- package/dist/cli/doctor.d.ts +52 -0
- package/dist/cli/doctor.js +467 -0
- package/dist/cli/eject.d.ts +29 -0
- package/dist/cli/eject.js +141 -0
- package/dist/cli/extract/apply.d.ts +26 -0
- package/dist/cli/extract/apply.js +123 -0
- package/dist/cli/extract/claude-cli-harness.d.ts +18 -0
- package/dist/cli/extract/claude-cli-harness.js +132 -0
- package/dist/cli/extract/claude-harness.d.ts +14 -0
- package/dist/cli/extract/claude-harness.js +127 -0
- package/dist/cli/extract/codex-cli-harness.d.ts +31 -0
- package/dist/cli/extract/codex-cli-harness.js +154 -0
- package/dist/cli/extract/delegate.d.ts +21 -0
- package/dist/cli/extract/delegate.js +77 -0
- package/dist/cli/extract/extraction.d.ts +65 -0
- package/dist/cli/extract/extraction.js +300 -0
- package/dist/cli/extract/gateway-fuel.d.ts +69 -0
- package/dist/cli/extract/gateway-fuel.js +69 -0
- package/dist/cli/extract/harness.d.ts +114 -0
- package/dist/cli/extract/harness.js +84 -0
- package/dist/cli/extract/index.d.ts +9 -0
- package/dist/cli/extract/index.js +9 -0
- package/dist/cli/extract/npx-engine-harness.d.ts +73 -0
- package/dist/cli/extract/npx-engine-harness.js +225 -0
- package/dist/cli/extract/stages.d.ts +149 -0
- package/dist/cli/extract/stages.js +398 -0
- package/dist/cli/framework.d.ts +8 -0
- package/dist/cli/framework.js +36 -0
- package/dist/cli/init-auth.d.ts +67 -0
- package/dist/cli/init-auth.js +142 -0
- package/dist/cli/init-scaffolds.d.ts +28 -0
- package/dist/cli/init-scaffolds.js +235 -0
- package/dist/cli/init.d.ts +115 -0
- package/dist/cli/init.js +1008 -0
- package/dist/cli/mcp/index.d.ts +7 -0
- package/dist/cli/mcp/index.js +59 -0
- package/dist/cli/mcp/registry.d.ts +9 -0
- package/dist/cli/mcp/registry.js +117 -0
- package/dist/cli/mcp/server-json.d.ts +12 -0
- package/dist/cli/mcp/server-json.js +65 -0
- package/dist/cli/mcp/server.schema.json +574 -0
- package/dist/cli/mcp/verify-domain.d.ts +11 -0
- package/dist/cli/mcp/verify-domain.js +48 -0
- package/dist/cli/playground/app/embed-entry.d.ts +20 -0
- package/dist/cli/playground/app/embed-entry.js +46 -0
- package/dist/cli/playground/app/fake-client.d.ts +3 -0
- package/dist/cli/playground/app/fake-client.js +215 -0
- package/dist/cli/playground/app/fixtures.d.ts +47 -0
- package/dist/cli/playground/app/fixtures.js +472 -0
- package/dist/cli/playground/app/main.d.ts +1 -0
- package/dist/cli/playground/app/main.js +108 -0
- package/dist/cli/playground/app/scenario-mount.d.ts +13 -0
- package/dist/cli/playground/app/scenario-mount.js +48 -0
- package/dist/cli/playground/app/scenarios.d.ts +25 -0
- package/dist/cli/playground/app/scenarios.js +152 -0
- package/dist/cli/playground/app/theme-editor.d.ts +13 -0
- package/dist/cli/playground/app/theme-editor.js +149 -0
- package/dist/cli/playground/app/theme-state.d.ts +29 -0
- package/dist/cli/playground/app/theme-state.js +151 -0
- package/dist/cli/playground/bundle.gen.d.ts +2 -0
- package/dist/cli/playground/bundle.gen.js +2 -0
- package/dist/cli/playground/embed-bundle.gen.d.ts +2 -0
- package/dist/cli/playground/embed-bundle.gen.js +2 -0
- package/dist/cli/playground.d.ts +33 -0
- package/dist/cli/playground.js +120 -0
- package/dist/cli/pretty.d.ts +48 -0
- package/dist/cli/pretty.js +330 -0
- package/dist/cli/refine.d.ts +43 -0
- package/dist/cli/refine.js +181 -0
- package/dist/cli/semantics.d.ts +31 -0
- package/dist/cli/semantics.js +122 -0
- package/dist/cli/shared.d.ts +68 -0
- package/dist/cli/shared.js +184 -0
- package/dist/cli/sync.d.ts +36 -0
- package/dist/cli/sync.js +202 -0
- package/dist/cli/theme/color.d.ts +15 -0
- package/dist/cli/theme/color.js +188 -0
- package/dist/cli/theme/css-vars.d.ts +13 -0
- package/dist/cli/theme/css-vars.js +40 -0
- package/dist/cli/theme/entry-candidates.d.ts +5 -0
- package/dist/cli/theme/entry-candidates.js +22 -0
- package/dist/cli/theme/extract-theme.d.ts +184 -0
- package/dist/cli/theme/extract-theme.js +358 -0
- package/dist/cli/theme/walk.d.ts +2 -0
- package/dist/cli/theme/walk.js +24 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +333 -0
- package/dist/cloud-apps.d.ts +17 -0
- package/dist/cloud-apps.js +46 -0
- package/dist/cloud-console.d.ts +32 -0
- package/dist/cloud-console.js +72 -0
- package/dist/cloud-tools.d.ts +23 -0
- package/dist/cloud-tools.js +196 -0
- package/dist/connections.d.ts +71 -0
- package/dist/connections.js +192 -0
- package/dist/deployment-identity.d.ts +12 -0
- package/dist/deployment-identity.js +69 -0
- package/dist/dev-creds/model.d.ts +65 -0
- package/dist/dev-creds/model.js +159 -0
- package/dist/dev-creds/resolve.d.ts +36 -0
- package/dist/dev-creds/resolve.js +57 -0
- package/dist/hosted-store.d.ts +52 -0
- package/dist/hosted-store.js +322 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1 -0
- package/dist/mastra.d.ts +46 -0
- package/dist/mastra.js +88 -0
- package/dist/react.d.ts +10 -0
- package/dist/react.js +32 -0
- package/dist/refine.d.ts +244 -0
- package/dist/refine.js +599 -0
- package/dist/remixable.d.ts +18 -0
- package/dist/remixable.js +42 -0
- package/dist/runtime-capture.d.ts +18 -0
- package/dist/runtime-capture.js +111 -0
- package/dist/sandbox-wire.d.ts +101 -0
- package/dist/sandbox-wire.js +101 -0
- package/dist/sandbox.d.ts +38 -0
- package/dist/sandbox.js +389 -0
- package/dist/server.d.ts +216 -0
- package/dist/server.js +1320 -0
- package/dist/sync-impact.d.ts +14 -0
- package/dist/sync-impact.js +75 -0
- package/dist/turn-liveness.d.ts +33 -0
- package/dist/turn-liveness.js +105 -0
- package/dist/wire/approvals.d.ts +10 -0
- package/dist/wire/approvals.js +59 -0
- package/dist/wire/apps.d.ts +5 -0
- package/dist/wire/apps.js +155 -0
- package/dist/wire/automations.d.ts +5 -0
- package/dist/wire/automations.js +57 -0
- package/dist/wire/box.d.ts +3 -0
- package/dist/wire/box.js +268 -0
- package/dist/wire/connections.d.ts +5 -0
- package/dist/wire/connections.js +47 -0
- package/dist/wire/context.d.ts +22 -0
- package/dist/wire/context.js +190 -0
- package/dist/wire/doctor.d.ts +19 -0
- package/dist/wire/doctor.js +116 -0
- package/dist/wire/misc.d.ts +18 -0
- package/dist/wire/misc.js +227 -0
- package/dist/wire/shared.d.ts +158 -0
- package/dist/wire/shared.js +134 -0
- package/dist/wire/threads.d.ts +3 -0
- package/dist/wire/threads.js +68 -0
- package/package.json +151 -0
- package/skills/vendo-setup/SKILL.md +128 -0
package/dist/cli/sync.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { vendoSync } from "@vendoai/actions";
|
|
3
|
+
import { pushSyncReport } from "./cloud/services.js";
|
|
4
|
+
import { syncSemantics } from "./semantics.js";
|
|
5
|
+
import { consoleOutput, withCommandRun } from "./shared.js";
|
|
6
|
+
function impactResponse(value) {
|
|
7
|
+
if (typeof value !== "object" || value === null || !Array.isArray(value.impact)) {
|
|
8
|
+
throw new Error("invalid sync impact response");
|
|
9
|
+
}
|
|
10
|
+
const impact = value.impact;
|
|
11
|
+
for (const entry of impact) {
|
|
12
|
+
if (typeof entry !== "object" || entry === null)
|
|
13
|
+
throw new Error("invalid sync impact response");
|
|
14
|
+
const candidate = entry;
|
|
15
|
+
if (typeof candidate.tool !== "string" || !Array.isArray(candidate.apps)
|
|
16
|
+
|| !Array.isArray(candidate.automations) || typeof candidate.grants !== "number") {
|
|
17
|
+
throw new Error("invalid sync impact response");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return impact;
|
|
21
|
+
}
|
|
22
|
+
function printImpact(output, impact) {
|
|
23
|
+
for (const entry of impact) {
|
|
24
|
+
const categories = [
|
|
25
|
+
[entry.automations.length, "automation"],
|
|
26
|
+
[entry.apps.length, "app"],
|
|
27
|
+
[entry.grants, "grant"],
|
|
28
|
+
];
|
|
29
|
+
const references = categories
|
|
30
|
+
.filter(([count]) => count > 0)
|
|
31
|
+
.map(([count, label]) => `${count} ${label}${count === 1 ? "" : "s"}`);
|
|
32
|
+
output.log(references.length === 0
|
|
33
|
+
? `impact: ${entry.tool} no saved references`
|
|
34
|
+
: `impact: ${entry.tool} breaks ${references.join(", ")}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function nonzero(entry) {
|
|
38
|
+
return entry.apps.length > 0 || entry.automations.length > 0 || entry.grants > 0;
|
|
39
|
+
}
|
|
40
|
+
/** 04-actions §1 / 09-vendo §5 — fail-soft extraction, strict CI gate. */
|
|
41
|
+
export async function runSync(options) {
|
|
42
|
+
return withCommandRun({
|
|
43
|
+
command: "sync",
|
|
44
|
+
root: options.targetDir,
|
|
45
|
+
...(options.telemetry === undefined ? {} : { telemetry: options.telemetry }),
|
|
46
|
+
}, () => sync(options));
|
|
47
|
+
}
|
|
48
|
+
async function sync(options) {
|
|
49
|
+
const output = options.output ?? consoleOutput;
|
|
50
|
+
const json = options.json === true;
|
|
51
|
+
// In --json mode, human lines that duplicate report fields are dropped and
|
|
52
|
+
// CLI-level events collect into `notes`; stdout carries exactly one object.
|
|
53
|
+
const notes = [];
|
|
54
|
+
const note = (message) => { if (json)
|
|
55
|
+
notes.push(message);
|
|
56
|
+
else
|
|
57
|
+
output.log(message); };
|
|
58
|
+
const noteError = (message) => { if (json)
|
|
59
|
+
notes.push(message);
|
|
60
|
+
else
|
|
61
|
+
output.error(message); };
|
|
62
|
+
try {
|
|
63
|
+
const root = resolve(options.targetDir);
|
|
64
|
+
const report = await (options.sync ?? vendoSync)({
|
|
65
|
+
root,
|
|
66
|
+
out: join(root, ".vendo"),
|
|
67
|
+
// The CLI needs the report to compute exit 2 vs 3; it applies strictness below.
|
|
68
|
+
strict: false,
|
|
69
|
+
});
|
|
70
|
+
if (!json) {
|
|
71
|
+
for (const warning of report.warnings)
|
|
72
|
+
output.error(`warning: ${warning}`);
|
|
73
|
+
output.log(`tools: +${report.tools.added.length} -${report.tools.removed.length} ~${report.tools.changed.length}`);
|
|
74
|
+
output.log(`pins: ${report.pins.captured.length} captured, ${report.pins.drifted.length} drifted`);
|
|
75
|
+
output.log(`catalog.json: ${report.catalog.discovered} discovered, ${report.catalog.registered} registered`);
|
|
76
|
+
if (report.pins.drifted.length > 0) {
|
|
77
|
+
// 06-apps §8 — drift never auto-rebases: the fork's owner decides.
|
|
78
|
+
output.log(`drifted: ${report.pins.drifted.join(", ")} — existing forks stay on the old capture until each owner rebases (POST /apps/:id/rebase-pin or the vendo_apps_rebase_pin agent tool)`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Unresolved slots fail the run regardless of --strict (silent remix skips
|
|
82
|
+
// are eliminated), but impact analysis, the report push, and the breaking
|
|
83
|
+
// gate below still execute so the most severe exit code wins. In --json
|
|
84
|
+
// mode the human lines are dropped: the pins ride in report.unresolvedPins.
|
|
85
|
+
let unresolvedExit = 0;
|
|
86
|
+
if (report.unresolvedPins.length > 0) {
|
|
87
|
+
if (!json) {
|
|
88
|
+
output.error("unresolved remixable slots:");
|
|
89
|
+
for (const pin of report.unresolvedPins) {
|
|
90
|
+
output.error(` ${pin.slot} [${pin.reason}]: ${pin.hint}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
unresolvedExit = 2;
|
|
94
|
+
}
|
|
95
|
+
const wireUrl = (options.url ?? process.env.VENDO_URL ?? "http://localhost:3000/api/vendo").replace(/\/+$/, "");
|
|
96
|
+
// W3 — field semantics + domain manifest into .vendo/semantics.json
|
|
97
|
+
// (one-time inference via the dev server; host edits preserved).
|
|
98
|
+
// Fail-soft like everything else in sync.
|
|
99
|
+
try {
|
|
100
|
+
await syncSemantics({
|
|
101
|
+
vendoDir: join(root, ".vendo"),
|
|
102
|
+
url: wireUrl,
|
|
103
|
+
...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
|
|
104
|
+
note,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
note(`semantics sync failed soft: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
109
|
+
}
|
|
110
|
+
const tools = [...new Set([
|
|
111
|
+
...report.breaking.map((breaking) => breaking.tool),
|
|
112
|
+
...report.tools.changed,
|
|
113
|
+
])];
|
|
114
|
+
let impact;
|
|
115
|
+
if (tools.length > 0) {
|
|
116
|
+
const impactUrl = wireUrl;
|
|
117
|
+
try {
|
|
118
|
+
const response = await (options.fetchImpl ?? fetch)(`${impactUrl}/sync/impact`, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers: { accept: "application/json", "content-type": "application/json" },
|
|
121
|
+
body: JSON.stringify({ tools }),
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok)
|
|
124
|
+
throw new Error(`sync impact returned ${response.status}`);
|
|
125
|
+
impact = impactResponse(await response.json());
|
|
126
|
+
if (!json)
|
|
127
|
+
printImpact(output, impact);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
note(`impact unknown — dev server not reachable at ${impactUrl}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (options.report === true) {
|
|
134
|
+
const apiKey = options.apiKey ?? process.env.VENDO_API_KEY;
|
|
135
|
+
if (!apiKey) {
|
|
136
|
+
noteError("--report requires VENDO_API_KEY or --key");
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const payload = {
|
|
140
|
+
report,
|
|
141
|
+
...(impact === undefined ? {} : { impact }),
|
|
142
|
+
at: new Date().toISOString(),
|
|
143
|
+
};
|
|
144
|
+
try {
|
|
145
|
+
if (options.push !== undefined)
|
|
146
|
+
await options.push(payload);
|
|
147
|
+
else
|
|
148
|
+
await pushSyncReport(payload, {
|
|
149
|
+
apiKey,
|
|
150
|
+
...(options.apiUrl === undefined ? {} : { apiUrl: options.apiUrl }),
|
|
151
|
+
...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
noteError(`warning: failed to push sync report: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Unresolved slots set the floor; the strict breaking gate overrides with
|
|
160
|
+
// its own (equal-or-worse) code, so the most severe exit wins.
|
|
161
|
+
let exitCode = unresolvedExit;
|
|
162
|
+
if (options.strict === true && report.breaking.length > 0) {
|
|
163
|
+
if (!json)
|
|
164
|
+
for (const breaking of report.breaking)
|
|
165
|
+
output.error(`breaking: ${breaking.tool} ${breaking.change}`);
|
|
166
|
+
const breakingTools = new Set(report.breaking.map((breaking) => breaking.tool));
|
|
167
|
+
exitCode = impact?.some((entry) => breakingTools.has(entry.tool) && nonzero(entry)) === true ? 3 : 2;
|
|
168
|
+
}
|
|
169
|
+
if (json) {
|
|
170
|
+
const result = {
|
|
171
|
+
ok: exitCode === 0,
|
|
172
|
+
exitCode,
|
|
173
|
+
report,
|
|
174
|
+
// Nothing changed → nothing could be impacted; changes without a
|
|
175
|
+
// reachable dev server → unknown, surfaced as null plus a note.
|
|
176
|
+
impact: impact ?? (tools.length === 0 ? [] : null),
|
|
177
|
+
notes,
|
|
178
|
+
};
|
|
179
|
+
output.log(JSON.stringify(result, null, 2));
|
|
180
|
+
}
|
|
181
|
+
return exitCode;
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
const message = `sync failed soft: ${error instanceof Error ? error.message : "unknown error"}`;
|
|
185
|
+
const exitCode = options.strict === true ? 2 : 0;
|
|
186
|
+
if (json) {
|
|
187
|
+
const result = {
|
|
188
|
+
ok: exitCode === 0,
|
|
189
|
+
exitCode,
|
|
190
|
+
report: { tools: { added: [], removed: [], changed: [] }, breaking: [], pins: { captured: [], drifted: [] }, unresolvedPins: [], catalog: { discovered: 0, registered: 0 }, warnings: [] },
|
|
191
|
+
impact: null,
|
|
192
|
+
notes,
|
|
193
|
+
error: message,
|
|
194
|
+
};
|
|
195
|
+
output.log(JSON.stringify(result, null, 2));
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
output.error(`warning: ${message}`);
|
|
199
|
+
}
|
|
200
|
+
return exitCode;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CssVarDecl } from "./css-vars.js";
|
|
2
|
+
/** Any supported CSS color form to lowercase 6-digit hex; null when not one.
|
|
3
|
+
* A trailing `!important` is declaration noise, not color — stripped. */
|
|
4
|
+
export declare function normalizeColor(value: string): string | null;
|
|
5
|
+
/**
|
|
6
|
+
* Resolve `var(--x)` / `var(--x, fallback)` references against the collected
|
|
7
|
+
* declarations (bounded depth). Returns null when a reference cannot be
|
|
8
|
+
* resolved — theme.json carries fully-resolved primitives only (frozen theme
|
|
9
|
+
* contract), so an unresolved reference means the slot is not exactly readable.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveCssVarRefs(value: string, vars: CssVarDecl[], depth?: number): string | null;
|
|
12
|
+
/** px/rem length to canonical px; null for any other unit or expression. */
|
|
13
|
+
export declare function normalizeLength(value: string): string | null;
|
|
14
|
+
/** Pick black or white by the larger WCAG contrast ratio against the accent. */
|
|
15
|
+
export declare function contrastingText(accent: string): "#000000" | "#ffffff";
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic CSS color/length normalization for exact token reads. Every
|
|
3
|
+
* conversion here is published spec math (CSS Color 4), not inference: a
|
|
4
|
+
* conventional token's declared value is turned into the concrete hex/px the
|
|
5
|
+
* frozen VendoTheme contract carries. Values with real transparency are
|
|
6
|
+
* rejected (theme slots are opaque paints), as is anything unparseable —
|
|
7
|
+
* an unreadable value is treated as absent, never guessed at.
|
|
8
|
+
*/
|
|
9
|
+
const HEX = /^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
10
|
+
function normalizeHex(value) {
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
if (!HEX.test(trimmed))
|
|
13
|
+
return null;
|
|
14
|
+
let h = trimmed.slice(1);
|
|
15
|
+
if (h.length === 3 || h.length === 4)
|
|
16
|
+
h = [...h].map((c) => c + c).join("");
|
|
17
|
+
if (h.length === 8) {
|
|
18
|
+
const alpha = parseInt(h.slice(6, 8), 16);
|
|
19
|
+
if (alpha !== 255)
|
|
20
|
+
return null;
|
|
21
|
+
h = h.slice(0, 6);
|
|
22
|
+
}
|
|
23
|
+
return `#${h.slice(0, 6).toLowerCase()}`;
|
|
24
|
+
}
|
|
25
|
+
function clamp01(value) {
|
|
26
|
+
return Math.min(1, Math.max(0, value));
|
|
27
|
+
}
|
|
28
|
+
function byteToHex(value) {
|
|
29
|
+
return Math.round(clamp01(value) * 255).toString(16).padStart(2, "0");
|
|
30
|
+
}
|
|
31
|
+
function rgbToHex(r, g, b) {
|
|
32
|
+
return `#${byteToHex(r)}${byteToHex(g)}${byteToHex(b)}`;
|
|
33
|
+
}
|
|
34
|
+
function parseAlpha(value) {
|
|
35
|
+
if (!value)
|
|
36
|
+
return 1;
|
|
37
|
+
const trimmed = value.trim();
|
|
38
|
+
if (trimmed.endsWith("%"))
|
|
39
|
+
return Number(trimmed.slice(0, -1)) / 100;
|
|
40
|
+
return Number(trimmed);
|
|
41
|
+
}
|
|
42
|
+
function hslToHex(hue, saturation, lightness) {
|
|
43
|
+
const h = (((hue % 360) + 360) % 360) / 360;
|
|
44
|
+
const s = clamp01(saturation / 100);
|
|
45
|
+
const l = clamp01(lightness / 100);
|
|
46
|
+
if (s === 0)
|
|
47
|
+
return rgbToHex(l, l, l);
|
|
48
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
49
|
+
const p = 2 * l - q;
|
|
50
|
+
const channel = (offset) => {
|
|
51
|
+
let t = h + offset;
|
|
52
|
+
if (t < 0)
|
|
53
|
+
t += 1;
|
|
54
|
+
if (t > 1)
|
|
55
|
+
t -= 1;
|
|
56
|
+
if (t < 1 / 6)
|
|
57
|
+
return p + (q - p) * 6 * t;
|
|
58
|
+
if (t < 1 / 2)
|
|
59
|
+
return q;
|
|
60
|
+
if (t < 2 / 3)
|
|
61
|
+
return p + (q - p) * (2 / 3 - t) * 6;
|
|
62
|
+
return p;
|
|
63
|
+
};
|
|
64
|
+
return rgbToHex(channel(1 / 3), channel(0), channel(-1 / 3));
|
|
65
|
+
}
|
|
66
|
+
/** `hsl()`/`hsla()` plus the bare shadcn triplet form (`222.2 47.4% 11.2%`). */
|
|
67
|
+
function parseHsl(value) {
|
|
68
|
+
const trimmed = value.trim();
|
|
69
|
+
const fn = trimmed.match(/^hsla?\(([\s\S]+)\)$/i);
|
|
70
|
+
const body = fn?.[1]?.trim() ?? trimmed;
|
|
71
|
+
const slashParts = body.split("/");
|
|
72
|
+
if (slashParts.length > 2)
|
|
73
|
+
return null;
|
|
74
|
+
const rawParts = slashParts[0].includes(",")
|
|
75
|
+
? slashParts[0].split(",").map((part) => part.trim())
|
|
76
|
+
: slashParts[0].trim().split(/\s+/);
|
|
77
|
+
const alpha = parseAlpha(slashParts[1] ?? (rawParts.length === 4 ? rawParts[3] : undefined));
|
|
78
|
+
if (!Number.isFinite(alpha) || alpha < 0.999)
|
|
79
|
+
return null;
|
|
80
|
+
const parts = rawParts.slice(0, 3);
|
|
81
|
+
if (parts.length !== 3 || !parts[1]?.endsWith("%") || !parts[2]?.endsWith("%"))
|
|
82
|
+
return null;
|
|
83
|
+
const hue = Number(parts[0]);
|
|
84
|
+
const saturation = Number(parts[1].slice(0, -1));
|
|
85
|
+
const lightness = Number(parts[2].slice(0, -1));
|
|
86
|
+
if (![hue, saturation, lightness].every(Number.isFinite))
|
|
87
|
+
return null;
|
|
88
|
+
return hslToHex(hue, saturation, lightness);
|
|
89
|
+
}
|
|
90
|
+
/** Bare space-separated `R G B` byte triplet (Tailwind `rgb(var(--x)) `style). */
|
|
91
|
+
function parseRgbTriplet(value) {
|
|
92
|
+
const body = value.trim().replace(/^rgba?\(([\s\S]+)\)$/i, "$1");
|
|
93
|
+
const slashParts = body.split("/");
|
|
94
|
+
if (slashParts.length > 2)
|
|
95
|
+
return null;
|
|
96
|
+
const alpha = parseAlpha(slashParts[1]);
|
|
97
|
+
if (!Number.isFinite(alpha) || alpha < 0.999)
|
|
98
|
+
return null;
|
|
99
|
+
const parts = slashParts[0].trim().split(/[\s,]+/);
|
|
100
|
+
if (parts.length !== 3 || parts.some((part) => part.endsWith("%")))
|
|
101
|
+
return null;
|
|
102
|
+
const channels = parts.map((part) => Number(part));
|
|
103
|
+
if (channels.some((channel) => !Number.isFinite(channel) || channel < 0 || channel > 255))
|
|
104
|
+
return null;
|
|
105
|
+
return `#${channels.map((channel) => Math.round(channel).toString(16).padStart(2, "0")).join("")}`;
|
|
106
|
+
}
|
|
107
|
+
function linearToSrgb(value) {
|
|
108
|
+
const v = clamp01(value);
|
|
109
|
+
return v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;
|
|
110
|
+
}
|
|
111
|
+
/** CSS Color 4 §"OKLCH" — the spec's published OKLab→sRGB constants. */
|
|
112
|
+
function parseOklch(value) {
|
|
113
|
+
const trimmed = value.trim();
|
|
114
|
+
const match = trimmed.match(/^oklch\(\s*([+-]?(?:\d+|\d*\.\d+)%?)\s+([+-]?(?:\d+|\d*\.\d+))\s+([+-]?(?:\d+|\d*\.\d+)(?:deg)?)\s*(?:\/\s*([^)]+))?\)$/i);
|
|
115
|
+
if (!match)
|
|
116
|
+
return null;
|
|
117
|
+
const alpha = parseAlpha(match[4]);
|
|
118
|
+
if (!Number.isFinite(alpha) || alpha < 0.999)
|
|
119
|
+
return null;
|
|
120
|
+
const lRaw = match[1];
|
|
121
|
+
const lightness = lRaw.endsWith("%") ? Number(lRaw.slice(0, -1)) / 100 : Number(lRaw);
|
|
122
|
+
const chroma = Number(match[2]);
|
|
123
|
+
const hue = Number(match[3].replace(/deg$/i, ""));
|
|
124
|
+
if (![lightness, chroma, hue].every(Number.isFinite))
|
|
125
|
+
return null;
|
|
126
|
+
const a = chroma * Math.cos((hue * Math.PI) / 180);
|
|
127
|
+
const b = chroma * Math.sin((hue * Math.PI) / 180);
|
|
128
|
+
const lPrime = lightness + 0.3963377774 * a + 0.2158037573 * b;
|
|
129
|
+
const mPrime = lightness - 0.1055613458 * a - 0.0638541728 * b;
|
|
130
|
+
const sPrime = lightness - 0.0894841775 * a - 1.291485548 * b;
|
|
131
|
+
const l = lPrime ** 3;
|
|
132
|
+
const m = mPrime ** 3;
|
|
133
|
+
const s = sPrime ** 3;
|
|
134
|
+
return rgbToHex(linearToSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s), linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s), linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s));
|
|
135
|
+
}
|
|
136
|
+
/** Any supported CSS color form to lowercase 6-digit hex; null when not one.
|
|
137
|
+
* A trailing `!important` is declaration noise, not color — stripped. */
|
|
138
|
+
export function normalizeColor(value) {
|
|
139
|
+
const bare = value.replace(/\s*!important\s*$/i, "");
|
|
140
|
+
return normalizeHex(bare) ?? parseRgbTriplet(bare) ?? parseHsl(bare) ?? parseOklch(bare);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Resolve `var(--x)` / `var(--x, fallback)` references against the collected
|
|
144
|
+
* declarations (bounded depth). Returns null when a reference cannot be
|
|
145
|
+
* resolved — theme.json carries fully-resolved primitives only (frozen theme
|
|
146
|
+
* contract), so an unresolved reference means the slot is not exactly readable.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveCssVarRefs(value, vars, depth = 6) {
|
|
149
|
+
if (!value.includes("var("))
|
|
150
|
+
return value;
|
|
151
|
+
if (depth <= 0)
|
|
152
|
+
return null;
|
|
153
|
+
const byName = new Map(vars.filter((v) => !v.darkScope).map((v) => [v.name, v.value]));
|
|
154
|
+
const substituted = value.replace(/var\(\s*(--[\w-]+)\s*(?:,\s*([^()]+))?\)/g, (_m, name, fallback) => {
|
|
155
|
+
return byName.get(name) ?? fallback?.trim() ?? "var()";
|
|
156
|
+
});
|
|
157
|
+
if (substituted.includes("var()"))
|
|
158
|
+
return null; // unresolvable reference
|
|
159
|
+
return resolveCssVarRefs(substituted, vars, depth - 1);
|
|
160
|
+
}
|
|
161
|
+
/** px/rem length to canonical px; null for any other unit or expression. */
|
|
162
|
+
export function normalizeLength(value) {
|
|
163
|
+
const trimmed = value.replace(/\s*!important\s*$/i, "").trim();
|
|
164
|
+
const px = trimmed.match(/^((?:\d+|\d*\.\d+))px$/);
|
|
165
|
+
if (px)
|
|
166
|
+
return `${Number(px[1])}px`;
|
|
167
|
+
const rem = trimmed.match(/^((?:\d+|\d*\.\d+))rem$/);
|
|
168
|
+
if (rem)
|
|
169
|
+
return `${Number(rem[1]) * 16}px`;
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
function relativeLuminance(hex) {
|
|
173
|
+
const normalized = normalizeHex(hex);
|
|
174
|
+
if (!normalized)
|
|
175
|
+
return 0;
|
|
176
|
+
const channels = [1, 3, 5].map((index) => parseInt(normalized.slice(index, index + 2), 16) / 255);
|
|
177
|
+
const [r, g, b] = channels.map((channel) => channel <= 0.04045
|
|
178
|
+
? channel / 12.92
|
|
179
|
+
: ((channel + 0.055) / 1.055) ** 2.4);
|
|
180
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
181
|
+
}
|
|
182
|
+
/** Pick black or white by the larger WCAG contrast ratio against the accent. */
|
|
183
|
+
export function contrastingText(accent) {
|
|
184
|
+
const luminance = relativeLuminance(accent);
|
|
185
|
+
const blackContrast = (luminance + 0.05) / 0.05;
|
|
186
|
+
const whiteContrast = 1.05 / (luminance + 0.05);
|
|
187
|
+
return blackContrast >= whiteContrast ? "#000000" : "#ffffff";
|
|
188
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal CSS custom-property declaration scanner. Not a full CSS parser —
|
|
3
|
+
* tracks brace depth and whether the enclosing block/at-rule looks dark-scoped
|
|
4
|
+
* (`.dark`, `[data-theme="dark"]`, `@media (prefers-color-scheme: dark)`).
|
|
5
|
+
* Good enough for design-token sheets; anything it misses is hand-editable output.
|
|
6
|
+
*/
|
|
7
|
+
export interface CssVarDecl {
|
|
8
|
+
name: string;
|
|
9
|
+
value: string;
|
|
10
|
+
file: string;
|
|
11
|
+
darkScope: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function parseCssVars(css: string, file: string): CssVarDecl[];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const DARK_SELECTOR = /(\.dark\b|\[data-theme=["']?dark["']?\]|prefers-color-scheme:\s*dark)/;
|
|
2
|
+
export function parseCssVars(css, file) {
|
|
3
|
+
const out = [];
|
|
4
|
+
// Strip comments first.
|
|
5
|
+
const src = css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
6
|
+
// Stack of "is this block dark-scoped" flags.
|
|
7
|
+
const darkStack = [];
|
|
8
|
+
let selectorBuf = "";
|
|
9
|
+
// A declaration ends at `;`, at the closing `}` of its block (trailing
|
|
10
|
+
// semicolons are optional in CSS and absent in minified sheets), or at
|
|
11
|
+
// end-of-input.
|
|
12
|
+
const flushDecl = () => {
|
|
13
|
+
const decl = selectorBuf.trim();
|
|
14
|
+
const m = decl.match(/^(--[\w-]+)\s*:\s*(.+)$/s);
|
|
15
|
+
if (m && m[1] && m[2]) {
|
|
16
|
+
out.push({ name: m[1], value: m[2].trim(), file, darkScope: darkStack.some(Boolean) });
|
|
17
|
+
}
|
|
18
|
+
selectorBuf = "";
|
|
19
|
+
};
|
|
20
|
+
for (let i = 0; i < src.length; i++) {
|
|
21
|
+
const ch = src[i];
|
|
22
|
+
if (ch === "{") {
|
|
23
|
+
const parentDark = darkStack.some(Boolean);
|
|
24
|
+
darkStack.push(parentDark || DARK_SELECTOR.test(selectorBuf));
|
|
25
|
+
selectorBuf = "";
|
|
26
|
+
}
|
|
27
|
+
else if (ch === "}") {
|
|
28
|
+
flushDecl();
|
|
29
|
+
darkStack.pop();
|
|
30
|
+
}
|
|
31
|
+
else if (ch === ";") {
|
|
32
|
+
flushDecl();
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
selectorBuf += ch;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
flushDecl();
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Conventional Next.js entry-file paths (app-router layouts + pages-router
|
|
2
|
+
* `_app`), relative to a target repo's root. extract-theme.ts reads the
|
|
3
|
+
* first hit as the root layout: the anchor for the CSS import graph and part
|
|
4
|
+
* of the LLM pass's evidence (next/font imports live here). */
|
|
5
|
+
export declare const ENTRY_FILE_CANDIDATES: string[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Conventional Next.js entry-file paths (app-router layouts + pages-router
|
|
2
|
+
* `_app`), relative to a target repo's root. extract-theme.ts reads the
|
|
3
|
+
* first hit as the root layout: the anchor for the CSS import graph and part
|
|
4
|
+
* of the LLM pass's evidence (next/font imports live here). */
|
|
5
|
+
export const ENTRY_FILE_CANDIDATES = [
|
|
6
|
+
"app/layout.tsx",
|
|
7
|
+
"app/layout.jsx",
|
|
8
|
+
"app/layout.ts",
|
|
9
|
+
"app/layout.js",
|
|
10
|
+
"src/app/layout.tsx",
|
|
11
|
+
"src/app/layout.jsx",
|
|
12
|
+
"src/app/layout.ts",
|
|
13
|
+
"src/app/layout.js",
|
|
14
|
+
"pages/_app.tsx",
|
|
15
|
+
"pages/_app.jsx",
|
|
16
|
+
"pages/_app.ts",
|
|
17
|
+
"pages/_app.js",
|
|
18
|
+
"src/pages/_app.tsx",
|
|
19
|
+
"src/pages/_app.jsx",
|
|
20
|
+
"src/pages/_app.ts",
|
|
21
|
+
"src/pages/_app.js",
|
|
22
|
+
];
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Theme extraction, exact-then-staged (kill-list §B2, re-derived Task 2/4):
|
|
4
|
+
*
|
|
5
|
+
* 1. Allowlist fast-path — conventional shadcn/Tailwind tokens are read
|
|
6
|
+
* EXACTLY (`--primary`, `--background`, `--font-sans`, ... and their
|
|
7
|
+
* Tailwind-v4 `--color-*` spellings). No name scoring, no inference. This
|
|
8
|
+
* file does ONLY this: `extractTheme` is fully deterministic — no model
|
|
9
|
+
* call, no network, no credential.
|
|
10
|
+
* 2. Whatever the allowlist leaves unfilled rides init's consent-gated AI
|
|
11
|
+
* pass (`runAiExtraction` → `runStagedExtraction`'s theme stage, in
|
|
12
|
+
* `../extract/stages.ts`) — the SAME harness seam as the tool-description
|
|
13
|
+
* polish, over Read/Glob/Grep instead of a fixed evidence-file set.
|
|
14
|
+
* `applyThemeDraft` below merges that stage's parsed artifact back onto
|
|
15
|
+
* this file's exact-only summary.
|
|
16
|
+
* 3. Anything neither path fills falls back to neutral defaults and is
|
|
17
|
+
* reported as defaulted — a miss is visible, never a silent wrong brand.
|
|
18
|
+
*
|
|
19
|
+
* theme.json stays the editable source of truth; init shows the palette for
|
|
20
|
+
* a one-glance confirm and asks only about model-flagged uncertainty.
|
|
21
|
+
*/
|
|
22
|
+
export interface ThemeSlotValues {
|
|
23
|
+
accent: string;
|
|
24
|
+
accentText: string;
|
|
25
|
+
background: string;
|
|
26
|
+
border: string;
|
|
27
|
+
danger: string;
|
|
28
|
+
surface: string;
|
|
29
|
+
text: string;
|
|
30
|
+
mutedText: string;
|
|
31
|
+
radius: string;
|
|
32
|
+
fontFamily: string;
|
|
33
|
+
headingFamily: string;
|
|
34
|
+
baseSize: string;
|
|
35
|
+
density: "compact" | "comfortable";
|
|
36
|
+
motion: "full" | "reduced";
|
|
37
|
+
}
|
|
38
|
+
export interface ThemeUncertainty {
|
|
39
|
+
slot: keyof ThemeSlotValues;
|
|
40
|
+
note: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ThemeSummary {
|
|
43
|
+
/** Fully resolved values used to assemble the frozen VendoTheme contract. */
|
|
44
|
+
slots: ThemeSlotValues;
|
|
45
|
+
/** slot -> provenance: the exact token name, "(model)", or a derivation. */
|
|
46
|
+
matched: Record<string, string>;
|
|
47
|
+
/** Slots that fell back to neutral defaults (reported, never silent). */
|
|
48
|
+
defaulted: string[];
|
|
49
|
+
/** Model-flagged genuine uncertainty — init's only theme question trigger. */
|
|
50
|
+
uncertain: ThemeUncertainty[];
|
|
51
|
+
usedModel: boolean;
|
|
52
|
+
errors: string[];
|
|
53
|
+
hasDarkVariant: boolean;
|
|
54
|
+
/** Slots the exact allowlist pass did not read exactly — the staged theme
|
|
55
|
+
* pass's `needed` input, and the only slots `applyThemeDraft` may fill. */
|
|
56
|
+
needed: SlotKey[];
|
|
57
|
+
/** The CSS/layout/tailwind-config paths the context gatherer collected,
|
|
58
|
+
* repo-relative — seeded as evidence-path hints for the staged pass. */
|
|
59
|
+
evidencePaths: string[];
|
|
60
|
+
}
|
|
61
|
+
type SlotKey = keyof ThemeSlotValues;
|
|
62
|
+
/** Brand-defining slots: any of these missing after the exact pass triggers
|
|
63
|
+
* the staged theme pass, and only doubt about these becomes an init
|
|
64
|
+
* question. The remaining slots (accentText, headingFamily, baseSize,
|
|
65
|
+
* density, motion) derive or default safely — accentText derives from the
|
|
66
|
+
* accent by WCAG contrast, headingFamily inherits fontFamily, and the rest
|
|
67
|
+
* have safe brand-neutral defaults — so they never trigger a call or a
|
|
68
|
+
* question on their own. */
|
|
69
|
+
export declare const BRAND_SLOTS: readonly SlotKey[];
|
|
70
|
+
export declare const modelThemeSchema: z.ZodObject<{
|
|
71
|
+
slots: z.ZodObject<{
|
|
72
|
+
accent: z.ZodOptional<z.ZodString>;
|
|
73
|
+
accentText: z.ZodOptional<z.ZodString>;
|
|
74
|
+
background: z.ZodOptional<z.ZodString>;
|
|
75
|
+
border: z.ZodOptional<z.ZodString>;
|
|
76
|
+
danger: z.ZodOptional<z.ZodString>;
|
|
77
|
+
surface: z.ZodOptional<z.ZodString>;
|
|
78
|
+
text: z.ZodOptional<z.ZodString>;
|
|
79
|
+
mutedText: z.ZodOptional<z.ZodString>;
|
|
80
|
+
radius: z.ZodOptional<z.ZodString>;
|
|
81
|
+
fontFamily: z.ZodOptional<z.ZodString>;
|
|
82
|
+
headingFamily: z.ZodOptional<z.ZodString>;
|
|
83
|
+
baseSize: z.ZodOptional<z.ZodString>;
|
|
84
|
+
density: z.ZodOptional<z.ZodEnum<["compact", "comfortable"]>>;
|
|
85
|
+
motion: z.ZodOptional<z.ZodEnum<["full", "reduced"]>>;
|
|
86
|
+
}, "strip", z.ZodTypeAny, {
|
|
87
|
+
text?: string | undefined;
|
|
88
|
+
radius?: string | undefined;
|
|
89
|
+
density?: "compact" | "comfortable" | undefined;
|
|
90
|
+
motion?: "full" | "reduced" | undefined;
|
|
91
|
+
background?: string | undefined;
|
|
92
|
+
surface?: string | undefined;
|
|
93
|
+
accent?: string | undefined;
|
|
94
|
+
accentText?: string | undefined;
|
|
95
|
+
danger?: string | undefined;
|
|
96
|
+
border?: string | undefined;
|
|
97
|
+
fontFamily?: string | undefined;
|
|
98
|
+
headingFamily?: string | undefined;
|
|
99
|
+
baseSize?: string | undefined;
|
|
100
|
+
mutedText?: string | undefined;
|
|
101
|
+
}, {
|
|
102
|
+
text?: string | undefined;
|
|
103
|
+
radius?: string | undefined;
|
|
104
|
+
density?: "compact" | "comfortable" | undefined;
|
|
105
|
+
motion?: "full" | "reduced" | undefined;
|
|
106
|
+
background?: string | undefined;
|
|
107
|
+
surface?: string | undefined;
|
|
108
|
+
accent?: string | undefined;
|
|
109
|
+
accentText?: string | undefined;
|
|
110
|
+
danger?: string | undefined;
|
|
111
|
+
border?: string | undefined;
|
|
112
|
+
fontFamily?: string | undefined;
|
|
113
|
+
headingFamily?: string | undefined;
|
|
114
|
+
baseSize?: string | undefined;
|
|
115
|
+
mutedText?: string | undefined;
|
|
116
|
+
}>;
|
|
117
|
+
uncertain: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
118
|
+
slot: z.ZodString;
|
|
119
|
+
note: z.ZodString;
|
|
120
|
+
}, "strip", z.ZodTypeAny, {
|
|
121
|
+
slot: string;
|
|
122
|
+
note: string;
|
|
123
|
+
}, {
|
|
124
|
+
slot: string;
|
|
125
|
+
note: string;
|
|
126
|
+
}>, "many">>;
|
|
127
|
+
}, "strip", z.ZodTypeAny, {
|
|
128
|
+
slots: {
|
|
129
|
+
text?: string | undefined;
|
|
130
|
+
radius?: string | undefined;
|
|
131
|
+
density?: "compact" | "comfortable" | undefined;
|
|
132
|
+
motion?: "full" | "reduced" | undefined;
|
|
133
|
+
background?: string | undefined;
|
|
134
|
+
surface?: string | undefined;
|
|
135
|
+
accent?: string | undefined;
|
|
136
|
+
accentText?: string | undefined;
|
|
137
|
+
danger?: string | undefined;
|
|
138
|
+
border?: string | undefined;
|
|
139
|
+
fontFamily?: string | undefined;
|
|
140
|
+
headingFamily?: string | undefined;
|
|
141
|
+
baseSize?: string | undefined;
|
|
142
|
+
mutedText?: string | undefined;
|
|
143
|
+
};
|
|
144
|
+
uncertain?: {
|
|
145
|
+
slot: string;
|
|
146
|
+
note: string;
|
|
147
|
+
}[] | undefined;
|
|
148
|
+
}, {
|
|
149
|
+
slots: {
|
|
150
|
+
text?: string | undefined;
|
|
151
|
+
radius?: string | undefined;
|
|
152
|
+
density?: "compact" | "comfortable" | undefined;
|
|
153
|
+
motion?: "full" | "reduced" | undefined;
|
|
154
|
+
background?: string | undefined;
|
|
155
|
+
surface?: string | undefined;
|
|
156
|
+
accent?: string | undefined;
|
|
157
|
+
accentText?: string | undefined;
|
|
158
|
+
danger?: string | undefined;
|
|
159
|
+
border?: string | undefined;
|
|
160
|
+
fontFamily?: string | undefined;
|
|
161
|
+
headingFamily?: string | undefined;
|
|
162
|
+
baseSize?: string | undefined;
|
|
163
|
+
mutedText?: string | undefined;
|
|
164
|
+
};
|
|
165
|
+
uncertain?: {
|
|
166
|
+
slot: string;
|
|
167
|
+
note: string;
|
|
168
|
+
}[] | undefined;
|
|
169
|
+
}>;
|
|
170
|
+
/** Deterministic validation of a proposed slot value (model or human). */
|
|
171
|
+
export declare function validateSlotValue(slot: SlotKey, raw: string): string | null;
|
|
172
|
+
export declare function extractTheme(targetDir: string): Promise<ThemeSummary>;
|
|
173
|
+
/**
|
|
174
|
+
* Merges a parsed theme-stage artifact (`modelThemeSchema`) onto an
|
|
175
|
+
* exact-only `ThemeSummary` — the precedence law (kill-list §B2, Task 2):
|
|
176
|
+
* exact reads are never overwritten; only `needed` slots may be filled; every
|
|
177
|
+
* proposed value passes through `validateSlotValue` (invalid values are
|
|
178
|
+
* ignored, the slot stays defaulted/derived); a model-provided accentText
|
|
179
|
+
* stands; accentText re-derives by contrast only when the model filled
|
|
180
|
+
* accent but not accentText and accentText itself wasn't an exact read;
|
|
181
|
+
* headingFamily inherits fontFamily under the mirror condition.
|
|
182
|
+
*/
|
|
183
|
+
export declare function applyThemeDraft(summary: ThemeSummary, draft: z.infer<typeof modelThemeSchema>): ThemeSummary;
|
|
184
|
+
export {};
|