@titan-design/code-graph 0.10.0 → 0.13.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
@@ -73,19 +73,50 @@ index version 0.14.0:
73
73
  first `#`. A top-level declaration's qualified name is its own name
74
74
  (`src/a.ts#createThing`). A member or nested declaration is prefixed by its enclosing named
75
75
  scopes, joined with `.`: `src/a.ts#Job.run`, `src/a.ts#outer.helper`, and
76
- `src/a.ts#handlers.onClick` for a method of `const handlers = {…}`. Anonymous scopes, such
77
- as a callback argument or an unbound class expression, add no segment, so ids do not depend
78
- on declaration order. One scope binds a name once: a getter/setter pair, a Python property's
76
+ `src/a.ts#handlers.onClick` for a method of `const handlers = {…}`. An unbound callback
77
+ argument, object or class expression adds one `<anonymous>` segment, and consecutive
78
+ anonymous scopes collapse to one, so
79
+ `src/a.ts#App.<anonymous>.onHash` is a function declared inside a callback in `App`. The
80
+ segment keeps it apart from a member `App.onHash`, and ids do not depend on declaration
81
+ order. One scope binds a name once: a getter/setter pair, a Python property's
79
82
  accessors, and overloads each share one node. Index versions before 0.14.0 keyed members by
80
83
  bare name, so same-named methods in one file collapsed into one node (TP-182).
81
84
  - An **external** id is `npm:<package>` (scope-aware) or the `node:` builtin verbatim.
82
85
 
83
- `NodeKind`, `EdgeKind`, and the `role` vocabulary are unchanged. So is the property the DAG
86
+ `NodeKind` and `EdgeKind` are unchanged, and the `role` vocabulary only grew (see
87
+ [File roles](#file-roles)). So is the property the DAG
84
88
  check rests on: an import of a workspace package by its published name resolves to that
85
89
  package's source file, not to an `npm:` external, by remapping the `dist/*.d.ts` entry
86
90
  ts-morph resolves back onto `src/`. That remap needs the target package built, which is why
87
91
  `pnpm build` precedes both `pnpm test` and `dag:check`.
88
92
 
93
+ ## File roles
94
+
95
+ Every file and module node carries a `role`, one of `ALL_ROLES`: `test`, `fixture`, `story`,
96
+ `lab`, `barrel`, `types`, `config`, `script`, `entry`, `generated` and `source`. The first
97
+ match wins, in this order:
98
+
99
+ 1. `generated`: a `.gitattributes linguist-generated` path or a `*.gen.*`/`generated/` path.
100
+ 2. A configured glob from `.codewatch/roles.json` (below).
101
+ 3. `test`, then `story` (`*.stories.{js,jsx,ts,tsx}` with an optional `c`/`m`, and `*.mdx`),
102
+ so a story under `fixtures/` is still a story.
103
+ 4. `fixture`, `script`, `entry` (a `#!` shebang), `barrel`, `types`, `config`, else `source`.
104
+
105
+ `UNIMPORTED_ROLES` lists the roles nothing imports by design (`test`, `fixture`, `story`, `lab`,
106
+ `config`, `script`, `entry`). Dead-module reachability seeds from them plus `barrel`.
107
+
108
+ `lab` has no built-in rule, since a `lab/` directory name is too generic to guess. A repo
109
+ assigns it, or any other role, with `.codewatch/roles.json`, which maps roles to
110
+ `.gitattributes`-style globs matched against file ids:
111
+
112
+ ```json
113
+ { "lab": ["packages/ui/src/lab/**"] }
114
+ ```
115
+
116
+ `loadRoleGlobs(repoRoot)` reads that file (an absent file configures nothing, an unknown role
117
+ throws), and `computeRoleHints` passes the result to `annotateRoles` as `roleGlobs`. `story`,
118
+ `lab` and roles.json arrived in index version 0.21.0.
119
+
89
120
  ## Identity across renames
90
121
 
91
122
  Added in index version 0.15.0 (TP-187). Git rename detection writes `id_alias` rows mapping a
@@ -227,7 +258,14 @@ the file is unreadable.
227
258
  The rules engine turns a snapshot into pass/fail against a `check.json`. Seven rule types:
228
259
  `metric-max`, `metric-min`, `metric-product-max`, `metric-outlier`, `forbid-import`,
229
260
  `layered-deps`, and `no-internal-only-barrels`. Severity defaults to `error`; only new errors
230
- fail a check.
261
+ fail a check. `layered-deps` takes `excludeRoles`: an import is dropped when its source or
262
+ destination file has an excluded role. `forbid-import` takes `except`: destination patterns
263
+ that `to` matches but the rule allows, such as one sanctioned entry file.
264
+
265
+ Validation rejects a rule whose `severity` is anything but `error` or `warning`, whose `kind`
266
+ is not a node kind (`package`, `module`, `file`, `symbol`, `external`), or whose `exclude` is
267
+ not an array of strings. Each of those mistakes used to load silently and disable the rule:
268
+ `"Error"` counted as a warning, `"files"` matched no node, and a string `exclude` was dropped.
231
269
 
232
270
  `metric-outlier` takes its threshold from the snapshot instead of the rule:
233
271
  `{ "type": "metric-outlier", "id": "long-fn", "metric": "symbol_body_lines", "kind": "symbol", "percentile": 95 }`
@@ -281,6 +319,12 @@ file is dropped from coupling too. `symbolSetHash({ unitId, symbolIds }, footpri
281
319
  doc unit's sorted `[symbolId, footprint.hash]` pairs, so it ignores member order and changes when
282
320
  a member is added, removed or changed.
283
321
 
322
+ `diffFootprints(store, { fromSnapshotId, toSnapshotId, ...options })` compares the footprints of
323
+ two snapshots. From-side ids follow the alias chain first, and a symbol under a moved file follows
324
+ its file, so a move reads as `changed` with `reasons: ["renamed"]` alone when its footprint held.
325
+ Each other change is `added`, `removed`, or `changed` with the differing parts (`signature`,
326
+ `consumers`, `coupling`). `files` rolls the changes up to their declaring files, never to consumers.
327
+
284
328
  `scripts/dag-check-self.mjs` in the repo root runs this repo's DAG check on this engine
285
329
  instead of codewatch's CLI.
286
330
 
@@ -390,6 +434,80 @@ snapshotSymbolCoupling(store, snapshotId); // symbol pairs co-imported by 2+ fil
390
434
  The pure `computePageRank`, `computeRelevance`, `computeSymbolConsumers`, and
391
435
  `computeSymbolCoupling` take node and edge arrays instead of a store.
392
436
 
437
+ ### The `./analysis` subpath
438
+
439
+ `@titan-design/code-graph/analysis` re-exports the report, dashboard, and package-architecture
440
+ derivations below without the root's ts-morph, tree-sitter, and SQLite, so a browser bundle
441
+ can import it. `analysis/graph-report-browser-safe.test.ts` keeps its whole import closure
442
+ free of packages and Node builtins, and fails if the barrel re-exports a module it does not
443
+ check. Symbol coupling is not on it: `symbol-coupling.ts` still reaches `node:path`.
444
+
445
+ ### Dashboard derivations
446
+
447
+ Ported with TP-918 from codewatch's `graph dashboard`, unchanged apart from import paths. All
448
+ are pure functions over rows the caller has already read, so they run in a browser:
449
+
450
+ - `collectNodeMetrics` folds metric rows into per-node `NodeMetrics`; `collectSymbolUtil`
451
+ pairs symbol nodes with their utilization; `buildNodeMetrics`, `buildCentralFiles`,
452
+ `buildHotExports`, and `buildBlastRadius` shape them for the files a `GraphReportResult`
453
+ references (`referencedNodes`).
454
+ - `buildSymbolCouplingPayload` caps `computeSymbolCoupling` and `computeSymbolConsumers`
455
+ into co-imported pairs and per-file consumer groups.
456
+ - `classifyCoupling` marks a co-changed pair hidden, expected, or unindexed against a
457
+ `SnapshotContext`; build its `linkedPairs` with `pairKey`.
458
+ - `computeHealth` sums four capped penalties into a score out of 100 with its breakdown.
459
+ Each component has a stable `key` and its `cap`. An optional second argument weighs
460
+ them the caller's way; `DEFAULT_HEALTH_WEIGHTS` is the dashboard's.
461
+
462
+ ### Context dossier and bundle
463
+
464
+ Ported with TP-1454 from codewatch's `graph context`, unchanged apart from import paths. All
465
+ three are deterministic projections of rows the caller has already read; no LLM is involved.
466
+
467
+ - `buildContextDossier(input)` shapes one file or symbol into a `ContextDossier`: metrics,
468
+ churn, centrality, ownership, consumers split into source and test files, coupling
469
+ partners, and blast radius. A file target lists its symbols, exports first, each with an
470
+ `importance` that splits the file's centrality by utilization share. The record carries
471
+ `schemaVersion` (`CONTEXT_SCHEMA_VERSION`) so a store can invalidate old records.
472
+ - `renderContextMarkdown(dossier)` renders the same facts as markdown.
473
+ - `buildContextBundle(input)` wraps a dossier with the source text of the target's span (read
474
+ from `repoRoot`, so this one touches the filesystem), its `references` and `imports` edges as
475
+ explicit callers, dependencies, and coupling partners, and its `coverage_pct`. Pass
476
+ `relevanceByFile` (from `computeRelevance`) and `targetFileId` to order edges by relevance
477
+ to the target instead of by weight. `renderBundleText(bundle)` concatenates it for an
478
+ embedder or an LLM.
479
+
480
+ ### Unused exports and dead modules
481
+
482
+ Ported with TP-1467 from codewatch's `graph report`, unchanged apart from import paths. Both
483
+ are leads, not verdicts, and both drop files that `keepNode` rejects:
484
+
485
+ - `topUnusedExports(symbolNodes, publicApi, ctx, limit)` lists exported symbols whose
486
+ `utilization` is 0 or absent, ranked internal first, then by `symbol_cognitive`
487
+ descending. `publicApiFiles(nodes, edges)` builds `publicApi`: the files a `barrel`-role
488
+ node re-exports one hop away, whose exports may still have npm consumers.
489
+ - `topDeadModules(nodes, edges, ctx, limit)` lists files that a forward walk over `imports`
490
+ and `re-exports` edges never reaches, ranked by `loc`. The walk starts from files with the
491
+ role `entry`, `barrel`, `test`, `script`, `config` or `fixture`, and from any
492
+ `main.{ts,tsx,js,jsx}`.
493
+
494
+ These differ from `pnpm dead:check`, which reads edges rather than `utilization` and follows
495
+ re-exports transitively from package-manifest entries.
496
+
497
+ ### Growth and untested risks
498
+
499
+ Ported with TP-1468 from codewatch's `graph report`, unchanged apart from import paths. Both
500
+ take a `ReportContext` and a limit, return `GrowthRiskRow[]` and `UntestedRiskRow[]`, and drop
501
+ files that `keepNode` rejects:
502
+
503
+ - `topGrowthRisks(ctx, limit)` lists files with a structural scaling smell: loop nesting of
504
+ depth 2 or more, `recursive_functions`, or `search_in_loop`. It is a heuristic, not a Big-O
505
+ bound. Ranked by `loop_depth`, then smell count.
506
+ - `topUntestedRisks(ctx, limit)` ranks `hotspot × (1 − coverage_pct / 100)`, where the hotspot
507
+ score is churn × complexity, after Adam Tornhill and CodeScene. Files with no
508
+ `coverage_pct` metric or full coverage are left out, so a repo with no coverage overlay
509
+ gets an empty list.
510
+
393
511
  ### Partition quality
394
512
 
395
513
  `computePartitionQuality` scores a package partition of the file graph: per-package cohesion,
@@ -409,6 +527,18 @@ invertBuckets(fileByPackage); // file id -> package id, skipping the "" unassign
409
527
  re-exports, transitively. It over-attributes: one import of one name through a barrel becomes
410
528
  one edge per re-export target, which is why it is off by default.
411
529
 
530
+ ### Package architecture
531
+
532
+ Ported with TP-917 from codewatch's `graph arch`, unchanged apart from import paths.
533
+ `computeArch` aggregates a snapshot's file edges into package-to-package counts over a list
534
+ of `PackageRoot`s the caller supplies (codewatch detects them from `package.json` files; that
535
+ walk stays in codewatch). It drops test and fixture files, honours `exclude`, `excludeRole`,
536
+ `includeExternal` (one `EXTERNAL_BUCKET` node), and `minEdges`, and with `depth: "modules"` or
537
+ `maxPackageSize` drills packages over the threshold (`DEFAULT_MAX_PACKAGE_SIZE`, 30 files)
538
+ into sub-directory nodes. `bucketFilesByPackage` assigns file ids by longest package prefix,
539
+ with unmatched files under `""`. `filteredFileIds`, `aggregateEdges`, `toSortedEdges`, and
540
+ `packagesReferencedByEdges` are the steps it composes.
541
+
412
542
  ### Pruning snapshots
413
543
 
414
544
  `planPrune` keeps the most recent `keep` snapshots (default 10) plus every snapshot whose ref
@@ -0,0 +1,2 @@
1
+ export { A as ArchEdge, m as ArchPackage, n as ArchResult, o as ArchSubNode, B as BlastRadiusEntry, p as BucketableViolation, q as BusFactorChange, r as BusFactorRow, s as CentralRow, u as ComputeArchInput, v as ComputeDriftInput, w as CouplingClass, x as CouplingDelta, y as CouplingRow, D as DEFAULT_HEALTH_WEIGHTS, z as DEFAULT_MAX_PACKAGE_SIZE, H as DeadModuleRow, J as EXTERNAL_BUCKET, L as GraphReportResult, M as GrowthRiskRow, O as HealthComponent, P as HealthComponentKey, Q as HealthInput, U as HealthWeights, W as HotExport, X as HotspotDelta, Y as HotspotRow, a4 as NewHotspot, f as NodeMetrics, a8 as PackageRoot, a9 as PackageStats, aa as PairCoupling, ac as PartitionQualityInput, ad as PartitionQualityResult, ae as PenaltyWeight, af as ReportContext, ag as ReportContextInput, ah as ReportDrift, ai as SnapshotContext, g as SymbolConsumers, an as SymbolUtil, ao as TestCoverageRow, T as TestSourceLink, ap as UnchangedViolation, aq as UntestedRiskRow, ar as UnusedExportRow, V as ViolationBuckets, at as aggregateEdges, au as bucketFilesByPackage, av as bucketViolations, aw as buildBlastRadius, ax as buildCentralFiles, ay as buildHotExports, az as buildNodeMetrics, aA as buildReportContext, aC as busFactorOf, aD as classifyCoupling, aE as collectNodeMetrics, aF as collectSymbolUtil, aG as computeArch, aH as computeHealth, aI as computePartitionQuality, aJ as computeReportDrift, aK as computeSymbolConsumers, aM as filteredFileIds, b7 as hotspotComplexityOf, aO as hotspotScoreOf, aQ as keepNode, aR as linkTestsToSources, aS as lookupMetric, aT as packagesReferencedByEdges, aU as pairKey, aV as publicApiFiles, aX as referencedNodes, aZ as toSortedEdges, a_ as topBusFactorRisks, a$ as topCentralFiles, b0 as topDeadModules, b1 as topGrowthRisks, b2 as topHotspots, b3 as topTestCoverageRisks, b4 as topUntestedRisks, b5 as topUnusedExports } from '../browser-BBzShneN.js';
2
+ import '../change-coupling-CT3bcZMD.js';
@@ -0,0 +1,84 @@
1
+ import {
2
+ DEFAULT_HEALTH_WEIGHTS,
3
+ DEFAULT_MAX_PACKAGE_SIZE,
4
+ EXTERNAL_BUCKET,
5
+ aggregateEdges,
6
+ bucketFilesByPackage,
7
+ bucketViolations,
8
+ buildBlastRadius,
9
+ buildCentralFiles,
10
+ buildHotExports,
11
+ buildNodeMetrics,
12
+ buildReportContext,
13
+ busFactorOf,
14
+ classifyCoupling,
15
+ collectNodeMetrics,
16
+ collectSymbolUtil,
17
+ computeArch,
18
+ computeHealth,
19
+ computePartitionQuality,
20
+ computeReportDrift,
21
+ computeSymbolConsumers,
22
+ filteredFileIds,
23
+ hotspotComplexityOf,
24
+ hotspotScoreOf,
25
+ keepNode,
26
+ linkTestsToSources,
27
+ lookupMetric,
28
+ packagesReferencedByEdges,
29
+ pairKey,
30
+ publicApiFiles,
31
+ referencedNodes,
32
+ toSortedEdges,
33
+ topBusFactorRisks,
34
+ topCentralFiles,
35
+ topDeadModules,
36
+ topGrowthRisks,
37
+ topHotspots,
38
+ topTestCoverageRisks,
39
+ topUntestedRisks,
40
+ topUnusedExports
41
+ } from "../chunk-L5KU543A.js";
42
+ import "../chunk-HIVVDGWE.js";
43
+ export {
44
+ DEFAULT_HEALTH_WEIGHTS,
45
+ DEFAULT_MAX_PACKAGE_SIZE,
46
+ EXTERNAL_BUCKET,
47
+ aggregateEdges,
48
+ bucketFilesByPackage,
49
+ bucketViolations,
50
+ buildBlastRadius,
51
+ buildCentralFiles,
52
+ buildHotExports,
53
+ buildNodeMetrics,
54
+ buildReportContext,
55
+ busFactorOf,
56
+ classifyCoupling,
57
+ collectNodeMetrics,
58
+ collectSymbolUtil,
59
+ computeArch,
60
+ computeHealth,
61
+ computePartitionQuality,
62
+ computeReportDrift,
63
+ computeSymbolConsumers,
64
+ filteredFileIds,
65
+ hotspotComplexityOf,
66
+ hotspotScoreOf,
67
+ keepNode,
68
+ linkTestsToSources,
69
+ lookupMetric,
70
+ packagesReferencedByEdges,
71
+ pairKey,
72
+ publicApiFiles,
73
+ referencedNodes,
74
+ toSortedEdges,
75
+ topBusFactorRisks,
76
+ topCentralFiles,
77
+ topDeadModules,
78
+ topGrowthRisks,
79
+ topHotspots,
80
+ topTestCoverageRisks,
81
+ topUntestedRisks,
82
+ topUnusedExports
83
+ };
84
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}