@titan-design/code-read 0.0.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/LICENSE +21 -0
- package/README.md +185 -0
- package/dist/chunk-VXQFCVGB.js +1326 -0
- package/dist/chunk-VXQFCVGB.js.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +416 -0
- package/dist/index.js.map +1 -0
- package/dist/query/index.d.ts +953 -0
- package/dist/query/index.js +89 -0
- package/dist/query/index.js.map +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Henry Jewkes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# @titan-design/code-read
|
|
2
|
+
|
|
3
|
+
A versioned read API over `@titan-design/code-graph` snapshots, for three kinds of consumer:
|
|
4
|
+
a drill-down report UI (live or from a static export), agents over MCP, and workflows. The
|
|
5
|
+
package holds one contract, one per-snapshot `ReadModel`, and one pure query function per
|
|
6
|
+
command. The daemon's commands and a browser's static dataset both answer through those
|
|
7
|
+
functions.
|
|
8
|
+
|
|
9
|
+
Tier 2 of the titan-platform DAG (TP-184). Depends on `code-graph`, `registry`, and
|
|
10
|
+
`rpc-protocol`; `zod` is a peer.
|
|
11
|
+
|
|
12
|
+
## Two entry points
|
|
13
|
+
|
|
14
|
+
| Import | Runs in | Holds |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| `@titan-design/code-read/query` | browser or Node | the contract (`CONTRACT`, `CODE_READ_API_VERSION`, zod schemas), `ReadModel` and `buildReadModel`, the `ReadSource` seam, `QUERIES`, `createQueryResolver` |
|
|
17
|
+
| `@titan-design/code-read` | Node | everything in `./query`, plus `loadReadModel`, `createLiveSource` (SQLite plus an LRU of models), and `registerCodeReadCommands` |
|
|
18
|
+
|
|
19
|
+
`./query` imports only its own files, `zod`, and `rpc-protocol`. Three rules in
|
|
20
|
+
`.codewatch/check.json` (`code-read-query-*`) and `src/browser-safe.test.ts` enforce that.
|
|
21
|
+
The test bundles the subpath with esbuild for `platform: "browser"` and expects no warnings.
|
|
22
|
+
|
|
23
|
+
## Commands (contract 0.1.2)
|
|
24
|
+
|
|
25
|
+
| Command | Args | Result |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| `api.describe` | none | `api`, `dataset`, `commands`, `newest`, `indexVersions`, `capabilities`, `metrics` (catalogue descriptors with provenance), `rules` |
|
|
28
|
+
| `snapshot.list` | `ref?`, `limit` (1 to 500, default 50) | `snapshots`, newest first |
|
|
29
|
+
| `hierarchy.get` | `snapshot?`, `root?`, `depth` (1 to 8, default 2), `metrics` (default `["loc"]`), `baseline?`, `include_symbols`, `exclude_roles` | `snapshotId`, `baselineSnapshotId?`, `comparable?`, `nodes` (flat, shallowest first, with `parentId`, `depth`, `childCount`, `values`, `missing?`, `deltas?`), `truncated` |
|
|
30
|
+
| `node.get` | `snapshot?`, `id`, `baseline?`, `metrics` (default: every one that applies) | `node`, `ancestors` (repo first), `childCounts`, `metrics` (value, `direction`, `rollup`, `percentile`, `siblingMedian`, `siblingRank`, `siblingCount`, `baseline?`, `delta?`, `missing?`) |
|
|
31
|
+
| `node.resolve` | exactly one of `query` or `path`, plus `line?` with `path`, `limit` (1 to 50, default 10) | `candidates`: `node`, `score`, `match` |
|
|
32
|
+
| `findings.list` | `snapshot?`, `baseline?`, `scope?`, filters `rule`, `severity`, `tool`, `provenance`, `kind`, `status` (arrays, empty means all), `sort` (`severity` default, `excess`, `value`, `path`, `rule`), `order` (`desc` default), `offset`, `limit` (0 to 500, default 20), `facets` | `snapshotId`, `baselineSnapshotId?`, `comparable?`, `rows` (`Finding`), `total`, `facets?` |
|
|
33
|
+
| `finding.get` | `snapshot?`, `id`, `baseline?`, `context_lines` (0 to 20, default 5) | `finding`, `rule` (with `text`), `measured`, `why`, `excerpt` (or null with `excerptMissing`), `related` (at most 10) |
|
|
34
|
+
| `node.neighbors` | `snapshot?`, `id` (a stored node), `direction` (`both` default), `edge_kinds`, `metrics` (default `loc`, `utilization`), `offset`, `limit` (1 to 100, default 20) | `snapshotId`, `node`, `inbound`, `outbound` (each `node`, `kind`, `weight`, `specifier?`, `values`), `total` per side |
|
|
35
|
+
|
|
36
|
+
Arguments are snake_case and results are camelCase. `snapshot` and `baseline` take an id, a
|
|
37
|
+
digit string, or a ref name (that ref's newest snapshot). The rest of the design's 14
|
|
38
|
+
commands arrive in later minor versions of the contract.
|
|
39
|
+
|
|
40
|
+
## The hierarchy
|
|
41
|
+
|
|
42
|
+
code-graph stores files and symbols only, so `./query` synthesizes the rest per snapshot:
|
|
43
|
+
|
|
44
|
+
- **Ids.** The repo is `""`. A directory is its path plus `/`, such as `src/util/`; no file id
|
|
45
|
+
ends in `/`. Module and external nodes are not in the hierarchy.
|
|
46
|
+
- **Class level.** A symbol named `Job.run` hangs under `Job` when that symbol exists, else
|
|
47
|
+
under the nearest existing scope. A bare name (snapshots before code-graph 0.14.0) hangs
|
|
48
|
+
under the smallest symbol whose span encloses it, else under its file.
|
|
49
|
+
- **Rollups.** A directory's value, or a file's value for a symbol-only metric, combines the
|
|
50
|
+
stored values below it by the catalogue's `rollup` rule: `sum`, `max`, or `mean`. A file
|
|
51
|
+
metric rolls up from files only, so nothing counts twice. `absent: "zero"` counts a
|
|
52
|
+
missing row as 0; `absent: "exclude"` leaves it out.
|
|
53
|
+
- **Why a value is null.** `missing` says why: `no-rollup` (the rule is `none`, as for commit
|
|
54
|
+
and author counts, bus factor, and fan-in; directory-level history is TP-233),
|
|
55
|
+
`not-measured`, `not-applicable` (such as `loc` on a symbol), or `not-in-snapshot`.
|
|
56
|
+
- **Roles.** `exclude_roles` drops files of those roles from the rows, the rollups, and the
|
|
57
|
+
baseline alike.
|
|
58
|
+
- **Deltas.** Baseline nodes match by id only; alias following is TP-187. `comparable` is
|
|
59
|
+
false when the two snapshots have different index versions.
|
|
60
|
+
- **Size.** `hierarchy.get` returns at most `HIERARCHY_ROW_CAP` (5,000) rows breadth-first
|
|
61
|
+
and sets `truncated`. `childCount` always counts every child, symbols included.
|
|
62
|
+
|
|
63
|
+
`node.get` percentiles are the share of same-kind nodes whose value is at most the node's,
|
|
64
|
+
0 to 100. Siblings are the same-kind children of the same parent, the node included; rank 1
|
|
65
|
+
is the largest value. Neither number knows which way is worse. Read `direction` on the same
|
|
66
|
+
metric before labelling anything "top" or "best".
|
|
67
|
+
|
|
68
|
+
Worked example. A file with `loc: 3` among sibling files with 0, 1, 2, and 2 lines gets
|
|
69
|
+
`siblingRank: 1`, and 7 of the snapshot's 10 files have at most 3 lines, so `percentile: 70`.
|
|
70
|
+
`loc` is `direction: "higher-worse"`, so rank 1 is the file with the most lines. It is the
|
|
71
|
+
worst offender in its directory, not the best file. For `bus_factor_30d`
|
|
72
|
+
(`direction: "lower-worse"`), rank 1 is the file with the most authors covering its churn,
|
|
73
|
+
which is the safest file. For a `neutral` metric such as `churn_30d`, rank 1 only means
|
|
74
|
+
"largest".
|
|
75
|
+
|
|
76
|
+
`node.resolve` ports codewatch's `rankSearch` cascade over directories, files, and symbols,
|
|
77
|
+
case-insensitive: exact id or name 100, id suffix after `/` or `#` 80, name prefix 60, id
|
|
78
|
+
substring 40, name substring 30, ties by id. One addition for qualified names: a symbol whose
|
|
79
|
+
last segment equals the query, such as `run` for `Task.run`, scores as a suffix. A `path`
|
|
80
|
+
with a `line`, or a `query` of the form `path:line`, maps each matching file to the innermost
|
|
81
|
+
symbol whose span holds the line.
|
|
82
|
+
|
|
83
|
+
## Findings
|
|
84
|
+
|
|
85
|
+
Until the findings store exists (design gap G11), every finding is a check-rule violation
|
|
86
|
+
**derived on read**. The live source runs the product's rules through code-graph's own
|
|
87
|
+
`snapshotViolations` when it loads a snapshot, so `findings.list` returns exactly what
|
|
88
|
+
`graph check` reports. Each row says `provenance: { kind: "derived", source: "check/<rule>" }`
|
|
89
|
+
and `tool: "check"`. Stored findings, verdicts, and themes will arrive behind the same
|
|
90
|
+
`Finding` schema.
|
|
91
|
+
|
|
92
|
+
- **Ids.** A finding's id is code-graph's `violationKey`: `rule|node` or
|
|
93
|
+
`rule|node|destination`. Treat it as opaque. It stays the same across snapshots while
|
|
94
|
+
the rule id, the node id, and the destination are unchanged. A moved file gets a new id
|
|
95
|
+
until rename-aware keys land (TP-187).
|
|
96
|
+
- **Order.** `sort` picks the primary key and `order` flips only that key. Ties always break
|
|
97
|
+
the same way: severity (error, warning, info, then unknown), then larger `excess`, then
|
|
98
|
+
node path, rule, and id. The id is unique, so the order is total and `offset` pages never
|
|
99
|
+
repeat or skip a row. Rows with no `excess` or `value` sort last in either direction.
|
|
100
|
+
- **Excess.** `value / threshold` for a maximum rule and `threshold / value` for a minimum
|
|
101
|
+
rule, so larger is always worse. Import rules have no threshold, so `excess` is null.
|
|
102
|
+
- **Facets.** With `facets: true`, counts per `rule`, `severity`, `tool`, `provenance`,
|
|
103
|
+
`kind`, and `child`, plus `status` with a baseline. They count every row that passed the
|
|
104
|
+
filters, so each facet sums to `total`. `child` is the scope's child (a directory id or a
|
|
105
|
+
file) that holds the finding, which is what a drill-down view shows next. `limit: 0` with
|
|
106
|
+
`facets: true` gives headline counts in one call.
|
|
107
|
+
- **Scope.** A directory scope holds everything under it, a file scope holds the file and
|
|
108
|
+
its symbols, and the repo (`""`, the default) holds everything.
|
|
109
|
+
- **Status.** With `baseline`, each row is `new`, `carryover`, `worsened`, or `improved`
|
|
110
|
+
(by `excess`), and findings that no longer occur come back from the baseline as
|
|
111
|
+
`resolved`, with the baseline's `snapshotId`. A `status` filter without a baseline is
|
|
112
|
+
DATAERR.
|
|
113
|
+
- **Excerpts.** `finding.get` shows the flagged lines plus `context_lines` either side,
|
|
114
|
+
clipped to the file and capped at `EXCERPT_LINE_CAP` (80) lines with `truncated: true`.
|
|
115
|
+
Edges carry no line numbers, so an import finding's flagged line is the one that names
|
|
116
|
+
the import's specifier in quotes; the live source finds it when it loads the snapshot. A
|
|
117
|
+
metric finding covers its whole file: no `range`, no highlight, and the excerpt starts at
|
|
118
|
+
line 1. The live source reads the working tree under `repoRoot` and serves a file only
|
|
119
|
+
when its hash equals the snapshot's fingerprint. Otherwise `excerpt` is null and
|
|
120
|
+
`excerptMissing` says `changed-since-snapshot`. A static source serves the windows it
|
|
121
|
+
exported and says `not-in-export` for the rest.
|
|
122
|
+
- **Neighbours.** `node.neighbors` needs a stored node: a file, symbol, module, or
|
|
123
|
+
external. A synthesized directory is DATAERR. Each side is ranked by edge `weight`
|
|
124
|
+
(code-graph's reference count), heaviest first, then by neighbour id and edge kind, and
|
|
125
|
+
paged on its own. Empty `edge_kinds` means every kind except `references` for a
|
|
126
|
+
non-symbol node, because `references` edges are the symbol layer.
|
|
127
|
+
|
|
128
|
+
**Trap: a derived finding is not a record.** It exists only while its rule, at its
|
|
129
|
+
current threshold, still fires. Change a rule's threshold, rename a rule, or remove it,
|
|
130
|
+
and its findings vanish from every snapshot, old ones included, and their ids never come
|
|
131
|
+
back. A finding's `status` also depends on the baseline you pass: the same row is
|
|
132
|
+
`carryover` against one snapshot and `new` against another. Do not persist a derived id
|
|
133
|
+
as if it were durable until the findings store lands.
|
|
134
|
+
|
|
135
|
+
## Serving the commands
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { openCodeGraph, loadCheckRules } from "@titan-design/code-graph";
|
|
139
|
+
import { registerCodeReadCommands } from "@titan-design/code-read";
|
|
140
|
+
import { startDaemon } from "@titan-design/daemon";
|
|
141
|
+
import { createRegistry } from "@titan-design/registry";
|
|
142
|
+
|
|
143
|
+
const registry = createRegistry();
|
|
144
|
+
const rules = await loadCheckRules(".codewatch/check.json");
|
|
145
|
+
registerCodeReadCommands(registry, {
|
|
146
|
+
openStore: () => openCodeGraph(".codewatch/graph.db"),
|
|
147
|
+
rules: () => rules,
|
|
148
|
+
repoRoot: gitToplevel,
|
|
149
|
+
});
|
|
150
|
+
await startDaemon({ registry, createContext: () => ({ warnings: [], format: "json" }), version, stateDir, toolPrefix: "codewatch__" });
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`rules` must return the same array while the rules are unchanged: the live source compares
|
|
154
|
+
arrays by identity and drops every cached model when a different one comes back.
|
|
155
|
+
`repoRoot` is the git toplevel the snapshots were indexed from; leave it out and
|
|
156
|
+
`finding.get` returns no excerpts. Deriving findings adds a rule-evaluation pass to each model
|
|
157
|
+
load, about 25 ms on titan-platform's own index.
|
|
158
|
+
|
|
159
|
+
The daemon then answers `POST /rpc/api.describe` and the MCP tool `codewatch__api__describe`
|
|
160
|
+
with the same envelope as an in-process `invokeCommand`. The daemon lists every registered
|
|
161
|
+
command as a tool. To follow the design and put only the agent-shaped commands on MCP,
|
|
162
|
+
register only the `defineCodeReadCommands(source)` entries whose names are in
|
|
163
|
+
`AGENT_COMMANDS` on the registry that serves MCP.
|
|
164
|
+
|
|
165
|
+
## Answering without a daemon
|
|
166
|
+
|
|
167
|
+
`createQueryResolver(source)` returns `(name, args) => JsonEnvelope`. It validates raw args
|
|
168
|
+
against the contract, runs the query, and never throws. It uses the same codes as the
|
|
169
|
+
daemon: `DATAERR` for bad args, `NOINPUT` for a missing snapshot, `UNAVAILABLE` for a command
|
|
170
|
+
the source does not serve, and `USAGE` for an unknown name. The resolver fits
|
|
171
|
+
`@titan-design/rpc-client`'s static `resolve` option. Any `ReadSource` works, for example
|
|
172
|
+
one decoded from a static dataset.
|
|
173
|
+
|
|
174
|
+
## Versioning
|
|
175
|
+
|
|
176
|
+
`CODE_READ_API_VERSION` is the contract's semver. `contract.lock.json` stores every
|
|
177
|
+
command's args and result as JSON Schema. `src/contract-lock.test.ts` fails when the schemas
|
|
178
|
+
change and the version does not. Its failure message says whether the change is breaking
|
|
179
|
+
and names each breaking path. After bumping, regenerate the lock:
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
UPDATE_CONTRACT_LOCK=1 pnpm vitest run packages/code-read/src/contract-lock.test.ts
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The lock leaves out descriptions, so a documentation edit needs no bump.
|