@decantr/cli 2.7.0 → 2.8.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.
@@ -15,6 +15,9 @@ import {
15
15
  syncRegistry,
16
16
  writeExecutionPackBundleArtifacts
17
17
  } from "./chunk-V3XAQWKD.js";
18
+ import {
19
+ resolveWorkspaceInfo
20
+ } from "./chunk-VE6N3XWG.js";
18
21
  import {
19
22
  buildGuardRegistryContext,
20
23
  createDoctrineMap,
@@ -34,10 +37,10 @@ import {
34
37
  } from "./chunk-KT2ROK2D.js";
35
38
 
36
39
  // src/index.ts
37
- import { existsSync as existsSync27, mkdirSync as mkdirSync13, readdirSync as readdirSync7, readFileSync as readFileSync20, writeFileSync as writeFileSync16 } from "fs";
38
- import { basename as basename2, dirname as dirname4, isAbsolute, join as join28, resolve as resolve4 } from "path";
40
+ import { existsSync as existsSync27, mkdirSync as mkdirSync14, readdirSync as readdirSync7, readFileSync as readFileSync20, writeFileSync as writeFileSync17 } from "fs";
41
+ import { basename as basename3, dirname as dirname3, isAbsolute, join as join28, resolve as resolve3 } from "path";
39
42
  import { fileURLToPath as fileURLToPath2 } from "url";
40
- import { evaluateGuard, isV4 as isV47, validateEssence as validateEssence2 } from "@decantr/essence-spec";
43
+ import { evaluateGuard, isV4 as isV48, validateEssence as validateEssence2 } from "@decantr/essence-spec";
41
44
  import {
42
45
  CONTENT_TYPE_TO_API_CONTENT_TYPE as CONTENT_TYPE_TO_API_CONTENT_TYPE3,
43
46
  CONTENT_TYPES as GET_CONTENT_TYPES,
@@ -5271,6 +5274,488 @@ async function cmdThemeSwitch(themeName, args, projectRoot = process.cwd()) {
5271
5274
  console.log(`${YELLOW7}Guard will flag code using old tokens. Run \`decantr check\`.${RESET12}`);
5272
5275
  }
5273
5276
 
5277
+ // src/local-law.ts
5278
+ import { execFileSync } from "child_process";
5279
+ import { existsSync as existsSync25, mkdirSync as mkdirSync12, readdirSync as readdirSync5, readFileSync as readFileSync18, statSync as statSync5, writeFileSync as writeFileSync15 } from "fs";
5280
+ import { basename as basename2, extname, join as join26, relative as relative2, sep } from "path";
5281
+ import { isV4 as isV47 } from "@decantr/essence-spec";
5282
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
5283
+ ".astro",
5284
+ ".html",
5285
+ ".js",
5286
+ ".jsx",
5287
+ ".svelte",
5288
+ ".ts",
5289
+ ".tsx",
5290
+ ".vue"
5291
+ ]);
5292
+ var UI_TEMPLATE_EXTENSIONS = /* @__PURE__ */ new Set([".astro", ".html", ".jsx", ".svelte", ".tsx", ".vue"]);
5293
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
5294
+ ".decantr",
5295
+ ".git",
5296
+ ".next",
5297
+ ".nuxt",
5298
+ ".svelte-kit",
5299
+ "build",
5300
+ "coverage",
5301
+ "dist",
5302
+ "node_modules",
5303
+ "out"
5304
+ ]);
5305
+ var DEFAULT_RULE_EXTENSIONS = [".astro", ".html", ".jsx", ".svelte", ".tsx", ".vue"];
5306
+ function localPatternsProposalPath(projectRoot) {
5307
+ return join26(projectRoot, ".decantr", "local-patterns.proposal.json");
5308
+ }
5309
+ function localPatternsPath(projectRoot) {
5310
+ return join26(projectRoot, ".decantr", "local-patterns.json");
5311
+ }
5312
+ function localRulesProposalPath(projectRoot) {
5313
+ return join26(projectRoot, ".decantr", "rules.proposal.json");
5314
+ }
5315
+ function localRulesPath(projectRoot) {
5316
+ return join26(projectRoot, ".decantr", "rules.json");
5317
+ }
5318
+ function readLocalPatternPack(projectRoot) {
5319
+ return readJsonFile(localPatternsPath(projectRoot));
5320
+ }
5321
+ function readLocalRuleManifest(projectRoot) {
5322
+ return readJsonFile(localRulesPath(projectRoot));
5323
+ }
5324
+ function createBrownfieldCodifyProposal(input) {
5325
+ const sourceFiles = input.fromAudit ? listSourceFiles(input.projectRoot, 800) : [];
5326
+ const evidence = summarizeSourceEvidence(input.projectRoot, sourceFiles);
5327
+ const routes = input.essence && isV47(input.essence) ? Object.keys(input.essence.blueprint.routes ?? {}).sort() : [];
5328
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
5329
+ const patternPack = {
5330
+ version: 2,
5331
+ generatedAt,
5332
+ status: "proposal",
5333
+ source: input.fromAudit ? "decantr codify --from-audit" : "decantr codify",
5334
+ project: {
5335
+ framework: input.detected.framework,
5336
+ packageManager: input.detected.packageManager,
5337
+ hasTailwind: input.detected.hasTailwind,
5338
+ ruleFiles: input.detected.existingRuleFiles,
5339
+ routeCount: routes.length
5340
+ },
5341
+ purpose: "Project-owned Brownfield UI law. Review and edit before accepting; Decantr treats this as authoritative only after it is copied to .decantr/local-patterns.json.",
5342
+ patterns: [
5343
+ {
5344
+ id: "button",
5345
+ label: "Button primitives",
5346
+ role: "Actions and command triggers",
5347
+ appliesTo: [
5348
+ "primary action",
5349
+ "secondary action",
5350
+ "tertiary action",
5351
+ "destructive action",
5352
+ "icon action"
5353
+ ],
5354
+ componentPaths: evidence.buttonComponents,
5355
+ decide: "Define primary, secondary, tertiary, destructive, icon-only, disabled, and loading button variants from this app.",
5356
+ evidence: evidence.buttonComponents.length ? evidence.buttonComponents : [
5357
+ "No obvious Button wrapper found yet. Add the project-owned wrapper path before strict enforcement."
5358
+ ],
5359
+ forbiddenAlternatives: ["New one-off button variants without updating this manifest."]
5360
+ },
5361
+ {
5362
+ id: "surface-card",
5363
+ label: "Cards and surfaces",
5364
+ role: "Cards, panels, and reusable content surfaces",
5365
+ appliesTo: ["cards", "panels", "modals", "list items", "dashboard tiles"],
5366
+ componentPaths: evidence.cardComponents,
5367
+ decide: "Define the canonical card background, border, radius, shadow, padding, density, and hover treatment.",
5368
+ classHints: evidence.cardClassHints,
5369
+ evidence: evidence.cardComponents.length ? evidence.cardComponents : [
5370
+ "No obvious Card wrapper found yet. Add the project-owned wrapper path or class recipe."
5371
+ ],
5372
+ forbiddenAlternatives: ["Flat ad hoc cards with unique color/radius/shadow recipes."]
5373
+ },
5374
+ {
5375
+ id: "page-shell",
5376
+ label: "Page shell and spacing",
5377
+ role: "Route shell, navigation, gutters, max-width, and scroll ownership",
5378
+ appliesTo: ["routes", "layouts", "navigation shells", "scroll containers"],
5379
+ componentPaths: evidence.shellComponents,
5380
+ decide: "Define which layout owns max width, gutters, sticky chrome, responsive breakpoints, and scroll containers.",
5381
+ evidence: evidence.shellComponents.length ? evidence.shellComponents : ["Add root layout, shell, or app frame files that establish route chrome and spacing."],
5382
+ forbiddenAlternatives: [
5383
+ "Each page inventing independent max-width, padding, or sticky nav rules."
5384
+ ]
5385
+ },
5386
+ {
5387
+ id: "form-control",
5388
+ label: "Form controls",
5389
+ role: "Inputs, labels, validation, and form actions",
5390
+ appliesTo: ["inputs", "selects", "textareas", "validation messages", "form actions"],
5391
+ componentPaths: evidence.formComponents,
5392
+ decide: "Define input height, label placement, error copy, disabled state, required state, and focus treatment.",
5393
+ evidence: evidence.formComponents.length ? evidence.formComponents : ["Add form field wrapper paths and validation examples."],
5394
+ forbiddenAlternatives: [
5395
+ "Unlabeled one-off inputs or validation states that do not match the app standard."
5396
+ ]
5397
+ },
5398
+ {
5399
+ id: "theme-variant",
5400
+ label: "Theme variants",
5401
+ role: "Light, dark, brand, density, and tenant/theme variants observed in the app",
5402
+ appliesTo: ["theme toggles", "mode-specific classes", "brand variants", "tenant variants"],
5403
+ componentPaths: evidence.themeComponents,
5404
+ decide: "Document which theme variants exist, where they are toggled, and which tokens/classes are legal per variant.",
5405
+ evidence: evidence.themeComponents.length ? evidence.themeComponents : ["If the app has dark/light or brand variants, add the toggles/providers here."],
5406
+ forbiddenAlternatives: [
5407
+ "Component-local theme forks that bypass shared theme providers or tokens."
5408
+ ]
5409
+ }
5410
+ ],
5411
+ starterRules: [
5412
+ "Prefer project-owned wrappers for repeated primitives once they exist.",
5413
+ "Avoid raw hex/rgb values in component templates unless documented as dynamic data.",
5414
+ "Avoid static inline styles for reusable visual treatment.",
5415
+ "When adding a new route, map it to an existing local pattern before inventing a new visual variant.",
5416
+ "When adding a theme variant, update .decantr/theme-inventory.json and this local pattern pack."
5417
+ ],
5418
+ nextSteps: [
5419
+ "Edit this proposal with real component paths and token/class recipes.",
5420
+ "Run decantr codify --accept after review.",
5421
+ "Use decantr task <route> before LLM edits so local law appears in task context.",
5422
+ "Run decantr verify --brownfield --local-patterns after edits.",
5423
+ "Wire deterministic project rules into ESLint, Biome, Storybook, visual tests, or CI where Decantr should not guess."
5424
+ ]
5425
+ };
5426
+ const ruleManifest = {
5427
+ version: 1,
5428
+ status: "proposal",
5429
+ generatedAt,
5430
+ source: input.fromAudit ? "decantr codify --from-audit" : "decantr codify",
5431
+ purpose: "Mechanical Brownfield checks owned by this project. These rules are intentionally local and stack-agnostic; edit before accepting.",
5432
+ enforcement: {
5433
+ defaultSeverity: "warn",
5434
+ mode: "warn",
5435
+ notes: [
5436
+ "Decantr local rules are a guardrail, not a replacement for ESLint, Biome, type checks, tests, or visual regression.",
5437
+ "Keep rules narrow enough that an LLM can fix findings without rewriting the app.",
5438
+ "Use error severity only after the team agrees the rule is stable."
5439
+ ]
5440
+ },
5441
+ rules: [
5442
+ {
5443
+ id: "no-inline-style",
5444
+ type: "forbid-regex",
5445
+ enabled: true,
5446
+ severity: "warn",
5447
+ description: "Reusable UI should not add static inline style attributes.",
5448
+ includeExtensions: DEFAULT_RULE_EXTENSIONS,
5449
+ pattern: "\\bstyle\\s*=",
5450
+ message: "Inline style found in UI template.",
5451
+ suggestedFix: "Move reusable visual treatment into the project style system, component wrapper, token, or documented local pattern.",
5452
+ maxFindings: 25
5453
+ },
5454
+ {
5455
+ id: "no-raw-color-literals",
5456
+ type: "forbid-regex",
5457
+ enabled: true,
5458
+ severity: "warn",
5459
+ description: "Component templates should not introduce raw hex/rgb color literals.",
5460
+ includeExtensions: DEFAULT_RULE_EXTENSIONS,
5461
+ pattern: "#(?:[0-9a-fA-F]{3,8})\\b|rgba?\\s*\\(",
5462
+ message: "Raw color literal found in UI template.",
5463
+ suggestedFix: "Use an existing project token/class, or document the exception in .decantr/local-patterns.json if the value is data-driven.",
5464
+ maxFindings: 25
5465
+ },
5466
+ {
5467
+ id: "prefer-button-wrapper",
5468
+ type: "forbid-regex",
5469
+ enabled: evidence.buttonComponents.length > 0,
5470
+ severity: "info",
5471
+ description: "Prefer the project-owned button primitive instead of new raw button markup.",
5472
+ includeExtensions: DEFAULT_RULE_EXTENSIONS,
5473
+ pattern: "<button[\\s>]",
5474
+ message: "Raw <button> usage found outside the detected button wrapper.",
5475
+ suggestedFix: "Use the project-owned Button primitive, or add this file to allowedPaths if it is the primitive implementation.",
5476
+ allowedPaths: evidence.buttonComponents,
5477
+ maxFindings: 50
5478
+ }
5479
+ ]
5480
+ };
5481
+ return { patternPack, ruleManifest };
5482
+ }
5483
+ function writeBrownfieldCodifyProposal(projectRoot, proposal) {
5484
+ const decantrDir = join26(projectRoot, ".decantr");
5485
+ mkdirSync12(decantrDir, { recursive: true });
5486
+ const patternPath = localPatternsProposalPath(projectRoot);
5487
+ const rulesPath = localRulesProposalPath(projectRoot);
5488
+ writeFileSync15(patternPath, `${JSON.stringify(proposal.patternPack, null, 2)}
5489
+ `, "utf-8");
5490
+ writeFileSync15(rulesPath, `${JSON.stringify(proposal.ruleManifest, null, 2)}
5491
+ `, "utf-8");
5492
+ return { patternPath, rulesPath };
5493
+ }
5494
+ function acceptBrownfieldLocalLaw(projectRoot) {
5495
+ const patternProposal = readJsonFile(localPatternsProposalPath(projectRoot));
5496
+ const ruleProposal = readJsonFile(localRulesProposalPath(projectRoot));
5497
+ const acceptedAt = (/* @__PURE__ */ new Date()).toISOString();
5498
+ let patternAcceptedPath = null;
5499
+ let rulesAcceptedPath = null;
5500
+ if (patternProposal) {
5501
+ patternProposal.status = "accepted";
5502
+ patternProposal.acceptedAt = acceptedAt;
5503
+ patternAcceptedPath = localPatternsPath(projectRoot);
5504
+ writeFileSync15(patternAcceptedPath, `${JSON.stringify(patternProposal, null, 2)}
5505
+ `, "utf-8");
5506
+ }
5507
+ if (ruleProposal) {
5508
+ ruleProposal.status = "accepted";
5509
+ ruleProposal.acceptedAt = acceptedAt;
5510
+ rulesAcceptedPath = localRulesPath(projectRoot);
5511
+ writeFileSync15(rulesAcceptedPath, `${JSON.stringify(ruleProposal, null, 2)}
5512
+ `, "utf-8");
5513
+ }
5514
+ return { patternAcceptedPath, rulesAcceptedPath };
5515
+ }
5516
+ function validateLocalLaw(projectRoot) {
5517
+ const patternsPath = localPatternsPath(projectRoot);
5518
+ const rulesPath = localRulesPath(projectRoot);
5519
+ const patternPack = readJsonFile(patternsPath);
5520
+ const ruleManifest = readJsonFile(rulesPath);
5521
+ const warnings = [];
5522
+ if (patternPack) {
5523
+ const patternIds = /* @__PURE__ */ new Set();
5524
+ const patterns = Array.isArray(patternPack.patterns) ? patternPack.patterns : [];
5525
+ if (patterns.length === 0) {
5526
+ warnings.push(".decantr/local-patterns.json has no patterns.");
5527
+ }
5528
+ for (const pattern of patterns) {
5529
+ const id = typeof pattern.id === "string" ? pattern.id.trim() : "";
5530
+ if (!id) warnings.push("A local pattern is missing an id.");
5531
+ if (id && patternIds.has(id)) warnings.push(`Duplicate local pattern id: ${id}`);
5532
+ if (id) patternIds.add(id);
5533
+ const paths = Array.isArray(pattern.componentPaths) ? pattern.componentPaths : [];
5534
+ const evidence = Array.isArray(pattern.evidence) ? pattern.evidence : [];
5535
+ const todoEvidence = Array.isArray(pattern.evidenceToCollect) ? pattern.evidenceToCollect : [];
5536
+ if (id && paths.length === 0 && evidence.length === 0 && todoEvidence.length > 0) {
5537
+ warnings.push(
5538
+ `Local pattern ${id} still reads like a TODO; add concrete component paths or evidence.`
5539
+ );
5540
+ }
5541
+ }
5542
+ }
5543
+ if (ruleManifest && !Array.isArray(ruleManifest.rules)) {
5544
+ warnings.push(".decantr/rules.json has no rules array.");
5545
+ }
5546
+ const findings = ruleManifest ? scanLocalRules(projectRoot, ruleManifest) : [];
5547
+ return {
5548
+ patternsPath,
5549
+ rulesPath,
5550
+ patternPackPresent: Boolean(patternPack),
5551
+ ruleManifestPresent: Boolean(ruleManifest),
5552
+ warnings,
5553
+ findings
5554
+ };
5555
+ }
5556
+ function createLocalLawTaskSummary(projectRoot) {
5557
+ const patternPack = readLocalPatternPack(projectRoot);
5558
+ const ruleManifest = readLocalRuleManifest(projectRoot);
5559
+ const patterns = (patternPack?.patterns ?? []).map((pattern) => ({
5560
+ id: typeof pattern.id === "string" ? pattern.id : "unknown",
5561
+ role: typeof pattern.role === "string" ? pattern.role : null,
5562
+ componentPaths: Array.isArray(pattern.componentPaths) ? pattern.componentPaths.filter((entry) => typeof entry === "string") : []
5563
+ }));
5564
+ const rules = (ruleManifest?.rules ?? []).map((rule) => ({
5565
+ id: rule.id,
5566
+ severity: rule.severity,
5567
+ enabled: rule.enabled,
5568
+ description: rule.description
5569
+ }));
5570
+ return {
5571
+ patternsPath: patternPack ? ".decantr/local-patterns.json" : null,
5572
+ rulesPath: ruleManifest ? ".decantr/rules.json" : null,
5573
+ patternCount: patterns.length,
5574
+ ruleCount: rules.length,
5575
+ patterns,
5576
+ rules
5577
+ };
5578
+ }
5579
+ function changedFiles(projectRoot, since) {
5580
+ const changed = /* @__PURE__ */ new Set();
5581
+ try {
5582
+ const commands = since ? [
5583
+ ["diff", "--name-only", since, "--"],
5584
+ ["diff", "--name-only", "--cached"]
5585
+ ] : [
5586
+ ["diff", "--name-only"],
5587
+ ["diff", "--name-only", "--cached"]
5588
+ ];
5589
+ for (const args of commands) {
5590
+ const output = execFileSync("git", args, {
5591
+ cwd: projectRoot,
5592
+ encoding: "utf-8",
5593
+ stdio: ["ignore", "pipe", "ignore"]
5594
+ });
5595
+ for (const line of output.split(/\r?\n/)) {
5596
+ const file = line.trim();
5597
+ if (file) changed.add(normalizePath(file));
5598
+ }
5599
+ }
5600
+ } catch {
5601
+ }
5602
+ return [...changed].sort();
5603
+ }
5604
+ function routeImpacts(projectRoot, files) {
5605
+ const analysis = readJsonFile(
5606
+ join26(projectRoot, ".decantr", "analysis.json")
5607
+ );
5608
+ const routeEntries = analysis?.routes?.routes ?? [];
5609
+ const impacted = /* @__PURE__ */ new Set();
5610
+ for (const file of files) {
5611
+ for (const route of routeEntries) {
5612
+ if (route.file && pathMatches(file, route.file)) {
5613
+ if (route.path) impacted.add(route.path);
5614
+ }
5615
+ }
5616
+ }
5617
+ return [...impacted].sort();
5618
+ }
5619
+ function scanLocalRules(projectRoot, manifest) {
5620
+ const findings = [];
5621
+ const files = listSourceFiles(projectRoot, 1200);
5622
+ for (const rule of manifest.rules ?? []) {
5623
+ if (!rule.enabled || rule.type !== "forbid-regex") continue;
5624
+ const extensions = new Set(
5625
+ rule.includeExtensions?.length ? rule.includeExtensions : DEFAULT_RULE_EXTENSIONS
5626
+ );
5627
+ let regex;
5628
+ try {
5629
+ regex = new RegExp(rule.pattern, "g");
5630
+ } catch {
5631
+ findings.push({
5632
+ ruleId: rule.id,
5633
+ severity: "error",
5634
+ file: ".decantr/rules.json",
5635
+ line: 1,
5636
+ column: 1,
5637
+ excerpt: rule.pattern,
5638
+ message: `Invalid regex for local rule ${rule.id}.`,
5639
+ suggestedFix: "Edit .decantr/rules.json so the pattern is a valid JavaScript regular expression."
5640
+ });
5641
+ continue;
5642
+ }
5643
+ let ruleFindingCount = 0;
5644
+ for (const file of files) {
5645
+ if (!extensions.has(extname(file.absolute))) continue;
5646
+ if (pathAllowed(file.relative, rule.allowedPaths ?? [])) continue;
5647
+ const contents = readFileSync18(file.absolute, "utf-8");
5648
+ for (const match of contents.matchAll(regex)) {
5649
+ const index = match.index ?? 0;
5650
+ const position = lineColumnAt(contents, index);
5651
+ findings.push({
5652
+ ruleId: rule.id,
5653
+ severity: rule.severity,
5654
+ file: file.relative,
5655
+ line: position.line,
5656
+ column: position.column,
5657
+ excerpt: lineAt(contents, position.line).trim().slice(0, 180),
5658
+ message: rule.message,
5659
+ suggestedFix: rule.suggestedFix
5660
+ });
5661
+ ruleFindingCount += 1;
5662
+ if (rule.maxFindings && ruleFindingCount >= rule.maxFindings) break;
5663
+ }
5664
+ if (rule.maxFindings && ruleFindingCount >= rule.maxFindings) break;
5665
+ }
5666
+ }
5667
+ return findings;
5668
+ }
5669
+ function summarizeSourceEvidence(projectRoot, files) {
5670
+ const componentPaths = files.filter((file) => /(^|[/\\])components?([/\\]|$)|(^|[/\\])ui([/\\]|$)/i.test(file.relative)).map((file) => file.relative);
5671
+ const byName = (terms) => componentPaths.filter((file) => terms.some((term) => basename2(file).toLowerCase().includes(term))).slice(0, 12);
5672
+ const themeComponents = componentPaths.filter((file) => /theme|provider|mode|appearance|tenant|brand/i.test(file)).slice(0, 12);
5673
+ const shellComponents = files.filter((file) => /layout|shell|frame|app|root|nav|sidebar/i.test(basename2(file.relative))).map((file) => file.relative).slice(0, 12);
5674
+ return {
5675
+ buttonComponents: byName(["button", "action"]),
5676
+ cardComponents: byName(["card", "panel", "surface", "tile"]),
5677
+ formComponents: byName(["input", "field", "form", "select", "textarea"]),
5678
+ shellComponents,
5679
+ themeComponents,
5680
+ cardClassHints: collectClassHints(projectRoot, files, ["card", "panel", "surface", "tile"])
5681
+ };
5682
+ }
5683
+ function collectClassHints(projectRoot, files, terms) {
5684
+ const hints = /* @__PURE__ */ new Map();
5685
+ for (const file of files) {
5686
+ if (!UI_TEMPLATE_EXTENSIONS.has(extname(file.absolute))) continue;
5687
+ const content = readFileSync18(join26(projectRoot, file.relative), "utf-8");
5688
+ if (!terms.some((term) => content.toLowerCase().includes(term))) continue;
5689
+ const matches = content.matchAll(/\bclass(?:Name)?\s*=\s*["'`]([^"'`]+)["'`]/g);
5690
+ for (const match of matches) {
5691
+ const value = match[1].trim();
5692
+ if (!/(card|panel|surface|rounded|shadow|border|bg-|p-\d|px-|py-)/i.test(value)) continue;
5693
+ hints.set(value, (hints.get(value) ?? 0) + 1);
5694
+ }
5695
+ }
5696
+ return [...hints.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8).map(([hint]) => hint);
5697
+ }
5698
+ function listSourceFiles(projectRoot, maxFiles) {
5699
+ const files = [];
5700
+ const visit = (dir) => {
5701
+ if (files.length >= maxFiles) return;
5702
+ let entries;
5703
+ try {
5704
+ entries = readdirSync5(dir);
5705
+ } catch {
5706
+ return;
5707
+ }
5708
+ for (const entry of entries) {
5709
+ if (files.length >= maxFiles) return;
5710
+ if (IGNORED_DIRS.has(entry)) continue;
5711
+ const absolute = join26(dir, entry);
5712
+ let stat;
5713
+ try {
5714
+ stat = statSync5(absolute);
5715
+ } catch {
5716
+ continue;
5717
+ }
5718
+ if (stat.isDirectory()) {
5719
+ visit(absolute);
5720
+ } else if (stat.isFile() && SOURCE_EXTENSIONS.has(extname(entry))) {
5721
+ files.push({ absolute, relative: normalizePath(relative2(projectRoot, absolute)) });
5722
+ }
5723
+ }
5724
+ };
5725
+ visit(projectRoot);
5726
+ return files.sort((a, b) => a.relative.localeCompare(b.relative));
5727
+ }
5728
+ function readJsonFile(path) {
5729
+ if (!existsSync25(path)) return null;
5730
+ try {
5731
+ return JSON.parse(readFileSync18(path, "utf-8"));
5732
+ } catch {
5733
+ return null;
5734
+ }
5735
+ }
5736
+ function pathAllowed(file, allowedPaths) {
5737
+ return allowedPaths.some((allowedPath) => pathMatches(file, allowedPath));
5738
+ }
5739
+ function pathMatches(file, pattern) {
5740
+ const normalizedFile = normalizePath(file);
5741
+ const normalizedPattern = normalizePath(pattern);
5742
+ return normalizedFile === normalizedPattern || normalizedFile.endsWith(`/${normalizedPattern}`);
5743
+ }
5744
+ function normalizePath(path) {
5745
+ return path.split(sep).join("/").replace(/\\/g, "/");
5746
+ }
5747
+ function lineColumnAt(contents, index) {
5748
+ const before = contents.slice(0, index);
5749
+ const lines = before.split(/\r?\n/);
5750
+ return {
5751
+ line: lines.length,
5752
+ column: lines[lines.length - 1].length + 1
5753
+ };
5754
+ }
5755
+ function lineAt(contents, line) {
5756
+ return contents.split(/\r?\n/)[line - 1] ?? "";
5757
+ }
5758
+
5274
5759
  // src/prompts.ts
5275
5760
  import { createInterface } from "readline";
5276
5761
  var BOLD6 = "\x1B[1m";
@@ -5282,10 +5767,10 @@ var CYAN7 = "\x1B[36m";
5282
5767
  function ask(question, defaultValue) {
5283
5768
  const rl = createInterface({ input: process.stdin, output: process.stdout });
5284
5769
  const prompt = defaultValue ? `${question} ${DIM13}(${defaultValue})${RESET13}: ` : `${question}: `;
5285
- return new Promise((resolve5) => {
5770
+ return new Promise((resolve4) => {
5286
5771
  rl.question(prompt, (answer) => {
5287
5772
  rl.close();
5288
- resolve5(answer.trim() || defaultValue || "");
5773
+ resolve4(answer.trim() || defaultValue || "");
5289
5774
  });
5290
5775
  });
5291
5776
  }
@@ -5536,7 +6021,7 @@ function mergeWithDefaults(flags, detected, workflowSeed) {
5536
6021
  }
5537
6022
  async function runSimplifiedInit(blueprints) {
5538
6023
  const rl = createInterface({ input: process.stdin, output: process.stdout });
5539
- const question = (q) => new Promise((resolve5) => rl.question(q, resolve5));
6024
+ const question = (q) => new Promise((resolve4) => rl.question(q, resolve4));
5540
6025
  console.log("\n? What blueprint would you like to scaffold?\n");
5541
6026
  console.log(" 1. Decantr default (recommended)");
5542
6027
  console.log(" 2. Search registry...\n");
@@ -5568,8 +6053,8 @@ async function runSimplifiedInit(blueprints) {
5568
6053
  }
5569
6054
 
5570
6055
  // src/theme-commands.ts
5571
- import { existsSync as existsSync25, mkdirSync as mkdirSync12, readdirSync as readdirSync5, readFileSync as readFileSync18, rmSync as rmSync3, writeFileSync as writeFileSync15 } from "fs";
5572
- import { join as join26 } from "path";
6056
+ import { existsSync as existsSync26, mkdirSync as mkdirSync13, readdirSync as readdirSync6, readFileSync as readFileSync19, rmSync as rmSync3, writeFileSync as writeFileSync16 } from "fs";
6057
+ import { join as join27 } from "path";
5573
6058
  var REQUIRED_FIELDS = [
5574
6059
  "$schema",
5575
6060
  "id",
@@ -5629,20 +6114,20 @@ function validateCustomTheme(theme) {
5629
6114
  };
5630
6115
  }
5631
6116
  function createTheme(projectRoot, id, name) {
5632
- const customThemesDir = join26(projectRoot, ".decantr", "custom", "themes");
5633
- const themePath = join26(customThemesDir, `${id}.json`);
5634
- const howToPath = join26(customThemesDir, "how-to-theme.md");
5635
- mkdirSync12(customThemesDir, { recursive: true });
5636
- if (existsSync25(themePath)) {
6117
+ const customThemesDir = join27(projectRoot, ".decantr", "custom", "themes");
6118
+ const themePath = join27(customThemesDir, `${id}.json`);
6119
+ const howToPath = join27(customThemesDir, "how-to-theme.md");
6120
+ mkdirSync13(customThemesDir, { recursive: true });
6121
+ if (existsSync26(themePath)) {
5637
6122
  return {
5638
6123
  success: false,
5639
6124
  error: `Theme "${id}" already exists at ${themePath}`
5640
6125
  };
5641
6126
  }
5642
6127
  const skeleton = getThemeSkeleton(id, name);
5643
- writeFileSync15(themePath, JSON.stringify(skeleton, null, 2));
5644
- if (!existsSync25(howToPath)) {
5645
- writeFileSync15(howToPath, getHowToThemeDoc());
6128
+ writeFileSync16(themePath, JSON.stringify(skeleton, null, 2));
6129
+ if (!existsSync26(howToPath)) {
6130
+ writeFileSync16(howToPath, getHowToThemeDoc());
5646
6131
  }
5647
6132
  return {
5648
6133
  success: true,
@@ -5650,17 +6135,17 @@ function createTheme(projectRoot, id, name) {
5650
6135
  };
5651
6136
  }
5652
6137
  function listCustomThemes(projectRoot) {
5653
- const customThemesDir = join26(projectRoot, ".decantr", "custom", "themes");
5654
- if (!existsSync25(customThemesDir)) {
6138
+ const customThemesDir = join27(projectRoot, ".decantr", "custom", "themes");
6139
+ if (!existsSync26(customThemesDir)) {
5655
6140
  return [];
5656
6141
  }
5657
6142
  const themes = [];
5658
6143
  try {
5659
- const files = readdirSync5(customThemesDir).filter((f) => f.endsWith(".json"));
6144
+ const files = readdirSync6(customThemesDir).filter((f) => f.endsWith(".json"));
5660
6145
  for (const file of files) {
5661
- const filePath = join26(customThemesDir, file);
6146
+ const filePath = join27(customThemesDir, file);
5662
6147
  try {
5663
- const data = JSON.parse(readFileSync18(filePath, "utf-8"));
6148
+ const data = JSON.parse(readFileSync19(filePath, "utf-8"));
5664
6149
  themes.push({
5665
6150
  id: data.id || file.replace(".json", ""),
5666
6151
  name: data.name || data.id,
@@ -5675,8 +6160,8 @@ function listCustomThemes(projectRoot) {
5675
6160
  return themes;
5676
6161
  }
5677
6162
  function deleteTheme(projectRoot, id) {
5678
- const themePath = join26(projectRoot, ".decantr", "custom", "themes", `${id}.json`);
5679
- if (!existsSync25(themePath)) {
6163
+ const themePath = join27(projectRoot, ".decantr", "custom", "themes", `${id}.json`);
6164
+ if (!existsSync26(themePath)) {
5680
6165
  return {
5681
6166
  success: false,
5682
6167
  error: `Theme "${id}" not found at ${themePath}`
@@ -5693,7 +6178,7 @@ function deleteTheme(projectRoot, id) {
5693
6178
  }
5694
6179
  }
5695
6180
  function importTheme(projectRoot, sourcePath) {
5696
- if (!existsSync25(sourcePath)) {
6181
+ if (!existsSync26(sourcePath)) {
5697
6182
  return {
5698
6183
  success: false,
5699
6184
  errors: [`Source file not found: ${sourcePath}`]
@@ -5701,7 +6186,7 @@ function importTheme(projectRoot, sourcePath) {
5701
6186
  }
5702
6187
  let theme;
5703
6188
  try {
5704
- theme = JSON.parse(readFileSync18(sourcePath, "utf-8"));
6189
+ theme = JSON.parse(readFileSync19(sourcePath, "utf-8"));
5705
6190
  } catch (e) {
5706
6191
  return {
5707
6192
  success: false,
@@ -5717,90 +6202,20 @@ function importTheme(projectRoot, sourcePath) {
5717
6202
  }
5718
6203
  theme.source = "custom";
5719
6204
  const id = theme.id;
5720
- const customThemesDir = join26(projectRoot, ".decantr", "custom", "themes");
5721
- const destPath = join26(customThemesDir, `${id}.json`);
5722
- mkdirSync12(customThemesDir, { recursive: true });
5723
- const howToPath = join26(customThemesDir, "how-to-theme.md");
5724
- if (!existsSync25(howToPath)) {
5725
- writeFileSync15(howToPath, getHowToThemeDoc());
5726
- }
5727
- writeFileSync15(destPath, JSON.stringify(theme, null, 2));
6205
+ const customThemesDir = join27(projectRoot, ".decantr", "custom", "themes");
6206
+ const destPath = join27(customThemesDir, `${id}.json`);
6207
+ mkdirSync13(customThemesDir, { recursive: true });
6208
+ const howToPath = join27(customThemesDir, "how-to-theme.md");
6209
+ if (!existsSync26(howToPath)) {
6210
+ writeFileSync16(howToPath, getHowToThemeDoc());
6211
+ }
6212
+ writeFileSync16(destPath, JSON.stringify(theme, null, 2));
5728
6213
  return {
5729
6214
  success: true,
5730
6215
  path: destPath
5731
6216
  };
5732
6217
  }
5733
6218
 
5734
- // src/workspace.ts
5735
- import { existsSync as existsSync26, readdirSync as readdirSync6, readFileSync as readFileSync19 } from "fs";
5736
- import { dirname as dirname3, join as join27, resolve as resolve3 } from "path";
5737
- function readPackageJson(dir) {
5738
- const path = join27(dir, "package.json");
5739
- if (!existsSync26(path)) return null;
5740
- try {
5741
- return JSON.parse(readFileSync19(path, "utf-8"));
5742
- } catch {
5743
- return null;
5744
- }
5745
- }
5746
- function hasWorkspaceMarker(dir) {
5747
- if (existsSync26(join27(dir, "pnpm-workspace.yaml")) || existsSync26(join27(dir, "turbo.json")) || existsSync26(join27(dir, "nx.json"))) {
5748
- return true;
5749
- }
5750
- const pkg = readPackageJson(dir);
5751
- return Boolean(pkg?.workspaces);
5752
- }
5753
- function findWorkspaceRoot(startDir) {
5754
- let current = resolve3(startDir);
5755
- while (true) {
5756
- if (hasWorkspaceMarker(current)) return current;
5757
- const parent = dirname3(current);
5758
- if (parent === current) return null;
5759
- current = parent;
5760
- }
5761
- }
5762
- function looksLikeApp(dir) {
5763
- if (existsSync26(join27(dir, "next.config.js")) || existsSync26(join27(dir, "next.config.ts")) || existsSync26(join27(dir, "next.config.mjs")) || existsSync26(join27(dir, "vite.config.ts")) || existsSync26(join27(dir, "vite.config.js")) || existsSync26(join27(dir, "angular.json")) || existsSync26(join27(dir, "svelte.config.js")) || existsSync26(join27(dir, "svelte.config.ts")) || existsSync26(join27(dir, "astro.config.mjs")) || existsSync26(join27(dir, "src")) || existsSync26(join27(dir, "app")) || existsSync26(join27(dir, "pages"))) {
5764
- return true;
5765
- }
5766
- const pkg = readPackageJson(dir);
5767
- const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
5768
- return Boolean(
5769
- deps.react || deps.next || deps.vue || deps.svelte || deps["@angular/core"] || deps.astro || deps.nuxt
5770
- );
5771
- }
5772
- function listWorkspaceApps(workspaceRoot) {
5773
- const candidates = [];
5774
- for (const base of ["apps", "packages"]) {
5775
- const baseDir = join27(workspaceRoot, base);
5776
- if (!existsSync26(baseDir)) continue;
5777
- for (const entry of readdirSync6(baseDir, { withFileTypes: true })) {
5778
- if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
5779
- const candidate = join27(baseDir, entry.name);
5780
- if (looksLikeApp(candidate)) {
5781
- candidates.push(`${base}/${entry.name}`);
5782
- }
5783
- }
5784
- }
5785
- return candidates.sort();
5786
- }
5787
- function resolveWorkspaceInfo(cwd, projectArg) {
5788
- const absoluteCwd = resolve3(cwd);
5789
- const workspaceRoot = findWorkspaceRoot(absoluteCwd) ?? absoluteCwd;
5790
- const appRoot = projectArg ? resolve3(absoluteCwd, projectArg) : absoluteCwd;
5791
- const appCandidates = listWorkspaceApps(workspaceRoot);
5792
- const projectScope = workspaceRoot !== appRoot || appCandidates.length > 0 ? "workspace-app" : "single-app";
5793
- const requiresProjectSelection = !projectArg && workspaceRoot === absoluteCwd && appCandidates.length > 1;
5794
- return {
5795
- cwd: absoluteCwd,
5796
- workspaceRoot,
5797
- appRoot,
5798
- projectScope,
5799
- appCandidates,
5800
- requiresProjectSelection
5801
- };
5802
- }
5803
-
5804
6219
  // src/index.ts
5805
6220
  var BOLD7 = "\x1B[1m";
5806
6221
  var DIM14 = "\x1B[2m";
@@ -6328,7 +6743,7 @@ function getPublicAPIClient() {
6328
6743
  });
6329
6744
  }
6330
6745
  function resolveUserPath(inputPath, cwd = process.cwd()) {
6331
- return isAbsolute(inputPath) ? inputPath : resolve4(cwd, inputPath);
6746
+ return isAbsolute(inputPath) ? inputPath : resolve3(cwd, inputPath);
6332
6747
  }
6333
6748
  function extractHostedAssetPaths(indexHtml) {
6334
6749
  const assetPaths = /* @__PURE__ */ new Set();
@@ -6379,7 +6794,7 @@ function readHostedSourceSnapshot(sourcePath) {
6379
6794
  "build",
6380
6795
  "coverage"
6381
6796
  ]);
6382
- const rootPrefix = basename2(resolvedSourcePath);
6797
+ const rootPrefix = basename3(resolvedSourcePath);
6383
6798
  const walk = (absoluteDir, relativeDir) => {
6384
6799
  for (const entry of readdirSync7(absoluteDir, { withFileTypes: true })) {
6385
6800
  if (ignoredDirNames.has(entry.name)) continue;
@@ -6551,7 +6966,7 @@ async function printHostedExecutionPackBundle(essencePath, namespace, jsonOutput
6551
6966
  let writtenContextPaths = [];
6552
6967
  if (writeContext) {
6553
6968
  const contextDir = join28(process.cwd(), ".decantr", "context");
6554
- mkdirSync13(contextDir, { recursive: true });
6969
+ mkdirSync14(contextDir, { recursive: true });
6555
6970
  const written = writeExecutionPackBundleArtifacts(
6556
6971
  contextDir,
6557
6972
  bundle
@@ -6592,7 +7007,7 @@ function resolvePagePackIdForRoute(essencePath, route) {
6592
7007
  throw new Error(`Essence file not found at ${essencePath}`);
6593
7008
  }
6594
7009
  const essence = JSON.parse(readFileSync20(essencePath, "utf-8"));
6595
- if (!isV47(essence)) {
7010
+ if (!isV48(essence)) {
6596
7011
  throw new Error("Route-based pack resolution requires Essence v4.0.0.");
6597
7012
  }
6598
7013
  const target = essence.blueprint.routes?.[route];
@@ -6625,16 +7040,16 @@ async function printHostedSelectedExecutionPack(packType, id, essencePath, names
6625
7040
  let writtenContextDir = null;
6626
7041
  if (writeContext) {
6627
7042
  const contextDir = join28(process.cwd(), ".decantr", "context");
6628
- mkdirSync13(contextDir, { recursive: true });
6629
- writeFileSync16(
7043
+ mkdirSync14(contextDir, { recursive: true });
7044
+ writeFileSync17(
6630
7045
  join28(contextDir, "pack-manifest.json"),
6631
7046
  JSON.stringify(selected.manifest, null, 2) + "\n"
6632
7047
  );
6633
7048
  const manifestEntry = selected.selector.packType === "scaffold" ? selected.manifest.scaffold : selected.selector.packType === "review" ? selected.manifest.review : selected.selector.packType === "section" ? selected.manifest.sections.find((entry) => entry.id === selected.selector.id) : selected.selector.packType === "page" ? selected.manifest.pages.find((entry) => entry.id === selected.selector.id) : selected.manifest.mutations.find((entry) => entry.id === selected.selector.id);
6634
7049
  const markdownFile = manifestEntry?.markdown ?? `${selected.selector.packType}${selected.selector.id ? `-${selected.selector.id}` : ""}-pack.md`;
6635
7050
  const jsonFile = manifestEntry?.json ?? `${selected.selector.packType}${selected.selector.id ? `-${selected.selector.id}` : ""}-pack.json`;
6636
- writeFileSync16(join28(contextDir, markdownFile), selected.pack.renderedMarkdown);
6637
- writeFileSync16(join28(contextDir, jsonFile), JSON.stringify(selected.pack, null, 2) + "\n");
7051
+ writeFileSync17(join28(contextDir, markdownFile), selected.pack.renderedMarkdown);
7052
+ writeFileSync17(join28(contextDir, jsonFile), JSON.stringify(selected.pack, null, 2) + "\n");
6638
7053
  writtenContextDir = contextDir;
6639
7054
  }
6640
7055
  if (jsonOutput) {
@@ -6671,8 +7086,8 @@ async function printHostedExecutionPackManifest(essencePath, namespace, jsonOutp
6671
7086
  let writtenContextDir = null;
6672
7087
  if (writeContext) {
6673
7088
  const contextDir = join28(process.cwd(), ".decantr", "context");
6674
- mkdirSync13(contextDir, { recursive: true });
6675
- writeFileSync16(join28(contextDir, "pack-manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
7089
+ mkdirSync14(contextDir, { recursive: true });
7090
+ writeFileSync17(join28(contextDir, "pack-manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
6676
7091
  writtenContextDir = contextDir;
6677
7092
  }
6678
7093
  if (jsonOutput) {
@@ -6711,7 +7126,7 @@ async function hydrateHostedExecutionPacksIfMissing(projectRoot, namespace = "@o
6711
7126
  const client = getPublicAPIClient();
6712
7127
  const essence = JSON.parse(readFileSync20(essencePath, "utf-8"));
6713
7128
  const bundle = await client.compileExecutionPacks(essence, { namespace });
6714
- mkdirSync13(contextDir, { recursive: true });
7129
+ mkdirSync14(contextDir, { recursive: true });
6715
7130
  writeExecutionPackBundleArtifacts(contextDir, bundle);
6716
7131
  return { attempted: true, hydrated: true, scope: "bundle" };
6717
7132
  } catch {
@@ -6739,14 +7154,14 @@ async function hydrateHostedReviewPackIfMissing(projectRoot, namespace = "@offic
6739
7154
  },
6740
7155
  { namespace }
6741
7156
  );
6742
- mkdirSync13(contextDir, { recursive: true });
6743
- writeFileSync16(join28(contextDir, "review-pack.md"), selected.pack.renderedMarkdown);
6744
- writeFileSync16(
7157
+ mkdirSync14(contextDir, { recursive: true });
7158
+ writeFileSync17(join28(contextDir, "review-pack.md"), selected.pack.renderedMarkdown);
7159
+ writeFileSync17(
6745
7160
  join28(contextDir, "review-pack.json"),
6746
7161
  JSON.stringify(selected.pack, null, 2) + "\n"
6747
7162
  );
6748
7163
  if (!existsSync27(manifestPath)) {
6749
- writeFileSync16(manifestPath, JSON.stringify(selected.manifest, null, 2) + "\n");
7164
+ writeFileSync17(manifestPath, JSON.stringify(selected.manifest, null, 2) + "\n");
6750
7165
  }
6751
7166
  return { attempted: true, hydrated: true, scope: "review" };
6752
7167
  } catch {
@@ -7082,7 +7497,7 @@ async function cmdValidate(path) {
7082
7497
  process.exitCode = 1;
7083
7498
  return;
7084
7499
  }
7085
- const detectedVersion = isV47(essence) ? "v4" : "legacy";
7500
+ const detectedVersion = isV48(essence) ? "v4" : "legacy";
7086
7501
  console.log(`${DIM14}Detected essence version: ${detectedVersion}${RESET14}`);
7087
7502
  const result = validateEssence2(essence);
7088
7503
  if (result.valid) {
@@ -7217,7 +7632,7 @@ ${CYAN8}Telemetry enabled.${RESET14} Decantr will send privacy-filtered CLI prod
7217
7632
  console.log(`${DIM14}Set "telemetry": false in .decantr/project.json to opt out.${RESET14}`);
7218
7633
  }
7219
7634
  function readCliPackageVersion() {
7220
- const here = dirname4(fileURLToPath2(import.meta.url));
7635
+ const here = dirname3(fileURLToPath2(import.meta.url));
7221
7636
  const candidates = [join28(here, "..", "package.json"), join28(here, "..", "..", "package.json")];
7222
7637
  for (const candidate of candidates) {
7223
7638
  try {
@@ -7238,13 +7653,13 @@ function backupExistingEssence(projectRoot, label) {
7238
7653
  projectRoot,
7239
7654
  `decantr.essence.${label}.${timestampForFile()}.backup.json`
7240
7655
  );
7241
- writeFileSync16(backupPath, readFileSync20(essencePath, "utf-8"), "utf-8");
7656
+ writeFileSync17(backupPath, readFileSync20(essencePath, "utf-8"), "utf-8");
7242
7657
  return backupPath;
7243
7658
  }
7244
7659
  function writeBrownfieldProjectJson(input) {
7245
7660
  const decantrDir = join28(input.projectRoot, ".decantr");
7246
- mkdirSync13(join28(decantrDir, "context"), { recursive: true });
7247
- mkdirSync13(join28(decantrDir, "cache"), { recursive: true });
7661
+ mkdirSync14(join28(decantrDir, "context"), { recursive: true });
7662
+ mkdirSync14(join28(decantrDir, "cache"), { recursive: true });
7248
7663
  const now = (/* @__PURE__ */ new Date()).toISOString();
7249
7664
  const projectJson = {
7250
7665
  detected: {
@@ -7285,7 +7700,7 @@ function writeBrownfieldProjectJson(input) {
7285
7700
  }
7286
7701
  }
7287
7702
  };
7288
- writeFileSync16(join28(decantrDir, "project.json"), JSON.stringify(projectJson, null, 2) + "\n");
7703
+ writeFileSync17(join28(decantrDir, "project.json"), JSON.stringify(projectJson, null, 2) + "\n");
7289
7704
  }
7290
7705
  async function applyAcceptedBrownfieldProposal(input) {
7291
7706
  const proposal = readBrownfieldProposal(input.projectRoot);
@@ -7317,7 +7732,7 @@ async function applyAcceptedBrownfieldProposal(input) {
7317
7732
  }
7318
7733
  if (input.mode === "merge" && hasEssence) {
7319
7734
  const existing = JSON.parse(readFileSync20(essencePath, "utf-8"));
7320
- if (!isV47(existing)) {
7735
+ if (!isV48(existing)) {
7321
7736
  console.log(
7322
7737
  error3(
7323
7738
  "Existing essence is not v4. Run `decantr migrate --to v4` before merging a brownfield proposal."
@@ -7353,7 +7768,7 @@ async function applyAcceptedBrownfieldProposal(input) {
7353
7768
  assistantBridge: input.assistantBridge,
7354
7769
  mode: input.mode
7355
7770
  });
7356
- writeFileSync16(essencePath, JSON.stringify(essence, null, 2) + "\n", "utf-8");
7771
+ writeFileSync17(essencePath, JSON.stringify(essence, null, 2) + "\n", "utf-8");
7357
7772
  const registryClient = new RegistryClient({
7358
7773
  cacheDir: join28(input.projectRoot, ".decantr", "cache"),
7359
7774
  offline: true,
@@ -7414,8 +7829,7 @@ async function applyAcceptedBrownfieldProposal(input) {
7414
7829
  async function cmdInit(args) {
7415
7830
  const workspaceInfo = resolveWorkspaceInfo(process.cwd(), args.project);
7416
7831
  if (args.yes && workspaceInfo.requiresProjectSelection) {
7417
- console.log(error3("This looks like a workspace root with multiple app candidates."));
7418
- console.log(dim3(`Use --project=<path>. Candidates: ${workspaceInfo.appCandidates.join(", ")}`));
7832
+ printWorkspaceProjectSelection(workspaceInfo, "init");
7419
7833
  process.exitCode = 1;
7420
7834
  return;
7421
7835
  }
@@ -7899,7 +8313,7 @@ Validation warnings: ${validation.errors.join(", ")}`));
7899
8313
  }
7900
8314
  console.log("");
7901
8315
  let promptPages;
7902
- if (isV47(essence)) {
8316
+ if (isV48(essence)) {
7903
8317
  const allPages = essence.blueprint.sections.flatMap(
7904
8318
  (s) => s.pages.map((p) => ({ ...p, _sectionId: s.id, _shell: s.shell }))
7905
8319
  );
@@ -7957,14 +8371,14 @@ async function cmdStatus() {
7957
8371
  try {
7958
8372
  const essence = JSON.parse(readFileSync20(essencePath, "utf-8"));
7959
8373
  const validation = validateEssence2(essence);
7960
- const essenceVersion = isV47(essence) ? "v4" : "legacy";
8374
+ const essenceVersion = isV48(essence) ? "v4" : "legacy";
7961
8375
  console.log(`${BOLD7}Essence:${RESET14}`);
7962
8376
  if (validation.valid) {
7963
8377
  console.log(` ${GREEN14}Valid${RESET14} (${essenceVersion})`);
7964
8378
  } else {
7965
8379
  console.log(` ${RED11}Invalid: ${validation.errors.join(", ")}${RESET14}`);
7966
8380
  }
7967
- if (isV47(essence)) {
8381
+ if (isV48(essence)) {
7968
8382
  const v4 = essence;
7969
8383
  const sections = v4.blueprint.sections;
7970
8384
  const flatPages = sections.flatMap((section) => section.pages ?? []);
@@ -8353,12 +8767,69 @@ function withoutWorkflowOnlyFlags(args) {
8353
8767
  }
8354
8768
  return stripped;
8355
8769
  }
8356
- function resolveWorkflowProject(flags) {
8770
+ function withProject(command, projectArg) {
8771
+ return projectArg ? `${command} --project ${projectArg}` : command;
8772
+ }
8773
+ function firstWorkspaceCandidate(workspaceInfo) {
8774
+ return workspaceInfo.appCandidates[0] ?? "apps/web";
8775
+ }
8776
+ function printWorkspaceProjectSelection(workspaceInfo, commandName = "command") {
8777
+ const candidate = firstWorkspaceCandidate(workspaceInfo);
8778
+ const noun = commandName === "adopt" ? "Brownfield adoption" : `decantr ${commandName}`;
8779
+ console.log(error3(`${noun} needs an app path.`));
8780
+ console.log("");
8781
+ console.log(`${BOLD7}This looks like a monorepo.${RESET14}`);
8782
+ console.log("Install Decantr at the workspace root, then attach it to one app with --project.");
8783
+ console.log("");
8784
+ console.log("App candidates:");
8785
+ for (const appCandidate of workspaceInfo.appCandidates) {
8786
+ console.log(` ${appCandidate}`);
8787
+ }
8788
+ console.log("");
8789
+ console.log("Start by attaching one app:");
8790
+ console.log(` ${cyan3(`decantr adopt --project ${candidate} --yes`)}`);
8791
+ console.log("");
8792
+ console.log("Optional visual evidence after the app is running:");
8793
+ console.log(
8794
+ ` ${cyan3(`decantr verify --project ${candidate} --base-url http://localhost:3000 --evidence`)}`
8795
+ );
8796
+ }
8797
+ function printMonorepoSetupGuidance(workspaceInfo) {
8798
+ const candidate = firstWorkspaceCandidate(workspaceInfo);
8799
+ console.log(heading2("Decantr Setup"));
8800
+ console.log(`${BOLD7}This looks like a monorepo.${RESET14}`);
8801
+ console.log(` Workspace root: ${workspaceInfo.workspaceRoot}`);
8802
+ console.log("");
8803
+ console.log(
8804
+ "Install Decantr at the workspace root, then attach it to the app you want Decantr to govern."
8805
+ );
8806
+ console.log("");
8807
+ console.log("App candidates:");
8808
+ for (const appCandidate of workspaceInfo.appCandidates) {
8809
+ console.log(` ${appCandidate}`);
8810
+ }
8811
+ console.log("");
8812
+ console.log(`${BOLD7}Start here:${RESET14}`);
8813
+ console.log(
8814
+ ` ${cyan3("decantr workspace list")} Show attached projects and app candidates`
8815
+ );
8816
+ console.log(
8817
+ ` ${cyan3(`decantr adopt --project ${candidate} --yes`)} Attach Decantr to one app`
8818
+ );
8819
+ console.log(
8820
+ ` ${cyan3(`decantr codify --from-audit --project ${candidate}`)} Propose project-owned UI law`
8821
+ );
8822
+ console.log("");
8823
+ console.log(`${BOLD7}Optional visual evidence:${RESET14}`);
8824
+ console.log(
8825
+ ` ${cyan3(`decantr verify --project ${candidate} --base-url http://localhost:3000 --evidence`)}`
8826
+ );
8827
+ }
8828
+ function resolveWorkflowProject(flags, commandName = "command") {
8357
8829
  const projectArg = flagString(flags, "project");
8358
8830
  const workspaceInfo = resolveWorkspaceInfo(process.cwd(), projectArg);
8359
8831
  if (workspaceInfo.requiresProjectSelection) {
8360
- console.log(error3("This looks like a workspace root with multiple app candidates."));
8361
- console.log(dim3(`Use --project=<path>. Candidates: ${workspaceInfo.appCandidates.join(", ")}`));
8832
+ printWorkspaceProjectSelection(workspaceInfo, commandName);
8362
8833
  process.exitCode = 1;
8363
8834
  return null;
8364
8835
  }
@@ -8374,8 +8845,17 @@ function printWorkflowPlan(title, steps) {
8374
8845
  }
8375
8846
  async function cmdSetupWorkflow(args) {
8376
8847
  const { flags } = parseLooseArgs(args);
8377
- const workspaceInfo = resolveWorkflowProject(flags);
8378
- if (!workspaceInfo) return;
8848
+ const projectArg = flagString(flags, "project");
8849
+ const workspaceInfo = resolveWorkspaceInfo(process.cwd(), projectArg);
8850
+ if (!projectArg && workspaceInfo.workspaceRoot === workspaceInfo.cwd && workspaceInfo.appCandidates.length > 0) {
8851
+ printMonorepoSetupGuidance(workspaceInfo);
8852
+ return;
8853
+ }
8854
+ if (workspaceInfo.requiresProjectSelection) {
8855
+ printWorkspaceProjectSelection(workspaceInfo, "setup");
8856
+ process.exitCode = 1;
8857
+ return;
8858
+ }
8379
8859
  const detected = detectProject(workspaceInfo.appRoot);
8380
8860
  const hasFootprint = detected.framework !== "unknown" || detected.packageManager !== "unknown" || detected.hasTypeScript || detected.hasTailwind || detected.existingRuleFiles.length > 0;
8381
8861
  console.log(heading2("Decantr Setup"));
@@ -8384,18 +8864,30 @@ async function cmdSetupWorkflow(args) {
8384
8864
  console.log("");
8385
8865
  if (detected.existingEssence) {
8386
8866
  console.log(`${BOLD7}Recommended path:${RESET14} maintain an attached Decantr project`);
8387
- console.log(` ${cyan3('decantr task <route> "<change>"')} Prepare LLM context before edits`);
8388
- console.log(` ${cyan3("decantr verify")} Run local health and drift checks`);
8389
- console.log(` ${cyan3("decantr codify")} Propose project-owned UI patterns`);
8867
+ console.log(
8868
+ ` ${cyan3(withProject('decantr task <route> "<change>"', projectArg))} Prepare LLM context before edits`
8869
+ );
8870
+ console.log(
8871
+ ` ${cyan3(withProject("decantr verify --brownfield", projectArg))} Run local health and drift checks`
8872
+ );
8873
+ console.log(
8874
+ ` ${cyan3(withProject("decantr codify --from-audit", projectArg))} Propose project-owned local law`
8875
+ );
8390
8876
  return;
8391
8877
  }
8392
8878
  if (hasFootprint) {
8393
8879
  console.log(`${BOLD7}Recommended path:${RESET14} brownfield adoption`);
8394
- console.log(` ${cyan3("decantr adopt --yes")} Analyze, attach, and verify`);
8395
8880
  console.log(
8396
- ` ${cyan3("decantr adopt --base-url http://localhost:3000 --evidence --yes")} Include visual evidence`
8881
+ ` ${cyan3(withProject("decantr adopt --yes", projectArg))} Analyze, attach, and verify`
8882
+ );
8883
+ console.log(
8884
+ ` ${cyan3(withProject("decantr codify --from-audit", projectArg))} Propose local UI law`
8885
+ );
8886
+ console.log("");
8887
+ console.log(`${BOLD7}Optional visual evidence after the app is running:${RESET14}`);
8888
+ console.log(
8889
+ ` ${cyan3(withProject("decantr verify --base-url http://localhost:3000 --evidence", projectArg))}`
8397
8890
  );
8398
- console.log(` ${cyan3("decantr codify")} Propose local UI law`);
8399
8891
  return;
8400
8892
  }
8401
8893
  console.log(`${BOLD7}Recommended path:${RESET14} greenfield start`);
@@ -8404,9 +8896,10 @@ async function cmdSetupWorkflow(args) {
8404
8896
  }
8405
8897
  async function cmdAdoptWorkflow(args) {
8406
8898
  const { flags } = parseLooseArgs(args);
8407
- const workspaceInfo = resolveWorkflowProject(flags);
8899
+ const workspaceInfo = resolveWorkflowProject(flags, "adopt");
8408
8900
  if (!workspaceInfo) return;
8409
8901
  const projectRoot = workspaceInfo.appRoot;
8902
+ const projectArg = flagString(flags, "project");
8410
8903
  const dryRun = flagBoolean(flags, "dry-run");
8411
8904
  const yes = flagBoolean(flags, "yes") || flagBoolean(flags, "y");
8412
8905
  const baseUrl = flagString(flags, "base-url");
@@ -8476,10 +8969,22 @@ async function cmdAdoptWorkflow(args) {
8476
8969
  });
8477
8970
  }
8478
8971
  console.log("");
8479
- console.log(`${BOLD7}Next useful commands:${RESET14}`);
8480
- console.log(` ${cyan3('decantr task <route> "<change>"')} Give your LLM route-specific context`);
8481
- console.log(` ${cyan3("decantr codify")} Propose project-owned UI patterns`);
8482
- console.log(` ${cyan3("decantr verify --since-baseline")} Compare future work against this baseline`);
8972
+ console.log(`${BOLD7}Brownfield operating loop:${RESET14}`);
8973
+ console.log(
8974
+ ` ${cyan3(withProject("decantr codify --from-audit", projectArg))} Discover and propose project-owned UI law`
8975
+ );
8976
+ console.log(
8977
+ ` ${cyan3(withProject("decantr codify --accept", projectArg))} Accept reviewed local patterns and rules`
8978
+ );
8979
+ console.log(
8980
+ ` ${cyan3(withProject('decantr task <route> "<change>"', projectArg))} Give your LLM route-specific context before edits`
8981
+ );
8982
+ console.log(
8983
+ ` ${cyan3(withProject("decantr verify --brownfield --local-patterns", projectArg))} Check contract, health, and local law after edits`
8984
+ );
8985
+ console.log(
8986
+ ` ${cyan3(withProject("decantr verify --since-baseline", projectArg))} Compare future work against this baseline`
8987
+ );
8483
8988
  }
8484
8989
  async function cmdVerifyWorkflow(args) {
8485
8990
  const { flags } = parseLooseArgs(args);
@@ -8490,16 +8995,17 @@ async function cmdVerifyWorkflow(args) {
8490
8995
  return;
8491
8996
  }
8492
8997
  if (workspaceMode) {
8493
- const { cmdWorkspace } = await import("./workspace-KSFWRZEX.js");
8998
+ const { cmdWorkspace } = await import("./workspace-U7J3CJY3.js");
8494
8999
  await cmdWorkspace(process.cwd(), ["workspace", "health", ...withoutWorkflowOnlyFlags(args)]);
8495
9000
  return;
8496
9001
  }
8497
- const workspaceInfo = resolveWorkflowProject(flags);
9002
+ const workspaceInfo = resolveWorkflowProject(flags, "verify");
8498
9003
  if (!workspaceInfo) return;
8499
9004
  const brownfield = flagBoolean(flags, "brownfield");
8500
9005
  const localPatterns = flagBoolean(flags, "local-patterns");
8501
9006
  const evidence = flagBoolean(flags, "evidence");
8502
9007
  const baseUrl = flagString(flags, "base-url");
9008
+ const failOn = flagString(flags, "fail-on") ?? "error";
8503
9009
  const healthArgs = ["health", ...withoutWorkflowOnlyFlags(args)];
8504
9010
  if (flagBoolean(flags, "baseline") && !healthArgs.includes("--save-baseline")) {
8505
9011
  healthArgs.push("--save-baseline");
@@ -8535,16 +9041,51 @@ async function cmdVerifyWorkflow(args) {
8535
9041
  const { cmdHealth, parseHealthArgs } = await import("./health-ETZXWGTW.js");
8536
9042
  await cmdHealth(workspaceInfo.appRoot, parseHealthArgs(healthArgs));
8537
9043
  if (localPatterns) {
8538
- const localPatternsPath = join28(workspaceInfo.appRoot, ".decantr", "local-patterns.json");
8539
- if (!existsSync27(localPatternsPath)) {
8540
- console.log("");
8541
- console.log(
8542
- `${YELLOW9}Local pattern pack missing.${RESET14} Run ${cyan3("decantr codify --accept")} after reviewing the proposal.`
8543
- );
9044
+ const validation = validateLocalLaw(workspaceInfo.appRoot);
9045
+ if (!validation.patternPackPresent) {
9046
+ if (!quietOutput) {
9047
+ console.log("");
9048
+ console.log(
9049
+ `${YELLOW9}Local pattern pack missing.${RESET14} Run ${cyan3("decantr codify --from-audit")}, review the proposal, then run ${cyan3("decantr codify --accept")}.`
9050
+ );
9051
+ }
8544
9052
  process.exitCode = process.exitCode || 1;
8545
9053
  } else {
8546
- console.log("");
8547
- console.log(`${GREEN14}Local pattern pack found:${RESET14} ${localPatternsPath}`);
9054
+ const blockingFindings = failOn === "none" ? [] : validation.findings.filter(
9055
+ (finding) => failOn === "warn" ? finding.severity === "warn" || finding.severity === "error" : finding.severity === "error"
9056
+ );
9057
+ const blockingWarnings = failOn === "warn" ? validation.warnings : [];
9058
+ if (!quietOutput) {
9059
+ console.log("");
9060
+ console.log(`${GREEN14}Local pattern pack found:${RESET14} ${validation.patternsPath}`);
9061
+ if (validation.ruleManifestPresent) {
9062
+ console.log(`${GREEN14}Local rule manifest found:${RESET14} ${validation.rulesPath}`);
9063
+ } else {
9064
+ console.log(
9065
+ `${YELLOW9}Local rule manifest missing.${RESET14} Run ${cyan3("decantr codify --from-audit")} to propose .decantr/rules.json.`
9066
+ );
9067
+ }
9068
+ for (const warning of validation.warnings.slice(0, 8)) {
9069
+ console.log(`${YELLOW9}warn${RESET14} ${warning}`);
9070
+ }
9071
+ if (validation.findings.length > 0) {
9072
+ console.log("");
9073
+ console.log(`${BOLD7}Local law findings:${RESET14}`);
9074
+ for (const finding of validation.findings.slice(0, 20)) {
9075
+ console.log(
9076
+ ` ${finding.severity.toUpperCase()} ${finding.ruleId} ${finding.file}:${finding.line}:${finding.column} ${finding.message}`
9077
+ );
9078
+ }
9079
+ if (validation.findings.length > 20) {
9080
+ console.log(dim3(` ...${validation.findings.length - 20} more finding(s)`));
9081
+ }
9082
+ } else if (validation.ruleManifestPresent) {
9083
+ console.log(`${GREEN14}Local rule checks passed.${RESET14}`);
9084
+ }
9085
+ }
9086
+ if (blockingFindings.length > 0 || blockingWarnings.length > 0) {
9087
+ process.exitCode = process.exitCode || 1;
9088
+ }
8548
9089
  }
8549
9090
  }
8550
9091
  if (guardExitCode && guardExitCode !== 0 && (!process.exitCode || process.exitCode === 0)) {
@@ -8561,11 +9102,15 @@ function readJsonIfPresent(path) {
8561
9102
  }
8562
9103
  async function cmdTaskWorkflow(args) {
8563
9104
  const { flags, positional } = parseLooseArgs(args);
8564
- const workspaceInfo = resolveWorkflowProject(flags);
9105
+ const workspaceInfo = resolveWorkflowProject(flags, "task");
8565
9106
  if (!workspaceInfo) return;
8566
9107
  const routeInput = positional[0];
8567
9108
  if (!routeInput) {
8568
- console.error(error3('Usage: decantr task <route> ["task summary"] [--project <path>] [--json]'));
9109
+ console.error(
9110
+ error3(
9111
+ 'Usage: decantr task <route> ["task summary"] [--project <path>] [--since origin/main] [--json]'
9112
+ )
9113
+ );
8569
9114
  process.exitCode = 1;
8570
9115
  return;
8571
9116
  }
@@ -8574,11 +9119,13 @@ async function cmdTaskWorkflow(args) {
8574
9119
  const essencePath = join28(workspaceInfo.appRoot, "decantr.essence.json");
8575
9120
  const essence = readJsonIfPresent(essencePath);
8576
9121
  if (!essence) {
8577
- console.error(error3("No decantr.essence.json found. Run `decantr adopt` or `decantr init` first."));
9122
+ console.error(
9123
+ error3("No decantr.essence.json found. Run `decantr adopt` or `decantr init` first.")
9124
+ );
8578
9125
  process.exitCode = 1;
8579
9126
  return;
8580
9127
  }
8581
- if (!isV47(essence)) {
9128
+ if (!isV48(essence)) {
8582
9129
  console.error(error3("Task context requires Essence v4. Run `decantr migrate --to v4` first."));
8583
9130
  process.exitCode = 1;
8584
9131
  return;
@@ -8599,7 +9146,12 @@ async function cmdTaskWorkflow(args) {
8599
9146
  const sectionPack = manifest?.sections?.find((entry) => entry.id === target.section);
8600
9147
  const visualManifest = readJsonIfPresent(join28(workspaceInfo.appRoot, ".decantr", "evidence", "visual-manifest.json"));
8601
9148
  const screenshot = visualManifest?.routes?.find((entry) => entry.route === route)?.screenshot;
8602
- const localPatternsPath = join28(workspaceInfo.appRoot, ".decantr", "local-patterns.json");
9149
+ const localPatternPackPath = localPatternsPath(workspaceInfo.appRoot);
9150
+ const localRuleManifestPath = localRulesPath(workspaceInfo.appRoot);
9151
+ const localLaw = createLocalLawTaskSummary(workspaceInfo.appRoot);
9152
+ const changedSince = flagString(flags, "since");
9153
+ const currentChangedFiles = changedFiles(workspaceInfo.appRoot, changedSince);
9154
+ const changedRoutes = routeImpacts(workspaceInfo.appRoot, currentChangedFiles);
8603
9155
  const context = {
8604
9156
  route,
8605
9157
  task: taskSummary || null,
@@ -8613,9 +9165,14 @@ async function cmdTaskWorkflow(args) {
8613
9165
  manifest?.scaffold?.markdown ? join28(".decantr/context", manifest.scaffold.markdown) : null,
8614
9166
  ".decantr/context/scaffold.md",
8615
9167
  "DECANTR.md",
8616
- existsSync27(localPatternsPath) ? ".decantr/local-patterns.json" : null
9168
+ existsSync27(localPatternPackPath) ? ".decantr/local-patterns.json" : null,
9169
+ existsSync27(localRuleManifestPath) ? ".decantr/rules.json" : null
8617
9170
  ].filter(Boolean),
8618
- screenshot: screenshot ?? null
9171
+ screenshot: screenshot ?? null,
9172
+ localLaw,
9173
+ changedFiles: currentChangedFiles,
9174
+ changedRoutes,
9175
+ verifyCommand: "decantr verify --brownfield --local-patterns"
8619
9176
  };
8620
9177
  if (flagBoolean(flags, "json")) {
8621
9178
  console.log(JSON.stringify(context, null, 2));
@@ -8637,95 +9194,105 @@ async function cmdTaskWorkflow(args) {
8637
9194
  console.log(`${BOLD7}Visual evidence:${RESET14}`);
8638
9195
  console.log(` ${cyan3(context.screenshot)}`);
8639
9196
  }
9197
+ if (context.localLaw.patternCount > 0 || context.localLaw.ruleCount > 0) {
9198
+ console.log("");
9199
+ console.log(`${BOLD7}Project-owned local law:${RESET14}`);
9200
+ if (context.localLaw.patternsPath) {
9201
+ console.log(
9202
+ ` Patterns: ${cyan3(context.localLaw.patternsPath)} (${context.localLaw.patternCount})`
9203
+ );
9204
+ }
9205
+ if (context.localLaw.rulesPath) {
9206
+ console.log(` Rules: ${cyan3(context.localLaw.rulesPath)} (${context.localLaw.ruleCount})`);
9207
+ }
9208
+ for (const pattern of context.localLaw.patterns.slice(0, 4)) {
9209
+ const pathHint = pattern.componentPaths.length > 0 ? ` \u2014 ${pattern.componentPaths.slice(0, 2).join(", ")}` : "";
9210
+ console.log(` ${pattern.id}: ${pattern.role ?? "local pattern"}${pathHint}`);
9211
+ }
9212
+ } else {
9213
+ console.log("");
9214
+ console.log(`${BOLD7}Project-owned local law:${RESET14}`);
9215
+ console.log(
9216
+ ` ${YELLOW9}Not codified yet.${RESET14} Run ${cyan3("decantr codify --from-audit")} after adoption.`
9217
+ );
9218
+ }
9219
+ if (context.changedFiles.length > 0) {
9220
+ console.log("");
9221
+ console.log(`${BOLD7}Changed-file context:${RESET14}`);
9222
+ for (const file of context.changedFiles.slice(0, 8)) {
9223
+ console.log(` ${file}`);
9224
+ }
9225
+ if (context.changedFiles.length > 8) {
9226
+ console.log(dim3(` ...${context.changedFiles.length - 8} more changed file(s)`));
9227
+ }
9228
+ if (context.changedRoutes.length > 0) {
9229
+ console.log(` Impacted routes: ${context.changedRoutes.join(", ")}`);
9230
+ }
9231
+ }
8640
9232
  console.log("");
8641
9233
  console.log(`${BOLD7}LLM instruction:${RESET14}`);
8642
9234
  console.log(
8643
- " Preserve the existing runtime and styling system. Use the route pack, section context, local patterns, and visual evidence above as the task contract before changing code."
9235
+ " Preserve the existing runtime and styling system. Use the route pack, section context, local laws, changed-file impact, and visual evidence above as the task contract before changing code."
8644
9236
  );
9237
+ console.log(` After editing, run ${cyan3(context.verifyCommand)}.`);
8645
9238
  }
8646
9239
  async function cmdCodifyWorkflow(args) {
8647
9240
  const { flags } = parseLooseArgs(args);
8648
- const workspaceInfo = resolveWorkflowProject(flags);
9241
+ const workspaceInfo = resolveWorkflowProject(flags, "codify");
8649
9242
  if (!workspaceInfo) return;
8650
- const decantrDir = join28(workspaceInfo.appRoot, ".decantr");
8651
- const proposalPathLocal = join28(decantrDir, "local-patterns.proposal.json");
8652
- const acceptedPath = join28(decantrDir, "local-patterns.json");
8653
9243
  if (flagBoolean(flags, "accept")) {
8654
- if (!existsSync27(proposalPathLocal)) {
8655
- console.error(error3("No .decantr/local-patterns.proposal.json found. Run `decantr codify` first."));
9244
+ if (!existsSync27(localPatternsProposalPath(workspaceInfo.appRoot)) && !existsSync27(localRulesProposalPath(workspaceInfo.appRoot))) {
9245
+ console.error(
9246
+ error3(
9247
+ "No local law proposal found. Run `decantr codify --from-audit` or `decantr codify` first."
9248
+ )
9249
+ );
8656
9250
  process.exitCode = 1;
8657
9251
  return;
8658
9252
  }
8659
- writeFileSync16(acceptedPath, readFileSync20(proposalPathLocal, "utf-8"), "utf-8");
8660
- console.log(success3(`Accepted local pattern pack: ${acceptedPath}`));
8661
- console.log(dim3("Run `decantr verify --local-patterns` to require the pack during verification."));
9253
+ const result2 = acceptBrownfieldLocalLaw(workspaceInfo.appRoot);
9254
+ if (result2.patternAcceptedPath) {
9255
+ console.log(success3(`Accepted local pattern pack: ${result2.patternAcceptedPath}`));
9256
+ }
9257
+ if (result2.rulesAcceptedPath) {
9258
+ console.log(success3(`Accepted local rule manifest: ${result2.rulesAcceptedPath}`));
9259
+ }
9260
+ console.log(dim3("Run `decantr verify --brownfield --local-patterns` after project edits."));
8662
9261
  return;
8663
9262
  }
8664
- mkdirSync13(decantrDir, { recursive: true });
8665
9263
  const detected = detectProject(workspaceInfo.appRoot);
8666
- const essence = readJsonIfPresent(join28(workspaceInfo.appRoot, "decantr.essence.json"));
8667
- const routes = essence && isV47(essence) ? Object.keys(essence.blueprint.routes ?? {}).sort() : [];
8668
- const proposal = {
8669
- version: 1,
8670
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
8671
- status: "proposal",
8672
- source: "decantr codify",
8673
- project: {
8674
- framework: detected.framework,
8675
- packageManager: detected.packageManager,
8676
- hasTailwind: detected.hasTailwind,
8677
- ruleFiles: detected.existingRuleFiles,
8678
- routeCount: routes.length
8679
- },
8680
- purpose: "Project-owned Brownfield UI law. Review and edit before accepting; Decantr does not treat this as authoritative until copied to .decantr/local-patterns.json.",
8681
- patterns: [
8682
- {
8683
- id: "button",
8684
- role: "Actions and command triggers",
8685
- decide: "Define primary, secondary, tertiary, destructive, icon-only, and loading button variants from this app.",
8686
- evidenceToCollect: ["component wrapper path", "allowed classes/tokens", "forbidden raw <button> usage"]
8687
- },
8688
- {
8689
- id: "surface-card",
8690
- role: "Cards, panels, and content surfaces",
8691
- decide: "Define the canonical card background, border, radius, shadow, padding, and hover treatment.",
8692
- evidenceToCollect: ["shared card component", "token/class recipe", "allowed density variants"]
8693
- },
8694
- {
8695
- id: "page-shell",
8696
- role: "Route shell, nav, spacing, and scroll ownership",
8697
- decide: "Define which layout owns max width, gutters, sticky chrome, and scroll containers.",
8698
- evidenceToCollect: ["root layout path", "page template path", "responsive breakpoints"]
8699
- },
8700
- {
8701
- id: "form-control",
8702
- role: "Inputs, labels, validation, and form actions",
8703
- decide: "Define input height, label placement, error copy, disabled state, and focus treatment.",
8704
- evidenceToCollect: ["form field wrapper", "validation pattern", "accessibility expectations"]
8705
- }
8706
- ],
8707
- starterRules: [
8708
- "Prefer project-owned wrappers for repeated primitives once they exist.",
8709
- "Avoid raw hex/rgb values in component templates unless explicitly documented as dynamic data.",
8710
- "Avoid static inline styles for reusable visual treatment.",
8711
- "When adding a new route, map it to an existing local pattern before inventing a new visual variant."
8712
- ],
8713
- nextSteps: [
8714
- "Edit this proposal with real component paths and token/class recipes.",
8715
- "Run decantr codify --accept after review.",
8716
- "Use decantr task <route> before LLM edits so local patterns appear in the task context.",
8717
- "Wire mechanical enforcement through ESLint/Biome/project tests for rules Decantr cannot reliably infer."
8718
- ]
8719
- };
8720
- writeFileSync16(proposalPathLocal, JSON.stringify(proposal, null, 2) + "\n", "utf-8");
8721
- console.log(success3(`Wrote local pattern proposal: ${proposalPathLocal}`));
8722
- console.log(dim3("Review it, add real component paths/token recipes, then run `decantr codify --accept`."));
9264
+ const essence = readJsonIfPresent(
9265
+ join28(workspaceInfo.appRoot, "decantr.essence.json")
9266
+ );
9267
+ const fromAudit = flagBoolean(flags, "from-audit") || flagBoolean(flags, "discover-local-patterns") || flagBoolean(flags, "codify-local-patterns");
9268
+ const proposal = createBrownfieldCodifyProposal({
9269
+ projectRoot: workspaceInfo.appRoot,
9270
+ detected,
9271
+ essence,
9272
+ fromAudit
9273
+ });
9274
+ const result = writeBrownfieldCodifyProposal(workspaceInfo.appRoot, proposal);
9275
+ console.log(success3(`Wrote local pattern proposal: ${result.patternPath}`));
9276
+ console.log(success3(`Wrote local rule proposal: ${result.rulesPath}`));
9277
+ if (fromAudit) {
9278
+ console.log(
9279
+ dim3("Proposal includes source-derived component candidates and starter mechanical rules.")
9280
+ );
9281
+ }
9282
+ console.log(
9283
+ dim3(
9284
+ "Review both files, add real component paths/token recipes, then run `decantr codify --accept`."
9285
+ )
9286
+ );
8723
9287
  }
8724
9288
  async function cmdContentWorkflow(args) {
8725
9289
  const subcommand = args[1] ?? "check";
8726
9290
  if (subcommand === "check" || subcommand === "health") {
8727
9291
  const { cmdContentHealth, parseContentHealthArgs } = await import("./content-health-QQHBR6XG.js");
8728
- await cmdContentHealth(process.cwd(), parseContentHealthArgs(["content-health", ...args.slice(2)]));
9292
+ await cmdContentHealth(
9293
+ process.cwd(),
9294
+ parseContentHealthArgs(["content-health", ...args.slice(2)])
9295
+ );
8729
9296
  return;
8730
9297
  }
8731
9298
  if (subcommand === "create") {
@@ -8761,9 +9328,9 @@ ${BOLD7}Usage:${RESET14}
8761
9328
  decantr setup [--project <path>]
8762
9329
  decantr new <name> [--blueprint=X] [--archetype=X] [--theme=X] [--workflow=greenfield] [--adoption=decantr-css] [--telemetry]
8763
9330
  decantr adopt [--project <path>] [--base-url <url>] [--evidence] [--ci] [--yes]
8764
- decantr task <route> ["task summary"] [--project <path>] [--json]
9331
+ decantr task <route> ["task summary"] [--project <path>] [--since origin/main] [--json]
8765
9332
  decantr verify [--project <path>] [--brownfield] [--local-patterns] [health options]
8766
- decantr codify [--accept] [--project <path>]
9333
+ decantr codify [--from-audit] [--accept] [--project <path>]
8767
9334
  decantr studio [--port 4319] [--host 127.0.0.1] [--report decantr-health.json] [--workspace]
8768
9335
 
8769
9336
  ${BOLD7}Advanced primitives:${RESET14}
@@ -8834,9 +9401,9 @@ ${BOLD7}Commands:${RESET14}
8834
9401
  ${cyan3("setup")} Detect project state and recommend the right Decantr workflow
8835
9402
  ${cyan3("new")} Create a new greenfield workspace and bootstrap the available starter adapter
8836
9403
  ${cyan3("adopt")} Brownfield one-liner: analyze, attach, verify, and show next steps
8837
- ${cyan3("task")} Prepare route/task context for an AI coding assistant
9404
+ ${cyan3("task")} Prepare route/task context, local law, evidence, and changed-file impact for an AI coding assistant
8838
9405
  ${cyan3("verify")} One reliability gate over Project Health, Brownfield checks, baselines, and evidence
8839
- ${cyan3("codify")} Propose or accept project-owned Brownfield UI patterns
9406
+ ${cyan3("codify")} Propose or accept project-owned Brownfield UI patterns and rules
8840
9407
  ${cyan3("studio")} Open a local Project Health dashboard backed by the same report
8841
9408
  ${cyan3("content")} Content-author namespace: check, create, publish
8842
9409
 
@@ -8874,11 +9441,13 @@ ${BOLD7}Advanced commands:${RESET14}
8874
9441
  ${BOLD7}Examples:${RESET14}
8875
9442
  decantr setup
8876
9443
  decantr new my-app --blueprint=carbon-ai-portal
8877
- decantr adopt --base-url http://localhost:3000 --evidence --yes
9444
+ decantr adopt --yes
9445
+ decantr adopt --project apps/web --yes
8878
9446
  decantr task /feed "add saved recipe actions"
8879
9447
  decantr verify --brownfield --local-patterns
9448
+ decantr verify --base-url http://localhost:3000 --evidence
8880
9449
  decantr verify --since-baseline
8881
- decantr codify
9450
+ decantr codify --from-audit
8882
9451
  decantr codify --accept
8883
9452
  decantr content check --ci --fail-on error
8884
9453
  decantr magic "AI chatbot with dark cyber theme \u2014 bold and futuristic"
@@ -8928,9 +9497,10 @@ ${BOLD7}Examples:${RESET14}
8928
9497
  ${BOLD7}Workflow Model:${RESET14}
8929
9498
  ${cyan3("Greenfield blueprint")} decantr new my-app --blueprint=X --workflow=greenfield --adoption=decantr-css
8930
9499
  ${cyan3("Greenfield contract")} decantr init --workflow=greenfield --adoption=contract-only
8931
- ${cyan3("Brownfield adoption")} decantr adopt --base-url <url> --evidence --yes
8932
- ${cyan3("Daily LLM work")} decantr task <route> "<change>" -> decantr verify
8933
- ${cyan3("Project-owned law")} decantr codify -> edit proposal -> decantr codify --accept
9500
+ ${cyan3("Brownfield adoption")} decantr adopt --yes
9501
+ ${cyan3("Brownfield monorepo")} decantr adopt --project apps/web --yes
9502
+ ${cyan3("Daily LLM work")} decantr task <route> "<change>" -> decantr verify --brownfield --local-patterns
9503
+ ${cyan3("Project-owned law")} decantr codify --from-audit -> edit proposal -> decantr codify --accept
8934
9504
  ${cyan3("Hybrid composition")} decantr add/remove, decantr theme switch, decantr registry, decantr upgrade
8935
9505
 
8936
9506
  ${BOLD7}Bootstrap adapters:${RESET14}
@@ -9003,7 +9573,7 @@ ${BOLD7}Examples:${RESET14}
9003
9573
  }
9004
9574
  function cmdWorkspaceHelp() {
9005
9575
  console.log(`
9006
- ${BOLD7}decantr workspace${RESET14} \u2014 Inspect Decantr projects across a monorepo
9576
+ ${BOLD7}decantr workspace${RESET14} \u2014 Inspect Decantr projects and app candidates across a monorepo
9007
9577
 
9008
9578
  ${BOLD7}Usage:${RESET14}
9009
9579
  decantr workspace list [--json]
@@ -9012,6 +9582,7 @@ ${BOLD7}Usage:${RESET14}
9012
9582
 
9013
9583
  ${BOLD7}Examples:${RESET14}
9014
9584
  decantr workspace list
9585
+ decantr adopt --project apps/web --yes
9015
9586
  decantr workspace health
9016
9587
  decantr workspace health --json --output .decantr/workspace-health.json
9017
9588
  decantr workspace health --changed --since origin/main
@@ -9111,11 +9682,11 @@ ${BOLD7}Examples:${RESET14}
9111
9682
  }
9112
9683
  function cmdAdoptHelp() {
9113
9684
  console.log(`
9114
- ${BOLD7}decantr adopt${RESET14} \u2014 Brownfield one-liner: analyze, attach, verify, and show the next step
9685
+ ${BOLD7}decantr adopt${RESET14} \u2014 Brownfield one-liner: analyze, attach, verify, and show the operating loop
9115
9686
 
9116
9687
  ${BOLD7}Usage:${RESET14}
9117
9688
  decantr adopt [--project <path>] [--yes] [--dry-run]
9118
- decantr adopt --base-url <url> [--evidence] [--ci] [--yes]
9689
+ decantr adopt [--project <path>] --base-url <url> [--evidence] [--ci] [--yes]
9119
9690
 
9120
9691
  ${BOLD7}Options:${RESET14}
9121
9692
  --project App path inside a workspace/monorepo
@@ -9133,8 +9704,10 @@ ${BOLD7}Options:${RESET14}
9133
9704
 
9134
9705
  ${BOLD7}Examples:${RESET14}
9135
9706
  decantr adopt --yes
9136
- decantr adopt --base-url http://localhost:3000 --evidence --yes
9707
+ decantr adopt --project apps/web --yes
9708
+ decantr adopt --project apps/web --base-url http://localhost:3000 --evidence --yes
9137
9709
  decantr adopt --project apps/web --ci --yes
9710
+ decantr codify --from-audit --project apps/web
9138
9711
  `);
9139
9712
  }
9140
9713
  function cmdVerifyHelp() {
@@ -9151,6 +9724,8 @@ ${BOLD7}Usage:${RESET14}
9151
9724
  ${BOLD7}Examples:${RESET14}
9152
9725
  decantr verify
9153
9726
  decantr verify --brownfield --local-patterns
9727
+ decantr verify --brownfield --local-patterns --project apps/web
9728
+ decantr verify --brownfield --local-patterns --fail-on warn
9154
9729
  decantr verify --base-url http://localhost:3000 --evidence
9155
9730
  decantr verify --workspace --changed --since origin/main
9156
9731
  decantr verify init-ci --project apps/web
@@ -9161,25 +9736,27 @@ function cmdTaskHelp() {
9161
9736
  ${BOLD7}decantr task${RESET14} \u2014 Prepare compact route/task context for an AI coding assistant
9162
9737
 
9163
9738
  ${BOLD7}Usage:${RESET14}
9164
- decantr task <route> ["task summary"] [--project <path>] [--json]
9739
+ decantr task <route> ["task summary"] [--project <path>] [--since origin/main] [--json]
9165
9740
 
9166
9741
  ${BOLD7}Examples:${RESET14}
9167
9742
  decantr task /feed "add saved recipe actions"
9743
+ decantr task /feed "add saved recipe actions" --since origin/main
9168
9744
  decantr task /profile --json
9169
9745
  `);
9170
9746
  }
9171
9747
  function cmdCodifyHelp() {
9172
9748
  console.log(`
9173
- ${BOLD7}decantr codify${RESET14} \u2014 Propose or accept project-owned Brownfield UI patterns
9749
+ ${BOLD7}decantr codify${RESET14} \u2014 Propose or accept project-owned Brownfield UI patterns and rules
9174
9750
 
9175
9751
  ${BOLD7}Usage:${RESET14}
9176
- decantr codify [--project <path>]
9752
+ decantr codify [--from-audit] [--project <path>]
9177
9753
  decantr codify --accept [--project <path>]
9178
9754
 
9179
9755
  ${BOLD7}Examples:${RESET14}
9180
9756
  decantr codify
9757
+ decantr codify --from-audit
9181
9758
  decantr codify --accept
9182
- decantr verify --local-patterns
9759
+ decantr verify --brownfield --local-patterns
9183
9760
  `);
9184
9761
  }
9185
9762
  function cmdContentHelp() {
@@ -9253,7 +9830,7 @@ async function main() {
9253
9830
  }
9254
9831
  if (command === "--version" || command === "-v" || command === "version") {
9255
9832
  try {
9256
- const here = dirname4(fileURLToPath2(import.meta.url));
9833
+ const here = dirname3(fileURLToPath2(import.meta.url));
9257
9834
  const candidates = [join28(here, "..", "package.json"), join28(here, "..", "..", "package.json")];
9258
9835
  for (const candidate of candidates) {
9259
9836
  if (existsSync27(candidate)) {
@@ -9436,7 +10013,7 @@ async function main() {
9436
10013
  cmdStudioHelp();
9437
10014
  break;
9438
10015
  }
9439
- const { cmdStudio, parseStudioArgs } = await import("./studio-MKLBUC3A.js");
10016
+ const { cmdStudio, parseStudioArgs } = await import("./studio-G3YOU5YF.js");
9440
10017
  await cmdStudio(process.cwd(), parseStudioArgs(args));
9441
10018
  } catch (e) {
9442
10019
  console.error(error3(e.message));
@@ -9450,7 +10027,7 @@ async function main() {
9450
10027
  cmdWorkspaceHelp();
9451
10028
  break;
9452
10029
  }
9453
- const { cmdWorkspace } = await import("./workspace-KSFWRZEX.js");
10030
+ const { cmdWorkspace } = await import("./workspace-U7J3CJY3.js");
9454
10031
  await cmdWorkspace(process.cwd(), args);
9455
10032
  } catch (e) {
9456
10033
  console.error(error3(e.message));
@@ -9886,10 +10463,7 @@ async function main() {
9886
10463
  }
9887
10464
  const workspaceInfo = resolveWorkspaceInfo(process.cwd(), projectArg);
9888
10465
  if (workspaceInfo.requiresProjectSelection) {
9889
- console.log(error3("This looks like a workspace root with multiple app candidates."));
9890
- console.log(
9891
- dim3(`Use --project=<path>. Candidates: ${workspaceInfo.appCandidates.join(", ")}`)
9892
- );
10466
+ printWorkspaceProjectSelection(workspaceInfo, "analyze");
9893
10467
  process.exitCode = 1;
9894
10468
  break;
9895
10469
  }
@@ -9916,6 +10490,11 @@ async function main() {
9916
10490
  }
9917
10491
  }
9918
10492
  const workspaceInfo = resolveWorkspaceInfo(process.cwd(), projectArg);
10493
+ if (workspaceInfo.requiresProjectSelection) {
10494
+ printWorkspaceProjectSelection(workspaceInfo, "rules");
10495
+ process.exitCode = 1;
10496
+ break;
10497
+ }
9919
10498
  const detected = detectProject(workspaceInfo.appRoot);
9920
10499
  if (subcommand === "preview") {
9921
10500
  console.log(