@ctxo/cli 0.9.3 → 0.11.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,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ArchitecturalOverlay,
4
+ BlastRadiusCalculator,
4
5
  BoundaryViolationDetector,
5
6
  CommunitySnapshotWriter,
6
7
  DeadCodeDetector,
@@ -18,26 +19,28 @@ import {
18
19
  loadConfig,
19
20
  makeGlobMatcher,
20
21
  watchSnapshotMinFileChanges
21
- } from "./chunk-HXEPS2XY.js";
22
+ } from "./chunk-HZ4UGA6J.js";
22
23
  import {
23
- atomicWrite,
24
- createLogger
25
- } from "./chunk-6EKERTJ7.js";
24
+ atomicWrite
25
+ } from "./chunk-RFEXMBTG.js";
26
26
  import {
27
27
  JsonIndexReader
28
- } from "./chunk-2TVHKCCU.js";
28
+ } from "./chunk-CDGQGCVN.js";
29
+ import {
30
+ createLogger
31
+ } from "./chunk-5DSKZJNN.js";
29
32
  import {
30
33
  StalenessDetector
31
34
  } from "./chunk-PSKDDLSF.js";
32
35
 
33
36
  // src/cli/cli-router.ts
34
- import { readFileSync as readFileSync15, existsSync as existsSync23 } from "fs";
35
- import { dirname as dirname8, join as join25 } from "path";
37
+ import { readFileSync as readFileSync17, existsSync as existsSync27 } from "fs";
38
+ import { dirname as dirname9, join as join31 } from "path";
36
39
 
37
40
  // src/cli/index-command.ts
38
41
  import { execFileSync as execFileSync2 } from "child_process";
39
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, existsSync as existsSync9, statSync, readdirSync as readdirSync2 } from "fs";
40
- import { join as join8, relative, extname as extname3 } from "path";
42
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, existsSync as existsSync10, statSync, readdirSync as readdirSync2 } from "fs";
43
+ import { join as join9, relative, extname as extname3 } from "path";
41
44
  import { createRequire as createRequire3 } from "module";
42
45
 
43
46
  // src/core/staleness/content-hasher.ts
@@ -263,9 +266,9 @@ async function loadPlugins(projectRoot, options = {}) {
263
266
  }
264
267
 
265
268
  // src/cli/install-command.ts
266
- import { existsSync as existsSync5 } from "fs";
269
+ import { existsSync as existsSync6 } from "fs";
267
270
  import { createRequire as createRequire2 } from "module";
268
- import { join as join4 } from "path";
271
+ import { join as join5 } from "path";
269
272
 
270
273
  // src/core/detection/detect-languages.ts
271
274
  import { execFileSync } from "child_process";
@@ -574,7 +577,53 @@ function resolveBinary(command) {
574
577
  return `${command}.cmd`;
575
578
  }
576
579
 
580
+ // src/core/detection/detect-java-runtime.ts
581
+ import { spawnSync } from "child_process";
582
+ import { existsSync as existsSync5 } from "fs";
583
+ import { join as join4 } from "path";
584
+ function parseJavaMajor(output) {
585
+ const m = output.match(/version "([^"]+)"/);
586
+ if (!m) return void 0;
587
+ const parts = m[1].split(".");
588
+ const major = parts[0] === "1" && parts[1] ? parseInt(parts[1], 10) : parseInt(parts[0], 10);
589
+ return Number.isNaN(major) ? void 0 : major;
590
+ }
591
+ var _javaMajorCache = void 0;
592
+ function detectJavaMajor() {
593
+ if (_javaMajorCache !== void 0) {
594
+ return _javaMajorCache === null ? void 0 : _javaMajorCache;
595
+ }
596
+ const home = process.env["CTXO_JAVA_HOME"] || process.env["JAVA_HOME"];
597
+ const bin = home ? join4(home, "bin", process.platform === "win32" ? "java.exe" : "java") : "java";
598
+ if (home && !existsSync5(bin)) {
599
+ _javaMajorCache = null;
600
+ return void 0;
601
+ }
602
+ const r = spawnSync(bin, ["-version"], {
603
+ encoding: "utf-8",
604
+ timeout: 1e4,
605
+ windowsHide: true
606
+ });
607
+ if (r.error || r.status === null) {
608
+ _javaMajorCache = null;
609
+ return void 0;
610
+ }
611
+ const major = parseJavaMajor(`${r.stderr ?? ""}${r.stdout ?? ""}`);
612
+ _javaMajorCache = major ?? null;
613
+ return major;
614
+ }
615
+ function javaRuntimeAvailable() {
616
+ return (detectJavaMajor() ?? 0) >= 17;
617
+ }
618
+
577
619
  // src/cli/install-command.ts
620
+ function resolveJavaPackages(opts) {
621
+ if (opts.fullTier && opts.syntaxOnly) throw new Error("Cannot combine --full-tier with --syntax-only.");
622
+ const base = ["@ctxo/lang-java"];
623
+ if (opts.syntaxOnly) return base;
624
+ if (opts.fullTier || opts.jreAvailable) return [...base, "@ctxo/lang-java-analyzer"];
625
+ return base;
626
+ }
578
627
  var InstallCommand = class {
579
628
  constructor(projectRoot) {
580
629
  this.projectRoot = projectRoot;
@@ -598,7 +647,7 @@ var InstallCommand = class {
598
647
  process.exitCode = 1;
599
648
  return;
600
649
  }
601
- if (!options.global && !existsSync5(join4(this.projectRoot, "package.json"))) {
650
+ if (!options.global && !existsSync6(join5(this.projectRoot, "package.json"))) {
602
651
  console.error("[ctxo] No package.json in the current project. Re-run with --global or create a package.json first.");
603
652
  process.exitCode = 1;
604
653
  return;
@@ -608,9 +657,28 @@ var InstallCommand = class {
608
657
  process.exitCode = 1;
609
658
  return;
610
659
  }
611
- const specifiers = resolvedLangs.map(
612
- (lang) => options.version ? `${officialPluginFor(lang)}@${options.version}` : officialPluginFor(lang)
613
- );
660
+ const specifiers = [];
661
+ for (const lang of resolvedLangs) {
662
+ if (lang === "java") {
663
+ const jreAvailable = javaRuntimeAvailable();
664
+ if (!jreAvailable && !options.fullTier && !options.syntaxOnly) {
665
+ console.error('[ctxo] JRE 17+ not found; installing Java syntax tier. Install a JRE then run "ctxo install java --full-tier" for resolved call/use edges.');
666
+ }
667
+ let javaPkgs;
668
+ try {
669
+ javaPkgs = resolveJavaPackages({ jreAvailable, fullTier: options.fullTier, syntaxOnly: options.syntaxOnly });
670
+ } catch (err) {
671
+ console.error(`[ctxo] ${err.message}`);
672
+ process.exitCode = 1;
673
+ return;
674
+ }
675
+ for (const pkg of javaPkgs) {
676
+ specifiers.push(options.version ? `${pkg}@${options.version}` : pkg);
677
+ }
678
+ } else {
679
+ specifiers.push(options.version ? `${officialPluginFor(lang)}@${options.version}` : officialPluginFor(lang));
680
+ }
681
+ }
614
682
  const invocation = buildInstallCommand(resolution.manager, specifiers, {
615
683
  global: options.global,
616
684
  workspaceRoot: !options.global && isWorkspaceRoot(this.projectRoot)
@@ -702,12 +770,12 @@ function isLockedCI(env) {
702
770
  }
703
771
 
704
772
  // src/adapters/storage/json-index-writer.ts
705
- import { mkdirSync, unlinkSync, existsSync as existsSync6 } from "fs";
706
- import { dirname as dirname4, join as join5, resolve as resolve2, sep } from "path";
773
+ import { mkdirSync, unlinkSync, existsSync as existsSync7 } from "fs";
774
+ import { dirname as dirname4, join as join6, resolve as resolve2, sep } from "path";
707
775
  var JsonIndexWriter = class {
708
776
  indexDir;
709
777
  constructor(ctxoRoot) {
710
- this.indexDir = join5(ctxoRoot, "index");
778
+ this.indexDir = join6(ctxoRoot, "index");
711
779
  }
712
780
  write(fileIndex) {
713
781
  if (!fileIndex.file) {
@@ -721,7 +789,7 @@ var JsonIndexWriter = class {
721
789
  }
722
790
  writeCoChanges(matrix) {
723
791
  mkdirSync(this.indexDir, { recursive: true });
724
- const targetPath = join5(this.indexDir, "co-changes.json");
792
+ const targetPath = join6(this.indexDir, "co-changes.json");
725
793
  this.atomicWrite(targetPath, JSON.stringify(matrix, null, 2));
726
794
  }
727
795
  atomicWrite(targetPath, content) {
@@ -729,7 +797,7 @@ var JsonIndexWriter = class {
729
797
  }
730
798
  delete(relativePath) {
731
799
  const targetPath = this.resolveIndexPath(relativePath);
732
- if (existsSync6(targetPath)) {
800
+ if (existsSync7(targetPath)) {
733
801
  unlinkSync(targetPath);
734
802
  }
735
803
  }
@@ -757,19 +825,19 @@ var JsonIndexWriter = class {
757
825
  };
758
826
 
759
827
  // src/adapters/storage/schema-manager.ts
760
- import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
761
- import { join as join6, dirname as dirname5 } from "path";
828
+ import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync8, mkdirSync as mkdirSync2 } from "fs";
829
+ import { join as join7, dirname as dirname5 } from "path";
762
830
  var CURRENT_SCHEMA_VERSION = "1.0.0";
763
831
  var SchemaManager = class {
764
832
  versionFilePath;
765
833
  constructor(ctxoRoot) {
766
- this.versionFilePath = join6(ctxoRoot, "index", "schema-version");
834
+ this.versionFilePath = join7(ctxoRoot, "index", "schema-version");
767
835
  }
768
836
  currentVersion() {
769
837
  return CURRENT_SCHEMA_VERSION;
770
838
  }
771
839
  readStoredVersion() {
772
- if (!existsSync7(this.versionFilePath)) {
840
+ if (!existsSync8(this.versionFilePath)) {
773
841
  return void 0;
774
842
  }
775
843
  return readFileSync4(this.versionFilePath, "utf-8").trim();
@@ -786,8 +854,8 @@ var SchemaManager = class {
786
854
  };
787
855
 
788
856
  // src/adapters/storage/architecture-artifacts-writer.ts
789
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "fs";
790
- import { join as join7, resolve as resolve3 } from "path";
857
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "fs";
858
+ import { join as join8, resolve as resolve3 } from "path";
791
859
  import { tmpdir } from "os";
792
860
  var log3 = createLogger("ctxo:architecture-artifacts");
793
861
  var DRIFT_FILE_NAME = "drift-events.json";
@@ -795,20 +863,20 @@ var VIOLATIONS_FILE_NAME = "boundary-violations.json";
795
863
  var ArchitectureArtifactsWriter = class {
796
864
  indexDir;
797
865
  constructor(ctxoRoot, options = {}) {
798
- this.indexDir = join7(ctxoRoot, "index");
866
+ this.indexDir = join8(ctxoRoot, "index");
799
867
  assertSafeWritePath(ctxoRoot, options.allowProductionPath ?? false);
800
868
  }
801
869
  writeDriftEvents(artifact) {
802
870
  mkdirSync3(this.indexDir, { recursive: true });
803
- atomicWrite(join7(this.indexDir, DRIFT_FILE_NAME), JSON.stringify(artifact, null, 2));
871
+ atomicWrite(join8(this.indexDir, DRIFT_FILE_NAME), JSON.stringify(artifact, null, 2));
804
872
  }
805
873
  writeBoundaryViolations(artifact) {
806
874
  mkdirSync3(this.indexDir, { recursive: true });
807
- atomicWrite(join7(this.indexDir, VIOLATIONS_FILE_NAME), JSON.stringify(artifact, null, 2));
875
+ atomicWrite(join8(this.indexDir, VIOLATIONS_FILE_NAME), JSON.stringify(artifact, null, 2));
808
876
  }
809
877
  readDriftEvents() {
810
- const path = join7(this.indexDir, DRIFT_FILE_NAME);
811
- if (!existsSync8(path)) return void 0;
878
+ const path = join8(this.indexDir, DRIFT_FILE_NAME);
879
+ if (!existsSync9(path)) return void 0;
812
880
  try {
813
881
  return JSON.parse(readFileSync5(path, "utf-8"));
814
882
  } catch (err) {
@@ -817,8 +885,8 @@ var ArchitectureArtifactsWriter = class {
817
885
  }
818
886
  }
819
887
  readBoundaryViolations() {
820
- const path = join7(this.indexDir, VIOLATIONS_FILE_NAME);
821
- if (!existsSync8(path)) return void 0;
888
+ const path = join8(this.indexDir, VIOLATIONS_FILE_NAME);
889
+ if (!existsSync9(path)) return void 0;
822
890
  try {
823
891
  return JSON.parse(readFileSync5(path, "utf-8"));
824
892
  } catch (err) {
@@ -1127,7 +1195,7 @@ var IndexCommand = class {
1127
1195
  supportedExtensions;
1128
1196
  constructor(projectRoot, ctxoRoot) {
1129
1197
  this.projectRoot = projectRoot;
1130
- this.ctxoRoot = ctxoRoot ?? join8(projectRoot, ".ctxo");
1198
+ this.ctxoRoot = ctxoRoot ?? join9(projectRoot, ".ctxo");
1131
1199
  this.supportedExtensions = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".go", ".cs"]);
1132
1200
  }
1133
1201
  loadIgnoreConfig() {
@@ -1155,7 +1223,7 @@ var IndexCommand = class {
1155
1223
  }
1156
1224
  const ignore = this.loadIgnoreConfig();
1157
1225
  const registry = new LanguageAdapterRegistry();
1158
- const { tsMorphLike, roslynAdapter, csharpTier, goTier } = await this.registerDiscoveredPlugins(registry, ignore.ignoreProjectPatterns);
1226
+ const { tsMorphLike, roslynAdapter, csharpTier, goTier, javaTier } = await this.registerDiscoveredPlugins(registry, ignore.ignoreProjectPatterns);
1159
1227
  this.supportedExtensions = registry.getSupportedExtensions();
1160
1228
  this.printLanguagePreview();
1161
1229
  const writer = new JsonIndexWriter(this.ctxoRoot);
@@ -1165,7 +1233,7 @@ var IndexCommand = class {
1165
1233
  const revertDetector = new RevertDetector();
1166
1234
  let files;
1167
1235
  if (options.file) {
1168
- const fullPath = join8(this.projectRoot, options.file);
1236
+ const fullPath = join9(this.projectRoot, options.file);
1169
1237
  files = [fullPath];
1170
1238
  console.error(`[ctxo] Incremental re-index: ${options.file}`);
1171
1239
  } else {
@@ -1178,8 +1246,8 @@ var IndexCommand = class {
1178
1246
  console.error(`[ctxo] Building codebase index... Found ${files.length} source files`);
1179
1247
  }
1180
1248
  try {
1181
- const { sweepStaleTmpFiles } = await import("./atomic-write-AQNYJFAG.js");
1182
- const swept = sweepStaleTmpFiles(join8(this.ctxoRoot, "index"));
1249
+ const { sweepStaleTmpFiles } = await import("./atomic-write-BAFH464Q.js");
1250
+ const swept = sweepStaleTmpFiles(join9(this.ctxoRoot, "index"));
1183
1251
  if (swept > 0) {
1184
1252
  console.error(`[ctxo] Cleaned ${swept} orphan .tmp file(s) from previous interrupted run`);
1185
1253
  }
@@ -1266,7 +1334,7 @@ var IndexCommand = class {
1266
1334
  }
1267
1335
  let communitySnapshot;
1268
1336
  if (options.skipCommunity && pendingIndices.length > 0) {
1269
- if (existsSync9(join8(this.ctxoRoot, "index", "communities.json"))) {
1337
+ if (existsSync10(join9(this.ctxoRoot, "index", "communities.json"))) {
1270
1338
  console.error(
1271
1339
  `[ctxo] WARN stale communities.json present \u2014 --skip-community preserved existing snapshot. Run \`ctxo index\` (full) to refresh.`
1272
1340
  );
@@ -1279,6 +1347,7 @@ var IndexCommand = class {
1279
1347
  {
1280
1348
  csharpTier,
1281
1349
  goTier,
1350
+ javaTier,
1282
1351
  tsCount: graphIndices.filter((idx) => /\.(ts|tsx|js|jsx)$/.test(idx.file)).length
1283
1352
  },
1284
1353
  gitAdapter
@@ -1316,10 +1385,12 @@ var IndexCommand = class {
1316
1385
  const csCount = pendingIndices.filter((e) => e.relativePath.endsWith(".cs")).length;
1317
1386
  const tsCount = pendingIndices.filter((e) => /\.(ts|tsx|js|jsx)$/.test(e.relativePath)).length;
1318
1387
  const goCount = pendingIndices.filter((e) => e.relativePath.endsWith(".go")).length;
1388
+ const javaCount = pendingIndices.filter((e) => e.relativePath.endsWith(".java")).length;
1319
1389
  console.error(`[ctxo] Index complete: ${processed} files indexed`);
1320
1390
  if (tsCount > 0) console.error(`[ctxo] TypeScript/JS: ${tsCount} files (full tier)`);
1321
1391
  if (csCount > 0) console.error(`[ctxo] C#: ${csCount} files (${csharpTier} tier${csharpTier === "syntax" ? " - .NET SDK 8+ for full analysis" : ""})`);
1322
1392
  if (goCount > 0) console.error(`[ctxo] Go: ${goCount} files (${goTier} tier${goTier === "syntax" ? " - Go 1.22+ for full analysis" : ""})`);
1393
+ if (javaCount > 0) console.error(`[ctxo] Java: ${javaCount} files (${javaTier} tier${javaTier === "syntax" ? ' - JRE 17+ & "ctxo install java --full-tier" for full analysis' : ""})`);
1323
1394
  if (roslynAdapter) await roslynAdapter.dispose();
1324
1395
  }
1325
1396
  async autoInstallMissingPlugins() {
@@ -1355,6 +1426,7 @@ var IndexCommand = class {
1355
1426
  let roslynAdapter = null;
1356
1427
  let csharpTier = "unavailable";
1357
1428
  let goTier = "unavailable";
1429
+ let javaTier = "unavailable";
1358
1430
  for (const { plugin, adapter } of loaded) {
1359
1431
  if (typeof adapter.initialize === "function") {
1360
1432
  try {
@@ -1378,11 +1450,15 @@ var IndexCommand = class {
1378
1450
  if (plugin.id === "go" && typeof goCandidate.getTier === "function") {
1379
1451
  goTier = goCandidate.getTier();
1380
1452
  }
1381
- const activeTier = plugin.id === "csharp" ? csharpTier : plugin.id === "go" ? goTier : plugin.tier;
1453
+ const javaCandidate = adapter;
1454
+ if (plugin.id === "java" && typeof javaCandidate.getTier === "function") {
1455
+ javaTier = javaCandidate.getTier();
1456
+ }
1457
+ const activeTier = plugin.id === "csharp" ? csharpTier : plugin.id === "go" ? goTier : plugin.id === "java" ? javaTier : plugin.tier;
1382
1458
  const tierSuffix = activeTier !== plugin.tier ? ` (active: ${activeTier})` : "";
1383
1459
  console.error(`[ctxo] Plugin ${plugin.id}@${plugin.version} (${plugin.tier} tier${tierSuffix}) \u2014 ${plugin.extensions.join(", ")}`);
1384
1460
  }
1385
- return { tsMorphLike, roslynAdapter, csharpTier, goTier };
1461
+ return { tsMorphLike, roslynAdapter, csharpTier, goTier, javaTier };
1386
1462
  }
1387
1463
  discoverFilesIn(root, ignoreFile = () => false) {
1388
1464
  try {
@@ -1391,18 +1467,18 @@ var IndexCommand = class {
1391
1467
  encoding: "utf-8",
1392
1468
  maxBuffer: 10 * 1024 * 1024
1393
1469
  });
1394
- return output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => this.isSupportedExtension(line)).map((line) => join8(root, line)).filter((abs) => {
1470
+ return output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => this.isSupportedExtension(line)).map((line) => join9(root, line)).filter((abs) => {
1395
1471
  const rel = relative(this.projectRoot, abs).replace(/\\/g, "/");
1396
1472
  return !ignoreFile(rel);
1397
- }).filter((abs) => existsSync9(abs));
1473
+ }).filter((abs) => existsSync10(abs));
1398
1474
  } catch {
1399
1475
  console.error(`[ctxo] git ls-files failed for ${root}`);
1400
1476
  return [];
1401
1477
  }
1402
1478
  }
1403
1479
  discoverWorkspaces(ignoreProject = () => false) {
1404
- const pkgPath = join8(this.projectRoot, "package.json");
1405
- if (!existsSync9(pkgPath)) return [this.projectRoot];
1480
+ const pkgPath = join9(this.projectRoot, "package.json");
1481
+ if (!existsSync10(pkgPath)) return [this.projectRoot];
1406
1482
  try {
1407
1483
  const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
1408
1484
  const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
@@ -1410,17 +1486,17 @@ var IndexCommand = class {
1410
1486
  const resolved = [];
1411
1487
  for (const ws of workspaces) {
1412
1488
  if (ws.endsWith("/*") || ws.endsWith("\\*")) {
1413
- const parentDir = join8(this.projectRoot, ws.slice(0, -2));
1414
- if (existsSync9(parentDir)) {
1489
+ const parentDir = join9(this.projectRoot, ws.slice(0, -2));
1490
+ if (existsSync10(parentDir)) {
1415
1491
  for (const entry of readdirSync2(parentDir, { withFileTypes: true })) {
1416
1492
  if (entry.isDirectory()) {
1417
- resolved.push(join8(parentDir, entry.name));
1493
+ resolved.push(join9(parentDir, entry.name));
1418
1494
  }
1419
1495
  }
1420
1496
  }
1421
1497
  } else {
1422
- const wsPath = join8(this.projectRoot, ws);
1423
- if (existsSync9(wsPath)) {
1498
+ const wsPath = join9(this.projectRoot, ws);
1499
+ if (existsSync10(wsPath)) {
1424
1500
  resolved.push(wsPath);
1425
1501
  }
1426
1502
  }
@@ -1453,7 +1529,7 @@ var IndexCommand = class {
1453
1529
  if (_roslyn) await _roslyn.dispose();
1454
1530
  const hasher = new ContentHasher();
1455
1531
  const files = this.discoverFilesIn(this.projectRoot);
1456
- const reader = new (await import("./json-index-reader-XOEE6VMD.js")).JsonIndexReader(this.ctxoRoot);
1532
+ const reader = new (await import("./json-index-reader-UYS74USB.js")).JsonIndexReader(this.ctxoRoot);
1457
1533
  const indices = reader.readAll();
1458
1534
  const indexedMap = new Map(indices.map((i) => [i.file, i]));
1459
1535
  let staleCount = 0;
@@ -1466,7 +1542,7 @@ var IndexCommand = class {
1466
1542
  staleCount++;
1467
1543
  continue;
1468
1544
  }
1469
- if (!existsSync9(filePath)) {
1545
+ if (!existsSync10(filePath)) {
1470
1546
  console.error(`[ctxo] DELETED: ${relativePath}`);
1471
1547
  staleCount++;
1472
1548
  continue;
@@ -1492,13 +1568,13 @@ var IndexCommand = class {
1492
1568
  console.error("[ctxo] Index is up to date");
1493
1569
  }
1494
1570
  ensureGitignore() {
1495
- const gitignorePath = join8(this.projectRoot, ".gitignore");
1571
+ const gitignorePath = join9(this.projectRoot, ".gitignore");
1496
1572
  const cachePattern = ".ctxo/.cache/";
1497
1573
  const suffix = `
1498
1574
  # Ctxo local cache (never committed)
1499
1575
  ${cachePattern}
1500
1576
  `;
1501
- const existing = existsSync9(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
1577
+ const existing = existsSync10(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
1502
1578
  if (existing.includes(cachePattern)) return;
1503
1579
  writeFileSync2(gitignorePath, existing + suffix, "utf-8");
1504
1580
  console.error("[ctxo] Added .ctxo/.cache/ to .gitignore");
@@ -1617,8 +1693,8 @@ ${cachePattern}
1617
1693
  }
1618
1694
  };
1619
1695
  function pickEdgeQuality(tiers) {
1620
- const hasSyntaxOnly = tiers.csharpTier === "syntax" || tiers.goTier === "syntax";
1621
- const hasFull = tiers.tsCount > 0 || tiers.csharpTier === "full" || tiers.goTier === "full";
1696
+ const hasSyntaxOnly = tiers.csharpTier === "syntax" || tiers.goTier === "syntax" || tiers.javaTier === "syntax";
1697
+ const hasFull = tiers.tsCount > 0 || tiers.csharpTier === "full" || tiers.goTier === "full" || tiers.javaTier === "full";
1622
1698
  if (hasSyntaxOnly && hasFull) return "mixed";
1623
1699
  if (hasSyntaxOnly) return "syntax-only";
1624
1700
  return "full";
@@ -1643,11 +1719,11 @@ function countCommunities(snapshot) {
1643
1719
  }
1644
1720
 
1645
1721
  // src/cli/sync-command.ts
1646
- import { join as join9 } from "path";
1722
+ import { join as join10 } from "path";
1647
1723
  var SyncCommand = class {
1648
1724
  ctxoRoot;
1649
1725
  constructor(projectRoot) {
1650
- this.ctxoRoot = join9(projectRoot, ".ctxo");
1726
+ this.ctxoRoot = join10(projectRoot, ".ctxo");
1651
1727
  }
1652
1728
  async run() {
1653
1729
  console.error("[ctxo] Rebuilding SQLite cache from committed JSON index...");
@@ -1662,19 +1738,19 @@ var SyncCommand = class {
1662
1738
  };
1663
1739
 
1664
1740
  // src/cli/status-command.ts
1665
- import { join as join10 } from "path";
1666
- import { existsSync as existsSync10 } from "fs";
1741
+ import { join as join11 } from "path";
1742
+ import { existsSync as existsSync11 } from "fs";
1667
1743
  import { execFileSync as execFileSync3 } from "child_process";
1668
1744
  var StatusCommand = class {
1669
1745
  projectRoot;
1670
1746
  ctxoRoot;
1671
1747
  constructor(projectRoot) {
1672
1748
  this.projectRoot = projectRoot;
1673
- this.ctxoRoot = join10(projectRoot, ".ctxo");
1749
+ this.ctxoRoot = join11(projectRoot, ".ctxo");
1674
1750
  }
1675
1751
  run() {
1676
- const indexDir = join10(this.ctxoRoot, "index");
1677
- if (!existsSync10(indexDir)) {
1752
+ const indexDir = join11(this.ctxoRoot, "index");
1753
+ if (!existsSync11(indexDir)) {
1678
1754
  console.error('[ctxo] No index found. Run "ctxo index" first.');
1679
1755
  return;
1680
1756
  }
@@ -1689,7 +1765,7 @@ var StatusCommand = class {
1689
1765
  console.error(` Indexed files: ${indices.length}`);
1690
1766
  console.error(` Total symbols: ${totalSymbols}`);
1691
1767
  console.error(` Total edges: ${totalEdges}`);
1692
- const cacheExists = existsSync10(join10(this.ctxoRoot, ".cache", "symbols.db"));
1768
+ const cacheExists = existsSync11(join11(this.ctxoRoot, ".cache", "symbols.db"));
1693
1769
  console.error(` SQLite cache: ${cacheExists ? "present" : "missing (run ctxo sync)"}`);
1694
1770
  if (indices.length > 0) {
1695
1771
  console.error("");
@@ -1718,7 +1794,7 @@ var StatusCommand = class {
1718
1794
 
1719
1795
  // src/cli/verify-command.ts
1720
1796
  import { mkdtempSync, rmSync } from "fs";
1721
- import { join as join11 } from "path";
1797
+ import { join as join12 } from "path";
1722
1798
  import { tmpdir as tmpdir2 } from "os";
1723
1799
  var VerifyCommand = class {
1724
1800
  projectRoot;
@@ -1727,12 +1803,12 @@ var VerifyCommand = class {
1727
1803
  }
1728
1804
  async run() {
1729
1805
  console.error("[ctxo] Verifying index freshness...");
1730
- const tempDir = mkdtempSync(join11(tmpdir2(), "ctxo-verify-"));
1806
+ const tempDir = mkdtempSync(join12(tmpdir2(), "ctxo-verify-"));
1731
1807
  try {
1732
- const tempCtxo = join11(tempDir, ".ctxo");
1808
+ const tempCtxo = join12(tempDir, ".ctxo");
1733
1809
  const indexCmd = new IndexCommand(this.projectRoot, tempCtxo);
1734
1810
  await indexCmd.run({ skipSideEffects: true });
1735
- const committedReader = new JsonIndexReader(join11(this.projectRoot, ".ctxo"));
1811
+ const committedReader = new JsonIndexReader(join12(this.projectRoot, ".ctxo"));
1736
1812
  const freshReader = new JsonIndexReader(tempCtxo);
1737
1813
  const committedIndices = committedReader.readAll();
1738
1814
  const freshIndices = freshReader.readAll();
@@ -1765,13 +1841,41 @@ var VerifyCommand = class {
1765
1841
  };
1766
1842
 
1767
1843
  // src/cli/init-command.ts
1768
- import { join as join13 } from "path";
1769
- import { existsSync as existsSync12, readFileSync as readFileSync8, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, chmodSync } from "fs";
1844
+ import { join as join14 } from "path";
1845
+ import { existsSync as existsSync13, readFileSync as readFileSync8, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, chmodSync } from "fs";
1770
1846
  import { createRequire as createRequire4 } from "module";
1771
1847
 
1772
1848
  // src/cli/ai-rules.ts
1773
- import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
1774
- import { join as join12, dirname as dirname6 } from "path";
1849
+ import { existsSync as existsSync12, readFileSync as readFileSync7, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
1850
+ import { join as join13, dirname as dirname6 } from "path";
1851
+ var CTXO_HOOK_COMMAND = "ctxo gate-hook";
1852
+ function ensureClaudeHook(projectRoot) {
1853
+ const filePath = join13(projectRoot, ".claude", "settings.json");
1854
+ const dir = dirname6(filePath);
1855
+ if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
1856
+ const ctxoEntry = { matcher: "Edit", hooks: [{ type: "command", command: CTXO_HOOK_COMMAND }] };
1857
+ if (!existsSync12(filePath)) {
1858
+ writeFileSync3(filePath, JSON.stringify({ hooks: { PreToolUse: [ctxoEntry] } }, null, 2) + "\n", "utf-8");
1859
+ return { file: ".claude/settings.json", action: "created" };
1860
+ }
1861
+ let existing;
1862
+ try {
1863
+ existing = JSON.parse(readFileSync7(filePath, "utf-8"));
1864
+ } catch {
1865
+ writeFileSync3(filePath, JSON.stringify({ hooks: { PreToolUse: [ctxoEntry] } }, null, 2) + "\n", "utf-8");
1866
+ return { file: ".claude/settings.json", action: "updated" };
1867
+ }
1868
+ const hooks = existing.hooks ?? {};
1869
+ const pre = hooks.PreToolUse ?? [];
1870
+ if (pre.some((e) => e.hooks?.some((h) => h.command === CTXO_HOOK_COMMAND))) {
1871
+ return { file: ".claude/settings.json", action: "skipped" };
1872
+ }
1873
+ pre.push(ctxoEntry);
1874
+ hooks.PreToolUse = pre;
1875
+ existing.hooks = hooks;
1876
+ writeFileSync3(filePath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
1877
+ return { file: ".claude/settings.json", action: "updated" };
1878
+ }
1775
1879
  var PLATFORMS = [
1776
1880
  { id: "claude-code", name: "Claude Code", file: "CLAUDE.md", mode: "append", detectPaths: ["CLAUDE.md", ".claude"], starred: true },
1777
1881
  { id: "cursor", name: "Cursor", file: ".cursor/rules/ctxo.mdc", mode: "create", detectPaths: [".cursor", ".cursorrules"], starred: false },
@@ -1786,7 +1890,7 @@ var PLATFORMS = [
1786
1890
  function detectPlatforms(projectRoot) {
1787
1891
  const detected = /* @__PURE__ */ new Set();
1788
1892
  for (const p of PLATFORMS) {
1789
- if (p.detectPaths.some((d) => existsSync11(join12(projectRoot, d)))) {
1893
+ if (p.detectPaths.some((d) => existsSync12(join13(projectRoot, d)))) {
1790
1894
  detected.add(p.id);
1791
1895
  }
1792
1896
  }
@@ -1848,15 +1952,15 @@ function generateRules(platformId) {
1848
1952
  function installRules(projectRoot, platformId) {
1849
1953
  const platform = PLATFORMS.find((p) => p.id === platformId);
1850
1954
  if (!platform) throw new Error(`Unknown platform: ${platformId}`);
1851
- const filePath = join12(projectRoot, platform.file);
1955
+ const filePath = join13(projectRoot, platform.file);
1852
1956
  const content = generateRules(platformId);
1853
1957
  const dir = dirname6(filePath);
1854
- if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
1958
+ if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
1855
1959
  if (platform.mode === "create") {
1856
1960
  writeFileSync3(filePath, content, "utf-8");
1857
- return { file: platform.file, action: existsSync11(filePath) ? "updated" : "created" };
1961
+ return { file: platform.file, action: existsSync12(filePath) ? "updated" : "created" };
1858
1962
  }
1859
- if (!existsSync11(filePath)) {
1963
+ if (!existsSync12(filePath)) {
1860
1964
  writeFileSync3(filePath, content, "utf-8");
1861
1965
  return { file: platform.file, action: "created" };
1862
1966
  }
@@ -1878,10 +1982,41 @@ ${content}${SECTION_END}
1878
1982
  function escapeRegExp(s) {
1879
1983
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1880
1984
  }
1985
+ var SKILLS = ["ctxo-understand", "ctxo-safe-edit", "ctxo-review-pr"];
1986
+ var SKILL_TARGET_DIR = {
1987
+ "claude-code": (root, skill) => join13(root, ".claude", "skills", skill, "SKILL.md"),
1988
+ cursor: (root, skill) => join13(root, ".cursor", "rules", `${skill}.mdc`)
1989
+ };
1990
+ function skillsSourceDir() {
1991
+ let dir = import.meta.dirname;
1992
+ for (let i = 0; i < 10; i++) {
1993
+ const candidate = join13(dir, "skills");
1994
+ if (existsSync12(join13(candidate, "ctxo-safe-edit", "SKILL.md"))) return candidate;
1995
+ const parent = join13(dir, "..");
1996
+ if (parent === dir) break;
1997
+ dir = parent;
1998
+ }
1999
+ throw new Error("Could not locate ctxo skills/ directory.");
2000
+ }
2001
+ function installSkills(projectRoot, platformId) {
2002
+ const target = SKILL_TARGET_DIR[platformId];
2003
+ if (!target) return [];
2004
+ const src = skillsSourceDir();
2005
+ const out = [];
2006
+ for (const skill of SKILLS) {
2007
+ const from = join13(src, skill, "SKILL.md");
2008
+ const to = target(projectRoot, skill);
2009
+ mkdirSync4(dirname6(to), { recursive: true });
2010
+ const existed = existsSync12(to);
2011
+ writeFileSync3(to, readFileSync7(from, "utf-8"), "utf-8");
2012
+ out.push({ file: to, action: existed ? "updated" : "created" });
2013
+ }
2014
+ return out;
2015
+ }
1881
2016
  var GITIGNORE_ENTRY = ".ctxo/.cache/";
1882
2017
  function ensureGitignore(projectRoot) {
1883
- const filePath = join12(projectRoot, ".gitignore");
1884
- if (!existsSync11(filePath)) {
2018
+ const filePath = join13(projectRoot, ".gitignore");
2019
+ if (!existsSync12(filePath)) {
1885
2020
  writeFileSync3(filePath, `# Ctxo local cache (auto-rebuilt, never committed)
1886
2021
  ${GITIGNORE_ENTRY}
1887
2022
  `, "utf-8");
@@ -1916,12 +2051,16 @@ index:
1916
2051
  ignoreProjects: []
1917
2052
  # - "packages/experimental-*"
1918
2053
  # - "examples/*"
2054
+
2055
+ gate:
2056
+ enabled: true
2057
+ sensitivity: balanced # strict | balanced | lenient
1919
2058
  `;
1920
2059
  function ensureConfig(projectRoot) {
1921
- const filePath = join12(projectRoot, ".ctxo", "config.yaml");
2060
+ const filePath = join13(projectRoot, ".ctxo", "config.yaml");
1922
2061
  const dir = dirname6(filePath);
1923
- if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
1924
- if (existsSync11(filePath)) {
2062
+ if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
2063
+ if (existsSync12(filePath)) {
1925
2064
  return { file: ".ctxo/config.yaml", action: "skipped" };
1926
2065
  }
1927
2066
  writeFileSync3(filePath, DEFAULT_CONFIG, "utf-8");
@@ -1956,11 +2095,11 @@ function getMcpConfigTargets(selectedPlatformIds) {
1956
2095
  return [...targets.values()];
1957
2096
  }
1958
2097
  function ensureMcpConfig(projectRoot, target) {
1959
- const filePath = join12(projectRoot, target.file);
2098
+ const filePath = join13(projectRoot, target.file);
1960
2099
  const dir = dirname6(filePath);
1961
- if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
2100
+ if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
1962
2101
  const entry = target.extraFields ? { ...target.extraFields, ...CTXO_MCP_ENTRY } : { ...CTXO_MCP_ENTRY };
1963
- if (!existsSync11(filePath)) {
2102
+ if (!existsSync12(filePath)) {
1964
2103
  const config = { [target.serverKey]: { ctxo: entry } };
1965
2104
  writeFileSync3(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1966
2105
  return { file: target.file, action: "created" };
@@ -2076,13 +2215,13 @@ var InitCommand = class {
2076
2215
  this.projectRoot = projectRoot;
2077
2216
  }
2078
2217
  installHooks() {
2079
- const hooksDir = join13(this.projectRoot, ".git", "hooks");
2218
+ const hooksDir = join14(this.projectRoot, ".git", "hooks");
2080
2219
  mkdirSync5(hooksDir, { recursive: true });
2081
2220
  this.installHook(hooksDir, "post-commit", POST_COMMIT_CONTENT);
2082
2221
  this.installHook(hooksDir, "post-merge", POST_MERGE_CONTENT);
2083
2222
  }
2084
2223
  async run(options = {}) {
2085
- if (!existsSync12(join13(this.projectRoot, ".git"))) {
2224
+ if (!existsSync13(join14(this.projectRoot, ".git"))) {
2086
2225
  console.error('[ctxo] Not a git repository. Run "git init" first.');
2087
2226
  process.exit(1);
2088
2227
  }
@@ -2119,8 +2258,8 @@ var InitCommand = class {
2119
2258
  clack.cancel("Setup cancelled.");
2120
2259
  process.exit(0);
2121
2260
  }
2122
- const fullPath = join13(this.projectRoot, indexDir);
2123
- if (!existsSync12(fullPath)) mkdirSync5(fullPath, { recursive: true });
2261
+ const fullPath = join14(this.projectRoot, indexDir);
2262
+ if (!existsSync13(fullPath)) mkdirSync5(fullPath, { recursive: true });
2124
2263
  console.error("");
2125
2264
  }
2126
2265
  stepNum++;
@@ -2209,6 +2348,18 @@ var InitCommand = class {
2209
2348
  }
2210
2349
  console.error("");
2211
2350
  }
2351
+ let installGuard = false;
2352
+ if (!options.rulesOnly && selectedTools.includes("claude-code")) {
2353
+ const guard = await clack.confirm({
2354
+ message: pc.cyan("Install the safe-edit guard? Blocks edits to high-impact symbols until blast radius is checked."),
2355
+ initialValue: true
2356
+ });
2357
+ if (clack.isCancel(guard)) {
2358
+ clack.cancel("Setup cancelled.");
2359
+ process.exit(0);
2360
+ }
2361
+ installGuard = guard;
2362
+ }
2212
2363
  const s = clack.spinner();
2213
2364
  s.start(pc.dim("Configuring..."));
2214
2365
  const results = [];
@@ -2226,6 +2377,9 @@ var InitCommand = class {
2226
2377
  for (const toolId of selectedTools) {
2227
2378
  const result = installRules(this.projectRoot, toolId);
2228
2379
  results.push(`${pc.green("\u2713")} ${pc.bold(result.file)} ${pc.dim(result.action)}`);
2380
+ for (const sk of installSkills(this.projectRoot, toolId)) {
2381
+ results.push(`${pc.green("\u2713")} ${pc.bold(sk.file)} ${pc.dim(sk.action + " \u2014 skill")}`);
2382
+ }
2229
2383
  }
2230
2384
  const mcpTargets = getMcpConfigTargets(selectedTools);
2231
2385
  for (const target of mcpTargets) {
@@ -2238,6 +2392,10 @@ var InitCommand = class {
2238
2392
  this.installHooks();
2239
2393
  results.push(`${pc.green("\u2713")} ${pc.bold("post-commit, post-merge")} ${pc.dim("hooks installed")}`);
2240
2394
  }
2395
+ if (installGuard) {
2396
+ const r = ensureClaudeHook(this.projectRoot);
2397
+ if (r.action !== "skipped") results.push(`${pc.green("\u2713")} ${pc.bold(r.file)} ${pc.dim(r.action + " \u2014 safe-edit guard")}`);
2398
+ }
2241
2399
  if (pluginsToInstall.length > 0) {
2242
2400
  s.stop(pc.dim("Installing language plugins..."));
2243
2401
  const installer = new InstallCommand(this.projectRoot);
@@ -2277,8 +2435,8 @@ var InitCommand = class {
2277
2435
  const gitignoreResult = ensureGitignore(this.projectRoot);
2278
2436
  const configResult = ensureConfig(this.projectRoot);
2279
2437
  if (!options.rulesOnly) {
2280
- const indexDir = join13(this.projectRoot, ".ctxo", "index");
2281
- if (!existsSync12(indexDir)) mkdirSync5(indexDir, { recursive: true });
2438
+ const indexDir = join14(this.projectRoot, ".ctxo", "index");
2439
+ if (!existsSync13(indexDir)) mkdirSync5(indexDir, { recursive: true });
2282
2440
  console.error("[ctxo] \u2713 .ctxo/index/ ready");
2283
2441
  }
2284
2442
  if (configResult.action !== "skipped") {
@@ -2290,6 +2448,9 @@ var InitCommand = class {
2290
2448
  for (const toolId of toolIds) {
2291
2449
  const result = installRules(this.projectRoot, toolId);
2292
2450
  console.error(`[ctxo] \u2713 ${result.file} \u2014 ${result.action}`);
2451
+ for (const sk of installSkills(this.projectRoot, toolId)) {
2452
+ console.error(`[ctxo] \u2713 ${sk.file} \u2014 ${sk.action} (skill)`);
2453
+ }
2293
2454
  }
2294
2455
  const mcpTargets = getMcpConfigTargets(toolIds);
2295
2456
  for (const target of mcpTargets) {
@@ -2302,6 +2463,10 @@ var InitCommand = class {
2302
2463
  this.installHooks();
2303
2464
  console.error("[ctxo] \u2713 Git hooks installed");
2304
2465
  }
2466
+ if (!options.rulesOnly && toolIds.includes("claude-code")) {
2467
+ const r = ensureClaudeHook(this.projectRoot);
2468
+ if (r.action !== "skipped") console.error(`[ctxo] \u2713 ${r.file} \u2014 ${r.action} (safe-edit guard)`);
2469
+ }
2305
2470
  if (!options.rulesOnly && !options.noInstall) {
2306
2471
  const detection = detectLanguages(this.projectRoot);
2307
2472
  const needed = decideNeededLanguages(detection);
@@ -2330,7 +2495,7 @@ var InitCommand = class {
2330
2495
  for (const id of toolIds) {
2331
2496
  const platform = PLATFORMS.find((p) => p.id === id);
2332
2497
  if (platform) {
2333
- const exists = existsSync12(join13(this.projectRoot, platform.file));
2498
+ const exists = existsSync13(join14(this.projectRoot, platform.file));
2334
2499
  const action = platform.mode === "append" && exists ? "append section" : "create";
2335
2500
  console.error(` ${platform.file.padEnd(42)} (${action})`);
2336
2501
  }
@@ -2339,27 +2504,31 @@ var InitCommand = class {
2339
2504
  console.error(" .git/hooks/post-commit (git hook)");
2340
2505
  console.error(" .git/hooks/post-merge (git hook)");
2341
2506
  }
2507
+ if (!options.rulesOnly && toolIds.includes("claude-code")) {
2508
+ console.error(" .claude/settings.json (safe-edit guard hook)");
2509
+ console.error(" .claude/skills/ctxo-*/SKILL.md (skills)");
2510
+ }
2342
2511
  }
2343
2512
  /* ---------------------------------------------------------------- */
2344
2513
  /* Helpers */
2345
2514
  /* ---------------------------------------------------------------- */
2346
2515
  async getVersion() {
2347
2516
  try {
2348
- const pkgPath = join13(this.projectRoot, "node_modules", "@ctxo", "cli", "package.json");
2349
- if (existsSync12(pkgPath)) {
2517
+ const pkgPath = join14(this.projectRoot, "node_modules", "@ctxo", "cli", "package.json");
2518
+ if (existsSync13(pkgPath)) {
2350
2519
  return JSON.parse(readFileSync8(pkgPath, "utf-8")).version;
2351
2520
  }
2352
- const { createRequire: createRequire8 } = await import("module");
2353
- const require4 = createRequire8(import.meta.url);
2354
- return require4("../../package.json").version;
2521
+ const { createRequire: createRequire9 } = await import("module");
2522
+ const require5 = createRequire9(import.meta.url);
2523
+ return require5("../../package.json").version;
2355
2524
  } catch {
2356
2525
  return void 0;
2357
2526
  }
2358
2527
  }
2359
2528
  installHook(hooksDir, hookName, hookContent) {
2360
- const hookPath = join13(hooksDir, hookName);
2529
+ const hookPath = join14(hooksDir, hookName);
2361
2530
  let existing = "";
2362
- if (existsSync12(hookPath)) {
2531
+ if (existsSync13(hookPath)) {
2363
2532
  existing = readFileSync8(hookPath, "utf-8");
2364
2533
  if (existing.includes(CTXO_START)) return;
2365
2534
  } else {
@@ -2372,7 +2541,7 @@ var InitCommand = class {
2372
2541
  };
2373
2542
 
2374
2543
  // src/cli/watch-command.ts
2375
- import { join as join14, extname as extname4, relative as relative2 } from "path";
2544
+ import { join as join15, extname as extname4, relative as relative2 } from "path";
2376
2545
  import { readFileSync as readFileSync9 } from "fs";
2377
2546
 
2378
2547
  // src/adapters/watcher/chokidar-watcher-adapter.ts
@@ -2422,12 +2591,12 @@ var WatchCommand = class {
2422
2591
  ctxoRoot;
2423
2592
  constructor(projectRoot) {
2424
2593
  this.projectRoot = projectRoot;
2425
- this.ctxoRoot = join14(projectRoot, ".ctxo");
2594
+ this.ctxoRoot = join15(projectRoot, ".ctxo");
2426
2595
  }
2427
2596
  async run() {
2428
2597
  console.error("[ctxo] Starting file watcher...");
2429
2598
  const registry = new LanguageAdapterRegistry();
2430
- let roslynAdapter = null;
2599
+ const keepAliveByExt = /* @__PURE__ */ new Map();
2431
2600
  for (const { plugin, adapter } of await loadPlugins(this.projectRoot)) {
2432
2601
  if (typeof adapter.initialize === "function") {
2433
2602
  try {
@@ -2438,15 +2607,12 @@ var WatchCommand = class {
2438
2607
  }
2439
2608
  }
2440
2609
  registry.register(plugin.extensions, adapter);
2441
- const csCandidate = adapter;
2442
- if (typeof csCandidate.getRoslynDelegate === "function") {
2443
- const roslyn = csCandidate.getRoslynDelegate();
2444
- if (roslyn?.isReady()) {
2445
- const started = await roslyn.startKeepAlive();
2446
- if (started) {
2447
- roslynAdapter = roslyn;
2448
- console.error("[ctxo] C# watch: Roslyn keep-alive active (full tier, <100ms per change)");
2449
- }
2610
+ const capable = adapter;
2611
+ if (typeof capable.getIncrementalReindex === "function") {
2612
+ const cap = capable.getIncrementalReindex();
2613
+ if (cap?.isReady() && await cap.startKeepAlive()) {
2614
+ for (const ext of plugin.extensions) keepAliveByExt.set(ext.toLowerCase(), cap);
2615
+ console.error(`[ctxo] ${plugin.id} watch: keep-alive active (full tier, fast incremental re-index)`);
2450
2616
  }
2451
2617
  }
2452
2618
  console.error(`[ctxo] Plugin ${plugin.id}@${plugin.version} (${plugin.tier} tier)`);
@@ -2467,8 +2633,9 @@ var WatchCommand = class {
2467
2633
  if (!adapter) return;
2468
2634
  const relativePath = relative2(this.projectRoot, filePath).replace(/\\/g, "/");
2469
2635
  try {
2470
- if (roslynAdapter?.isReady() && filePath.endsWith(".cs")) {
2471
- const result = await roslynAdapter.reindexFile(relativePath);
2636
+ const cap = keepAliveByExt.get(extname4(filePath).toLowerCase());
2637
+ if (cap?.isReady()) {
2638
+ const result = await cap.reindexFile(relativePath);
2472
2639
  if (result) {
2473
2640
  const commits2 = await gitAdapter.getCommitHistory(relativePath);
2474
2641
  const intent2 = commits2.map((c) => ({
@@ -2504,7 +2671,7 @@ var WatchCommand = class {
2504
2671
  };
2505
2672
  writer.write(fileIndex2);
2506
2673
  storage.writeSymbolFile(fileIndex2);
2507
- console.error(`[ctxo] Re-indexed (Roslyn): ${relativePath}`);
2674
+ console.error(`[ctxo] Re-indexed (keep-alive): ${relativePath}`);
2508
2675
  return;
2509
2676
  }
2510
2677
  }
@@ -2592,8 +2759,9 @@ var WatchCommand = class {
2592
2759
  clearTimeout(timeout);
2593
2760
  }
2594
2761
  if (communitySnapshotTimer) clearTimeout(communitySnapshotTimer);
2595
- const roslynShutdown = roslynAdapter ? roslynAdapter.dispose() : Promise.resolve();
2596
- roslynShutdown.then(() => {
2762
+ const uniques = new Set(keepAliveByExt.values());
2763
+ const shutdowns = Promise.all([...uniques].map((c) => c.dispose()));
2764
+ shutdowns.then(() => {
2597
2765
  watcher.stop().then(() => {
2598
2766
  storage.close();
2599
2767
  console.error("[ctxo] Watcher stopped");
@@ -2637,21 +2805,21 @@ function readHeadSha() {
2637
2805
  }
2638
2806
 
2639
2807
  // src/cli/stats-command.ts
2640
- import { join as join15 } from "path";
2641
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
2808
+ import { join as join16 } from "path";
2809
+ import { existsSync as existsSync14, readFileSync as readFileSync10 } from "fs";
2642
2810
  import initSqlJs from "sql.js";
2643
2811
  var StatsCommand = class {
2644
2812
  ctxoRoot;
2645
2813
  constructor(projectRoot) {
2646
- this.ctxoRoot = join15(projectRoot, ".ctxo");
2814
+ this.ctxoRoot = join16(projectRoot, ".ctxo");
2647
2815
  }
2648
2816
  async run(options) {
2649
2817
  if (!this.isStatsEnabled()) {
2650
2818
  console.error("[ctxo] Stats collection is disabled in .ctxo/config.yaml");
2651
2819
  return;
2652
2820
  }
2653
- const dbPath = join15(this.ctxoRoot, ".cache", "symbols.db");
2654
- if (!existsSync13(dbPath)) {
2821
+ const dbPath = join16(this.ctxoRoot, ".cache", "symbols.db");
2822
+ if (!existsSync14(dbPath)) {
2655
2823
  if (options.json) {
2656
2824
  process.stdout.write(JSON.stringify(this.emptyReport(options.days)) + "\n");
2657
2825
  } else {
@@ -2667,8 +2835,8 @@ var StatsCommand = class {
2667
2835
  if (options.clear) {
2668
2836
  recorder.clear();
2669
2837
  const data = db.export();
2670
- const { writeFileSync: writeFileSync6 } = await import("fs");
2671
- writeFileSync6(dbPath, Buffer.from(data));
2838
+ const { writeFileSync: writeFileSync7 } = await import("fs");
2839
+ writeFileSync7(dbPath, Buffer.from(data));
2672
2840
  console.error("[ctxo] Session data cleared.");
2673
2841
  return;
2674
2842
  }
@@ -2698,8 +2866,8 @@ var StatsCommand = class {
2698
2866
  }
2699
2867
  }
2700
2868
  isStatsEnabled() {
2701
- const configPath = join15(this.ctxoRoot, "config.yaml");
2702
- if (!existsSync13(configPath)) return true;
2869
+ const configPath = join16(this.ctxoRoot, "config.yaml");
2870
+ if (!existsSync14(configPath)) return true;
2703
2871
  try {
2704
2872
  const raw = readFileSync10(configPath, "utf-8");
2705
2873
  const match = raw.match(/stats:\s*\n\s*enabled:\s*(true|false)/);
@@ -2790,8 +2958,8 @@ var StatsCommand = class {
2790
2958
  };
2791
2959
 
2792
2960
  // src/cli/doctor-command.ts
2793
- import { join as join22 } from "path";
2794
- import { existsSync as existsSync20 } from "fs";
2961
+ import { join as join24 } from "path";
2962
+ import { existsSync as existsSync22 } from "fs";
2795
2963
 
2796
2964
  // src/adapters/diagnostics/health-checker.ts
2797
2965
  var log4 = createLogger("ctxo:doctor");
@@ -3126,10 +3294,111 @@ function pluginResolves(packageName) {
3126
3294
  }
3127
3295
  }
3128
3296
 
3297
+ // src/adapters/diagnostics/checks/java-tier-check.ts
3298
+ import { createRequire as createRequire7 } from "module";
3299
+ import { existsSync as existsSync15, readdirSync as readdirSync3 } from "fs";
3300
+ import { join as join17, dirname as dirname7 } from "path";
3301
+ var require4 = createRequire7(import.meta.url);
3302
+ function evaluateJavaTier(i) {
3303
+ const id = "java_tier";
3304
+ const title = "Java analysis tier";
3305
+ if (!i.hasJava) {
3306
+ return { id, title, status: "pass", message: "no Java sources detected" };
3307
+ }
3308
+ const jreOk = (i.jreMajor ?? 0) >= 17;
3309
+ if (jreOk && i.analyzerInstalled) {
3310
+ return { id, title, status: "pass", message: `full tier (JRE ${i.jreMajor}, analyzer installed)` };
3311
+ }
3312
+ if (jreOk && !i.analyzerInstalled) {
3313
+ return {
3314
+ id,
3315
+ title,
3316
+ status: "warn",
3317
+ message: `syntax tier (JRE ${i.jreMajor} present, analyzer missing)`,
3318
+ fix: 'Run "ctxo install java --full-tier" for resolved call/use edges'
3319
+ };
3320
+ }
3321
+ if (!jreOk && i.analyzerInstalled) {
3322
+ return {
3323
+ id,
3324
+ title,
3325
+ status: "warn",
3326
+ message: "syntax tier (analyzer installed but no JRE 17+)",
3327
+ fix: "Install a JRE 17+ and ensure it is on PATH (or set JAVA_HOME)"
3328
+ };
3329
+ }
3330
+ return {
3331
+ id,
3332
+ title,
3333
+ status: "pass",
3334
+ message: 'syntax tier (install JRE 17+ and run "ctxo install java --full-tier" for full tier)'
3335
+ };
3336
+ }
3337
+ function analyzerJarPresent() {
3338
+ try {
3339
+ const pkgJson = require4.resolve("@ctxo/lang-java-analyzer/package.json");
3340
+ const jarPath = join17(dirname7(pkgJson), "jar", "ctxo-jdt-analyzer.jar");
3341
+ return existsSync15(jarPath);
3342
+ } catch {
3343
+ return false;
3344
+ }
3345
+ }
3346
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
3347
+ "node_modules",
3348
+ "target",
3349
+ "build",
3350
+ "out",
3351
+ "bin",
3352
+ ".git",
3353
+ ".ctxo",
3354
+ "dist",
3355
+ "coverage"
3356
+ ]);
3357
+ function findJavaFile(root, maxDepth) {
3358
+ function walk(dir, depth) {
3359
+ let entries;
3360
+ try {
3361
+ entries = readdirSync3(dir, { withFileTypes: true });
3362
+ } catch {
3363
+ return false;
3364
+ }
3365
+ for (const entry of entries) {
3366
+ if (entry.isSymbolicLink()) continue;
3367
+ if (entry.isFile() && entry.name.endsWith(".java")) return true;
3368
+ }
3369
+ if (depth >= maxDepth) return false;
3370
+ for (const entry of entries) {
3371
+ if (entry.isSymbolicLink()) continue;
3372
+ if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
3373
+ if (walk(join17(dir, entry.name), depth + 1)) return true;
3374
+ }
3375
+ }
3376
+ return false;
3377
+ }
3378
+ return walk(root, 0);
3379
+ }
3380
+ function projectHasJava(root) {
3381
+ for (const f of ["pom.xml", "build.gradle", "build.gradle.kts"]) {
3382
+ if (existsSync15(join17(root, f))) return true;
3383
+ }
3384
+ return findJavaFile(root, 4);
3385
+ }
3386
+ var JavaTierCheck = class {
3387
+ id = "java_tier";
3388
+ title = "Java analysis tier";
3389
+ async run(ctx) {
3390
+ return evaluateJavaTier({
3391
+ hasJava: projectHasJava(ctx.projectRoot),
3392
+ jreMajor: detectJavaMajor(),
3393
+ analyzerInstalled: analyzerJarPresent()
3394
+ });
3395
+ }
3396
+ };
3397
+
3129
3398
  // src/adapters/diagnostics/checks/git-check.ts
3130
3399
  import { execFileSync as execFileSync5 } from "child_process";
3131
- import { existsSync as existsSync14 } from "fs";
3132
- import { join as join16 } from "path";
3400
+ import { existsSync as existsSync16 } from "fs";
3401
+ import { join as join18 } from "path";
3133
3402
  var GitBinaryCheck = class {
3134
3403
  id = "git_binary";
3135
3404
  title = "Git available";
@@ -3146,7 +3415,7 @@ var GitRepoCheck = class {
3146
3415
  id = "git_repo";
3147
3416
  title = "Git repository";
3148
3417
  async run(ctx) {
3149
- if (existsSync14(join16(ctx.projectRoot, ".git"))) {
3418
+ if (existsSync16(join18(ctx.projectRoot, ".git"))) {
3150
3419
  return { id: this.id, title: this.title, status: "pass", message: ctx.projectRoot, value: ctx.projectRoot };
3151
3420
  }
3152
3421
  return { id: this.id, title: this.title, status: "fail", message: "Not a git repository", fix: 'Run "git init" or clone a repository' };
@@ -3154,9 +3423,9 @@ var GitRepoCheck = class {
3154
3423
  };
3155
3424
 
3156
3425
  // src/adapters/diagnostics/checks/index-check.ts
3157
- import { existsSync as existsSync15 } from "fs";
3426
+ import { existsSync as existsSync17 } from "fs";
3158
3427
  import { execFileSync as execFileSync6 } from "child_process";
3159
- import { join as join17 } from "path";
3428
+ import { join as join19 } from "path";
3160
3429
  function pass2(id, title, message, value) {
3161
3430
  return { id, title, status: "pass", message, value };
3162
3431
  }
@@ -3173,8 +3442,8 @@ var IndexDirectoryCheck = class {
3173
3442
  id = "index_directory";
3174
3443
  title = "Index directory";
3175
3444
  async run(ctx) {
3176
- const indexDir = join17(ctx.ctxoRoot, "index");
3177
- if (!existsSync15(indexDir)) {
3445
+ const indexDir = join19(ctx.ctxoRoot, "index");
3446
+ if (!existsSync17(indexDir)) {
3178
3447
  return fail(this.id, this.title, "No index directory", 'Run "ctxo index"');
3179
3448
  }
3180
3449
  const indices = getIndices(ctx);
@@ -3268,8 +3537,8 @@ var CoChangesCacheCheck = class {
3268
3537
  id = "co_changes_cache";
3269
3538
  title = "Co-changes cache";
3270
3539
  async run(ctx) {
3271
- const coChangesPath = join17(ctx.ctxoRoot, "index", "co-changes.json");
3272
- if (existsSync15(coChangesPath)) {
3540
+ const coChangesPath = join19(ctx.ctxoRoot, "index", "co-changes.json");
3541
+ if (existsSync17(coChangesPath)) {
3273
3542
  return pass2(this.id, this.title, "present");
3274
3543
  }
3275
3544
  return warn2(this.id, this.title, "missing (co-change analysis disabled)", 'Run "ctxo index" with git history');
@@ -3293,14 +3562,14 @@ var SchemaVersionCheck = class {
3293
3562
  };
3294
3563
 
3295
3564
  // src/adapters/diagnostics/checks/storage-check.ts
3296
- import { existsSync as existsSync16, readFileSync as readFileSync11 } from "fs";
3297
- import { join as join18 } from "path";
3565
+ import { existsSync as existsSync18, readFileSync as readFileSync11 } from "fs";
3566
+ import { join as join20 } from "path";
3298
3567
  var SqliteCacheCheck = class {
3299
3568
  id = "sqlite_cache";
3300
3569
  title = "SQLite cache";
3301
3570
  async run(ctx) {
3302
- const dbPath = join18(ctx.ctxoRoot, ".cache", "symbols.db");
3303
- if (!existsSync16(dbPath)) {
3571
+ const dbPath = join20(ctx.ctxoRoot, ".cache", "symbols.db");
3572
+ if (!existsSync18(dbPath)) {
3304
3573
  return { id: this.id, title: this.title, status: "warn", message: "SQLite cache missing", fix: 'Run "ctxo sync" to rebuild' };
3305
3574
  }
3306
3575
  try {
@@ -3322,14 +3591,14 @@ var SqliteCacheCheck = class {
3322
3591
  };
3323
3592
 
3324
3593
  // src/adapters/diagnostics/checks/config-check.ts
3325
- import { existsSync as existsSync17, readFileSync as readFileSync12 } from "fs";
3326
- import { join as join19 } from "path";
3594
+ import { existsSync as existsSync19, readFileSync as readFileSync12 } from "fs";
3595
+ import { join as join21 } from "path";
3327
3596
  var ConfigFileCheck = class {
3328
3597
  id = "config_file";
3329
3598
  title = "Config file";
3330
3599
  async run(ctx) {
3331
- const configPath = join19(ctx.ctxoRoot, "config.yaml");
3332
- if (!existsSync17(configPath)) {
3600
+ const configPath = join21(ctx.ctxoRoot, "config.yaml");
3601
+ if (!existsSync19(configPath)) {
3333
3602
  return { id: this.id, title: this.title, status: "warn", message: "No config.yaml (using defaults)" };
3334
3603
  }
3335
3604
  try {
@@ -3367,8 +3636,8 @@ var ConfigFileCheck = class {
3367
3636
  };
3368
3637
 
3369
3638
  // src/adapters/diagnostics/checks/disk-check.ts
3370
- import { existsSync as existsSync18, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
3371
- import { join as join20 } from "path";
3639
+ import { existsSync as existsSync20, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
3640
+ import { join as join22 } from "path";
3372
3641
  var DEFAULT_THRESHOLDS = { warnMb: 100, failMb: 500 };
3373
3642
  var DiskUsageCheck = class {
3374
3643
  id = "disk_usage";
@@ -3378,7 +3647,7 @@ var DiskUsageCheck = class {
3378
3647
  this.thresholds = thresholds;
3379
3648
  }
3380
3649
  async run(ctx) {
3381
- if (!existsSync18(ctx.ctxoRoot)) {
3650
+ if (!existsSync20(ctx.ctxoRoot)) {
3382
3651
  return { id: this.id, title: this.title, status: "pass", message: "no .ctxo/ directory", value: "0" };
3383
3652
  }
3384
3653
  const bytes = this.getDirSize(ctx.ctxoRoot);
@@ -3395,12 +3664,12 @@ var DiskUsageCheck = class {
3395
3664
  getDirSize(dirPath) {
3396
3665
  let total = 0;
3397
3666
  try {
3398
- const entries = readdirSync3(dirPath, { withFileTypes: true, recursive: true });
3667
+ const entries = readdirSync4(dirPath, { withFileTypes: true, recursive: true });
3399
3668
  for (const entry of entries) {
3400
3669
  if (entry.isFile()) {
3401
3670
  try {
3402
3671
  const parent = entry.parentPath ?? entry.path ?? dirPath;
3403
- total += statSync2(join20(parent, entry.name)).size;
3672
+ total += statSync2(join22(parent, entry.name)).size;
3404
3673
  } catch {
3405
3674
  }
3406
3675
  }
@@ -3413,9 +3682,9 @@ var DiskUsageCheck = class {
3413
3682
 
3414
3683
  // src/cli/doctor-fix.ts
3415
3684
  import { execFileSync as execFileSync7 } from "child_process";
3416
- import { appendFileSync, existsSync as existsSync19, mkdirSync as mkdirSync6, rmSync as rmSync2, unlinkSync as unlinkSync2 } from "fs";
3417
- import { createRequire as createRequire7 } from "module";
3418
- import { dirname as dirname7, join as join21 } from "path";
3685
+ import { appendFileSync, existsSync as existsSync21, mkdirSync as mkdirSync6, rmSync as rmSync2, unlinkSync as unlinkSync2 } from "fs";
3686
+ import { createRequire as createRequire8 } from "module";
3687
+ import { dirname as dirname8, join as join23 } from "path";
3419
3688
  async function applyFixes(report, options) {
3420
3689
  if (isCI(process.env) && !options.yes && !options.dryRun) {
3421
3690
  return {
@@ -3550,8 +3819,8 @@ async function fixIndex(projectRoot, dryRun) {
3550
3819
  }
3551
3820
  async function pruneOrphans(projectRoot, ctxoRoot, dryRun) {
3552
3821
  if (dryRun) return;
3553
- const indexDir = join21(ctxoRoot, "index");
3554
- if (!existsSync19(indexDir)) return;
3822
+ const indexDir = join23(ctxoRoot, "index");
3823
+ if (!existsSync21(indexDir)) return;
3555
3824
  const sourceFiles = listGitFiles2(projectRoot);
3556
3825
  if (sourceFiles.size === 0) return;
3557
3826
  const reader = new JsonIndexReader(ctxoRoot);
@@ -3559,9 +3828,9 @@ async function pruneOrphans(projectRoot, ctxoRoot, dryRun) {
3559
3828
  let removed = 0;
3560
3829
  for (const idx of indices) {
3561
3830
  if (sourceFiles.has(idx.file)) continue;
3562
- const jsonPath = join21(indexDir, `${idx.file}.json`);
3831
+ const jsonPath = join23(indexDir, `${idx.file}.json`);
3563
3832
  try {
3564
- if (existsSync19(jsonPath)) {
3833
+ if (existsSync21(jsonPath)) {
3565
3834
  unlinkSync2(jsonPath);
3566
3835
  removed += 1;
3567
3836
  pruneEmptyParents(jsonPath, indexDir);
@@ -3572,14 +3841,14 @@ async function pruneOrphans(projectRoot, ctxoRoot, dryRun) {
3572
3841
  if (removed === 0) return;
3573
3842
  }
3574
3843
  function pruneEmptyParents(filePath, stopAt) {
3575
- let dir = dirname7(filePath);
3844
+ let dir = dirname8(filePath);
3576
3845
  while (dir !== stopAt && dir.startsWith(stopAt)) {
3577
3846
  try {
3578
3847
  rmSync2(dir, { recursive: false });
3579
3848
  } catch {
3580
3849
  return;
3581
3850
  }
3582
- dir = dirname7(dir);
3851
+ dir = dirname8(dir);
3583
3852
  }
3584
3853
  }
3585
3854
  function listGitFiles2(projectRoot) {
@@ -3595,12 +3864,12 @@ function listGitFiles2(projectRoot) {
3595
3864
  }
3596
3865
  }
3597
3866
  async function fixSqliteCache(projectRoot, ctxoRoot, dryRun) {
3598
- const cacheDir = join21(ctxoRoot, ".cache");
3867
+ const cacheDir = join23(ctxoRoot, ".cache");
3599
3868
  if (dryRun) return;
3600
- if (existsSync19(cacheDir)) rmSync2(cacheDir, { recursive: true, force: true });
3869
+ if (existsSync21(cacheDir)) rmSync2(cacheDir, { recursive: true, force: true });
3601
3870
  await new SyncCommand(projectRoot).run();
3602
3871
  }
3603
- var doctorRequire = createRequire7(import.meta.url);
3872
+ var doctorRequire = createRequire8(import.meta.url);
3604
3873
  function canResolvePlugin(lang) {
3605
3874
  try {
3606
3875
  doctorRequire.resolve(`${officialPluginFor(lang)}/package.json`);
@@ -3614,8 +3883,8 @@ function isCI(env) {
3614
3883
  }
3615
3884
  function writeLog(ctxoRoot, attempts, halted, reason) {
3616
3885
  try {
3617
- if (!existsSync19(ctxoRoot)) mkdirSync6(ctxoRoot, { recursive: true });
3618
- const path = join21(ctxoRoot, "doctor-fix.log");
3886
+ if (!existsSync21(ctxoRoot)) mkdirSync6(ctxoRoot, { recursive: true });
3887
+ const path = join23(ctxoRoot, "doctor-fix.log");
3619
3888
  const ts = (/* @__PURE__ */ new Date()).toISOString();
3620
3889
  const lines = [`# ${ts} doctor --fix${halted ? " (halted: " + reason + ")" : ""}`];
3621
3890
  for (const a of attempts) {
@@ -3648,10 +3917,10 @@ var DoctorCommand = class {
3648
3917
  ctxoRoot;
3649
3918
  constructor(projectRoot) {
3650
3919
  this.projectRoot = projectRoot;
3651
- this.ctxoRoot = join22(projectRoot, ".ctxo");
3920
+ this.ctxoRoot = join24(projectRoot, ".ctxo");
3652
3921
  }
3653
3922
  async run(options = {}) {
3654
- const { getVersion: getVersion2 } = await import("./cli-router-CQJ7CN6E.js");
3923
+ const { getVersion: getVersion2 } = await import("./cli-router-NFAGLJ46.js");
3655
3924
  console.error(`ctxo v${getVersion2()} \u2014 health check
3656
3925
  `);
3657
3926
  const checks = [
@@ -3666,6 +3935,7 @@ var DoctorCommand = class {
3666
3935
  new TsMorphCheck(),
3667
3936
  new TreeSitterCheck(),
3668
3937
  new LanguageCoverageCheck(),
3938
+ new JavaTierCheck(),
3669
3939
  new DiskUsageCheck(),
3670
3940
  new SymbolCountCheck(),
3671
3941
  new EdgeCountCheck(),
@@ -3673,8 +3943,8 @@ var DoctorCommand = class {
3673
3943
  new CoChangesCacheCheck(),
3674
3944
  new SchemaVersionCheck()
3675
3945
  ];
3676
- const indexDir = join22(this.ctxoRoot, "index");
3677
- const indices = existsSync20(indexDir) ? new JsonIndexReader(this.ctxoRoot).readAll() : [];
3946
+ const indexDir = join24(this.ctxoRoot, "index");
3947
+ const indices = existsSync22(indexDir) ? new JsonIndexReader(this.ctxoRoot).readAll() : [];
3678
3948
  const checker = new HealthChecker(checks, { quiet: Boolean(options.quiet) });
3679
3949
  const report = await checker.runAll({
3680
3950
  projectRoot: this.projectRoot,
@@ -3715,36 +3985,35 @@ var DoctorCommand = class {
3715
3985
  };
3716
3986
 
3717
3987
  // src/cli/visualize-command.ts
3718
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync5, existsSync as existsSync21 } from "fs";
3719
- import { join as join23, basename } from "path";
3988
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync5, existsSync as existsSync23 } from "fs";
3989
+ import { join as join25, basename } from "path";
3720
3990
  import { execFile } from "child_process";
3991
+
3992
+ // src/cli/graph-loader.ts
3993
+ function loadGraph(ctxoRoot) {
3994
+ const indices = new JsonIndexReader(ctxoRoot).readAll();
3995
+ const graph = new SymbolGraph();
3996
+ for (const fi of indices) for (const sym of fi.symbols) graph.addNode(sym);
3997
+ for (const fi of indices) for (const edge of fi.edges) graph.addEdge(edge);
3998
+ return { graph, indices };
3999
+ }
4000
+
4001
+ // src/cli/visualize-command.ts
3721
4002
  var VisualizeCommand = class {
3722
4003
  constructor(projectRoot) {
3723
4004
  this.projectRoot = projectRoot;
3724
- this.ctxoRoot = join23(projectRoot, ".ctxo");
4005
+ this.ctxoRoot = join25(projectRoot, ".ctxo");
3725
4006
  }
3726
4007
  projectRoot;
3727
4008
  ctxoRoot;
3728
4009
  async run(options = {}) {
3729
- const reader = new JsonIndexReader(this.ctxoRoot);
3730
- const indices = reader.readAll();
4010
+ const { graph, indices } = loadGraph(this.ctxoRoot);
3731
4011
  if (indices.length === 0) {
3732
4012
  console.error('[ctxo] No index found. Run "ctxo index" first.');
3733
4013
  process.exit(1);
3734
4014
  return;
3735
4015
  }
3736
4016
  console.error(`[ctxo] Building graph from ${indices.length} files...`);
3737
- const graph = new SymbolGraph();
3738
- for (const fileIndex of indices) {
3739
- for (const sym of fileIndex.symbols) {
3740
- graph.addNode(sym);
3741
- }
3742
- }
3743
- for (const fileIndex of indices) {
3744
- for (const edge of fileIndex.edges) {
3745
- graph.addEdge(edge);
3746
- }
3747
- }
3748
4017
  console.error(`[ctxo] Graph: ${graph.nodeCount} symbols, ${graph.edgeCount} edges`);
3749
4018
  const pageRank = new PageRankCalculator();
3750
4019
  const prResult = pageRank.calculate(graph, { limit: 1e5 });
@@ -3876,7 +4145,7 @@ var VisualizeCommand = class {
3876
4145
  const templatePath = this.getTemplatePath();
3877
4146
  const template = readFileSync13(templatePath, "utf-8");
3878
4147
  const html = template.replace("/*__CTXO_DATA__*/null", JSON.stringify(payload));
3879
- const outputPath = options.output ?? join23(this.ctxoRoot, "visualize.html");
4148
+ const outputPath = options.output ?? join25(this.ctxoRoot, "visualize.html");
3880
4149
  writeFileSync5(outputPath, html, "utf-8");
3881
4150
  console.error(`[ctxo] Wrote visualization to ${outputPath}`);
3882
4151
  console.error(`[ctxo] ${nodes.length} symbols, ${edges.length} edges, ${deadResult.deadSymbols.length} dead`);
@@ -3887,9 +4156,9 @@ var VisualizeCommand = class {
3887
4156
  getTemplatePath() {
3888
4157
  let dir = import.meta.dirname;
3889
4158
  for (let i = 0; i < 10; i++) {
3890
- const candidate = join23(dir, "pages", "visualize-template.html");
3891
- if (existsSync21(candidate)) return candidate;
3892
- const parent = join23(dir, "..");
4159
+ const candidate = join25(dir, "pages", "visualize-template.html");
4160
+ if (existsSync23(candidate)) return candidate;
4161
+ const parent = join25(dir, "..");
3893
4162
  if (parent === dir) break;
3894
4163
  dir = parent;
3895
4164
  }
@@ -3913,8 +4182,8 @@ var VisualizeCommand = class {
3913
4182
  };
3914
4183
 
3915
4184
  // src/cli/update-command.ts
3916
- import { existsSync as existsSync22, readFileSync as readFileSync14 } from "fs";
3917
- import { join as join24 } from "path";
4185
+ import { existsSync as existsSync24, readFileSync as readFileSync14 } from "fs";
4186
+ import { join as join26 } from "path";
3918
4187
 
3919
4188
  // src/core/update/registry-client.ts
3920
4189
  import { request } from "https";
@@ -4232,8 +4501,8 @@ var UpdateCommand = class {
4232
4501
  return this.projectHasCtxoDep() ? "execute" : "print";
4233
4502
  }
4234
4503
  projectHasCtxoDep() {
4235
- const pkg = join24(this.projectRoot, "package.json");
4236
- if (!existsSync22(pkg)) return false;
4504
+ const pkg = join26(this.projectRoot, "package.json");
4505
+ if (!existsSync24(pkg)) return false;
4237
4506
  try {
4238
4507
  const json = JSON.parse(readFileSync14(pkg, "utf-8"));
4239
4508
  const merged = { ...json.dependencies ?? {}, ...json.devDependencies ?? {} };
@@ -4269,20 +4538,326 @@ function isLockedCI2(env) {
4269
4538
  return env["CI"] === "true" || env["CI"] === "1";
4270
4539
  }
4271
4540
 
4541
+ // src/cli/blast-radius-command.ts
4542
+ import { join as join27 } from "path";
4543
+ var BlastRadiusCommand = class {
4544
+ ctxoRoot;
4545
+ constructor(projectRoot) {
4546
+ this.ctxoRoot = join27(projectRoot, ".ctxo");
4547
+ }
4548
+ run(options) {
4549
+ const { graph } = loadGraph(this.ctxoRoot);
4550
+ if (!graph.hasNode(options.symbolId)) {
4551
+ process.stdout.write(JSON.stringify({ found: false, symbolId: options.symbolId }) + "\n");
4552
+ return;
4553
+ }
4554
+ const result = new BlastRadiusCalculator().calculate(graph, options.symbolId);
4555
+ const payload = {
4556
+ found: true,
4557
+ symbolId: options.symbolId,
4558
+ directDependentsCount: result.directDependentsCount,
4559
+ confirmedCount: result.confirmedCount,
4560
+ likelyCount: result.likelyCount,
4561
+ potentialCount: result.potentialCount,
4562
+ overallRiskScore: result.overallRiskScore,
4563
+ impactedSymbols: result.impactedSymbols.map((s) => ({
4564
+ symbolId: s.symbolId,
4565
+ confidence: s.confidence,
4566
+ depth: s.depth
4567
+ }))
4568
+ };
4569
+ process.stdout.write(JSON.stringify(payload) + "\n");
4570
+ }
4571
+ };
4572
+
4573
+ // src/cli/gate-hook-command.ts
4574
+ import { join as join29, relative as relative3, sep as sep2 } from "path";
4575
+ import { existsSync as existsSync26, readFileSync as readFileSync16 } from "fs";
4576
+
4577
+ // src/core/gate/gate-config.ts
4578
+ var DIAL = {
4579
+ strict: { percentile: 30, minDependents: 2 },
4580
+ balanced: { percentile: 15, minDependents: 3 },
4581
+ lenient: { percentile: 5, minDependents: 5 }
4582
+ };
4583
+ function resolveGateThresholds(gate) {
4584
+ const dial = DIAL[gate?.sensitivity ?? "balanced"];
4585
+ return {
4586
+ enabled: gate?.enabled ?? true,
4587
+ percentile: gate?.percentile ?? dial.percentile,
4588
+ minDependents: gate?.minDependents ?? dial.minDependents
4589
+ };
4590
+ }
4591
+
4592
+ // src/core/gate/symbol-locator.ts
4593
+ function lineOfSnippet(source, snippet) {
4594
+ const idx = source.indexOf(snippet);
4595
+ if (idx === -1) return null;
4596
+ let line = 0;
4597
+ for (let i = 0; i < idx; i++) {
4598
+ if (source.codePointAt(i) === 10) line++;
4599
+ }
4600
+ return line;
4601
+ }
4602
+ function locateSymbolAtLine(fileIndex, line) {
4603
+ let best = null;
4604
+ for (const s of fileIndex.symbols) {
4605
+ if (line >= s.startLine && line <= s.endLine) {
4606
+ const span = s.endLine - s.startLine;
4607
+ if (best === null || span < best.span) best = { id: s.symbolId, span };
4608
+ }
4609
+ }
4610
+ return best?.id ?? null;
4611
+ }
4612
+
4613
+ // src/core/gate/transcript-scan.ts
4614
+ function fileAndName(symbolId) {
4615
+ const [file, name] = symbolId.split("::");
4616
+ return `${file}::${name}`;
4617
+ }
4618
+ function transcriptHasBlastCheck(lines, symbolId) {
4619
+ const target = fileAndName(symbolId);
4620
+ for (const line of lines) {
4621
+ if (!line.includes("get_blast_radius")) continue;
4622
+ let entry;
4623
+ try {
4624
+ entry = JSON.parse(line);
4625
+ } catch {
4626
+ continue;
4627
+ }
4628
+ const content = entry?.message?.content;
4629
+ if (!Array.isArray(content)) continue;
4630
+ for (const block of content) {
4631
+ const b = block;
4632
+ if (b?.type === "tool_use" && typeof b.name === "string" && b.name.includes("get_blast_radius")) {
4633
+ const id = b.input?.symbolId;
4634
+ if (typeof id === "string" && fileAndName(id) === target) return true;
4635
+ }
4636
+ }
4637
+ }
4638
+ return false;
4639
+ }
4640
+
4641
+ // src/adapters/gate/gate-session-store.ts
4642
+ import { existsSync as existsSync25, mkdirSync as mkdirSync7, readFileSync as readFileSync15, writeFileSync as writeFileSync6 } from "fs";
4643
+ import { join as join28 } from "path";
4644
+ var log7 = createLogger("ctxo:gate-store");
4645
+ var GateSessionStore = class {
4646
+ constructor(cacheDir) {
4647
+ this.cacheDir = cacheDir;
4648
+ }
4649
+ cacheDir;
4650
+ file(sessionId) {
4651
+ const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
4652
+ return join28(this.cacheDir, "gate", `${safe}.json`);
4653
+ }
4654
+ read(sessionId) {
4655
+ const f = this.file(sessionId);
4656
+ if (!existsSync25(f)) return /* @__PURE__ */ new Set();
4657
+ try {
4658
+ return new Set(JSON.parse(readFileSync15(f, "utf-8")));
4659
+ } catch (err) {
4660
+ log7.error(`${err.message}`);
4661
+ return /* @__PURE__ */ new Set();
4662
+ }
4663
+ }
4664
+ hasWarned(sessionId, symbolId) {
4665
+ return this.read(sessionId).has(symbolId);
4666
+ }
4667
+ recordWarned(sessionId, symbolId) {
4668
+ try {
4669
+ const set = this.read(sessionId);
4670
+ set.add(symbolId);
4671
+ const f = this.file(sessionId);
4672
+ mkdirSync7(join28(this.cacheDir, "gate"), { recursive: true });
4673
+ writeFileSync6(f, JSON.stringify([...set]), "utf-8");
4674
+ } catch (err) {
4675
+ log7.error(`${err.message}`);
4676
+ }
4677
+ }
4678
+ };
4679
+
4680
+ // src/core/gate/percentile.ts
4681
+ function percentileCutoff(scores, topPercent) {
4682
+ if (scores.length === 0) return Infinity;
4683
+ const sorted = [...scores].sort((a, b) => a - b);
4684
+ const rank = Math.ceil((100 - topPercent) / 100 * sorted.length);
4685
+ const idx = Math.min(Math.max(rank, 0), sorted.length - 1);
4686
+ return sorted[idx];
4687
+ }
4688
+
4689
+ // src/core/gate/gate-decision.ts
4690
+ var MAX_TOP_DEPENDENTS = 8;
4691
+ function shouldBlock(input, t) {
4692
+ if (!t.enabled) return false;
4693
+ return input.riskCount >= t.minDependents && input.importanceScore >= input.importanceCutoff;
4694
+ }
4695
+ function formatBlockReason(symbolName, riskCount, topDependents) {
4696
+ const list = topDependents.slice(0, MAX_TOP_DEPENDENTS).map((d) => ` - ${d}`).join("\n");
4697
+ return [
4698
+ `\u26A0 ctxo guard: "${symbolName}" is a high-impact symbol - ${riskCount} confirmed/likely dependents may break.`,
4699
+ topDependents.length > 0 ? `Top dependents:
4700
+ ${list}` : "",
4701
+ `Call get_blast_radius / get_why_context before editing. This guard fires once per symbol per session - re-issue the edit to proceed.`
4702
+ ].filter(Boolean).join("\n");
4703
+ }
4704
+
4705
+ // src/cli/gate-hook-command.ts
4706
+ var log8 = createLogger("ctxo:gate-hook");
4707
+ var RANK_ALL = 1e5;
4708
+ var GateHookCommand = class {
4709
+ constructor(projectRoot) {
4710
+ this.projectRoot = projectRoot;
4711
+ this.ctxoRoot = join29(projectRoot, ".ctxo");
4712
+ this.cacheDir = join29(this.ctxoRoot, ".cache");
4713
+ }
4714
+ projectRoot;
4715
+ ctxoRoot;
4716
+ cacheDir;
4717
+ /** Pure-ish decision: never throws (fail-open). */
4718
+ evaluate(payload) {
4719
+ try {
4720
+ if (payload.tool_name !== "Edit") return { block: false };
4721
+ const filePath = payload.tool_input?.file_path;
4722
+ const oldString = payload.tool_input?.old_string;
4723
+ if (!filePath || !oldString) return { block: false };
4724
+ const loaded = loadConfig(this.ctxoRoot);
4725
+ const thresholds = resolveGateThresholds(loaded.config.gate);
4726
+ if (!thresholds.enabled) return { block: false };
4727
+ const relPath = relative3(this.projectRoot, filePath).split(sep2).join("/");
4728
+ const indexFile = join29(this.ctxoRoot, "index", `${relPath}.json`);
4729
+ if (!existsSync26(indexFile)) return { block: false };
4730
+ const fileIndex = new JsonIndexReader(this.ctxoRoot).readSingle(indexFile);
4731
+ if (!fileIndex) return { block: false };
4732
+ if (!existsSync26(filePath)) return { block: false };
4733
+ const source = readFileSync16(filePath, "utf-8");
4734
+ const line = lineOfSnippet(source, oldString);
4735
+ if (line === null) return { block: false };
4736
+ const symbolId = locateSymbolAtLine(fileIndex, line);
4737
+ if (!symbolId) return { block: false };
4738
+ const store = new GateSessionStore(this.cacheDir);
4739
+ if (store.hasWarned(payload.session_id, symbolId)) return { block: false };
4740
+ if (payload.transcript_path && existsSync26(payload.transcript_path)) {
4741
+ const lines = readFileSync16(payload.transcript_path, "utf-8").split("\n").filter(Boolean);
4742
+ if (transcriptHasBlastCheck(lines, symbolId)) return { block: false };
4743
+ }
4744
+ const { graph } = loadGraph(this.ctxoRoot);
4745
+ if (!graph.hasNode(symbolId)) return { block: false };
4746
+ const blast = new BlastRadiusCalculator().calculate(graph, symbolId);
4747
+ const riskCount = blast.confirmedCount + blast.likelyCount;
4748
+ const pr = new PageRankCalculator().calculate(graph, { limit: RANK_ALL });
4749
+ const prMap = new Map(pr.rankings.map((r) => [r.symbolId, r.score]));
4750
+ const cutoff = percentileCutoff([...prMap.values()], thresholds.percentile);
4751
+ const importanceScore = prMap.get(symbolId) ?? 0;
4752
+ const block = shouldBlock({ riskCount, importanceScore, importanceCutoff: cutoff }, thresholds);
4753
+ if (!block) return { block: false };
4754
+ store.recordWarned(payload.session_id, symbolId);
4755
+ const name = symbolId.split("::")[1] ?? symbolId;
4756
+ const top = blast.impactedSymbols.filter((s) => s.confidence !== "potential").map((s) => s.symbolId);
4757
+ return { block: true, reason: formatBlockReason(name, riskCount, top) };
4758
+ } catch (err) {
4759
+ log8.error(`${err.message}`);
4760
+ return { block: false };
4761
+ }
4762
+ }
4763
+ async run() {
4764
+ try {
4765
+ const payload = await this.readStdin();
4766
+ if (payload) {
4767
+ const decision = this.evaluate(payload);
4768
+ if (decision.block) this.emitBlock(decision.reason ?? "ctxo guard");
4769
+ }
4770
+ } catch (err) {
4771
+ log8.error(`${err.message}`);
4772
+ }
4773
+ process.exit(0);
4774
+ }
4775
+ /** PreToolUse block contract (verified): exit 0 + stdout JSON deny. */
4776
+ emitBlock(reason) {
4777
+ process.stdout.write(JSON.stringify({
4778
+ hookSpecificOutput: {
4779
+ hookEventName: "PreToolUse",
4780
+ permissionDecision: "deny",
4781
+ permissionDecisionReason: reason
4782
+ }
4783
+ }) + "\n");
4784
+ process.exit(0);
4785
+ }
4786
+ readStdin() {
4787
+ return new Promise((resolve4) => {
4788
+ let data = "";
4789
+ process.stdin.setEncoding("utf-8");
4790
+ process.stdin.on("data", (c) => {
4791
+ data += c;
4792
+ });
4793
+ process.stdin.on("end", () => {
4794
+ try {
4795
+ resolve4(JSON.parse(data));
4796
+ } catch {
4797
+ resolve4(null);
4798
+ }
4799
+ });
4800
+ process.stdin.on("error", () => resolve4(null));
4801
+ });
4802
+ }
4803
+ };
4804
+
4805
+ // src/cli/gate-command.ts
4806
+ import { join as join30 } from "path";
4807
+ var RANK_ALL2 = 1e5;
4808
+ var GateCommand = class {
4809
+ ctxoRoot;
4810
+ constructor(projectRoot) {
4811
+ this.ctxoRoot = join30(projectRoot, ".ctxo");
4812
+ }
4813
+ preview(options = {}) {
4814
+ const gate = loadConfig(this.ctxoRoot).config.gate;
4815
+ const thresholds = resolveGateThresholds(gate);
4816
+ const { graph } = loadGraph(this.ctxoRoot);
4817
+ const pr = new PageRankCalculator().calculate(graph, { limit: RANK_ALL2 });
4818
+ const prMap = new Map(pr.rankings.map((r) => [r.symbolId, r.score]));
4819
+ const cutoff = percentileCutoff([...prMap.values()], thresholds.percentile);
4820
+ const calc = new BlastRadiusCalculator();
4821
+ const firing = [];
4822
+ for (const [symbolId, importance] of prMap) {
4823
+ if (importance < cutoff) continue;
4824
+ const blast = calc.calculate(graph, symbolId);
4825
+ const riskCount = blast.confirmedCount + blast.likelyCount;
4826
+ if (shouldBlock({ riskCount, importanceScore: importance, importanceCutoff: cutoff }, thresholds)) {
4827
+ firing.push({ symbolId, riskCount, importance: Math.round(importance * 1e4) / 1e4 });
4828
+ }
4829
+ }
4830
+ firing.sort((a, b) => b.riskCount - a.riskCount);
4831
+ const payload = {
4832
+ sensitivity: gate?.sensitivity ?? "balanced",
4833
+ thresholds,
4834
+ totalSymbols: graph.nodeCount,
4835
+ wouldFireCount: firing.length,
4836
+ symbols: firing.slice(0, 50)
4837
+ };
4838
+ if (options.json) {
4839
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
4840
+ return;
4841
+ }
4842
+ console.error(`[ctxo] gate preview (${payload.sensitivity}): ${firing.length}/${graph.nodeCount} symbols would gate.`);
4843
+ for (const f of firing.slice(0, 25)) console.error(` ${f.riskCount} dep ${f.symbolId}`);
4844
+ }
4845
+ };
4846
+
4272
4847
  // src/cli/cli-router.ts
4273
4848
  function getVersion() {
4274
4849
  let dir = import.meta.dirname;
4275
4850
  for (let i = 0; i < 10; i++) {
4276
- const pkg = join25(dir, "package.json");
4277
- if (existsSync23(pkg)) {
4851
+ const pkg = join31(dir, "package.json");
4852
+ if (existsSync27(pkg)) {
4278
4853
  try {
4279
- const json = JSON.parse(readFileSync15(pkg, "utf-8"));
4854
+ const json = JSON.parse(readFileSync17(pkg, "utf-8"));
4280
4855
  return json.version ?? "unknown";
4281
4856
  } catch {
4282
4857
  break;
4283
4858
  }
4284
4859
  }
4285
- const parent = join25(dir, "..");
4860
+ const parent = join31(dir, "..");
4286
4861
  if (parent === dir) break;
4287
4862
  dir = parent;
4288
4863
  }
@@ -4291,8 +4866,8 @@ function getVersion() {
4291
4866
  function locateGitRoot(startDir) {
4292
4867
  let dir = startDir;
4293
4868
  for (let i = 0; i < 12; i++) {
4294
- if (existsSync23(join25(dir, ".git"))) return dir;
4295
- const parent = dirname8(dir);
4869
+ if (existsSync27(join31(dir, ".git"))) return dir;
4870
+ const parent = dirname9(dir);
4296
4871
  if (parent === dir) break;
4297
4872
  dir = parent;
4298
4873
  }
@@ -4409,6 +4984,8 @@ var CliRouter = class {
4409
4984
  else if (a === "--global" || a === "-g") flagValues["global"] = true;
4410
4985
  else if (a === "--dry-run") flagValues["dryRun"] = true;
4411
4986
  else if (a === "--force") flagValues["force"] = true;
4987
+ else if (a === "--full-tier") flagValues["fullTier"] = true;
4988
+ else if (a === "--syntax-only") flagValues["syntaxOnly"] = true;
4412
4989
  else if (a === "--pm") {
4413
4990
  const next = args[++i];
4414
4991
  if (!next) {
@@ -4439,6 +5016,8 @@ var CliRouter = class {
4439
5016
  global: flagValues["global"] === true,
4440
5017
  dryRun: flagValues["dryRun"] === true,
4441
5018
  force: flagValues["force"] === true,
5019
+ fullTier: flagValues["fullTier"] === true,
5020
+ syntaxOnly: flagValues["syntaxOnly"] === true,
4442
5021
  pm: typeof flagValues["pm"] === "string" ? flagValues["pm"] : void 0,
4443
5022
  version: typeof flagValues["version"] === "string" ? flagValues["version"] : void 0
4444
5023
  });
@@ -4481,6 +5060,28 @@ var CliRouter = class {
4481
5060
  });
4482
5061
  break;
4483
5062
  }
5063
+ case "gate": {
5064
+ if (args.includes("--preview")) {
5065
+ new GateCommand(this.projectRoot).preview({ json: args.includes("--json") });
5066
+ } else {
5067
+ console.error("[ctxo] usage: ctxo gate --preview [--json]");
5068
+ process.exit(1);
5069
+ }
5070
+ break;
5071
+ }
5072
+ case "gate-hook":
5073
+ await new GateHookCommand(this.projectRoot).run();
5074
+ break;
5075
+ case "blast-radius": {
5076
+ const symbolId = args[1];
5077
+ if (!symbolId || symbolId.startsWith("--")) {
5078
+ console.error("[ctxo] usage: ctxo blast-radius <symbolId> [--json]");
5079
+ process.exit(1);
5080
+ return;
5081
+ }
5082
+ new BlastRadiusCommand(this.projectRoot).run({ symbolId, json: args.includes("--json") });
5083
+ break;
5084
+ }
4484
5085
  case "visualize": {
4485
5086
  const outputIdx = args.indexOf("--output");
4486
5087
  const outputArg = outputIdx !== -1 ? args[outputIdx + 1] : void 0;
@@ -4535,6 +5136,9 @@ Usage:
4535
5136
  ctxo update --json Machine-readable output
4536
5137
  ctxo update --pm <pm> Override package manager (npm|pnpm|yarn|bun)
4537
5138
  ctxo update --force Bypass CI lockfile guard
5139
+ ctxo blast-radius <symbolId> Blast radius for one symbol (--json for machine output)
5140
+ ctxo gate --preview Preview which symbols the safe-edit guard would block
5141
+ ctxo gate --preview --json Same, machine-readable output
4538
5142
  ctxo --version Print core version
4539
5143
  ctxo --version --verbose Print core + plugins + runtime
4540
5144
  ctxo --version --json Machine-readable version payload
@@ -4548,4 +5152,4 @@ export {
4548
5152
  getVersion,
4549
5153
  locateGitRoot
4550
5154
  };
4551
- //# sourceMappingURL=cli-router-CQJ7CN6E.js.map
5155
+ //# sourceMappingURL=cli-router-NFAGLJ46.js.map