@decantr/cli 3.4.2 → 3.5.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 CHANGED
@@ -22,7 +22,7 @@ Use `decantr setup` when you are unsure which path applies. It detects whether t
22
22
  Use `decantr scan` when you want a zero-commit Brownfield preview. It reads local files, detects framework/routes/styling/static-hosting/assistant-rule signals, previews typed Contract graph readiness in memory when a Decantr contract already exists, reports the contract capsule source-handle count and limit, prints a terminal report, and writes no `.decantr` files or report artifacts. Add `--json` when automation needs the `ScanReportV1` payload.
23
23
  Use `decantr new` for a greenfield workspace in a fresh directory. With a starter-kit blueprint or archetype it uses the runnable adapter and Decantr CSS; without certified vocabulary content it creates a contract-only workspace unless you explicitly pass `--adoption=decantr-css`.
24
24
  Use `decantr adopt` when you already have an app and want Decantr governance without adopting a blueprint. Brownfield attach is proposal-driven: Decantr inventories the app, writes an observed essence proposal, hydrates hosted execution packs when online, and only applies the contract when you explicitly accept or merge it.
25
- Use `decantr studio` after adoption when you want a local visual view of routes, findings, and attention areas. 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, 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. 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.
25
+ 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.
26
26
  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.
27
27
  Use `decantr init`, `decantr analyze`, `decantr check`, and `decantr health` as advanced primitives when you need direct control over one step.
28
28
 
@@ -50,6 +50,7 @@ decantr studio
50
50
  decantr doctor
51
51
  decantr ci --fail-on error
52
52
  decantr ci init
53
+ decantr resolve
53
54
  decantr codify --from-audit --style-bridge
54
55
  decantr codify --map-pattern hero
55
56
  decantr codify --accept
@@ -130,6 +131,7 @@ decantr adopt --yes
130
131
  decantr adopt --project apps/web --yes
131
132
  decantr doctor
132
133
  decantr doctor --project apps/web
134
+ decantr resolve --project apps/web
133
135
  decantr codify --from-audit --style-bridge
134
136
  decantr codify --map-pattern hero --project apps/web
135
137
  decantr codify --accept
@@ -182,7 +184,7 @@ decantr showcase verification --json
182
184
 
183
185
  `decantr ci` is the blessed non-mutating automation gate. It runs the Project Health surface with adoption-mode-aware local law checks and emits a schema-backed CI report. `decantr ci init` writes root GitHub workflows or portable generic snippets using the detected package manager and pinned local CLI command instead of `@latest`; if the root manifest has not pinned Decantr yet, it prints the exact install command first.
184
186
 
185
- `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 `ProjectHealthReport` with a status, score, route summary, pack summary, findings, stable diagnostic codes, typed repair IDs, 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.
187
+ `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.
186
188
 
187
189
  ```bash
188
190
  decantr verify
@@ -230,7 +232,7 @@ decantr verify --workspace --changed --since origin/main
230
232
 
231
233
  In observed Brownfield projects, common section shorthands are accepted for page and feature additions when they resolve unambiguously. For example, `decantr add page app/settings --route /settings --project apps/web` and `decantr add feature saved-recipes --section app --project apps/web` resolve `app` to the single primary section, such as `observed-primary`, so docs and LLM prompts do not have to guess generated section IDs first.
232
234
 
233
- `decantr studio` starts a local-only dashboard powered by the same report. It uses Node built-ins only and serves `GET /`, `GET /api/health`, and `POST /api/refresh`.
235
+ `decantr studio` starts a local-only Control Room powered by the same v2 report. It uses Node built-ins only and serves `GET /`, `GET /api/health`, `GET /api/control-room`, `GET /api/resolve`, `GET /api/evidence`, `GET /api/graph-impact`, `GET /api/task-preview`, `GET /api/proof`, and `POST /api/refresh`.
234
236
 
235
237
  ```bash
236
238
  decantr studio
@@ -239,7 +241,7 @@ decantr studio --report decantr-health.json
239
241
  decantr studio --workspace
240
242
  ```
241
243
 
242
- Studio is for local triage, not Decantr admin telemetry. The Overview keeps the first decision simple: pick the issue to fix first, review the full AI repair prompt before copying it, switch to manual guidance or commands, and expand project details when route/runtime/pack evidence matters. The tabs cover Overview, Routes, Drift, Findings, Remediation, CI, and Packs without uploading source code, prompts, file paths, or project data.
244
+ Studio is for local triage, not Decantr admin telemetry. The Control Room keeps the first decision simple: inspect loop state, next action, authority lane, blocking findings, evidence tier, graph impact, and copyable commands. The views cover Control Room, Routes, Graph Impact, Authority Resolver, Evidence, Repairs, and CI/Benchmarks without uploading source code, prompts, file paths, or project data.
243
245
 
244
246
  Workspace Studio uses `decantr workspace health` behind `GET /api/workspace` and `POST /api/workspace/refresh` so large monorepos can triage many Decantr projects from one local dashboard.
245
247
 
package/dist/bin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-32WTNJQU.js";
2
+ import "./chunk-S3ZDXBRM.js";
3
3
  import "./chunk-SIDKK73N.js";
4
- import "./chunk-BDA6TWV3.js";
5
- import "./chunk-PQKTJGYL.js";
6
- import "./chunk-2GCVVEQC.js";
4
+ import "./chunk-MDQSTAP5.js";
5
+ import "./chunk-EUEQMXN5.js";
6
+ import "./chunk-PM7DKABI.js";
@@ -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
@@ -2729,18 +2733,20 @@ function buildGraphArtifacts(projectRoot, options = {}) {
2729
2733
  const sources = sourceArtifacts(projectRoot, componentReuseAudit);
2730
2734
  const combinedSourceHash = sourceHash(sources);
2731
2735
  const previousSnapshot = readJsonFile3(paths.snapshot);
2732
- const createdAt = previousSnapshot?.source_hash === combinedSourceHash ? previousSnapshot.created_at : (/* @__PURE__ */ new Date()).toISOString();
2736
+ const previousDiff = readJsonFile3(paths.diff);
2737
+ const sourceUnchanged = previousSnapshot?.source_hash === combinedSourceHash;
2738
+ const createdAt = sourceUnchanged ? previousSnapshot.created_at : (/* @__PURE__ */ new Date()).toISOString();
2733
2739
  const snapshotId = `graph:${combinedSourceHash.replace(/^sha256:/, "").slice(0, 12)}`;
2734
2740
  paths = withSnapshotHistoryPath(paths, snapshotId);
2735
2741
  const baseSnapshot = buildGraphSnapshotFromEssence(essence, {
2736
2742
  snapshotId,
2737
- parentId: previousSnapshot && previousSnapshot.id !== snapshotId ? previousSnapshot.id : void 0,
2743
+ parentId: sourceUnchanged ? previousSnapshot.parent_id : previousSnapshot ? previousSnapshot.id : void 0,
2738
2744
  sourceHash: combinedSourceHash,
2739
2745
  createdAt,
2740
2746
  sourceArtifact: sources[0]
2741
2747
  });
2742
2748
  const snapshot = augmentProjectGraph(baseSnapshot, projectRoot, sources, componentReuseAudit);
2743
- const diff = previousSnapshot ? diffGraphSnapshots(previousSnapshot, snapshot) : {
2749
+ const diff = sourceUnchanged && previousDiff?.to === snapshot.id ? previousDiff : previousSnapshot ? diffGraphSnapshots(previousSnapshot, snapshot) : {
2744
2750
  $schema: GRAPH_DIFF_SCHEMA_URL,
2745
2751
  id: `diff:${snapshot.id}:${snapshot.id}`,
2746
2752
  from: snapshot.id,
@@ -3210,7 +3216,7 @@ var RED2 = "\x1B[31m";
3210
3216
  var GREEN2 = "\x1B[32m";
3211
3217
  var CYAN = "\x1B[36m";
3212
3218
  var YELLOW = "\x1B[33m";
3213
- var PROJECT_HEALTH_SCHEMA_URL = "https://decantr.ai/schemas/project-health-report.v1.json";
3219
+ var PROJECT_HEALTH_SCHEMA_URL = PROJECT_HEALTH_REPORT_V2_SCHEMA_URL;
3214
3220
  var DEFAULT_HEALTH_CI_WORKFLOW_PATH = ".github/workflows/decantr-health.yml";
3215
3221
  var DEFAULT_HEALTH_CI_REPORT_PATH = "decantr-health.md";
3216
3222
  var DEFAULT_HEALTH_CI_JSON_PATH = "decantr-health.json";
@@ -4860,7 +4866,7 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
4860
4866
  }));
4861
4867
  const finalCounts = countFindings(repairPlanFindings);
4862
4868
  const commandContext = commandContextForProject(projectRoot);
4863
- return {
4869
+ const baseReport = {
4864
4870
  $schema: PROJECT_HEALTH_SCHEMA_URL,
4865
4871
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4866
4872
  projectRoot,
@@ -4901,6 +4907,33 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
4901
4907
  },
4902
4908
  findings: repairPlanFindings
4903
4909
  };
4910
+ const evidenceTier = createEvidenceTier(baseReport, {
4911
+ runtimeProbeCount: browserVerification ? Math.max(1, browserVerification.evidence.screenshots.length) : void 0,
4912
+ visualArtifactCount: browserVerification?.evidence.screenshots.length ?? 0
4913
+ });
4914
+ const authority = createAuthorityResolution(baseReport);
4915
+ const loop = createLoopReadiness(baseReport, authority, evidenceTier);
4916
+ return {
4917
+ ...baseReport,
4918
+ evidenceTier,
4919
+ authority,
4920
+ loop,
4921
+ findings: repairPlanFindings.map((finding) => {
4922
+ const conflict = authority.conflicts.find((entry) => entry.id === finding.id);
4923
+ return {
4924
+ ...finding,
4925
+ evidenceTier,
4926
+ authorityLane: conflict?.lane ?? authority.activeLane,
4927
+ resolutionActions: conflict?.recommendedActions,
4928
+ privacy: {
4929
+ sourceIncluded: false,
4930
+ redacted: true,
4931
+ localOnly: true
4932
+ },
4933
+ loopVerdict: loop.state
4934
+ };
4935
+ })
4936
+ };
4904
4937
  }
4905
4938
  function colorForStatus(status) {
4906
4939
  if (status === "healthy") return GREEN2;
@@ -4954,6 +4987,11 @@ function formatProjectHealthText(report) {
4954
4987
  ` Packs: manifest ${report.packs.manifestPresent ? "present" : "missing"} | review ${report.packs.reviewPackPresent ? "present" : "missing"} | pages ${report.packs.pagePackCount}`,
4955
4988
  ` Graph: ${report.graph.current === null ? "not attached" : report.graph.current ? "current" : "stale"} | capsule ${report.graph.capsulePresent ? "present" : "missing"} | sources ${report.graph.sourceArtifactCount}`,
4956
4989
  "",
4990
+ `${BOLD}Control loop:${RESET2}`,
4991
+ ` State: ${report.loop.state} | evidence ${report.evidenceTier.confidence.level} (${report.evidenceTier.confidence.score})`,
4992
+ ` Authority: ${report.authority.activeLane} \u2014 ${report.authority.summary}`,
4993
+ ` Next: ${report.loop.nextActions[0] ?? report.loop.verifyCommand}`,
4994
+ "",
4957
4995
  `${BOLD}Findings:${RESET2}`
4958
4996
  ];
4959
4997
  if (report.findings.length === 0) {
@@ -4986,6 +5024,7 @@ function formatProjectHealthText(report) {
4986
5024
  }
4987
5025
  lines.push("");
4988
5026
  lines.push(`${BOLD}CI:${RESET2} ${report.ci.recommendedCommand}`);
5027
+ lines.push(`${BOLD}Loop verify:${RESET2} ${report.loop.verifyCommand}`);
4989
5028
  return `${lines.join("\n")}
4990
5029
  `;
4991
5030
  }
@@ -5001,6 +5040,9 @@ function formatProjectHealthMarkdown(report) {
5001
5040
  `- Runtime audit: ${report.summary.runtimeAuditChecked ? report.summary.runtimePassed ? "passed" : "failed" : "not checked"}`,
5002
5041
  `- Packs: manifest ${report.packs.manifestPresent ? "present" : "missing"}, review ${report.packs.reviewPackPresent ? "present" : "missing"}`,
5003
5042
  `- Graph: ${report.graph.current === null ? "not attached" : report.graph.current ? "current" : "stale"}, capsule ${report.graph.capsulePresent ? "present" : "missing"}, sources ${report.graph.sourceArtifactCount}`,
5043
+ `- Loop: **${report.loop.state}** (${report.loop.status})`,
5044
+ `- Evidence tier: **${report.evidenceTier.stage}** / ${report.evidenceTier.confidence.level}`,
5045
+ `- Authority: **${report.authority.activeLane}**`,
5004
5046
  "",
5005
5047
  "## Findings",
5006
5048
  ""
@@ -5084,6 +5126,12 @@ function formatDiagnosticCatalogText() {
5084
5126
  async function createProjectEvidenceBundle(projectRoot, report, options = {}) {
5085
5127
  const audit = await auditProject(projectRoot);
5086
5128
  const assertions = createContractAssertions(projectRoot, audit);
5129
+ const visualManifestPath = join5(projectRoot, ".decantr", "evidence", "visual-manifest.json");
5130
+ const browserEvidence = await browserEvidenceFromOptions(
5131
+ projectRoot,
5132
+ options,
5133
+ report.routes.declared
5134
+ );
5087
5135
  return createEvidenceBundle({
5088
5136
  projectRoot,
5089
5137
  report,
@@ -5091,7 +5139,36 @@ async function createProjectEvidenceBundle(projectRoot, report, options = {}) {
5091
5139
  assertions,
5092
5140
  workspaceConfigPath: existsSync5(join5(projectRoot, ".decantr", "workspace.json")) ? join5(projectRoot, ".decantr", "workspace.json") : null,
5093
5141
  designTokensPath: resolveOptionalPath(projectRoot, options.designTokensPath) ?? null,
5094
- browser: await browserEvidenceFromOptions(projectRoot, options, report.routes.declared),
5142
+ visualManifestPath: existsSync5(visualManifestPath) ? visualManifestPath : null,
5143
+ artifacts: [
5144
+ {
5145
+ id: "artifact:evidence-bundle",
5146
+ kind: "evidence-bundle",
5147
+ path: ".decantr/evidence/evidence-bundle.json",
5148
+ hash: null,
5149
+ localOnly: true,
5150
+ redacted: true
5151
+ },
5152
+ ...existsSync5(visualManifestPath) ? [
5153
+ {
5154
+ id: "artifact:visual-manifest",
5155
+ kind: "visual-manifest",
5156
+ path: ".decantr/evidence/visual-manifest.json",
5157
+ hash: null,
5158
+ localOnly: true,
5159
+ redacted: true
5160
+ }
5161
+ ] : [],
5162
+ ...(browserEvidence?.screenshots ?? []).map((screenshot, index) => ({
5163
+ id: `artifact:screenshot:${index + 1}`,
5164
+ kind: "screenshot",
5165
+ path: screenshot,
5166
+ hash: null,
5167
+ localOnly: true,
5168
+ redacted: false
5169
+ }))
5170
+ ],
5171
+ browser: browserEvidence,
5095
5172
  designTokens: collectDesignTokenEvidence(projectRoot, options.designTokensPath)
5096
5173
  });
5097
5174
  }
@@ -1,19 +1,20 @@
1
1
  import {
2
2
  createProjectHealthReport,
3
3
  listWorkspaceAppCandidates
4
- } from "./chunk-PQKTJGYL.js";
4
+ } from "./chunk-EUEQMXN5.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) {
@@ -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
  }