@decantr/cli 2.10.0 → 2.12.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/README.md CHANGED
@@ -20,7 +20,7 @@ npx @decantr/cli new my-app --blueprint=esports-hq
20
20
  Use `decantr setup` when you are unsure which path applies. It detects whether the repo is empty, already attached, or a Brownfield app and recommends the right entry path.
21
21
  Use `decantr new` for a greenfield workspace in a fresh directory. With a blueprint/archetype it uses the runnable adapter and Decantr CSS; without registry content it creates a contract-only workspace unless you explicitly pass `--adoption=decantr-css`.
22
22
  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.
23
- Use `decantr doctor` when the next step is unclear, `decantr task` before asking an LLM to modify a route, `decantr verify` after the edit, and `decantr ci` in required automation. Use `decantr codify --from-audit` when you want project-owned UI patterns and local rules 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 and rules as project authority.
23
+ Use `decantr doctor` when the next step is unclear, `decantr task` before asking an LLM to modify a route, `decantr verify` after the edit, and `decantr ci` in required automation. Use `decantr codify --from-audit --style-bridge` when you want project-owned UI patterns, 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, rules, and style bridge mappings as project authority.
24
24
  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.
25
25
  Use `decantr init`, `decantr analyze`, `decantr check`, and `decantr health` as advanced primitives when you need direct control over one step.
26
26
 
@@ -45,7 +45,7 @@ decantr adopt --yes
45
45
  decantr doctor
46
46
  decantr ci --fail-on error
47
47
  decantr ci init
48
- decantr codify --from-audit
48
+ decantr codify --from-audit --style-bridge
49
49
  decantr codify --accept
50
50
  decantr task /feed "add saved recipe actions"
51
51
  decantr verify --brownfield --local-patterns
@@ -75,12 +75,13 @@ pnpm exec decantr setup
75
75
  pnpm exec decantr workspace list
76
76
  pnpm exec decantr adopt --project apps/web --yes
77
77
  pnpm exec decantr doctor --project apps/web
78
+ pnpm exec decantr codify --from-audit --style-bridge --project apps/web
78
79
  pnpm exec decantr ci init --project apps/web
79
80
  ```
80
81
 
81
82
  Assistant rule integration is preview-first: `--assistant-bridge=preview` writes `.decantr/context/assistant-bridge.md`, `decantr rules preview` prints the bridge, and `--assistant-bridge=apply` or `decantr rules apply` mutates supported rule files with idempotent marked blocks.
82
83
 
83
- Brownfield analysis also writes `.decantr/doctrine-map.json`, a ranked source-precedence map across security/data, architecture, design-system, workflow/CI, feature/business, assistant-specific, stale, and unsafe-to-cite evidence. It also writes `.decantr/brownfield-intelligence.json`, `.decantr/theme-inventory.json`, and `.decantr/enrichment-backlog.md`. The proposal groups routes into observed semantic domains such as auth, RBAC, billing, reporting, facilities, settings, and public surfaces across Next App/Pages Router, React Router, Angular Router, SvelteKit, Vue Router, and Nuxt file routes. Existing styling systems such as Tailwind, Bootstrap, MUI, Chakra, plain CSS, and Decantr CSS are observed as evidence instead of replaced. Theme variants are observed in the theme inventory without changing Essence V4. `decantr codify --from-audit` proposes `.decantr/local-patterns.proposal.json` and `.decantr/rules.proposal.json` with Hybrid authority guidance; after review, `decantr codify --accept` promotes `.decantr/local-patterns.json` and `.decantr/rules.json`. `decantr doctor` reports whether the app is contract-only, Hybrid local law, style bridge, Decantr CSS, or Hybrid composition. `decantr task` prints that authority block and warns before mixing runtimes or adding Decantr CSS to a non-Decantr-CSS app. `decantr verify --brownfield --local-patterns` uses the Brownfield guard layer plus accepted local law to flag actionable missing doctrine coverage, unsafe context, missing assistant bridges, style drift, raw local-rule violations, and unsafe defaults without treating current database migrations as stale docs.
84
+ Brownfield analysis also writes `.decantr/doctrine-map.json`, a ranked source-precedence map across security/data, architecture, design-system, workflow/CI, feature/business, assistant-specific, stale, and unsafe-to-cite evidence. It also writes `.decantr/brownfield-intelligence.json`, `.decantr/theme-inventory.json`, and `.decantr/enrichment-backlog.md`. The proposal groups routes into observed semantic domains such as auth, RBAC, billing, reporting, facilities, settings, and public surfaces across Next App/Pages Router, React Router, Angular Router, SvelteKit, Vue Router, and Nuxt file routes. Existing styling systems such as Tailwind, Bootstrap, MUI, Chakra, plain CSS, and Decantr CSS are observed as evidence instead of replaced. Theme variants are observed in the theme inventory without changing Essence V4. `decantr codify --from-audit` proposes `.decantr/local-patterns.proposal.json` and `.decantr/rules.proposal.json` with Hybrid authority guidance and source-derived button/card class hints; `decantr codify --style-bridge` proposes `.decantr/style-bridge.proposal.json`, mapping Decantr intent to project-owned tokens/classes without requiring `@decantr/css`. After review, `decantr codify --accept` promotes whichever proposals exist. `decantr doctor` reports whether the app is contract-only, Hybrid local law, style bridge, Decantr CSS, or Hybrid composition. `decantr task` prints that authority block and warns before mixing runtimes or adding Decantr CSS to a non-Decantr-CSS app. `decantr suggest --from-code` surfaces accepted local patterns and style bridge mappings from the app root or selected `--project`, and `decantr ci` prints accepted local-rule findings plus style bridge status in text, markdown, and JSON reports. `decantr verify --brownfield --local-patterns` uses the Brownfield guard layer plus accepted local law to flag actionable missing doctrine coverage, unsafe context, missing assistant bridges, style drift, raw local-rule violations, and unsafe defaults without treating current database migrations as stale docs.
84
85
 
85
86
  ## What It Does
86
87
 
@@ -111,12 +112,13 @@ decantr adopt --yes
111
112
  decantr adopt --project apps/web --yes
112
113
  decantr doctor
113
114
  decantr doctor --project apps/web
114
- decantr codify --from-audit
115
+ decantr codify --from-audit --style-bridge
115
116
  decantr codify --accept
116
117
  decantr task /feed "add saved recipe actions"
117
118
  decantr verify --brownfield --local-patterns
118
119
  decantr verify --base-url http://localhost:3000 --evidence
119
120
  decantr ci --project apps/web
121
+ decantr ci --project apps/web --fail-on warn
120
122
  decantr ci --workspace --changed --since origin/main
121
123
  decantr ci init --project apps/web
122
124
  decantr init --existing --blueprint=esports-hq
package/dist/bin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-HQUTNZKF.js";
2
+ import "./chunk-SWPAAAO4.js";
3
3
  import "./chunk-IQZCIMQJ.js";
4
- import "./chunk-QDMXZSVK.js";
5
- import "./chunk-WPP3JEMC.js";
6
- import "./chunk-4SZ4SEKT.js";
4
+ import "./chunk-F6SWDGKO.js";
5
+ import "./chunk-PHRVC575.js";
6
+ import "./chunk-CAKQWTZS.js";
@@ -660,8 +660,8 @@ function scanRoutes(projectRoot) {
660
660
  }
661
661
 
662
662
  // src/analyzers/styling.ts
663
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
664
- import { join as join3 } from "path";
663
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
664
+ import { join as join3, relative as relative3 } from "path";
665
665
  var TAILWIND_CONFIGS = [
666
666
  "tailwind.config.js",
667
667
  "tailwind.config.ts",
@@ -674,8 +674,10 @@ var GLOBALS_CSS_PATHS = [
674
674
  "src/styles/global.css",
675
675
  "src/styles/globals.css",
676
676
  "src/styles/main.css",
677
+ "src/styles/themes.css",
677
678
  "src/styles.css",
678
679
  "styles/globals.css",
680
+ "styles/themes.css",
679
681
  "styles.css",
680
682
  "assets/css/main.css",
681
683
  "src/index.css",
@@ -716,8 +718,15 @@ function extractCSSVariables(content) {
716
718
  return { colors, variables };
717
719
  }
718
720
  function detectDarkMode(projectRoot, cssContents) {
721
+ const darkSelectors = [
722
+ /\.dark\b/i,
723
+ /html\.dark\b/i,
724
+ /\[data-theme\s*=\s*["']dark["']\]/i,
725
+ /prefers-color-scheme\s*:\s*dark/i,
726
+ /color-scheme\s*:\s*dark/i
727
+ ];
719
728
  for (const cssContent of cssContents) {
720
- if (cssContent.includes(".dark") || cssContent.includes('[data-theme="dark"]') || cssContent.includes("prefers-color-scheme: dark") || cssContent.includes("color-scheme: dark")) {
729
+ if (darkSelectors.some((selector) => selector.test(cssContent))) {
721
730
  return true;
722
731
  }
723
732
  }
@@ -763,6 +772,46 @@ function detectDarkMode(projectRoot, cssContents) {
763
772
  }
764
773
  return false;
765
774
  }
775
+ function collectCssFiles(projectRoot) {
776
+ const seen = /* @__PURE__ */ new Set();
777
+ const ordered = [];
778
+ const add = (path) => {
779
+ if (seen.has(path)) return;
780
+ if (!existsSync3(join3(projectRoot, path))) return;
781
+ seen.add(path);
782
+ ordered.push(path);
783
+ };
784
+ for (const rel of GLOBALS_CSS_PATHS) add(rel);
785
+ for (const rel of DECANTR_STYLE_PATHS) add(rel);
786
+ const roots = ["src/styles", "styles", "assets/css", "src/app", "app"];
787
+ const visit = (dir) => {
788
+ const absolute = join3(projectRoot, dir);
789
+ if (!existsSync3(absolute)) return;
790
+ let entries;
791
+ try {
792
+ entries = readdirSync3(absolute);
793
+ } catch {
794
+ return;
795
+ }
796
+ for (const entry of entries) {
797
+ const child = join3(absolute, entry);
798
+ let stat;
799
+ try {
800
+ stat = statSync3(child);
801
+ } catch {
802
+ continue;
803
+ }
804
+ if (stat.isDirectory()) {
805
+ if (entry === "node_modules" || entry === ".next" || entry === "dist") continue;
806
+ visit(relative3(projectRoot, child).replace(/\\/g, "/"));
807
+ } else if (stat.isFile() && /\.css$/i.test(entry)) {
808
+ add(relative3(projectRoot, child).replace(/\\/g, "/"));
809
+ }
810
+ }
811
+ };
812
+ for (const root of roots) visit(root);
813
+ return ordered.slice(0, 80);
814
+ }
766
815
  function scanStyling(projectRoot) {
767
816
  let approach = "unknown";
768
817
  let configFile;
@@ -811,23 +860,12 @@ function scanStyling(projectRoot) {
811
860
  configFile = decantrStyleFiles.join(" + ");
812
861
  }
813
862
  const cssContents = [];
814
- for (const rel of GLOBALS_CSS_PATHS) {
863
+ const cssFiles = collectCssFiles(projectRoot);
864
+ for (const rel of cssFiles) {
815
865
  const fullPath = join3(projectRoot, rel);
816
- if (existsSync3(fullPath)) {
817
- try {
818
- cssContents.push(readFileSync3(fullPath, "utf-8"));
819
- } catch {
820
- }
821
- }
822
- }
823
- for (const rel of DECANTR_STYLE_PATHS) {
824
- if (GLOBALS_CSS_PATHS.includes(rel)) continue;
825
- const fullPath = join3(projectRoot, rel);
826
- if (existsSync3(fullPath)) {
827
- try {
828
- cssContents.push(readFileSync3(fullPath, "utf-8"));
829
- } catch {
830
- }
866
+ try {
867
+ cssContents.push(readFileSync3(fullPath, "utf-8"));
868
+ } catch {
831
869
  }
832
870
  }
833
871
  let colors = {};
@@ -841,7 +879,7 @@ function scanStyling(projectRoot) {
841
879
  const darkMode = detectDarkMode(projectRoot, cssContents);
842
880
  if (approach === "unknown" && cssContents.length > 0) {
843
881
  approach = "css";
844
- configFile = GLOBALS_CSS_PATHS.find((rel) => existsSync3(join3(projectRoot, rel)));
882
+ configFile = cssFiles[0];
845
883
  }
846
884
  return {
847
885
  approach,
@@ -1206,11 +1244,11 @@ function scanBrownfieldIssues(projectRoot, essence) {
1206
1244
  }
1207
1245
 
1208
1246
  // src/guard-context.ts
1209
- import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync7 } from "fs";
1247
+ import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync7 } from "fs";
1210
1248
  import { join as join7 } from "path";
1211
1249
 
1212
1250
  // src/bundled-content.ts
1213
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
1251
+ import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "fs";
1214
1252
  import { dirname, join as join6 } from "path";
1215
1253
  import { fileURLToPath } from "url";
1216
1254
  function bundledDirCandidates(contentType) {
@@ -1244,7 +1282,7 @@ function loadBundledContentList(contentType) {
1244
1282
  for (const dir of bundledDirCandidates(contentType)) {
1245
1283
  if (!existsSync6(dir)) continue;
1246
1284
  try {
1247
- for (const file of readdirSync3(dir).filter((name) => name.endsWith(".json"))) {
1285
+ for (const file of readdirSync4(dir).filter((name) => name.endsWith(".json"))) {
1248
1286
  const id = file.replace(/\.json$/, "");
1249
1287
  if (seen.has(id)) continue;
1250
1288
  const path = join6(dir, file);
@@ -1262,7 +1300,7 @@ function loadBundledContentList(contentType) {
1262
1300
  function loadJsonEntries(dir) {
1263
1301
  if (!existsSync7(dir)) return [];
1264
1302
  try {
1265
- return readdirSync4(dir).filter((file) => file.endsWith(".json") && file !== "index.json").map((file) => JSON.parse(readFileSync7(join7(dir, file), "utf-8")));
1303
+ return readdirSync5(dir).filter((file) => file.endsWith(".json") && file !== "index.json").map((file) => JSON.parse(readFileSync7(join7(dir, file), "utf-8")));
1266
1304
  } catch {
1267
1305
  return [];
1268
1306
  }
@@ -1307,8 +1345,8 @@ function buildGuardRegistryContext(projectRoot = process.cwd()) {
1307
1345
  }
1308
1346
 
1309
1347
  // src/lib/scan-interactions.ts
1310
- import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
1311
- import { extname as extname2, join as join8, relative as relative3 } from "path";
1348
+ import { existsSync as existsSync8, readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
1349
+ import { extname as extname2, join as join8, relative as relative4 } from "path";
1312
1350
  import { verifyInteractionsInSource } from "@decantr/verifier";
1313
1351
  var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js", ".html", ".mdx"]);
1314
1352
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
@@ -1344,7 +1382,7 @@ function walkSourceTree(rootDir) {
1344
1382
  function walk2(dir) {
1345
1383
  let entries;
1346
1384
  try {
1347
- entries = readdirSync5(dir);
1385
+ entries = readdirSync6(dir);
1348
1386
  } catch {
1349
1387
  return;
1350
1388
  }
@@ -1353,11 +1391,11 @@ function walkSourceTree(rootDir) {
1353
1391
  const fullPath = join8(dir, entry);
1354
1392
  let s;
1355
1393
  try {
1356
- s = statSync3(fullPath);
1394
+ s = statSync4(fullPath);
1357
1395
  } catch {
1358
1396
  continue;
1359
1397
  }
1360
- const relativePath = relative3(rootDir, fullPath) || entry;
1398
+ const relativePath = relative4(rootDir, fullPath) || entry;
1361
1399
  if (s.isDirectory()) {
1362
1400
  walk2(fullPath);
1363
1401
  } else if (s.isFile() && SCAN_EXTENSIONS.has(extname2(entry))) {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createProjectHealthReport,
3
3
  listWorkspaceAppCandidates
4
- } from "./chunk-WPP3JEMC.js";
4
+ } from "./chunk-PHRVC575.js";
5
5
 
6
6
  // src/commands/workspace.ts
7
7
  import { execFileSync } from "child_process";
@@ -3,7 +3,7 @@ import {
3
3
  sendProjectHealthCiFailedTelemetry,
4
4
  sendProjectHealthPromptTelemetry,
5
5
  sendProjectHealthReportTelemetry
6
- } from "./chunk-4SZ4SEKT.js";
6
+ } from "./chunk-CAKQWTZS.js";
7
7
 
8
8
  // src/commands/health.ts
9
9
  import { execFileSync } from "child_process";
@@ -330,10 +330,11 @@ function normalizeHealthCategory(category, source) {
330
330
  return category;
331
331
  }
332
332
  function contractAssertionApplies(assertion, metadata) {
333
- if (assertion.rule === "tokens-file-present" && metadata.adoptionMode === "contract-only") {
333
+ const projectOwnedStyling = metadata.adoptionMode === "contract-only" || metadata.adoptionMode === "style-bridge";
334
+ if (assertion.rule === "tokens-file-present" && projectOwnedStyling) {
334
335
  return false;
335
336
  }
336
- if (metadata.adoptionMode === "contract-only" && (assertion.rule === "pack-manifest-present" || assertion.rule === "review-pack-present")) {
337
+ if (projectOwnedStyling && (assertion.rule === "pack-manifest-present" || assertion.rule === "review-pack-present")) {
337
338
  return false;
338
339
  }
339
340
  return true;
@@ -713,7 +714,7 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
713
714
  message: "Browser verification was requested, but Playwright is not installed in this project.",
714
715
  evidence: ["Expected dependency: playwright or @playwright/test"],
715
716
  rule: "browser-playwright-missing",
716
- suggestedFix: "Install Playwright in the project or rerun without `--browser` for static-only evidence.",
717
+ suggestedFix: "Install Playwright in the project or omit browser/base-url evidence for static-only health checks.",
717
718
  baseId: "playwright-missing"
718
719
  });
719
720
  return {
@@ -1404,7 +1405,7 @@ async function cmdHealth(projectRoot = process.cwd(), options = {}) {
1404
1405
  const reason = evidenceBundle.browser.findings[0] ?? "Playwright is not available to Decantr in this project.";
1405
1406
  console.log(`${YELLOW}Browser evidence unavailable:${RESET} ${reason}`);
1406
1407
  console.log(
1407
- `${DIM}Static evidence was still written. Install Playwright or rerun without --browser if screenshots are not needed.${RESET}`
1408
+ `${DIM}Static evidence was still written. Install Playwright or omit --browser/--base-url evidence if screenshots are not needed.${RESET}`
1408
1409
  );
1409
1410
  }
1410
1411
  }