@ctxo/cli 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,7 +36,7 @@ It's not a skill gap. It's a **sensory gap**. The agent is navigating your codeb
36
36
 
37
37
  The core shift: your agent stops reacting to files it stumbles into and starts planning from a complete map. **Blast radius before the edit. Git intent before the bug fix. Importer list before the rename.**
38
38
 
39
- Ctxo indexes your repo once, kept fresh by file watchers and git hooks, into a deterministic graph: every symbol, every edge (imports, calls, extends, implements, uses, and the data edges reads, writes and executes), every relevant git commit with intent classified, every anti-pattern. All exposed through 14 semantic MCP tools.
39
+ Ctxo indexes your repo once, kept fresh by file watchers and git hooks, into a deterministic graph: every symbol, every edge (imports, calls, extends, implements, uses, and the data edges reads, writes and executes), every relevant git commit with intent classified, every anti-pattern. All exposed through 14 semantic MCP tools, plus 2 domain tools with `@ctxo/domain`.
40
40
 
41
41
  One `get_blast_radius` call replaces an entire ripgrep/read spiral. One `get_pr_impact` replaces a full review session of "wait, what calls this?"
42
42
 
@@ -141,6 +141,30 @@ ctxo contexts --json # the full contexts-view.json
141
141
 
142
142
  Agents get the same view from `get_architectural_overlay(mode: "contexts")`, and one candidate in full with `candidate: <id>`.
143
143
 
144
+ ## Cards that go stale
145
+
146
+ An agent changes the lending rules in code, and the card that describes them stays as it was. Name your bounded contexts in a registry, and Ctxo checks the structure, never the meaning: a context's code changed in the session without a card update or a `Domain-Unchanged` commit trailer, a rule ID that no card defines, a card link that no longer resolves, an edge that crosses a context boundary outside its contracts. The opt-in `@ctxo/domain` package adds the commands and a Claude Code guard; Ctxo never writes the cards.
147
+
148
+ ```shell
149
+ ctxo install domain # the opt-in @ctxo/domain package
150
+ ctxo init --domain # registry skeleton, fold guard, skill, domain config
151
+ ctxo contexts-touched --since origin/main # which named contexts a branch changed, and which are resolved
152
+ ctxo verify-domain --since origin/main # CI gate: registry, cards, rule IDs, loader rules, boundaries
153
+ ctxo domain rules --write # path-scoped rules that point the agent at the card
154
+ ctxo domain discover # propose tracker prefixes and test conventions from the repo
155
+ ```
156
+
157
+ With cards in place, `ctxo index` links each rule to the tests that pin it and the code that implements it, and the agent sees the rules while it works: `get_blast_radius` lists the rules a change may break and the cards to update, `get_why_context` the decisions behind them, `get_context_for_task` a card excerpt, `get_pr_impact` the contexts touched and the rules without tests. Two tools answer domain questions directly:
158
+
159
+ | Tool | Answers |
160
+ | --- | --- |
161
+ | `get_domain_context` | The card of the context that owns a symbol or file: terms, rules with their tests and code, contracts, decisions |
162
+ | `get_rule` | One rule by ID: the tests that pin it (and how each was found), the symbols that implement it, the decisions behind it |
163
+
164
+ `get_ranked_context` with `scope: "domain"` searches the cards and the documents they link to.
165
+
166
+ See [Ctxo domain conventions v1](docs/features/domain-knowledge.md#ctxo-domain-conventions-v1) for the registry, the card template and the adoption ladder.
167
+
144
168
  ## Supported Languages
145
169
 
146
170
  | Language | Plugin | Tier | Notes |
@@ -156,6 +180,7 @@ Install via the CLI shortcut:
156
180
  ```bash
157
181
  ctxo install typescript go csharp # syntax or full tier auto-detected
158
182
  ctxo install sql # T-SQL plugin (alias tsql)
183
+ ctxo install domain # opt-in domain knowledge layer (@ctxo/domain)
159
184
  ctxo install java # installs syntax tier; adds full-tier analyzer when JRE 11+ detected
160
185
  ctxo install java --full-tier # force full-tier analyzer install
161
186
  ctxo install java --syntax-only # skip analyzer, syntax tier only
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createLogger
4
+ } from "./chunk-5DSKZJNN.js";
5
+
6
+ // src/adapters/language/plugin-discovery.ts
7
+ import { existsSync, readFileSync, realpathSync } from "fs";
8
+ import { createRequire } from "module";
9
+ import { dirname, isAbsolute, join, resolve } from "path";
10
+ import { pathToFileURL } from "url";
11
+ import { SUPPORTED_API_VERSIONS } from "@ctxo/plugin-api";
12
+ var log = createLogger("ctxo:plugin-discovery");
13
+ var OFFICIAL_PREFIX = "@ctxo/lang-";
14
+ var COMMUNITY_PREFIX = "ctxo-lang-";
15
+ function readManifestDeps(manifestPath) {
16
+ try {
17
+ const raw = readFileSync(manifestPath, "utf-8");
18
+ const manifest = JSON.parse(raw);
19
+ const all = {
20
+ ...manifest.dependencies,
21
+ ...manifest.devDependencies,
22
+ ...manifest.peerDependencies
23
+ };
24
+ return Object.keys(all).filter(
25
+ (name) => name.startsWith(OFFICIAL_PREFIX) || name.startsWith(COMMUNITY_PREFIX)
26
+ );
27
+ } catch (err) {
28
+ log.warn(`Failed to read manifest ${manifestPath}: ${err.message}`);
29
+ return [];
30
+ }
31
+ }
32
+ function isLocalSpecifier(spec) {
33
+ return spec.startsWith(".") || spec.startsWith("/") || isAbsolute(spec);
34
+ }
35
+ function pickEsmEntry(exportsField) {
36
+ if (!exportsField) return void 0;
37
+ if (typeof exportsField === "string") return exportsField;
38
+ if (typeof exportsField !== "object") return void 0;
39
+ const table = exportsField;
40
+ const dot = "." in table ? table["."] : exportsField;
41
+ if (typeof dot === "string") return dot;
42
+ if (!dot || typeof dot !== "object") return void 0;
43
+ for (const key of ["import", "node", "default"]) {
44
+ const nested = dot[key];
45
+ if (typeof nested === "string") return nested;
46
+ if (nested && typeof nested === "object") {
47
+ for (const inner of ["default", "import", "node"]) {
48
+ const v = nested[inner];
49
+ if (typeof v === "string") return v;
50
+ }
51
+ }
52
+ }
53
+ return void 0;
54
+ }
55
+ var cliFallbackRequire = createRequire(import.meta.url);
56
+ function resolvePluginPackageJson(specifier, projectRequire) {
57
+ try {
58
+ return projectRequire.resolve(`${specifier}/package.json`);
59
+ } catch (primary) {
60
+ try {
61
+ return cliFallbackRequire.resolve(`${specifier}/package.json`);
62
+ } catch {
63
+ throw primary;
64
+ }
65
+ }
66
+ }
67
+ function readPackageJsonVia(specifier, req) {
68
+ const files = [];
69
+ try {
70
+ files.push(req.resolve(`${specifier}/package.json`));
71
+ } catch {
72
+ for (const base of req.resolve.paths(specifier) ?? []) {
73
+ files.push(join(base, specifier, "package.json"));
74
+ }
75
+ }
76
+ for (const file of files) {
77
+ try {
78
+ return { pkgDir: dirname(file), raw: readFileSync(file, "utf-8") };
79
+ } catch {
80
+ }
81
+ }
82
+ return void 0;
83
+ }
84
+ function resolvePackageCandidates(specifier, projectRoot, cliAnchor) {
85
+ const lookups = [
86
+ { source: "project", req: createRequire(join(projectRoot, "package.json")) },
87
+ {
88
+ source: "cli",
89
+ req: cliAnchor === void 0 ? cliFallbackRequire : createRequire(join(cliAnchor, "package.json"))
90
+ }
91
+ ];
92
+ const found = [];
93
+ const seen = /* @__PURE__ */ new Set();
94
+ for (const { source, req } of lookups) {
95
+ const read = readPackageJsonVia(specifier, req);
96
+ if (!read) continue;
97
+ try {
98
+ const real = realpathSync(read.pkgDir);
99
+ if (seen.has(real)) continue;
100
+ seen.add(real);
101
+ const pkg = JSON.parse(read.raw);
102
+ const entry = pickEsmEntry(pkg.exports) ?? pkg.module ?? pkg.main ?? "index.js";
103
+ found.push({
104
+ name: pkg.name ?? specifier,
105
+ version: pkg.version ?? "0.0.0",
106
+ pkgDir: real,
107
+ entryUrl: pathToFileURL(resolve(real, entry)).href,
108
+ source
109
+ });
110
+ } catch (err) {
111
+ log.warn(`Skipped ${specifier} candidate in ${read.pkgDir}: ${err.message}`);
112
+ }
113
+ }
114
+ return found;
115
+ }
116
+ function resolvePluginEntryUrl(specifier, manifestRequire) {
117
+ const pkgJsonPath = resolvePluginPackageJson(specifier, manifestRequire);
118
+ const pkgDir = dirname(pkgJsonPath);
119
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
120
+ const entry = pickEsmEntry(pkgJson.exports) ?? pkgJson.module ?? pkgJson.main ?? "index.js";
121
+ return pathToFileURL(resolve(pkgDir, entry)).href;
122
+ }
123
+ async function tryLoad(specifier, baseDir, manifestRequire) {
124
+ try {
125
+ const target = isLocalSpecifier(specifier) ? pathToFileURL(resolve(baseDir, specifier)).href : resolvePluginEntryUrl(specifier, manifestRequire);
126
+ const mod = await import(target);
127
+ const candidate = mod.default ?? mod.plugin ?? mod;
128
+ if (!candidate || typeof candidate !== "object") {
129
+ return { specifier, reason: "module export is not an object" };
130
+ }
131
+ if (!SUPPORTED_API_VERSIONS.includes(candidate.apiVersion)) {
132
+ return {
133
+ specifier,
134
+ reason: `unsupported apiVersion ${String(candidate.apiVersion)} (supported: ${SUPPORTED_API_VERSIONS.join(", ")})`
135
+ };
136
+ }
137
+ if (typeof candidate.createAdapter !== "function") {
138
+ return { specifier, reason: "missing createAdapter()" };
139
+ }
140
+ if (typeof candidate.id !== "string" || candidate.id.length === 0) {
141
+ return { specifier, reason: "missing or empty id" };
142
+ }
143
+ if (!Array.isArray(candidate.extensions)) {
144
+ return { specifier, reason: "extensions must be an array" };
145
+ }
146
+ if (candidate.tier !== "syntax" && candidate.tier !== "full") {
147
+ return { specifier, reason: `invalid tier ${String(candidate.tier)}` };
148
+ }
149
+ return candidate;
150
+ } catch (err) {
151
+ return { specifier, reason: err.message };
152
+ }
153
+ }
154
+ async function discoverPlugins(options) {
155
+ const { manifestPath } = options;
156
+ if (!existsSync(manifestPath)) {
157
+ log.info(`Manifest not found at ${manifestPath} \u2014 no plugins discovered`);
158
+ return { plugins: [], failures: [] };
159
+ }
160
+ const baseDir = dirname(manifestPath);
161
+ const specifiers = options.explicit ?? readManifestDeps(manifestPath);
162
+ const manifestRequire = createRequire(pathToFileURL(manifestPath).href);
163
+ const plugins = [];
164
+ const failures = [];
165
+ for (const spec of specifiers) {
166
+ if (options.shouldSkipSpecifier?.(spec)) {
167
+ log.info(`Skipping plugin ${spec} (matched ignoreProjects)`);
168
+ continue;
169
+ }
170
+ const result = await tryLoad(spec, baseDir, manifestRequire);
171
+ if ("apiVersion" in result) {
172
+ plugins.push({ plugin: result, specifier: spec });
173
+ } else {
174
+ failures.push(result);
175
+ log.warn(`Skipped plugin ${spec}: ${result.reason}`);
176
+ }
177
+ }
178
+ return { plugins, failures };
179
+ }
180
+
181
+ export {
182
+ resolvePluginPackageJson,
183
+ resolvePackageCandidates,
184
+ discoverPlugins
185
+ };
186
+ //# sourceMappingURL=chunk-2Q76LH2H.js.map
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/cli-paths.ts
4
+ import { existsSync, readFileSync } from "fs";
5
+ import { dirname, join } from "path";
6
+ function findCliPackageJson() {
7
+ let dir = import.meta.dirname;
8
+ for (let i = 0; i < 10; i++) {
9
+ const pkg = join(dir, "package.json");
10
+ if (existsSync(pkg)) return pkg;
11
+ const parent = join(dir, "..");
12
+ if (parent === dir) break;
13
+ dir = parent;
14
+ }
15
+ return void 0;
16
+ }
17
+ function getVersion() {
18
+ const pkg = findCliPackageJson();
19
+ if (pkg === void 0) return "unknown";
20
+ try {
21
+ const json = JSON.parse(readFileSync(pkg, "utf-8"));
22
+ return json.version ?? "unknown";
23
+ } catch {
24
+ return "unknown";
25
+ }
26
+ }
27
+ function getCliPackageRoot() {
28
+ const pkg = findCliPackageJson();
29
+ return pkg !== void 0 ? dirname(pkg) : dirname(import.meta.dirname);
30
+ }
31
+ function locateGitRoot(startDir) {
32
+ let dir = startDir;
33
+ for (let i = 0; i < 12; i++) {
34
+ if (existsSync(join(dir, ".git"))) return dir;
35
+ const parent = dirname(dir);
36
+ if (parent === dir) break;
37
+ dir = parent;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ // src/core/compare-codepoint.ts
43
+ function compareCodepoint(a, b) {
44
+ return a < b ? -1 : a > b ? 1 : 0;
45
+ }
46
+
47
+ export {
48
+ compareCodepoint,
49
+ getVersion,
50
+ getCliPackageRoot,
51
+ locateGitRoot
52
+ };
53
+ //# sourceMappingURL=chunk-72CEQEQD.js.map