@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.
- package/README.md +31 -4
- package/dist/bin.js +2 -2
- package/dist/{chunk-2JWVKBNB.js → chunk-6BRD6DTB.js} +915 -327
- package/dist/chunk-AUQXYJ7T.js +316 -0
- package/dist/{chunk-3H3HWDJA.js → chunk-OD46PCR6.js} +354 -17
- package/dist/{chunk-WDA4SHIQ.js → chunk-P4NUDLWB.js} +109 -9
- package/dist/{health-EENY3BFS.js → health-ZXOPGNBZ.js} +5 -1
- package/dist/index.js +2 -2
- package/dist/{studio-TBJPZZHA.js → studio-LHQXHBE7.js} +63 -1
- package/dist/{upgrade-PL755AF7.js → upgrade-HSPWYROM.js} +1 -1
- package/dist/workspace-MOLAGT2B.js +21 -0
- package/package.json +22 -5
- package/src/templates/decantr-health.workflow.yml.template +2 -2
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
389
|
-
score: scoreFromCounts(
|
|
675
|
+
status: statusFromCounts(finalCounts),
|
|
676
|
+
score: scoreFromCounts(finalCounts),
|
|
390
677
|
summary: {
|
|
391
|
-
...
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4171
|
-
|
|
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-
|
|
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,
|