@decantr/cli 1.11.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,139 +1,8 @@
1
- // src/lib/scan-interactions.ts
2
- import { existsSync, readdirSync, readFileSync, statSync } from "fs";
3
- import { extname, join } from "path";
4
- import { verifyInteractionsInSource } from "@decantr/verifier";
5
- var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js", ".html", ".mdx"]);
6
- var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
7
- "node_modules",
8
- ".decantr",
9
- ".git",
10
- "dist",
11
- "build",
12
- ".next",
13
- ".turbo",
14
- "coverage",
15
- ".cache"
16
- ]);
17
- var MAX_FILE_SIZE = 1024 * 1024;
18
- function walkSourceTree(rootDir) {
19
- const sources = /* @__PURE__ */ new Map();
20
- function walk2(dir) {
21
- let entries;
22
- try {
23
- entries = readdirSync(dir);
24
- } catch {
25
- return;
26
- }
27
- for (const entry of entries) {
28
- if (SKIP_DIRECTORIES.has(entry)) continue;
29
- const fullPath = join(dir, entry);
30
- let s;
31
- try {
32
- s = statSync(fullPath);
33
- } catch {
34
- continue;
35
- }
36
- if (s.isDirectory()) {
37
- walk2(fullPath);
38
- } else if (s.isFile() && SCAN_EXTENSIONS.has(extname(entry))) {
39
- if (s.size > MAX_FILE_SIZE) continue;
40
- try {
41
- sources.set(fullPath, readFileSync(fullPath, "utf8"));
42
- } catch {
43
- }
44
- }
45
- }
46
- }
47
- walk2(rootDir);
48
- return sources;
49
- }
50
- function collectDeclaredInteractions(projectRoot) {
51
- const manifestPath = join(projectRoot, ".decantr", "context", "pack-manifest.json");
52
- if (!existsSync(manifestPath)) return [];
53
- let manifest;
54
- try {
55
- manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
56
- } catch {
57
- return [];
58
- }
59
- const all = [];
60
- const pages = manifest.pages ?? [];
61
- const contextDir = join(projectRoot, ".decantr", "context");
62
- for (const page of pages) {
63
- const packPath = join(contextDir, page.json);
64
- if (!existsSync(packPath)) continue;
65
- let pack;
66
- try {
67
- pack = JSON.parse(readFileSync(packPath, "utf8"));
68
- } catch {
69
- continue;
70
- }
71
- const patterns = pack.data?.patterns ?? [];
72
- for (const pat of patterns) {
73
- if (Array.isArray(pat.interactions)) {
74
- all.push(...pat.interactions);
75
- }
76
- }
77
- }
78
- return all;
79
- }
80
- function scanProjectInteractions(projectRoot) {
81
- const declared = collectDeclaredInteractions(projectRoot);
82
- if (declared.length === 0) return [];
83
- const sources = walkSourceTree(projectRoot);
84
- if (sources.size === 0) return [];
85
- const missing = verifyInteractionsInSource(declared, sources);
86
- return missing.map(({ interaction, suggestion }) => `${interaction} \u2192 ${suggestion}`);
87
- }
88
-
89
- // src/guard-context.ts
90
- import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
91
- import { join as join2 } from "path";
92
- function loadJsonEntries(dir) {
93
- if (!existsSync2(dir)) return [];
94
- try {
95
- return readdirSync2(dir).filter((file) => file.endsWith(".json") && file !== "index.json").map((file) => JSON.parse(readFileSync2(join2(dir, file), "utf-8")));
96
- } catch {
97
- return [];
98
- }
99
- }
100
- function buildGuardRegistryContext(projectRoot = process.cwd()) {
101
- const themeRegistry = /* @__PURE__ */ new Map();
102
- const patternRegistry = /* @__PURE__ */ new Map();
103
- const cacheDir = join2(projectRoot, ".decantr", "cache");
104
- const customDir = join2(projectRoot, ".decantr", "custom");
105
- for (const data of loadJsonEntries(join2(cacheDir, "@official", "themes"))) {
106
- if (typeof data.id === "string" && !themeRegistry.has(data.id)) {
107
- themeRegistry.set(data.id, {
108
- modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
109
- });
110
- }
111
- }
112
- for (const data of loadJsonEntries(join2(customDir, "themes"))) {
113
- if (typeof data.id === "string") {
114
- themeRegistry.set(`custom:${data.id}`, {
115
- modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
116
- });
117
- }
118
- }
119
- for (const data of loadJsonEntries(join2(cacheDir, "@official", "patterns"))) {
120
- if (typeof data.id === "string" && !patternRegistry.has(data.id)) {
121
- patternRegistry.set(data.id, data);
122
- }
123
- }
124
- for (const data of loadJsonEntries(join2(customDir, "patterns"))) {
125
- if (typeof data.id === "string") {
126
- patternRegistry.set(data.id, data);
127
- }
128
- }
129
- return { themeRegistry, patternRegistry };
130
- }
131
-
132
1
  // src/telemetry.ts
133
2
  import { randomUUID } from "crypto";
134
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
135
4
  import { homedir } from "os";
136
- import { dirname, join as join3 } from "path";
5
+ import { dirname, join, resolve } from "path";
137
6
  import { fileURLToPath } from "url";
138
7
  import {
139
8
  createFetchTelemetrySink,
@@ -159,21 +28,21 @@ async function sendGuardMetrics(metrics) {
159
28
  }
160
29
  }
161
30
  function isOptedIn(projectRoot) {
162
- const projectJsonPath = join3(projectRoot, ".decantr", "project.json");
163
- if (!existsSync3(projectJsonPath)) return false;
31
+ const projectJsonPath = join(projectRoot, ".decantr", "project.json");
32
+ if (!existsSync(projectJsonPath)) return false;
164
33
  try {
165
- const data = JSON.parse(readFileSync3(projectJsonPath, "utf-8"));
34
+ const data = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
166
35
  return data.telemetry === true;
167
36
  } catch {
168
37
  return false;
169
38
  }
170
39
  }
171
40
  function optIn(projectRoot) {
172
- const projectJsonPath = join3(projectRoot, ".decantr", "project.json");
41
+ const projectJsonPath = join(projectRoot, ".decantr", "project.json");
173
42
  let data = {};
174
- if (existsSync3(projectJsonPath)) {
43
+ if (existsSync(projectJsonPath)) {
175
44
  try {
176
- data = JSON.parse(readFileSync3(projectJsonPath, "utf-8"));
45
+ data = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
177
46
  } catch {
178
47
  }
179
48
  }
@@ -181,16 +50,19 @@ function optIn(projectRoot) {
181
50
  mkdirSync(dirname(projectJsonPath), { recursive: true });
182
51
  writeFileSync(projectJsonPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
183
52
  }
184
- async function sendCliCommandTelemetry(input) {
185
- const projectRoot = input.projectRoot ?? process.cwd();
186
- const command = normalizeCommand(input.args[0]);
187
- if (!isOptedIn(projectRoot) || !command || command === "help" || command === "version") {
53
+ async function captureCliTelemetryEvent(input) {
54
+ const projectRoot = resolveCliTelemetryProjectRoot(
55
+ input.projectRoot ?? process.cwd(),
56
+ input.args ?? []
57
+ );
58
+ if (!isOptedIn(projectRoot)) {
188
59
  return;
189
60
  }
190
61
  const identities = ensureTelemetryIdentities(projectRoot);
191
62
  if (!identities) {
192
63
  return;
193
64
  }
65
+ const registrySource = input.registrySource ?? getRegistrySourceProperty(input.properties) ?? inferRegistrySource(input.args ?? []);
194
66
  const client = createTelemetryClient({
195
67
  sink: createFetchTelemetrySink({
196
68
  endpoint: getTelemetryEventsEndpoint(),
@@ -198,7 +70,7 @@ async function sendCliCommandTelemetry(input) {
198
70
  })
199
71
  });
200
72
  const event = {
201
- name: "cli.command.completed",
73
+ name: input.name,
202
74
  context: {
203
75
  source: "cli",
204
76
  actorType: getTelemetryActorType(),
@@ -206,26 +78,196 @@ async function sendCliCommandTelemetry(input) {
206
78
  decantrVersion: getCliVersion(),
207
79
  installId: identities.installId,
208
80
  projectId: identities.projectId,
209
- registrySource: inferRegistrySource(input.args)
81
+ registrySource
210
82
  },
211
- properties: {
212
- command,
213
- success: input.success,
214
- durationMs: input.durationMs,
215
- adoptionMode: inferAdoptionMode(input.args),
216
- errorCode: input.success ? void 0 : "cli_command_failed",
217
- offline: input.args.includes("--offline"),
218
- projectScope: inferProjectScope(projectRoot),
219
- registrySource: inferRegistrySource(input.args),
220
- targetFramework: inferFlagValue(input.args, "--target"),
221
- workflowMode: inferWorkflowMode(input.args)
222
- }
83
+ properties: input.properties
223
84
  };
224
85
  try {
225
86
  await client.capture(event);
226
87
  } catch {
227
88
  }
228
89
  }
90
+ async function sendCliCommandTelemetry(input) {
91
+ const projectRoot = resolveCliTelemetryProjectRoot(input.projectRoot ?? process.cwd(), input.args);
92
+ const command = normalizeCommand(input.args[0]);
93
+ if (!isOptedIn(projectRoot) || !command || command === "help" || command === "version") {
94
+ return;
95
+ }
96
+ const properties = buildCliLifecycleProperties({
97
+ args: input.args,
98
+ command,
99
+ durationMs: input.durationMs,
100
+ projectRoot,
101
+ success: input.success
102
+ });
103
+ await captureCliTelemetryEvent({
104
+ args: input.args,
105
+ name: "cli.command.completed",
106
+ projectRoot,
107
+ properties,
108
+ registrySource: properties.registrySource
109
+ });
110
+ const lifecycleEventName = lifecycleTelemetryEventName(command);
111
+ if (lifecycleEventName) {
112
+ await captureCliTelemetryEvent({
113
+ args: input.args,
114
+ name: lifecycleEventName,
115
+ projectRoot,
116
+ properties,
117
+ registrySource: properties.registrySource
118
+ });
119
+ }
120
+ }
121
+ async function sendProjectHealthReportTelemetry(input) {
122
+ const projectRoot = input.projectRoot ?? process.cwd();
123
+ const properties = buildProjectHealthTelemetryProperties(input, projectRoot);
124
+ await captureCliTelemetryEvent({
125
+ name: "health.report.generated",
126
+ projectRoot,
127
+ properties
128
+ });
129
+ if (input.report.status === "healthy") {
130
+ await captureCliTelemetryEvent({
131
+ name: "decantr.health.healthy",
132
+ projectRoot,
133
+ properties
134
+ });
135
+ }
136
+ }
137
+ async function sendNewProjectCompletedTelemetry(input) {
138
+ const projectRoot = input.projectRoot ?? process.cwd();
139
+ const args = input.args ?? ["new"];
140
+ const base = buildCliLifecycleProperties({
141
+ args,
142
+ command: "new",
143
+ durationMs: input.durationMs,
144
+ projectRoot,
145
+ success: input.success
146
+ });
147
+ const properties = {
148
+ ...base,
149
+ command: "new"
150
+ };
151
+ await captureCliTelemetryEvent({
152
+ args,
153
+ name: "decantr.new.completed",
154
+ projectRoot,
155
+ properties,
156
+ registrySource: properties.registrySource
157
+ });
158
+ }
159
+ async function sendProjectHealthPromptTelemetry(input) {
160
+ const projectRoot = input.projectRoot ?? process.cwd();
161
+ const finding = input.finding;
162
+ const properties = {
163
+ success: Boolean(finding),
164
+ findingFound: Boolean(finding),
165
+ adoptionMode: normalizeAdoptionMode(input.report.summary.adoptionMode),
166
+ ci: input.ci ?? false,
167
+ findingSeverity: normalizeFindingSeverity(finding?.severity),
168
+ findingSource: normalizeFindingSource(finding?.source),
169
+ projectScope: inferProjectScope(projectRoot),
170
+ workflowMode: normalizeWorkflowMode(input.report.summary.workflowMode)
171
+ };
172
+ await captureCliTelemetryEvent({
173
+ name: "health.finding.prompt_requested",
174
+ projectRoot,
175
+ properties
176
+ });
177
+ }
178
+ async function sendProjectHealthCiFailedTelemetry(input) {
179
+ const projectRoot = input.projectRoot ?? process.cwd();
180
+ const properties = {
181
+ ...buildProjectHealthTelemetryProperties(input, projectRoot),
182
+ errorCode: "project_health_ci_failed",
183
+ failOn: input.failOn,
184
+ success: false
185
+ };
186
+ await captureCliTelemetryEvent({
187
+ name: "health.ci.failed",
188
+ projectRoot,
189
+ properties
190
+ });
191
+ }
192
+ async function sendStudioStartedTelemetry(input) {
193
+ const projectRoot = input.projectRoot ?? process.cwd();
194
+ const metadata = readProjectTelemetryMetadata(projectRoot);
195
+ const properties = {
196
+ success: true,
197
+ hostMode: isLoopbackHost(input.host) ? "loopback" : "custom",
198
+ port: input.port,
199
+ adoptionMode: metadata.adoptionMode,
200
+ projectScope: inferProjectScope(projectRoot),
201
+ workflowMode: metadata.workflowMode
202
+ };
203
+ await captureCliTelemetryEvent({
204
+ name: "studio.started",
205
+ projectRoot,
206
+ properties
207
+ });
208
+ }
209
+ async function sendStudioHealthRefreshedTelemetry(input) {
210
+ const projectRoot = input.projectRoot ?? process.cwd();
211
+ const properties = {
212
+ ...buildProjectHealthTelemetryProperties(input, projectRoot),
213
+ trigger: input.trigger ?? "api-refresh"
214
+ };
215
+ await captureCliTelemetryEvent({
216
+ name: "studio.health_refreshed",
217
+ projectRoot,
218
+ properties
219
+ });
220
+ }
221
+ function buildCliLifecycleProperties(input) {
222
+ const metadata = readProjectTelemetryMetadata(input.projectRoot);
223
+ const registrySource = inferRegistrySource(input.args);
224
+ return {
225
+ command: input.command,
226
+ success: input.success,
227
+ durationMs: input.durationMs,
228
+ adoptionMode: inferAdoptionMode(input.args) ?? metadata.adoptionMode,
229
+ errorCode: input.success ? void 0 : "cli_command_failed",
230
+ offline: input.args.includes("--offline"),
231
+ projectScope: metadata.projectScope ?? inferProjectScope(input.projectRoot),
232
+ registrySource,
233
+ targetFramework: inferFlagValue(input.args, "--target"),
234
+ workflowMode: inferWorkflowMode(input.args) ?? metadata.workflowMode
235
+ };
236
+ }
237
+ function lifecycleTelemetryEventName(command) {
238
+ if (command === "check") return "decantr.check.completed";
239
+ if (command === "init") return "decantr.init.completed";
240
+ if (command === "refresh") return "decantr.refresh.completed";
241
+ return null;
242
+ }
243
+ function buildProjectHealthTelemetryProperties(input, projectRoot) {
244
+ const { report } = input;
245
+ return {
246
+ success: true,
247
+ status: report.status,
248
+ score: report.score,
249
+ durationMs: input.durationMs,
250
+ adoptionMode: normalizeAdoptionMode(report.summary.adoptionMode),
251
+ ci: input.ci ?? false,
252
+ errorCount: report.summary.errorCount,
253
+ failOn: input.failOn,
254
+ findingCount: report.summary.findingCount,
255
+ format: input.format,
256
+ infoCount: report.summary.infoCount,
257
+ outputWritten: input.outputWritten ?? false,
258
+ packManifestPresent: report.summary.packManifestPresent,
259
+ pageCount: report.summary.pageCount,
260
+ projectScope: inferProjectScope(projectRoot),
261
+ reviewPackPresent: report.summary.reviewPackPresent,
262
+ routeCount: report.routes.declared.length,
263
+ runtimeAuditChecked: report.summary.runtimeAuditChecked,
264
+ runtimeMatchedCount: report.routes.runtimeMatched,
265
+ runtimePassed: report.summary.runtimePassed,
266
+ runtimeRouteCheckedCount: report.routes.runtimeChecked.length,
267
+ warnCount: report.summary.warnCount,
268
+ workflowMode: normalizeWorkflowMode(report.summary.workflowMode)
269
+ };
270
+ }
229
271
  function collectMetrics(essence, issues) {
230
272
  const dna = essence.dna ?? {};
231
273
  const blueprint = essence.blueprint ?? {};
@@ -263,12 +305,12 @@ function collectMetrics(essence, issues) {
263
305
  }
264
306
  function ensureTelemetryIdentities(projectRoot) {
265
307
  const installId = getOrCreateInstallId();
266
- const projectJsonPath = join3(projectRoot, ".decantr", "project.json");
267
- if (!existsSync3(projectJsonPath)) {
308
+ const projectJsonPath = join(projectRoot, ".decantr", "project.json");
309
+ if (!existsSync(projectJsonPath)) {
268
310
  return null;
269
311
  }
270
312
  try {
271
- const data = JSON.parse(readFileSync3(projectJsonPath, "utf-8"));
313
+ const data = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
272
314
  let projectId = typeof data.telemetryProjectId === "string" ? data.telemetryProjectId : void 0;
273
315
  if (!projectId) {
274
316
  projectId = `project_${randomUUID()}`;
@@ -282,10 +324,10 @@ function ensureTelemetryIdentities(projectRoot) {
282
324
  }
283
325
  function getOrCreateInstallId() {
284
326
  const configDir = getConfigDir();
285
- const configPath = join3(configDir, "config.json");
327
+ const configPath = join(configDir, "config.json");
286
328
  try {
287
- if (existsSync3(configPath)) {
288
- const data = JSON.parse(readFileSync3(configPath, "utf-8"));
329
+ if (existsSync(configPath)) {
330
+ const data = JSON.parse(readFileSync(configPath, "utf-8"));
289
331
  if (typeof data.telemetryInstallId === "string") {
290
332
  return data.telemetryInstallId;
291
333
  }
@@ -307,7 +349,7 @@ function getOrCreateInstallId() {
307
349
  }
308
350
  }
309
351
  function getConfigDir() {
310
- return process.env.DECANTR_CONFIG_DIR || join3(homedir(), ".config", "decantr");
352
+ return process.env.DECANTR_CONFIG_DIR || join(homedir(), ".config", "decantr");
311
353
  }
312
354
  function getTelemetryEventsEndpoint() {
313
355
  return process.env.DECANTR_TELEMETRY_ENDPOINT || DEFAULT_TELEMETRY_EVENTS_ENDPOINT;
@@ -316,6 +358,37 @@ function getTelemetryActorType() {
316
358
  const configured = process.env.DECANTR_TELEMETRY_ACTOR_TYPE;
317
359
  return isTelemetryActorType(configured) ? configured : "customer";
318
360
  }
361
+ function getRegistrySourceProperty(properties) {
362
+ const value = properties.registrySource;
363
+ return isRegistrySource(value) ? value : void 0;
364
+ }
365
+ function readProjectTelemetryMetadata(projectRoot) {
366
+ const data = readProjectJson(projectRoot);
367
+ const initialized = isRecord(data?.initialized) ? data.initialized : void 0;
368
+ return {
369
+ adoptionMode: normalizeAdoptionMode(initialized?.adoptionMode),
370
+ projectScope: normalizeProjectScope(initialized?.projectScope),
371
+ workflowMode: normalizeWorkflowMode(initialized?.workflowMode)
372
+ };
373
+ }
374
+ function resolveCliTelemetryProjectRoot(projectRoot, args) {
375
+ const projectFlag = inferFlagValue(args, "--project");
376
+ if (!projectFlag) return projectRoot;
377
+ const candidate = resolve(projectRoot, projectFlag);
378
+ return existsSync(join(candidate, ".decantr", "project.json")) ? candidate : projectRoot;
379
+ }
380
+ function readProjectJson(projectRoot) {
381
+ const projectJsonPath = join(projectRoot, ".decantr", "project.json");
382
+ if (!existsSync(projectJsonPath)) return null;
383
+ try {
384
+ return JSON.parse(readFileSync(projectJsonPath, "utf-8"));
385
+ } catch {
386
+ return null;
387
+ }
388
+ }
389
+ function isRecord(value) {
390
+ return typeof value === "object" && value !== null && !Array.isArray(value);
391
+ }
319
392
  function normalizeCommand(command) {
320
393
  if (!command) return null;
321
394
  if (command === "--help" || command === "-h") return "help";
@@ -336,13 +409,19 @@ function inferFlagValue(args, flag) {
336
409
  }
337
410
  function inferAdoptionMode(args) {
338
411
  const value = inferFlagValue(args, "--adoption");
412
+ return normalizeAdoptionMode(value);
413
+ }
414
+ function inferWorkflowMode(args) {
415
+ const value = inferFlagValue(args, "--workflow");
416
+ return normalizeWorkflowMode(value);
417
+ }
418
+ function normalizeAdoptionMode(value) {
339
419
  if (value === "contract-only" || value === "decantr-css" || value === "style-bridge") {
340
420
  return value;
341
421
  }
342
422
  return void 0;
343
423
  }
344
- function inferWorkflowMode(args) {
345
- const value = inferFlagValue(args, "--workflow");
424
+ function normalizeWorkflowMode(value) {
346
425
  if (value === "greenfield" || value === "greenfield-scaffold") {
347
426
  return "greenfield-scaffold";
348
427
  }
@@ -357,6 +436,20 @@ function inferWorkflowMode(args) {
357
436
  }
358
437
  return void 0;
359
438
  }
439
+ function normalizeProjectScope(value) {
440
+ if (value === "single-app" || value === "workspace-app") return value;
441
+ return void 0;
442
+ }
443
+ function normalizeFindingSeverity(value) {
444
+ if (value === "error" || value === "info" || value === "warn") return value;
445
+ return void 0;
446
+ }
447
+ function normalizeFindingSource(value) {
448
+ if (value === "audit" || value === "brownfield" || value === "check" || value === "interaction" || value === "pack" || value === "runtime") {
449
+ return value;
450
+ }
451
+ return void 0;
452
+ }
360
453
  function inferRegistrySource(args) {
361
454
  if (args.includes("--offline")) {
362
455
  return "cache";
@@ -366,16 +459,19 @@ function inferRegistrySource(args) {
366
459
  }
367
460
  return "official";
368
461
  }
462
+ function isRegistrySource(value) {
463
+ return value === "cache" || value === "custom" || value === "none" || value === "official" || value === "private";
464
+ }
369
465
  function inferProjectScope(projectRoot) {
370
- return existsSync3(join3(projectRoot, "pnpm-workspace.yaml")) || existsSync3(join3(projectRoot, "turbo.json")) || existsSync3(join3(projectRoot, "lerna.json")) ? "workspace-app" : "single-app";
466
+ return existsSync(join(projectRoot, "pnpm-workspace.yaml")) || existsSync(join(projectRoot, "turbo.json")) || existsSync(join(projectRoot, "lerna.json")) ? "workspace-app" : "single-app";
371
467
  }
372
468
  function getCliVersion() {
373
469
  try {
374
470
  const here = dirname(fileURLToPath(import.meta.url));
375
- const candidates = [join3(here, "..", "package.json"), join3(here, "..", "..", "package.json")];
471
+ const candidates = [join(here, "..", "package.json"), join(here, "..", "..", "package.json")];
376
472
  for (const candidate of candidates) {
377
- if (existsSync3(candidate)) {
378
- const pkg = JSON.parse(readFileSync3(candidate, "utf-8"));
473
+ if (existsSync(candidate)) {
474
+ const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
379
475
  if (pkg.version) {
380
476
  return pkg.version;
381
477
  }
@@ -385,6 +481,140 @@ function getCliVersion() {
385
481
  }
386
482
  return "unknown";
387
483
  }
484
+ function isLoopbackHost(host) {
485
+ return host === "127.0.0.1" || host === "localhost" || host === "::1";
486
+ }
487
+
488
+ // src/guard-context.ts
489
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
490
+ import { join as join2 } from "path";
491
+ function loadJsonEntries(dir) {
492
+ if (!existsSync2(dir)) return [];
493
+ try {
494
+ return readdirSync(dir).filter((file) => file.endsWith(".json") && file !== "index.json").map((file) => JSON.parse(readFileSync2(join2(dir, file), "utf-8")));
495
+ } catch {
496
+ return [];
497
+ }
498
+ }
499
+ function buildGuardRegistryContext(projectRoot = process.cwd()) {
500
+ const themeRegistry = /* @__PURE__ */ new Map();
501
+ const patternRegistry = /* @__PURE__ */ new Map();
502
+ const cacheDir = join2(projectRoot, ".decantr", "cache");
503
+ const customDir = join2(projectRoot, ".decantr", "custom");
504
+ for (const data of loadJsonEntries(join2(cacheDir, "@official", "themes"))) {
505
+ if (typeof data.id === "string" && !themeRegistry.has(data.id)) {
506
+ themeRegistry.set(data.id, {
507
+ modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
508
+ });
509
+ }
510
+ }
511
+ for (const data of loadJsonEntries(join2(customDir, "themes"))) {
512
+ if (typeof data.id === "string") {
513
+ themeRegistry.set(`custom:${data.id}`, {
514
+ modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
515
+ });
516
+ }
517
+ }
518
+ for (const data of loadJsonEntries(join2(cacheDir, "@official", "patterns"))) {
519
+ if (typeof data.id === "string" && !patternRegistry.has(data.id)) {
520
+ patternRegistry.set(data.id, data);
521
+ }
522
+ }
523
+ for (const data of loadJsonEntries(join2(customDir, "patterns"))) {
524
+ if (typeof data.id === "string") {
525
+ patternRegistry.set(data.id, data);
526
+ }
527
+ }
528
+ return { themeRegistry, patternRegistry };
529
+ }
530
+
531
+ // src/lib/scan-interactions.ts
532
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync } from "fs";
533
+ import { extname, join as join3 } from "path";
534
+ import { verifyInteractionsInSource } from "@decantr/verifier";
535
+ var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js", ".html", ".mdx"]);
536
+ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
537
+ "node_modules",
538
+ ".decantr",
539
+ ".git",
540
+ "dist",
541
+ "build",
542
+ ".next",
543
+ ".turbo",
544
+ "coverage",
545
+ ".cache"
546
+ ]);
547
+ var MAX_FILE_SIZE = 1024 * 1024;
548
+ function walkSourceTree(rootDir) {
549
+ const sources = /* @__PURE__ */ new Map();
550
+ function walk2(dir) {
551
+ let entries;
552
+ try {
553
+ entries = readdirSync2(dir);
554
+ } catch {
555
+ return;
556
+ }
557
+ for (const entry of entries) {
558
+ if (SKIP_DIRECTORIES.has(entry)) continue;
559
+ const fullPath = join3(dir, entry);
560
+ let s;
561
+ try {
562
+ s = statSync(fullPath);
563
+ } catch {
564
+ continue;
565
+ }
566
+ if (s.isDirectory()) {
567
+ walk2(fullPath);
568
+ } else if (s.isFile() && SCAN_EXTENSIONS.has(extname(entry))) {
569
+ if (s.size > MAX_FILE_SIZE) continue;
570
+ try {
571
+ sources.set(fullPath, readFileSync3(fullPath, "utf8"));
572
+ } catch {
573
+ }
574
+ }
575
+ }
576
+ }
577
+ walk2(rootDir);
578
+ return sources;
579
+ }
580
+ function collectDeclaredInteractions(projectRoot) {
581
+ const manifestPath = join3(projectRoot, ".decantr", "context", "pack-manifest.json");
582
+ if (!existsSync3(manifestPath)) return [];
583
+ let manifest;
584
+ try {
585
+ manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
586
+ } catch {
587
+ return [];
588
+ }
589
+ const all = [];
590
+ const pages = manifest.pages ?? [];
591
+ const contextDir = join3(projectRoot, ".decantr", "context");
592
+ for (const page of pages) {
593
+ const packPath = join3(contextDir, page.json);
594
+ if (!existsSync3(packPath)) continue;
595
+ let pack;
596
+ try {
597
+ pack = JSON.parse(readFileSync3(packPath, "utf8"));
598
+ } catch {
599
+ continue;
600
+ }
601
+ const patterns = pack.data?.patterns ?? [];
602
+ for (const pat of patterns) {
603
+ if (Array.isArray(pat.interactions)) {
604
+ all.push(...pat.interactions);
605
+ }
606
+ }
607
+ }
608
+ return all;
609
+ }
610
+ function scanProjectInteractions(projectRoot) {
611
+ const declared = collectDeclaredInteractions(projectRoot);
612
+ if (declared.length === 0) return [];
613
+ const sources = walkSourceTree(projectRoot);
614
+ if (sources.size === 0) return [];
615
+ const missing = verifyInteractionsInSource(declared, sources);
616
+ return missing.map(({ interaction, suggestion }) => `${interaction} \u2192 ${suggestion}`);
617
+ }
388
618
 
389
619
  // src/analyzers/routes.ts
390
620
  import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
@@ -1158,7 +1388,9 @@ function readSmallText(projectRoot, relPath) {
1158
1388
  }
1159
1389
  }
1160
1390
  function detectConflicts(projectRoot, items) {
1161
- const text = items.filter((item) => item.type === "file" && item.safeToCite && item.path.match(/\.(md|mdx|json|ts|js|yml|yaml)$/)).slice(0, 80).map((item) => readSmallText(projectRoot, item.path)).join("\n").toLowerCase();
1391
+ const text = items.filter(
1392
+ (item) => item.type === "file" && item.safeToCite && item.path.match(/\.(md|mdx|json|ts|js|yml|yaml)$/)
1393
+ ).slice(0, 80).map((item) => readSmallText(projectRoot, item.path)).join("\n").toLowerCase();
1162
1394
  const conflicts = [];
1163
1395
  const frameworkSignals = [
1164
1396
  ["next", /\bnext\.?js\b|\bapp router\b|\bpages router\b/],
@@ -1172,9 +1404,7 @@ function detectConflicts(projectRoot, items) {
1172
1404
  );
1173
1405
  }
1174
1406
  const forbidsTailwind = /\b(do not|don't|avoid|forbid|forbidden)\s+use\s+tailwind\b|\bno\s+tailwind\b/.test(text);
1175
- const endorsesTailwind = /\btailwind\.config\b|\btailwindcss\b|\b@tailwind\b|\btailwind\s+classes\b/.test(
1176
- text
1177
- );
1407
+ const endorsesTailwind = /\btailwind\.config\b|\btailwindcss\b|\b@tailwind\b|\btailwind\s+classes\b/.test(text);
1178
1408
  if (forbidsTailwind && endorsesTailwind) {
1179
1409
  conflicts.push("Ambient docs contain both Tailwind usage and anti-Tailwind language.");
1180
1410
  }
@@ -1190,9 +1420,9 @@ function detectDecantrEssenceStaleRisk(projectRoot, items) {
1190
1420
  try {
1191
1421
  const essence = JSON.parse(content);
1192
1422
  const risks = [];
1193
- if (essence.version !== "3.1.0") {
1423
+ if (essence.version !== "4.0.0") {
1194
1424
  risks.push(
1195
- `decantr.essence.json uses Decantr essence version ${essence.version ?? "unknown"}; migrate or review before treating it as current brownfield doctrine.`
1425
+ `decantr.essence.json uses Decantr essence version ${essence.version ?? "unknown"}; run decantr migrate --to v4 or review before treating it as current brownfield doctrine.`
1196
1426
  );
1197
1427
  }
1198
1428
  if (essence.dna?.theme?.id === "luminarum" && essence.structure) {
@@ -1210,7 +1440,9 @@ function detectDecantrEssenceStaleRisk(projectRoot, items) {
1210
1440
  function detectStaleRisks(projectRoot, items) {
1211
1441
  const pathRisks = items.filter(
1212
1442
  (item) => item.role === "stale-or-historical" || /complete|summary|legacy|deprecated/i.test(item.path)
1213
- ).slice(0, 12).map((item) => `${item.path} may be historical; verify before treating it as current doctrine.`);
1443
+ ).slice(0, 12).map(
1444
+ (item) => `${item.path} may be historical; verify before treating it as current doctrine.`
1445
+ );
1214
1446
  return [...pathRisks, ...detectDecantrEssenceStaleRisk(projectRoot, items)];
1215
1447
  }
1216
1448
  function scanAmbientContext(projectRoot) {
@@ -1402,17 +1634,23 @@ function readDoctrineMap(projectRoot) {
1402
1634
  }
1403
1635
 
1404
1636
  export {
1405
- scanProjectInteractions,
1406
1637
  scanRoutes,
1407
1638
  scanStyling,
1408
1639
  scanAmbientContext,
1409
1640
  createDoctrineMap,
1410
1641
  writeDoctrineMap,
1411
1642
  readDoctrineMap,
1412
- buildGuardRegistryContext,
1413
1643
  sendGuardMetrics,
1414
1644
  isOptedIn,
1415
1645
  optIn,
1416
1646
  sendCliCommandTelemetry,
1417
- collectMetrics
1647
+ sendProjectHealthReportTelemetry,
1648
+ sendNewProjectCompletedTelemetry,
1649
+ sendProjectHealthPromptTelemetry,
1650
+ sendProjectHealthCiFailedTelemetry,
1651
+ sendStudioStartedTelemetry,
1652
+ sendStudioHealthRefreshedTelemetry,
1653
+ collectMetrics,
1654
+ buildGuardRegistryContext,
1655
+ scanProjectInteractions
1418
1656
  };