@tendrilapp/cli 0.1.23 → 0.1.25
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/dist/SKILL.md +64 -15
- package/dist/tendril-mcp.js +4 -4
- package/dist/tendril.js +1132 -705
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -14,11 +14,11 @@ var __export = (target, all) => {
|
|
|
14
14
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
//
|
|
17
|
+
// packages/core/src/ir.ts
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
var IrLayoutSchema, IrMotionSchema, IrPositionSchema, IrNodeSchema, ComponentIrSchema;
|
|
20
20
|
var init_ir = __esm({
|
|
21
|
-
"
|
|
21
|
+
"packages/core/src/ir.ts"() {
|
|
22
22
|
"use strict";
|
|
23
23
|
IrLayoutSchema = z.object({
|
|
24
24
|
mode: z.enum(["flex-row", "flex-column", "grid", "none"]),
|
|
@@ -110,14 +110,14 @@ var init_ir = __esm({
|
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
//
|
|
113
|
+
// packages/figma/src/source.ts
|
|
114
114
|
var init_source = __esm({
|
|
115
|
-
"
|
|
115
|
+
"packages/figma/src/source.ts"() {
|
|
116
116
|
"use strict";
|
|
117
117
|
}
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
-
//
|
|
120
|
+
// packages/figma/src/svg-safety.ts
|
|
121
121
|
function checkSvgSafety(source) {
|
|
122
122
|
const issues = [];
|
|
123
123
|
if (!/<\s*svg\b/i.test(source)) issues.push("not an <svg> document");
|
|
@@ -132,7 +132,7 @@ function assetExportName(key) {
|
|
|
132
132
|
}
|
|
133
133
|
var UNSAFE_PATTERNS;
|
|
134
134
|
var init_svg_safety = __esm({
|
|
135
|
-
"
|
|
135
|
+
"packages/figma/src/svg-safety.ts"() {
|
|
136
136
|
"use strict";
|
|
137
137
|
UNSAFE_PATTERNS = [
|
|
138
138
|
[/<\s*script\b/i, "script element"],
|
|
@@ -146,11 +146,11 @@ var init_svg_safety = __esm({
|
|
|
146
146
|
}
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
//
|
|
149
|
+
// packages/figma/src/provided.ts
|
|
150
150
|
import { z as z2 } from "zod";
|
|
151
151
|
var ProvidedVariableSchema, RangeSchema, PinnedPlacementSchema, VariantValueFactsSchema, ProvidedMetadataSchema, ProvidedContextSchema, ProvidedContextSource;
|
|
152
152
|
var init_provided = __esm({
|
|
153
|
-
"
|
|
153
|
+
"packages/figma/src/provided.ts"() {
|
|
154
154
|
"use strict";
|
|
155
155
|
init_svg_safety();
|
|
156
156
|
ProvidedVariableSchema = z2.object({
|
|
@@ -269,7 +269,7 @@ var init_provided = __esm({
|
|
|
269
269
|
}
|
|
270
270
|
});
|
|
271
271
|
|
|
272
|
-
//
|
|
272
|
+
// packages/figma/src/mcp/client.ts
|
|
273
273
|
function parseMessages(body, contentType) {
|
|
274
274
|
if (body.trim() === "") return [];
|
|
275
275
|
if (!contentType.includes("text/event-stream")) {
|
|
@@ -285,7 +285,7 @@ function parseMessages(body, contentType) {
|
|
|
285
285
|
}
|
|
286
286
|
var PROTOCOL_VERSION, McpUnavailableError, McpProtocolError, McpHttpClient;
|
|
287
287
|
var init_client = __esm({
|
|
288
|
-
"
|
|
288
|
+
"packages/figma/src/mcp/client.ts"() {
|
|
289
289
|
"use strict";
|
|
290
290
|
PROTOCOL_VERSION = "2025-06-18";
|
|
291
291
|
McpUnavailableError = class extends Error {
|
|
@@ -389,7 +389,7 @@ var init_client = __esm({
|
|
|
389
389
|
}
|
|
390
390
|
});
|
|
391
391
|
|
|
392
|
-
//
|
|
392
|
+
// packages/figma/src/mcp/normalize.ts
|
|
393
393
|
function decodeXmlEntities(v) {
|
|
394
394
|
return v.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
395
395
|
}
|
|
@@ -472,7 +472,7 @@ function mergeVariantAxes(childNames) {
|
|
|
472
472
|
}
|
|
473
473
|
var TAG_TO_TYPE, TAG_RE, ATTR_RE;
|
|
474
474
|
var init_normalize = __esm({
|
|
475
|
-
"
|
|
475
|
+
"packages/figma/src/mcp/normalize.ts"() {
|
|
476
476
|
"use strict";
|
|
477
477
|
init_svg_safety();
|
|
478
478
|
TAG_TO_TYPE = {
|
|
@@ -488,10 +488,10 @@ var init_normalize = __esm({
|
|
|
488
488
|
}
|
|
489
489
|
});
|
|
490
490
|
|
|
491
|
-
//
|
|
491
|
+
// packages/figma/src/mock/fixtures/button.ts
|
|
492
492
|
var buttonVariables, buttonMetadata, buttonNodeTree;
|
|
493
493
|
var init_button = __esm({
|
|
494
|
-
"
|
|
494
|
+
"packages/figma/src/mock/fixtures/button.ts"() {
|
|
495
495
|
"use strict";
|
|
496
496
|
buttonVariables = [
|
|
497
497
|
{
|
|
@@ -600,10 +600,10 @@ var init_button = __esm({
|
|
|
600
600
|
}
|
|
601
601
|
});
|
|
602
602
|
|
|
603
|
-
//
|
|
603
|
+
// packages/figma/src/mock/mock-source.ts
|
|
604
604
|
var MockFigmaSource;
|
|
605
605
|
var init_mock_source = __esm({
|
|
606
|
-
"
|
|
606
|
+
"packages/figma/src/mock/mock-source.ts"() {
|
|
607
607
|
"use strict";
|
|
608
608
|
init_button();
|
|
609
609
|
MockFigmaSource = class {
|
|
@@ -640,7 +640,7 @@ var init_mock_source = __esm({
|
|
|
640
640
|
}
|
|
641
641
|
});
|
|
642
642
|
|
|
643
|
-
//
|
|
643
|
+
// packages/figma/src/css-ident.ts
|
|
644
644
|
function canonicalCssIdentPart(part) {
|
|
645
645
|
return part.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
646
646
|
}
|
|
@@ -649,27 +649,27 @@ function variableNameToCssVar(name) {
|
|
|
649
649
|
return `--${parts.join("-")}`;
|
|
650
650
|
}
|
|
651
651
|
var init_css_ident = __esm({
|
|
652
|
-
"
|
|
652
|
+
"packages/figma/src/css-ident.ts"() {
|
|
653
653
|
"use strict";
|
|
654
654
|
}
|
|
655
655
|
});
|
|
656
656
|
|
|
657
|
-
//
|
|
657
|
+
// packages/figma/src/mcp/var-decoder.ts
|
|
658
658
|
var init_var_decoder = __esm({
|
|
659
|
-
"
|
|
659
|
+
"packages/figma/src/mcp/var-decoder.ts"() {
|
|
660
660
|
"use strict";
|
|
661
661
|
init_css_ident();
|
|
662
662
|
}
|
|
663
663
|
});
|
|
664
664
|
|
|
665
|
-
//
|
|
665
|
+
// packages/figma/src/mcp/font-normalize.ts
|
|
666
666
|
var init_font_normalize = __esm({
|
|
667
|
-
"
|
|
667
|
+
"packages/figma/src/mcp/font-normalize.ts"() {
|
|
668
668
|
"use strict";
|
|
669
669
|
}
|
|
670
670
|
});
|
|
671
671
|
|
|
672
|
-
//
|
|
672
|
+
// packages/figma/src/provided-recording.ts
|
|
673
673
|
import { z as z3 } from "zod";
|
|
674
674
|
function checkPngPayload(base64) {
|
|
675
675
|
let raw;
|
|
@@ -684,7 +684,7 @@ function checkPngPayload(base64) {
|
|
|
684
684
|
}
|
|
685
685
|
var MAX_EMISSION_BYTES, MAX_IMAGE_BYTES, MAX_ASSET_BYTES, PNG_MAGIC, TextEnvelopeSchema, ImageEnvelopeSchema, ProvidedRepSchema, ManifestWireSchema, ProvidedRecordingSetSchema;
|
|
686
686
|
var init_provided_recording = __esm({
|
|
687
|
-
"
|
|
687
|
+
"packages/figma/src/provided-recording.ts"() {
|
|
688
688
|
"use strict";
|
|
689
689
|
init_svg_safety();
|
|
690
690
|
MAX_EMISSION_BYTES = 2 * 1024 * 1024;
|
|
@@ -752,7 +752,7 @@ var init_provided_recording = __esm({
|
|
|
752
752
|
}
|
|
753
753
|
});
|
|
754
754
|
|
|
755
|
-
//
|
|
755
|
+
// packages/figma/src/recording/axis-defaults.ts
|
|
756
756
|
function resolveAxisDefaultWithRule(domain, recorded, override) {
|
|
757
757
|
if (override !== void 0) return { value: override, rule: "override" };
|
|
758
758
|
const literal = domain.find((v) => kebab(v) === "default" || kebab(v) === "enabled");
|
|
@@ -787,7 +787,7 @@ function resolveAxisDefault(domain, recorded, override) {
|
|
|
787
787
|
}
|
|
788
788
|
var INTERACTION_STATES, BOOLEAN_STATES, ENGAGED_STATES, INTERACTION_EVIDENCE_VALUES, kebab, OFF_VALUES, ON_VALUES;
|
|
789
789
|
var init_axis_defaults = __esm({
|
|
790
|
-
"
|
|
790
|
+
"packages/figma/src/recording/axis-defaults.ts"() {
|
|
791
791
|
"use strict";
|
|
792
792
|
INTERACTION_STATES = /* @__PURE__ */ new Set(["hover", "focus", "focus-visible", "active", "pressed"]);
|
|
793
793
|
BOOLEAN_STATES = /* @__PURE__ */ new Set(["disabled", "loading"]);
|
|
@@ -810,7 +810,7 @@ var init_axis_defaults = __esm({
|
|
|
810
810
|
}
|
|
811
811
|
});
|
|
812
812
|
|
|
813
|
-
//
|
|
813
|
+
// packages/figma/src/recording/plan.ts
|
|
814
814
|
function singleAxes(name) {
|
|
815
815
|
const parsed = parseVariantAxes(name);
|
|
816
816
|
if (parsed === void 0) return void 0;
|
|
@@ -920,7 +920,7 @@ function planQueue(symbols, opts = {}) {
|
|
|
920
920
|
}
|
|
921
921
|
var kebab2;
|
|
922
922
|
var init_plan = __esm({
|
|
923
|
-
"
|
|
923
|
+
"packages/figma/src/recording/plan.ts"() {
|
|
924
924
|
"use strict";
|
|
925
925
|
init_normalize();
|
|
926
926
|
init_axis_defaults();
|
|
@@ -928,7 +928,75 @@ var init_plan = __esm({
|
|
|
928
928
|
}
|
|
929
929
|
});
|
|
930
930
|
|
|
931
|
-
//
|
|
931
|
+
// packages/figma/src/recording/envelope-content.ts
|
|
932
|
+
function envelopeParts(payload) {
|
|
933
|
+
const content = payload?.content;
|
|
934
|
+
if (!Array.isArray(content)) return null;
|
|
935
|
+
const blocks = content;
|
|
936
|
+
const text = blocks.map((b) => typeof b.text === "string" ? b.text : "").filter((t) => t !== "").join("\n");
|
|
937
|
+
const image = blocks.find((b) => b.type === "image" && typeof b.data === "string");
|
|
938
|
+
return image === void 0 ? { text } : { text, imageData: image.data };
|
|
939
|
+
}
|
|
940
|
+
function namedCause(text, table) {
|
|
941
|
+
return table.find((entry) => entry.pattern.test(text))?.cause;
|
|
942
|
+
}
|
|
943
|
+
function jsonObjectPayload(text) {
|
|
944
|
+
try {
|
|
945
|
+
const value = JSON.parse(text);
|
|
946
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
947
|
+
} catch {
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
function checkEnvelopeContent(tool, payload) {
|
|
952
|
+
const parts = envelopeParts(payload);
|
|
953
|
+
if (parts === null) return { ok: true };
|
|
954
|
+
const { text, imageData } = parts;
|
|
955
|
+
const serviceError = namedCause(text, SERVICE_ERROR_SIGNATURES);
|
|
956
|
+
if (serviceError !== void 0) return { ok: false, reason: serviceError };
|
|
957
|
+
const reject = (expected) => {
|
|
958
|
+
const hint = namedCause(text, FAILURE_HINTS);
|
|
959
|
+
return { ok: false, reason: hint === void 0 ? expected : `${hint} \u2014 ${expected}` };
|
|
960
|
+
};
|
|
961
|
+
switch (tool) {
|
|
962
|
+
case "get_metadata":
|
|
963
|
+
case "get_metadata_interior":
|
|
964
|
+
return NODE_MARKUP.test(text) ? { ok: true } : reject('no node markup in the response (a get_metadata response contains elements like <symbol id="\u2026"> or <frame id="\u2026">)');
|
|
965
|
+
case "get_design_context":
|
|
966
|
+
return CODE_DECLARATION.test(text) || ANY_MARKUP.test(text) ? { ok: true } : reject("no code emission or markup in the response (a get_design_context response contains a component's code, or node markup when the design is too large)");
|
|
967
|
+
case "get_variable_defs":
|
|
968
|
+
return jsonObjectPayload(text) === null ? reject("the response is not a JSON object of variable definitions") : { ok: true };
|
|
969
|
+
case "get_screenshot": {
|
|
970
|
+
if (imageData === void 0) return reject("no image block in the response (a get_screenshot envelope carries base64 PNG bytes)");
|
|
971
|
+
const png = checkPngPayload(imageData);
|
|
972
|
+
return png.ok ? { ok: true } : reject(png.reason);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
var NODE_MARKUP, ANY_MARKUP, CODE_DECLARATION, SERVICE_ERROR_SIGNATURES, FAILURE_HINTS;
|
|
977
|
+
var init_envelope_content = __esm({
|
|
978
|
+
"packages/figma/src/recording/envelope-content.ts"() {
|
|
979
|
+
"use strict";
|
|
980
|
+
init_provided_recording();
|
|
981
|
+
NODE_MARKUP = /<[A-Za-z][\w:.-]*(?:\s[^>]*)?\sid="[^"]+"/;
|
|
982
|
+
ANY_MARKUP = /<[A-Za-z][\w:.-]*(?:\s[^>]*)?\/?>/;
|
|
983
|
+
CODE_DECLARATION = /(?:^|\n)[ \t]*(?:const|let|var|function|class|export|import|type|interface|async|@)\b/;
|
|
984
|
+
SERVICE_ERROR_SIGNATURES = [
|
|
985
|
+
{ pattern: /reached the .{0,60}tool call limit/i, cause: "the Figma MCP reported that the account's tool-call limit is exhausted" },
|
|
986
|
+
{ pattern: /tool call limit for your/i, cause: "the Figma MCP reported that the account's tool-call limit is exhausted" },
|
|
987
|
+
{ pattern: /upgrade your (?:seat|plan)/i, cause: "the Figma MCP returned an upgrade/quota notice" }
|
|
988
|
+
];
|
|
989
|
+
FAILURE_HINTS = [
|
|
990
|
+
{ pattern: /\brate[- ]limit/i, cause: "the response looks like a rate-limit error" },
|
|
991
|
+
{ pattern: /\b(?:429|too many requests)\b/i, cause: "the response looks like a rate-limit error" },
|
|
992
|
+
{ pattern: /\b(?:quota|billing|subscription|seat)\b/i, cause: "the response looks like a quota/plan notice" },
|
|
993
|
+
{ pattern: /\b(?:unauthorized|forbidden|not authorized|access denied|401|403)\b/i, cause: "the response looks like an authorization error" },
|
|
994
|
+
{ pattern: /\b(?:error|failed|failure|timed out|timeout)\b/i, cause: "the response looks like an error message" }
|
|
995
|
+
];
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// packages/figma/src/recording/session.ts
|
|
932
1000
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
933
1001
|
import path from "node:path";
|
|
934
1002
|
import { z as z4 } from "zod";
|
|
@@ -1036,10 +1104,14 @@ function ingestEnvelope(setDir, slug, tool, payload) {
|
|
|
1036
1104
|
if (!RECORD_TOOLS.includes(tool)) {
|
|
1037
1105
|
throw new Error(`unknown tool "${tool}" \u2014 expected one of ${RECORD_TOOLS.join(", ")}`);
|
|
1038
1106
|
}
|
|
1107
|
+
const content = checkEnvelopeContent(tool, payload);
|
|
1108
|
+
if (!content.ok) {
|
|
1109
|
+
throw new Error(`${slug}/${tool} was NOT recorded \u2014 ${content.reason}. ${REINGEST_GUIDANCE}`);
|
|
1110
|
+
}
|
|
1039
1111
|
const schema = tool === "get_screenshot" ? ImageEnvelopeSchema : TextEnvelopeSchema;
|
|
1040
1112
|
const parsed = schema.safeParse(payload);
|
|
1041
1113
|
if (!parsed.success) {
|
|
1042
|
-
throw new Error(`${slug}/${tool}
|
|
1114
|
+
throw new Error(`${slug}/${tool} was NOT recorded \u2014 envelope rejected at the boundary: ${parsed.error.issues[0]?.message ?? "invalid"}. ${REINGEST_GUIDANCE}`);
|
|
1043
1115
|
}
|
|
1044
1116
|
const file = containedPath(setDir, slug, `${tool}.json`);
|
|
1045
1117
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -1076,12 +1148,13 @@ function ingestAsset(setDir, slug, name, content) {
|
|
|
1076
1148
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
1077
1149
|
writeFileSync(file, content);
|
|
1078
1150
|
}
|
|
1079
|
-
var RECORD_TOOLS, INTERIOR_TOOL, REQUIRED_TOOLS, SessionManifestSchema, manifestPath, PROTOCOL_ORDER, byProtocolOrder;
|
|
1151
|
+
var RECORD_TOOLS, INTERIOR_TOOL, REQUIRED_TOOLS, SessionManifestSchema, manifestPath, PROTOCOL_ORDER, byProtocolOrder, REINGEST_GUIDANCE;
|
|
1080
1152
|
var init_session = __esm({
|
|
1081
|
-
"
|
|
1153
|
+
"packages/figma/src/recording/session.ts"() {
|
|
1082
1154
|
"use strict";
|
|
1083
1155
|
init_svg_safety();
|
|
1084
1156
|
init_provided_recording();
|
|
1157
|
+
init_envelope_content();
|
|
1085
1158
|
init_plan();
|
|
1086
1159
|
RECORD_TOOLS = ["get_design_context", "get_metadata", "get_screenshot", "get_variable_defs", "get_metadata_interior"];
|
|
1087
1160
|
INTERIOR_TOOL = "get_metadata_interior";
|
|
@@ -1120,10 +1193,11 @@ var init_session = __esm({
|
|
|
1120
1193
|
manifestPath = (setDir) => path.join(setDir, "recording-set.json");
|
|
1121
1194
|
PROTOCOL_ORDER = ["get_metadata", "get_design_context", "get_screenshot", "get_variable_defs", "get_metadata_interior"];
|
|
1122
1195
|
byProtocolOrder = (tools) => [...tools].sort((a, b) => PROTOCOL_ORDER.indexOf(a) - PROTOCOL_ORDER.indexOf(b));
|
|
1196
|
+
REINGEST_GUIDANCE = "Nothing was written. Recording persists per rep and resumes from disk, so re-run the same command with the real response; if the Figma MCP reported a tool-call or rate limit, wait for the quota to reset first.";
|
|
1123
1197
|
}
|
|
1124
1198
|
});
|
|
1125
1199
|
|
|
1126
|
-
//
|
|
1200
|
+
// packages/figma/src/recording/roles.ts
|
|
1127
1201
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
1128
1202
|
import path2 from "node:path";
|
|
1129
1203
|
function emissionIdSets(emission) {
|
|
@@ -1192,16 +1266,16 @@ function deriveRoles(setDir, manifest) {
|
|
|
1192
1266
|
}
|
|
1193
1267
|
var NODE_ID_RE;
|
|
1194
1268
|
var init_roles = __esm({
|
|
1195
|
-
"
|
|
1269
|
+
"packages/figma/src/recording/roles.ts"() {
|
|
1196
1270
|
"use strict";
|
|
1197
1271
|
init_session();
|
|
1198
1272
|
NODE_ID_RE = /data-node-id="([^"]+)"/g;
|
|
1199
1273
|
}
|
|
1200
1274
|
});
|
|
1201
1275
|
|
|
1202
|
-
//
|
|
1276
|
+
// packages/figma/src/index.ts
|
|
1203
1277
|
var init_src = __esm({
|
|
1204
|
-
"
|
|
1278
|
+
"packages/figma/src/index.ts"() {
|
|
1205
1279
|
"use strict";
|
|
1206
1280
|
init_source();
|
|
1207
1281
|
init_provided();
|
|
@@ -1216,16 +1290,17 @@ var init_src = __esm({
|
|
|
1216
1290
|
init_axis_defaults();
|
|
1217
1291
|
init_plan();
|
|
1218
1292
|
init_session();
|
|
1293
|
+
init_envelope_content();
|
|
1219
1294
|
init_roles();
|
|
1220
1295
|
}
|
|
1221
1296
|
});
|
|
1222
1297
|
|
|
1223
|
-
//
|
|
1298
|
+
// packages/tokens/src/dtcg.ts
|
|
1224
1299
|
function variableNameToPath(name) {
|
|
1225
1300
|
return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
|
|
1226
1301
|
}
|
|
1227
|
-
function tokenPathToCssVar(
|
|
1228
|
-
return `--${
|
|
1302
|
+
function tokenPathToCssVar(path40) {
|
|
1303
|
+
return `--${path40.join("-")}`;
|
|
1229
1304
|
}
|
|
1230
1305
|
function toDtcgToken(variable, defaultMode) {
|
|
1231
1306
|
const modes = Object.keys(variable.valuesByMode);
|
|
@@ -1269,11 +1344,11 @@ function toDtcgToken(variable, defaultMode) {
|
|
|
1269
1344
|
}
|
|
1270
1345
|
function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
1271
1346
|
const entries = variables.map((variable) => {
|
|
1272
|
-
const
|
|
1273
|
-
if (
|
|
1347
|
+
const path40 = variableNameToPath(variable.name);
|
|
1348
|
+
if (path40.length === 0) {
|
|
1274
1349
|
throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
|
|
1275
1350
|
}
|
|
1276
|
-
return { variable, path:
|
|
1351
|
+
return { variable, path: path40 };
|
|
1277
1352
|
});
|
|
1278
1353
|
const groupPrefixes = /* @__PURE__ */ new Set();
|
|
1279
1354
|
for (const e of entries) {
|
|
@@ -1294,21 +1369,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
|
1294
1369
|
}
|
|
1295
1370
|
const tokens = {};
|
|
1296
1371
|
const flat = [];
|
|
1297
|
-
for (const { variable, path:
|
|
1372
|
+
for (const { variable, path: path40 } of entries) {
|
|
1298
1373
|
const token = toDtcgToken(variable, defaultMode);
|
|
1299
1374
|
let group = tokens;
|
|
1300
|
-
for (const segment of
|
|
1375
|
+
for (const segment of path40.slice(0, -1)) {
|
|
1301
1376
|
const existing = group[segment];
|
|
1302
1377
|
group = existing ?? (group[segment] = {});
|
|
1303
1378
|
}
|
|
1304
|
-
const leaf =
|
|
1379
|
+
const leaf = path40[path40.length - 1];
|
|
1305
1380
|
if (group[leaf] !== void 0) {
|
|
1306
|
-
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${
|
|
1381
|
+
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path40.join(".")}" (variable ${variable.id})`);
|
|
1307
1382
|
}
|
|
1308
1383
|
group[leaf] = token;
|
|
1309
1384
|
flat.push({
|
|
1310
|
-
path:
|
|
1311
|
-
cssVar: tokenPathToCssVar(
|
|
1385
|
+
path: path40.join("."),
|
|
1386
|
+
cssVar: tokenPathToCssVar(path40),
|
|
1312
1387
|
type: token.$type,
|
|
1313
1388
|
value: token.$value
|
|
1314
1389
|
});
|
|
@@ -1326,7 +1401,7 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
|
1326
1401
|
}
|
|
1327
1402
|
var DIMENSION_COLLECTIONS, DtcgMappingError;
|
|
1328
1403
|
var init_dtcg = __esm({
|
|
1329
|
-
"
|
|
1404
|
+
"packages/tokens/src/dtcg.ts"() {
|
|
1330
1405
|
"use strict";
|
|
1331
1406
|
init_src();
|
|
1332
1407
|
DIMENSION_COLLECTIONS = /* @__PURE__ */ new Set(["spacing", "radius", "size", "border-width"]);
|
|
@@ -1341,7 +1416,7 @@ var init_dtcg = __esm({
|
|
|
1341
1416
|
}
|
|
1342
1417
|
});
|
|
1343
1418
|
|
|
1344
|
-
//
|
|
1419
|
+
// packages/tokens/src/emit.ts
|
|
1345
1420
|
function decomposeFontValue(raw, valueByCssVar) {
|
|
1346
1421
|
const field = (name) => new RegExp(`${name}:\\s*("[^"]*"|[^,)]+)`).exec(raw)?.[1]?.trim();
|
|
1347
1422
|
const resolve = (v) => {
|
|
@@ -1408,7 +1483,7 @@ export default {
|
|
|
1408
1483
|
}
|
|
1409
1484
|
var STYLE_WEIGHTS;
|
|
1410
1485
|
var init_emit = __esm({
|
|
1411
|
-
"
|
|
1486
|
+
"packages/tokens/src/emit.ts"() {
|
|
1412
1487
|
"use strict";
|
|
1413
1488
|
init_src();
|
|
1414
1489
|
STYLE_WEIGHTS = {
|
|
@@ -1427,7 +1502,7 @@ var init_emit = __esm({
|
|
|
1427
1502
|
}
|
|
1428
1503
|
});
|
|
1429
1504
|
|
|
1430
|
-
//
|
|
1505
|
+
// packages/tokens/src/synthesize.ts
|
|
1431
1506
|
function synthesizeSpacingVariables(tree, variantFacts, existing) {
|
|
1432
1507
|
if (existing.some((v) => v.collection === "spacing")) return [];
|
|
1433
1508
|
const values = /* @__PURE__ */ new Set();
|
|
@@ -1466,7 +1541,7 @@ function synthesizeSpacingVariables(tree, variantFacts, existing) {
|
|
|
1466
1541
|
}
|
|
1467
1542
|
var SPACING_FIELDS;
|
|
1468
1543
|
var init_synthesize = __esm({
|
|
1469
|
-
"
|
|
1544
|
+
"packages/tokens/src/synthesize.ts"() {
|
|
1470
1545
|
"use strict";
|
|
1471
1546
|
SPACING_FIELDS = [
|
|
1472
1547
|
"paddingTop",
|
|
@@ -1478,9 +1553,9 @@ var init_synthesize = __esm({
|
|
|
1478
1553
|
}
|
|
1479
1554
|
});
|
|
1480
1555
|
|
|
1481
|
-
//
|
|
1556
|
+
// packages/tokens/src/index.ts
|
|
1482
1557
|
var init_src2 = __esm({
|
|
1483
|
-
"
|
|
1558
|
+
"packages/tokens/src/index.ts"() {
|
|
1484
1559
|
"use strict";
|
|
1485
1560
|
init_dtcg();
|
|
1486
1561
|
init_emit();
|
|
@@ -1489,7 +1564,7 @@ var init_src2 = __esm({
|
|
|
1489
1564
|
}
|
|
1490
1565
|
});
|
|
1491
1566
|
|
|
1492
|
-
//
|
|
1567
|
+
// packages/core/src/ir-builder.ts
|
|
1493
1568
|
function isObject(value) {
|
|
1494
1569
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1495
1570
|
}
|
|
@@ -1497,9 +1572,9 @@ function boundId(value) {
|
|
|
1497
1572
|
return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
|
|
1498
1573
|
}
|
|
1499
1574
|
function resolveBinding(ctx, id) {
|
|
1500
|
-
const
|
|
1501
|
-
if (
|
|
1502
|
-
return
|
|
1575
|
+
const path40 = ctx.pathById.get(id);
|
|
1576
|
+
if (path40 === void 0) ctx.unresolved.add(id);
|
|
1577
|
+
return path40;
|
|
1503
1578
|
}
|
|
1504
1579
|
function parseVariantProps(name) {
|
|
1505
1580
|
if (!name.includes("=")) return void 0;
|
|
@@ -1534,8 +1609,8 @@ function walk(ctx, raw) {
|
|
|
1534
1609
|
if (!isObject(paint) || paint["visible"] === false) continue;
|
|
1535
1610
|
const id = boundId(paint);
|
|
1536
1611
|
if (id !== void 0) {
|
|
1537
|
-
const
|
|
1538
|
-
if (
|
|
1612
|
+
const path40 = resolveBinding(ctx, id);
|
|
1613
|
+
if (path40 !== void 0) tokens.add(path40);
|
|
1539
1614
|
} else if (typeof paint["color"] === "string") {
|
|
1540
1615
|
ctx.hardcoded.push({ node: name, property, value: paint["color"] });
|
|
1541
1616
|
}
|
|
@@ -1543,8 +1618,8 @@ function walk(ctx, raw) {
|
|
|
1543
1618
|
}
|
|
1544
1619
|
const radiusId = boundId(raw["cornerRadius"]);
|
|
1545
1620
|
if (radiusId !== void 0) {
|
|
1546
|
-
const
|
|
1547
|
-
if (
|
|
1621
|
+
const path40 = resolveBinding(ctx, radiusId);
|
|
1622
|
+
if (path40 !== void 0) tokens.add(path40);
|
|
1548
1623
|
} else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
|
|
1549
1624
|
ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
|
|
1550
1625
|
}
|
|
@@ -1554,10 +1629,10 @@ function walk(ctx, raw) {
|
|
|
1554
1629
|
layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
|
|
1555
1630
|
const gapId = boundId(raw["itemSpacing"]);
|
|
1556
1631
|
if (gapId !== void 0) {
|
|
1557
|
-
const
|
|
1558
|
-
if (
|
|
1559
|
-
layout.gap =
|
|
1560
|
-
tokens.add(
|
|
1632
|
+
const path40 = resolveBinding(ctx, gapId);
|
|
1633
|
+
if (path40 !== void 0) {
|
|
1634
|
+
layout.gap = path40;
|
|
1635
|
+
tokens.add(path40);
|
|
1561
1636
|
}
|
|
1562
1637
|
} else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
|
|
1563
1638
|
ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
|
|
@@ -1566,10 +1641,10 @@ function walk(ctx, raw) {
|
|
|
1566
1641
|
for (const field of PADDING_FIELDS) {
|
|
1567
1642
|
const id = boundId(raw[field]);
|
|
1568
1643
|
if (id !== void 0) {
|
|
1569
|
-
const
|
|
1570
|
-
if (
|
|
1571
|
-
paddingPaths.push(
|
|
1572
|
-
tokens.add(
|
|
1644
|
+
const path40 = resolveBinding(ctx, id);
|
|
1645
|
+
if (path40 !== void 0) {
|
|
1646
|
+
paddingPaths.push(path40);
|
|
1647
|
+
tokens.add(path40);
|
|
1573
1648
|
}
|
|
1574
1649
|
} else if (typeof raw[field] === "number" && raw[field] !== 0) {
|
|
1575
1650
|
ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
|
|
@@ -1674,7 +1749,7 @@ function canonicalizeFactIdents(variantFacts) {
|
|
|
1674
1749
|
}
|
|
1675
1750
|
var RAW_PAYLOAD_FLAG_BYTES, MIN_HEALTHY_RATIO, RATIO_CHECK_MIN_BYTES, PADDING_FIELDS;
|
|
1676
1751
|
var init_ir_builder = __esm({
|
|
1677
|
-
"
|
|
1752
|
+
"packages/core/src/ir-builder.ts"() {
|
|
1678
1753
|
"use strict";
|
|
1679
1754
|
init_src2();
|
|
1680
1755
|
init_src2();
|
|
@@ -1685,10 +1760,10 @@ var init_ir_builder = __esm({
|
|
|
1685
1760
|
}
|
|
1686
1761
|
});
|
|
1687
1762
|
|
|
1688
|
-
//
|
|
1763
|
+
// packages/core/src/exit-codes.ts
|
|
1689
1764
|
var ExitCode;
|
|
1690
1765
|
var init_exit_codes = __esm({
|
|
1691
|
-
"
|
|
1766
|
+
"packages/core/src/exit-codes.ts"() {
|
|
1692
1767
|
"use strict";
|
|
1693
1768
|
ExitCode = {
|
|
1694
1769
|
Success: 0,
|
|
@@ -1709,7 +1784,7 @@ var init_exit_codes = __esm({
|
|
|
1709
1784
|
}
|
|
1710
1785
|
});
|
|
1711
1786
|
|
|
1712
|
-
//
|
|
1787
|
+
// packages/core/src/readiness.ts
|
|
1713
1788
|
function collectDefaultNamedLayers(tree, found) {
|
|
1714
1789
|
const name = tree["name"];
|
|
1715
1790
|
const visible = tree["visible"];
|
|
@@ -1759,15 +1834,15 @@ function scoreReadiness(metadata, tree) {
|
|
|
1759
1834
|
}
|
|
1760
1835
|
var DEFAULT_LAYER_NAME;
|
|
1761
1836
|
var init_readiness = __esm({
|
|
1762
|
-
"
|
|
1837
|
+
"packages/core/src/readiness.ts"() {
|
|
1763
1838
|
"use strict";
|
|
1764
1839
|
DEFAULT_LAYER_NAME = /^(Group|Frame|Rectangle|Ellipse|Vector|Component)\s*\d*$/;
|
|
1765
1840
|
}
|
|
1766
1841
|
});
|
|
1767
1842
|
|
|
1768
|
-
//
|
|
1843
|
+
// packages/core/src/index.ts
|
|
1769
1844
|
var init_src3 = __esm({
|
|
1770
|
-
"
|
|
1845
|
+
"packages/core/src/index.ts"() {
|
|
1771
1846
|
"use strict";
|
|
1772
1847
|
init_ir();
|
|
1773
1848
|
init_ir_builder();
|
|
@@ -1776,20 +1851,72 @@ var init_src3 = __esm({
|
|
|
1776
1851
|
}
|
|
1777
1852
|
});
|
|
1778
1853
|
|
|
1779
|
-
//
|
|
1780
|
-
import {
|
|
1781
|
-
import { existsSync as existsSync3, readdirSync } from "node:fs";
|
|
1854
|
+
// packages/cli/src/invocation.ts
|
|
1855
|
+
import { existsSync as existsSync3, realpathSync } from "node:fs";
|
|
1782
1856
|
import path3 from "node:path";
|
|
1857
|
+
import { fileURLToPath } from "node:url";
|
|
1858
|
+
function findPathTendril(pathEnv, platform) {
|
|
1859
|
+
const dirs = pathEnv.split(path3.delimiter).filter((d) => d !== "" && !/node_modules[\\/]\.bin/.test(d) && !/[\\/]_npx[\\/]/.test(d));
|
|
1860
|
+
const names = platform === "win32" ? ["tendril.cmd", "tendril.bat"] : ["tendril"];
|
|
1861
|
+
for (const dir of dirs) {
|
|
1862
|
+
for (const name of names) {
|
|
1863
|
+
const candidate = path3.join(dir, name);
|
|
1864
|
+
if (existsSync3(candidate)) return candidate;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
return null;
|
|
1868
|
+
}
|
|
1869
|
+
function packageRootOf(file) {
|
|
1870
|
+
let dir = path3.dirname(file);
|
|
1871
|
+
for (; ; ) {
|
|
1872
|
+
if (existsSync3(path3.join(dir, "package.json"))) return dir;
|
|
1873
|
+
const parent = path3.dirname(dir);
|
|
1874
|
+
if (parent === dir) return null;
|
|
1875
|
+
dir = parent;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
function resolveInvocation(pathEnv, platform, selfFile = fileURLToPath(import.meta.url)) {
|
|
1879
|
+
const binary = findPathTendril(pathEnv, platform);
|
|
1880
|
+
if (binary === null) return NPX_INVOCATION;
|
|
1881
|
+
try {
|
|
1882
|
+
const onPath = packageRootOf(realpathSync(binary));
|
|
1883
|
+
const self = packageRootOf(realpathSync(selfFile));
|
|
1884
|
+
if (onPath === null || self === null) return NPX_INVOCATION;
|
|
1885
|
+
return realpathSync(onPath) === realpathSync(self) ? "tendril" : NPX_INVOCATION;
|
|
1886
|
+
} catch {
|
|
1887
|
+
return NPX_INVOCATION;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
function tendrilInvocation() {
|
|
1891
|
+
cached ??= resolveInvocation(process.env["PATH"] ?? "", process.platform);
|
|
1892
|
+
return cached;
|
|
1893
|
+
}
|
|
1894
|
+
function tendrilCommand(args) {
|
|
1895
|
+
return `${tendrilInvocation()} ${args}`;
|
|
1896
|
+
}
|
|
1897
|
+
var NPX_INVOCATION, cached;
|
|
1898
|
+
var init_invocation = __esm({
|
|
1899
|
+
"packages/cli/src/invocation.ts"() {
|
|
1900
|
+
"use strict";
|
|
1901
|
+
NPX_INVOCATION = "npx -y -p @tendrilapp/cli@latest tendril";
|
|
1902
|
+
cached = null;
|
|
1903
|
+
}
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1906
|
+
// packages/verify/src/browser.ts
|
|
1907
|
+
import { execFileSync } from "node:child_process";
|
|
1908
|
+
import { existsSync as existsSync4, readdirSync } from "node:fs";
|
|
1909
|
+
import path4 from "node:path";
|
|
1783
1910
|
function resolveChrome() {
|
|
1784
1911
|
const fromEnv = process.env["TENDRIL_CHROME"] ?? process.env["CHROME_PATH"];
|
|
1785
1912
|
if (fromEnv !== void 0 && fromEnv !== "") {
|
|
1786
|
-
if (!
|
|
1913
|
+
if (!existsSync4(fromEnv)) {
|
|
1787
1914
|
throw new Error(`CHROME_PATH points at ${fromEnv}, which does not exist \u2014 fix the variable or unset it to use discovery`);
|
|
1788
1915
|
}
|
|
1789
1916
|
return fromEnv;
|
|
1790
1917
|
}
|
|
1791
1918
|
const candidates = process.platform === "darwin" ? MAC_CANDIDATES : process.platform === "win32" ? WIN_CANDIDATES : [];
|
|
1792
|
-
for (const c of candidates) if (
|
|
1919
|
+
for (const c of candidates) if (existsSync4(c)) return c;
|
|
1793
1920
|
if (process.platform !== "win32") {
|
|
1794
1921
|
for (const name of PATH_NAMES) {
|
|
1795
1922
|
try {
|
|
@@ -1803,7 +1930,7 @@ function resolveChrome() {
|
|
|
1803
1930
|
}
|
|
1804
1931
|
function versionFromInstallDir(exePath) {
|
|
1805
1932
|
try {
|
|
1806
|
-
const builds = readdirSync(
|
|
1933
|
+
const builds = readdirSync(path4.dirname(exePath), { withFileTypes: true }).filter((e) => e.isDirectory() && /^\d+(\.\d+){3}$/.test(e.name)).map((e) => e.name.split(".").map(Number));
|
|
1807
1934
|
builds.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2] || a[3] - b[3]);
|
|
1808
1935
|
return builds.length === 0 ? null : builds[builds.length - 1].join(".");
|
|
1809
1936
|
} catch {
|
|
@@ -1815,7 +1942,7 @@ function windowsBrowserName(exePath) {
|
|
|
1815
1942
|
if (p.includes("\\google\\chrome\\")) return "Google Chrome";
|
|
1816
1943
|
if (p.includes("\\microsoft\\edge\\")) return "Microsoft Edge";
|
|
1817
1944
|
if (p.includes("chromium")) return "Chromium";
|
|
1818
|
-
return
|
|
1945
|
+
return path4.win32.basename(exePath, ".exe");
|
|
1819
1946
|
}
|
|
1820
1947
|
function chromeVersion() {
|
|
1821
1948
|
if (_version !== void 0) return _version;
|
|
@@ -1834,7 +1961,7 @@ function chromeVersion() {
|
|
|
1834
1961
|
}
|
|
1835
1962
|
var MAC_CANDIDATES, WIN_CANDIDATES, PATH_NAMES, _version;
|
|
1836
1963
|
var init_browser = __esm({
|
|
1837
|
-
"
|
|
1964
|
+
"packages/verify/src/browser.ts"() {
|
|
1838
1965
|
"use strict";
|
|
1839
1966
|
MAC_CANDIDATES = [
|
|
1840
1967
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
@@ -1849,33 +1976,33 @@ var init_browser = __esm({
|
|
|
1849
1976
|
}
|
|
1850
1977
|
});
|
|
1851
1978
|
|
|
1852
|
-
//
|
|
1853
|
-
import { existsSync as
|
|
1979
|
+
// packages/verify/src/runtime.ts
|
|
1980
|
+
import { existsSync as existsSync5, mkdtempSync, symlinkSync } from "node:fs";
|
|
1854
1981
|
import { createRequire } from "node:module";
|
|
1855
1982
|
import os from "node:os";
|
|
1856
|
-
import
|
|
1857
|
-
import { fileURLToPath } from "node:url";
|
|
1983
|
+
import path5 from "node:path";
|
|
1984
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1858
1985
|
function runtimePackageRoot() {
|
|
1859
1986
|
const env = process.env["TENDRIL_PACKAGE_ROOT"];
|
|
1860
|
-
if (env !== void 0 && env !== "") return
|
|
1861
|
-
return
|
|
1987
|
+
if (env !== void 0 && env !== "") return path5.resolve(env);
|
|
1988
|
+
return path5.resolve(path5.dirname(fileURLToPath2(import.meta.url)), "..");
|
|
1862
1989
|
}
|
|
1863
1990
|
function runtimeNodeModules() {
|
|
1864
1991
|
const root = runtimePackageRoot();
|
|
1865
|
-
for (let dir = root; ; dir =
|
|
1866
|
-
const candidate =
|
|
1867
|
-
if (
|
|
1868
|
-
if (
|
|
1992
|
+
for (let dir = root; ; dir = path5.dirname(dir)) {
|
|
1993
|
+
const candidate = path5.basename(dir) === "node_modules" ? dir : path5.join(dir, "node_modules");
|
|
1994
|
+
if (existsSync5(path5.join(candidate, "react"))) return candidate;
|
|
1995
|
+
if (path5.dirname(dir) === dir) break;
|
|
1869
1996
|
}
|
|
1870
1997
|
throw new Error(
|
|
1871
1998
|
`Tendril's installed dependencies are missing: no node_modules containing react found at or above ${root}. This is an INSTALLATION problem, not a component error \u2014 reinstall with \`npm install -g @tendrilapp/cli\` (or \`tendrilapp\`) and retry.`
|
|
1872
1999
|
);
|
|
1873
2000
|
}
|
|
1874
2001
|
function newScratchDir(prefix) {
|
|
1875
|
-
const dir = mkdtempSync(
|
|
2002
|
+
const dir = mkdtempSync(path5.join(os.tmpdir(), `tendril-${prefix}-`));
|
|
1876
2003
|
const nodeModules = runtimeNodeModules();
|
|
1877
2004
|
try {
|
|
1878
|
-
symlinkSync(nodeModules,
|
|
2005
|
+
symlinkSync(nodeModules, path5.join(dir, "node_modules"), "junction");
|
|
1879
2006
|
} catch (err) {
|
|
1880
2007
|
throw new Error(
|
|
1881
2008
|
`Tendril could not link its dependencies into the scratch dir (${nodeModules} -> ${dir}): ${err instanceof Error ? err.message : String(err)}. This is an environment problem, not a component error.`
|
|
@@ -1884,7 +2011,7 @@ function newScratchDir(prefix) {
|
|
|
1884
2011
|
return dir;
|
|
1885
2012
|
}
|
|
1886
2013
|
function reactPinPlugin() {
|
|
1887
|
-
const req = createRequire(
|
|
2014
|
+
const req = createRequire(path5.join(runtimePackageRoot(), "package.json"));
|
|
1888
2015
|
return {
|
|
1889
2016
|
name: "tendril-react-pin",
|
|
1890
2017
|
setup(b) {
|
|
@@ -1899,12 +2026,12 @@ function reactPinPlugin() {
|
|
|
1899
2026
|
};
|
|
1900
2027
|
}
|
|
1901
2028
|
var init_runtime = __esm({
|
|
1902
|
-
"
|
|
2029
|
+
"packages/verify/src/runtime.ts"() {
|
|
1903
2030
|
"use strict";
|
|
1904
2031
|
}
|
|
1905
2032
|
});
|
|
1906
2033
|
|
|
1907
|
-
//
|
|
2034
|
+
// packages/verify/src/gates.ts
|
|
1908
2035
|
function scanForbiddenPatterns(source) {
|
|
1909
2036
|
const violations = [];
|
|
1910
2037
|
const lines = source.split("\n");
|
|
@@ -1923,7 +2050,7 @@ function scanForbiddenPatterns(source) {
|
|
|
1923
2050
|
}
|
|
1924
2051
|
var FORBIDDEN_PATTERNS;
|
|
1925
2052
|
var init_gates = __esm({
|
|
1926
|
-
"
|
|
2053
|
+
"packages/verify/src/gates.ts"() {
|
|
1927
2054
|
"use strict";
|
|
1928
2055
|
FORBIDDEN_PATTERNS = [
|
|
1929
2056
|
{ name: "eslint-disable", regex: /eslint-disable/ },
|
|
@@ -1935,11 +2062,11 @@ var init_gates = __esm({
|
|
|
1935
2062
|
}
|
|
1936
2063
|
});
|
|
1937
2064
|
|
|
1938
|
-
//
|
|
2065
|
+
// packages/verify/src/tsc-check.ts
|
|
1939
2066
|
import ts from "typescript";
|
|
1940
|
-
import
|
|
2067
|
+
import path6 from "node:path";
|
|
1941
2068
|
function runTscStrict(files) {
|
|
1942
|
-
const program = ts.createProgram(files.map((f) =>
|
|
2069
|
+
const program = ts.createProgram(files.map((f) => path6.resolve(f)), STRICT_OPTIONS);
|
|
1943
2070
|
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
1944
2071
|
const mapped = diagnostics.map((d) => {
|
|
1945
2072
|
const file = d.file?.fileName;
|
|
@@ -1955,7 +2082,7 @@ function runTscStrict(files) {
|
|
|
1955
2082
|
}
|
|
1956
2083
|
var STRICT_OPTIONS;
|
|
1957
2084
|
var init_tsc_check = __esm({
|
|
1958
|
-
"
|
|
2085
|
+
"packages/verify/src/tsc-check.ts"() {
|
|
1959
2086
|
"use strict";
|
|
1960
2087
|
STRICT_OPTIONS = {
|
|
1961
2088
|
strict: true,
|
|
@@ -1975,7 +2102,7 @@ var init_tsc_check = __esm({
|
|
|
1975
2102
|
}
|
|
1976
2103
|
});
|
|
1977
2104
|
|
|
1978
|
-
//
|
|
2105
|
+
// packages/verify/src/token-lint.ts
|
|
1979
2106
|
import strictValue from "stylelint-declaration-strict-value";
|
|
1980
2107
|
import stylelint from "stylelint";
|
|
1981
2108
|
async function runTokenLint(css, fileLabel = "generated.css", definedVars2, recordedMapEmpty = false) {
|
|
@@ -2036,7 +2163,7 @@ async function runTokenLint(css, fileLabel = "generated.css", definedVars2, reco
|
|
|
2036
2163
|
}
|
|
2037
2164
|
var TOKEN_ENFORCED_PROPERTIES, ALLOWED_KEYWORDS, STYLELINT_CONFIG, LEGAL_CUSTOM_PROP;
|
|
2038
2165
|
var init_token_lint = __esm({
|
|
2039
|
-
"
|
|
2166
|
+
"packages/verify/src/token-lint.ts"() {
|
|
2040
2167
|
"use strict";
|
|
2041
2168
|
TOKEN_ENFORCED_PROPERTIES = [
|
|
2042
2169
|
"/color$/",
|
|
@@ -2096,9 +2223,9 @@ var init_token_lint = __esm({
|
|
|
2096
2223
|
}
|
|
2097
2224
|
});
|
|
2098
2225
|
|
|
2099
|
-
//
|
|
2226
|
+
// packages/verify/src/loop.ts
|
|
2100
2227
|
import { rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2101
|
-
import
|
|
2228
|
+
import path7 from "node:path";
|
|
2102
2229
|
async function runChecks(componentName, files, extraFiles, definedVars2) {
|
|
2103
2230
|
const findings = [];
|
|
2104
2231
|
for (const violation of scanForbiddenPatterns(files.tsx)) {
|
|
@@ -2113,10 +2240,10 @@ async function runChecks(componentName, files, extraFiles, definedVars2) {
|
|
|
2113
2240
|
}
|
|
2114
2241
|
const workDir = newScratchDir("loop");
|
|
2115
2242
|
try {
|
|
2116
|
-
const tsxPath =
|
|
2243
|
+
const tsxPath = path7.join(workDir, `${componentName}.tsx`);
|
|
2117
2244
|
writeFileSync2(tsxPath, files.tsx);
|
|
2118
2245
|
for (const [name, content] of Object.entries(extraFiles ?? {})) {
|
|
2119
|
-
writeFileSync2(
|
|
2246
|
+
writeFileSync2(path7.join(workDir, name), content);
|
|
2120
2247
|
}
|
|
2121
2248
|
const tsc = runTscStrict([tsxPath]);
|
|
2122
2249
|
for (const d of tsc.diagnostics) {
|
|
@@ -2170,7 +2297,7 @@ function definedVarsFrom(tokensCss) {
|
|
|
2170
2297
|
}
|
|
2171
2298
|
var SKIPPED_CHECKS;
|
|
2172
2299
|
var init_loop = __esm({
|
|
2173
|
-
"
|
|
2300
|
+
"packages/verify/src/loop.ts"() {
|
|
2174
2301
|
"use strict";
|
|
2175
2302
|
init_runtime();
|
|
2176
2303
|
init_gates();
|
|
@@ -2180,7 +2307,7 @@ var init_loop = __esm({
|
|
|
2180
2307
|
}
|
|
2181
2308
|
});
|
|
2182
2309
|
|
|
2183
|
-
//
|
|
2310
|
+
// packages/verify/src/report.ts
|
|
2184
2311
|
import { z as z5 } from "zod";
|
|
2185
2312
|
function isTokenEnforcedProperty(property) {
|
|
2186
2313
|
return enforcedMatchers.some((m) => m.test(property));
|
|
@@ -2220,7 +2347,7 @@ function buildReport(input) {
|
|
|
2220
2347
|
}
|
|
2221
2348
|
var ReportSchema, enforcedMatchers;
|
|
2222
2349
|
var init_report = __esm({
|
|
2223
|
-
"
|
|
2350
|
+
"packages/verify/src/report.ts"() {
|
|
2224
2351
|
"use strict";
|
|
2225
2352
|
init_token_lint();
|
|
2226
2353
|
ReportSchema = z5.object({
|
|
@@ -2282,7 +2409,7 @@ var init_report = __esm({
|
|
|
2282
2409
|
}
|
|
2283
2410
|
});
|
|
2284
2411
|
|
|
2285
|
-
//
|
|
2412
|
+
// packages/verify/src/mount-limits.ts
|
|
2286
2413
|
function lcdTextEnabled() {
|
|
2287
2414
|
return process.env["TENDRIL_ENABLE_LCD_TEXT"] === "1";
|
|
2288
2415
|
}
|
|
@@ -2300,14 +2427,14 @@ function raceMountDeadline(work, deadlineMs, onDeadline) {
|
|
|
2300
2427
|
}
|
|
2301
2428
|
var MEMORY_CAP_ARG, ENGINE_MOUNT_DEADLINE_MS;
|
|
2302
2429
|
var init_mount_limits = __esm({
|
|
2303
|
-
"
|
|
2430
|
+
"packages/verify/src/mount-limits.ts"() {
|
|
2304
2431
|
"use strict";
|
|
2305
2432
|
MEMORY_CAP_ARG = "--js-flags=--max-old-space-size=512";
|
|
2306
2433
|
ENGINE_MOUNT_DEADLINE_MS = 3e4;
|
|
2307
2434
|
}
|
|
2308
2435
|
});
|
|
2309
2436
|
|
|
2310
|
-
//
|
|
2437
|
+
// packages/verify/src/image-diff.ts
|
|
2311
2438
|
import { PNG } from "pngjs";
|
|
2312
2439
|
import pixelmatch from "pixelmatch";
|
|
2313
2440
|
function channelDistance(a, b) {
|
|
@@ -2749,7 +2876,7 @@ function cropPng(png, x, y, width, height) {
|
|
|
2749
2876
|
}
|
|
2750
2877
|
var INK_DELTA, ABSENT_RADIUS, ABSENT_MIN_PX;
|
|
2751
2878
|
var init_image_diff = __esm({
|
|
2752
|
-
"
|
|
2879
|
+
"packages/verify/src/image-diff.ts"() {
|
|
2753
2880
|
"use strict";
|
|
2754
2881
|
INK_DELTA = 30;
|
|
2755
2882
|
ABSENT_RADIUS = 3;
|
|
@@ -2757,9 +2884,9 @@ var init_image_diff = __esm({
|
|
|
2757
2884
|
}
|
|
2758
2885
|
});
|
|
2759
2886
|
|
|
2760
|
-
//
|
|
2761
|
-
import
|
|
2762
|
-
import { fileURLToPath as
|
|
2887
|
+
// packages/verify/src/visual-facts.ts
|
|
2888
|
+
import path8 from "node:path";
|
|
2889
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2763
2890
|
import { build } from "esbuild";
|
|
2764
2891
|
import { chromium } from "playwright-core";
|
|
2765
2892
|
function resolveAxisValue(declared, props, axis, value) {
|
|
@@ -3298,12 +3425,12 @@ ${input.files.css}
|
|
|
3298
3425
|
}
|
|
3299
3426
|
var RESOLVE_DIR, TOLERANCE_PX, WIDTH_SLACK, IMAGE_SIMILARITY_FLOOR, INK_RECALL_FLOOR;
|
|
3300
3427
|
var init_visual_facts = __esm({
|
|
3301
|
-
"
|
|
3428
|
+
"packages/verify/src/visual-facts.ts"() {
|
|
3302
3429
|
"use strict";
|
|
3303
3430
|
init_browser();
|
|
3304
3431
|
init_mount_limits();
|
|
3305
3432
|
init_image_diff();
|
|
3306
|
-
RESOLVE_DIR =
|
|
3433
|
+
RESOLVE_DIR = path8.resolve(path8.dirname(fileURLToPath3(import.meta.url)), "..");
|
|
3307
3434
|
TOLERANCE_PX = 2;
|
|
3308
3435
|
WIDTH_SLACK = 0.25;
|
|
3309
3436
|
IMAGE_SIMILARITY_FLOOR = 0.8;
|
|
@@ -3311,27 +3438,27 @@ var init_visual_facts = __esm({
|
|
|
3311
3438
|
}
|
|
3312
3439
|
});
|
|
3313
3440
|
|
|
3314
|
-
//
|
|
3315
|
-
import
|
|
3316
|
-
import { fileURLToPath as
|
|
3441
|
+
// packages/verify/src/paths.ts
|
|
3442
|
+
import path9 from "node:path";
|
|
3443
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3317
3444
|
var VERIFY_PKG_DIR, REPO_ROOT;
|
|
3318
3445
|
var init_paths = __esm({
|
|
3319
|
-
"
|
|
3446
|
+
"packages/verify/src/paths.ts"() {
|
|
3320
3447
|
"use strict";
|
|
3321
|
-
VERIFY_PKG_DIR =
|
|
3322
|
-
REPO_ROOT =
|
|
3448
|
+
VERIFY_PKG_DIR = path9.resolve(path9.dirname(fileURLToPath4(import.meta.url)), "..");
|
|
3449
|
+
REPO_ROOT = path9.resolve(VERIFY_PKG_DIR, "..", "..");
|
|
3323
3450
|
}
|
|
3324
3451
|
});
|
|
3325
3452
|
|
|
3326
|
-
//
|
|
3453
|
+
// packages/verify/src/font-resolve.ts
|
|
3327
3454
|
import { createHash } from "node:crypto";
|
|
3328
|
-
import { existsSync as
|
|
3455
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3329
3456
|
import os2 from "node:os";
|
|
3330
|
-
import
|
|
3457
|
+
import path10 from "node:path";
|
|
3331
3458
|
function fontCacheDir() {
|
|
3332
3459
|
const env = process.env["TENDRIL_FONT_CACHE"];
|
|
3333
|
-
if (env !== void 0 && env !== "") return
|
|
3334
|
-
return
|
|
3460
|
+
if (env !== void 0 && env !== "") return path10.resolve(env);
|
|
3461
|
+
return path10.join(os2.homedir(), ".tendril", "fonts");
|
|
3335
3462
|
}
|
|
3336
3463
|
function normalizeFontLicense(value) {
|
|
3337
3464
|
return typeof value === "string" && FONT_LICENSES.includes(value) ? value : "unknown";
|
|
@@ -3396,25 +3523,25 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3396
3523
|
}
|
|
3397
3524
|
const bytes = new Uint8Array(await fileRes.arrayBuffer());
|
|
3398
3525
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
3399
|
-
const file =
|
|
3526
|
+
const file = path10.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}.woff2`);
|
|
3400
3527
|
writeFileSync3(file, bytes);
|
|
3401
3528
|
resolved.push({ family, weight, source: url, sha256, file, license: meta.license, ...meta.family !== null ? { servedFamily: meta.family } : {} });
|
|
3402
3529
|
} catch (err) {
|
|
3403
3530
|
failures.push({ family, weight, reason: `download failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
3404
3531
|
}
|
|
3405
3532
|
}
|
|
3406
|
-
const mPath =
|
|
3407
|
-
const prior =
|
|
3408
|
-
const portable2 = resolved.map((m) => ({ ...m, file:
|
|
3533
|
+
const mPath = path10.join(cacheDir, "manifest.json");
|
|
3534
|
+
const prior = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3535
|
+
const portable2 = resolved.map((m) => ({ ...m, file: path10.basename(m.file) }));
|
|
3409
3536
|
const merged = [...prior.filter((p) => !resolved.some((m) => m.family === p.family && m.weight === p.weight)), ...portable2];
|
|
3410
3537
|
if (resolved.length > 0) writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
3411
3538
|
`);
|
|
3412
3539
|
return { resolved, failures };
|
|
3413
3540
|
}
|
|
3414
3541
|
function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3415
|
-
const src =
|
|
3416
|
-
if (!
|
|
3417
|
-
const ext =
|
|
3542
|
+
const src = path10.resolve(filePath);
|
|
3543
|
+
if (!existsSync6(src)) throw new Error(`font file not found: ${src}`);
|
|
3544
|
+
const ext = path10.extname(src).toLowerCase();
|
|
3418
3545
|
if (![".woff2", ".woff", ".ttf", ".otf"].includes(ext)) {
|
|
3419
3546
|
throw new Error(`unsupported font file "${ext}" \u2014 use .woff2 (preferred), .woff, .ttf or .otf`);
|
|
3420
3547
|
}
|
|
@@ -3422,20 +3549,20 @@ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3422
3549
|
if (bytes.length === 0) throw new Error(`font file is empty: ${src}`);
|
|
3423
3550
|
mkdirSync2(cacheDir, { recursive: true });
|
|
3424
3551
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
3425
|
-
const file =
|
|
3552
|
+
const file = path10.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}${ext}`);
|
|
3426
3553
|
writeFileSync3(file, bytes);
|
|
3427
|
-
const face = { family, weight, source: `local:${
|
|
3428
|
-
const mPath =
|
|
3429
|
-
const prior =
|
|
3430
|
-
const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file:
|
|
3554
|
+
const face = { family, weight, source: `local:${path10.basename(src)}`, sha256, file, license: "unknown" };
|
|
3555
|
+
const mPath = path10.join(cacheDir, "manifest.json");
|
|
3556
|
+
const prior = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3557
|
+
const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file: path10.basename(file) }];
|
|
3431
3558
|
writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
3432
3559
|
`);
|
|
3433
3560
|
return face;
|
|
3434
3561
|
}
|
|
3435
3562
|
function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3436
3563
|
const lock = JSON.parse(readFileSync3(lockPath, "utf8"));
|
|
3437
|
-
const mPath =
|
|
3438
|
-
const manifest =
|
|
3564
|
+
const mPath = path10.join(cacheDir, "manifest.json");
|
|
3565
|
+
const manifest = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3439
3566
|
return lock.map((l) => {
|
|
3440
3567
|
const m = manifest.find((x) => x.family === l.family && x.weight === l.weight);
|
|
3441
3568
|
if (m === void 0) return { family: l.family, weight: l.weight, status: "missing" };
|
|
@@ -3443,8 +3570,8 @@ function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3443
3570
|
});
|
|
3444
3571
|
}
|
|
3445
3572
|
function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3446
|
-
const mPath =
|
|
3447
|
-
if (!
|
|
3573
|
+
const mPath = path10.join(cacheDir, "manifest.json");
|
|
3574
|
+
if (!existsSync6(mPath)) return [];
|
|
3448
3575
|
let entries;
|
|
3449
3576
|
try {
|
|
3450
3577
|
entries = JSON.parse(readFileSync3(mPath, "utf8"));
|
|
@@ -3461,8 +3588,8 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3461
3588
|
return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
|
|
3462
3589
|
}
|
|
3463
3590
|
function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3464
|
-
const mPath =
|
|
3465
|
-
const manifest =
|
|
3591
|
+
const mPath = path10.join(cacheDir, "manifest.json");
|
|
3592
|
+
const manifest = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3466
3593
|
const wanted = new Set(families.map((f) => f.toLowerCase()));
|
|
3467
3594
|
return manifest.filter((f) => wanted.has(f.family.toLowerCase())).map((f) => ({
|
|
3468
3595
|
family: f.family,
|
|
@@ -3475,8 +3602,8 @@ function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3475
3602
|
}));
|
|
3476
3603
|
}
|
|
3477
3604
|
function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3478
|
-
const mPath =
|
|
3479
|
-
if (!
|
|
3605
|
+
const mPath = path10.join(cacheDir, "manifest.json");
|
|
3606
|
+
if (!existsSync6(mPath)) return [];
|
|
3480
3607
|
let entries;
|
|
3481
3608
|
try {
|
|
3482
3609
|
entries = JSON.parse(readFileSync3(mPath, "utf8"));
|
|
@@ -3488,8 +3615,8 @@ function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3488
3615
|
).map((e) => ({ family: e.family, weight: e.weight, sha256: e.sha256 }));
|
|
3489
3616
|
}
|
|
3490
3617
|
function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3491
|
-
const mPath =
|
|
3492
|
-
if (!
|
|
3618
|
+
const mPath = path10.join(cacheDir, "manifest.json");
|
|
3619
|
+
if (!existsSync6(mPath)) return [];
|
|
3493
3620
|
let entries;
|
|
3494
3621
|
try {
|
|
3495
3622
|
entries = JSON.parse(readFileSync3(mPath, "utf8"));
|
|
@@ -3499,8 +3626,8 @@ function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3499
3626
|
const byFamily = /* @__PURE__ */ new Map();
|
|
3500
3627
|
for (const e of entries) {
|
|
3501
3628
|
if (typeof e.family !== "string" || typeof e.weight !== "number" || typeof e.file !== "string") continue;
|
|
3502
|
-
const file =
|
|
3503
|
-
if (!
|
|
3629
|
+
const file = path10.isAbsolute(e.file) && existsSync6(e.file) ? e.file : path10.resolve(cacheDir, path10.basename(e.file));
|
|
3630
|
+
if (!existsSync6(file)) continue;
|
|
3504
3631
|
if (createHash("sha256").update(readFileSync3(file)).digest("hex") !== e.sha256) continue;
|
|
3505
3632
|
const set = byFamily.get(e.family) ?? /* @__PURE__ */ new Set();
|
|
3506
3633
|
set.add(e.weight);
|
|
@@ -3510,7 +3637,7 @@ function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3510
3637
|
}
|
|
3511
3638
|
var DEFAULT_FONT_CACHE, UA, FONT_LICENSES, GOOGLE_LICENSE_IDS, GOOGLE_FONT_FILE_PREFIX;
|
|
3512
3639
|
var init_font_resolve = __esm({
|
|
3513
|
-
"
|
|
3640
|
+
"packages/verify/src/font-resolve.ts"() {
|
|
3514
3641
|
"use strict";
|
|
3515
3642
|
DEFAULT_FONT_CACHE = fontCacheDir();
|
|
3516
3643
|
UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36";
|
|
@@ -3525,65 +3652,76 @@ var init_font_resolve = __esm({
|
|
|
3525
3652
|
}
|
|
3526
3653
|
});
|
|
3527
3654
|
|
|
3528
|
-
//
|
|
3655
|
+
// packages/verify/src/font-faces.ts
|
|
3529
3656
|
import { createHash as createHash2 } from "node:crypto";
|
|
3530
|
-
import { existsSync as
|
|
3531
|
-
import
|
|
3532
|
-
function
|
|
3533
|
-
if (!
|
|
3657
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
|
|
3658
|
+
import path11 from "node:path";
|
|
3659
|
+
function injectedGroups(manifestPath2) {
|
|
3660
|
+
if (!existsSync7(manifestPath2)) return { groups: [], shared: false };
|
|
3534
3661
|
const claimed = JSON.parse(readFileSync4(manifestPath2, "utf8"));
|
|
3535
|
-
const
|
|
3536
|
-
if (path10.isAbsolute(f) && existsSync6(f)) return f;
|
|
3537
|
-
return path10.resolve(path10.dirname(manifestPath2), path10.basename(f));
|
|
3538
|
-
};
|
|
3539
|
-
const manifest = claimed.filter((f) => {
|
|
3540
|
-
const file = resolveEntry(f.file);
|
|
3541
|
-
if (!existsSync6(file)) return false;
|
|
3542
|
-
return createHash2("sha256").update(readFileSync4(file)).digest("hex") === f.sha256;
|
|
3543
|
-
});
|
|
3544
|
-
const resolveFile = (f) => {
|
|
3545
|
-
if (path10.isAbsolute(f) && existsSync6(f)) return f;
|
|
3546
|
-
return path10.resolve(path10.dirname(manifestPath2), path10.basename(f));
|
|
3547
|
-
};
|
|
3662
|
+
const resolveFile = (f) => path11.isAbsolute(f) && existsSync7(f) ? f : path11.resolve(path11.dirname(manifestPath2), path11.basename(f));
|
|
3548
3663
|
const byFile = /* @__PURE__ */ new Map();
|
|
3549
|
-
for (const f of
|
|
3664
|
+
for (const f of claimed) {
|
|
3665
|
+
const file = resolveFile(f.file);
|
|
3666
|
+
if (!existsSync7(file)) continue;
|
|
3667
|
+
if (createHash2("sha256").update(readFileSync4(file)).digest("hex") !== f.sha256) continue;
|
|
3550
3668
|
const k = `${f.family}:${f.file}`;
|
|
3551
|
-
const e = byFile.get(k) ?? { family: f.family, weights: [], file
|
|
3669
|
+
const e = byFile.get(k) ?? { family: f.family, weights: [], file };
|
|
3552
3670
|
e.weights.push(f.weight);
|
|
3553
3671
|
byFile.set(k, e);
|
|
3554
3672
|
}
|
|
3555
|
-
const
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3673
|
+
const groups = [...byFile.values()];
|
|
3674
|
+
return { groups, shared: new Set(groups.map((e) => e.file)).size < groups.length };
|
|
3675
|
+
}
|
|
3676
|
+
function fontFaceCss(manifestPath2 = path11.join(fontCacheDir(), "manifest.json")) {
|
|
3677
|
+
const { groups, shared } = injectedGroups(manifestPath2);
|
|
3678
|
+
return groups.map((e) => {
|
|
3679
|
+
const weight = shared || e.weights.length > 1 ? `${SPAN[0]} ${SPAN[1]}` : String(e.weights[0]);
|
|
3559
3680
|
return `@font-face { font-family: '${e.family}'; font-weight: ${weight}; font-style: normal; src: url(data:font/woff2;base64,${readFileSync4(e.file).toString("base64")}) format('woff2'); }`;
|
|
3560
3681
|
}).join("\n");
|
|
3561
3682
|
}
|
|
3683
|
+
function injectedFamilyWeights(manifestPath2 = path11.join(fontCacheDir(), "manifest.json")) {
|
|
3684
|
+
const { groups, shared } = injectedGroups(manifestPath2);
|
|
3685
|
+
const out = /* @__PURE__ */ new Map();
|
|
3686
|
+
for (const g of groups) {
|
|
3687
|
+
const key = g.family.toLowerCase();
|
|
3688
|
+
const e = out.get(key) ?? { weights: /* @__PURE__ */ new Set(), span: false };
|
|
3689
|
+
for (const w of g.weights) e.weights.add(w);
|
|
3690
|
+
if (shared || g.weights.length > 1) e.span = true;
|
|
3691
|
+
out.set(key, e);
|
|
3692
|
+
}
|
|
3693
|
+
return out;
|
|
3694
|
+
}
|
|
3695
|
+
function servesWeight(coverage, weight) {
|
|
3696
|
+
return coverage.weights.has(weight) || coverage.span && weight >= SPAN[0] && weight <= SPAN[1];
|
|
3697
|
+
}
|
|
3698
|
+
var SPAN;
|
|
3562
3699
|
var init_font_faces = __esm({
|
|
3563
|
-
"
|
|
3700
|
+
"packages/verify/src/font-faces.ts"() {
|
|
3564
3701
|
"use strict";
|
|
3565
3702
|
init_font_resolve();
|
|
3703
|
+
SPAN = [100, 700];
|
|
3566
3704
|
}
|
|
3567
3705
|
});
|
|
3568
3706
|
|
|
3569
|
-
//
|
|
3570
|
-
import { readFileSync as readFileSync5, readdirSync as readdirSync2, existsSync as
|
|
3571
|
-
import
|
|
3707
|
+
// packages/verify/src/admission.ts
|
|
3708
|
+
import { readFileSync as readFileSync5, readdirSync as readdirSync2, existsSync as existsSync8, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3709
|
+
import path12 from "node:path";
|
|
3572
3710
|
import { build as build2 } from "esbuild";
|
|
3573
3711
|
import postcss from "postcss";
|
|
3574
3712
|
import tailwindcss from "tailwindcss";
|
|
3575
3713
|
import { chromium as chromium2 } from "playwright-core";
|
|
3576
3714
|
function fontWeightsByFamily() {
|
|
3577
|
-
const mPath =
|
|
3715
|
+
const mPath = path12.join(fontCacheDir(), "manifest.json");
|
|
3578
3716
|
const out = /* @__PURE__ */ new Map();
|
|
3579
|
-
if (!
|
|
3717
|
+
if (!existsSync8(mPath)) return out;
|
|
3580
3718
|
for (const f of JSON.parse(readFileSync5(mPath, "utf8")))
|
|
3581
3719
|
out.set(f.family, [...out.get(f.family) ?? [], f.weight]);
|
|
3582
3720
|
return out;
|
|
3583
3721
|
}
|
|
3584
3722
|
var FONT_WEIGHTS;
|
|
3585
3723
|
var init_admission = __esm({
|
|
3586
|
-
"
|
|
3724
|
+
"packages/verify/src/admission.ts"() {
|
|
3587
3725
|
"use strict";
|
|
3588
3726
|
init_paths();
|
|
3589
3727
|
init_src();
|
|
@@ -3596,11 +3734,11 @@ var init_admission = __esm({
|
|
|
3596
3734
|
}
|
|
3597
3735
|
});
|
|
3598
3736
|
|
|
3599
|
-
//
|
|
3600
|
-
import
|
|
3737
|
+
// packages/verify/src/tasks.ts
|
|
3738
|
+
import path13 from "node:path";
|
|
3601
3739
|
var CALENDAR_CONFIGS, CALENDAR_API, CALENDAR_BEHAVIORS, BUTTON_CONFIGS, BUTTON_API, COMBO_FIX, COMBO_CONFIGS, COMBO_API, MODAL_CONFIGS, MODAL_API, BUTTON_BEHAVIORS, COMBO_BEHAVIORS, MODAL_BEHAVIORS, TASKS;
|
|
3602
3740
|
var init_tasks = __esm({
|
|
3603
|
-
"
|
|
3741
|
+
"packages/verify/src/tasks.ts"() {
|
|
3604
3742
|
"use strict";
|
|
3605
3743
|
init_paths();
|
|
3606
3744
|
CALENDAR_CONFIGS = [
|
|
@@ -3677,7 +3815,7 @@ export function Button(props: {
|
|
|
3677
3815
|
[key: string]: unknown; // MUST spread unknown props (incl. data-*) onto the root element
|
|
3678
3816
|
})
|
|
3679
3817
|
|
|
3680
|
-
Rules: plain CSS in styles.css (tokens.css optional, loaded first) \u2014 no Tailwind, no imports beyond react/react-dom. disabled buttons are unfocusable; the spinner honors prefers-reduced-motion (animation: none under the media query). Interactive states are REAL (:hover, :focus-visible) AND statically forceable via the data-tendril-state attribute ("hover" | "focus-visible") spread onto the root; forced and real selectors must share one declaration block, e.g. :is(:hover, [data-tendril-state~="hover"]).
|
|
3818
|
+
Rules: plain CSS in styles.css (tokens.css optional, loaded first) \u2014 no Tailwind, no imports beyond react/react-dom. disabled buttons are unfocusable; the spinner honors prefers-reduced-motion (animation: none under the media query). Interactive states are REAL (:hover, :focus-visible) AND statically forceable via the data-tendril-state attribute ("hover" | "focus-visible") spread onto the root; forced and real selectors must share one declaration block, e.g. :is(:hover, [data-tendril-state~="hover"]). ROOT SIZE IS YOURS AND PIXELS DO NOT CHECK IT, IN EITHER DIRECTION: the mount floors your root to the recorded box (#root > *{min-width:<recorded w>px;min-height:<recorded h>px} \u2014 a floor, never a cap) and the image the scorer compares is a CROP of that box. So a button that computes NARROWER or SHORTER is stretched back and captures byte-identically to a correct one (measured on a real <button>), and an oversized one shows only where its own border, radius or shadow lands back inside the crop. Take both root dimensions from the recorded box, never from a score. Fonts: 'Inter' is provided. Inline all SVGs (loading spinner, icons).
|
|
3681
3819
|
|
|
3682
3820
|
BEHAVIORAL CONTRACT (machine-verified, gating): the root is a focusable <button> with cursor:pointer; real :hover visibly changes it; the loading spinner has a RUNNING CSS animation. Static lookalikes fail.`;
|
|
3683
3821
|
COMBO_FIX = {
|
|
@@ -3757,7 +3895,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
3757
3895
|
];
|
|
3758
3896
|
TASKS = {
|
|
3759
3897
|
calendar: {
|
|
3760
|
-
set:
|
|
3898
|
+
set: path13.join(REPO_ROOT, "examples/recordings/shadcn-poc-calendar"),
|
|
3761
3899
|
entry: "Calendar.tsx",
|
|
3762
3900
|
configs: CALENDAR_CONFIGS,
|
|
3763
3901
|
systemApi: CALENDAR_API,
|
|
@@ -3765,7 +3903,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
3765
3903
|
prelude: { controls: ['[data-tendril-part="day"]'], textInputs: [] }
|
|
3766
3904
|
},
|
|
3767
3905
|
"shadcn-button": {
|
|
3768
|
-
set:
|
|
3906
|
+
set: path13.join(REPO_ROOT, "examples/recordings/shadcn-poc-button"),
|
|
3769
3907
|
entry: "Button.tsx",
|
|
3770
3908
|
configs: BUTTON_CONFIGS,
|
|
3771
3909
|
systemApi: BUTTON_API,
|
|
@@ -3773,7 +3911,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
3773
3911
|
prelude: { controls: ["> *"], textInputs: [] }
|
|
3774
3912
|
},
|
|
3775
3913
|
combobox: {
|
|
3776
|
-
set:
|
|
3914
|
+
set: path13.join(REPO_ROOT, "examples/recordings/carbon-poc-combobox"),
|
|
3777
3915
|
entry: "ComboBox.tsx",
|
|
3778
3916
|
configs: COMBO_CONFIGS,
|
|
3779
3917
|
systemApi: COMBO_API,
|
|
@@ -3781,7 +3919,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
3781
3919
|
prelude: { controls: ['[role="option"]'], textInputs: ["input"], popover: { selector: '[role="listbox"]', trigger: "input" } }
|
|
3782
3920
|
},
|
|
3783
3921
|
modal: {
|
|
3784
|
-
set:
|
|
3922
|
+
set: path13.join(REPO_ROOT, "examples/recordings/carbon-poc-modal"),
|
|
3785
3923
|
entry: "Modal.tsx",
|
|
3786
3924
|
configs: MODAL_CONFIGS,
|
|
3787
3925
|
systemApi: MODAL_API,
|
|
@@ -3792,15 +3930,15 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
3792
3930
|
}
|
|
3793
3931
|
});
|
|
3794
3932
|
|
|
3795
|
-
//
|
|
3933
|
+
// packages/verify/src/behavior.ts
|
|
3796
3934
|
var behavior_exports = {};
|
|
3797
3935
|
__export(behavior_exports, {
|
|
3798
3936
|
checkBehaviors: () => checkBehaviors,
|
|
3799
3937
|
compileMount: () => compileMount,
|
|
3800
3938
|
recordingIsDark: () => recordingIsDark
|
|
3801
3939
|
});
|
|
3802
|
-
import { existsSync as
|
|
3803
|
-
import
|
|
3940
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
|
|
3941
|
+
import path14 from "node:path";
|
|
3804
3942
|
import { build as build3 } from "esbuild";
|
|
3805
3943
|
import { chromium as chromium3 } from "playwright-core";
|
|
3806
3944
|
import { PNG as PNG2 } from "pngjs";
|
|
@@ -3809,12 +3947,12 @@ function getFontFaces() {
|
|
|
3809
3947
|
return _fontFaces;
|
|
3810
3948
|
}
|
|
3811
3949
|
async function compileMount(task, bundleDir) {
|
|
3812
|
-
const entryTsx =
|
|
3813
|
-
if (!
|
|
3950
|
+
const entryTsx = path14.join(bundleDir, task.entry);
|
|
3951
|
+
if (!existsSync9(entryTsx)) return { error: `${task.entry} missing` };
|
|
3814
3952
|
const mountSrc = `
|
|
3815
3953
|
import { createElement } from "react";
|
|
3816
3954
|
import { createRoot } from "react-dom/client";
|
|
3817
|
-
import * as B from ${JSON.stringify(
|
|
3955
|
+
import * as B from ${JSON.stringify(path14.resolve(entryTsx))};
|
|
3818
3956
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; spyProps?: string[] } }).__cfg;
|
|
3819
3957
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
3820
3958
|
// Callbacks cannot ride the JSON config: specs NAME spy props and the
|
|
@@ -4066,8 +4204,8 @@ async function runSteps(page, spec, renderPose) {
|
|
|
4066
4204
|
function recordingIsDark(task) {
|
|
4067
4205
|
const rep = task.configs[0]?.rep;
|
|
4068
4206
|
if (rep === void 0) return false;
|
|
4069
|
-
const f =
|
|
4070
|
-
if (!
|
|
4207
|
+
const f = path14.join(task.set, rep, "get_screenshot.json");
|
|
4208
|
+
if (!existsSync9(f)) return false;
|
|
4071
4209
|
try {
|
|
4072
4210
|
const env = JSON.parse(readFileSync6(f, "utf8")).content.find((c) => c.type === "image");
|
|
4073
4211
|
if (env?.data === void 0) return false;
|
|
@@ -4141,7 +4279,7 @@ async function checkBehaviors(task, bundleDir, opts = {}) {
|
|
|
4141
4279
|
const deadlineMs = timeoutMs + 1e4;
|
|
4142
4280
|
const js = await compileMount(task, bundleDir);
|
|
4143
4281
|
if (typeof js !== "string") return task.behaviors.map((b) => ({ id: b.id, pass: false, detail: js.error }));
|
|
4144
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
4282
|
+
const css = ["tokens.css", "styles.css"].map((f) => path14.join(bundleDir, f)).filter((f) => existsSync9(f)).map((f) => readFileSync6(f, "utf8")).join("\n");
|
|
4145
4283
|
const server = await chromium3.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
4146
4284
|
const browser = await chromium3.connect(server.wsEndpoint());
|
|
4147
4285
|
const results = [];
|
|
@@ -4248,7 +4386,7 @@ body{margin:0;padding:20px}
|
|
|
4248
4386
|
}
|
|
4249
4387
|
var _fontFaces, ANIMATED_PROBE, COMMIT_GAP_CLOSED, POSE_GAP_FLOOR, PRELUDE_ROOT_PROPS;
|
|
4250
4388
|
var init_behavior = __esm({
|
|
4251
|
-
"
|
|
4389
|
+
"packages/verify/src/behavior.ts"() {
|
|
4252
4390
|
"use strict";
|
|
4253
4391
|
init_runtime();
|
|
4254
4392
|
init_browser();
|
|
@@ -4283,9 +4421,9 @@ var init_behavior = __esm({
|
|
|
4283
4421
|
}
|
|
4284
4422
|
});
|
|
4285
4423
|
|
|
4286
|
-
//
|
|
4287
|
-
import { readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as
|
|
4288
|
-
import
|
|
4424
|
+
// packages/verify/src/bundle-quality.ts
|
|
4425
|
+
import { readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "node:fs";
|
|
4426
|
+
import path15 from "node:path";
|
|
4289
4427
|
function definedVars(tokensCss) {
|
|
4290
4428
|
if (tokensCss === void 0) return void 0;
|
|
4291
4429
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -4294,7 +4432,7 @@ function definedVars(tokensCss) {
|
|
|
4294
4432
|
}
|
|
4295
4433
|
function recordedTokenMapEmpty(setDir, reps) {
|
|
4296
4434
|
const readMap = (file) => {
|
|
4297
|
-
if (!
|
|
4435
|
+
if (!existsSync10(file)) return void 0;
|
|
4298
4436
|
try {
|
|
4299
4437
|
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync7(file, "utf8"))) || "{}");
|
|
4300
4438
|
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
@@ -4302,28 +4440,93 @@ function recordedTokenMapEmpty(setDir, reps) {
|
|
|
4302
4440
|
return {};
|
|
4303
4441
|
}
|
|
4304
4442
|
};
|
|
4305
|
-
const setLevel = readMap(
|
|
4443
|
+
const setLevel = readMap(path15.join(setDir, "get_variable_defs.json"));
|
|
4306
4444
|
if (setLevel !== void 0) return Object.keys(setLevel).length === 0;
|
|
4307
4445
|
let recorded = false;
|
|
4308
4446
|
for (const rep of reps) {
|
|
4309
|
-
const m = readMap(
|
|
4447
|
+
const m = readMap(path15.join(setDir, rep, "get_variable_defs.json"));
|
|
4310
4448
|
if (m === void 0) continue;
|
|
4311
4449
|
recorded = true;
|
|
4312
4450
|
if (Object.keys(m).length > 0) return false;
|
|
4313
4451
|
}
|
|
4314
4452
|
return recorded;
|
|
4315
4453
|
}
|
|
4316
|
-
|
|
4454
|
+
function scannable(css) {
|
|
4455
|
+
const blank = (m) => m.replace(/[^\n]/g, " ");
|
|
4456
|
+
return css.replace(/\/\*[^]*?\*\//g, blank).replace(/@font-face\s*{[^}]*}/g, blank);
|
|
4457
|
+
}
|
|
4458
|
+
function usedWeights(css) {
|
|
4459
|
+
const out = /* @__PURE__ */ new Set();
|
|
4460
|
+
for (const m of css.matchAll(/font-weight\s*:\s*([^;}]+)/g)) {
|
|
4461
|
+
const v = declValue(m[1]).toLowerCase();
|
|
4462
|
+
if (v === "bold") out.add(700);
|
|
4463
|
+
else if (v === "normal") out.add(400);
|
|
4464
|
+
else if (/^\d{1,4}$/.test(v)) out.add(Number(v));
|
|
4465
|
+
}
|
|
4466
|
+
return [...out].sort((a, b) => a - b);
|
|
4467
|
+
}
|
|
4468
|
+
function fontStacks(css) {
|
|
4469
|
+
const out = [];
|
|
4470
|
+
for (const m of css.matchAll(/font-family\s*:\s*([^;}]+)/g)) {
|
|
4471
|
+
const text = declValue(m[1]);
|
|
4472
|
+
const families = text.split(",").map((f) => f.trim().replace(/^['"]|['"]$/g, "")).filter((f) => f !== "");
|
|
4473
|
+
if (families.length > 0) out.push({ families, line: css.slice(0, m.index).split("\n").length, text });
|
|
4474
|
+
}
|
|
4475
|
+
return out;
|
|
4476
|
+
}
|
|
4477
|
+
function fontStackFindings(sheets, coverage) {
|
|
4478
|
+
if (coverage.size === 0) return [];
|
|
4479
|
+
const scanned = sheets.map((s) => ({ file: s.file, css: scannable(s.css) }));
|
|
4480
|
+
const weights = [...new Set(scanned.flatMap((s) => usedWeights(s.css)))].sort((a, b) => a - b);
|
|
4481
|
+
if (weights.length === 0) return [];
|
|
4317
4482
|
const findings = [];
|
|
4318
|
-
const
|
|
4319
|
-
const
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4483
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4484
|
+
for (const sheet of scanned) {
|
|
4485
|
+
for (const stack of fontStacks(sheet.css)) {
|
|
4486
|
+
if (seen.has(stack.text)) continue;
|
|
4487
|
+
seen.add(stack.text);
|
|
4488
|
+
const family = stack.families[0];
|
|
4489
|
+
const bound = coverage.get(family.toLowerCase());
|
|
4490
|
+
if (bound === void 0) continue;
|
|
4491
|
+
const unserved = weights.filter((w) => !servesWeight(bound, w));
|
|
4492
|
+
if (unserved.length === 0) continue;
|
|
4493
|
+
const rescue = stack.families.slice(1).find((f) => {
|
|
4494
|
+
const c = coverage.get(f.toLowerCase());
|
|
4495
|
+
return c !== void 0 && unserved.every((w) => servesWeight(c, w));
|
|
4496
|
+
});
|
|
4497
|
+
const has = bound.span ? "100\u2013700" : [...bound.weights].sort((a, b) => a - b).join(", ");
|
|
4498
|
+
const list = unserved.join(", ");
|
|
4499
|
+
const renders = !bound.span && bound.weights.size === 1 ? `renders in the ${[...bound.weights][0]} face` : "renders in whichever of those faces is nearest";
|
|
4500
|
+
findings.push({
|
|
4501
|
+
kind: "font-stack",
|
|
4502
|
+
file: sheet.file,
|
|
4503
|
+
line: stack.line,
|
|
4504
|
+
message: `font-family ${stack.text} binds to '${family}', which the font kit provides at ${has} \u2014 CSS matches the FAMILY first and picks a weight only inside it (a later family is never consulted for a missing weight, and the prelude's font-synthesis: none rules out faux-bold), so text at weight ${list} ${renders}, silently. ` + (rescue !== void 0 ? `'${rescue}' in this same stack provides ${list}: list it first.` : `No family in this stack provides ${list} \u2014 if the recording shows ${unserved.length === 1 ? "that weight" : "those weights"}, run \`tendril fonts resolve "${family}" --weights ${unserved.join(" ")}\` and re-score.`) + " ADVISORY: the finding gates nothing \u2014 but the wrong face changes pixels, and this fix alone took a measured run from 7 to 9 certified."
|
|
4505
|
+
});
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
return findings;
|
|
4509
|
+
}
|
|
4510
|
+
async function checkBundleQuality(bundleDir, entry, set, fontManifest) {
|
|
4511
|
+
const findings = [];
|
|
4512
|
+
const entryPath = path15.join(bundleDir, entry);
|
|
4513
|
+
const cssPath = path15.join(bundleDir, "styles.css");
|
|
4514
|
+
const tokensPath = path15.join(bundleDir, "tokens.css");
|
|
4515
|
+
const css = existsSync10(cssPath) ? readFileSync7(cssPath, "utf8") : "";
|
|
4516
|
+
const tokensCss = existsSync10(tokensPath) ? readFileSync7(tokensPath, "utf8") : void 0;
|
|
4517
|
+
findings.push(
|
|
4518
|
+
...fontStackFindings(
|
|
4519
|
+
[
|
|
4520
|
+
{ file: "styles.css", css },
|
|
4521
|
+
...tokensCss === void 0 ? [] : [{ file: "tokens.css", css: tokensCss }]
|
|
4522
|
+
],
|
|
4523
|
+
injectedFamilyWeights(fontManifest)
|
|
4524
|
+
)
|
|
4525
|
+
);
|
|
4526
|
+
if (existsSync10(entryPath)) {
|
|
4324
4527
|
const workDir = newScratchDir("quality");
|
|
4325
4528
|
try {
|
|
4326
|
-
const tsxPath =
|
|
4529
|
+
const tsxPath = path15.join(workDir, entry);
|
|
4327
4530
|
writeFileSync5(tsxPath, readFileSync7(entryPath, "utf8"));
|
|
4328
4531
|
for (const d of runTscStrict([tsxPath]).diagnostics) {
|
|
4329
4532
|
findings.push({ kind: "tsc", file: entry, ...d.line === void 0 ? {} : { line: d.line }, message: `TS${d.code}: ${d.message}` });
|
|
@@ -4340,17 +4543,20 @@ async function checkBundleQuality(bundleDir, entry, set) {
|
|
|
4340
4543
|
}
|
|
4341
4544
|
return { findings, tokensAbsent: tokensCss === void 0 && !/var\(\s*--/.test(css) };
|
|
4342
4545
|
}
|
|
4546
|
+
var declValue;
|
|
4343
4547
|
var init_bundle_quality = __esm({
|
|
4344
|
-
"
|
|
4548
|
+
"packages/verify/src/bundle-quality.ts"() {
|
|
4345
4549
|
"use strict";
|
|
4346
4550
|
init_src();
|
|
4551
|
+
init_font_faces();
|
|
4347
4552
|
init_runtime();
|
|
4348
4553
|
init_tsc_check();
|
|
4349
4554
|
init_token_lint();
|
|
4555
|
+
declValue = (raw) => raw.trim().replace(/\s*!important$/i, "").trim();
|
|
4350
4556
|
}
|
|
4351
4557
|
});
|
|
4352
4558
|
|
|
4353
|
-
//
|
|
4559
|
+
// packages/verify/src/effect-geometry.ts
|
|
4354
4560
|
function fold(into, dx, dy, radius, spread) {
|
|
4355
4561
|
const reach = radius + spread;
|
|
4356
4562
|
into.top = Math.max(into.top, reach - dy);
|
|
@@ -4382,15 +4588,15 @@ function extentsExplainPad(near, far, pad, tolerance = 2) {
|
|
|
4382
4588
|
}
|
|
4383
4589
|
var NO_EXTENTS;
|
|
4384
4590
|
var init_effect_geometry = __esm({
|
|
4385
|
-
"
|
|
4591
|
+
"packages/verify/src/effect-geometry.ts"() {
|
|
4386
4592
|
"use strict";
|
|
4387
4593
|
NO_EXTENTS = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
4388
4594
|
}
|
|
4389
4595
|
});
|
|
4390
4596
|
|
|
4391
|
-
//
|
|
4392
|
-
import { existsSync as
|
|
4393
|
-
import
|
|
4597
|
+
// packages/verify/src/bundle-score.ts
|
|
4598
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4599
|
+
import path16 from "node:path";
|
|
4394
4600
|
import { build as build4 } from "esbuild";
|
|
4395
4601
|
import { chromium as chromium4 } from "playwright-core";
|
|
4396
4602
|
function getFontFaces2() {
|
|
@@ -4444,7 +4650,7 @@ function chooseSeat(cropAt, ref, padW, padH, backdrop, origin, extents) {
|
|
|
4444
4650
|
}
|
|
4445
4651
|
function metadataRoot(set, rep) {
|
|
4446
4652
|
try {
|
|
4447
|
-
const text = JSON.parse(readFileSync8(
|
|
4653
|
+
const text = JSON.parse(readFileSync8(path16.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4448
4654
|
return parseMetadataStructure(text);
|
|
4449
4655
|
} catch {
|
|
4450
4656
|
return void 0;
|
|
@@ -4499,17 +4705,17 @@ function smallSemanticNodes(set, rep, maxArea = 1024) {
|
|
|
4499
4705
|
});
|
|
4500
4706
|
}
|
|
4501
4707
|
function repMeta(set, rep) {
|
|
4502
|
-
const text = JSON.parse(readFileSync8(
|
|
4708
|
+
const text = JSON.parse(readFileSync8(path16.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4503
4709
|
const root = parseMetadataStructure(text);
|
|
4504
4710
|
return { w: Math.round(root.width ?? 100), h: Math.round(root.height ?? 40) };
|
|
4505
4711
|
}
|
|
4506
4712
|
function repRef(set, rep) {
|
|
4507
|
-
const env = JSON.parse(readFileSync8(
|
|
4713
|
+
const env = JSON.parse(readFileSync8(path16.join(set, rep, "get_screenshot.json"), "utf8")).content.find((c) => c.type === "image");
|
|
4508
4714
|
return Uint8Array.from(Buffer.from(env?.data ?? "", "base64"));
|
|
4509
4715
|
}
|
|
4510
4716
|
function repEffectExtents(set, rep) {
|
|
4511
|
-
const file =
|
|
4512
|
-
if (!
|
|
4717
|
+
const file = path16.join(set, rep, "get_design_context.json");
|
|
4718
|
+
if (!existsSync11(file)) return void 0;
|
|
4513
4719
|
try {
|
|
4514
4720
|
const text = JSON.parse(readFileSync8(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4515
4721
|
const extents = shadowExtents(text);
|
|
@@ -4522,13 +4728,13 @@ async function scoreBundleForTask(task, bundleDir, bar = BAR, opts = {}) {
|
|
|
4522
4728
|
if (opts.evidenceDir !== void 0) mkdirSync3(opts.evidenceDir, { recursive: true });
|
|
4523
4729
|
const CONFIGS2 = task.configs;
|
|
4524
4730
|
const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
|
|
4525
|
-
const entryTsx =
|
|
4526
|
-
if (!
|
|
4527
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
4731
|
+
const entryTsx = path16.join(bundleDir, task.entry);
|
|
4732
|
+
if (!existsSync11(entryTsx)) return CONFIGS2.map((c) => ({ rep: c.rep, similarity: 0, inkRecall: 0, exact: { similarity: 0, inkRecall: 0 }, pass: false, error: `${task.entry} missing` }));
|
|
4733
|
+
const css = ["tokens.css", "styles.css"].map((f) => path16.join(bundleDir, f)).filter((f) => existsSync11(f)).map((f) => readFileSync8(f, "utf8")).join("\n");
|
|
4528
4734
|
const mountSrc = `
|
|
4529
4735
|
import { createElement } from "react";
|
|
4530
4736
|
import { createRoot } from "react-dom/client";
|
|
4531
|
-
import * as B from ${JSON.stringify(
|
|
4737
|
+
import * as B from ${JSON.stringify(path16.resolve(entryTsx))};
|
|
4532
4738
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
|
|
4533
4739
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
4534
4740
|
const root = document.getElementById("root");
|
|
@@ -4623,12 +4829,12 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
|
|
|
4623
4829
|
return name === void 0 ? c : { ...c, name };
|
|
4624
4830
|
});
|
|
4625
4831
|
if (opts.evidenceDir !== void 0) {
|
|
4626
|
-
writeFileSync6(
|
|
4627
|
-
writeFileSync6(
|
|
4628
|
-
writeFileSync6(
|
|
4832
|
+
writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
|
|
4833
|
+
writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
|
|
4834
|
+
writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref));
|
|
4629
4835
|
for (const [i, c] of absent.slice(0, 3).entries()) {
|
|
4630
|
-
writeFileSync6(
|
|
4631
|
-
writeFileSync6(
|
|
4836
|
+
writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
|
|
4837
|
+
writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-render.png`), zoomCrop(scored, c));
|
|
4632
4838
|
}
|
|
4633
4839
|
}
|
|
4634
4840
|
return {
|
|
@@ -4688,7 +4894,7 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
|
|
|
4688
4894
|
}
|
|
4689
4895
|
var BAR, MARGIN, _fontFaces2, CONFIGS, SEAT_WINDOW, EFFECT_BLEED_MAX;
|
|
4690
4896
|
var init_bundle_score = __esm({
|
|
4691
|
-
"
|
|
4897
|
+
"packages/verify/src/bundle-score.ts"() {
|
|
4692
4898
|
"use strict";
|
|
4693
4899
|
init_runtime();
|
|
4694
4900
|
init_browser();
|
|
@@ -4706,11 +4912,11 @@ var init_bundle_score = __esm({
|
|
|
4706
4912
|
}
|
|
4707
4913
|
});
|
|
4708
4914
|
|
|
4709
|
-
//
|
|
4915
|
+
// packages/verify/src/containment.ts
|
|
4710
4916
|
import { build as build5 } from "esbuild";
|
|
4711
4917
|
import { chromium as chromium5 } from "playwright-core";
|
|
4712
4918
|
var init_containment = __esm({
|
|
4713
|
-
"
|
|
4919
|
+
"packages/verify/src/containment.ts"() {
|
|
4714
4920
|
"use strict";
|
|
4715
4921
|
init_runtime();
|
|
4716
4922
|
init_browser();
|
|
@@ -4720,14 +4926,14 @@ var init_containment = __esm({
|
|
|
4720
4926
|
}
|
|
4721
4927
|
});
|
|
4722
4928
|
|
|
4723
|
-
//
|
|
4929
|
+
// packages/verify/src/prelude.ts
|
|
4724
4930
|
var PRELUDE_CONTRACT;
|
|
4725
4931
|
var init_prelude = __esm({
|
|
4726
|
-
"
|
|
4932
|
+
"packages/verify/src/prelude.ts"() {
|
|
4727
4933
|
"use strict";
|
|
4728
4934
|
PRELUDE_CONTRACT = `BUNDLE PRELUDE (machine-verified on computed styles \u2014 the harness page provides NONE of this; your CSS must):
|
|
4729
4935
|
- Containment reset on the component subtree: margin 0, box-sizing border-box, line-height/letter-spacing/font-family inherited from the root (host globals must not leak in).
|
|
4730
|
-
- Component root: -webkit-font-smoothing: antialiased and -moz-osx-font-smoothing: grayscale (recorded rasterization); font-synthesis: none (
|
|
4936
|
+
- Component root: -webkit-font-smoothing: antialiased and -moz-osx-font-smoothing: grayscale (recorded rasterization); font-synthesis: none (no faux bold or italic \u2014 text renders in the face your stack actually binds, so a weight the kit cannot serve reaches the score instead of being faked; it does NOT fall through to the next family in the stack); color-scheme MATCHING YOUR RECORDING (light for a light capture, dark for a dark one) and direction: ltr \u2014 pin them, so the render cannot follow the viewer OS preference and drift from the capture it is graded against; isolation: isolate (own stacking context; overlay z-indexes never fight the host).
|
|
4731
4937
|
- Interactive controls (buttons, options): touch-action: manipulation and user-select: none. Text inputs stay selectable (never user-select: none on them).
|
|
4732
4938
|
- Scrollable popovers/menus: overscroll-behavior: contain.
|
|
4733
4939
|
- Focus indicators bind to :focus-visible, never bare :focus. If the recording contains a focus pose, style the indicator from that recorded truth. If NO focus pose is recorded, do NOT invent ring colors/widths/offsets \u2014 an invented ring is unrecorded pixels; keep the browser's default indicator for keyboard focus and state the gap in your report. Text inputs match :focus-visible even on mouse click BY SPEC; the ONE permitted refinement is suppressing the indicator on a POSITIVELY OBSERVED pointer press, fail-safe toward showing it (programmatic focus, restored focus, and assistive tech all count as keyboard and keep the ring \u2014 WCAG 2.4.7 binds keyboard operation). Never suppress more broadly than an observed press, and never restyle what you kept.
|
|
@@ -4735,9 +4941,9 @@ var init_prelude = __esm({
|
|
|
4735
4941
|
}
|
|
4736
4942
|
});
|
|
4737
4943
|
|
|
4738
|
-
//
|
|
4739
|
-
import { existsSync as
|
|
4740
|
-
import
|
|
4944
|
+
// packages/verify/src/parity.ts
|
|
4945
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
|
|
4946
|
+
import path17 from "node:path";
|
|
4741
4947
|
import { chromium as chromium6 } from "playwright-core";
|
|
4742
4948
|
function getFontFaces3() {
|
|
4743
4949
|
_fontFaces3 ??= fontFaceCss();
|
|
@@ -4766,7 +4972,7 @@ async function checkHoverParity(task, bundleDir, opts = {}) {
|
|
|
4766
4972
|
const deadlineMs = timeoutMs + 1e4;
|
|
4767
4973
|
const js = await compileMount(task, bundleDir);
|
|
4768
4974
|
if (typeof js !== "string") return configs.map((c) => ({ id: `parity:${c.rep}`, pass: false, detail: js.error }));
|
|
4769
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
4975
|
+
const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) => existsSync12(f)).map((f) => readFileSync9(f, "utf8")).join("\n");
|
|
4770
4976
|
const server = await chromium6.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
4771
4977
|
const browser = await chromium6.connect(server.wsEndpoint());
|
|
4772
4978
|
const results = [];
|
|
@@ -4835,7 +5041,7 @@ body{margin:0;padding:20px}
|
|
|
4835
5041
|
}
|
|
4836
5042
|
var _fontFaces3;
|
|
4837
5043
|
var init_parity = __esm({
|
|
4838
|
-
"
|
|
5044
|
+
"packages/verify/src/parity.ts"() {
|
|
4839
5045
|
"use strict";
|
|
4840
5046
|
init_browser();
|
|
4841
5047
|
init_behavior();
|
|
@@ -4844,10 +5050,10 @@ var init_parity = __esm({
|
|
|
4844
5050
|
}
|
|
4845
5051
|
});
|
|
4846
5052
|
|
|
4847
|
-
//
|
|
5053
|
+
// packages/verify/src/composition.ts
|
|
4848
5054
|
import { createRequire as createRequire2 } from "node:module";
|
|
4849
|
-
import { existsSync as
|
|
4850
|
-
import
|
|
5055
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
|
|
5056
|
+
import path18 from "node:path";
|
|
4851
5057
|
import { build as build6 } from "esbuild";
|
|
4852
5058
|
import { chromium as chromium7 } from "playwright-core";
|
|
4853
5059
|
function getFontFaces4() {
|
|
@@ -4855,9 +5061,9 @@ function getFontFaces4() {
|
|
|
4855
5061
|
return _fontFaces4;
|
|
4856
5062
|
}
|
|
4857
5063
|
async function compileInstrumentedMount(task, bundleDir) {
|
|
4858
|
-
const entryTsx =
|
|
4859
|
-
if (!
|
|
4860
|
-
const requireFromVerify = createRequire2(
|
|
5064
|
+
const entryTsx = path18.join(bundleDir, task.entry);
|
|
5065
|
+
if (!existsSync13(entryTsx)) return { error: `${task.entry} missing` };
|
|
5066
|
+
const requireFromVerify = createRequire2(path18.join(VERIFY_PKG_DIR, "package.json"));
|
|
4861
5067
|
let realJsxPath;
|
|
4862
5068
|
try {
|
|
4863
5069
|
realJsxPath = requireFromVerify.resolve("react/jsx-runtime");
|
|
@@ -4868,7 +5074,7 @@ async function compileInstrumentedMount(task, bundleDir) {
|
|
|
4868
5074
|
import { createElement } from "react";
|
|
4869
5075
|
import { createRoot } from "react-dom/client";
|
|
4870
5076
|
import { __registerParts } from "react/jsx-runtime";
|
|
4871
|
-
import * as B from ${JSON.stringify(
|
|
5077
|
+
import * as B from ${JSON.stringify(path18.resolve(entryTsx))};
|
|
4872
5078
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; partComponents: string[] } }).__cfg;
|
|
4873
5079
|
const pairs: Array<[unknown, string]> = [];
|
|
4874
5080
|
for (const name of cfg.partComponents) {
|
|
@@ -4923,7 +5129,7 @@ function expectedParts(task, roles, mainSlug) {
|
|
|
4923
5129
|
}
|
|
4924
5130
|
function interiorRegions(setDir, roles) {
|
|
4925
5131
|
const mains = roles.main;
|
|
4926
|
-
const withInterior = mains.filter((m) =>
|
|
5132
|
+
const withInterior = mains.filter((m) => existsSync13(path18.join(setDir, m, "get_metadata_interior.json")));
|
|
4927
5133
|
if (withInterior.length === 0) {
|
|
4928
5134
|
return { unavailable: "no interior geometry recorded for any main (pre-obligation set) \u2014 re-record with `tendril record` to enable crop checks" };
|
|
4929
5135
|
}
|
|
@@ -4940,7 +5146,7 @@ async function checkCropComposition(task, bundleDir, regions, opts = {}) {
|
|
|
4940
5146
|
if (typeof js !== "string") {
|
|
4941
5147
|
return regions.map((r) => ({ id: cropId(r), pass: false, similarity: 0, inkRecall: 0, detail: js.error }));
|
|
4942
5148
|
}
|
|
4943
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
5149
|
+
const css = ["tokens.css", "styles.css"].map((f) => path18.join(bundleDir, f)).filter((f) => existsSync13(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
|
|
4944
5150
|
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
4945
5151
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
4946
5152
|
const PAD = 4;
|
|
@@ -5045,7 +5251,7 @@ async function checkStructuralComposition(task, bundleDir, roles, opts = {}) {
|
|
|
5045
5251
|
const deadlineMs = timeoutMs + 1e4;
|
|
5046
5252
|
const js = await compileInstrumentedMount(task, bundleDir);
|
|
5047
5253
|
if (typeof js !== "string") return [...results, ...mains.map((m) => ({ id: `composition:${m}`, pass: false, detail: js.error }))];
|
|
5048
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
5254
|
+
const css = ["tokens.css", "styles.css"].map((f) => path18.join(bundleDir, f)).filter((f) => existsSync13(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
|
|
5049
5255
|
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
5050
5256
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
5051
5257
|
try {
|
|
@@ -5110,7 +5316,7 @@ body{margin:0;padding:20px}
|
|
|
5110
5316
|
}
|
|
5111
5317
|
var _fontFaces4, REAL_JSX_SPEC, JSX_SHIM, CROP_BAR, cropId;
|
|
5112
5318
|
var init_composition = __esm({
|
|
5113
|
-
"
|
|
5319
|
+
"packages/verify/src/composition.ts"() {
|
|
5114
5320
|
"use strict";
|
|
5115
5321
|
init_runtime();
|
|
5116
5322
|
init_browser();
|
|
@@ -5134,18 +5340,18 @@ export const jsxs = (t, p, k) => real.jsxs(t, inject(t, p), k);
|
|
|
5134
5340
|
}
|
|
5135
5341
|
});
|
|
5136
5342
|
|
|
5137
|
-
//
|
|
5138
|
-
import { existsSync as
|
|
5139
|
-
import
|
|
5343
|
+
// packages/verify/src/occlusion.ts
|
|
5344
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
5345
|
+
import path19 from "node:path";
|
|
5140
5346
|
import { build as build7 } from "esbuild";
|
|
5141
5347
|
import { chromium as chromium8 } from "playwright-core";
|
|
5142
5348
|
async function compileTwoUp(task, bundleDir) {
|
|
5143
|
-
const entryTsx =
|
|
5144
|
-
if (!
|
|
5349
|
+
const entryTsx = path19.join(bundleDir, task.entry);
|
|
5350
|
+
if (!existsSync14(entryTsx)) return { error: `${task.entry} missing` };
|
|
5145
5351
|
const src = `
|
|
5146
5352
|
import { createElement } from "react";
|
|
5147
5353
|
import { createRoot } from "react-dom/client";
|
|
5148
|
-
import * as B from ${JSON.stringify(
|
|
5354
|
+
import * as B from ${JSON.stringify(path19.resolve(entryTsx))};
|
|
5149
5355
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
|
|
5150
5356
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
5151
5357
|
for (const id of ["first", "second"]) {
|
|
@@ -5283,7 +5489,7 @@ body{margin:0;padding:16px;display:flex;flex-direction:column;gap:8px;align-item
|
|
|
5283
5489
|
}
|
|
5284
5490
|
var OVERLAY_SELECTOR;
|
|
5285
5491
|
var init_occlusion = __esm({
|
|
5286
|
-
"
|
|
5492
|
+
"packages/verify/src/occlusion.ts"() {
|
|
5287
5493
|
"use strict";
|
|
5288
5494
|
init_runtime();
|
|
5289
5495
|
init_browser();
|
|
@@ -5292,9 +5498,9 @@ var init_occlusion = __esm({
|
|
|
5292
5498
|
}
|
|
5293
5499
|
});
|
|
5294
5500
|
|
|
5295
|
-
//
|
|
5501
|
+
// packages/verify/src/index.ts
|
|
5296
5502
|
var init_src4 = __esm({
|
|
5297
|
-
"
|
|
5503
|
+
"packages/verify/src/index.ts"() {
|
|
5298
5504
|
"use strict";
|
|
5299
5505
|
init_browser();
|
|
5300
5506
|
init_runtime();
|
|
@@ -5322,22 +5528,22 @@ var init_src4 = __esm({
|
|
|
5322
5528
|
}
|
|
5323
5529
|
});
|
|
5324
5530
|
|
|
5325
|
-
//
|
|
5326
|
-
import { existsSync as
|
|
5327
|
-
import
|
|
5531
|
+
// packages/cli/src/environment.ts
|
|
5532
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11 } from "node:fs";
|
|
5533
|
+
import path20 from "node:path";
|
|
5328
5534
|
import { createHash as createHash3 } from "node:crypto";
|
|
5329
|
-
import { fileURLToPath as
|
|
5535
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
5330
5536
|
function cliVersion() {
|
|
5331
5537
|
try {
|
|
5332
|
-
return JSON.parse(readFileSync11(
|
|
5538
|
+
return JSON.parse(readFileSync11(path20.join(path20.dirname(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
|
|
5333
5539
|
} catch {
|
|
5334
5540
|
return "dev";
|
|
5335
5541
|
}
|
|
5336
5542
|
}
|
|
5337
5543
|
function environmentStamp(taskFamilies) {
|
|
5338
|
-
const manifestPath2 =
|
|
5544
|
+
const manifestPath2 = path20.join(fontCacheDir(), "manifest.json");
|
|
5339
5545
|
let fontsHash = null;
|
|
5340
|
-
if (
|
|
5546
|
+
if (existsSync15(manifestPath2)) {
|
|
5341
5547
|
try {
|
|
5342
5548
|
const entries = JSON.parse(readFileSync11(manifestPath2, "utf8"));
|
|
5343
5549
|
const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
|
|
@@ -5357,20 +5563,20 @@ function environmentStamp(taskFamilies) {
|
|
|
5357
5563
|
return { chrome, chromeVersion: ver === null ? null : ver + lcdSuffix, fontsManifestSha256: fontsHash };
|
|
5358
5564
|
}
|
|
5359
5565
|
var init_environment = __esm({
|
|
5360
|
-
"
|
|
5566
|
+
"packages/cli/src/environment.ts"() {
|
|
5361
5567
|
"use strict";
|
|
5362
5568
|
init_src4();
|
|
5363
5569
|
}
|
|
5364
5570
|
});
|
|
5365
5571
|
|
|
5366
|
-
//
|
|
5572
|
+
// packages/cli/src/describe.ts
|
|
5367
5573
|
function printDescription(description) {
|
|
5368
5574
|
process.stdout.write(`${JSON.stringify(description, null, 2)}
|
|
5369
5575
|
`);
|
|
5370
5576
|
}
|
|
5371
5577
|
var COMMON_EXIT_CODES;
|
|
5372
5578
|
var init_describe = __esm({
|
|
5373
|
-
"
|
|
5579
|
+
"packages/cli/src/describe.ts"() {
|
|
5374
5580
|
"use strict";
|
|
5375
5581
|
COMMON_EXIT_CODES = {
|
|
5376
5582
|
0: "success",
|
|
@@ -5382,9 +5588,9 @@ var init_describe = __esm({
|
|
|
5382
5588
|
}
|
|
5383
5589
|
});
|
|
5384
5590
|
|
|
5385
|
-
//
|
|
5386
|
-
import { existsSync as
|
|
5387
|
-
import
|
|
5591
|
+
// packages/cli/src/env.ts
|
|
5592
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
|
|
5593
|
+
import path21 from "node:path";
|
|
5388
5594
|
function parseEnv(content) {
|
|
5389
5595
|
const entries = /* @__PURE__ */ new Map();
|
|
5390
5596
|
for (const line of content.split("\n")) {
|
|
@@ -5396,17 +5602,17 @@ function parseEnv(content) {
|
|
|
5396
5602
|
function resolveCredential(name) {
|
|
5397
5603
|
const fromProcess = process.env[name];
|
|
5398
5604
|
if (fromProcess) return fromProcess;
|
|
5399
|
-
const envPath =
|
|
5400
|
-
if (!
|
|
5605
|
+
const envPath = path21.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
|
|
5606
|
+
if (!existsSync16(envPath)) return void 0;
|
|
5401
5607
|
return parseEnv(readFileSync12(envPath, "utf8")).get(name);
|
|
5402
5608
|
}
|
|
5403
5609
|
var init_env = __esm({
|
|
5404
|
-
"
|
|
5610
|
+
"packages/cli/src/env.ts"() {
|
|
5405
5611
|
"use strict";
|
|
5406
5612
|
}
|
|
5407
5613
|
});
|
|
5408
5614
|
|
|
5409
|
-
//
|
|
5615
|
+
// packages/cli/src/output.ts
|
|
5410
5616
|
var output_exports = {};
|
|
5411
5617
|
__export(output_exports, {
|
|
5412
5618
|
PROGRESS_PREFIX: () => PROGRESS_PREFIX,
|
|
@@ -5450,23 +5656,23 @@ function warn(flags, message) {
|
|
|
5450
5656
|
}
|
|
5451
5657
|
var PROGRESS_PREFIX;
|
|
5452
5658
|
var init_output = __esm({
|
|
5453
|
-
"
|
|
5659
|
+
"packages/cli/src/output.ts"() {
|
|
5454
5660
|
"use strict";
|
|
5455
5661
|
init_src3();
|
|
5456
5662
|
PROGRESS_PREFIX = "@tendril-progress ";
|
|
5457
5663
|
}
|
|
5458
5664
|
});
|
|
5459
5665
|
|
|
5460
|
-
//
|
|
5461
|
-
import { chmodSync, existsSync as
|
|
5666
|
+
// packages/cli/src/entitlement.ts
|
|
5667
|
+
import { chmodSync, existsSync as existsSync17, mkdirSync as mkdirSync4, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "node:fs";
|
|
5462
5668
|
import crypto from "node:crypto";
|
|
5463
5669
|
import os3 from "node:os";
|
|
5464
|
-
import
|
|
5670
|
+
import path22 from "node:path";
|
|
5465
5671
|
function entitlementPath() {
|
|
5466
|
-
return process.env["TENDRIL_ENTITLEMENT_PATH"] ??
|
|
5672
|
+
return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path22.join(os3.homedir(), ".tendril", "entitlement.json");
|
|
5467
5673
|
}
|
|
5468
5674
|
function readStoredEntitlement(file = entitlementPath()) {
|
|
5469
|
-
if (!
|
|
5675
|
+
if (!existsSync17(file)) return void 0;
|
|
5470
5676
|
try {
|
|
5471
5677
|
const parsed = JSON.parse(readFileSync13(file, "utf8"));
|
|
5472
5678
|
if (typeof parsed.token !== "string" || typeof parsed.lastRefreshAt !== "number") return void 0;
|
|
@@ -5476,7 +5682,7 @@ function readStoredEntitlement(file = entitlementPath()) {
|
|
|
5476
5682
|
}
|
|
5477
5683
|
}
|
|
5478
5684
|
function writeStoredEntitlement(stored, file = entitlementPath()) {
|
|
5479
|
-
mkdirSync4(
|
|
5685
|
+
mkdirSync4(path22.dirname(file), { recursive: true });
|
|
5480
5686
|
writeFileSync7(file, `${JSON.stringify(stored, null, 2)}
|
|
5481
5687
|
`);
|
|
5482
5688
|
chmodSync(file, 384);
|
|
@@ -5502,11 +5708,11 @@ function checkEntitlement(opts = {}) {
|
|
|
5502
5708
|
const now = opts.now ?? Date.now();
|
|
5503
5709
|
const stored = "stored" in opts ? opts.stored : readStoredEntitlement();
|
|
5504
5710
|
if (stored === void 0) {
|
|
5505
|
-
return { ok: false, code: "entitlement-required", error: "no entitlement on this machine \u2014 record and generate need an active Tendril plan (verify stays free, always)", remediation:
|
|
5711
|
+
return { ok: false, code: "entitlement-required", error: "no entitlement on this machine \u2014 record and generate need an active Tendril plan (verify stays free, always)", remediation: activateRemediation() };
|
|
5506
5712
|
}
|
|
5507
5713
|
const parsed = parseEntitlementToken(stored.token);
|
|
5508
5714
|
if ("error" in parsed) {
|
|
5509
|
-
return { ok: false, code: "entitlement-invalid", error: `stored entitlement is unreadable: ${parsed.error}`, remediation:
|
|
5715
|
+
return { ok: false, code: "entitlement-invalid", error: `stored entitlement is unreadable: ${parsed.error}`, remediation: activateRemediation() };
|
|
5510
5716
|
}
|
|
5511
5717
|
const pem = keys[parsed.claims.kid];
|
|
5512
5718
|
const valid = pem !== void 0 && (() => {
|
|
@@ -5517,14 +5723,14 @@ function checkEntitlement(opts = {}) {
|
|
|
5517
5723
|
}
|
|
5518
5724
|
})();
|
|
5519
5725
|
if (!valid) {
|
|
5520
|
-
return { ok: false, code: "entitlement-invalid", error: "stored entitlement failed signature verification", remediation:
|
|
5726
|
+
return { ok: false, code: "entitlement-invalid", error: "stored entitlement failed signature verification", remediation: activateRemediation() };
|
|
5521
5727
|
}
|
|
5522
5728
|
if (now > parsed.claims.exp + TOLERANCE_MS) {
|
|
5523
5729
|
return {
|
|
5524
5730
|
ok: false,
|
|
5525
5731
|
code: "entitlement-expired",
|
|
5526
5732
|
error: "entitlement expired (and the offline tolerance window has passed)",
|
|
5527
|
-
remediation: `Reconnect and ${
|
|
5733
|
+
remediation: `Reconnect and ${activateRemediation()}`
|
|
5528
5734
|
};
|
|
5529
5735
|
}
|
|
5530
5736
|
if (now < stored.lastRefreshAt - CLOCK_ROLLBACK_MS) {
|
|
@@ -5532,7 +5738,7 @@ function checkEntitlement(opts = {}) {
|
|
|
5532
5738
|
ok: false,
|
|
5533
5739
|
code: "entitlement-clock",
|
|
5534
5740
|
error: "system clock sits more than a day before the last entitlement refresh",
|
|
5535
|
-
remediation: `Fix the system clock, then ${
|
|
5741
|
+
remediation: `Fix the system clock, then ${activateRemediation()}`
|
|
5536
5742
|
};
|
|
5537
5743
|
}
|
|
5538
5744
|
return { ok: true, mode: "active", claims: parsed.claims, stale: now > parsed.claims.exp };
|
|
@@ -5543,26 +5749,27 @@ function requireEntitlement(flags) {
|
|
|
5543
5749
|
fail(flags, ExitCode.Auth, { error: status.error, code: status.code, remediation: status.remediation });
|
|
5544
5750
|
}
|
|
5545
5751
|
}
|
|
5546
|
-
var ENT_PREFIX, PUBLIC_KEYS, TOLERANCE_MS, CLOCK_ROLLBACK_MS, b64urlDecode,
|
|
5752
|
+
var ENT_PREFIX, PUBLIC_KEYS, TOLERANCE_MS, CLOCK_ROLLBACK_MS, b64urlDecode, activateRemediation;
|
|
5547
5753
|
var init_entitlement = __esm({
|
|
5548
|
-
"
|
|
5754
|
+
"packages/cli/src/entitlement.ts"() {
|
|
5549
5755
|
"use strict";
|
|
5550
5756
|
init_src3();
|
|
5757
|
+
init_invocation();
|
|
5551
5758
|
init_output();
|
|
5552
5759
|
ENT_PREFIX = "tendril-ent.v1.";
|
|
5553
5760
|
PUBLIC_KEYS = {};
|
|
5554
5761
|
TOLERANCE_MS = 24 * 60 * 60 * 1e3;
|
|
5555
5762
|
CLOCK_ROLLBACK_MS = 24 * 60 * 60 * 1e3;
|
|
5556
5763
|
b64urlDecode = (s) => Buffer.from(s, "base64url");
|
|
5557
|
-
|
|
5764
|
+
activateRemediation = () => `Run \`${tendrilCommand("activate")}\` in your terminal (a browser approval \u2014 never paste license material into an agent chat).`;
|
|
5558
5765
|
}
|
|
5559
5766
|
});
|
|
5560
5767
|
|
|
5561
|
-
//
|
|
5768
|
+
// packages/cli/src/commands/doctor.ts
|
|
5562
5769
|
import { spawnSync } from "node:child_process";
|
|
5563
|
-
import { existsSync as
|
|
5770
|
+
import { existsSync as existsSync18, readFileSync as readFileSync14, readdirSync as readdirSync3 } from "node:fs";
|
|
5564
5771
|
import os4 from "node:os";
|
|
5565
|
-
import
|
|
5772
|
+
import path23 from "node:path";
|
|
5566
5773
|
async function runDoctorChecks(options) {
|
|
5567
5774
|
const checks = [];
|
|
5568
5775
|
const mcpUrl = options.mcpUrl ?? DEFAULT_MCP_URL;
|
|
@@ -5592,7 +5799,7 @@ async function runDoctorChecks(options) {
|
|
|
5592
5799
|
name: "openrouter-key",
|
|
5593
5800
|
ok: false,
|
|
5594
5801
|
detail: "OPENROUTER_API_KEY not set \u2014 optional; only the curated API-model engine needs it",
|
|
5595
|
-
remediation:
|
|
5802
|
+
remediation: `Run \`${tendrilCommand("init")}\` to store your OpenRouter key (BYOK) if you plan to use the curated engine.`
|
|
5596
5803
|
}
|
|
5597
5804
|
);
|
|
5598
5805
|
try {
|
|
@@ -5606,17 +5813,17 @@ async function runDoctorChecks(options) {
|
|
|
5606
5813
|
remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
|
|
5607
5814
|
});
|
|
5608
5815
|
}
|
|
5609
|
-
const fontManifest =
|
|
5816
|
+
const fontManifest = path23.join(fontCacheDir(), "manifest.json");
|
|
5610
5817
|
checks.push(
|
|
5611
|
-
|
|
5818
|
+
existsSync18(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync14(fontManifest, "utf8")).length} faces)` } : {
|
|
5612
5819
|
name: "font-cache",
|
|
5613
5820
|
ok: true,
|
|
5614
5821
|
detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
|
|
5615
|
-
remediation:
|
|
5822
|
+
remediation: `Nothing to do now: \`${tendrilCommand("fonts resolve --set <recording-dir>")}\` fetches exactly what a recording declares, and generate/verify name that command \u2014 with the set filled in \u2014 when they need it.`
|
|
5616
5823
|
}
|
|
5617
5824
|
);
|
|
5618
|
-
const pluginRoot =
|
|
5619
|
-
if (
|
|
5825
|
+
const pluginRoot = path23.join(os4.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
|
|
5826
|
+
if (existsSync18(pluginRoot)) {
|
|
5620
5827
|
try {
|
|
5621
5828
|
const versions = readdirSync3(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
|
|
5622
5829
|
const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
|
|
@@ -5660,17 +5867,6 @@ async function runDoctorChecks(options) {
|
|
|
5660
5867
|
});
|
|
5661
5868
|
return { ok: checks.filter((c) => c.name !== "figma-pat" && c.name !== "openrouter-key" && c.name !== "path-skew").every((c) => c.ok), checks };
|
|
5662
5869
|
}
|
|
5663
|
-
function findPathTendril(pathEnv, platform) {
|
|
5664
|
-
const dirs = pathEnv.split(path22.delimiter).filter((d) => d !== "" && !/node_modules[\\/]\.bin/.test(d) && !/[\\/]_npx[\\/]/.test(d));
|
|
5665
|
-
const names = platform === "win32" ? ["tendril.cmd", "tendril.bat"] : ["tendril"];
|
|
5666
|
-
for (const dir of dirs) {
|
|
5667
|
-
for (const name of names) {
|
|
5668
|
-
const candidate = path22.join(dir, name);
|
|
5669
|
-
if (existsSync17(candidate)) return candidate;
|
|
5670
|
-
}
|
|
5671
|
-
}
|
|
5672
|
-
return null;
|
|
5673
|
-
}
|
|
5674
5870
|
function probeVersion(binary) {
|
|
5675
5871
|
const windowsShim = /\.(cmd|bat)$/i.test(binary);
|
|
5676
5872
|
const res = windowsShim ? spawnSync(`"${binary}" --version`, { shell: true, timeout: 5e3, encoding: "utf8" }) : spawnSync(binary, ["--version"], { timeout: 5e3, encoding: "utf8" });
|
|
@@ -5735,13 +5931,14 @@ async function runDoctor(flags) {
|
|
|
5735
5931
|
}
|
|
5736
5932
|
var DEFAULT_MCP_URL, DOCTOR_DESCRIPTION;
|
|
5737
5933
|
var init_doctor = __esm({
|
|
5738
|
-
"
|
|
5934
|
+
"packages/cli/src/commands/doctor.ts"() {
|
|
5739
5935
|
"use strict";
|
|
5740
5936
|
init_src4();
|
|
5741
5937
|
init_src();
|
|
5742
5938
|
init_describe();
|
|
5743
5939
|
init_env();
|
|
5744
5940
|
init_environment();
|
|
5941
|
+
init_invocation();
|
|
5745
5942
|
init_output();
|
|
5746
5943
|
init_entitlement();
|
|
5747
5944
|
DEFAULT_MCP_URL = "http://127.0.0.1:3845/mcp";
|
|
@@ -5763,7 +5960,7 @@ var init_doctor = __esm({
|
|
|
5763
5960
|
}
|
|
5764
5961
|
});
|
|
5765
5962
|
|
|
5766
|
-
//
|
|
5963
|
+
// packages/llm/src/model-config.ts
|
|
5767
5964
|
import { z as z6 } from "zod";
|
|
5768
5965
|
function resolveModel(config, requestedId) {
|
|
5769
5966
|
const entry = config.allowlist.find((m) => m.id === requestedId);
|
|
@@ -5784,7 +5981,7 @@ function resolveModel(config, requestedId) {
|
|
|
5784
5981
|
}
|
|
5785
5982
|
var ModelStatusSchema, ModelEntrySchema, ModelConfigSchema, DEFAULT_MODEL_CONFIG;
|
|
5786
5983
|
var init_model_config = __esm({
|
|
5787
|
-
"
|
|
5984
|
+
"packages/llm/src/model-config.ts"() {
|
|
5788
5985
|
"use strict";
|
|
5789
5986
|
ModelStatusSchema = z6.enum(["verified", "degraded"]);
|
|
5790
5987
|
ModelEntrySchema = z6.object({
|
|
@@ -5873,10 +6070,10 @@ var init_model_config = __esm({
|
|
|
5873
6070
|
}
|
|
5874
6071
|
});
|
|
5875
6072
|
|
|
5876
|
-
//
|
|
6073
|
+
// packages/llm/src/openrouter.ts
|
|
5877
6074
|
var FALLBACK_KINDS, DEFAULT_REQUEST_TIMEOUT_MS, OpenRouterClient;
|
|
5878
6075
|
var init_openrouter = __esm({
|
|
5879
|
-
"
|
|
6076
|
+
"packages/llm/src/openrouter.ts"() {
|
|
5880
6077
|
"use strict";
|
|
5881
6078
|
FALLBACK_KINDS = /* @__PURE__ */ new Set(["rate-limit", "server", "network", "timeout"]);
|
|
5882
6079
|
DEFAULT_REQUEST_TIMEOUT_MS = 6e5;
|
|
@@ -6003,7 +6200,7 @@ var init_openrouter = __esm({
|
|
|
6003
6200
|
}
|
|
6004
6201
|
});
|
|
6005
6202
|
|
|
6006
|
-
//
|
|
6203
|
+
// packages/llm/src/semantics.ts
|
|
6007
6204
|
import { z as z7 } from "zod";
|
|
6008
6205
|
function portable(node) {
|
|
6009
6206
|
if (Array.isArray(node)) return node.map(portable);
|
|
@@ -6041,7 +6238,7 @@ function docsJsonSchema() {
|
|
|
6041
6238
|
}
|
|
6042
6239
|
var PropTypeSchema, SemanticsSchema, CodegenOutputSchema, DocsOutputSchema;
|
|
6043
6240
|
var init_semantics = __esm({
|
|
6044
|
-
"
|
|
6241
|
+
"packages/llm/src/semantics.ts"() {
|
|
6045
6242
|
"use strict";
|
|
6046
6243
|
PropTypeSchema = z7.discriminatedUnion("kind", [
|
|
6047
6244
|
z7.object({ kind: z7.literal("enum"), values: z7.array(z7.string()).min(1) }),
|
|
@@ -6130,10 +6327,10 @@ var init_semantics = __esm({
|
|
|
6130
6327
|
}
|
|
6131
6328
|
});
|
|
6132
6329
|
|
|
6133
|
-
//
|
|
6330
|
+
// packages/llm/src/golden/button.ts
|
|
6134
6331
|
var goldenButtonSemantics, goldenButtonCode, goldenButtonDocs;
|
|
6135
6332
|
var init_button2 = __esm({
|
|
6136
|
-
"
|
|
6333
|
+
"packages/llm/src/golden/button.ts"() {
|
|
6137
6334
|
"use strict";
|
|
6138
6335
|
goldenButtonSemantics = {
|
|
6139
6336
|
componentName: "Button",
|
|
@@ -6291,7 +6488,7 @@ handling come from the platform.
|
|
|
6291
6488
|
}
|
|
6292
6489
|
});
|
|
6293
6490
|
|
|
6294
|
-
//
|
|
6491
|
+
// packages/llm/src/prompt.ts
|
|
6295
6492
|
function buildStablePrefix(input) {
|
|
6296
6493
|
const tokens = [...input.tokenLines].sort().join("\n");
|
|
6297
6494
|
const golden = `GOLDEN EXAMPLE (canonical quality bar)
|
|
@@ -6325,7 +6522,7 @@ ${input.irJson}` });
|
|
|
6325
6522
|
}
|
|
6326
6523
|
var SYSTEM_RULES;
|
|
6327
6524
|
var init_prompt = __esm({
|
|
6328
|
-
"
|
|
6525
|
+
"packages/llm/src/prompt.ts"() {
|
|
6329
6526
|
"use strict";
|
|
6330
6527
|
init_button2();
|
|
6331
6528
|
SYSTEM_RULES = `You generate production React components from a design-system IR.
|
|
@@ -6421,10 +6618,10 @@ strictly in the requested JSON shape.`;
|
|
|
6421
6618
|
}
|
|
6422
6619
|
});
|
|
6423
6620
|
|
|
6424
|
-
//
|
|
6621
|
+
// packages/llm/src/generation-model.ts
|
|
6425
6622
|
var MockGenerationModel, MAX_SCHEMA_ATTEMPTS, OpenRouterGenerationModel;
|
|
6426
6623
|
var init_generation_model = __esm({
|
|
6427
|
-
"
|
|
6624
|
+
"packages/llm/src/generation-model.ts"() {
|
|
6428
6625
|
"use strict";
|
|
6429
6626
|
init_openrouter();
|
|
6430
6627
|
init_prompt();
|
|
@@ -6581,9 +6778,9 @@ ${JSON.stringify(semantics)}`,
|
|
|
6581
6778
|
}
|
|
6582
6779
|
});
|
|
6583
6780
|
|
|
6584
|
-
//
|
|
6781
|
+
// packages/llm/src/index.ts
|
|
6585
6782
|
var init_src5 = __esm({
|
|
6586
|
-
"
|
|
6783
|
+
"packages/llm/src/index.ts"() {
|
|
6587
6784
|
"use strict";
|
|
6588
6785
|
init_model_config();
|
|
6589
6786
|
init_openrouter();
|
|
@@ -6594,11 +6791,11 @@ var init_src5 = __esm({
|
|
|
6594
6791
|
}
|
|
6595
6792
|
});
|
|
6596
6793
|
|
|
6597
|
-
//
|
|
6794
|
+
// packages/metadata/src/component-json.ts
|
|
6598
6795
|
import { z as z8 } from "zod";
|
|
6599
6796
|
var TokenUseSchema, AntiPatternSchema, ComponentJsonSchema;
|
|
6600
6797
|
var init_component_json = __esm({
|
|
6601
|
-
"
|
|
6798
|
+
"packages/metadata/src/component-json.ts"() {
|
|
6602
6799
|
"use strict";
|
|
6603
6800
|
TokenUseSchema = z8.object({
|
|
6604
6801
|
/** DTCG token path, e.g. "color.primary". */
|
|
@@ -6650,7 +6847,7 @@ var init_component_json = __esm({
|
|
|
6650
6847
|
}
|
|
6651
6848
|
});
|
|
6652
6849
|
|
|
6653
|
-
//
|
|
6850
|
+
// packages/metadata/src/extract.ts
|
|
6654
6851
|
import { parse } from "react-docgen";
|
|
6655
6852
|
function extractProps(tsxSource, filename) {
|
|
6656
6853
|
const docs = parse(tsxSource, { filename });
|
|
@@ -6723,13 +6920,13 @@ function buildComponentJson(input) {
|
|
|
6723
6920
|
});
|
|
6724
6921
|
}
|
|
6725
6922
|
var init_extract = __esm({
|
|
6726
|
-
"
|
|
6923
|
+
"packages/metadata/src/extract.ts"() {
|
|
6727
6924
|
"use strict";
|
|
6728
6925
|
init_component_json();
|
|
6729
6926
|
}
|
|
6730
6927
|
});
|
|
6731
6928
|
|
|
6732
|
-
//
|
|
6929
|
+
// packages/metadata/src/recording-set.ts
|
|
6733
6930
|
import { z as z9 } from "zod";
|
|
6734
6931
|
function roleLossToken(loss) {
|
|
6735
6932
|
return loss.kind === "main" ? `main:${loss.main}` : `part:${loss.part}${EDGE_ARROW}${loss.main}`;
|
|
@@ -6790,7 +6987,7 @@ function validateRecordingSet(manifest, fileExists) {
|
|
|
6790
6987
|
}
|
|
6791
6988
|
var RECORDING_SET_VERSION, EnvelopeSchema, RepEntrySchema, RolesSchema, EDGE_ARROW, RecordingSetManifestSchema, REQUIRED_REP_FILES;
|
|
6792
6989
|
var init_recording_set = __esm({
|
|
6793
|
-
"
|
|
6990
|
+
"packages/metadata/src/recording-set.ts"() {
|
|
6794
6991
|
"use strict";
|
|
6795
6992
|
RECORDING_SET_VERSION = 1;
|
|
6796
6993
|
EnvelopeSchema = z9.object({
|
|
@@ -6851,7 +7048,7 @@ var init_recording_set = __esm({
|
|
|
6851
7048
|
}
|
|
6852
7049
|
});
|
|
6853
7050
|
|
|
6854
|
-
//
|
|
7051
|
+
// packages/metadata/src/bundle.ts
|
|
6855
7052
|
import { z as z10 } from "zod";
|
|
6856
7053
|
function readBundleManifest(raw) {
|
|
6857
7054
|
if (Buffer.byteLength(raw, "utf8") > MAX_BUNDLE_MANIFEST_BYTES) {
|
|
@@ -6920,7 +7117,7 @@ function hashRecordingSet(relPaths, readFile, sha256) {
|
|
|
6920
7117
|
}
|
|
6921
7118
|
var BUNDLE_VERSION, ConfigStatusSchema, PinnedPropSchema, PropAdapterSchema, RequiredFontSchema, ConfigClaimSchema, BehaviorClaimSchema, BundleProvenanceSchema, BundleComponentJsonSchema, MAX_BUNDLE_MANIFEST_BYTES, MAX_BUNDLE_SOURCE_BYTES;
|
|
6922
7119
|
var init_bundle = __esm({
|
|
6923
|
-
"
|
|
7120
|
+
"packages/metadata/src/bundle.ts"() {
|
|
6924
7121
|
"use strict";
|
|
6925
7122
|
init_recording_set();
|
|
6926
7123
|
BUNDLE_VERSION = 1;
|
|
@@ -7018,9 +7215,9 @@ var init_bundle = __esm({
|
|
|
7018
7215
|
}
|
|
7019
7216
|
});
|
|
7020
7217
|
|
|
7021
|
-
//
|
|
7218
|
+
// packages/metadata/src/index.ts
|
|
7022
7219
|
var init_src6 = __esm({
|
|
7023
|
-
"
|
|
7220
|
+
"packages/metadata/src/index.ts"() {
|
|
7024
7221
|
"use strict";
|
|
7025
7222
|
init_component_json();
|
|
7026
7223
|
init_extract();
|
|
@@ -7029,7 +7226,7 @@ var init_src6 = __esm({
|
|
|
7029
7226
|
}
|
|
7030
7227
|
});
|
|
7031
7228
|
|
|
7032
|
-
//
|
|
7229
|
+
// packages/cli/src/commands/activate.ts
|
|
7033
7230
|
var activate_exports = {};
|
|
7034
7231
|
__export(activate_exports, {
|
|
7035
7232
|
ENTITLEMENT_SERVICE_URL: () => ENTITLEMENT_SERVICE_URL,
|
|
@@ -7054,7 +7251,7 @@ async function runActivate(flags) {
|
|
|
7054
7251
|
fail(flags, ExitCode.Auth, {
|
|
7055
7252
|
error: `could not start activation: ${err instanceof Error ? err.message : String(err)}`,
|
|
7056
7253
|
code: "entitlement-service-unreachable",
|
|
7057
|
-
remediation:
|
|
7254
|
+
remediation: `Check your connection and retry \`${tendrilCommand("activate")}\`.`
|
|
7058
7255
|
});
|
|
7059
7256
|
}
|
|
7060
7257
|
process.stderr.write(`To activate Tendril, visit:
|
|
@@ -7081,7 +7278,7 @@ Waiting for approval\u2026
|
|
|
7081
7278
|
fail(flags, ExitCode.Auth, {
|
|
7082
7279
|
error: `activation was not approved (HTTP ${res.status})`,
|
|
7083
7280
|
code: "entitlement-denied",
|
|
7084
|
-
remediation:
|
|
7281
|
+
remediation: `Retry \`${tendrilCommand("activate")}\`; if it persists, check the account's plan in the portal.`
|
|
7085
7282
|
});
|
|
7086
7283
|
}
|
|
7087
7284
|
token = (await res.json()).token;
|
|
@@ -7091,7 +7288,7 @@ Waiting for approval\u2026
|
|
|
7091
7288
|
fail(flags, ExitCode.Auth, {
|
|
7092
7289
|
error: "activation timed out before the browser approval arrived",
|
|
7093
7290
|
code: "entitlement-timeout",
|
|
7094
|
-
remediation:
|
|
7291
|
+
remediation: `Run \`${tendrilCommand("activate")}\` again and complete the browser step within the shown window.`
|
|
7095
7292
|
});
|
|
7096
7293
|
}
|
|
7097
7294
|
const parsed = parseEntitlementToken(token);
|
|
@@ -7099,7 +7296,7 @@ Waiting for approval\u2026
|
|
|
7099
7296
|
fail(flags, ExitCode.Auth, {
|
|
7100
7297
|
error: `service returned an unusable token: ${parsed.error}`,
|
|
7101
7298
|
code: "entitlement-invalid",
|
|
7102
|
-
remediation:
|
|
7299
|
+
remediation: `Retry \`${tendrilCommand("activate")}\`; report this if it persists \u2014 it is a service-side fault.`
|
|
7103
7300
|
});
|
|
7104
7301
|
}
|
|
7105
7302
|
if (Object.keys(PUBLIC_KEYS).length > 0) {
|
|
@@ -7114,23 +7311,26 @@ Waiting for approval\u2026
|
|
|
7114
7311
|
}
|
|
7115
7312
|
var ENTITLEMENT_SERVICE_URL;
|
|
7116
7313
|
var init_activate = __esm({
|
|
7117
|
-
"
|
|
7314
|
+
"packages/cli/src/commands/activate.ts"() {
|
|
7118
7315
|
"use strict";
|
|
7119
7316
|
init_src3();
|
|
7120
7317
|
init_entitlement();
|
|
7318
|
+
init_invocation();
|
|
7121
7319
|
init_output();
|
|
7122
7320
|
ENTITLEMENT_SERVICE_URL = void 0;
|
|
7123
7321
|
}
|
|
7124
7322
|
});
|
|
7125
7323
|
|
|
7126
|
-
//
|
|
7324
|
+
// packages/cli/src/commands/record.ts
|
|
7127
7325
|
var record_exports = {};
|
|
7128
7326
|
__export(record_exports, {
|
|
7129
7327
|
instanceLeads: () => instanceLeads,
|
|
7328
|
+
interactionDisclosure: () => interactionDisclosure,
|
|
7130
7329
|
isFigmaAssetUrl: () => isFigmaAssetUrl,
|
|
7131
7330
|
isLocalAssetUrl: () => isLocalAssetUrl,
|
|
7132
7331
|
narrowedRoles: () => narrowedRoles,
|
|
7133
7332
|
nextPayload: () => nextPayload,
|
|
7333
|
+
recordsInteractionState: () => recordsInteractionState,
|
|
7134
7334
|
runRecordAsset: () => runRecordAsset,
|
|
7135
7335
|
runRecordFetch: () => runRecordFetch,
|
|
7136
7336
|
runRecordFinish: () => runRecordFinish,
|
|
@@ -7140,10 +7340,30 @@ __export(record_exports, {
|
|
|
7140
7340
|
runRecordPlan: () => runRecordPlan,
|
|
7141
7341
|
runRecordStatus: () => runRecordStatus
|
|
7142
7342
|
});
|
|
7143
|
-
import { existsSync as
|
|
7343
|
+
import { existsSync as existsSync20, mkdtempSync as mkdtempSync2, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "node:fs";
|
|
7144
7344
|
import os5 from "node:os";
|
|
7145
|
-
import
|
|
7345
|
+
import path26 from "node:path";
|
|
7146
7346
|
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
7347
|
+
function recordsInteractionState(reports) {
|
|
7348
|
+
const evident = (s) => stateTokens(s).some((t) => INTERACTION_EVIDENCE_VALUES.has(t));
|
|
7349
|
+
return reports.some((r) => evident(r.axis) || r.domain.some(evident));
|
|
7350
|
+
}
|
|
7351
|
+
function interactionDisclosure(component, reports) {
|
|
7352
|
+
const recordedVariants = reports.map((r) => ({ axis: r.axis, values: r.domain }));
|
|
7353
|
+
const variantSummary = recordedVariants.length === 0 ? "" : recordedVariants.length === 1 ? `${recordedVariants[0].axis} variants for ${andList(recordedVariants[0].values)}` : `variants for ${andList(recordedVariants.map((v) => `${v.axis} (${andList(v.values)})`))}`;
|
|
7354
|
+
const has = recordedVariants.length > 0;
|
|
7355
|
+
const statement = `Your ${component} component ${has ? `has ${variantSummary} \u2014 but none of them is a hover, focus or pressed state` : "has no variants at all, so none of it is a hover, focus or pressed state"}. Nothing in the recording shows how ${component} looks when someone hovers it, tabs to it, or is using it, so Tendril has no picture of those to check the build against. Tendril still builds controls as real, working controls \u2014 never a static lookalike \u2014 and, with no focus state recorded, it uses the browser's own focus indicator rather than inventing a focus ring. You do not need to answer this: recording carries on either way.`;
|
|
7356
|
+
const designFix = `If ${component} should look different when it is focused or hovered, that change belongs in Figma rather than in the code: add a Focus (or Hover) variant to the ${component} component set, record it, and Tendril will match it exactly. If ${component} is not something people interact with, there is nothing to do.`;
|
|
7357
|
+
return {
|
|
7358
|
+
component,
|
|
7359
|
+
recordedVariants,
|
|
7360
|
+
variantSummary,
|
|
7361
|
+
statement,
|
|
7362
|
+
designFix,
|
|
7363
|
+
instruction: "DISCLOSURE, not a question. Say `statement` and `designFix` to a present user at PLAN time, in the SAME message as defaultsToConfirm and every other plan-time question \u2014 one message per session, never one per component and never mid-generation. No answer is required and nothing blocks; proceed with recording whatever they say. State ONLY what `recordedVariants` contains: never infer from the component's NAME what kind of control it is. Non-interactive runs: put the statement in your report.",
|
|
7364
|
+
blocking: false
|
|
7365
|
+
};
|
|
7366
|
+
}
|
|
7147
7367
|
function symbolsFromMetadataEnvelope(file, sourceFrame) {
|
|
7148
7368
|
const env = JSON.parse(readFileSync16(file, "utf8"));
|
|
7149
7369
|
const text = env.content.map((c) => c.text ?? "").join("\n");
|
|
@@ -7203,7 +7423,7 @@ function runRecordPlan(opts) {
|
|
|
7203
7423
|
if (rawFile !== void 0) {
|
|
7204
7424
|
try {
|
|
7205
7425
|
const envelope = rawEnvelopeFromFile(rawFile, opts.metadataRawPartsFile !== void 0);
|
|
7206
|
-
const tmp =
|
|
7426
|
+
const tmp = path26.join(mkdtempSync2(path26.join(os5.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
|
|
7207
7427
|
writeFileSync9(tmp, JSON.stringify(envelope));
|
|
7208
7428
|
metadataEntries.push({ file: tmp });
|
|
7209
7429
|
} catch (err) {
|
|
@@ -7225,7 +7445,7 @@ function runRecordPlan(opts) {
|
|
|
7225
7445
|
let metadataTruncated = false;
|
|
7226
7446
|
for (const { file, frame } of metadataEntries) {
|
|
7227
7447
|
try {
|
|
7228
|
-
const parsed = symbolsFromMetadataEnvelope(
|
|
7448
|
+
const parsed = symbolsFromMetadataEnvelope(path26.resolve(file), frame);
|
|
7229
7449
|
symbols.push(...parsed.symbols);
|
|
7230
7450
|
if (parsed.truncated) metadataTruncated = true;
|
|
7231
7451
|
} catch (err) {
|
|
@@ -7258,7 +7478,7 @@ function runRecordPlan(opts) {
|
|
|
7258
7478
|
if (symbols.length === 0) {
|
|
7259
7479
|
const leads = metadataEntries.flatMap(({ file }) => {
|
|
7260
7480
|
try {
|
|
7261
|
-
const env = JSON.parse(readFileSync16(
|
|
7481
|
+
const env = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
|
|
7262
7482
|
return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
|
|
7263
7483
|
} catch {
|
|
7264
7484
|
return [];
|
|
@@ -7297,8 +7517,47 @@ function runRecordPlan(opts) {
|
|
|
7297
7517
|
const { manifest, plan, resumed, toppedUp } = planSet(opts.setDir, opts.component, symbols, { ...Object.keys(defaults).length > 0 ? { defaults } : {}, ...opts.sample === true ? { sample: true } : {} });
|
|
7298
7518
|
const defaultReports = reportAxisDefaults(symbols, Object.keys(defaults).length > 0 ? defaults : void 0);
|
|
7299
7519
|
const toConfirm = resumed ? [] : defaultReports.filter((r) => (r.rule === "non-interaction" || r.rule === "frequency") && r.domain.length > 1);
|
|
7300
|
-
const
|
|
7301
|
-
const
|
|
7520
|
+
const interactionStatesToConfirm = recordsInteractionState(defaultReports) ? void 0 : interactionDisclosure(manifest.component, defaultReports);
|
|
7521
|
+
const poses = manifest.reps.length;
|
|
7522
|
+
const callLow = poses * FIGMA_CALLS_PER_REP;
|
|
7523
|
+
const callHigh = poses * FIGMA_CALLS_PER_REP_WORST;
|
|
7524
|
+
const posesRemaining = sessionStatus(opts.setDir).reps.filter((r) => r.missing.length > 0).length;
|
|
7525
|
+
const callsNeeded = posesRemaining * FIGMA_CALLS_PER_REP;
|
|
7526
|
+
const callSentence = posesRemaining === poses ? `${poses} poses \xD7 ${FIGMA_CALLS_PER_REP} calls per pose = ${callsNeeded} Figma calls` : `${posesRemaining} poses still to record \xD7 ${FIGMA_CALLS_PER_REP} calls per pose = ${callsNeeded} Figma calls (${poses - posesRemaining} of ${poses} already on disk)`;
|
|
7527
|
+
const arithmetic = `${callSentence} (up to ${posesRemaining * FIGMA_CALLS_PER_REP_WORST} if screenshots or assets need a second fetch)`;
|
|
7528
|
+
const multiDay = callsNeeded > MULTI_DAY_CALL_THRESHOLD;
|
|
7529
|
+
const feasibilityCheck = {
|
|
7530
|
+
arithmetic,
|
|
7531
|
+
callsNeeded,
|
|
7532
|
+
callsWorstCase: posesRemaining * FIGMA_CALLS_PER_REP_WORST,
|
|
7533
|
+
posesToRecord: posesRemaining,
|
|
7534
|
+
callsPerPose: FIGMA_CALLS_PER_REP,
|
|
7535
|
+
instruction: "BEFORE the first recording call, call the Figma MCP `whoami` tool \u2014 it names the seat and plan and is EXEMPT from Figma's tool-call limits, so this check costs nothing and spends no quota. Read the daily tool-call allowance for that seat/plan and divide: callsNeeded \xF7 allowance = days this set needs. Tendril owns the multiplication above; the allowance is Figma's and is theirs to change, so take it from whoami and never from memory.",
|
|
7536
|
+
surface: multiDay ? "This set exceeds every daily allowance we have heard of, so it CANNOT finish in one day. State the verdict to the user in plain numbers BEFORE recording anything and let them choose from `options` \u2014 do not start on the assumption it will fit." : "Say nothing about cost if the division fits inside one day's allowance \u2014 a set that comfortably fits is not worth the user's attention. Surface the verdict and `options` only when it does not fit.",
|
|
7537
|
+
verdictTemplate: `${callSentence}. Your <seat> seat on <plan> allows <allowance>/day. This set needs about <days> day(s) at that rate.`,
|
|
7538
|
+
options: [
|
|
7539
|
+
{
|
|
7540
|
+
id: "pace",
|
|
7541
|
+
decidedBy: "user, and you can carry it out",
|
|
7542
|
+
text: "Spread the recording across days, staying inside the daily allowance. Nothing is lost at a limit: the set persists after every pose and re-running the same command resumes from disk, so a rate limit costs waiting, never recorded work."
|
|
7543
|
+
},
|
|
7544
|
+
{
|
|
7545
|
+
id: "sample",
|
|
7546
|
+
// Sampling trades away lattice coverage, so it stays a human
|
|
7547
|
+
// decision structurally, not by instruction: it exists only as
|
|
7548
|
+
// a CLI flag and the MCP surface has no parameter for it.
|
|
7549
|
+
decidedBy: "HUMAN ONLY \u2014 offer it, never choose it, and you cannot run it",
|
|
7550
|
+
text: "Record a reduced pose set (anchor + one-factor sweeps + conflict crosses) instead of the full variant matrix. This buys calls with coverage \u2014 sampling is blind to multi-axis interactions and the report discloses the poses it skipped \u2014 so only the user may accept that trade. There is no tool parameter for it; the user runs it themselves in their own terminal, before any pose is recorded.",
|
|
7551
|
+
userRuns: [`rm ${path26.join(opts.setDir, "recording-set.json")}`, `${tendrilCommand(`record plan --set ${opts.setDir} --component ${manifest.component}`)} --sample`]
|
|
7552
|
+
},
|
|
7553
|
+
{
|
|
7554
|
+
id: "larger-allowance",
|
|
7555
|
+
decidedBy: "user",
|
|
7556
|
+
text: "A seat or plan with a larger daily allowance, if their organisation allows it. Figma's terms, not ours \u2014 state it as an option, never as advice to spend money."
|
|
7557
|
+
}
|
|
7558
|
+
],
|
|
7559
|
+
limitHintIfWhoamiIsSilent: LIMIT_HINT
|
|
7560
|
+
};
|
|
7302
7561
|
emitData(
|
|
7303
7562
|
opts,
|
|
7304
7563
|
{
|
|
@@ -7314,11 +7573,19 @@ function runRecordPlan(opts) {
|
|
|
7314
7573
|
...metadataTruncated ? { metadataTruncated: true } : {},
|
|
7315
7574
|
...toppedUp !== void 0 ? { toppedUp } : {},
|
|
7316
7575
|
notRecorded: manifest.notRecorded ?? null,
|
|
7317
|
-
|
|
7576
|
+
// The whole-set ledger, numbers only — what recording this queue
|
|
7577
|
+
// from nothing costs. What it costs FROM HERE, and what to do
|
|
7578
|
+
// about it, is feasibilityCheck's job.
|
|
7579
|
+
figmaCallEstimate: { reps: poses, callsPerRep: FIGMA_CALLS_PER_REP, calls: `~${callLow}\u2013${callHigh}`, callsMin: callLow, callsMax: callHigh },
|
|
7580
|
+
// A sibling of defaultsToConfirm: a handshake the agent completes
|
|
7581
|
+
// with the user before the queue costs anything, not a field to
|
|
7582
|
+
// relay as duration.
|
|
7583
|
+
feasibilityCheck,
|
|
7318
7584
|
// Cold-start fix (run 7): the first instruction rides the plan
|
|
7319
7585
|
// response, so record_next is never needed to begin — it exists
|
|
7320
7586
|
// only for resuming.
|
|
7321
7587
|
next: nextPayload(opts.setDir),
|
|
7588
|
+
...interactionStatesToConfirm !== void 0 ? { interactionStatesToConfirm } : {},
|
|
7322
7589
|
...toConfirm.length > 0 ? {
|
|
7323
7590
|
defaultsToConfirm: {
|
|
7324
7591
|
instruction: "HEURISTIC defaults: the designer named no literal Default and no override was given, so the code guessed the component's zero point \u2014 the pose an empty-props mount shows and the pose behaviour checks aim at. When a USER is present, ask each question below BEFORE recording; if they pick a different value, re-run this exact plan command with --default <Axis>=<Value> (allowed until the first envelope is ingested; frozen with the recording after). Non-interactive: proceed with the resolved values and state them in your report.",
|
|
@@ -7331,6 +7598,42 @@ function runRecordPlan(opts) {
|
|
|
7331
7598
|
} : {}
|
|
7332
7599
|
},
|
|
7333
7600
|
() => {
|
|
7601
|
+
const writeFeasibility = () => {
|
|
7602
|
+
if (callsNeeded === 0) return;
|
|
7603
|
+
process.stdout.write(`recording cost: ${arithmetic}
|
|
7604
|
+
`);
|
|
7605
|
+
if (multiDay) {
|
|
7606
|
+
process.stdout.write(`FEASIBILITY: ${callsNeeded} calls exceeds every daily Figma allowance we have heard of \u2014 this CANNOT finish in one day.
|
|
7607
|
+
`);
|
|
7608
|
+
process.stdout.write(` ask Figma whoami (free \u2014 exempt from tool-call limits) for this seat's daily allowance, divide ${callsNeeded} by it, and say how many DAYS this needs before recording
|
|
7609
|
+
`);
|
|
7610
|
+
process.stdout.write(` (a) pace it across days \u2014 the set persists after every pose, so a rate limit costs waiting, never recorded work
|
|
7611
|
+
`);
|
|
7612
|
+
process.stdout.write(` (b) record fewer poses \u2014 YOUR call, not an agent's: delete recording-set.json and re-plan with --sample (buys calls with coverage)
|
|
7613
|
+
`);
|
|
7614
|
+
process.stdout.write(` (c) a seat or plan with a larger daily allowance
|
|
7615
|
+
`);
|
|
7616
|
+
} else {
|
|
7617
|
+
process.stdout.write(`FEASIBILITY: check ${callsNeeded} against this seat's daily Figma allowance (ask whoami \u2014 it is free) before recording; it only needs raising if it does not fit
|
|
7618
|
+
`);
|
|
7619
|
+
}
|
|
7620
|
+
};
|
|
7621
|
+
const writeInteraction = () => {
|
|
7622
|
+
if (interactionStatesToConfirm === void 0) return;
|
|
7623
|
+
const d = interactionStatesToConfirm;
|
|
7624
|
+
process.stdout.write(
|
|
7625
|
+
`CONFIRM states: ${d.component} records no hover, focus or pressed state${d.recordedVariants.length > 0 ? ` \u2014 it has ${d.variantSummary}` : " (it has no variants at all)"}
|
|
7626
|
+
`
|
|
7627
|
+
);
|
|
7628
|
+
process.stdout.write(` nothing in the recording shows how it looks when someone hovers it, tabs to it, or is using it
|
|
7629
|
+
`);
|
|
7630
|
+
process.stdout.write(` Tendril still builds controls as real controls \u2014 never a static lookalike \u2014 and uses the browser's own focus indicator instead of inventing a focus ring
|
|
7631
|
+
`);
|
|
7632
|
+
process.stdout.write(` want a focus ring of your own? add a Focus variant to the ${d.component} component set in Figma and record again \u2014 Tendril will match it exactly
|
|
7633
|
+
`);
|
|
7634
|
+
process.stdout.write(` nothing to answer \u2014 recording carries on either way
|
|
7635
|
+
`);
|
|
7636
|
+
};
|
|
7334
7637
|
if (resumed) {
|
|
7335
7638
|
if (opts.sample === true && (manifest.planMode ?? "sample") === "full") {
|
|
7336
7639
|
process.stdout.write("NOTE: --sample has no effect on a resumed full-matrix set \u2014 delete recording-set.json to re-plan sampled (partial coverage is a deliberate choice)\n");
|
|
@@ -7344,18 +7647,20 @@ function runRecordPlan(opts) {
|
|
|
7344
7647
|
process.stdout.write(`resumed existing plan (${manifest.reps.length} reps, ${manifest.planMode ?? "sample"} mode) \u2014 delete recording-set.json to re-plan
|
|
7345
7648
|
`);
|
|
7346
7649
|
}
|
|
7650
|
+
writeFeasibility();
|
|
7651
|
+
writeInteraction();
|
|
7347
7652
|
return;
|
|
7348
7653
|
}
|
|
7349
7654
|
for (const r of plan.reps) process.stdout.write(`planned ${r.slug.padEnd(24)} ${r.nodeId} (${r.tier})
|
|
7350
7655
|
`);
|
|
7351
7656
|
if (plan.notRecorded.length > 0) process.stdout.write(`not recorded (${plan.notRecorded.length}): disclosed in the manifest
|
|
7352
7657
|
`);
|
|
7353
|
-
|
|
7354
|
-
`);
|
|
7658
|
+
writeFeasibility();
|
|
7355
7659
|
for (const q2 of toConfirm) {
|
|
7356
7660
|
process.stdout.write(`CONFIRM ${q2.axis}: default resolved to "${q2.value}" by heuristic (${q2.rule}) \u2014 ask the user; change with --default before recording
|
|
7357
7661
|
`);
|
|
7358
7662
|
}
|
|
7663
|
+
writeInteraction();
|
|
7359
7664
|
}
|
|
7360
7665
|
);
|
|
7361
7666
|
}
|
|
@@ -7363,7 +7668,7 @@ function nextPayload(setDir) {
|
|
|
7363
7668
|
const instruction = nextInstruction(setDir);
|
|
7364
7669
|
const status = sessionStatus(setDir);
|
|
7365
7670
|
const progress = { recordedReps: status.reps.filter((x) => x.missing.length === 0).length, totalReps: status.reps.length };
|
|
7366
|
-
if (instruction === null && !
|
|
7671
|
+
if (instruction === null && !existsSync20(path26.join(setDir, "get_variable_defs.json"))) {
|
|
7367
7672
|
const manifest = loadManifest(setDir);
|
|
7368
7673
|
const frameNode = Object.keys(manifest.sourceFrames ?? {})[0] ?? manifest.reps[0]?.nodeId ?? "";
|
|
7369
7674
|
return { slug: "__set__", nodeId: frameNode, tool: "get_variable_defs", note: `SET-LEVEL: call get_variable_defs on the component frame and ingest with --rep __set__. ${ENVELOPE_HELP}`, progress };
|
|
@@ -7380,7 +7685,7 @@ function runRecordNext(opts) {
|
|
|
7380
7685
|
const progress = payload["progress"];
|
|
7381
7686
|
process.stdout.write(`[${progress.recordedReps}/${progress.totalReps} reps] ${payload["tool"]} for ${payload["slug"]} (node ${payload["nodeId"]})
|
|
7382
7687
|
\u2192 ${payload["note"]}
|
|
7383
|
-
\u2192 then:
|
|
7688
|
+
\u2192 then: ${tendrilCommand(`record ingest --set ${path26.resolve(opts.setDir)} --rep ${payload["slug"]} --tool ${payload["tool"]} --file <envelope.json>`)}
|
|
7384
7689
|
`);
|
|
7385
7690
|
});
|
|
7386
7691
|
}
|
|
@@ -7454,7 +7759,7 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
7454
7759
|
const skipped = [];
|
|
7455
7760
|
const failed = [];
|
|
7456
7761
|
for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
|
|
7457
|
-
if (
|
|
7762
|
+
if (existsSync20(path26.join(setDir, rep, name))) {
|
|
7458
7763
|
skipped.push(name);
|
|
7459
7764
|
continue;
|
|
7460
7765
|
}
|
|
@@ -7476,16 +7781,16 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
7476
7781
|
}
|
|
7477
7782
|
function rawEnvelopeFromFile(file, parts) {
|
|
7478
7783
|
if (parts) {
|
|
7479
|
-
const blocks = JSON.parse(readFileSync16(
|
|
7784
|
+
const blocks = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
|
|
7480
7785
|
if (!Array.isArray(blocks) || blocks.length === 0 || blocks.some((p) => typeof p !== "string")) throw new Error("parts file must be a non-empty JSON array of strings");
|
|
7481
7786
|
return { content: blocks.map((text) => ({ type: "text", text })) };
|
|
7482
7787
|
}
|
|
7483
|
-
return { content: [{ type: "text", text: readFileSync16(
|
|
7788
|
+
return { content: [{ type: "text", text: readFileSync16(path26.resolve(file), "utf8") }] };
|
|
7484
7789
|
}
|
|
7485
7790
|
async function runRecordIngest(opts) {
|
|
7486
7791
|
let payload;
|
|
7487
7792
|
try {
|
|
7488
|
-
payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync16(
|
|
7793
|
+
payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync16(path26.resolve(opts.file), "utf8"));
|
|
7489
7794
|
} catch (err) {
|
|
7490
7795
|
fail(opts, ExitCode.InputValidation, {
|
|
7491
7796
|
error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -7497,7 +7802,7 @@ async function runRecordIngest(opts) {
|
|
|
7497
7802
|
fail(opts, ExitCode.InputValidation, {
|
|
7498
7803
|
error: "--raw is for text tool responses; screenshots are binary",
|
|
7499
7804
|
code: "envelope-invalid",
|
|
7500
|
-
remediation:
|
|
7805
|
+
remediation: `Use \`${tendrilCommand(`record fetch --set ${path26.resolve(opts.setDir)} --rep ${opts.rep} --tool ${opts.tool} --url <image_url>`)}\` instead.`
|
|
7501
7806
|
});
|
|
7502
7807
|
}
|
|
7503
7808
|
if (opts.rep === "__set__" && opts.tool === "get_variable_defs") {
|
|
@@ -7509,7 +7814,15 @@ async function runRecordIngest(opts) {
|
|
|
7509
7814
|
remediation: "Save the get_variable_defs response verbatim as a text envelope."
|
|
7510
7815
|
});
|
|
7511
7816
|
}
|
|
7512
|
-
|
|
7817
|
+
const content = checkEnvelopeContent("get_variable_defs", payload);
|
|
7818
|
+
if (!content.ok) {
|
|
7819
|
+
fail(opts, ExitCode.InputValidation, {
|
|
7820
|
+
error: `set-level get_variable_defs was NOT recorded \u2014 ${content.reason}`,
|
|
7821
|
+
code: "envelope-rejected",
|
|
7822
|
+
remediation: REINGEST_GUIDANCE
|
|
7823
|
+
});
|
|
7824
|
+
}
|
|
7825
|
+
writeFileSync9(path26.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
|
|
7513
7826
|
`);
|
|
7514
7827
|
emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
|
|
7515
7828
|
process.stdout.write("set-level get_variable_defs ingested\n");
|
|
@@ -7525,7 +7838,7 @@ async function runRecordIngest(opts) {
|
|
|
7525
7838
|
if (assets !== void 0) {
|
|
7526
7839
|
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
7527
7840
|
`);
|
|
7528
|
-
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it
|
|
7841
|
+
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path26.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
|
|
7529
7842
|
`);
|
|
7530
7843
|
}
|
|
7531
7844
|
});
|
|
@@ -7533,7 +7846,7 @@ async function runRecordIngest(opts) {
|
|
|
7533
7846
|
fail(opts, ExitCode.InputValidation, {
|
|
7534
7847
|
error: err instanceof Error ? err.message : String(err),
|
|
7535
7848
|
code: "envelope-rejected",
|
|
7536
|
-
remediation: "
|
|
7849
|
+
remediation: "Fix what the error names, then re-run this exact command \u2014 recording resumes from disk, so nothing already recorded is lost."
|
|
7537
7850
|
});
|
|
7538
7851
|
}
|
|
7539
7852
|
}
|
|
@@ -7598,14 +7911,14 @@ async function runRecordIngestRep(opts) {
|
|
|
7598
7911
|
if (assets !== void 0) {
|
|
7599
7912
|
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
7600
7913
|
`);
|
|
7601
|
-
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it
|
|
7914
|
+
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path26.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
|
|
7602
7915
|
`);
|
|
7603
7916
|
}
|
|
7604
7917
|
});
|
|
7605
7918
|
}
|
|
7606
7919
|
function runRecordAsset(opts) {
|
|
7607
7920
|
if (opts.dir !== void 0) {
|
|
7608
|
-
const dir =
|
|
7921
|
+
const dir = path26.resolve(opts.dir);
|
|
7609
7922
|
const names = readdirSync5(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
|
|
7610
7923
|
if (names.length === 0) {
|
|
7611
7924
|
fail(opts, ExitCode.InputValidation, {
|
|
@@ -7617,7 +7930,7 @@ function runRecordAsset(opts) {
|
|
|
7617
7930
|
const ingested = [];
|
|
7618
7931
|
try {
|
|
7619
7932
|
for (const name of names) {
|
|
7620
|
-
ingestAsset(opts.setDir, opts.rep, name, readFileSync16(
|
|
7933
|
+
ingestAsset(opts.setDir, opts.rep, name, readFileSync16(path26.join(dir, name)));
|
|
7621
7934
|
ingested.push(name);
|
|
7622
7935
|
}
|
|
7623
7936
|
} catch (err) {
|
|
@@ -7637,11 +7950,11 @@ function runRecordAsset(opts) {
|
|
|
7637
7950
|
fail(opts, ExitCode.InputValidation, {
|
|
7638
7951
|
error: "pass --name and --file for a single asset, or --dir for a batch",
|
|
7639
7952
|
code: "asset-rejected",
|
|
7640
|
-
remediation:
|
|
7953
|
+
remediation: tendrilCommand(`record asset --set ${path26.resolve(opts.setDir)} --rep ${opts.rep} --dir <downloads-dir>`)
|
|
7641
7954
|
});
|
|
7642
7955
|
}
|
|
7643
7956
|
try {
|
|
7644
|
-
ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync16(
|
|
7957
|
+
ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync16(path26.resolve(opts.file)));
|
|
7645
7958
|
emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
|
|
7646
7959
|
process.stdout.write(`ingested ${opts.rep}/${opts.name}
|
|
7647
7960
|
`);
|
|
@@ -7677,7 +7990,7 @@ function narrowedRoles(derived, override) {
|
|
|
7677
7990
|
function rolesFromFile(opts, file, derived) {
|
|
7678
7991
|
let json;
|
|
7679
7992
|
try {
|
|
7680
|
-
json = JSON.parse(readFileSync16(
|
|
7993
|
+
json = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
|
|
7681
7994
|
} catch (err) {
|
|
7682
7995
|
fail(opts, ExitCode.InputValidation, {
|
|
7683
7996
|
error: `cannot read roles file ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -7715,11 +8028,11 @@ function rolesFromFile(opts, file, derived) {
|
|
|
7715
8028
|
};
|
|
7716
8029
|
}
|
|
7717
8030
|
function runRecordFinish(opts) {
|
|
7718
|
-
if (!
|
|
8031
|
+
if (!existsSync20(path26.join(opts.setDir, "recording-set.json"))) {
|
|
7719
8032
|
fail(opts, ExitCode.InputValidation, {
|
|
7720
8033
|
error: `no recording-set.json in ${opts.setDir}`,
|
|
7721
8034
|
code: "no-recording-set",
|
|
7722
|
-
remediation: `Run
|
|
8035
|
+
remediation: `Run \`${tendrilCommand(`record plan --set ${path26.resolve(opts.setDir)} --component <name> --metadata <envelope.json>`)}\` first \u2014 \`record finish\` closes a set the planner opened.`
|
|
7723
8036
|
});
|
|
7724
8037
|
}
|
|
7725
8038
|
const { manifest, raw } = readManifestFile(opts.setDir);
|
|
@@ -7747,17 +8060,17 @@ function runRecordFinish(opts) {
|
|
|
7747
8060
|
fail(opts, ExitCode.ConfirmationRequired, {
|
|
7748
8061
|
error: "--confirm-roles (and --roles-file) require an interactive terminal \u2014 this confirmation is human-only",
|
|
7749
8062
|
code: "roles-confirmation-not-interactive",
|
|
7750
|
-
remediation:
|
|
8063
|
+
remediation: `A human runs \`${tendrilCommand(`record finish --set ${path26.resolve(opts.setDir)} --confirm-roles`)}\` in their own terminal. Agents: show the proposal to your operator instead of confirming it.`
|
|
7751
8064
|
});
|
|
7752
8065
|
}
|
|
7753
8066
|
const merged = { ...raw, roles };
|
|
7754
|
-
const { issues } = validateRecordingSet(merged, (rel) =>
|
|
8067
|
+
const { issues } = validateRecordingSet(merged, (rel) => existsSync20(path26.join(opts.setDir, rel)));
|
|
7755
8068
|
const errors = issues.filter((i) => i.severity === "error");
|
|
7756
8069
|
if (errors.length > 0) {
|
|
7757
8070
|
fail(opts, ExitCode.InputValidation, {
|
|
7758
8071
|
error: `recording set rejected \u2014 roles NOT written: ${errors.map((e) => e.message).join("; ")}`,
|
|
7759
8072
|
code: "recording-set-invalid",
|
|
7760
|
-
remediation:
|
|
8073
|
+
remediation: `Fix the set (\`${tendrilCommand(`record status --set ${path26.resolve(opts.setDir)}`)}\` lists missing envelopes) or the roles graph, then re-run \`record finish\`.`
|
|
7761
8074
|
});
|
|
7762
8075
|
}
|
|
7763
8076
|
for (const issue of issues) if (issue.severity === "warning") warn(opts, issue.message);
|
|
@@ -7768,29 +8081,49 @@ function runRecordFinish(opts) {
|
|
|
7768
8081
|
`);
|
|
7769
8082
|
}
|
|
7770
8083
|
}
|
|
7771
|
-
var ENVELOPE_HELP, isAutoFetchAssetUrl, describeRoleLoss;
|
|
8084
|
+
var FIGMA_CALLS_PER_REP, FIGMA_CALLS_PER_REP_WORST, MULTI_DAY_CALL_THRESHOLD, LIMIT_HINT, stateTokens, MAX_SPOKEN_VALUES, andList, ENVELOPE_HELP, isAutoFetchAssetUrl, describeRoleLoss;
|
|
7772
8085
|
var init_record = __esm({
|
|
7773
|
-
"
|
|
8086
|
+
"packages/cli/src/commands/record.ts"() {
|
|
7774
8087
|
"use strict";
|
|
7775
8088
|
init_src3();
|
|
7776
8089
|
init_src();
|
|
7777
8090
|
init_src6();
|
|
7778
8091
|
init_output();
|
|
7779
8092
|
init_entitlement();
|
|
7780
|
-
|
|
8093
|
+
init_invocation();
|
|
8094
|
+
FIGMA_CALLS_PER_REP = 3;
|
|
8095
|
+
FIGMA_CALLS_PER_REP_WORST = 4;
|
|
8096
|
+
MULTI_DAY_CALL_THRESHOLD = 600;
|
|
8097
|
+
LIMIT_HINT = {
|
|
8098
|
+
status: "UNVERIFIED \u2014 recorded 2026-08-14 from Figma's published documentation and one field report; Figma changes these at will.",
|
|
8099
|
+
use: "Use ONLY if whoami states no allowance, and say it is unverified when you show it. The allowance whoami reports always wins.",
|
|
8100
|
+
perDayByPlan: {
|
|
8101
|
+
"Dev/Full seat, Professional": "~200 tool calls/day",
|
|
8102
|
+
"Dev/Full seat, Organization or Enterprise": "~600 tool calls/day",
|
|
8103
|
+
Starter: "~6 tool calls/month"
|
|
8104
|
+
},
|
|
8105
|
+
burst: "A per-minute ceiling is documented alongside the daily one, so parallel recorders can trip a limit long before the daily budget runs out."
|
|
8106
|
+
};
|
|
8107
|
+
stateTokens = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").split("-").filter((t) => t !== "");
|
|
8108
|
+
MAX_SPOKEN_VALUES = 6;
|
|
8109
|
+
andList = (items) => {
|
|
8110
|
+
const shown = items.length <= MAX_SPOKEN_VALUES ? items : [...items.slice(0, MAX_SPOKEN_VALUES), `${items.length - MAX_SPOKEN_VALUES} more`];
|
|
8111
|
+
return shown.length <= 1 ? shown[0] ?? "" : `${shown.slice(0, -1).join(", ")} and ${shown[shown.length - 1]}`;
|
|
8112
|
+
};
|
|
8113
|
+
ENVELOPE_HELP = `Envelope format: text tools save {"content":[{"type":"text","text":"<VERBATIM response text incl. any Currently-selected-nodes block>"}]}; get_screenshot: do NOT download the image yourself \u2014 pass its image_url to \`${tendrilCommand("record fetch")}\` (MCP: tendril_record_fetch), which pulls the bytes to disk directly. That keeps the pixel ground truth out of your context and costs one approval instead of a shell command per asset. Assets are asset-<first-8-hex-of-figma-uuid>.<ext>; their URLs appear as const declarations inside the design-context text and also expire.`;
|
|
7781
8114
|
isAutoFetchAssetUrl = (url) => isFigmaAssetUrl(url) || isLocalAssetUrl(url);
|
|
7782
8115
|
describeRoleLoss = (loss) => loss.kind === "main" ? `drops main "${loss.main}"` : `stops "${loss.part}" being a part of main "${loss.main}"`;
|
|
7783
8116
|
}
|
|
7784
8117
|
});
|
|
7785
8118
|
|
|
7786
|
-
//
|
|
8119
|
+
// packages/generate/src/engine.ts
|
|
7787
8120
|
var init_engine = __esm({
|
|
7788
|
-
"
|
|
8121
|
+
"packages/generate/src/engine.ts"() {
|
|
7789
8122
|
"use strict";
|
|
7790
8123
|
}
|
|
7791
8124
|
});
|
|
7792
8125
|
|
|
7793
|
-
//
|
|
8126
|
+
// packages/generate/src/parse.ts
|
|
7794
8127
|
function parseFiles(text, entry) {
|
|
7795
8128
|
const out = /* @__PURE__ */ new Map();
|
|
7796
8129
|
const re = /```[a-z]*[^\n]*\n(?:\/\/|\/\*)[ \t]*FILE:[ \t]*([\w.]+)[ \t]*(?:\*\/)?[ \t]*\n([\s\S]*?)```/g;
|
|
@@ -7823,15 +8156,15 @@ ${content}\`\`\``);
|
|
|
7823
8156
|
return blocks.join("\n");
|
|
7824
8157
|
}
|
|
7825
8158
|
var init_parse = __esm({
|
|
7826
|
-
"
|
|
8159
|
+
"packages/generate/src/parse.ts"() {
|
|
7827
8160
|
"use strict";
|
|
7828
8161
|
}
|
|
7829
8162
|
});
|
|
7830
8163
|
|
|
7831
|
-
//
|
|
8164
|
+
// packages/generate/src/engine-curated.ts
|
|
7832
8165
|
var CuratedEngine;
|
|
7833
8166
|
var init_engine_curated = __esm({
|
|
7834
|
-
"
|
|
8167
|
+
"packages/generate/src/engine-curated.ts"() {
|
|
7835
8168
|
"use strict";
|
|
7836
8169
|
init_parse();
|
|
7837
8170
|
CuratedEngine = class {
|
|
@@ -7969,9 +8302,9 @@ var init_engine_curated = __esm({
|
|
|
7969
8302
|
}
|
|
7970
8303
|
});
|
|
7971
8304
|
|
|
7972
|
-
//
|
|
7973
|
-
import { existsSync as
|
|
7974
|
-
import
|
|
8305
|
+
// packages/generate/src/loop.ts
|
|
8306
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync6, readFileSync as readFileSync17, renameSync, writeFileSync as writeFileSync10 } from "node:fs";
|
|
8307
|
+
import path27 from "node:path";
|
|
7975
8308
|
import { z as z11 } from "zod";
|
|
7976
8309
|
function objective(scores, behaviors) {
|
|
7977
8310
|
const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
|
|
@@ -8005,12 +8338,12 @@ ${preludeLines.join("\n")}` : ""}${absentLines.length > 0 ? `
|
|
|
8005
8338
|
MISSING FEATURES (absent-ink clusters \u2014 recorded marks your render leaves out or paints invisibly; GATING at the cert bar. Fix the named node's ink \u2014 a config carrying one cannot certify):
|
|
8006
8339
|
${absentLines.join("\n")}` : ""}
|
|
8007
8340
|
|
|
8008
|
-
Fix the FAIL configs (region coordinates are in the recorded screenshot's frame; ink<1 means recorded foreground pixels your render does not cover). Reading the pair: ink=1 with
|
|
8341
|
+
Fix the FAIL configs (region coordinates are in the recorded screenshot's frame; ink<1 means recorded foreground pixels your render does not cover). Reading the pair: ink credits a recorded pixel when ANY render ink lands within 1px of it, so ink=1 proves coverage \u2014 not presence, shape, or colour. A wrong-shaped mark over the right region, or a wrong-coloured one that is still ink, scores 1.000; and a recorded mark within ~30 channel-sum of the backdrop (#f6f6f6 on white is 27) is not ink at all, so it can be absent at ink 1.000 with no MISSING line. With ink=1 AND low sim, coverage held while pixels differ, so start with the TWO causes below \u2014 but never read ink=1 as "nothing is missing": confirm every faint or small recorded mark (hairlines, dividers, low-contrast controls) exists in your render. GEOMETRY: wrong position/size/radius; the diff shows shifted edges and bands. WRONG TEXT WEIGHT OR FACE: the diff shows a uniform haze over glyph runs; CSS binds the font FAMILY before the weight, a later family in the stack is never consulted for a weight the first one lacks, and font-synthesis: none rules out faux-bold \u2014 so a stack led by a family the kit holds at one weight renders every other weight in that face, silently. Check the font stack against the kit's cached faces before concluding geometry is at fault. LOW ink with high sim means recorded marks are absent or painted invisibly. Do not regress PASS configs. ${mode === "files" ? "Update the files in your candidate directory and re-run the score." : "Reply with the complete corrected files in the same FILE format."}`;
|
|
8009
8342
|
}
|
|
8010
8343
|
function archivePriorRun(outDir) {
|
|
8011
|
-
if (!
|
|
8344
|
+
if (!existsSync21(path27.join(outDir, "run-log.json")) && !existsSync21(path27.join(outDir, "loop-state.json"))) return void 0;
|
|
8012
8345
|
let n = 1;
|
|
8013
|
-
while (
|
|
8346
|
+
while (existsSync21(`${outDir}-prev-${n}`)) n += 1;
|
|
8014
8347
|
renameSync(outDir, `${outDir}-prev-${n}`);
|
|
8015
8348
|
return `${outDir}-prev-${n}`;
|
|
8016
8349
|
}
|
|
@@ -8019,14 +8352,14 @@ async function runEngineLoop(opts) {
|
|
|
8019
8352
|
const plateau = opts.plateau ?? 2;
|
|
8020
8353
|
const progress = opts.onProgress ?? (() => {
|
|
8021
8354
|
});
|
|
8022
|
-
const statePath =
|
|
8023
|
-
const resuming = opts.resume === true &&
|
|
8355
|
+
const statePath = path27.join(opts.outDir, "loop-state.json");
|
|
8356
|
+
const resuming = opts.resume === true && existsSync21(statePath);
|
|
8024
8357
|
if (!resuming) {
|
|
8025
8358
|
const archived = archivePriorRun(opts.outDir);
|
|
8026
8359
|
if (archived !== void 0) progress(`previous run archived to ${archived}`);
|
|
8027
8360
|
}
|
|
8028
8361
|
mkdirSync6(opts.outDir, { recursive: true });
|
|
8029
|
-
const scratch =
|
|
8362
|
+
const scratch = path27.join(opts.outDir, ".candidate");
|
|
8030
8363
|
let attempts = [];
|
|
8031
8364
|
let log = [];
|
|
8032
8365
|
let best;
|
|
@@ -8054,7 +8387,7 @@ async function runEngineLoop(opts) {
|
|
|
8054
8387
|
};
|
|
8055
8388
|
const writeCandidate = (files) => {
|
|
8056
8389
|
mkdirSync6(scratch, { recursive: true });
|
|
8057
|
-
for (const [name, content] of Object.entries(files)) writeFileSync10(
|
|
8390
|
+
for (const [name, content] of Object.entries(files)) writeFileSync10(path27.join(scratch, name), content);
|
|
8058
8391
|
};
|
|
8059
8392
|
const scoreCandidate = async (candidate, iter, usd, modelMs) => {
|
|
8060
8393
|
writeCandidate(candidate.files);
|
|
@@ -8112,8 +8445,8 @@ async function runEngineLoop(opts) {
|
|
|
8112
8445
|
const usd = candidate.usage?.usd ?? 0;
|
|
8113
8446
|
spentUsd += usd;
|
|
8114
8447
|
if (candidate.raw !== void 0) {
|
|
8115
|
-
mkdirSync6(
|
|
8116
|
-
writeFileSync10(
|
|
8448
|
+
mkdirSync6(path27.join(opts.outDir, "responses"), { recursive: true });
|
|
8449
|
+
writeFileSync10(path27.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
|
|
8117
8450
|
}
|
|
8118
8451
|
if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
|
|
8119
8452
|
const finish = candidate.usage?.finishReason ?? "?";
|
|
@@ -8139,10 +8472,10 @@ async function runEngineLoop(opts) {
|
|
|
8139
8472
|
}
|
|
8140
8473
|
}
|
|
8141
8474
|
}
|
|
8142
|
-
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(
|
|
8475
|
+
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(path27.join(opts.outDir, name), content);
|
|
8143
8476
|
const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
|
|
8144
8477
|
writeFileSync10(
|
|
8145
|
-
|
|
8478
|
+
path27.join(opts.outDir, "run-log.json"),
|
|
8146
8479
|
`${JSON.stringify(
|
|
8147
8480
|
{
|
|
8148
8481
|
...opts.meta,
|
|
@@ -8163,7 +8496,7 @@ async function runEngineLoop(opts) {
|
|
|
8163
8496
|
}
|
|
8164
8497
|
var better, RegionSchema, ConfigScoreSchema, BehaviorResultSchema, LoopStateSchema;
|
|
8165
8498
|
var init_loop2 = __esm({
|
|
8166
|
-
"
|
|
8499
|
+
"packages/generate/src/loop.ts"() {
|
|
8167
8500
|
"use strict";
|
|
8168
8501
|
better = (a, b) => a[0] !== b[0] ? a[0] > b[0] : a[1] !== b[1] ? a[1] > b[1] : a[2] > b[2];
|
|
8169
8502
|
RegionSchema = z11.object({ x0: z11.number(), y0: z11.number(), x1: z11.number(), y1: z11.number(), density: z11.number() });
|
|
@@ -8208,9 +8541,9 @@ var init_loop2 = __esm({
|
|
|
8208
8541
|
}
|
|
8209
8542
|
});
|
|
8210
8543
|
|
|
8211
|
-
//
|
|
8212
|
-
import { existsSync as
|
|
8213
|
-
import
|
|
8544
|
+
// packages/generate/src/brief.ts
|
|
8545
|
+
import { existsSync as existsSync22, readFileSync as readFileSync18 } from "node:fs";
|
|
8546
|
+
import path28 from "node:path";
|
|
8214
8547
|
function singleAxes2(name) {
|
|
8215
8548
|
const parsed = parseVariantAxes(name);
|
|
8216
8549
|
if (parsed === void 0) return void 0;
|
|
@@ -8369,7 +8702,7 @@ ${propLines.join("\n")}
|
|
|
8369
8702
|
|
|
8370
8703
|
${syntheticCombos.length > 0 ? `UNRECORDED REACHABLE COMBINATIONS: the design's exclusive axis cannot express ${syntheticCombos.join(", ")} \u2014 the API split makes them reachable with NO recorded truth. Compose them from the recorded per-axis truth (paint variables: one axis sets, the other consumes), never invent a bespoke look, and list them in your report.
|
|
8371
8704
|
` : ""}${slots.length > 0 ? `CONTENT PROPS: every string prop defaults to its RECORDED text \u2014 render the PROP, never a hardcoded literal. Configs pass per-pose recorded strings wherever the recording varies, and pixels enforce them: a hardcoded string fails those configs. Content presence (a heading that only exists in some poses) follows the AXIS props; the string prop only supplies the text. RICH-TEXT DEFAULTS: when the recorded content is a multi-run rich node (mixed weights, an underlined link span) whose concatenation is the prop's default string, a plain-string default would flatten the recorded formatting \u2014 default the parameter to undefined and render the recorded rich markup when the prop is absent; a caller-supplied string then renders plainly. That mirrors the design tool's own emission logic and keeps every recorded pose pixel-exact.
|
|
8372
|
-
` : ""}Rules: generated element ids come from React's useId() \u2014 never Math.random() in render/state init (SSR hydration hazard; measured: two same-day bundles disagreed). Plain CSS in styles.css (tokens.css optional, loaded first) \u2014 no Tailwind, no imports beyond react/react-dom, and NEVER import the stylesheet from the entry module: the harness injects tokens.css and styles.css itself, and an entry that imports CSS does not compile here (measured cost: one full round, every config 0). The component renders the RECORDED content as its defaults \u2014 reproduce it from the emissions. ${forcingCanon}${fontsLine}
|
|
8705
|
+
` : ""}Rules: generated element ids come from React's useId() \u2014 never Math.random() in render/state init (SSR hydration hazard; measured: two same-day bundles disagreed). Plain CSS in styles.css (tokens.css optional, loaded first) \u2014 no Tailwind, no imports beyond react/react-dom, and NEVER import the stylesheet from the entry module: the harness injects tokens.css and styles.css itself, and an entry that imports CSS does not compile here (measured cost: one full round, every config 0). The component renders the RECORDED content as its defaults \u2014 reproduce it from the emissions. ${forcingCanon}${fontsLine}ROOT SIZE IS YOURS AND PIXELS DO NOT CHECK IT, IN EITHER DIRECTION: the mount floors your root to the recorded box (#root > *{min-width:<recorded w>px;min-height:<recorded h>px} \u2014 a floor, never a cap), and the image the scorer compares is a CROP of that box, so paint outside it is never compared at all. Measured through the real scorer against a correct 120x40 root: an UNDERSIZED 80x28 root and OVERSIZED 160x52 and 124x44 roots ALL scored sim 1.000 / ink 1.000 with a byte-identical crop, as long as internals stay literal and top-left anchored. An oversized root shows only when its own border, radius or shadow \u2014 or a re-centred internal layout \u2014 lands back inside the crop. And some roots are not floored at all: a non-replaced INLINE root (a bare <label>, or an <a href> for a link variant) ignores width/height/min-width/min-height entirely and renders content-sized whatever you declare. So take both root dimensions from the recorded box in the payload, and give the root a display that honours them \u2014 no score, passing or failing, is evidence they are right. FLUIDITY AFFORDANCE (emit it always): every ROOT width declaration rides width: var(--tendril-root-width, <recorded>px) with that pose's recorded width as the fallback \u2014 per-variant root widths reuse the SAME property name with their own recorded fallbacks. With the property unset this is byte-equivalent truth: identical pixels, identical operability measurement (measured: flipping roots to a bare 100% held 20/20 pixels but made a commit check unmeasurable \u2014 the recorded pin carries real signal). A consumer makes an instance fluid by setting --tendril-root-width (e.g. 100%) on a wrapper, no edit to certified CSS. Widths only: recorded heights and internal geometry stay literal \u2014 fixed heights are often genuine design intent.`;
|
|
8373
8706
|
const mappedTokens = /* @__PURE__ */ new Set([...forcedStates, ...props.filter((p) => p.kind === "boolean").map((p) => p.name)]);
|
|
8374
8707
|
const unmappedInteractionEvidence = interactionEvidence.filter((e) => {
|
|
8375
8708
|
const sel = e.endsWith(" (selection axis)");
|
|
@@ -8449,8 +8782,8 @@ function envelopeText(file) {
|
|
|
8449
8782
|
}
|
|
8450
8783
|
function dismissEvidence(setDir, repSlugs) {
|
|
8451
8784
|
for (const slug of repSlugs) {
|
|
8452
|
-
const f =
|
|
8453
|
-
if (!
|
|
8785
|
+
const f = path28.join(setDir, slug, "get_design_context.json");
|
|
8786
|
+
if (!existsSync22(f)) continue;
|
|
8454
8787
|
const text = envelopeText(f);
|
|
8455
8788
|
const propHit = /[{,]\s*(\w*dismiss\w*)\s*=\s*(?:true|false)\b/i.exec(text) ?? /\b(\w*dismiss\w*)\??\s*:\s*boolean\b/i.exec(text);
|
|
8456
8789
|
if (propHit !== null) return `emission prop "${propHit[1]}"`;
|
|
@@ -8494,13 +8827,13 @@ function recordedFontNeeds(setDir, opts = {}) {
|
|
|
8494
8827
|
}
|
|
8495
8828
|
};
|
|
8496
8829
|
const manifest = loadManifest(setDir);
|
|
8497
|
-
const setDefs =
|
|
8498
|
-
if (
|
|
8830
|
+
const setDefs = path28.join(setDir, "get_variable_defs.json");
|
|
8831
|
+
if (existsSync22(setDefs)) fromDefs(envelopeText(setDefs));
|
|
8499
8832
|
for (const rep of manifest.reps) {
|
|
8500
|
-
const ctx =
|
|
8501
|
-
if (
|
|
8502
|
-
const defs =
|
|
8503
|
-
if (
|
|
8833
|
+
const ctx = path28.join(setDir, rep.slug, "get_design_context.json");
|
|
8834
|
+
if (existsSync22(ctx)) fromEmission(envelopeText(ctx));
|
|
8835
|
+
const defs = path28.join(setDir, rep.slug, "get_variable_defs.json");
|
|
8836
|
+
if (existsSync22(defs)) fromDefs(envelopeText(defs));
|
|
8504
8837
|
}
|
|
8505
8838
|
return [...byFamily.entries()].map(([family, paired]) => {
|
|
8506
8839
|
const weights = /* @__PURE__ */ new Set([...paired, ...opts.pairedOnly === true ? [] : unpaired]);
|
|
@@ -8511,8 +8844,8 @@ function symbolFontGlyphCount(setDir, reps) {
|
|
|
8511
8844
|
const PUA = /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u;
|
|
8512
8845
|
const glyphs = /* @__PURE__ */ new Set();
|
|
8513
8846
|
for (const rep of reps) {
|
|
8514
|
-
const file =
|
|
8515
|
-
if (!
|
|
8847
|
+
const file = path28.join(setDir, rep, "get_metadata.json");
|
|
8848
|
+
if (!existsSync22(file)) continue;
|
|
8516
8849
|
try {
|
|
8517
8850
|
const text = JSON.parse(readFileSync18(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
8518
8851
|
for (const m of text.matchAll(/<text\s[^>]*name="([^"]*)"/g)) {
|
|
@@ -8540,8 +8873,8 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
8540
8873
|
const propRep = [];
|
|
8541
8874
|
const perRep = [];
|
|
8542
8875
|
for (const slug of repSlugs) {
|
|
8543
|
-
const f =
|
|
8544
|
-
if (!
|
|
8876
|
+
const f = path28.join(setDir, slug, "get_design_context.json");
|
|
8877
|
+
if (!existsSync22(f)) continue;
|
|
8545
8878
|
const code = envelopeText(f);
|
|
8546
8879
|
const props = /* @__PURE__ */ new Map();
|
|
8547
8880
|
for (const m of code.matchAll(/[{,]\s*(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"/g)) {
|
|
@@ -8566,8 +8899,8 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
8566
8899
|
}
|
|
8567
8900
|
const axisValuesBySlug = /* @__PURE__ */ new Map();
|
|
8568
8901
|
for (const slug of repSlugs) {
|
|
8569
|
-
const metaFile =
|
|
8570
|
-
if (!
|
|
8902
|
+
const metaFile = path28.join(setDir, slug, "get_metadata.json");
|
|
8903
|
+
if (!existsSync22(metaFile)) continue;
|
|
8571
8904
|
const name = symbolName(envelopeText(metaFile));
|
|
8572
8905
|
if (name === void 0) continue;
|
|
8573
8906
|
const values = /* @__PURE__ */ new Set();
|
|
@@ -8666,8 +8999,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
8666
8999
|
const poses = [];
|
|
8667
9000
|
const missing = [];
|
|
8668
9001
|
for (const rep of manifest.reps) {
|
|
8669
|
-
const metaFile =
|
|
8670
|
-
if (!
|
|
9002
|
+
const metaFile = path28.join(setDir, rep.slug, "get_metadata.json");
|
|
9003
|
+
if (!existsSync22(metaFile)) {
|
|
8671
9004
|
missing.push(rep.slug);
|
|
8672
9005
|
continue;
|
|
8673
9006
|
}
|
|
@@ -8681,8 +9014,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
8681
9014
|
if (missing.length > 0) {
|
|
8682
9015
|
throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
|
|
8683
9016
|
}
|
|
8684
|
-
const setMeta =
|
|
8685
|
-
const latticeNames = manifest.latticeNames ?? (
|
|
9017
|
+
const setMeta = path28.join(setDir, "get_metadata.json");
|
|
9018
|
+
const latticeNames = manifest.latticeNames ?? (existsSync22(setMeta) ? [...envelopeText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
|
|
8686
9019
|
const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
|
|
8687
9020
|
const recordedFonts = recordedFontFamilies(setDir);
|
|
8688
9021
|
const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
|
|
@@ -8767,7 +9100,7 @@ ALL prose instructions live ABOVE the task payload \u2014 the payload contains o
|
|
|
8767
9100
|
|
|
8768
9101
|
ASSETS: inline the SVG assets you RENDER byte-verbatim, unchanged \u2014 never redraw or approximate an icon. Recorded assets that no scored config displays may be omitted. Two techniques reconcile that rule with reuse (both measured at 1.000): a glyph recorded once but shown in several colors keeps its bytes (fill attribute included) and is repainted with a CSS fill rule \u2014 a CSS declaration outranks an SVG presentation attribute, so one verbatim copy serves every tone; and multi-part glyphs needing fractional placement can nest each verbatim asset as a child <svg x= y=> inside one integer-origin frame (SVG user-space coordinates are exact), an alternative to the transform: scale() pattern.
|
|
8769
9102
|
|
|
8770
|
-
GEOMETRY ARBITRATION (when emission styling and the recorded box disagree, the BOX wins): Figma strokes are INSIDE the box \u2014 border+padding sums that overshoot a recorded dimension mean use an inset box-shadow or subtract the border from the padding. That rule covers strokes AT the box edge only: when a config's reference PNG is LARGER than its recorded box, the recording itself proves an OUTWARD effect \u2014 a hover ring, glow, or shadow past the frame \u2014 and the outward part is drawn outward (box-shadow spread, outline), never forced inside. On such configs the score report carries a "seat" field ({x, y} \u2014 where the recorded box sits inside the larger reference), derived from the recording's own effect geometry (shadow offset/radius/spread) \u2014 informational, so an offset shadow's asymmetric bleed is not misread as a registration error. Following strokes-inside against a padded reference contradicts the recorded pad and cost a measured five configs a full round. The harness mounts each config at its recorded box (width \xD7 height,
|
|
9103
|
+
GEOMETRY ARBITRATION (when emission styling and the recorded box disagree, the BOX wins): Figma strokes are INSIDE the box \u2014 border+padding sums that overshoot a recorded dimension mean use an inset box-shadow or subtract the border from the padding. That rule covers strokes AT the box edge only: when a config's reference PNG is LARGER than its recorded box, the recording itself proves an OUTWARD effect \u2014 a hover ring, glow, or shadow past the frame \u2014 and the outward part is drawn outward (box-shadow spread, outline), never forced inside. On such configs the score report carries a "seat" field ({x, y} \u2014 where the recorded box sits inside the larger reference), derived from the recording's own effect geometry (shadow offset/radius/spread) \u2014 informational, so an offset shadow's asymmetric bleed is not misread as a registration error. Following strokes-inside against a padded reference contradicts the recorded pad and cost a measured five configs a full round. The harness mounts each config at its recorded box (width \xD7 height) and FLOORS your root to it, never caps it; build to those recorded dimensions, not to guessed viewports \u2014 the prescribed API below says what that floor hides. Known rasterizer delta: Chrome often seats small text ONE PIXEL HIGHER than Figma in an identically sized box. Correct it with a PAINT-ONLY offset on those text runs \u2014 position: relative with top: 1px \u2014 never with padding or margin, which would grow the recorded box this same paragraph calls truth. TREAT IT AS A MEASUREMENT, NOT A RULE: measured cases are 12px/16px and 14px/20px needing the nudge and 14px/18px not, so font size alone does not predict it and neither does any formula we can currently defend. RULE ORDER when both could apply (run 7: a config sat 0.0004 under the bar AND showed uniform spread \u2014 the rules pointed opposite ways): read the DIFF FIRST; the uniform-spread stop-rule below OUTRANKS the try-it rule here. Only when the diff shows a shifted band: apply the nudge, re-score, and keep it only if it helped. Four measured signatures, so do not expect one: on one kit it drove ink recall to 1.000; on another ink was already 1.000 and only similarity moved (mean 0.961 \u2192 0.976, four configs from 0.003 above the bar to 0.028); on a third it REGRESSED a passing bundle from 9/9 to 3/9 configs and had to be reverted; on a fourth it made every FAILING config worse too (tinted in-section surfaces: ink dropped ~0.011 across all eight configs and pushed a passing one under the bar). It is a hypothesis to score, never a default \u2014 NEVER apply it to a bundle that already passes, and when the failing family's diff shows a uniform low-density spread across the glyph band rather than a shifted band, the cause is rasterization weight, not seating: the nudge cannot help and a heavier hypothesis has no recorded truth to justify it \u2014 report the honest sub-bar result instead. Design-token NAMES in the payload are Figma names \u2014 canonicalize to valid CSS idents (lowercase kebab, e.g. "Text/text-primary" \u2192 --text-text-primary) if you emit tokens.css; literal values are equally acceptable. NEVER invent a token to satisfy a lint finding: quality findings are advisory, and a recorded value the kit has no token for is CORRECT as a literal \u2014 a made-up token name corrupts the tokens file as a record of the kit.
|
|
8771
9104
|
|
|
8772
9105
|
RECORDED BOXES ARE TRUTH even when inconsistent: the same string may
|
|
8773
9106
|
have different recorded widths across variants (designer resizing) \u2014 a
|
|
@@ -8784,13 +9117,13 @@ PAINT & PLATFORM TRAPS (each measured on a paid run; every one passed typecheck,
|
|
|
8784
9117
|
- A native <dialog> carries user-agent padding: 1em. Override every side, or the root grows past its recorded box and every band inside shifts.
|
|
8785
9118
|
- THE PAGE CANVAS IS NOT YOURS. Never paint the recording's page background into the component \u2014 no canvas-coloured plates across the root box, no square shadow spread carrying the canvas past the frame. The mount composites your render over the recorded canvas, so transparency wherever the recording shows canvas is both correct and scores correctly; the score report's canvasCoupling counts canvas pixels your render refuses to let the page repaint, and a component that paints the canvas is wrong on every real page.
|
|
8786
9119
|
- TOKENS SCOPE TO YOUR ROOT CLASS, NEVER :root. Bundles compose on real pages: token names on :root collide across independently generated components and the last stylesheet loaded silently rewrites the others (measured: two colliding names flipped certified surfaces translucent). Declare every token under the component's root class.
|
|
8787
|
-
- WHEN TWO AXES BOTH CONTROL PAINT, THEY COMPOSE THROUGH CUSTOM PROPERTIES \u2014 one axis SETS variables, the other CONSUMES them. Direct paint rules on both axes have equal specificity, so source order silently drops one axis for exactly the crossed poses (measured: variant \xD7 tone \u2014 primary+critical rendered dark neutral instead of the recorded red
|
|
9120
|
+
- WHEN TWO AXES BOTH CONTROL PAINT, THEY COMPOSE THROUGH CUSTOM PROPERTIES \u2014 one axis SETS variables, the other CONSUMES them. Direct paint rules on both axes have equal specificity, so source order silently drops one axis for exactly the crossed poses (measured: variant \xD7 tone \u2014 primary+critical rendered dark neutral instead of the recorded red). CROSSED POSES ARE SCORED: the harness mounts every RECORDED pose, crosses included \u2014 measured on a bundle passing 20/20 (13 single-axis configs, 7 crosses), adding one later same-specificity rule left every single-axis config the rule did not itself target byte-stable and dropped all five crossed configs it touched from ~0.99 to sim 0.09\u20130.16. Only a cross with NO recorded pose goes ungraded.
|
|
8788
9121
|
|
|
8789
9122
|
INTERACTION-READY BY DEFAULT: components are real controls, never static lookalikes \u2014 use the native element matching the archetype (button, input[type=radio|checkbox], select\u2026), real handlers, keyboard operability, and real state (checked/disabled/:hover/:focus-visible). Behavioral checks fail statues. (The forcing-hook rule is specified once, in the prescribed API below.) Your file runs in a bare browser bundle: it must be fully self-contained. IMPORT EVERY React API you use explicitly \u2014 e.g. import { useState, useRef, useEffect } from "react" \u2014 nothing is provided globally; a missing import crashes the mount and every config scores 0.
|
|
8790
9123
|
|
|
8791
9124
|
STATE SEMANTICS (prescribed \u2014 do not choose your own):
|
|
8792
9125
|
- The component OWNS its interaction state, seeded from the prop. A selection/checked prop supplies the INITIAL value; user interaction updates internal state and must visibly commit without any parent re-render. A purely controlled component that ignores its own clicks fails the behavioral checks. Call an on\u2026Change callback when one is in the prescribed API, but never depend on it to update your own paint.
|
|
8793
|
-
- Read-only means operable but not committable: the control stays focusable and in the accessibility tree with aria-readonly="true", and cancels commit on both pointer and keyboard. Do NOT
|
|
9126
|
+
- Read-only means operable but not committable: the control stays focusable and in the accessibility tree with aria-readonly="true", and cancels commit on both pointer and keyboard. Do NOT fake it with pointer-events:none or disabled. pointer-events:none removes only the POINTER path \u2014 the element stops being hit-tested, so clicks land on whatever is behind it \u2014 while Tab still reaches it and .focus() still lands: the control still takes focus and still commits from the KEYBOARD (measured in Chrome: Space checked a pointer-events:none checkbox). Of the two, ONLY disabled refuses focus, which is why reaching for pointer-events:none to make a pose unfocusable fails disabled-not-focusable with "disabled element took focus".
|
|
8794
9127
|
- GEOMETRY FOLLOWS PROPS; ONLY PAINT FOLLOWS LIVE STATE. Per-variant sizing must key off the pose props the caller passed, never off live interaction state \u2014 otherwise clicking resizes the component (a recorded selected/unselected width difference is a designer artifact, not a consequence of selection). The scorer renders static poses and structurally cannot see this, so it is on you.
|
|
8795
9128
|
|
|
8796
9129
|
${systemApi}
|
|
@@ -8801,7 +9134,7 @@ RESOLVED FOR THIS RECORDING: it is ${opts.colorScheme}-mode truth, so pin color-
|
|
|
8801
9134
|
}
|
|
8802
9135
|
var PoseCompletenessError, kebab3, camel, pascal, RESERVED_PROPS, axisPropName, isStateAxis, DISMISS_NAMES, symbolName, STYLE_WEIGHTS2, styleWeight;
|
|
8803
9136
|
var init_brief = __esm({
|
|
8804
|
-
"
|
|
9137
|
+
"packages/generate/src/brief.ts"() {
|
|
8805
9138
|
"use strict";
|
|
8806
9139
|
init_src();
|
|
8807
9140
|
init_src4();
|
|
@@ -8855,11 +9188,11 @@ var init_brief = __esm({
|
|
|
8855
9188
|
}
|
|
8856
9189
|
});
|
|
8857
9190
|
|
|
8858
|
-
//
|
|
8859
|
-
import { existsSync as
|
|
8860
|
-
import
|
|
9191
|
+
// packages/generate/src/segments.ts
|
|
9192
|
+
import { existsSync as existsSync23, readFileSync as readFileSync19, readdirSync as readdirSync6 } from "node:fs";
|
|
9193
|
+
import path29 from "node:path";
|
|
8861
9194
|
function repText(set, rep, tool) {
|
|
8862
|
-
const env = JSON.parse(readFileSync19(
|
|
9195
|
+
const env = JSON.parse(readFileSync19(path29.join(set, rep, `${tool}.json`), "utf8"));
|
|
8863
9196
|
return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
|
|
8864
9197
|
}
|
|
8865
9198
|
function stripFigmaInstructions(emission) {
|
|
@@ -8920,16 +9253,16 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
|
|
|
8920
9253
|
function buildSegments(task, mode = "fenced") {
|
|
8921
9254
|
const SET = task.set;
|
|
8922
9255
|
let rawDefs = {};
|
|
8923
|
-
if (
|
|
8924
|
-
const text = envelopeFirstTextPart(JSON.parse(readFileSync19(
|
|
9256
|
+
if (existsSync23(path29.join(SET, "get_variable_defs.json"))) {
|
|
9257
|
+
const text = envelopeFirstTextPart(JSON.parse(readFileSync19(path29.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
|
|
8925
9258
|
try {
|
|
8926
9259
|
rawDefs = JSON.parse(text);
|
|
8927
9260
|
} catch {
|
|
8928
9261
|
}
|
|
8929
9262
|
} else {
|
|
8930
9263
|
for (const cfg of task.configs) {
|
|
8931
|
-
const f =
|
|
8932
|
-
if (!
|
|
9264
|
+
const f = path29.join(SET, cfg.rep, "get_variable_defs.json");
|
|
9265
|
+
if (!existsSync23(f)) continue;
|
|
8933
9266
|
const text = envelopeFirstTextPart(JSON.parse(readFileSync19(f, "utf8"))) || "{}";
|
|
8934
9267
|
try {
|
|
8935
9268
|
for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
|
|
@@ -8938,8 +9271,8 @@ function buildSegments(task, mode = "fenced") {
|
|
|
8938
9271
|
}
|
|
8939
9272
|
}
|
|
8940
9273
|
const emissionTexts = task.configs.map((cfg) => {
|
|
8941
|
-
const f =
|
|
8942
|
-
return
|
|
9274
|
+
const f = path29.join(SET, cfg.rep, "get_design_context.json");
|
|
9275
|
+
return existsSync23(f) ? envelopeFirstTextPart(JSON.parse(readFileSync19(f, "utf8"))) : "";
|
|
8943
9276
|
});
|
|
8944
9277
|
const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
|
|
8945
9278
|
const defs = JSON.stringify(map, null, 1);
|
|
@@ -8954,9 +9287,9 @@ ${defs}
|
|
|
8954
9287
|
for (const cfg of task.configs) {
|
|
8955
9288
|
const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
|
|
8956
9289
|
const emission = stripFigmaInstructions(repText(SET, cfg.rep, "get_design_context"));
|
|
8957
|
-
const assets = readdirSync6(
|
|
9290
|
+
const assets = readdirSync6(path29.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
|
|
8958
9291
|
\`\`\`svg
|
|
8959
|
-
${readFileSync19(
|
|
9292
|
+
${readFileSync19(path29.join(SET, cfg.rep, f), "utf8")}
|
|
8960
9293
|
\`\`\``).join("\n");
|
|
8961
9294
|
parts.push(`
|
|
8962
9295
|
## Config ${cfg.rep} \u2192 ${cfg.component} ${JSON.stringify(cfg.props)}
|
|
@@ -8981,20 +9314,20 @@ Optionally a third block with \`/* FILE: tokens.css */\`.`);
|
|
|
8981
9314
|
} else {
|
|
8982
9315
|
parts.push(`
|
|
8983
9316
|
## Output format
|
|
8984
|
-
Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into
|
|
9317
|
+
Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into ONE candidate directory: \`tendril-out/${path29.basename(task.set)}-candidate/\` unless you were handed another path. Pass that same directory to \`tendril engine score\` every round and keep writing into it \u2014 the scorer stamps the bundle there, writes its evidence beside your files, and appends its score-history.jsonl lines there (one when a round starts, one when it scores); a fresh directory each round throws all of that away. Do not paste file contents into chat \u2014 the scorer reads the directory.`);
|
|
8985
9318
|
}
|
|
8986
9319
|
return parts.join("\n");
|
|
8987
9320
|
}
|
|
8988
9321
|
var cssIdent;
|
|
8989
9322
|
var init_segments = __esm({
|
|
8990
|
-
"
|
|
9323
|
+
"packages/generate/src/segments.ts"() {
|
|
8991
9324
|
"use strict";
|
|
8992
9325
|
init_src();
|
|
8993
9326
|
cssIdent = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
8994
9327
|
}
|
|
8995
9328
|
});
|
|
8996
9329
|
|
|
8997
|
-
//
|
|
9330
|
+
// packages/generate/src/consent.ts
|
|
8998
9331
|
function formatConsentLines(e) {
|
|
8999
9332
|
return [
|
|
9000
9333
|
`engine=${e.engineId} model=${e.modelId} route=${e.providerRoute}`,
|
|
@@ -9004,12 +9337,12 @@ function formatConsentLines(e) {
|
|
|
9004
9337
|
];
|
|
9005
9338
|
}
|
|
9006
9339
|
var init_consent = __esm({
|
|
9007
|
-
"
|
|
9340
|
+
"packages/generate/src/consent.ts"() {
|
|
9008
9341
|
"use strict";
|
|
9009
9342
|
}
|
|
9010
9343
|
});
|
|
9011
9344
|
|
|
9012
|
-
//
|
|
9345
|
+
// packages/generate/src/adapter.ts
|
|
9013
9346
|
function resolveAdapterSettings(entry, overrides = {}) {
|
|
9014
9347
|
const notes = [];
|
|
9015
9348
|
let maxTokens = DEFAULT_MAX_TOKENS;
|
|
@@ -9041,16 +9374,16 @@ function resolveAdapterSettings(entry, overrides = {}) {
|
|
|
9041
9374
|
}
|
|
9042
9375
|
var DEFAULT_MAX_TOKENS;
|
|
9043
9376
|
var init_adapter = __esm({
|
|
9044
|
-
"
|
|
9377
|
+
"packages/generate/src/adapter.ts"() {
|
|
9045
9378
|
"use strict";
|
|
9046
9379
|
DEFAULT_MAX_TOKENS = 2e4;
|
|
9047
9380
|
}
|
|
9048
9381
|
});
|
|
9049
9382
|
|
|
9050
|
-
//
|
|
9383
|
+
// packages/generate/src/bundle-emit.ts
|
|
9051
9384
|
import { createHash as createHash4 } from "node:crypto";
|
|
9052
|
-
import { copyFileSync, existsSync as
|
|
9053
|
-
import
|
|
9385
|
+
import { copyFileSync, existsSync as existsSync24, mkdirSync as mkdirSync7, readFileSync as readFileSync20, readdirSync as readdirSync7, rmSync as rmSync3, writeFileSync as writeFileSync11 } from "node:fs";
|
|
9386
|
+
import path30 from "node:path";
|
|
9054
9387
|
function pinFromConfigs(configs) {
|
|
9055
9388
|
const domains = /* @__PURE__ */ new Map();
|
|
9056
9389
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -9119,9 +9452,9 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9119
9452
|
const notices = [];
|
|
9120
9453
|
const licenseTexts = /* @__PURE__ */ new Map();
|
|
9121
9454
|
for (const face of faces) {
|
|
9122
|
-
const src =
|
|
9123
|
-
const target = `./fonts/${
|
|
9124
|
-
const format = FONT_FORMATS[
|
|
9455
|
+
const src = path30.join(cacheDir, path30.basename(face.file));
|
|
9456
|
+
const target = `./fonts/${path30.basename(face.file)}`;
|
|
9457
|
+
const format = FONT_FORMATS[path30.extname(face.file).toLowerCase()] ?? "truetype";
|
|
9125
9458
|
const family = face.family.replace(/['\\]/g, "").replace(/\*\//g, "");
|
|
9126
9459
|
const decl = `@font-face { font-family: '${family}'; font-weight: ${face.weight}; font-style: normal; src: url(${target}) format('${format}'); }`;
|
|
9127
9460
|
const license = normalizeFontLicense(face.license);
|
|
@@ -9151,14 +9484,14 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9151
9484
|
`/* ${decl} */`
|
|
9152
9485
|
);
|
|
9153
9486
|
}
|
|
9154
|
-
} else if (
|
|
9155
|
-
mkdirSync7(
|
|
9156
|
-
copyFileSync(src,
|
|
9487
|
+
} else if (existsSync24(src) && createHash4("sha256").update(readFileSync20(src)).digest("hex") === face.sha256) {
|
|
9488
|
+
mkdirSync7(path30.join(bundleDir, "fonts"), { recursive: true });
|
|
9489
|
+
copyFileSync(src, path30.join(bundleDir, "fonts", path30.basename(face.file)));
|
|
9157
9490
|
licenseTexts.set(terms.file, terms.text);
|
|
9158
9491
|
const upstream = upstreamAttribution(face);
|
|
9159
9492
|
notices.push(
|
|
9160
9493
|
"",
|
|
9161
|
-
`${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${
|
|
9494
|
+
`${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${path30.basename(face.file)}`,
|
|
9162
9495
|
` licence: ${terms.title} \u2014 full text in ./${terms.file}`,
|
|
9163
9496
|
` source: ${face.source}`,
|
|
9164
9497
|
` sha256: ${face.sha256}`,
|
|
@@ -9172,9 +9505,9 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9172
9505
|
}
|
|
9173
9506
|
if (lines.length === 0) return null;
|
|
9174
9507
|
if (notices.length > 0) {
|
|
9175
|
-
const fontsDir =
|
|
9176
|
-
for (const [file, text] of licenseTexts) writeFileSync11(
|
|
9177
|
-
writeFileSync11(
|
|
9508
|
+
const fontsDir = path30.join(bundleDir, "fonts");
|
|
9509
|
+
for (const [file, text] of licenseTexts) writeFileSync11(path30.join(fontsDir, file), text);
|
|
9510
|
+
writeFileSync11(path30.join(fontsDir, "NOTICE.txt"), `${[NOTICE_PREAMBLE, ...notices].join("\n")}
|
|
9178
9511
|
`);
|
|
9179
9512
|
header.push(
|
|
9180
9513
|
"/* REDISTRIBUTION: the face files in ./fonts/ are redistributed under the licences",
|
|
@@ -9186,8 +9519,8 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9186
9519
|
`;
|
|
9187
9520
|
}
|
|
9188
9521
|
function countLatticeSymbols(setDir) {
|
|
9189
|
-
const manifestFile =
|
|
9190
|
-
if (
|
|
9522
|
+
const manifestFile = path30.join(setDir, "recording-set.json");
|
|
9523
|
+
if (existsSync24(manifestFile)) {
|
|
9191
9524
|
try {
|
|
9192
9525
|
const lattice = JSON.parse(readFileSync20(manifestFile, "utf8")).latticeNames;
|
|
9193
9526
|
if (lattice !== void 0 && lattice.length > 0) return lattice.length;
|
|
@@ -9195,9 +9528,9 @@ function countLatticeSymbols(setDir) {
|
|
|
9195
9528
|
}
|
|
9196
9529
|
}
|
|
9197
9530
|
const files = [
|
|
9198
|
-
|
|
9199
|
-
...
|
|
9200
|
-
].filter((f) =>
|
|
9531
|
+
path30.join(setDir, "get_metadata.json"),
|
|
9532
|
+
...existsSync24(setDir) ? readdirSync7(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path30.join(setDir, f)) : []
|
|
9533
|
+
].filter((f) => existsSync24(f));
|
|
9201
9534
|
if (files.length === 0) return null;
|
|
9202
9535
|
let count = 0;
|
|
9203
9536
|
for (const f of files) {
|
|
@@ -9209,21 +9542,21 @@ function countLatticeSymbols(setDir) {
|
|
|
9209
9542
|
function recordingSetHash(setDir, configs) {
|
|
9210
9543
|
const relPaths = [];
|
|
9211
9544
|
for (const name of ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json"]) {
|
|
9212
|
-
if (
|
|
9545
|
+
if (existsSync24(path30.join(setDir, name))) relPaths.push(name);
|
|
9213
9546
|
}
|
|
9214
9547
|
for (const cfg of configs) {
|
|
9215
9548
|
for (const f of ["get_design_context.json", "get_metadata.json", "get_screenshot.json", "get_variable_defs.json"]) {
|
|
9216
|
-
if (
|
|
9549
|
+
if (existsSync24(path30.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
|
|
9217
9550
|
}
|
|
9218
|
-
if (
|
|
9219
|
-
for (const asset of readdirSync7(
|
|
9551
|
+
if (existsSync24(path30.join(setDir, cfg.rep))) {
|
|
9552
|
+
for (const asset of readdirSync7(path30.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
|
|
9220
9553
|
relPaths.push(`${cfg.rep}/${asset}`);
|
|
9221
9554
|
}
|
|
9222
9555
|
}
|
|
9223
9556
|
}
|
|
9224
9557
|
return hashRecordingSet(
|
|
9225
9558
|
relPaths,
|
|
9226
|
-
(p) => new Uint8Array(readFileSync20(
|
|
9559
|
+
(p) => new Uint8Array(readFileSync20(path30.join(setDir, p))),
|
|
9227
9560
|
(chunks) => {
|
|
9228
9561
|
const h = createHash4("sha256");
|
|
9229
9562
|
for (const c of chunks) h.update(c);
|
|
@@ -9246,7 +9579,7 @@ function emitBundleV1(opts) {
|
|
|
9246
9579
|
const lattice = countLatticeSymbols(opts.task.set);
|
|
9247
9580
|
const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:"));
|
|
9248
9581
|
const prelude = opts.behaviors.filter((b) => b.id.startsWith("prelude:"));
|
|
9249
|
-
const cssFiles = ["styles.css", "tokens.css"].map((f) =>
|
|
9582
|
+
const cssFiles = ["styles.css", "tokens.css"].map((f) => path30.join(opts.bundleDir, f)).filter((f) => existsSync24(f));
|
|
9250
9583
|
const families = cssFontFamilies(cssFiles.map((f) => readFileSync20(f, "utf8")).join("\n"));
|
|
9251
9584
|
const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
|
|
9252
9585
|
family: f.family,
|
|
@@ -9276,7 +9609,7 @@ function emitBundleV1(opts) {
|
|
|
9276
9609
|
// resolvable via verify's --set override).
|
|
9277
9610
|
path: (() => {
|
|
9278
9611
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
9279
|
-
const rel =
|
|
9612
|
+
const rel = path30.relative(base, opts.task.set);
|
|
9280
9613
|
return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
|
|
9281
9614
|
})(),
|
|
9282
9615
|
component: opts.componentName,
|
|
@@ -9300,12 +9633,12 @@ function emitBundleV1(opts) {
|
|
|
9300
9633
|
})
|
|
9301
9634
|
};
|
|
9302
9635
|
const written = [];
|
|
9303
|
-
const manifestPath2 =
|
|
9636
|
+
const manifestPath2 = path30.join(opts.bundleDir, "component.json");
|
|
9304
9637
|
writeFileSync11(manifestPath2, `${JSON.stringify(manifest, null, 2)}
|
|
9305
9638
|
`);
|
|
9306
9639
|
written.push(manifestPath2);
|
|
9307
|
-
const stylesPath =
|
|
9308
|
-
if (
|
|
9640
|
+
const stylesPath = path30.join(opts.bundleDir, "styles.css");
|
|
9641
|
+
if (existsSync24(stylesPath)) {
|
|
9309
9642
|
const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
|
|
9310
9643
|
const current = readFileSync20(stylesPath, "utf8");
|
|
9311
9644
|
const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
|
|
@@ -9313,8 +9646,8 @@ function emitBundleV1(opts) {
|
|
|
9313
9646
|
${stripped}`);
|
|
9314
9647
|
written.push(stylesPath);
|
|
9315
9648
|
}
|
|
9316
|
-
const fontsCssPath =
|
|
9317
|
-
rmSync3(
|
|
9649
|
+
const fontsCssPath = path30.join(opts.bundleDir, "fonts.css");
|
|
9650
|
+
rmSync3(path30.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
|
|
9318
9651
|
rmSync3(fontsCssPath, { force: true });
|
|
9319
9652
|
const fontsCss = fontsCssFor(requiredFontsManifest(families, opts.fontCacheDir), opts.bundleDir, opts.fontCacheDir, opts.substitutedFamilies ?? []);
|
|
9320
9653
|
if (fontsCss !== null) {
|
|
@@ -9327,7 +9660,7 @@ ${stripped}`);
|
|
|
9327
9660
|
}
|
|
9328
9661
|
var FONT_FORMATS, BARS, NOTICE_PREAMBLE, OFL_1_1_TEXT, UFL_1_0_TEXT, APACHE_2_0_TEXT, REDISTRIBUTABLE;
|
|
9329
9662
|
var init_bundle_emit = __esm({
|
|
9330
|
-
"
|
|
9663
|
+
"packages/generate/src/bundle-emit.ts"() {
|
|
9331
9664
|
"use strict";
|
|
9332
9665
|
init_src6();
|
|
9333
9666
|
init_src();
|
|
@@ -9742,9 +10075,9 @@ DEALINGS IN THE FONT SOFTWARE.
|
|
|
9742
10075
|
}
|
|
9743
10076
|
});
|
|
9744
10077
|
|
|
9745
|
-
//
|
|
10078
|
+
// packages/generate/src/index.ts
|
|
9746
10079
|
var init_src7 = __esm({
|
|
9747
|
-
"
|
|
10080
|
+
"packages/generate/src/index.ts"() {
|
|
9748
10081
|
"use strict";
|
|
9749
10082
|
init_engine();
|
|
9750
10083
|
init_engine_curated();
|
|
@@ -9758,7 +10091,7 @@ var init_src7 = __esm({
|
|
|
9758
10091
|
}
|
|
9759
10092
|
});
|
|
9760
10093
|
|
|
9761
|
-
//
|
|
10094
|
+
// packages/cli/src/commands/fonts.ts
|
|
9762
10095
|
var fonts_exports = {};
|
|
9763
10096
|
__export(fonts_exports, {
|
|
9764
10097
|
DEFAULT_FONT_CACHE: () => DEFAULT_FONT_CACHE,
|
|
@@ -9768,8 +10101,8 @@ __export(fonts_exports, {
|
|
|
9768
10101
|
runFontsResolveSet: () => runFontsResolveSet,
|
|
9769
10102
|
runFontsStatus: () => runFontsStatus
|
|
9770
10103
|
});
|
|
9771
|
-
import { existsSync as
|
|
9772
|
-
import
|
|
10104
|
+
import { existsSync as existsSync25, readFileSync as readFileSync21 } from "node:fs";
|
|
10105
|
+
import path31 from "node:path";
|
|
9773
10106
|
async function runFontsResolve(opts) {
|
|
9774
10107
|
const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
|
|
9775
10108
|
emitData(opts, result, () => {
|
|
@@ -9784,7 +10117,7 @@ async function runFontsResolve(opts) {
|
|
|
9784
10117
|
}
|
|
9785
10118
|
}
|
|
9786
10119
|
async function runFontsResolveSet(opts) {
|
|
9787
|
-
const setDir =
|
|
10120
|
+
const setDir = path31.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
|
|
9788
10121
|
let needs = [];
|
|
9789
10122
|
try {
|
|
9790
10123
|
needs = recordedFontNeeds(setDir);
|
|
@@ -9792,19 +10125,19 @@ async function runFontsResolveSet(opts) {
|
|
|
9792
10125
|
fail(opts, ExitCode.InputValidation, {
|
|
9793
10126
|
error: `cannot read font needs from ${setDir}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
9794
10127
|
code: "font-needs-unreadable",
|
|
9795
|
-
remediation:
|
|
10128
|
+
remediation: `Point --set at a directory recorded with \`${tendrilCommand("record plan")}\` (it carries recording-set.json), or resolve a family explicitly: ${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}.`
|
|
9796
10129
|
});
|
|
9797
10130
|
}
|
|
9798
10131
|
if (needs.length === 0) {
|
|
9799
10132
|
fail(opts, ExitCode.InputValidation, {
|
|
9800
10133
|
error: `recording set ${setDir} declares no detectable font families`,
|
|
9801
10134
|
code: "font-needs-empty",
|
|
9802
|
-
remediation:
|
|
10135
|
+
remediation: `Resolve the family the design uses explicitly: ${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}.`
|
|
9803
10136
|
});
|
|
9804
10137
|
}
|
|
9805
10138
|
const inCache = new Set(resolvedFontFamilies(opts.cacheDir).flatMap((f) => f.weights.map((w) => `${f.family.toLowerCase()}:${w}`)));
|
|
9806
10139
|
const unstamped = new Map(facesMissingLicense(opts.cacheDir).map((f) => [`${f.family.toLowerCase()}:${f.weight}`, f.sha256]));
|
|
9807
|
-
const
|
|
10140
|
+
const cached2 = [];
|
|
9808
10141
|
const resolved = [];
|
|
9809
10142
|
const failures = [];
|
|
9810
10143
|
const relicensed = [];
|
|
@@ -9817,7 +10150,7 @@ async function runFontsResolveSet(opts) {
|
|
|
9817
10150
|
const key = `${need.family.toLowerCase()}:${w}`;
|
|
9818
10151
|
if (!inCache.has(key)) missing.push(w);
|
|
9819
10152
|
else if (unstamped.has(key)) toStamp.push(w);
|
|
9820
|
-
else
|
|
10153
|
+
else cached2.push({ family: need.family, weight: w });
|
|
9821
10154
|
}
|
|
9822
10155
|
if (missing.length > 0) {
|
|
9823
10156
|
const result = await resolveFonts(need.family, missing, opts.cacheDir);
|
|
@@ -9834,10 +10167,10 @@ async function runFontsResolveSet(opts) {
|
|
|
9834
10167
|
unlicensed.push(...result.failures);
|
|
9835
10168
|
}
|
|
9836
10169
|
}
|
|
9837
|
-
emitData(opts, { set: setDir, needs, cached, resolved, relicensed, unlicensed, failures }, () => {
|
|
10170
|
+
emitData(opts, { set: setDir, needs, cached: cached2, resolved, relicensed, unlicensed, failures }, () => {
|
|
9838
10171
|
process.stdout.write(`set declares: ${needs.map((n) => `${n.family} (${n.weights.join(", ")})`).join(" \xB7 ")}
|
|
9839
10172
|
`);
|
|
9840
|
-
for (const f of
|
|
10173
|
+
for (const f of cached2) process.stdout.write(`cached ${f.family} ${f.weight}
|
|
9841
10174
|
`);
|
|
9842
10175
|
for (const f of resolved) process.stdout.write(`resolved ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
|
|
9843
10176
|
`);
|
|
@@ -9857,16 +10190,16 @@ async function runFontsResolveSet(opts) {
|
|
|
9857
10190
|
}
|
|
9858
10191
|
}
|
|
9859
10192
|
function runFontsStatus(opts) {
|
|
9860
|
-
const manifestPath2 =
|
|
9861
|
-
if (!
|
|
10193
|
+
const manifestPath2 = path31.join(opts.cacheDir, "manifest.json");
|
|
10194
|
+
if (!existsSync25(manifestPath2)) {
|
|
9862
10195
|
fail(opts, ExitCode.FontsUnproven, {
|
|
9863
10196
|
error: `no font cache at ${opts.cacheDir}`,
|
|
9864
10197
|
code: "fonts-unresolved",
|
|
9865
|
-
remediation:
|
|
10198
|
+
remediation: `Run \`${tendrilCommand("fonts resolve --set <recording-dir>")}\` (or \`${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}\`) first.`
|
|
9866
10199
|
});
|
|
9867
10200
|
}
|
|
9868
10201
|
const faces = JSON.parse(readFileSync21(manifestPath2, "utf8"));
|
|
9869
|
-
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(
|
|
10202
|
+
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path31.resolve(opts.lock), opts.cacheDir) : null;
|
|
9870
10203
|
emitData(opts, { faces, lockVerdicts }, () => {
|
|
9871
10204
|
for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
|
|
9872
10205
|
`);
|
|
@@ -9897,7 +10230,10 @@ function runFontsAdd(opts) {
|
|
|
9897
10230
|
fail(opts, ExitCode.InputValidation, {
|
|
9898
10231
|
error: err instanceof Error ? err.message : String(err),
|
|
9899
10232
|
code: "font-add-failed",
|
|
9900
|
-
|
|
10233
|
+
// The face file is the user's own licensed asset: this command
|
|
10234
|
+
// can name every argument but that one, so it stays a marked
|
|
10235
|
+
// blank rather than a fabricated path that looks real.
|
|
10236
|
+
remediation: `Pass a font file you are licensed to use, by absolute path: ${tendrilCommand(`fonts add "${opts.family}" ${opts.weight} <absolute-path-to-face.woff2>`)}`
|
|
9901
10237
|
});
|
|
9902
10238
|
}
|
|
9903
10239
|
emitData(opts, face, () => {
|
|
@@ -9906,28 +10242,32 @@ function runFontsAdd(opts) {
|
|
|
9906
10242
|
});
|
|
9907
10243
|
}
|
|
9908
10244
|
var init_fonts = __esm({
|
|
9909
|
-
"
|
|
10245
|
+
"packages/cli/src/commands/fonts.ts"() {
|
|
9910
10246
|
"use strict";
|
|
9911
10247
|
init_src3();
|
|
9912
10248
|
init_src4();
|
|
9913
10249
|
init_src7();
|
|
9914
10250
|
init_output();
|
|
10251
|
+
init_invocation();
|
|
9915
10252
|
}
|
|
9916
10253
|
});
|
|
9917
10254
|
|
|
9918
|
-
//
|
|
10255
|
+
// packages/cli/src/font-guidance.ts
|
|
10256
|
+
import path32 from "node:path";
|
|
9919
10257
|
function fontsUnprovenRemediation(setDir) {
|
|
9920
|
-
|
|
10258
|
+
const set = setDir === void 0 ? void 0 : path32.resolve(setDir);
|
|
10259
|
+
if (set !== void 0) {
|
|
9921
10260
|
try {
|
|
9922
|
-
const needs = recordedFontNeeds(
|
|
10261
|
+
const needs = recordedFontNeeds(set);
|
|
9923
10262
|
if (needs.length > 0) {
|
|
9924
10263
|
const families = needs.map((n) => `"${n.family}"`).join(", ");
|
|
9925
|
-
return `Run
|
|
10264
|
+
return `Run \`${tendrilCommand(`fonts resolve --set ${set}`)}\` \u2014 the recording declares ${families}; faces land in ${DEFAULT_FONT_CACHE}.`;
|
|
9926
10265
|
}
|
|
9927
10266
|
} catch {
|
|
9928
10267
|
}
|
|
9929
10268
|
}
|
|
9930
|
-
|
|
10269
|
+
const target = set ?? "<recording-dir>";
|
|
10270
|
+
return `Run \`${tendrilCommand(`fonts resolve --set ${target}`)}\` (or \`${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}\`) to populate the font cache \u2014 the cache read is ${DEFAULT_FONT_CACHE}.`;
|
|
9931
10271
|
}
|
|
9932
10272
|
function taskFontFamilies(setDir) {
|
|
9933
10273
|
try {
|
|
@@ -9969,28 +10309,33 @@ function missingWeights(setDir, cacheDir) {
|
|
|
9969
10309
|
return gaps;
|
|
9970
10310
|
}
|
|
9971
10311
|
var init_font_guidance = __esm({
|
|
9972
|
-
"
|
|
10312
|
+
"packages/cli/src/font-guidance.ts"() {
|
|
9973
10313
|
"use strict";
|
|
9974
10314
|
init_src4();
|
|
9975
10315
|
init_src7();
|
|
10316
|
+
init_invocation();
|
|
9976
10317
|
}
|
|
9977
10318
|
});
|
|
9978
10319
|
|
|
9979
|
-
//
|
|
10320
|
+
// packages/cli/src/commands/verify.ts
|
|
9980
10321
|
var verify_exports = {};
|
|
9981
10322
|
__export(verify_exports, {
|
|
9982
10323
|
NO_ROLE_MANIFEST: () => NO_ROLE_MANIFEST,
|
|
9983
10324
|
ROLES_NOT_RESOLVED: () => ROLES_NOT_RESOLVED,
|
|
9984
10325
|
checkSummarySegments: () => checkSummarySegments,
|
|
9985
10326
|
compositionReport: () => compositionReport,
|
|
10327
|
+
eyeCheck: () => eyeCheck,
|
|
9986
10328
|
failureTally: () => failureTally,
|
|
9987
10329
|
foldConfigStatus: () => foldConfigStatus,
|
|
9988
10330
|
interactionCoverage: () => interactionCoverage,
|
|
10331
|
+
occlusionReport: () => occlusionReport,
|
|
10332
|
+
operabilityLine: () => operabilityLine,
|
|
10333
|
+
operabilityReport: () => operabilityReport,
|
|
9989
10334
|
resolveComposition: () => resolveComposition,
|
|
9990
10335
|
runVerify: () => runVerify
|
|
9991
10336
|
});
|
|
9992
|
-
import { existsSync as
|
|
9993
|
-
import
|
|
10337
|
+
import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node:fs";
|
|
10338
|
+
import path33 from "node:path";
|
|
9994
10339
|
function interactionCoverage(behaviors) {
|
|
9995
10340
|
const interaction = behaviors.filter((b) => !b.id.startsWith("prelude:"));
|
|
9996
10341
|
return {
|
|
@@ -10000,6 +10345,43 @@ function interactionCoverage(behaviors) {
|
|
|
10000
10345
|
operability: interaction.length > 0 && interaction.every((b) => b.pass) ? "verified" : "unverified"
|
|
10001
10346
|
};
|
|
10002
10347
|
}
|
|
10348
|
+
function operabilityReport(input) {
|
|
10349
|
+
const { interactionChecks, interactionPassed, preludeChecks } = interactionCoverage(input.behaviors);
|
|
10350
|
+
const prelude = ` The ${preludeChecks} behavior check(s) that did run are page-level prelude style hygiene, which says nothing about whether the component works.`;
|
|
10351
|
+
if (interactionChecks === 0) {
|
|
10352
|
+
const evidence = input.interactionEvidence;
|
|
10353
|
+
if (evidence === void 0) {
|
|
10354
|
+
return {
|
|
10355
|
+
short: "nothing authored; recorded poses never derived",
|
|
10356
|
+
unavailable: `no interaction behaviours were authored for this component, and this run never derived the recording's interactive poses \u2014 nothing about focus, typing, or keyboard operation was measured, and nothing here rules out a static lookalike.${prelude}`
|
|
10357
|
+
};
|
|
10358
|
+
}
|
|
10359
|
+
if (evidence.length > 0) {
|
|
10360
|
+
return {
|
|
10361
|
+
short: "interactive poses recorded, none authored",
|
|
10362
|
+
unavailable: `the recording PROVES interactive poses (${evidence.join(
|
|
10363
|
+
", "
|
|
10364
|
+
)}) and ZERO interaction behaviours were authored to cover them \u2014 nothing about focus, typing, or keyboard operation was measured; this is an instrument failure, not a working component.${prelude}`
|
|
10365
|
+
};
|
|
10366
|
+
}
|
|
10367
|
+
return { short: "no interactive poses recorded", unavailable: `${NO_INTERACTIVE_POSES}${prelude}` };
|
|
10368
|
+
}
|
|
10369
|
+
if (interactionPassed < interactionChecks) {
|
|
10370
|
+
const failed = interactionChecks - interactionPassed;
|
|
10371
|
+
return {
|
|
10372
|
+
checks: interactionChecks,
|
|
10373
|
+
passed: interactionPassed,
|
|
10374
|
+
short: `${failed} of ${interactionChecks} interaction check(s) failed`,
|
|
10375
|
+
unverified: `the interaction checks RAN and ${failed} of ${interactionChecks} failed (each named in the FAIL behavior rows) \u2014 this component WAS exercised and did not behave as the recording requires; unverified here means measured and wrong, not unmeasured.`
|
|
10376
|
+
};
|
|
10377
|
+
}
|
|
10378
|
+
return { checks: interactionChecks, passed: interactionPassed };
|
|
10379
|
+
}
|
|
10380
|
+
function operabilityLine(state) {
|
|
10381
|
+
if ("unavailable" in state) return `UNVERIFIED operability \u2014 ${state.unavailable}`;
|
|
10382
|
+
if ("unverified" in state) return `UNVERIFIED operability \u2014 ${state.unverified}`;
|
|
10383
|
+
return void 0;
|
|
10384
|
+
}
|
|
10003
10385
|
function foldConfigStatus(s, failDemotions, substitutedFamilies) {
|
|
10004
10386
|
const { exact: _exact, ...reported } = s;
|
|
10005
10387
|
let status = tierOf(s, BARS2.cert);
|
|
@@ -10038,8 +10420,18 @@ function checkSummarySegments(input) {
|
|
|
10038
10420
|
const tally = (rows) => `${rows.filter((r) => r.pass).length}/${rows.length}`;
|
|
10039
10421
|
const { availability } = input;
|
|
10040
10422
|
const composition = availability.unavailable !== void 0 ? `composition NOT CHECKED (${availability.short})` : `composition ${tally(input.structural)} structural, ${input.crops !== void 0 ? `${tally(input.crops)} crops` : "crops unavailable"} (roles: ${rolesSourceLabel(availability.roles)}${(availability.roles.narrowingAccepted ?? []).length > 0 ? `, ${(availability.roles.narrowingAccepted ?? []).length} narrowing(s) accepted: ${(availability.roles.narrowingAccepted ?? []).join(", ")}` : ""})`;
|
|
10041
|
-
const
|
|
10042
|
-
|
|
10423
|
+
const occ = occlusionReport(input.occlusion);
|
|
10424
|
+
const occlusion = "unavailable" in occ ? `occlusion not applicable (${NO_OVERLAY_DECLARED})` : `occlusion ${tally(input.occlusion)}`;
|
|
10425
|
+
const operability = "short" in input.operability ? `operability UNVERIFIED (${input.operability.short})` : `operability ${input.operability.passed}/${input.operability.checks} interaction`;
|
|
10426
|
+
return ` \xB7 ${operability} \xB7 ${composition} \xB7 ${occlusion}`;
|
|
10427
|
+
}
|
|
10428
|
+
function occlusionReport(occlusion) {
|
|
10429
|
+
if (occlusion.length === 0) {
|
|
10430
|
+
return {
|
|
10431
|
+
unavailable: "the bundle declares no overlay (no open-state config and no prelude popover) \u2014 the sibling-occlusion check DOES NOT APPLY and nothing about overlay stacking was measured; an empty check list is this state, not a clean bill"
|
|
10432
|
+
};
|
|
10433
|
+
}
|
|
10434
|
+
return { checks: occlusion.length, passed: occlusion.filter((o) => o.pass).length };
|
|
10043
10435
|
}
|
|
10044
10436
|
function compositionReport(input) {
|
|
10045
10437
|
const { availability, regions } = input;
|
|
@@ -10057,6 +10449,13 @@ function compositionReport(input) {
|
|
|
10057
10449
|
crops: input.crops ?? { unavailable: cropsUnavailable }
|
|
10058
10450
|
};
|
|
10059
10451
|
}
|
|
10452
|
+
function eyeCheck(bundleDir) {
|
|
10453
|
+
return {
|
|
10454
|
+
command: tendrilCommand(`inspect "${bundleDir}"`),
|
|
10455
|
+
sheetPath: path33.join(bundleDir, "verify-evidence", "inspect.html"),
|
|
10456
|
+
note: "magnified recorded-vs-rendered crops of every small node; scores cannot see shape. The command WRITES/refreshes the sheet at sheetPath \u2014 re-run it after every verify; an existing sheet may show stale crops."
|
|
10457
|
+
};
|
|
10458
|
+
}
|
|
10060
10459
|
function failureTally(t) {
|
|
10061
10460
|
return `${t.configs.length} config(s), ${t.behaviors.length} behavior(s), ${t.structural.length + t.crops.length} composition check(s), ${t.occlusion.length} occlusion check(s) below the ${t.bar} bar${t.evidenceUnverified ? "; PLUS the interaction-evidence instrument gate failed (operability could not be verified \u2014 see FAIL interaction-evidence above)" : ""} \u2014 bundle written, verdict honest (Q4)`;
|
|
10062
10461
|
}
|
|
@@ -10065,7 +10464,7 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
10065
10464
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
10066
10465
|
const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
10067
10466
|
const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
|
|
10068
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
10467
|
+
const registry = Object.values(TASKS).find((t) => path33.resolve(t.set) === path33.resolve(setDir));
|
|
10069
10468
|
const authored = (() => {
|
|
10070
10469
|
if (registry !== void 0) return void 0;
|
|
10071
10470
|
try {
|
|
@@ -10074,7 +10473,7 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
10074
10473
|
fail(opts, ExitCode.InputValidation, {
|
|
10075
10474
|
error: `cannot author the verification task from ${setDir}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
10076
10475
|
code: "AUTHORING_FAILED",
|
|
10077
|
-
remediation:
|
|
10476
|
+
remediation: `Fix the recording set (duplicate/colliding poses fail authoring by design \u2014 check \`${tendrilCommand(`record status --set ${setDir}`)}\` and the set's variant names).`
|
|
10078
10477
|
});
|
|
10079
10478
|
}
|
|
10080
10479
|
})();
|
|
@@ -10091,24 +10490,27 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
10091
10490
|
task,
|
|
10092
10491
|
unmapped,
|
|
10093
10492
|
adapterOnly,
|
|
10094
|
-
|
|
10493
|
+
// Registry sets carry hand-declared behaviors and no derivation
|
|
10494
|
+
// runs, so their evidence is UNKNOWN, not empty: an empty list is
|
|
10495
|
+
// spent downstream as "the recording holds no interactive pose".
|
|
10496
|
+
interactionEvidence: authored?.api.interactionEvidence,
|
|
10095
10497
|
unmappedInteractionEvidence: authored?.api.unmappedInteractionEvidence ?? []
|
|
10096
10498
|
};
|
|
10097
10499
|
}
|
|
10098
10500
|
async function runVerify(opts) {
|
|
10099
10501
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
10100
|
-
const setOverride = opts.set !== void 0 ?
|
|
10101
|
-
opts = { ...opts, bundleDir:
|
|
10102
|
-
if (!
|
|
10502
|
+
const setOverride = opts.set !== void 0 ? path33.resolve(callerCwd, opts.set) : void 0;
|
|
10503
|
+
opts = { ...opts, bundleDir: path33.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
|
|
10504
|
+
if (!existsSync26(opts.bundleDir)) {
|
|
10103
10505
|
fail(opts, ExitCode.InputValidation, {
|
|
10104
10506
|
error: `bundle directory not found: ${opts.bundleDir}`,
|
|
10105
10507
|
code: "bundle-missing",
|
|
10106
10508
|
remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
|
|
10107
10509
|
});
|
|
10108
10510
|
}
|
|
10109
|
-
const manifestPath2 =
|
|
10511
|
+
const manifestPath2 = path33.join(opts.bundleDir, "component.json");
|
|
10110
10512
|
let manifest;
|
|
10111
|
-
if (
|
|
10513
|
+
if (existsSync26(manifestPath2)) {
|
|
10112
10514
|
const { manifest: parsed, issues } = readBundleManifest(readFileSync22(manifestPath2, "utf8"));
|
|
10113
10515
|
if (issues.length > 0) {
|
|
10114
10516
|
fail(opts, ExitCode.InputValidation, {
|
|
@@ -10121,7 +10523,7 @@ async function runVerify(opts) {
|
|
|
10121
10523
|
}
|
|
10122
10524
|
let task;
|
|
10123
10525
|
let unmapped = [];
|
|
10124
|
-
let interactionEvidence
|
|
10526
|
+
let interactionEvidence;
|
|
10125
10527
|
let unmappedInteractionEvidence = [];
|
|
10126
10528
|
let availability = ROLES_NOT_RESOLVED;
|
|
10127
10529
|
if (opts.task !== void 0) {
|
|
@@ -10136,21 +10538,21 @@ async function runVerify(opts) {
|
|
|
10136
10538
|
task = registry;
|
|
10137
10539
|
} else if (manifest !== void 0) {
|
|
10138
10540
|
const resolveSetDir = (p) => {
|
|
10139
|
-
if (
|
|
10140
|
-
const fromRepo =
|
|
10141
|
-
if (
|
|
10142
|
-
return
|
|
10541
|
+
if (path33.isAbsolute(p)) return p;
|
|
10542
|
+
const fromRepo = path33.resolve(REPO_ROOT, p);
|
|
10543
|
+
if (existsSync26(fromRepo)) return fromRepo;
|
|
10544
|
+
return path33.resolve(callerCwd, p);
|
|
10143
10545
|
};
|
|
10144
10546
|
const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
|
|
10145
|
-
if (!
|
|
10547
|
+
if (!existsSync26(path33.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path33.resolve(t.set) === path33.resolve(setDir))) {
|
|
10146
10548
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
10147
10549
|
error: `recording set not found or unmanifested: ${setDir}`,
|
|
10148
10550
|
code: "recording-set-missing",
|
|
10149
10551
|
remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
|
|
10150
10552
|
});
|
|
10151
10553
|
}
|
|
10152
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
10153
|
-
if (registry !== void 0 && !
|
|
10554
|
+
const registry = Object.values(TASKS).find((t) => path33.resolve(t.set) === path33.resolve(setDir));
|
|
10555
|
+
if (registry !== void 0 && !existsSync26(path33.join(setDir, "recording-set.json"))) {
|
|
10154
10556
|
const recordedSlugs = registry.configs.map((c) => c.rep);
|
|
10155
10557
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
10156
10558
|
unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
@@ -10174,8 +10576,8 @@ async function runVerify(opts) {
|
|
|
10174
10576
|
warn(opts, `recording set content differs from the bundle's provenance stamp (${hash.slice(0, 12)}\u2026 vs ${manifest.provenance.recordingSet.hash.slice(0, 12)}\u2026) \u2014 scores apply to the CURRENT set`);
|
|
10175
10577
|
}
|
|
10176
10578
|
for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
|
|
10177
|
-
const p =
|
|
10178
|
-
if (!
|
|
10579
|
+
const p = path33.join(opts.bundleDir, name);
|
|
10580
|
+
if (!existsSync26(p)) continue;
|
|
10179
10581
|
const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync22(p)));
|
|
10180
10582
|
if (issues.length > 0) {
|
|
10181
10583
|
fail(opts, ExitCode.InputValidation, {
|
|
@@ -10214,7 +10616,7 @@ async function runVerify(opts) {
|
|
|
10214
10616
|
warn(opts, `font weights (advisory): the recording implies weights ${g.declared.join(", ")} for "${g.family}" \u2014 cache provides ${g.provided.join(", ")}; nearest-weight rendering may shift stroke density (certification stays family-gated; implied weights can leak across families)`);
|
|
10215
10617
|
}
|
|
10216
10618
|
const missing = task.configs.filter(
|
|
10217
|
-
(c) => !
|
|
10619
|
+
(c) => !existsSync26(path33.join(task.set, c.rep, "get_screenshot.json")) || !existsSync26(path33.join(task.set, c.rep, "get_metadata.json"))
|
|
10218
10620
|
);
|
|
10219
10621
|
if (missing.length > 0) {
|
|
10220
10622
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -10224,10 +10626,10 @@ async function runVerify(opts) {
|
|
|
10224
10626
|
});
|
|
10225
10627
|
}
|
|
10226
10628
|
const bar = BARS2[opts.bar];
|
|
10227
|
-
const evidenceDir =
|
|
10629
|
+
const evidenceDir = path33.join(opts.bundleDir, "verify-evidence");
|
|
10228
10630
|
const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
10229
10631
|
const quality = await checkBundleQuality(opts.bundleDir, task.entry, { dir: task.set, reps: task.configs.map((c) => c.rep) });
|
|
10230
|
-
const bundleCss = ["tokens.css", "styles.css"].map((f) =>
|
|
10632
|
+
const bundleCss = ["tokens.css", "styles.css"].map((f) => path33.join(opts.bundleDir, f)).filter((f) => existsSync26(f)).map((f) => readFileSync22(f, "utf8")).join("\n");
|
|
10231
10633
|
const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
|
|
10232
10634
|
const parity = await checkHoverParity(task, opts.bundleDir);
|
|
10233
10635
|
const behaviors = [...await checkBehaviors(task, opts.bundleDir), ...parity];
|
|
@@ -10258,7 +10660,8 @@ async function runVerify(opts) {
|
|
|
10258
10660
|
const certified = statuses.filter((s) => s.status === "certified").length;
|
|
10259
10661
|
const occlusionFailures = occlusion.filter((o) => !o.pass);
|
|
10260
10662
|
const coverage = interactionCoverage(behaviors);
|
|
10261
|
-
const
|
|
10663
|
+
const operability = operabilityReport({ behaviors, interactionEvidence });
|
|
10664
|
+
const evidenceUnverified = (interactionEvidence?.length ?? 0) > 0 && coverage.interactionChecks === 0;
|
|
10262
10665
|
const okExceptDemotion = pixelFailures.length === 0 && behaviorFailures.length === 0 && structuralFailures.length === 0 && cropFailures.length === 0 && occlusionFailures.length === 0 && !evidenceUnverified;
|
|
10263
10666
|
const certBlockedByAbsentInk = opts.bar === "cert" ? absentInkDemoted : [];
|
|
10264
10667
|
const ok = okExceptDemotion && certBlockedByAbsentInk.length === 0;
|
|
@@ -10298,7 +10701,11 @@ async function runVerify(opts) {
|
|
|
10298
10701
|
// read exactly like a verified one — measured, a statue and a real
|
|
10299
10702
|
// control produced the same 6/6. The split is the honest form.
|
|
10300
10703
|
...coverage,
|
|
10301
|
-
...interactionEvidence.length > 0 ? { interactionEvidence } : {},
|
|
10704
|
+
...interactionEvidence !== void 0 && interactionEvidence.length > 0 ? { interactionEvidence } : {},
|
|
10705
|
+
// The one-word verdict's CAUSE, in the channel that has no summary
|
|
10706
|
+
// line to carry it. `operability` above keeps its key and values;
|
|
10707
|
+
// this says which of its meanings the run actually produced.
|
|
10708
|
+
operabilityCheck: operability,
|
|
10302
10709
|
// Poses the recording proves interactive that no operability
|
|
10303
10710
|
// check covers — pixel-verified only. The first cold kit shipped
|
|
10304
10711
|
// pressed/focus this way with nothing in the report saying so.
|
|
@@ -10312,6 +10719,10 @@ async function runVerify(opts) {
|
|
|
10312
10719
|
// silently inherited neither.
|
|
10313
10720
|
quality: { findings: quality.findings, tokensAbsent: quality.tokensAbsent },
|
|
10314
10721
|
occlusion,
|
|
10722
|
+
// The rows' STATE, which the rows alone cannot carry (see
|
|
10723
|
+
// occlusionReport): the agent channel had no counterpart to the
|
|
10724
|
+
// human line's "occlusion not applicable (no overlay declared)".
|
|
10725
|
+
occlusionCheck: occlusionReport(occlusion),
|
|
10315
10726
|
configs: statuses,
|
|
10316
10727
|
behaviors,
|
|
10317
10728
|
evidence: { dir: evidenceDir, files: "per config: <rep>-render.png, <rep>-ref.png, <rep>-diff.png" },
|
|
@@ -10323,15 +10734,7 @@ async function runVerify(opts) {
|
|
|
10323
10734
|
...certBlockedByAbsentInk.length > 0 ? { certBlockedByAbsentInk } : {},
|
|
10324
10735
|
...weightGaps.length > 0 ? { fontWeightGaps: weightGaps } : {},
|
|
10325
10736
|
...lcdTextEnabled() ? { environmentOverrides: ["enable-lcd-text"] } : {},
|
|
10326
|
-
|
|
10327
|
-
// footer, so MCP-driven agents — the documented path — were never
|
|
10328
|
-
// told the check exists and shipped without ever opening a sheet.
|
|
10329
|
-
// Structured here so every consumer sees it.
|
|
10330
|
-
eyeCheck: {
|
|
10331
|
-
command: `tendril inspect "${opts.bundleDir}"`,
|
|
10332
|
-
sheetPath: path31.join(opts.bundleDir, "verify-evidence", "inspect.html"),
|
|
10333
|
-
note: "magnified recorded-vs-rendered crops of every small node; scores cannot see shape. The command WRITES/refreshes the sheet at sheetPath \u2014 re-run it after every verify; an existing sheet may show stale crops."
|
|
10334
|
-
}
|
|
10737
|
+
eyeCheck: eyeCheck(opts.bundleDir)
|
|
10335
10738
|
};
|
|
10336
10739
|
emitData(opts, report, () => {
|
|
10337
10740
|
for (const s of statuses) {
|
|
@@ -10363,11 +10766,10 @@ async function runVerify(opts) {
|
|
|
10363
10766
|
process.stdout.write(`UNAVAILABLE composition crops \u2014 ${regionsOut.unavailable}
|
|
10364
10767
|
`);
|
|
10365
10768
|
}
|
|
10366
|
-
const ic = interactionCoverage(behaviors);
|
|
10367
10769
|
process.stdout.write(
|
|
10368
10770
|
`
|
|
10369
10771
|
${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuses.length} \u2265 pass bar \xB7 behaviors ${behaviors.length - behaviorFailures.length}/${behaviors.length}${checkSummarySegments(
|
|
10370
|
-
{ availability, structural, crops, occlusion }
|
|
10772
|
+
{ availability, structural, crops, occlusion, operability }
|
|
10371
10773
|
)}
|
|
10372
10774
|
`
|
|
10373
10775
|
);
|
|
@@ -10396,14 +10798,13 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
10396
10798
|
}
|
|
10397
10799
|
if (evidenceUnverified) {
|
|
10398
10800
|
process.stdout.write(
|
|
10399
|
-
`FAIL interaction-evidence \u2014 the recording proves interactive poses (${interactionEvidence.join(", ")}) and ZERO interaction behaviors were verified. The authoring vocabulary did not map them; this is an instrument failure, not a verified component. Mappable today: ${[...INTERACTION_STATES, ...BOOLEAN_STATES].join("/")} \u2014 on an axis named State (checked/selected state axes are recorded as evidence but not yet authorable \u2014 widening is on the roadmap; this is not fixable from component code).
|
|
10400
|
-
`
|
|
10401
|
-
);
|
|
10402
|
-
} else if (ic.interactionChecks === 0) {
|
|
10403
|
-
process.stdout.write(
|
|
10404
|
-
`UNVERIFIED operability \u2014 0 interaction checks were authored for this component; the behaviors above are page-level style hygiene. Nothing here says the component responds to a user.
|
|
10801
|
+
`FAIL interaction-evidence \u2014 the recording proves interactive poses (${(interactionEvidence ?? []).join(", ")}) and ZERO interaction behaviors were verified. The authoring vocabulary did not map them; this is an instrument failure, not a verified component. Mappable today: ${[...INTERACTION_STATES, ...BOOLEAN_STATES].join("/")} \u2014 on an axis named State (checked/selected state axes are recorded as evidence but not yet authorable \u2014 widening is on the roadmap; this is not fixable from component code).
|
|
10405
10802
|
`
|
|
10406
10803
|
);
|
|
10804
|
+
} else {
|
|
10805
|
+
const line = operabilityLine(operability);
|
|
10806
|
+
if (line !== void 0) process.stdout.write(`${line}
|
|
10807
|
+
`);
|
|
10407
10808
|
}
|
|
10408
10809
|
if (unmappedInteractionEvidence.length > 0) {
|
|
10409
10810
|
process.stdout.write(
|
|
@@ -10425,18 +10826,20 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
10425
10826
|
}
|
|
10426
10827
|
process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
|
|
10427
10828
|
`);
|
|
10428
|
-
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) =>
|
|
10829
|
+
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) => path33.join(opts.bundleDir, f)).filter((f) => existsSync26(f)).map((f) => readFileSync22(f, "utf8")).join("\n")));
|
|
10429
10830
|
if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
|
|
10430
10831
|
process.stdout.write(`fonts: scored with Tendril-cache faces \u2014 a consuming app must provision the same families (the bundle ships fonts.css when faces are shippable; sha-pinned list in component.json requiredFonts)
|
|
10431
10832
|
`);
|
|
10432
10833
|
}
|
|
10433
10834
|
for (const g of weightGaps) {
|
|
10434
|
-
process.stdout.write(
|
|
10435
|
-
`);
|
|
10835
|
+
process.stdout.write(
|
|
10836
|
+
`fonts (weight gap): "${g.family}" declares ${g.declared.join(", ")} \u2014 cache provides ${g.provided.join(", ")}; nearest-weight rendering may shift stroke density (advisory, never a gate). Fix: ${tendrilCommand(`fonts resolve --set ${task.set}`)}
|
|
10837
|
+
`
|
|
10838
|
+
);
|
|
10436
10839
|
}
|
|
10437
10840
|
process.stdout.write(`evidence: ${evidenceDir} (render/ref/diff per config)
|
|
10438
10841
|
`);
|
|
10439
|
-
process.stdout.write(`eye check:
|
|
10842
|
+
process.stdout.write(`eye check: ${report.eyeCheck.command} \u2014 magnified recorded-vs-rendered crops of every small node; scores cannot see shape (re-run after every verify)
|
|
10440
10843
|
`);
|
|
10441
10844
|
});
|
|
10442
10845
|
if (opts.bar === "cert" && substitutedFamilies.length > 0) {
|
|
@@ -10476,9 +10879,9 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
10476
10879
|
process.exitCode = ExitCode.VerificationFailed;
|
|
10477
10880
|
}
|
|
10478
10881
|
}
|
|
10479
|
-
var BARS2, NO_ROLE_MANIFEST, ROLES_NOT_RESOLVED, UNSTAMPED_ROLES;
|
|
10882
|
+
var BARS2, NO_INTERACTIVE_POSES, NO_ROLE_MANIFEST, ROLES_NOT_RESOLVED, UNSTAMPED_ROLES, NO_OVERLAY_DECLARED;
|
|
10480
10883
|
var init_verify = __esm({
|
|
10481
|
-
"
|
|
10884
|
+
"packages/cli/src/commands/verify.ts"() {
|
|
10482
10885
|
"use strict";
|
|
10483
10886
|
init_src3();
|
|
10484
10887
|
init_src6();
|
|
@@ -10487,11 +10890,13 @@ var init_verify = __esm({
|
|
|
10487
10890
|
init_src4();
|
|
10488
10891
|
init_environment();
|
|
10489
10892
|
init_font_guidance();
|
|
10893
|
+
init_invocation();
|
|
10490
10894
|
init_output();
|
|
10491
10895
|
BARS2 = {
|
|
10492
10896
|
pass: { sim: 0.95, ink: 0.95 },
|
|
10493
10897
|
cert: { sim: 0.97, ink: 0.95 }
|
|
10494
10898
|
};
|
|
10899
|
+
NO_INTERACTIVE_POSES = "no interactive poses recorded \u2014 nothing about focus, typing, or keyboard operation was measured; a component that is not a control passes this field.";
|
|
10495
10900
|
NO_ROLE_MANIFEST = Object.freeze({
|
|
10496
10901
|
short: "no role manifest",
|
|
10497
10902
|
unavailable: "the recording set declares no role manifest \u2014 composition (structural + crop) was NEVER CHECKED; nothing here says the main renders the SHIPPED part modules rather than a pixel-identical re-implementation"
|
|
@@ -10501,45 +10906,46 @@ var init_verify = __esm({
|
|
|
10501
10906
|
unavailable: "this run never resolved a role manifest \u2014 the legacy --task adapter and pre-manifest reference sets skip the set's roles block entirely, so composition (structural + crop) was NEVER CHECKED and the set was never asked what it declares; nothing here says the main renders the SHIPPED part modules rather than a pixel-identical re-implementation"
|
|
10502
10907
|
});
|
|
10503
10908
|
UNSTAMPED_ROLES = "unstamped";
|
|
10909
|
+
NO_OVERLAY_DECLARED = "no overlay declared";
|
|
10504
10910
|
}
|
|
10505
10911
|
});
|
|
10506
10912
|
|
|
10507
|
-
//
|
|
10913
|
+
// packages/cli/src/commands/engine.ts
|
|
10508
10914
|
var engine_exports = {};
|
|
10509
10915
|
__export(engine_exports, {
|
|
10510
10916
|
runEngineBrief: () => runEngineBrief,
|
|
10511
10917
|
runEngineScore: () => runEngineScore
|
|
10512
10918
|
});
|
|
10513
|
-
import { appendFileSync, existsSync as
|
|
10514
|
-
import
|
|
10919
|
+
import { appendFileSync, existsSync as existsSync27, mkdirSync as mkdirSync8, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "node:fs";
|
|
10920
|
+
import path34 from "node:path";
|
|
10515
10921
|
function resolveEngineTask(opts, callerCwd) {
|
|
10516
|
-
const asPath =
|
|
10517
|
-
const isSet =
|
|
10922
|
+
const asPath = path34.resolve(callerCwd, opts.taskOrSet);
|
|
10923
|
+
const isSet = existsSync27(path34.join(asPath, "recording-set.json"));
|
|
10518
10924
|
const registry = TASKS[opts.taskOrSet];
|
|
10519
|
-
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, disclosures: [], interactionEvidence:
|
|
10925
|
+
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: void 0 };
|
|
10520
10926
|
if (isSet) {
|
|
10521
10927
|
try {
|
|
10522
10928
|
const authored = authorTaskFromSet(asPath);
|
|
10523
10929
|
for (const d of authored.disclosures) warn(opts, d);
|
|
10524
|
-
return { task: authored.task, name:
|
|
10930
|
+
return { task: authored.task, name: path34.basename(asPath), ref: asPath, disclosures: authored.disclosures, interactionEvidence: authored.api.interactionEvidence, apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates } };
|
|
10525
10931
|
} catch (err) {
|
|
10526
10932
|
fail(opts, ExitCode.InputValidation, {
|
|
10527
10933
|
error: `cannot author a task from ${asPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
10528
10934
|
code: "AUTHORING_FAILED",
|
|
10529
|
-
remediation:
|
|
10935
|
+
remediation: `Complete the recording set (${tendrilCommand(`record status --set ${asPath}`)}) or use a reference task name.`
|
|
10530
10936
|
});
|
|
10531
10937
|
}
|
|
10532
10938
|
}
|
|
10533
10939
|
fail(opts, ExitCode.InputValidation, {
|
|
10534
10940
|
error: `"${opts.taskOrSet}" is neither a reference task nor a recorded set`,
|
|
10535
10941
|
code: "UNKNOWN_RECORDED_INPUT",
|
|
10536
|
-
remediation: `Pick a task (${Object.keys(TASKS).join(", ")}) or point at a set recorded with
|
|
10942
|
+
remediation: `Pick a task (${Object.keys(TASKS).join(", ")}) or point at a set recorded with \`${tendrilCommand("record plan")}\`.`
|
|
10537
10943
|
});
|
|
10538
10944
|
}
|
|
10539
10945
|
function runEngineBrief(opts) {
|
|
10540
10946
|
requireEntitlement(opts);
|
|
10541
10947
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
10542
|
-
const { task, name, disclosures } = resolveEngineTask(opts, callerCwd);
|
|
10948
|
+
const { task, name, ref, disclosures } = resolveEngineTask(opts, callerCwd);
|
|
10543
10949
|
const bar = BARS3[opts.bar];
|
|
10544
10950
|
const disclosureBlock = disclosures.length > 0 ? `
|
|
10545
10951
|
|
|
@@ -10548,8 +10954,8 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
10548
10954
|
const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock;
|
|
10549
10955
|
const segments = buildSegments(task, "files");
|
|
10550
10956
|
let notRecorded;
|
|
10551
|
-
const manifestPath2 =
|
|
10552
|
-
if (
|
|
10957
|
+
const manifestPath2 = path34.join(task.set, "recording-set.json");
|
|
10958
|
+
if (existsSync27(manifestPath2)) {
|
|
10553
10959
|
notRecorded = JSON.parse(readFileSync23(manifestPath2, "utf8")).notRecorded;
|
|
10554
10960
|
}
|
|
10555
10961
|
const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
|
|
@@ -10558,7 +10964,7 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
10558
10964
|
DO NOT INVENT PIXELS FOR THESE POSES. Implement them ONLY as the composition of recorded per-axis truth (the custom-property composition rule: one paint axis sets variables, the other consumes) and list every such pose in your report as UNRECORDED-COMPOSED. Recording them is the real fix: re-plan the set (full matrix is the default) and record the missing poses.
|
|
10559
10965
|
${notRecorded}` : "";
|
|
10560
10966
|
let fontProvisioning;
|
|
10561
|
-
if (
|
|
10967
|
+
if (existsSync27(manifestPath2)) {
|
|
10562
10968
|
const missingFams = unprovisionedFamilies(task.set);
|
|
10563
10969
|
const unprovided = unprovisionedFaces(task.set);
|
|
10564
10970
|
const weightOnly = missingFams.length === 0;
|
|
@@ -10566,16 +10972,17 @@ ${notRecorded}` : "";
|
|
|
10566
10972
|
const names = unprovided.map((f) => `'${f}'`).join(", ");
|
|
10567
10973
|
const PROPRIETARY = /^(sf pro|sf compact|sf mono|new york|pingfang|segoe ui|helvetica neue|proxima nova|avenir)/i;
|
|
10568
10974
|
const allProprietary = unprovided.every((f) => PROPRIETARY.test(f.trim()));
|
|
10975
|
+
const resolveCommand = tendrilCommand(`fonts resolve --set ${task.set}`);
|
|
10569
10976
|
fontProvisioning = {
|
|
10570
10977
|
unprovided,
|
|
10571
10978
|
question: {
|
|
10572
10979
|
prompt: weightOnly ? `This design implies font ${unprovided.length === 1 ? "weight" : "weights"} the local kit lacks: ${names} (the ${missingFams.length === 0 && unprovided.length === 1 ? "family itself is" : "families themselves are"} provisioned). How should it be handled?` : `This design's text uses ${names}, which is not in the local font kit. How should it be handled?`,
|
|
10573
10980
|
options: [
|
|
10574
|
-
allProprietary && !weightOnly ? `(Recommended) ${names} ${unprovided.length === 1 ? "is a proprietary face" : "are proprietary faces"} \u2014 Google Fonts cannot serve ${unprovided.length === 1 ? "it" : "them"}, so skip \`fonts resolve\`: run
|
|
10981
|
+
allProprietary && !weightOnly ? `(Recommended) ${names} ${unprovided.length === 1 ? "is a proprietary face" : "are proprietary faces"} \u2014 Google Fonts cannot serve ${unprovided.length === 1 ? "it" : "them"}, so skip \`fonts resolve\`: run \`${tendrilCommand('fonts add "<Family>" <weight> <file>')}\` with the .woff2/.ttf/.otf you hold a licence for (the file never leaves this machine), then re-run this brief \u2014 exact text fidelity.` : `(Recommended) Run \`${resolveCommand}\` \u2014 open-source families are fetched automatically. Any face that fails there is one you license privately: run \`${tendrilCommand('fonts add "<Family>" <weight> <file>')}\` with the .woff2/.ttf/.otf you hold (the file never leaves this machine). Then re-run this brief \u2014 exact text fidelity.`,
|
|
10575
10982
|
weightOnly ? "Continue with nearest-weight rendering: the family is provisioned, certification remains possible, and the gap rides the report as an advisory (fontWeightGaps); resolving removes it." : 'Continue with a substitute face: generation proceeds in a provided family, no config can score "certified" under it (cert-bar runs exit fonts-unproven after their report), the substitution is disclosed, and small text differences are expected and not fixable from CSS.'
|
|
10576
10983
|
]
|
|
10577
10984
|
},
|
|
10578
|
-
nonInteractive: weightOnly ? `Run
|
|
10985
|
+
nonInteractive: weightOnly ? `Run \`${resolveCommand}\` if quick, or continue \u2014 the weight gap is an advisory, never a gate.` : opts.bar === "cert" ? `Resolve the families first (\`${resolveCommand}\`) \u2014 this loop targets the cert bar, and scoring exits fonts-unproven under a substitute, so continuing without resolving is a dead end.` : "Continue with the substitute and state the substitution in your report."
|
|
10579
10986
|
};
|
|
10580
10987
|
}
|
|
10581
10988
|
}
|
|
@@ -10583,8 +10990,8 @@ ${notRecorded}` : "";
|
|
|
10583
10990
|
|
|
10584
10991
|
=== TASK PAYLOAD (recorded truth, verbatim) ===
|
|
10585
10992
|
${segments}`;
|
|
10586
|
-
const payloadFile =
|
|
10587
|
-
mkdirSync8(
|
|
10993
|
+
const payloadFile = path34.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
|
|
10994
|
+
mkdirSync8(path34.dirname(payloadFile), { recursive: true });
|
|
10588
10995
|
writeFileSync12(payloadFile, payload);
|
|
10589
10996
|
emitData(
|
|
10590
10997
|
opts,
|
|
@@ -10613,7 +11020,7 @@ ${segments}`;
|
|
|
10613
11020
|
"Settle the proposer model (see modelSelection \u2014 ask the user when one is present), then:",
|
|
10614
11021
|
`Read ${payloadFile} completely \u2014 it is the system brief plus every recorded config's emission, box, and assets.`,
|
|
10615
11022
|
`Write the complete bundle files (${task.entry}, styles.css, optional tokens.css) into a candidate directory.`,
|
|
10616
|
-
`Run
|
|
11023
|
+
`Run \`${tendrilCommand(`engine score ${ref} <candidateDir> --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"} --json`)}\` \u2014 the CLI is the sole judge, and it refuses to score an undeclared model.`,
|
|
10617
11024
|
"Apply the returned feedback and re-score. Stop when all checks pass or two consecutive scores fail to improve.",
|
|
10618
11025
|
"Never edit recording sets; never claim scores yourself \u2014 only the score command's output counts."
|
|
10619
11026
|
]
|
|
@@ -10626,18 +11033,23 @@ ${segments}`;
|
|
|
10626
11033
|
}
|
|
10627
11034
|
);
|
|
10628
11035
|
}
|
|
11036
|
+
function appendScoreHistory(candidateDir, entry) {
|
|
11037
|
+
appendFileSync(path34.join(candidateDir, "score-history.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
|
|
11038
|
+
`);
|
|
11039
|
+
}
|
|
10629
11040
|
async function runEngineScore(opts) {
|
|
10630
11041
|
requireEntitlement(opts);
|
|
10631
11042
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
10632
|
-
const candidateDir =
|
|
11043
|
+
const candidateDir = path34.resolve(callerCwd, opts.candidateDir);
|
|
10633
11044
|
const { task, name, apiPin, interactionEvidence } = resolveEngineTask(opts, callerCwd);
|
|
10634
|
-
if (!
|
|
11045
|
+
if (!existsSync27(candidateDir)) {
|
|
10635
11046
|
fail(opts, ExitCode.InputValidation, {
|
|
10636
11047
|
error: `candidate directory not found: ${candidateDir}`,
|
|
10637
11048
|
code: "candidate-missing",
|
|
10638
11049
|
remediation: `Write ${task.entry} and styles.css into a directory and pass it.`
|
|
10639
11050
|
});
|
|
10640
11051
|
}
|
|
11052
|
+
appendScoreHistory(candidateDir, { event: "round-start", bar: opts.bar });
|
|
10641
11053
|
if (!fontsResolved()) {
|
|
10642
11054
|
fail(opts, ExitCode.FontsUnproven, {
|
|
10643
11055
|
error: "resolved font cache absent \u2014 scoring under system font substitution is refused",
|
|
@@ -10655,10 +11067,10 @@ async function runEngineScore(opts) {
|
|
|
10655
11067
|
for (const g of missingWeights(task.set)) {
|
|
10656
11068
|
warn(opts, `font weights (advisory): the recording implies weights ${g.declared.join(", ")} for "${g.family}" \u2014 cache provides ${g.provided.join(", ")}; nearest-weight rendering may shift stroke density (certification stays family-gated; implied weights can leak across families)`);
|
|
10657
11069
|
}
|
|
10658
|
-
if (opts.rebind !== true &&
|
|
11070
|
+
if (opts.rebind !== true && existsSync27(path34.join(candidateDir, "component.json"))) {
|
|
10659
11071
|
const prior = (() => {
|
|
10660
11072
|
try {
|
|
10661
|
-
const read = readBundleManifest(readFileSync23(
|
|
11073
|
+
const read = readBundleManifest(readFileSync23(path34.join(candidateDir, "component.json"), "utf8"));
|
|
10662
11074
|
return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
|
|
10663
11075
|
} catch {
|
|
10664
11076
|
return { unreadable: true };
|
|
@@ -10680,7 +11092,7 @@ async function runEngineScore(opts) {
|
|
|
10680
11092
|
}
|
|
10681
11093
|
}
|
|
10682
11094
|
const bar = BARS3[opts.bar];
|
|
10683
|
-
const evidenceDir =
|
|
11095
|
+
const evidenceDir = path34.join(candidateDir, "verify-evidence");
|
|
10684
11096
|
const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
10685
11097
|
const parity = await checkHoverParity(task, candidateDir);
|
|
10686
11098
|
const behaviors = [...await checkBehaviors(task, candidateDir), ...parity];
|
|
@@ -10723,11 +11135,10 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
10723
11135
|
environment: environmentStamp(taskFontFamilies(task.set)),
|
|
10724
11136
|
substitutedFamilies
|
|
10725
11137
|
});
|
|
10726
|
-
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
10730
|
-
);
|
|
11138
|
+
appendScoreHistory(candidateDir, { event: "round-scored", bar: opts.bar, pass: obj[0], total, certified: certifiedReps.length, floor: obj[1], mean: obj[2] });
|
|
11139
|
+
const coverage = interactionCoverage(behaviors);
|
|
11140
|
+
const operability = operabilityReport({ behaviors, interactionEvidence });
|
|
11141
|
+
const evidenceUnverified = (interactionEvidence?.length ?? 0) > 0 && coverage.interactionChecks === 0;
|
|
10731
11142
|
emitData(
|
|
10732
11143
|
opts,
|
|
10733
11144
|
{
|
|
@@ -10741,7 +11152,14 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
10741
11152
|
// The loop's oracle must say what verify says: without this an
|
|
10742
11153
|
// MCP-driven agent literally cannot tell a hollow verdict (all
|
|
10743
11154
|
// behaviour passes are prelude hygiene) from a verified one.
|
|
10744
|
-
|
|
11155
|
+
//
|
|
11156
|
+
// `operability` alone is ONE WORD covering four different states,
|
|
11157
|
+
// and this is the channel where run 15's miss happened: the agent
|
|
11158
|
+
// driving the loop reads this JSON and nothing else. It gets the
|
|
11159
|
+
// same sentence verify's report carries, from the same function —
|
|
11160
|
+
// fixing verify's channel and not this one would have left the
|
|
11161
|
+
// reader who actually acts on it exactly where they were.
|
|
11162
|
+
coverage: { ...coverage, operabilityCheck: operability },
|
|
10745
11163
|
parityCoverage,
|
|
10746
11164
|
evidenceDir,
|
|
10747
11165
|
bundleManifest: emitted.written[0],
|
|
@@ -10752,7 +11170,7 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
10752
11170
|
// Run 11: generators read allPass:true and reported success on
|
|
10753
11171
|
// bundles verify then FAILED on the interaction-evidence gate —
|
|
10754
11172
|
// the oracle must say what verify will say, including this.
|
|
10755
|
-
...
|
|
11173
|
+
...evidenceUnverified ? { interactionEvidenceUnverified: true, verifyWillFail: "interaction-evidence \u2014 the recording proves interactive poses none of the authored behaviors cover; not fixable from component code; REPORT it, do not iterate on it" } : {},
|
|
10756
11174
|
certification: { certified: certifiedReps.length, total: scores.length, bar: certBar, note: "tierOf on exact values + parity and absent-ink demotion; verify's composition checks can demote further" }
|
|
10757
11175
|
},
|
|
10758
11176
|
() => {
|
|
@@ -10763,12 +11181,12 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
10763
11181
|
process.stdout.write(`
|
|
10764
11182
|
${obj[0]}/${total} \xB7 certified ${certifiedReps.length}/${total} \xB7 floor=${obj[1].toFixed(3)} \xB7 mean=${obj[2].toFixed(3)} \xB7 evidence: ${evidenceDir}
|
|
10765
11183
|
`);
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
process.stdout.write(`INSTRUMENT GAP \u2014 the recording proves interactive poses (${interactionEvidence.join(", ")}) and zero interaction behaviors were authored; verify WILL fail this bundle (interaction-evidence). Not fixable from component code \u2014 report it, do not iterate on it.
|
|
11184
|
+
if (evidenceUnverified) {
|
|
11185
|
+
process.stdout.write(`INSTRUMENT GAP \u2014 the recording proves interactive poses (${(interactionEvidence ?? []).join(", ")}) and zero interaction behaviors were authored; verify WILL fail this bundle (interaction-evidence). Not fixable from component code \u2014 report it, do not iterate on it.
|
|
10769
11186
|
`);
|
|
10770
|
-
} else
|
|
10771
|
-
|
|
11187
|
+
} else {
|
|
11188
|
+
const line = operabilityLine(operability);
|
|
11189
|
+
if (line !== void 0) process.stdout.write(`${line}
|
|
10772
11190
|
`);
|
|
10773
11191
|
}
|
|
10774
11192
|
}
|
|
@@ -10793,7 +11211,7 @@ ${obj[0]}/${total} \xB7 certified ${certifiedReps.length}/${total} \xB7 floor=${
|
|
|
10793
11211
|
}
|
|
10794
11212
|
var BARS3;
|
|
10795
11213
|
var init_engine2 = __esm({
|
|
10796
|
-
"
|
|
11214
|
+
"packages/cli/src/commands/engine.ts"() {
|
|
10797
11215
|
"use strict";
|
|
10798
11216
|
init_src3();
|
|
10799
11217
|
init_src7();
|
|
@@ -10802,6 +11220,7 @@ var init_engine2 = __esm({
|
|
|
10802
11220
|
init_font_guidance();
|
|
10803
11221
|
init_src4();
|
|
10804
11222
|
init_output();
|
|
11223
|
+
init_invocation();
|
|
10805
11224
|
init_entitlement();
|
|
10806
11225
|
init_verify();
|
|
10807
11226
|
init_src4();
|
|
@@ -10812,16 +11231,16 @@ var init_engine2 = __esm({
|
|
|
10812
11231
|
}
|
|
10813
11232
|
});
|
|
10814
11233
|
|
|
10815
|
-
//
|
|
11234
|
+
// packages/cli/src/commands/codeconnect.ts
|
|
10816
11235
|
var codeconnect_exports = {};
|
|
10817
11236
|
__export(codeconnect_exports, {
|
|
10818
11237
|
runCodeConnect: () => runCodeConnect
|
|
10819
11238
|
});
|
|
10820
|
-
import { existsSync as
|
|
10821
|
-
import
|
|
11239
|
+
import { existsSync as existsSync28, readFileSync as readFileSync24, writeFileSync as writeFileSync13 } from "node:fs";
|
|
11240
|
+
import path35 from "node:path";
|
|
10822
11241
|
function runCodeConnect(opts) {
|
|
10823
11242
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
10824
|
-
const bundleDir =
|
|
11243
|
+
const bundleDir = path35.resolve(callerCwd, opts.bundleDir);
|
|
10825
11244
|
let url;
|
|
10826
11245
|
try {
|
|
10827
11246
|
url = new URL(opts.figmaUrl);
|
|
@@ -10837,7 +11256,7 @@ function runCodeConnect(opts) {
|
|
|
10837
11256
|
}
|
|
10838
11257
|
let manifest;
|
|
10839
11258
|
try {
|
|
10840
|
-
const read = readBundleManifest(readFileSync24(
|
|
11259
|
+
const read = readBundleManifest(readFileSync24(path35.join(bundleDir, "component.json"), "utf8"));
|
|
10841
11260
|
if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
|
|
10842
11261
|
manifest = read.manifest;
|
|
10843
11262
|
} catch (err) {
|
|
@@ -10847,8 +11266,8 @@ function runCodeConnect(opts) {
|
|
|
10847
11266
|
remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
|
|
10848
11267
|
});
|
|
10849
11268
|
}
|
|
10850
|
-
const setDir =
|
|
10851
|
-
if (!
|
|
11269
|
+
const setDir = path35.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
|
|
11270
|
+
if (!existsSync28(path35.join(setDir, "recording-set.json"))) {
|
|
10852
11271
|
fail(opts, ExitCode.InputValidation, {
|
|
10853
11272
|
error: `recording set not found at ${setDir}`,
|
|
10854
11273
|
code: "codeconnect-no-set",
|
|
@@ -10862,15 +11281,15 @@ function runCodeConnect(opts) {
|
|
|
10862
11281
|
fail(opts, ExitCode.InputValidation, {
|
|
10863
11282
|
error: `cannot author the API from ${setDir}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
10864
11283
|
code: "codeconnect-authoring-failed",
|
|
10865
|
-
remediation:
|
|
11284
|
+
remediation: `The set must be a complete protocol recording (${tendrilCommand(`record status --set ${setDir}`)}).`
|
|
10866
11285
|
});
|
|
10867
11286
|
}
|
|
10868
11287
|
const api = authored.api;
|
|
10869
11288
|
const component = api.component;
|
|
10870
11289
|
const recManifest = loadManifest(setDir);
|
|
10871
11290
|
const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
|
|
10872
|
-
const meta =
|
|
10873
|
-
if (!
|
|
11291
|
+
const meta = path35.join(setDir, r.slug, "get_metadata.json");
|
|
11292
|
+
if (!existsSync28(meta)) return void 0;
|
|
10874
11293
|
try {
|
|
10875
11294
|
return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(readFileSync24(meta, "utf8"))))?.[1];
|
|
10876
11295
|
} catch {
|
|
@@ -10889,7 +11308,7 @@ function runCodeConnect(opts) {
|
|
|
10889
11308
|
fail(opts, ExitCode.InputValidation, {
|
|
10890
11309
|
error: "the recording set's content no longer matches the hash this bundle was scored against \u2014 its verification claims describe a different recording",
|
|
10891
11310
|
code: "codeconnect-set-drift",
|
|
10892
|
-
remediation:
|
|
11311
|
+
remediation: `Re-run \`${tendrilCommand(`verify ${bundleDir} --set ${setDir}`)}\` (or engine score) against the current set, then re-emit.`
|
|
10893
11312
|
});
|
|
10894
11313
|
}
|
|
10895
11314
|
const boolProps = api.props.filter((p) => p.kind === "boolean");
|
|
@@ -10930,7 +11349,7 @@ function runCodeConnect(opts) {
|
|
|
10930
11349
|
axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
|
|
10931
11350
|
fragmentVars.push(varName);
|
|
10932
11351
|
}
|
|
10933
|
-
const entryRel =
|
|
11352
|
+
const entryRel = path35.relative(callerCwd, path35.join(bundleDir, manifest.entry));
|
|
10934
11353
|
const trust = manifest.trustStatement.split("\n")[0] ?? "";
|
|
10935
11354
|
const lines = [
|
|
10936
11355
|
`// url=${opts.figmaUrl}`,
|
|
@@ -10951,7 +11370,7 @@ function runCodeConnect(opts) {
|
|
|
10951
11370
|
`}`,
|
|
10952
11371
|
``
|
|
10953
11372
|
].join("\n");
|
|
10954
|
-
const outFile =
|
|
11373
|
+
const outFile = path35.resolve(callerCwd, opts.out ?? path35.join(bundleDir, `${component}.figma.ts`));
|
|
10955
11374
|
writeFileSync13(outFile, lines);
|
|
10956
11375
|
emitData(
|
|
10957
11376
|
opts,
|
|
@@ -10976,48 +11395,49 @@ function runCodeConnect(opts) {
|
|
|
10976
11395
|
}
|
|
10977
11396
|
var kebab4, q;
|
|
10978
11397
|
var init_codeconnect = __esm({
|
|
10979
|
-
"
|
|
11398
|
+
"packages/cli/src/commands/codeconnect.ts"() {
|
|
10980
11399
|
"use strict";
|
|
10981
11400
|
init_src3();
|
|
10982
11401
|
init_src7();
|
|
10983
11402
|
init_src();
|
|
10984
11403
|
init_src6();
|
|
11404
|
+
init_invocation();
|
|
10985
11405
|
init_output();
|
|
10986
11406
|
kebab4 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
10987
11407
|
q = (s) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}'`;
|
|
10988
11408
|
}
|
|
10989
11409
|
});
|
|
10990
11410
|
|
|
10991
|
-
//
|
|
11411
|
+
// packages/mcp/src/server.ts
|
|
10992
11412
|
import { createHash as createHash5 } from "node:crypto";
|
|
10993
|
-
import { existsSync as
|
|
11413
|
+
import { existsSync as existsSync29, mkdtempSync as mkdtempSync3, readFileSync as readFileSync25, readdirSync as readdirSync8, writeFileSync as writeFileSync14 } from "node:fs";
|
|
10994
11414
|
import os6 from "node:os";
|
|
10995
|
-
import
|
|
10996
|
-
import { fileURLToPath as
|
|
11415
|
+
import path36 from "node:path";
|
|
11416
|
+
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
10997
11417
|
import { z as z12 } from "zod";
|
|
10998
11418
|
function sourceHash() {
|
|
10999
|
-
const dir =
|
|
11419
|
+
const dir = path36.dirname(fileURLToPath6(import.meta.url));
|
|
11000
11420
|
const h = createHash5("sha256");
|
|
11001
11421
|
for (const f of readdirSync8(dir).filter((n) => n.endsWith(".ts")).sort()) {
|
|
11002
11422
|
h.update(f);
|
|
11003
|
-
h.update(readFileSync25(
|
|
11423
|
+
h.update(readFileSync25(path36.join(dir, f)));
|
|
11004
11424
|
}
|
|
11005
11425
|
return h.digest("hex").slice(0, 16);
|
|
11006
11426
|
}
|
|
11007
11427
|
var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
|
|
11008
11428
|
var init_server = __esm({
|
|
11009
|
-
"
|
|
11429
|
+
"packages/mcp/src/server.ts"() {
|
|
11010
11430
|
"use strict";
|
|
11011
|
-
REPO_ROOT3 =
|
|
11012
|
-
CLI_BIN =
|
|
11013
|
-
BUNDLED_CLI =
|
|
11014
|
-
CLI_SPAWN =
|
|
11431
|
+
REPO_ROOT3 = path36.resolve(path36.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
|
|
11432
|
+
CLI_BIN = path36.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
|
|
11433
|
+
BUNDLED_CLI = path36.join(path36.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
|
|
11434
|
+
CLI_SPAWN = existsSync29(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
|
|
11015
11435
|
str = (d) => z12.string().describe(d);
|
|
11016
11436
|
optStr = (d) => z12.string().optional().describe(d);
|
|
11017
11437
|
TOOLS = [
|
|
11018
11438
|
{
|
|
11019
11439
|
name: "tendril_record_plan",
|
|
11020
|
-
description: "ENTRY POINT for implementing/building a React component from a Figma design or figma.com URL \u2014 start the Tendril pipeline here (after loading the tendril skill, if installed). Plans a recording session: computes the rep queue (anchor + one-factor + conflict crosses) from the verbatim get_metadata response (pass its blocks via metadataParts \u2014 no file to write) and persists the set manifest. Resumes if the set already exists. The output may carry USER QUESTIONS \u2014 defaultsToConfirm (which pose is the component's default) or a multiple-component-sets error (which set to record): render them to a present user and apply the answers via `defaults` / `componentSet`; non-interactive runs follow each question's stated fallback.
|
|
11440
|
+
description: "ENTRY POINT for implementing/building a React component from a Figma design or figma.com URL \u2014 start the Tendril pipeline here (after loading the tendril skill, if installed). Plans a recording session: computes the rep queue (anchor + one-factor + conflict crosses) from the verbatim get_metadata response (pass its blocks via metadataParts \u2014 no file to write) and persists the set manifest. Resumes if the set already exists. The output may carry USER QUESTIONS \u2014 defaultsToConfirm (which pose is the component's default) or a multiple-component-sets error (which set to record): render them to a present user and apply the answers via `defaults` / `componentSet`; non-interactive runs follow each question's stated fallback. It may also carry `interactionStatesToConfirm` \u2014 the recording holds no hover/focus/pressed state, so nothing shows how the component behaves when someone uses it: say its `statement` and `designFix` in that SAME one message (the fix is a Figma variant, not code). It is a disclosure, not a gate \u2014 no answer is required and recording proceeds regardless. The output also carries `feasibilityCheck`: the call arithmetic for this queue plus the free `whoami` check that turns it into a verdict \u2014 complete that handshake BEFORE the first recording call, and surface the verdict to the user when the set does not fit their daily allowance.",
|
|
11021
11441
|
schema: z12.object({
|
|
11022
11442
|
setDir: str("recording set directory to create/resume"),
|
|
11023
11443
|
component: str("component/system name"),
|
|
@@ -11041,7 +11461,7 @@ var init_server = __esm({
|
|
|
11041
11461
|
const single = i["metadata"];
|
|
11042
11462
|
const parts = i["metadataParts"];
|
|
11043
11463
|
if (single !== void 0 || parts !== void 0) {
|
|
11044
|
-
const tmp =
|
|
11464
|
+
const tmp = path36.join(mkdtempSync3(path36.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
11045
11465
|
if (single !== void 0) {
|
|
11046
11466
|
writeFileSync14(tmp, single);
|
|
11047
11467
|
argvOut.push("--metadata-raw-file", tmp);
|
|
@@ -11112,7 +11532,7 @@ var init_server = __esm({
|
|
|
11112
11532
|
const bridge = (label, single, parts) => {
|
|
11113
11533
|
if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
|
|
11114
11534
|
if (single === void 0 && parts === void 0) return;
|
|
11115
|
-
const tmp =
|
|
11535
|
+
const tmp = path36.join(mkdtempSync3(path36.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
11116
11536
|
if (single !== void 0) {
|
|
11117
11537
|
writeFileSync14(tmp, single);
|
|
11118
11538
|
argvOut.push(`--${label}-file`, tmp);
|
|
@@ -11155,7 +11575,7 @@ var init_server = __esm({
|
|
|
11155
11575
|
const file = i["file"];
|
|
11156
11576
|
if ([text, texts, file].filter((x) => x !== void 0).length !== 1) throw new Error("pass exactly one of `text` (single-block response), `texts` (multi-block response), or `file` (a saved envelope)");
|
|
11157
11577
|
if (file !== void 0) return [...base, "--file", file];
|
|
11158
|
-
const tmp =
|
|
11578
|
+
const tmp = path36.join(mkdtempSync3(path36.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
11159
11579
|
if (text !== void 0) {
|
|
11160
11580
|
writeFileSync14(tmp, text);
|
|
11161
11581
|
return [...base, "--file", tmp, "--raw"];
|
|
@@ -11302,7 +11722,7 @@ var init_server = __esm({
|
|
|
11302
11722
|
}
|
|
11303
11723
|
});
|
|
11304
11724
|
|
|
11305
|
-
//
|
|
11725
|
+
// packages/cli/src/commands/permissions.ts
|
|
11306
11726
|
var permissions_exports = {};
|
|
11307
11727
|
__export(permissions_exports, {
|
|
11308
11728
|
PERMISSIONS_DESCRIPTION: () => PERMISSIONS_DESCRIPTION,
|
|
@@ -11310,12 +11730,12 @@ __export(permissions_exports, {
|
|
|
11310
11730
|
mergeAllowlist: () => mergeAllowlist,
|
|
11311
11731
|
runPermissions: () => runPermissions
|
|
11312
11732
|
});
|
|
11313
|
-
import { existsSync as
|
|
11733
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync9, readFileSync as readFileSync26, writeFileSync as writeFileSync15 } from "node:fs";
|
|
11314
11734
|
import os7 from "node:os";
|
|
11315
|
-
import
|
|
11735
|
+
import path37 from "node:path";
|
|
11316
11736
|
function mergeAllowlist(file, entries) {
|
|
11317
11737
|
let settings = {};
|
|
11318
|
-
if (
|
|
11738
|
+
if (existsSync30(file) && readFileSync26(file, "utf8").trim() !== "") {
|
|
11319
11739
|
settings = JSON.parse(readFileSync26(file, "utf8"));
|
|
11320
11740
|
if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
|
|
11321
11741
|
}
|
|
@@ -11328,7 +11748,7 @@ function mergeAllowlist(file, entries) {
|
|
|
11328
11748
|
const alreadyPresent = entries.filter((e) => present.has(e));
|
|
11329
11749
|
if (added.length > 0) {
|
|
11330
11750
|
allow.push(...added);
|
|
11331
|
-
mkdirSync9(
|
|
11751
|
+
mkdirSync9(path37.dirname(file), { recursive: true });
|
|
11332
11752
|
writeFileSync15(file, `${JSON.stringify(settings, null, 2)}
|
|
11333
11753
|
`);
|
|
11334
11754
|
}
|
|
@@ -11364,7 +11784,7 @@ async function runPermissions(flags) {
|
|
|
11364
11784
|
const result = await buildPermissions({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} });
|
|
11365
11785
|
if (flags.write) {
|
|
11366
11786
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
11367
|
-
const file = flags.user ?
|
|
11787
|
+
const file = flags.user ? path37.join(os7.homedir(), ".claude", "settings.json") : path37.join(base, ".claude", "settings.local.json");
|
|
11368
11788
|
if (flags.dryRun) {
|
|
11369
11789
|
emitData(flags, { file, wouldAdd: result.toolEntries }, () => {
|
|
11370
11790
|
process.stdout.write(`dry-run: would merge ${result.toolEntries.length} per-tool entries into ${file}
|
|
@@ -11397,7 +11817,7 @@ async function runPermissions(flags) {
|
|
|
11397
11817
|
`Claude Code allowlist for the Tendril pipeline.
|
|
11398
11818
|
One command installs it (project-local, idempotent):
|
|
11399
11819
|
|
|
11400
|
-
|
|
11820
|
+
${tendrilCommand("permissions --claude --write")}
|
|
11401
11821
|
|
|
11402
11822
|
Or paste into .claude/settings.json under permissions.allow:
|
|
11403
11823
|
|
|
@@ -11416,12 +11836,13 @@ ${result.directConfigNote}
|
|
|
11416
11836
|
}
|
|
11417
11837
|
var FIGMA_TOOL_FALLBACK, PERMISSIONS_DESCRIPTION, TENDRIL_PLUGIN_PREFIX, FIGMA_PLUGIN_PREFIX;
|
|
11418
11838
|
var init_permissions = __esm({
|
|
11419
|
-
"
|
|
11839
|
+
"packages/cli/src/commands/permissions.ts"() {
|
|
11420
11840
|
"use strict";
|
|
11421
11841
|
init_src3();
|
|
11422
11842
|
init_src();
|
|
11423
11843
|
init_server();
|
|
11424
11844
|
init_describe();
|
|
11845
|
+
init_invocation();
|
|
11425
11846
|
init_output();
|
|
11426
11847
|
init_doctor();
|
|
11427
11848
|
FIGMA_TOOL_FALLBACK = ["get_metadata", "get_design_context", "get_screenshot", "get_variable_defs", "get_motion_context", "get_figjam"];
|
|
@@ -11451,27 +11872,27 @@ var init_permissions = __esm({
|
|
|
11451
11872
|
}
|
|
11452
11873
|
});
|
|
11453
11874
|
|
|
11454
|
-
//
|
|
11875
|
+
// packages/cli/src/commands/inspect.ts
|
|
11455
11876
|
var inspect_exports = {};
|
|
11456
11877
|
__export(inspect_exports, {
|
|
11457
11878
|
INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
|
|
11458
11879
|
runInspect: () => runInspect
|
|
11459
11880
|
});
|
|
11460
|
-
import { existsSync as
|
|
11461
|
-
import
|
|
11881
|
+
import { existsSync as existsSync31, readFileSync as readFileSync27, writeFileSync as writeFileSync16 } from "node:fs";
|
|
11882
|
+
import path38 from "node:path";
|
|
11462
11883
|
async function runInspect(opts) {
|
|
11463
11884
|
if (opts.describe) {
|
|
11464
11885
|
printDescription(INSPECT_DESCRIPTION);
|
|
11465
11886
|
return;
|
|
11466
11887
|
}
|
|
11467
|
-
const bundleDir =
|
|
11468
|
-
const evidenceDir =
|
|
11469
|
-
const manifestPath2 =
|
|
11470
|
-
if (!
|
|
11888
|
+
const bundleDir = path38.resolve(opts.bundleDir);
|
|
11889
|
+
const evidenceDir = path38.join(bundleDir, "verify-evidence");
|
|
11890
|
+
const manifestPath2 = path38.join(bundleDir, "component.json");
|
|
11891
|
+
if (!existsSync31(evidenceDir) || !existsSync31(manifestPath2)) {
|
|
11471
11892
|
fail(opts, ExitCode.InputValidation, {
|
|
11472
|
-
error: `nothing to inspect in ${bundleDir} \u2014 ${
|
|
11893
|
+
error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync31(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
|
|
11473
11894
|
code: "no-evidence",
|
|
11474
|
-
remediation:
|
|
11895
|
+
remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
|
|
11475
11896
|
});
|
|
11476
11897
|
}
|
|
11477
11898
|
const { manifest } = readBundleManifest(readFileSync27(manifestPath2, "utf8"));
|
|
@@ -11482,27 +11903,27 @@ async function runInspect(opts) {
|
|
|
11482
11903
|
remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
|
|
11483
11904
|
});
|
|
11484
11905
|
}
|
|
11485
|
-
const setDir =
|
|
11486
|
-
const reps = Object.keys(manifest.propAdapter).filter((rep) =>
|
|
11906
|
+
const setDir = path38.resolve(opts.set ?? manifest.provenance.recordingSet.path);
|
|
11907
|
+
const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync31(path38.join(evidenceDir, `${rep}-ref.png`)) && existsSync31(path38.join(evidenceDir, `${rep}-render.png`)));
|
|
11487
11908
|
if (reps.length === 0) {
|
|
11488
11909
|
fail(opts, ExitCode.InputValidation, {
|
|
11489
11910
|
error: "verify-evidence holds no ref/render pairs for this bundle's configs",
|
|
11490
11911
|
code: "no-evidence",
|
|
11491
|
-
remediation:
|
|
11912
|
+
remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
|
|
11492
11913
|
});
|
|
11493
11914
|
}
|
|
11494
11915
|
let crops = 0;
|
|
11495
11916
|
const sections = [];
|
|
11496
11917
|
for (const rep of reps) {
|
|
11497
|
-
const ref = new Uint8Array(readFileSync27(
|
|
11498
|
-
const render = new Uint8Array(readFileSync27(
|
|
11918
|
+
const ref = new Uint8Array(readFileSync27(path38.join(evidenceDir, `${rep}-ref.png`)));
|
|
11919
|
+
const render = new Uint8Array(readFileSync27(path38.join(evidenceDir, `${rep}-render.png`)));
|
|
11499
11920
|
const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
|
|
11500
11921
|
const cells = [];
|
|
11501
11922
|
for (const [i, n] of nodes.entries()) {
|
|
11502
11923
|
const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
|
|
11503
11924
|
try {
|
|
11504
|
-
writeFileSync16(
|
|
11505
|
-
writeFileSync16(
|
|
11925
|
+
writeFileSync16(path38.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
|
|
11926
|
+
writeFileSync16(path38.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
|
|
11506
11927
|
} catch {
|
|
11507
11928
|
continue;
|
|
11508
11929
|
}
|
|
@@ -11515,7 +11936,7 @@ async function runInspect(opts) {
|
|
|
11515
11936
|
`<section><h2>${esc(rep)}</h2><div class="full"><span><em>recorded</em><img src="./${rep}-ref.png"></span><span><em>rendered</em><img src="./${rep}-render.png"></span><span><em>diff</em><img src="./${rep}-diff.png"></span></div>` + (cells.length > 0 ? `<div class="grid">${cells.join("")}</div>` : `<p class="none">no small recorded nodes in this config's sweep</p>`) + `</section>`
|
|
11516
11937
|
);
|
|
11517
11938
|
}
|
|
11518
|
-
const sheet =
|
|
11939
|
+
const sheet = path38.join(evidenceDir, "inspect.html");
|
|
11519
11940
|
writeFileSync16(
|
|
11520
11941
|
sheet,
|
|
11521
11942
|
`<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
|
|
@@ -11540,12 +11961,13 @@ ${reps.length} config(s), ${crops} detail crop pair(s) \u2014 open the sheet and
|
|
|
11540
11961
|
}
|
|
11541
11962
|
var INSPECT_DESCRIPTION, esc;
|
|
11542
11963
|
var init_inspect = __esm({
|
|
11543
|
-
"
|
|
11964
|
+
"packages/cli/src/commands/inspect.ts"() {
|
|
11544
11965
|
"use strict";
|
|
11545
11966
|
init_src3();
|
|
11546
11967
|
init_src6();
|
|
11547
11968
|
init_src4();
|
|
11548
11969
|
init_describe();
|
|
11970
|
+
init_invocation();
|
|
11549
11971
|
init_output();
|
|
11550
11972
|
INSPECT_DESCRIPTION = {
|
|
11551
11973
|
name: "inspect",
|
|
@@ -11568,7 +11990,7 @@ var init_inspect = __esm({
|
|
|
11568
11990
|
}
|
|
11569
11991
|
});
|
|
11570
11992
|
|
|
11571
|
-
//
|
|
11993
|
+
// packages/cli/src/commands/generate-route.ts
|
|
11572
11994
|
var generate_route_exports = {};
|
|
11573
11995
|
__export(generate_route_exports, {
|
|
11574
11996
|
routeGenerate: () => routeGenerate
|
|
@@ -11581,28 +12003,28 @@ function routeGenerate(input) {
|
|
|
11581
12003
|
return { path: "facts", ...input.url !== void 0 ? { url: input.url } : {} };
|
|
11582
12004
|
}
|
|
11583
12005
|
var init_generate_route = __esm({
|
|
11584
|
-
"
|
|
12006
|
+
"packages/cli/src/commands/generate-route.ts"() {
|
|
11585
12007
|
"use strict";
|
|
11586
12008
|
}
|
|
11587
12009
|
});
|
|
11588
12010
|
|
|
11589
|
-
//
|
|
12011
|
+
// packages/cli/src/commands/generate-recorded.ts
|
|
11590
12012
|
var generate_recorded_exports = {};
|
|
11591
12013
|
__export(generate_recorded_exports, {
|
|
11592
12014
|
runGenerateRecorded: () => runGenerateRecorded
|
|
11593
12015
|
});
|
|
11594
12016
|
import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
|
|
11595
|
-
import { existsSync as
|
|
11596
|
-
import
|
|
12017
|
+
import { existsSync as existsSync32, readFileSync as readFileSync28 } from "node:fs";
|
|
12018
|
+
import path39 from "node:path";
|
|
11597
12019
|
async function runGenerateRecorded(opts) {
|
|
11598
12020
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
11599
|
-
const outDirAbs =
|
|
11600
|
-
const recordedAsPath =
|
|
12021
|
+
const outDirAbs = path39.resolve(callerCwd, opts.out);
|
|
12022
|
+
const recordedAsPath = path39.resolve(callerCwd, opts.recorded);
|
|
11601
12023
|
let task;
|
|
11602
12024
|
let taskName;
|
|
11603
12025
|
let authoredApi;
|
|
11604
12026
|
let composition;
|
|
11605
|
-
const isSet =
|
|
12027
|
+
const isSet = existsSync32(path39.join(recordedAsPath, "recording-set.json"));
|
|
11606
12028
|
const registry = TASKS[opts.recorded];
|
|
11607
12029
|
if (registry !== void 0 && !isSet) {
|
|
11608
12030
|
task = registry;
|
|
@@ -11611,7 +12033,7 @@ async function runGenerateRecorded(opts) {
|
|
|
11611
12033
|
try {
|
|
11612
12034
|
const authored = authorTaskFromSet(recordedAsPath);
|
|
11613
12035
|
task = authored.task;
|
|
11614
|
-
taskName =
|
|
12036
|
+
taskName = path39.basename(recordedAsPath);
|
|
11615
12037
|
authoredApi = authored.api;
|
|
11616
12038
|
const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
|
|
11617
12039
|
if (roles.success) composition = roles.data;
|
|
@@ -11620,14 +12042,14 @@ async function runGenerateRecorded(opts) {
|
|
|
11620
12042
|
fail(opts, ExitCode.InputValidation, {
|
|
11621
12043
|
error: `cannot author a task from ${recordedAsPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
11622
12044
|
code: "AUTHORING_FAILED",
|
|
11623
|
-
remediation:
|
|
12045
|
+
remediation: `Complete the recording set (${tendrilCommand(`record status --set ${recordedAsPath}`)}) or use a reference-corpus task name.`
|
|
11624
12046
|
});
|
|
11625
12047
|
}
|
|
11626
12048
|
} else {
|
|
11627
12049
|
fail(opts, ExitCode.InputValidation, {
|
|
11628
12050
|
error: `"${opts.recorded}" is neither a reference task nor a recorded set (no recording-set.json found)`,
|
|
11629
12051
|
code: "UNKNOWN_RECORDED_INPUT",
|
|
11630
|
-
remediation: `Pick a task (${Object.keys(TASKS).join(", ")}) or point at a set recorded with
|
|
12052
|
+
remediation: `Pick a task (${Object.keys(TASKS).join(", ")}) or point at a set recorded with \`${tendrilCommand("record plan")}\`.`
|
|
11631
12053
|
});
|
|
11632
12054
|
}
|
|
11633
12055
|
if (!fontsResolved()) {
|
|
@@ -11645,13 +12067,13 @@ async function runGenerateRecorded(opts) {
|
|
|
11645
12067
|
warn(opts, `font weights (advisory): the recording implies weights ${g.declared.join(", ")} for "${g.family}" \u2014 cache provides ${g.provided.join(", ")}; nearest-weight rendering may shift stroke density (certification stays family-gated; implied weights can leak across families)`);
|
|
11646
12068
|
}
|
|
11647
12069
|
const missing = task.configs.filter(
|
|
11648
|
-
(c) => !
|
|
12070
|
+
(c) => !existsSync32(path39.join(task.set, c.rep, "get_screenshot.json")) || !existsSync32(path39.join(task.set, c.rep, "get_metadata.json")) || !existsSync32(path39.join(task.set, c.rep, "get_design_context.json"))
|
|
11649
12071
|
);
|
|
11650
12072
|
if (missing.length > 0) {
|
|
11651
12073
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
11652
12074
|
error: `recording set ${task.set} is missing envelopes for ${missing.length} rep(s): ${missing.slice(0, 3).map((c) => c.rep).join(", ")}${missing.length > 3 ? ", \u2026" : ""}`,
|
|
11653
12075
|
code: "recording-incomplete",
|
|
11654
|
-
remediation:
|
|
12076
|
+
remediation: `Resume the recording session (\`${tendrilCommand(`record next --set ${task.set}`)}\`) until \`${tendrilCommand(`record status --set ${task.set}`)}\` reports the set complete.`
|
|
11655
12077
|
});
|
|
11656
12078
|
}
|
|
11657
12079
|
const modelId = opts.model ?? DEFAULT_MODEL_CONFIG.curatedDefault ?? DEFAULT_MODEL_CONFIG.bulk;
|
|
@@ -11672,7 +12094,7 @@ async function runGenerateRecorded(opts) {
|
|
|
11672
12094
|
fail(opts, ExitCode.Auth, {
|
|
11673
12095
|
error: "OPENROUTER_API_KEY is not configured \u2014 the curated engine needs an OpenRouter-compatible key.",
|
|
11674
12096
|
code: "MISSING_OPENROUTER_KEY",
|
|
11675
|
-
remediation:
|
|
12097
|
+
remediation: `Two paths: run \`${tendrilCommand("init")}\` to store your OpenRouter key (BYOK \u2014 your key, your inference), or drive the agent-harness engine explicitly with your own agent host.`
|
|
11676
12098
|
});
|
|
11677
12099
|
}
|
|
11678
12100
|
const bar = BARS4[opts.bar];
|
|
@@ -11715,8 +12137,8 @@ async function runGenerateRecorded(opts) {
|
|
|
11715
12137
|
` : `${line}
|
|
11716
12138
|
`);
|
|
11717
12139
|
if (opts.dryRun) {
|
|
11718
|
-
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite:
|
|
11719
|
-
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${
|
|
12140
|
+
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path39.join(outDirAbs, taskName) }, () => {
|
|
12141
|
+
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path39.join(outDirAbs, taskName)})
|
|
11720
12142
|
`);
|
|
11721
12143
|
});
|
|
11722
12144
|
return;
|
|
@@ -11739,10 +12161,10 @@ async function runGenerateRecorded(opts) {
|
|
|
11739
12161
|
});
|
|
11740
12162
|
}
|
|
11741
12163
|
}
|
|
11742
|
-
const bundleDir =
|
|
11743
|
-
if (
|
|
12164
|
+
const bundleDir = path39.join(outDirAbs, taskName);
|
|
12165
|
+
if (existsSync32(path39.join(bundleDir, "component.json"))) {
|
|
11744
12166
|
try {
|
|
11745
|
-
const prior = readBundleManifest(readFileSync28(
|
|
12167
|
+
const prior = readBundleManifest(readFileSync28(path39.join(bundleDir, "component.json"), "utf8")).manifest;
|
|
11746
12168
|
if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
|
|
11747
12169
|
fail(opts, ExitCode.InputValidation, {
|
|
11748
12170
|
error: `${bundleDir} already holds a bundle bound to recording set "${prior.provenance.recordingSet.path}" \u2014 generating here against a different set would silently rewrite its verification identity`,
|
|
@@ -11868,7 +12290,7 @@ ${certified}/${statuses.length} certified \xB7 ${statuses.length - pixelFailures
|
|
|
11868
12290
|
}
|
|
11869
12291
|
var BARS4;
|
|
11870
12292
|
var init_generate_recorded = __esm({
|
|
11871
|
-
"
|
|
12293
|
+
"packages/cli/src/commands/generate-recorded.ts"() {
|
|
11872
12294
|
"use strict";
|
|
11873
12295
|
init_src3();
|
|
11874
12296
|
init_src5();
|
|
@@ -11879,6 +12301,7 @@ var init_generate_recorded = __esm({
|
|
|
11879
12301
|
init_env();
|
|
11880
12302
|
init_environment();
|
|
11881
12303
|
init_font_guidance();
|
|
12304
|
+
init_invocation();
|
|
11882
12305
|
init_output();
|
|
11883
12306
|
BARS4 = {
|
|
11884
12307
|
pass: { sim: 0.95, ink: 0.95 },
|
|
@@ -11887,24 +12310,27 @@ var init_generate_recorded = __esm({
|
|
|
11887
12310
|
}
|
|
11888
12311
|
});
|
|
11889
12312
|
|
|
11890
|
-
//
|
|
12313
|
+
// packages/cli/src/bin.ts
|
|
11891
12314
|
init_src3();
|
|
12315
|
+
init_invocation();
|
|
11892
12316
|
import { CommanderError } from "commander";
|
|
11893
12317
|
|
|
11894
|
-
//
|
|
12318
|
+
// packages/cli/src/program.ts
|
|
11895
12319
|
init_src3();
|
|
11896
12320
|
init_environment();
|
|
12321
|
+
init_invocation();
|
|
11897
12322
|
init_doctor();
|
|
11898
12323
|
import { Command } from "commander";
|
|
11899
12324
|
|
|
11900
|
-
//
|
|
12325
|
+
// packages/cli/src/commands/init.ts
|
|
11901
12326
|
init_src3();
|
|
11902
12327
|
init_describe();
|
|
11903
12328
|
init_env();
|
|
12329
|
+
init_invocation();
|
|
11904
12330
|
init_output();
|
|
11905
12331
|
import { intro, isCancel, outro, password } from "@clack/prompts";
|
|
11906
12332
|
import fs from "node:fs";
|
|
11907
|
-
import
|
|
12333
|
+
import path24 from "node:path";
|
|
11908
12334
|
var INIT_DESCRIPTION = {
|
|
11909
12335
|
name: "init",
|
|
11910
12336
|
summary: "Configure Figma and OpenRouter credentials in .env (idempotent).",
|
|
@@ -11941,7 +12367,7 @@ async function runInit(flags) {
|
|
|
11941
12367
|
printDescription(INIT_DESCRIPTION);
|
|
11942
12368
|
return;
|
|
11943
12369
|
}
|
|
11944
|
-
const envPath =
|
|
12370
|
+
const envPath = path24.resolve(process.cwd(), ".env");
|
|
11945
12371
|
const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
|
|
11946
12372
|
let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
|
|
11947
12373
|
let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
|
|
@@ -11950,7 +12376,7 @@ async function runInit(flags) {
|
|
|
11950
12376
|
fail(flags, ExitCode.InputValidation, {
|
|
11951
12377
|
error: "Missing credentials and prompts are disabled.",
|
|
11952
12378
|
code: "MISSING_CREDENTIALS",
|
|
11953
|
-
remediation:
|
|
12379
|
+
remediation: `Pass --figma-token <token> and --openrouter-key <key>, or run \`${tendrilCommand("init")}\` in an interactive terminal.`
|
|
11954
12380
|
});
|
|
11955
12381
|
}
|
|
11956
12382
|
if (interactive) {
|
|
@@ -11962,7 +12388,7 @@ async function runInit(flags) {
|
|
|
11962
12388
|
next.set(ENV_KEYS.figma, figmaToken);
|
|
11963
12389
|
next.set(ENV_KEYS.openrouter, openrouterKey);
|
|
11964
12390
|
const changed = existing.get(ENV_KEYS.figma) !== next.get(ENV_KEYS.figma) || existing.get(ENV_KEYS.openrouter) !== next.get(ENV_KEYS.openrouter);
|
|
11965
|
-
const gitignorePath =
|
|
12391
|
+
const gitignorePath = path24.resolve(process.cwd(), ".gitignore");
|
|
11966
12392
|
const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
11967
12393
|
const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
|
|
11968
12394
|
if (flags.dryRun) {
|
|
@@ -12007,26 +12433,27 @@ async function runInit(flags) {
|
|
|
12007
12433
|
});
|
|
12008
12434
|
}
|
|
12009
12435
|
|
|
12010
|
-
//
|
|
12436
|
+
// packages/cli/src/commands/generate.ts
|
|
12011
12437
|
init_src3();
|
|
12012
12438
|
init_src();
|
|
12013
12439
|
init_src5();
|
|
12014
12440
|
init_src2();
|
|
12015
12441
|
init_describe();
|
|
12016
12442
|
init_env();
|
|
12443
|
+
init_invocation();
|
|
12017
12444
|
init_output();
|
|
12018
12445
|
init_entitlement();
|
|
12019
12446
|
import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
|
|
12020
|
-
import { readFileSync as readFileSync15, readdirSync as readdirSync4, existsSync as
|
|
12447
|
+
import { readFileSync as readFileSync15, readdirSync as readdirSync4, existsSync as existsSync19 } from "node:fs";
|
|
12021
12448
|
|
|
12022
|
-
//
|
|
12449
|
+
// packages/cli/src/pipeline.ts
|
|
12023
12450
|
init_src2();
|
|
12024
12451
|
init_src4();
|
|
12025
12452
|
init_src6();
|
|
12026
12453
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
12027
|
-
import
|
|
12454
|
+
import path25 from "node:path";
|
|
12028
12455
|
|
|
12029
|
-
//
|
|
12456
|
+
// packages/cli/src/assets-module.ts
|
|
12030
12457
|
init_src();
|
|
12031
12458
|
var KEBAB_ATTRS = {
|
|
12032
12459
|
"fill-rule": "fillRule",
|
|
@@ -12137,7 +12564,7 @@ function buildAssetsModule(assets, aliases = {}) {
|
|
|
12137
12564
|
return { source, flags };
|
|
12138
12565
|
}
|
|
12139
12566
|
|
|
12140
|
-
//
|
|
12567
|
+
// packages/cli/src/pipeline.ts
|
|
12141
12568
|
var VerifyFailedError = class extends Error {
|
|
12142
12569
|
constructor(loop) {
|
|
12143
12570
|
super(
|
|
@@ -12360,7 +12787,7 @@ async function runGenerationPipeline(input) {
|
|
|
12360
12787
|
});
|
|
12361
12788
|
const written = [];
|
|
12362
12789
|
if (!input.dryRun) {
|
|
12363
|
-
const dir =
|
|
12790
|
+
const dir = path25.resolve(input.outDir, semantics.componentName);
|
|
12364
12791
|
mkdirSync5(dir, { recursive: true });
|
|
12365
12792
|
const files = {
|
|
12366
12793
|
// Bundle-local tokens: THE emission the component's CSS resolves
|
|
@@ -12384,13 +12811,13 @@ async function runGenerationPipeline(input) {
|
|
|
12384
12811
|
`
|
|
12385
12812
|
};
|
|
12386
12813
|
for (const [name, content] of Object.entries(files)) {
|
|
12387
|
-
const filePath =
|
|
12814
|
+
const filePath = path25.join(dir, name);
|
|
12388
12815
|
writeFileSync8(filePath, content);
|
|
12389
12816
|
written.push(filePath);
|
|
12390
12817
|
}
|
|
12391
12818
|
for (const artifact of emitTokenArtifacts(input.mapping)) {
|
|
12392
|
-
const filePath =
|
|
12393
|
-
mkdirSync5(
|
|
12819
|
+
const filePath = path25.resolve(input.outDir, artifact.path);
|
|
12820
|
+
mkdirSync5(path25.dirname(filePath), { recursive: true });
|
|
12394
12821
|
writeFileSync8(filePath, artifact.content);
|
|
12395
12822
|
written.push(filePath);
|
|
12396
12823
|
}
|
|
@@ -12398,7 +12825,7 @@ async function runGenerationPipeline(input) {
|
|
|
12398
12825
|
return { semantics, loop: finalLoop, report, componentJson, usageMd: docs.usageMd, written };
|
|
12399
12826
|
}
|
|
12400
12827
|
|
|
12401
|
-
//
|
|
12828
|
+
// packages/cli/src/commands/generate.ts
|
|
12402
12829
|
var TOOL_VERSION = "0.0.0";
|
|
12403
12830
|
var GENERATE_DESCRIPTION = {
|
|
12404
12831
|
name: "generate",
|
|
@@ -12454,7 +12881,7 @@ function resolveProvidedSource(flags, contextFile) {
|
|
|
12454
12881
|
fail(flags, ExitCode.InputValidation, {
|
|
12455
12882
|
error: `Cannot read context file "${contextFile}".`,
|
|
12456
12883
|
code: "CONTEXT_UNREADABLE",
|
|
12457
|
-
remediation:
|
|
12884
|
+
remediation: `Pass a readable JSON file via --context; run \`${tendrilCommand("generate --describe")}\` for the payload shape.`
|
|
12458
12885
|
});
|
|
12459
12886
|
}
|
|
12460
12887
|
try {
|
|
@@ -12463,7 +12890,7 @@ function resolveProvidedSource(flags, contextFile) {
|
|
|
12463
12890
|
fail(flags, ExitCode.InputValidation, {
|
|
12464
12891
|
error: `Context file "${contextFile}" is not a valid design-context payload: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
12465
12892
|
code: "CONTEXT_INVALID",
|
|
12466
|
-
remediation:
|
|
12893
|
+
remediation: `The payload must match { nodeTree, variables[], metadata, screenshotBase64? } \u2014 see \`${tendrilCommand("generate --describe")}\` and docs/adr/003-figma-access-strategy.md.`
|
|
12467
12894
|
});
|
|
12468
12895
|
}
|
|
12469
12896
|
}
|
|
@@ -12473,7 +12900,7 @@ function resolveSource(flags, url) {
|
|
|
12473
12900
|
fail(flags, ExitCode.General, {
|
|
12474
12901
|
error: "Live Figma transports (desktop MCP / REST) are not wired up yet.",
|
|
12475
12902
|
code: "NOT_IMPLEMENTED",
|
|
12476
|
-
remediation:
|
|
12903
|
+
remediation: `Use \`${tendrilCommand("generate --context <file>")}\` with an agent-fetched payload, or mock://button for the offline fixture; live transports are task 1.2 in docs/PLAN.md.`
|
|
12477
12904
|
});
|
|
12478
12905
|
}
|
|
12479
12906
|
fail(flags, ExitCode.InputValidation, {
|
|
@@ -12492,7 +12919,7 @@ async function runGenerate(url, flags) {
|
|
|
12492
12919
|
fail(flags, ExitCode.InputValidation, {
|
|
12493
12920
|
error: "Missing input: pass a recording set or reference task (recorded truth, the default), mock://button, or --context <file>.",
|
|
12494
12921
|
code: "MISSING_ARGUMENT",
|
|
12495
|
-
remediation:
|
|
12922
|
+
remediation: `Run \`${tendrilCommand("generate --describe")}\` to see arguments and examples.`
|
|
12496
12923
|
});
|
|
12497
12924
|
}
|
|
12498
12925
|
url ??= `provided://${flags.context}`;
|
|
@@ -12569,10 +12996,10 @@ token mapping (${mapping.flat.length} variables):
|
|
|
12569
12996
|
let initialCode;
|
|
12570
12997
|
let initialSemantics;
|
|
12571
12998
|
try {
|
|
12572
|
-
if (
|
|
12999
|
+
if (existsSync19(flags.out)) {
|
|
12573
13000
|
for (const entry of readdirSync4(flags.out)) {
|
|
12574
13001
|
const cjPath = `${flags.out}/${entry}/component.json`;
|
|
12575
|
-
if (!
|
|
13002
|
+
if (!existsSync19(cjPath)) continue;
|
|
12576
13003
|
const cj = JSON.parse(readFileSync15(cjPath, "utf8"));
|
|
12577
13004
|
if (cj.name !== void 0 && Array.isArray(cj.props)) {
|
|
12578
13005
|
previousApi = JSON.stringify({
|
|
@@ -12581,13 +13008,13 @@ token mapping (${mapping.flat.length} variables):
|
|
|
12581
13008
|
});
|
|
12582
13009
|
const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
|
|
12583
13010
|
const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
|
|
12584
|
-
if (flags.refine &&
|
|
13011
|
+
if (flags.refine && existsSync19(tsxPath) && existsSync19(cssPath)) {
|
|
12585
13012
|
initialCode = {
|
|
12586
13013
|
tsx: readFileSync15(tsxPath, "utf8"),
|
|
12587
13014
|
css: readFileSync15(cssPath, "utf8")
|
|
12588
13015
|
};
|
|
12589
13016
|
const semPath = `${flags.out}/${entry}/semantics.json`;
|
|
12590
|
-
if (
|
|
13017
|
+
if (existsSync19(semPath)) {
|
|
12591
13018
|
initialSemantics = JSON.parse(readFileSync15(semPath, "utf8"));
|
|
12592
13019
|
}
|
|
12593
13020
|
}
|
|
@@ -12617,7 +13044,7 @@ token mapping (${mapping.flat.length} variables):
|
|
|
12617
13044
|
fail(flags, ExitCode.Auth, {
|
|
12618
13045
|
error: "OPENROUTER_API_KEY is not configured.",
|
|
12619
13046
|
code: "MISSING_OPENROUTER_KEY",
|
|
12620
|
-
remediation:
|
|
13047
|
+
remediation: `Run \`${tendrilCommand("init")}\` to store your OpenRouter key in .env (BYOK \u2014 your key, your inference).`
|
|
12621
13048
|
});
|
|
12622
13049
|
}
|
|
12623
13050
|
genModel = new OpenRouterGenerationModel(new OpenRouterClient({ apiKey }), model.id, {
|
|
@@ -12726,7 +13153,7 @@ bundle written to ${flags.out}/${result.semantics.componentName}/:
|
|
|
12726
13153
|
);
|
|
12727
13154
|
}
|
|
12728
13155
|
|
|
12729
|
-
//
|
|
13156
|
+
// packages/cli/src/program.ts
|
|
12730
13157
|
init_src4();
|
|
12731
13158
|
function globalFlags(cmd) {
|
|
12732
13159
|
const opts = cmd.optsWithGlobals();
|
|
@@ -12840,7 +13267,7 @@ function buildProgram() {
|
|
|
12840
13267
|
fail2(flags, ExitCode.InputValidation, {
|
|
12841
13268
|
error: family === void 0 ? "nothing to resolve \u2014 pass a family name or --set <recording-dir>" : "pass a family name OR --set, not both \u2014 --set derives families from the recording itself",
|
|
12842
13269
|
code: "INPUT_VALIDATION",
|
|
12843
|
-
remediation:
|
|
13270
|
+
remediation: `${tendrilCommand("fonts resolve --set <recording-dir>")}, or ${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}`
|
|
12844
13271
|
});
|
|
12845
13272
|
}
|
|
12846
13273
|
if (set !== void 0) {
|
|
@@ -12983,7 +13410,7 @@ function buildProgram() {
|
|
|
12983
13410
|
return program;
|
|
12984
13411
|
}
|
|
12985
13412
|
|
|
12986
|
-
//
|
|
13413
|
+
// packages/cli/src/bin.ts
|
|
12987
13414
|
try {
|
|
12988
13415
|
await buildProgram().parseAsync(process.argv);
|
|
12989
13416
|
} catch (err) {
|
|
@@ -12994,7 +13421,7 @@ try {
|
|
|
12994
13421
|
const cliError2 = {
|
|
12995
13422
|
error: err.message,
|
|
12996
13423
|
code: "INPUT_VALIDATION",
|
|
12997
|
-
remediation:
|
|
13424
|
+
remediation: `Run \`${tendrilCommand("--help")}\` or \`${tendrilCommand("<command> --describe")}\` for usage.`
|
|
12998
13425
|
};
|
|
12999
13426
|
process.stderr.write(`${JSON.stringify(cliError2)}
|
|
13000
13427
|
`);
|