@decantr/cli 3.0.2 → 3.1.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.
package/dist/bin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-GN63PIKK.js";
2
+ import "./chunk-WSL6ZSTZ.js";
3
3
  import "./chunk-SIDKK73N.js";
4
- import "./chunk-B2PJDAMS.js";
5
- import "./chunk-4NDOHCYY.js";
6
- import "./chunk-7Z74ETDR.js";
4
+ import "./chunk-TXNLMAN5.js";
5
+ import "./chunk-BY6S6IWG.js";
6
+ import "./chunk-GPN2THFD.js";
@@ -3,14 +3,14 @@ import {
3
3
  sendProjectHealthCiFailedTelemetry,
4
4
  sendProjectHealthPromptTelemetry,
5
5
  sendProjectHealthReportTelemetry
6
- } from "./chunk-7Z74ETDR.js";
6
+ } from "./chunk-GPN2THFD.js";
7
7
 
8
8
  // src/commands/health.ts
9
9
  import { execFileSync as execFileSync2 } from "child_process";
10
10
  import { createHash as createHash2 } from "crypto";
11
11
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
12
12
  import { createRequire } from "module";
13
- import { dirname as dirname3, isAbsolute as isAbsolute3, join as join5, relative as relative3, resolve as resolve2 } from "path";
13
+ import { dirname as dirname3, isAbsolute as isAbsolute4, join as join5, relative as relative3, resolve as resolve3 } from "path";
14
14
  import { fileURLToPath } from "url";
15
15
  import {
16
16
  anchorFindingsToGraph,
@@ -114,8 +114,8 @@ function resolveWorkspaceInfo(cwd, projectArg) {
114
114
 
115
115
  // src/commands/graph.ts
116
116
  import { createHash } from "crypto";
117
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
118
- import { dirname as dirname2, isAbsolute as isAbsolute2, join as join4, relative as relative2 } from "path";
117
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
118
+ import { dirname as dirname2, isAbsolute as isAbsolute3, join as join4, relative as relative2 } from "path";
119
119
  import {
120
120
  buildContractCapsuleFromSnapshot,
121
121
  buildGraphImpactContext,
@@ -138,7 +138,7 @@ import {
138
138
  // src/local-law.ts
139
139
  import { execFileSync } from "child_process";
140
140
  import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync, writeFileSync } from "fs";
141
- import { basename, extname, join as join2, relative, sep } from "path";
141
+ import { basename, extname, isAbsolute as isAbsolute2, join as join2, relative, resolve as resolve2, sep } from "path";
142
142
  import { isV4 } from "@decantr/essence-spec";
143
143
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
144
144
  ".astro",
@@ -827,12 +827,13 @@ function createLocalLawTaskSummary(projectRoot) {
827
827
  function changedFiles(projectRoot, since) {
828
828
  const changed = /* @__PURE__ */ new Set();
829
829
  try {
830
+ const gitRoot = gitTopLevel(projectRoot) ?? projectRoot;
830
831
  const commands = since ? [
831
- ["diff", "--name-only", since, "--"],
832
- ["diff", "--name-only", "--cached"]
832
+ ["diff", "--name-only", "--relative", since, "--"],
833
+ ["diff", "--name-only", "--relative", "--cached"]
833
834
  ] : [
834
- ["diff", "--name-only"],
835
- ["diff", "--name-only", "--cached"]
835
+ ["diff", "--name-only", "--relative"],
836
+ ["diff", "--name-only", "--relative", "--cached"]
836
837
  ];
837
838
  for (const args of commands) {
838
839
  const output = execFileSync("git", args, {
@@ -842,13 +843,38 @@ function changedFiles(projectRoot, since) {
842
843
  });
843
844
  for (const line of output.split(/\r?\n/)) {
844
845
  const file = line.trim();
845
- if (file) changed.add(normalizePath(file));
846
+ const projectFile = changedFileForProject(projectRoot, gitRoot, file);
847
+ if (projectFile) changed.add(projectFile);
846
848
  }
847
849
  }
848
850
  } catch {
849
851
  }
850
852
  return [...changed].sort();
851
853
  }
854
+ function gitTopLevel(projectRoot) {
855
+ try {
856
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
857
+ cwd: projectRoot,
858
+ encoding: "utf-8",
859
+ stdio: ["ignore", "pipe", "ignore"]
860
+ }).trim();
861
+ } catch {
862
+ return null;
863
+ }
864
+ }
865
+ function changedFileForProject(projectRoot, gitRoot, file) {
866
+ if (!file) return null;
867
+ const absoluteProjectRoot = resolve2(projectRoot);
868
+ const candidateAbsoluteFiles = isAbsolute2(file) ? [file] : [join2(absoluteProjectRoot, file), join2(gitRoot, file)];
869
+ for (const absoluteFile of candidateAbsoluteFiles) {
870
+ const projectRelative = normalizePath(relative(absoluteProjectRoot, absoluteFile));
871
+ if (!projectRelative || projectRelative.startsWith("../") || projectRelative === "..") {
872
+ continue;
873
+ }
874
+ return projectRelative;
875
+ }
876
+ return null;
877
+ }
852
878
  function routeImpacts(projectRoot, files) {
853
879
  const analysis = readJsonFile(
854
880
  join2(projectRoot, ".decantr", "analysis.json")
@@ -1647,17 +1673,24 @@ function readGraphSnapshotSelection(artifacts, snapshotId) {
1647
1673
  }
1648
1674
  function projectRelativePath(projectRoot, path) {
1649
1675
  if (!path) return null;
1650
- const absolutePath = isAbsolute2(path) ? path : join4(projectRoot, path);
1676
+ const absolutePath = isAbsolute3(path) ? path : join4(projectRoot, path);
1651
1677
  const relativePath = relative2(projectRoot, absolutePath).replace(/\\/g, "/");
1652
- if (!relativePath || relativePath.startsWith("..") || isAbsolute2(relativePath)) {
1678
+ if (!relativePath || relativePath.startsWith("..") || isAbsolute3(relativePath)) {
1653
1679
  return null;
1654
1680
  }
1655
1681
  return relativePath;
1656
1682
  }
1683
+ function pathIsFile(path) {
1684
+ try {
1685
+ return statSync2(path).isFile();
1686
+ } catch {
1687
+ return false;
1688
+ }
1689
+ }
1657
1690
  function existingProjectRelativePath(projectRoot, path) {
1658
1691
  const relativePath = projectRelativePath(projectRoot, path);
1659
1692
  if (!relativePath) return null;
1660
- return existsSync4(join4(projectRoot, relativePath)) ? relativePath : null;
1693
+ return pathIsFile(join4(projectRoot, relativePath)) ? relativePath : null;
1661
1694
  }
1662
1695
  function stripJsonComments(value) {
1663
1696
  return value.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
@@ -1708,7 +1741,7 @@ function existingImportCandidate(projectRoot, candidate) {
1708
1741
  join4(relativeCandidate, "index.cts").replace(/\\/g, "/")
1709
1742
  ];
1710
1743
  for (const possible of candidates) {
1711
- if (existsSync4(join4(projectRoot, possible))) return possible;
1744
+ if (pathIsFile(join4(projectRoot, possible))) return possible;
1712
1745
  }
1713
1746
  return null;
1714
1747
  }
@@ -1841,7 +1874,7 @@ function sourceArtifacts(projectRoot, componentReuseAudit = null) {
1841
1874
  const routeFile = projectRelativePath(projectRoot, route.file);
1842
1875
  if (!routeFile) continue;
1843
1876
  const routeFilePath = join4(projectRoot, routeFile);
1844
- if (!existsSync4(routeFilePath)) continue;
1877
+ if (!pathIsFile(routeFilePath)) continue;
1845
1878
  sources.push({
1846
1879
  id: `src:${routeFile}`,
1847
1880
  kind: "route-source",
@@ -1860,7 +1893,7 @@ function sourceArtifacts(projectRoot, componentReuseAudit = null) {
1860
1893
  const componentFile = projectRelativePath(projectRoot, declaration.file);
1861
1894
  if (!componentFile) continue;
1862
1895
  const componentPath = join4(projectRoot, componentFile);
1863
- if (!existsSync4(componentPath)) continue;
1896
+ if (!pathIsFile(componentPath)) continue;
1864
1897
  if (sources.some((source) => source.id === `src:${componentFile}`)) continue;
1865
1898
  sources.push({
1866
1899
  id: `src:${componentFile}`,
@@ -1879,7 +1912,7 @@ function sourceArtifacts(projectRoot, componentReuseAudit = null) {
1879
1912
  const importingFile = projectRelativePath(projectRoot, importReference.file);
1880
1913
  if (!importingFile) continue;
1881
1914
  const importingPath = join4(projectRoot, importingFile);
1882
- if (existsSync4(importingPath) && !sources.some((source) => source.id === `src:${importingFile}`)) {
1915
+ if (pathIsFile(importingPath) && !sources.some((source) => source.id === `src:${importingFile}`)) {
1883
1916
  sources.push({
1884
1917
  id: `src:${importingFile}`,
1885
1918
  kind: "code-source",
@@ -1899,7 +1932,7 @@ function sourceArtifacts(projectRoot, componentReuseAudit = null) {
1899
1932
  );
1900
1933
  if (!importedFile) continue;
1901
1934
  const importedPath = join4(projectRoot, importedFile);
1902
- if (!existsSync4(importedPath)) continue;
1935
+ if (!pathIsFile(importedPath)) continue;
1903
1936
  if (sources.some((source) => source.id === `src:${importedFile}`)) continue;
1904
1937
  sources.push({
1905
1938
  id: `src:${importedFile}`,
@@ -1928,7 +1961,7 @@ function sourceArtifacts(projectRoot, componentReuseAudit = null) {
1928
1961
  for (const route of visualManifest.routes ?? []) {
1929
1962
  if (!route.screenshot) continue;
1930
1963
  const screenshotPath = join4(projectRoot, route.screenshot);
1931
- if (!existsSync4(screenshotPath)) continue;
1964
+ if (!pathIsFile(screenshotPath)) continue;
1932
1965
  sources.push({
1933
1966
  id: `src:${route.screenshot}`,
1934
1967
  kind: "visual-screenshot",
@@ -2596,7 +2629,7 @@ function augmentProjectGraph(snapshot, projectRoot, sources, componentReuseAudit
2596
2629
  function pathForDisplay(projectRoot, path, displayRoot) {
2597
2630
  if (!displayRoot) return path.replace(`${projectRoot}/`, "");
2598
2631
  const relativePath = relative2(displayRoot, path).replace(/\\/g, "/");
2599
- if (relativePath && !relativePath.startsWith("..") && !isAbsolute2(relativePath)) {
2632
+ if (relativePath && !relativePath.startsWith("..") && !isAbsolute3(relativePath)) {
2600
2633
  return relativePath;
2601
2634
  }
2602
2635
  return path;
@@ -2836,7 +2869,7 @@ function graphSourceNodeIdForFile(projectRoot, snapshot, file) {
2836
2869
  })?.id ?? null;
2837
2870
  }
2838
2871
  function resolvePathFromCwd(path) {
2839
- return isAbsolute2(path) ? path : join4(process.cwd(), path);
2872
+ return isAbsolute3(path) ? path : join4(process.cwd(), path);
2840
2873
  }
2841
2874
  function impactContextPayload(impactContext, selection) {
2842
2875
  if (!selection) return void 0;
@@ -3467,7 +3500,7 @@ function commandsForFinding(source) {
3467
3500
  case "runtime":
3468
3501
  return ["npm run build", "decantr health"];
3469
3502
  case "interaction":
3470
- return ["decantr check --strict", "decantr health"];
3503
+ return ["decantr verify --brownfield --local-patterns", "decantr verify --evidence"];
3471
3504
  case "assertion":
3472
3505
  return ["decantr refresh", "decantr health --evidence"];
3473
3506
  case "browser":
@@ -3477,7 +3510,7 @@ function commandsForFinding(source) {
3477
3510
  case "style-bridge":
3478
3511
  return ["decantr codify --style-bridge", "decantr verify --evidence"];
3479
3512
  case "graph":
3480
- return ["decantr graph", "decantr health"];
3513
+ return ["decantr graph", "decantr verify --evidence"];
3481
3514
  case "check":
3482
3515
  return ["decantr check", "decantr health"];
3483
3516
  default:
@@ -3531,7 +3564,7 @@ function commandContextForProject(projectRoot) {
3531
3564
  /\\/g,
3532
3565
  "/"
3533
3566
  );
3534
- const projectPath = relativeProjectPath && !relativeProjectPath.startsWith("..") && !isAbsolute3(relativeProjectPath) ? relativeProjectPath : null;
3567
+ const projectPath = relativeProjectPath && !relativeProjectPath.startsWith("..") && !isAbsolute4(relativeProjectPath) ? relativeProjectPath : null;
3535
3568
  const projectFlag = projectPath ? ` --project ${projectPath}` : "";
3536
3569
  const essencePath = projectPath ? `${projectPath}/decantr.essence.json` : "decantr.essence.json";
3537
3570
  return {
@@ -3566,6 +3599,10 @@ function rewriteHealthCommand(command, context) {
3566
3599
  /^decantr graph\b/,
3567
3600
  `decantr graph --project ${context.projectPath}`
3568
3601
  );
3602
+ rewritten = rewritten.replace(
3603
+ /^decantr verify\b/,
3604
+ `decantr verify --project ${context.projectPath}`
3605
+ );
3569
3606
  rewritten = rewritten.replace(/^decantr audit\b/, context.verifyCommand);
3570
3607
  rewritten = rewritten.replace(/^decantr health\b/, context.verifyCommand);
3571
3608
  return rewritten;
@@ -3796,7 +3833,7 @@ function isDuplicateFinding(existing, finding) {
3796
3833
  }
3797
3834
  function resolveOptionalPath(projectRoot, path) {
3798
3835
  if (!path) return void 0;
3799
- return isAbsolute3(path) ? path : resolve2(projectRoot, path);
3836
+ return isAbsolute4(path) ? path : resolve3(projectRoot, path);
3800
3837
  }
3801
3838
  function hasProjectPlaywright(projectRoot) {
3802
3839
  try {
@@ -4013,7 +4050,7 @@ function parseDecantrCssTokenNames(projectRoot) {
4013
4050
  function collectDesignTokenEvidence(projectRoot, designTokensPath) {
4014
4051
  const resolved = resolveOptionalPath(projectRoot, designTokensPath);
4015
4052
  if (!resolved) return void 0;
4016
- const sourceLabel = isAbsolute3(designTokensPath ?? "") ? "<design-tokens>" : designTokensPath ?? "<design-tokens>";
4053
+ const sourceLabel = isAbsolute4(designTokensPath ?? "") ? "<design-tokens>" : designTokensPath ?? "<design-tokens>";
4017
4054
  if (!existsSync5(resolved)) {
4018
4055
  return {
4019
4056
  source: sourceLabel,
@@ -4609,7 +4646,7 @@ async function cmdHealth(projectRoot = process.cwd(), options = {}) {
4609
4646
  const format2 = resolveFormat(options);
4610
4647
  const payload2 = format2 === "json" ? formatDiagnosticCatalogJson() : format2 === "markdown" ? formatDiagnosticCatalogMarkdown() : formatDiagnosticCatalogText();
4611
4648
  if (options.output) {
4612
- const outputPath = isAbsolute3(options.output) ? options.output : join5(projectRoot, options.output);
4649
+ const outputPath = isAbsolute4(options.output) ? options.output : join5(projectRoot, options.output);
4613
4650
  mkdirSync4(dirname3(outputPath), { recursive: true });
4614
4651
  writeFileSync4(outputPath, payload2, "utf-8");
4615
4652
  if (!options.ci) {
@@ -4664,7 +4701,7 @@ async function cmdHealth(projectRoot = process.cwd(), options = {}) {
4664
4701
  ` : format === "json" ? formatProjectHealthJson(report) : format === "markdown" ? formatProjectHealthMarkdown(report) : formatProjectHealthText(report);
4665
4702
  const payload = baselineComparison && !options.evidence && format === "text" ? `${basePayload}${formatBaselineComparisonText(baselineComparison)}` : basePayload;
4666
4703
  if (options.output) {
4667
- const outputPath = isAbsolute3(options.output) ? options.output : join5(projectRoot, options.output);
4704
+ const outputPath = isAbsolute4(options.output) ? options.output : join5(projectRoot, options.output);
4668
4705
  mkdirSync4(dirname3(outputPath), { recursive: true });
4669
4706
  writeFileSync4(outputPath, payload, "utf-8");
4670
4707
  if (!options.ci) {
@@ -456,6 +456,8 @@ function walkSvelteKitRoutes(dir, baseDir, segments) {
456
456
  return routes;
457
457
  }
458
458
  var ROUTER_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".ts", ".jsx", ".js"]);
459
+ var ROUTE_VARIABLE_NAMES = "(?:path|pathname|route|currentPath|currentRoute|locationPath|activePath)";
460
+ var ROUTE_ASSET_EXTENSION_RE = /\.(?:avif|bmp|css|gif|ico|jpeg|jpg|js|json|map|mp4|pdf|png|svg|webp|woff2?)$/i;
459
461
  function collectRouteCandidateFiles(dir, files, depth = 0) {
460
462
  if (depth > 5) return;
461
463
  let entries;
@@ -496,30 +498,72 @@ function scanReactRouter(projectRoot) {
496
498
  continue;
497
499
  }
498
500
  const isReactRouterFile = content.includes("react-router-dom") || content.includes("react-router") || content.includes("<Routes") || content.includes("createBrowserRouter") || content.includes("createHashRouter") || content.includes("RouterProvider") || content.includes("HashRouter") || content.includes("BrowserRouter");
499
- if (!isReactRouterFile) continue;
500
501
  const relativePath = relative2(projectRoot, absolutePath);
501
502
  const pathMatches = /* @__PURE__ */ new Set();
502
- for (const match of content.matchAll(/<Route\b[^>]*\bpath=["'`]([^"'`]+)["'`]/g)) {
503
- pathMatches.add(match[1]);
503
+ if (isReactRouterFile) {
504
+ for (const match of content.matchAll(/<Route\b[^>]*\bpath=["'`]([^"'`]+)["'`]/g)) {
505
+ pathMatches.add(match[1]);
506
+ }
507
+ for (const match of content.matchAll(/\bpath\s*:\s*["'`]([^"'`]+)["'`]/g)) {
508
+ pathMatches.add(match[1]);
509
+ }
504
510
  }
505
- for (const match of content.matchAll(/\bpath\s*:\s*["'`]([^"'`]+)["'`]/g)) {
506
- pathMatches.add(match[1]);
511
+ for (const route of detectPathnameBranchRoutes(content)) {
512
+ pathMatches.add(route);
507
513
  }
514
+ if (!isReactRouterFile && pathMatches.size === 0) continue;
508
515
  if (pathMatches.size === 0 && (content.includes("<Routes") || content.includes("RouterProvider"))) {
509
516
  pathMatches.add("/");
510
517
  }
511
518
  for (const path of pathMatches) {
512
- if (!routeMap.has(path)) {
513
- routeMap.set(path, {
514
- path,
515
- file: relativePath,
516
- hasLayout: false
517
- });
518
- }
519
+ const routePath = normalizeDetectedRouteLiteral(path);
520
+ if (!routePath || routeMap.has(routePath)) continue;
521
+ routeMap.set(routePath, {
522
+ path: routePath,
523
+ file: relativePath,
524
+ hasLayout: false
525
+ });
519
526
  }
520
527
  }
521
528
  return [...routeMap.values()];
522
529
  }
530
+ function normalizeDetectedRouteLiteral(value) {
531
+ const cleaned = value.trim().split(/[?#]/)[0];
532
+ if (!cleaned || cleaned === "/") return "/";
533
+ if (cleaned === "*" || cleaned === "**" || cleaned.startsWith("#")) return null;
534
+ if (!cleaned.startsWith("/") || cleaned.startsWith("//")) return null;
535
+ if (ROUTE_ASSET_EXTENSION_RE.test(cleaned)) return null;
536
+ return cleaned.replace(/\/+$/g, "") || "/";
537
+ }
538
+ function collectRouteLiterals(pattern, content, routes) {
539
+ let count = 0;
540
+ for (const match of content.matchAll(pattern)) {
541
+ const route = normalizeDetectedRouteLiteral(match[1] ?? "");
542
+ if (!route) continue;
543
+ routes.add(route);
544
+ count += 1;
545
+ }
546
+ return count;
547
+ }
548
+ function detectPathnameBranchRoutes(content) {
549
+ const routes = /* @__PURE__ */ new Set();
550
+ const comparison = new RegExp(
551
+ `\\b${ROUTE_VARIABLE_NAMES}\\b\\s*(?:===|!==|==|!=)\\s*["'\`](\\/[^"'\`]+)["'\`]`,
552
+ "g"
553
+ );
554
+ const reversedComparison = new RegExp(
555
+ `["'\`](\\/[^"'\`]+)["'\`]\\s*(?:===|!==|==|!=)\\s*\\b${ROUTE_VARIABLE_NAMES}\\b`,
556
+ "g"
557
+ );
558
+ const strongMatches = collectRouteLiterals(comparison, content, routes) + collectRouteLiterals(reversedComparison, content, routes) + collectRouteLiterals(/\bcase\s+["'`](\/[^"'`]+)["'`]\s*:/g, content, routes);
559
+ const hasPathnameSignal = /\b(?:window\.|document\.)?location\.pathname\b|\bpathname\b/.test(
560
+ content
561
+ );
562
+ if (strongMatches > 0 || hasPathnameSignal) {
563
+ collectRouteLiterals(/\b(?:href|to)\s*=\s*["'`](\/[^"'`]+)["'`]/g, content, routes);
564
+ }
565
+ return [...routes];
566
+ }
523
567
  function hasReactRouterDependency(projectRoot) {
524
568
  return hasDependency(projectRoot, ["react-router", "react-router-dom"]);
525
569
  }
@@ -1091,9 +1135,37 @@ function routePathname(route) {
1091
1135
  function declaredRouteObserved(route, observedRoutes) {
1092
1136
  return observedRoutes.has(route) || route.includes("?") && observedRoutes.has(routePathname(route));
1093
1137
  }
1138
+ function sectionForRoute(essence, route) {
1139
+ const mappedSectionId = essence.blueprint.routes?.[route]?.section;
1140
+ const sections = essence.blueprint.sections ?? [];
1141
+ if (mappedSectionId) {
1142
+ return sections.find((section) => section.id === mappedSectionId) ?? null;
1143
+ }
1144
+ return sections.find(
1145
+ (section) => section.pages?.some((page) => page.route === route || page.route === routePathname(route))
1146
+ ) ?? null;
1147
+ }
1148
+ function readObservedRouteSource(projectRoot, file) {
1149
+ if (!file || file === ".") return "";
1150
+ try {
1151
+ return readFileSync5(join5(projectRoot, file), "utf-8");
1152
+ } catch {
1153
+ return "";
1154
+ }
1155
+ }
1156
+ function routeLooksPublicFullBleed(path, file, code) {
1157
+ return /\b(?:pricing|plans|marketing|landing|public|hero|full-bleed|fullbleed|marketing-bleed)\b/i.test(
1158
+ `${path} ${file} ${code}`
1159
+ );
1160
+ }
1161
+ function routeLooksAppShell(file, code) {
1162
+ return /\b(?:sidebar|side-nav|sidenav|app-frame|dashboard-shell|app-shell)\b|<\s*(?:Sidebar|AppShell|DashboardShell)\b/i.test(
1163
+ `${file} ${code}`
1164
+ );
1165
+ }
1094
1166
  function hasDoctrineEffect(essence, key) {
1095
1167
  const effects = essence.dna.constraints?.effects;
1096
- return Boolean(effects && effects[key]);
1168
+ return Boolean(effects?.[key]);
1097
1169
  }
1098
1170
  function hasActionableDoctrineSource(doctrine, area) {
1099
1171
  return doctrine.sources.some(
@@ -1168,6 +1240,32 @@ function scanBrownfieldIssues(projectRoot, essence) {
1168
1240
  suggestion: "Confirm whether these are generated/dynamic routes or stale contract entries."
1169
1241
  });
1170
1242
  }
1243
+ const shellDriftRoutes = [];
1244
+ for (const route of routes.routes.slice(0, 80)) {
1245
+ const section = sectionForRoute(essence, route.path);
1246
+ const shell = section?.shell ?? "";
1247
+ if (!shell) continue;
1248
+ const source = readObservedRouteSource(projectRoot, route.file);
1249
+ const shellHasSidebar = /\bsidebar\b/i.test(shell);
1250
+ const shellIsPublicLight = /^(?:main-only|main-footer|topnav-main|topnav-main-footer)$/i.test(
1251
+ shell
1252
+ );
1253
+ if (shellHasSidebar && routeLooksPublicFullBleed(route.path, route.file, source)) {
1254
+ shellDriftRoutes.push(`${route.path} (${shell} vs public/full-bleed source)`);
1255
+ } else if (shellIsPublicLight && routeLooksAppShell(route.file, source)) {
1256
+ shellDriftRoutes.push(`${route.path} (${shell} vs app-shell source)`);
1257
+ }
1258
+ }
1259
+ if (shellDriftRoutes.length > 0) {
1260
+ issues.push({
1261
+ type: "warning",
1262
+ rule: "brownfield-shell-drift",
1263
+ message: `Observed route shell signals disagree with the Decantr section shell: ${routeLabel(
1264
+ shellDriftRoutes
1265
+ )}.`,
1266
+ suggestion: "Regenerate and merge a brownfield proposal, or update the affected section shell after reviewing the route source."
1267
+ });
1268
+ }
1171
1269
  const adoptionMode = projectJson.initialized?.adoptionMode;
1172
1270
  const themeId = essence.dna.theme.id;
1173
1271
  if (adoptionMode === "contract-only" && themeId === "luminarum" && (styling.approach !== "unknown" || styling.cssVariables.length > 0)) {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createProjectHealthReport,
3
3
  listWorkspaceAppCandidates
4
- } from "./chunk-4NDOHCYY.js";
4
+ } from "./chunk-BY6S6IWG.js";
5
5
 
6
6
  // src/commands/workspace.ts
7
7
  import { execFileSync } from "child_process";
@@ -22,7 +22,7 @@ import {
22
22
  listWorkspaceCandidates,
23
23
  listWorkspaceProjects,
24
24
  shouldFailWorkspaceHealth
25
- } from "./chunk-B2PJDAMS.js";
25
+ } from "./chunk-TXNLMAN5.js";
26
26
  import {
27
27
  acceptBrownfieldLocalLaw,
28
28
  acceptStyleBridge,
@@ -52,7 +52,7 @@ import {
52
52
  writeBrownfieldCodifyProposal,
53
53
  writeHostedPatternMappingProposal,
54
54
  writeStyleBridgeProposal
55
- } from "./chunk-4NDOHCYY.js";
55
+ } from "./chunk-BY6S6IWG.js";
56
56
  import {
57
57
  buildGuardRegistryContext,
58
58
  createDoctrineMap,
@@ -69,7 +69,7 @@ import {
69
69
  sendCliCommandTelemetry,
70
70
  sendNewProjectCompletedTelemetry,
71
71
  writeDoctrineMap
72
- } from "./chunk-7Z74ETDR.js";
72
+ } from "./chunk-GPN2THFD.js";
73
73
 
74
74
  // src/index.ts
75
75
  import { existsSync as existsSync28, mkdirSync as mkdirSync15, readdirSync as readdirSync8, readFileSync as readFileSync21, writeFileSync as writeFileSync18 } from "fs";
@@ -1523,6 +1523,35 @@ function doctrineEffects(ambient) {
1523
1523
  }
1524
1524
  return Object.keys(effects).length > 0 ? effects : void 0;
1525
1525
  }
1526
+ function readRouteSource(projectRoot, file) {
1527
+ if (!file || file === ".") return "";
1528
+ try {
1529
+ return readFileSync4(join5(projectRoot, file), "utf-8");
1530
+ } catch {
1531
+ return "";
1532
+ }
1533
+ }
1534
+ function publicSurfaceShell(input, code) {
1535
+ const hasFooter = input.layout.hasFooter || /\bfooter\b|<\s*Footer\b/i.test(code);
1536
+ const hasTopNav = input.layout.hasTopNav || /\b(?:topnav|top-nav|navbar|header)\b|<\s*(?:Header|Nav|Navbar)\b/i.test(code);
1537
+ if (hasTopNav && hasFooter) return "topnav-main-footer";
1538
+ if (hasTopNav) return "topnav-main";
1539
+ if (hasFooter) return "main-footer";
1540
+ return "main-only";
1541
+ }
1542
+ function routeShellPattern(input, route, classified, fallbackShell) {
1543
+ const code = readRouteSource(input.project.projectRoot, route.file);
1544
+ const routeEvidence = `${route.path} ${route.file} ${code}`;
1545
+ if (classified.role === "public" || /\b(?:pricing|plans|marketing|landing|public|hero|full-bleed|fullbleed|marketing-bleed)\b/i.test(
1546
+ routeEvidence
1547
+ )) {
1548
+ return publicSurfaceShell(input, code);
1549
+ }
1550
+ if (/\b(?:sidebar|side-nav|sidenav|app-frame|dashboard-shell)\b/i.test(routeEvidence)) {
1551
+ return fallbackShell;
1552
+ }
1553
+ return fallbackShell;
1554
+ }
1526
1555
  function createBrownfieldProposal(input) {
1527
1556
  const target = input.project.framework !== "unknown" ? input.project.framework : "generic-web";
1528
1557
  const shell = input.layout.shellPattern || "observed-existing-shell";
@@ -1531,10 +1560,11 @@ function createBrownfieldProposal(input) {
1531
1560
  const observedRoutes = input.routes.routes.length > 0 ? input.routes.routes : [{ path: "/", file: ".", hasLayout: false }];
1532
1561
  for (const route of observedRoutes) {
1533
1562
  const classified = routeDomain(route.path);
1563
+ const observedShell = routeShellPattern(input, route, classified, shell);
1534
1564
  const section = sectionMap.get(classified.sectionId) ?? {
1535
1565
  id: classified.sectionId,
1536
1566
  role: classified.role,
1537
- shell,
1567
+ shell: observedShell,
1538
1568
  features: featuresForSection(classified.sectionId, input.features.detected),
1539
1569
  description: classified.description,
1540
1570
  pages: [],
@@ -1550,7 +1580,8 @@ function createBrownfieldProposal(input) {
1550
1580
  route: route.path,
1551
1581
  layout: ["existing-surface"],
1552
1582
  directives: [
1553
- `Observed source: ${route.file}. Treat this route as existing product surface, not scaffold territory.`
1583
+ `Observed source: ${route.file}. Treat this route as existing product surface, not scaffold territory.`,
1584
+ `Observed shell: ${observedShell}. Preserve this route's shell posture unless the user explicitly approves a layout migration.`
1554
1585
  ]
1555
1586
  });
1556
1587
  }
@@ -10828,7 +10859,7 @@ async function cmdAdoptWorkflow(args) {
10828
10859
  await cmdGraph(projectRoot, { displayRoot: process.cwd() });
10829
10860
  if (process.exitCode && process.exitCode !== 0) return;
10830
10861
  if (runVerify) {
10831
- const { cmdHealth } = await import("./health-DDT4RABT.js");
10862
+ const { cmdHealth } = await import("./health-IOTIZT7Z.js");
10832
10863
  await cmdHealth(projectRoot, {
10833
10864
  browser: runBrowser,
10834
10865
  browserBaseUrl: baseUrl,
@@ -10899,7 +10930,7 @@ async function cmdVerifyWorkflow(args) {
10899
10930
  return;
10900
10931
  }
10901
10932
  if (workspaceMode) {
10902
- const { cmdWorkspace } = await import("./workspace-3XNCMZ5L.js");
10933
+ const { cmdWorkspace } = await import("./workspace-72SJI3MW.js");
10903
10934
  await cmdWorkspace(process.cwd(), ["workspace", "health", ...withoutWorkflowOnlyFlags(args)]);
10904
10935
  return;
10905
10936
  }
@@ -10932,7 +10963,7 @@ async function cmdVerifyWorkflow(args) {
10932
10963
  }
10933
10964
  let guardExitCode;
10934
10965
  if (brownfield) {
10935
- const { cmdHeal, collectCheckIssues } = await import("./heal-HHVVTVIH.js");
10966
+ const { cmdHeal, collectCheckIssues } = await import("./heal-2DKBFWB6.js");
10936
10967
  if (quietOutput) {
10937
10968
  const result = collectCheckIssues(workspaceInfo.appRoot, { brownfield: true });
10938
10969
  guardExitCode = result.issues.some((issue) => issue.type === "error") ? 1 : void 0;
@@ -10942,7 +10973,7 @@ async function cmdVerifyWorkflow(args) {
10942
10973
  process.exitCode = void 0;
10943
10974
  }
10944
10975
  }
10945
- const { cmdHealth, parseHealthArgs } = await import("./health-DDT4RABT.js");
10976
+ const { cmdHealth, parseHealthArgs } = await import("./health-IOTIZT7Z.js");
10946
10977
  await cmdHealth(workspaceInfo.appRoot, parseHealthArgs(healthArgs));
10947
10978
  if (localPatterns) {
10948
10979
  const validation = validateLocalLaw(workspaceInfo.appRoot);
@@ -12445,7 +12476,7 @@ async function main() {
12445
12476
  `${YELLOW12}Note: \`decantr heal\` is deprecated. Use \`decantr check\` instead.${RESET16}`
12446
12477
  );
12447
12478
  }
12448
- const { cmdHeal } = await import("./heal-HHVVTVIH.js");
12479
+ const { cmdHeal } = await import("./heal-2DKBFWB6.js");
12449
12480
  const { flags } = parseLooseArgs(args);
12450
12481
  const workspaceInfo = resolveWorkflowProject(flags, "check");
12451
12482
  if (!workspaceInfo) break;
@@ -12470,7 +12501,7 @@ async function main() {
12470
12501
  const { flags } = parseLooseArgs(args);
12471
12502
  const workspaceInfo = resolveWorkflowProject(flags, "health");
12472
12503
  if (!workspaceInfo) break;
12473
- const { cmdHealth, parseHealthArgs } = await import("./health-DDT4RABT.js");
12504
+ const { cmdHealth, parseHealthArgs } = await import("./health-IOTIZT7Z.js");
12474
12505
  await cmdHealth(workspaceInfo.appRoot, parseHealthArgs(stripProjectArgs(args)));
12475
12506
  } catch (e) {
12476
12507
  console.error(error2(e.message));
@@ -12498,7 +12529,7 @@ async function main() {
12498
12529
  cmdStudioHelp();
12499
12530
  break;
12500
12531
  }
12501
- const { cmdStudio, parseStudioArgs } = await import("./studio-WDJ3BOCU.js");
12532
+ const { cmdStudio, parseStudioArgs } = await import("./studio-DLWUMCC3.js");
12502
12533
  await cmdStudio(process.cwd(), parseStudioArgs(args));
12503
12534
  } catch (e) {
12504
12535
  console.error(error2(e.message));
@@ -12512,7 +12543,7 @@ async function main() {
12512
12543
  cmdWorkspaceHelp();
12513
12544
  break;
12514
12545
  }
12515
- const { cmdWorkspace } = await import("./workspace-3XNCMZ5L.js");
12546
+ const { cmdWorkspace } = await import("./workspace-72SJI3MW.js");
12516
12547
  await cmdWorkspace(process.cwd(), args);
12517
12548
  } catch (e) {
12518
12549
  console.error(error2(e.message));
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  cmdHeal,
3
3
  collectCheckIssues
4
- } from "./chunk-7Z74ETDR.js";
4
+ } from "./chunk-GPN2THFD.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-4NDOHCYY.js";
17
- import "./chunk-7Z74ETDR.js";
16
+ } from "./chunk-BY6S6IWG.js";
17
+ import "./chunk-GPN2THFD.js";
18
18
  export {
19
19
  cmdHealth,
20
20
  collectDesignTokenEvidence,
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import "./chunk-GN63PIKK.js";
1
+ import "./chunk-WSL6ZSTZ.js";
2
2
  import "./chunk-SIDKK73N.js";
3
- import "./chunk-B2PJDAMS.js";
4
- import "./chunk-4NDOHCYY.js";
5
- import "./chunk-7Z74ETDR.js";
3
+ import "./chunk-TXNLMAN5.js";
4
+ import "./chunk-BY6S6IWG.js";
5
+ import "./chunk-GPN2THFD.js";
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  createWorkspaceHealthReport
3
- } from "./chunk-B2PJDAMS.js";
3
+ } from "./chunk-TXNLMAN5.js";
4
4
  import {
5
5
  createProjectHealthReport
6
- } from "./chunk-4NDOHCYY.js";
6
+ } from "./chunk-BY6S6IWG.js";
7
7
  import {
8
8
  sendStudioHealthRefreshedTelemetry,
9
9
  sendStudioStartedTelemetry
10
- } from "./chunk-7Z74ETDR.js";
10
+ } from "./chunk-GPN2THFD.js";
11
11
 
12
12
  // src/commands/studio.ts
13
13
  import { readFileSync } from "fs";
@@ -7,9 +7,9 @@ import {
7
7
  listWorkspaceProjects,
8
8
  parseWorkspaceArgs,
9
9
  shouldFailWorkspaceHealth
10
- } from "./chunk-B2PJDAMS.js";
11
- import "./chunk-4NDOHCYY.js";
12
- import "./chunk-7Z74ETDR.js";
10
+ } from "./chunk-TXNLMAN5.js";
11
+ import "./chunk-BY6S6IWG.js";
12
+ import "./chunk-GPN2THFD.js";
13
13
  export {
14
14
  cmdWorkspace,
15
15
  createWorkspaceHealthReport,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decantr/cli",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "description": "Decantr CLI - adopt, verify, graph, and govern frontend codebases touched by AI agents",
5
5
  "keywords": [
6
6
  "decantr",
@@ -49,11 +49,11 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "ajv": "^8.20.0",
52
- "@decantr/core": "3.0.2",
53
- "@decantr/registry": "3.0.2",
54
- "@decantr/verifier": "3.0.2",
55
- "@decantr/essence-spec": "3.0.2",
56
- "@decantr/telemetry": "3.0.2"
52
+ "@decantr/core": "3.1.0",
53
+ "@decantr/registry": "3.1.0",
54
+ "@decantr/essence-spec": "3.1.0",
55
+ "@decantr/telemetry": "3.1.0",
56
+ "@decantr/verifier": "3.1.0"
57
57
  },
58
58
  "scripts": {
59
59
  "build": "tsup",