@decantr/cli 2.7.0 → 2.8.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 +9 -7
- package/dist/bin.js +1 -1
- package/dist/{chunk-ZYHR3BGT.js → chunk-QTPNV5WU.js} +798 -271
- package/dist/index.js +1 -1
- package/package.json +4 -4
|
@@ -34,10 +34,10 @@ import {
|
|
|
34
34
|
} from "./chunk-KT2ROK2D.js";
|
|
35
35
|
|
|
36
36
|
// src/index.ts
|
|
37
|
-
import { existsSync as
|
|
38
|
-
import { basename as
|
|
37
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync14, readdirSync as readdirSync8, readFileSync as readFileSync21, writeFileSync as writeFileSync17 } from "fs";
|
|
38
|
+
import { basename as basename3, dirname as dirname4, isAbsolute, join as join29, resolve as resolve4 } from "path";
|
|
39
39
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
40
|
-
import { evaluateGuard, isV4 as
|
|
40
|
+
import { evaluateGuard, isV4 as isV48, validateEssence as validateEssence2 } from "@decantr/essence-spec";
|
|
41
41
|
import {
|
|
42
42
|
CONTENT_TYPE_TO_API_CONTENT_TYPE as CONTENT_TYPE_TO_API_CONTENT_TYPE3,
|
|
43
43
|
CONTENT_TYPES as GET_CONTENT_TYPES,
|
|
@@ -5271,6 +5271,488 @@ async function cmdThemeSwitch(themeName, args, projectRoot = process.cwd()) {
|
|
|
5271
5271
|
console.log(`${YELLOW7}Guard will flag code using old tokens. Run \`decantr check\`.${RESET12}`);
|
|
5272
5272
|
}
|
|
5273
5273
|
|
|
5274
|
+
// src/local-law.ts
|
|
5275
|
+
import { execFileSync } from "child_process";
|
|
5276
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync12, readdirSync as readdirSync5, readFileSync as readFileSync18, statSync as statSync5, writeFileSync as writeFileSync15 } from "fs";
|
|
5277
|
+
import { basename as basename2, extname, join as join26, relative as relative2, sep } from "path";
|
|
5278
|
+
import { isV4 as isV47 } from "@decantr/essence-spec";
|
|
5279
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
5280
|
+
".astro",
|
|
5281
|
+
".html",
|
|
5282
|
+
".js",
|
|
5283
|
+
".jsx",
|
|
5284
|
+
".svelte",
|
|
5285
|
+
".ts",
|
|
5286
|
+
".tsx",
|
|
5287
|
+
".vue"
|
|
5288
|
+
]);
|
|
5289
|
+
var UI_TEMPLATE_EXTENSIONS = /* @__PURE__ */ new Set([".astro", ".html", ".jsx", ".svelte", ".tsx", ".vue"]);
|
|
5290
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
5291
|
+
".decantr",
|
|
5292
|
+
".git",
|
|
5293
|
+
".next",
|
|
5294
|
+
".nuxt",
|
|
5295
|
+
".svelte-kit",
|
|
5296
|
+
"build",
|
|
5297
|
+
"coverage",
|
|
5298
|
+
"dist",
|
|
5299
|
+
"node_modules",
|
|
5300
|
+
"out"
|
|
5301
|
+
]);
|
|
5302
|
+
var DEFAULT_RULE_EXTENSIONS = [".astro", ".html", ".jsx", ".svelte", ".tsx", ".vue"];
|
|
5303
|
+
function localPatternsProposalPath(projectRoot) {
|
|
5304
|
+
return join26(projectRoot, ".decantr", "local-patterns.proposal.json");
|
|
5305
|
+
}
|
|
5306
|
+
function localPatternsPath(projectRoot) {
|
|
5307
|
+
return join26(projectRoot, ".decantr", "local-patterns.json");
|
|
5308
|
+
}
|
|
5309
|
+
function localRulesProposalPath(projectRoot) {
|
|
5310
|
+
return join26(projectRoot, ".decantr", "rules.proposal.json");
|
|
5311
|
+
}
|
|
5312
|
+
function localRulesPath(projectRoot) {
|
|
5313
|
+
return join26(projectRoot, ".decantr", "rules.json");
|
|
5314
|
+
}
|
|
5315
|
+
function readLocalPatternPack(projectRoot) {
|
|
5316
|
+
return readJsonFile(localPatternsPath(projectRoot));
|
|
5317
|
+
}
|
|
5318
|
+
function readLocalRuleManifest(projectRoot) {
|
|
5319
|
+
return readJsonFile(localRulesPath(projectRoot));
|
|
5320
|
+
}
|
|
5321
|
+
function createBrownfieldCodifyProposal(input) {
|
|
5322
|
+
const sourceFiles = input.fromAudit ? listSourceFiles(input.projectRoot, 800) : [];
|
|
5323
|
+
const evidence = summarizeSourceEvidence(input.projectRoot, sourceFiles);
|
|
5324
|
+
const routes = input.essence && isV47(input.essence) ? Object.keys(input.essence.blueprint.routes ?? {}).sort() : [];
|
|
5325
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5326
|
+
const patternPack = {
|
|
5327
|
+
version: 2,
|
|
5328
|
+
generatedAt,
|
|
5329
|
+
status: "proposal",
|
|
5330
|
+
source: input.fromAudit ? "decantr codify --from-audit" : "decantr codify",
|
|
5331
|
+
project: {
|
|
5332
|
+
framework: input.detected.framework,
|
|
5333
|
+
packageManager: input.detected.packageManager,
|
|
5334
|
+
hasTailwind: input.detected.hasTailwind,
|
|
5335
|
+
ruleFiles: input.detected.existingRuleFiles,
|
|
5336
|
+
routeCount: routes.length
|
|
5337
|
+
},
|
|
5338
|
+
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.",
|
|
5339
|
+
patterns: [
|
|
5340
|
+
{
|
|
5341
|
+
id: "button",
|
|
5342
|
+
label: "Button primitives",
|
|
5343
|
+
role: "Actions and command triggers",
|
|
5344
|
+
appliesTo: [
|
|
5345
|
+
"primary action",
|
|
5346
|
+
"secondary action",
|
|
5347
|
+
"tertiary action",
|
|
5348
|
+
"destructive action",
|
|
5349
|
+
"icon action"
|
|
5350
|
+
],
|
|
5351
|
+
componentPaths: evidence.buttonComponents,
|
|
5352
|
+
decide: "Define primary, secondary, tertiary, destructive, icon-only, disabled, and loading button variants from this app.",
|
|
5353
|
+
evidence: evidence.buttonComponents.length ? evidence.buttonComponents : [
|
|
5354
|
+
"No obvious Button wrapper found yet. Add the project-owned wrapper path before strict enforcement."
|
|
5355
|
+
],
|
|
5356
|
+
forbiddenAlternatives: ["New one-off button variants without updating this manifest."]
|
|
5357
|
+
},
|
|
5358
|
+
{
|
|
5359
|
+
id: "surface-card",
|
|
5360
|
+
label: "Cards and surfaces",
|
|
5361
|
+
role: "Cards, panels, and reusable content surfaces",
|
|
5362
|
+
appliesTo: ["cards", "panels", "modals", "list items", "dashboard tiles"],
|
|
5363
|
+
componentPaths: evidence.cardComponents,
|
|
5364
|
+
decide: "Define the canonical card background, border, radius, shadow, padding, density, and hover treatment.",
|
|
5365
|
+
classHints: evidence.cardClassHints,
|
|
5366
|
+
evidence: evidence.cardComponents.length ? evidence.cardComponents : [
|
|
5367
|
+
"No obvious Card wrapper found yet. Add the project-owned wrapper path or class recipe."
|
|
5368
|
+
],
|
|
5369
|
+
forbiddenAlternatives: ["Flat ad hoc cards with unique color/radius/shadow recipes."]
|
|
5370
|
+
},
|
|
5371
|
+
{
|
|
5372
|
+
id: "page-shell",
|
|
5373
|
+
label: "Page shell and spacing",
|
|
5374
|
+
role: "Route shell, navigation, gutters, max-width, and scroll ownership",
|
|
5375
|
+
appliesTo: ["routes", "layouts", "navigation shells", "scroll containers"],
|
|
5376
|
+
componentPaths: evidence.shellComponents,
|
|
5377
|
+
decide: "Define which layout owns max width, gutters, sticky chrome, responsive breakpoints, and scroll containers.",
|
|
5378
|
+
evidence: evidence.shellComponents.length ? evidence.shellComponents : ["Add root layout, shell, or app frame files that establish route chrome and spacing."],
|
|
5379
|
+
forbiddenAlternatives: [
|
|
5380
|
+
"Each page inventing independent max-width, padding, or sticky nav rules."
|
|
5381
|
+
]
|
|
5382
|
+
},
|
|
5383
|
+
{
|
|
5384
|
+
id: "form-control",
|
|
5385
|
+
label: "Form controls",
|
|
5386
|
+
role: "Inputs, labels, validation, and form actions",
|
|
5387
|
+
appliesTo: ["inputs", "selects", "textareas", "validation messages", "form actions"],
|
|
5388
|
+
componentPaths: evidence.formComponents,
|
|
5389
|
+
decide: "Define input height, label placement, error copy, disabled state, required state, and focus treatment.",
|
|
5390
|
+
evidence: evidence.formComponents.length ? evidence.formComponents : ["Add form field wrapper paths and validation examples."],
|
|
5391
|
+
forbiddenAlternatives: [
|
|
5392
|
+
"Unlabeled one-off inputs or validation states that do not match the app standard."
|
|
5393
|
+
]
|
|
5394
|
+
},
|
|
5395
|
+
{
|
|
5396
|
+
id: "theme-variant",
|
|
5397
|
+
label: "Theme variants",
|
|
5398
|
+
role: "Light, dark, brand, density, and tenant/theme variants observed in the app",
|
|
5399
|
+
appliesTo: ["theme toggles", "mode-specific classes", "brand variants", "tenant variants"],
|
|
5400
|
+
componentPaths: evidence.themeComponents,
|
|
5401
|
+
decide: "Document which theme variants exist, where they are toggled, and which tokens/classes are legal per variant.",
|
|
5402
|
+
evidence: evidence.themeComponents.length ? evidence.themeComponents : ["If the app has dark/light or brand variants, add the toggles/providers here."],
|
|
5403
|
+
forbiddenAlternatives: [
|
|
5404
|
+
"Component-local theme forks that bypass shared theme providers or tokens."
|
|
5405
|
+
]
|
|
5406
|
+
}
|
|
5407
|
+
],
|
|
5408
|
+
starterRules: [
|
|
5409
|
+
"Prefer project-owned wrappers for repeated primitives once they exist.",
|
|
5410
|
+
"Avoid raw hex/rgb values in component templates unless documented as dynamic data.",
|
|
5411
|
+
"Avoid static inline styles for reusable visual treatment.",
|
|
5412
|
+
"When adding a new route, map it to an existing local pattern before inventing a new visual variant.",
|
|
5413
|
+
"When adding a theme variant, update .decantr/theme-inventory.json and this local pattern pack."
|
|
5414
|
+
],
|
|
5415
|
+
nextSteps: [
|
|
5416
|
+
"Edit this proposal with real component paths and token/class recipes.",
|
|
5417
|
+
"Run decantr codify --accept after review.",
|
|
5418
|
+
"Use decantr task <route> before LLM edits so local law appears in task context.",
|
|
5419
|
+
"Run decantr verify --brownfield --local-patterns after edits.",
|
|
5420
|
+
"Wire deterministic project rules into ESLint, Biome, Storybook, visual tests, or CI where Decantr should not guess."
|
|
5421
|
+
]
|
|
5422
|
+
};
|
|
5423
|
+
const ruleManifest = {
|
|
5424
|
+
version: 1,
|
|
5425
|
+
status: "proposal",
|
|
5426
|
+
generatedAt,
|
|
5427
|
+
source: input.fromAudit ? "decantr codify --from-audit" : "decantr codify",
|
|
5428
|
+
purpose: "Mechanical Brownfield checks owned by this project. These rules are intentionally local and stack-agnostic; edit before accepting.",
|
|
5429
|
+
enforcement: {
|
|
5430
|
+
defaultSeverity: "warn",
|
|
5431
|
+
mode: "warn",
|
|
5432
|
+
notes: [
|
|
5433
|
+
"Decantr local rules are a guardrail, not a replacement for ESLint, Biome, type checks, tests, or visual regression.",
|
|
5434
|
+
"Keep rules narrow enough that an LLM can fix findings without rewriting the app.",
|
|
5435
|
+
"Use error severity only after the team agrees the rule is stable."
|
|
5436
|
+
]
|
|
5437
|
+
},
|
|
5438
|
+
rules: [
|
|
5439
|
+
{
|
|
5440
|
+
id: "no-inline-style",
|
|
5441
|
+
type: "forbid-regex",
|
|
5442
|
+
enabled: true,
|
|
5443
|
+
severity: "warn",
|
|
5444
|
+
description: "Reusable UI should not add static inline style attributes.",
|
|
5445
|
+
includeExtensions: DEFAULT_RULE_EXTENSIONS,
|
|
5446
|
+
pattern: "\\bstyle\\s*=",
|
|
5447
|
+
message: "Inline style found in UI template.",
|
|
5448
|
+
suggestedFix: "Move reusable visual treatment into the project style system, component wrapper, token, or documented local pattern.",
|
|
5449
|
+
maxFindings: 25
|
|
5450
|
+
},
|
|
5451
|
+
{
|
|
5452
|
+
id: "no-raw-color-literals",
|
|
5453
|
+
type: "forbid-regex",
|
|
5454
|
+
enabled: true,
|
|
5455
|
+
severity: "warn",
|
|
5456
|
+
description: "Component templates should not introduce raw hex/rgb color literals.",
|
|
5457
|
+
includeExtensions: DEFAULT_RULE_EXTENSIONS,
|
|
5458
|
+
pattern: "#(?:[0-9a-fA-F]{3,8})\\b|rgba?\\s*\\(",
|
|
5459
|
+
message: "Raw color literal found in UI template.",
|
|
5460
|
+
suggestedFix: "Use an existing project token/class, or document the exception in .decantr/local-patterns.json if the value is data-driven.",
|
|
5461
|
+
maxFindings: 25
|
|
5462
|
+
},
|
|
5463
|
+
{
|
|
5464
|
+
id: "prefer-button-wrapper",
|
|
5465
|
+
type: "forbid-regex",
|
|
5466
|
+
enabled: evidence.buttonComponents.length > 0,
|
|
5467
|
+
severity: "info",
|
|
5468
|
+
description: "Prefer the project-owned button primitive instead of new raw button markup.",
|
|
5469
|
+
includeExtensions: DEFAULT_RULE_EXTENSIONS,
|
|
5470
|
+
pattern: "<button[\\s>]",
|
|
5471
|
+
message: "Raw <button> usage found outside the detected button wrapper.",
|
|
5472
|
+
suggestedFix: "Use the project-owned Button primitive, or add this file to allowedPaths if it is the primitive implementation.",
|
|
5473
|
+
allowedPaths: evidence.buttonComponents,
|
|
5474
|
+
maxFindings: 50
|
|
5475
|
+
}
|
|
5476
|
+
]
|
|
5477
|
+
};
|
|
5478
|
+
return { patternPack, ruleManifest };
|
|
5479
|
+
}
|
|
5480
|
+
function writeBrownfieldCodifyProposal(projectRoot, proposal) {
|
|
5481
|
+
const decantrDir = join26(projectRoot, ".decantr");
|
|
5482
|
+
mkdirSync12(decantrDir, { recursive: true });
|
|
5483
|
+
const patternPath = localPatternsProposalPath(projectRoot);
|
|
5484
|
+
const rulesPath = localRulesProposalPath(projectRoot);
|
|
5485
|
+
writeFileSync15(patternPath, `${JSON.stringify(proposal.patternPack, null, 2)}
|
|
5486
|
+
`, "utf-8");
|
|
5487
|
+
writeFileSync15(rulesPath, `${JSON.stringify(proposal.ruleManifest, null, 2)}
|
|
5488
|
+
`, "utf-8");
|
|
5489
|
+
return { patternPath, rulesPath };
|
|
5490
|
+
}
|
|
5491
|
+
function acceptBrownfieldLocalLaw(projectRoot) {
|
|
5492
|
+
const patternProposal = readJsonFile(localPatternsProposalPath(projectRoot));
|
|
5493
|
+
const ruleProposal = readJsonFile(localRulesProposalPath(projectRoot));
|
|
5494
|
+
const acceptedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5495
|
+
let patternAcceptedPath = null;
|
|
5496
|
+
let rulesAcceptedPath = null;
|
|
5497
|
+
if (patternProposal) {
|
|
5498
|
+
patternProposal.status = "accepted";
|
|
5499
|
+
patternProposal.acceptedAt = acceptedAt;
|
|
5500
|
+
patternAcceptedPath = localPatternsPath(projectRoot);
|
|
5501
|
+
writeFileSync15(patternAcceptedPath, `${JSON.stringify(patternProposal, null, 2)}
|
|
5502
|
+
`, "utf-8");
|
|
5503
|
+
}
|
|
5504
|
+
if (ruleProposal) {
|
|
5505
|
+
ruleProposal.status = "accepted";
|
|
5506
|
+
ruleProposal.acceptedAt = acceptedAt;
|
|
5507
|
+
rulesAcceptedPath = localRulesPath(projectRoot);
|
|
5508
|
+
writeFileSync15(rulesAcceptedPath, `${JSON.stringify(ruleProposal, null, 2)}
|
|
5509
|
+
`, "utf-8");
|
|
5510
|
+
}
|
|
5511
|
+
return { patternAcceptedPath, rulesAcceptedPath };
|
|
5512
|
+
}
|
|
5513
|
+
function validateLocalLaw(projectRoot) {
|
|
5514
|
+
const patternsPath = localPatternsPath(projectRoot);
|
|
5515
|
+
const rulesPath = localRulesPath(projectRoot);
|
|
5516
|
+
const patternPack = readJsonFile(patternsPath);
|
|
5517
|
+
const ruleManifest = readJsonFile(rulesPath);
|
|
5518
|
+
const warnings = [];
|
|
5519
|
+
if (patternPack) {
|
|
5520
|
+
const patternIds = /* @__PURE__ */ new Set();
|
|
5521
|
+
const patterns = Array.isArray(patternPack.patterns) ? patternPack.patterns : [];
|
|
5522
|
+
if (patterns.length === 0) {
|
|
5523
|
+
warnings.push(".decantr/local-patterns.json has no patterns.");
|
|
5524
|
+
}
|
|
5525
|
+
for (const pattern of patterns) {
|
|
5526
|
+
const id = typeof pattern.id === "string" ? pattern.id.trim() : "";
|
|
5527
|
+
if (!id) warnings.push("A local pattern is missing an id.");
|
|
5528
|
+
if (id && patternIds.has(id)) warnings.push(`Duplicate local pattern id: ${id}`);
|
|
5529
|
+
if (id) patternIds.add(id);
|
|
5530
|
+
const paths = Array.isArray(pattern.componentPaths) ? pattern.componentPaths : [];
|
|
5531
|
+
const evidence = Array.isArray(pattern.evidence) ? pattern.evidence : [];
|
|
5532
|
+
const todoEvidence = Array.isArray(pattern.evidenceToCollect) ? pattern.evidenceToCollect : [];
|
|
5533
|
+
if (id && paths.length === 0 && evidence.length === 0 && todoEvidence.length > 0) {
|
|
5534
|
+
warnings.push(
|
|
5535
|
+
`Local pattern ${id} still reads like a TODO; add concrete component paths or evidence.`
|
|
5536
|
+
);
|
|
5537
|
+
}
|
|
5538
|
+
}
|
|
5539
|
+
}
|
|
5540
|
+
if (ruleManifest && !Array.isArray(ruleManifest.rules)) {
|
|
5541
|
+
warnings.push(".decantr/rules.json has no rules array.");
|
|
5542
|
+
}
|
|
5543
|
+
const findings = ruleManifest ? scanLocalRules(projectRoot, ruleManifest) : [];
|
|
5544
|
+
return {
|
|
5545
|
+
patternsPath,
|
|
5546
|
+
rulesPath,
|
|
5547
|
+
patternPackPresent: Boolean(patternPack),
|
|
5548
|
+
ruleManifestPresent: Boolean(ruleManifest),
|
|
5549
|
+
warnings,
|
|
5550
|
+
findings
|
|
5551
|
+
};
|
|
5552
|
+
}
|
|
5553
|
+
function createLocalLawTaskSummary(projectRoot) {
|
|
5554
|
+
const patternPack = readLocalPatternPack(projectRoot);
|
|
5555
|
+
const ruleManifest = readLocalRuleManifest(projectRoot);
|
|
5556
|
+
const patterns = (patternPack?.patterns ?? []).map((pattern) => ({
|
|
5557
|
+
id: typeof pattern.id === "string" ? pattern.id : "unknown",
|
|
5558
|
+
role: typeof pattern.role === "string" ? pattern.role : null,
|
|
5559
|
+
componentPaths: Array.isArray(pattern.componentPaths) ? pattern.componentPaths.filter((entry) => typeof entry === "string") : []
|
|
5560
|
+
}));
|
|
5561
|
+
const rules = (ruleManifest?.rules ?? []).map((rule) => ({
|
|
5562
|
+
id: rule.id,
|
|
5563
|
+
severity: rule.severity,
|
|
5564
|
+
enabled: rule.enabled,
|
|
5565
|
+
description: rule.description
|
|
5566
|
+
}));
|
|
5567
|
+
return {
|
|
5568
|
+
patternsPath: patternPack ? ".decantr/local-patterns.json" : null,
|
|
5569
|
+
rulesPath: ruleManifest ? ".decantr/rules.json" : null,
|
|
5570
|
+
patternCount: patterns.length,
|
|
5571
|
+
ruleCount: rules.length,
|
|
5572
|
+
patterns,
|
|
5573
|
+
rules
|
|
5574
|
+
};
|
|
5575
|
+
}
|
|
5576
|
+
function changedFiles(projectRoot, since) {
|
|
5577
|
+
const changed = /* @__PURE__ */ new Set();
|
|
5578
|
+
try {
|
|
5579
|
+
const commands = since ? [
|
|
5580
|
+
["diff", "--name-only", since, "--"],
|
|
5581
|
+
["diff", "--name-only", "--cached"]
|
|
5582
|
+
] : [
|
|
5583
|
+
["diff", "--name-only"],
|
|
5584
|
+
["diff", "--name-only", "--cached"]
|
|
5585
|
+
];
|
|
5586
|
+
for (const args of commands) {
|
|
5587
|
+
const output = execFileSync("git", args, {
|
|
5588
|
+
cwd: projectRoot,
|
|
5589
|
+
encoding: "utf-8",
|
|
5590
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5591
|
+
});
|
|
5592
|
+
for (const line of output.split(/\r?\n/)) {
|
|
5593
|
+
const file = line.trim();
|
|
5594
|
+
if (file) changed.add(normalizePath(file));
|
|
5595
|
+
}
|
|
5596
|
+
}
|
|
5597
|
+
} catch {
|
|
5598
|
+
}
|
|
5599
|
+
return [...changed].sort();
|
|
5600
|
+
}
|
|
5601
|
+
function routeImpacts(projectRoot, files) {
|
|
5602
|
+
const analysis = readJsonFile(
|
|
5603
|
+
join26(projectRoot, ".decantr", "analysis.json")
|
|
5604
|
+
);
|
|
5605
|
+
const routeEntries = analysis?.routes?.routes ?? [];
|
|
5606
|
+
const impacted = /* @__PURE__ */ new Set();
|
|
5607
|
+
for (const file of files) {
|
|
5608
|
+
for (const route of routeEntries) {
|
|
5609
|
+
if (route.file && pathMatches(file, route.file)) {
|
|
5610
|
+
if (route.path) impacted.add(route.path);
|
|
5611
|
+
}
|
|
5612
|
+
}
|
|
5613
|
+
}
|
|
5614
|
+
return [...impacted].sort();
|
|
5615
|
+
}
|
|
5616
|
+
function scanLocalRules(projectRoot, manifest) {
|
|
5617
|
+
const findings = [];
|
|
5618
|
+
const files = listSourceFiles(projectRoot, 1200);
|
|
5619
|
+
for (const rule of manifest.rules ?? []) {
|
|
5620
|
+
if (!rule.enabled || rule.type !== "forbid-regex") continue;
|
|
5621
|
+
const extensions = new Set(
|
|
5622
|
+
rule.includeExtensions?.length ? rule.includeExtensions : DEFAULT_RULE_EXTENSIONS
|
|
5623
|
+
);
|
|
5624
|
+
let regex;
|
|
5625
|
+
try {
|
|
5626
|
+
regex = new RegExp(rule.pattern, "g");
|
|
5627
|
+
} catch {
|
|
5628
|
+
findings.push({
|
|
5629
|
+
ruleId: rule.id,
|
|
5630
|
+
severity: "error",
|
|
5631
|
+
file: ".decantr/rules.json",
|
|
5632
|
+
line: 1,
|
|
5633
|
+
column: 1,
|
|
5634
|
+
excerpt: rule.pattern,
|
|
5635
|
+
message: `Invalid regex for local rule ${rule.id}.`,
|
|
5636
|
+
suggestedFix: "Edit .decantr/rules.json so the pattern is a valid JavaScript regular expression."
|
|
5637
|
+
});
|
|
5638
|
+
continue;
|
|
5639
|
+
}
|
|
5640
|
+
let ruleFindingCount = 0;
|
|
5641
|
+
for (const file of files) {
|
|
5642
|
+
if (!extensions.has(extname(file.absolute))) continue;
|
|
5643
|
+
if (pathAllowed(file.relative, rule.allowedPaths ?? [])) continue;
|
|
5644
|
+
const contents = readFileSync18(file.absolute, "utf-8");
|
|
5645
|
+
for (const match of contents.matchAll(regex)) {
|
|
5646
|
+
const index = match.index ?? 0;
|
|
5647
|
+
const position = lineColumnAt(contents, index);
|
|
5648
|
+
findings.push({
|
|
5649
|
+
ruleId: rule.id,
|
|
5650
|
+
severity: rule.severity,
|
|
5651
|
+
file: file.relative,
|
|
5652
|
+
line: position.line,
|
|
5653
|
+
column: position.column,
|
|
5654
|
+
excerpt: lineAt(contents, position.line).trim().slice(0, 180),
|
|
5655
|
+
message: rule.message,
|
|
5656
|
+
suggestedFix: rule.suggestedFix
|
|
5657
|
+
});
|
|
5658
|
+
ruleFindingCount += 1;
|
|
5659
|
+
if (rule.maxFindings && ruleFindingCount >= rule.maxFindings) break;
|
|
5660
|
+
}
|
|
5661
|
+
if (rule.maxFindings && ruleFindingCount >= rule.maxFindings) break;
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
5664
|
+
return findings;
|
|
5665
|
+
}
|
|
5666
|
+
function summarizeSourceEvidence(projectRoot, files) {
|
|
5667
|
+
const componentPaths = files.filter((file) => /(^|[/\\])components?([/\\]|$)|(^|[/\\])ui([/\\]|$)/i.test(file.relative)).map((file) => file.relative);
|
|
5668
|
+
const byName = (terms) => componentPaths.filter((file) => terms.some((term) => basename2(file).toLowerCase().includes(term))).slice(0, 12);
|
|
5669
|
+
const themeComponents = componentPaths.filter((file) => /theme|provider|mode|appearance|tenant|brand/i.test(file)).slice(0, 12);
|
|
5670
|
+
const shellComponents = files.filter((file) => /layout|shell|frame|app|root|nav|sidebar/i.test(basename2(file.relative))).map((file) => file.relative).slice(0, 12);
|
|
5671
|
+
return {
|
|
5672
|
+
buttonComponents: byName(["button", "action"]),
|
|
5673
|
+
cardComponents: byName(["card", "panel", "surface", "tile"]),
|
|
5674
|
+
formComponents: byName(["input", "field", "form", "select", "textarea"]),
|
|
5675
|
+
shellComponents,
|
|
5676
|
+
themeComponents,
|
|
5677
|
+
cardClassHints: collectClassHints(projectRoot, files, ["card", "panel", "surface", "tile"])
|
|
5678
|
+
};
|
|
5679
|
+
}
|
|
5680
|
+
function collectClassHints(projectRoot, files, terms) {
|
|
5681
|
+
const hints = /* @__PURE__ */ new Map();
|
|
5682
|
+
for (const file of files) {
|
|
5683
|
+
if (!UI_TEMPLATE_EXTENSIONS.has(extname(file.absolute))) continue;
|
|
5684
|
+
const content = readFileSync18(join26(projectRoot, file.relative), "utf-8");
|
|
5685
|
+
if (!terms.some((term) => content.toLowerCase().includes(term))) continue;
|
|
5686
|
+
const matches = content.matchAll(/\bclass(?:Name)?\s*=\s*["'`]([^"'`]+)["'`]/g);
|
|
5687
|
+
for (const match of matches) {
|
|
5688
|
+
const value = match[1].trim();
|
|
5689
|
+
if (!/(card|panel|surface|rounded|shadow|border|bg-|p-\d|px-|py-)/i.test(value)) continue;
|
|
5690
|
+
hints.set(value, (hints.get(value) ?? 0) + 1);
|
|
5691
|
+
}
|
|
5692
|
+
}
|
|
5693
|
+
return [...hints.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8).map(([hint]) => hint);
|
|
5694
|
+
}
|
|
5695
|
+
function listSourceFiles(projectRoot, maxFiles) {
|
|
5696
|
+
const files = [];
|
|
5697
|
+
const visit = (dir) => {
|
|
5698
|
+
if (files.length >= maxFiles) return;
|
|
5699
|
+
let entries;
|
|
5700
|
+
try {
|
|
5701
|
+
entries = readdirSync5(dir);
|
|
5702
|
+
} catch {
|
|
5703
|
+
return;
|
|
5704
|
+
}
|
|
5705
|
+
for (const entry of entries) {
|
|
5706
|
+
if (files.length >= maxFiles) return;
|
|
5707
|
+
if (IGNORED_DIRS.has(entry)) continue;
|
|
5708
|
+
const absolute = join26(dir, entry);
|
|
5709
|
+
let stat;
|
|
5710
|
+
try {
|
|
5711
|
+
stat = statSync5(absolute);
|
|
5712
|
+
} catch {
|
|
5713
|
+
continue;
|
|
5714
|
+
}
|
|
5715
|
+
if (stat.isDirectory()) {
|
|
5716
|
+
visit(absolute);
|
|
5717
|
+
} else if (stat.isFile() && SOURCE_EXTENSIONS.has(extname(entry))) {
|
|
5718
|
+
files.push({ absolute, relative: normalizePath(relative2(projectRoot, absolute)) });
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
};
|
|
5722
|
+
visit(projectRoot);
|
|
5723
|
+
return files.sort((a, b) => a.relative.localeCompare(b.relative));
|
|
5724
|
+
}
|
|
5725
|
+
function readJsonFile(path) {
|
|
5726
|
+
if (!existsSync25(path)) return null;
|
|
5727
|
+
try {
|
|
5728
|
+
return JSON.parse(readFileSync18(path, "utf-8"));
|
|
5729
|
+
} catch {
|
|
5730
|
+
return null;
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
function pathAllowed(file, allowedPaths) {
|
|
5734
|
+
return allowedPaths.some((allowedPath) => pathMatches(file, allowedPath));
|
|
5735
|
+
}
|
|
5736
|
+
function pathMatches(file, pattern) {
|
|
5737
|
+
const normalizedFile = normalizePath(file);
|
|
5738
|
+
const normalizedPattern = normalizePath(pattern);
|
|
5739
|
+
return normalizedFile === normalizedPattern || normalizedFile.endsWith(`/${normalizedPattern}`);
|
|
5740
|
+
}
|
|
5741
|
+
function normalizePath(path) {
|
|
5742
|
+
return path.split(sep).join("/").replace(/\\/g, "/");
|
|
5743
|
+
}
|
|
5744
|
+
function lineColumnAt(contents, index) {
|
|
5745
|
+
const before = contents.slice(0, index);
|
|
5746
|
+
const lines = before.split(/\r?\n/);
|
|
5747
|
+
return {
|
|
5748
|
+
line: lines.length,
|
|
5749
|
+
column: lines[lines.length - 1].length + 1
|
|
5750
|
+
};
|
|
5751
|
+
}
|
|
5752
|
+
function lineAt(contents, line) {
|
|
5753
|
+
return contents.split(/\r?\n/)[line - 1] ?? "";
|
|
5754
|
+
}
|
|
5755
|
+
|
|
5274
5756
|
// src/prompts.ts
|
|
5275
5757
|
import { createInterface } from "readline";
|
|
5276
5758
|
var BOLD6 = "\x1B[1m";
|
|
@@ -5568,8 +6050,8 @@ async function runSimplifiedInit(blueprints) {
|
|
|
5568
6050
|
}
|
|
5569
6051
|
|
|
5570
6052
|
// src/theme-commands.ts
|
|
5571
|
-
import { existsSync as
|
|
5572
|
-
import { join as
|
|
6053
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync13, readdirSync as readdirSync6, readFileSync as readFileSync19, rmSync as rmSync3, writeFileSync as writeFileSync16 } from "fs";
|
|
6054
|
+
import { join as join27 } from "path";
|
|
5573
6055
|
var REQUIRED_FIELDS = [
|
|
5574
6056
|
"$schema",
|
|
5575
6057
|
"id",
|
|
@@ -5629,20 +6111,20 @@ function validateCustomTheme(theme) {
|
|
|
5629
6111
|
};
|
|
5630
6112
|
}
|
|
5631
6113
|
function createTheme(projectRoot, id, name) {
|
|
5632
|
-
const customThemesDir =
|
|
5633
|
-
const themePath =
|
|
5634
|
-
const howToPath =
|
|
5635
|
-
|
|
5636
|
-
if (
|
|
6114
|
+
const customThemesDir = join27(projectRoot, ".decantr", "custom", "themes");
|
|
6115
|
+
const themePath = join27(customThemesDir, `${id}.json`);
|
|
6116
|
+
const howToPath = join27(customThemesDir, "how-to-theme.md");
|
|
6117
|
+
mkdirSync13(customThemesDir, { recursive: true });
|
|
6118
|
+
if (existsSync26(themePath)) {
|
|
5637
6119
|
return {
|
|
5638
6120
|
success: false,
|
|
5639
6121
|
error: `Theme "${id}" already exists at ${themePath}`
|
|
5640
6122
|
};
|
|
5641
6123
|
}
|
|
5642
6124
|
const skeleton = getThemeSkeleton(id, name);
|
|
5643
|
-
|
|
5644
|
-
if (!
|
|
5645
|
-
|
|
6125
|
+
writeFileSync16(themePath, JSON.stringify(skeleton, null, 2));
|
|
6126
|
+
if (!existsSync26(howToPath)) {
|
|
6127
|
+
writeFileSync16(howToPath, getHowToThemeDoc());
|
|
5646
6128
|
}
|
|
5647
6129
|
return {
|
|
5648
6130
|
success: true,
|
|
@@ -5650,17 +6132,17 @@ function createTheme(projectRoot, id, name) {
|
|
|
5650
6132
|
};
|
|
5651
6133
|
}
|
|
5652
6134
|
function listCustomThemes(projectRoot) {
|
|
5653
|
-
const customThemesDir =
|
|
5654
|
-
if (!
|
|
6135
|
+
const customThemesDir = join27(projectRoot, ".decantr", "custom", "themes");
|
|
6136
|
+
if (!existsSync26(customThemesDir)) {
|
|
5655
6137
|
return [];
|
|
5656
6138
|
}
|
|
5657
6139
|
const themes = [];
|
|
5658
6140
|
try {
|
|
5659
|
-
const files =
|
|
6141
|
+
const files = readdirSync6(customThemesDir).filter((f) => f.endsWith(".json"));
|
|
5660
6142
|
for (const file of files) {
|
|
5661
|
-
const filePath =
|
|
6143
|
+
const filePath = join27(customThemesDir, file);
|
|
5662
6144
|
try {
|
|
5663
|
-
const data = JSON.parse(
|
|
6145
|
+
const data = JSON.parse(readFileSync19(filePath, "utf-8"));
|
|
5664
6146
|
themes.push({
|
|
5665
6147
|
id: data.id || file.replace(".json", ""),
|
|
5666
6148
|
name: data.name || data.id,
|
|
@@ -5675,8 +6157,8 @@ function listCustomThemes(projectRoot) {
|
|
|
5675
6157
|
return themes;
|
|
5676
6158
|
}
|
|
5677
6159
|
function deleteTheme(projectRoot, id) {
|
|
5678
|
-
const themePath =
|
|
5679
|
-
if (!
|
|
6160
|
+
const themePath = join27(projectRoot, ".decantr", "custom", "themes", `${id}.json`);
|
|
6161
|
+
if (!existsSync26(themePath)) {
|
|
5680
6162
|
return {
|
|
5681
6163
|
success: false,
|
|
5682
6164
|
error: `Theme "${id}" not found at ${themePath}`
|
|
@@ -5693,7 +6175,7 @@ function deleteTheme(projectRoot, id) {
|
|
|
5693
6175
|
}
|
|
5694
6176
|
}
|
|
5695
6177
|
function importTheme(projectRoot, sourcePath) {
|
|
5696
|
-
if (!
|
|
6178
|
+
if (!existsSync26(sourcePath)) {
|
|
5697
6179
|
return {
|
|
5698
6180
|
success: false,
|
|
5699
6181
|
errors: [`Source file not found: ${sourcePath}`]
|
|
@@ -5701,7 +6183,7 @@ function importTheme(projectRoot, sourcePath) {
|
|
|
5701
6183
|
}
|
|
5702
6184
|
let theme;
|
|
5703
6185
|
try {
|
|
5704
|
-
theme = JSON.parse(
|
|
6186
|
+
theme = JSON.parse(readFileSync19(sourcePath, "utf-8"));
|
|
5705
6187
|
} catch (e) {
|
|
5706
6188
|
return {
|
|
5707
6189
|
success: false,
|
|
@@ -5717,14 +6199,14 @@ function importTheme(projectRoot, sourcePath) {
|
|
|
5717
6199
|
}
|
|
5718
6200
|
theme.source = "custom";
|
|
5719
6201
|
const id = theme.id;
|
|
5720
|
-
const customThemesDir =
|
|
5721
|
-
const destPath =
|
|
5722
|
-
|
|
5723
|
-
const howToPath =
|
|
5724
|
-
if (!
|
|
5725
|
-
|
|
5726
|
-
}
|
|
5727
|
-
|
|
6202
|
+
const customThemesDir = join27(projectRoot, ".decantr", "custom", "themes");
|
|
6203
|
+
const destPath = join27(customThemesDir, `${id}.json`);
|
|
6204
|
+
mkdirSync13(customThemesDir, { recursive: true });
|
|
6205
|
+
const howToPath = join27(customThemesDir, "how-to-theme.md");
|
|
6206
|
+
if (!existsSync26(howToPath)) {
|
|
6207
|
+
writeFileSync16(howToPath, getHowToThemeDoc());
|
|
6208
|
+
}
|
|
6209
|
+
writeFileSync16(destPath, JSON.stringify(theme, null, 2));
|
|
5728
6210
|
return {
|
|
5729
6211
|
success: true,
|
|
5730
6212
|
path: destPath
|
|
@@ -5732,19 +6214,19 @@ function importTheme(projectRoot, sourcePath) {
|
|
|
5732
6214
|
}
|
|
5733
6215
|
|
|
5734
6216
|
// src/workspace.ts
|
|
5735
|
-
import { existsSync as
|
|
5736
|
-
import { dirname as dirname3, join as
|
|
6217
|
+
import { existsSync as existsSync27, readdirSync as readdirSync7, readFileSync as readFileSync20 } from "fs";
|
|
6218
|
+
import { dirname as dirname3, join as join28, resolve as resolve3 } from "path";
|
|
5737
6219
|
function readPackageJson(dir) {
|
|
5738
|
-
const path =
|
|
5739
|
-
if (!
|
|
6220
|
+
const path = join28(dir, "package.json");
|
|
6221
|
+
if (!existsSync27(path)) return null;
|
|
5740
6222
|
try {
|
|
5741
|
-
return JSON.parse(
|
|
6223
|
+
return JSON.parse(readFileSync20(path, "utf-8"));
|
|
5742
6224
|
} catch {
|
|
5743
6225
|
return null;
|
|
5744
6226
|
}
|
|
5745
6227
|
}
|
|
5746
6228
|
function hasWorkspaceMarker(dir) {
|
|
5747
|
-
if (
|
|
6229
|
+
if (existsSync27(join28(dir, "pnpm-workspace.yaml")) || existsSync27(join28(dir, "turbo.json")) || existsSync27(join28(dir, "nx.json"))) {
|
|
5748
6230
|
return true;
|
|
5749
6231
|
}
|
|
5750
6232
|
const pkg = readPackageJson(dir);
|
|
@@ -5760,7 +6242,7 @@ function findWorkspaceRoot(startDir) {
|
|
|
5760
6242
|
}
|
|
5761
6243
|
}
|
|
5762
6244
|
function looksLikeApp(dir) {
|
|
5763
|
-
if (
|
|
6245
|
+
if (existsSync27(join28(dir, "next.config.js")) || existsSync27(join28(dir, "next.config.ts")) || existsSync27(join28(dir, "next.config.mjs")) || existsSync27(join28(dir, "vite.config.ts")) || existsSync27(join28(dir, "vite.config.js")) || existsSync27(join28(dir, "angular.json")) || existsSync27(join28(dir, "svelte.config.js")) || existsSync27(join28(dir, "svelte.config.ts")) || existsSync27(join28(dir, "astro.config.mjs")) || existsSync27(join28(dir, "src")) || existsSync27(join28(dir, "app")) || existsSync27(join28(dir, "pages"))) {
|
|
5764
6246
|
return true;
|
|
5765
6247
|
}
|
|
5766
6248
|
const pkg = readPackageJson(dir);
|
|
@@ -5772,11 +6254,11 @@ function looksLikeApp(dir) {
|
|
|
5772
6254
|
function listWorkspaceApps(workspaceRoot) {
|
|
5773
6255
|
const candidates = [];
|
|
5774
6256
|
for (const base of ["apps", "packages"]) {
|
|
5775
|
-
const baseDir =
|
|
5776
|
-
if (!
|
|
5777
|
-
for (const entry of
|
|
6257
|
+
const baseDir = join28(workspaceRoot, base);
|
|
6258
|
+
if (!existsSync27(baseDir)) continue;
|
|
6259
|
+
for (const entry of readdirSync7(baseDir, { withFileTypes: true })) {
|
|
5778
6260
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
5779
|
-
const candidate =
|
|
6261
|
+
const candidate = join28(baseDir, entry.name);
|
|
5780
6262
|
if (looksLikeApp(candidate)) {
|
|
5781
6263
|
candidates.push(`${base}/${entry.name}`);
|
|
5782
6264
|
}
|
|
@@ -6341,18 +6823,18 @@ function extractHostedAssetPaths(indexHtml) {
|
|
|
6341
6823
|
return [...assetPaths];
|
|
6342
6824
|
}
|
|
6343
6825
|
function readHostedDistSnapshot(distPath) {
|
|
6344
|
-
const resolvedDistPath = distPath ? resolveUserPath(distPath) :
|
|
6345
|
-
const indexPath =
|
|
6346
|
-
if (!
|
|
6826
|
+
const resolvedDistPath = distPath ? resolveUserPath(distPath) : join29(process.cwd(), "dist");
|
|
6827
|
+
const indexPath = join29(resolvedDistPath, "index.html");
|
|
6828
|
+
if (!existsSync28(indexPath)) {
|
|
6347
6829
|
return void 0;
|
|
6348
6830
|
}
|
|
6349
|
-
const indexHtml =
|
|
6831
|
+
const indexHtml = readFileSync21(indexPath, "utf-8");
|
|
6350
6832
|
const assetPaths = extractHostedAssetPaths(indexHtml);
|
|
6351
6833
|
const assets = {};
|
|
6352
6834
|
for (const assetPath of assetPaths) {
|
|
6353
|
-
const assetFilePath =
|
|
6354
|
-
if (
|
|
6355
|
-
assets[assetPath] =
|
|
6835
|
+
const assetFilePath = join29(resolvedDistPath, assetPath.replace(/^[/\\]+/, ""));
|
|
6836
|
+
if (existsSync28(assetFilePath)) {
|
|
6837
|
+
assets[assetPath] = readFileSync21(assetFilePath, "utf-8");
|
|
6356
6838
|
}
|
|
6357
6839
|
}
|
|
6358
6840
|
return {
|
|
@@ -6367,7 +6849,7 @@ function isHostedSourceSnapshotFile(path) {
|
|
|
6367
6849
|
function readHostedSourceSnapshot(sourcePath) {
|
|
6368
6850
|
if (!sourcePath) return void 0;
|
|
6369
6851
|
const resolvedSourcePath = resolveUserPath(sourcePath);
|
|
6370
|
-
if (!
|
|
6852
|
+
if (!existsSync28(resolvedSourcePath)) {
|
|
6371
6853
|
return void 0;
|
|
6372
6854
|
}
|
|
6373
6855
|
const files = {};
|
|
@@ -6379,19 +6861,19 @@ function readHostedSourceSnapshot(sourcePath) {
|
|
|
6379
6861
|
"build",
|
|
6380
6862
|
"coverage"
|
|
6381
6863
|
]);
|
|
6382
|
-
const rootPrefix =
|
|
6864
|
+
const rootPrefix = basename3(resolvedSourcePath);
|
|
6383
6865
|
const walk = (absoluteDir, relativeDir) => {
|
|
6384
|
-
for (const entry of
|
|
6866
|
+
for (const entry of readdirSync8(absoluteDir, { withFileTypes: true })) {
|
|
6385
6867
|
if (ignoredDirNames.has(entry.name)) continue;
|
|
6386
|
-
const absolutePath =
|
|
6387
|
-
const relativePath =
|
|
6868
|
+
const absolutePath = join29(absoluteDir, entry.name);
|
|
6869
|
+
const relativePath = join29(relativeDir, entry.name).replace(/\\/g, "/");
|
|
6388
6870
|
if (entry.isDirectory()) {
|
|
6389
6871
|
walk(absolutePath, relativePath);
|
|
6390
6872
|
continue;
|
|
6391
6873
|
}
|
|
6392
6874
|
if (!entry.isFile()) continue;
|
|
6393
6875
|
if (!isHostedSourceSnapshotFile(relativePath)) continue;
|
|
6394
|
-
files[relativePath] =
|
|
6876
|
+
files[relativePath] = readFileSync21(absolutePath, "utf-8");
|
|
6395
6877
|
}
|
|
6396
6878
|
};
|
|
6397
6879
|
walk(resolvedSourcePath, rootPrefix);
|
|
@@ -6542,16 +7024,16 @@ async function printRegistryIntelligenceSummary(namespace, jsonOutput = false) {
|
|
|
6542
7024
|
}
|
|
6543
7025
|
async function printHostedExecutionPackBundle(essencePath, namespace, jsonOutput = false, writeContext = false) {
|
|
6544
7026
|
const client = getPublicAPIClient();
|
|
6545
|
-
const resolvedPath = essencePath ? resolveUserPath(essencePath) :
|
|
6546
|
-
if (!
|
|
7027
|
+
const resolvedPath = essencePath ? resolveUserPath(essencePath) : join29(process.cwd(), "decantr.essence.json");
|
|
7028
|
+
if (!existsSync28(resolvedPath)) {
|
|
6547
7029
|
throw new Error(`Essence file not found at ${resolvedPath}`);
|
|
6548
7030
|
}
|
|
6549
|
-
const essence = JSON.parse(
|
|
7031
|
+
const essence = JSON.parse(readFileSync21(resolvedPath, "utf-8"));
|
|
6550
7032
|
const bundle = await client.compileExecutionPacks(essence, namespace ? { namespace } : void 0);
|
|
6551
7033
|
let writtenContextPaths = [];
|
|
6552
7034
|
if (writeContext) {
|
|
6553
|
-
const contextDir =
|
|
6554
|
-
|
|
7035
|
+
const contextDir = join29(process.cwd(), ".decantr", "context");
|
|
7036
|
+
mkdirSync14(contextDir, { recursive: true });
|
|
6555
7037
|
const written = writeExecutionPackBundleArtifacts(
|
|
6556
7038
|
contextDir,
|
|
6557
7039
|
bundle
|
|
@@ -6576,7 +7058,7 @@ async function printHostedExecutionPackBundle(essencePath, namespace, jsonOutput
|
|
|
6576
7058
|
console.log(` Sections: ${typedBundle.sections.length}`);
|
|
6577
7059
|
console.log(` Mutations: ${typedBundle.mutations.length}`);
|
|
6578
7060
|
if (writeContext) {
|
|
6579
|
-
console.log(` Context bundle: ${
|
|
7061
|
+
console.log(` Context bundle: ${join29(process.cwd(), ".decantr", "context")}`);
|
|
6580
7062
|
console.log(` Files written: ${writtenContextPaths.length}`);
|
|
6581
7063
|
}
|
|
6582
7064
|
console.log("");
|
|
@@ -6588,11 +7070,11 @@ async function printHostedExecutionPackBundle(essencePath, namespace, jsonOutput
|
|
|
6588
7070
|
}
|
|
6589
7071
|
}
|
|
6590
7072
|
function resolvePagePackIdForRoute(essencePath, route) {
|
|
6591
|
-
if (!
|
|
7073
|
+
if (!existsSync28(essencePath)) {
|
|
6592
7074
|
throw new Error(`Essence file not found at ${essencePath}`);
|
|
6593
7075
|
}
|
|
6594
|
-
const essence = JSON.parse(
|
|
6595
|
-
if (!
|
|
7076
|
+
const essence = JSON.parse(readFileSync21(essencePath, "utf-8"));
|
|
7077
|
+
if (!isV48(essence)) {
|
|
6596
7078
|
throw new Error("Route-based pack resolution requires Essence v4.0.0.");
|
|
6597
7079
|
}
|
|
6598
7080
|
const target = essence.blueprint.routes?.[route];
|
|
@@ -6606,14 +7088,14 @@ function resolvePagePackIdForRoute(essencePath, route) {
|
|
|
6606
7088
|
}
|
|
6607
7089
|
async function printHostedSelectedExecutionPack(packType, id, essencePath, namespace, jsonOutput = false, writeContext = false) {
|
|
6608
7090
|
const client = getPublicAPIClient();
|
|
6609
|
-
const resolvedPath = essencePath ? resolveUserPath(essencePath) :
|
|
6610
|
-
if (!
|
|
7091
|
+
const resolvedPath = essencePath ? resolveUserPath(essencePath) : join29(process.cwd(), "decantr.essence.json");
|
|
7092
|
+
if (!existsSync28(resolvedPath)) {
|
|
6611
7093
|
throw new Error(`Essence file not found at ${resolvedPath}`);
|
|
6612
7094
|
}
|
|
6613
7095
|
if ((packType === "section" || packType === "page" || packType === "mutation") && !id) {
|
|
6614
7096
|
throw new Error(`Pack type "${packType}" requires an id.`);
|
|
6615
7097
|
}
|
|
6616
|
-
const essence = JSON.parse(
|
|
7098
|
+
const essence = JSON.parse(readFileSync21(resolvedPath, "utf-8"));
|
|
6617
7099
|
const selected = await client.selectExecutionPack(
|
|
6618
7100
|
{
|
|
6619
7101
|
essence,
|
|
@@ -6624,17 +7106,17 @@ async function printHostedSelectedExecutionPack(packType, id, essencePath, names
|
|
|
6624
7106
|
);
|
|
6625
7107
|
let writtenContextDir = null;
|
|
6626
7108
|
if (writeContext) {
|
|
6627
|
-
const contextDir =
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
7109
|
+
const contextDir = join29(process.cwd(), ".decantr", "context");
|
|
7110
|
+
mkdirSync14(contextDir, { recursive: true });
|
|
7111
|
+
writeFileSync17(
|
|
7112
|
+
join29(contextDir, "pack-manifest.json"),
|
|
6631
7113
|
JSON.stringify(selected.manifest, null, 2) + "\n"
|
|
6632
7114
|
);
|
|
6633
7115
|
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
7116
|
const markdownFile = manifestEntry?.markdown ?? `${selected.selector.packType}${selected.selector.id ? `-${selected.selector.id}` : ""}-pack.md`;
|
|
6635
7117
|
const jsonFile = manifestEntry?.json ?? `${selected.selector.packType}${selected.selector.id ? `-${selected.selector.id}` : ""}-pack.json`;
|
|
6636
|
-
|
|
6637
|
-
|
|
7118
|
+
writeFileSync17(join29(contextDir, markdownFile), selected.pack.renderedMarkdown);
|
|
7119
|
+
writeFileSync17(join29(contextDir, jsonFile), JSON.stringify(selected.pack, null, 2) + "\n");
|
|
6638
7120
|
writtenContextDir = contextDir;
|
|
6639
7121
|
}
|
|
6640
7122
|
if (jsonOutput) {
|
|
@@ -6659,20 +7141,20 @@ async function printHostedSelectedExecutionPack(packType, id, essencePath, names
|
|
|
6659
7141
|
}
|
|
6660
7142
|
async function printHostedExecutionPackManifest(essencePath, namespace, jsonOutput = false, writeContext = false) {
|
|
6661
7143
|
const client = getPublicAPIClient();
|
|
6662
|
-
const resolvedPath = essencePath ? resolveUserPath(essencePath) :
|
|
6663
|
-
if (!
|
|
7144
|
+
const resolvedPath = essencePath ? resolveUserPath(essencePath) : join29(process.cwd(), "decantr.essence.json");
|
|
7145
|
+
if (!existsSync28(resolvedPath)) {
|
|
6664
7146
|
throw new Error(`Essence file not found at ${resolvedPath}`);
|
|
6665
7147
|
}
|
|
6666
|
-
const essence = JSON.parse(
|
|
7148
|
+
const essence = JSON.parse(readFileSync21(resolvedPath, "utf-8"));
|
|
6667
7149
|
const manifest = await client.getExecutionPackManifest(
|
|
6668
7150
|
essence,
|
|
6669
7151
|
namespace ? { namespace } : void 0
|
|
6670
7152
|
);
|
|
6671
7153
|
let writtenContextDir = null;
|
|
6672
7154
|
if (writeContext) {
|
|
6673
|
-
const contextDir =
|
|
6674
|
-
|
|
6675
|
-
|
|
7155
|
+
const contextDir = join29(process.cwd(), ".decantr", "context");
|
|
7156
|
+
mkdirSync14(contextDir, { recursive: true });
|
|
7157
|
+
writeFileSync17(join29(contextDir, "pack-manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
6676
7158
|
writtenContextDir = contextDir;
|
|
6677
7159
|
}
|
|
6678
7160
|
if (jsonOutput) {
|
|
@@ -6693,14 +7175,14 @@ async function printHostedExecutionPackManifest(essencePath, namespace, jsonOutp
|
|
|
6693
7175
|
}
|
|
6694
7176
|
}
|
|
6695
7177
|
async function hydrateHostedExecutionPacksIfMissing(projectRoot, namespace = "@official") {
|
|
6696
|
-
const contextDir =
|
|
6697
|
-
const reviewPackPath =
|
|
6698
|
-
const manifestPath =
|
|
6699
|
-
if (
|
|
7178
|
+
const contextDir = join29(projectRoot, ".decantr", "context");
|
|
7179
|
+
const reviewPackPath = join29(contextDir, "review-pack.json");
|
|
7180
|
+
const manifestPath = join29(contextDir, "pack-manifest.json");
|
|
7181
|
+
if (existsSync28(reviewPackPath) && existsSync28(manifestPath)) {
|
|
6700
7182
|
return { attempted: false, hydrated: false };
|
|
6701
7183
|
}
|
|
6702
|
-
const essencePath =
|
|
6703
|
-
if (!
|
|
7184
|
+
const essencePath = join29(projectRoot, "decantr.essence.json");
|
|
7185
|
+
if (!existsSync28(essencePath)) {
|
|
6704
7186
|
return { attempted: false, hydrated: false };
|
|
6705
7187
|
}
|
|
6706
7188
|
const reviewHydration = await hydrateHostedReviewPackIfMissing(projectRoot, namespace);
|
|
@@ -6709,9 +7191,9 @@ async function hydrateHostedExecutionPacksIfMissing(projectRoot, namespace = "@o
|
|
|
6709
7191
|
}
|
|
6710
7192
|
try {
|
|
6711
7193
|
const client = getPublicAPIClient();
|
|
6712
|
-
const essence = JSON.parse(
|
|
7194
|
+
const essence = JSON.parse(readFileSync21(essencePath, "utf-8"));
|
|
6713
7195
|
const bundle = await client.compileExecutionPacks(essence, { namespace });
|
|
6714
|
-
|
|
7196
|
+
mkdirSync14(contextDir, { recursive: true });
|
|
6715
7197
|
writeExecutionPackBundleArtifacts(contextDir, bundle);
|
|
6716
7198
|
return { attempted: true, hydrated: true, scope: "bundle" };
|
|
6717
7199
|
} catch {
|
|
@@ -6719,19 +7201,19 @@ async function hydrateHostedExecutionPacksIfMissing(projectRoot, namespace = "@o
|
|
|
6719
7201
|
}
|
|
6720
7202
|
}
|
|
6721
7203
|
async function hydrateHostedReviewPackIfMissing(projectRoot, namespace = "@official") {
|
|
6722
|
-
const contextDir =
|
|
6723
|
-
const reviewPackPath =
|
|
6724
|
-
const manifestPath =
|
|
6725
|
-
if (
|
|
7204
|
+
const contextDir = join29(projectRoot, ".decantr", "context");
|
|
7205
|
+
const reviewPackPath = join29(contextDir, "review-pack.json");
|
|
7206
|
+
const manifestPath = join29(contextDir, "pack-manifest.json");
|
|
7207
|
+
if (existsSync28(reviewPackPath) && existsSync28(manifestPath)) {
|
|
6726
7208
|
return { attempted: false, hydrated: false };
|
|
6727
7209
|
}
|
|
6728
|
-
const essencePath =
|
|
6729
|
-
if (!
|
|
7210
|
+
const essencePath = join29(projectRoot, "decantr.essence.json");
|
|
7211
|
+
if (!existsSync28(essencePath)) {
|
|
6730
7212
|
return { attempted: false, hydrated: false };
|
|
6731
7213
|
}
|
|
6732
7214
|
try {
|
|
6733
7215
|
const client = getPublicAPIClient();
|
|
6734
|
-
const essence = JSON.parse(
|
|
7216
|
+
const essence = JSON.parse(readFileSync21(essencePath, "utf-8"));
|
|
6735
7217
|
const selected = await client.selectExecutionPack(
|
|
6736
7218
|
{
|
|
6737
7219
|
essence,
|
|
@@ -6739,14 +7221,14 @@ async function hydrateHostedReviewPackIfMissing(projectRoot, namespace = "@offic
|
|
|
6739
7221
|
},
|
|
6740
7222
|
{ namespace }
|
|
6741
7223
|
);
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
7224
|
+
mkdirSync14(contextDir, { recursive: true });
|
|
7225
|
+
writeFileSync17(join29(contextDir, "review-pack.md"), selected.pack.renderedMarkdown);
|
|
7226
|
+
writeFileSync17(
|
|
7227
|
+
join29(contextDir, "review-pack.json"),
|
|
6746
7228
|
JSON.stringify(selected.pack, null, 2) + "\n"
|
|
6747
7229
|
);
|
|
6748
|
-
if (!
|
|
6749
|
-
|
|
7230
|
+
if (!existsSync28(manifestPath)) {
|
|
7231
|
+
writeFileSync17(manifestPath, JSON.stringify(selected.manifest, null, 2) + "\n");
|
|
6750
7232
|
}
|
|
6751
7233
|
return { attempted: true, hydrated: true, scope: "review" };
|
|
6752
7234
|
} catch {
|
|
@@ -6756,17 +7238,17 @@ async function hydrateHostedReviewPackIfMissing(projectRoot, namespace = "@offic
|
|
|
6756
7238
|
async function printHostedFileCritique(sourcePath, namespace, jsonOutput = false, essencePath, treatmentsPath) {
|
|
6757
7239
|
const client = getPublicAPIClient();
|
|
6758
7240
|
const resolvedSourcePath = resolveUserPath(sourcePath);
|
|
6759
|
-
const resolvedEssencePath = essencePath ? resolveUserPath(essencePath) :
|
|
6760
|
-
const resolvedTreatmentsPath = treatmentsPath ? resolveUserPath(treatmentsPath) :
|
|
6761
|
-
if (!
|
|
7241
|
+
const resolvedEssencePath = essencePath ? resolveUserPath(essencePath) : join29(process.cwd(), "decantr.essence.json");
|
|
7242
|
+
const resolvedTreatmentsPath = treatmentsPath ? resolveUserPath(treatmentsPath) : join29(process.cwd(), "src", "styles", "treatments.css");
|
|
7243
|
+
if (!existsSync28(resolvedSourcePath)) {
|
|
6762
7244
|
throw new Error(`Source file not found at ${resolvedSourcePath}`);
|
|
6763
7245
|
}
|
|
6764
|
-
if (!
|
|
7246
|
+
if (!existsSync28(resolvedEssencePath)) {
|
|
6765
7247
|
throw new Error(`Essence file not found at ${resolvedEssencePath}`);
|
|
6766
7248
|
}
|
|
6767
|
-
const code =
|
|
6768
|
-
const essence = JSON.parse(
|
|
6769
|
-
const treatmentsCss =
|
|
7249
|
+
const code = readFileSync21(resolvedSourcePath, "utf-8");
|
|
7250
|
+
const essence = JSON.parse(readFileSync21(resolvedEssencePath, "utf-8"));
|
|
7251
|
+
const treatmentsCss = existsSync28(resolvedTreatmentsPath) ? readFileSync21(resolvedTreatmentsPath, "utf-8") : void 0;
|
|
6770
7252
|
const report = await client.critiqueFile(
|
|
6771
7253
|
{
|
|
6772
7254
|
essence,
|
|
@@ -6790,11 +7272,11 @@ async function printHostedFileCritique(sourcePath, namespace, jsonOutput = false
|
|
|
6790
7272
|
}
|
|
6791
7273
|
async function printHostedProjectAudit(namespace, jsonOutput = false, essencePath, distPath, sourcesPath) {
|
|
6792
7274
|
const client = getPublicAPIClient();
|
|
6793
|
-
const resolvedEssencePath = essencePath ? resolveUserPath(essencePath) :
|
|
6794
|
-
if (!
|
|
7275
|
+
const resolvedEssencePath = essencePath ? resolveUserPath(essencePath) : join29(process.cwd(), "decantr.essence.json");
|
|
7276
|
+
if (!existsSync28(resolvedEssencePath)) {
|
|
6795
7277
|
throw new Error(`Essence file not found at ${resolvedEssencePath}`);
|
|
6796
7278
|
}
|
|
6797
|
-
const essence = JSON.parse(
|
|
7279
|
+
const essence = JSON.parse(readFileSync21(resolvedEssencePath, "utf-8"));
|
|
6798
7280
|
const dist = readHostedDistSnapshot(distPath);
|
|
6799
7281
|
const sources = readHostedSourceSnapshot(sourcesPath);
|
|
6800
7282
|
const report = await client.auditProject(
|
|
@@ -6812,7 +7294,7 @@ async function printHostedProjectAudit(namespace, jsonOutput = false, essencePat
|
|
|
6812
7294
|
console.log(heading2("Hosted Project Audit"));
|
|
6813
7295
|
console.log(` Essence: ${resolvedEssencePath}`);
|
|
6814
7296
|
console.log(
|
|
6815
|
-
` Dist snapshot: ${dist ? distPath ? resolveUserPath(distPath) :
|
|
7297
|
+
` Dist snapshot: ${dist ? distPath ? resolveUserPath(distPath) : join29(process.cwd(), "dist") : "none"}`
|
|
6816
7298
|
);
|
|
6817
7299
|
console.log(
|
|
6818
7300
|
` Source snapshot: ${sources && sourcesPath ? resolveUserPath(sourcesPath) : "none"}`
|
|
@@ -6915,21 +7397,21 @@ function patternCandidateFromRegistryItem(item, source) {
|
|
|
6915
7397
|
function readSuggestCodeContext(route, file) {
|
|
6916
7398
|
const pieces = [];
|
|
6917
7399
|
if (file) {
|
|
6918
|
-
const resolved = isAbsolute(file) ? file :
|
|
6919
|
-
if (
|
|
6920
|
-
pieces.push(
|
|
7400
|
+
const resolved = isAbsolute(file) ? file : join29(process.cwd(), file);
|
|
7401
|
+
if (existsSync28(resolved)) {
|
|
7402
|
+
pieces.push(readFileSync21(resolved, "utf-8"));
|
|
6921
7403
|
}
|
|
6922
7404
|
}
|
|
6923
7405
|
if (route) {
|
|
6924
|
-
const analysisPath =
|
|
6925
|
-
if (
|
|
7406
|
+
const analysisPath = join29(process.cwd(), ".decantr", "analysis.json");
|
|
7407
|
+
if (existsSync28(analysisPath)) {
|
|
6926
7408
|
try {
|
|
6927
|
-
const analysis = JSON.parse(
|
|
7409
|
+
const analysis = JSON.parse(readFileSync21(analysisPath, "utf-8"));
|
|
6928
7410
|
const routeEntry = analysis.routes?.routes?.find((entry) => entry.path === route);
|
|
6929
7411
|
if (routeEntry?.file) {
|
|
6930
|
-
const resolved =
|
|
6931
|
-
if (
|
|
6932
|
-
pieces.push(
|
|
7412
|
+
const resolved = join29(process.cwd(), routeEntry.file);
|
|
7413
|
+
if (existsSync28(resolved)) {
|
|
7414
|
+
pieces.push(readFileSync21(resolved, "utf-8"));
|
|
6933
7415
|
}
|
|
6934
7416
|
}
|
|
6935
7417
|
} catch {
|
|
@@ -6986,7 +7468,7 @@ async function cmdSuggest(query, options = {}) {
|
|
|
6986
7468
|
}
|
|
6987
7469
|
}
|
|
6988
7470
|
const registryClient = new RegistryClient({
|
|
6989
|
-
cacheDir:
|
|
7471
|
+
cacheDir: join29(process.cwd(), ".decantr", "cache")
|
|
6990
7472
|
});
|
|
6991
7473
|
const code = options.fromCode || options.file ? readSuggestCodeContext(options.route, options.file) : "";
|
|
6992
7474
|
const candidates = await loadPatternDiscoveryCandidates(registryClient);
|
|
@@ -7048,7 +7530,7 @@ async function cmdGet(type, id) {
|
|
|
7048
7530
|
}
|
|
7049
7531
|
const apiType = CONTENT_TYPE_TO_API_CONTENT_TYPE3[type];
|
|
7050
7532
|
const registryClient = new RegistryClient({
|
|
7051
|
-
cacheDir:
|
|
7533
|
+
cacheDir: join29(process.cwd(), ".decantr", "cache")
|
|
7052
7534
|
});
|
|
7053
7535
|
const result = await registryClient.fetchContentItem(apiType, id);
|
|
7054
7536
|
if (result) {
|
|
@@ -7065,10 +7547,10 @@ async function cmdGet(type, id) {
|
|
|
7065
7547
|
return;
|
|
7066
7548
|
}
|
|
7067
7549
|
async function cmdValidate(path) {
|
|
7068
|
-
const essencePath = path ||
|
|
7550
|
+
const essencePath = path || join29(process.cwd(), "decantr.essence.json");
|
|
7069
7551
|
let raw;
|
|
7070
7552
|
try {
|
|
7071
|
-
raw =
|
|
7553
|
+
raw = readFileSync21(essencePath, "utf-8");
|
|
7072
7554
|
} catch {
|
|
7073
7555
|
console.error(error3(`Could not read ${essencePath}`));
|
|
7074
7556
|
process.exitCode = 1;
|
|
@@ -7082,7 +7564,7 @@ async function cmdValidate(path) {
|
|
|
7082
7564
|
process.exitCode = 1;
|
|
7083
7565
|
return;
|
|
7084
7566
|
}
|
|
7085
|
-
const detectedVersion =
|
|
7567
|
+
const detectedVersion = isV48(essence) ? "v4" : "legacy";
|
|
7086
7568
|
console.log(`${DIM14}Detected essence version: ${detectedVersion}${RESET14}`);
|
|
7087
7569
|
const result = validateEssence2(essence);
|
|
7088
7570
|
if (result.valid) {
|
|
@@ -7135,7 +7617,7 @@ async function cmdList(type, sort, recommended, intelligenceSource, blueprintSet
|
|
|
7135
7617
|
return;
|
|
7136
7618
|
}
|
|
7137
7619
|
const registryClient = new RegistryClient({
|
|
7138
|
-
cacheDir:
|
|
7620
|
+
cacheDir: join29(process.cwd(), ".decantr", "cache")
|
|
7139
7621
|
});
|
|
7140
7622
|
const result = await registryClient.fetchContentList(
|
|
7141
7623
|
type,
|
|
@@ -7218,10 +7700,10 @@ ${CYAN8}Telemetry enabled.${RESET14} Decantr will send privacy-filtered CLI prod
|
|
|
7218
7700
|
}
|
|
7219
7701
|
function readCliPackageVersion() {
|
|
7220
7702
|
const here = dirname4(fileURLToPath2(import.meta.url));
|
|
7221
|
-
const candidates = [
|
|
7703
|
+
const candidates = [join29(here, "..", "package.json"), join29(here, "..", "..", "package.json")];
|
|
7222
7704
|
for (const candidate of candidates) {
|
|
7223
7705
|
try {
|
|
7224
|
-
const pkg = JSON.parse(
|
|
7706
|
+
const pkg = JSON.parse(readFileSync21(candidate, "utf-8"));
|
|
7225
7707
|
if (pkg.version) return pkg.version;
|
|
7226
7708
|
} catch {
|
|
7227
7709
|
}
|
|
@@ -7232,19 +7714,19 @@ function timestampForFile() {
|
|
|
7232
7714
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
7233
7715
|
}
|
|
7234
7716
|
function backupExistingEssence(projectRoot, label) {
|
|
7235
|
-
const essencePath =
|
|
7236
|
-
if (!
|
|
7237
|
-
const backupPath =
|
|
7717
|
+
const essencePath = join29(projectRoot, "decantr.essence.json");
|
|
7718
|
+
if (!existsSync28(essencePath)) return null;
|
|
7719
|
+
const backupPath = join29(
|
|
7238
7720
|
projectRoot,
|
|
7239
7721
|
`decantr.essence.${label}.${timestampForFile()}.backup.json`
|
|
7240
7722
|
);
|
|
7241
|
-
|
|
7723
|
+
writeFileSync17(backupPath, readFileSync21(essencePath, "utf-8"), "utf-8");
|
|
7242
7724
|
return backupPath;
|
|
7243
7725
|
}
|
|
7244
7726
|
function writeBrownfieldProjectJson(input) {
|
|
7245
|
-
const decantrDir =
|
|
7246
|
-
|
|
7247
|
-
|
|
7727
|
+
const decantrDir = join29(input.projectRoot, ".decantr");
|
|
7728
|
+
mkdirSync14(join29(decantrDir, "context"), { recursive: true });
|
|
7729
|
+
mkdirSync14(join29(decantrDir, "cache"), { recursive: true });
|
|
7248
7730
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7249
7731
|
const projectJson = {
|
|
7250
7732
|
detected: {
|
|
@@ -7285,7 +7767,7 @@ function writeBrownfieldProjectJson(input) {
|
|
|
7285
7767
|
}
|
|
7286
7768
|
}
|
|
7287
7769
|
};
|
|
7288
|
-
|
|
7770
|
+
writeFileSync17(join29(decantrDir, "project.json"), JSON.stringify(projectJson, null, 2) + "\n");
|
|
7289
7771
|
}
|
|
7290
7772
|
async function applyAcceptedBrownfieldProposal(input) {
|
|
7291
7773
|
const proposal = readBrownfieldProposal(input.projectRoot);
|
|
@@ -7301,8 +7783,8 @@ async function applyAcceptedBrownfieldProposal(input) {
|
|
|
7301
7783
|
process.exitCode = 1;
|
|
7302
7784
|
return;
|
|
7303
7785
|
}
|
|
7304
|
-
const essencePath =
|
|
7305
|
-
const hasEssence =
|
|
7786
|
+
const essencePath = join29(input.projectRoot, "decantr.essence.json");
|
|
7787
|
+
const hasEssence = existsSync28(essencePath);
|
|
7306
7788
|
let essence;
|
|
7307
7789
|
let backupPath = null;
|
|
7308
7790
|
if (input.mode === "accept" && hasEssence) {
|
|
@@ -7316,8 +7798,8 @@ async function applyAcceptedBrownfieldProposal(input) {
|
|
|
7316
7798
|
return;
|
|
7317
7799
|
}
|
|
7318
7800
|
if (input.mode === "merge" && hasEssence) {
|
|
7319
|
-
const existing = JSON.parse(
|
|
7320
|
-
if (!
|
|
7801
|
+
const existing = JSON.parse(readFileSync21(essencePath, "utf-8"));
|
|
7802
|
+
if (!isV48(existing)) {
|
|
7321
7803
|
console.log(
|
|
7322
7804
|
error3(
|
|
7323
7805
|
"Existing essence is not v4. Run `decantr migrate --to v4` before merging a brownfield proposal."
|
|
@@ -7353,9 +7835,9 @@ async function applyAcceptedBrownfieldProposal(input) {
|
|
|
7353
7835
|
assistantBridge: input.assistantBridge,
|
|
7354
7836
|
mode: input.mode
|
|
7355
7837
|
});
|
|
7356
|
-
|
|
7838
|
+
writeFileSync17(essencePath, JSON.stringify(essence, null, 2) + "\n", "utf-8");
|
|
7357
7839
|
const registryClient = new RegistryClient({
|
|
7358
|
-
cacheDir:
|
|
7840
|
+
cacheDir: join29(input.projectRoot, ".decantr", "cache"),
|
|
7359
7841
|
offline: true,
|
|
7360
7842
|
projectRoot: input.projectRoot
|
|
7361
7843
|
});
|
|
@@ -7509,7 +7991,7 @@ async function cmdInit(args) {
|
|
|
7509
7991
|
}
|
|
7510
7992
|
}
|
|
7511
7993
|
const registryClient = new RegistryClient({
|
|
7512
|
-
cacheDir:
|
|
7994
|
+
cacheDir: join29(projectRoot, ".decantr", "cache"),
|
|
7513
7995
|
apiUrl: args.registry,
|
|
7514
7996
|
offline: args.offline,
|
|
7515
7997
|
projectRoot
|
|
@@ -7839,7 +8321,7 @@ ${YELLOW9}You're offline. Scaffolding Decantr default.${RESET14}`);
|
|
|
7839
8321
|
if (appliedRuleFiles.length > 0) {
|
|
7840
8322
|
console.log(` ${dim3(`Rule bridge applied: ${appliedRuleFiles.join(", ")}`)}`);
|
|
7841
8323
|
}
|
|
7842
|
-
if (!
|
|
8324
|
+
if (!existsSync28(join29(projectRoot, "package.json"))) {
|
|
7843
8325
|
console.log("");
|
|
7844
8326
|
console.log(
|
|
7845
8327
|
dim3(` Note: ${cyan3("decantr init")} created Decantr contract/context files only.`)
|
|
@@ -7850,7 +8332,7 @@ ${YELLOW9}You're offline. Scaffolding Decantr default.${RESET14}`);
|
|
|
7850
8332
|
)
|
|
7851
8333
|
);
|
|
7852
8334
|
}
|
|
7853
|
-
const hasCompiledPacks =
|
|
8335
|
+
const hasCompiledPacks = existsSync28(join29(projectRoot, ".decantr", "context", "scaffold-pack.md"));
|
|
7854
8336
|
console.log("");
|
|
7855
8337
|
console.log(" Next steps:");
|
|
7856
8338
|
if (hasCompiledPacks) {
|
|
@@ -7890,7 +8372,7 @@ ${YELLOW9}You're offline. Scaffolding Decantr default.${RESET14}`);
|
|
|
7890
8372
|
console.log(` ${cyan3("decantr upgrade")} Update to latest patterns`);
|
|
7891
8373
|
console.log(` ${cyan3("decantr check")} Detect drift issues`);
|
|
7892
8374
|
console.log(` ${cyan3("decantr migrate --to v4")} Migrate older essence files to v4`);
|
|
7893
|
-
const essenceContent =
|
|
8375
|
+
const essenceContent = readFileSync21(result.essencePath, "utf-8");
|
|
7894
8376
|
const essence = JSON.parse(essenceContent);
|
|
7895
8377
|
const validation = validateEssence2(essence);
|
|
7896
8378
|
if (!validation.valid) {
|
|
@@ -7899,7 +8381,7 @@ Validation warnings: ${validation.errors.join(", ")}`));
|
|
|
7899
8381
|
}
|
|
7900
8382
|
console.log("");
|
|
7901
8383
|
let promptPages;
|
|
7902
|
-
if (
|
|
8384
|
+
if (isV48(essence)) {
|
|
7903
8385
|
const allPages = essence.blueprint.sections.flatMap(
|
|
7904
8386
|
(s) => s.pages.map((p) => ({ ...p, _sectionId: s.id, _shell: s.shell }))
|
|
7905
8387
|
);
|
|
@@ -7946,25 +8428,25 @@ Validation warnings: ${validation.errors.join(", ")}`));
|
|
|
7946
8428
|
}
|
|
7947
8429
|
async function cmdStatus() {
|
|
7948
8430
|
const projectRoot = process.cwd();
|
|
7949
|
-
const essencePath =
|
|
7950
|
-
const projectJsonPath =
|
|
8431
|
+
const essencePath = join29(projectRoot, "decantr.essence.json");
|
|
8432
|
+
const projectJsonPath = join29(projectRoot, ".decantr", "project.json");
|
|
7951
8433
|
console.log(heading2("Decantr Project Status"));
|
|
7952
|
-
if (!
|
|
8434
|
+
if (!existsSync28(essencePath)) {
|
|
7953
8435
|
console.log(`${RED11}No decantr.essence.json found.${RESET14}`);
|
|
7954
8436
|
console.log(dim3('Run "decantr init" to create one.'));
|
|
7955
8437
|
return;
|
|
7956
8438
|
}
|
|
7957
8439
|
try {
|
|
7958
|
-
const essence = JSON.parse(
|
|
8440
|
+
const essence = JSON.parse(readFileSync21(essencePath, "utf-8"));
|
|
7959
8441
|
const validation = validateEssence2(essence);
|
|
7960
|
-
const essenceVersion =
|
|
8442
|
+
const essenceVersion = isV48(essence) ? "v4" : "legacy";
|
|
7961
8443
|
console.log(`${BOLD7}Essence:${RESET14}`);
|
|
7962
8444
|
if (validation.valid) {
|
|
7963
8445
|
console.log(` ${GREEN14}Valid${RESET14} (${essenceVersion})`);
|
|
7964
8446
|
} else {
|
|
7965
8447
|
console.log(` ${RED11}Invalid: ${validation.errors.join(", ")}${RESET14}`);
|
|
7966
8448
|
}
|
|
7967
|
-
if (
|
|
8449
|
+
if (isV48(essence)) {
|
|
7968
8450
|
const v4 = essence;
|
|
7969
8451
|
const sections = v4.blueprint.sections;
|
|
7970
8452
|
const flatPages = sections.flatMap((section) => section.pages ?? []);
|
|
@@ -8003,9 +8485,9 @@ async function cmdStatus() {
|
|
|
8003
8485
|
}
|
|
8004
8486
|
console.log("");
|
|
8005
8487
|
console.log(`${BOLD7}Sync Status:${RESET14}`);
|
|
8006
|
-
if (
|
|
8488
|
+
if (existsSync28(projectJsonPath)) {
|
|
8007
8489
|
try {
|
|
8008
|
-
const projectJson = JSON.parse(
|
|
8490
|
+
const projectJson = JSON.parse(readFileSync21(projectJsonPath, "utf-8"));
|
|
8009
8491
|
const syncStatus = projectJson.sync?.status || "unknown";
|
|
8010
8492
|
const lastSync = projectJson.sync?.lastSync || "never";
|
|
8011
8493
|
const source = projectJson.sync?.registrySource || "unknown";
|
|
@@ -8023,7 +8505,7 @@ async function cmdStatus() {
|
|
|
8023
8505
|
}
|
|
8024
8506
|
async function cmdSync() {
|
|
8025
8507
|
const projectRoot = process.cwd();
|
|
8026
|
-
const cacheDir =
|
|
8508
|
+
const cacheDir = join29(projectRoot, ".decantr", "cache");
|
|
8027
8509
|
console.log(heading2("Syncing registry content..."));
|
|
8028
8510
|
const result = await syncRegistry(cacheDir);
|
|
8029
8511
|
if (result.synced.length > 0) {
|
|
@@ -8218,14 +8700,14 @@ ${BOLD7}Examples:${RESET14}
|
|
|
8218
8700
|
process.exitCode = 1;
|
|
8219
8701
|
return;
|
|
8220
8702
|
}
|
|
8221
|
-
const themePath =
|
|
8222
|
-
if (!
|
|
8703
|
+
const themePath = join29(projectRoot, ".decantr", "custom", "themes", `${name}.json`);
|
|
8704
|
+
if (!existsSync28(themePath)) {
|
|
8223
8705
|
console.error(error3(`Theme "${name}" not found at ${themePath}`));
|
|
8224
8706
|
process.exitCode = 1;
|
|
8225
8707
|
return;
|
|
8226
8708
|
}
|
|
8227
8709
|
try {
|
|
8228
|
-
const theme = JSON.parse(
|
|
8710
|
+
const theme = JSON.parse(readFileSync21(themePath, "utf-8"));
|
|
8229
8711
|
const result = validateCustomTheme(theme);
|
|
8230
8712
|
if (result.valid) {
|
|
8231
8713
|
console.log(success3(`Custom theme "${name}" is valid`));
|
|
@@ -8385,8 +8867,8 @@ async function cmdSetupWorkflow(args) {
|
|
|
8385
8867
|
if (detected.existingEssence) {
|
|
8386
8868
|
console.log(`${BOLD7}Recommended path:${RESET14} maintain an attached Decantr project`);
|
|
8387
8869
|
console.log(` ${cyan3('decantr task <route> "<change>"')} Prepare LLM context before edits`);
|
|
8388
|
-
console.log(` ${cyan3("decantr verify")}
|
|
8389
|
-
console.log(` ${cyan3("decantr codify")}
|
|
8870
|
+
console.log(` ${cyan3("decantr verify --brownfield")} Run local health and drift checks`);
|
|
8871
|
+
console.log(` ${cyan3("decantr codify --from-audit")} Propose project-owned local law`);
|
|
8390
8872
|
return;
|
|
8391
8873
|
}
|
|
8392
8874
|
if (hasFootprint) {
|
|
@@ -8395,7 +8877,7 @@ async function cmdSetupWorkflow(args) {
|
|
|
8395
8877
|
console.log(
|
|
8396
8878
|
` ${cyan3("decantr adopt --base-url http://localhost:3000 --evidence --yes")} Include visual evidence`
|
|
8397
8879
|
);
|
|
8398
|
-
console.log(` ${cyan3("decantr codify")}
|
|
8880
|
+
console.log(` ${cyan3("decantr codify --from-audit")} Propose local UI law`);
|
|
8399
8881
|
return;
|
|
8400
8882
|
}
|
|
8401
8883
|
console.log(`${BOLD7}Recommended path:${RESET14} greenfield start`);
|
|
@@ -8416,7 +8898,7 @@ async function cmdAdoptWorkflow(args) {
|
|
|
8416
8898
|
const saveBaseline = flagBoolean(flags, "baseline", true) || flagBoolean(flags, "save-baseline");
|
|
8417
8899
|
const initCi = flagBoolean(flags, "ci") || flagBoolean(flags, "init-ci");
|
|
8418
8900
|
const assistantBridge = flagString(flags, "assistant-bridge");
|
|
8419
|
-
const hasEssence =
|
|
8901
|
+
const hasEssence = existsSync28(join29(projectRoot, "decantr.essence.json"));
|
|
8420
8902
|
const proposalFlag = flagBoolean(flags, "replace-essence") ? "--replace-essence" : flagBoolean(flags, "merge-proposal") || hasEssence ? "--merge-proposal" : "--accept-proposal";
|
|
8421
8903
|
const steps = [
|
|
8422
8904
|
"analyze current app and write .decantr/brownfield intelligence",
|
|
@@ -8476,10 +8958,12 @@ async function cmdAdoptWorkflow(args) {
|
|
|
8476
8958
|
});
|
|
8477
8959
|
}
|
|
8478
8960
|
console.log("");
|
|
8479
|
-
console.log(`${BOLD7}
|
|
8480
|
-
console.log(` ${cyan3(
|
|
8481
|
-
console.log(` ${cyan3("decantr codify")}
|
|
8482
|
-
console.log(` ${cyan3(
|
|
8961
|
+
console.log(`${BOLD7}Brownfield operating loop:${RESET14}`);
|
|
8962
|
+
console.log(` ${cyan3("decantr codify --from-audit")} Discover and propose project-owned UI law`);
|
|
8963
|
+
console.log(` ${cyan3("decantr codify --accept")} Accept reviewed local patterns and rules`);
|
|
8964
|
+
console.log(` ${cyan3('decantr task <route> "<change>"')} Give your LLM route-specific context before edits`);
|
|
8965
|
+
console.log(` ${cyan3("decantr verify --brownfield --local-patterns")} Check contract, health, and local law after edits`);
|
|
8966
|
+
console.log(` ${cyan3("decantr verify --since-baseline")} Compare future work against this baseline`);
|
|
8483
8967
|
}
|
|
8484
8968
|
async function cmdVerifyWorkflow(args) {
|
|
8485
8969
|
const { flags } = parseLooseArgs(args);
|
|
@@ -8500,6 +8984,7 @@ async function cmdVerifyWorkflow(args) {
|
|
|
8500
8984
|
const localPatterns = flagBoolean(flags, "local-patterns");
|
|
8501
8985
|
const evidence = flagBoolean(flags, "evidence");
|
|
8502
8986
|
const baseUrl = flagString(flags, "base-url");
|
|
8987
|
+
const failOn = flagString(flags, "fail-on") ?? "error";
|
|
8503
8988
|
const healthArgs = ["health", ...withoutWorkflowOnlyFlags(args)];
|
|
8504
8989
|
if (flagBoolean(flags, "baseline") && !healthArgs.includes("--save-baseline")) {
|
|
8505
8990
|
healthArgs.push("--save-baseline");
|
|
@@ -8535,16 +9020,51 @@ async function cmdVerifyWorkflow(args) {
|
|
|
8535
9020
|
const { cmdHealth, parseHealthArgs } = await import("./health-ETZXWGTW.js");
|
|
8536
9021
|
await cmdHealth(workspaceInfo.appRoot, parseHealthArgs(healthArgs));
|
|
8537
9022
|
if (localPatterns) {
|
|
8538
|
-
const
|
|
8539
|
-
if (!
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
9023
|
+
const validation = validateLocalLaw(workspaceInfo.appRoot);
|
|
9024
|
+
if (!validation.patternPackPresent) {
|
|
9025
|
+
if (!quietOutput) {
|
|
9026
|
+
console.log("");
|
|
9027
|
+
console.log(
|
|
9028
|
+
`${YELLOW9}Local pattern pack missing.${RESET14} Run ${cyan3("decantr codify --from-audit")}, review the proposal, then run ${cyan3("decantr codify --accept")}.`
|
|
9029
|
+
);
|
|
9030
|
+
}
|
|
8544
9031
|
process.exitCode = process.exitCode || 1;
|
|
8545
9032
|
} else {
|
|
8546
|
-
|
|
8547
|
-
|
|
9033
|
+
const blockingFindings = failOn === "none" ? [] : validation.findings.filter(
|
|
9034
|
+
(finding) => failOn === "warn" ? finding.severity === "warn" || finding.severity === "error" : finding.severity === "error"
|
|
9035
|
+
);
|
|
9036
|
+
const blockingWarnings = failOn === "warn" ? validation.warnings : [];
|
|
9037
|
+
if (!quietOutput) {
|
|
9038
|
+
console.log("");
|
|
9039
|
+
console.log(`${GREEN14}Local pattern pack found:${RESET14} ${validation.patternsPath}`);
|
|
9040
|
+
if (validation.ruleManifestPresent) {
|
|
9041
|
+
console.log(`${GREEN14}Local rule manifest found:${RESET14} ${validation.rulesPath}`);
|
|
9042
|
+
} else {
|
|
9043
|
+
console.log(
|
|
9044
|
+
`${YELLOW9}Local rule manifest missing.${RESET14} Run ${cyan3("decantr codify --from-audit")} to propose .decantr/rules.json.`
|
|
9045
|
+
);
|
|
9046
|
+
}
|
|
9047
|
+
for (const warning of validation.warnings.slice(0, 8)) {
|
|
9048
|
+
console.log(`${YELLOW9}warn${RESET14} ${warning}`);
|
|
9049
|
+
}
|
|
9050
|
+
if (validation.findings.length > 0) {
|
|
9051
|
+
console.log("");
|
|
9052
|
+
console.log(`${BOLD7}Local law findings:${RESET14}`);
|
|
9053
|
+
for (const finding of validation.findings.slice(0, 20)) {
|
|
9054
|
+
console.log(
|
|
9055
|
+
` ${finding.severity.toUpperCase()} ${finding.ruleId} ${finding.file}:${finding.line}:${finding.column} ${finding.message}`
|
|
9056
|
+
);
|
|
9057
|
+
}
|
|
9058
|
+
if (validation.findings.length > 20) {
|
|
9059
|
+
console.log(dim3(` ...${validation.findings.length - 20} more finding(s)`));
|
|
9060
|
+
}
|
|
9061
|
+
} else if (validation.ruleManifestPresent) {
|
|
9062
|
+
console.log(`${GREEN14}Local rule checks passed.${RESET14}`);
|
|
9063
|
+
}
|
|
9064
|
+
}
|
|
9065
|
+
if (blockingFindings.length > 0 || blockingWarnings.length > 0) {
|
|
9066
|
+
process.exitCode = process.exitCode || 1;
|
|
9067
|
+
}
|
|
8548
9068
|
}
|
|
8549
9069
|
}
|
|
8550
9070
|
if (guardExitCode && guardExitCode !== 0 && (!process.exitCode || process.exitCode === 0)) {
|
|
@@ -8552,9 +9072,9 @@ async function cmdVerifyWorkflow(args) {
|
|
|
8552
9072
|
}
|
|
8553
9073
|
}
|
|
8554
9074
|
function readJsonIfPresent(path) {
|
|
8555
|
-
if (!
|
|
9075
|
+
if (!existsSync28(path)) return null;
|
|
8556
9076
|
try {
|
|
8557
|
-
return JSON.parse(
|
|
9077
|
+
return JSON.parse(readFileSync21(path, "utf-8"));
|
|
8558
9078
|
} catch {
|
|
8559
9079
|
return null;
|
|
8560
9080
|
}
|
|
@@ -8565,20 +9085,20 @@ async function cmdTaskWorkflow(args) {
|
|
|
8565
9085
|
if (!workspaceInfo) return;
|
|
8566
9086
|
const routeInput = positional[0];
|
|
8567
9087
|
if (!routeInput) {
|
|
8568
|
-
console.error(error3('Usage: decantr task <route> ["task summary"] [--project <path>] [--json]'));
|
|
9088
|
+
console.error(error3('Usage: decantr task <route> ["task summary"] [--project <path>] [--since origin/main] [--json]'));
|
|
8569
9089
|
process.exitCode = 1;
|
|
8570
9090
|
return;
|
|
8571
9091
|
}
|
|
8572
9092
|
const route = routeInput.startsWith("/") ? routeInput : `/${routeInput}`;
|
|
8573
9093
|
const taskSummary = positional.slice(1).join(" ").trim();
|
|
8574
|
-
const essencePath =
|
|
9094
|
+
const essencePath = join29(workspaceInfo.appRoot, "decantr.essence.json");
|
|
8575
9095
|
const essence = readJsonIfPresent(essencePath);
|
|
8576
9096
|
if (!essence) {
|
|
8577
9097
|
console.error(error3("No decantr.essence.json found. Run `decantr adopt` or `decantr init` first."));
|
|
8578
9098
|
process.exitCode = 1;
|
|
8579
9099
|
return;
|
|
8580
9100
|
}
|
|
8581
|
-
if (!
|
|
9101
|
+
if (!isV48(essence)) {
|
|
8582
9102
|
console.error(error3("Task context requires Essence v4. Run `decantr migrate --to v4` first."));
|
|
8583
9103
|
process.exitCode = 1;
|
|
8584
9104
|
return;
|
|
@@ -8593,13 +9113,18 @@ async function cmdTaskWorkflow(args) {
|
|
|
8593
9113
|
}
|
|
8594
9114
|
const section = essence.blueprint.sections.find((entry) => entry.id === target.section);
|
|
8595
9115
|
const page = section?.pages.find((entry) => entry.id === target.page);
|
|
8596
|
-
const contextDir =
|
|
8597
|
-
const manifest = readJsonIfPresent(
|
|
9116
|
+
const contextDir = join29(workspaceInfo.appRoot, ".decantr", "context");
|
|
9117
|
+
const manifest = readJsonIfPresent(join29(contextDir, "pack-manifest.json"));
|
|
8598
9118
|
const pagePack = manifest?.pages?.find((entry) => entry.id === target.page);
|
|
8599
9119
|
const sectionPack = manifest?.sections?.find((entry) => entry.id === target.section);
|
|
8600
|
-
const visualManifest = readJsonIfPresent(
|
|
9120
|
+
const visualManifest = readJsonIfPresent(join29(workspaceInfo.appRoot, ".decantr", "evidence", "visual-manifest.json"));
|
|
8601
9121
|
const screenshot = visualManifest?.routes?.find((entry) => entry.route === route)?.screenshot;
|
|
8602
|
-
const
|
|
9122
|
+
const localPatternPackPath = localPatternsPath(workspaceInfo.appRoot);
|
|
9123
|
+
const localRuleManifestPath = localRulesPath(workspaceInfo.appRoot);
|
|
9124
|
+
const localLaw = createLocalLawTaskSummary(workspaceInfo.appRoot);
|
|
9125
|
+
const changedSince = flagString(flags, "since");
|
|
9126
|
+
const currentChangedFiles = changedFiles(workspaceInfo.appRoot, changedSince);
|
|
9127
|
+
const changedRoutes = routeImpacts(workspaceInfo.appRoot, currentChangedFiles);
|
|
8603
9128
|
const context = {
|
|
8604
9129
|
route,
|
|
8605
9130
|
task: taskSummary || null,
|
|
@@ -8608,14 +9133,19 @@ async function cmdTaskWorkflow(args) {
|
|
|
8608
9133
|
shell: page?.shell ?? section?.shell ?? null,
|
|
8609
9134
|
patterns: page?.layout?.map(extractPatternName) ?? [],
|
|
8610
9135
|
read: [
|
|
8611
|
-
pagePack ?
|
|
8612
|
-
sectionPack ?
|
|
8613
|
-
manifest?.scaffold?.markdown ?
|
|
9136
|
+
pagePack ? join29(".decantr/context", pagePack.markdown) : null,
|
|
9137
|
+
sectionPack ? join29(".decantr/context", sectionPack.markdown) : null,
|
|
9138
|
+
manifest?.scaffold?.markdown ? join29(".decantr/context", manifest.scaffold.markdown) : null,
|
|
8614
9139
|
".decantr/context/scaffold.md",
|
|
8615
9140
|
"DECANTR.md",
|
|
8616
|
-
|
|
9141
|
+
existsSync28(localPatternPackPath) ? ".decantr/local-patterns.json" : null,
|
|
9142
|
+
existsSync28(localRuleManifestPath) ? ".decantr/rules.json" : null
|
|
8617
9143
|
].filter(Boolean),
|
|
8618
|
-
screenshot: screenshot ?? null
|
|
9144
|
+
screenshot: screenshot ?? null,
|
|
9145
|
+
localLaw,
|
|
9146
|
+
changedFiles: currentChangedFiles,
|
|
9147
|
+
changedRoutes,
|
|
9148
|
+
verifyCommand: "decantr verify --brownfield --local-patterns"
|
|
8619
9149
|
};
|
|
8620
9150
|
if (flagBoolean(flags, "json")) {
|
|
8621
9151
|
console.log(JSON.stringify(context, null, 2));
|
|
@@ -8637,89 +9167,82 @@ async function cmdTaskWorkflow(args) {
|
|
|
8637
9167
|
console.log(`${BOLD7}Visual evidence:${RESET14}`);
|
|
8638
9168
|
console.log(` ${cyan3(context.screenshot)}`);
|
|
8639
9169
|
}
|
|
9170
|
+
if (context.localLaw.patternCount > 0 || context.localLaw.ruleCount > 0) {
|
|
9171
|
+
console.log("");
|
|
9172
|
+
console.log(`${BOLD7}Project-owned local law:${RESET14}`);
|
|
9173
|
+
if (context.localLaw.patternsPath) {
|
|
9174
|
+
console.log(` Patterns: ${cyan3(context.localLaw.patternsPath)} (${context.localLaw.patternCount})`);
|
|
9175
|
+
}
|
|
9176
|
+
if (context.localLaw.rulesPath) {
|
|
9177
|
+
console.log(` Rules: ${cyan3(context.localLaw.rulesPath)} (${context.localLaw.ruleCount})`);
|
|
9178
|
+
}
|
|
9179
|
+
for (const pattern of context.localLaw.patterns.slice(0, 4)) {
|
|
9180
|
+
const pathHint = pattern.componentPaths.length > 0 ? ` \u2014 ${pattern.componentPaths.slice(0, 2).join(", ")}` : "";
|
|
9181
|
+
console.log(` ${pattern.id}: ${pattern.role ?? "local pattern"}${pathHint}`);
|
|
9182
|
+
}
|
|
9183
|
+
} else {
|
|
9184
|
+
console.log("");
|
|
9185
|
+
console.log(`${BOLD7}Project-owned local law:${RESET14}`);
|
|
9186
|
+
console.log(` ${YELLOW9}Not codified yet.${RESET14} Run ${cyan3("decantr codify --from-audit")} after adoption.`);
|
|
9187
|
+
}
|
|
9188
|
+
if (context.changedFiles.length > 0) {
|
|
9189
|
+
console.log("");
|
|
9190
|
+
console.log(`${BOLD7}Changed-file context:${RESET14}`);
|
|
9191
|
+
for (const file of context.changedFiles.slice(0, 8)) {
|
|
9192
|
+
console.log(` ${file}`);
|
|
9193
|
+
}
|
|
9194
|
+
if (context.changedFiles.length > 8) {
|
|
9195
|
+
console.log(dim3(` ...${context.changedFiles.length - 8} more changed file(s)`));
|
|
9196
|
+
}
|
|
9197
|
+
if (context.changedRoutes.length > 0) {
|
|
9198
|
+
console.log(` Impacted routes: ${context.changedRoutes.join(", ")}`);
|
|
9199
|
+
}
|
|
9200
|
+
}
|
|
8640
9201
|
console.log("");
|
|
8641
9202
|
console.log(`${BOLD7}LLM instruction:${RESET14}`);
|
|
8642
9203
|
console.log(
|
|
8643
|
-
" Preserve the existing runtime and styling system. Use the route pack, section context, local
|
|
9204
|
+
" 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
9205
|
);
|
|
9206
|
+
console.log(` After editing, run ${cyan3(context.verifyCommand)}.`);
|
|
8645
9207
|
}
|
|
8646
9208
|
async function cmdCodifyWorkflow(args) {
|
|
8647
9209
|
const { flags } = parseLooseArgs(args);
|
|
8648
9210
|
const workspaceInfo = resolveWorkflowProject(flags);
|
|
8649
9211
|
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
9212
|
if (flagBoolean(flags, "accept")) {
|
|
8654
|
-
if (!
|
|
8655
|
-
console.error(
|
|
9213
|
+
if (!existsSync28(localPatternsProposalPath(workspaceInfo.appRoot)) && !existsSync28(localRulesProposalPath(workspaceInfo.appRoot))) {
|
|
9214
|
+
console.error(
|
|
9215
|
+
error3("No local law proposal found. Run `decantr codify --from-audit` or `decantr codify` first.")
|
|
9216
|
+
);
|
|
8656
9217
|
process.exitCode = 1;
|
|
8657
9218
|
return;
|
|
8658
9219
|
}
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
9220
|
+
const result2 = acceptBrownfieldLocalLaw(workspaceInfo.appRoot);
|
|
9221
|
+
if (result2.patternAcceptedPath) {
|
|
9222
|
+
console.log(success3(`Accepted local pattern pack: ${result2.patternAcceptedPath}`));
|
|
9223
|
+
}
|
|
9224
|
+
if (result2.rulesAcceptedPath) {
|
|
9225
|
+
console.log(success3(`Accepted local rule manifest: ${result2.rulesAcceptedPath}`));
|
|
9226
|
+
}
|
|
9227
|
+
console.log(dim3("Run `decantr verify --brownfield --local-patterns` after project edits."));
|
|
8662
9228
|
return;
|
|
8663
9229
|
}
|
|
8664
|
-
mkdirSync13(decantrDir, { recursive: true });
|
|
8665
9230
|
const detected = detectProject(workspaceInfo.appRoot);
|
|
8666
|
-
const essence = readJsonIfPresent(
|
|
8667
|
-
const
|
|
8668
|
-
const proposal = {
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
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`."));
|
|
9231
|
+
const essence = readJsonIfPresent(join29(workspaceInfo.appRoot, "decantr.essence.json"));
|
|
9232
|
+
const fromAudit = flagBoolean(flags, "from-audit") || flagBoolean(flags, "discover-local-patterns") || flagBoolean(flags, "codify-local-patterns");
|
|
9233
|
+
const proposal = createBrownfieldCodifyProposal({
|
|
9234
|
+
projectRoot: workspaceInfo.appRoot,
|
|
9235
|
+
detected,
|
|
9236
|
+
essence,
|
|
9237
|
+
fromAudit
|
|
9238
|
+
});
|
|
9239
|
+
const result = writeBrownfieldCodifyProposal(workspaceInfo.appRoot, proposal);
|
|
9240
|
+
console.log(success3(`Wrote local pattern proposal: ${result.patternPath}`));
|
|
9241
|
+
console.log(success3(`Wrote local rule proposal: ${result.rulesPath}`));
|
|
9242
|
+
if (fromAudit) {
|
|
9243
|
+
console.log(dim3("Proposal includes source-derived component candidates and starter mechanical rules."));
|
|
9244
|
+
}
|
|
9245
|
+
console.log(dim3("Review both files, add real component paths/token recipes, then run `decantr codify --accept`."));
|
|
8723
9246
|
}
|
|
8724
9247
|
async function cmdContentWorkflow(args) {
|
|
8725
9248
|
const subcommand = args[1] ?? "check";
|
|
@@ -8761,9 +9284,9 @@ ${BOLD7}Usage:${RESET14}
|
|
|
8761
9284
|
decantr setup [--project <path>]
|
|
8762
9285
|
decantr new <name> [--blueprint=X] [--archetype=X] [--theme=X] [--workflow=greenfield] [--adoption=decantr-css] [--telemetry]
|
|
8763
9286
|
decantr adopt [--project <path>] [--base-url <url>] [--evidence] [--ci] [--yes]
|
|
8764
|
-
decantr task <route> ["task summary"] [--project <path>] [--json]
|
|
9287
|
+
decantr task <route> ["task summary"] [--project <path>] [--since origin/main] [--json]
|
|
8765
9288
|
decantr verify [--project <path>] [--brownfield] [--local-patterns] [health options]
|
|
8766
|
-
decantr codify [--accept] [--project <path>]
|
|
9289
|
+
decantr codify [--from-audit] [--accept] [--project <path>]
|
|
8767
9290
|
decantr studio [--port 4319] [--host 127.0.0.1] [--report decantr-health.json] [--workspace]
|
|
8768
9291
|
|
|
8769
9292
|
${BOLD7}Advanced primitives:${RESET14}
|
|
@@ -8834,9 +9357,9 @@ ${BOLD7}Commands:${RESET14}
|
|
|
8834
9357
|
${cyan3("setup")} Detect project state and recommend the right Decantr workflow
|
|
8835
9358
|
${cyan3("new")} Create a new greenfield workspace and bootstrap the available starter adapter
|
|
8836
9359
|
${cyan3("adopt")} Brownfield one-liner: analyze, attach, verify, and show next steps
|
|
8837
|
-
${cyan3("task")} Prepare route/task context for an AI coding assistant
|
|
9360
|
+
${cyan3("task")} Prepare route/task context, local law, evidence, and changed-file impact for an AI coding assistant
|
|
8838
9361
|
${cyan3("verify")} One reliability gate over Project Health, Brownfield checks, baselines, and evidence
|
|
8839
|
-
${cyan3("codify")} Propose or accept project-owned Brownfield UI patterns
|
|
9362
|
+
${cyan3("codify")} Propose or accept project-owned Brownfield UI patterns and rules
|
|
8840
9363
|
${cyan3("studio")} Open a local Project Health dashboard backed by the same report
|
|
8841
9364
|
${cyan3("content")} Content-author namespace: check, create, publish
|
|
8842
9365
|
|
|
@@ -8878,7 +9401,7 @@ ${BOLD7}Examples:${RESET14}
|
|
|
8878
9401
|
decantr task /feed "add saved recipe actions"
|
|
8879
9402
|
decantr verify --brownfield --local-patterns
|
|
8880
9403
|
decantr verify --since-baseline
|
|
8881
|
-
decantr codify
|
|
9404
|
+
decantr codify --from-audit
|
|
8882
9405
|
decantr codify --accept
|
|
8883
9406
|
decantr content check --ci --fail-on error
|
|
8884
9407
|
decantr magic "AI chatbot with dark cyber theme \u2014 bold and futuristic"
|
|
@@ -8929,8 +9452,8 @@ ${BOLD7}Workflow Model:${RESET14}
|
|
|
8929
9452
|
${cyan3("Greenfield blueprint")} decantr new my-app --blueprint=X --workflow=greenfield --adoption=decantr-css
|
|
8930
9453
|
${cyan3("Greenfield contract")} decantr init --workflow=greenfield --adoption=contract-only
|
|
8931
9454
|
${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
|
|
9455
|
+
${cyan3("Daily LLM work")} decantr task <route> "<change>" -> decantr verify --brownfield --local-patterns
|
|
9456
|
+
${cyan3("Project-owned law")} decantr codify --from-audit -> edit proposal -> decantr codify --accept
|
|
8934
9457
|
${cyan3("Hybrid composition")} decantr add/remove, decantr theme switch, decantr registry, decantr upgrade
|
|
8935
9458
|
|
|
8936
9459
|
${BOLD7}Bootstrap adapters:${RESET14}
|
|
@@ -9111,7 +9634,7 @@ ${BOLD7}Examples:${RESET14}
|
|
|
9111
9634
|
}
|
|
9112
9635
|
function cmdAdoptHelp() {
|
|
9113
9636
|
console.log(`
|
|
9114
|
-
${BOLD7}decantr adopt${RESET14} \u2014 Brownfield one-liner: analyze, attach, verify, and show the
|
|
9637
|
+
${BOLD7}decantr adopt${RESET14} \u2014 Brownfield one-liner: analyze, attach, verify, and show the operating loop
|
|
9115
9638
|
|
|
9116
9639
|
${BOLD7}Usage:${RESET14}
|
|
9117
9640
|
decantr adopt [--project <path>] [--yes] [--dry-run]
|
|
@@ -9135,6 +9658,7 @@ ${BOLD7}Examples:${RESET14}
|
|
|
9135
9658
|
decantr adopt --yes
|
|
9136
9659
|
decantr adopt --base-url http://localhost:3000 --evidence --yes
|
|
9137
9660
|
decantr adopt --project apps/web --ci --yes
|
|
9661
|
+
decantr codify --from-audit
|
|
9138
9662
|
`);
|
|
9139
9663
|
}
|
|
9140
9664
|
function cmdVerifyHelp() {
|
|
@@ -9151,6 +9675,7 @@ ${BOLD7}Usage:${RESET14}
|
|
|
9151
9675
|
${BOLD7}Examples:${RESET14}
|
|
9152
9676
|
decantr verify
|
|
9153
9677
|
decantr verify --brownfield --local-patterns
|
|
9678
|
+
decantr verify --brownfield --local-patterns --fail-on warn
|
|
9154
9679
|
decantr verify --base-url http://localhost:3000 --evidence
|
|
9155
9680
|
decantr verify --workspace --changed --since origin/main
|
|
9156
9681
|
decantr verify init-ci --project apps/web
|
|
@@ -9161,25 +9686,27 @@ function cmdTaskHelp() {
|
|
|
9161
9686
|
${BOLD7}decantr task${RESET14} \u2014 Prepare compact route/task context for an AI coding assistant
|
|
9162
9687
|
|
|
9163
9688
|
${BOLD7}Usage:${RESET14}
|
|
9164
|
-
decantr task <route> ["task summary"] [--project <path>] [--json]
|
|
9689
|
+
decantr task <route> ["task summary"] [--project <path>] [--since origin/main] [--json]
|
|
9165
9690
|
|
|
9166
9691
|
${BOLD7}Examples:${RESET14}
|
|
9167
9692
|
decantr task /feed "add saved recipe actions"
|
|
9693
|
+
decantr task /feed "add saved recipe actions" --since origin/main
|
|
9168
9694
|
decantr task /profile --json
|
|
9169
9695
|
`);
|
|
9170
9696
|
}
|
|
9171
9697
|
function cmdCodifyHelp() {
|
|
9172
9698
|
console.log(`
|
|
9173
|
-
${BOLD7}decantr codify${RESET14} \u2014 Propose or accept project-owned Brownfield UI patterns
|
|
9699
|
+
${BOLD7}decantr codify${RESET14} \u2014 Propose or accept project-owned Brownfield UI patterns and rules
|
|
9174
9700
|
|
|
9175
9701
|
${BOLD7}Usage:${RESET14}
|
|
9176
|
-
decantr codify [--project <path>]
|
|
9702
|
+
decantr codify [--from-audit] [--project <path>]
|
|
9177
9703
|
decantr codify --accept [--project <path>]
|
|
9178
9704
|
|
|
9179
9705
|
${BOLD7}Examples:${RESET14}
|
|
9180
9706
|
decantr codify
|
|
9707
|
+
decantr codify --from-audit
|
|
9181
9708
|
decantr codify --accept
|
|
9182
|
-
decantr verify --local-patterns
|
|
9709
|
+
decantr verify --brownfield --local-patterns
|
|
9183
9710
|
`);
|
|
9184
9711
|
}
|
|
9185
9712
|
function cmdContentHelp() {
|
|
@@ -9254,10 +9781,10 @@ async function main() {
|
|
|
9254
9781
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
9255
9782
|
try {
|
|
9256
9783
|
const here = dirname4(fileURLToPath2(import.meta.url));
|
|
9257
|
-
const candidates = [
|
|
9784
|
+
const candidates = [join29(here, "..", "package.json"), join29(here, "..", "..", "package.json")];
|
|
9258
9785
|
for (const candidate of candidates) {
|
|
9259
|
-
if (
|
|
9260
|
-
const pkg = JSON.parse(
|
|
9786
|
+
if (existsSync28(candidate)) {
|
|
9787
|
+
const pkg = JSON.parse(readFileSync21(candidate, "utf-8"));
|
|
9261
9788
|
if (pkg.version) {
|
|
9262
9789
|
console.log(pkg.version);
|
|
9263
9790
|
return;
|
|
@@ -9730,7 +10257,7 @@ async function main() {
|
|
|
9730
10257
|
break;
|
|
9731
10258
|
}
|
|
9732
10259
|
if (packType === "page" && route && !id) {
|
|
9733
|
-
const resolvedPath = essencePath ? resolveUserPath(essencePath) :
|
|
10260
|
+
const resolvedPath = essencePath ? resolveUserPath(essencePath) : join29(process.cwd(), "decantr.essence.json");
|
|
9734
10261
|
id = resolvePagePackIdForRoute(resolvedPath, route);
|
|
9735
10262
|
}
|
|
9736
10263
|
await printHostedSelectedExecutionPack(
|