@decantr/cli 2.3.1 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,11 +8,14 @@ import {
8
8
  } from "./chunk-IEW2QFYI.js";
9
9
 
10
10
  // src/commands/health.ts
11
+ import { createRequire } from "module";
11
12
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
12
- import { dirname, join } from "path";
13
+ import { dirname, isAbsolute, join, resolve } from "path";
13
14
  import { fileURLToPath } from "url";
14
15
  import {
15
- auditProject
16
+ auditProject,
17
+ createContractAssertions,
18
+ createEvidenceBundle
16
19
  } from "@decantr/verifier";
17
20
  var BOLD = "\x1B[1m";
18
21
  var DIM = "\x1B[2m";
@@ -120,16 +123,20 @@ function prefixArtifactPath(projectPath, artifactPath) {
120
123
  }
121
124
  function renderProjectHealthCiWorkflow(options = {}) {
122
125
  const failOn = normalizeHealthFailOn(options.failOn);
123
- const projectPath = validateProjectPath(options.projectPath);
126
+ const projectPath = options.workspace ? void 0 : validateProjectPath(options.projectPath);
124
127
  const reportPath = validateArtifactPath(
125
- options.reportPath || DEFAULT_HEALTH_CI_REPORT_PATH,
128
+ options.reportPath || (options.workspace ? ".decantr/workspace-health.md" : DEFAULT_HEALTH_CI_REPORT_PATH),
126
129
  "--report-path"
127
130
  );
128
- const jsonPath = validateArtifactPath(options.jsonPath || DEFAULT_HEALTH_CI_JSON_PATH, "--json-path");
131
+ const jsonPath = validateArtifactPath(
132
+ options.jsonPath || (options.workspace ? ".decantr/workspace-health.json" : DEFAULT_HEALTH_CI_JSON_PATH),
133
+ "--json-path"
134
+ );
129
135
  const template = loadHealthTemplate("decantr-health.workflow.yml.template");
130
136
  return renderTemplate(template, {
131
137
  CLI_PACKAGE: normalizeCliPackageSpecifier(options.cliVersion),
132
138
  FAIL_ON: failOn,
139
+ HEALTH_COMMAND: options.workspace ? "workspace health" : "health",
133
140
  PROJECT_WORKING_DIRECTORY: projectPath ? ` working-directory: ${projectPath}
134
141
  ` : "",
135
142
  REPORT_PATH: reportPath,
@@ -151,7 +158,7 @@ function writeProjectHealthCiWorkflow(projectRoot, options = {}) {
151
158
  }
152
159
  mkdirSync(dirname(workflowPath), { recursive: true });
153
160
  writeFileSync(workflowPath, renderProjectHealthCiWorkflow(options), "utf-8");
154
- const projectPath = validateProjectPath(options.projectPath);
161
+ const projectPath = options.workspace ? void 0 : validateProjectPath(options.projectPath);
155
162
  const result = {
156
163
  path: workflowRelativePath,
157
164
  created: !alreadyExists,
@@ -159,6 +166,7 @@ function writeProjectHealthCiWorkflow(projectRoot, options = {}) {
159
166
  failOn: normalizeHealthFailOn(options.failOn)
160
167
  };
161
168
  if (projectPath) result.projectPath = projectPath;
169
+ if (options.workspace) result.workspace = true;
162
170
  return result;
163
171
  }
164
172
  function collectDeclaredRoutes(essence) {
@@ -230,6 +238,12 @@ function commandsForFinding(source) {
230
238
  return ["npm run build", "decantr health"];
231
239
  case "interaction":
232
240
  return ["decantr check --strict", "decantr health"];
241
+ case "assertion":
242
+ return ["decantr refresh", "decantr health --evidence"];
243
+ case "browser":
244
+ return ["decantr health --browser", "decantr health --evidence"];
245
+ case "design-token":
246
+ return ["decantr export --to figma-tokens", "decantr health --evidence"];
233
247
  case "check":
234
248
  return ["decantr check", "decantr health"];
235
249
  default:
@@ -252,6 +266,7 @@ ${input.evidence.map((entry) => `- ${entry}`).join("\n")}` : null,
252
266
  input.suggestedFix ? `Suggested fix: ${input.suggestedFix}` : null,
253
267
  "",
254
268
  "Make the smallest coherent code or contract change that resolves this finding. Preserve the existing framework, routing, styling system, and Decantr workflow mode unless the finding explicitly requires a contract update.",
269
+ "Do not rewrite unrelated routes, replace the styling system, remove existing product behavior, or regenerate Decantr artifacts unless the finding is about stale or missing generated context.",
255
270
  "",
256
271
  `After the fix, run:
257
272
  ${input.commands.map((command) => `- ${command}`).join("\n")}`
@@ -316,7 +331,256 @@ function isDuplicateFinding(existing, finding) {
316
331
  existing.add(key);
317
332
  return false;
318
333
  }
319
- async function createProjectHealthReport(projectRoot = process.cwd()) {
334
+ function resolveOptionalPath(projectRoot, path) {
335
+ if (!path) return void 0;
336
+ return isAbsolute(path) ? path : resolve(projectRoot, path);
337
+ }
338
+ function hasProjectPlaywright(projectRoot) {
339
+ try {
340
+ const requireFromProject = createRequire(join(projectRoot, "package.json"));
341
+ requireFromProject.resolve("playwright");
342
+ return true;
343
+ } catch {
344
+ try {
345
+ const requireFromProject = createRequire(join(projectRoot, "package.json"));
346
+ requireFromProject.resolve("@playwright/test");
347
+ return true;
348
+ } catch {
349
+ return false;
350
+ }
351
+ }
352
+ }
353
+ function loadProjectPlaywright(projectRoot) {
354
+ const requireFromProject = createRequire(join(projectRoot, "package.json"));
355
+ for (const packageName of ["playwright", "@playwright/test"]) {
356
+ try {
357
+ const loaded = requireFromProject(packageName);
358
+ if (loaded.chromium?.launch) return loaded;
359
+ } catch {
360
+ }
361
+ }
362
+ return null;
363
+ }
364
+ function browserRouteUrl(baseUrl, route) {
365
+ return new URL(route || "/", baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
366
+ }
367
+ function browserScreenshotRelativePath(route) {
368
+ const name = slugify(route === "/" ? "root" : route) || "root";
369
+ return `.decantr/evidence/screenshots/${name}.png`;
370
+ }
371
+ async function collectBrowserVerification(projectRoot, options, declaredRoutes) {
372
+ if (!options.browser) return null;
373
+ if (!hasProjectPlaywright(projectRoot)) {
374
+ const finding = createHealthFinding({
375
+ source: "browser",
376
+ category: "Browser Verification",
377
+ severity: options.requireBrowser ? "error" : "warn",
378
+ message: "Browser verification was requested, but Playwright is not installed in this project.",
379
+ evidence: ["Expected dependency: playwright or @playwright/test"],
380
+ rule: "browser-playwright-missing",
381
+ suggestedFix: "Install Playwright in the project or rerun without `--browser` for static-only evidence.",
382
+ baseId: "playwright-missing"
383
+ });
384
+ return {
385
+ finding,
386
+ evidence: {
387
+ enabled: true,
388
+ status: "unavailable",
389
+ baseUrl: options.browserBaseUrl ?? null,
390
+ screenshots: [],
391
+ findings: [finding.message]
392
+ }
393
+ };
394
+ }
395
+ if (!options.browserBaseUrl) {
396
+ const finding = createHealthFinding({
397
+ source: "browser",
398
+ category: "Browser Verification",
399
+ severity: options.requireBrowser ? "error" : "warn",
400
+ message: "Browser verification was requested, but no base URL was provided for rendered route checks.",
401
+ evidence: ["Pass --base-url <url> or set DECANTR_BROWSER_BASE_URL."],
402
+ rule: "browser-base-url-missing",
403
+ suggestedFix: "Start the app and rerun with `decantr health --browser --base-url <url>`.",
404
+ baseId: "base-url-missing"
405
+ });
406
+ return {
407
+ finding,
408
+ evidence: {
409
+ enabled: true,
410
+ status: "unavailable",
411
+ baseUrl: null,
412
+ screenshots: [],
413
+ findings: [finding.message]
414
+ }
415
+ };
416
+ }
417
+ const playwright = loadProjectPlaywright(projectRoot);
418
+ if (!playwright) {
419
+ const finding = createHealthFinding({
420
+ source: "browser",
421
+ category: "Browser Verification",
422
+ severity: options.requireBrowser ? "error" : "warn",
423
+ message: "Playwright is installed, but Decantr could not load a Chromium browser adapter.",
424
+ evidence: ["Expected chromium.launch from playwright or @playwright/test."],
425
+ rule: "browser-adapter-missing",
426
+ suggestedFix: "Repair the local Playwright install and rerun `decantr health --browser`.",
427
+ baseId: "adapter-missing"
428
+ });
429
+ return {
430
+ finding,
431
+ evidence: {
432
+ enabled: true,
433
+ status: "unavailable",
434
+ baseUrl: options.browserBaseUrl,
435
+ screenshots: [],
436
+ findings: [finding.message]
437
+ }
438
+ };
439
+ }
440
+ const routes = (declaredRoutes.length > 0 ? declaredRoutes : ["/"]).slice(0, 12);
441
+ const screenshots = [];
442
+ const browserFindings = [];
443
+ const screenshotDir = join(projectRoot, ".decantr", "evidence", "screenshots");
444
+ mkdirSync(screenshotDir, { recursive: true });
445
+ let browser = null;
446
+ try {
447
+ browser = await playwright.chromium.launch({ headless: true });
448
+ const page = await browser.newPage();
449
+ for (const route of routes) {
450
+ const url = browserRouteUrl(options.browserBaseUrl, route);
451
+ const relativePath = browserScreenshotRelativePath(route);
452
+ try {
453
+ await page.goto(url, { waitUntil: "networkidle", timeout: 15e3 });
454
+ await page.screenshot({ path: join(projectRoot, relativePath), fullPage: true });
455
+ screenshots.push(relativePath);
456
+ } catch (error) {
457
+ browserFindings.push(`${route}: ${error.message}`);
458
+ }
459
+ }
460
+ } catch (error) {
461
+ browserFindings.push(error.message);
462
+ } finally {
463
+ if (browser) await browser.close();
464
+ }
465
+ if (browserFindings.length > 0) {
466
+ const finding = createHealthFinding({
467
+ source: "browser",
468
+ category: "Browser Verification",
469
+ severity: options.requireBrowser ? "error" : "warn",
470
+ message: "Browser verification could not render every declared route.",
471
+ evidence: browserFindings.slice(0, 5),
472
+ rule: "browser-route-verification-failed",
473
+ suggestedFix: "Start the app at the provided base URL, fix route render errors, and rerun `decantr health --browser --evidence`.",
474
+ baseId: "route-verification-failed"
475
+ });
476
+ return {
477
+ finding,
478
+ evidence: {
479
+ enabled: true,
480
+ status: "failed",
481
+ baseUrl: options.browserBaseUrl,
482
+ screenshots,
483
+ findings: browserFindings
484
+ }
485
+ };
486
+ }
487
+ return {
488
+ finding: null,
489
+ evidence: {
490
+ enabled: true,
491
+ status: "passed",
492
+ baseUrl: options.browserBaseUrl,
493
+ screenshots,
494
+ findings: []
495
+ }
496
+ };
497
+ }
498
+ function flattenDesignTokenKeys(value, prefix = "") {
499
+ const keys = /* @__PURE__ */ new Set();
500
+ if (!value || typeof value !== "object" || Array.isArray(value)) return keys;
501
+ for (const [rawKey, rawValue] of Object.entries(value)) {
502
+ const key = prefix ? `${prefix}.${rawKey}` : rawKey;
503
+ if (rawValue && typeof rawValue === "object" && !Array.isArray(rawValue) && ("$value" in rawValue || "value" in rawValue)) {
504
+ keys.add(key);
505
+ keys.add(rawKey);
506
+ } else if (rawValue && typeof rawValue === "object" && !Array.isArray(rawValue)) {
507
+ for (const nested of flattenDesignTokenKeys(rawValue, key)) keys.add(nested);
508
+ }
509
+ }
510
+ return keys;
511
+ }
512
+ function parseDecantrCssTokenNames(projectRoot) {
513
+ const tokensPath = join(projectRoot, "src", "styles", "tokens.css");
514
+ if (!existsSync(tokensPath)) return [];
515
+ const css = readFileSync(tokensPath, "utf-8");
516
+ const names = /* @__PURE__ */ new Set();
517
+ for (const match of css.matchAll(/(--d-[\w-]+)\s*:/g)) {
518
+ names.add(match[1]);
519
+ }
520
+ return [...names].sort();
521
+ }
522
+ function collectDesignTokenEvidence(projectRoot, designTokensPath) {
523
+ const resolved = resolveOptionalPath(projectRoot, designTokensPath);
524
+ if (!resolved) return void 0;
525
+ const sourceLabel = isAbsolute(designTokensPath ?? "") ? "<design-tokens>" : designTokensPath ?? "<design-tokens>";
526
+ if (!existsSync(resolved)) {
527
+ return {
528
+ source: sourceLabel,
529
+ status: "error",
530
+ compared: 0,
531
+ matched: 0,
532
+ missing: ["design-token-source-missing"]
533
+ };
534
+ }
535
+ const decantrTokens = parseDecantrCssTokenNames(projectRoot);
536
+ const parsed = JSON.parse(readFileSync(resolved, "utf-8"));
537
+ const designKeys = flattenDesignTokenKeys(parsed);
538
+ const missing = decantrTokens.filter((token) => {
539
+ const bare = token.replace(/^--/, "");
540
+ return !designKeys.has(token) && !designKeys.has(bare) && !designKeys.has(bare.replace(/^d-/, ""));
541
+ });
542
+ return {
543
+ source: sourceLabel,
544
+ status: missing.length === 0 ? "passed" : "warning",
545
+ compared: decantrTokens.length,
546
+ matched: decantrTokens.length - missing.length,
547
+ missing
548
+ };
549
+ }
550
+ function collectDesignTokenFinding(projectRoot, designTokensPath) {
551
+ const evidence = collectDesignTokenEvidence(projectRoot, designTokensPath);
552
+ if (!evidence) return null;
553
+ if (evidence.status === "passed") {
554
+ return createHealthFinding({
555
+ source: "design-token",
556
+ category: "Design Tokens",
557
+ severity: "info",
558
+ message: "Imported design-token source covers Decantr token names.",
559
+ evidence: [`matched=${evidence.matched}/${evidence.compared}`],
560
+ rule: "design-token-coverage",
561
+ baseId: "coverage-passed"
562
+ });
563
+ }
564
+ return createHealthFinding({
565
+ source: "design-token",
566
+ category: "Design Tokens",
567
+ severity: evidence.status === "error" ? "error" : "warn",
568
+ message: "Imported design-token source does not cover all Decantr token names.",
569
+ evidence: [
570
+ `matched=${evidence.matched}/${evidence.compared}`,
571
+ evidence.missing.slice(0, 12).join(", ") || "No Decantr CSS tokens found."
572
+ ],
573
+ rule: "design-token-coverage",
574
+ suggestedFix: "Update the Figma/Tokens Studio export or Decantr token mapping so shared UI policy can be verified.",
575
+ baseId: "coverage-missing"
576
+ });
577
+ }
578
+ async function browserEvidenceFromOptions(projectRoot, options, declaredRoutes) {
579
+ if (!options.browser) return void 0;
580
+ const result = await collectBrowserVerification(projectRoot, options, declaredRoutes);
581
+ return result?.evidence;
582
+ }
583
+ async function createProjectHealthReport(projectRoot = process.cwd(), options = {}) {
320
584
  const metadata = readProjectMetadata(projectRoot);
321
585
  const audit = await auditProject(projectRoot);
322
586
  const findings = [];
@@ -336,6 +600,25 @@ async function createProjectHealthReport(projectRoot = process.cwd()) {
336
600
  });
337
601
  if (!isDuplicateFinding(seen, healthFinding)) findings.push(healthFinding);
338
602
  }
603
+ for (const contractAssertion of createContractAssertions(projectRoot, audit)) {
604
+ if (contractAssertion.status !== "failed") continue;
605
+ const healthFinding = createHealthFinding({
606
+ source: "assertion",
607
+ category: `Contract ${contractAssertion.category}`,
608
+ severity: contractAssertion.severity,
609
+ message: contractAssertion.message,
610
+ evidence: contractAssertion.evidence,
611
+ target: contractAssertion.target,
612
+ rule: contractAssertion.rule,
613
+ suggestedFix: contractAssertion.suggestedFix,
614
+ baseId: contractAssertion.id
615
+ });
616
+ if (!isDuplicateFinding(seen, healthFinding)) findings.push(healthFinding);
617
+ }
618
+ const designTokenFinding = collectDesignTokenFinding(projectRoot, options.designTokensPath);
619
+ if (designTokenFinding && !isDuplicateFinding(seen, designTokenFinding)) {
620
+ findings.push(designTokenFinding);
621
+ }
339
622
  try {
340
623
  const check = collectCheckIssues(projectRoot, { brownfield: metadata.autoBrownfield });
341
624
  for (const issue of check.issues) {
@@ -378,17 +661,21 @@ async function createProjectHealthReport(projectRoot = process.cwd()) {
378
661
  })
379
662
  );
380
663
  }
381
- const counts = countFindings(findings);
382
664
  const declaredRoutes = collectDeclaredRoutes(audit.essence);
383
665
  const manifest = audit.packManifest;
666
+ const browserVerification = await collectBrowserVerification(projectRoot, options, declaredRoutes);
667
+ if (browserVerification?.finding && !isDuplicateFinding(seen, browserVerification.finding)) {
668
+ findings.push(browserVerification.finding);
669
+ }
670
+ const finalCounts = countFindings(findings);
384
671
  return {
385
672
  $schema: PROJECT_HEALTH_SCHEMA_URL,
386
673
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
387
674
  projectRoot,
388
- status: statusFromCounts(counts),
389
- score: scoreFromCounts(counts),
675
+ status: statusFromCounts(finalCounts),
676
+ score: scoreFromCounts(finalCounts),
390
677
  summary: {
391
- ...counts,
678
+ ...finalCounts,
392
679
  findingCount: findings.length,
393
680
  workflowMode: metadata.workflowMode,
394
681
  adoptionMode: metadata.adoptionMode,
@@ -509,6 +796,20 @@ function formatProjectHealthJson(report) {
509
796
  return `${JSON.stringify(report, null, 2)}
510
797
  `;
511
798
  }
799
+ async function createProjectEvidenceBundle(projectRoot, report, options = {}) {
800
+ const audit = await auditProject(projectRoot);
801
+ const assertions = createContractAssertions(projectRoot, audit);
802
+ return createEvidenceBundle({
803
+ projectRoot,
804
+ report,
805
+ audit,
806
+ assertions,
807
+ workspaceConfigPath: existsSync(join(projectRoot, ".decantr", "workspace.json")) ? join(projectRoot, ".decantr", "workspace.json") : null,
808
+ designTokensPath: resolveOptionalPath(projectRoot, options.designTokensPath) ?? null,
809
+ browser: await browserEvidenceFromOptions(projectRoot, options, report.routes.declared),
810
+ designTokens: collectDesignTokenEvidence(projectRoot, options.designTokensPath)
811
+ });
812
+ }
512
813
  function resolveFormat(options) {
513
814
  if (options.json) return "json";
514
815
  if (options.markdown) return "markdown";
@@ -529,7 +830,12 @@ async function cmdHealth(projectRoot = process.cwd(), options = {}) {
529
830
  if (result.projectPath) {
530
831
  console.log(`${DIM}Project: ${result.projectPath}${RESET}`);
531
832
  }
532
- console.log(`${DIM}CI gate: decantr health --ci --fail-on ${result.failOn}${RESET}`);
833
+ if (result.workspace) {
834
+ console.log(`${DIM}Workspace mode enabled.${RESET}`);
835
+ }
836
+ console.log(
837
+ `${DIM}CI gate: decantr ${result.workspace ? "workspace health" : "health"} --ci --fail-on ${result.failOn}${RESET}`
838
+ );
533
839
  } catch (e) {
534
840
  console.error(`${RED}${e.message}${RESET}`);
535
841
  process.exitCode = 1;
@@ -537,7 +843,13 @@ async function cmdHealth(projectRoot = process.cwd(), options = {}) {
537
843
  return;
538
844
  }
539
845
  const startedAt = Date.now();
540
- const report = await createProjectHealthReport(projectRoot);
846
+ const reportOptions = {
847
+ browser: options.browser,
848
+ requireBrowser: options.requireBrowser,
849
+ browserBaseUrl: options.browserBaseUrl ?? process.env.DECANTR_BROWSER_BASE_URL,
850
+ designTokensPath: options.designTokensPath
851
+ };
852
+ const report = await createProjectHealthReport(projectRoot, reportOptions);
541
853
  if (options.promptId) {
542
854
  const finding = report.findings.find((entry) => entry.id === options.promptId);
543
855
  await sendProjectHealthReportTelemetry({
@@ -562,11 +874,16 @@ async function cmdHealth(projectRoot = process.cwd(), options = {}) {
562
874
  }
563
875
  const format = resolveFormat(options);
564
876
  const failOn = options.failOn ?? "error";
565
- const payload = format === "json" ? formatProjectHealthJson(report) : format === "markdown" ? formatProjectHealthMarkdown(report) : formatProjectHealthText(report);
877
+ const payload = options.evidence ? `${JSON.stringify(await createProjectEvidenceBundle(projectRoot, report, reportOptions), null, 2)}
878
+ ` : format === "json" ? formatProjectHealthJson(report) : format === "markdown" ? formatProjectHealthMarkdown(report) : formatProjectHealthText(report);
566
879
  if (options.output) {
567
- writeFileSync(options.output, payload, "utf-8");
880
+ const outputPath = isAbsolute(options.output) ? options.output : join(projectRoot, options.output);
881
+ mkdirSync(dirname(outputPath), { recursive: true });
882
+ writeFileSync(outputPath, payload, "utf-8");
568
883
  if (!options.ci) {
569
- console.log(`${GREEN}Wrote Decantr health report:${RESET} ${options.output}`);
884
+ console.log(
885
+ `${GREEN}Wrote Decantr ${options.evidence ? "evidence bundle" : "health report"}:${RESET} ${options.output}`
886
+ );
570
887
  }
571
888
  } else {
572
889
  process.stdout.write(payload);
@@ -629,10 +946,12 @@ function parseHealthArgs(args) {
629
946
  options.initCi.projectPath = args[++index];
630
947
  } else if (arg.startsWith("--project=")) {
631
948
  options.initCi.projectPath = arg.split("=")[1];
949
+ } else if (arg === "--workspace") {
950
+ options.initCi.workspace = true;
632
951
  }
633
952
  }
634
953
  normalizeHealthFailOn(options.initCi.failOn);
635
- validateProjectPath(options.initCi.projectPath);
954
+ if (!options.initCi.workspace) validateProjectPath(options.initCi.projectPath);
636
955
  return options;
637
956
  }
638
957
  for (let index = 1; index < args.length; index += 1) {
@@ -641,6 +960,22 @@ function parseHealthArgs(args) {
641
960
  options.json = true;
642
961
  } else if (arg === "--markdown") {
643
962
  options.markdown = true;
963
+ } else if (arg === "--evidence") {
964
+ options.evidence = true;
965
+ options.json = true;
966
+ } else if (arg === "--browser") {
967
+ options.browser = true;
968
+ } else if (arg === "--require-browser") {
969
+ options.browser = true;
970
+ options.requireBrowser = true;
971
+ } else if (arg === "--base-url" && args[index + 1]) {
972
+ options.browserBaseUrl = args[++index];
973
+ } else if (arg.startsWith("--base-url=")) {
974
+ options.browserBaseUrl = arg.split("=")[1];
975
+ } else if (arg === "--design-tokens" && args[index + 1]) {
976
+ options.designTokensPath = args[++index];
977
+ } else if (arg.startsWith("--design-tokens=")) {
978
+ options.designTokensPath = arg.split("=")[1];
644
979
  } else if (arg === "--ci") {
645
980
  options.ci = true;
646
981
  } else if (arg === "--format" && args[index + 1]) {
@@ -673,10 +1008,12 @@ function parseHealthArgs(args) {
673
1008
  export {
674
1009
  renderProjectHealthCiWorkflow,
675
1010
  writeProjectHealthCiWorkflow,
1011
+ collectDesignTokenEvidence,
676
1012
  createProjectHealthReport,
677
1013
  formatProjectHealthText,
678
1014
  formatProjectHealthMarkdown,
679
1015
  formatProjectHealthJson,
1016
+ createProjectEvidenceBundle,
680
1017
  shouldFailHealth,
681
1018
  cmdHealth,
682
1019
  parseHealthArgs
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from
3
3
  import { join } from "path";
4
4
  import { API_CONTENT_TYPES, RegistryAPIClient } from "@decantr/registry";
5
5
  var DEFAULT_API_URL = "https://api.decantr.ai/v1";
6
+ var REGISTRY_SYNC_PAGE_SIZE = 100;
6
7
  var ALL_CONTENT_TYPES = API_CONTENT_TYPES;
7
8
  function loadFromCache(cacheDir, contentType, id, namespace) {
8
9
  const nsDir = namespace ? join(cacheDir, namespace) : cacheDir;
@@ -21,6 +22,62 @@ function saveToCache(cacheDir, contentType, id, data, namespace = "@official") {
21
22
  const cachePath = id ? join(dir, `${id}.json`) : join(dir, "index.json");
22
23
  writeFileSync(cachePath, JSON.stringify(data, null, 2));
23
24
  }
25
+ function contentCacheKey(item) {
26
+ if (!item || typeof item !== "object") return null;
27
+ const record = item;
28
+ const data = record.data && typeof record.data === "object" ? record.data : null;
29
+ return typeof record.slug === "string" && record.slug || data && typeof data.id === "string" && data.id || data && typeof data.slug === "string" && data.slug || typeof record.id === "string" && record.id || null;
30
+ }
31
+ function normalizeCacheItem(item, fallbackId) {
32
+ if (!item || typeof item !== "object") return item;
33
+ const record = item;
34
+ const data = record.data && typeof record.data === "object" ? record.data : null;
35
+ if (data) {
36
+ return {
37
+ ...data,
38
+ id: typeof data.id === "string" ? data.id : fallbackId
39
+ };
40
+ }
41
+ return {
42
+ ...record,
43
+ publicId: typeof record.id === "string" && record.id !== fallbackId ? record.id : void 0,
44
+ id: fallbackId
45
+ };
46
+ }
47
+ async function fetchAllContent(apiClient, contentType, params = {}) {
48
+ const items = [];
49
+ const seen = /* @__PURE__ */ new Set();
50
+ let total = null;
51
+ let offset = 0;
52
+ for (let page = 0; page < 200; page++) {
53
+ const result = await apiClient.listContent(contentType, {
54
+ ...params,
55
+ limit: REGISTRY_SYNC_PAGE_SIZE,
56
+ offset
57
+ });
58
+ if (total == null && typeof result.total === "number") {
59
+ total = result.total;
60
+ }
61
+ let newItems = 0;
62
+ for (const item of result.items) {
63
+ const key = contentCacheKey(item) ?? `offset:${offset + newItems}`;
64
+ if (seen.has(key)) continue;
65
+ seen.add(key);
66
+ items.push(item);
67
+ newItems++;
68
+ }
69
+ if (result.items.length === 0) break;
70
+ if (total != null && items.length >= total) break;
71
+ if (newItems === 0) break;
72
+ offset += result.items.length;
73
+ }
74
+ return {
75
+ items,
76
+ total: total ?? items.length,
77
+ limit: REGISTRY_SYNC_PAGE_SIZE,
78
+ offset: 0
79
+ };
80
+ }
24
81
  var RegistryClient = class {
25
82
  cacheDir;
26
83
  apiUrl;
@@ -81,7 +138,7 @@ var RegistryClient = class {
81
138
  let source = { type: "cache" };
82
139
  if (!this.offline) {
83
140
  try {
84
- const apiResult = await this.apiClient.listContent(contentType, {
141
+ const apiResult = await fetchAllContent(this.apiClient, contentType, {
85
142
  namespace,
86
143
  sort,
87
144
  recommended,
@@ -196,15 +253,17 @@ async function syncRegistry(cacheDir, apiUrl = DEFAULT_API_URL) {
196
253
  }
197
254
  for (const type of ALL_CONTENT_TYPES) {
198
255
  try {
199
- const result = await apiClient.listContent(type, { namespace: "@official" });
256
+ const result = await fetchAllContent(apiClient, type, { namespace: "@official" });
200
257
  saveToCache(cacheDir, type, null, result, "@official");
201
258
  for (const item of result.items) {
202
- const slug = item.slug;
203
- const data = item.data;
204
- const innerSlug = data?.id || data?.slug;
205
- const cacheKey = slug || innerSlug || item.id;
259
+ const cacheKey = contentCacheKey(item);
206
260
  if (cacheKey) {
207
- saveToCache(cacheDir, type, cacheKey, item, "@official");
261
+ let cacheItem = normalizeCacheItem(item, cacheKey);
262
+ try {
263
+ cacheItem = await apiClient.getContent(type, "@official", cacheKey);
264
+ } catch {
265
+ }
266
+ saveToCache(cacheDir, type, cacheKey, cacheItem, "@official");
208
267
  }
209
268
  }
210
269
  synced.push(type);
@@ -3910,6 +3969,44 @@ function buildFlagsString(options) {
3910
3969
  }
3911
3970
  return flags.join(" ");
3912
3971
  }
3972
+ function serializeConstraintValue(value) {
3973
+ if (value === void 0 || value === null) return void 0;
3974
+ if (typeof value === "string") return value;
3975
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
3976
+ try {
3977
+ return JSON.stringify(value);
3978
+ } catch {
3979
+ return String(value);
3980
+ }
3981
+ }
3982
+ function normalizeBlueprintDesignConstraints(designConstraints) {
3983
+ if (!designConstraints || typeof designConstraints !== "object" || Array.isArray(designConstraints)) {
3984
+ return void 0;
3985
+ }
3986
+ const constraints = {};
3987
+ const effects = {};
3988
+ for (const [key, value] of Object.entries(designConstraints)) {
3989
+ if (key === "effects" && value && typeof value === "object" && !Array.isArray(value)) {
3990
+ for (const [effectKey, effectValue] of Object.entries(value)) {
3991
+ const serialized2 = serializeConstraintValue(effectValue);
3992
+ if (serialized2) effects[effectKey] = serialized2;
3993
+ }
3994
+ continue;
3995
+ }
3996
+ const serialized = serializeConstraintValue(value);
3997
+ if (!serialized) continue;
3998
+ if (key === "mode" && typeof value === "string") constraints.mode = value;
3999
+ else if (key === "typography" && typeof value === "string") constraints.typography = value;
4000
+ else if (key === "borders" && typeof value === "string") constraints.borders = value;
4001
+ else if (key === "corners" && typeof value === "string") constraints.corners = value;
4002
+ else if (key === "shadows" && typeof value === "string") constraints.shadows = value;
4003
+ else effects[key] = serialized;
4004
+ }
4005
+ if (Object.keys(effects).length > 0) {
4006
+ constraints.effects = effects;
4007
+ }
4008
+ return Object.keys(constraints).length > 0 ? constraints : void 0;
4009
+ }
3913
4010
  function generateTaskContextV4(templateName, essence) {
3914
4011
  const template = loadTemplate(templateName);
3915
4012
  const sections = essence.blueprint.sections;
@@ -4167,8 +4264,11 @@ async function scaffoldProject(projectRoot, options, detected, registry, archety
4167
4264
  if (blueprintData?.personality?.length) {
4168
4265
  essenceV4.dna.personality = typeof blueprintData.personality === "string" ? [blueprintData.personality] : blueprintData.personality;
4169
4266
  }
4170
- if (blueprintData?.design_constraints) {
4171
- essenceV4.dna.constraints = blueprintData.design_constraints;
4267
+ const normalizedDesignConstraints = normalizeBlueprintDesignConstraints(
4268
+ blueprintData?.design_constraints
4269
+ );
4270
+ if (normalizedDesignConstraints) {
4271
+ essenceV4.dna.constraints = normalizedDesignConstraints;
4172
4272
  }
4173
4273
  if (blueprintData?.seo_hints) {
4174
4274
  essenceV4.meta.seo = blueprintData.seo_hints;
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  cmdHealth,
3
+ collectDesignTokenEvidence,
4
+ createProjectEvidenceBundle,
3
5
  createProjectHealthReport,
4
6
  formatProjectHealthJson,
5
7
  formatProjectHealthMarkdown,
@@ -8,11 +10,13 @@ import {
8
10
  renderProjectHealthCiWorkflow,
9
11
  shouldFailHealth,
10
12
  writeProjectHealthCiWorkflow
11
- } from "./chunk-3H3HWDJA.js";
13
+ } from "./chunk-OD46PCR6.js";
12
14
  import "./chunk-NBJCO4G5.js";
13
15
  import "./chunk-IEW2QFYI.js";
14
16
  export {
15
17
  cmdHealth,
18
+ collectDesignTokenEvidence,
19
+ createProjectEvidenceBundle,
16
20
  createProjectHealthReport,
17
21
  formatProjectHealthJson,
18
22
  formatProjectHealthMarkdown,