@tendrilapp/cli 0.1.39 → 0.1.41
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 +24 -5
- package/dist/tendril-mcp.js +16 -2
- package/dist/tendril.js +1921 -536
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -1217,7 +1217,7 @@ function clampedReference(setDir, slug, payload) {
|
|
|
1217
1217
|
const dims = referencePngDims(payload);
|
|
1218
1218
|
if (dims === void 0) return void 0;
|
|
1219
1219
|
if (dims.w >= box.w - 1 && dims.h >= box.h - 1) return void 0;
|
|
1220
|
-
return `the reference image is ${dims.w}\xD7${dims.h} but the recorded box is ${box.w}\xD7${box.h} \u2014 it was SCALED DOWN, so it is not pixel ground truth. Re-run get_screenshot passing
|
|
1220
|
+
return `the reference image is ${dims.w}\xD7${dims.h} but the recorded box is ${box.w}\xD7${box.h} \u2014 it was SCALED DOWN, so it is not pixel ground truth. Re-run get_screenshot passing contentsOnly: true and maxDimension: 4096 (the tool defaults to 1024 and clamps anything longer; a flat generous cap is always at least the ${Math.max(box.w, box.h)} this box needs, and contentsOnly keeps the editor's component-set chrome out of the padding)`;
|
|
1221
1221
|
}
|
|
1222
1222
|
function referencePngDims(payload) {
|
|
1223
1223
|
const parts = payload?.content;
|
|
@@ -1980,8 +1980,8 @@ var init_src = __esm({
|
|
|
1980
1980
|
function variableNameToPath(name) {
|
|
1981
1981
|
return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
|
|
1982
1982
|
}
|
|
1983
|
-
function tokenPathToCssVar(
|
|
1984
|
-
return `--${
|
|
1983
|
+
function tokenPathToCssVar(path54) {
|
|
1984
|
+
return `--${path54.join("-")}`;
|
|
1985
1985
|
}
|
|
1986
1986
|
function toDtcgToken(variable, defaultMode) {
|
|
1987
1987
|
const modes = Object.keys(variable.valuesByMode);
|
|
@@ -2025,11 +2025,11 @@ function toDtcgToken(variable, defaultMode) {
|
|
|
2025
2025
|
}
|
|
2026
2026
|
function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
2027
2027
|
const entries = variables.map((variable) => {
|
|
2028
|
-
const
|
|
2029
|
-
if (
|
|
2028
|
+
const path54 = variableNameToPath(variable.name);
|
|
2029
|
+
if (path54.length === 0) {
|
|
2030
2030
|
throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
|
|
2031
2031
|
}
|
|
2032
|
-
return { variable, path:
|
|
2032
|
+
return { variable, path: path54 };
|
|
2033
2033
|
});
|
|
2034
2034
|
const groupPrefixes = /* @__PURE__ */ new Set();
|
|
2035
2035
|
for (const e of entries) {
|
|
@@ -2050,21 +2050,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
|
2050
2050
|
}
|
|
2051
2051
|
const tokens = {};
|
|
2052
2052
|
const flat = [];
|
|
2053
|
-
for (const { variable, path:
|
|
2053
|
+
for (const { variable, path: path54 } of entries) {
|
|
2054
2054
|
const token = toDtcgToken(variable, defaultMode);
|
|
2055
2055
|
let group = tokens;
|
|
2056
|
-
for (const segment of
|
|
2056
|
+
for (const segment of path54.slice(0, -1)) {
|
|
2057
2057
|
const existing = group[segment];
|
|
2058
2058
|
group = existing ?? (group[segment] = {});
|
|
2059
2059
|
}
|
|
2060
|
-
const leaf =
|
|
2060
|
+
const leaf = path54[path54.length - 1];
|
|
2061
2061
|
if (group[leaf] !== void 0) {
|
|
2062
|
-
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${
|
|
2062
|
+
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path54.join(".")}" (variable ${variable.id})`);
|
|
2063
2063
|
}
|
|
2064
2064
|
group[leaf] = token;
|
|
2065
2065
|
flat.push({
|
|
2066
|
-
path:
|
|
2067
|
-
cssVar: tokenPathToCssVar(
|
|
2066
|
+
path: path54.join("."),
|
|
2067
|
+
cssVar: tokenPathToCssVar(path54),
|
|
2068
2068
|
type: token.$type,
|
|
2069
2069
|
value: token.$value
|
|
2070
2070
|
});
|
|
@@ -2253,9 +2253,9 @@ function boundId(value) {
|
|
|
2253
2253
|
return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
|
|
2254
2254
|
}
|
|
2255
2255
|
function resolveBinding(ctx, id) {
|
|
2256
|
-
const
|
|
2257
|
-
if (
|
|
2258
|
-
return
|
|
2256
|
+
const path54 = ctx.pathById.get(id);
|
|
2257
|
+
if (path54 === void 0) ctx.unresolved.add(id);
|
|
2258
|
+
return path54;
|
|
2259
2259
|
}
|
|
2260
2260
|
function parseVariantProps(name) {
|
|
2261
2261
|
if (!name.includes("=")) return void 0;
|
|
@@ -2290,8 +2290,8 @@ function walk(ctx, raw) {
|
|
|
2290
2290
|
if (!isObject(paint) || paint["visible"] === false) continue;
|
|
2291
2291
|
const id = boundId(paint);
|
|
2292
2292
|
if (id !== void 0) {
|
|
2293
|
-
const
|
|
2294
|
-
if (
|
|
2293
|
+
const path54 = resolveBinding(ctx, id);
|
|
2294
|
+
if (path54 !== void 0) tokens.add(path54);
|
|
2295
2295
|
} else if (typeof paint["color"] === "string") {
|
|
2296
2296
|
ctx.hardcoded.push({ node: name, property, value: paint["color"] });
|
|
2297
2297
|
}
|
|
@@ -2299,8 +2299,8 @@ function walk(ctx, raw) {
|
|
|
2299
2299
|
}
|
|
2300
2300
|
const radiusId = boundId(raw["cornerRadius"]);
|
|
2301
2301
|
if (radiusId !== void 0) {
|
|
2302
|
-
const
|
|
2303
|
-
if (
|
|
2302
|
+
const path54 = resolveBinding(ctx, radiusId);
|
|
2303
|
+
if (path54 !== void 0) tokens.add(path54);
|
|
2304
2304
|
} else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
|
|
2305
2305
|
ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
|
|
2306
2306
|
}
|
|
@@ -2310,10 +2310,10 @@ function walk(ctx, raw) {
|
|
|
2310
2310
|
layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
|
|
2311
2311
|
const gapId = boundId(raw["itemSpacing"]);
|
|
2312
2312
|
if (gapId !== void 0) {
|
|
2313
|
-
const
|
|
2314
|
-
if (
|
|
2315
|
-
layout.gap =
|
|
2316
|
-
tokens.add(
|
|
2313
|
+
const path54 = resolveBinding(ctx, gapId);
|
|
2314
|
+
if (path54 !== void 0) {
|
|
2315
|
+
layout.gap = path54;
|
|
2316
|
+
tokens.add(path54);
|
|
2317
2317
|
}
|
|
2318
2318
|
} else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
|
|
2319
2319
|
ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
|
|
@@ -2322,10 +2322,10 @@ function walk(ctx, raw) {
|
|
|
2322
2322
|
for (const field of PADDING_FIELDS) {
|
|
2323
2323
|
const id = boundId(raw[field]);
|
|
2324
2324
|
if (id !== void 0) {
|
|
2325
|
-
const
|
|
2326
|
-
if (
|
|
2327
|
-
paddingPaths.push(
|
|
2328
|
-
tokens.add(
|
|
2325
|
+
const path54 = resolveBinding(ctx, id);
|
|
2326
|
+
if (path54 !== void 0) {
|
|
2327
|
+
paddingPaths.push(path54);
|
|
2328
|
+
tokens.add(path54);
|
|
2329
2329
|
}
|
|
2330
2330
|
} else if (typeof raw[field] === "number" && raw[field] !== 0) {
|
|
2331
2331
|
ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
|
|
@@ -2533,9 +2533,17 @@ var init_src3 = __esm({
|
|
|
2533
2533
|
});
|
|
2534
2534
|
|
|
2535
2535
|
// packages/cli/src/invocation.ts
|
|
2536
|
-
import { existsSync as existsSync5, realpathSync } from "node:fs";
|
|
2536
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, realpathSync } from "node:fs";
|
|
2537
2537
|
import path5 from "node:path";
|
|
2538
2538
|
import { fileURLToPath } from "node:url";
|
|
2539
|
+
function isDevTree(root) {
|
|
2540
|
+
try {
|
|
2541
|
+
const pkg = JSON.parse(readFileSync5(path5.join(root, "package.json"), "utf8"));
|
|
2542
|
+
return pkg.private === true && pkg.name === "@tendrilapp/cli";
|
|
2543
|
+
} catch {
|
|
2544
|
+
return false;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2539
2547
|
function findPathTendril(pathEnv, platform) {
|
|
2540
2548
|
const dirs = pathEnv.split(path5.delimiter).filter((d) => d !== "" && !/node_modules[\\/]\.bin/.test(d) && !/[\\/]_npx[\\/]/.test(d));
|
|
2541
2549
|
const names = platform === "win32" ? ["tendril.cmd", "tendril.bat"] : ["tendril"];
|
|
@@ -2557,11 +2565,17 @@ function packageRootOf(file) {
|
|
|
2557
2565
|
}
|
|
2558
2566
|
}
|
|
2559
2567
|
function resolveInvocation(pathEnv, platform, selfFile = fileURLToPath(import.meta.url)) {
|
|
2568
|
+
let self = null;
|
|
2569
|
+
try {
|
|
2570
|
+
self = packageRootOf(realpathSync(selfFile));
|
|
2571
|
+
} catch {
|
|
2572
|
+
self = null;
|
|
2573
|
+
}
|
|
2574
|
+
if (self !== null && isDevTree(self)) return DEV_INVOCATION;
|
|
2560
2575
|
const binary = findPathTendril(pathEnv, platform);
|
|
2561
2576
|
if (binary === null) return NPX_INVOCATION;
|
|
2562
2577
|
try {
|
|
2563
2578
|
const onPath = packageRootOf(realpathSync(binary));
|
|
2564
|
-
const self = packageRootOf(realpathSync(selfFile));
|
|
2565
2579
|
if (onPath === null || self === null) return NPX_INVOCATION;
|
|
2566
2580
|
return realpathSync(onPath) === realpathSync(self) ? "tendril" : NPX_INVOCATION;
|
|
2567
2581
|
} catch {
|
|
@@ -2579,11 +2593,12 @@ function quoteArg(value) {
|
|
|
2579
2593
|
if (/["`$]/.test(value)) throw new Error(`refusing to emit a shell argument containing a double quote, backtick or $: ${value}`);
|
|
2580
2594
|
return /[\s'*?[\]()&;|<>#~]/.test(value) ? `"${value}"` : value;
|
|
2581
2595
|
}
|
|
2582
|
-
var NPX_INVOCATION, cached;
|
|
2596
|
+
var NPX_INVOCATION, DEV_INVOCATION, cached;
|
|
2583
2597
|
var init_invocation = __esm({
|
|
2584
2598
|
"packages/cli/src/invocation.ts"() {
|
|
2585
2599
|
"use strict";
|
|
2586
2600
|
NPX_INVOCATION = "npx -y -p @tendrilapp/cli@latest tendril";
|
|
2601
|
+
DEV_INVOCATION = "pnpm --filter @tendrilapp/cli dev";
|
|
2587
2602
|
cached = null;
|
|
2588
2603
|
}
|
|
2589
2604
|
});
|
|
@@ -3308,6 +3323,7 @@ function absentInkClusters(render, reference, background = [255, 255, 255]) {
|
|
|
3308
3323
|
let x1 = sx;
|
|
3309
3324
|
let y1 = sy;
|
|
3310
3325
|
let px = 0;
|
|
3326
|
+
let chromePx = 0;
|
|
3311
3327
|
const stack = [si];
|
|
3312
3328
|
seen[si] = 1;
|
|
3313
3329
|
while (stack.length > 0) {
|
|
@@ -3315,6 +3331,8 @@ function absentInkClusters(render, reference, background = [255, 255, 255]) {
|
|
|
3315
3331
|
const cx = i % width;
|
|
3316
3332
|
const cy = (i - cx) / width;
|
|
3317
3333
|
px += 1;
|
|
3334
|
+
const ci = i * 4;
|
|
3335
|
+
if (canvasB.data[ci] === FIGMA_CHROME[0] && canvasB.data[ci + 1] === FIGMA_CHROME[1] && canvasB.data[ci + 2] === FIGMA_CHROME[2]) chromePx += 1;
|
|
3318
3336
|
if (cx < x0) x0 = cx;
|
|
3319
3337
|
if (cy < y0) y0 = cy;
|
|
3320
3338
|
if (cx > x1) x1 = cx;
|
|
@@ -3332,7 +3350,7 @@ function absentInkClusters(render, reference, background = [255, 255, 255]) {
|
|
|
3332
3350
|
}
|
|
3333
3351
|
}
|
|
3334
3352
|
}
|
|
3335
|
-
if (px >= ABSENT_MIN_PX) found.push({ x: x0, y: y0, w: x1 - x0 + 1, h: y1 - y0 + 1, px, memberX: sx, memberY: sy });
|
|
3353
|
+
if (px >= ABSENT_MIN_PX) found.push({ x: x0, y: y0, w: x1 - x0 + 1, h: y1 - y0 + 1, px, memberX: sx, memberY: sy, ...chromePx * 2 >= px ? { chrome: true } : {} });
|
|
3336
3354
|
}
|
|
3337
3355
|
}
|
|
3338
3356
|
}
|
|
@@ -3604,11 +3622,12 @@ function cropPng(png, x, y, width, height) {
|
|
|
3604
3622
|
}
|
|
3605
3623
|
return new Uint8Array(PNG.sync.write(out));
|
|
3606
3624
|
}
|
|
3607
|
-
var INK_DELTA, ABSENT_RADIUS, ABSENT_MIN_PX, DIFF_LEGEND;
|
|
3625
|
+
var INK_DELTA, FIGMA_CHROME, ABSENT_RADIUS, ABSENT_MIN_PX, DIFF_LEGEND, DIFF_LEGEND_TEXT;
|
|
3608
3626
|
var init_image_diff = __esm({
|
|
3609
3627
|
"packages/verify/src/image-diff.ts"() {
|
|
3610
3628
|
"use strict";
|
|
3611
3629
|
INK_DELTA = 30;
|
|
3630
|
+
FIGMA_CHROME = [138, 56, 245];
|
|
3612
3631
|
ABSENT_RADIUS = 3;
|
|
3613
3632
|
ABSENT_MIN_PX = 6;
|
|
3614
3633
|
DIFF_LEGEND = {
|
|
@@ -3623,8 +3642,45 @@ var init_image_diff = __esm({
|
|
|
3623
3642
|
* ALREADY FORGIVEN by inkRecall. Dimmed grey, so it reads as tolerated
|
|
3624
3643
|
* rather than as a defect to chase (measured at only 3.0% of red, but
|
|
3625
3644
|
* chasing it is exactly what the nudge saga was). */
|
|
3626
|
-
shift: [150, 150, 150]
|
|
3645
|
+
shift: [150, 150, 150],
|
|
3646
|
+
/** pixelmatch's OWN anti-aliasing colour, passed through unrepainted.
|
|
3647
|
+
* `includeAA` is false, so these pixels are painted but NOT counted as
|
|
3648
|
+
* mismatches — they are in neither the similarity number nor the ink
|
|
3649
|
+
* deficit. Listed here because the first field run measured 343-380 of
|
|
3650
|
+
* them per config and the reader had a fourth colour with no key
|
|
3651
|
+
* (CompTest run, 2026-08-19). Do not repaint it: passing pixelmatch's
|
|
3652
|
+
* own marking through unchanged is what keeps "not counted" honest. */
|
|
3653
|
+
antialias: [255, 255, 0]
|
|
3627
3654
|
};
|
|
3655
|
+
DIFF_LEGEND_TEXT = [
|
|
3656
|
+
"Tendril diff image legend \u2014 what each colour means.",
|
|
3657
|
+
"",
|
|
3658
|
+
"A verdict has TWO gates and this one image serves both, so the colour",
|
|
3659
|
+
"says WHICH number a pixel belongs to. Marked pixels are exactly the",
|
|
3660
|
+
"ones pixelmatch marks; only their colour carries the cause.",
|
|
3661
|
+
"",
|
|
3662
|
+
` #ff0000 RED reference ink the render never painted, and no render`,
|
|
3663
|
+
` ink within 1px. THE INK DEFICIT \u2014 the only category`,
|
|
3664
|
+
` that moves inkRecall.`,
|
|
3665
|
+
` #ffaa00 AMBER ink in BOTH images, different colour. Moves`,
|
|
3666
|
+
` similarity. CANNOT move inkRecall.`,
|
|
3667
|
+
` #0078ff BLUE render ink where the reference records none. Moves`,
|
|
3668
|
+
` similarity. Cannot move inkRecall.`,
|
|
3669
|
+
` #969696 GREY reference ink covered within 1px \u2014 a displacement`,
|
|
3670
|
+
` inkRecall ALREADY FORGIVES. Not a defect to chase.`,
|
|
3671
|
+
` #ffff00 YELLOW pixelmatch judged this anti-aliasing. Painted but`,
|
|
3672
|
+
` NOT COUNTED: in neither the similarity number nor`,
|
|
3673
|
+
` the ink deficit.`,
|
|
3674
|
+
"",
|
|
3675
|
+
"Raw marked-pixel count is a poor guide to cause: a field run measured a",
|
|
3676
|
+
"config where BLUE dominated 1797 to 270 while only the 270 red pixels",
|
|
3677
|
+
"demoted it.",
|
|
3678
|
+
"",
|
|
3679
|
+
"If red clusters sit OUTSIDE the recorded node box, suspect the",
|
|
3680
|
+
"reference rather than the code \u2014 Figma pads exports to contain outer",
|
|
3681
|
+
"effects, and the pad can capture the editor's own component-set chrome.",
|
|
3682
|
+
""
|
|
3683
|
+
].join("\n");
|
|
3628
3684
|
}
|
|
3629
3685
|
});
|
|
3630
3686
|
|
|
@@ -4309,7 +4365,7 @@ var init_font_collection = __esm({
|
|
|
4309
4365
|
});
|
|
4310
4366
|
|
|
4311
4367
|
// packages/verify/src/font-discovery.ts
|
|
4312
|
-
import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as
|
|
4368
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync6, realpathSync as realpathSync2 } from "node:fs";
|
|
4313
4369
|
import os2 from "node:os";
|
|
4314
4370
|
import path12 from "node:path";
|
|
4315
4371
|
function weightFromSubfamily(subfamily) {
|
|
@@ -4357,7 +4413,7 @@ function faceAt(bytes, view, dirOffset, file, faceIndex) {
|
|
|
4357
4413
|
}
|
|
4358
4414
|
function facesInFile(file) {
|
|
4359
4415
|
try {
|
|
4360
|
-
const bytes = new Uint8Array(
|
|
4416
|
+
const bytes = new Uint8Array(readFileSync6(file));
|
|
4361
4417
|
if (bytes.length < 12) return [];
|
|
4362
4418
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
4363
4419
|
if (isCollection(bytes)) {
|
|
@@ -4449,7 +4505,7 @@ var init_font_discovery = __esm({
|
|
|
4449
4505
|
|
|
4450
4506
|
// packages/verify/src/font-resolve.ts
|
|
4451
4507
|
import { createHash as createHash2 } from "node:crypto";
|
|
4452
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as
|
|
4508
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4453
4509
|
import os3 from "node:os";
|
|
4454
4510
|
import path13 from "node:path";
|
|
4455
4511
|
function fontCacheDir() {
|
|
@@ -4528,7 +4584,7 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
4528
4584
|
}
|
|
4529
4585
|
}
|
|
4530
4586
|
const mPath = path13.join(cacheDir, "manifest.json");
|
|
4531
|
-
const prior = existsSync9(mPath) ? JSON.parse(
|
|
4587
|
+
const prior = existsSync9(mPath) ? JSON.parse(readFileSync7(mPath, "utf8")) : [];
|
|
4532
4588
|
const portable2 = resolved.map((m) => ({ ...m, file: path13.basename(m.file) }));
|
|
4533
4589
|
const merged = [...prior.filter((p) => !resolved.some((m) => m.family === p.family && m.weight === p.weight)), ...portable2];
|
|
4534
4590
|
if (resolved.length > 0) writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
@@ -4542,7 +4598,7 @@ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, f
|
|
|
4542
4598
|
if (![".woff2", ".woff", ".ttf", ".otf", ".ttc"].includes(ext)) {
|
|
4543
4599
|
throw new Error(`unsupported font file "${ext}" \u2014 use .woff2 (preferred), .woff, .ttf, .otf or .ttc`);
|
|
4544
4600
|
}
|
|
4545
|
-
let bytes = new Uint8Array(
|
|
4601
|
+
let bytes = new Uint8Array(readFileSync7(src));
|
|
4546
4602
|
if (bytes.length === 0) throw new Error(`font file is empty: ${src}`);
|
|
4547
4603
|
let storedExt = ext;
|
|
4548
4604
|
if (isCollection(bytes)) {
|
|
@@ -4565,16 +4621,16 @@ ${shown}`
|
|
|
4565
4621
|
writeFileSync3(file, bytes);
|
|
4566
4622
|
const face = { family, weight, source: `${provenance}:${path13.basename(src)}`, sha256, file, license: "unknown" };
|
|
4567
4623
|
const mPath = path13.join(cacheDir, "manifest.json");
|
|
4568
|
-
const prior = existsSync9(mPath) ? JSON.parse(
|
|
4624
|
+
const prior = existsSync9(mPath) ? JSON.parse(readFileSync7(mPath, "utf8")) : [];
|
|
4569
4625
|
const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file: path13.basename(file) }];
|
|
4570
4626
|
writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
4571
4627
|
`);
|
|
4572
4628
|
return face;
|
|
4573
4629
|
}
|
|
4574
4630
|
function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
4575
|
-
const lock = JSON.parse(
|
|
4631
|
+
const lock = JSON.parse(readFileSync7(lockPath, "utf8"));
|
|
4576
4632
|
const mPath = path13.join(cacheDir, "manifest.json");
|
|
4577
|
-
const manifest = existsSync9(mPath) ? JSON.parse(
|
|
4633
|
+
const manifest = existsSync9(mPath) ? JSON.parse(readFileSync7(mPath, "utf8")) : [];
|
|
4578
4634
|
return lock.map((l) => {
|
|
4579
4635
|
const m = manifest.find((x) => x.family === l.family && x.weight === l.weight);
|
|
4580
4636
|
if (m === void 0) return { family: l.family, weight: l.weight, status: "missing" };
|
|
@@ -4586,7 +4642,7 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
4586
4642
|
if (!existsSync9(mPath)) return [];
|
|
4587
4643
|
let entries;
|
|
4588
4644
|
try {
|
|
4589
|
-
entries = JSON.parse(
|
|
4645
|
+
entries = JSON.parse(readFileSync7(mPath, "utf8"));
|
|
4590
4646
|
} catch {
|
|
4591
4647
|
return [];
|
|
4592
4648
|
}
|
|
@@ -4601,7 +4657,7 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
4601
4657
|
}
|
|
4602
4658
|
function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
4603
4659
|
const mPath = path13.join(cacheDir, "manifest.json");
|
|
4604
|
-
const manifest = existsSync9(mPath) ? JSON.parse(
|
|
4660
|
+
const manifest = existsSync9(mPath) ? JSON.parse(readFileSync7(mPath, "utf8")) : [];
|
|
4605
4661
|
const wanted = new Set(families.map((f) => f.toLowerCase()));
|
|
4606
4662
|
return manifest.filter((f) => wanted.has(f.family.toLowerCase())).map((f) => ({
|
|
4607
4663
|
family: f.family,
|
|
@@ -4618,7 +4674,7 @@ function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
4618
4674
|
if (!existsSync9(mPath)) return [];
|
|
4619
4675
|
let entries;
|
|
4620
4676
|
try {
|
|
4621
|
-
entries = JSON.parse(
|
|
4677
|
+
entries = JSON.parse(readFileSync7(mPath, "utf8"));
|
|
4622
4678
|
} catch {
|
|
4623
4679
|
return [];
|
|
4624
4680
|
}
|
|
@@ -4631,7 +4687,7 @@ function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
4631
4687
|
if (!existsSync9(mPath)) return [];
|
|
4632
4688
|
let entries;
|
|
4633
4689
|
try {
|
|
4634
|
-
entries = JSON.parse(
|
|
4690
|
+
entries = JSON.parse(readFileSync7(mPath, "utf8"));
|
|
4635
4691
|
} catch {
|
|
4636
4692
|
return [];
|
|
4637
4693
|
}
|
|
@@ -4640,7 +4696,7 @@ function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
4640
4696
|
if (typeof e.family !== "string" || typeof e.weight !== "number" || typeof e.file !== "string") continue;
|
|
4641
4697
|
const file = path13.isAbsolute(e.file) && existsSync9(e.file) ? e.file : path13.resolve(cacheDir, path13.basename(e.file));
|
|
4642
4698
|
if (!existsSync9(file)) continue;
|
|
4643
|
-
if (createHash2("sha256").update(
|
|
4699
|
+
if (createHash2("sha256").update(readFileSync7(file)).digest("hex") !== e.sha256) continue;
|
|
4644
4700
|
const set = byFamily.get(e.family) ?? /* @__PURE__ */ new Set();
|
|
4645
4701
|
set.add(e.weight);
|
|
4646
4702
|
byFamily.set(e.family, set);
|
|
@@ -4662,7 +4718,7 @@ function addSystemFamily(family, opts = {}) {
|
|
|
4662
4718
|
const overwrote = [];
|
|
4663
4719
|
const cacheDir = opts.cacheDir ?? DEFAULT_FONT_CACHE;
|
|
4664
4720
|
const manifestFile = path13.join(cacheDir, "manifest.json");
|
|
4665
|
-
const prior = existsSync9(manifestFile) ? JSON.parse(
|
|
4721
|
+
const prior = existsSync9(manifestFile) ? JSON.parse(readFileSync7(manifestFile, "utf8")) : [];
|
|
4666
4722
|
const taken = /* @__PURE__ */ new Set();
|
|
4667
4723
|
for (const face of faces) {
|
|
4668
4724
|
const skip = (reason) => skipped.push({ subfamily: face.subfamily, weight: face.weight, reason });
|
|
@@ -4724,17 +4780,17 @@ var init_font_resolve = __esm({
|
|
|
4724
4780
|
|
|
4725
4781
|
// packages/verify/src/font-faces.ts
|
|
4726
4782
|
import { createHash as createHash3 } from "node:crypto";
|
|
4727
|
-
import { existsSync as existsSync10, readFileSync as
|
|
4783
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
|
|
4728
4784
|
import path14 from "node:path";
|
|
4729
4785
|
function injectedGroups(manifestPath2) {
|
|
4730
4786
|
if (!existsSync10(manifestPath2)) return { groups: [], shared: false };
|
|
4731
|
-
const claimed = JSON.parse(
|
|
4787
|
+
const claimed = JSON.parse(readFileSync8(manifestPath2, "utf8"));
|
|
4732
4788
|
const resolveFile = (f) => path14.isAbsolute(f) && existsSync10(f) ? f : path14.resolve(path14.dirname(manifestPath2), path14.basename(f));
|
|
4733
4789
|
const byFile = /* @__PURE__ */ new Map();
|
|
4734
4790
|
for (const f of claimed) {
|
|
4735
4791
|
const file = resolveFile(f.file);
|
|
4736
4792
|
if (!existsSync10(file)) continue;
|
|
4737
|
-
if (createHash3("sha256").update(
|
|
4793
|
+
if (createHash3("sha256").update(readFileSync8(file)).digest("hex") !== f.sha256) continue;
|
|
4738
4794
|
const k = `${f.family}:${f.file}`;
|
|
4739
4795
|
const e = byFile.get(k) ?? { family: f.family, weights: [], file };
|
|
4740
4796
|
e.weights.push(f.weight);
|
|
@@ -4747,7 +4803,7 @@ function fontFaceCss(manifestPath2 = path14.join(fontCacheDir(), "manifest.json"
|
|
|
4747
4803
|
const { groups, shared } = injectedGroups(manifestPath2);
|
|
4748
4804
|
return groups.map((e) => {
|
|
4749
4805
|
const weight = shared || e.weights.length > 1 ? `${SPAN[0]} ${SPAN[1]}` : String(e.weights[0]);
|
|
4750
|
-
return `@font-face { font-family: '${e.family}'; font-weight: ${weight}; font-style: normal; src: url(data:font/woff2;base64,${
|
|
4806
|
+
return `@font-face { font-family: '${e.family}'; font-weight: ${weight}; font-style: normal; src: url(data:font/woff2;base64,${readFileSync8(e.file).toString("base64")}) format('woff2'); }`;
|
|
4751
4807
|
}).join("\n");
|
|
4752
4808
|
}
|
|
4753
4809
|
function injectedFamilyWeights(manifestPath2 = path14.join(fontCacheDir(), "manifest.json")) {
|
|
@@ -4775,7 +4831,7 @@ var init_font_faces = __esm({
|
|
|
4775
4831
|
});
|
|
4776
4832
|
|
|
4777
4833
|
// packages/verify/src/admission.ts
|
|
4778
|
-
import { readFileSync as
|
|
4834
|
+
import { readFileSync as readFileSync9, readdirSync as readdirSync4, existsSync as existsSync11, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4779
4835
|
import path15 from "node:path";
|
|
4780
4836
|
import { build as build2 } from "esbuild";
|
|
4781
4837
|
import postcss from "postcss";
|
|
@@ -4785,7 +4841,7 @@ function fontWeightsByFamily() {
|
|
|
4785
4841
|
const mPath = path15.join(fontCacheDir(), "manifest.json");
|
|
4786
4842
|
const out = /* @__PURE__ */ new Map();
|
|
4787
4843
|
if (!existsSync11(mPath)) return out;
|
|
4788
|
-
for (const f of JSON.parse(
|
|
4844
|
+
for (const f of JSON.parse(readFileSync9(mPath, "utf8")))
|
|
4789
4845
|
out.set(f.family, [...out.get(f.family) ?? [], f.weight]);
|
|
4790
4846
|
return out;
|
|
4791
4847
|
}
|
|
@@ -4805,7 +4861,7 @@ var init_admission = __esm({
|
|
|
4805
4861
|
});
|
|
4806
4862
|
|
|
4807
4863
|
// packages/verify/src/candidate-css.ts
|
|
4808
|
-
import { existsSync as existsSync12, readFileSync as
|
|
4864
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
|
|
4809
4865
|
import path16 from "node:path";
|
|
4810
4866
|
function candidateCss(bundleDir) {
|
|
4811
4867
|
const files = ["tokens.css", "styles.css"].map((f) => path16.join(bundleDir, f));
|
|
@@ -4824,7 +4880,7 @@ function candidateCss(bundleDir) {
|
|
|
4824
4880
|
}
|
|
4825
4881
|
files.push(path16.join(dir, "tokens.css"), path16.join(dir, "styles.css"));
|
|
4826
4882
|
}
|
|
4827
|
-
return files.filter((f) => existsSync12(f)).map((f) =>
|
|
4883
|
+
return files.filter((f) => existsSync12(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
|
|
4828
4884
|
}
|
|
4829
4885
|
var init_candidate_css = __esm({
|
|
4830
4886
|
"packages/verify/src/candidate-css.ts"() {
|
|
@@ -5042,7 +5098,7 @@ __export(behavior_exports, {
|
|
|
5042
5098
|
compileMount: () => compileMount,
|
|
5043
5099
|
recordingIsDark: () => recordingIsDark
|
|
5044
5100
|
});
|
|
5045
|
-
import { existsSync as existsSync13, readFileSync as
|
|
5101
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11 } from "node:fs";
|
|
5046
5102
|
import path18 from "node:path";
|
|
5047
5103
|
import { build as build3 } from "esbuild";
|
|
5048
5104
|
import { chromium as chromium3 } from "playwright-core";
|
|
@@ -5118,8 +5174,8 @@ async function runSteps(page, spec, renderPose) {
|
|
|
5118
5174
|
if (renderPose === void 0) return { id: spec.id, pass: false, detail: "commitMatchesPose requires a pose renderer" };
|
|
5119
5175
|
await settle(page);
|
|
5120
5176
|
const idle = await shotRoot(page);
|
|
5121
|
-
for (const
|
|
5122
|
-
await page.waitForTimeout(
|
|
5177
|
+
for (const delay2 of [70, 130, 190]) {
|
|
5178
|
+
await page.waitForTimeout(delay2);
|
|
5123
5179
|
await freezeAnimations(page);
|
|
5124
5180
|
if (!(await shotRoot(page)).equals(idle)) {
|
|
5125
5181
|
return { id: spec.id, pass: false, detail: "unmeasurable: the component's pixels change at rest (free-running animation or a timer in YOUR candidate) \u2014 the committed state cannot be observed through it; this IS fixable from code: keep every scored pose reachable with all motion idle (freezable CSS animation, no JS-driven repaints)" };
|
|
@@ -5414,7 +5470,7 @@ function recordingIsDark(task) {
|
|
|
5414
5470
|
const f = path18.join(task.set, rep, "get_screenshot.json");
|
|
5415
5471
|
if (!existsSync13(f)) return false;
|
|
5416
5472
|
try {
|
|
5417
|
-
const env = JSON.parse(
|
|
5473
|
+
const env = JSON.parse(readFileSync11(f, "utf8")).content.find((c) => c.type === "image");
|
|
5418
5474
|
if (env?.data === void 0) return false;
|
|
5419
5475
|
const png = PNG2.sync.read(Buffer.from(env.data, "base64"));
|
|
5420
5476
|
let sum = 0;
|
|
@@ -6030,6 +6086,11 @@ var init_bundle = __esm({
|
|
|
6030
6086
|
rep: z9.string(),
|
|
6031
6087
|
similarity: z9.number(),
|
|
6032
6088
|
inkRecall: z9.number(),
|
|
6089
|
+
/** Pre-deadband measured values. Declared so the field SURVIVES the
|
|
6090
|
+
* schema round-trip — the emitter writes it (bundle-emit) and zod was
|
|
6091
|
+
* silently stripping it back out, so a reader could not tell a
|
|
6092
|
+
* rounded 0.970 from a measured one. */
|
|
6093
|
+
exact: z9.object({ similarity: z9.number(), inkRecall: z9.number() }).optional(),
|
|
6033
6094
|
status: ConfigStatusSchema
|
|
6034
6095
|
});
|
|
6035
6096
|
BehaviorClaimSchema = z9.object({
|
|
@@ -6051,7 +6112,18 @@ var init_bundle = __esm({
|
|
|
6051
6112
|
licenseNote: z9.string().optional(),
|
|
6052
6113
|
/** Content hash over the set manifest + rep envelopes (the identity
|
|
6053
6114
|
* verify compares against, not the path). */
|
|
6054
|
-
hash: z9.string()
|
|
6115
|
+
hash: z9.string(),
|
|
6116
|
+
/** KIT IDENTITY (PROV-1). The Figma file key and component-set node
|
|
6117
|
+
* the recording came from. Both are captured at plan time in the
|
|
6118
|
+
* recording set and were being dropped here, which left a published
|
|
6119
|
+
* bundle unable to say which kit it derives from — while the portal's
|
|
6120
|
+
* consent model is scoped to exactly that file key (ADR-018 §3), and
|
|
6121
|
+
* the right to publish attaches to a kit rather than to a folder
|
|
6122
|
+
* someone named. Optional: recordings planned before this field
|
|
6123
|
+
* existed carry neither, and a set can be a handed-in selection with
|
|
6124
|
+
* no component set at all. Absent means NOT RECORDED, never "no kit". */
|
|
6125
|
+
figmaFile: z9.string().optional(),
|
|
6126
|
+
componentSetNode: z9.string().optional()
|
|
6055
6127
|
}),
|
|
6056
6128
|
environment: z9.object({
|
|
6057
6129
|
chrome: z9.string(),
|
|
@@ -6100,6 +6172,198 @@ var init_bundle = __esm({
|
|
|
6100
6172
|
}
|
|
6101
6173
|
});
|
|
6102
6174
|
|
|
6175
|
+
// packages/metadata/src/published-surface.ts
|
|
6176
|
+
function classifyBundleSurface(files, opts) {
|
|
6177
|
+
const published = [];
|
|
6178
|
+
const excluded = [];
|
|
6179
|
+
const unknown = [];
|
|
6180
|
+
for (const raw of files) {
|
|
6181
|
+
const path54 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6182
|
+
const inEvidence = path54.startsWith(`${EVIDENCE_DIR}/`);
|
|
6183
|
+
const name = inEvidence ? path54.slice(EVIDENCE_DIR.length + 1) : path54;
|
|
6184
|
+
if (name.includes("/")) {
|
|
6185
|
+
unknown.push(path54);
|
|
6186
|
+
continue;
|
|
6187
|
+
}
|
|
6188
|
+
if (inEvidence) {
|
|
6189
|
+
if (name === "verify-report.json") published.push({ path: path54, role: "verify-report" });
|
|
6190
|
+
else if (name === "diff-legend.txt") published.push({ path: path54, role: "diff-legend" });
|
|
6191
|
+
else if (name === "inspect.html") published.push({ path: path54, role: "inspect-sheet" });
|
|
6192
|
+
else {
|
|
6193
|
+
const hit = EVIDENCE_PATTERNS.find((p) => p.re.test(name));
|
|
6194
|
+
if (hit !== void 0) published.push({ path: path54, role: hit.role });
|
|
6195
|
+
else unknown.push(path54);
|
|
6196
|
+
}
|
|
6197
|
+
continue;
|
|
6198
|
+
}
|
|
6199
|
+
if (name === opts.entry) published.push({ path: path54, role: "entry" });
|
|
6200
|
+
else if (name === "styles.css") published.push({ path: path54, role: "styles" });
|
|
6201
|
+
else if (name === "tokens.css") published.push({ path: path54, role: "tokens" });
|
|
6202
|
+
else if (name === "fonts.css") published.push({ path: path54, role: "fonts" });
|
|
6203
|
+
else if (name === "component.json") published.push({ path: path54, role: "manifest" });
|
|
6204
|
+
else {
|
|
6205
|
+
const skip = EXCLUDED.find((e) => e.test(name));
|
|
6206
|
+
if (skip !== void 0) excluded.push({ path: path54, reason: skip.reason });
|
|
6207
|
+
else unknown.push(path54);
|
|
6208
|
+
}
|
|
6209
|
+
}
|
|
6210
|
+
const roles = new Set(published.map((p) => p.role));
|
|
6211
|
+
return { published, excluded, unknown, missingRequired: REQUIRED_ROLES.filter((r) => !roles.has(r)) };
|
|
6212
|
+
}
|
|
6213
|
+
function missingInspectCrops(sheetText, publishedPaths) {
|
|
6214
|
+
const held = new Set(publishedPaths);
|
|
6215
|
+
const missing = /* @__PURE__ */ new Set();
|
|
6216
|
+
for (const [, name] of sheetText.matchAll(/src="\.\/([^"]+)"/g)) {
|
|
6217
|
+
const path54 = `${EVIDENCE_DIR}/${name}`;
|
|
6218
|
+
if (!held.has(path54)) missing.add(path54);
|
|
6219
|
+
}
|
|
6220
|
+
return [...missing].sort();
|
|
6221
|
+
}
|
|
6222
|
+
var EVIDENCE_DIR, REQUIRED_ROLES, EXCLUDED, EVIDENCE_PATTERNS, SCORED_FILE_ROLES;
|
|
6223
|
+
var init_published_surface = __esm({
|
|
6224
|
+
"packages/metadata/src/published-surface.ts"() {
|
|
6225
|
+
"use strict";
|
|
6226
|
+
EVIDENCE_DIR = "verify-evidence";
|
|
6227
|
+
REQUIRED_ROLES = ["entry", "styles", "manifest", "verify-report", "evidence-triple"];
|
|
6228
|
+
EXCLUDED = [
|
|
6229
|
+
{ test: (n) => n === "score-history.jsonl", reason: "generation loop score history" },
|
|
6230
|
+
{ test: (n) => n === "loop-state.json", reason: "generation loop state" },
|
|
6231
|
+
{ test: (n) => n === "run-log.json", reason: "generation run log" },
|
|
6232
|
+
{ test: (n) => n.endsWith(".md"), reason: "brief / debrief / model response" },
|
|
6233
|
+
{ test: (n) => n === ".DS_Store", reason: "macOS directory metadata" }
|
|
6234
|
+
];
|
|
6235
|
+
EVIDENCE_PATTERNS = [
|
|
6236
|
+
{ re: /^.+-absent-\d+-(?:ref|render)\.png$/, role: "evidence-absent-crop" },
|
|
6237
|
+
{ re: /^.+-inspect-\d+-(?:ref|render)\.png$/, role: "evidence-zoom-crop" },
|
|
6238
|
+
{ re: /^.+-(?:ref|render|diff)\.png$/, role: "evidence-triple" }
|
|
6239
|
+
];
|
|
6240
|
+
SCORED_FILE_ROLES = /* @__PURE__ */ new Set([
|
|
6241
|
+
"entry",
|
|
6242
|
+
"styles",
|
|
6243
|
+
"tokens",
|
|
6244
|
+
"fonts",
|
|
6245
|
+
"manifest",
|
|
6246
|
+
"evidence-triple",
|
|
6247
|
+
"evidence-absent-crop",
|
|
6248
|
+
"diff-legend"
|
|
6249
|
+
]);
|
|
6250
|
+
}
|
|
6251
|
+
});
|
|
6252
|
+
|
|
6253
|
+
// packages/metadata/src/bundle-files.ts
|
|
6254
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
6255
|
+
import { readFileSync as readFileSync12, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
|
|
6256
|
+
import path19 from "node:path";
|
|
6257
|
+
function bundleFiles(dir, prefix = "") {
|
|
6258
|
+
const out = [];
|
|
6259
|
+
for (const entry of readdirSync6(dir).sort()) {
|
|
6260
|
+
const full = path19.join(dir, entry);
|
|
6261
|
+
const rel = prefix === "" ? entry : `${prefix}/${entry}`;
|
|
6262
|
+
if (statSync3(full).isDirectory()) out.push(...bundleFiles(full, rel));
|
|
6263
|
+
else out.push(rel);
|
|
6264
|
+
}
|
|
6265
|
+
return out;
|
|
6266
|
+
}
|
|
6267
|
+
function digestScoredFiles(bundleDir, entry) {
|
|
6268
|
+
const surface = classifyBundleSurface(bundleFiles(bundleDir), { entry });
|
|
6269
|
+
const out = {};
|
|
6270
|
+
for (const file of surface.published) {
|
|
6271
|
+
if (!SCORED_FILE_ROLES.has(file.role)) continue;
|
|
6272
|
+
out[file.path] = createHash4("sha256").update(readFileSync12(path19.join(bundleDir, file.path))).digest("hex");
|
|
6273
|
+
}
|
|
6274
|
+
return out;
|
|
6275
|
+
}
|
|
6276
|
+
var init_bundle_files = __esm({
|
|
6277
|
+
"packages/metadata/src/bundle-files.ts"() {
|
|
6278
|
+
"use strict";
|
|
6279
|
+
init_published_surface();
|
|
6280
|
+
}
|
|
6281
|
+
});
|
|
6282
|
+
|
|
6283
|
+
// packages/metadata/src/verify-report.ts
|
|
6284
|
+
function verifyReportForTransport(report, bundleName) {
|
|
6285
|
+
const { dir: _dir, ...evidence } = report.evidence;
|
|
6286
|
+
const { eyeCheck: _eyeCheck, ...rest } = report;
|
|
6287
|
+
return { ...rest, bundle: bundleName, evidence };
|
|
6288
|
+
}
|
|
6289
|
+
function nonEmptyArray(value) {
|
|
6290
|
+
return Array.isArray(value) && value.length > 0;
|
|
6291
|
+
}
|
|
6292
|
+
function undisclosedTrustFacts(report) {
|
|
6293
|
+
if (report === null || typeof report !== "object" || Array.isArray(report)) return [];
|
|
6294
|
+
const r = report;
|
|
6295
|
+
const facts = [];
|
|
6296
|
+
if (r["recordingSetDrift"] !== void 0 && r["recordingSetDrift"] !== null) {
|
|
6297
|
+
facts.push({
|
|
6298
|
+
pointer: "$.recordingSetDrift",
|
|
6299
|
+
consequence: "the recording set that was scored is not the one the bundle stamps, so these scores describe a different design than the bundle claims"
|
|
6300
|
+
});
|
|
6301
|
+
}
|
|
6302
|
+
if (nonEmptyArray(r["substitutedFamilies"])) {
|
|
6303
|
+
facts.push({
|
|
6304
|
+
pointer: "$.substitutedFamilies",
|
|
6305
|
+
consequence: "a font family was substituted, so the scores measure a substitute face and no config can be certified under one"
|
|
6306
|
+
});
|
|
6307
|
+
}
|
|
6308
|
+
if (nonEmptyArray(r["certBlockedByAbsentInk"])) {
|
|
6309
|
+
facts.push({
|
|
6310
|
+
pointer: "$.certBlockedByAbsentInk",
|
|
6311
|
+
consequence: "certification was blocked by absent ink, which a verdict alone does not say"
|
|
6312
|
+
});
|
|
6313
|
+
}
|
|
6314
|
+
return facts;
|
|
6315
|
+
}
|
|
6316
|
+
function isSetHash(value) {
|
|
6317
|
+
return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
|
|
6318
|
+
}
|
|
6319
|
+
function readScoredFiles(report) {
|
|
6320
|
+
if (report === null || typeof report !== "object" || Array.isArray(report)) return void 0;
|
|
6321
|
+
const value = report["scoredFiles"];
|
|
6322
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
6323
|
+
const entries = Object.entries(value);
|
|
6324
|
+
if (entries.length === 0) return void 0;
|
|
6325
|
+
const out = {};
|
|
6326
|
+
for (const [path54, digest] of entries) {
|
|
6327
|
+
if (path54 === "" || path54.startsWith("/") || path54.includes("..")) return void 0;
|
|
6328
|
+
if (!isSetHash(digest)) return void 0;
|
|
6329
|
+
out[path54] = digest;
|
|
6330
|
+
}
|
|
6331
|
+
return out;
|
|
6332
|
+
}
|
|
6333
|
+
function compareScoredFiles(recorded, actual) {
|
|
6334
|
+
const missing = [];
|
|
6335
|
+
const unscored = [];
|
|
6336
|
+
const changed = [];
|
|
6337
|
+
for (const [path54, digest] of Object.entries(recorded)) {
|
|
6338
|
+
if (!(path54 in actual)) missing.push(path54);
|
|
6339
|
+
else if (actual[path54] !== digest) changed.push(path54);
|
|
6340
|
+
}
|
|
6341
|
+
for (const path54 of Object.keys(actual)) if (!(path54 in recorded)) unscored.push(path54);
|
|
6342
|
+
return { missing: missing.sort(), unscored: unscored.sort(), changed: changed.sort() };
|
|
6343
|
+
}
|
|
6344
|
+
function scoredRecordingSetHash(report) {
|
|
6345
|
+
if (report === null || typeof report !== "object" || Array.isArray(report)) return void 0;
|
|
6346
|
+
const r = report;
|
|
6347
|
+
const drift = r["recordingSetDrift"];
|
|
6348
|
+
if (drift !== null && typeof drift === "object" && !Array.isArray(drift)) {
|
|
6349
|
+
const current = drift["current"];
|
|
6350
|
+
if (isSetHash(current)) return current;
|
|
6351
|
+
}
|
|
6352
|
+
const manifest = r["bundleManifest"];
|
|
6353
|
+
if (manifest !== null && typeof manifest === "object" && !Array.isArray(manifest)) {
|
|
6354
|
+
const stamped = manifest["recordingSetHash"];
|
|
6355
|
+
if (isSetHash(stamped)) return stamped;
|
|
6356
|
+
}
|
|
6357
|
+
return void 0;
|
|
6358
|
+
}
|
|
6359
|
+
var VERIFY_REPORT_FILENAME;
|
|
6360
|
+
var init_verify_report = __esm({
|
|
6361
|
+
"packages/metadata/src/verify-report.ts"() {
|
|
6362
|
+
"use strict";
|
|
6363
|
+
VERIFY_REPORT_FILENAME = "verify-report.json";
|
|
6364
|
+
}
|
|
6365
|
+
});
|
|
6366
|
+
|
|
6103
6367
|
// packages/metadata/src/motion-css.ts
|
|
6104
6368
|
var MOTION_CSS_PATTERN, scannableCss, MOTION_TOKEN_NAMES;
|
|
6105
6369
|
var init_motion_css = __esm({
|
|
@@ -6325,14 +6589,17 @@ var init_src4 = __esm({
|
|
|
6325
6589
|
init_extract();
|
|
6326
6590
|
init_recording_set();
|
|
6327
6591
|
init_bundle();
|
|
6592
|
+
init_bundle_files();
|
|
6593
|
+
init_verify_report();
|
|
6594
|
+
init_published_surface();
|
|
6328
6595
|
init_motion_css();
|
|
6329
6596
|
init_profile();
|
|
6330
6597
|
}
|
|
6331
6598
|
});
|
|
6332
6599
|
|
|
6333
6600
|
// packages/verify/src/bundle-quality.ts
|
|
6334
|
-
import { readFileSync as
|
|
6335
|
-
import
|
|
6601
|
+
import { readFileSync as readFileSync13, readdirSync as readdirSync7, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync14 } from "node:fs";
|
|
6602
|
+
import path20 from "node:path";
|
|
6336
6603
|
function unscopedReduceBlockFindings(file, css) {
|
|
6337
6604
|
const stripped = css.replace(/\/\*[\s\S]*?\*\//g, (c) => c.replace(/[^\n]/g, " "));
|
|
6338
6605
|
const media = /@media[^{]*prefers-reduced-motion\s*:\s*reduce[^{]*\{/.exec(stripped);
|
|
@@ -6390,16 +6657,16 @@ function recordedAssetFindings(entryFile, entrySource, recordedPaths) {
|
|
|
6390
6657
|
function recordedAssetPaths(setDir, reps) {
|
|
6391
6658
|
const map = /* @__PURE__ */ new Map();
|
|
6392
6659
|
for (const rep of reps) {
|
|
6393
|
-
const dir =
|
|
6660
|
+
const dir = path20.join(setDir, rep);
|
|
6394
6661
|
let files;
|
|
6395
6662
|
try {
|
|
6396
|
-
files =
|
|
6663
|
+
files = readdirSync7(dir).filter((f) => f.startsWith("asset-") && f.endsWith(".svg"));
|
|
6397
6664
|
} catch {
|
|
6398
6665
|
continue;
|
|
6399
6666
|
}
|
|
6400
6667
|
for (const f of files) {
|
|
6401
6668
|
try {
|
|
6402
|
-
const svg =
|
|
6669
|
+
const svg = readFileSync13(path20.join(dir, f), "utf8");
|
|
6403
6670
|
for (const m of svg.matchAll(/\sd="([^"]+)"/g)) {
|
|
6404
6671
|
const d = m[1];
|
|
6405
6672
|
const owners = map.get(d) ?? [];
|
|
@@ -6467,17 +6734,17 @@ function recordedTokenMapState(setDir, reps) {
|
|
|
6467
6734
|
const readMap = (file) => {
|
|
6468
6735
|
if (!existsSync14(file)) return void 0;
|
|
6469
6736
|
try {
|
|
6470
|
-
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(
|
|
6737
|
+
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync13(file, "utf8"))) || "{}");
|
|
6471
6738
|
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
6472
6739
|
} catch {
|
|
6473
6740
|
return {};
|
|
6474
6741
|
}
|
|
6475
6742
|
};
|
|
6476
|
-
const setLevel = readMap(
|
|
6743
|
+
const setLevel = readMap(path20.join(setDir, "get_variable_defs.json"));
|
|
6477
6744
|
if (setLevel !== void 0) return Object.keys(setLevel).length === 0 ? "empty" : "populated";
|
|
6478
6745
|
let recorded = false;
|
|
6479
6746
|
for (const rep of reps) {
|
|
6480
|
-
const m = readMap(
|
|
6747
|
+
const m = readMap(path20.join(setDir, rep, "get_variable_defs.json"));
|
|
6481
6748
|
if (m === void 0) continue;
|
|
6482
6749
|
recorded = true;
|
|
6483
6750
|
if (Object.keys(m).length > 0) return "populated";
|
|
@@ -6489,7 +6756,7 @@ function recordedTokensByValue(setDir, reps) {
|
|
|
6489
6756
|
const readMap = (file) => {
|
|
6490
6757
|
if (!existsSync14(file)) return void 0;
|
|
6491
6758
|
try {
|
|
6492
|
-
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(
|
|
6759
|
+
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync13(file, "utf8"))) || "{}");
|
|
6493
6760
|
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
6494
6761
|
} catch {
|
|
6495
6762
|
return void 0;
|
|
@@ -6507,8 +6774,8 @@ function recordedTokensByValue(setDir, reps) {
|
|
|
6507
6774
|
byValue.set(key, candidates);
|
|
6508
6775
|
}
|
|
6509
6776
|
};
|
|
6510
|
-
absorb(readMap(
|
|
6511
|
-
for (const rep of reps) absorb(readMap(
|
|
6777
|
+
absorb(readMap(path20.join(setDir, "get_variable_defs.json")));
|
|
6778
|
+
for (const rep of reps) absorb(readMap(path20.join(setDir, rep, "get_variable_defs.json")));
|
|
6512
6779
|
return byValue;
|
|
6513
6780
|
}
|
|
6514
6781
|
function scannable(css) {
|
|
@@ -6617,11 +6884,11 @@ function conventionsFindings(entrySource, css, profile) {
|
|
|
6617
6884
|
}
|
|
6618
6885
|
async function checkBundleQuality(bundleDir, entry, set, fontManifest, motion, contract, profile) {
|
|
6619
6886
|
const findings = [];
|
|
6620
|
-
const entryPath =
|
|
6621
|
-
const cssPath =
|
|
6622
|
-
const tokensPath =
|
|
6623
|
-
const css = existsSync14(cssPath) ?
|
|
6624
|
-
const tokensCss = existsSync14(tokensPath) ?
|
|
6887
|
+
const entryPath = path20.join(bundleDir, entry);
|
|
6888
|
+
const cssPath = path20.join(bundleDir, "styles.css");
|
|
6889
|
+
const tokensPath = path20.join(bundleDir, "tokens.css");
|
|
6890
|
+
const css = existsSync14(cssPath) ? readFileSync13(cssPath, "utf8") : "";
|
|
6891
|
+
const tokensCss = existsSync14(tokensPath) ? readFileSync13(tokensPath, "utf8") : void 0;
|
|
6625
6892
|
findings.push(
|
|
6626
6893
|
...fontStackFindings(
|
|
6627
6894
|
[
|
|
@@ -6634,8 +6901,8 @@ async function checkBundleQuality(bundleDir, entry, set, fontManifest, motion, c
|
|
|
6634
6901
|
if (existsSync14(entryPath)) {
|
|
6635
6902
|
const workDir = newScratchDir("quality");
|
|
6636
6903
|
try {
|
|
6637
|
-
const tsxPath =
|
|
6638
|
-
writeFileSync5(tsxPath,
|
|
6904
|
+
const tsxPath = path20.join(workDir, entry);
|
|
6905
|
+
writeFileSync5(tsxPath, readFileSync13(entryPath, "utf8"));
|
|
6639
6906
|
for (const d of runTscStrict([tsxPath]).diagnostics) {
|
|
6640
6907
|
if (d.code === 2307 && /['"]\.\/composed\//.test(d.message)) continue;
|
|
6641
6908
|
findings.push({ kind: "tsc", file: entry, ...d.line === void 0 ? {} : { line: d.line }, message: `TS${d.code}: ${d.message}` });
|
|
@@ -6645,7 +6912,7 @@ async function checkBundleQuality(bundleDir, entry, set, fontManifest, motion, c
|
|
|
6645
6912
|
}
|
|
6646
6913
|
}
|
|
6647
6914
|
if (contract !== void 0 && existsSync14(entryPath)) {
|
|
6648
|
-
findings.push(...expertLensFindings(entry,
|
|
6915
|
+
findings.push(...expertLensFindings(entry, readFileSync13(entryPath, "utf8"), contract));
|
|
6649
6916
|
}
|
|
6650
6917
|
for (const sheet of [
|
|
6651
6918
|
{ file: "styles.css", text: css },
|
|
@@ -6656,7 +6923,7 @@ async function checkBundleQuality(bundleDir, entry, set, fontManifest, motion, c
|
|
|
6656
6923
|
findings.push(...placeholderSelectorFindings(sheet.file, sheet.text));
|
|
6657
6924
|
}
|
|
6658
6925
|
if (set !== void 0 && existsSync14(entryPath)) {
|
|
6659
|
-
findings.push(...recordedAssetFindings(entry,
|
|
6926
|
+
findings.push(...recordedAssetFindings(entry, readFileSync13(entryPath, "utf8"), recordedAssetPaths(set.dir, set.reps)));
|
|
6660
6927
|
}
|
|
6661
6928
|
if (css !== "") {
|
|
6662
6929
|
const mapState = set === void 0 ? void 0 : recordedTokenMapState(set.dir, set.reps);
|
|
@@ -6678,7 +6945,7 @@ async function checkBundleQuality(bundleDir, entry, set, fontManifest, motion, c
|
|
|
6678
6945
|
})
|
|
6679
6946
|
);
|
|
6680
6947
|
if (profile !== void 0 && existsSync14(entryPath)) {
|
|
6681
|
-
findings.push(...conventionsFindings(
|
|
6948
|
+
findings.push(...conventionsFindings(readFileSync13(entryPath, "utf8"), css, profile));
|
|
6682
6949
|
}
|
|
6683
6950
|
return { findings, tokensAbsent: tokensCss === void 0 && !/var\(\s*--/.test(css) };
|
|
6684
6951
|
}
|
|
@@ -6779,8 +7046,8 @@ var init_effect_geometry = __esm({
|
|
|
6779
7046
|
});
|
|
6780
7047
|
|
|
6781
7048
|
// packages/verify/src/bundle-score.ts
|
|
6782
|
-
import { existsSync as existsSync15, mkdirSync as mkdirSync3, readFileSync as
|
|
6783
|
-
import
|
|
7049
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "node:fs";
|
|
7050
|
+
import path21 from "node:path";
|
|
6784
7051
|
import { build as build4 } from "esbuild";
|
|
6785
7052
|
import { chromium as chromium4 } from "playwright-core";
|
|
6786
7053
|
function getFontFaces2() {
|
|
@@ -6834,7 +7101,7 @@ function chooseSeat(cropAt, ref, padW, padH, backdrop, origin, extents) {
|
|
|
6834
7101
|
}
|
|
6835
7102
|
function metadataRoot(set, rep) {
|
|
6836
7103
|
try {
|
|
6837
|
-
const text = JSON.parse(
|
|
7104
|
+
const text = JSON.parse(readFileSync14(path21.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
6838
7105
|
return parseMetadataStructure(text);
|
|
6839
7106
|
} catch {
|
|
6840
7107
|
return void 0;
|
|
@@ -6889,19 +7156,19 @@ function smallSemanticNodes(set, rep, maxArea = 1024) {
|
|
|
6889
7156
|
});
|
|
6890
7157
|
}
|
|
6891
7158
|
function repMeta(set, rep) {
|
|
6892
|
-
const text = JSON.parse(
|
|
7159
|
+
const text = JSON.parse(readFileSync14(path21.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
6893
7160
|
const root = parseMetadataStructure(text);
|
|
6894
7161
|
return { w: Math.round(root.width ?? 100), h: Math.round(root.height ?? 40) };
|
|
6895
7162
|
}
|
|
6896
7163
|
function repRef(set, rep) {
|
|
6897
|
-
const env = JSON.parse(
|
|
7164
|
+
const env = JSON.parse(readFileSync14(path21.join(set, rep, "get_screenshot.json"), "utf8")).content.find((c) => c.type === "image");
|
|
6898
7165
|
return Uint8Array.from(Buffer.from(env?.data ?? "", "base64"));
|
|
6899
7166
|
}
|
|
6900
7167
|
function repEffectExtents(set, rep) {
|
|
6901
|
-
const file =
|
|
7168
|
+
const file = path21.join(set, rep, "get_design_context.json");
|
|
6902
7169
|
if (!existsSync15(file)) return void 0;
|
|
6903
7170
|
try {
|
|
6904
|
-
const text = JSON.parse(
|
|
7171
|
+
const text = JSON.parse(readFileSync14(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
6905
7172
|
const extents = shadowExtents(text);
|
|
6906
7173
|
return extents.top + extents.right + extents.bottom + extents.left > 0 ? extents : void 0;
|
|
6907
7174
|
} catch {
|
|
@@ -6909,16 +7176,19 @@ function repEffectExtents(set, rep) {
|
|
|
6909
7176
|
}
|
|
6910
7177
|
}
|
|
6911
7178
|
async function scoreBundleForTask(task, bundleDir, bar = BAR, opts = {}) {
|
|
6912
|
-
if (opts.evidenceDir !== void 0)
|
|
7179
|
+
if (opts.evidenceDir !== void 0) {
|
|
7180
|
+
mkdirSync3(opts.evidenceDir, { recursive: true });
|
|
7181
|
+
writeFileSync6(path21.join(opts.evidenceDir, "diff-legend.txt"), DIFF_LEGEND_TEXT);
|
|
7182
|
+
}
|
|
6913
7183
|
const CONFIGS2 = task.configs;
|
|
6914
7184
|
const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
|
|
6915
|
-
const entryTsx =
|
|
7185
|
+
const entryTsx = path21.join(bundleDir, task.entry);
|
|
6916
7186
|
if (!existsSync15(entryTsx)) return CONFIGS2.map((c) => ({ rep: c.rep, similarity: 0, inkRecall: 0, exact: { similarity: 0, inkRecall: 0 }, pass: false, error: `${task.entry} missing` }));
|
|
6917
7187
|
const css = candidateCss(bundleDir);
|
|
6918
7188
|
const mountSrc = `
|
|
6919
7189
|
import { createElement } from "react";
|
|
6920
7190
|
import { createRoot } from "react-dom/client";
|
|
6921
|
-
import * as B from ${JSON.stringify(
|
|
7191
|
+
import * as B from ${JSON.stringify(path21.resolve(entryTsx))};
|
|
6922
7192
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
|
|
6923
7193
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
6924
7194
|
const root = document.getElementById("root");
|
|
@@ -7012,16 +7282,17 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
|
|
|
7012
7282
|
const pass = r.similarity >= bar.sim && r.inkRecall >= bar.ink && !slab;
|
|
7013
7283
|
const region = pass ? void 0 : localizeDifference(scored, ref);
|
|
7014
7284
|
const absent = absentInkClusters(scored, ref, backdrop).map((c) => {
|
|
7285
|
+
if (c.chrome === true) return c;
|
|
7015
7286
|
const name = deepestNodeNameAt(task.set, cfg.rep, c.memberX - seat.x, c.memberY - seat.y);
|
|
7016
7287
|
return name === void 0 ? c : { ...c, name };
|
|
7017
7288
|
});
|
|
7018
7289
|
if (opts.evidenceDir !== void 0) {
|
|
7019
|
-
writeFileSync6(
|
|
7020
|
-
writeFileSync6(
|
|
7021
|
-
writeFileSync6(
|
|
7290
|
+
writeFileSync6(path21.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
|
|
7291
|
+
writeFileSync6(path21.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
|
|
7292
|
+
writeFileSync6(path21.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref, [], backdrop));
|
|
7022
7293
|
for (const [i, c] of absent.slice(0, 3).entries()) {
|
|
7023
|
-
writeFileSync6(
|
|
7024
|
-
writeFileSync6(
|
|
7294
|
+
writeFileSync6(path21.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
|
|
7295
|
+
writeFileSync6(path21.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-render.png`), zoomCrop(scored, c));
|
|
7025
7296
|
}
|
|
7026
7297
|
}
|
|
7027
7298
|
return {
|
|
@@ -7268,7 +7539,7 @@ var init_parity = __esm({
|
|
|
7268
7539
|
// packages/verify/src/composition.ts
|
|
7269
7540
|
import { createRequire as createRequire2 } from "node:module";
|
|
7270
7541
|
import { existsSync as existsSync16 } from "node:fs";
|
|
7271
|
-
import
|
|
7542
|
+
import path22 from "node:path";
|
|
7272
7543
|
import { build as build6 } from "esbuild";
|
|
7273
7544
|
import { chromium as chromium7 } from "playwright-core";
|
|
7274
7545
|
function getFontFaces4() {
|
|
@@ -7276,9 +7547,9 @@ function getFontFaces4() {
|
|
|
7276
7547
|
return _fontFaces4;
|
|
7277
7548
|
}
|
|
7278
7549
|
async function compileInstrumentedMount(task, bundleDir, composedParts = []) {
|
|
7279
|
-
const entryTsx =
|
|
7550
|
+
const entryTsx = path22.join(bundleDir, task.entry);
|
|
7280
7551
|
if (!existsSync16(entryTsx)) return { error: `${task.entry} missing` };
|
|
7281
|
-
const requireFromVerify = createRequire2(
|
|
7552
|
+
const requireFromVerify = createRequire2(path22.join(VERIFY_PKG_DIR, "package.json"));
|
|
7282
7553
|
let realJsxPath;
|
|
7283
7554
|
try {
|
|
7284
7555
|
realJsxPath = requireFromVerify.resolve("react/jsx-runtime");
|
|
@@ -7289,8 +7560,8 @@ async function compileInstrumentedMount(task, bundleDir, composedParts = []) {
|
|
|
7289
7560
|
import { createElement } from "react";
|
|
7290
7561
|
import { createRoot } from "react-dom/client";
|
|
7291
7562
|
import { __registerParts } from "react/jsx-runtime";
|
|
7292
|
-
import * as B from ${JSON.stringify(
|
|
7293
|
-
${composedParts.map((p, i) => `import * as CP${i} from ${JSON.stringify(
|
|
7563
|
+
import * as B from ${JSON.stringify(path22.resolve(entryTsx))};
|
|
7564
|
+
${composedParts.map((p, i) => `import * as CP${i} from ${JSON.stringify(path22.resolve(bundleDir, p.modulePath))};`).join("\n")}
|
|
7294
7565
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; partComponents: string[] } }).__cfg;
|
|
7295
7566
|
const pairs: Array<[unknown, string]> = [];
|
|
7296
7567
|
for (const name of cfg.partComponents) {
|
|
@@ -7346,7 +7617,7 @@ function expectedParts(task, roles, mainSlug) {
|
|
|
7346
7617
|
}
|
|
7347
7618
|
function interiorRegions(setDir, roles) {
|
|
7348
7619
|
const mains = roles.main;
|
|
7349
|
-
const withInterior = mains.filter((m) => existsSync16(
|
|
7620
|
+
const withInterior = mains.filter((m) => existsSync16(path22.join(setDir, m, "get_metadata_interior.json")));
|
|
7350
7621
|
if (withInterior.length === 0) {
|
|
7351
7622
|
return { unavailable: "no interior geometry recorded for any main (pre-obligation set) \u2014 re-record with `tendril record` to enable crop checks" };
|
|
7352
7623
|
}
|
|
@@ -7620,16 +7891,16 @@ export const jsxs = (t, p, k) => real.jsxs(t, inject(t, p), k);
|
|
|
7620
7891
|
|
|
7621
7892
|
// packages/verify/src/occlusion.ts
|
|
7622
7893
|
import { existsSync as existsSync17 } from "node:fs";
|
|
7623
|
-
import
|
|
7894
|
+
import path23 from "node:path";
|
|
7624
7895
|
import { build as build7 } from "esbuild";
|
|
7625
7896
|
import { chromium as chromium8 } from "playwright-core";
|
|
7626
7897
|
async function compileTwoUp(task, bundleDir) {
|
|
7627
|
-
const entryTsx =
|
|
7898
|
+
const entryTsx = path23.join(bundleDir, task.entry);
|
|
7628
7899
|
if (!existsSync17(entryTsx)) return { error: `${task.entry} missing` };
|
|
7629
7900
|
const src = `
|
|
7630
7901
|
import { createElement } from "react";
|
|
7631
7902
|
import { createRoot } from "react-dom/client";
|
|
7632
|
-
import * as B from ${JSON.stringify(
|
|
7903
|
+
import * as B from ${JSON.stringify(path23.resolve(entryTsx))};
|
|
7633
7904
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
|
|
7634
7905
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
7635
7906
|
for (const id of ["first", "second"]) {
|
|
@@ -7880,26 +8151,26 @@ var init_src5 = __esm({
|
|
|
7880
8151
|
});
|
|
7881
8152
|
|
|
7882
8153
|
// packages/cli/src/environment.ts
|
|
7883
|
-
import { existsSync as existsSync18, readFileSync as
|
|
7884
|
-
import
|
|
7885
|
-
import { createHash as
|
|
8154
|
+
import { existsSync as existsSync18, readFileSync as readFileSync16 } from "node:fs";
|
|
8155
|
+
import path24 from "node:path";
|
|
8156
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
7886
8157
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
7887
8158
|
function cliVersion() {
|
|
7888
8159
|
try {
|
|
7889
|
-
return JSON.parse(
|
|
8160
|
+
return JSON.parse(readFileSync16(path24.join(path24.dirname(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
|
|
7890
8161
|
} catch {
|
|
7891
8162
|
return "dev";
|
|
7892
8163
|
}
|
|
7893
8164
|
}
|
|
7894
8165
|
function environmentStamp(taskFamilies) {
|
|
7895
|
-
const manifestPath2 =
|
|
8166
|
+
const manifestPath2 = path24.join(fontCacheDir(), "manifest.json");
|
|
7896
8167
|
let fontsHash = null;
|
|
7897
8168
|
if (existsSync18(manifestPath2)) {
|
|
7898
8169
|
try {
|
|
7899
|
-
const entries = JSON.parse(
|
|
8170
|
+
const entries = JSON.parse(readFileSync16(manifestPath2, "utf8"));
|
|
7900
8171
|
const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
|
|
7901
8172
|
const faces = scoped.map((f) => `${f.family}:${f.weight}:${f.sha256}`).sort();
|
|
7902
|
-
fontsHash = faces.length === 0 ? null :
|
|
8173
|
+
fontsHash = faces.length === 0 ? null : createHash5("sha256").update(faces.join("\n")).digest("hex").slice(0, 16);
|
|
7903
8174
|
} catch {
|
|
7904
8175
|
fontsHash = null;
|
|
7905
8176
|
}
|
|
@@ -7940,8 +8211,8 @@ var init_describe = __esm({
|
|
|
7940
8211
|
});
|
|
7941
8212
|
|
|
7942
8213
|
// packages/cli/src/env.ts
|
|
7943
|
-
import { existsSync as existsSync19, readFileSync as
|
|
7944
|
-
import
|
|
8214
|
+
import { existsSync as existsSync19, readFileSync as readFileSync17 } from "node:fs";
|
|
8215
|
+
import path25 from "node:path";
|
|
7945
8216
|
function parseEnv(content) {
|
|
7946
8217
|
const entries = /* @__PURE__ */ new Map();
|
|
7947
8218
|
for (const line of content.split("\n")) {
|
|
@@ -7953,9 +8224,9 @@ function parseEnv(content) {
|
|
|
7953
8224
|
function resolveCredential(name) {
|
|
7954
8225
|
const fromProcess = process.env[name];
|
|
7955
8226
|
if (fromProcess) return fromProcess;
|
|
7956
|
-
const envPath =
|
|
8227
|
+
const envPath = path25.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
|
|
7957
8228
|
if (!existsSync19(envPath)) return void 0;
|
|
7958
|
-
return parseEnv(
|
|
8229
|
+
return parseEnv(readFileSync17(envPath, "utf8")).get(name);
|
|
7959
8230
|
}
|
|
7960
8231
|
var init_env = __esm({
|
|
7961
8232
|
"packages/cli/src/env.ts"() {
|
|
@@ -8014,18 +8285,200 @@ var init_output = __esm({
|
|
|
8014
8285
|
}
|
|
8015
8286
|
});
|
|
8016
8287
|
|
|
8288
|
+
// packages/cli/src/publish-client.ts
|
|
8289
|
+
import { chmodSync, existsSync as existsSync20, mkdirSync as mkdirSync4, readFileSync as readFileSync18, rmSync as rmSync3, writeFileSync as writeFileSync7 } from "node:fs";
|
|
8290
|
+
import os4 from "node:os";
|
|
8291
|
+
import path26 from "node:path";
|
|
8292
|
+
function sessionPath() {
|
|
8293
|
+
return process.env["TENDRIL_SESSION_PATH"] ?? path26.join(os4.homedir(), ".tendril", "session.json");
|
|
8294
|
+
}
|
|
8295
|
+
function readStoredSession(file = sessionPath()) {
|
|
8296
|
+
if (!existsSync20(file)) return void 0;
|
|
8297
|
+
try {
|
|
8298
|
+
const parsed = JSON.parse(readFileSync18(file, "utf8"));
|
|
8299
|
+
if (typeof parsed.origin !== "string" || typeof parsed.token !== "string") return void 0;
|
|
8300
|
+
return { origin: parsed.origin, token: parsed.token };
|
|
8301
|
+
} catch {
|
|
8302
|
+
return void 0;
|
|
8303
|
+
}
|
|
8304
|
+
}
|
|
8305
|
+
function writeStoredSession(session, file = sessionPath()) {
|
|
8306
|
+
mkdirSync4(path26.dirname(file), { recursive: true });
|
|
8307
|
+
writeFileSync7(file, `${JSON.stringify(session, null, 2)}
|
|
8308
|
+
`, { mode: 384 });
|
|
8309
|
+
chmodSync(file, 384);
|
|
8310
|
+
}
|
|
8311
|
+
function clearStoredSession(file = sessionPath()) {
|
|
8312
|
+
if (existsSync20(file)) rmSync3(file);
|
|
8313
|
+
}
|
|
8314
|
+
function tokenFor(origin, file = sessionPath()) {
|
|
8315
|
+
const fromEnv = process.env["TENDRIL_TOKEN"];
|
|
8316
|
+
if (fromEnv !== void 0 && fromEnv !== "") {
|
|
8317
|
+
const pinned = (process.env["TENDRIL_PORTAL_URL"] ?? "").replace(/\/+$/, "");
|
|
8318
|
+
if (pinned !== "" && pinned !== origin) return { ok: false, reason: "origin-mismatch", from: "env", boundTo: pinned };
|
|
8319
|
+
return { ok: true, token: fromEnv, from: "env" };
|
|
8320
|
+
}
|
|
8321
|
+
const stored = readStoredSession(file);
|
|
8322
|
+
if (stored === void 0) return { ok: false, reason: "none" };
|
|
8323
|
+
if (stored.origin !== origin) return { ok: false, reason: "origin-mismatch", from: "file", boundTo: stored.origin };
|
|
8324
|
+
return { ok: true, token: stored.token, from: "file" };
|
|
8325
|
+
}
|
|
8326
|
+
function isSecureOrigin(origin) {
|
|
8327
|
+
let url;
|
|
8328
|
+
try {
|
|
8329
|
+
url = new URL(origin);
|
|
8330
|
+
} catch {
|
|
8331
|
+
return false;
|
|
8332
|
+
}
|
|
8333
|
+
if (url.protocol === "https:") return true;
|
|
8334
|
+
return url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]");
|
|
8335
|
+
}
|
|
8336
|
+
function isRecord(value) {
|
|
8337
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8338
|
+
}
|
|
8339
|
+
function delay(ms) {
|
|
8340
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8341
|
+
}
|
|
8342
|
+
var InsecureOriginError, HttpPublishClient, REQUEST_TIMEOUT_MS, RETRIES, BACKOFF_MS;
|
|
8343
|
+
var init_publish_client = __esm({
|
|
8344
|
+
"packages/cli/src/publish-client.ts"() {
|
|
8345
|
+
"use strict";
|
|
8346
|
+
InsecureOriginError = class extends Error {
|
|
8347
|
+
constructor(origin) {
|
|
8348
|
+
super(`${origin} is not https, so a session token sent there would travel in the clear`);
|
|
8349
|
+
this.origin = origin;
|
|
8350
|
+
this.name = "InsecureOriginError";
|
|
8351
|
+
}
|
|
8352
|
+
origin;
|
|
8353
|
+
};
|
|
8354
|
+
HttpPublishClient = class {
|
|
8355
|
+
origin;
|
|
8356
|
+
token;
|
|
8357
|
+
send;
|
|
8358
|
+
constructor(options) {
|
|
8359
|
+
this.origin = options.origin.replace(/\/+$/, "");
|
|
8360
|
+
if (!isSecureOrigin(this.origin)) throw new InsecureOriginError(this.origin);
|
|
8361
|
+
this.token = options.token;
|
|
8362
|
+
this.send = options.fetch ?? globalThis.fetch;
|
|
8363
|
+
}
|
|
8364
|
+
acceptTerms(input) {
|
|
8365
|
+
return this.json("POST", "/api/consent", input);
|
|
8366
|
+
}
|
|
8367
|
+
begin(input) {
|
|
8368
|
+
return this.json("POST", "/api/publications", input, true);
|
|
8369
|
+
}
|
|
8370
|
+
async upload(input) {
|
|
8371
|
+
const encoded = input.relPath.split("/").map(encodeURIComponent).join("/");
|
|
8372
|
+
return this.request("PUT", `/api/publications/${encodeURIComponent(input.publicationId)}/objects/${encoded}`, {
|
|
8373
|
+
body: input.bytes,
|
|
8374
|
+
contentType: "application/octet-stream",
|
|
8375
|
+
retryable: true
|
|
8376
|
+
});
|
|
8377
|
+
}
|
|
8378
|
+
commit(input) {
|
|
8379
|
+
return this.json("POST", `/api/publications/${encodeURIComponent(input.publicationId)}/commit`, {}, true);
|
|
8380
|
+
}
|
|
8381
|
+
issueShareLink(input) {
|
|
8382
|
+
return this.json("POST", `/api/publications/${encodeURIComponent(input.publicationId)}/shares`, {
|
|
8383
|
+
expiresAt: input.expiresAt,
|
|
8384
|
+
recipientEmail: input.recipientEmail
|
|
8385
|
+
});
|
|
8386
|
+
}
|
|
8387
|
+
listShareLinks(input) {
|
|
8388
|
+
return this.request("GET", `/api/publications/${encodeURIComponent(input.publicationId)}/shares`, {
|
|
8389
|
+
body: "",
|
|
8390
|
+
contentType: "application/json",
|
|
8391
|
+
retryable: true
|
|
8392
|
+
});
|
|
8393
|
+
}
|
|
8394
|
+
revokeShareLink(input) {
|
|
8395
|
+
return this.request("DELETE", `/api/publications/${encodeURIComponent(input.publicationId)}/shares/${encodeURIComponent(input.shareLinkId)}`, {
|
|
8396
|
+
body: "",
|
|
8397
|
+
contentType: "application/json",
|
|
8398
|
+
retryable: true
|
|
8399
|
+
});
|
|
8400
|
+
}
|
|
8401
|
+
json(method, pathname, body, retryable = false) {
|
|
8402
|
+
return this.request(method, pathname, { body: JSON.stringify(body), contentType: "application/json", retryable });
|
|
8403
|
+
}
|
|
8404
|
+
async request(method, pathname, init) {
|
|
8405
|
+
let response;
|
|
8406
|
+
let lastError = "";
|
|
8407
|
+
const attempts = init.retryable === true ? RETRIES : 1;
|
|
8408
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
8409
|
+
try {
|
|
8410
|
+
response = await this.send(`${this.origin}${pathname}`, {
|
|
8411
|
+
method,
|
|
8412
|
+
headers: { authorization: `Bearer ${this.token}`, "content-type": init.contentType },
|
|
8413
|
+
// GET and DELETE carry nothing. `fetch` THROWS on a GET with a
|
|
8414
|
+
// body — even an empty string — so this is a hard requirement
|
|
8415
|
+
// rather than tidiness.
|
|
8416
|
+
...method === "GET" || method === "HEAD" || init.body === "" ? {} : { body: init.body },
|
|
8417
|
+
// A publish that hangs forever is worse than one that fails:
|
|
8418
|
+
// the user cannot tell it from slow, and there is nothing to
|
|
8419
|
+
// act on. Node's fetch has no default timeout at all.
|
|
8420
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
8421
|
+
});
|
|
8422
|
+
} catch (error) {
|
|
8423
|
+
lastError = error.message;
|
|
8424
|
+
if (attempt >= attempts) {
|
|
8425
|
+
return {
|
|
8426
|
+
ok: false,
|
|
8427
|
+
status: 0,
|
|
8428
|
+
refusal: `could not reach ${this.origin} after ${String(attempt)} ${attempt === 1 ? "try" : "tries"}: ${lastError}`
|
|
8429
|
+
};
|
|
8430
|
+
}
|
|
8431
|
+
await delay(BACKOFF_MS * attempt);
|
|
8432
|
+
continue;
|
|
8433
|
+
}
|
|
8434
|
+
if (response.status >= 500 && attempt < attempts) {
|
|
8435
|
+
await delay(BACKOFF_MS * attempt);
|
|
8436
|
+
continue;
|
|
8437
|
+
}
|
|
8438
|
+
break;
|
|
8439
|
+
}
|
|
8440
|
+
const text = await response.text();
|
|
8441
|
+
let parsed;
|
|
8442
|
+
try {
|
|
8443
|
+
parsed = JSON.parse(text);
|
|
8444
|
+
} catch {
|
|
8445
|
+
return {
|
|
8446
|
+
ok: false,
|
|
8447
|
+
status: response.status,
|
|
8448
|
+
refusal: `${this.origin} answered with ${String(response.status)} and something that is not JSON, so it is probably not a Tendril portal`
|
|
8449
|
+
};
|
|
8450
|
+
}
|
|
8451
|
+
const record = parsed ?? {};
|
|
8452
|
+
if (response.ok) return { ok: true, value: parsed };
|
|
8453
|
+
return {
|
|
8454
|
+
ok: false,
|
|
8455
|
+
status: response.status,
|
|
8456
|
+
refusal: typeof record["refusal"] === "string" ? record["refusal"] : `the portal refused with ${String(response.status)}`,
|
|
8457
|
+
...Array.isArray(record["detail"]) ? { detail: record["detail"] } : {},
|
|
8458
|
+
...Array.isArray(record["missing"]) ? { missing: record["missing"] } : {},
|
|
8459
|
+
...isRecord(record["needsConsent"]) ? { needsConsent: record["needsConsent"] } : {},
|
|
8460
|
+
...isRecord(record["needsConfirmation"]) ? { needsConfirmation: record["needsConfirmation"] } : {}
|
|
8461
|
+
};
|
|
8462
|
+
}
|
|
8463
|
+
};
|
|
8464
|
+
REQUEST_TIMEOUT_MS = 12e4;
|
|
8465
|
+
RETRIES = 3;
|
|
8466
|
+
BACKOFF_MS = 500;
|
|
8467
|
+
}
|
|
8468
|
+
});
|
|
8469
|
+
|
|
8017
8470
|
// packages/cli/src/entitlement.ts
|
|
8018
|
-
import { chmodSync, existsSync as
|
|
8471
|
+
import { chmodSync as chmodSync2, existsSync as existsSync21, mkdirSync as mkdirSync5, readFileSync as readFileSync19, writeFileSync as writeFileSync8 } from "node:fs";
|
|
8019
8472
|
import crypto from "node:crypto";
|
|
8020
|
-
import
|
|
8021
|
-
import
|
|
8473
|
+
import os5 from "node:os";
|
|
8474
|
+
import path27 from "node:path";
|
|
8022
8475
|
function entitlementPath() {
|
|
8023
|
-
return process.env["TENDRIL_ENTITLEMENT_PATH"] ??
|
|
8476
|
+
return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path27.join(os5.homedir(), ".tendril", "entitlement.json");
|
|
8024
8477
|
}
|
|
8025
8478
|
function readStoredEntitlement(file = entitlementPath()) {
|
|
8026
|
-
if (!
|
|
8479
|
+
if (!existsSync21(file)) return void 0;
|
|
8027
8480
|
try {
|
|
8028
|
-
const parsed = JSON.parse(
|
|
8481
|
+
const parsed = JSON.parse(readFileSync19(file, "utf8"));
|
|
8029
8482
|
if (typeof parsed.token !== "string" || typeof parsed.lastRefreshAt !== "number") return void 0;
|
|
8030
8483
|
return { token: parsed.token, lastRefreshAt: parsed.lastRefreshAt };
|
|
8031
8484
|
} catch {
|
|
@@ -8033,10 +8486,10 @@ function readStoredEntitlement(file = entitlementPath()) {
|
|
|
8033
8486
|
}
|
|
8034
8487
|
}
|
|
8035
8488
|
function writeStoredEntitlement(stored, file = entitlementPath()) {
|
|
8036
|
-
|
|
8037
|
-
|
|
8489
|
+
mkdirSync5(path27.dirname(file), { recursive: true });
|
|
8490
|
+
writeFileSync8(file, `${JSON.stringify(stored, null, 2)}
|
|
8038
8491
|
`);
|
|
8039
|
-
|
|
8492
|
+
chmodSync2(file, 384);
|
|
8040
8493
|
}
|
|
8041
8494
|
function parseEntitlementToken(token) {
|
|
8042
8495
|
if (!token.startsWith(ENT_PREFIX)) return { error: "not a tendril entitlement token" };
|
|
@@ -8118,9 +8571,9 @@ var init_entitlement = __esm({
|
|
|
8118
8571
|
|
|
8119
8572
|
// packages/cli/src/commands/doctor.ts
|
|
8120
8573
|
import { spawnSync } from "node:child_process";
|
|
8121
|
-
import { existsSync as
|
|
8122
|
-
import
|
|
8123
|
-
import
|
|
8574
|
+
import { existsSync as existsSync22, readFileSync as readFileSync20, readdirSync as readdirSync8 } from "node:fs";
|
|
8575
|
+
import os6 from "node:os";
|
|
8576
|
+
import path28 from "node:path";
|
|
8124
8577
|
function withDeadline(work, ms) {
|
|
8125
8578
|
return Promise.race([
|
|
8126
8579
|
work,
|
|
@@ -8180,19 +8633,19 @@ async function runDoctorChecks(options) {
|
|
|
8180
8633
|
remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
|
|
8181
8634
|
});
|
|
8182
8635
|
}
|
|
8183
|
-
const fontManifest =
|
|
8636
|
+
const fontManifest = path28.join(fontCacheDir(), "manifest.json");
|
|
8184
8637
|
checks.push(
|
|
8185
|
-
|
|
8638
|
+
existsSync22(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync20(fontManifest, "utf8")).length} faces)` } : {
|
|
8186
8639
|
name: "font-cache",
|
|
8187
8640
|
ok: true,
|
|
8188
8641
|
detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
|
|
8189
8642
|
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.`
|
|
8190
8643
|
}
|
|
8191
8644
|
);
|
|
8192
|
-
const pluginRoot =
|
|
8193
|
-
if (
|
|
8645
|
+
const pluginRoot = path28.join(os6.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
|
|
8646
|
+
if (existsSync22(pluginRoot)) {
|
|
8194
8647
|
try {
|
|
8195
|
-
const versions =
|
|
8648
|
+
const versions = readdirSync8(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
|
|
8196
8649
|
const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
|
|
8197
8650
|
if (newest !== void 0) {
|
|
8198
8651
|
const skewed = versionIsNewer(newest, cliVersion());
|
|
@@ -8226,13 +8679,52 @@ async function runDoctorChecks(options) {
|
|
|
8226
8679
|
checks.push(
|
|
8227
8680
|
ent.ok ? ent.mode === "pre-launch" ? { name: "entitlement", ok: true, detail: "pre-launch build \u2014 entitlement gates present but unarmed (no public key shipped); record/generate run freely" } : { name: "entitlement", ok: true, detail: `active plan "${ent.claims.plan}" until ${new Date(ent.claims.exp).toISOString().slice(0, 10)}${ent.stale ? " (stale \u2014 renews at next opportunity)" : ""}` } : { name: "entitlement", ok: false, detail: ent.error, remediation: ent.remediation }
|
|
8228
8681
|
);
|
|
8682
|
+
checks.push(await portalSessionCheck(options.fetchImpl ?? fetch));
|
|
8229
8683
|
const figmaToken = resolveCredential("FIGMA_TOKEN");
|
|
8230
8684
|
checks.push({
|
|
8231
8685
|
name: "figma-pat",
|
|
8232
8686
|
ok: true,
|
|
8233
8687
|
detail: figmaToken ? "FIGMA_TOKEN configured but unused \u2014 recording runs over the Figma MCP transport" : "FIGMA_TOKEN not set \u2014 not needed; recording runs over the Figma MCP transport"
|
|
8234
8688
|
});
|
|
8235
|
-
return {
|
|
8689
|
+
return {
|
|
8690
|
+
ok: checks.filter((c) => c.name !== "figma-pat" && c.name !== "openrouter-key" && c.name !== "path-skew" && c.name !== "portal-session").every((c) => c.ok),
|
|
8691
|
+
checks
|
|
8692
|
+
};
|
|
8693
|
+
}
|
|
8694
|
+
async function portalSessionCheck(fetchImpl) {
|
|
8695
|
+
const stored = readStoredSession();
|
|
8696
|
+
const connectHint = `Run ${tendrilCommand("login")} and approve in your browser (agents: call the tendril_login tool, relay the link and code, then tendril_login_wait) \u2014 or mint a token under "Connect your CLI" in your portal library and use ${tendrilCommand("login --paste")}.`;
|
|
8697
|
+
if (stored === void 0) {
|
|
8698
|
+
return { name: "portal-session", ok: true, detail: `not signed in to a portal \u2014 publishing needs it, verification never does. ${connectHint}` };
|
|
8699
|
+
}
|
|
8700
|
+
let response;
|
|
8701
|
+
try {
|
|
8702
|
+
const ctl = new AbortController();
|
|
8703
|
+
const timer = setTimeout(() => ctl.abort(), 5e3);
|
|
8704
|
+
timer.unref?.();
|
|
8705
|
+
response = await fetchImpl(`${stored.origin}/api/whoami`, { headers: { authorization: `Bearer ${stored.token}` }, signal: ctl.signal });
|
|
8706
|
+
clearTimeout(timer);
|
|
8707
|
+
} catch {
|
|
8708
|
+
return { name: "portal-session", ok: true, detail: `${stored.origin} did not answer \u2014 the stored session could not be checked. Publishing will say so precisely if it is actually dead.` };
|
|
8709
|
+
}
|
|
8710
|
+
if (!response.ok) {
|
|
8711
|
+
return {
|
|
8712
|
+
name: "portal-session",
|
|
8713
|
+
ok: false,
|
|
8714
|
+
detail: `the session stored for ${stored.origin} no longer works there (${String(response.status)}) \u2014 it was revoked or expired`,
|
|
8715
|
+
remediation: connectHint
|
|
8716
|
+
};
|
|
8717
|
+
}
|
|
8718
|
+
const body = await response.json();
|
|
8719
|
+
const email = typeof body.email === "string" && body.email !== "" ? ` as ${body.email}` : "";
|
|
8720
|
+
const expiry = typeof body.sessionExpiresAt === "string" ? body.sessionExpiresAt : null;
|
|
8721
|
+
const daysLeft = expiry === null ? null : Math.floor((Date.parse(expiry) - Date.now()) / 864e5);
|
|
8722
|
+
return {
|
|
8723
|
+
name: "portal-session",
|
|
8724
|
+
ok: true,
|
|
8725
|
+
detail: `signed in to ${stored.origin}${email}${expiry === null ? "" : `, session good until ${expiry.slice(0, 10)}`}`,
|
|
8726
|
+
...daysLeft !== null && daysLeft <= 7 ? { remediation: `That is ${String(daysLeft)} day(s) away \u2014 run ${tendrilCommand("login")} soon for a fresh session.` } : {}
|
|
8727
|
+
};
|
|
8236
8728
|
}
|
|
8237
8729
|
function probeVersion(binary) {
|
|
8238
8730
|
const windowsShim = /\.(cmd|bat)$/i.test(binary);
|
|
@@ -8307,6 +8799,7 @@ var init_doctor = __esm({
|
|
|
8307
8799
|
init_environment();
|
|
8308
8800
|
init_invocation();
|
|
8309
8801
|
init_output();
|
|
8802
|
+
init_publish_client();
|
|
8310
8803
|
init_entitlement();
|
|
8311
8804
|
DEFAULT_MCP_URL = "http://127.0.0.1:3845/mcp";
|
|
8312
8805
|
DOCTOR_DESCRIPTION = {
|
|
@@ -9422,8 +9915,8 @@ var init_engine_curated = __esm({
|
|
|
9422
9915
|
});
|
|
9423
9916
|
|
|
9424
9917
|
// packages/generate/src/loop.ts
|
|
9425
|
-
import { existsSync as
|
|
9426
|
-
import
|
|
9918
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync7, readFileSync as readFileSync22, renameSync, writeFileSync as writeFileSync10 } from "node:fs";
|
|
9919
|
+
import path31 from "node:path";
|
|
9427
9920
|
import { z as z13 } from "zod";
|
|
9428
9921
|
function objective(scores, behaviors) {
|
|
9429
9922
|
const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
|
|
@@ -9465,9 +9958,9 @@ ${absentLines.join("\n")}` : ""}
|
|
|
9465
9958
|
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."}`;
|
|
9466
9959
|
}
|
|
9467
9960
|
function archivePriorRun(outDir) {
|
|
9468
|
-
if (!
|
|
9961
|
+
if (!existsSync24(path31.join(outDir, "run-log.json")) && !existsSync24(path31.join(outDir, "loop-state.json"))) return void 0;
|
|
9469
9962
|
let n = 1;
|
|
9470
|
-
while (
|
|
9963
|
+
while (existsSync24(`${outDir}-prev-${n}`)) n += 1;
|
|
9471
9964
|
renameSync(outDir, `${outDir}-prev-${n}`);
|
|
9472
9965
|
return `${outDir}-prev-${n}`;
|
|
9473
9966
|
}
|
|
@@ -9476,14 +9969,14 @@ async function runEngineLoop(opts) {
|
|
|
9476
9969
|
const plateau = opts.plateau ?? 2;
|
|
9477
9970
|
const progress = opts.onProgress ?? (() => {
|
|
9478
9971
|
});
|
|
9479
|
-
const statePath =
|
|
9480
|
-
const resuming = opts.resume === true &&
|
|
9972
|
+
const statePath = path31.join(opts.outDir, "loop-state.json");
|
|
9973
|
+
const resuming = opts.resume === true && existsSync24(statePath);
|
|
9481
9974
|
if (!resuming) {
|
|
9482
9975
|
const archived = archivePriorRun(opts.outDir);
|
|
9483
9976
|
if (archived !== void 0) progress(`previous run archived to ${archived}`);
|
|
9484
9977
|
}
|
|
9485
|
-
|
|
9486
|
-
const scratch =
|
|
9978
|
+
mkdirSync7(opts.outDir, { recursive: true });
|
|
9979
|
+
const scratch = path31.join(opts.outDir, ".candidate");
|
|
9487
9980
|
let attempts = [];
|
|
9488
9981
|
let log = [];
|
|
9489
9982
|
let best;
|
|
@@ -9491,7 +9984,7 @@ async function runEngineLoop(opts) {
|
|
|
9491
9984
|
let nonAccepted = 0;
|
|
9492
9985
|
let stopReason = "max-iterations";
|
|
9493
9986
|
if (resuming) {
|
|
9494
|
-
const restored = LoopStateSchema.parse(JSON.parse(
|
|
9987
|
+
const restored = LoopStateSchema.parse(JSON.parse(readFileSync22(statePath, "utf8")));
|
|
9495
9988
|
attempts = restored.attempts;
|
|
9496
9989
|
log = restored.iterations;
|
|
9497
9990
|
spentUsd = restored.spentUsd;
|
|
@@ -9506,12 +9999,12 @@ async function runEngineLoop(opts) {
|
|
|
9506
9999
|
progress(`resumed: ${log.length} iteration(s), $${spentUsd.toFixed(3)} spent, best pass=${best?.objective[0] ?? 0}`);
|
|
9507
10000
|
}
|
|
9508
10001
|
const persist = () => {
|
|
9509
|
-
|
|
10002
|
+
writeFileSync10(statePath, `${JSON.stringify({ version: 1, spentUsd, attempts, iterations: log }, null, 1)}
|
|
9510
10003
|
`);
|
|
9511
10004
|
};
|
|
9512
10005
|
const writeCandidate = (files) => {
|
|
9513
|
-
|
|
9514
|
-
for (const [name, content] of Object.entries(files))
|
|
10006
|
+
mkdirSync7(scratch, { recursive: true });
|
|
10007
|
+
for (const [name, content] of Object.entries(files)) writeFileSync10(path31.join(scratch, name), content);
|
|
9515
10008
|
};
|
|
9516
10009
|
const scoreCandidate = async (candidate, iter, usd, modelMs) => {
|
|
9517
10010
|
writeCandidate(candidate.files);
|
|
@@ -9569,8 +10062,8 @@ async function runEngineLoop(opts) {
|
|
|
9569
10062
|
const usd = candidate.usage?.usd ?? 0;
|
|
9570
10063
|
spentUsd += usd;
|
|
9571
10064
|
if (candidate.raw !== void 0) {
|
|
9572
|
-
|
|
9573
|
-
|
|
10065
|
+
mkdirSync7(path31.join(opts.outDir, "responses"), { recursive: true });
|
|
10066
|
+
writeFileSync10(path31.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
|
|
9574
10067
|
}
|
|
9575
10068
|
if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
|
|
9576
10069
|
const finish = candidate.usage?.finishReason ?? "?";
|
|
@@ -9596,10 +10089,10 @@ async function runEngineLoop(opts) {
|
|
|
9596
10089
|
}
|
|
9597
10090
|
}
|
|
9598
10091
|
}
|
|
9599
|
-
if (best !== void 0) for (const [name, content] of Object.entries(best.files))
|
|
10092
|
+
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(path31.join(opts.outDir, name), content);
|
|
9600
10093
|
const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
|
|
9601
|
-
|
|
9602
|
-
|
|
10094
|
+
writeFileSync10(
|
|
10095
|
+
path31.join(opts.outDir, "run-log.json"),
|
|
9603
10096
|
`${JSON.stringify(
|
|
9604
10097
|
{
|
|
9605
10098
|
...opts.meta,
|
|
@@ -9666,8 +10159,8 @@ var init_loop2 = __esm({
|
|
|
9666
10159
|
});
|
|
9667
10160
|
|
|
9668
10161
|
// packages/generate/src/brief.ts
|
|
9669
|
-
import { existsSync as
|
|
9670
|
-
import
|
|
10162
|
+
import { existsSync as existsSync25, readFileSync as readFileSync23 } from "node:fs";
|
|
10163
|
+
import path32 from "node:path";
|
|
9671
10164
|
import { PNG as PNG3 } from "pngjs";
|
|
9672
10165
|
function singleAxes2(name) {
|
|
9673
10166
|
const parsed = parseVariantAxes(name);
|
|
@@ -10107,15 +10600,15 @@ function authorBehaviors(api, extras = {}) {
|
|
|
10107
10600
|
};
|
|
10108
10601
|
}
|
|
10109
10602
|
function envelopeText(file) {
|
|
10110
|
-
return envelopeFirstTextPart(JSON.parse(
|
|
10603
|
+
return envelopeFirstTextPart(JSON.parse(readFileSync23(file, "utf8")));
|
|
10111
10604
|
}
|
|
10112
10605
|
function metadataText(file) {
|
|
10113
|
-
return envelopeTextContent(JSON.parse(
|
|
10606
|
+
return envelopeTextContent(JSON.parse(readFileSync23(file, "utf8")));
|
|
10114
10607
|
}
|
|
10115
10608
|
function dismissEvidence(setDir, repSlugs) {
|
|
10116
10609
|
for (const slug of repSlugs) {
|
|
10117
|
-
const f =
|
|
10118
|
-
if (!
|
|
10610
|
+
const f = path32.join(setDir, slug, "get_design_context.json");
|
|
10611
|
+
if (!existsSync25(f)) continue;
|
|
10119
10612
|
const text = envelopeText(f);
|
|
10120
10613
|
const propHit = /[{,]\s*(\w*dismiss\w*)\s*=\s*true\b/i.exec(text);
|
|
10121
10614
|
if (propHit !== null) return { evidence: `emission prop "${propHit[1]}"`, visibleReps: [] };
|
|
@@ -10142,9 +10635,9 @@ function dismissEvidence(setDir, repSlugs) {
|
|
|
10142
10635
|
const glyphIsTheComponent = (() => {
|
|
10143
10636
|
const slugToCheck = vis.visibleIn[0];
|
|
10144
10637
|
if (slugToCheck === void 0) return false;
|
|
10145
|
-
const metaFile =
|
|
10638
|
+
const metaFile = path32.join(setDir, slugToCheck, "get_metadata.json");
|
|
10146
10639
|
try {
|
|
10147
|
-
const root = parseMetadataStructure(envelopeTextContent(JSON.parse(
|
|
10640
|
+
const root = parseMetadataStructure(envelopeTextContent(JSON.parse(readFileSync23(metaFile, "utf8"))));
|
|
10148
10641
|
if (root.children.length !== 1) return false;
|
|
10149
10642
|
const contains = (n) => normalizedLayerName(n.name) === norm2 || n.children.some(contains);
|
|
10150
10643
|
return contains(root.children[0]);
|
|
@@ -10164,10 +10657,10 @@ function dismissEvidence(setDir, repSlugs) {
|
|
|
10164
10657
|
return void 0;
|
|
10165
10658
|
}
|
|
10166
10659
|
function recordedReferencePng(setDir, slug) {
|
|
10167
|
-
const f =
|
|
10168
|
-
if (!
|
|
10660
|
+
const f = path32.join(setDir, slug, "get_screenshot.json");
|
|
10661
|
+
if (!existsSync25(f)) return void 0;
|
|
10169
10662
|
try {
|
|
10170
|
-
const env = JSON.parse(
|
|
10663
|
+
const env = JSON.parse(readFileSync23(f, "utf8")).content.find((c) => c.type === "image");
|
|
10171
10664
|
return env?.data === void 0 ? void 0 : Uint8Array.from(Buffer.from(env.data, "base64"));
|
|
10172
10665
|
} catch {
|
|
10173
10666
|
return void 0;
|
|
@@ -10208,7 +10701,7 @@ function recordedFontNeeds(setDir, opts = {}) {
|
|
|
10208
10701
|
for (const m of text.matchAll(/font-\['([^':\]]+)(?::([^'\]]+))?'/g)) famAdd(m[1], m[2] === void 0 ? void 0 : styleWeight(m[2]));
|
|
10209
10702
|
for (const m of text.matchAll(/family-name:var\([^,)]*,\s*'([^':\]]+)(?::([^'\]]+))?'/g)) famAdd(m[1], m[2] === void 0 ? void 0 : styleWeight(m[2]));
|
|
10210
10703
|
for (const m of text.matchAll(/\bfont-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)\b/g)) unpaired.add(STYLE_WEIGHTS2[m[1]]);
|
|
10211
|
-
for (const m of text.matchAll(/font-weight:\s*([1-9]
|
|
10704
|
+
for (const m of text.matchAll(/font-weight:\s*([1-9]\d{2})\b/g)) unpaired.add(Number(m[1]));
|
|
10212
10705
|
};
|
|
10213
10706
|
const fromDefs = (text) => {
|
|
10214
10707
|
let defs;
|
|
@@ -10226,15 +10719,34 @@ function recordedFontNeeds(setDir, opts = {}) {
|
|
|
10226
10719
|
}
|
|
10227
10720
|
if (/^[A-Za-z][A-Za-z0-9 ]{1,39}$/.test(v.trim())) famAdd(v.trim());
|
|
10228
10721
|
}
|
|
10722
|
+
fromFontCalls(defs);
|
|
10229
10723
|
};
|
|
10724
|
+
function fromFontCalls(defs) {
|
|
10725
|
+
for (const v of Object.values(defs)) {
|
|
10726
|
+
if (typeof v !== "string") continue;
|
|
10727
|
+
const call = /\bFont\(\s*family:\s*"?([^",]+)"?\s*,([^)]*)\)/.exec(v);
|
|
10728
|
+
if (call === null) continue;
|
|
10729
|
+
const rest = call[2];
|
|
10730
|
+
const explicit = /\bweight:\s*([1-9]\d{2})\b/.exec(rest);
|
|
10731
|
+
const styleRef = /\bstyle:\s*([^,)]+)/.exec(rest);
|
|
10732
|
+
let weight = explicit === null ? void 0 : Number(explicit[1]);
|
|
10733
|
+
if (weight === void 0 && styleRef !== null) {
|
|
10734
|
+
const ref = styleRef[1].trim().replace(/^"|"$/g, "");
|
|
10735
|
+
weight = styleWeight((typeof defs[ref] === "string" ? defs[ref] : ref).trim());
|
|
10736
|
+
}
|
|
10737
|
+
const famRef = call[1].trim();
|
|
10738
|
+
const family = typeof defs[famRef] === "string" ? defs[famRef].trim() : famRef;
|
|
10739
|
+
if (/^[A-Za-z][A-Za-z0-9 ]{1,39}$/.test(family)) famAdd(family, weight);
|
|
10740
|
+
}
|
|
10741
|
+
}
|
|
10230
10742
|
const manifest = loadManifest(setDir);
|
|
10231
|
-
const setDefs =
|
|
10232
|
-
if (
|
|
10743
|
+
const setDefs = path32.join(setDir, "get_variable_defs.json");
|
|
10744
|
+
if (existsSync25(setDefs)) fromDefs(envelopeText(setDefs));
|
|
10233
10745
|
for (const rep of manifest.reps) {
|
|
10234
|
-
const ctx =
|
|
10235
|
-
if (
|
|
10236
|
-
const defs =
|
|
10237
|
-
if (
|
|
10746
|
+
const ctx = path32.join(setDir, rep.slug, "get_design_context.json");
|
|
10747
|
+
if (existsSync25(ctx)) fromEmission(envelopeText(ctx));
|
|
10748
|
+
const defs = path32.join(setDir, rep.slug, "get_variable_defs.json");
|
|
10749
|
+
if (existsSync25(defs)) fromDefs(envelopeText(defs));
|
|
10238
10750
|
}
|
|
10239
10751
|
return [...byFamily.entries()].map(([family, paired]) => {
|
|
10240
10752
|
const weights = /* @__PURE__ */ new Set([...paired, ...opts.pairedOnly === true ? [] : unpaired]);
|
|
@@ -10245,10 +10757,10 @@ function symbolFontGlyphCount(setDir, reps) {
|
|
|
10245
10757
|
const PUA = /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u;
|
|
10246
10758
|
const glyphs = /* @__PURE__ */ new Set();
|
|
10247
10759
|
for (const rep of reps) {
|
|
10248
|
-
const file =
|
|
10249
|
-
if (!
|
|
10760
|
+
const file = path32.join(setDir, rep, "get_metadata.json");
|
|
10761
|
+
if (!existsSync25(file)) continue;
|
|
10250
10762
|
try {
|
|
10251
|
-
const text = JSON.parse(
|
|
10763
|
+
const text = JSON.parse(readFileSync23(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
10252
10764
|
for (const m of text.matchAll(/<text\s[^>]*name="([^"]*)"/g)) {
|
|
10253
10765
|
const name = decodeXmlEntities(m[1]).replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)));
|
|
10254
10766
|
if (PUA.test(name)) glyphs.add(name);
|
|
@@ -10274,8 +10786,8 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
10274
10786
|
const propRep = [];
|
|
10275
10787
|
const perRep = [];
|
|
10276
10788
|
for (const slug of repSlugs) {
|
|
10277
|
-
const f =
|
|
10278
|
-
if (!
|
|
10789
|
+
const f = path32.join(setDir, slug, "get_design_context.json");
|
|
10790
|
+
if (!existsSync25(f)) continue;
|
|
10279
10791
|
const code = envelopeText(f);
|
|
10280
10792
|
const props = /* @__PURE__ */ new Map();
|
|
10281
10793
|
for (const m of code.matchAll(/[{,]\s*(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"/g)) {
|
|
@@ -10301,8 +10813,8 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
10301
10813
|
const axisValuesBySlug = /* @__PURE__ */ new Map();
|
|
10302
10814
|
const valuesByAxis = /* @__PURE__ */ new Map();
|
|
10303
10815
|
for (const slug of repSlugs) {
|
|
10304
|
-
const metaFile =
|
|
10305
|
-
if (!
|
|
10816
|
+
const metaFile = path32.join(setDir, slug, "get_metadata.json");
|
|
10817
|
+
if (!existsSync25(metaFile)) continue;
|
|
10306
10818
|
const name = symbolName(metadataText(metaFile));
|
|
10307
10819
|
if (name === void 0) continue;
|
|
10308
10820
|
const values = /* @__PURE__ */ new Set();
|
|
@@ -10417,8 +10929,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
10417
10929
|
const poses = [];
|
|
10418
10930
|
const missing = [];
|
|
10419
10931
|
for (const rep of manifest.reps) {
|
|
10420
|
-
const metaFile =
|
|
10421
|
-
if (!
|
|
10932
|
+
const metaFile = path32.join(setDir, rep.slug, "get_metadata.json");
|
|
10933
|
+
if (!existsSync25(metaFile)) {
|
|
10422
10934
|
missing.push(rep.slug);
|
|
10423
10935
|
continue;
|
|
10424
10936
|
}
|
|
@@ -10432,8 +10944,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
10432
10944
|
if (missing.length > 0) {
|
|
10433
10945
|
throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
|
|
10434
10946
|
}
|
|
10435
|
-
const setMeta =
|
|
10436
|
-
const latticeNames = manifest.latticeNames ?? (
|
|
10947
|
+
const setMeta = path32.join(setDir, "get_metadata.json");
|
|
10948
|
+
const latticeNames = manifest.latticeNames ?? (existsSync25(setMeta) ? [...metadataText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
|
|
10437
10949
|
const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
|
|
10438
10950
|
const recordedFonts = recordedFontFamilies(setDir);
|
|
10439
10951
|
const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
|
|
@@ -10634,17 +11146,17 @@ var init_brief = __esm({
|
|
|
10634
11146
|
});
|
|
10635
11147
|
|
|
10636
11148
|
// packages/generate/src/segments.ts
|
|
10637
|
-
import { existsSync as
|
|
10638
|
-
import
|
|
11149
|
+
import { existsSync as existsSync26, readFileSync as readFileSync24, readdirSync as readdirSync10 } from "node:fs";
|
|
11150
|
+
import path33 from "node:path";
|
|
10639
11151
|
function repText(set, rep, tool) {
|
|
10640
|
-
const env = JSON.parse(
|
|
11152
|
+
const env = JSON.parse(readFileSync24(path33.join(set, rep, `${tool}.json`), "utf8"));
|
|
10641
11153
|
return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
|
|
10642
11154
|
}
|
|
10643
11155
|
function refPngDims(set, rep) {
|
|
10644
|
-
const f =
|
|
10645
|
-
if (!
|
|
11156
|
+
const f = path33.join(set, rep, "get_screenshot.json");
|
|
11157
|
+
if (!existsSync26(f)) return void 0;
|
|
10646
11158
|
try {
|
|
10647
|
-
const env = JSON.parse(
|
|
11159
|
+
const env = JSON.parse(readFileSync24(f, "utf8")).content.find((c) => c.type === "image");
|
|
10648
11160
|
if (env?.data === void 0) return void 0;
|
|
10649
11161
|
const buf = Buffer.from(env.data, "base64");
|
|
10650
11162
|
if (buf.length < 24 || buf.readUInt32BE(0) !== 2303741511) return void 0;
|
|
@@ -10710,20 +11222,20 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
|
|
|
10710
11222
|
}
|
|
10711
11223
|
function buildSegments(task, mode = "fenced") {
|
|
10712
11224
|
const SET = task.set;
|
|
10713
|
-
let defsRecorded =
|
|
11225
|
+
let defsRecorded = existsSync26(path33.join(SET, "get_variable_defs.json"));
|
|
10714
11226
|
let rawDefs = {};
|
|
10715
|
-
if (
|
|
10716
|
-
const text = envelopeFirstTextPart(JSON.parse(
|
|
11227
|
+
if (existsSync26(path33.join(SET, "get_variable_defs.json"))) {
|
|
11228
|
+
const text = envelopeFirstTextPart(JSON.parse(readFileSync24(path33.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
|
|
10717
11229
|
try {
|
|
10718
11230
|
rawDefs = JSON.parse(text);
|
|
10719
11231
|
} catch {
|
|
10720
11232
|
}
|
|
10721
11233
|
} else {
|
|
10722
11234
|
for (const cfg of task.configs) {
|
|
10723
|
-
const f =
|
|
10724
|
-
if (!
|
|
11235
|
+
const f = path33.join(SET, cfg.rep, "get_variable_defs.json");
|
|
11236
|
+
if (!existsSync26(f)) continue;
|
|
10725
11237
|
defsRecorded = true;
|
|
10726
|
-
const text = envelopeFirstTextPart(JSON.parse(
|
|
11238
|
+
const text = envelopeFirstTextPart(JSON.parse(readFileSync24(f, "utf8"))) || "{}";
|
|
10727
11239
|
try {
|
|
10728
11240
|
for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
|
|
10729
11241
|
} catch {
|
|
@@ -10731,8 +11243,8 @@ function buildSegments(task, mode = "fenced") {
|
|
|
10731
11243
|
}
|
|
10732
11244
|
}
|
|
10733
11245
|
const emissionTexts = task.configs.map((cfg) => {
|
|
10734
|
-
const f =
|
|
10735
|
-
return
|
|
11246
|
+
const f = path33.join(SET, cfg.rep, "get_design_context.json");
|
|
11247
|
+
return existsSync26(f) ? envelopeFirstTextPart(JSON.parse(readFileSync24(f, "utf8"))) : "";
|
|
10736
11248
|
});
|
|
10737
11249
|
const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
|
|
10738
11250
|
const defs = JSON.stringify(map, null, 1);
|
|
@@ -10750,9 +11262,9 @@ TOKEN MAP NEVER RECORDED: this set holds no get_variable_defs envelope, so wheth
|
|
|
10750
11262
|
for (const cfg of task.configs) {
|
|
10751
11263
|
const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
|
|
10752
11264
|
const emission = stripFigmaInstructions(repText(SET, cfg.rep, "get_design_context"));
|
|
10753
|
-
const assets =
|
|
11265
|
+
const assets = readdirSync10(path33.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
|
|
10754
11266
|
\`\`\`svg
|
|
10755
|
-
${
|
|
11267
|
+
${readFileSync24(path33.join(SET, cfg.rep, f), "utf8")}
|
|
10756
11268
|
\`\`\``).join("\n");
|
|
10757
11269
|
const refNote = (() => {
|
|
10758
11270
|
const dims = refPngDims(SET, cfg.rep);
|
|
@@ -10788,7 +11300,7 @@ Optionally a third block with \`/* FILE: tokens.css */\`.`);
|
|
|
10788
11300
|
} else {
|
|
10789
11301
|
parts.push(`
|
|
10790
11302
|
## Output format
|
|
10791
|
-
Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into ONE candidate directory: \`tendril-out/${
|
|
11303
|
+
Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into ONE candidate directory: \`tendril-out/${path33.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.`);
|
|
10792
11304
|
}
|
|
10793
11305
|
return parts.join("\n");
|
|
10794
11306
|
}
|
|
@@ -10855,9 +11367,9 @@ var init_adapter = __esm({
|
|
|
10855
11367
|
});
|
|
10856
11368
|
|
|
10857
11369
|
// packages/generate/src/bundle-emit.ts
|
|
10858
|
-
import { createHash as
|
|
10859
|
-
import { copyFileSync, existsSync as
|
|
10860
|
-
import
|
|
11370
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
11371
|
+
import { copyFileSync, existsSync as existsSync27, mkdirSync as mkdirSync8, readFileSync as readFileSync25, readdirSync as readdirSync11, rmSync as rmSync4, writeFileSync as writeFileSync11 } from "node:fs";
|
|
11372
|
+
import path34 from "node:path";
|
|
10861
11373
|
function pinFromConfigs(configs) {
|
|
10862
11374
|
const domains = /* @__PURE__ */ new Map();
|
|
10863
11375
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -10926,9 +11438,9 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
10926
11438
|
const notices = [];
|
|
10927
11439
|
const licenseTexts = /* @__PURE__ */ new Map();
|
|
10928
11440
|
for (const face of faces) {
|
|
10929
|
-
const src =
|
|
10930
|
-
const target = `./fonts/${
|
|
10931
|
-
const format = FONT_FORMATS[
|
|
11441
|
+
const src = path34.join(cacheDir, path34.basename(face.file));
|
|
11442
|
+
const target = `./fonts/${path34.basename(face.file)}`;
|
|
11443
|
+
const format = FONT_FORMATS[path34.extname(face.file).toLowerCase()] ?? "truetype";
|
|
10932
11444
|
const family = face.family.replace(/['\\]/g, "").replace(/\*\//g, "");
|
|
10933
11445
|
const decl = `@font-face { font-family: '${family}'; font-weight: ${face.weight}; font-style: normal; src: url(${target}) format('${format}'); }`;
|
|
10934
11446
|
const license = normalizeFontLicense(face.license);
|
|
@@ -10966,14 +11478,14 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
10966
11478
|
`/* ${decl} */`
|
|
10967
11479
|
);
|
|
10968
11480
|
}
|
|
10969
|
-
} else if (
|
|
10970
|
-
|
|
10971
|
-
copyFileSync(src,
|
|
11481
|
+
} else if (existsSync27(src) && createHash6("sha256").update(readFileSync25(src)).digest("hex") === face.sha256) {
|
|
11482
|
+
mkdirSync8(path34.join(bundleDir, "fonts"), { recursive: true });
|
|
11483
|
+
copyFileSync(src, path34.join(bundleDir, "fonts", path34.basename(face.file)));
|
|
10972
11484
|
licenseTexts.set(terms.file, terms.text);
|
|
10973
11485
|
const upstream = upstreamAttribution(face);
|
|
10974
11486
|
notices.push(
|
|
10975
11487
|
"",
|
|
10976
|
-
`${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${
|
|
11488
|
+
`${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${path34.basename(face.file)}`,
|
|
10977
11489
|
` licence: ${terms.title} \u2014 full text in ./${terms.file}`,
|
|
10978
11490
|
` source: ${face.source}`,
|
|
10979
11491
|
` sha256: ${face.sha256}`,
|
|
@@ -10987,9 +11499,9 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
10987
11499
|
}
|
|
10988
11500
|
if (lines.length === 0) return null;
|
|
10989
11501
|
if (notices.length > 0) {
|
|
10990
|
-
const fontsDir =
|
|
10991
|
-
for (const [file, text] of licenseTexts)
|
|
10992
|
-
|
|
11502
|
+
const fontsDir = path34.join(bundleDir, "fonts");
|
|
11503
|
+
for (const [file, text] of licenseTexts) writeFileSync11(path34.join(fontsDir, file), text);
|
|
11504
|
+
writeFileSync11(path34.join(fontsDir, "NOTICE.txt"), `${[NOTICE_PREAMBLE, ...notices].join("\n")}
|
|
10993
11505
|
`);
|
|
10994
11506
|
header.push(
|
|
10995
11507
|
"/* REDISTRIBUTION: the face files in ./fonts/ are redistributed under the licences",
|
|
@@ -11001,10 +11513,10 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
11001
11513
|
`;
|
|
11002
11514
|
}
|
|
11003
11515
|
function countLatticeSymbols(setDir) {
|
|
11004
|
-
const manifestFile =
|
|
11005
|
-
if (
|
|
11516
|
+
const manifestFile = path34.join(setDir, "recording-set.json");
|
|
11517
|
+
if (existsSync27(manifestFile)) {
|
|
11006
11518
|
try {
|
|
11007
|
-
const stored = JSON.parse(
|
|
11519
|
+
const stored = JSON.parse(readFileSync25(manifestFile, "utf8"));
|
|
11008
11520
|
if (stored.variantScope !== "component-set") return null;
|
|
11009
11521
|
const lattice = stored.latticeNames;
|
|
11010
11522
|
if (lattice !== void 0 && lattice.length > 0) return lattice.length;
|
|
@@ -11012,13 +11524,13 @@ function countLatticeSymbols(setDir) {
|
|
|
11012
11524
|
}
|
|
11013
11525
|
}
|
|
11014
11526
|
const files = [
|
|
11015
|
-
|
|
11016
|
-
...
|
|
11017
|
-
].filter((f) =>
|
|
11527
|
+
path34.join(setDir, "get_metadata.json"),
|
|
11528
|
+
...existsSync27(setDir) ? readdirSync11(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path34.join(setDir, f)) : []
|
|
11529
|
+
].filter((f) => existsSync27(f));
|
|
11018
11530
|
if (files.length === 0) return null;
|
|
11019
11531
|
let count = 0;
|
|
11020
11532
|
for (const f of files) {
|
|
11021
|
-
const text = envelopeTextContent(JSON.parse(
|
|
11533
|
+
const text = envelopeTextContent(JSON.parse(readFileSync25(f, "utf8")));
|
|
11022
11534
|
count += [...text.matchAll(/name="([^"]*)"/g)].filter((m) => m[1].includes("=")).length;
|
|
11023
11535
|
}
|
|
11024
11536
|
return count > 0 ? count : null;
|
|
@@ -11026,23 +11538,23 @@ function countLatticeSymbols(setDir) {
|
|
|
11026
11538
|
function recordingSetHash(setDir, configs) {
|
|
11027
11539
|
const relPaths = [];
|
|
11028
11540
|
for (const name of ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json"]) {
|
|
11029
|
-
if (
|
|
11541
|
+
if (existsSync27(path34.join(setDir, name))) relPaths.push(name);
|
|
11030
11542
|
}
|
|
11031
11543
|
for (const cfg of configs) {
|
|
11032
11544
|
for (const f of ["get_design_context.json", "get_metadata.json", "get_screenshot.json", "get_variable_defs.json"]) {
|
|
11033
|
-
if (
|
|
11545
|
+
if (existsSync27(path34.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
|
|
11034
11546
|
}
|
|
11035
|
-
if (
|
|
11036
|
-
for (const asset of
|
|
11547
|
+
if (existsSync27(path34.join(setDir, cfg.rep))) {
|
|
11548
|
+
for (const asset of readdirSync11(path34.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
|
|
11037
11549
|
relPaths.push(`${cfg.rep}/${asset}`);
|
|
11038
11550
|
}
|
|
11039
11551
|
}
|
|
11040
11552
|
}
|
|
11041
11553
|
return hashRecordingSet(
|
|
11042
11554
|
relPaths,
|
|
11043
|
-
(p) => new Uint8Array(
|
|
11555
|
+
(p) => new Uint8Array(readFileSync25(path34.join(setDir, p))),
|
|
11044
11556
|
(chunks) => {
|
|
11045
|
-
const h =
|
|
11557
|
+
const h = createHash6("sha256");
|
|
11046
11558
|
for (const c of chunks) h.update(c);
|
|
11047
11559
|
return h.digest("hex");
|
|
11048
11560
|
}
|
|
@@ -11052,6 +11564,17 @@ function statusOf(s) {
|
|
|
11052
11564
|
const tier = tierOf(s, BARS.cert);
|
|
11053
11565
|
return tier === "certified" && (s.absentInk?.length ?? 0) > 0 ? "pass" : tier;
|
|
11054
11566
|
}
|
|
11567
|
+
function kitIdentity(setDir) {
|
|
11568
|
+
try {
|
|
11569
|
+
const m = loadManifest(setDir);
|
|
11570
|
+
return {
|
|
11571
|
+
...typeof m.figmaFile === "string" ? { figmaFile: m.figmaFile } : {},
|
|
11572
|
+
...typeof m.componentSetNode === "string" ? { componentSetNode: m.componentSetNode } : {}
|
|
11573
|
+
};
|
|
11574
|
+
} catch {
|
|
11575
|
+
return {};
|
|
11576
|
+
}
|
|
11577
|
+
}
|
|
11055
11578
|
function emitBundleV1(opts) {
|
|
11056
11579
|
const substituted = (opts.substitutedFamilies ?? []).length > 0;
|
|
11057
11580
|
const parityFailed = new Set(opts.behaviors.filter((b) => b.id.startsWith("parity:") && !b.pass).map((b) => b.id.slice("parity:".length)));
|
|
@@ -11070,8 +11593,8 @@ function emitBundleV1(opts) {
|
|
|
11070
11593
|
const prelude = opts.behaviors.filter((b) => b.id.startsWith("prelude:"));
|
|
11071
11594
|
const parity = opts.behaviors.filter((b) => b.id.startsWith("parity:"));
|
|
11072
11595
|
const contract = opts.behaviors.filter((b) => CONTRACT_CHECK_IDS.has(b.id));
|
|
11073
|
-
const cssFiles = ["styles.css", "tokens.css"].map((f) =>
|
|
11074
|
-
const families = cssFontFamilies(cssFiles.map((f) =>
|
|
11596
|
+
const cssFiles = ["styles.css", "tokens.css"].map((f) => path34.join(opts.bundleDir, f)).filter((f) => existsSync27(f));
|
|
11597
|
+
const families = cssFontFamilies(cssFiles.map((f) => readFileSync25(f, "utf8")).join("\n"));
|
|
11075
11598
|
const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
|
|
11076
11599
|
family: f.family,
|
|
11077
11600
|
weight: f.weight,
|
|
@@ -11100,12 +11623,21 @@ function emitBundleV1(opts) {
|
|
|
11100
11623
|
// resolvable via verify's --set override).
|
|
11101
11624
|
path: (() => {
|
|
11102
11625
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
11103
|
-
const rel =
|
|
11626
|
+
const rel = path34.relative(base, opts.task.set);
|
|
11104
11627
|
return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
|
|
11105
11628
|
})(),
|
|
11106
11629
|
component: opts.componentName,
|
|
11107
11630
|
...opts.licenseNote !== void 0 ? { licenseNote: opts.licenseNote } : {},
|
|
11108
|
-
hash: recordingSetHash(opts.task.set, opts.task.configs)
|
|
11631
|
+
hash: recordingSetHash(opts.task.set, opts.task.configs),
|
|
11632
|
+
// PROV-1: kit identity, carried through from the recording set.
|
|
11633
|
+
// Captured at plan time and previously dropped here, which left
|
|
11634
|
+
// a published bundle unable to name the kit it derives from —
|
|
11635
|
+
// and the portal's consent is scoped to exactly this file key
|
|
11636
|
+
// (ADR-018 §3), because the right to publish attaches to a kit.
|
|
11637
|
+
// Read defensively: a set planned before these fields existed,
|
|
11638
|
+
// or one built from a handed-in selection, legitimately has
|
|
11639
|
+
// neither, and absent must read as NOT RECORDED.
|
|
11640
|
+
...kitIdentity(opts.task.set)
|
|
11109
11641
|
},
|
|
11110
11642
|
environment: { ...opts.environment, ...(opts.substitutedFamilies ?? []).length > 0 ? { substitutedFamilies: opts.substitutedFamilies } : {} },
|
|
11111
11643
|
coverage: { recordedConfigs: statuses.length, latticeConfigs: lattice },
|
|
@@ -11128,25 +11660,25 @@ function emitBundleV1(opts) {
|
|
|
11128
11660
|
})
|
|
11129
11661
|
};
|
|
11130
11662
|
const written = [];
|
|
11131
|
-
const manifestPath2 =
|
|
11132
|
-
|
|
11663
|
+
const manifestPath2 = path34.join(opts.bundleDir, "component.json");
|
|
11664
|
+
writeFileSync11(manifestPath2, `${JSON.stringify(manifest, null, 2)}
|
|
11133
11665
|
`);
|
|
11134
11666
|
written.push(manifestPath2);
|
|
11135
|
-
const stylesPath =
|
|
11136
|
-
if (
|
|
11667
|
+
const stylesPath = path34.join(opts.bundleDir, "styles.css");
|
|
11668
|
+
if (existsSync27(stylesPath)) {
|
|
11137
11669
|
const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
|
|
11138
|
-
const current =
|
|
11670
|
+
const current = readFileSync25(stylesPath, "utf8");
|
|
11139
11671
|
const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
|
|
11140
|
-
|
|
11672
|
+
writeFileSync11(stylesPath, `${comment}
|
|
11141
11673
|
${stripped}`);
|
|
11142
11674
|
written.push(stylesPath);
|
|
11143
11675
|
}
|
|
11144
|
-
const fontsCssPath =
|
|
11145
|
-
|
|
11146
|
-
|
|
11676
|
+
const fontsCssPath = path34.join(opts.bundleDir, "fonts.css");
|
|
11677
|
+
rmSync4(path34.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
|
|
11678
|
+
rmSync4(fontsCssPath, { force: true });
|
|
11147
11679
|
const fontsCss = fontsCssFor(requiredFontsManifest(families, opts.fontCacheDir), opts.bundleDir, opts.fontCacheDir, opts.substitutedFamilies ?? []);
|
|
11148
11680
|
if (fontsCss !== null) {
|
|
11149
|
-
|
|
11681
|
+
writeFileSync11(fontsCssPath, fontsCss);
|
|
11150
11682
|
written.push(fontsCssPath);
|
|
11151
11683
|
}
|
|
11152
11684
|
const unrecorded = lattice === null ? null : Math.max(0, lattice - statuses.length);
|
|
@@ -11574,9 +12106,9 @@ DEALINGS IN THE FONT SOFTWARE.
|
|
|
11574
12106
|
});
|
|
11575
12107
|
|
|
11576
12108
|
// packages/generate/src/compose-pins.ts
|
|
11577
|
-
import { createHash as
|
|
11578
|
-
import { existsSync as
|
|
11579
|
-
import
|
|
12109
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
12110
|
+
import { existsSync as existsSync28, readFileSync as readFileSync26, readdirSync as readdirSync12, realpathSync as realpathSync3, statSync as statSync4 } from "node:fs";
|
|
12111
|
+
import path35 from "node:path";
|
|
11580
12112
|
function bundleDirs(roots, depth = 4) {
|
|
11581
12113
|
const found = [];
|
|
11582
12114
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -11585,31 +12117,31 @@ function bundleDirs(roots, depth = 4) {
|
|
|
11585
12117
|
try {
|
|
11586
12118
|
key = realpathSync3(dir);
|
|
11587
12119
|
} catch {
|
|
11588
|
-
key =
|
|
12120
|
+
key = path35.resolve(dir);
|
|
11589
12121
|
}
|
|
11590
12122
|
if (seen.has(key)) return;
|
|
11591
12123
|
seen.add(key);
|
|
11592
|
-
if (
|
|
12124
|
+
if (existsSync28(path35.join(dir, "component.json"))) {
|
|
11593
12125
|
found.push(key);
|
|
11594
12126
|
return;
|
|
11595
12127
|
}
|
|
11596
12128
|
if (remaining === 0) return;
|
|
11597
12129
|
let entries;
|
|
11598
12130
|
try {
|
|
11599
|
-
entries =
|
|
12131
|
+
entries = readdirSync12(dir);
|
|
11600
12132
|
} catch {
|
|
11601
12133
|
return;
|
|
11602
12134
|
}
|
|
11603
12135
|
for (const e of entries) {
|
|
11604
12136
|
if (e === "node_modules" || e.startsWith(".")) continue;
|
|
11605
|
-
const full =
|
|
12137
|
+
const full = path35.join(dir, e);
|
|
11606
12138
|
try {
|
|
11607
|
-
if (
|
|
12139
|
+
if (statSync4(full).isDirectory()) walk2(full, remaining - 1);
|
|
11608
12140
|
} catch {
|
|
11609
12141
|
}
|
|
11610
12142
|
}
|
|
11611
12143
|
};
|
|
11612
|
-
for (const r of roots) walk2(
|
|
12144
|
+
for (const r of roots) walk2(path35.resolve(r), depth);
|
|
11613
12145
|
return found;
|
|
11614
12146
|
}
|
|
11615
12147
|
function composedPins(hostSet, libraryRoots) {
|
|
@@ -11628,7 +12160,7 @@ function composedPins(hostSet, libraryRoots) {
|
|
|
11628
12160
|
let pinned = false;
|
|
11629
12161
|
const failures = [];
|
|
11630
12162
|
for (const rel of partnerRels) {
|
|
11631
|
-
const partnerSet =
|
|
12163
|
+
const partnerSet = path35.resolve(hostSet, rel);
|
|
11632
12164
|
let partnerTask;
|
|
11633
12165
|
let partnerManifest;
|
|
11634
12166
|
try {
|
|
@@ -11657,7 +12189,7 @@ function composedPins(hostSet, libraryRoots) {
|
|
|
11657
12189
|
const wantHash = recordingSetHash(partnerSet, partnerTask.configs);
|
|
11658
12190
|
const matches = candidates.filter((dir) => {
|
|
11659
12191
|
try {
|
|
11660
|
-
const parsed = readBundleManifest(
|
|
12192
|
+
const parsed = readBundleManifest(readFileSync26(path35.join(dir, "component.json"), "utf8"));
|
|
11661
12193
|
return parsed.manifest?.provenance.recordingSet.hash === wantHash;
|
|
11662
12194
|
} catch {
|
|
11663
12195
|
return false;
|
|
@@ -11670,13 +12202,13 @@ function composedPins(hostSet, libraryRoots) {
|
|
|
11670
12202
|
continue;
|
|
11671
12203
|
}
|
|
11672
12204
|
if (matches.length > 1) {
|
|
11673
|
-
failures.push(`${rel}: ${matches.length} bundles join the same recording hash (${matches.map((m) =>
|
|
12205
|
+
failures.push(`${rel}: ${matches.length} bundles join the same recording hash (${matches.map((m) => path35.basename(m)).join(", ")}) \u2014 ambiguous; remove or point --library away from the duplicates`);
|
|
11674
12206
|
continue;
|
|
11675
12207
|
}
|
|
11676
12208
|
const bundleDir = matches[0];
|
|
11677
12209
|
let manifest;
|
|
11678
12210
|
try {
|
|
11679
|
-
manifest = readBundleManifest(
|
|
12211
|
+
manifest = readBundleManifest(readFileSync26(path35.join(bundleDir, "component.json"), "utf8")).manifest;
|
|
11680
12212
|
} catch {
|
|
11681
12213
|
manifest = void 0;
|
|
11682
12214
|
}
|
|
@@ -11693,8 +12225,8 @@ function composedPins(hostSet, libraryRoots) {
|
|
|
11693
12225
|
const moduleFiles = [];
|
|
11694
12226
|
let fileIssue;
|
|
11695
12227
|
for (const name of [manifest.entry, "styles.css", "tokens.css", "fonts.css"]) {
|
|
11696
|
-
const file =
|
|
11697
|
-
if (!
|
|
12228
|
+
const file = path35.join(bundleDir, name);
|
|
12229
|
+
if (!existsSync28(file)) {
|
|
11698
12230
|
if (name === manifest.entry || name === "styles.css") {
|
|
11699
12231
|
fileIssue = `${rel}: partner bundle is missing ${name}`;
|
|
11700
12232
|
break;
|
|
@@ -11703,7 +12235,7 @@ function composedPins(hostSet, libraryRoots) {
|
|
|
11703
12235
|
}
|
|
11704
12236
|
let bytes;
|
|
11705
12237
|
try {
|
|
11706
|
-
bytes =
|
|
12238
|
+
bytes = readFileSync26(file);
|
|
11707
12239
|
} catch {
|
|
11708
12240
|
fileIssue = `${rel}: partner file ${name} at ${bundleDir} is unreadable`;
|
|
11709
12241
|
break;
|
|
@@ -11712,7 +12244,7 @@ function composedPins(hostSet, libraryRoots) {
|
|
|
11712
12244
|
fileIssue = `${rel}: partner file ${name} exceeds the pin size cap (${bytes.byteLength} bytes)`;
|
|
11713
12245
|
break;
|
|
11714
12246
|
}
|
|
11715
|
-
moduleFiles.push({ name, content: bytes.toString("utf8"), sha256:
|
|
12247
|
+
moduleFiles.push({ name, content: bytes.toString("utf8"), sha256: createHash7("sha256").update(bytes).digest("hex") });
|
|
11716
12248
|
}
|
|
11717
12249
|
if (fileIssue !== void 0) {
|
|
11718
12250
|
failures.push(fileIssue);
|
|
@@ -11775,14 +12307,14 @@ function composedChecks(candidateDir, hostEntry, pins) {
|
|
|
11775
12307
|
const checks = [];
|
|
11776
12308
|
let entrySource = "";
|
|
11777
12309
|
try {
|
|
11778
|
-
entrySource =
|
|
12310
|
+
entrySource = readFileSync26(path35.join(candidateDir, hostEntry), "utf8");
|
|
11779
12311
|
} catch {
|
|
11780
12312
|
}
|
|
11781
|
-
const candidateRoot =
|
|
12313
|
+
const candidateRoot = path35.resolve(candidateDir);
|
|
11782
12314
|
for (const pin of pins) {
|
|
11783
12315
|
const dir = composedModuleDir(pin.partnerName);
|
|
11784
|
-
const resolvedDir =
|
|
11785
|
-
if (!resolvedDir.startsWith(candidateRoot +
|
|
12316
|
+
const resolvedDir = path35.resolve(candidateDir, dir);
|
|
12317
|
+
if (!resolvedDir.startsWith(candidateRoot + path35.sep)) {
|
|
11786
12318
|
checks.push({ id: `composition:${pin.pairKey}:module-verbatim`, pass: false, detail: `pin path ${dir}/ escapes the candidate directory \u2014 refused` });
|
|
11787
12319
|
checks.push({ id: `composition:${pin.pairKey}:imported`, pass: false, detail: `pin path ${dir}/ escapes the candidate directory \u2014 refused` });
|
|
11788
12320
|
continue;
|
|
@@ -11793,12 +12325,12 @@ function composedChecks(candidateDir, hostEntry, pins) {
|
|
|
11793
12325
|
wrong.push(`${f.name} refused (not a plain path segment)`);
|
|
11794
12326
|
continue;
|
|
11795
12327
|
}
|
|
11796
|
-
const target =
|
|
11797
|
-
if (!
|
|
12328
|
+
const target = path35.join(candidateDir, dir, f.name);
|
|
12329
|
+
if (!existsSync28(target)) {
|
|
11798
12330
|
wrong.push(`${f.name} missing`);
|
|
11799
12331
|
continue;
|
|
11800
12332
|
}
|
|
11801
|
-
const sha =
|
|
12333
|
+
const sha = createHash7("sha256").update(readFileSync26(target)).digest("hex");
|
|
11802
12334
|
if (sha !== f.sha256) wrong.push(`${f.name} differs from the pinned partner bytes`);
|
|
11803
12335
|
}
|
|
11804
12336
|
checks.push({
|
|
@@ -11823,10 +12355,10 @@ function rootClassesFor(emission, nodeId) {
|
|
|
11823
12355
|
}
|
|
11824
12356
|
function regionOverrides(hostSet, partnerSet, instances) {
|
|
11825
12357
|
const read = (setDir, rep) => {
|
|
11826
|
-
const f =
|
|
11827
|
-
if (!
|
|
12358
|
+
const f = path35.join(setDir, rep, "get_design_context.json");
|
|
12359
|
+
if (!existsSync28(f)) return void 0;
|
|
11828
12360
|
try {
|
|
11829
|
-
return envelopeFirstTextPart(JSON.parse(
|
|
12361
|
+
return envelopeFirstTextPart(JSON.parse(readFileSync26(f, "utf8")));
|
|
11830
12362
|
} catch {
|
|
11831
12363
|
return void 0;
|
|
11832
12364
|
}
|
|
@@ -11856,7 +12388,7 @@ var init_compose_pins = __esm({
|
|
|
11856
12388
|
init_src4();
|
|
11857
12389
|
init_brief();
|
|
11858
12390
|
init_bundle_emit();
|
|
11859
|
-
composedModuleDir = (partnerName) =>
|
|
12391
|
+
composedModuleDir = (partnerName) => path35.posix.join("composed", partnerName);
|
|
11860
12392
|
safeSegment = (s) => /^[A-Za-z0-9][A-Za-z0-9._ -]*$/.test(s) && !s.includes("..");
|
|
11861
12393
|
MAX_PINNED_FILE_BYTES = 1024 * 1024;
|
|
11862
12394
|
CONTEXT_LAYOUT_TOKENS = /* @__PURE__ */ new Set(["shrink-0", "grow", "flex-1", "basis-0", "self-stretch", "w-full", "h-full"]);
|
|
@@ -11864,8 +12396,8 @@ var init_compose_pins = __esm({
|
|
|
11864
12396
|
});
|
|
11865
12397
|
|
|
11866
12398
|
// packages/generate/src/motion.ts
|
|
11867
|
-
import { existsSync as
|
|
11868
|
-
import
|
|
12399
|
+
import { existsSync as existsSync29, readFileSync as readFileSync27, readdirSync as readdirSync13, statSync as statSync5 } from "node:fs";
|
|
12400
|
+
import path36 from "node:path";
|
|
11869
12401
|
function springProgress(u, bounce) {
|
|
11870
12402
|
const decay = Math.log(100);
|
|
11871
12403
|
if (bounce <= 0) {
|
|
@@ -11936,10 +12468,10 @@ function reportsNoMotion(text) {
|
|
|
11936
12468
|
});
|
|
11937
12469
|
}
|
|
11938
12470
|
function motionTruthFor(setDir) {
|
|
11939
|
-
const file =
|
|
11940
|
-
if (
|
|
12471
|
+
const file = path36.join(setDir, "get_motion_context.json");
|
|
12472
|
+
if (existsSync29(file) && usableEnvelope(file, "get_motion_context").ok) {
|
|
11941
12473
|
try {
|
|
11942
|
-
const text = envelopeTextContent(JSON.parse(
|
|
12474
|
+
const text = envelopeTextContent(JSON.parse(readFileSync27(file, "utf8")));
|
|
11943
12475
|
if (text.trim() === "") return { state: "recorded-empty" };
|
|
11944
12476
|
return reportsNoMotion(text) ? { state: "recorded-no-motion", text } : { state: "recorded", text };
|
|
11945
12477
|
} catch {
|
|
@@ -11952,21 +12484,21 @@ function motionTruthFor(setDir) {
|
|
|
11952
12484
|
}
|
|
11953
12485
|
}
|
|
11954
12486
|
function motionDisclosure(bundleDir, setDir) {
|
|
11955
|
-
const sheets = ["styles.css", "tokens.css"].map((f) =>
|
|
11956
|
-
const composedRoot =
|
|
12487
|
+
const sheets = ["styles.css", "tokens.css"].map((f) => path36.join(bundleDir, f));
|
|
12488
|
+
const composedRoot = path36.join(bundleDir, "composed");
|
|
11957
12489
|
try {
|
|
11958
|
-
for (const entry of
|
|
11959
|
-
const dir =
|
|
12490
|
+
for (const entry of readdirSync13(composedRoot).sort()) {
|
|
12491
|
+
const dir = path36.join(composedRoot, entry);
|
|
11960
12492
|
try {
|
|
11961
|
-
if (!
|
|
12493
|
+
if (!statSync5(dir).isDirectory()) continue;
|
|
11962
12494
|
} catch {
|
|
11963
12495
|
continue;
|
|
11964
12496
|
}
|
|
11965
|
-
sheets.push(
|
|
12497
|
+
sheets.push(path36.join(dir, "styles.css"), path36.join(dir, "tokens.css"));
|
|
11966
12498
|
}
|
|
11967
12499
|
} catch {
|
|
11968
12500
|
}
|
|
11969
|
-
const css = sheets.filter((f) =>
|
|
12501
|
+
const css = sheets.filter((f) => existsSync29(f)).map((f) => readFileSync27(f, "utf8")).join("\n");
|
|
11970
12502
|
if (!MOTION_CSS_PATTERN.test(scannableCss(css))) return { present: false };
|
|
11971
12503
|
return {
|
|
11972
12504
|
present: true,
|
|
@@ -12333,12 +12865,12 @@ var init_components = __esm({
|
|
|
12333
12865
|
|
|
12334
12866
|
// packages/generate/src/codebase/walk.ts
|
|
12335
12867
|
import fs2 from "node:fs";
|
|
12336
|
-
import
|
|
12868
|
+
import path37 from "node:path";
|
|
12337
12869
|
function resolvedPathIsExcluded(real, roots) {
|
|
12338
|
-
if (isNeverRead(
|
|
12870
|
+
if (isNeverRead(path37.basename(real))) return true;
|
|
12339
12871
|
for (const root of roots) {
|
|
12340
|
-
if (real !== root && !real.startsWith(root +
|
|
12341
|
-
for (const segment of
|
|
12872
|
+
if (real !== root && !real.startsWith(root + path37.sep)) continue;
|
|
12873
|
+
for (const segment of path37.relative(root, real).split(path37.sep).slice(0, -1)) {
|
|
12342
12874
|
if (segment.startsWith(".") || EXCLUDED_DIRS.has(segment)) return true;
|
|
12343
12875
|
}
|
|
12344
12876
|
}
|
|
@@ -12352,7 +12884,7 @@ function containedRealpath(abs, roots) {
|
|
|
12352
12884
|
return null;
|
|
12353
12885
|
}
|
|
12354
12886
|
for (const root of roots) {
|
|
12355
|
-
if (real === root || real.startsWith(root +
|
|
12887
|
+
if (real === root || real.startsWith(root + path37.sep)) return real;
|
|
12356
12888
|
}
|
|
12357
12889
|
return null;
|
|
12358
12890
|
}
|
|
@@ -12385,7 +12917,7 @@ function walkRepo(roots, limits, accept) {
|
|
|
12385
12917
|
continue;
|
|
12386
12918
|
}
|
|
12387
12919
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
12388
|
-
const abs =
|
|
12920
|
+
const abs = path37.join(frame.dir, entry.name);
|
|
12389
12921
|
if (EXCLUDED_DIRS.has(entry.name)) continue;
|
|
12390
12922
|
if (isNeverRead(entry.name)) continue;
|
|
12391
12923
|
const real = containedRealpath(abs, realRoots);
|
|
@@ -12473,18 +13005,18 @@ var init_walk = __esm({
|
|
|
12473
13005
|
/^\.netrc$/i
|
|
12474
13006
|
];
|
|
12475
13007
|
isNeverRead = (basename) => NEVER_READ.some((re) => re.test(basename));
|
|
12476
|
-
toRel = (root, abs) =>
|
|
13008
|
+
toRel = (root, abs) => path37.relative(root, abs).split(path37.sep).join(path37.posix.sep);
|
|
12477
13009
|
}
|
|
12478
13010
|
});
|
|
12479
13011
|
|
|
12480
13012
|
// packages/generate/src/codebase/scan.ts
|
|
12481
13013
|
import crypto2 from "node:crypto";
|
|
12482
13014
|
import fs3 from "node:fs";
|
|
12483
|
-
import
|
|
13015
|
+
import path38 from "node:path";
|
|
12484
13016
|
import postcss3 from "postcss";
|
|
12485
13017
|
function scanCodebase(options) {
|
|
12486
13018
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
12487
|
-
const roots = options.roots.map((r) =>
|
|
13019
|
+
const roots = options.roots.map((r) => path38.resolve(r));
|
|
12488
13020
|
const walk2 = walkRepo(
|
|
12489
13021
|
roots,
|
|
12490
13022
|
{
|
|
@@ -12504,7 +13036,7 @@ function scanCodebase(options) {
|
|
|
12504
13036
|
let bytesRead = 0;
|
|
12505
13037
|
let filesRead = 0;
|
|
12506
13038
|
for (const file of walk2.files) {
|
|
12507
|
-
const base =
|
|
13039
|
+
const base = path38.posix.basename(file.rel);
|
|
12508
13040
|
configFiles.add(file.rel);
|
|
12509
13041
|
if (/^tailwind\.config\./.test(base) || file.rel === "babel.config.js") continue;
|
|
12510
13042
|
const text = readTextFile(file.abs);
|
|
@@ -12520,7 +13052,7 @@ function scanCodebase(options) {
|
|
|
12520
13052
|
}
|
|
12521
13053
|
const css = extractCssCustomProperties(cssFiles.filter((f) => !f.rel.includes("..")));
|
|
12522
13054
|
const components = scanComponents(componentFiles);
|
|
12523
|
-
const packages = manifests.filter((m) =>
|
|
13055
|
+
const packages = manifests.filter((m) => path38.posix.basename(m.rel) === "package.json");
|
|
12524
13056
|
const styling = detectStyling(configFiles, cssFiles, componentFiles, manifests);
|
|
12525
13057
|
const classNameStyle = representativeClassNames(cssFiles, css.unparsed.length);
|
|
12526
13058
|
const disclosures = buildDisclosures(
|
|
@@ -12563,13 +13095,13 @@ function scanCodebase(options) {
|
|
|
12563
13095
|
},
|
|
12564
13096
|
components: {
|
|
12565
13097
|
entries: components.entries.slice(0, PROFILE_LIMITS.maxComponents),
|
|
12566
|
-
fileNaming: buildHistogram(componentFiles.map((f) => classifyFileName(
|
|
13098
|
+
fileNaming: buildHistogram(componentFiles.map((f) => classifyFileName(path38.posix.basename(f.rel)))),
|
|
12567
13099
|
directoryLayout: buildHistogram(componentFiles.map((f) => classifyDirectoryLayout(f.rel))),
|
|
12568
13100
|
exportStyle: buildHistogram(components.entries.map((e) => e.exportStyle)),
|
|
12569
13101
|
classNameStyle,
|
|
12570
13102
|
colocation: buildHistogram(collectColocation(componentFiles, cssFiles)),
|
|
12571
13103
|
barrelFiles: componentFiles.filter(
|
|
12572
|
-
(f) => /^index\.[tj]sx?$/.test(
|
|
13104
|
+
(f) => /^index\.[tj]sx?$/.test(path38.posix.basename(f.rel)) && isReExportOnly(f.text)
|
|
12573
13105
|
).length,
|
|
12574
13106
|
refForwarding: {
|
|
12575
13107
|
forwardRef: componentFiles.filter((f) => /\bforwardRef\s*[(<]/.test(f.text)).length,
|
|
@@ -12592,7 +13124,7 @@ function detectStyling(configFiles, cssFiles, componentFiles, manifests) {
|
|
|
12592
13124
|
};
|
|
12593
13125
|
const deps = /* @__PURE__ */ new Map();
|
|
12594
13126
|
for (const manifest of manifests) {
|
|
12595
|
-
if (
|
|
13127
|
+
if (path38.posix.basename(manifest.rel) !== "package.json") continue;
|
|
12596
13128
|
try {
|
|
12597
13129
|
const parsed = JSON.parse(manifest.text);
|
|
12598
13130
|
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
@@ -12604,7 +13136,7 @@ function detectStyling(configFiles, cssFiles, componentFiles, manifests) {
|
|
|
12604
13136
|
}
|
|
12605
13137
|
}
|
|
12606
13138
|
for (const cfg of configFiles) {
|
|
12607
|
-
const base =
|
|
13139
|
+
const base = path38.posix.basename(cfg);
|
|
12608
13140
|
if (/^tailwind\.config\./.test(base)) add("tailwind-v3", "file", cfg);
|
|
12609
13141
|
if (base === "components.json") add("shadcn-style", "file", cfg);
|
|
12610
13142
|
}
|
|
@@ -12662,8 +13194,8 @@ function collectClassNames(cssFiles) {
|
|
|
12662
13194
|
return [...distinct].sort().map(classifyClassName);
|
|
12663
13195
|
}
|
|
12664
13196
|
function classifyDirectoryLayout(rel) {
|
|
12665
|
-
const base =
|
|
12666
|
-
const dir =
|
|
13197
|
+
const base = path38.posix.basename(rel).replace(/\.[^.]+$/, "");
|
|
13198
|
+
const dir = path38.posix.basename(path38.posix.dirname(rel));
|
|
12667
13199
|
if (base === "index") return "component-dir";
|
|
12668
13200
|
if (base === dir) return "component-dir";
|
|
12669
13201
|
return "flat-file";
|
|
@@ -12687,7 +13219,7 @@ function detectFormatting(manifests, componentFiles) {
|
|
|
12687
13219
|
(a, b) => a.rel.split("/").length - b.rel.split("/").length || a.rel.localeCompare(b.rel)
|
|
12688
13220
|
);
|
|
12689
13221
|
for (const manifest of byDepth) {
|
|
12690
|
-
const base =
|
|
13222
|
+
const base = path38.posix.basename(manifest.rel);
|
|
12691
13223
|
if (!/^\.prettierrc/.test(base) && base !== "package.json") continue;
|
|
12692
13224
|
try {
|
|
12693
13225
|
const parsed = JSON.parse(manifest.text);
|
|
@@ -12705,7 +13237,7 @@ function detectFormatting(manifests, componentFiles) {
|
|
|
12705
13237
|
}
|
|
12706
13238
|
}
|
|
12707
13239
|
for (const manifest of byDepth) {
|
|
12708
|
-
if (
|
|
13240
|
+
if (path38.posix.basename(manifest.rel) !== ".editorconfig") continue;
|
|
12709
13241
|
const style = /indent_style\s*=\s*(tab|space)/.exec(manifest.text)?.[1];
|
|
12710
13242
|
const width = /indent_size\s*=\s*(\d+)/.exec(manifest.text)?.[1];
|
|
12711
13243
|
if (style || width) {
|
|
@@ -12776,12 +13308,12 @@ function buildDisclosures(detected, css, cappedOut, unrepresentativeClassNames)
|
|
|
12776
13308
|
return out;
|
|
12777
13309
|
}
|
|
12778
13310
|
function outPathIsGitIgnored(outPath) {
|
|
12779
|
-
const dir =
|
|
13311
|
+
const dir = path38.dirname(outPath);
|
|
12780
13312
|
try {
|
|
12781
|
-
const ignoreFile =
|
|
13313
|
+
const ignoreFile = path38.join(path38.dirname(dir), ".gitignore");
|
|
12782
13314
|
if (!fs3.existsSync(ignoreFile)) return false;
|
|
12783
13315
|
const patterns = fs3.readFileSync(ignoreFile, "utf8").split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
12784
|
-
const base =
|
|
13316
|
+
const base = path38.basename(dir);
|
|
12785
13317
|
return patterns.some((p) => p === base || p === `${base}/` || p === `/${base}` || p === `/${base}/`);
|
|
12786
13318
|
} catch {
|
|
12787
13319
|
return false;
|
|
@@ -12928,8 +13460,8 @@ __export(profile_exports, {
|
|
|
12928
13460
|
PROFILE_DESCRIPTION: () => PROFILE_DESCRIPTION,
|
|
12929
13461
|
runProfile: () => runProfile
|
|
12930
13462
|
});
|
|
12931
|
-
import { closeSync, constants, existsSync as
|
|
12932
|
-
import
|
|
13463
|
+
import { closeSync, constants, existsSync as existsSync30, mkdirSync as mkdirSync9, openSync, realpathSync as realpathSync4, writeFileSync as writeFileSync12 } from "node:fs";
|
|
13464
|
+
import path39 from "node:path";
|
|
12933
13465
|
function escapesScanRoot(outPath, scanRoot) {
|
|
12934
13466
|
const resolveExisting = (target) => {
|
|
12935
13467
|
let cursor = target;
|
|
@@ -12937,23 +13469,23 @@ function escapesScanRoot(outPath, scanRoot) {
|
|
|
12937
13469
|
try {
|
|
12938
13470
|
return realpathSync4(cursor);
|
|
12939
13471
|
} catch {
|
|
12940
|
-
const parent =
|
|
13472
|
+
const parent = path39.dirname(cursor);
|
|
12941
13473
|
if (parent === cursor) return cursor;
|
|
12942
13474
|
cursor = parent;
|
|
12943
13475
|
}
|
|
12944
13476
|
}
|
|
12945
13477
|
};
|
|
12946
13478
|
const root = resolveExisting(scanRoot);
|
|
12947
|
-
const dir = resolveExisting(
|
|
12948
|
-
return dir !== root && !dir.startsWith(root +
|
|
13479
|
+
const dir = resolveExisting(path39.dirname(outPath));
|
|
13480
|
+
return dir !== root && !dir.startsWith(root + path39.sep);
|
|
12949
13481
|
}
|
|
12950
13482
|
function runProfile(options) {
|
|
12951
13483
|
if (options.describe) {
|
|
12952
13484
|
printDescription(PROFILE_DESCRIPTION);
|
|
12953
13485
|
return;
|
|
12954
13486
|
}
|
|
12955
|
-
const dir =
|
|
12956
|
-
if (!
|
|
13487
|
+
const dir = path39.resolve(options.dir ?? ".");
|
|
13488
|
+
if (!existsSync30(dir)) {
|
|
12957
13489
|
fail(options, ExitCode.InputValidation, {
|
|
12958
13490
|
error: `no such directory: ${dir}`,
|
|
12959
13491
|
code: "profile_dir_missing",
|
|
@@ -12961,7 +13493,7 @@ function runProfile(options) {
|
|
|
12961
13493
|
});
|
|
12962
13494
|
}
|
|
12963
13495
|
const profile = scanCodebase({ roots: [dir], ...options.now ? { now: options.now } : {} });
|
|
12964
|
-
const outPath =
|
|
13496
|
+
const outPath = path39.resolve(options.out ?? path39.join(dir, "tendril-out", "codebase-profile.json"));
|
|
12965
13497
|
if (!options.dryRun) {
|
|
12966
13498
|
if (options.out === void 0 && escapesScanRoot(outPath, dir)) {
|
|
12967
13499
|
fail(options, ExitCode.InputValidation, {
|
|
@@ -12970,10 +13502,10 @@ function runProfile(options) {
|
|
|
12970
13502
|
remediation: `\`tendril-out\` in that project is a symlink pointing outside it, so writing the profile there could overwrite an unrelated file. Remove the symlink, or choose an explicit destination: \`${tendrilCommand(`profile --dir ${quoteArg(dir)} --out ./codebase-profile.json`)}\`.`
|
|
12971
13503
|
});
|
|
12972
13504
|
}
|
|
12973
|
-
|
|
13505
|
+
mkdirSync9(path39.dirname(outPath), { recursive: true });
|
|
12974
13506
|
const handle = openSync(outPath, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW, 420);
|
|
12975
13507
|
try {
|
|
12976
|
-
|
|
13508
|
+
writeFileSync12(handle, `${JSON.stringify(profile, null, 2)}
|
|
12977
13509
|
`, "utf8");
|
|
12978
13510
|
} finally {
|
|
12979
13511
|
closeSync(handle);
|
|
@@ -13035,7 +13567,7 @@ Written to ${outPath}
|
|
|
13035
13567
|
`);
|
|
13036
13568
|
if (!ignored) {
|
|
13037
13569
|
process.stdout.write(
|
|
13038
|
-
` NOTE: ${
|
|
13570
|
+
` NOTE: ${path39.basename(path39.dirname(outPath))}/ is not gitignored here \u2014 add it to .gitignore, or this profile will show up in your next commit.
|
|
13039
13571
|
`
|
|
13040
13572
|
);
|
|
13041
13573
|
}
|
|
@@ -13174,11 +13706,11 @@ __export(compose_exports, {
|
|
|
13174
13706
|
compositionPairsFor: () => compositionPairsFor,
|
|
13175
13707
|
runCompose: () => runCompose
|
|
13176
13708
|
});
|
|
13177
|
-
import { createHash as
|
|
13178
|
-
import { existsSync as
|
|
13179
|
-
import
|
|
13709
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
13710
|
+
import { existsSync as existsSync31, readFileSync as readFileSync28 } from "node:fs";
|
|
13711
|
+
import path40 from "node:path";
|
|
13180
13712
|
function compositionPairsFor(hostSet, roots) {
|
|
13181
|
-
const scanRoots = [.../* @__PURE__ */ new Set([...roots,
|
|
13713
|
+
const scanRoots = [.../* @__PURE__ */ new Set([...roots, path40.dirname(hostSet)])];
|
|
13182
13714
|
const edges = composeReport(buildComposeIndex(scanRoots));
|
|
13183
13715
|
const pairs = substitutionPairs(edges, hostSet);
|
|
13184
13716
|
const { raw } = readManifestFile(hostSet);
|
|
@@ -13210,7 +13742,7 @@ function substitutionPairs(edges, hostSet) {
|
|
|
13210
13742
|
});
|
|
13211
13743
|
}
|
|
13212
13744
|
const pair = pairs.get(key);
|
|
13213
|
-
const poseDisplay = e.pose.reps.map((r) => `${
|
|
13745
|
+
const poseDisplay = e.pose.reps.map((r) => `${path40.basename(r.dir)}:${r.slug}`).join(", ");
|
|
13214
13746
|
pair.instances.push({ hostRep: e.hostRep, instanceId: e.instanceId, poseVariantNodeId: e.pose.variantNodeId, ...poseDisplay !== "" ? { poseDisplay } : {} });
|
|
13215
13747
|
for (const d of e.disclosures) if (!pair.disclosures.includes(d)) pair.disclosures.push(d);
|
|
13216
13748
|
}
|
|
@@ -13222,7 +13754,7 @@ function runCompose(flags) {
|
|
|
13222
13754
|
return;
|
|
13223
13755
|
}
|
|
13224
13756
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
13225
|
-
const roots = flags.library !== void 0 && flags.library.length > 0 ? flags.library.map((d) =>
|
|
13757
|
+
const roots = flags.library !== void 0 && flags.library.length > 0 ? flags.library.map((d) => path40.resolve(base, d)) : [base];
|
|
13226
13758
|
if (flags.set === void 0 && (flags.confirmCompositions === true || flags.decline !== void 0 && flags.decline.length > 0)) {
|
|
13227
13759
|
fail(flags, ExitCode.InputValidation, {
|
|
13228
13760
|
error: "--confirm-compositions/--decline require --set <host> \u2014 a decision needs the host set it decides for",
|
|
@@ -13231,7 +13763,7 @@ function runCompose(flags) {
|
|
|
13231
13763
|
});
|
|
13232
13764
|
}
|
|
13233
13765
|
if (flags.set !== void 0) {
|
|
13234
|
-
runComposeConfirm(flags,
|
|
13766
|
+
runComposeConfirm(flags, path40.resolve(base, flags.set), roots);
|
|
13235
13767
|
return;
|
|
13236
13768
|
}
|
|
13237
13769
|
const index = buildComposeIndex(roots);
|
|
@@ -13249,7 +13781,7 @@ function runCompose(flags) {
|
|
|
13249
13781
|
}
|
|
13250
13782
|
let lastHost = "";
|
|
13251
13783
|
for (const e of edges) {
|
|
13252
|
-
const host = `${
|
|
13784
|
+
const host = `${path40.basename(e.hostSet)}`;
|
|
13253
13785
|
if (host !== lastHost) {
|
|
13254
13786
|
process.stdout.write(`
|
|
13255
13787
|
${host}
|
|
@@ -13257,7 +13789,7 @@ ${host}
|
|
|
13257
13789
|
lastHost = host;
|
|
13258
13790
|
}
|
|
13259
13791
|
const partners = e.partners.map((p) => p.displayName).join(" / ") || "\u2014";
|
|
13260
|
-
const pose = e.pose !== void 0 ? ` pose=${e.pose.variantNodeId} (${e.pose.reps.map((r) => `${
|
|
13792
|
+
const pose = e.pose !== void 0 ? ` pose=${e.pose.variantNodeId} (${e.pose.reps.map((r) => `${path40.basename(r.dir)}:${r.slug}`).join(", ")})` : "";
|
|
13261
13793
|
process.stdout.write(` ${e.kind.toUpperCase().padEnd(12)} ${e.hostRep}/${e.instanceId} "${e.instanceName}" \u2192 ${partners}${pose}
|
|
13262
13794
|
`);
|
|
13263
13795
|
for (const d of e.disclosures) process.stdout.write(` \xB7 ${d}
|
|
@@ -13269,14 +13801,14 @@ ${NOTE}
|
|
|
13269
13801
|
});
|
|
13270
13802
|
}
|
|
13271
13803
|
function runComposeConfirm(flags, hostSet, roots) {
|
|
13272
|
-
if (!
|
|
13804
|
+
if (!existsSync31(path40.join(hostSet, "recording-set.json"))) {
|
|
13273
13805
|
fail(flags, ExitCode.InputValidation, {
|
|
13274
13806
|
error: `no recording-set.json in ${hostSet}`,
|
|
13275
13807
|
code: "no-recording-set",
|
|
13276
13808
|
remediation: "Point --set at a recorded host set (the directory holding recording-set.json)."
|
|
13277
13809
|
});
|
|
13278
13810
|
}
|
|
13279
|
-
const scanRoots = [.../* @__PURE__ */ new Set([...roots,
|
|
13811
|
+
const scanRoots = [.../* @__PURE__ */ new Set([...roots, path40.dirname(hostSet)])];
|
|
13280
13812
|
const index = buildComposeIndex(scanRoots);
|
|
13281
13813
|
const edges = composeReport(index);
|
|
13282
13814
|
const pairs = substitutionPairs(edges, hostSet);
|
|
@@ -13359,7 +13891,7 @@ PAIR ${p.displayName}${p.figmaFile !== void 0 ? ` (file ${p.figmaFile})` : " \u2
|
|
|
13359
13891
|
// full recording-set hash join lands with pin authoring, where
|
|
13360
13892
|
// task configs exist.)
|
|
13361
13893
|
manifestSha256: Object.fromEntries(
|
|
13362
|
-
p.partnerDirs.map((d) => [
|
|
13894
|
+
p.partnerDirs.map((d) => [path40.relative(hostSet, d), createHash8("sha256").update(readFileSync28(path40.join(d, "recording-set.json"))).digest("hex")])
|
|
13363
13895
|
)
|
|
13364
13896
|
},
|
|
13365
13897
|
// Schema shape ONLY (review: the display-side poseDisplay field
|
|
@@ -13438,10 +13970,10 @@ __export(record_exports, {
|
|
|
13438
13970
|
runRecordPlan: () => runRecordPlan,
|
|
13439
13971
|
runRecordStatus: () => runRecordStatus
|
|
13440
13972
|
});
|
|
13441
|
-
import { existsSync as
|
|
13442
|
-
import
|
|
13443
|
-
import
|
|
13444
|
-
import { writeFileSync as
|
|
13973
|
+
import { existsSync as existsSync32, mkdtempSync as mkdtempSync2, readFileSync as readFileSync29, readdirSync as readdirSync14 } from "node:fs";
|
|
13974
|
+
import os7 from "node:os";
|
|
13975
|
+
import path41 from "node:path";
|
|
13976
|
+
import { writeFileSync as writeFileSync13 } from "node:fs";
|
|
13445
13977
|
function recordsInteractionState(reports) {
|
|
13446
13978
|
const evident = (s) => stateTokens(s).some((t) => INTERACTION_TREATMENT_VALUES.has(t));
|
|
13447
13979
|
return reports.some((r) => evident(r.axis) || r.domain.some(evident));
|
|
@@ -13463,7 +13995,7 @@ function interactionDisclosure(component, reports) {
|
|
|
13463
13995
|
};
|
|
13464
13996
|
}
|
|
13465
13997
|
function symbolsFromMetadataEnvelope(file, sourceFrame) {
|
|
13466
|
-
const env = JSON.parse(
|
|
13998
|
+
const env = JSON.parse(readFileSync29(file, "utf8"));
|
|
13467
13999
|
const text = env.content.map((c) => c.text ?? "").join("\n");
|
|
13468
14000
|
const symbols = [];
|
|
13469
14001
|
const walk2 = (node, ancestor) => {
|
|
@@ -13521,8 +14053,8 @@ function runRecordPlan(opts) {
|
|
|
13521
14053
|
if (rawFile !== void 0) {
|
|
13522
14054
|
try {
|
|
13523
14055
|
const envelope = rawEnvelopeFromFile(rawFile, opts.metadataRawPartsFile !== void 0);
|
|
13524
|
-
const tmp =
|
|
13525
|
-
|
|
14056
|
+
const tmp = path41.join(mkdtempSync2(path41.join(os7.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
|
|
14057
|
+
writeFileSync13(tmp, JSON.stringify(envelope));
|
|
13526
14058
|
metadataEntries.push({ file: tmp });
|
|
13527
14059
|
} catch (err) {
|
|
13528
14060
|
fail(opts, ExitCode.InputValidation, {
|
|
@@ -13543,7 +14075,7 @@ function runRecordPlan(opts) {
|
|
|
13543
14075
|
let metadataTruncated = false;
|
|
13544
14076
|
for (const { file, frame } of metadataEntries) {
|
|
13545
14077
|
try {
|
|
13546
|
-
const parsed = symbolsFromMetadataEnvelope(
|
|
14078
|
+
const parsed = symbolsFromMetadataEnvelope(path41.resolve(file), frame);
|
|
13547
14079
|
symbols.push(...parsed.symbols);
|
|
13548
14080
|
if (parsed.truncated) metadataTruncated = true;
|
|
13549
14081
|
} catch (err) {
|
|
@@ -13577,7 +14109,7 @@ function runRecordPlan(opts) {
|
|
|
13577
14109
|
if (symbols.length === 0) {
|
|
13578
14110
|
const leads = metadataEntries.flatMap(({ file }) => {
|
|
13579
14111
|
try {
|
|
13580
|
-
const env = JSON.parse(
|
|
14112
|
+
const env = JSON.parse(readFileSync29(path41.resolve(file), "utf8"));
|
|
13581
14113
|
return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
|
|
13582
14114
|
} catch {
|
|
13583
14115
|
return [];
|
|
@@ -13670,7 +14202,7 @@ function runRecordPlan(opts) {
|
|
|
13670
14202
|
// a CLI flag and the MCP surface has no parameter for it.
|
|
13671
14203
|
decidedBy: "HUMAN ONLY \u2014 offer it, never choose it, and you cannot run it",
|
|
13672
14204
|
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.",
|
|
13673
|
-
userRuns: [`rm ${quoteArg(
|
|
14205
|
+
userRuns: [`rm ${quoteArg(path41.join(opts.setDir, "recording-set.json"))}`, `${tendrilCommand(`record plan --set ${quoteArg(opts.setDir)} --component ${quoteArg(manifest.component)}`)} --sample`]
|
|
13674
14206
|
},
|
|
13675
14207
|
{
|
|
13676
14208
|
id: "larger-allowance",
|
|
@@ -13868,7 +14400,7 @@ function runRecordNext(opts) {
|
|
|
13868
14400
|
const progress = payload["progress"];
|
|
13869
14401
|
process.stdout.write(`[${progress.recordedReps}/${progress.totalReps} reps] ${payload["tool"]} for ${payload["slug"]} (node ${payload["nodeId"]})
|
|
13870
14402
|
\u2192 ${payload["note"]}
|
|
13871
|
-
\u2192 then: ${tendrilCommand(`record ingest --set ${
|
|
14403
|
+
\u2192 then: ${tendrilCommand(`record ingest --set ${path41.resolve(opts.setDir)} --rep ${payload["slug"]} --tool ${payload["tool"]} --file <envelope.json>`)}
|
|
13872
14404
|
`);
|
|
13873
14405
|
});
|
|
13874
14406
|
}
|
|
@@ -13942,7 +14474,7 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
13942
14474
|
const skipped = [];
|
|
13943
14475
|
const failed = [];
|
|
13944
14476
|
for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
|
|
13945
|
-
if (
|
|
14477
|
+
if (existsSync32(path41.join(setDir, rep, name))) {
|
|
13946
14478
|
skipped.push(name);
|
|
13947
14479
|
continue;
|
|
13948
14480
|
}
|
|
@@ -13964,16 +14496,16 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
13964
14496
|
}
|
|
13965
14497
|
function rawEnvelopeFromFile(file, parts) {
|
|
13966
14498
|
if (parts) {
|
|
13967
|
-
const blocks = JSON.parse(
|
|
14499
|
+
const blocks = JSON.parse(readFileSync29(path41.resolve(file), "utf8"));
|
|
13968
14500
|
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");
|
|
13969
14501
|
return { content: blocks.map((text) => ({ type: "text", text })) };
|
|
13970
14502
|
}
|
|
13971
|
-
return { content: [{ type: "text", text:
|
|
14503
|
+
return { content: [{ type: "text", text: readFileSync29(path41.resolve(file), "utf8") }] };
|
|
13972
14504
|
}
|
|
13973
14505
|
async function runRecordIngest(opts) {
|
|
13974
14506
|
let payload;
|
|
13975
14507
|
try {
|
|
13976
|
-
payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(
|
|
14508
|
+
payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync29(path41.resolve(opts.file), "utf8"));
|
|
13977
14509
|
} catch (err) {
|
|
13978
14510
|
fail(opts, ExitCode.InputValidation, {
|
|
13979
14511
|
error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -13985,7 +14517,7 @@ async function runRecordIngest(opts) {
|
|
|
13985
14517
|
fail(opts, ExitCode.InputValidation, {
|
|
13986
14518
|
error: "--raw is for text tool responses; screenshots are binary",
|
|
13987
14519
|
code: "envelope-invalid",
|
|
13988
|
-
remediation: `Use \`${tendrilCommand(`record fetch --set ${
|
|
14520
|
+
remediation: `Use \`${tendrilCommand(`record fetch --set ${path41.resolve(opts.setDir)} --rep ${opts.rep} --tool ${opts.tool} --url <image_url>`)}\` instead.`
|
|
13989
14521
|
});
|
|
13990
14522
|
}
|
|
13991
14523
|
if (opts.rep === "__set__" && opts.tool === "get_variable_defs") {
|
|
@@ -14005,7 +14537,7 @@ async function runRecordIngest(opts) {
|
|
|
14005
14537
|
remediation: REINGEST_GUIDANCE
|
|
14006
14538
|
});
|
|
14007
14539
|
}
|
|
14008
|
-
|
|
14540
|
+
writeFileSync13(path41.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
|
|
14009
14541
|
`);
|
|
14010
14542
|
emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
|
|
14011
14543
|
process.stdout.write("set-level get_variable_defs ingested\n");
|
|
@@ -14029,7 +14561,7 @@ async function runRecordIngest(opts) {
|
|
|
14029
14561
|
remediation: REINGEST_GUIDANCE
|
|
14030
14562
|
});
|
|
14031
14563
|
}
|
|
14032
|
-
|
|
14564
|
+
writeFileSync13(path41.join(opts.setDir, "get_motion_context.json"), `${JSON.stringify(payload, null, 1)}
|
|
14033
14565
|
`);
|
|
14034
14566
|
emitData(opts, { ingested: "get_motion_context", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
|
|
14035
14567
|
process.stdout.write("set-level get_motion_context ingested\n");
|
|
@@ -14045,7 +14577,7 @@ async function runRecordIngest(opts) {
|
|
|
14045
14577
|
if (assets !== void 0) {
|
|
14046
14578
|
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
14047
14579
|
`);
|
|
14048
|
-
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${
|
|
14580
|
+
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path41.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
|
|
14049
14581
|
`);
|
|
14050
14582
|
}
|
|
14051
14583
|
});
|
|
@@ -14118,15 +14650,15 @@ async function runRecordIngestRep(opts) {
|
|
|
14118
14650
|
if (assets !== void 0) {
|
|
14119
14651
|
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
14120
14652
|
`);
|
|
14121
|
-
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${
|
|
14653
|
+
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path41.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
|
|
14122
14654
|
`);
|
|
14123
14655
|
}
|
|
14124
14656
|
});
|
|
14125
14657
|
}
|
|
14126
14658
|
function runRecordAsset(opts) {
|
|
14127
14659
|
if (opts.dir !== void 0) {
|
|
14128
|
-
const dir =
|
|
14129
|
-
const names =
|
|
14660
|
+
const dir = path41.resolve(opts.dir);
|
|
14661
|
+
const names = readdirSync14(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
|
|
14130
14662
|
if (names.length === 0) {
|
|
14131
14663
|
fail(opts, ExitCode.InputValidation, {
|
|
14132
14664
|
error: `no asset-*.<ext> files found in ${dir}`,
|
|
@@ -14137,7 +14669,7 @@ function runRecordAsset(opts) {
|
|
|
14137
14669
|
const ingested = [];
|
|
14138
14670
|
try {
|
|
14139
14671
|
for (const name of names) {
|
|
14140
|
-
ingestAsset(opts.setDir, opts.rep, name,
|
|
14672
|
+
ingestAsset(opts.setDir, opts.rep, name, readFileSync29(path41.join(dir, name)));
|
|
14141
14673
|
ingested.push(name);
|
|
14142
14674
|
}
|
|
14143
14675
|
} catch (err) {
|
|
@@ -14157,11 +14689,11 @@ function runRecordAsset(opts) {
|
|
|
14157
14689
|
fail(opts, ExitCode.InputValidation, {
|
|
14158
14690
|
error: "pass --name and --file for a single asset, or --dir for a batch",
|
|
14159
14691
|
code: "asset-rejected",
|
|
14160
|
-
remediation: tendrilCommand(`record asset --set ${
|
|
14692
|
+
remediation: tendrilCommand(`record asset --set ${path41.resolve(opts.setDir)} --rep ${opts.rep} --dir <downloads-dir>`)
|
|
14161
14693
|
});
|
|
14162
14694
|
}
|
|
14163
14695
|
try {
|
|
14164
|
-
ingestAsset(opts.setDir, opts.rep, opts.name,
|
|
14696
|
+
ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync29(path41.resolve(opts.file)));
|
|
14165
14697
|
emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
|
|
14166
14698
|
process.stdout.write(`ingested ${opts.rep}/${opts.name}
|
|
14167
14699
|
`);
|
|
@@ -14178,8 +14710,8 @@ function runRecordStatus(opts) {
|
|
|
14178
14710
|
const status = sessionStatus(opts.setDir);
|
|
14179
14711
|
const composition = (() => {
|
|
14180
14712
|
try {
|
|
14181
|
-
const setDir =
|
|
14182
|
-
const { open, standing, invalid } = compositionPairsFor(setDir, [
|
|
14713
|
+
const setDir = path41.resolve(opts.setDir);
|
|
14714
|
+
const { open, standing, invalid } = compositionPairsFor(setDir, [path41.dirname(setDir)]);
|
|
14183
14715
|
return {
|
|
14184
14716
|
openPairs: open.map((p) => ({ displayName: p.displayName, pairKey: p.key, instances: p.instances.length })),
|
|
14185
14717
|
confirmed: standing.filter((s) => s.status === "confirmed").length,
|
|
@@ -14200,7 +14732,7 @@ function runRecordStatus(opts) {
|
|
|
14200
14732
|
}
|
|
14201
14733
|
}
|
|
14202
14734
|
process.stdout.write(
|
|
14203
|
-
status.motion.recorded ? motionTruthFor(
|
|
14735
|
+
status.motion.recorded ? motionTruthFor(path41.resolve(opts.setDir)).state === "recorded-no-motion" ? "MOTION set-level motion context recorded \u2014 the response reports NO motion data (no keyframe tracks, no snippets); briefs prescribe default doctrine and say so. This is the instrument's answer, not proof the design has no transitions\n" : status.motion.asked ? "MOTION set-level motion context recorded\n" : "MOTION set-level motion context recorded (ingested onto a set that predates the obligation \u2014 briefs will quote it as recorded truth)\n" : status.motion.asked ? status.motion.invalid !== void 0 ? `MOTION set-level motion file is UNUSABLE (${status.motion.invalid}) \u2014 re-record it via \`record next\`
|
|
14204
14736
|
` : "MOTION set-level motion context not yet recorded \u2014 `record next` names the call once the reps and token map are done\n" : "MOTION never asked \u2014 this set predates the motion-capture obligation (fresh plans record it; briefs prescribe default motion doctrine only)\n"
|
|
14205
14737
|
);
|
|
14206
14738
|
if ("unavailable" in composition) {
|
|
@@ -14208,7 +14740,7 @@ function runRecordStatus(opts) {
|
|
|
14208
14740
|
`);
|
|
14209
14741
|
} else if (composition.openPairs.length > 0) {
|
|
14210
14742
|
process.stdout.write(
|
|
14211
|
-
`COMPOSITION ${composition.openPairs.length} unconfirmed partner pair(s): ${composition.openPairs.map((p) => `${p.displayName} [pair-key ${p.pairKey}] (${p.instances} instance(s))`).join(", ")} \u2014 this set's recording references other recorded components. Before generating the HOST, record/generate the partner and have a human decide the pairing: \`${tendrilCommand(`compose --set ${
|
|
14743
|
+
`COMPOSITION ${composition.openPairs.length} unconfirmed partner pair(s): ${composition.openPairs.map((p) => `${p.displayName} [pair-key ${p.pairKey}] (${p.instances} instance(s))`).join(", ")} \u2014 this set's recording references other recorded components. Before generating the HOST, record/generate the partner and have a human decide the pairing: \`${tendrilCommand(`compose --set ${path41.resolve(opts.setDir)}`)}\` lists it; confirmation is human-only.
|
|
14212
14744
|
`
|
|
14213
14745
|
);
|
|
14214
14746
|
} else if (composition.confirmed > 0) {
|
|
@@ -14248,7 +14780,7 @@ function narrowedRoles(derived, override) {
|
|
|
14248
14780
|
function rolesFromFile(opts, file, derived) {
|
|
14249
14781
|
let json;
|
|
14250
14782
|
try {
|
|
14251
|
-
json = JSON.parse(
|
|
14783
|
+
json = JSON.parse(readFileSync29(path41.resolve(file), "utf8"));
|
|
14252
14784
|
} catch (err) {
|
|
14253
14785
|
fail(opts, ExitCode.InputValidation, {
|
|
14254
14786
|
error: `cannot read roles file ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -14286,11 +14818,11 @@ function rolesFromFile(opts, file, derived) {
|
|
|
14286
14818
|
};
|
|
14287
14819
|
}
|
|
14288
14820
|
function runRecordFinish(opts) {
|
|
14289
|
-
if (!
|
|
14821
|
+
if (!existsSync32(path41.join(opts.setDir, "recording-set.json"))) {
|
|
14290
14822
|
fail(opts, ExitCode.InputValidation, {
|
|
14291
14823
|
error: `no recording-set.json in ${opts.setDir}`,
|
|
14292
14824
|
code: "no-recording-set",
|
|
14293
|
-
remediation: `Run \`${tendrilCommand(`record plan --set ${
|
|
14825
|
+
remediation: `Run \`${tendrilCommand(`record plan --set ${path41.resolve(opts.setDir)} --component <name> --metadata <envelope.json>`)}\` first \u2014 \`record finish\` closes a set the planner opened.`
|
|
14294
14826
|
});
|
|
14295
14827
|
}
|
|
14296
14828
|
const { manifest, raw } = readManifestFile(opts.setDir);
|
|
@@ -14318,17 +14850,17 @@ function runRecordFinish(opts) {
|
|
|
14318
14850
|
fail(opts, ExitCode.ConfirmationRequired, {
|
|
14319
14851
|
error: "--confirm-roles (and --roles-file) require an interactive terminal \u2014 this confirmation is human-only",
|
|
14320
14852
|
code: "roles-confirmation-not-interactive",
|
|
14321
|
-
remediation: `A human runs \`${tendrilCommand(`record finish --set ${
|
|
14853
|
+
remediation: `A human runs \`${tendrilCommand(`record finish --set ${path41.resolve(opts.setDir)} --confirm-roles`)}\` in their own terminal. Agents: show the proposal to your operator instead of confirming it.`
|
|
14322
14854
|
});
|
|
14323
14855
|
}
|
|
14324
14856
|
const merged = { ...raw, roles };
|
|
14325
|
-
const { issues } = validateRecordingSet(merged, (rel) =>
|
|
14857
|
+
const { issues } = validateRecordingSet(merged, (rel) => existsSync32(path41.join(opts.setDir, rel)));
|
|
14326
14858
|
const errors = issues.filter((i) => i.severity === "error");
|
|
14327
14859
|
if (errors.length > 0) {
|
|
14328
14860
|
fail(opts, ExitCode.InputValidation, {
|
|
14329
14861
|
error: `recording set rejected \u2014 roles NOT written: ${errors.map((e) => e.message).join("; ")}`,
|
|
14330
14862
|
code: "recording-set-invalid",
|
|
14331
|
-
remediation: `Fix the set (\`${tendrilCommand(`record status --set ${
|
|
14863
|
+
remediation: `Fix the set (\`${tendrilCommand(`record status --set ${path41.resolve(opts.setDir)}`)}\` lists missing envelopes) or the roles graph, then re-run \`record finish\`.`
|
|
14332
14864
|
});
|
|
14333
14865
|
}
|
|
14334
14866
|
for (const issue of issues) if (issue.severity === "warning") warn(opts, issue.message);
|
|
@@ -14371,16 +14903,16 @@ var init_record = __esm({
|
|
|
14371
14903
|
const shown = items.length <= MAX_SPOKEN_VALUES ? items : [...items.slice(0, MAX_SPOKEN_VALUES), `${items.length - MAX_SPOKEN_VALUES} more`];
|
|
14372
14904
|
return shown.length <= 1 ? shown[0] ?? "" : `${shown.slice(0, -1).join(", ")} and ${shown[shown.length - 1]}`;
|
|
14373
14905
|
};
|
|
14374
|
-
ENVELOPE_HELP = `Envelope format: text tools save {"content":[{"type":"text","text":"<VERBATIM response text incl. any Currently-selected-nodes block>"}]}; VERBATIM MEANS EVERY PART, NOT JUST THE TEXT ONE: if a response carries additional content blocks \u2014 an image block especially \u2014 keep them in the envelope you save. get_design_context is documented to return a screenshot alongside the code, and whether it actually does is a question our whole recorded corpus cannot answer because every saved envelope holds text only. Saving what arrives settles it at no extra call. get_screenshot: pass
|
|
14906
|
+
ENVELOPE_HELP = `Envelope format: text tools save {"content":[{"type":"text","text":"<VERBATIM response text incl. any Currently-selected-nodes block>"}]}; VERBATIM MEANS EVERY PART, NOT JUST THE TEXT ONE: if a response carries additional content blocks \u2014 an image block especially \u2014 keep them in the envelope you save. get_design_context is documented to return a screenshot alongside the code, and whether it actually does is a question our whole recorded corpus cannot answer because every saved envelope holds text only. Saving what arrives settles it at no extra call. get_screenshot: pass contentsOnly: true AND maxDimension: 4096 \u2014 always, on every call, whatever the node's size. contentsOnly renders the node in isolation; without it the export is what the CANVAS looks like, which for a node inside a component set includes the editor's own component-set chrome (#8a38f5 dashes) baked into the padding. That chrome is reference ink no implementation can paint, so it lands as absent-ink clusters and demotes an otherwise perfect component: measured 2026-08-19, a Modal scored 0/3 certified whose every absent cluster was chrome, and the same render against a contentsOnly reference scored 3/3 (ink 0.9949 -> 0.9996). Isolation does NOT drop the outer effects we deliberately capture \u2014 the padded dimensions come back byte-identical (1276x199 and 1836x175, drop shadow intact). maxDimension is a CAP, not a target: a 56x28 node requested at a high cap comes back 56x28, never upscaled (measured across 24 poses, 2026-08-19). The tool DEFAULTS TO 1024 and clamps anything longer, which silently yields a downscaled reference that is not pixel ground truth, and ingest refuses that. Do NOT compute the cap from the node box: Figma PADS the export to contain outer effects, so the reference is routinely larger than the box it came from \u2014 a recorded 1800px-wide modal exported at 1836px, and a cap sized to the box would have been refused. A flat generous cap removes the arithmetic and the whole failure class. Then 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.`;
|
|
14375
14907
|
isAutoFetchAssetUrl = (url) => isFigmaAssetUrl(url) || isLocalAssetUrl(url);
|
|
14376
14908
|
describeRoleLoss = (loss) => loss.kind === "main" ? `drops main "${loss.main}"` : `stops "${loss.part}" being a part of main "${loss.main}"`;
|
|
14377
14909
|
}
|
|
14378
14910
|
});
|
|
14379
14911
|
|
|
14380
14912
|
// packages/cli/src/font-guidance.ts
|
|
14381
|
-
import
|
|
14913
|
+
import path42 from "node:path";
|
|
14382
14914
|
function fontsUnprovenRemediation(setDir) {
|
|
14383
|
-
const set = setDir === void 0 ? void 0 :
|
|
14915
|
+
const set = setDir === void 0 ? void 0 : path42.resolve(setDir);
|
|
14384
14916
|
if (set !== void 0) {
|
|
14385
14917
|
try {
|
|
14386
14918
|
const needs = recordedFontNeeds(set);
|
|
@@ -14455,8 +14987,8 @@ __export(fonts_exports, {
|
|
|
14455
14987
|
runFontsResolveSet: () => runFontsResolveSet,
|
|
14456
14988
|
runFontsStatus: () => runFontsStatus
|
|
14457
14989
|
});
|
|
14458
|
-
import { existsSync as
|
|
14459
|
-
import
|
|
14990
|
+
import { existsSync as existsSync33, readFileSync as readFileSync30 } from "node:fs";
|
|
14991
|
+
import path43 from "node:path";
|
|
14460
14992
|
async function runFontsResolve(opts) {
|
|
14461
14993
|
const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
|
|
14462
14994
|
const queryRefusal = systemFaceRefusal(opts.family.trim());
|
|
@@ -14477,7 +15009,7 @@ async function runFontsResolve(opts) {
|
|
|
14477
15009
|
}
|
|
14478
15010
|
}
|
|
14479
15011
|
async function runFontsResolveSet(opts) {
|
|
14480
|
-
const setDir =
|
|
15012
|
+
const setDir = path43.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
|
|
14481
15013
|
let needs = [];
|
|
14482
15014
|
try {
|
|
14483
15015
|
needs = recordedFontNeeds(setDir);
|
|
@@ -14528,7 +15060,10 @@ async function runFontsResolveSet(opts) {
|
|
|
14528
15060
|
}
|
|
14529
15061
|
}
|
|
14530
15062
|
const refusals = [...new Set(failures.map((f) => f.family))].map((family) => ({ family, reason: systemFaceRefusal(family.trim()) })).filter((r) => r.reason !== void 0);
|
|
14531
|
-
|
|
15063
|
+
const withAFace = new Set([...cached2, ...resolved, ...relicensed].map((f) => f.family.toLowerCase()));
|
|
15064
|
+
const familyMisses = failures.filter((f) => !withAFace.has(f.family.toLowerCase()));
|
|
15065
|
+
const weightMisses = failures.filter((f) => withAFace.has(f.family.toLowerCase()));
|
|
15066
|
+
emitData(opts, { set: setDir, needs, cached: cached2, resolved, relicensed, unlicensed, failures, familyMisses, weightMisses, ...refusals.length > 0 ? { refusals } : {} }, () => {
|
|
14532
15067
|
process.stdout.write(`set declares: ${needs.map((n) => `${n.family} (${n.weights.join(", ")})`).join(" \xB7 ")}
|
|
14533
15068
|
`);
|
|
14534
15069
|
for (const f of cached2) process.stdout.write(`cached ${f.family} ${f.weight}
|
|
@@ -14554,22 +15089,31 @@ async function runFontsResolveSet(opts) {
|
|
|
14554
15089
|
if (byteDrift.length > 0) {
|
|
14555
15090
|
warn(opts, `${byteDrift.map((f) => `${f.family} ${f.weight}`).join(", ")}: the foundry now serves different bytes than the cache held \u2014 the faces were re-fetched to record their licence, so re-pin any font lock deliberately`);
|
|
14556
15091
|
}
|
|
14557
|
-
if (
|
|
14558
|
-
|
|
15092
|
+
if (weightMisses.length > 0) {
|
|
15093
|
+
const byFamily = [...new Set(weightMisses.map((f) => f.family))].map(
|
|
15094
|
+
(fam) => `"${fam}" ${weightMisses.filter((f) => f.family === fam).map((f) => f.weight).join(", ")}`
|
|
15095
|
+
);
|
|
15096
|
+
warn(
|
|
15097
|
+
opts,
|
|
15098
|
+
`weight instance(s) unavailable: ${byFamily.join("; ")} \u2014 the FAMILY is provisioned, so the mount renders these at the nearest cached weight. Verify reports that as an advisory and never gates on it, so this run is not a failure. If the exact instance matters, register a file you licence: ${tendrilCommand('fonts add "<Family>" <weight> <file>')}.`
|
|
15099
|
+
);
|
|
15100
|
+
}
|
|
15101
|
+
if (familyMisses.length > 0) {
|
|
15102
|
+
warn(opts, `${familyMisses.length} face(s) unresolved with NO cached face for the family \u2014 verification will refuse to certify under substitution`);
|
|
14559
15103
|
process.exitCode = ExitCode.FontsUnproven;
|
|
14560
15104
|
}
|
|
14561
15105
|
}
|
|
14562
15106
|
function runFontsStatus(opts) {
|
|
14563
|
-
const manifestPath2 =
|
|
14564
|
-
if (!
|
|
15107
|
+
const manifestPath2 = path43.join(opts.cacheDir, "manifest.json");
|
|
15108
|
+
if (!existsSync33(manifestPath2)) {
|
|
14565
15109
|
fail(opts, ExitCode.FontsUnproven, {
|
|
14566
15110
|
error: `no font cache at ${opts.cacheDir}`,
|
|
14567
15111
|
code: "fonts-unresolved",
|
|
14568
15112
|
remediation: `Run \`${tendrilCommand("fonts resolve --set <recording-dir>")}\` (or \`${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}\`) first.`
|
|
14569
15113
|
});
|
|
14570
15114
|
}
|
|
14571
|
-
const faces = JSON.parse(
|
|
14572
|
-
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(
|
|
15115
|
+
const faces = JSON.parse(readFileSync30(manifestPath2, "utf8"));
|
|
15116
|
+
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path43.resolve(opts.lock), opts.cacheDir) : null;
|
|
14573
15117
|
emitData(opts, { faces, lockVerdicts }, () => {
|
|
14574
15118
|
for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
|
|
14575
15119
|
`);
|
|
@@ -14613,13 +15157,13 @@ function familyMismatch(family, declared) {
|
|
|
14613
15157
|
}
|
|
14614
15158
|
function runFontsAdd(opts) {
|
|
14615
15159
|
if (opts.set !== void 0) {
|
|
14616
|
-
const declared = taskFontFamilies(
|
|
15160
|
+
const declared = taskFontFamilies(path43.resolve(opts.set)) ?? [];
|
|
14617
15161
|
const mismatch = familyMismatch(opts.family, declared);
|
|
14618
15162
|
if (mismatch !== void 0) {
|
|
14619
15163
|
fail(opts, ExitCode.InputValidation, {
|
|
14620
15164
|
error: `the recording set does not declare a family named "${opts.family}" \u2014 it declares ${declared.map((d) => `"${d}"`).join(", ")}. Adding it under this name would cache a face the mount never matches, and scoring would keep refusing for the family that is still missing.`,
|
|
14621
15165
|
code: "font-family-not-declared",
|
|
14622
|
-
remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add "${mismatch.nearest}" ${opts.weight} ${opts.file} --set ${
|
|
15166
|
+
remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add "${mismatch.nearest}" ${opts.weight} ${opts.file} --set ${path43.resolve(opts.set)}`)}` : `Use one of the declared families exactly as the recording spells it, or drop --set to add a family this set does not declare.`
|
|
14623
15167
|
});
|
|
14624
15168
|
}
|
|
14625
15169
|
} else {
|
|
@@ -14678,13 +15222,13 @@ cache them: ${tendrilCommand(`fonts add-system ${quoteArg(opts.family)}`)}
|
|
|
14678
15222
|
}
|
|
14679
15223
|
function runFontsAddSystem(opts) {
|
|
14680
15224
|
if (opts.set !== void 0) {
|
|
14681
|
-
const declared = taskFontFamilies(
|
|
15225
|
+
const declared = taskFontFamilies(path43.resolve(opts.set)) ?? [];
|
|
14682
15226
|
const mismatch = familyMismatch(opts.family, declared);
|
|
14683
15227
|
if (mismatch !== void 0) {
|
|
14684
15228
|
fail(opts, ExitCode.InputValidation, {
|
|
14685
15229
|
error: `the recording set does not declare a family named "${opts.family}" \u2014 it declares ${declared.map((d) => `"${d}"`).join(", ")}. A face cached under a name the mount never matches leaves scoring refusing for the family that is still missing.`,
|
|
14686
15230
|
code: "font-family-not-declared",
|
|
14687
|
-
remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add-system ${quoteArg(mismatch.nearest)} --set ${quoteArg(
|
|
15231
|
+
remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add-system ${quoteArg(mismatch.nearest)} --set ${quoteArg(path43.resolve(opts.set))}`)}` : `Use one of the declared families exactly as the recording spells it, or drop --set.`
|
|
14688
15232
|
});
|
|
14689
15233
|
}
|
|
14690
15234
|
} else {
|
|
@@ -14734,12 +15278,12 @@ var init_fonts = __esm({
|
|
|
14734
15278
|
});
|
|
14735
15279
|
|
|
14736
15280
|
// packages/cli/src/profile-input.ts
|
|
14737
|
-
import { existsSync as
|
|
14738
|
-
import
|
|
15281
|
+
import { existsSync as existsSync34, readFileSync as readFileSync31 } from "node:fs";
|
|
15282
|
+
import path44 from "node:path";
|
|
14739
15283
|
function loadCodebaseProfile(flags, profilePath) {
|
|
14740
15284
|
if (profilePath === void 0) return null;
|
|
14741
|
-
const abs =
|
|
14742
|
-
if (!
|
|
15285
|
+
const abs = path44.resolve(profilePath);
|
|
15286
|
+
if (!existsSync34(abs)) {
|
|
14743
15287
|
fail(flags, ExitCode.InputValidation, {
|
|
14744
15288
|
error: `no profile at ${abs}`,
|
|
14745
15289
|
code: "profile_missing",
|
|
@@ -14747,7 +15291,7 @@ function loadCodebaseProfile(flags, profilePath) {
|
|
|
14747
15291
|
});
|
|
14748
15292
|
}
|
|
14749
15293
|
try {
|
|
14750
|
-
return readCodebaseProfile(
|
|
15294
|
+
return readCodebaseProfile(readFileSync31(abs, "utf8"));
|
|
14751
15295
|
} catch (error) {
|
|
14752
15296
|
fail(flags, ExitCode.InputValidation, {
|
|
14753
15297
|
error: `profile at ${abs} is not a valid codebase profile: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -14789,8 +15333,8 @@ __export(verify_exports, {
|
|
|
14789
15333
|
runVerify: () => runVerify,
|
|
14790
15334
|
verdictCaveatsFor: () => verdictCaveatsFor
|
|
14791
15335
|
});
|
|
14792
|
-
import { existsSync as
|
|
14793
|
-
import
|
|
15336
|
+
import { existsSync as existsSync35, readFileSync as readFileSync32, rmSync as rmSync5, writeFileSync as writeFileSync14 } from "node:fs";
|
|
15337
|
+
import path45 from "node:path";
|
|
14794
15338
|
function interactionCoverage(behaviors) {
|
|
14795
15339
|
const parity = behaviors.filter((b) => b.id.startsWith("parity:"));
|
|
14796
15340
|
const content = behaviors.filter((b) => b.id.startsWith("content-prop-renders("));
|
|
@@ -14867,20 +15411,29 @@ function operabilityLine(state) {
|
|
|
14867
15411
|
return void 0;
|
|
14868
15412
|
}
|
|
14869
15413
|
function foldConfigStatus(s, failDemotions, substitutedFamilies) {
|
|
14870
|
-
const { exact: _exact, ...reported } = s;
|
|
14871
15414
|
let status = tierOf(s, BARS2.cert);
|
|
14872
15415
|
const certDemote = [];
|
|
14873
15416
|
let absentInkDemoted = false;
|
|
14874
15417
|
if (status === "certified" && s.absentInk !== void 0 && s.absentInk.length > 0) {
|
|
14875
15418
|
status = "pass";
|
|
14876
15419
|
absentInkDemoted = true;
|
|
14877
|
-
certDemote.push(
|
|
15420
|
+
certDemote.push(
|
|
15421
|
+
...s.absentInk.map(
|
|
15422
|
+
(c) => (
|
|
15423
|
+
// A chrome cluster is a defect in the RECORDING, and saying
|
|
15424
|
+
// "missing or invisible feature" points the reader at code that
|
|
15425
|
+
// is not wrong — run 26's operator spent a round hunting a
|
|
15426
|
+
// layout bug that did not exist.
|
|
15427
|
+
c.chrome === true ? `absent ink: ${c.px}px of Figma's component-set CHROME baked into the reference \u2014 this is the editor's own outline, not your component, and no implementation can paint it. RE-RECORD this rep with get_screenshot contentsOnly: true (the outer effect survives isolation; only the chrome goes).` : `absent ink: ${c.name ?? "unnamed region"} \u2014 ${c.px}px of recorded ink with no render ink within reach (missing or invisible feature)`
|
|
15428
|
+
)
|
|
15429
|
+
)
|
|
15430
|
+
);
|
|
14878
15431
|
}
|
|
14879
15432
|
if (status === "certified" && substitutedFamilies.length > 0) {
|
|
14880
15433
|
status = "pass";
|
|
14881
15434
|
certDemote.push(`substituted fonts: cache lacks ${substitutedFamilies.map((f) => `"${f}"`).join(", ")} \u2014 certification never measures a substitute face`);
|
|
14882
15435
|
}
|
|
14883
|
-
const base = certDemote.length > 0 ? { ...
|
|
15436
|
+
const base = certDemote.length > 0 ? { ...s, status, demotedBy: certDemote } : { ...s, status };
|
|
14884
15437
|
return {
|
|
14885
15438
|
row: failDemotions === void 0 ? base : { ...base, status: "fail", demotedBy: [...certDemote, ...failDemotions] },
|
|
14886
15439
|
absentInkDemoted
|
|
@@ -15022,10 +15575,26 @@ function compositionReport(input) {
|
|
|
15022
15575
|
function eyeCheck(bundleDir) {
|
|
15023
15576
|
return {
|
|
15024
15577
|
command: tendrilCommand(`inspect "${bundleDir}"`),
|
|
15025
|
-
sheetPath:
|
|
15578
|
+
sheetPath: path45.join(bundleDir, "verify-evidence", "inspect.html"),
|
|
15026
15579
|
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."
|
|
15027
15580
|
};
|
|
15028
15581
|
}
|
|
15582
|
+
function evidenceArtifacts(evidenceDir, reps) {
|
|
15583
|
+
const named = (name) => existsSync35(path45.join(evidenceDir, name)) ? name : null;
|
|
15584
|
+
return {
|
|
15585
|
+
legend: named("diff-legend.txt"),
|
|
15586
|
+
configs: reps.map((rep) => {
|
|
15587
|
+
const absentInk = [];
|
|
15588
|
+
for (let i = 0; ; i += 1) {
|
|
15589
|
+
const ref = named(`${rep}-absent-${i}-ref.png`);
|
|
15590
|
+
const render = named(`${rep}-absent-${i}-render.png`);
|
|
15591
|
+
if (ref === null || render === null) break;
|
|
15592
|
+
absentInk.push({ ref, render });
|
|
15593
|
+
}
|
|
15594
|
+
return { rep, render: named(`${rep}-render.png`), ref: named(`${rep}-ref.png`), diff: named(`${rep}-diff.png`), absentInk };
|
|
15595
|
+
})
|
|
15596
|
+
};
|
|
15597
|
+
}
|
|
15029
15598
|
function failureTally(t) {
|
|
15030
15599
|
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)`;
|
|
15031
15600
|
}
|
|
@@ -15059,7 +15628,7 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
15059
15628
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
15060
15629
|
const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
15061
15630
|
const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
|
|
15062
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
15631
|
+
const registry = Object.values(TASKS).find((t) => path45.resolve(t.set) === path45.resolve(setDir));
|
|
15063
15632
|
const authored = (() => {
|
|
15064
15633
|
if (registry !== void 0) return void 0;
|
|
15065
15634
|
try {
|
|
@@ -15119,19 +15688,19 @@ function verdictCaveatsFor(input) {
|
|
|
15119
15688
|
async function runVerify(opts) {
|
|
15120
15689
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
15121
15690
|
let recordingSetDrift;
|
|
15122
|
-
const setOverride = opts.set !== void 0 ?
|
|
15123
|
-
opts = { ...opts, bundleDir:
|
|
15124
|
-
if (!
|
|
15691
|
+
const setOverride = opts.set !== void 0 ? path45.resolve(callerCwd, opts.set) : void 0;
|
|
15692
|
+
opts = { ...opts, bundleDir: path45.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
|
|
15693
|
+
if (!existsSync35(opts.bundleDir)) {
|
|
15125
15694
|
fail(opts, ExitCode.InputValidation, {
|
|
15126
15695
|
error: `bundle directory not found: ${opts.bundleDir}`,
|
|
15127
15696
|
code: "bundle-missing",
|
|
15128
15697
|
remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
|
|
15129
15698
|
});
|
|
15130
15699
|
}
|
|
15131
|
-
const manifestPath2 =
|
|
15700
|
+
const manifestPath2 = path45.join(opts.bundleDir, "component.json");
|
|
15132
15701
|
let manifest;
|
|
15133
|
-
if (
|
|
15134
|
-
const { manifest: parsed, issues } = readBundleManifest(
|
|
15702
|
+
if (existsSync35(manifestPath2)) {
|
|
15703
|
+
const { manifest: parsed, issues } = readBundleManifest(readFileSync32(manifestPath2, "utf8"));
|
|
15135
15704
|
if (issues.length > 0) {
|
|
15136
15705
|
fail(opts, ExitCode.InputValidation, {
|
|
15137
15706
|
error: `bundle manifest rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -15162,21 +15731,21 @@ async function runVerify(opts) {
|
|
|
15162
15731
|
task = registry;
|
|
15163
15732
|
} else if (manifest !== void 0) {
|
|
15164
15733
|
const resolveSetDir = (p) => {
|
|
15165
|
-
if (
|
|
15166
|
-
const fromRepo =
|
|
15167
|
-
if (
|
|
15168
|
-
return
|
|
15734
|
+
if (path45.isAbsolute(p)) return p;
|
|
15735
|
+
const fromRepo = path45.resolve(REPO_ROOT, p);
|
|
15736
|
+
if (existsSync35(fromRepo)) return fromRepo;
|
|
15737
|
+
return path45.resolve(callerCwd, p);
|
|
15169
15738
|
};
|
|
15170
15739
|
const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
|
|
15171
|
-
if (!
|
|
15740
|
+
if (!existsSync35(path45.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path45.resolve(t.set) === path45.resolve(setDir))) {
|
|
15172
15741
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
15173
15742
|
error: `recording set not found or unmanifested: ${setDir}`,
|
|
15174
15743
|
code: "recording-set-missing",
|
|
15175
15744
|
remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
|
|
15176
15745
|
});
|
|
15177
15746
|
}
|
|
15178
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
15179
|
-
if (registry !== void 0 && !
|
|
15747
|
+
const registry = Object.values(TASKS).find((t) => path45.resolve(t.set) === path45.resolve(setDir));
|
|
15748
|
+
if (registry !== void 0 && !existsSync35(path45.join(setDir, "recording-set.json"))) {
|
|
15180
15749
|
const recordedSlugs = registry.configs.map((c) => c.rep);
|
|
15181
15750
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
15182
15751
|
unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
@@ -15208,9 +15777,9 @@ async function runVerify(opts) {
|
|
|
15208
15777
|
recordingSetDrift = { stamped: manifest.provenance.recordingSet.hash, current: hash };
|
|
15209
15778
|
}
|
|
15210
15779
|
for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
|
|
15211
|
-
const p =
|
|
15212
|
-
if (!
|
|
15213
|
-
const issues = checkBundleSourceFile(name, new Uint8Array(
|
|
15780
|
+
const p = path45.join(opts.bundleDir, name);
|
|
15781
|
+
if (!existsSync35(p)) continue;
|
|
15782
|
+
const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync32(p)));
|
|
15214
15783
|
if (issues.length > 0) {
|
|
15215
15784
|
fail(opts, ExitCode.InputValidation, {
|
|
15216
15785
|
error: `bundle source rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -15248,7 +15817,7 @@ async function runVerify(opts) {
|
|
|
15248
15817
|
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)`);
|
|
15249
15818
|
}
|
|
15250
15819
|
const missing = task.configs.filter(
|
|
15251
|
-
(c) => !
|
|
15820
|
+
(c) => !existsSync35(path45.join(task.set, c.rep, "get_screenshot.json")) || !existsSync35(path45.join(task.set, c.rep, "get_metadata.json"))
|
|
15252
15821
|
);
|
|
15253
15822
|
if (missing.length > 0) {
|
|
15254
15823
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -15258,7 +15827,8 @@ async function runVerify(opts) {
|
|
|
15258
15827
|
});
|
|
15259
15828
|
}
|
|
15260
15829
|
const bar = BARS2[opts.bar];
|
|
15261
|
-
const evidenceDir =
|
|
15830
|
+
const evidenceDir = path45.join(opts.bundleDir, "verify-evidence");
|
|
15831
|
+
rmSync5(evidenceDir, { recursive: true, force: true });
|
|
15262
15832
|
const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
15263
15833
|
const quality = await checkBundleQuality(
|
|
15264
15834
|
opts.bundleDir,
|
|
@@ -15275,7 +15845,7 @@ async function runVerify(opts) {
|
|
|
15275
15845
|
// ASKED, never "follows every convention".
|
|
15276
15846
|
loadCodebaseProfile(opts, opts.profile) ?? void 0
|
|
15277
15847
|
);
|
|
15278
|
-
const bundleCss = ["tokens.css", "styles.css"].map((f) =>
|
|
15848
|
+
const bundleCss = ["tokens.css", "styles.css"].map((f) => path45.join(opts.bundleDir, f)).filter((f) => existsSync35(f)).map((f) => readFileSync32(f, "utf8")).join("\n");
|
|
15279
15849
|
const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss, { ...authorityConfigs !== void 0 ? { authority: authorityConfigs } : {} });
|
|
15280
15850
|
const parity = await checkHoverParity(task, opts.bundleDir, authorityConfigs ?? task.configs);
|
|
15281
15851
|
const framing = checkAdapterFraming(
|
|
@@ -15291,10 +15861,10 @@ async function runVerify(opts) {
|
|
|
15291
15861
|
warn(opts, `compositions extension REJECTED (${crossComposition.malformed}) \u2014 the cross-bundle backstop did NOT run over it; repair the manifest entry and re-verify. This is an instrument failure, not a clean bill.`);
|
|
15292
15862
|
}
|
|
15293
15863
|
const verifyCallerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
15294
|
-
const verifyPins = crossComposition.rows.length > 0 ? composedPins(task.set, [opts.library !== void 0 ?
|
|
15864
|
+
const verifyPins = crossComposition.rows.length > 0 ? composedPins(task.set, [opts.library !== void 0 ? path45.resolve(verifyCallerCwd, opts.library) : verifyCallerCwd]) : { pins: [], issues: [] };
|
|
15295
15865
|
const staticComposedChecks = composedChecks(opts.bundleDir, task.entry, verifyPins.pins);
|
|
15296
15866
|
const renderExpectations = verifyPins.pins.map((pin) => ({
|
|
15297
|
-
modulePath:
|
|
15867
|
+
modulePath: path45.join(composedModuleDir(pin.partnerName), pin.entryComponent),
|
|
15298
15868
|
component: pin.entryComponent,
|
|
15299
15869
|
stampName: `${pin.pairKey}:${pin.entryComponent}`,
|
|
15300
15870
|
hostReps: [...new Set(pin.instances.map((i) => i.hostRep))],
|
|
@@ -15352,8 +15922,10 @@ async function runVerify(opts) {
|
|
|
15352
15922
|
}
|
|
15353
15923
|
})();
|
|
15354
15924
|
const compositionBlock = compositionReport({ availability, structural, crops, regions: regionsOut, composedPairs: composedPairs.size });
|
|
15925
|
+
const scoredFiles = digestScoredFiles(opts.bundleDir, manifest !== void 0 ? manifest.entry : task.entry);
|
|
15355
15926
|
const report = {
|
|
15356
15927
|
bundle: opts.bundleDir,
|
|
15928
|
+
scoredFiles,
|
|
15357
15929
|
...opts.task !== void 0 ? { task: opts.task } : {},
|
|
15358
15930
|
...manifest !== void 0 ? { bundleManifest: { name: manifest.name, bundleVersion: manifest.bundleVersion, recordingSetHash: manifest.provenance.recordingSet.hash } } : {},
|
|
15359
15931
|
targetBar: opts.bar,
|
|
@@ -15418,7 +15990,7 @@ async function runVerify(opts) {
|
|
|
15418
15990
|
} : {},
|
|
15419
15991
|
configs: statuses,
|
|
15420
15992
|
behaviors,
|
|
15421
|
-
evidence: { dir: evidenceDir,
|
|
15993
|
+
evidence: { dir: evidenceDir, ...evidenceArtifacts(evidenceDir, statuses.map((s) => s.rep)) },
|
|
15422
15994
|
composition: compositionBlock,
|
|
15423
15995
|
verdict: ok ? "verified" : "verification-failed",
|
|
15424
15996
|
// Run-23 R5: the one-word verdict printed beside "coverage
|
|
@@ -15579,7 +16151,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
15579
16151
|
}
|
|
15580
16152
|
process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
|
|
15581
16153
|
`);
|
|
15582
|
-
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) =>
|
|
16154
|
+
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) => path45.join(opts.bundleDir, f)).filter((f) => existsSync35(f)).map((f) => readFileSync32(f, "utf8")).join("\n")));
|
|
15583
16155
|
if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
|
|
15584
16156
|
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)
|
|
15585
16157
|
`);
|
|
@@ -15612,7 +16184,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
15612
16184
|
if (certBlockedByAbsentInk.length > 0) {
|
|
15613
16185
|
warn(
|
|
15614
16186
|
opts,
|
|
15615
|
-
`${certBlockedByAbsentInk.length} config(s) demoted by absent-ink clusters (${certBlockedByAbsentInk.join(", ")}) \u2014 certification never ships a missing or invisible feature; each cluster is named in the report above. Fix the component and re-verify, or verify at --bar pass for the disclosed result.`
|
|
16187
|
+
`${certBlockedByAbsentInk.length} config(s) demoted by absent-ink clusters (${certBlockedByAbsentInk.join(", ")}) \u2014 certification never ships a missing or invisible feature; each cluster is named in the report above. Fix the component and re-verify, or verify at --bar pass for the disclosed result. BEFORE changing code, check WHERE the clusters sit: open the -absent-*-ref.png crops in the evidence dir. If the missing ink lies at the extreme EDGES of the reference rather than inside the component, it is probably not yours \u2014 Figma pads an export to contain outer effects (a shadow, a glow), and the pad can capture the editor's own component-set chrome, which is purple #8a38f5 and which no correct implementation can ever paint. Measured 2026-08-19: a shadowed modal was demoted 0/3 with EVERY absent cluster made of chrome, while the same kit's unshadowed button, exported with no pad, certified 24/24 \u2014 and re-recording with contentsOnly: true took the same code to 3/3. That is a recording artifact, not a defect in your component.`
|
|
15616
16188
|
);
|
|
15617
16189
|
process.exitCode = ExitCode.VerificationFailed;
|
|
15618
16190
|
}
|
|
@@ -15631,8 +16203,27 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
15631
16203
|
);
|
|
15632
16204
|
process.exitCode = ExitCode.VerificationFailed;
|
|
15633
16205
|
}
|
|
16206
|
+
persistReport(opts, report, evidenceDir);
|
|
16207
|
+
}
|
|
16208
|
+
function persistReport(opts, report, evidenceDir) {
|
|
16209
|
+
if (!existsSync35(evidenceDir)) return;
|
|
16210
|
+
const rulerExit = process.exitCode === void 0 ? 0 : Number(process.exitCode);
|
|
16211
|
+
const withExit = {
|
|
16212
|
+
...report,
|
|
16213
|
+
rulerExit,
|
|
16214
|
+
...rulerExit !== 0 ? { rulerRefusal: EXIT_REFUSALS[rulerExit] ?? `ruler exited ${rulerExit}` } : {}
|
|
16215
|
+
};
|
|
16216
|
+
try {
|
|
16217
|
+
writeFileSync14(
|
|
16218
|
+
path45.join(evidenceDir, VERIFY_REPORT_FILENAME),
|
|
16219
|
+
`${JSON.stringify(verifyReportForTransport(withExit, path45.basename(opts.bundleDir)), null, 2)}
|
|
16220
|
+
`
|
|
16221
|
+
);
|
|
16222
|
+
} catch (e) {
|
|
16223
|
+
warn(opts, `could not write ${VERIFY_REPORT_FILENAME} (${e instanceof Error ? e.message : String(e)}) \u2014 the verdict above stands, but this run left no persisted report beside its evidence.`);
|
|
16224
|
+
}
|
|
15634
16225
|
}
|
|
15635
|
-
var BARS2, NO_INTERACTIVE_POSES, NO_ROLE_MANIFEST, ROLES_NOT_RESOLVED, UNSTAMPED_ROLES, COMPOSE_ON_GENERATE_ARMED, NO_OVERLAY_DECLARED;
|
|
16226
|
+
var BARS2, NO_INTERACTIVE_POSES, NO_ROLE_MANIFEST, ROLES_NOT_RESOLVED, UNSTAMPED_ROLES, COMPOSE_ON_GENERATE_ARMED, NO_OVERLAY_DECLARED, EXIT_REFUSALS;
|
|
15636
16227
|
var init_verify = __esm({
|
|
15637
16228
|
"packages/cli/src/commands/verify.ts"() {
|
|
15638
16229
|
"use strict";
|
|
@@ -15662,6 +16253,10 @@ var init_verify = __esm({
|
|
|
15662
16253
|
UNSTAMPED_ROLES = "unstamped";
|
|
15663
16254
|
COMPOSE_ON_GENERATE_ARMED = false;
|
|
15664
16255
|
NO_OVERLAY_DECLARED = "no overlay declared";
|
|
16256
|
+
EXIT_REFUSALS = {
|
|
16257
|
+
[ExitCode.VerificationFailed]: "the ruler refused this run: one or more checks fell below the requested bar",
|
|
16258
|
+
[ExitCode.FontsUnproven]: "the ruler refused this run: required font faces were not provisioned, so scores measure a substitute"
|
|
16259
|
+
};
|
|
15665
16260
|
}
|
|
15666
16261
|
});
|
|
15667
16262
|
|
|
@@ -15671,11 +16266,11 @@ __export(engine_exports, {
|
|
|
15671
16266
|
runEngineBrief: () => runEngineBrief,
|
|
15672
16267
|
runEngineScore: () => runEngineScore
|
|
15673
16268
|
});
|
|
15674
|
-
import { appendFileSync, existsSync as
|
|
15675
|
-
import
|
|
16269
|
+
import { appendFileSync, existsSync as existsSync36, mkdirSync as mkdirSync10, readFileSync as readFileSync33, writeFileSync as writeFileSync15 } from "node:fs";
|
|
16270
|
+
import path46 from "node:path";
|
|
15676
16271
|
function resolveEngineTask(opts, callerCwd) {
|
|
15677
|
-
const asPath =
|
|
15678
|
-
const isSet =
|
|
16272
|
+
const asPath = path46.resolve(callerCwd, opts.taskOrSet);
|
|
16273
|
+
const isSet = existsSync36(path46.join(asPath, "recording-set.json"));
|
|
15679
16274
|
const registry = TASKS[opts.taskOrSet];
|
|
15680
16275
|
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: void 0 };
|
|
15681
16276
|
if (isSet) {
|
|
@@ -15684,7 +16279,7 @@ function resolveEngineTask(opts, callerCwd) {
|
|
|
15684
16279
|
for (const d of authored.disclosures) warn(opts, d);
|
|
15685
16280
|
return {
|
|
15686
16281
|
task: authored.task,
|
|
15687
|
-
name:
|
|
16282
|
+
name: path46.basename(asPath),
|
|
15688
16283
|
ref: asPath,
|
|
15689
16284
|
disclosures: authored.disclosures,
|
|
15690
16285
|
interactionEvidence: authored.api.interactionEvidence,
|
|
@@ -15712,13 +16307,13 @@ function runEngineBrief(opts) {
|
|
|
15712
16307
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
15713
16308
|
const { task, name, ref, disclosures } = resolveEngineTask(opts, callerCwd);
|
|
15714
16309
|
const bar = BARS3[opts.bar];
|
|
15715
|
-
if (
|
|
16310
|
+
if (existsSync36(path46.join(task.set, "recording-set.json"))) {
|
|
15716
16311
|
try {
|
|
15717
|
-
const { open } = compositionPairsFor(
|
|
16312
|
+
const { open } = compositionPairsFor(path46.resolve(task.set), [opts.library !== void 0 ? path46.resolve(callerCwd, opts.library) : callerCwd]);
|
|
15718
16313
|
if (open.length > 0) {
|
|
15719
16314
|
const componentNames = [...new Set(open.map((p) => p.displayName))];
|
|
15720
16315
|
disclosures.push(
|
|
15721
|
-
`COMPOSITION PAIRS UNDECIDED: this recording references ${componentNames.length} other recorded component(s) (${open.map((p) => `${p.displayName} [pair-key ${p.key}]: ${p.instances.length} instance(s)`).join("; ")}) and no human has decided the pairing. This brief treats the component as SELF-CONTAINED \u2014 you will re-implement the partner's pixels locally. If composition is wanted: generate the partner bundle first, have a human run \`${tendrilCommand(`compose --set ${
|
|
16316
|
+
`COMPOSITION PAIRS UNDECIDED: this recording references ${componentNames.length} other recorded component(s) (${open.map((p) => `${p.displayName} [pair-key ${p.key}]: ${p.instances.length} instance(s)`).join("; ")}) and no human has decided the pairing. This brief treats the component as SELF-CONTAINED \u2014 you will re-implement the partner's pixels locally. If composition is wanted: generate the partner bundle first, have a human run \`${tendrilCommand(`compose --set ${path46.resolve(task.set)}`)}\` in their terminal, and re-emit this brief. Proceeding as-is is valid; it just is not composition.`
|
|
15722
16317
|
);
|
|
15723
16318
|
}
|
|
15724
16319
|
} catch (err) {
|
|
@@ -15735,9 +16330,9 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
15735
16330
|
const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock + motionBriefSection(task.set) + conventions;
|
|
15736
16331
|
const segments = buildSegments(task, "files");
|
|
15737
16332
|
let notRecorded;
|
|
15738
|
-
const manifestPath2 =
|
|
15739
|
-
if (
|
|
15740
|
-
notRecorded = JSON.parse(
|
|
16333
|
+
const manifestPath2 = path46.join(task.set, "recording-set.json");
|
|
16334
|
+
if (existsSync36(manifestPath2)) {
|
|
16335
|
+
notRecorded = JSON.parse(readFileSync33(manifestPath2, "utf8")).notRecorded;
|
|
15741
16336
|
}
|
|
15742
16337
|
const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
|
|
15743
16338
|
|
|
@@ -15745,7 +16340,7 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
15745
16340
|
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.
|
|
15746
16341
|
${notRecorded}` : "";
|
|
15747
16342
|
let fontProvisioning;
|
|
15748
|
-
if (
|
|
16343
|
+
if (existsSync36(manifestPath2)) {
|
|
15749
16344
|
const missingFams = unprovisionedFamilies(task.set);
|
|
15750
16345
|
const unprovided = unprovisionedFaces(task.set);
|
|
15751
16346
|
const weightOnly = missingFams.length === 0;
|
|
@@ -15767,7 +16362,7 @@ ${notRecorded}` : "";
|
|
|
15767
16362
|
};
|
|
15768
16363
|
}
|
|
15769
16364
|
}
|
|
15770
|
-
const pinsResult = composedPins(task.set, [opts.library !== void 0 ?
|
|
16365
|
+
const pinsResult = composedPins(task.set, [opts.library !== void 0 ? path46.resolve(callerCwd, opts.library) : callerCwd]);
|
|
15771
16366
|
const jsxProp = ([k, v]) => typeof v === "string" ? `${k}=${JSON.stringify(v)}` : `${k}={${JSON.stringify(v)}}`;
|
|
15772
16367
|
const composedBlock = pinsResult.pins.length === 0 && pinsResult.issues.length === 0 ? "" : (pinsResult.pins.length > 0 ? `
|
|
15773
16368
|
|
|
@@ -15803,10 +16398,10 @@ ${pinsResult.issues.map((i) => `- ${i}`).join("\n")}` : "");
|
|
|
15803
16398
|
|
|
15804
16399
|
=== TASK PAYLOAD (recorded truth, verbatim) ===
|
|
15805
16400
|
${segments}`;
|
|
15806
|
-
const payloadFile =
|
|
15807
|
-
const candidateDirSuggestion =
|
|
15808
|
-
|
|
15809
|
-
|
|
16401
|
+
const payloadFile = path46.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
|
|
16402
|
+
const candidateDirSuggestion = path46.resolve(callerCwd, `tendril-out/${name}-candidate`);
|
|
16403
|
+
mkdirSync10(path46.dirname(payloadFile), { recursive: true });
|
|
16404
|
+
writeFileSync15(payloadFile, payload);
|
|
15810
16405
|
emitData(
|
|
15811
16406
|
opts,
|
|
15812
16407
|
{
|
|
@@ -15852,7 +16447,7 @@ ${segments}`;
|
|
|
15852
16447
|
// command must search the same bundle roots the pins came
|
|
15853
16448
|
// from, or the oracle and the brief describe different worlds.
|
|
15854
16449
|
`Run \`${tendrilCommand(
|
|
15855
|
-
`engine score ${quoteArg(ref)} ${quoteArg(candidateDirSuggestion)} --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"}${opts.library !== void 0 ? ` --library ${quoteArg(
|
|
16450
|
+
`engine score ${quoteArg(ref)} ${quoteArg(candidateDirSuggestion)} --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"}${opts.library !== void 0 ? ` --library ${quoteArg(path46.resolve(callerCwd, opts.library))}` : ""} --json`
|
|
15856
16451
|
)}\` \u2014 the CLI is the sole judge, and it refuses to score an undeclared model. If you wrote the bundle elsewhere, pass that directory instead.`,
|
|
15857
16452
|
"Apply the returned feedback and re-score. Stop when all checks pass. Two consecutive non-improving scores mean INSPECT the verify-evidence diffs before deciding \u2014 stop only when inspection yields no fix hypothesis (a measured run was byte-identical twice, then went 27/27 after reading the diffs).",
|
|
15858
16453
|
"Never edit recording sets; never claim scores yourself \u2014 only the score command's output counts."
|
|
@@ -15867,8 +16462,8 @@ ${segments}`;
|
|
|
15867
16462
|
);
|
|
15868
16463
|
}
|
|
15869
16464
|
function appendScoreHistory(candidateDir, entry) {
|
|
15870
|
-
const file =
|
|
15871
|
-
const starts =
|
|
16465
|
+
const file = path46.join(candidateDir, "score-history.jsonl");
|
|
16466
|
+
const starts = existsSync36(file) ? readFileSync33(file, "utf8").split("\n").filter((l) => l.includes('"event":"round-start"')).length : 0;
|
|
15872
16467
|
const round = entry.event === "round-start" ? starts + 1 : Math.max(1, starts);
|
|
15873
16468
|
appendFileSync(file, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), round, ...entry })}
|
|
15874
16469
|
`);
|
|
@@ -15876,9 +16471,9 @@ function appendScoreHistory(candidateDir, entry) {
|
|
|
15876
16471
|
async function runEngineScore(opts) {
|
|
15877
16472
|
requireEntitlement(opts);
|
|
15878
16473
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
15879
|
-
const candidateDir =
|
|
16474
|
+
const candidateDir = path46.resolve(callerCwd, opts.candidateDir);
|
|
15880
16475
|
const { task, name, apiPin, interactionEvidence, unmappedInteractionEvidence: interactionEvidenceUnmapped, satisfiabilityDemoted, expertContract } = resolveEngineTask(opts, callerCwd);
|
|
15881
|
-
if (!
|
|
16476
|
+
if (!existsSync36(candidateDir)) {
|
|
15882
16477
|
fail(opts, ExitCode.InputValidation, {
|
|
15883
16478
|
error: `candidate directory not found: ${candidateDir}`,
|
|
15884
16479
|
code: "candidate-missing",
|
|
@@ -15903,10 +16498,10 @@ async function runEngineScore(opts) {
|
|
|
15903
16498
|
for (const g of missingWeights(task.set)) {
|
|
15904
16499
|
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)`);
|
|
15905
16500
|
}
|
|
15906
|
-
if (opts.rebind !== true &&
|
|
16501
|
+
if (opts.rebind !== true && existsSync36(path46.join(candidateDir, "component.json"))) {
|
|
15907
16502
|
const prior = (() => {
|
|
15908
16503
|
try {
|
|
15909
|
-
const read = readBundleManifest(
|
|
16504
|
+
const read = readBundleManifest(readFileSync33(path46.join(candidateDir, "component.json"), "utf8"));
|
|
15910
16505
|
return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
|
|
15911
16506
|
} catch {
|
|
15912
16507
|
return { unreadable: true };
|
|
@@ -15928,12 +16523,12 @@ async function runEngineScore(opts) {
|
|
|
15928
16523
|
}
|
|
15929
16524
|
}
|
|
15930
16525
|
const bar = BARS3[opts.bar];
|
|
15931
|
-
const evidenceDir =
|
|
16526
|
+
const evidenceDir = path46.join(candidateDir, "verify-evidence");
|
|
15932
16527
|
const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
15933
16528
|
const parity = await checkHoverParity(task, candidateDir, task.configs);
|
|
15934
16529
|
const occlusionRows = await checkSiblingOcclusion(task, candidateDir, candidateCss(candidateDir), { authority: task.configs });
|
|
15935
16530
|
const occlusionFailures = occlusionRows.filter((o) => !o.pass);
|
|
15936
|
-
const scorePins = composedPins(task.set, [opts.library !== void 0 ?
|
|
16531
|
+
const scorePins = composedPins(task.set, [opts.library !== void 0 ? path46.resolve(callerCwd, opts.library) : callerCwd]);
|
|
15937
16532
|
const composition = composedChecks(candidateDir, task.entry, scorePins.pins);
|
|
15938
16533
|
const behaviors = [...await checkBehaviors(task, candidateDir), ...parity, ...composition];
|
|
15939
16534
|
const parityCoverage = parity.length > 0 ? `${parity.filter((p) => p.pass).length}/${parity.length} hover-forced configs` : "not applicable (no hover-forced configs in this set)";
|
|
@@ -16151,11 +16746,11 @@ var codeconnect_exports = {};
|
|
|
16151
16746
|
__export(codeconnect_exports, {
|
|
16152
16747
|
runCodeConnect: () => runCodeConnect
|
|
16153
16748
|
});
|
|
16154
|
-
import { existsSync as
|
|
16155
|
-
import
|
|
16749
|
+
import { existsSync as existsSync37, readFileSync as readFileSync34, writeFileSync as writeFileSync16 } from "node:fs";
|
|
16750
|
+
import path47 from "node:path";
|
|
16156
16751
|
function runCodeConnect(opts) {
|
|
16157
16752
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
16158
|
-
const bundleDir =
|
|
16753
|
+
const bundleDir = path47.resolve(callerCwd, opts.bundleDir);
|
|
16159
16754
|
let url;
|
|
16160
16755
|
try {
|
|
16161
16756
|
url = new URL(opts.figmaUrl);
|
|
@@ -16171,7 +16766,7 @@ function runCodeConnect(opts) {
|
|
|
16171
16766
|
}
|
|
16172
16767
|
let manifest;
|
|
16173
16768
|
try {
|
|
16174
|
-
const read = readBundleManifest(
|
|
16769
|
+
const read = readBundleManifest(readFileSync34(path47.join(bundleDir, "component.json"), "utf8"));
|
|
16175
16770
|
if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
|
|
16176
16771
|
manifest = read.manifest;
|
|
16177
16772
|
} catch (err) {
|
|
@@ -16181,8 +16776,8 @@ function runCodeConnect(opts) {
|
|
|
16181
16776
|
remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
|
|
16182
16777
|
});
|
|
16183
16778
|
}
|
|
16184
|
-
const setDir =
|
|
16185
|
-
if (!
|
|
16779
|
+
const setDir = path47.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
|
|
16780
|
+
if (!existsSync37(path47.join(setDir, "recording-set.json"))) {
|
|
16186
16781
|
fail(opts, ExitCode.InputValidation, {
|
|
16187
16782
|
error: `recording set not found at ${setDir}`,
|
|
16188
16783
|
code: "codeconnect-no-set",
|
|
@@ -16203,10 +16798,10 @@ function runCodeConnect(opts) {
|
|
|
16203
16798
|
const component = api.component;
|
|
16204
16799
|
const recManifest = loadManifest(setDir);
|
|
16205
16800
|
const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
|
|
16206
|
-
const meta =
|
|
16207
|
-
if (!
|
|
16801
|
+
const meta = path47.join(setDir, r.slug, "get_metadata.json");
|
|
16802
|
+
if (!existsSync37(meta)) return void 0;
|
|
16208
16803
|
try {
|
|
16209
|
-
return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(
|
|
16804
|
+
return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(readFileSync34(meta, "utf8"))))?.[1];
|
|
16210
16805
|
} catch {
|
|
16211
16806
|
return void 0;
|
|
16212
16807
|
}
|
|
@@ -16271,7 +16866,7 @@ function runCodeConnect(opts) {
|
|
|
16271
16866
|
axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
|
|
16272
16867
|
fragmentVars.push(varName);
|
|
16273
16868
|
}
|
|
16274
|
-
const entryRel =
|
|
16869
|
+
const entryRel = path47.relative(callerCwd, path47.join(bundleDir, manifest.entry));
|
|
16275
16870
|
const trust = manifest.trustStatement.split("\n")[0] ?? "";
|
|
16276
16871
|
const lines = [
|
|
16277
16872
|
`// url=${opts.figmaUrl}`,
|
|
@@ -16295,8 +16890,8 @@ function runCodeConnect(opts) {
|
|
|
16295
16890
|
`}`,
|
|
16296
16891
|
``
|
|
16297
16892
|
].join("\n");
|
|
16298
|
-
const outFile =
|
|
16299
|
-
|
|
16893
|
+
const outFile = path47.resolve(callerCwd, opts.out ?? path47.join(bundleDir, `${component}.figma.ts`));
|
|
16894
|
+
writeFileSync16(outFile, lines);
|
|
16300
16895
|
emitData(
|
|
16301
16896
|
opts,
|
|
16302
16897
|
{
|
|
@@ -16334,18 +16929,18 @@ var init_codeconnect = __esm({
|
|
|
16334
16929
|
});
|
|
16335
16930
|
|
|
16336
16931
|
// packages/mcp/src/server.ts
|
|
16337
|
-
import { createHash as
|
|
16338
|
-
import { existsSync as
|
|
16339
|
-
import
|
|
16340
|
-
import
|
|
16932
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
16933
|
+
import { existsSync as existsSync38, mkdtempSync as mkdtempSync3, readFileSync as readFileSync35, readdirSync as readdirSync15, writeFileSync as writeFileSync17 } from "node:fs";
|
|
16934
|
+
import os8 from "node:os";
|
|
16935
|
+
import path48 from "node:path";
|
|
16341
16936
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
16342
16937
|
import { z as z14 } from "zod";
|
|
16343
16938
|
function sourceHash() {
|
|
16344
|
-
const dir =
|
|
16345
|
-
const h =
|
|
16346
|
-
for (const f of
|
|
16939
|
+
const dir = path48.dirname(fileURLToPath6(import.meta.url));
|
|
16940
|
+
const h = createHash9("sha256");
|
|
16941
|
+
for (const f of readdirSync15(dir).filter((n) => n.endsWith(".ts")).sort()) {
|
|
16347
16942
|
h.update(f);
|
|
16348
|
-
h.update(
|
|
16943
|
+
h.update(readFileSync35(path48.join(dir, f)));
|
|
16349
16944
|
}
|
|
16350
16945
|
return h.digest("hex").slice(0, 16);
|
|
16351
16946
|
}
|
|
@@ -16353,10 +16948,10 @@ var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
|
|
|
16353
16948
|
var init_server = __esm({
|
|
16354
16949
|
"packages/mcp/src/server.ts"() {
|
|
16355
16950
|
"use strict";
|
|
16356
|
-
REPO_ROOT3 =
|
|
16357
|
-
CLI_BIN =
|
|
16358
|
-
BUNDLED_CLI =
|
|
16359
|
-
CLI_SPAWN =
|
|
16951
|
+
REPO_ROOT3 = path48.resolve(path48.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
|
|
16952
|
+
CLI_BIN = path48.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
|
|
16953
|
+
BUNDLED_CLI = path48.join(path48.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
|
|
16954
|
+
CLI_SPAWN = existsSync38(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
|
|
16360
16955
|
str = (d) => z14.string().describe(d);
|
|
16361
16956
|
optStr = (d) => z14.string().optional().describe(d);
|
|
16362
16957
|
TOOLS = [
|
|
@@ -16387,13 +16982,13 @@ var init_server = __esm({
|
|
|
16387
16982
|
const single = i["metadata"];
|
|
16388
16983
|
const parts = i["metadataParts"];
|
|
16389
16984
|
if (single !== void 0 || parts !== void 0) {
|
|
16390
|
-
const tmp =
|
|
16985
|
+
const tmp = path48.join(mkdtempSync3(path48.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
16391
16986
|
if (single !== void 0) {
|
|
16392
|
-
|
|
16987
|
+
writeFileSync17(tmp, single);
|
|
16393
16988
|
argvOut.push("--metadata-raw-file", tmp);
|
|
16394
16989
|
} else {
|
|
16395
16990
|
if (parts.length === 0) throw new Error("`metadataParts` must be a non-empty array of block texts");
|
|
16396
|
-
|
|
16991
|
+
writeFileSync17(tmp, JSON.stringify(parts));
|
|
16397
16992
|
argvOut.push("--metadata-raw-parts-file", tmp);
|
|
16398
16993
|
}
|
|
16399
16994
|
}
|
|
@@ -16425,10 +17020,24 @@ var init_server = __esm({
|
|
|
16425
17020
|
{
|
|
16426
17021
|
name: "tendril_doctor",
|
|
16427
17022
|
annotations: { readOnlyHint: true },
|
|
16428
|
-
description: "Machine readiness + version status in one shot: installed vs latest version (with publish date and update remediation), browser identity, font-cache state, Figma desktop MCP reachability. Use to self-diagnose before recording/scoring, or whenever versions are in question. Exit 1 = something not ready; the report says exactly what and how to fix it.",
|
|
17023
|
+
description: "Machine readiness + version status in one shot: installed vs latest version (with publish date and update remediation), browser identity, font-cache state, Figma desktop MCP reachability, and whether this machine holds a LIVE portal session (checked against the portal itself; informational \u2014 publishing needs it, verification never does; when absent, offer tendril_login). Use to self-diagnose before recording/scoring, or whenever versions are in question. Exit 1 = something not ready; the report says exactly what and how to fix it.",
|
|
16429
17024
|
schema: z14.object({}),
|
|
16430
17025
|
argv: () => ["doctor"]
|
|
16431
17026
|
},
|
|
17027
|
+
{
|
|
17028
|
+
name: "tendril_login",
|
|
17029
|
+
description: "Connect this machine to the user's Tendril portal account WITHOUT the user touching a terminal \u2014 phase one of the browser-approve sign-in. Starts the handshake, opens their browser to the approve page, and returns a verification link plus a short code. RELAY BOTH to the user verbatim: they click Approve on the page, and they must check the page shows EXACTLY this code (that check is the phishing defence \u2014 an attacker can send someone else's approve link). Then call tendril_login_wait to finish. Offer this whenever tendril_doctor reports no portal session and the user wants to publish or share; publishing needs it, verification never does.",
|
|
17030
|
+
schema: z14.object({
|
|
17031
|
+
portal: optStr("portal origin override for self-hosted portals (defaults to https://app.trytendril.com)")
|
|
17032
|
+
}),
|
|
17033
|
+
argv: (i) => ["login", "--device-start", ...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []]
|
|
17034
|
+
},
|
|
17035
|
+
{
|
|
17036
|
+
name: "tendril_login_wait",
|
|
17037
|
+
description: "Phase two of the browser-approve sign-in: waits (up to ten minutes, with live progress) for the user to click Approve on the page tendril_login opened, then stores the session on this machine. Call it right after relaying the link and code. A denial, a lapse, and success each come back as their own sentence; confirm the result to the user, and on success they are ready to publish.",
|
|
17038
|
+
schema: z14.object({}),
|
|
17039
|
+
argv: () => ["login", "--device-wait"]
|
|
17040
|
+
},
|
|
16432
17041
|
{
|
|
16433
17042
|
name: "tendril_record_next",
|
|
16434
17043
|
annotations: { readOnlyHint: true },
|
|
@@ -16449,7 +17058,7 @@ var init_server = __esm({
|
|
|
16449
17058
|
},
|
|
16450
17059
|
{
|
|
16451
17060
|
name: "tendril_record_ingest_rep",
|
|
16452
|
-
description: "Ingest a rep's ENTIRE recording in ONE call \u2014 the get_metadata response, the get_design_context response, and the get_screenshot image_url together. Make the three Figma calls first, in protocol order (get_metadata, then get_design_context with excludeScreenshot=true, then get_screenshot), then pass all three here VERBATIM. PREFER THIS over three separate ingest/fetch calls: one approvable operation per rep instead of three. Pieces land independently: on a partial failure the error names exactly which piece(s) to re-record \u2014 the rest are already on disk. The response carries `next` and, for design context, `assets` (auto-fetched server-side; only listed failures need record_asset).",
|
|
17061
|
+
description: "Ingest a rep's ENTIRE recording in ONE call \u2014 the get_metadata response, the get_design_context response, and the get_screenshot image_url together. Make the three Figma calls first, in protocol order (get_metadata, then get_design_context with excludeScreenshot=true, then get_screenshot with contentsOnly=true and maxDimension=4096 \u2014 isolation keeps the editor's component-set chrome out of the padded export, where it would otherwise score as unpaintable reference ink), then pass all three here VERBATIM. PREFER THIS over three separate ingest/fetch calls: one approvable operation per rep instead of three. Pieces land independently: on a partial failure the error names exactly which piece(s) to re-record \u2014 the rest are already on disk. The response carries `next` and, for design context, `assets` (auto-fetched server-side; only listed failures need record_asset).",
|
|
16453
17062
|
schema: z14.object({
|
|
16454
17063
|
setDir: str("recording set directory"),
|
|
16455
17064
|
rep: str("planned rep slug"),
|
|
@@ -16470,14 +17079,14 @@ var init_server = __esm({
|
|
|
16470
17079
|
const bridge = (label, single, parts) => {
|
|
16471
17080
|
if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
|
|
16472
17081
|
if (single === void 0 && parts === void 0) return;
|
|
16473
|
-
const tmp =
|
|
17082
|
+
const tmp = path48.join(mkdtempSync3(path48.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
16474
17083
|
if (single !== void 0) {
|
|
16475
|
-
|
|
17084
|
+
writeFileSync17(tmp, single);
|
|
16476
17085
|
argvOut.push(`--${label}-file`, tmp);
|
|
16477
17086
|
} else {
|
|
16478
17087
|
const blocks = parts;
|
|
16479
17088
|
if (blocks.length === 0) throw new Error(`\`${label}Parts\` must be a non-empty array of block texts`);
|
|
16480
|
-
|
|
17089
|
+
writeFileSync17(tmp, JSON.stringify(blocks));
|
|
16481
17090
|
argvOut.push(`--${label}-parts-file`, tmp);
|
|
16482
17091
|
}
|
|
16483
17092
|
};
|
|
@@ -16518,12 +17127,12 @@ var init_server = __esm({
|
|
|
16518
17127
|
const file = i["file"];
|
|
16519
17128
|
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)");
|
|
16520
17129
|
if (file !== void 0) return [...base, "--file", file];
|
|
16521
|
-
const tmp =
|
|
17130
|
+
const tmp = path48.join(mkdtempSync3(path48.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
16522
17131
|
if (text !== void 0) {
|
|
16523
|
-
|
|
17132
|
+
writeFileSync17(tmp, text);
|
|
16524
17133
|
return [...base, "--file", tmp, "--raw"];
|
|
16525
17134
|
}
|
|
16526
|
-
|
|
17135
|
+
writeFileSync17(tmp, JSON.stringify(texts));
|
|
16527
17136
|
return [...base, "--file", tmp, "--raw-parts"];
|
|
16528
17137
|
}
|
|
16529
17138
|
},
|
|
@@ -16680,13 +17289,13 @@ __export(permissions_exports, {
|
|
|
16680
17289
|
runPermissions: () => runPermissions,
|
|
16681
17290
|
writeSelection: () => writeSelection
|
|
16682
17291
|
});
|
|
16683
|
-
import { existsSync as
|
|
16684
|
-
import
|
|
16685
|
-
import
|
|
17292
|
+
import { existsSync as existsSync39, mkdirSync as mkdirSync11, readFileSync as readFileSync36, writeFileSync as writeFileSync18 } from "node:fs";
|
|
17293
|
+
import os9 from "node:os";
|
|
17294
|
+
import path49 from "node:path";
|
|
16686
17295
|
function mergeAllowlist(file, entries, denyEntries = []) {
|
|
16687
17296
|
let settings = {};
|
|
16688
|
-
if (
|
|
16689
|
-
settings = JSON.parse(
|
|
17297
|
+
if (existsSync39(file) && readFileSync36(file, "utf8").trim() !== "") {
|
|
17298
|
+
settings = JSON.parse(readFileSync36(file, "utf8"));
|
|
16690
17299
|
if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
|
|
16691
17300
|
}
|
|
16692
17301
|
const permissions = settings["permissions"] ??= {};
|
|
@@ -16706,8 +17315,8 @@ function mergeAllowlist(file, entries, denyEntries = []) {
|
|
|
16706
17315
|
}
|
|
16707
17316
|
if (added.length > 0 || denyAdded.length > 0) {
|
|
16708
17317
|
allow.push(...added);
|
|
16709
|
-
|
|
16710
|
-
|
|
17318
|
+
mkdirSync11(path49.dirname(file), { recursive: true });
|
|
17319
|
+
writeFileSync18(file, `${JSON.stringify(settings, null, 2)}
|
|
16711
17320
|
`);
|
|
16712
17321
|
}
|
|
16713
17322
|
return { added, alreadyPresent, denyAdded };
|
|
@@ -16771,7 +17380,7 @@ async function runPermissions(flags) {
|
|
|
16771
17380
|
}
|
|
16772
17381
|
if (flags.write) {
|
|
16773
17382
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
16774
|
-
const file = flags.user ?
|
|
17383
|
+
const file = flags.user ? path49.join(os9.homedir(), ".claude", "settings.json") : path49.join(base, ".claude", "settings.local.json");
|
|
16775
17384
|
const { entries, denyEntries } = writeSelection(result, flags.user === true);
|
|
16776
17385
|
if (flags.dryRun) {
|
|
16777
17386
|
emitData(flags, { file, wouldAdd: entries, wouldDeny: denyEntries }, () => {
|
|
@@ -16920,24 +17529,51 @@ __export(inspect_exports, {
|
|
|
16920
17529
|
INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
|
|
16921
17530
|
runInspect: () => runInspect
|
|
16922
17531
|
});
|
|
16923
|
-
import { existsSync as
|
|
16924
|
-
import
|
|
17532
|
+
import { existsSync as existsSync40, readFileSync as readFileSync37, writeFileSync as writeFileSync19 } from "node:fs";
|
|
17533
|
+
import path50 from "node:path";
|
|
17534
|
+
function readVerifyReport(evidenceDir) {
|
|
17535
|
+
const p = path50.join(evidenceDir, VERIFY_REPORT_FILENAME);
|
|
17536
|
+
if (!existsSync40(p)) return void 0;
|
|
17537
|
+
try {
|
|
17538
|
+
return JSON.parse(readFileSync37(p, "utf8"));
|
|
17539
|
+
} catch {
|
|
17540
|
+
return void 0;
|
|
17541
|
+
}
|
|
17542
|
+
}
|
|
17543
|
+
function verdictBlock(report) {
|
|
17544
|
+
if (report === void 0) {
|
|
17545
|
+
return `<div class="verdict none"><h2>No verdict on file</h2><p>This evidence carries no <code>${VERIFY_REPORT_FILENAME}</code>, so these images are unlabelled pixels \u2014 nothing here says what passed. Re-run <code>tendril verify</code> to write one.</p></div>`;
|
|
17546
|
+
}
|
|
17547
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
17548
|
+
for (const c of report.configs) buckets.set(c.status, (buckets.get(c.status) ?? 0) + 1);
|
|
17549
|
+
const split = [...buckets.entries()].map(([k, n]) => `<span class="tier t-${esc(k)}">${n} ${esc(k)}</span>`).join("");
|
|
17550
|
+
const failed = report.behaviors.filter((b) => !b.pass);
|
|
17551
|
+
return `<div class="verdict ${report.verdict === "verified" ? "ok" : "bad"}"><h2>${esc(report.verdict)} <small>at bar \u201C${esc(report.targetBar)}\u201D</small></h2><p class="tiers">${split}<span class="tier">${report.behaviors.length - failed.length}/${report.behaviors.length} behaviors</span></p>` + (failed.length > 0 ? `<ul class="fails">${failed.map((b) => `<li><code>${esc(b.id)}</code>${b.detail !== void 0 ? ` \u2014 ${esc(b.detail)}` : ""}</li>`).join("")}</ul>` : "") + (report.verdictCaveats.length > 0 ? `<h3>What was never asked</h3><ul class="caveats">${report.verdictCaveats.map((c) => `<li>${esc(c)}</li>`).join("")}</ul>` : `<p class="caveats">No caveats: every question this bar asks was answered.</p>`) + `</div>`;
|
|
17552
|
+
}
|
|
17553
|
+
function scoreLine(report, rep) {
|
|
17554
|
+
const c = report?.configs.find((x) => x.rep === rep);
|
|
17555
|
+
if (c === void 0) return "";
|
|
17556
|
+
const ex = c.exact !== void 0 ? ` <span class="exact">measured ${c.exact.similarity.toFixed(6)} / ${c.exact.inkRecall.toFixed(6)}</span>` : "";
|
|
17557
|
+
const why = Array.isArray(c.demotedBy) && c.demotedBy.length > 0 ? `<span class="demoted">demoted: ${esc(c.demotedBy.join("; "))}</span>` : "";
|
|
17558
|
+
const err = typeof c.error === "string" ? `<span class="demoted">${esc(c.error)}</span>` : "";
|
|
17559
|
+
return `<p class="scores"><span class="tier t-${esc(c.status)}">${esc(c.status)}</span> sim ${c.similarity} \xB7 ink ${c.inkRecall}${ex} ${why} ${err}</p>`;
|
|
17560
|
+
}
|
|
16925
17561
|
async function runInspect(opts) {
|
|
16926
17562
|
if (opts.describe) {
|
|
16927
17563
|
printDescription(INSPECT_DESCRIPTION);
|
|
16928
17564
|
return;
|
|
16929
17565
|
}
|
|
16930
|
-
const bundleDir =
|
|
16931
|
-
const evidenceDir =
|
|
16932
|
-
const manifestPath2 =
|
|
16933
|
-
if (!
|
|
17566
|
+
const bundleDir = path50.resolve(opts.bundleDir);
|
|
17567
|
+
const evidenceDir = path50.join(bundleDir, "verify-evidence");
|
|
17568
|
+
const manifestPath2 = path50.join(bundleDir, "component.json");
|
|
17569
|
+
if (!existsSync40(evidenceDir) || !existsSync40(manifestPath2)) {
|
|
16934
17570
|
fail(opts, ExitCode.InputValidation, {
|
|
16935
|
-
error: `nothing to inspect in ${bundleDir} \u2014 ${
|
|
17571
|
+
error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync40(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
|
|
16936
17572
|
code: "no-evidence",
|
|
16937
17573
|
remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
|
|
16938
17574
|
});
|
|
16939
17575
|
}
|
|
16940
|
-
const { manifest } = readBundleManifest(
|
|
17576
|
+
const { manifest } = readBundleManifest(readFileSync37(manifestPath2, "utf8"));
|
|
16941
17577
|
if (manifest === void 0) {
|
|
16942
17578
|
fail(opts, ExitCode.InputValidation, {
|
|
16943
17579
|
error: "component.json did not parse as a bundle manifest",
|
|
@@ -16945,8 +17581,9 @@ async function runInspect(opts) {
|
|
|
16945
17581
|
remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
|
|
16946
17582
|
});
|
|
16947
17583
|
}
|
|
16948
|
-
const setDir =
|
|
16949
|
-
const
|
|
17584
|
+
const setDir = path50.resolve(opts.set ?? manifest.provenance.recordingSet.path);
|
|
17585
|
+
const report = readVerifyReport(evidenceDir);
|
|
17586
|
+
const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync40(path50.join(evidenceDir, `${rep}-ref.png`)) && existsSync40(path50.join(evidenceDir, `${rep}-render.png`)));
|
|
16950
17587
|
if (reps.length === 0) {
|
|
16951
17588
|
fail(opts, ExitCode.InputValidation, {
|
|
16952
17589
|
error: "verify-evidence holds no ref/render pairs for this bundle's configs",
|
|
@@ -16957,15 +17594,15 @@ async function runInspect(opts) {
|
|
|
16957
17594
|
let crops = 0;
|
|
16958
17595
|
const sections = [];
|
|
16959
17596
|
for (const rep of reps) {
|
|
16960
|
-
const ref = new Uint8Array(
|
|
16961
|
-
const render = new Uint8Array(
|
|
17597
|
+
const ref = new Uint8Array(readFileSync37(path50.join(evidenceDir, `${rep}-ref.png`)));
|
|
17598
|
+
const render = new Uint8Array(readFileSync37(path50.join(evidenceDir, `${rep}-render.png`)));
|
|
16962
17599
|
const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
|
|
16963
17600
|
const cells = [];
|
|
16964
17601
|
for (const [i, n] of nodes.entries()) {
|
|
16965
17602
|
const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
|
|
16966
17603
|
try {
|
|
16967
|
-
|
|
16968
|
-
|
|
17604
|
+
writeFileSync19(path50.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
|
|
17605
|
+
writeFileSync19(path50.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
|
|
16969
17606
|
} catch {
|
|
16970
17607
|
continue;
|
|
16971
17608
|
}
|
|
@@ -16975,11 +17612,15 @@ async function runInspect(opts) {
|
|
|
16975
17612
|
);
|
|
16976
17613
|
}
|
|
16977
17614
|
sections.push(
|
|
16978
|
-
`<section><h2>${esc(rep)}</h2
|
|
17615
|
+
`<section><h2>${esc(rep)}</h2>` + scoreLine(report, rep) + `<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>`
|
|
16979
17616
|
);
|
|
16980
17617
|
}
|
|
16981
|
-
const
|
|
16982
|
-
|
|
17618
|
+
for (const c of report?.configs ?? []) {
|
|
17619
|
+
if (reps.includes(c.rep)) continue;
|
|
17620
|
+
sections.push(`<section class="missing"><h2>${esc(c.rep)}</h2>${scoreLine(report, c.rep)}<p class="none">No evidence images for this config \u2014 it was scored, but nothing was captured to look at.</p></section>`);
|
|
17621
|
+
}
|
|
17622
|
+
const sheet = path50.join(evidenceDir, "inspect.html");
|
|
17623
|
+
writeFileSync19(
|
|
16983
17624
|
sheet,
|
|
16984
17625
|
`<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
|
|
16985
17626
|
body{font:14px/1.45 system-ui,sans-serif;margin:24px;background:#fff;color:#111}
|
|
@@ -16988,11 +17629,23 @@ h1{font-size:20px} h2{font-size:16px;border-top:1px solid #ddd;padding-top:16px}
|
|
|
16988
17629
|
.full img{max-width:400px;border:1px solid #ccc} .pair img{border:1px solid #ccc;image-rendering:pixelated}
|
|
16989
17630
|
figure{margin:0 0 16px} figcaption{margin-bottom:4px} em{display:block;color:#666;font-style:normal;font-size:12px}
|
|
16990
17631
|
.grid{display:flex;flex-wrap:wrap;gap:20px;margin-top:12px} .none{color:#666}
|
|
16991
|
-
|
|
16992
|
-
|
|
17632
|
+
.verdict{border:1px solid #ddd;border-left-width:5px;padding:12px 16px;margin:16px 0;background:#fafafa}
|
|
17633
|
+
.verdict.ok{border-left-color:#1a7f37} .verdict.bad{border-left-color:#b35900} .verdict.none{border-left-color:#999}
|
|
17634
|
+
.verdict h2{border:0;padding:0;margin:0 0 8px} .verdict h3{font-size:13px;margin:12px 0 4px;color:#444}
|
|
17635
|
+
.verdict small{font-weight:400;color:#666}
|
|
17636
|
+
.tiers{margin:0;display:flex;gap:8px;flex-wrap:wrap}
|
|
17637
|
+
.tier{display:inline-block;padding:1px 8px;border:1px solid #ccc;border-radius:10px;font-size:12px;background:#fff}
|
|
17638
|
+
.t-certified{border-color:#1a7f37;color:#1a7f37} .t-pass{border-color:#8a6d00;color:#8a6d00} .t-fail{border-color:#b3261e;color:#b3261e}
|
|
17639
|
+
.caveats,.fails{margin:4px 0 0;padding-left:20px;color:#444} .caveats li,.fails li{margin:2px 0}
|
|
17640
|
+
.scores{margin:4px 0 10px;color:#333} .exact{color:#666;font-size:12px} .demoted{color:#b3261e;font-size:12px;margin-left:8px}
|
|
17641
|
+
.missing{opacity:.85} .legend{white-space:pre-wrap;font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;background:#fafafa;border:1px solid #ddd;padding:12px;overflow-x:auto}
|
|
17642
|
+
</style><h1>${esc(manifest.name)} \u2014 verdict and evidence</h1>
|
|
17643
|
+
${verdictBlock(report)}
|
|
17644
|
+
<p>Below, every crop is a recorded node small enough that global metrics weight it as a rounding error.
|
|
16993
17645
|
Scan the pairs: anything present on the left and missing/invisible on the right is a defect,
|
|
16994
17646
|
whatever the scores said. Verdicts come from <code>tendril verify</code> \u2014 this sheet only shows.</p>
|
|
16995
17647
|
${sections.join("\n")}
|
|
17648
|
+
<section><h2>Diff colours</h2><div class="legend">${esc(DIFF_LEGEND_TEXT)}</div></section>
|
|
16996
17649
|
`
|
|
16997
17650
|
);
|
|
16998
17651
|
emitData(opts, { sheet, configs: reps.length, crops }, () => {
|
|
@@ -17032,6 +17685,694 @@ var init_inspect = __esm({
|
|
|
17032
17685
|
}
|
|
17033
17686
|
});
|
|
17034
17687
|
|
|
17688
|
+
// packages/cli/src/commands/login.ts
|
|
17689
|
+
var login_exports = {};
|
|
17690
|
+
__export(login_exports, {
|
|
17691
|
+
DEFAULT_PORTAL_ORIGIN: () => DEFAULT_PORTAL_ORIGIN,
|
|
17692
|
+
runLogin: () => runLogin,
|
|
17693
|
+
runLogout: () => runLogout
|
|
17694
|
+
});
|
|
17695
|
+
import { spawn } from "node:child_process";
|
|
17696
|
+
import { existsSync as existsSync41, mkdirSync as mkdirSync12, readFileSync as readFileSync38, rmSync as rmSync6, writeFileSync as writeFileSync20 } from "node:fs";
|
|
17697
|
+
import path51 from "node:path";
|
|
17698
|
+
import { isCancel as isCancel3, password as password2 } from "@clack/prompts";
|
|
17699
|
+
async function runLogin(opts, deps) {
|
|
17700
|
+
const origin = (opts.to ?? process.env["TENDRIL_PORTAL_URL"] ?? DEFAULT_PORTAL_ORIGIN).replace(/\/+$/, "");
|
|
17701
|
+
if (!isSecureOrigin(origin)) {
|
|
17702
|
+
fail(opts, ExitCode.InputValidation, {
|
|
17703
|
+
error: `${origin} is not https, so a session token sent there would travel in the clear`,
|
|
17704
|
+
code: "portal-not-https",
|
|
17705
|
+
remediation: "Use the https:// address of your portal. Only 127.0.0.1 and localhost are exempt, for local development."
|
|
17706
|
+
});
|
|
17707
|
+
}
|
|
17708
|
+
if (opts.deviceStart === true) {
|
|
17709
|
+
await deviceStartPhase(opts, origin, deps ?? realDeps());
|
|
17710
|
+
return;
|
|
17711
|
+
}
|
|
17712
|
+
if (opts.deviceWait === true) {
|
|
17713
|
+
await deviceWaitPhase(opts, deps ?? realDeps());
|
|
17714
|
+
return;
|
|
17715
|
+
}
|
|
17716
|
+
if (process.stdin.isTTY === true && opts.paste !== true) {
|
|
17717
|
+
await deviceLogin(opts, origin, deps ?? realDeps());
|
|
17718
|
+
return;
|
|
17719
|
+
}
|
|
17720
|
+
await pasteLogin(opts, origin);
|
|
17721
|
+
}
|
|
17722
|
+
async function deviceLogin(opts, origin, deps) {
|
|
17723
|
+
const started = await startHandshake(opts, origin, deps);
|
|
17724
|
+
process.stderr.write(`
|
|
17725
|
+
Open this link to approve the sign-in:
|
|
17726
|
+
|
|
17727
|
+
${started.verificationUrl}
|
|
17728
|
+
|
|
17729
|
+
`);
|
|
17730
|
+
process.stderr.write(` The page must show this code: ${started.userCode}
|
|
17731
|
+
`);
|
|
17732
|
+
process.stderr.write(` Approve it only if this terminal is yours.
|
|
17733
|
+
|
|
17734
|
+
`);
|
|
17735
|
+
deps.openBrowser(started.verificationUrl);
|
|
17736
|
+
process.stderr.write(` Waiting for the browser (about ten minutes before this code lapses) `);
|
|
17737
|
+
const outcome = await waitForDecision(opts, origin, deps, started, () => process.stderr.write("."));
|
|
17738
|
+
process.stderr.write("\n");
|
|
17739
|
+
settleDecision(opts, origin, outcome);
|
|
17740
|
+
}
|
|
17741
|
+
async function waitForDecision(opts, origin, deps, started, onTick) {
|
|
17742
|
+
const interval = Math.max(1, started.intervalSeconds);
|
|
17743
|
+
const attempts = Math.ceil(660 / interval);
|
|
17744
|
+
for (let n = 0; n < attempts; n += 1) {
|
|
17745
|
+
await deps.sleep(interval);
|
|
17746
|
+
const state = await pollHandshake(opts, origin, deps, started.deviceCode);
|
|
17747
|
+
if (state.status === "pending") {
|
|
17748
|
+
onTick();
|
|
17749
|
+
continue;
|
|
17750
|
+
}
|
|
17751
|
+
if (state.status === "approved") return state;
|
|
17752
|
+
return { status: state.status };
|
|
17753
|
+
}
|
|
17754
|
+
return { status: "gave-up" };
|
|
17755
|
+
}
|
|
17756
|
+
function settleDecision(opts, origin, outcome) {
|
|
17757
|
+
switch (outcome.status) {
|
|
17758
|
+
case "approved":
|
|
17759
|
+
writeStoredSession({ origin, token: outcome.token });
|
|
17760
|
+
emitData(opts, { origin, storedAt: sessionPath(), via: "device" }, () => {
|
|
17761
|
+
process.stdout.write(`signed in to ${origin}
|
|
17762
|
+
`);
|
|
17763
|
+
process.stdout.write(` token stored at ${sessionPath()} (readable only by you)
|
|
17764
|
+
`);
|
|
17765
|
+
});
|
|
17766
|
+
return;
|
|
17767
|
+
case "denied":
|
|
17768
|
+
fail(opts, ExitCode.General, {
|
|
17769
|
+
error: "the sign-in was denied in the browser",
|
|
17770
|
+
code: "login-denied",
|
|
17771
|
+
remediation: `If that was not you saying no, run ${tendrilCommand("login")} again and approve the fresh code.`
|
|
17772
|
+
});
|
|
17773
|
+
break;
|
|
17774
|
+
case "expired":
|
|
17775
|
+
case "gave-up":
|
|
17776
|
+
fail(opts, ExitCode.General, {
|
|
17777
|
+
error: outcome.status === "expired" ? "the sign-in code lapsed before anyone approved it \u2014 they live ten minutes" : "gave up waiting for the browser approval",
|
|
17778
|
+
code: "login-expired",
|
|
17779
|
+
remediation: `Run ${tendrilCommand("login")} again for a fresh code.`
|
|
17780
|
+
});
|
|
17781
|
+
break;
|
|
17782
|
+
default:
|
|
17783
|
+
fail(opts, ExitCode.General, {
|
|
17784
|
+
error: "the portal no longer recognises this sign-in attempt",
|
|
17785
|
+
code: "login-unknown",
|
|
17786
|
+
remediation: `Run ${tendrilCommand("login")} again. If this repeats, the approval may be racing another terminal \u2014 approve only one at a time.`
|
|
17787
|
+
});
|
|
17788
|
+
}
|
|
17789
|
+
}
|
|
17790
|
+
function pendingLoginPath() {
|
|
17791
|
+
return path51.join(path51.dirname(sessionPath()), "pending-login.json");
|
|
17792
|
+
}
|
|
17793
|
+
async function deviceStartPhase(opts, origin, deps) {
|
|
17794
|
+
const started = await startHandshake(opts, origin, deps);
|
|
17795
|
+
const file = pendingLoginPath();
|
|
17796
|
+
mkdirSync12(path51.dirname(file), { recursive: true });
|
|
17797
|
+
writeFileSync20(file, `${JSON.stringify({ origin, ...started }, null, 2)}
|
|
17798
|
+
`, { mode: 384 });
|
|
17799
|
+
deps.openBrowser(started.verificationUrl);
|
|
17800
|
+
emitData(
|
|
17801
|
+
opts,
|
|
17802
|
+
{
|
|
17803
|
+
verificationUrl: started.verificationUrl,
|
|
17804
|
+
userCode: started.userCode,
|
|
17805
|
+
expiresAt: started.expiresAt,
|
|
17806
|
+
origin,
|
|
17807
|
+
next: "Show the user the link and the code \u2014 they approve in the browser, and must check the page shows this exact code. Then run login --device-wait (the tendril_login_wait tool) to finish."
|
|
17808
|
+
},
|
|
17809
|
+
() => {
|
|
17810
|
+
process.stdout.write(`open ${started.verificationUrl}
|
|
17811
|
+
`);
|
|
17812
|
+
process.stdout.write(` the page must show: ${started.userCode}
|
|
17813
|
+
`);
|
|
17814
|
+
process.stdout.write(` then: ${tendrilCommand("login --device-wait")}
|
|
17815
|
+
`);
|
|
17816
|
+
}
|
|
17817
|
+
);
|
|
17818
|
+
}
|
|
17819
|
+
async function deviceWaitPhase(opts, deps) {
|
|
17820
|
+
const file = pendingLoginPath();
|
|
17821
|
+
let pending;
|
|
17822
|
+
if (existsSync41(file)) {
|
|
17823
|
+
try {
|
|
17824
|
+
const parsed = JSON.parse(readFileSync38(file, "utf8"));
|
|
17825
|
+
if (typeof parsed.origin === "string" && typeof parsed.deviceCode === "string" && typeof parsed.intervalSeconds === "number") {
|
|
17826
|
+
pending = parsed;
|
|
17827
|
+
}
|
|
17828
|
+
} catch {
|
|
17829
|
+
}
|
|
17830
|
+
}
|
|
17831
|
+
if (pending === void 0) {
|
|
17832
|
+
fail(opts, ExitCode.InputValidation, {
|
|
17833
|
+
error: "there is no sign-in waiting to finish",
|
|
17834
|
+
code: "no-pending-login",
|
|
17835
|
+
remediation: `Start one first: ${tendrilCommand("login --device-start")} (the tendril_login tool).`
|
|
17836
|
+
});
|
|
17837
|
+
}
|
|
17838
|
+
const done = () => rmSync6(file, { force: true });
|
|
17839
|
+
const total = Math.ceil(660 / Math.max(1, pending.intervalSeconds));
|
|
17840
|
+
let ticks = 0;
|
|
17841
|
+
const outcome = await waitForDecision(opts, pending.origin, deps, pending, () => {
|
|
17842
|
+
ticks += 1;
|
|
17843
|
+
emitProgress(ticks, total, "waiting for the browser approval");
|
|
17844
|
+
});
|
|
17845
|
+
done();
|
|
17846
|
+
settleDecision(opts, pending.origin, outcome);
|
|
17847
|
+
}
|
|
17848
|
+
async function startHandshake(opts, origin, deps) {
|
|
17849
|
+
let response;
|
|
17850
|
+
try {
|
|
17851
|
+
response = await deps.fetch(`${origin}/api/device`, { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
|
|
17852
|
+
} catch (error) {
|
|
17853
|
+
return unreachable(opts, origin, error.message);
|
|
17854
|
+
}
|
|
17855
|
+
if (!response.ok) return unreachable(opts, origin, `the portal answered ${String(response.status)}`);
|
|
17856
|
+
const body = await response.json();
|
|
17857
|
+
if (typeof body.deviceCode !== "string" || typeof body.userCode !== "string" || typeof body.verificationUrl !== "string" || typeof body.intervalSeconds !== "number" || typeof body.expiresAt !== "string") {
|
|
17858
|
+
return unreachable(opts, origin, "the portal's answer was not a device handshake");
|
|
17859
|
+
}
|
|
17860
|
+
return body;
|
|
17861
|
+
}
|
|
17862
|
+
async function pollHandshake(opts, origin, deps, deviceCode) {
|
|
17863
|
+
let response;
|
|
17864
|
+
try {
|
|
17865
|
+
response = await deps.fetch(`${origin}/api/device/token`, {
|
|
17866
|
+
method: "POST",
|
|
17867
|
+
headers: { "content-type": "application/json" },
|
|
17868
|
+
body: JSON.stringify({ deviceCode })
|
|
17869
|
+
});
|
|
17870
|
+
} catch {
|
|
17871
|
+
return { status: "pending" };
|
|
17872
|
+
}
|
|
17873
|
+
if (!response.ok) return { status: "pending" };
|
|
17874
|
+
const body = await response.json();
|
|
17875
|
+
if (body.status === "approved" && typeof body.token === "string") return { status: "approved", token: body.token };
|
|
17876
|
+
if (body.status === "denied" || body.status === "expired" || body.status === "unknown") return { status: body.status };
|
|
17877
|
+
return { status: "pending" };
|
|
17878
|
+
}
|
|
17879
|
+
function unreachable(opts, origin, detail) {
|
|
17880
|
+
fail(opts, ExitCode.General, {
|
|
17881
|
+
error: `could not start a sign-in with ${origin}: ${detail}`,
|
|
17882
|
+
code: "portal-unreachable",
|
|
17883
|
+
remediation: "Check your connection and try again. If your portal is self-hosted, confirm the --to URL."
|
|
17884
|
+
});
|
|
17885
|
+
}
|
|
17886
|
+
function realDeps() {
|
|
17887
|
+
return {
|
|
17888
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
17889
|
+
sleep: (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1e3)),
|
|
17890
|
+
openBrowser: (url) => {
|
|
17891
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
17892
|
+
try {
|
|
17893
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).on("error", () => void 0).unref();
|
|
17894
|
+
} catch {
|
|
17895
|
+
}
|
|
17896
|
+
}
|
|
17897
|
+
};
|
|
17898
|
+
}
|
|
17899
|
+
async function pasteLogin(opts, origin) {
|
|
17900
|
+
const token = await readToken(opts);
|
|
17901
|
+
if (token === void 0 || token.trim() === "") {
|
|
17902
|
+
fail(opts, ExitCode.InputValidation, {
|
|
17903
|
+
error: "no token was given",
|
|
17904
|
+
code: "no-token",
|
|
17905
|
+
remediation: `Paste the token when prompted, or pipe it: \`echo "<token>" | ${tendrilCommand(`login --to ${origin}`)}\`.`
|
|
17906
|
+
});
|
|
17907
|
+
}
|
|
17908
|
+
const trimmed = token.trim();
|
|
17909
|
+
if (!/^[A-Za-z0-9_-]{16,512}$/.test(trimmed)) {
|
|
17910
|
+
fail(opts, ExitCode.InputValidation, {
|
|
17911
|
+
error: "that does not look like a Tendril session token",
|
|
17912
|
+
code: "malformed-token",
|
|
17913
|
+
remediation: "Tokens are a single line of letters, digits, hyphens and underscores. Check for a stray space or a truncated paste."
|
|
17914
|
+
});
|
|
17915
|
+
}
|
|
17916
|
+
writeStoredSession({ origin, token: trimmed });
|
|
17917
|
+
emitData(opts, { origin, storedAt: sessionPath(), via: "paste" }, () => {
|
|
17918
|
+
process.stdout.write(`signed in to ${origin}
|
|
17919
|
+
`);
|
|
17920
|
+
process.stdout.write(` token stored at ${sessionPath()} (readable only by you)
|
|
17921
|
+
`);
|
|
17922
|
+
});
|
|
17923
|
+
}
|
|
17924
|
+
async function readToken(opts) {
|
|
17925
|
+
if (process.stdin.isTTY === true) {
|
|
17926
|
+
const entered = await password2({ message: "Paste your Tendril portal token (it will not be shown)" });
|
|
17927
|
+
if (isCancel3(entered)) {
|
|
17928
|
+
fail(opts, ExitCode.General, { error: "cancelled", code: "cancelled", remediation: "Run it again when you have the token." });
|
|
17929
|
+
}
|
|
17930
|
+
return entered;
|
|
17931
|
+
}
|
|
17932
|
+
const chunks = [];
|
|
17933
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
17934
|
+
return Buffer.concat(chunks).toString("utf8").split("\n")[0];
|
|
17935
|
+
}
|
|
17936
|
+
function runLogout(opts) {
|
|
17937
|
+
const stored = readStoredSession();
|
|
17938
|
+
if (stored === void 0) {
|
|
17939
|
+
emitData(opts, { signedOut: true, wasSignedIn: false }, () => process.stdout.write("you were not signed in\n"));
|
|
17940
|
+
return;
|
|
17941
|
+
}
|
|
17942
|
+
clearStoredSession();
|
|
17943
|
+
emitData(opts, { signedOut: true, wasSignedIn: true, origin: stored.origin }, () => {
|
|
17944
|
+
process.stdout.write(`this machine has forgotten its session for ${stored.origin}
|
|
17945
|
+
`);
|
|
17946
|
+
process.stdout.write(" the session itself is still valid \u2014 sign out everywhere from the portal to end it\n");
|
|
17947
|
+
});
|
|
17948
|
+
}
|
|
17949
|
+
var DEFAULT_PORTAL_ORIGIN;
|
|
17950
|
+
var init_login = __esm({
|
|
17951
|
+
"packages/cli/src/commands/login.ts"() {
|
|
17952
|
+
"use strict";
|
|
17953
|
+
init_src3();
|
|
17954
|
+
init_invocation();
|
|
17955
|
+
init_output();
|
|
17956
|
+
init_publish_client();
|
|
17957
|
+
DEFAULT_PORTAL_ORIGIN = "https://app.trytendril.com";
|
|
17958
|
+
}
|
|
17959
|
+
});
|
|
17960
|
+
|
|
17961
|
+
// packages/cli/src/commands/share.ts
|
|
17962
|
+
var share_exports = {};
|
|
17963
|
+
__export(share_exports, {
|
|
17964
|
+
runShare: () => runShare
|
|
17965
|
+
});
|
|
17966
|
+
async function runShare(opts) {
|
|
17967
|
+
const origin = (opts.to ?? process.env["TENDRIL_PORTAL_URL"] ?? "").replace(/\/+$/, "");
|
|
17968
|
+
if (origin === "") {
|
|
17969
|
+
fail(opts, ExitCode.InputValidation, {
|
|
17970
|
+
error: "no portal to share from",
|
|
17971
|
+
code: "no-portal-configured",
|
|
17972
|
+
remediation: `Pass \`--to <url>\` or set TENDRIL_PORTAL_URL.`
|
|
17973
|
+
});
|
|
17974
|
+
}
|
|
17975
|
+
if (!isSecureOrigin(origin)) {
|
|
17976
|
+
fail(opts, ExitCode.InputValidation, {
|
|
17977
|
+
error: `${origin} is not https, so a session token sent there would travel in the clear`,
|
|
17978
|
+
code: "portal-not-https",
|
|
17979
|
+
remediation: "Use the https:// address of your portal. Only 127.0.0.1 and localhost are exempt, for local development."
|
|
17980
|
+
});
|
|
17981
|
+
}
|
|
17982
|
+
const found = tokenFor(origin);
|
|
17983
|
+
if (!found.ok) {
|
|
17984
|
+
fail(opts, ExitCode.Auth, {
|
|
17985
|
+
error: found.reason === "origin-mismatch" ? `the session available here belongs to ${found.boundTo}, not ${origin}` : `no session for ${origin}`,
|
|
17986
|
+
code: found.reason === "origin-mismatch" ? "session-belongs-to-another-portal" : "not-signed-in",
|
|
17987
|
+
remediation: `Run \`${tendrilCommand(`login --to ${origin}`)}\`.`
|
|
17988
|
+
});
|
|
17989
|
+
}
|
|
17990
|
+
const client = new HttpPublishClient({ origin, token: found.token });
|
|
17991
|
+
if (opts.list === true) {
|
|
17992
|
+
const listed = await client.listShareLinks({ publicationId: opts.publicationId });
|
|
17993
|
+
if (!listed.ok) refuse(opts, listed);
|
|
17994
|
+
emitData(opts, listed.value, () => {
|
|
17995
|
+
if (listed.value.links.length === 0) {
|
|
17996
|
+
process.stdout.write("no links have been issued for this publication\n");
|
|
17997
|
+
return;
|
|
17998
|
+
}
|
|
17999
|
+
process.stdout.write(`${String(listed.value.links.length)} link(s) for this publication:
|
|
18000
|
+
`);
|
|
18001
|
+
for (const link of listed.value.links) {
|
|
18002
|
+
const ended = link.revoked_at !== null ? ` REVOKED ${link.revoked_at}` : link.expires_at !== null ? ` expires ${link.expires_at}` : " open-ended";
|
|
18003
|
+
process.stdout.write(` ${link.id}${ended}${link.recipient_email === null ? "" : ` \u2014 for ${link.recipient_email}`}
|
|
18004
|
+
`);
|
|
18005
|
+
}
|
|
18006
|
+
process.stdout.write(" (the links themselves are not stored \u2014 only their digests, so a lost link is a new link)\n");
|
|
18007
|
+
});
|
|
18008
|
+
return;
|
|
18009
|
+
}
|
|
18010
|
+
if (opts.revoke !== void 0) {
|
|
18011
|
+
const revoked = await client.revokeShareLink({ publicationId: opts.publicationId, shareLinkId: opts.revoke });
|
|
18012
|
+
if (!revoked.ok) refuse(opts, revoked);
|
|
18013
|
+
emitData(opts, revoked.value, () => {
|
|
18014
|
+
process.stdout.write(`revoked ${opts.revoke ?? ""}
|
|
18015
|
+
`);
|
|
18016
|
+
process.stdout.write(" anyone holding that link now gets told it was revoked, not that it never existed\n");
|
|
18017
|
+
});
|
|
18018
|
+
return;
|
|
18019
|
+
}
|
|
18020
|
+
const expiresAt = resolveExpiry(opts);
|
|
18021
|
+
const issued = await client.issueShareLink({
|
|
18022
|
+
publicationId: opts.publicationId,
|
|
18023
|
+
expiresAt,
|
|
18024
|
+
recipientEmail: opts.recipient ?? null
|
|
18025
|
+
});
|
|
18026
|
+
if (!issued.ok) refuse(opts, issued);
|
|
18027
|
+
emitData(opts, issued.value, () => {
|
|
18028
|
+
process.stdout.write(`${issued.value.url}
|
|
18029
|
+
|
|
18030
|
+
`);
|
|
18031
|
+
process.stdout.write(" Anyone with this link can read this one publication and its evidence.\n");
|
|
18032
|
+
process.stdout.write(" It is not tied to a person and needs no account.\n");
|
|
18033
|
+
process.stdout.write(
|
|
18034
|
+
issued.value.expiresAt === null ? " It does not expire. Revoke it with --revoke when you are done.\n" : ` It stops working at ${issued.value.expiresAt}.
|
|
18035
|
+
`
|
|
18036
|
+
);
|
|
18037
|
+
process.stdout.write(" This is the only time the link is shown \u2014 only its digest is stored.\n");
|
|
18038
|
+
});
|
|
18039
|
+
}
|
|
18040
|
+
function resolveExpiry(opts) {
|
|
18041
|
+
if (opts.expires === void 0) return null;
|
|
18042
|
+
const days = Number(opts.expires);
|
|
18043
|
+
if (Number.isInteger(days) && days > 0) {
|
|
18044
|
+
return new Date(Date.now() + days * 24 * 60 * 60 * 1e3).toISOString().replace(/\.\d{3}Z$/, (m) => m);
|
|
18045
|
+
}
|
|
18046
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(opts.expires)) return opts.expires;
|
|
18047
|
+
fail(opts, ExitCode.InputValidation, {
|
|
18048
|
+
error: `--expires takes a number of days (e.g. 7) or an ISO-8601 UTC timestamp with milliseconds \u2014 got ${JSON.stringify(opts.expires)}`,
|
|
18049
|
+
code: "bad-expiry",
|
|
18050
|
+
remediation: "Try `--expires 7` for a week, or omit it for a link that does not expire."
|
|
18051
|
+
});
|
|
18052
|
+
}
|
|
18053
|
+
function refuse(opts, sent) {
|
|
18054
|
+
fail(opts, sent.status === 401 ? ExitCode.Auth : ExitCode.General, {
|
|
18055
|
+
error: sent.refusal,
|
|
18056
|
+
code: "share-refused",
|
|
18057
|
+
remediation: sent.status === 404 ? "Check the publication id \u2014 only its owner can share it." : `Fix what is named above and run \`${tendrilCommand(`share ${opts.publicationId}`)}\` again.`
|
|
18058
|
+
});
|
|
18059
|
+
}
|
|
18060
|
+
var init_share = __esm({
|
|
18061
|
+
"packages/cli/src/commands/share.ts"() {
|
|
18062
|
+
"use strict";
|
|
18063
|
+
init_src3();
|
|
18064
|
+
init_invocation();
|
|
18065
|
+
init_output();
|
|
18066
|
+
init_publish_client();
|
|
18067
|
+
}
|
|
18068
|
+
});
|
|
18069
|
+
|
|
18070
|
+
// packages/cli/src/commands/publish.ts
|
|
18071
|
+
var publish_exports = {};
|
|
18072
|
+
__export(publish_exports, {
|
|
18073
|
+
runPublish: () => runPublish
|
|
18074
|
+
});
|
|
18075
|
+
import { existsSync as existsSync42, readFileSync as readFileSync39 } from "node:fs";
|
|
18076
|
+
import path52 from "node:path";
|
|
18077
|
+
async function runPublish(opts) {
|
|
18078
|
+
const bundleDir = path52.resolve(opts.bundleDir);
|
|
18079
|
+
const bundle = readBundle(opts, bundleDir);
|
|
18080
|
+
const report = bundle.report;
|
|
18081
|
+
if (typeof report["rulerExit"] !== "number" || !Number.isInteger(report["rulerExit"])) {
|
|
18082
|
+
fail(opts, ExitCode.InputValidation, {
|
|
18083
|
+
error: `${VERIFY_REPORT_FILENAME} does not state the ruler's exit code, and an absent exit code is not a passing one`,
|
|
18084
|
+
code: "report-has-no-exit-code",
|
|
18085
|
+
remediation: `Re-run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` with a current CLI \u2014 this report predates the field.`
|
|
18086
|
+
});
|
|
18087
|
+
}
|
|
18088
|
+
if (report["rulerExit"] !== 0) {
|
|
18089
|
+
const why = typeof report["rulerRefusal"] === "string" ? ` \u2014 ${report["rulerRefusal"]}` : "";
|
|
18090
|
+
fail(opts, ExitCode.VerificationFailed, {
|
|
18091
|
+
error: `the ruler refused this run (exit ${String(report["rulerExit"])}), so it has nothing to publish${why}`,
|
|
18092
|
+
code: "run-refused-by-ruler",
|
|
18093
|
+
remediation: `Fix what the report names, re-run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` until it passes, then publish. A declined verdict never becomes a page.`
|
|
18094
|
+
});
|
|
18095
|
+
}
|
|
18096
|
+
const undisclosed = undisclosedTrustFacts(report);
|
|
18097
|
+
if (undisclosed.length > 0) {
|
|
18098
|
+
fail(opts, ExitCode.InputValidation, {
|
|
18099
|
+
error: `this report carries a disclosure a published page cannot yet show, and a verdict shown without it is worse than no page: ${undisclosed.map((f) => `${f.pointer} \u2014 ${f.consequence}`).join("; ")}`,
|
|
18100
|
+
code: "report-carries-an-unrenderable-disclosure",
|
|
18101
|
+
remediation: "Resolve what the disclosure names \u2014 re-record so the set matches the bundle's stamp, or resolve the substituted font families \u2014 then re-verify and publish the clean run."
|
|
18102
|
+
});
|
|
18103
|
+
}
|
|
18104
|
+
if (scoredRecordingSetHash(report) === void 0) {
|
|
18105
|
+
fail(opts, ExitCode.InputValidation, {
|
|
18106
|
+
error: "this report does not name the recording set these scores were measured against",
|
|
18107
|
+
code: "report-names-no-recording-set",
|
|
18108
|
+
remediation: `Re-run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` with a current CLI \u2014 the set identity rides the report.`
|
|
18109
|
+
});
|
|
18110
|
+
}
|
|
18111
|
+
const componentName = (opts.name ?? bundle.manifest.name).trim();
|
|
18112
|
+
const surface = classifyBundleSurface(bundle.files, { entry: bundle.manifest.entry });
|
|
18113
|
+
if (surface.unknown.length > 0) {
|
|
18114
|
+
fail(opts, ExitCode.InputValidation, {
|
|
18115
|
+
error: `this bundle carries files a portal does not recognise, and publishing guesses at nothing: ${surface.unknown.join(", ")}`,
|
|
18116
|
+
code: "bundle-carries-unrecognised-files",
|
|
18117
|
+
remediation: `Remove them from ${opts.bundleDir}, or re-emit the bundle. Publishing them would ship something nobody reviewed; dropping them silently would ship evidence with a hole in it.`
|
|
18118
|
+
});
|
|
18119
|
+
}
|
|
18120
|
+
const sheetEntry = surface.published.find((p) => p.role === "inspect-sheet");
|
|
18121
|
+
if (sheetEntry !== void 0) {
|
|
18122
|
+
const missingCrops = missingInspectCrops(
|
|
18123
|
+
readFileSync39(path52.join(bundleDir, sheetEntry.path), "utf8"),
|
|
18124
|
+
surface.published.map((p) => p.path)
|
|
18125
|
+
);
|
|
18126
|
+
if (missingCrops.length > 0) {
|
|
18127
|
+
fail(opts, ExitCode.InputValidation, {
|
|
18128
|
+
error: `this bundle's inspect sheet references images it does not carry, and half a sheet reads as a broken component: ${missingCrops.join(", ")}`,
|
|
18129
|
+
code: "inspect-sheet-without-its-crops",
|
|
18130
|
+
remediation: `Re-run \`${tendrilCommand(`inspect ${opts.bundleDir}`)}\` to rebuild the sheet and its crops together, or delete the sheet and publish without it.`
|
|
18131
|
+
});
|
|
18132
|
+
}
|
|
18133
|
+
}
|
|
18134
|
+
if (surface.missingRequired.length > 0) {
|
|
18135
|
+
fail(opts, ExitCode.InputValidation, {
|
|
18136
|
+
error: `this bundle is missing what a publishable bundle cannot be without: ${surface.missingRequired.join(", ")}`,
|
|
18137
|
+
code: "bundle-missing-required-role",
|
|
18138
|
+
remediation: `Run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` \u2014 the evidence a page shows is written by that run. Required: ${REQUIRED_ROLES.join(", ")}.`
|
|
18139
|
+
});
|
|
18140
|
+
}
|
|
18141
|
+
const recorded = readScoredFiles(report);
|
|
18142
|
+
if (recorded === void 0) {
|
|
18143
|
+
fail(opts, ExitCode.InputValidation, {
|
|
18144
|
+
error: `${VERIFY_REPORT_FILENAME} does not record the files this run was scored beside, so nothing ties this bundle's bytes to its verdict`,
|
|
18145
|
+
code: "report-records-no-file-digests",
|
|
18146
|
+
remediation: `Re-run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` with a current CLI \u2014 this report predates the field.`
|
|
18147
|
+
});
|
|
18148
|
+
} else {
|
|
18149
|
+
const delta = compareScoredFiles(recorded, digestScoredFiles(bundleDir, bundle.manifest.entry));
|
|
18150
|
+
const disagreements = [
|
|
18151
|
+
...delta.changed.map((p) => `${p} (bytes differ from the scored run)`),
|
|
18152
|
+
...delta.missing.map((p) => `${p} (scored, no longer in the bundle)`),
|
|
18153
|
+
...delta.unscored.map((p) => `${p} (in the bundle, never scored)`)
|
|
18154
|
+
];
|
|
18155
|
+
if (disagreements.length > 0) {
|
|
18156
|
+
fail(opts, ExitCode.InputValidation, {
|
|
18157
|
+
error: `this bundle is not the one the ruler scored: ${disagreements.join("; ")}`,
|
|
18158
|
+
code: "bundle-disagrees-with-scored-files",
|
|
18159
|
+
remediation: `Re-run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` so the verdict describes these bytes. A page must never show code the ruler never saw.`
|
|
18160
|
+
});
|
|
18161
|
+
}
|
|
18162
|
+
}
|
|
18163
|
+
const figmaFile = bundle.manifest.provenance.recordingSet.figmaFile ?? null;
|
|
18164
|
+
if (opts.dryRun) {
|
|
18165
|
+
emitData(
|
|
18166
|
+
opts,
|
|
18167
|
+
{
|
|
18168
|
+
bundleDir,
|
|
18169
|
+
componentName,
|
|
18170
|
+
figmaFile,
|
|
18171
|
+
entry: bundle.manifest.entry,
|
|
18172
|
+
publishes: surface.published,
|
|
18173
|
+
excluded: surface.excluded,
|
|
18174
|
+
rulerVersion: report["environment"]?.["ruler"] ?? null,
|
|
18175
|
+
wouldPublish: true
|
|
18176
|
+
},
|
|
18177
|
+
() => {
|
|
18178
|
+
process.stdout.write(`${componentName} would publish ${String(surface.published.length)} files:
|
|
18179
|
+
`);
|
|
18180
|
+
for (const entry of surface.published) process.stdout.write(` ${entry.path} (${entry.role})
|
|
18181
|
+
`);
|
|
18182
|
+
for (const left of surface.excluded) process.stdout.write(` \u2014 leaving ${left.path} behind (${left.reason})
|
|
18183
|
+
`);
|
|
18184
|
+
}
|
|
18185
|
+
);
|
|
18186
|
+
return;
|
|
18187
|
+
}
|
|
18188
|
+
const origin = (opts.to ?? process.env["TENDRIL_PORTAL_URL"] ?? "").replace(/\/+$/, "");
|
|
18189
|
+
const client = opts.client ?? httpClient(opts, origin);
|
|
18190
|
+
if (opts.confirmPublish === true && process.stdin.isTTY !== true) {
|
|
18191
|
+
fail(opts, ExitCode.ConfirmationRequired, {
|
|
18192
|
+
error: "--confirm-publish requires an interactive terminal \u2014 putting a component on the internet is a human-only decision",
|
|
18193
|
+
code: "publish-confirmation-not-interactive",
|
|
18194
|
+
remediation: `A human runs \`${tendrilCommand(`publish ${opts.bundleDir} --confirm-publish`)}\` in their own terminal. Agents: show this to your operator instead of confirming it.`
|
|
18195
|
+
});
|
|
18196
|
+
}
|
|
18197
|
+
if (opts.acceptTerms === true) {
|
|
18198
|
+
const accepted = await client.acceptTerms({ figmaFile });
|
|
18199
|
+
if (!accepted.ok) refuse2(opts, accepted, "accept-terms-refused");
|
|
18200
|
+
}
|
|
18201
|
+
const opened = await client.begin({
|
|
18202
|
+
componentName,
|
|
18203
|
+
figmaFile,
|
|
18204
|
+
entry: bundle.manifest.entry,
|
|
18205
|
+
files: bundle.files,
|
|
18206
|
+
report: bundle.reportText,
|
|
18207
|
+
confirmed: opts.confirmPublish === true
|
|
18208
|
+
});
|
|
18209
|
+
if (!opened.ok) {
|
|
18210
|
+
if (opened.needsConsent !== void 0) {
|
|
18211
|
+
fail(opts, ExitCode.ConfirmationRequired, {
|
|
18212
|
+
error: `publishing this design system needs an acceptance of the publishing terms (version ${opened.needsConsent.termsVersion}) first`,
|
|
18213
|
+
code: "publishing-terms-not-accepted",
|
|
18214
|
+
remediation: `Read the terms, then run \`${tendrilCommand(`publish ${opts.bundleDir} --accept-terms --confirm-publish`)}\`. The acceptance covers this design system only.`
|
|
18215
|
+
});
|
|
18216
|
+
}
|
|
18217
|
+
if (opened.needsConfirmation !== void 0) {
|
|
18218
|
+
fail(opts, ExitCode.ConfirmationRequired, {
|
|
18219
|
+
error: `publishing ${JSON.stringify(opened.needsConfirmation.componentName)} for the first time is a decision a person makes, not a build step`,
|
|
18220
|
+
code: "first-publish-unconfirmed",
|
|
18221
|
+
remediation: `A human runs \`${tendrilCommand(`publish ${opts.bundleDir} --confirm-publish`)}\` in their own terminal. Later publishes of this component need no flag.`
|
|
18222
|
+
});
|
|
18223
|
+
}
|
|
18224
|
+
refuse2(opts, opened, "publish-refused");
|
|
18225
|
+
}
|
|
18226
|
+
const uploaded = [];
|
|
18227
|
+
for (const object of opened.value.plan.objects) {
|
|
18228
|
+
const file = path52.join(bundleDir, object.relPath);
|
|
18229
|
+
if (!existsSync42(file)) {
|
|
18230
|
+
fail(opts, ExitCode.InputValidation, {
|
|
18231
|
+
error: `the portal expects ${object.relPath}, and it is not in ${opts.bundleDir}`,
|
|
18232
|
+
code: "planned-file-missing",
|
|
18233
|
+
remediation: `Re-run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` and publish again \u2014 a publication is refused rather than going live with a hole in its evidence.`
|
|
18234
|
+
});
|
|
18235
|
+
}
|
|
18236
|
+
const sent = await client.upload({
|
|
18237
|
+
publicationId: opened.value.publicationId,
|
|
18238
|
+
relPath: object.relPath,
|
|
18239
|
+
bytes: new Uint8Array(readFileSync39(file))
|
|
18240
|
+
});
|
|
18241
|
+
if (!sent.ok) refuse2(opts, sent, "upload-refused");
|
|
18242
|
+
uploaded.push({ relPath: object.relPath, deduplicated: sent.value.deduplicated });
|
|
18243
|
+
}
|
|
18244
|
+
const committed = await client.commit({ publicationId: opened.value.publicationId });
|
|
18245
|
+
if (!committed.ok) {
|
|
18246
|
+
if (committed.missing !== void 0 && committed.missing.length > 0) {
|
|
18247
|
+
fail(opts, ExitCode.General, {
|
|
18248
|
+
error: `this publication is missing objects the bundle declared, and a verdict beside missing evidence is worse than no page: ${committed.missing.join(", ")}`,
|
|
18249
|
+
code: "publication-incomplete",
|
|
18250
|
+
remediation: `Run \`${tendrilCommand(`publish ${opts.bundleDir}`)}\` again \u2014 it rejoins this same unfinished publication and re-sends only what is missing.`
|
|
18251
|
+
});
|
|
18252
|
+
}
|
|
18253
|
+
refuse2(opts, committed, "commit-refused");
|
|
18254
|
+
}
|
|
18255
|
+
emitData(
|
|
18256
|
+
opts,
|
|
18257
|
+
{
|
|
18258
|
+
publicationId: committed.value.publicationId,
|
|
18259
|
+
url: committed.value.url,
|
|
18260
|
+
componentName,
|
|
18261
|
+
figmaFile: opened.value.figmaFile,
|
|
18262
|
+
rulerVersion: opened.value.rulerVersion,
|
|
18263
|
+
resumed: opened.value.resumed === true,
|
|
18264
|
+
files: uploaded
|
|
18265
|
+
},
|
|
18266
|
+
() => {
|
|
18267
|
+
const reused = uploaded.filter((u) => u.deduplicated).length;
|
|
18268
|
+
if (opened.value.resumed === true) process.stdout.write(`resumed the unfinished publish of ${componentName}
|
|
18269
|
+
`);
|
|
18270
|
+
process.stdout.write(`published ${componentName} \u2014 ${String(uploaded.length)} files`);
|
|
18271
|
+
process.stdout.write(reused > 0 ? ` (${String(reused)} you already had)
|
|
18272
|
+
` : "\n");
|
|
18273
|
+
process.stdout.write(` ${committed.value.url}
|
|
18274
|
+
`);
|
|
18275
|
+
process.stdout.write(` verdict as scored by ruler ${opened.value.rulerVersion}
|
|
18276
|
+
`);
|
|
18277
|
+
}
|
|
18278
|
+
);
|
|
18279
|
+
}
|
|
18280
|
+
function readBundle(opts, bundleDir) {
|
|
18281
|
+
const manifestPath2 = path52.join(bundleDir, "component.json");
|
|
18282
|
+
const reportPath = path52.join(bundleDir, EVIDENCE_DIR, VERIFY_REPORT_FILENAME);
|
|
18283
|
+
if (!existsSync42(manifestPath2)) {
|
|
18284
|
+
fail(opts, ExitCode.InputValidation, {
|
|
18285
|
+
error: `there is no component.json in ${opts.bundleDir}, so this is not a bundle`,
|
|
18286
|
+
code: "not-a-bundle",
|
|
18287
|
+
remediation: `Point publish at a directory a Tendril run produced. \`${tendrilCommand("generate --help")}\` shows how one is made.`
|
|
18288
|
+
});
|
|
18289
|
+
}
|
|
18290
|
+
if (!existsSync42(reportPath)) {
|
|
18291
|
+
fail(opts, ExitCode.InputValidation, {
|
|
18292
|
+
error: `this bundle has never been verified \u2014 there is no ${EVIDENCE_DIR}/${VERIFY_REPORT_FILENAME}`,
|
|
18293
|
+
code: "bundle-not-verified",
|
|
18294
|
+
remediation: `Run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` first. Verification is free and needs no account; publishing without it would put a page up with no verdict on it.`
|
|
18295
|
+
});
|
|
18296
|
+
}
|
|
18297
|
+
const { manifest } = readBundleManifest(readFileSync39(manifestPath2, "utf8"));
|
|
18298
|
+
if (manifest === void 0) {
|
|
18299
|
+
fail(opts, ExitCode.InputValidation, {
|
|
18300
|
+
error: "component.json did not parse as a bundle manifest",
|
|
18301
|
+
code: "no-mount-contract",
|
|
18302
|
+
remediation: `Re-emit the bundle \u2014 \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` rewrites component.json \u2014 then publish.`
|
|
18303
|
+
});
|
|
18304
|
+
}
|
|
18305
|
+
const reportText = readFileSync39(reportPath, "utf8");
|
|
18306
|
+
let report;
|
|
18307
|
+
try {
|
|
18308
|
+
report = JSON.parse(reportText);
|
|
18309
|
+
} catch (error) {
|
|
18310
|
+
fail(opts, ExitCode.InputValidation, {
|
|
18311
|
+
error: `${EVIDENCE_DIR}/${VERIFY_REPORT_FILENAME} is not parseable JSON: ${error.message}`,
|
|
18312
|
+
code: "report-unparseable",
|
|
18313
|
+
remediation: `Re-run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` \u2014 that file is written by the ruler and should never be edited by hand.`
|
|
18314
|
+
});
|
|
18315
|
+
}
|
|
18316
|
+
if (report === null || typeof report !== "object" || Array.isArray(report)) {
|
|
18317
|
+
fail(opts, ExitCode.InputValidation, {
|
|
18318
|
+
error: `${EVIDENCE_DIR}/${VERIFY_REPORT_FILENAME} parses, but a verify report is a JSON object and this is not one`,
|
|
18319
|
+
code: "report-not-an-object",
|
|
18320
|
+
remediation: `Re-run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\`.`
|
|
18321
|
+
});
|
|
18322
|
+
}
|
|
18323
|
+
return { manifest, report, reportText, files: bundleFiles(bundleDir) };
|
|
18324
|
+
}
|
|
18325
|
+
function httpClient(opts, origin) {
|
|
18326
|
+
if (origin === "") {
|
|
18327
|
+
fail(opts, ExitCode.InputValidation, {
|
|
18328
|
+
error: "no portal to publish to",
|
|
18329
|
+
code: "no-portal-configured",
|
|
18330
|
+
remediation: `Pass \`--to <url>\` or set TENDRIL_PORTAL_URL. \`${tendrilCommand(`publish ${opts.bundleDir} --dry-run`)}\` shows exactly what would be published without needing one.`
|
|
18331
|
+
});
|
|
18332
|
+
}
|
|
18333
|
+
if (!isSecureOrigin(origin)) {
|
|
18334
|
+
fail(opts, ExitCode.InputValidation, {
|
|
18335
|
+
error: `${origin} is not https, so a session token sent there would travel in the clear`,
|
|
18336
|
+
code: "portal-not-https",
|
|
18337
|
+
remediation: "Use the https:// address of your portal. Only 127.0.0.1 and localhost are exempt, for local development."
|
|
18338
|
+
});
|
|
18339
|
+
}
|
|
18340
|
+
const found = tokenFor(origin);
|
|
18341
|
+
if (!found.ok && found.reason === "origin-mismatch") {
|
|
18342
|
+
fail(opts, ExitCode.Auth, {
|
|
18343
|
+
error: `the session available here belongs to ${found.boundTo}, and this would publish to ${origin}`,
|
|
18344
|
+
code: "session-belongs-to-another-portal",
|
|
18345
|
+
remediation: found.from === "env" ? `TENDRIL_TOKEN is pinned to TENDRIL_PORTAL_URL (${found.boundTo}). Publish to that portal, or set both to ${origin} together \u2014 a token is a credential for one host.` : `Sign in to ${origin}. The stored session is for ${found.boundTo}, and sending it elsewhere would hand that host a live credential.`
|
|
18346
|
+
});
|
|
18347
|
+
}
|
|
18348
|
+
if (!found.ok) {
|
|
18349
|
+
fail(opts, ExitCode.Auth, {
|
|
18350
|
+
error: `no session for ${origin}`,
|
|
18351
|
+
code: "not-signed-in",
|
|
18352
|
+
remediation: `Run \`${tendrilCommand(`login --to ${origin}`)}\` and paste the token your portal operator gave you (or set TENDRIL_TOKEN and TENDRIL_PORTAL_URL together for this shell). Verification stays free and account-less; only publishing needs an account.`
|
|
18353
|
+
});
|
|
18354
|
+
}
|
|
18355
|
+
return new HttpPublishClient({ origin, token: found.token });
|
|
18356
|
+
}
|
|
18357
|
+
function refuse2(opts, sent, code) {
|
|
18358
|
+
const detail = sent.detail === void 0 || sent.detail.length === 0 ? "" : `: ${sent.detail.join(", ")}`;
|
|
18359
|
+
fail(opts, sent.status === 401 ? ExitCode.Auth : ExitCode.General, {
|
|
18360
|
+
error: `${sent.refusal}${detail}`,
|
|
18361
|
+
code,
|
|
18362
|
+
remediation: sent.status === 401 ? `That session is no longer valid. Run \`${tendrilCommand(`login --to ${(opts.to ?? process.env["TENDRIL_PORTAL_URL"] ?? "").replace(/\/+$/, "")}`)}\` and paste a fresh token.` : sent.status >= 500 ? `The portal failed on its side. Quote the error id above to whoever runs it, then run \`${tendrilCommand(`publish ${opts.bundleDir}`)}\` again \u2014 a retry rejoins this same unfinished publication.` : `Fix what is named above and run \`${tendrilCommand(`publish ${opts.bundleDir}`)}\` again \u2014 it rejoins this same unfinished publication rather than starting a second one.`
|
|
18363
|
+
});
|
|
18364
|
+
}
|
|
18365
|
+
var init_publish = __esm({
|
|
18366
|
+
"packages/cli/src/commands/publish.ts"() {
|
|
18367
|
+
"use strict";
|
|
18368
|
+
init_src3();
|
|
18369
|
+
init_src4();
|
|
18370
|
+
init_invocation();
|
|
18371
|
+
init_output();
|
|
18372
|
+
init_publish_client();
|
|
18373
|
+
}
|
|
18374
|
+
});
|
|
18375
|
+
|
|
17035
18376
|
// packages/cli/src/commands/generate-route.ts
|
|
17036
18377
|
var generate_route_exports = {};
|
|
17037
18378
|
__export(generate_route_exports, {
|
|
@@ -17055,18 +18396,18 @@ var generate_recorded_exports = {};
|
|
|
17055
18396
|
__export(generate_recorded_exports, {
|
|
17056
18397
|
runGenerateRecorded: () => runGenerateRecorded
|
|
17057
18398
|
});
|
|
17058
|
-
import { confirm as confirm3, isCancel as
|
|
17059
|
-
import { existsSync as
|
|
17060
|
-
import
|
|
18399
|
+
import { confirm as confirm3, isCancel as isCancel4 } from "@clack/prompts";
|
|
18400
|
+
import { existsSync as existsSync43, readFileSync as readFileSync40 } from "node:fs";
|
|
18401
|
+
import path53 from "node:path";
|
|
17061
18402
|
async function runGenerateRecorded(opts) {
|
|
17062
18403
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
17063
|
-
const outDirAbs =
|
|
17064
|
-
const recordedAsPath =
|
|
18404
|
+
const outDirAbs = path53.resolve(callerCwd, opts.out);
|
|
18405
|
+
const recordedAsPath = path53.resolve(callerCwd, opts.recorded);
|
|
17065
18406
|
let task;
|
|
17066
18407
|
let taskName;
|
|
17067
18408
|
let authoredApi;
|
|
17068
18409
|
let composition;
|
|
17069
|
-
const isSet =
|
|
18410
|
+
const isSet = existsSync43(path53.join(recordedAsPath, "recording-set.json"));
|
|
17070
18411
|
const registry = TASKS[opts.recorded];
|
|
17071
18412
|
if (registry !== void 0 && !isSet) {
|
|
17072
18413
|
task = registry;
|
|
@@ -17075,7 +18416,7 @@ async function runGenerateRecorded(opts) {
|
|
|
17075
18416
|
try {
|
|
17076
18417
|
const authored = authorTaskFromSet(recordedAsPath);
|
|
17077
18418
|
task = authored.task;
|
|
17078
|
-
taskName =
|
|
18419
|
+
taskName = path53.basename(recordedAsPath);
|
|
17079
18420
|
authoredApi = authored.api;
|
|
17080
18421
|
const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
|
|
17081
18422
|
if (roles.success) composition = roles.data;
|
|
@@ -17109,7 +18450,7 @@ async function runGenerateRecorded(opts) {
|
|
|
17109
18450
|
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)`);
|
|
17110
18451
|
}
|
|
17111
18452
|
const missing = task.configs.filter(
|
|
17112
|
-
(c) => !
|
|
18453
|
+
(c) => !existsSync43(path53.join(task.set, c.rep, "get_screenshot.json")) || !existsSync43(path53.join(task.set, c.rep, "get_metadata.json")) || !existsSync43(path53.join(task.set, c.rep, "get_design_context.json"))
|
|
17113
18454
|
);
|
|
17114
18455
|
if (missing.length > 0) {
|
|
17115
18456
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -17179,8 +18520,8 @@ async function runGenerateRecorded(opts) {
|
|
|
17179
18520
|
` : `${line}
|
|
17180
18521
|
`);
|
|
17181
18522
|
if (opts.dryRun) {
|
|
17182
|
-
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite:
|
|
17183
|
-
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${
|
|
18523
|
+
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path53.join(outDirAbs, taskName) }, () => {
|
|
18524
|
+
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path53.join(outDirAbs, taskName)})
|
|
17184
18525
|
`);
|
|
17185
18526
|
});
|
|
17186
18527
|
return;
|
|
@@ -17195,7 +18536,7 @@ async function runGenerateRecorded(opts) {
|
|
|
17195
18536
|
});
|
|
17196
18537
|
}
|
|
17197
18538
|
const accepted = await confirm3({ message: `Proceed? (cap $${opts.capUsd.toFixed(2)}, ${opts.maxIterations} iterations max)` });
|
|
17198
|
-
if (
|
|
18539
|
+
if (isCancel4(accepted) || accepted !== true) {
|
|
17199
18540
|
fail(opts, ExitCode.ConfirmationRequired, {
|
|
17200
18541
|
error: "Run was not confirmed.",
|
|
17201
18542
|
code: "CONFIRMATION_DECLINED",
|
|
@@ -17203,10 +18544,10 @@ async function runGenerateRecorded(opts) {
|
|
|
17203
18544
|
});
|
|
17204
18545
|
}
|
|
17205
18546
|
}
|
|
17206
|
-
const bundleDir =
|
|
17207
|
-
if (
|
|
18547
|
+
const bundleDir = path53.join(outDirAbs, taskName);
|
|
18548
|
+
if (existsSync43(path53.join(bundleDir, "component.json"))) {
|
|
17208
18549
|
try {
|
|
17209
|
-
const prior = readBundleManifest(
|
|
18550
|
+
const prior = readBundleManifest(readFileSync40(path53.join(bundleDir, "component.json"), "utf8")).manifest;
|
|
17210
18551
|
if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
|
|
17211
18552
|
fail(opts, ExitCode.InputValidation, {
|
|
17212
18553
|
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`,
|
|
@@ -17373,7 +18714,7 @@ init_invocation();
|
|
|
17373
18714
|
init_output();
|
|
17374
18715
|
import { intro, isCancel, outro, password } from "@clack/prompts";
|
|
17375
18716
|
import fs from "node:fs";
|
|
17376
|
-
import
|
|
18717
|
+
import path29 from "node:path";
|
|
17377
18718
|
var INIT_DESCRIPTION = {
|
|
17378
18719
|
name: "init",
|
|
17379
18720
|
summary: "Configure the OpenRouter credential in .env, and optionally a Figma token (idempotent).",
|
|
@@ -17415,7 +18756,7 @@ async function runInit(flags) {
|
|
|
17415
18756
|
printDescription(INIT_DESCRIPTION);
|
|
17416
18757
|
return;
|
|
17417
18758
|
}
|
|
17418
|
-
const envPath =
|
|
18759
|
+
const envPath = path29.resolve(process.cwd(), ".env");
|
|
17419
18760
|
const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
|
|
17420
18761
|
let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
|
|
17421
18762
|
let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
|
|
@@ -17436,7 +18777,7 @@ async function runInit(flags) {
|
|
|
17436
18777
|
if (openrouterKey) next.set(ENV_KEYS.openrouter, openrouterKey);
|
|
17437
18778
|
if (figmaToken) next.set(ENV_KEYS.figma, figmaToken);
|
|
17438
18779
|
const changed = [...next].some(([key, value]) => existing.get(key) !== value);
|
|
17439
|
-
const gitignorePath =
|
|
18780
|
+
const gitignorePath = path29.resolve(process.cwd(), ".gitignore");
|
|
17440
18781
|
const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
17441
18782
|
const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
|
|
17442
18783
|
if (flags.dryRun) {
|
|
@@ -17492,14 +18833,14 @@ init_invocation();
|
|
|
17492
18833
|
init_output();
|
|
17493
18834
|
init_entitlement();
|
|
17494
18835
|
import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
|
|
17495
|
-
import { readFileSync as
|
|
18836
|
+
import { readFileSync as readFileSync21, readdirSync as readdirSync9, existsSync as existsSync23 } from "node:fs";
|
|
17496
18837
|
|
|
17497
18838
|
// packages/cli/src/pipeline.ts
|
|
17498
18839
|
init_src2();
|
|
17499
18840
|
init_src5();
|
|
17500
18841
|
init_src4();
|
|
17501
|
-
import { mkdirSync as
|
|
17502
|
-
import
|
|
18842
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync9 } from "node:fs";
|
|
18843
|
+
import path30 from "node:path";
|
|
17503
18844
|
|
|
17504
18845
|
// packages/cli/src/assets-module.ts
|
|
17505
18846
|
init_src();
|
|
@@ -17835,8 +19176,8 @@ async function runGenerationPipeline(input) {
|
|
|
17835
19176
|
});
|
|
17836
19177
|
const written = [];
|
|
17837
19178
|
if (!input.dryRun) {
|
|
17838
|
-
const dir =
|
|
17839
|
-
|
|
19179
|
+
const dir = path30.resolve(input.outDir, semantics.componentName);
|
|
19180
|
+
mkdirSync6(dir, { recursive: true });
|
|
17840
19181
|
const files = {
|
|
17841
19182
|
// Bundle-local tokens: THE emission the component's CSS resolves
|
|
17842
19183
|
// against — the same artifact the verify harness injects. A preview
|
|
@@ -17859,14 +19200,14 @@ async function runGenerationPipeline(input) {
|
|
|
17859
19200
|
`
|
|
17860
19201
|
};
|
|
17861
19202
|
for (const [name, content] of Object.entries(files)) {
|
|
17862
|
-
const filePath =
|
|
17863
|
-
|
|
19203
|
+
const filePath = path30.join(dir, name);
|
|
19204
|
+
writeFileSync9(filePath, content);
|
|
17864
19205
|
written.push(filePath);
|
|
17865
19206
|
}
|
|
17866
19207
|
for (const artifact of emitTokenArtifacts(input.mapping)) {
|
|
17867
|
-
const filePath =
|
|
17868
|
-
|
|
17869
|
-
|
|
19208
|
+
const filePath = path30.resolve(input.outDir, artifact.path);
|
|
19209
|
+
mkdirSync6(path30.dirname(filePath), { recursive: true });
|
|
19210
|
+
writeFileSync9(filePath, artifact.content);
|
|
17870
19211
|
written.push(filePath);
|
|
17871
19212
|
}
|
|
17872
19213
|
}
|
|
@@ -17924,7 +19265,7 @@ var GENERATE_DESCRIPTION = {
|
|
|
17924
19265
|
function resolveProvidedSource(flags, contextFile) {
|
|
17925
19266
|
let raw;
|
|
17926
19267
|
try {
|
|
17927
|
-
raw =
|
|
19268
|
+
raw = readFileSync21(contextFile, "utf8");
|
|
17928
19269
|
} catch {
|
|
17929
19270
|
fail(flags, ExitCode.InputValidation, {
|
|
17930
19271
|
error: `Cannot read context file "${contextFile}".`,
|
|
@@ -18044,11 +19385,11 @@ token mapping (${mapping.flat.length} variables):
|
|
|
18044
19385
|
let initialCode;
|
|
18045
19386
|
let initialSemantics;
|
|
18046
19387
|
try {
|
|
18047
|
-
if (
|
|
18048
|
-
for (const entry of
|
|
19388
|
+
if (existsSync23(flags.out)) {
|
|
19389
|
+
for (const entry of readdirSync9(flags.out)) {
|
|
18049
19390
|
const cjPath = `${flags.out}/${entry}/component.json`;
|
|
18050
|
-
if (!
|
|
18051
|
-
const cj = JSON.parse(
|
|
19391
|
+
if (!existsSync23(cjPath)) continue;
|
|
19392
|
+
const cj = JSON.parse(readFileSync21(cjPath, "utf8"));
|
|
18052
19393
|
if (cj.name !== void 0 && Array.isArray(cj.props)) {
|
|
18053
19394
|
previousApi = JSON.stringify({
|
|
18054
19395
|
componentName: cj.name,
|
|
@@ -18056,14 +19397,14 @@ token mapping (${mapping.flat.length} variables):
|
|
|
18056
19397
|
});
|
|
18057
19398
|
const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
|
|
18058
19399
|
const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
|
|
18059
|
-
if (flags.refine &&
|
|
19400
|
+
if (flags.refine && existsSync23(tsxPath) && existsSync23(cssPath)) {
|
|
18060
19401
|
initialCode = {
|
|
18061
|
-
tsx:
|
|
18062
|
-
css:
|
|
19402
|
+
tsx: readFileSync21(tsxPath, "utf8"),
|
|
19403
|
+
css: readFileSync21(cssPath, "utf8")
|
|
18063
19404
|
};
|
|
18064
19405
|
const semPath = `${flags.out}/${entry}/semantics.json`;
|
|
18065
|
-
if (
|
|
18066
|
-
initialSemantics = JSON.parse(
|
|
19406
|
+
if (existsSync23(semPath)) {
|
|
19407
|
+
initialSemantics = JSON.parse(readFileSync21(semPath, "utf8"));
|
|
18067
19408
|
}
|
|
18068
19409
|
}
|
|
18069
19410
|
break;
|
|
@@ -18458,6 +19799,50 @@ function buildProgram() {
|
|
|
18458
19799
|
...local["maxArea"] !== void 0 ? { maxArea: Number(local["maxArea"]) } : {}
|
|
18459
19800
|
});
|
|
18460
19801
|
});
|
|
19802
|
+
program.command("login").description("Sign this machine in to the Tendril portal (approve in your browser), so `publish` and `share` can reach it. Verification never needs this.").option("--to <url>", "the portal to sign in to (defaults to the hosted portal; or set TENDRIL_PORTAL_URL)").option("--paste", "paste a token minted on the portal's Connect page instead of approving in the browser").option("--device-start", "agents: start the browser approval and return the link and code immediately").option("--device-wait", "agents: wait for the browser approval started by --device-start").action(async (_o, cmd) => {
|
|
19803
|
+
const flags = globalFlags(cmd.parent);
|
|
19804
|
+
const local = cmd.opts();
|
|
19805
|
+
const { runLogin: runLogin2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
19806
|
+
await runLogin2({
|
|
19807
|
+
...flags,
|
|
19808
|
+
...local["to"] !== void 0 ? { to: local["to"] } : {},
|
|
19809
|
+
...local["paste"] !== void 0 ? { paste: local["paste"] } : {},
|
|
19810
|
+
...local["deviceStart"] !== void 0 ? { deviceStart: local["deviceStart"] } : {},
|
|
19811
|
+
...local["deviceWait"] !== void 0 ? { deviceWait: local["deviceWait"] } : {}
|
|
19812
|
+
});
|
|
19813
|
+
});
|
|
19814
|
+
program.command("logout").description("Forget this machine's stored portal session. Does not end the session itself.").action(async (_o, cmd) => {
|
|
19815
|
+
const flags = globalFlags(cmd.parent);
|
|
19816
|
+
const { runLogout: runLogout2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
19817
|
+
runLogout2(flags);
|
|
19818
|
+
});
|
|
19819
|
+
program.command("share").description("Create a link that opens one publication's verdict and evidence for someone with no account. Anyone holding the link can read it.").argument("<publicationId>", "the publication to share, as `publish` printed it").option("--to <url>", "the portal (or set TENDRIL_PORTAL_URL)").option("--expires <days|timestamp>", "a number of days, or an ISO-8601 UTC timestamp; omit for a link that does not expire").option("--recipient <email>", "recorded so you can see who a link was for; never used to authenticate").option("--list", "show the links outstanding for this publication").option("--revoke <shareLinkId>", "stop a link working").action(async (publicationId, _o, cmd) => {
|
|
19820
|
+
const flags = globalFlags(cmd.parent);
|
|
19821
|
+
const local = cmd.opts();
|
|
19822
|
+
const { runShare: runShare2 } = await Promise.resolve().then(() => (init_share(), share_exports));
|
|
19823
|
+
await runShare2({
|
|
19824
|
+
...flags,
|
|
19825
|
+
publicationId,
|
|
19826
|
+
...local["to"] !== void 0 ? { to: local["to"] } : {},
|
|
19827
|
+
...local["expires"] !== void 0 ? { expires: String(local["expires"]) } : {},
|
|
19828
|
+
...local["recipient"] !== void 0 ? { recipient: local["recipient"] } : {},
|
|
19829
|
+
...local["list"] !== void 0 ? { list: local["list"] } : {},
|
|
19830
|
+
...local["revoke"] !== void 0 ? { revoke: local["revoke"] } : {}
|
|
19831
|
+
});
|
|
19832
|
+
});
|
|
19833
|
+
program.command("publish").description("Put a verified bundle and its evidence on a page, at a URL you can send to someone. Refuses a run the ruler declined.").argument("<bundleDir>", "bundle directory that has already been verified").option("--to <url>", "the portal to publish to (or set TENDRIL_PORTAL_URL)").option("--name <name>", "library label for this component (default: the bundle manifest's own name)").option("--accept-terms", "accept the current publishing terms for this design system as part of this run").option("--confirm-publish", "you, a person, saying this component may be published \u2014 required the first time, and refused without an interactive terminal").action(async (bundleDir, _o, cmd) => {
|
|
19834
|
+
const flags = globalFlags(cmd.parent);
|
|
19835
|
+
const local = cmd.opts();
|
|
19836
|
+
const { runPublish: runPublish2 } = await Promise.resolve().then(() => (init_publish(), publish_exports));
|
|
19837
|
+
await runPublish2({
|
|
19838
|
+
...flags,
|
|
19839
|
+
bundleDir,
|
|
19840
|
+
...local["to"] !== void 0 ? { to: local["to"] } : {},
|
|
19841
|
+
...local["name"] !== void 0 ? { name: local["name"] } : {},
|
|
19842
|
+
...local["acceptTerms"] !== void 0 ? { acceptTerms: local["acceptTerms"] } : {},
|
|
19843
|
+
...local["confirmPublish"] !== void 0 ? { confirmPublish: local["confirmPublish"] } : {}
|
|
19844
|
+
});
|
|
19845
|
+
});
|
|
18461
19846
|
program.command("verify").description("Recompute verification for a generated bundle against its recording set (per-config status, behaviors, honest exit codes).").argument("<bundleDir>", "bundle directory to verify").option("--task <name>", "legacy task adapter for bundles WITHOUT component.json (bundle v1 carries its own prop manifest)").option("--set <dir>", "recording set directory (overrides the bundle's provenance path)").option("--bar <bar>", "target bar: pass (0.95) or cert (0.97)", "pass").option("--library <dir>", "workspace root holding confirmed partners generated bundles (ADR-013 composed pins; default: current directory)").option("--profile <file>", "a `tendril profile` artifact; reports whether the bundle followed your codebase conventions (never gates the verdict)").action(async (bundleDir, _opts, cmd) => {
|
|
18462
19847
|
const flags = globalFlags(cmd);
|
|
18463
19848
|
const local = cmd.opts();
|