@decantr/cli 3.4.1 → 3.5.0

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.
@@ -562,6 +562,11 @@ function detectPathnameBranchRoutes(content) {
562
562
  if (strongMatches > 0 || hasPathnameSignal) {
563
563
  collectRouteLiterals(/\b(?:href|to)\s*=\s*["'`](\/[^"'`]+)["'`]/g, content, routes);
564
564
  }
565
+ if (hasPathnameSignal && (/\?\s*["'`]\/["'`]\s*:/.test(content) || /\|\|\s*["'`]\/["'`]/.test(content) || /\b(?:defaultRoute|defaultPath|fallbackRoute|fallbackPath)\s*[:=]\s*["'`]\/["'`]/.test(
566
+ content
567
+ ))) {
568
+ routes.add("/");
569
+ }
565
570
  return [...routes];
566
571
  }
567
572
  function hasReactRouterDependency(projectRoot) {
@@ -1441,6 +1446,18 @@ function loadJsonEntries(dir) {
1441
1446
  return [];
1442
1447
  }
1443
1448
  }
1449
+ function readJson(path) {
1450
+ if (!existsSync7(path)) return null;
1451
+ try {
1452
+ return JSON.parse(readFileSync7(path, "utf-8"));
1453
+ } catch {
1454
+ return null;
1455
+ }
1456
+ }
1457
+ function addPattern(registry, id, source, data = null) {
1458
+ if (typeof id !== "string" || !id.trim() || registry.has(id)) return;
1459
+ registry.set(id, data ?? { id, source });
1460
+ }
1444
1461
  function buildGuardRegistryContext(projectRoot = process.cwd()) {
1445
1462
  const themeRegistry = /* @__PURE__ */ new Map();
1446
1463
  const patternRegistry = /* @__PURE__ */ new Map();
@@ -1461,21 +1478,22 @@ function buildGuardRegistryContext(projectRoot = process.cwd()) {
1461
1478
  }
1462
1479
  }
1463
1480
  for (const data of loadJsonEntries(join7(cacheDir, "@official", "patterns"))) {
1464
- if (typeof data.id === "string" && !patternRegistry.has(data.id)) {
1465
- patternRegistry.set(data.id, data);
1466
- }
1481
+ addPattern(patternRegistry, data.id, "cache", data);
1467
1482
  }
1468
1483
  for (const entry of loadBundledContentList("patterns")) {
1469
1484
  const data = entry.data;
1470
1485
  const id = typeof data.id === "string" ? data.id : entry.id;
1471
- if (!patternRegistry.has(id)) {
1472
- patternRegistry.set(id, data);
1473
- }
1486
+ addPattern(patternRegistry, id, "bundled", data);
1474
1487
  }
1475
1488
  for (const data of loadJsonEntries(join7(customDir, "patterns"))) {
1476
- if (typeof data.id === "string") {
1477
- patternRegistry.set(data.id, data);
1478
- }
1489
+ addPattern(patternRegistry, data.id, "custom", data);
1490
+ }
1491
+ const localPatterns = readJson(join7(projectRoot, ".decantr", "local-patterns.json"));
1492
+ const patterns = Array.isArray(localPatterns?.patterns) ? localPatterns.patterns : [];
1493
+ for (const pattern of patterns) {
1494
+ if (!pattern || typeof pattern !== "object") continue;
1495
+ const data = pattern;
1496
+ addPattern(patternRegistry, data.id, "local-law", data);
1479
1497
  }
1480
1498
  return { themeRegistry, patternRegistry };
1481
1499
  }
@@ -1,19 +1,20 @@
1
1
  import {
2
2
  createProjectHealthReport,
3
3
  listWorkspaceAppCandidates
4
- } from "./chunk-BDVI3SSP.js";
4
+ } from "./chunk-T45GFC6F.js";
5
5
 
6
6
  // src/commands/workspace.ts
7
7
  import { execFileSync } from "child_process";
8
8
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
9
9
  import { dirname, join, relative, resolve } from "path";
10
+ import { WORKSPACE_HEALTH_REPORT_V2_SCHEMA_URL } from "@decantr/verifier";
10
11
  var BOLD = "\x1B[1m";
11
12
  var DIM = "\x1B[2m";
12
13
  var GREEN = "\x1B[32m";
13
14
  var RED = "\x1B[31m";
14
15
  var YELLOW = "\x1B[33m";
15
16
  var RESET = "\x1B[0m";
16
- var WORKSPACE_HEALTH_SCHEMA_URL = "https://decantr.ai/schemas/workspace-health-report.v1.json";
17
+ var WORKSPACE_HEALTH_SCHEMA_URL = WORKSPACE_HEALTH_REPORT_V2_SCHEMA_URL;
17
18
  var DEFAULT_IGNORES = /* @__PURE__ */ new Set([
18
19
  ".git",
19
20
  ".next",
@@ -181,7 +182,9 @@ async function createWorkspaceHealthReport(root = process.cwd(), options = {}) {
181
182
  durationMs: Date.now() - startedAt,
182
183
  changed: options.changedOnly ? projectChanged(project, changed) : false,
183
184
  source: project.source,
184
- error: null
185
+ error: null,
186
+ loopState: report.loop.state,
187
+ loopNextAction: report.loop.nextActions[0] ?? null
185
188
  };
186
189
  } catch (error) {
187
190
  return {
@@ -196,23 +199,44 @@ async function createWorkspaceHealthReport(root = process.cwd(), options = {}) {
196
199
  durationMs: Date.now() - startedAt,
197
200
  changed: options.changedOnly ? projectChanged(project, changed) : false,
198
201
  source: project.source,
199
- error: error.message
202
+ error: error.message,
203
+ loopState: "blocked_missing_context",
204
+ loopNextAction: "Fix the project health failure, then rerun workspace health."
200
205
  };
201
206
  }
202
207
  });
208
+ const summary = {
209
+ projectCount: allProjects.length,
210
+ checkedCount: checked.length,
211
+ healthyCount: checked.filter((project) => project.status === "healthy").length,
212
+ warningCount: checked.filter((project) => project.status === "warning").length,
213
+ errorCount: checked.filter((project) => project.status === "error").length,
214
+ failedCount: checked.filter((project) => project.status === "failed").length
215
+ };
216
+ const blockedCount = checked.filter(
217
+ (project) => project.loopState?.startsWith("blocked") || project.loopState === "human_resolution_required"
218
+ ).length;
219
+ const repairRequiredCount = checked.filter(
220
+ (project) => project.loopState === "repair_required"
221
+ ).length;
222
+ const workspaceLoopState = checked.length === 0 ? "needs_context" : blockedCount > 0 ? "human_resolution_required" : repairRequiredCount > 0 || summary.errorCount > 0 || summary.warningCount > 0 ? "repair_required" : "verified";
223
+ const workspaceLoopStatus = workspaceLoopState === "human_resolution_required" || workspaceLoopState.startsWith("blocked") ? "blocked" : summary.errorCount > 0 || summary.failedCount > 0 ? "error" : summary.warningCount > 0 ? "warning" : "healthy";
203
224
  return {
204
225
  $schema: WORKSPACE_HEALTH_SCHEMA_URL,
205
226
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
206
227
  workspaceRoot,
207
228
  changedOnly: options.changedOnly ?? false,
208
229
  since: options.changedOnly ? since : null,
209
- summary: {
210
- projectCount: allProjects.length,
211
- checkedCount: checked.length,
212
- healthyCount: checked.filter((project) => project.status === "healthy").length,
213
- warningCount: checked.filter((project) => project.status === "warning").length,
214
- errorCount: checked.filter((project) => project.status === "error").length,
215
- failedCount: checked.filter((project) => project.status === "failed").length
230
+ summary,
231
+ loop: {
232
+ state: workspaceLoopState,
233
+ status: workspaceLoopStatus,
234
+ projectCount: checked.length,
235
+ blockedCount,
236
+ repairRequiredCount,
237
+ nextActions: [
238
+ workspaceLoopState === "verified" ? "Workspace loop verified." : 'Open the highest-risk project, run `decantr task <route> "<intent>"`, repair, then rerun `decantr verify`.'
239
+ ]
216
240
  },
217
241
  projects: checked
218
242
  };
@@ -223,6 +247,7 @@ function formatWorkspaceHealthText(report) {
223
247
  "",
224
248
  `Projects: ${report.summary.checkedCount}/${report.summary.projectCount}`,
225
249
  `Healthy: ${report.summary.healthyCount} | Warnings: ${report.summary.warningCount} | Errors: ${report.summary.errorCount} | Failed: ${report.summary.failedCount}`,
250
+ `Loop: ${report.loop.state} | blocked ${report.loop.blockedCount} | repair ${report.loop.repairRequiredCount}`,
226
251
  ""
227
252
  ];
228
253
  for (const project of report.projects) {
@@ -3,7 +3,7 @@ import {
3
3
  sendProjectHealthCiFailedTelemetry,
4
4
  sendProjectHealthPromptTelemetry,
5
5
  sendProjectHealthReportTelemetry
6
- } from "./chunk-2GCVVEQC.js";
6
+ } from "./chunk-PM7DKABI.js";
7
7
 
8
8
  // src/commands/health.ts
9
9
  import { execFileSync as execFileSync2 } from "child_process";
@@ -16,10 +16,14 @@ import {
16
16
  anchorFindingsToGraph,
17
17
  auditProject,
18
18
  buildProjectHealthRepairPlan,
19
+ createAuthorityResolution,
19
20
  createContractAssertions,
20
21
  createEvidenceBundle,
22
+ createEvidenceTier,
23
+ createLoopReadiness,
21
24
  deriveVerificationDiagnostic,
22
- KNOWN_VERIFICATION_DIAGNOSTICS
25
+ KNOWN_VERIFICATION_DIAGNOSTICS,
26
+ PROJECT_HEALTH_REPORT_V2_SCHEMA_URL
23
27
  } from "@decantr/verifier";
24
28
 
25
29
  // src/workspace.ts
@@ -3210,7 +3214,7 @@ var RED2 = "\x1B[31m";
3210
3214
  var GREEN2 = "\x1B[32m";
3211
3215
  var CYAN = "\x1B[36m";
3212
3216
  var YELLOW = "\x1B[33m";
3213
- var PROJECT_HEALTH_SCHEMA_URL = "https://decantr.ai/schemas/project-health-report.v1.json";
3217
+ var PROJECT_HEALTH_SCHEMA_URL = PROJECT_HEALTH_REPORT_V2_SCHEMA_URL;
3214
3218
  var DEFAULT_HEALTH_CI_WORKFLOW_PATH = ".github/workflows/decantr-health.yml";
3215
3219
  var DEFAULT_HEALTH_CI_REPORT_PATH = "decantr-health.md";
3216
3220
  var DEFAULT_HEALTH_CI_JSON_PATH = "decantr-health.json";
@@ -4860,7 +4864,7 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
4860
4864
  }));
4861
4865
  const finalCounts = countFindings(repairPlanFindings);
4862
4866
  const commandContext = commandContextForProject(projectRoot);
4863
- return {
4867
+ const baseReport = {
4864
4868
  $schema: PROJECT_HEALTH_SCHEMA_URL,
4865
4869
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4866
4870
  projectRoot,
@@ -4901,12 +4905,69 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
4901
4905
  },
4902
4906
  findings: repairPlanFindings
4903
4907
  };
4908
+ const evidenceTier = createEvidenceTier(baseReport, {
4909
+ runtimeProbeCount: browserVerification ? Math.max(1, browserVerification.evidence.screenshots.length) : void 0,
4910
+ visualArtifactCount: browserVerification?.evidence.screenshots.length ?? 0
4911
+ });
4912
+ const authority = createAuthorityResolution(baseReport);
4913
+ const loop = createLoopReadiness(baseReport, authority, evidenceTier);
4914
+ return {
4915
+ ...baseReport,
4916
+ evidenceTier,
4917
+ authority,
4918
+ loop,
4919
+ findings: repairPlanFindings.map((finding) => {
4920
+ const conflict = authority.conflicts.find((entry) => entry.id === finding.id);
4921
+ return {
4922
+ ...finding,
4923
+ evidenceTier,
4924
+ authorityLane: conflict?.lane ?? authority.activeLane,
4925
+ resolutionActions: conflict?.recommendedActions,
4926
+ privacy: {
4927
+ sourceIncluded: false,
4928
+ redacted: true,
4929
+ localOnly: true
4930
+ },
4931
+ loopVerdict: loop.state
4932
+ };
4933
+ })
4934
+ };
4904
4935
  }
4905
4936
  function colorForStatus(status) {
4906
4937
  if (status === "healthy") return GREEN2;
4907
4938
  if (status === "warning") return YELLOW;
4908
4939
  return RED2;
4909
4940
  }
4941
+ var DNA_RULES = /* @__PURE__ */ new Set(["style", "density", "accessibility", "theme-mode", "theme"]);
4942
+ var BLUEPRINT_RULES = /* @__PURE__ */ new Set([
4943
+ "structure",
4944
+ "layout",
4945
+ "pattern-exists",
4946
+ "page-route-required",
4947
+ "page-pack-count-mismatch"
4948
+ ]);
4949
+ function formatHumanFindingLabel(finding) {
4950
+ const severity = finding.severity;
4951
+ const rule = finding.rule?.toLowerCase() ?? "";
4952
+ const category = finding.category.toLowerCase();
4953
+ const source = finding.source;
4954
+ if (source === "brownfield" || rule.startsWith("brownfield-")) {
4955
+ return "Brownfield drift";
4956
+ }
4957
+ if (source === "style-bridge") {
4958
+ return `Style bridge ${severity}`;
4959
+ }
4960
+ if (source === "interaction") {
4961
+ return `Interaction ${severity}`;
4962
+ }
4963
+ if (DNA_RULES.has(rule) || category.includes("dna")) {
4964
+ return `DNA ${severity}`;
4965
+ }
4966
+ if (BLUEPRINT_RULES.has(rule) || category.includes("blueprint")) {
4967
+ return `Blueprint ${severity === "error" ? "error" : "warning"}`;
4968
+ }
4969
+ return severity.toUpperCase();
4970
+ }
4910
4971
  function formatProjectHealthText(report) {
4911
4972
  const color = colorForStatus(report.status);
4912
4973
  const commandContext = commandContextForProject(report.projectRoot);
@@ -4924,6 +4985,11 @@ function formatProjectHealthText(report) {
4924
4985
  ` Packs: manifest ${report.packs.manifestPresent ? "present" : "missing"} | review ${report.packs.reviewPackPresent ? "present" : "missing"} | pages ${report.packs.pagePackCount}`,
4925
4986
  ` Graph: ${report.graph.current === null ? "not attached" : report.graph.current ? "current" : "stale"} | capsule ${report.graph.capsulePresent ? "present" : "missing"} | sources ${report.graph.sourceArtifactCount}`,
4926
4987
  "",
4988
+ `${BOLD}Control loop:${RESET2}`,
4989
+ ` State: ${report.loop.state} | evidence ${report.evidenceTier.confidence.level} (${report.evidenceTier.confidence.score})`,
4990
+ ` Authority: ${report.authority.activeLane} \u2014 ${report.authority.summary}`,
4991
+ ` Next: ${report.loop.nextActions[0] ?? report.loop.verifyCommand}`,
4992
+ "",
4927
4993
  `${BOLD}Findings:${RESET2}`
4928
4994
  ];
4929
4995
  if (report.findings.length === 0) {
@@ -4932,7 +4998,7 @@ function formatProjectHealthText(report) {
4932
4998
  for (const finding of report.findings) {
4933
4999
  const findingColor = finding.severity === "error" ? RED2 : finding.severity === "warn" ? YELLOW : CYAN;
4934
5000
  lines.push(
4935
- ` ${findingColor}[${finding.severity.toUpperCase()}]${RESET2} ${finding.id}: ${finding.message}`
5001
+ ` ${findingColor}[${formatHumanFindingLabel(finding)}]${RESET2} ${finding.id}: ${finding.message}`
4936
5002
  );
4937
5003
  if (finding.evidence.length > 0) {
4938
5004
  lines.push(` ${DIM2}${finding.evidence[0]}${RESET2}`);
@@ -4956,6 +5022,7 @@ function formatProjectHealthText(report) {
4956
5022
  }
4957
5023
  lines.push("");
4958
5024
  lines.push(`${BOLD}CI:${RESET2} ${report.ci.recommendedCommand}`);
5025
+ lines.push(`${BOLD}Loop verify:${RESET2} ${report.loop.verifyCommand}`);
4959
5026
  return `${lines.join("\n")}
4960
5027
  `;
4961
5028
  }
@@ -4971,6 +5038,9 @@ function formatProjectHealthMarkdown(report) {
4971
5038
  `- Runtime audit: ${report.summary.runtimeAuditChecked ? report.summary.runtimePassed ? "passed" : "failed" : "not checked"}`,
4972
5039
  `- Packs: manifest ${report.packs.manifestPresent ? "present" : "missing"}, review ${report.packs.reviewPackPresent ? "present" : "missing"}`,
4973
5040
  `- Graph: ${report.graph.current === null ? "not attached" : report.graph.current ? "current" : "stale"}, capsule ${report.graph.capsulePresent ? "present" : "missing"}, sources ${report.graph.sourceArtifactCount}`,
5041
+ `- Loop: **${report.loop.state}** (${report.loop.status})`,
5042
+ `- Evidence tier: **${report.evidenceTier.stage}** / ${report.evidenceTier.confidence.level}`,
5043
+ `- Authority: **${report.authority.activeLane}**`,
4974
5044
  "",
4975
5045
  "## Findings",
4976
5046
  ""
@@ -5054,6 +5124,12 @@ function formatDiagnosticCatalogText() {
5054
5124
  async function createProjectEvidenceBundle(projectRoot, report, options = {}) {
5055
5125
  const audit = await auditProject(projectRoot);
5056
5126
  const assertions = createContractAssertions(projectRoot, audit);
5127
+ const visualManifestPath = join5(projectRoot, ".decantr", "evidence", "visual-manifest.json");
5128
+ const browserEvidence = await browserEvidenceFromOptions(
5129
+ projectRoot,
5130
+ options,
5131
+ report.routes.declared
5132
+ );
5057
5133
  return createEvidenceBundle({
5058
5134
  projectRoot,
5059
5135
  report,
@@ -5061,7 +5137,36 @@ async function createProjectEvidenceBundle(projectRoot, report, options = {}) {
5061
5137
  assertions,
5062
5138
  workspaceConfigPath: existsSync5(join5(projectRoot, ".decantr", "workspace.json")) ? join5(projectRoot, ".decantr", "workspace.json") : null,
5063
5139
  designTokensPath: resolveOptionalPath(projectRoot, options.designTokensPath) ?? null,
5064
- browser: await browserEvidenceFromOptions(projectRoot, options, report.routes.declared),
5140
+ visualManifestPath: existsSync5(visualManifestPath) ? visualManifestPath : null,
5141
+ artifacts: [
5142
+ {
5143
+ id: "artifact:evidence-bundle",
5144
+ kind: "evidence-bundle",
5145
+ path: ".decantr/evidence/evidence-bundle.json",
5146
+ hash: null,
5147
+ localOnly: true,
5148
+ redacted: true
5149
+ },
5150
+ ...existsSync5(visualManifestPath) ? [
5151
+ {
5152
+ id: "artifact:visual-manifest",
5153
+ kind: "visual-manifest",
5154
+ path: ".decantr/evidence/visual-manifest.json",
5155
+ hash: null,
5156
+ localOnly: true,
5157
+ redacted: true
5158
+ }
5159
+ ] : [],
5160
+ ...(browserEvidence?.screenshots ?? []).map((screenshot, index) => ({
5161
+ id: `artifact:screenshot:${index + 1}`,
5162
+ kind: "screenshot",
5163
+ path: screenshot,
5164
+ hash: null,
5165
+ localOnly: true,
5166
+ redacted: false
5167
+ }))
5168
+ ],
5169
+ browser: browserEvidence,
5065
5170
  designTokens: collectDesignTokenEvidence(projectRoot, options.designTokensPath)
5066
5171
  });
5067
5172
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  cmdHeal,
3
3
  collectCheckIssues
4
- } from "./chunk-2GCVVEQC.js";
4
+ } from "./chunk-PM7DKABI.js";
5
5
  export {
6
6
  cmdHeal,
7
7
  collectCheckIssues
@@ -13,8 +13,8 @@ import {
13
13
  renderProjectHealthCiWorkflow,
14
14
  shouldFailHealth,
15
15
  writeProjectHealthCiWorkflow
16
- } from "./chunk-BDVI3SSP.js";
17
- import "./chunk-2GCVVEQC.js";
16
+ } from "./chunk-T45GFC6F.js";
17
+ import "./chunk-PM7DKABI.js";
18
18
  export {
19
19
  cmdHealth,
20
20
  collectDesignTokenEvidence,
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import "./chunk-33IZQXZH.js";
1
+ import "./chunk-LTNGAAV4.js";
2
2
  import "./chunk-SIDKK73N.js";
3
- import "./chunk-UGRAHWCG.js";
4
- import "./chunk-BDVI3SSP.js";
5
- import "./chunk-2GCVVEQC.js";
3
+ import "./chunk-RJ6PT7WP.js";
4
+ import "./chunk-T45GFC6F.js";
5
+ import "./chunk-PM7DKABI.js";