New:Socket for Asana Is Now Available.Learn more
Get Started

@decantr/cli

Package Overview
Dependencies
Maintainers
1
Versions
116
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@decantr/cli - npm Package Compare versions

Comparing version
3.5.5
to
3.6.0
+386
dist/chunk-56HUEUO3.js
import {
createProjectHealthReport,
listWorkspaceAppCandidateDetails
} from "./chunk-WAGVDMJV.js";
// src/commands/workspace.ts
import { execFileSync } from "child_process";
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
import { dirname, join, relative, resolve } from "path";
import { WORKSPACE_HEALTH_REPORT_V2_SCHEMA_URL } from "@decantr/verifier";
var BOLD = "\x1B[1m";
var DIM = "\x1B[2m";
var GREEN = "\x1B[32m";
var RED = "\x1B[31m";
var YELLOW = "\x1B[33m";
var RESET = "\x1B[0m";
var WORKSPACE_HEALTH_SCHEMA_URL = WORKSPACE_HEALTH_REPORT_V2_SCHEMA_URL;
var DEFAULT_IGNORES = /* @__PURE__ */ new Set([
".git",
".next",
".turbo",
".vercel",
"coverage",
"dist",
"node_modules",
"playwright-report"
]);
function workspaceConfigPath(root) {
return join(root, ".decantr", "workspace.json");
}
function readWorkspaceConfig(root) {
const path = workspaceConfigPath(root);
if (!existsSync(path)) return null;
return JSON.parse(readFileSync(path, "utf-8"));
}
function normalizeProjectPath(raw) {
const normalized = raw.replace(/^\.\/+/, "").replace(/\/+$/, "");
if (!normalized || normalized.startsWith("/") || normalized.includes("..") || normalized.includes("\\") || /\s/.test(normalized)) {
throw new Error(`Invalid workspace project path: ${raw}`);
}
return normalized;
}
function projectIdFromPath(path) {
return path.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "project";
}
function discoverProjectPaths(root, config) {
const ignored = /* @__PURE__ */ new Set([...config?.ignore ?? [], ...DEFAULT_IGNORES]);
const results = /* @__PURE__ */ new Set();
function walk(dir, depth) {
if (depth > 6) return;
const rel = relative(root, dir).replace(/\\/g, "/");
if (rel && [...ignored].some((entry) => rel === entry || rel.startsWith(`${entry}/`))) return;
if (existsSync(join(dir, "decantr.essence.json"))) {
results.add(rel || ".");
return;
}
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
if (ignored.has(entry.name)) continue;
walk(join(dir, entry.name), depth + 1);
}
}
walk(root, 0);
return [...results].sort();
}
function listWorkspaceProjects(root = process.cwd()) {
const workspaceRoot = resolve(root);
const config = readWorkspaceConfig(workspaceRoot);
const byPath = /* @__PURE__ */ new Map();
for (const project of config?.projects ?? []) {
const path = normalizeProjectPath(project.path);
byPath.set(path, {
id: project.id ?? projectIdFromPath(path),
path,
absolutePath: resolve(workspaceRoot, path),
owner: project.owner ?? null,
tags: project.tags ?? [],
criticality: project.criticality ?? "normal",
browser: project.browser ?? config?.browser ?? false,
source: "manifest"
});
}
for (const path of discoverProjectPaths(workspaceRoot, config)) {
if (byPath.has(path)) continue;
byPath.set(path, {
id: projectIdFromPath(path),
path,
absolutePath: resolve(workspaceRoot, path),
owner: null,
tags: [],
criticality: "normal",
browser: config?.browser ?? false,
source: "auto"
});
}
return [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
}
function listWorkspaceCandidates(root = process.cwd(), projects = listWorkspaceProjects(root)) {
const attached = new Set(projects.map((project) => project.path));
return listWorkspaceAppCandidateDetails(root).map((candidate, index) => ({
path: candidate.path,
attached: attached.has(candidate.path),
suggestedAdoptCommand: `decantr adopt --project ${candidate.path} --yes`,
rank: index + 1,
score: candidate.score,
category: candidate.category,
reason: candidate.reason
}));
}
function changedPaths(root, since) {
try {
const output = execFileSync("git", ["diff", "--name-only", since, "--"], {
cwd: root,
encoding: "utf-8",
stdio: ["ignore", "pipe", "ignore"]
});
return new Set(
output.split("\n").map((line) => line.trim()).filter(Boolean)
);
} catch {
return /* @__PURE__ */ new Set();
}
}
function projectChanged(project, changed) {
if (changed.size === 0) return false;
const prefix = project.path === "." ? "" : `${project.path}/`;
for (const path of changed) {
if (project.path === "." || path === project.path || path.startsWith(prefix)) return true;
}
return false;
}
async function withTimeout(promise, timeoutMs, label) {
let timeout;
const timer = new Promise((_, reject) => {
timeout = setTimeout(
() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),
timeoutMs
);
});
try {
return await Promise.race([promise, timer]);
} finally {
if (timeout) clearTimeout(timeout);
}
}
async function mapLimited(items, concurrency, fn) {
const results = new Array(items.length);
let next = 0;
async function worker() {
while (next < items.length) {
const index = next++;
results[index] = await fn(items[index]);
}
}
await Promise.all(Array.from({ length: Math.max(1, concurrency) }, () => worker()));
return results;
}
async function createWorkspaceHealthReport(root = process.cwd(), options = {}) {
const workspaceRoot = resolve(root);
const config = readWorkspaceConfig(workspaceRoot);
const since = options.since ?? "origin/main";
const changed = options.changedOnly ? changedPaths(workspaceRoot, since) : /* @__PURE__ */ new Set();
const allProjects = listWorkspaceProjects(workspaceRoot);
const projects = options.changedOnly ? allProjects.filter((project) => projectChanged(project, changed)) : allProjects;
const concurrency = options.concurrency ?? config?.concurrency ?? 4;
const timeoutMs = options.timeoutMs ?? config?.timeoutMs ?? 12e4;
const checked = await mapLimited(projects, concurrency, async (project) => {
const startedAt = Date.now();
try {
const report = await withTimeout(
createProjectHealthReport(project.absolutePath, {
browser: options.browser ?? project.browser
}),
timeoutMs,
project.path
);
return {
id: project.id,
path: project.path,
status: report.status,
score: report.score,
errorCount: report.summary.errorCount,
warnCount: report.summary.warnCount,
infoCount: report.summary.infoCount,
findingCount: report.summary.findingCount,
durationMs: Date.now() - startedAt,
changed: options.changedOnly ? projectChanged(project, changed) : false,
source: project.source,
error: null,
loopState: report.loop.state,
loopNextAction: report.loop.nextActions[0] ?? null
};
} catch (error) {
return {
id: project.id,
path: project.path,
status: "failed",
score: 0,
errorCount: 1,
warnCount: 0,
infoCount: 0,
findingCount: 1,
durationMs: Date.now() - startedAt,
changed: options.changedOnly ? projectChanged(project, changed) : false,
source: project.source,
error: error.message,
loopState: "blocked_missing_context",
loopNextAction: "Fix the project health failure, then rerun workspace health."
};
}
});
const summary = {
projectCount: allProjects.length,
checkedCount: checked.length,
healthyCount: checked.filter((project) => project.status === "healthy").length,
warningCount: checked.filter((project) => project.status === "warning").length,
errorCount: checked.filter((project) => project.status === "error").length,
failedCount: checked.filter((project) => project.status === "failed").length
};
const blockedCount = checked.filter(
(project) => project.loopState?.startsWith("blocked") || project.loopState === "human_resolution_required"
).length;
const repairRequiredCount = checked.filter(
(project) => project.loopState === "repair_required"
).length;
const workspaceLoopState = checked.length === 0 ? "needs_context" : blockedCount > 0 ? "human_resolution_required" : repairRequiredCount > 0 || summary.errorCount > 0 || summary.warningCount > 0 ? "repair_required" : "verified";
const workspaceLoopStatus = workspaceLoopState === "human_resolution_required" || workspaceLoopState.startsWith("blocked") ? "blocked" : summary.errorCount > 0 || summary.failedCount > 0 ? "error" : summary.warningCount > 0 ? "warning" : "healthy";
return {
$schema: WORKSPACE_HEALTH_SCHEMA_URL,
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
workspaceRoot,
changedOnly: options.changedOnly ?? false,
since: options.changedOnly ? since : null,
summary,
loop: {
state: workspaceLoopState,
status: workspaceLoopStatus,
projectCount: checked.length,
blockedCount,
repairRequiredCount,
nextActions: [
workspaceLoopState === "verified" ? "Workspace loop verified." : 'Open the highest-risk project, run `decantr task <route> "<intent>"`, repair, then rerun `decantr verify`.'
]
},
projects: checked
};
}
function formatWorkspaceHealthText(report) {
const lines = [
`${BOLD}Decantr Workspace Health${RESET}`,
"",
`Projects: ${report.summary.checkedCount}/${report.summary.projectCount}`,
`Healthy: ${report.summary.healthyCount} | Warnings: ${report.summary.warningCount} | Errors: ${report.summary.errorCount} | Failed: ${report.summary.failedCount}`,
`Loop: ${report.loop.state} | blocked ${report.loop.blockedCount} | repair ${report.loop.repairRequiredCount}`,
""
];
for (const project of report.projects) {
const color = project.status === "healthy" ? GREEN : project.status === "warning" ? YELLOW : RED;
lines.push(
`${color}${String(project.status).toUpperCase()}${RESET} ${project.path} score ${project.score}/100 findings ${project.findingCount}`
);
if (project.error) lines.push(` ${DIM}${project.error}${RESET}`);
}
return `${lines.join("\n")}
`;
}
function formatWorkspaceHealthMarkdown(report) {
const lines = [
"# Decantr Workspace Health",
"",
`- Projects checked: **${report.summary.checkedCount}/${report.summary.projectCount}**`,
`- Healthy: ${report.summary.healthyCount}`,
`- Warnings: ${report.summary.warningCount}`,
`- Errors: ${report.summary.errorCount}`,
`- Failed: ${report.summary.failedCount}`,
"",
"| Project | Status | Score | Findings | Source |",
"| --- | --- | ---: | ---: | --- |"
];
for (const project of report.projects) {
lines.push(
`| \`${project.path}\` | ${project.status} | ${project.score} | ${project.findingCount} | ${project.source} |`
);
}
return `${lines.join("\n")}
`;
}
function shouldFailWorkspaceHealth(report, failOn = "error") {
if (failOn === "none") return false;
if (report.summary.failedCount > 0 || report.summary.errorCount > 0) return true;
return failOn === "warn" && report.summary.warningCount > 0;
}
function parseHealthFailOn(value) {
if (value === "warn" || value === "none") return value;
return "error";
}
function parseWorkspaceArgs(args) {
const subcommand = args[1] === "health" ? "health" : "list";
const options = { subcommand };
for (let index = 2; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--json") options.json = true;
else if (arg === "--markdown") options.markdown = true;
else if (arg === "--ci") options.ci = true;
else if (arg === "--browser") options.browser = true;
else if (arg === "--changed") options.changedOnly = true;
else if (arg === "--since" && args[index + 1]) options.since = args[++index];
else if (arg.startsWith("--since=")) options.since = arg.split("=")[1];
else if (arg === "--output" && args[index + 1]) options.output = args[++index];
else if (arg.startsWith("--output=")) options.output = arg.split("=")[1];
else if (arg === "--fail-on" && args[index + 1])
options.failOn = parseHealthFailOn(args[++index]);
else if (arg.startsWith("--fail-on=")) options.failOn = parseHealthFailOn(arg.split("=")[1]);
else if (arg === "--concurrency" && args[index + 1])
options.concurrency = Number(args[++index]);
else if (arg.startsWith("--concurrency=")) options.concurrency = Number(arg.split("=")[1]);
else if (arg === "--timeout-ms" && args[index + 1]) options.timeoutMs = Number(args[++index]);
else if (arg.startsWith("--timeout-ms=")) options.timeoutMs = Number(arg.split("=")[1]);
}
return options;
}
async function cmdWorkspace(workspaceRoot = process.cwd(), args = ["workspace"]) {
const options = parseWorkspaceArgs(args);
if (options.subcommand === "list") {
const projects = listWorkspaceProjects(workspaceRoot);
const candidates = listWorkspaceCandidates(workspaceRoot, projects);
const unattachedCandidates = candidates.filter((candidate) => !candidate.attached);
const payload2 = `${JSON.stringify({ projects, candidates }, null, 2)}
`;
if (options.json) {
process.stdout.write(payload2);
return;
}
console.log(`${BOLD}Decantr workspace projects${RESET}`);
console.log("");
console.log("Attached Decantr projects:");
if (projects.length === 0) {
console.log(` ${DIM}(none yet)${RESET}`);
} else {
for (const project of projects) {
console.log(` ${project.path} ${DIM}${project.source}${RESET}`);
}
}
if (candidates.length > 0) {
console.log("");
console.log("App candidates:");
for (const candidate of candidates) {
const status = candidate.attached ? `${GREEN}attached${RESET}` : `${YELLOW}unattached${RESET}`;
console.log(
` #${candidate.rank} ${candidate.path} ${DIM}${status} \xB7 ${candidate.category} \xB7 score ${candidate.score}${RESET}`
);
}
}
if (unattachedCandidates.length > 0) {
console.log("");
console.log(projects.length > 0 ? "Attach another app:" : "Start by attaching one app:");
console.log(` ${unattachedCandidates[0].suggestedAdoptCommand}`);
}
return;
}
const report = await createWorkspaceHealthReport(workspaceRoot, options);
const payload = options.json ? `${JSON.stringify(report, null, 2)}
` : options.markdown ? formatWorkspaceHealthMarkdown(report) : formatWorkspaceHealthText(report);
if (options.output) {
mkdirSync(dirname(resolve(workspaceRoot, options.output)), { recursive: true });
writeFileSync(resolve(workspaceRoot, options.output), payload, "utf-8");
if (!options.ci)
console.log(`${GREEN}Wrote Decantr workspace health:${RESET} ${options.output}`);
} else {
process.stdout.write(payload);
}
if (options.ci && shouldFailWorkspaceHealth(report, options.failOn ?? "error")) {
process.exitCode = 1;
}
}
export {
listWorkspaceProjects,
listWorkspaceCandidates,
createWorkspaceHealthReport,
formatWorkspaceHealthText,
formatWorkspaceHealthMarkdown,
shouldFailWorkspaceHealth,
parseWorkspaceArgs,
cmdWorkspace
};

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

import {
cmdHealth,
collectDesignTokenEvidence,
createProjectEvidenceBundle,
createProjectHealthReport,
formatDiagnosticCatalogJson,
formatDiagnosticCatalogMarkdown,
formatDiagnosticCatalogText,
formatProjectHealthJson,
formatProjectHealthMarkdown,
formatProjectHealthText,
parseHealthArgs,
renderProjectHealthCiWorkflow,
shouldFailHealth,
writeProjectHealthCiWorkflow
} from "./chunk-WAGVDMJV.js";
import "./chunk-24JR4ZNG.js";
export {
cmdHealth,
collectDesignTokenEvidence,
createProjectEvidenceBundle,
createProjectHealthReport,
formatDiagnosticCatalogJson,
formatDiagnosticCatalogMarkdown,
formatDiagnosticCatalogText,
formatProjectHealthJson,
formatProjectHealthMarkdown,
formatProjectHealthText,
parseHealthArgs,
renderProjectHealthCiWorkflow,
shouldFailHealth,
writeProjectHealthCiWorkflow
};

Sorry, the diff of this file is too big to display

import {
cmdWorkspace,
createWorkspaceHealthReport,
formatWorkspaceHealthMarkdown,
formatWorkspaceHealthText,
listWorkspaceCandidates,
listWorkspaceProjects,
parseWorkspaceArgs,
shouldFailWorkspaceHealth
} from "./chunk-56HUEUO3.js";
import "./chunk-WAGVDMJV.js";
import "./chunk-24JR4ZNG.js";
export {
cmdWorkspace,
createWorkspaceHealthReport,
formatWorkspaceHealthMarkdown,
formatWorkspaceHealthText,
listWorkspaceCandidates,
listWorkspaceProjects,
parseWorkspaceArgs,
shouldFailWorkspaceHealth
};
+3
-3
#!/usr/bin/env node
import "./chunk-4O74E2PS.js";
import "./chunk-TGVBWTDK.js";
import "./chunk-SIDKK73N.js";
import "./chunk-FIWVRMZN.js";
import "./chunk-U62GGKUO.js";
import "./chunk-56HUEUO3.js";
import "./chunk-WAGVDMJV.js";
import "./chunk-24JR4ZNG.js";

@@ -1,5 +0,5 @@

import "./chunk-4O74E2PS.js";
import "./chunk-TGVBWTDK.js";
import "./chunk-SIDKK73N.js";
import "./chunk-FIWVRMZN.js";
import "./chunk-U62GGKUO.js";
import "./chunk-56HUEUO3.js";
import "./chunk-WAGVDMJV.js";
import "./chunk-24JR4ZNG.js";
{
"name": "@decantr/cli",
"version": "3.5.5",
"version": "3.6.0",
"description": "Decantr CLI - adopt, verify, graph, and govern frontend codebases touched by AI agents",

@@ -52,7 +52,7 @@ "keywords": [

"ajv": "^8.20.0",
"@decantr/core": "3.5.0",
"@decantr/core": "3.6.0",
"@decantr/telemetry": "3.4.0",
"@decantr/essence-spec": "3.4.0",
"@decantr/registry": "3.4.0",
"@decantr/telemetry": "3.4.0",
"@decantr/verifier": "3.5.5"
"@decantr/verifier": "3.6.0",
"@decantr/registry": "3.4.0"
},

@@ -59,0 +59,0 @@ "scripts": {

@@ -26,3 +26,3 @@ # @decantr/cli

Use `decantr studio` after adoption when you want a local Control Room for routes, findings, evidence, authority, and next actions. Use `decantr doctor` when the next step is unclear, `decantr task <route> "<intent>"` before asking an LLM to modify a route, `decantr verify` after the edit, `decantr resolve` when source and contract disagree, and `decantr ci` in required automation. If runtime source and Decantr context disagree, report the drift instead of guessing; in Brownfield the existing source is observed truth, accepted local law/style bridge is project authority where present, Essence V4 is the structural contract, and hosted packs stay advisory until mapped into local law. Use `decantr graph` when you want the Decantr 3 typed Contract graph, typed graph diff summary, manifest, content-addressed snapshot history, and cache-friendly contract capsule written under `.decantr/graph`; the capsule includes a bounded SourceArtifact path index so agents can discover valid file-impact handles without reading the full snapshot, and `--capsule-source-limit <count>` can tune that index for large repos. Add `--route /feed --task "improve loading" --json` when you want the exact task-ranked route-scoped subgraph an agent should inspect before editing, `--node cmp:button --impact --json` when you need the graph-shaped blast radius for a component, token, rule, finding, or source artifact, or `--file src/app/page.tsx --impact --json` when the agent knows the source file it is about to change. Route and impact ranking use deterministic weighted traversal plus local personalized PageRank and task boosts. Use `--snapshot-id <id>` to inspect a replayable history snapshot and `--compare-to <id> --include-diff-ops --json` to compare the selected/current graph against a prior snapshot. Use `decantr codify --from-audit --style-bridge` when you want project-owned UI patterns, optional `behavior_obligations`, local rules, and token/class bridge mappings such as button/card/shell/theme standards to appear in future task context and verification. Once accepted, that local law is the first Hybrid lane: the app still owns source and styling, but Decantr treats accepted local patterns, behavior obligations, rules, and style bridge mappings as project authority.
In monorepos, app-scoped commands accept `--project <app-path>`. `setup` shows attach guidance before adoption and the day-two loop after adoption. Hosted pack hydration also follows the essence path: `decantr registry compile-packs apps/web/decantr.essence.json --write-context` writes into `apps/web/.decantr/context`. In contract-only/offline Brownfield, deferred hosted packs are optional context unless a present manifest references missing files.
In monorepos, app-scoped commands accept `--project <app-path>`. `setup` shows attach guidance before adoption and the day-two loop after adoption. Candidate discovery ranks product UI apps ahead of docs, Storybook, API, MCP helper, workbench, and package surfaces; `decantr workspace list --json` includes rank, category, score, and reason metadata so automation can explain why `apps/web`, `apps/remix`, or `apps/dashboard` was suggested first. Hosted pack hydration also follows the essence path: `decantr registry compile-packs apps/web/decantr.essence.json --write-context` writes into `apps/web/.decantr/context`. In contract-only/offline Brownfield, deferred hosted packs are optional context unless a present manifest references missing files.
Use `decantr init`, `decantr analyze`, `decantr check`, and `decantr health` as advanced primitives when you need direct control over one step.

@@ -107,2 +107,3 @@

- supports explicit workflow lanes: greenfield blueprint, greenfield contract-only, brownfield adoption, Hybrid local law, Hybrid style bridge, Hybrid Decantr CSS, and hybrid composition
- ranks monorepo app candidates with explainable metadata so product UI apps come before docs, Storybook, API, helper packages, and workbench surfaces
- generates execution-pack context files for AI coding assistants

@@ -114,2 +115,3 @@ - generates typed Contract graph artifacts, replayable snapshot history, graph diffs, manifests, source-file impact context, style-bridge Token nodes, behavior-obligation LocalRule nodes, and `contract-capsule.json` for agent sessions

- searches the registry and showcase benchmark corpus
- runs real-world corpus harnesses with timing percentiles, slow-command budgets, root-smoke/app-scoped classification, and stable failure categories
- filters blueprints through public portfolio sets: `All`, `Featured`, `Certified`, and opt-in `Labs`

@@ -190,3 +192,3 @@ - syncs paginated hosted vocabulary content into a full slug-keyed local cache for offline guards and context generation

`decantr health` remains the advanced project observability primitive. It composes the existing verifier audit, guard checks, brownfield route drift checks, runtime evidence, component reuse drift, accepted style bridge drift, accepted behavior-obligation checks, typed Contract graph freshness, and execution-pack files into a v2 `ProjectHealthReport` with status, score, route summary, pack summary, findings, stable diagnostic codes, typed repair IDs, evidence tier, authority resolution, loop readiness, and AI-ready remediation prompts. The graph freshness slice emits `GRAPH001` / `regenerate-typed-graph` when an attached app has missing, stale, or non-derivable `.decantr/graph` artifacts. The component reuse slice emits `COMP001` / `import-existing-component` when production source locally redeclares a common primitive that already exists as an exported reusable component, and `COMP010` / `replace-raw-control-with-local-component` when production JSX renders raw controls such as `<button>` while the project already owns a reusable primitive. The behavior-obligation slice emits `A11Y010`, `A11Y011`, `INT010`, `INT011`, `INT012`, `INT013`, and `COMP020` for high-confidence dialog/form regressions such as missing accessible names, missing label associations, missing visible destructive consequence copy, missing cancel affordances, missing submitting guards, implicit form button types, or bypassed project-owned interaction primitives. The style bridge slice emits `TOKEN010` / `replace-arbitrary-style-with-bridge-token` when production JSX, common class helpers, hardcoded inline color styles, or hardcoded visual values in CSS/module stylesheets bypass `.decantr/style-bridge.json` after it has been accepted as project-owned style authority. The baseline slice emits `VISUAL010` / `review-visual-baseline-drift` when `--since-baseline` detects changed screenshot hashes. When `.decantr/graph/graph.snapshot.json` exists, each finding is anchored to the most specific graph node Decantr can resolve, and JSON, markdown, text output, repair prompts, and Evidence Bundles carry that anchor. `decantr graph` also writes content-addressed history snapshots under `.decantr/graph/snapshots/` so repeated graph runs can be replayed across an AI edit sequence. When `.decantr/analysis.json` exists, `decantr graph` links observed routes/pages to implementation source artifacts and links exported reusable component declarations to their source files. When browser evidence writes `.decantr/evidence/visual-manifest.json`, `decantr graph` ingests it as local route/page Evidence nodes without uploading screenshots. When `.decantr/evidence/latest.json` exists, `decantr graph` can also materialize saved findings, evidence strings, graph anchors, repair IDs, and referenced repair/read target files as typed graph nodes and edges. When `.decantr/health-baseline-diff.json` exists, baseline changed files become file-level temporal evidence in the graph.
`decantr health` remains the advanced project observability primitive. It composes the existing verifier audit, guard checks, brownfield route drift checks, runtime evidence, component reuse drift, accepted style bridge drift, accepted behavior-obligation checks, typed Contract graph freshness, and execution-pack files into a v2 `ProjectHealthReport` with status, score, route summary, pack summary, findings, stable diagnostic codes, typed repair IDs, evidence tier, authority resolution, loop readiness, and AI-ready remediation prompts. The graph freshness slice emits `GRAPH001` / `regenerate-typed-graph` when an attached app has missing, stale, or non-derivable `.decantr/graph` artifacts. The component reuse slice emits `COMP001` / `import-existing-component` when production source locally redeclares a common primitive that already exists as an exported reusable component, and `COMP010` / `replace-raw-control-with-local-component` when production JSX renders generic raw controls such as `<button>` or text-like `<input>` while the project already owns a reusable primitive. Specialized inputs such as file, hidden, checkbox, radio, color, range, and Dropzone `getInputProps()` controls are not treated as generic `Input` drift. The behavior-obligation slice emits `A11Y010`, `A11Y011`, `INT010`, `INT011`, `INT012`, `INT013`, and `COMP020` for high-confidence dialog/form regressions such as missing accessible names, missing label associations, missing visible destructive consequence copy, missing cancel affordances, missing submitting guards, implicit form button types, or bypassed project-owned interaction primitives. The style bridge slice emits `TOKEN010` / `replace-arbitrary-style-with-bridge-token` when production JSX, common class helpers, hardcoded inline color styles, or hardcoded visual values in CSS/module stylesheets bypass `.decantr/style-bridge.json` after it has been accepted as project-owned style authority. The baseline slice emits `VISUAL010` / `review-visual-baseline-drift` when `--since-baseline` detects changed screenshot hashes. When `.decantr/graph/graph.snapshot.json` exists, each finding is anchored to the most specific graph node Decantr can resolve, and JSON, markdown, text output, repair prompts, and Evidence Bundles carry that anchor. `decantr graph` also writes content-addressed history snapshots under `.decantr/graph/snapshots/` so repeated graph runs can be replayed across an AI edit sequence. When `.decantr/analysis.json` exists, `decantr graph` links observed routes/pages to implementation source artifacts and links exported reusable component declarations to their source files. When browser evidence writes `.decantr/evidence/visual-manifest.json`, `decantr graph` ingests it as local route/page Evidence nodes without uploading screenshots. When `.decantr/evidence/latest.json` exists, `decantr graph` can also materialize saved findings, evidence strings, graph anchors, repair IDs, and referenced repair/read target files as typed graph nodes and edges. When `.decantr/health-baseline-diff.json` exists, baseline changed files become file-level temporal evidence in the graph.

@@ -193,0 +195,0 @@ ```bash

Sorry, the diff of this file is too big to display

import {
createProjectHealthReport,
listWorkspaceAppCandidates
} from "./chunk-U62GGKUO.js";
// src/commands/workspace.ts
import { execFileSync } from "child_process";
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
import { dirname, join, relative, resolve } from "path";
import { WORKSPACE_HEALTH_REPORT_V2_SCHEMA_URL } from "@decantr/verifier";
var BOLD = "\x1B[1m";
var DIM = "\x1B[2m";
var GREEN = "\x1B[32m";
var RED = "\x1B[31m";
var YELLOW = "\x1B[33m";
var RESET = "\x1B[0m";
var WORKSPACE_HEALTH_SCHEMA_URL = WORKSPACE_HEALTH_REPORT_V2_SCHEMA_URL;
var DEFAULT_IGNORES = /* @__PURE__ */ new Set([
".git",
".next",
".turbo",
".vercel",
"coverage",
"dist",
"node_modules",
"playwright-report"
]);
function workspaceConfigPath(root) {
return join(root, ".decantr", "workspace.json");
}
function readWorkspaceConfig(root) {
const path = workspaceConfigPath(root);
if (!existsSync(path)) return null;
return JSON.parse(readFileSync(path, "utf-8"));
}
function normalizeProjectPath(raw) {
const normalized = raw.replace(/^\.\/+/, "").replace(/\/+$/, "");
if (!normalized || normalized.startsWith("/") || normalized.includes("..") || normalized.includes("\\") || /\s/.test(normalized)) {
throw new Error(`Invalid workspace project path: ${raw}`);
}
return normalized;
}
function projectIdFromPath(path) {
return path.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "project";
}
function discoverProjectPaths(root, config) {
const ignored = /* @__PURE__ */ new Set([...config?.ignore ?? [], ...DEFAULT_IGNORES]);
const results = /* @__PURE__ */ new Set();
function walk(dir, depth) {
if (depth > 6) return;
const rel = relative(root, dir).replace(/\\/g, "/");
if (rel && [...ignored].some((entry) => rel === entry || rel.startsWith(`${entry}/`))) return;
if (existsSync(join(dir, "decantr.essence.json"))) {
results.add(rel || ".");
return;
}
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
if (ignored.has(entry.name)) continue;
walk(join(dir, entry.name), depth + 1);
}
}
walk(root, 0);
return [...results].sort();
}
function listWorkspaceProjects(root = process.cwd()) {
const workspaceRoot = resolve(root);
const config = readWorkspaceConfig(workspaceRoot);
const byPath = /* @__PURE__ */ new Map();
for (const project of config?.projects ?? []) {
const path = normalizeProjectPath(project.path);
byPath.set(path, {
id: project.id ?? projectIdFromPath(path),
path,
absolutePath: resolve(workspaceRoot, path),
owner: project.owner ?? null,
tags: project.tags ?? [],
criticality: project.criticality ?? "normal",
browser: project.browser ?? config?.browser ?? false,
source: "manifest"
});
}
for (const path of discoverProjectPaths(workspaceRoot, config)) {
if (byPath.has(path)) continue;
byPath.set(path, {
id: projectIdFromPath(path),
path,
absolutePath: resolve(workspaceRoot, path),
owner: null,
tags: [],
criticality: "normal",
browser: config?.browser ?? false,
source: "auto"
});
}
return [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
}
function listWorkspaceCandidates(root = process.cwd(), projects = listWorkspaceProjects(root)) {
const attached = new Set(projects.map((project) => project.path));
return listWorkspaceAppCandidates(root).map((path) => ({
path,
attached: attached.has(path),
suggestedAdoptCommand: `decantr adopt --project ${path} --yes`
}));
}
function changedPaths(root, since) {
try {
const output = execFileSync("git", ["diff", "--name-only", since, "--"], {
cwd: root,
encoding: "utf-8",
stdio: ["ignore", "pipe", "ignore"]
});
return new Set(
output.split("\n").map((line) => line.trim()).filter(Boolean)
);
} catch {
return /* @__PURE__ */ new Set();
}
}
function projectChanged(project, changed) {
if (changed.size === 0) return false;
const prefix = project.path === "." ? "" : `${project.path}/`;
for (const path of changed) {
if (project.path === "." || path === project.path || path.startsWith(prefix)) return true;
}
return false;
}
async function withTimeout(promise, timeoutMs, label) {
let timeout;
const timer = new Promise((_, reject) => {
timeout = setTimeout(
() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),
timeoutMs
);
});
try {
return await Promise.race([promise, timer]);
} finally {
if (timeout) clearTimeout(timeout);
}
}
async function mapLimited(items, concurrency, fn) {
const results = new Array(items.length);
let next = 0;
async function worker() {
while (next < items.length) {
const index = next++;
results[index] = await fn(items[index]);
}
}
await Promise.all(Array.from({ length: Math.max(1, concurrency) }, () => worker()));
return results;
}
async function createWorkspaceHealthReport(root = process.cwd(), options = {}) {
const workspaceRoot = resolve(root);
const config = readWorkspaceConfig(workspaceRoot);
const since = options.since ?? "origin/main";
const changed = options.changedOnly ? changedPaths(workspaceRoot, since) : /* @__PURE__ */ new Set();
const allProjects = listWorkspaceProjects(workspaceRoot);
const projects = options.changedOnly ? allProjects.filter((project) => projectChanged(project, changed)) : allProjects;
const concurrency = options.concurrency ?? config?.concurrency ?? 4;
const timeoutMs = options.timeoutMs ?? config?.timeoutMs ?? 12e4;
const checked = await mapLimited(projects, concurrency, async (project) => {
const startedAt = Date.now();
try {
const report = await withTimeout(
createProjectHealthReport(project.absolutePath, {
browser: options.browser ?? project.browser
}),
timeoutMs,
project.path
);
return {
id: project.id,
path: project.path,
status: report.status,
score: report.score,
errorCount: report.summary.errorCount,
warnCount: report.summary.warnCount,
infoCount: report.summary.infoCount,
findingCount: report.summary.findingCount,
durationMs: Date.now() - startedAt,
changed: options.changedOnly ? projectChanged(project, changed) : false,
source: project.source,
error: null,
loopState: report.loop.state,
loopNextAction: report.loop.nextActions[0] ?? null
};
} catch (error) {
return {
id: project.id,
path: project.path,
status: "failed",
score: 0,
errorCount: 1,
warnCount: 0,
infoCount: 0,
findingCount: 1,
durationMs: Date.now() - startedAt,
changed: options.changedOnly ? projectChanged(project, changed) : false,
source: project.source,
error: error.message,
loopState: "blocked_missing_context",
loopNextAction: "Fix the project health failure, then rerun workspace health."
};
}
});
const summary = {
projectCount: allProjects.length,
checkedCount: checked.length,
healthyCount: checked.filter((project) => project.status === "healthy").length,
warningCount: checked.filter((project) => project.status === "warning").length,
errorCount: checked.filter((project) => project.status === "error").length,
failedCount: checked.filter((project) => project.status === "failed").length
};
const blockedCount = checked.filter(
(project) => project.loopState?.startsWith("blocked") || project.loopState === "human_resolution_required"
).length;
const repairRequiredCount = checked.filter(
(project) => project.loopState === "repair_required"
).length;
const workspaceLoopState = checked.length === 0 ? "needs_context" : blockedCount > 0 ? "human_resolution_required" : repairRequiredCount > 0 || summary.errorCount > 0 || summary.warningCount > 0 ? "repair_required" : "verified";
const workspaceLoopStatus = workspaceLoopState === "human_resolution_required" || workspaceLoopState.startsWith("blocked") ? "blocked" : summary.errorCount > 0 || summary.failedCount > 0 ? "error" : summary.warningCount > 0 ? "warning" : "healthy";
return {
$schema: WORKSPACE_HEALTH_SCHEMA_URL,
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
workspaceRoot,
changedOnly: options.changedOnly ?? false,
since: options.changedOnly ? since : null,
summary,
loop: {
state: workspaceLoopState,
status: workspaceLoopStatus,
projectCount: checked.length,
blockedCount,
repairRequiredCount,
nextActions: [
workspaceLoopState === "verified" ? "Workspace loop verified." : 'Open the highest-risk project, run `decantr task <route> "<intent>"`, repair, then rerun `decantr verify`.'
]
},
projects: checked
};
}
function formatWorkspaceHealthText(report) {
const lines = [
`${BOLD}Decantr Workspace Health${RESET}`,
"",
`Projects: ${report.summary.checkedCount}/${report.summary.projectCount}`,
`Healthy: ${report.summary.healthyCount} | Warnings: ${report.summary.warningCount} | Errors: ${report.summary.errorCount} | Failed: ${report.summary.failedCount}`,
`Loop: ${report.loop.state} | blocked ${report.loop.blockedCount} | repair ${report.loop.repairRequiredCount}`,
""
];
for (const project of report.projects) {
const color = project.status === "healthy" ? GREEN : project.status === "warning" ? YELLOW : RED;
lines.push(
`${color}${String(project.status).toUpperCase()}${RESET} ${project.path} score ${project.score}/100 findings ${project.findingCount}`
);
if (project.error) lines.push(` ${DIM}${project.error}${RESET}`);
}
return `${lines.join("\n")}
`;
}
function formatWorkspaceHealthMarkdown(report) {
const lines = [
"# Decantr Workspace Health",
"",
`- Projects checked: **${report.summary.checkedCount}/${report.summary.projectCount}**`,
`- Healthy: ${report.summary.healthyCount}`,
`- Warnings: ${report.summary.warningCount}`,
`- Errors: ${report.summary.errorCount}`,
`- Failed: ${report.summary.failedCount}`,
"",
"| Project | Status | Score | Findings | Source |",
"| --- | --- | ---: | ---: | --- |"
];
for (const project of report.projects) {
lines.push(
`| \`${project.path}\` | ${project.status} | ${project.score} | ${project.findingCount} | ${project.source} |`
);
}
return `${lines.join("\n")}
`;
}
function shouldFailWorkspaceHealth(report, failOn = "error") {
if (failOn === "none") return false;
if (report.summary.failedCount > 0 || report.summary.errorCount > 0) return true;
return failOn === "warn" && report.summary.warningCount > 0;
}
function parseHealthFailOn(value) {
if (value === "warn" || value === "none") return value;
return "error";
}
function parseWorkspaceArgs(args) {
const subcommand = args[1] === "health" ? "health" : "list";
const options = { subcommand };
for (let index = 2; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--json") options.json = true;
else if (arg === "--markdown") options.markdown = true;
else if (arg === "--ci") options.ci = true;
else if (arg === "--browser") options.browser = true;
else if (arg === "--changed") options.changedOnly = true;
else if (arg === "--since" && args[index + 1]) options.since = args[++index];
else if (arg.startsWith("--since=")) options.since = arg.split("=")[1];
else if (arg === "--output" && args[index + 1]) options.output = args[++index];
else if (arg.startsWith("--output=")) options.output = arg.split("=")[1];
else if (arg === "--fail-on" && args[index + 1])
options.failOn = parseHealthFailOn(args[++index]);
else if (arg.startsWith("--fail-on=")) options.failOn = parseHealthFailOn(arg.split("=")[1]);
else if (arg === "--concurrency" && args[index + 1])
options.concurrency = Number(args[++index]);
else if (arg.startsWith("--concurrency=")) options.concurrency = Number(arg.split("=")[1]);
else if (arg === "--timeout-ms" && args[index + 1]) options.timeoutMs = Number(args[++index]);
else if (arg.startsWith("--timeout-ms=")) options.timeoutMs = Number(arg.split("=")[1]);
}
return options;
}
async function cmdWorkspace(workspaceRoot = process.cwd(), args = ["workspace"]) {
const options = parseWorkspaceArgs(args);
if (options.subcommand === "list") {
const projects = listWorkspaceProjects(workspaceRoot);
const candidates = listWorkspaceCandidates(workspaceRoot, projects);
const unattachedCandidates = candidates.filter((candidate) => !candidate.attached);
const payload2 = `${JSON.stringify({ projects, candidates }, null, 2)}
`;
if (options.json) {
process.stdout.write(payload2);
return;
}
console.log(`${BOLD}Decantr workspace projects${RESET}`);
console.log("");
console.log("Attached Decantr projects:");
if (projects.length === 0) {
console.log(` ${DIM}(none yet)${RESET}`);
} else {
for (const project of projects) {
console.log(` ${project.path} ${DIM}${project.source}${RESET}`);
}
}
if (candidates.length > 0) {
console.log("");
console.log("App candidates:");
for (const candidate of candidates) {
const status = candidate.attached ? `${GREEN}attached${RESET}` : `${YELLOW}unattached${RESET}`;
console.log(` ${candidate.path} ${DIM}${status}${RESET}`);
}
}
if (unattachedCandidates.length > 0) {
console.log("");
console.log(projects.length > 0 ? "Attach another app:" : "Start by attaching one app:");
console.log(` ${unattachedCandidates[0].suggestedAdoptCommand}`);
}
return;
}
const report = await createWorkspaceHealthReport(workspaceRoot, options);
const payload = options.json ? `${JSON.stringify(report, null, 2)}
` : options.markdown ? formatWorkspaceHealthMarkdown(report) : formatWorkspaceHealthText(report);
if (options.output) {
mkdirSync(dirname(resolve(workspaceRoot, options.output)), { recursive: true });
writeFileSync(resolve(workspaceRoot, options.output), payload, "utf-8");
if (!options.ci)
console.log(`${GREEN}Wrote Decantr workspace health:${RESET} ${options.output}`);
} else {
process.stdout.write(payload);
}
if (options.ci && shouldFailWorkspaceHealth(report, options.failOn ?? "error")) {
process.exitCode = 1;
}
}
export {
listWorkspaceProjects,
listWorkspaceCandidates,
createWorkspaceHealthReport,
formatWorkspaceHealthText,
formatWorkspaceHealthMarkdown,
shouldFailWorkspaceHealth,
parseWorkspaceArgs,
cmdWorkspace
};

Sorry, the diff of this file is too big to display

import {
cmdHealth,
collectDesignTokenEvidence,
createProjectEvidenceBundle,
createProjectHealthReport,
formatDiagnosticCatalogJson,
formatDiagnosticCatalogMarkdown,
formatDiagnosticCatalogText,
formatProjectHealthJson,
formatProjectHealthMarkdown,
formatProjectHealthText,
parseHealthArgs,
renderProjectHealthCiWorkflow,
shouldFailHealth,
writeProjectHealthCiWorkflow
} from "./chunk-U62GGKUO.js";
import "./chunk-24JR4ZNG.js";
export {
cmdHealth,
collectDesignTokenEvidence,
createProjectEvidenceBundle,
createProjectHealthReport,
formatDiagnosticCatalogJson,
formatDiagnosticCatalogMarkdown,
formatDiagnosticCatalogText,
formatProjectHealthJson,
formatProjectHealthMarkdown,
formatProjectHealthText,
parseHealthArgs,
renderProjectHealthCiWorkflow,
shouldFailHealth,
writeProjectHealthCiWorkflow
};

Sorry, the diff of this file is too big to display

import {
cmdWorkspace,
createWorkspaceHealthReport,
formatWorkspaceHealthMarkdown,
formatWorkspaceHealthText,
listWorkspaceCandidates,
listWorkspaceProjects,
parseWorkspaceArgs,
shouldFailWorkspaceHealth
} from "./chunk-FIWVRMZN.js";
import "./chunk-U62GGKUO.js";
import "./chunk-24JR4ZNG.js";
export {
cmdWorkspace,
createWorkspaceHealthReport,
formatWorkspaceHealthMarkdown,
formatWorkspaceHealthText,
listWorkspaceCandidates,
listWorkspaceProjects,
parseWorkspaceArgs,
shouldFailWorkspaceHealth
};