@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 +4 -4
- package/dist/{chunk-4NDOHCYY.js → chunk-BY6S6IWG.js} +65 -28
- package/dist/{chunk-7Z74ETDR.js → chunk-GPN2THFD.js} +111 -13
- package/dist/{chunk-B2PJDAMS.js → chunk-TXNLMAN5.js} +1 -1
- package/dist/{chunk-GN63PIKK.js → chunk-WSL6ZSTZ.js} +44 -13
- package/dist/{heal-HHVVTVIH.js → heal-2DKBFWB6.js} +1 -1
- package/dist/{health-DDT4RABT.js → health-IOTIZT7Z.js} +2 -2
- package/dist/index.js +4 -4
- package/dist/{studio-WDJ3BOCU.js → studio-DLWUMCC3.js} +3 -3
- package/dist/{workspace-3XNCMZ5L.js → workspace-72SJI3MW.js} +3 -3
- package/package.json +6 -6
package/dist/bin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "./chunk-
|
|
2
|
+
import "./chunk-WSL6ZSTZ.js";
|
|
3
3
|
import "./chunk-SIDKK73N.js";
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
1676
|
+
const absolutePath = isAbsolute3(path) ? path : join4(projectRoot, path);
|
|
1651
1677
|
const relativePath = relative2(projectRoot, absolutePath).replace(/\\/g, "/");
|
|
1652
|
-
if (!relativePath || relativePath.startsWith("..") ||
|
|
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
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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("..") && !
|
|
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
|
|
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
|
|
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
|
|
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("..") && !
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
503
|
-
|
|
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
|
|
506
|
-
pathMatches.add(
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
|
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)) {
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
listWorkspaceCandidates,
|
|
23
23
|
listWorkspaceProjects,
|
|
24
24
|
shouldFailWorkspaceHealth
|
|
25
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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));
|
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
renderProjectHealthCiWorkflow,
|
|
14
14
|
shouldFailHealth,
|
|
15
15
|
writeProjectHealthCiWorkflow
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import "./chunk-
|
|
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-
|
|
1
|
+
import "./chunk-WSL6ZSTZ.js";
|
|
2
2
|
import "./chunk-SIDKK73N.js";
|
|
3
|
-
import "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
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-
|
|
3
|
+
} from "./chunk-TXNLMAN5.js";
|
|
4
4
|
import {
|
|
5
5
|
createProjectHealthReport
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-BY6S6IWG.js";
|
|
7
7
|
import {
|
|
8
8
|
sendStudioHealthRefreshedTelemetry,
|
|
9
9
|
sendStudioStartedTelemetry
|
|
10
|
-
} from "./chunk-
|
|
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-
|
|
11
|
-
import "./chunk-
|
|
12
|
-
import "./chunk-
|
|
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
|
|
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
|
|
53
|
-
"@decantr/registry": "3.0
|
|
54
|
-
"@decantr/
|
|
55
|
-
"@decantr/
|
|
56
|
-
"@decantr/
|
|
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",
|