@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.
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createModuleHost
4
+ } from "./chunk-JDL6GGTO.js";
5
+ import "./chunk-IT426P3F.js";
6
+ import "./chunk-I7RPXT6L.js";
7
+ import "./chunk-72CEQEQD.js";
8
+ import "./chunk-EC6INGPV.js";
9
+ import "./chunk-5DSKZJNN.js";
10
+ export {
11
+ createModuleHost
12
+ };
13
+ //# sourceMappingURL=module-host-KTGDODES.js.map
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DOMAIN_MODULE_PACKAGE,
4
+ loadCtxoModules,
5
+ sameMinor
6
+ } from "./chunk-LAX6VVBU.js";
7
+ import "./chunk-2Q76LH2H.js";
8
+ import "./chunk-5DSKZJNN.js";
9
+ export {
10
+ DOMAIN_MODULE_PACKAGE,
11
+ loadCtxoModules,
12
+ sameMinor
13
+ };
14
+ //# sourceMappingURL=module-loader-B3LYF3W7.js.map
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ discoverPlugins,
4
+ resolvePackageCandidates,
5
+ resolvePluginPackageJson
6
+ } from "./chunk-2Q76LH2H.js";
7
+ import "./chunk-5DSKZJNN.js";
8
+ export {
9
+ discoverPlugins,
10
+ resolvePackageCandidates,
11
+ resolvePluginPackageJson
12
+ };
13
+ //# sourceMappingURL=plugin-discovery-XRXI3IJF.js.map
package/llms-full.txt CHANGED
@@ -13,6 +13,9 @@ ctxo index --check # CI gate: fail if index stale (writes nothing, re
13
13
  ctxo index --commit HEAD # Re-index the files one commit changed (what the post-commit hook runs)
14
14
  ctxo index --files a.ts b.ts # Re-index only these files (alias: --file <path>)
15
15
  ctxo # Start MCP server (stdio transport)
16
+ ctxo install domain # opt-in domain knowledge layer (@ctxo/domain)
17
+ ctxo init --domain # registry skeleton, fold guard hooks, skill, domain config
18
+ ctxo verify-domain --since origin/main # CI gate for contexts, cards, rule IDs, boundaries
16
19
  ```
17
20
 
18
21
  MCP client configuration:
@@ -50,7 +53,7 @@ Source paths referenced below live under `packages/cli/src/`.
50
53
 
51
54
  ---
52
55
 
53
- ## Tool Reference (14 tools)
56
+ ## Tool Reference (14 core tools, plus 2 with @ctxo/domain)
54
57
 
55
58
  ### get_logic_slice
56
59
  Retrieve a symbol and all its transitive dependencies.
@@ -79,6 +82,8 @@ Returns: impactedSymbols array with symbolId, depth, riskScore, confidence, edge
79
82
  Also returns coChangedFiles (when co-change data exists): up to 10 files that co-change with the target's file at frequency >= 0.5 but have no graph path to it, each with file, frequency, sharedCommits. Informational: not counted in tiers, risk scores or the safe-edit gate.
80
83
  Every copy of a table, view or procedure that several files define, and its 2-part ID, reports the same dependents (those of the winning definition).
81
84
 
85
+ With @ctxo/domain and a domain graph: `rulesAffected` (card rules linked to the target or an impacted symbol, target first; each `{ ruleId, title, context, card, via: [{ symbolId, link: code|test }] (up to 5), viaCount }`) and `cardsToUpdate` (`{ context, card }`, registry order). Capped at 50 with `<name>Total`.
86
+
82
87
  When to use: BEFORE modifying any symbol, including a SQL table, view or procedure.
83
88
  Instead of: get_logic_slice (forward deps), get_pr_impact (PR-level).
84
89
 
@@ -103,6 +108,8 @@ Parameters:
103
108
 
104
109
  Returns: commitHistory array + antiPatternWarnings (reverts, rollbacks, undo patterns), and driftSignals when a community snapshot exists. driftSignals carries confidence (low below 3 distinct commits, medium 3-6, high 7+), distinctCommits (different commits in the snapshot history, other than the current one), snapshotsAvailable (history entries passed in, kept for compatibility) and an optional hint.
105
110
 
111
+ With @ctxo/domain and a domain graph: `decisions`, up to 5 dated card decisions of the symbol's context (those naming a rule linked to the symbol first, each group newest first; `decisionsTotal` when more), each `{ id, date, text, card, line, rules, provenance }`; provenance entries are `{ kind: "file", path, exists, label? }` or `{ kind: "external", value }` (never fetched). `[]` when the file has no single owning context.
112
+
106
113
  When to use: Understanding WHY code was written this way, checking for problem history.
107
114
  Pair with: get_change_intelligence (complexity/churn scores).
108
115
 
@@ -146,6 +153,8 @@ Task types:
146
153
 
147
154
  Context entries that depend on the symbol (its blast radius) carry an optional confidence tier (confirmed|likely|potential); co-change history can raise potential to likely. Entries carry no symbol annotations (use get_logic_slice level 4). A 2-part data ID (`dbo.loans::table`) is accepted.
148
155
 
156
+ With @ctxo/domain and a domain graph: `cardExcerpt` `{ context?, card?, rules: [{ ruleId, title, linkedTo: target|neighbour, text? }], terms: [{ term, definition }], contracts?, tokens, omitted }`. Up to a quarter of tokenBudget is reserved before the code context is packed; the excerpt fills tokenBudget - totalTokens, so the sum never exceeds the budget. Left out only when even an empty excerpt does not fit.
157
+
149
158
  When to use: BEST starting point when you know the symbol AND your intent.
150
159
 
151
160
  ### get_ranked_context
@@ -157,7 +166,9 @@ Parameters:
157
166
  - strategy (enum: combined|dependency|importance, default combined): Ranking strategy
158
167
  - searchMode (enum: fts|legacy, default fts): "fts" is BM25 with camelCase splitting, trigram fallback and automatic typo correction (reported in `fuzzyCorrection`); "legacy" is substring matching
159
168
 
160
- Returns: Symbols ranked by BM25 relevance + PageRank importance, packed within budget.
169
+ - scope (enum: code|domain, default code; only with @ctxo/domain): "domain" searches rules, terms, decisions, card sections and in-repo provenance files (BM25 k1 1.2, b 0.75, no PageRank; strategy and searchMode do not apply). Without the package the parameter does not exist.
170
+
171
+ Returns: Symbols ranked by BM25 relevance + PageRank importance, packed within budget. With scope "domain": `{ query, scope, results: [{ id, kind: rule|term|decision|section|provenance, title, context?, path, line?, excerpt, relevanceScore, tokens }], totalTokens, tokenBudget, searchMetrics }`.
161
172
 
162
173
  When to use: Have a question/topic but don't know which symbol to look at.
163
174
  Instead of: search_symbols (exact name/regex search).
@@ -229,8 +240,31 @@ Parameters:
229
240
 
230
241
  Returns: changedFiles, changedSymbols, totalImpact, riskLevel (low|medium|high), per-file blast radius, co-change data (coChangedWith per file), boundaryViolations (confidence from distinctCommits) and clustersAffected.
231
242
 
243
+ With @ctxo/domain and a domain graph: `contextsTouched` (`{ id, status, card?, cardChanged, acknowledged?, fileCount, files }` with up to 10 files; over the merge base of since and HEAD plus the working tree, while `files` stays `git diff <since>`), `cardsChanged` (registry cards changed in the range or the working tree) and `rulesWithoutTests` (rules of touched contexts without a test link, with `untested` reasons). Capped at 50 with `<name>Total`; left out when git cannot resolve since. An empty diff also carries `_meta`.
244
+
232
245
  When to use: FIRST tool when reviewing a PR or evaluating recent commits.
233
246
 
247
+ ### get_domain_context (with @ctxo/domain)
248
+ The card of the named context that owns a symbol or file.
249
+
250
+ Parameters:
251
+ - symbolId (string, optional): Symbol ID; its file part selects the context (the symbol need not be indexed). Wins over path
252
+ - path (string, optional): Repo-relative file path, or a context's card path
253
+
254
+ Returns: `context` (id, name, rung, card, rulePrefix, paths), `terms` (definitions with origin card|glossary, up to 15 `identifiers`, `identifierCount`), `rules` (each with `untested?`, up to 10 `tests` and 10 `code` links with `sources`, `testCount`, `codeCount`), `contracts` (outbound, inbound, text, references), `decisions` (newest first, capped at 50) and `openQuestions`. With neither parameter: `contexts` with ruleCount, rulesWithTests, termCount, decisionCount. Misses: `{ found: false, hint }` (no registry, invalid or changed registry, no graph, no owning context, a path outside the project). After a card edit not yet indexed: `stale: true` and a hint.
255
+
256
+ When to use: Before changing code in a named context; to learn the rules, terms and contracts behind it.
257
+
258
+ ### get_rule (with @ctxo/domain)
259
+ One domain rule by ID.
260
+
261
+ Parameters:
262
+ - ruleId (string, required): e.g. LEND-03; LEND03, Lend03, lend_03 and the id of a domain-scope hit (rule:LEND-03) also match
263
+
264
+ Returns: `rule` (ruleId, title, text, line, untested?, context, card), `tests` and `symbols` (each `{ file, symbolId?, testName?, fallback?, sources: [{ source, annotation?, file, line }] }`; test sources annotation|category|test-title|rule-comment|mention|card-tests, code sources card-code|rule-comment), `decisions` mentioning it, `provenance` (with the decision it came from) and `unresolved` card values when any. `{ found: false, hint: "No rule <ID> in any card." }` for an unknown ID.
265
+
266
+ When to use: A change may affect a business rule; find the tests that pin it and the decisions behind it.
267
+
234
268
  ---
235
269
 
236
270
  ## Tool Selection Guide
@@ -278,6 +312,10 @@ Checking if safe to delete/rename?
278
312
 
279
313
  Working with class hierarchies?
280
314
  -> get_class_hierarchy (extends/implements tree)
315
+
316
+ Need the business rules or decisions behind code? (with @ctxo/domain)
317
+ -> get_domain_context (the card of the context that owns the symbol or file)
318
+ -> then get_rule (one rule: tests, code, decisions)
281
319
  ```
282
320
 
283
321
  ---
@@ -494,6 +532,87 @@ Title path joined with ` > `; whitespace collapsed; runs of `:` collapsed and a
494
532
 
495
533
  ---
496
534
 
535
+ ## Domain Knowledge Layer: `@ctxo/domain` (0.14.0, #156 PR 2B)
536
+
537
+ An opt-in package that lets a team name its bounded contexts and keep markdown cards in step with code. Ctxo supplies structure, linkage, verification and retrieval; it never writes domain text or fetches external documentation. Feature doc: docs/features/domain-knowledge.md ("Named contexts and the domain package", "Ctxo domain conventions v1"). Design record: docs/architecture/ADR/adr-018-domain-knowledge-layer.md.
538
+
539
+ ### Install and loading
540
+
541
+ - `ctxo install domain` installs `@ctxo/domain` at the CLI's version (globally when the project has no `package.json`; `npm i -g @ctxo/cli @ctxo/domain @ctxo/lang-sql` is the supported path for such repositories). `ctxo init --domain` also sets it up.
542
+ - The CLI loads the package from the project's `node_modules`, then from next to itself, through `@ctxo/cli/internal` (types only, plus `createModuleHost`). Only a copy at the same `major.minor` loads; `@ctxo/cli` and `@ctxo/domain` share one version.
543
+ - Without the package: no domain commands, fields or hooks. A known domain command name (`verify-domain`, `contexts-touched`, `domain`) prints an install hint and exits 1; `ctxo domain-guard-hook` exits 0 without output; `ctxo --help` ends with one install hint line. With the package and no registry: the commands print a hint and exit 0 and no `context` fields appear.
544
+
545
+ ### Registry and cards
546
+
547
+ - `docs/domain/contexts.yaml` (`domain.registry`): `version: 1`, `contexts[]` with `id` (kebab-case, permanent), `name`, `card` (optional only at rung fear), `paths` (globs), `rung` (fear, pinned, named, bounded, boring), `rulePrefix`, `contracts` (target context to allowed target files); `unowned.allow`, `glossary` (`docs/domain/glossary.md`), `contextMap` (`docs/domain/context-map.md`, a table with Context and Rung columns that must agree with the registry).
548
+ - A file belongs to at most one context. Unowned files warn by default (`domain.coverage.unowned: error` to fail).
549
+ - Card: five H2 headings in order (Language; Rules and invariants; Boundary contracts; Decisions; Open questions). Rules are `### <ID> <title>` under Rules and invariants, with optional `Tests:`, `Code:` and `Untested:` lines. A rule needs a test link, a `Tests:` line or an `Untested:` line.
550
+ - Rule IDs match `domain.rules.idPattern` (default `^[A-Z][A-Z0-9]*-\d+$`), are permanent and unique across cards, and use the context's `rulePrefix` when it has one. A test links to a rule through `[Trait("Rule", "LEND-03")]` (keys in `domain.tests.ruleTraitKeys`), `Category` / `TestCategory` / `Tag` values, `rule: LEND-03` comments, test titles that start with an ID, or the card's `Tests:` line. Tracker prefixes (`domain.rules.trackerPrefixes`) never link from plain text.
551
+
552
+ ### Commands
553
+
554
+ - `ctxo contexts-touched [--since <ref>] [--committed | --staged] [--json] [--check]`: contexts whose paths hold a changed non-test file, with status `card-changed`, `acknowledged` (a `Domain-Unchanged` trailer in the range), `unresolved` or `no-card`. `--check` exits 1 on unresolved; 2 on registry problems, a missing base (CI needs `fetch-depth: 0`) or git failure.
555
+ - `ctxo verify-domain [--since <ref>] [--boundaries by-rung|report] [--json]`: checks 1 registry and config, 2 cards present, 3 path globs match, 4a single owner, 4b unowned files, 5 card headings, 6 rule IDs and test links, 7 card links and anchors, 8 generated loader rules, 9 context map rungs, 10 boundaries, 11 fold since a ref (with `--since`). Exit 0 (warnings allowed), 1 errors, 2 missing index, git failure or internal error. `--json` writes `{ registry, checks, summary, exitCode }` to stdout; stdout is empty on exit 2 and with no registry.
556
+ - `ctxo domain rules --write | --check`: one pointer file per context with a card: `.claude/rules/ctxo-context-<id>.md` (front matter `paths`) and, when `.cursor/` exists, `.cursor/rules/ctxo-context-<id>.mdc`. Marker `<!-- generated by ctxo domain rules; do not edit -->`. `--check` exits 1 on missing, outdated, stale and blocked files (a hand-written file at a generated path; rename or delete it).
557
+ - `ctxo domain discover [--json] [--write]`: card rule prefixes, tracker-like keys in commit subjects and branch names (5 or more is proposed as a tracker prefix), test trait keys, uncovered test folders; `--write` merges the proposal into `.ctxo/config.yaml`.
558
+ - `ctxo init --domain [--boundaries <by-rung|report>]` or `--only domain`: config defaults, registry skeleton, discovery with confirmation, loader rules, guard hooks (SessionStart, Stop, TaskCompleted) and the `ctxo-domain-fold` skill (Claude Code and Cursor).
559
+
560
+ ### Fold guard and trailer
561
+
562
+ - `ctxo domain-guard-hook` judges only the commits of the current session (`startHead..HEAD`, recorded at SessionStart, intersected with the branch range from `domain.guard.base`, `origin/HEAD`, `main` or `master`; unpushed commits on the default branch). It blocks once per fingerprint (HEAD plus unresolved IDs): `Stop` prints `{"decision":"block","reason":...}` and exits 0, `TaskCompleted` prints to stderr and exits 2. `stop_hook_active` and every internal error allow.
563
+ - Trailer: `Domain-Unchanged: <id>[, <id>...] [(reason)]`, read with `git log --format=%(trailers:key=Domain-Unchanged,valueonly)` over the evaluated range only. A card change in the range also resolves a context. An empty commit with the trailer is fine.
564
+
565
+ ### Boundaries, drift and fields
566
+
567
+ - `ctxo index` writes `.ctxo/index/domain/contexts.json`, `boundaries.json` and `history/` (10 per-commit entries), each with a `ctxoArtifact` marker that the index reader skips.
568
+ - Edges between contexts are allowed when the target file matches the source context's `contracts.<target>`; otherwise a violation: an error for `blockingRungs` (default bounded, boring) in `by-rung` mode, else a warning (`report` mode: always a warning). Edges from test files and to unowned files are skipped.
569
+ - Additive fields: `context` (set when exactly one context owns the file) on search_symbols results, get_blast_radius impacted symbols, find_importers importers, get_logic_slice roots and get_pr_impact changed symbols; `contextBoundaryViolations` (capped at 50, with `contextBoundaryViolationsTotal`) on get_pr_impact; `contextDrift` (`crossShare`, `trend`, `movedFiles`, `distinctCommits`, `confidence`) on get_why_context.
570
+
571
+ ### Doctor and global installs
572
+
573
+ - `domain_registry`, `domain_agent_setup` and `domain_conventions` (all pass or warn; a repo with neither `.claude/` nor `.cursor/` is checked as Claude Code and carries a standing agent-setup warning until `ctxo init --only domain`).
574
+ - `install_copies` (every project): warns, never fails, when hooks call `ctxo` and none is on PATH, or an npx-pinned MCP entry differs from the global CLI the hooks run. `ctxo init` from a global install writes MCP entries that call the global `ctxo`.
575
+
576
+ ### Config
577
+
578
+ `domain:` keys and defaults: `registry: docs/domain/contexts.yaml`, `coverage.unowned: warn`, `boundaries.enforce: by-rung`, `boundaries.blockingRungs: [bounded, boring]`, `rules.idPattern: ^[A-Z][A-Z0-9]*-\d+$`, `rules.trackerPrefixes: []`, `tests.ruleTraitKeys: [Rule]`, `guard.enabled: true`, `guard.base` (unset), `guard.subagents: false`. An invalid key falls back to its default and verify-domain check 1 reports it.
579
+
580
+ ### Adoption ladder
581
+
582
+ level 0: install and index; 1: registry with paths; 2: cards; 3: rule IDs and test links, through card `Tests:` lines, test names or framework tags; 4: contracts and shadow-spec resolutions. No level requires code changes.
583
+
584
+ ---
585
+
586
+ ## Domain Graph (unreleased, #156 PR 3)
587
+
588
+ New in the next release: with `@ctxo/domain` and a registry, `ctxo index` (and the post-commit hook, also for a card-only commit) writes `.ctxo/index/domain/graph.json` (`ctxoArtifact: "domain-graph"`, byte-identical for the same input). `ctxo watch` does not rebuild it. Code metrics never read it.
589
+
590
+ ### Nodes and links
591
+ - Nodes: `context:<id>`, `rule:<ID>`, `term:<context>/<term-key>` (repo-wide glossary terms use `*`), `decision:<context>/<date>-<n>`; search-only `section:<card>:<line>`.
592
+ - Links, each with `source` and the evidence location `at` (1-based): rule-test (`annotation`: xUnit Trait with a `domain.tests.ruleTraitKeys` key; `category`: NUnit Category, MSTest TestCategory, JUnit Tag; `test-title`; `rule-comment`; `mention`; `card-tests`), rule-symbol (`card-code`; `rule-comment` in indexed non-test files up to 1 MiB, linked to the symbol starting within three lines below when it sits inside the symbol containing the comment, else the innermost one), decision-provenance (`card-link`, `card-text`), decision-rule, term-identifier (`symbol-name`, `table-name`; contiguous token run with plural folding, own context only, at most 50 per term).
593
+ - Card values that resolve to nothing are kept in `unresolved` (`missing-file`, `not-a-symbol`).
594
+
595
+ ### Card format additions (Ctxo domain conventions v1)
596
+ - `Tests:` and `Code:` values: repo-relative paths or symbol IDs (anything with `::`); `file#Name` names a plain test. A symbol ID the index does not hold links to its file. C# symbol IDs differ between the Roslyn and tree-sitter tiers, so prefer file paths in C# `Code:` lines.
597
+ - `**Term**` items in Language; glossary items anywhere in glossary.md, `(context: <id>)` ties one to a context.
598
+ - Decision lines `- YYYY-MM-DD: text`; rule IDs written in them link the decision; relative links are provenance files, `#123`, URLs and tracker keys external strings (never fetched).
599
+ - An H1 ends the Rules and invariants section. Shared cards: rules belong to the first context by id (warned once).
600
+
601
+ ### Staleness
602
+ - Registry changed since the graph: no domain fields, tools answer the "run ctxo index" hint.
603
+ - Cards or glossary changed: answers from the graph with `stale: true` and a hint.
604
+
605
+ ### verify-domain check 6
606
+ - `<ID>: Code file "<file>" does not exist` (error, also for directories, like Tests).
607
+ - `<ID>: <field>: "<value>" is not an indexed symbol; it links to its file` (warning).
608
+ - `rule: comment names unknown rule <ID>` (warning, production code, only for a rule prefix).
609
+
610
+ ### Internal API (ADR-018 addendum)
611
+ - Async enrichers (awaited by get_blast_radius, get_pr_impact, get_why_context; dropped with a warning by synchronous tools), get_context_for_task as the seventh enrich-seam tool, `reserveTaskContextTokens` (summed, capped at a quarter of tokenBudget), `rankedContextScopes`, host services `createDocumentSearch`, `splitIdentifier`, `dataResourceKinds`.
612
+ - The ctxo-domain-fold skill marker is v0.15; `ctxo doctor` reports older copies as outdated until `ctxo init --only domain`.
613
+
614
+ ---
615
+
497
616
  ## Safe-Edit Guard (v0.8+)
498
617
 
499
618
  The safe-edit guard closes the loop between MCP tool availability and agent compliance. An agent with access to get_blast_radius can still skip it. The guard makes skipping impossible for high-impact symbols on Claude Code.
package/llms.txt CHANGED
@@ -22,11 +22,12 @@ ctxo # Start MCP server (stdio)
22
22
  - `@ctxo/lang-csharp` - Roslyn plugin (attribute annotations; SQL edges at known call sites when `@ctxo/lang-sql` is installed)
23
23
  - `@ctxo/lang-java` - tree-sitter Java plugin (syntax tier built-in; full tier via `@ctxo/lang-java-analyzer` companion, requires JRE 11+)
24
24
  - `@ctxo/lang-sql` - T-SQL plugin for `.sql` files (protocol v2): tables, views, procedures, functions and their reads/writes/executes
25
+ - `@ctxo/domain` - opt-in domain knowledge layer (ADR-018): named contexts, cards, the fold guard and the `verify-domain` CI gate; loaded by the CLI through `@ctxo/cli/internal`, shares one version with `@ctxo/cli`
25
26
  - `@ctxo/sql-extract` - zero-dependency T-SQL tokenizer and recognizers (a library, not a plugin; the one dependency plugins may take besides `@ctxo/plugin-api`)
26
27
 
27
28
  Language adapters ship as separate npm packages and are discovered by scanning the project `package.json` for `@ctxo/lang-*` or `ctxo-lang-*` dependencies.
28
29
 
29
- ## 14 Tools
30
+ ## Tools: 14 core, plus 2 with @ctxo/domain
30
31
 
31
32
  - get_logic_slice: Symbol + transitive dependencies (L1-L4 detail levels)
32
33
  - get_blast_radius: What breaks if a symbol changes (3-tier: confirmed/likely/potential; data edges are confirmed)
@@ -42,6 +43,8 @@ Language adapters ship as separate npm packages and are discovered by scanning t
42
43
  - get_class_hierarchy: Class inheritance tree (ancestors + descendants)
43
44
  - get_symbol_importance: PageRank centrality ranking
44
45
  - get_pr_impact: Full PR risk assessment in a single call
46
+ - get_domain_context (with @ctxo/domain): the card of the named context that owns a symbol or file: terms, rules with tests and code links, contracts, decisions, open questions. Parameters: `symbolId?`, `path?`; with neither, every context
47
+ - get_rule (with @ctxo/domain): one rule by ID (`ruleId`, variants such as LEND03 also match): text, context, tests with the source of each link, implementing symbols, decisions and provenance
45
48
 
46
49
  ## Tool Selection Guide
47
50
 
@@ -54,6 +57,26 @@ Language adapters ship as separate npm packages and are discovered by scanning t
54
57
  - Finding unused code? -> find_dead_code
55
58
  - Safe to delete? -> find_importers
56
59
  - Preparing a context map? -> get_architectural_overlay(mode: "contexts")
60
+ - Need the business rules or decisions behind code? -> get_domain_context, then get_rule (with @ctxo/domain)
61
+
62
+ ## New in the next release: the domain graph (#156, PR 3)
63
+
64
+ - `ctxo index` builds `.ctxo/index/domain/graph.json` from the cards and the glossary: rules linked to tests (xUnit `Trait` with a `domain.tests.ruleTraitKeys` key, NUnit `Category`, MSTest `TestCategory`, JUnit `Tag`, test titles, `rule:` comments, ID mentions in test files, card `Tests:` lines) and to code (card `Code:` lines, `rule: LEND-03` comments above declarations), decisions linked to provenance (repo files; `#123` and URLs kept as strings, never fetched), terms linked to symbol and table names. Rebuilt by `ctxo index` and the post-commit hook, not by `ctxo watch`; after a card edit the tools add `stale: true`.
65
+ - Fields with a graph (capped at 50, `decisions` at 5, `<name>Total` when cut, not truncatable): get_blast_radius `rulesAffected`, `cardsToUpdate`; get_why_context `decisions` (rule-linked first, then newest, with provenance); get_context_for_task `cardExcerpt` (target and neighbour rules, terms, contracts; within `tokenBudget`, up to a quarter reserved); get_pr_impact `contextsTouched` (merge base of `since` and HEAD plus the working tree), `cardsChanged`, `rulesWithoutTests`.
66
+ - Two tools, get_domain_context and get_rule: 14 core tools, 16 with `@ctxo/domain`. Both answer `{ found: false, hint }` without a registry or a graph.
67
+ - get_ranked_context gains `scope` with `@ctxo/domain`: `"code"` (default, unchanged) or `"domain"` (BM25 over rules, terms, decisions, card sections and in-repo provenance files; `strategy` and `searchMode` do not apply). Without the package there is no `scope` parameter.
68
+ - Card format additions (no code change needed): `Code:` lines, `**Term**` items, glossary `(context: <id>)`, dated decision lines. `Tests:`/`Code:` values with `::` are symbol IDs; use `file#Name` for a plain test name. `verify-domain` check 6 also errors on missing `Code:` files and directories, and warns on unindexed symbol IDs and unknown IDs in production `rule:` comments of a rule prefix.
69
+ - `@ctxo/cli` no longer publishes source maps (tarball about 0.8 MB unpacked). Internal API: async enrichers, `reserveTaskContextTokens`, `rankedContextScopes`, host services `createDocumentSearch`, `splitIdentifier`, `dataResourceKinds` (ADR-018 addendum).
70
+
71
+ ## Domain knowledge layer, `@ctxo/domain` (0.14.0, #156 PR 2B)
72
+
73
+ - Install: `ctxo install domain` (globally when the project has no `package.json`), then `ctxo init --domain`. Opt-in: without the package, no commands, fields or hooks, and a known domain command name prints an install hint and exits 1 (`ctxo domain-guard-hook` exits 0 without output). With the package but no registry, the domain commands print a hint and exit 0 and no `context` fields appear.
74
+ - Registry `docs/domain/contexts.yaml`: contexts with `id`, `name`, `card`, `paths`, `rung` (fear, pinned, named, bounded, boring), `rulePrefix`, `contracts`; `unowned.allow`, `glossary`, `contextMap`. A card has five H2 headings (Language; Rules and invariants; Boundary contracts; Decisions; Open questions) and rules as `### LEND-03 title` with optional `Tests:`, `Code:` and `Untested:` lines.
75
+ - Commands: `ctxo contexts-touched [--since <ref>] [--committed | --staged] [--json] [--check]`; `ctxo verify-domain [--since <ref>] [--boundaries by-rung|report] [--json]` (exit 0, 1 or 2; `--json` stdout is empty on exit 2 and with no registry); `ctxo domain rules --write | --check`; `ctxo domain discover [--json] [--write]`; `ctxo init --domain [--boundaries <mode>]` and `--only domain`.
76
+ - Fold guard (Claude Code): `ctxo domain-guard-hook` on SessionStart, Stop and TaskCompleted blocks once when the session's commits changed a context without a card change; resolve with a card commit or the trailer `Domain-Unchanged: <id>[, <id>] (reason)`.
77
+ - Config `domain:` in `.ctxo/config.yaml`: `registry`, `coverage.unowned` (warn | error), `boundaries.enforce` (by-rung | report) and `blockingRungs`, `rules.idPattern` and `trackerPrefixes`, `tests.ruleTraitKeys`, `guard.enabled`, `guard.base`, `guard.subagents`.
78
+ - Additive fields with `@ctxo/domain` and a registry: `context` on search_symbols, get_blast_radius, find_importers, get_logic_slice and get_pr_impact entries; `contextBoundaryViolations` on get_pr_impact; `contextDrift` on get_why_context. `ctxo index` writes `.ctxo/index/domain/`.
79
+ - Doctor: `domain_registry`, `domain_agent_setup`, `domain_conventions`, and `install_copies` (MCP entry and hooks start the same copy). Feature doc: docs/features/domain-knowledge.md ("Ctxo domain conventions v1").
57
80
 
58
81
  ## Unreleased: language parity for TypeScript, Java and Go (#156, PR 1C)
59
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctxo/cli",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
4
4
  "description": "MCP server delivering dependency-aware, history-enriched context for codebases",
5
5
  "type": "module",
6
6
  "engines": {
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "files": [
13
13
  "dist/",
14
+ "!dist/**/*.map",
14
15
  "pages/visualize-template.html",
15
16
  "skills/",
16
17
  "README.md",
@@ -19,6 +20,17 @@
19
20
  ],
20
21
  "main": "dist/index.js",
21
22
  "types": "dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ },
28
+ "./internal": {
29
+ "types": "./dist/internal.d.ts",
30
+ "import": "./dist/internal.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
22
34
  "keywords": [
23
35
  "mcp",
24
36
  "context",
@@ -53,10 +65,10 @@
53
65
  "typescript": "^6.0.3",
54
66
  "typescript-eslint": "^8.71.1",
55
67
  "vitest": "^5.0.1",
56
- "@ctxo/lang-csharp": "0.9.0",
57
- "@ctxo/lang-go": "0.10.0",
58
68
  "@ctxo/lang-java": "0.11.0",
69
+ "@ctxo/lang-go": "0.10.0",
59
70
  "@ctxo/lang-sql": "0.1.0",
71
+ "@ctxo/lang-csharp": "0.9.0",
60
72
  "@ctxo/lang-typescript": "0.9.0"
61
73
  },
62
74
  "repository": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/logger.ts"],"sourcesContent":["/**\n * Structured logger for Ctxo — all output goes to stderr (stdout reserved for MCP JSON-RPC).\n *\n * Supports debug mode via DEBUG environment variable:\n * DEBUG=ctxo:* — all debug output\n * DEBUG=ctxo:git — only git adapter debug\n * DEBUG=ctxo:index — only indexing debug\n * DEBUG=ctxo:mcp — only MCP handler debug\n * DEBUG=ctxo:storage — only storage adapter debug\n * DEBUG=ctxo:masking — only masking pipeline debug\n *\n * Always outputs: info, warn, error (regardless of DEBUG).\n * Only outputs debug when matching namespace is enabled.\n */\n\nfunction isDebugEnabled(namespace: string): boolean {\n const debugEnv = process.env['DEBUG'] ?? '';\n if (!debugEnv) return false;\n const patterns = debugEnv.split(',').map(p => p.trim());\n return patterns.some(p => {\n if (p === '*' || p === 'ctxo:*') return true;\n if (p === namespace) return true;\n if (p.endsWith(':*') && namespace.startsWith(p.slice(0, -1))) return true;\n return false;\n });\n}\n\nexport interface Logger {\n debug(message: string, ...args: unknown[]): void;\n info(message: string, ...args: unknown[]): void;\n warn(message: string, ...args: unknown[]): void;\n error(message: string, ...args: unknown[]): void;\n}\n\nexport function createLogger(namespace: string): Logger {\n const prefix = `[${namespace}]`;\n\n return {\n debug(message: string, ...args: unknown[]) {\n if (isDebugEnabled(namespace)) {\n console.error(`${prefix} DEBUG ${message}`, ...args);\n }\n },\n info(message: string, ...args: unknown[]) {\n console.error(`${prefix} ${message}`, ...args);\n },\n warn(message: string, ...args: unknown[]) {\n console.error(`${prefix} WARN ${message}`, ...args);\n },\n error(message: string, ...args: unknown[]) {\n console.error(`${prefix} ERROR ${message}`, ...args);\n },\n };\n}\n"],"mappings":";;;AAeA,SAAS,eAAe,WAA4B;AAClD,QAAM,WAAW,QAAQ,IAAI,OAAO,KAAK;AACzC,MAAI,CAAC,SAAU,QAAO;AACtB,QAAM,WAAW,SAAS,MAAM,GAAG,EAAE,IAAI,OAAK,EAAE,KAAK,CAAC;AACtD,SAAO,SAAS,KAAK,OAAK;AACxB,QAAI,MAAM,OAAO,MAAM,SAAU,QAAO;AACxC,QAAI,MAAM,UAAW,QAAO;AAC5B,QAAI,EAAE,SAAS,IAAI,KAAK,UAAU,WAAW,EAAE,MAAM,GAAG,EAAE,CAAC,EAAG,QAAO;AACrE,WAAO;AAAA,EACT,CAAC;AACH;AASO,SAAS,aAAa,WAA2B;AACtD,QAAM,SAAS,IAAI,SAAS;AAE5B,SAAO;AAAA,IACL,MAAM,YAAoB,MAAiB;AACzC,UAAI,eAAe,SAAS,GAAG;AAC7B,gBAAQ,MAAM,GAAG,MAAM,UAAU,OAAO,IAAI,GAAG,IAAI;AAAA,MACrD;AAAA,IACF;AAAA,IACA,KAAK,YAAoB,MAAiB;AACxC,cAAQ,MAAM,GAAG,MAAM,IAAI,OAAO,IAAI,GAAG,IAAI;AAAA,IAC/C;AAAA,IACA,KAAK,YAAoB,MAAiB;AACxC,cAAQ,MAAM,GAAG,MAAM,SAAS,OAAO,IAAI,GAAG,IAAI;AAAA,IACpD;AAAA,IACA,MAAM,YAAoB,MAAiB;AACzC,cAAQ,MAAM,GAAG,MAAM,UAAU,OAAO,IAAI,GAAG,IAAI;AAAA,IACrD;AAAA,EACF;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/adapters/storage/json-index-reader.ts","../src/core/types.ts"],"sourcesContent":["import { readFileSync, readdirSync, existsSync, realpathSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport {\n FileIndexSchema,\n SymbolNodeSchema,\n GraphEdgeSchema,\n CommitIntentSchema,\n AntiPatternSchema,\n CodeFactSchema,\n type FileIndex,\n} from '../../core/types.js';\nimport { createLogger } from '../../core/logger.js';\n\nconst log = createLogger('ctxo:json-reader');\n\n/** JSON files next to the index entries that are products of Ctxo, not entries: the reader skips them. */\nexport const INDEX_ARTIFACT_FILES: readonly string[] = [\n 'co-changes.json',\n 'communities.json',\n 'drift-events.json',\n 'boundary-violations.json',\n 'contexts-view.json',\n];\n\n/** The folder of archived community snapshots inside the index folder: not entries either. */\nexport const COMMUNITY_HISTORY_DIR = 'communities.history';\n\n/** One index entry and the JSON file on disk it was read from. */\nexport interface IndexEntry {\n readonly path: string;\n readonly index: FileIndex;\n}\n\nexport class JsonIndexReader {\n private readonly indexDir: string;\n\n constructor(ctxoRoot: string) {\n this.indexDir = join(ctxoRoot, 'index');\n }\n\n readAll(): FileIndex[] {\n return this.readEntries().map((entry) => entry.index);\n }\n\n /**\n * Every valid index entry together with the path of the JSON file it came from. A caller that wants to\n * delete an entry must use this path: the `file` field inside the JSON says which source file the entry\n * is for, but a corrupt or crafted file can claim any name, such as an artifact's.\n */\n readEntries(): IndexEntry[] {\n if (!existsSync(this.indexDir)) {\n return [];\n }\n\n const jsonFiles = this.collectJsonFiles(this.indexDir);\n const results: IndexEntry[] = [];\n\n for (const filePath of jsonFiles) {\n const parsed = this.readSingle(filePath);\n if (parsed) {\n results.push({ path: filePath, index: parsed });\n }\n }\n\n return results;\n }\n\n readSingle(absolutePath: string): FileIndex | undefined {\n try {\n const raw = readFileSync(absolutePath, 'utf-8');\n const data: unknown = JSON.parse(raw);\n const result = FileIndexSchema.safeParse(data);\n\n if (result.success) {\n return result.data;\n }\n\n // Strict parse failed — attempt resilient recovery: keep valid items, drop invalid ones.\n const rel = relative(this.indexDir, absolutePath);\n const raw_data = data as Record<string, unknown>;\n\n // Must have at least a valid file name and lastModified to be recoverable.\n if (typeof raw_data['file'] !== 'string' || !raw_data['file'] ||\n typeof raw_data['lastModified'] !== 'number') {\n console.error(\n `[ctxo:json-reader] Invalid schema in ${rel}: ${result.error.message}`,\n );\n return undefined;\n }\n\n const rawSymbols = Array.isArray(raw_data['symbols']) ? raw_data['symbols'] : [];\n const rawEdges = Array.isArray(raw_data['edges']) ? raw_data['edges'] : [];\n const rawIntent = Array.isArray(raw_data['intent']) ? raw_data['intent'] : [];\n const rawAntiPatterns = Array.isArray(raw_data['antiPatterns']) ? raw_data['antiPatterns'] : [];\n\n // A symbol that fails only because of its annotations is kept without them: annotations are\n // optional metadata and must not cost the symbol.\n let droppedAnnotations = 0;\n const validSymbols: unknown[] = [];\n for (const sym of rawSymbols) {\n if (SymbolNodeSchema.safeParse(sym).success) {\n validSymbols.push(sym);\n continue;\n }\n if (sym !== null && typeof sym === 'object' && 'annotations' in sym) {\n const { annotations: _dropped, ...withoutAnnotations } = sym as Record<string, unknown>;\n if (SymbolNodeSchema.safeParse(withoutAnnotations).success) {\n validSymbols.push(withoutAnnotations);\n droppedAnnotations++;\n }\n }\n }\n // A facts value that is present but not an array is treated as absent: facts are optional metadata\n // and must not cost the file. It counts as one dropped fact list in the warning.\n const factsPresent = raw_data['facts'] !== undefined;\n const rawFacts = Array.isArray(raw_data['facts']) ? raw_data['facts'] : undefined;\n const validFacts = rawFacts?.filter((f) => CodeFactSchema.safeParse(f).success);\n const validEdges = rawEdges.filter((e) => GraphEdgeSchema.safeParse(e).success);\n const validIntent = rawIntent.filter((i) => CommitIntentSchema.safeParse(i).success);\n const validAntiPatterns = rawAntiPatterns.filter((a) => AntiPatternSchema.safeParse(a).success);\n\n const droppedSymbols = rawSymbols.length - validSymbols.length;\n const droppedFacts = rawFacts && validFacts\n ? rawFacts.length - validFacts.length\n : factsPresent ? 1 : 0;\n const droppedEdges = rawEdges.length - validEdges.length;\n const droppedIntent = rawIntent.length - validIntent.length;\n const droppedAntiPatterns = rawAntiPatterns.length - validAntiPatterns.length;\n\n if (droppedSymbols > 0 || droppedEdges > 0 || droppedIntent > 0 || droppedAntiPatterns > 0 ||\n droppedAnnotations > 0 || droppedFacts > 0) {\n log.warn(\n `${rel}: dropped ${droppedSymbols} invalid symbol(s), ${droppedEdges} invalid edge(s), ${droppedIntent} invalid intent(s), ${droppedAntiPatterns} invalid antiPattern(s), ${droppedAnnotations} invalid annotation list(s), ${droppedFacts} invalid fact(s); kept ${validSymbols.length} symbol(s), ${validEdges.length} edge(s), ${validIntent.length} intent(s), ${validAntiPatterns.length} antiPattern(s)`,\n );\n }\n\n // Re-parse with the filtered arrays to get a fully-typed FileIndex.\n const recovered = FileIndexSchema.safeParse({\n ...raw_data,\n symbols: validSymbols,\n edges: validEdges,\n intent: validIntent,\n antiPatterns: validAntiPatterns,\n facts: validFacts,\n });\n\n if (!recovered.success) {\n console.error(\n `[ctxo:json-reader] Invalid schema in ${rel}: ${result.error.message}`,\n );\n return undefined;\n }\n\n return recovered.data;\n } catch (err) {\n const rel = relative(this.indexDir, absolutePath);\n console.error(\n `[ctxo:json-reader] Failed to read ${rel}: ${(err as Error).message}`,\n );\n return undefined;\n }\n }\n\n private collectJsonFiles(dir: string, visited: Set<string> = new Set()): string[] {\n const realDir = realpathSync(dir);\n if (visited.has(realDir)) return []; // Guard against symlink loops\n visited.add(realDir);\n\n const files: string[] = [];\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.isSymbolicLink()) continue; // Skip symlinks entirely\n\n const fullPath = join(dir, entry.name);\n if (entry.isDirectory()) {\n // Skip non-FileIndex directories under .ctxo/index/\n if (entry.name === COMMUNITY_HISTORY_DIR) continue;\n files.push(...this.collectJsonFiles(fullPath, visited));\n } else if (entry.name.endsWith('.json') && !INDEX_ARTIFACT_FILES.includes(entry.name)) {\n files.push(fullPath);\n }\n }\n\n return files;\n }\n}\n","import { z } from 'zod';\nimport { SYMBOL_KINDS, EDGE_KINDS, FACT_KINDS } from '@ctxo/plugin-api';\n\n// ── Symbol Kinds ────────────────────────────────────────────────\n\nexport { SYMBOL_KINDS };\n\nexport const SymbolKindSchema = z.enum(SYMBOL_KINDS);\nexport type SymbolKind = z.infer<typeof SymbolKindSchema>;\n\n// ── Edge Kinds ──────────────────────────────────────────────────\n\nexport { EDGE_KINDS };\n\nexport const EdgeKindSchema = z.enum(EDGE_KINDS);\nexport type EdgeKind = z.infer<typeof EdgeKindSchema>;\n\n// ── Detail Levels ───────────────────────────────────────────────\n\nexport const DETAIL_LEVELS = [1, 2, 3, 4] as const;\n\nexport const DetailLevelSchema = z.union([\n z.literal(1),\n z.literal(2),\n z.literal(3),\n z.literal(4),\n]);\nexport type DetailLevel = z.infer<typeof DetailLevelSchema>;\n\n// ── Symbol ID ───────────────────────────────────────────────────\n\n/**\n * Format: \"<relativeFile>::<name>::<kind>\"\n * Example: \"src/foo.ts::myFn::function\"\n */\nexport const SymbolIdSchema = z\n .string()\n .min(1)\n .refine(\n (value) => {\n const parts = value.split('::');\n if (parts.length !== 3) return false;\n const [file, name, kind] = parts;\n if (!file || !name || !kind) return false;\n return SymbolKindSchema.safeParse(kind).success;\n },\n { message: 'Symbol ID must match format \"<file>::<name>::<kind>\"' },\n );\nexport type SymbolId = z.infer<typeof SymbolIdSchema>;\n\n// ── Edge Target ─────────────────────────────────────────────────\n\n/**\n * An edge target: either a full 3-part symbol ID (\"<file>::<name>::<kind>\")\n * or a 2-part name-reference (\"<name>::<kind>\") for cross-file / unresolved\n * targets. The graph builder resolves name-references by name at query time.\n */\nexport const EdgeTargetSchema = z.string().refine(\n (s) => {\n const parts = s.split('::');\n if (parts.length !== 3 && parts.length !== 2) return false;\n if (parts.some((p) => p.length === 0)) return false;\n const kind = parts[parts.length - 1];\n return (SYMBOL_KINDS as readonly string[]).includes(kind as string);\n },\n { message: 'edge target must be <file>::<name>::<kind> or <name>::<kind>' },\n);\nexport type EdgeTarget = z.infer<typeof EdgeTargetSchema>;\n\n// ── Symbol Annotation ───────────────────────────────────────────\n\n/** Host-language attribute or decorator on a symbol (protocol v2). Shape only; plugins enforce caps. */\nexport const SymbolAnnotationSchema = z.object({\n name: z.string().min(1),\n args: z.array(z.string()).readonly().optional(),\n named: z.record(z.string(), z.string()).readonly().optional(),\n});\nexport type SymbolAnnotation = z.infer<typeof SymbolAnnotationSchema>;\n\n// ── Symbol Node ─────────────────────────────────────────────────\n\nexport const SymbolNodeSchema = z\n .object({\n symbolId: SymbolIdSchema,\n name: z.string().min(1),\n kind: SymbolKindSchema,\n startLine: z.number().int().nonnegative(),\n endLine: z.number().int().nonnegative(),\n startOffset: z.number().int().nonnegative().optional(),\n endOffset: z.number().int().nonnegative().optional(),\n annotations: z.array(SymbolAnnotationSchema).readonly().optional(),\n })\n .refine((node) => node.endLine >= node.startLine, {\n message: 'endLine must be >= startLine',\n });\nexport type SymbolNode = z.infer<typeof SymbolNodeSchema>;\n\n// ── Graph Edge ──────────────────────────────────────────────────\n\nexport const GraphEdgeSchema = z.object({\n from: SymbolIdSchema, // strict 3-part: from is always a local, resolved symbol\n to: EdgeTargetSchema, // 3-part OR 2-part name-ref for cross-file/unresolved targets\n kind: EdgeKindSchema,\n typeOnly: z.boolean().optional(),\n});\nexport type GraphEdge = z.infer<typeof GraphEdgeSchema>;\n\n// ── Commit Intent ───────────────────────────────────────────────\n\nexport const CommitIntentSchema = z.object({\n hash: z.string().min(1),\n message: z.string(),\n date: z.string().min(1),\n kind: z.literal('commit'),\n});\nexport type CommitIntent = z.infer<typeof CommitIntentSchema>;\n\n// ── Anti-Pattern ────────────────────────────────────────────────\n\nexport const AntiPatternSchema = z.object({\n hash: z.string().min(1),\n message: z.string(),\n date: z.string().min(1),\n});\nexport type AntiPattern = z.infer<typeof AntiPatternSchema>;\n\n// ── Code Fact ───────────────────────────────────────────────────\n\nexport const FactKindSchema = z.enum(FACT_KINDS);\nexport type FactKind = z.infer<typeof FactKindSchema>;\n\n/** A fact extracted from a symbol body (protocol v2). The line is 0-based, like a symbol's start line. */\nexport const CodeFactSchema = z.object({\n kind: FactKindSchema,\n symbolId: SymbolIdSchema,\n line: z.number().int().nonnegative(),\n text: z.string(),\n detail: z.record(z.string(), z.string()).readonly().optional(),\n});\nexport type CodeFact = z.infer<typeof CodeFactSchema>;\n\n// ── File Index ──────────────────────────────────────────────────\n\nexport const ComplexityMetricsSchema = z.object({\n symbolId: z.string().min(1),\n cyclomatic: z.number().nonnegative(),\n});\n\nexport const FileIndexSchema = z.object({\n file: z.string().min(1),\n lastModified: z.number().nonnegative(),\n contentHash: z.string().optional(),\n symbols: z.array(SymbolNodeSchema),\n edges: z.array(GraphEdgeSchema),\n complexity: z.array(ComplexityMetricsSchema).optional(),\n intent: z.array(CommitIntentSchema),\n antiPatterns: z.array(AntiPatternSchema),\n facts: z.array(CodeFactSchema).readonly().optional(),\n});\nexport type FileIndex = z.infer<typeof FileIndexSchema>;\n\n// ── Logic-Slice Result ──────────────────────────────────────────\n\nexport interface LogicSliceResult {\n readonly root: SymbolNode;\n readonly dependencies: readonly SymbolNode[];\n readonly edges: readonly GraphEdge[];\n}\n\n// ── Formatted Slice ─────────────────────────────────────────────\n\nexport interface TruncationInfo {\n readonly truncated: true;\n readonly reason: 'token_budget_exceeded';\n}\n\nexport interface FormattedSlice {\n readonly root: SymbolNode;\n readonly dependencies: readonly SymbolNode[];\n readonly edges: readonly GraphEdge[];\n readonly level: DetailLevel;\n readonly levelDescription?: string;\n readonly truncation?: TruncationInfo;\n}\n\n// ── Complexity & Churn ──────────────────────────────────────────\n\nexport interface ComplexityMetrics {\n readonly symbolId: string;\n readonly cyclomatic: number;\n}\n\nexport interface ChurnData {\n readonly filePath: string;\n readonly commitCount: number;\n}\n\nexport const SCORE_BANDS = ['low', 'medium', 'high'] as const;\nexport type ScoreBand = (typeof SCORE_BANDS)[number];\n\nexport interface ChangeIntelligenceScore {\n readonly symbolId: string;\n readonly complexity: number;\n readonly churn: number;\n readonly composite: number;\n readonly band: ScoreBand;\n}\n\n// ── Why-Context Result ──────────────────────────────────────────\n\nexport interface WhyContextResult {\n readonly commitHistory: readonly CommitIntent[];\n readonly antiPatternWarnings: readonly AntiPattern[];\n readonly changeIntelligence?: ChangeIntelligenceScore;\n}\n\n// ── Co-Change Analysis ─────────────────────────────────────────\n\nexport interface CoChangeEntry {\n readonly file1: string;\n readonly file2: string;\n readonly sharedCommits: number;\n readonly frequency: number;\n}\n\n/** The window a co-change matrix was built from and the filters that shaped it. */\nexport interface CoChangeWindowMeta {\n /** Configured commit limit: the last N non-merge commits from HEAD, or 'all'. */\n readonly maxCommits: number | 'all';\n /** Configured duration limit such as \"12m\", anchored to HEAD's committer date. */\n readonly since?: string;\n /** Short SHA of HEAD when the matrix was built. */\n readonly head: string;\n /** Commits inside the window. */\n readonly commitsScanned: number;\n /** Window commits that fed the counts. */\n readonly commitsUsed: number;\n /** Window commits skipped for touching more than `maxFilesPerCommit` included files. */\n readonly massSkipped: number;\n readonly maxFilesPerCommit: number;\n readonly minShared: number;\n}\n\nexport interface CoChangeMatrix {\n readonly version: 1;\n readonly timestamp: number;\n /** Absent in matrices written before the window was recorded. */\n readonly window?: CoChangeWindowMeta;\n readonly entries: readonly CoChangeEntry[];\n}\n\nconst CoChangeEntrySchema = z.object({\n file1: z.string().min(1),\n file2: z.string().min(1),\n sharedCommits: z.number().int().min(1),\n frequency: z.number().min(0).max(1),\n});\n\nconst CoChangeWindowMetaSchema = z.object({\n maxCommits: z.union([z.number().int().min(1), z.literal('all')]),\n since: z.string().optional(),\n head: z.string().min(1),\n commitsScanned: z.number().int().min(0),\n commitsUsed: z.number().int().min(0),\n massSkipped: z.number().int().min(0),\n maxFilesPerCommit: z.number().int().min(1),\n minShared: z.number().int().min(1),\n});\n\n/**\n * Validates `co-changes.json`. `entries` is required and `window` optional, so files written before\n * the window was recorded still load. Unknown keys are dropped rather than rejected, so a file from\n * a newer Ctxo stays readable.\n */\nexport const CoChangeMatrixSchema: z.ZodType<CoChangeMatrix> = z.object({\n version: z.literal(1),\n timestamp: z.number(),\n window: CoChangeWindowMetaSchema.optional(),\n entries: z.array(CoChangeEntrySchema),\n});\n\n// ── Commit Record (from git adapter) ────────────────────────────\n\nexport interface CommitRecord {\n readonly hash: string;\n readonly message: string;\n readonly date: string;\n readonly author: string;\n}\n\n// ── Change Log (from git adapter) ───────────────────────────────\n\n/** git name-status letters Ctxo keeps; A added, M modified, D deleted, R renamed, C copied, T type changed. */\nexport type FileChangeStatus = 'A' | 'M' | 'D' | 'R' | 'C' | 'T';\n\nexport interface FileChange {\n readonly status: FileChangeStatus;\n /** Path after the change: repo-relative, forward slashes. */\n readonly path: string;\n /** Path before the change; set only for renames (R) and copies (C). */\n readonly oldPath?: string;\n}\n\n/** One non-merge commit and the files it changed. */\nexport interface CommitChangeSet {\n /** Full commit SHA (git %H). */\n readonly hash: string;\n /** Subject line (git %s). */\n readonly message: string;\n /** Author date, strict ISO 8601 (git %aI). */\n readonly date: string;\n /** Author name (git %an). */\n readonly author: string;\n /** Committer date in unix seconds (git %ct). */\n readonly committedAt: number;\n readonly changes: readonly FileChange[];\n}\n\nexport interface ChangeLog {\n /** Short SHA of HEAD (git %h), which may itself be a merge commit. */\n readonly head: string;\n /** Committer date of HEAD in unix seconds (git %ct). */\n readonly headCommittedAt: number;\n /** Non-merge commits reachable from HEAD, newest first. */\n readonly commits: readonly CommitChangeSet[];\n}\n\n// ── Blame Line (from git adapter) ───────────────────────────────\n\nexport interface BlameLine {\n readonly hash: string;\n readonly lineNumber: number;\n readonly author: string;\n readonly date: string;\n}\n\n// ── Search Types ───────────────────────────────────────────────\n\nexport interface SearchResult {\n symbolId: string;\n name: string;\n kind: string;\n filePath: string;\n /** BM25 relevance score (higher = more relevant) */\n relevanceScore: number;\n /** PageRank importance score (0-1) */\n importanceScore: number;\n /** Final combined score after all boosts */\n combinedScore: number;\n}\n\nexport interface SearchMetrics {\n porterHits: number;\n trigramHits: number;\n phase2Activated: boolean;\n fuzzyApplied: boolean;\n latencyMs: number;\n}\n\nexport interface FuzzyCorrection {\n originalQuery: string;\n correctedQuery: string;\n corrections: Array<{\n original: string;\n corrected: string;\n distance: number;\n }>;\n}\n\nexport interface SearchResponse {\n query: string;\n results: SearchResult[];\n metrics: SearchMetrics;\n fuzzyCorrection?: FuzzyCorrection;\n}\n\n// ── Community Detection ────────────────────────────────────────\n\nexport interface CommunityEntry {\n readonly symbolId: string;\n readonly communityId: number;\n readonly communityLabel: string;\n}\n\nexport interface GodNode {\n readonly symbolId: string;\n readonly bridgedCommunities: readonly number[];\n readonly centralityScore: number;\n}\n\nexport const EDGE_QUALITY = ['full', 'syntax-only', 'mixed'] as const;\nexport type EdgeQuality = (typeof EDGE_QUALITY)[number];\n\nexport interface CommunitySnapshot {\n readonly version: 1;\n readonly computedAt: string;\n readonly commitSha: string;\n readonly modularity: number;\n readonly communities: readonly CommunityEntry[];\n readonly godNodes: readonly GodNode[];\n readonly edgeQuality: EdgeQuality;\n readonly crossClusterEdges: number;\n}\n\nexport interface DriftEvent {\n readonly symbolId: string;\n readonly movedFrom: { readonly id: number; readonly label: string };\n readonly movedTo: { readonly id: number; readonly label: string };\n readonly firstSeenInNewCluster: string;\n}\n\nexport interface BoundaryViolation {\n readonly from: {\n readonly symbolId: string;\n readonly communityId: number;\n readonly label: string;\n };\n readonly to: {\n readonly symbolId: string;\n readonly communityId: number;\n readonly label: string;\n };\n readonly edgeKind: EdgeKind;\n readonly historicalEdgesBetweenClusters: number;\n readonly severity: 'high' | 'medium';\n}\n\n// ── Persisted architectural artifacts ──────────────────────────\n// Written by `ctxo index` next to communities.json so the static\n// HTML visualizer can render Architecture views without re-running\n// detectors in the browser.\n\nexport type ConfidenceLevel = 'high' | 'medium' | 'low';\n\nexport interface DriftEventsArtifact {\n readonly version: 1;\n readonly computedAt: string;\n readonly commitSha: string;\n readonly confidence: ConfidenceLevel;\n /** The number of history entries passed to the detector (kept for compatibility; confidence is built on `distinctCommits`). */\n readonly snapshotsAvailable: number;\n /** Commits other than `commitSha` that the history covers; `confidence` is built on this. */\n readonly distinctCommits: number;\n readonly hint?: string;\n readonly events: readonly DriftEvent[];\n}\n\nexport interface BoundaryViolationsArtifact {\n readonly version: 1;\n readonly computedAt: string;\n readonly commitSha: string;\n readonly confidence: ConfidenceLevel;\n /** The number of history entries passed to the detector (kept for compatibility; confidence is built on `distinctCommits`). */\n readonly snapshotsAvailable: number;\n /** Commits other than `commitSha` that the history covers; `confidence` is built on this. */\n readonly distinctCommits: number;\n readonly hint?: string;\n readonly violations: readonly BoundaryViolation[];\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/staleness/staleness-detector.ts"],"sourcesContent":["import { statSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport interface StalenessWarning {\n readonly staleFiles: string[];\n readonly message: string;\n}\n\nexport class StalenessDetector {\n private readonly projectRoot: string;\n private readonly indexDir: string;\n\n constructor(projectRoot: string, ctxoRoot: string) {\n this.projectRoot = projectRoot;\n this.indexDir = join(ctxoRoot, 'index');\n }\n\n check(indexedFiles: readonly string[]): StalenessWarning | undefined {\n if (!existsSync(this.indexDir)) return undefined;\n\n const staleFiles: string[] = [];\n\n for (const relativePath of indexedFiles) {\n const sourcePath = join(this.projectRoot, relativePath);\n const indexPath = join(this.indexDir, `${relativePath}.json`);\n\n if (!existsSync(sourcePath) || !existsSync(indexPath)) continue;\n\n try {\n const sourceMtime = Math.floor(statSync(sourcePath).mtimeMs / 1000);\n const indexMtime = Math.floor(statSync(indexPath).mtimeMs / 1000);\n\n if (sourceMtime > indexMtime) {\n staleFiles.push(relativePath);\n }\n } catch {\n // Skip files we can't stat\n }\n }\n\n if (staleFiles.length === 0) return undefined;\n\n return {\n staleFiles,\n message: `Index may be stale for ${staleFiles.length} file(s). Run \"ctxo index\" to refresh.`,\n };\n }\n}\n"],"mappings":";;;AAAA,SAAS,UAAU,kBAAkB;AACrC,SAAS,YAAY;AAOd,IAAM,oBAAN,MAAwB;AAAA,EACZ;AAAA,EACA;AAAA,EAEjB,YAAY,aAAqB,UAAkB;AACjD,SAAK,cAAc;AACnB,SAAK,WAAW,KAAK,UAAU,OAAO;AAAA,EACxC;AAAA,EAEA,MAAM,cAA+D;AACnE,QAAI,CAAC,WAAW,KAAK,QAAQ,EAAG,QAAO;AAEvC,UAAM,aAAuB,CAAC;AAE9B,eAAW,gBAAgB,cAAc;AACvC,YAAM,aAAa,KAAK,KAAK,aAAa,YAAY;AACtD,YAAM,YAAY,KAAK,KAAK,UAAU,GAAG,YAAY,OAAO;AAE5D,UAAI,CAAC,WAAW,UAAU,KAAK,CAAC,WAAW,SAAS,EAAG;AAEvD,UAAI;AACF,cAAM,cAAc,KAAK,MAAM,SAAS,UAAU,EAAE,UAAU,GAAI;AAClE,cAAM,aAAa,KAAK,MAAM,SAAS,SAAS,EAAE,UAAU,GAAI;AAEhE,YAAI,cAAc,YAAY;AAC5B,qBAAW,KAAK,YAAY;AAAA,QAC9B;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,QAAI,WAAW,WAAW,EAAG,QAAO;AAEpC,WAAO;AAAA,MACL;AAAA,MACA,SAAS,0BAA0B,WAAW,MAAM;AAAA,IACtD;AAAA,EACF;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/adapters/storage/atomic-write.ts"],"sourcesContent":["import { readdirSync, renameSync, statSync, unlinkSync, writeFileSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { createLogger } from '../../core/logger.js';\n\nconst log = createLogger('ctxo:storage');\n\nconst TMP_SUFFIX_RE = /\\.\\d+\\.tmp$/;\nconst TMP_STALE_THRESHOLD_MS = 60_000; // anything older than 60 s is from a dead process\n\nexport function atomicWrite(targetPath: string, content: string): void {\n const tmpPath = `${targetPath}.${process.pid}.tmp`;\n writeFileSync(tmpPath, content, 'utf-8');\n renameSync(tmpPath, targetPath);\n}\n\n/**\n * Remove orphaned .<pid>.tmp files left behind when a previous write crashed\n * (SIGKILL / power loss / ENOSPC). Only files older than a threshold are\n * removed so we never race a concurrent writer.\n *\n * Safe to call on every startup — walks the supplied directory non-recursively.\n */\nexport function sweepStaleTmpFiles(dir: string, now: number = Date.now()): number {\n if (!existsSync(dir)) return 0;\n let cleaned = 0;\n try {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.isSymbolicLink()) continue;\n if (entry.isDirectory()) {\n cleaned += sweepStaleTmpFiles(join(dir, entry.name), now);\n continue;\n }\n if (!TMP_SUFFIX_RE.test(entry.name)) continue;\n const full = join(dir, entry.name);\n try {\n const age = now - statSync(full).mtimeMs;\n if (age < TMP_STALE_THRESHOLD_MS) continue;\n unlinkSync(full);\n cleaned++;\n } catch (err) {\n log.error(`failed to sweep ${entry.name}: ${(err as Error).message}`);\n }\n }\n } catch (err) {\n log.error(`failed to scan ${dir}: ${(err as Error).message}`);\n }\n return cleaned;\n}\n"],"mappings":";;;;;;AAAA,SAAS,aAAa,YAAY,UAAU,YAAY,eAAe,kBAAkB;AACzF,SAAS,YAAY;AAIrB,IAAM,MAAM,aAAa,cAAc;AAEvC,IAAM,gBAAgB;AACtB,IAAM,yBAAyB;AAExB,SAAS,YAAY,YAAoB,SAAuB;AACrE,QAAM,UAAU,GAAG,UAAU,IAAI,QAAQ,GAAG;AAC5C,gBAAc,SAAS,SAAS,OAAO;AACvC,aAAW,SAAS,UAAU;AAChC;AASO,SAAS,mBAAmB,KAAa,MAAc,KAAK,IAAI,GAAW;AAChF,MAAI,CAAC,WAAW,GAAG,EAAG,QAAO;AAC7B,MAAI,UAAU;AACd,MAAI;AACF,eAAW,SAAS,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC,GAAG;AAC7D,UAAI,MAAM,eAAe,EAAG;AAC5B,UAAI,MAAM,YAAY,GAAG;AACvB,mBAAW,mBAAmB,KAAK,KAAK,MAAM,IAAI,GAAG,GAAG;AACxD;AAAA,MACF;AACA,UAAI,CAAC,cAAc,KAAK,MAAM,IAAI,EAAG;AACrC,YAAM,OAAO,KAAK,KAAK,MAAM,IAAI;AACjC,UAAI;AACF,cAAM,MAAM,MAAM,SAAS,IAAI,EAAE;AACjC,YAAI,MAAM,uBAAwB;AAClC,mBAAW,IAAI;AACf;AAAA,MACF,SAAS,KAAK;AACZ,YAAI,MAAM,mBAAmB,MAAM,IAAI,KAAM,IAAc,OAAO,EAAE;AAAA,MACtE;AAAA,IACF;AAAA,EACF,SAAS,KAAK;AACZ,QAAI,MAAM,kBAAkB,GAAG,KAAM,IAAc,OAAO,EAAE;AAAA,EAC9D;AACA,SAAO;AACT;","names":[]}