@decantr/mcp-server 1.0.0-beta.5 → 1.0.0-beta.7
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/LICENSE +21 -0
- package/dist/bin.js +1 -1
- package/dist/chunk-IWYOFW2H.js +1108 -0
- package/dist/index.js +1 -1
- package/package.json +10 -8
- package/dist/chunk-QUSQLU5X.js +0 -512
|
@@ -0,0 +1,1108 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import {
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
CallToolRequestSchema
|
|
7
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
|
|
9
|
+
// src/tools.ts
|
|
10
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
11
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
12
|
+
import { validateEssence, evaluateGuard, isV3 as isV32 } from "@decantr/essence-spec";
|
|
13
|
+
import { resolvePatternPreset } from "@decantr/registry";
|
|
14
|
+
|
|
15
|
+
// src/helpers.ts
|
|
16
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
17
|
+
import { join, dirname } from "path";
|
|
18
|
+
import { RegistryAPIClient } from "@decantr/registry";
|
|
19
|
+
import { isV3, migrateV2ToV3 } from "@decantr/essence-spec";
|
|
20
|
+
var MAX_INPUT_LENGTH = 1e3;
|
|
21
|
+
function validateStringArg(args, field) {
|
|
22
|
+
const val = args[field];
|
|
23
|
+
if (!val || typeof val !== "string") {
|
|
24
|
+
return `Required parameter "${field}" must be a non-empty string.`;
|
|
25
|
+
}
|
|
26
|
+
if (val.length > MAX_INPUT_LENGTH) {
|
|
27
|
+
return `Parameter "${field}" exceeds maximum length of ${MAX_INPUT_LENGTH} characters.`;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function fuzzyScore(query, text) {
|
|
32
|
+
const q = query.toLowerCase();
|
|
33
|
+
const t = text.toLowerCase();
|
|
34
|
+
if (t === q) return 100;
|
|
35
|
+
if (t.startsWith(q)) return 90;
|
|
36
|
+
if (t.includes(q)) return 80;
|
|
37
|
+
let qi = 0;
|
|
38
|
+
for (let ti = 0; ti < t.length && qi < q.length; ti++) {
|
|
39
|
+
if (t[ti] === q[qi]) qi++;
|
|
40
|
+
}
|
|
41
|
+
return qi === q.length ? 60 : 0;
|
|
42
|
+
}
|
|
43
|
+
var _apiClient = null;
|
|
44
|
+
function getAPIClient() {
|
|
45
|
+
if (!_apiClient) {
|
|
46
|
+
_apiClient = new RegistryAPIClient({
|
|
47
|
+
baseUrl: process.env.DECANTR_API_URL || void 0,
|
|
48
|
+
apiKey: process.env.DECANTR_API_KEY || void 0
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return _apiClient;
|
|
52
|
+
}
|
|
53
|
+
async function readEssenceFile(essencePath) {
|
|
54
|
+
const resolvedPath = essencePath || join(process.cwd(), "decantr.essence.json");
|
|
55
|
+
const raw = await readFile(resolvedPath, "utf-8");
|
|
56
|
+
const essence = JSON.parse(raw);
|
|
57
|
+
return { essence, raw, path: resolvedPath };
|
|
58
|
+
}
|
|
59
|
+
async function writeEssenceFile(essencePath, essence) {
|
|
60
|
+
const dir = dirname(essencePath);
|
|
61
|
+
await mkdir(dir, { recursive: true });
|
|
62
|
+
await writeFile(essencePath, JSON.stringify(essence, null, 2) + "\n", "utf-8");
|
|
63
|
+
}
|
|
64
|
+
async function mutateEssenceFile(essencePath, mutate) {
|
|
65
|
+
const { essence, path } = await readEssenceFile(essencePath);
|
|
66
|
+
const v3 = isV3(essence) ? structuredClone(essence) : migrateV2ToV3(essence);
|
|
67
|
+
const updated = mutate(v3);
|
|
68
|
+
await writeEssenceFile(path, updated);
|
|
69
|
+
return { essence: updated, path };
|
|
70
|
+
}
|
|
71
|
+
async function readDriftLog(projectRoot) {
|
|
72
|
+
const root = projectRoot || process.cwd();
|
|
73
|
+
const logPath = join(root, ".decantr", "drift-log.json");
|
|
74
|
+
try {
|
|
75
|
+
const raw = await readFile(logPath, "utf-8");
|
|
76
|
+
return JSON.parse(raw);
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function writeDriftLog(entries, projectRoot) {
|
|
82
|
+
const root = projectRoot || process.cwd();
|
|
83
|
+
const logPath = join(root, ".decantr", "drift-log.json");
|
|
84
|
+
await mkdir(dirname(logPath), { recursive: true });
|
|
85
|
+
await writeFile(logPath, JSON.stringify(entries, null, 2) + "\n", "utf-8");
|
|
86
|
+
return logPath;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/tools.ts
|
|
90
|
+
var ZONE_ORDER = ["public", "gateway", "primary", "auxiliary"];
|
|
91
|
+
function deriveZones(inputs) {
|
|
92
|
+
const zoneMap = /* @__PURE__ */ new Map();
|
|
93
|
+
for (const input of inputs) {
|
|
94
|
+
const existing = zoneMap.get(input.role);
|
|
95
|
+
if (existing) {
|
|
96
|
+
existing.archetypes.push(input.archetypeId);
|
|
97
|
+
existing.features.push(...input.features);
|
|
98
|
+
existing.descriptions.push(input.description);
|
|
99
|
+
} else {
|
|
100
|
+
zoneMap.set(input.role, {
|
|
101
|
+
role: input.role,
|
|
102
|
+
archetypes: [input.archetypeId],
|
|
103
|
+
shell: input.shell,
|
|
104
|
+
features: [...input.features],
|
|
105
|
+
descriptions: [input.description]
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const zone of zoneMap.values()) {
|
|
110
|
+
zone.features = [...new Set(zone.features)];
|
|
111
|
+
}
|
|
112
|
+
return ZONE_ORDER.filter((role) => zoneMap.has(role)).map((role) => zoneMap.get(role));
|
|
113
|
+
}
|
|
114
|
+
var GATEWAY_TRIGGER_MAP = {
|
|
115
|
+
auth: "authentication",
|
|
116
|
+
login: "authentication",
|
|
117
|
+
mfa: "authentication",
|
|
118
|
+
payment: "payment",
|
|
119
|
+
subscription: "payment",
|
|
120
|
+
checkout: "payment",
|
|
121
|
+
onboarding: "onboarding",
|
|
122
|
+
"setup-wizard": "onboarding",
|
|
123
|
+
welcome: "onboarding",
|
|
124
|
+
invite: "invitation",
|
|
125
|
+
"access-code": "invitation"
|
|
126
|
+
};
|
|
127
|
+
function resolveGatewayTrigger(features) {
|
|
128
|
+
for (const feature of features) {
|
|
129
|
+
const trigger = GATEWAY_TRIGGER_MAP[feature];
|
|
130
|
+
if (trigger) return trigger;
|
|
131
|
+
}
|
|
132
|
+
return "authentication";
|
|
133
|
+
}
|
|
134
|
+
function deriveTransitions(zones) {
|
|
135
|
+
const transitions = [];
|
|
136
|
+
const roles = new Set(zones.map((z) => z.role));
|
|
137
|
+
const gateway = zones.find((z) => z.role === "gateway");
|
|
138
|
+
const gatewayTrigger = gateway ? resolveGatewayTrigger(gateway.features) : "authentication";
|
|
139
|
+
const hasApp = roles.has("primary") || roles.has("auxiliary");
|
|
140
|
+
const hasGateway = roles.has("gateway");
|
|
141
|
+
const hasPublic = roles.has("public");
|
|
142
|
+
if (hasPublic && hasGateway) {
|
|
143
|
+
transitions.push({ from: "public", to: "gateway", type: "conversion", trigger: gatewayTrigger });
|
|
144
|
+
}
|
|
145
|
+
if (hasPublic && hasApp && !hasGateway) {
|
|
146
|
+
transitions.push({ from: "public", to: "app", type: "conversion", trigger: "navigation" });
|
|
147
|
+
}
|
|
148
|
+
if (hasGateway && hasApp) {
|
|
149
|
+
transitions.push({ from: "gateway", to: "app", type: "gate-pass", trigger: gatewayTrigger });
|
|
150
|
+
transitions.push({ from: "app", to: "gateway", type: "gate-return", trigger: gatewayTrigger });
|
|
151
|
+
}
|
|
152
|
+
if (hasApp && hasPublic) {
|
|
153
|
+
transitions.push({ from: "app", to: "public", type: "navigation", trigger: "external" });
|
|
154
|
+
}
|
|
155
|
+
return transitions;
|
|
156
|
+
}
|
|
157
|
+
var READ_ONLY = {
|
|
158
|
+
readOnlyHint: true,
|
|
159
|
+
destructiveHint: false,
|
|
160
|
+
idempotentHint: true,
|
|
161
|
+
openWorldHint: false
|
|
162
|
+
};
|
|
163
|
+
var READ_ONLY_NETWORK = {
|
|
164
|
+
readOnlyHint: true,
|
|
165
|
+
destructiveHint: false,
|
|
166
|
+
idempotentHint: true,
|
|
167
|
+
openWorldHint: true
|
|
168
|
+
};
|
|
169
|
+
var WRITE_TOOL = {
|
|
170
|
+
readOnlyHint: false,
|
|
171
|
+
destructiveHint: false,
|
|
172
|
+
idempotentHint: false,
|
|
173
|
+
openWorldHint: false
|
|
174
|
+
};
|
|
175
|
+
var TOOLS = [
|
|
176
|
+
// 1. decantr_read_essence — local read
|
|
177
|
+
{
|
|
178
|
+
name: "decantr_read_essence",
|
|
179
|
+
title: "Read Essence",
|
|
180
|
+
description: "Read and return the current decantr.essence.json file from the working directory. For v3 files, optionally filter by layer (dna, blueprint, or full).",
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
path: { type: "string", description: "Optional path to essence file. Defaults to ./decantr.essence.json." },
|
|
185
|
+
layer: { type: "string", enum: ["dna", "blueprint", "full"], description: "For v3 essences: return only the specified layer. Defaults to full." }
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
annotations: READ_ONLY
|
|
189
|
+
},
|
|
190
|
+
// 2. decantr_validate — local read
|
|
191
|
+
{
|
|
192
|
+
name: "decantr_validate",
|
|
193
|
+
title: "Validate Essence",
|
|
194
|
+
description: "Validate a decantr.essence.json file against the schema and guard rules. For v3, reports DNA vs Blueprint violations separately.",
|
|
195
|
+
inputSchema: {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
path: { type: "string", description: "Path to essence file. Defaults to ./decantr.essence.json." }
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
annotations: READ_ONLY
|
|
202
|
+
},
|
|
203
|
+
// 3. decantr_search_registry — network
|
|
204
|
+
{
|
|
205
|
+
name: "decantr_search_registry",
|
|
206
|
+
title: "Search Registry",
|
|
207
|
+
description: "Search the Decantr community content registry for patterns, archetypes, recipes, and styles.",
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: {
|
|
211
|
+
query: { type: "string", description: 'Search query (e.g. "kanban", "neon", "dashboard")' },
|
|
212
|
+
type: { type: "string", description: "Filter by type: pattern, archetype, recipe, style" }
|
|
213
|
+
},
|
|
214
|
+
required: ["query"]
|
|
215
|
+
},
|
|
216
|
+
annotations: READ_ONLY_NETWORK
|
|
217
|
+
},
|
|
218
|
+
// 4. decantr_resolve_pattern — network
|
|
219
|
+
{
|
|
220
|
+
name: "decantr_resolve_pattern",
|
|
221
|
+
title: "Resolve Pattern",
|
|
222
|
+
description: "Get full pattern details including layout spec, components, presets, and code examples.",
|
|
223
|
+
inputSchema: {
|
|
224
|
+
type: "object",
|
|
225
|
+
properties: {
|
|
226
|
+
id: { type: "string", description: 'Pattern ID (e.g. "hero", "data-table", "kpi-grid")' },
|
|
227
|
+
preset: { type: "string", description: 'Optional preset name (e.g. "product", "content")' },
|
|
228
|
+
namespace: { type: "string", description: 'Namespace (default: "@official")' }
|
|
229
|
+
},
|
|
230
|
+
required: ["id"]
|
|
231
|
+
},
|
|
232
|
+
annotations: READ_ONLY_NETWORK
|
|
233
|
+
},
|
|
234
|
+
// 5. decantr_resolve_archetype — network
|
|
235
|
+
{
|
|
236
|
+
name: "decantr_resolve_archetype",
|
|
237
|
+
title: "Resolve Archetype",
|
|
238
|
+
description: "Get archetype details including default pages, layouts, features, and suggested theme.",
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: "object",
|
|
241
|
+
properties: {
|
|
242
|
+
id: { type: "string", description: 'Archetype ID (e.g. "saas-dashboard", "ecommerce")' },
|
|
243
|
+
namespace: { type: "string", description: 'Namespace (default: "@official")' }
|
|
244
|
+
},
|
|
245
|
+
required: ["id"]
|
|
246
|
+
},
|
|
247
|
+
annotations: READ_ONLY_NETWORK
|
|
248
|
+
},
|
|
249
|
+
// 6. decantr_resolve_recipe — network
|
|
250
|
+
{
|
|
251
|
+
name: "decantr_resolve_recipe",
|
|
252
|
+
title: "Resolve Recipe",
|
|
253
|
+
description: "Get recipe decoration rules including shell styles, spatial hints, visual effects, and pattern preferences.",
|
|
254
|
+
inputSchema: {
|
|
255
|
+
type: "object",
|
|
256
|
+
properties: {
|
|
257
|
+
id: { type: "string", description: 'Recipe ID (e.g. "auradecantism")' },
|
|
258
|
+
namespace: { type: "string", description: 'Namespace (default: "@official")' }
|
|
259
|
+
},
|
|
260
|
+
required: ["id"]
|
|
261
|
+
},
|
|
262
|
+
annotations: READ_ONLY_NETWORK
|
|
263
|
+
},
|
|
264
|
+
// 7. decantr_resolve_blueprint — network
|
|
265
|
+
{
|
|
266
|
+
name: "decantr_resolve_blueprint",
|
|
267
|
+
title: "Resolve Blueprint",
|
|
268
|
+
description: "Get a blueprint (app composition) with its archetype list, suggested theme, personality traits, and full page structure.",
|
|
269
|
+
inputSchema: {
|
|
270
|
+
type: "object",
|
|
271
|
+
properties: {
|
|
272
|
+
id: { type: "string", description: 'Blueprint ID (e.g. "saas-dashboard", "ecommerce", "portfolio")' },
|
|
273
|
+
namespace: { type: "string", description: 'Namespace (default: "@official")' }
|
|
274
|
+
},
|
|
275
|
+
required: ["id"]
|
|
276
|
+
},
|
|
277
|
+
annotations: READ_ONLY_NETWORK
|
|
278
|
+
},
|
|
279
|
+
// 8. decantr_suggest_patterns — network
|
|
280
|
+
{
|
|
281
|
+
name: "decantr_suggest_patterns",
|
|
282
|
+
title: "Suggest Patterns",
|
|
283
|
+
description: "Given a page description, suggest appropriate patterns from the registry. Returns ranked pattern matches with layout specs and component lists.",
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: "object",
|
|
286
|
+
properties: {
|
|
287
|
+
description: { type: "string", description: 'Description of the page or section (e.g. "dashboard with metrics and charts", "settings form with toggles")' }
|
|
288
|
+
},
|
|
289
|
+
required: ["description"]
|
|
290
|
+
},
|
|
291
|
+
annotations: READ_ONLY_NETWORK
|
|
292
|
+
},
|
|
293
|
+
// 9. decantr_check_drift — local read
|
|
294
|
+
{
|
|
295
|
+
name: "decantr_check_drift",
|
|
296
|
+
title: "Check Drift",
|
|
297
|
+
description: "Check if code changes violate the design intent captured in the Essence spec. For v3, returns separate dna_violations and blueprint_drift with autoFixable flags.",
|
|
298
|
+
inputSchema: {
|
|
299
|
+
type: "object",
|
|
300
|
+
properties: {
|
|
301
|
+
path: { type: "string", description: "Path to essence file. Defaults to ./decantr.essence.json." },
|
|
302
|
+
page_id: { type: "string", description: 'Page ID being modified (e.g. "overview", "settings")' },
|
|
303
|
+
components_used: {
|
|
304
|
+
type: "array",
|
|
305
|
+
items: { type: "string" },
|
|
306
|
+
description: "List of component names used in the generated code. Checked against page layout patterns."
|
|
307
|
+
},
|
|
308
|
+
theme_used: { type: "string", description: "Theme/style name used in the generated code" }
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
annotations: READ_ONLY
|
|
312
|
+
},
|
|
313
|
+
// 10. decantr_create_essence — network (fetches archetype)
|
|
314
|
+
{
|
|
315
|
+
name: "decantr_create_essence",
|
|
316
|
+
title: "Create Essence",
|
|
317
|
+
description: "Generate a valid v3 Essence spec skeleton from a project description. Returns a structured essence.json template based on the closest matching archetype and blueprint.",
|
|
318
|
+
inputSchema: {
|
|
319
|
+
type: "object",
|
|
320
|
+
properties: {
|
|
321
|
+
description: { type: "string", description: 'Natural language project description (e.g. "SaaS dashboard with analytics, user management, and billing")' },
|
|
322
|
+
framework: { type: "string", description: 'Target framework (e.g. "react", "vue", "svelte"). Defaults to "react".' }
|
|
323
|
+
},
|
|
324
|
+
required: ["description"]
|
|
325
|
+
},
|
|
326
|
+
annotations: READ_ONLY_NETWORK
|
|
327
|
+
},
|
|
328
|
+
// 11. decantr_accept_drift — WRITE tool (NEW)
|
|
329
|
+
{
|
|
330
|
+
name: "decantr_accept_drift",
|
|
331
|
+
title: "Accept Drift",
|
|
332
|
+
description: "Resolve guard violations by accepting, scoping, rejecting, or deferring drift. For DNA violations, requires explicit confirmation. Updates the essence file or drift log.",
|
|
333
|
+
inputSchema: {
|
|
334
|
+
type: "object",
|
|
335
|
+
properties: {
|
|
336
|
+
violations: {
|
|
337
|
+
type: "array",
|
|
338
|
+
items: {
|
|
339
|
+
type: "object",
|
|
340
|
+
properties: {
|
|
341
|
+
rule: { type: "string" },
|
|
342
|
+
page_id: { type: "string" },
|
|
343
|
+
details: { type: "string" }
|
|
344
|
+
},
|
|
345
|
+
required: ["rule"]
|
|
346
|
+
},
|
|
347
|
+
description: "The violations to resolve."
|
|
348
|
+
},
|
|
349
|
+
resolution: {
|
|
350
|
+
type: "string",
|
|
351
|
+
enum: ["accept", "accept_scoped", "reject", "defer"],
|
|
352
|
+
description: "How to resolve: accept updates the essence, accept_scoped limits to a page, reject is a no-op, defer logs for later."
|
|
353
|
+
},
|
|
354
|
+
scope: { type: "string", description: "For accept_scoped: the page or section scope." },
|
|
355
|
+
path: { type: "string", description: "Path to essence file. Defaults to ./decantr.essence.json." },
|
|
356
|
+
confirm_dna: { type: "boolean", description: "Required to be true when accepting DNA-layer violations." }
|
|
357
|
+
},
|
|
358
|
+
required: ["violations", "resolution"]
|
|
359
|
+
},
|
|
360
|
+
annotations: WRITE_TOOL
|
|
361
|
+
},
|
|
362
|
+
// 12. decantr_update_essence — WRITE tool (NEW)
|
|
363
|
+
{
|
|
364
|
+
name: "decantr_update_essence",
|
|
365
|
+
title: "Update Essence",
|
|
366
|
+
description: "Mutate the essence file: add/remove/update pages, update DNA or blueprint fields, add/remove features. Operates on v3 format (auto-migrates v2).",
|
|
367
|
+
inputSchema: {
|
|
368
|
+
type: "object",
|
|
369
|
+
properties: {
|
|
370
|
+
operation: {
|
|
371
|
+
type: "string",
|
|
372
|
+
enum: ["add_page", "remove_page", "update_page_layout", "update_dna", "update_blueprint", "add_feature", "remove_feature"],
|
|
373
|
+
description: "The mutation operation to perform."
|
|
374
|
+
},
|
|
375
|
+
payload: {
|
|
376
|
+
type: "object",
|
|
377
|
+
description: "Operation-specific payload. See tool docs for each operation."
|
|
378
|
+
},
|
|
379
|
+
path: { type: "string", description: "Path to essence file. Defaults to ./decantr.essence.json." }
|
|
380
|
+
},
|
|
381
|
+
required: ["operation", "payload"]
|
|
382
|
+
},
|
|
383
|
+
annotations: WRITE_TOOL
|
|
384
|
+
}
|
|
385
|
+
];
|
|
386
|
+
async function handleTool(name, args) {
|
|
387
|
+
const apiClient = getAPIClient();
|
|
388
|
+
switch (name) {
|
|
389
|
+
case "decantr_read_essence": {
|
|
390
|
+
const essencePath = args.path || join2(process.cwd(), "decantr.essence.json");
|
|
391
|
+
try {
|
|
392
|
+
const raw = await readFile2(essencePath, "utf-8");
|
|
393
|
+
const essence = JSON.parse(raw);
|
|
394
|
+
const layer = args.layer;
|
|
395
|
+
if (layer && isV32(essence)) {
|
|
396
|
+
if (layer === "dna") return essence.dna;
|
|
397
|
+
if (layer === "blueprint") return essence.blueprint;
|
|
398
|
+
}
|
|
399
|
+
return essence;
|
|
400
|
+
} catch (e) {
|
|
401
|
+
return { error: `Could not read essence file: ${e.message}` };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
case "decantr_validate": {
|
|
405
|
+
const essencePath = args.path || join2(process.cwd(), "decantr.essence.json");
|
|
406
|
+
let essence;
|
|
407
|
+
try {
|
|
408
|
+
essence = JSON.parse(await readFile2(essencePath, "utf-8"));
|
|
409
|
+
} catch (e) {
|
|
410
|
+
return { valid: false, errors: [`Could not read: ${e.message}`], guardViolations: [] };
|
|
411
|
+
}
|
|
412
|
+
const result = validateEssence(essence);
|
|
413
|
+
let guardViolations = [];
|
|
414
|
+
if (result.valid && typeof essence === "object" && essence !== null) {
|
|
415
|
+
try {
|
|
416
|
+
guardViolations = evaluateGuard(essence, {});
|
|
417
|
+
} catch {
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (result.valid && typeof essence === "object" && essence !== null && isV32(essence)) {
|
|
421
|
+
const dnaViolations = guardViolations.filter((v) => v.layer === "dna");
|
|
422
|
+
const blueprintViolations = guardViolations.filter((v) => v.layer === "blueprint");
|
|
423
|
+
const otherViolations = guardViolations.filter((v) => !v.layer);
|
|
424
|
+
return {
|
|
425
|
+
...result,
|
|
426
|
+
format: "v3",
|
|
427
|
+
dna_violations: dnaViolations,
|
|
428
|
+
blueprint_violations: blueprintViolations,
|
|
429
|
+
guardViolations: otherViolations
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
return { ...result, guardViolations };
|
|
433
|
+
}
|
|
434
|
+
case "decantr_search_registry": {
|
|
435
|
+
const err = validateStringArg(args, "query");
|
|
436
|
+
if (err) return { error: err };
|
|
437
|
+
try {
|
|
438
|
+
const response = await apiClient.search({
|
|
439
|
+
q: args.query,
|
|
440
|
+
type: args.type
|
|
441
|
+
});
|
|
442
|
+
return {
|
|
443
|
+
total: response.total,
|
|
444
|
+
results: response.results.map((r) => ({
|
|
445
|
+
type: r.type,
|
|
446
|
+
id: r.slug,
|
|
447
|
+
namespace: r.namespace,
|
|
448
|
+
name: r.name,
|
|
449
|
+
description: r.description,
|
|
450
|
+
install: `decantr get ${r.type} ${r.slug}`
|
|
451
|
+
}))
|
|
452
|
+
};
|
|
453
|
+
} catch (e) {
|
|
454
|
+
return { error: `Search failed: ${e.message}` };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
case "decantr_resolve_pattern": {
|
|
458
|
+
const err = validateStringArg(args, "id");
|
|
459
|
+
if (err) return { error: err };
|
|
460
|
+
const namespace = args.namespace || "@official";
|
|
461
|
+
try {
|
|
462
|
+
const pattern = await apiClient.getPattern(namespace, args.id);
|
|
463
|
+
const result = { found: true, ...pattern };
|
|
464
|
+
if (args.preset && typeof args.preset === "string") {
|
|
465
|
+
const preset = resolvePatternPreset(pattern, args.preset);
|
|
466
|
+
if (preset) result.resolvedPreset = preset;
|
|
467
|
+
}
|
|
468
|
+
return result;
|
|
469
|
+
} catch {
|
|
470
|
+
return { found: false, message: `Pattern "${args.id}" not found in ${namespace}.` };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
case "decantr_resolve_archetype": {
|
|
474
|
+
const err = validateStringArg(args, "id");
|
|
475
|
+
if (err) return { error: err };
|
|
476
|
+
const namespace = args.namespace || "@official";
|
|
477
|
+
try {
|
|
478
|
+
const archetype = await apiClient.getArchetype(namespace, args.id);
|
|
479
|
+
return { found: true, ...archetype };
|
|
480
|
+
} catch {
|
|
481
|
+
return { found: false, message: `Archetype "${args.id}" not found in ${namespace}.` };
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
case "decantr_resolve_recipe": {
|
|
485
|
+
const err = validateStringArg(args, "id");
|
|
486
|
+
if (err) return { error: err };
|
|
487
|
+
const namespace = args.namespace || "@official";
|
|
488
|
+
try {
|
|
489
|
+
const recipe = await apiClient.getRecipe(namespace, args.id);
|
|
490
|
+
return { found: true, ...recipe };
|
|
491
|
+
} catch {
|
|
492
|
+
return { found: false, message: `Recipe "${args.id}" not found in ${namespace}.` };
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
case "decantr_resolve_blueprint": {
|
|
496
|
+
const err = validateStringArg(args, "id");
|
|
497
|
+
if (err) return { error: err };
|
|
498
|
+
const namespace = args.namespace || "@official";
|
|
499
|
+
try {
|
|
500
|
+
const blueprint = await apiClient.getBlueprint(namespace, args.id);
|
|
501
|
+
let topology = null;
|
|
502
|
+
const composeEntries = blueprint.compose || blueprint.data?.compose;
|
|
503
|
+
if (composeEntries && Array.isArray(composeEntries) && composeEntries.length > 0) {
|
|
504
|
+
const zoneInputs = [];
|
|
505
|
+
const archetypePromises = composeEntries.map(async (entry) => {
|
|
506
|
+
const arcId = typeof entry === "string" ? entry : entry.archetype;
|
|
507
|
+
try {
|
|
508
|
+
const arch = await apiClient.getContent("archetypes", namespace, arcId);
|
|
509
|
+
const archData = arch.data || arch;
|
|
510
|
+
const explicitRole = typeof entry === "object" ? entry.role : void 0;
|
|
511
|
+
zoneInputs.push({
|
|
512
|
+
archetypeId: arcId,
|
|
513
|
+
role: explicitRole || archData.role || "auxiliary",
|
|
514
|
+
shell: archData.pages?.[0]?.shell || "sidebar-main",
|
|
515
|
+
features: archData.features || [],
|
|
516
|
+
description: archData.description || ""
|
|
517
|
+
});
|
|
518
|
+
} catch {
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
await Promise.all(archetypePromises);
|
|
522
|
+
if (zoneInputs.length > 0) {
|
|
523
|
+
const zones = deriveZones(zoneInputs);
|
|
524
|
+
const transitions = deriveTransitions(zones);
|
|
525
|
+
const primaryArchetype = zoneInputs.find((z) => z.role === "primary");
|
|
526
|
+
topology = {
|
|
527
|
+
zones: zones.map((z) => ({
|
|
528
|
+
role: z.role,
|
|
529
|
+
archetypes: z.archetypes,
|
|
530
|
+
shell: z.shell,
|
|
531
|
+
features: z.features,
|
|
532
|
+
purpose: z.descriptions.join(" ")
|
|
533
|
+
})),
|
|
534
|
+
transitions,
|
|
535
|
+
entryPoints: {
|
|
536
|
+
anonymous: "/",
|
|
537
|
+
authenticated: primaryArchetype ? `/${primaryArchetype.archetypeId}` : "/home"
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return { found: true, ...blueprint, ...topology ? { topology } : {} };
|
|
543
|
+
} catch {
|
|
544
|
+
return { found: false, message: `Blueprint "${args.id}" not found in ${namespace}.` };
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
case "decantr_suggest_patterns": {
|
|
548
|
+
const err = validateStringArg(args, "description");
|
|
549
|
+
if (err) return { error: err };
|
|
550
|
+
const desc = args.description.toLowerCase();
|
|
551
|
+
try {
|
|
552
|
+
const patternsResponse = await apiClient.listContent("patterns", {
|
|
553
|
+
namespace: "@official",
|
|
554
|
+
limit: 100
|
|
555
|
+
});
|
|
556
|
+
const suggestions = [];
|
|
557
|
+
for (const p of patternsResponse.items) {
|
|
558
|
+
const searchable = [
|
|
559
|
+
p.name || "",
|
|
560
|
+
p.description || "",
|
|
561
|
+
...p.components || [],
|
|
562
|
+
...p.tags || []
|
|
563
|
+
].join(" ").toLowerCase();
|
|
564
|
+
let score = 0;
|
|
565
|
+
const words = desc.split(/\s+/);
|
|
566
|
+
for (const word of words) {
|
|
567
|
+
if (word.length < 3) continue;
|
|
568
|
+
if (searchable.includes(word)) score += 10;
|
|
569
|
+
}
|
|
570
|
+
if (desc.includes("dashboard") && ["kpi-grid", "chart-grid", "data-table", "filter-bar"].includes(p.id)) score += 20;
|
|
571
|
+
if (desc.includes("metric") && p.id === "kpi-grid") score += 15;
|
|
572
|
+
if (desc.includes("chart") && p.id === "chart-grid") score += 15;
|
|
573
|
+
if (desc.includes("table") && p.id === "data-table") score += 15;
|
|
574
|
+
if (desc.includes("form") && p.id === "form-sections") score += 15;
|
|
575
|
+
if (desc.includes("setting") && p.id === "form-sections") score += 15;
|
|
576
|
+
if (desc.includes("landing") && ["hero", "cta-section", "card-grid"].includes(p.id)) score += 20;
|
|
577
|
+
if (desc.includes("hero") && p.id === "hero") score += 20;
|
|
578
|
+
if (desc.includes("ecommerce") && ["card-grid", "filter-bar", "detail-header"].includes(p.id)) score += 15;
|
|
579
|
+
if (desc.includes("product") && p.id === "card-grid") score += 15;
|
|
580
|
+
if (desc.includes("feed") && p.id === "activity-feed") score += 15;
|
|
581
|
+
if (desc.includes("filter") && p.id === "filter-bar") score += 15;
|
|
582
|
+
if (desc.includes("search") && p.id === "filter-bar") score += 10;
|
|
583
|
+
if (score > 0) {
|
|
584
|
+
const preset = p.presets ? Object.values(p.presets)[0] : null;
|
|
585
|
+
suggestions.push({
|
|
586
|
+
id: p.id,
|
|
587
|
+
score,
|
|
588
|
+
name: p.name || p.id,
|
|
589
|
+
description: p.description || "",
|
|
590
|
+
components: p.components || [],
|
|
591
|
+
layout: preset?.layout ? preset.layout.layout : "grid"
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
suggestions.sort((a, b) => b.score - a.score);
|
|
596
|
+
return {
|
|
597
|
+
query: args.description,
|
|
598
|
+
suggestions: suggestions.slice(0, 5),
|
|
599
|
+
total: suggestions.length
|
|
600
|
+
};
|
|
601
|
+
} catch (e) {
|
|
602
|
+
return { error: `Could not fetch patterns: ${e.message}` };
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
case "decantr_check_drift": {
|
|
606
|
+
const essencePath = args.path || join2(process.cwd(), "decantr.essence.json");
|
|
607
|
+
let essence;
|
|
608
|
+
try {
|
|
609
|
+
essence = JSON.parse(await readFile2(essencePath, "utf-8"));
|
|
610
|
+
} catch (e) {
|
|
611
|
+
return { error: `Could not read essence: ${e.message}` };
|
|
612
|
+
}
|
|
613
|
+
const validation = validateEssence(essence);
|
|
614
|
+
if (!validation.valid) {
|
|
615
|
+
return { drifted: true, reason: "invalid_essence", errors: validation.errors };
|
|
616
|
+
}
|
|
617
|
+
const violations = [];
|
|
618
|
+
if (args.theme_used && typeof args.theme_used === "string") {
|
|
619
|
+
let expectedStyle;
|
|
620
|
+
if (isV32(essence)) {
|
|
621
|
+
expectedStyle = essence.dna.theme.style;
|
|
622
|
+
} else {
|
|
623
|
+
const expectedTheme = essence.theme;
|
|
624
|
+
expectedStyle = expectedTheme?.style;
|
|
625
|
+
}
|
|
626
|
+
if (expectedStyle && args.theme_used !== expectedStyle) {
|
|
627
|
+
violations.push({
|
|
628
|
+
rule: "theme-match",
|
|
629
|
+
severity: "critical",
|
|
630
|
+
message: `Theme drift: code uses "${args.theme_used}" but Essence specifies "${expectedStyle}". Do not switch themes.`,
|
|
631
|
+
...isV32(essence) ? { layer: "dna", autoFixable: false } : {}
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (args.page_id && typeof args.page_id === "string") {
|
|
636
|
+
let pages;
|
|
637
|
+
if (isV32(essence)) {
|
|
638
|
+
pages = essence.blueprint.pages;
|
|
639
|
+
} else {
|
|
640
|
+
pages = essence.structure || [];
|
|
641
|
+
}
|
|
642
|
+
if (!pages.find((p) => p.id === args.page_id)) {
|
|
643
|
+
violations.push({
|
|
644
|
+
rule: "page-exists",
|
|
645
|
+
severity: "critical",
|
|
646
|
+
message: `Page "${args.page_id}" not found in Essence structure. Add it to the Essence before generating code for it.`,
|
|
647
|
+
...isV32(essence) ? {
|
|
648
|
+
layer: "blueprint",
|
|
649
|
+
autoFixable: true,
|
|
650
|
+
autoFix: { type: "add_page", patch: { id: args.page_id } }
|
|
651
|
+
} : {}
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (args.components_used && Array.isArray(args.components_used) && args.page_id && typeof args.page_id === "string") {
|
|
656
|
+
let pages;
|
|
657
|
+
if (isV32(essence)) {
|
|
658
|
+
pages = essence.blueprint.pages;
|
|
659
|
+
} else {
|
|
660
|
+
pages = essence.structure || [];
|
|
661
|
+
}
|
|
662
|
+
const page = pages.find((p) => p.id === args.page_id);
|
|
663
|
+
if (page && page.layout) {
|
|
664
|
+
const expectedPatterns = /* @__PURE__ */ new Set();
|
|
665
|
+
for (const item of page.layout) {
|
|
666
|
+
if (typeof item === "string") {
|
|
667
|
+
expectedPatterns.add(item);
|
|
668
|
+
} else if (typeof item === "object" && item !== null && "pattern" in item) {
|
|
669
|
+
expectedPatterns.add(item.pattern);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const componentsUsed = args.components_used;
|
|
673
|
+
const unmatchedComponents = [];
|
|
674
|
+
for (const comp of componentsUsed) {
|
|
675
|
+
const compLower = comp.toLowerCase().replace(/[_\s]/g, "-");
|
|
676
|
+
let matched = false;
|
|
677
|
+
for (const pattern of expectedPatterns) {
|
|
678
|
+
const patternLower = pattern.toLowerCase();
|
|
679
|
+
if (compLower.includes(patternLower) || patternLower.includes(compLower) || fuzzyScore(compLower, patternLower) >= 60) {
|
|
680
|
+
matched = true;
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (!matched) {
|
|
685
|
+
unmatchedComponents.push(comp);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (unmatchedComponents.length > 0) {
|
|
689
|
+
violations.push({
|
|
690
|
+
rule: "component-pattern-match",
|
|
691
|
+
severity: "warning",
|
|
692
|
+
message: `Components [${unmatchedComponents.join(", ")}] do not match any pattern in page "${args.page_id}" layout. Expected patterns: [${[...expectedPatterns].join(", ")}].`,
|
|
693
|
+
...isV32(essence) ? { layer: "blueprint", autoFixable: false } : {}
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
try {
|
|
699
|
+
const guardViolations = evaluateGuard(essence, {
|
|
700
|
+
pageId: args.page_id
|
|
701
|
+
});
|
|
702
|
+
for (const gv of guardViolations) {
|
|
703
|
+
violations.push({
|
|
704
|
+
rule: gv.rule || "guard",
|
|
705
|
+
severity: gv.severity || "warning",
|
|
706
|
+
message: gv.message || "Guard violation",
|
|
707
|
+
...gv.layer ? { layer: gv.layer } : {},
|
|
708
|
+
...gv.autoFixable !== void 0 ? { autoFixable: gv.autoFixable } : {},
|
|
709
|
+
...gv.autoFix ? { autoFix: gv.autoFix } : {}
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
714
|
+
if (isV32(essence)) {
|
|
715
|
+
const dnaViolations = violations.filter((v) => v.layer === "dna");
|
|
716
|
+
const blueprintDrift = violations.filter((v) => v.layer === "blueprint");
|
|
717
|
+
const other = violations.filter((v) => !v.layer);
|
|
718
|
+
return {
|
|
719
|
+
drifted: violations.length > 0,
|
|
720
|
+
dna_violations: dnaViolations,
|
|
721
|
+
blueprint_drift: blueprintDrift,
|
|
722
|
+
other_violations: other,
|
|
723
|
+
checkedAgainst: essencePath
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
drifted: violations.length > 0,
|
|
728
|
+
violations,
|
|
729
|
+
checkedAgainst: essencePath
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
case "decantr_create_essence": {
|
|
733
|
+
const err = validateStringArg(args, "description");
|
|
734
|
+
if (err) return { error: err };
|
|
735
|
+
const desc = args.description.toLowerCase();
|
|
736
|
+
const framework = args.framework || "react";
|
|
737
|
+
const archetypeScores = [];
|
|
738
|
+
const archetypeIds = [
|
|
739
|
+
"saas-dashboard",
|
|
740
|
+
"ecommerce",
|
|
741
|
+
"portfolio",
|
|
742
|
+
"content-site",
|
|
743
|
+
"financial-dashboard",
|
|
744
|
+
"cloud-platform",
|
|
745
|
+
"gaming-platform",
|
|
746
|
+
"ecommerce-admin",
|
|
747
|
+
"workbench"
|
|
748
|
+
];
|
|
749
|
+
for (const id of archetypeIds) {
|
|
750
|
+
let score = 0;
|
|
751
|
+
if (desc.includes("dashboard") && id.includes("dashboard")) score += 20;
|
|
752
|
+
if (desc.includes("saas") && id.includes("saas")) score += 20;
|
|
753
|
+
if (desc.includes("ecommerce") && id.includes("ecommerce")) score += 20;
|
|
754
|
+
if (desc.includes("shop") && id.includes("ecommerce")) score += 15;
|
|
755
|
+
if (desc.includes("portfolio") && id.includes("portfolio")) score += 20;
|
|
756
|
+
if (desc.includes("blog") && id.includes("content")) score += 15;
|
|
757
|
+
if (desc.includes("content") && id.includes("content")) score += 15;
|
|
758
|
+
if (desc.includes("finance") && id.includes("financial")) score += 20;
|
|
759
|
+
if (desc.includes("cloud") && id.includes("cloud")) score += 15;
|
|
760
|
+
if (desc.includes("game") && id.includes("gaming")) score += 15;
|
|
761
|
+
if (desc.includes("admin") && id.includes("admin")) score += 15;
|
|
762
|
+
if (desc.includes("analytics") && id.includes("dashboard")) score += 10;
|
|
763
|
+
if (desc.includes("tool") && id === "workbench") score += 10;
|
|
764
|
+
if (score > 0) archetypeScores.push({ id, score });
|
|
765
|
+
}
|
|
766
|
+
archetypeScores.sort((a, b) => b.score - a.score);
|
|
767
|
+
const bestMatch = archetypeScores[0]?.id || "saas-dashboard";
|
|
768
|
+
let pages;
|
|
769
|
+
let features = [];
|
|
770
|
+
try {
|
|
771
|
+
const archetype = await apiClient.getArchetype("@official", bestMatch);
|
|
772
|
+
pages = archetype.pages;
|
|
773
|
+
features = archetype.features || [];
|
|
774
|
+
} catch {
|
|
775
|
+
}
|
|
776
|
+
const rawPages = pages || [{ id: "home", shell: "full-bleed", default_layout: ["hero"] }];
|
|
777
|
+
const defaultShell = rawPages[0]?.shell || "sidebar-main";
|
|
778
|
+
const essence = {
|
|
779
|
+
version: "3.0.0",
|
|
780
|
+
dna: {
|
|
781
|
+
theme: {
|
|
782
|
+
style: "auradecantism",
|
|
783
|
+
mode: "dark",
|
|
784
|
+
recipe: "auradecantism",
|
|
785
|
+
shape: "rounded"
|
|
786
|
+
},
|
|
787
|
+
spacing: {
|
|
788
|
+
base_unit: 4,
|
|
789
|
+
scale: "linear",
|
|
790
|
+
density: "comfortable",
|
|
791
|
+
content_gap: "4"
|
|
792
|
+
},
|
|
793
|
+
typography: {
|
|
794
|
+
scale: "modular",
|
|
795
|
+
heading_weight: 600,
|
|
796
|
+
body_weight: 400
|
|
797
|
+
},
|
|
798
|
+
color: {
|
|
799
|
+
palette: "semantic",
|
|
800
|
+
accent_count: 1,
|
|
801
|
+
cvd_preference: "auto"
|
|
802
|
+
},
|
|
803
|
+
radius: {
|
|
804
|
+
philosophy: "rounded",
|
|
805
|
+
base: 8
|
|
806
|
+
},
|
|
807
|
+
elevation: {
|
|
808
|
+
system: "layered",
|
|
809
|
+
max_levels: 3
|
|
810
|
+
},
|
|
811
|
+
motion: {
|
|
812
|
+
preference: "subtle",
|
|
813
|
+
duration_scale: 1,
|
|
814
|
+
reduce_motion: true
|
|
815
|
+
},
|
|
816
|
+
accessibility: {
|
|
817
|
+
wcag_level: "AA",
|
|
818
|
+
focus_visible: true,
|
|
819
|
+
skip_nav: true
|
|
820
|
+
},
|
|
821
|
+
personality: ["professional"]
|
|
822
|
+
},
|
|
823
|
+
blueprint: {
|
|
824
|
+
shell: defaultShell,
|
|
825
|
+
pages: rawPages.map((p) => ({
|
|
826
|
+
id: p.id,
|
|
827
|
+
...p.shell !== defaultShell ? { shell_override: p.shell } : {},
|
|
828
|
+
layout: p.default_layout || []
|
|
829
|
+
})),
|
|
830
|
+
features
|
|
831
|
+
},
|
|
832
|
+
meta: {
|
|
833
|
+
archetype: bestMatch,
|
|
834
|
+
target: framework,
|
|
835
|
+
platform: { type: "spa", routing: "hash" },
|
|
836
|
+
guard: { mode: "strict", dna_enforcement: "error", blueprint_enforcement: "warn" }
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
return {
|
|
840
|
+
essence,
|
|
841
|
+
archetype: bestMatch,
|
|
842
|
+
format: "v3",
|
|
843
|
+
instructions: `Save this as decantr.essence.json in your project root. Review the dna (design tokens), blueprint (pages/features), and meta (project config) sections and adjust to match your needs. The guard rules will validate your code against this spec.`,
|
|
844
|
+
_generated: {
|
|
845
|
+
matched_archetype: bestMatch,
|
|
846
|
+
confidence: archetypeScores[0]?.score || 0,
|
|
847
|
+
alternatives: archetypeScores.slice(1, 4).map((a) => a.id),
|
|
848
|
+
description: args.description
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
case "decantr_accept_drift": {
|
|
853
|
+
const violations = args.violations;
|
|
854
|
+
const resolution = args.resolution;
|
|
855
|
+
if (!violations || !Array.isArray(violations) || violations.length === 0) {
|
|
856
|
+
return { error: 'Required parameter "violations" must be a non-empty array.' };
|
|
857
|
+
}
|
|
858
|
+
if (!resolution || !["accept", "accept_scoped", "reject", "defer"].includes(resolution)) {
|
|
859
|
+
return { error: 'Required parameter "resolution" must be one of: accept, accept_scoped, reject, defer.' };
|
|
860
|
+
}
|
|
861
|
+
const hasDnaViolation = violations.some((v) => {
|
|
862
|
+
const rule = v.rule;
|
|
863
|
+
return ["style", "recipe", "density", "theme-mode", "accessibility", "theme-match"].includes(rule);
|
|
864
|
+
});
|
|
865
|
+
if (hasDnaViolation && resolution !== "reject" && resolution !== "defer" && !args.confirm_dna) {
|
|
866
|
+
return {
|
|
867
|
+
error: "DNA-layer violations detected. Set confirm_dna: true to accept changes to design axioms (theme, style, density, etc.).",
|
|
868
|
+
requires_confirmation: true,
|
|
869
|
+
dna_rules_affected: violations.filter(
|
|
870
|
+
(v) => ["style", "recipe", "density", "theme-mode", "accessibility", "theme-match"].includes(v.rule)
|
|
871
|
+
).map((v) => v.rule)
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
if (resolution === "reject") {
|
|
875
|
+
return {
|
|
876
|
+
status: "rejected",
|
|
877
|
+
message: "Violations rejected. No changes made. Revert the code to match the essence spec.",
|
|
878
|
+
violations_count: violations.length
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
if (resolution === "defer") {
|
|
882
|
+
const projectRoot = args.path ? dirname2(args.path) : void 0;
|
|
883
|
+
const existingLog = await readDriftLog(projectRoot);
|
|
884
|
+
const newEntries = violations.map((v) => ({
|
|
885
|
+
rule: v.rule,
|
|
886
|
+
page_id: v.page_id,
|
|
887
|
+
details: v.details,
|
|
888
|
+
resolution: "deferred",
|
|
889
|
+
scope: args.scope || void 0,
|
|
890
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
891
|
+
}));
|
|
892
|
+
const updatedLog = [...existingLog, ...newEntries];
|
|
893
|
+
const logPath = await writeDriftLog(updatedLog, projectRoot);
|
|
894
|
+
return {
|
|
895
|
+
status: "deferred",
|
|
896
|
+
message: `${violations.length} violation(s) deferred to drift log.`,
|
|
897
|
+
log_path: logPath,
|
|
898
|
+
total_deferred: updatedLog.length
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
try {
|
|
902
|
+
const { essence, path } = await mutateEssenceFile(args.path, (v3) => {
|
|
903
|
+
for (const v of violations) {
|
|
904
|
+
applyDriftAcceptance(v3, v, resolution, args.scope);
|
|
905
|
+
}
|
|
906
|
+
return v3;
|
|
907
|
+
});
|
|
908
|
+
return {
|
|
909
|
+
status: resolution === "accept_scoped" ? "accepted_scoped" : "accepted",
|
|
910
|
+
message: `${violations.length} violation(s) resolved. Essence updated.`,
|
|
911
|
+
path,
|
|
912
|
+
scope: resolution === "accept_scoped" ? args.scope || "unscoped" : void 0
|
|
913
|
+
};
|
|
914
|
+
} catch (e) {
|
|
915
|
+
return { error: `Failed to update essence: ${e.message}` };
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
case "decantr_update_essence": {
|
|
919
|
+
const operation = args.operation;
|
|
920
|
+
const payload = args.payload;
|
|
921
|
+
if (!operation) {
|
|
922
|
+
return { error: 'Required parameter "operation" is missing.' };
|
|
923
|
+
}
|
|
924
|
+
if (!payload || typeof payload !== "object") {
|
|
925
|
+
return { error: 'Required parameter "payload" must be an object.' };
|
|
926
|
+
}
|
|
927
|
+
const validOps = ["add_page", "remove_page", "update_page_layout", "update_dna", "update_blueprint", "add_feature", "remove_feature"];
|
|
928
|
+
if (!validOps.includes(operation)) {
|
|
929
|
+
return { error: `Invalid operation "${operation}". Must be one of: ${validOps.join(", ")}` };
|
|
930
|
+
}
|
|
931
|
+
try {
|
|
932
|
+
const { essence, path } = await mutateEssenceFile(args.path, (v3) => {
|
|
933
|
+
return applyEssenceUpdate(v3, operation, payload);
|
|
934
|
+
});
|
|
935
|
+
return {
|
|
936
|
+
status: "updated",
|
|
937
|
+
operation,
|
|
938
|
+
path,
|
|
939
|
+
summary: describeUpdate(operation, payload)
|
|
940
|
+
};
|
|
941
|
+
} catch (e) {
|
|
942
|
+
return { error: `Failed to update essence: ${e.message}` };
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
default:
|
|
946
|
+
return { error: `Unknown tool: ${name}` };
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function applyDriftAcceptance(essence, violation, resolution, scope) {
|
|
950
|
+
switch (violation.rule) {
|
|
951
|
+
case "theme-match":
|
|
952
|
+
case "style": {
|
|
953
|
+
if (violation.details) {
|
|
954
|
+
essence.dna.theme.style = violation.details;
|
|
955
|
+
}
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
case "page-exists":
|
|
959
|
+
case "structure": {
|
|
960
|
+
if (violation.page_id) {
|
|
961
|
+
const existing = essence.blueprint.pages.find((p) => p.id === violation.page_id);
|
|
962
|
+
if (!existing) {
|
|
963
|
+
essence.blueprint.pages.push({
|
|
964
|
+
id: violation.page_id,
|
|
965
|
+
layout: []
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
case "layout": {
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
case "recipe": {
|
|
975
|
+
if (violation.details) {
|
|
976
|
+
essence.dna.theme.recipe = violation.details;
|
|
977
|
+
}
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
case "density": {
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
default:
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function applyEssenceUpdate(essence, operation, payload) {
|
|
988
|
+
switch (operation) {
|
|
989
|
+
case "add_page": {
|
|
990
|
+
const id = payload.id;
|
|
991
|
+
if (!id) throw new Error('Payload must include "id" for add_page.');
|
|
992
|
+
const existing = essence.blueprint.pages.find((p) => p.id === id);
|
|
993
|
+
if (existing) throw new Error(`Page "${id}" already exists.`);
|
|
994
|
+
essence.blueprint.pages.push({
|
|
995
|
+
id,
|
|
996
|
+
layout: payload.layout || [],
|
|
997
|
+
...payload.shell_override ? { shell_override: payload.shell_override } : {},
|
|
998
|
+
...payload.surface ? { surface: payload.surface } : {}
|
|
999
|
+
});
|
|
1000
|
+
break;
|
|
1001
|
+
}
|
|
1002
|
+
case "remove_page": {
|
|
1003
|
+
const id = payload.id;
|
|
1004
|
+
if (!id) throw new Error('Payload must include "id" for remove_page.');
|
|
1005
|
+
const idx = essence.blueprint.pages.findIndex((p) => p.id === id);
|
|
1006
|
+
if (idx === -1) throw new Error(`Page "${id}" not found.`);
|
|
1007
|
+
essence.blueprint.pages.splice(idx, 1);
|
|
1008
|
+
break;
|
|
1009
|
+
}
|
|
1010
|
+
case "update_page_layout": {
|
|
1011
|
+
const id = payload.id;
|
|
1012
|
+
const layout = payload.layout;
|
|
1013
|
+
if (!id) throw new Error('Payload must include "id" for update_page_layout.');
|
|
1014
|
+
if (!layout || !Array.isArray(layout)) throw new Error('Payload must include "layout" array for update_page_layout.');
|
|
1015
|
+
const page = essence.blueprint.pages.find((p) => p.id === id);
|
|
1016
|
+
if (!page) throw new Error(`Page "${id}" not found.`);
|
|
1017
|
+
page.layout = layout;
|
|
1018
|
+
break;
|
|
1019
|
+
}
|
|
1020
|
+
case "update_dna": {
|
|
1021
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
1022
|
+
if (key in essence.dna && typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1023
|
+
essence.dna[key] = {
|
|
1024
|
+
...essence.dna[key],
|
|
1025
|
+
...value
|
|
1026
|
+
};
|
|
1027
|
+
} else {
|
|
1028
|
+
essence.dna[key] = value;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
1033
|
+
case "update_blueprint": {
|
|
1034
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
1035
|
+
if (key === "pages") continue;
|
|
1036
|
+
essence.blueprint[key] = value;
|
|
1037
|
+
}
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
case "add_feature": {
|
|
1041
|
+
const feature = payload.feature;
|
|
1042
|
+
if (!feature) throw new Error('Payload must include "feature" for add_feature.');
|
|
1043
|
+
if (!essence.blueprint.features.includes(feature)) {
|
|
1044
|
+
essence.blueprint.features.push(feature);
|
|
1045
|
+
}
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
case "remove_feature": {
|
|
1049
|
+
const feature = payload.feature;
|
|
1050
|
+
if (!feature) throw new Error('Payload must include "feature" for remove_feature.');
|
|
1051
|
+
const idx = essence.blueprint.features.indexOf(feature);
|
|
1052
|
+
if (idx === -1) throw new Error(`Feature "${feature}" not found.`);
|
|
1053
|
+
essence.blueprint.features.splice(idx, 1);
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return essence;
|
|
1058
|
+
}
|
|
1059
|
+
function describeUpdate(operation, payload) {
|
|
1060
|
+
switch (operation) {
|
|
1061
|
+
case "add_page":
|
|
1062
|
+
return `Added page "${payload.id}".`;
|
|
1063
|
+
case "remove_page":
|
|
1064
|
+
return `Removed page "${payload.id}".`;
|
|
1065
|
+
case "update_page_layout":
|
|
1066
|
+
return `Updated layout for page "${payload.id}".`;
|
|
1067
|
+
case "update_dna":
|
|
1068
|
+
return `Updated DNA: ${Object.keys(payload).join(", ")}.`;
|
|
1069
|
+
case "update_blueprint":
|
|
1070
|
+
return `Updated blueprint: ${Object.keys(payload).join(", ")}.`;
|
|
1071
|
+
case "add_feature":
|
|
1072
|
+
return `Added feature "${payload.feature}".`;
|
|
1073
|
+
case "remove_feature":
|
|
1074
|
+
return `Removed feature "${payload.feature}".`;
|
|
1075
|
+
default:
|
|
1076
|
+
return `Performed ${operation}.`;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/index.ts
|
|
1081
|
+
var VERSION = "0.2.0";
|
|
1082
|
+
var server = new Server(
|
|
1083
|
+
{ name: "decantr", version: VERSION },
|
|
1084
|
+
{ capabilities: { tools: {} } }
|
|
1085
|
+
);
|
|
1086
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1087
|
+
return { tools: TOOLS };
|
|
1088
|
+
});
|
|
1089
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1090
|
+
const { name, arguments: args } = request.params;
|
|
1091
|
+
try {
|
|
1092
|
+
const result = await handleTool(name, args ?? {});
|
|
1093
|
+
const response = {
|
|
1094
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1095
|
+
};
|
|
1096
|
+
if (result && typeof result === "object" && "error" in result) {
|
|
1097
|
+
response.isError = true;
|
|
1098
|
+
}
|
|
1099
|
+
return response;
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
return {
|
|
1102
|
+
content: [{ type: "text", text: JSON.stringify({ error: err.message }) }],
|
|
1103
|
+
isError: true
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
var transport = new StdioServerTransport();
|
|
1108
|
+
await server.connect(transport);
|