1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
//! `architecture-metrics` analysis — repo-level structural-health
//! numbers over the resolved import graph, the kind you trend over time.
//!
//! - **`propagation_cost`** (`MacCormack`, Rusnak & Baldwin 2006) — the
//! density of the visibility (transitive-closure) matrix: "a change to
//! a random file can, on average, reach this fraction of the system".
//! - **`acd`** — Lakos's Average Component Dependency: the mean number of
//! files each file depends on directly *or transitively* (incl. self).
//! - **`nccd`** — Normalised Cumulative Component Dependency: `CCD`
//! divided by the `CCD` of a balanced binary tree of the same size.
//! `< 1` ≈ horizontal/flat, `> 1` ≈ vertical/layered, `> 2` ≈ likely
//! cyclic (Lakos 1996, *Large-Scale C++ Software Design*).
//! - **`dependency_cycles`** / **`largest_cycle`** — count of non-trivial
//! SCCs and the size of the biggest tangle.
//! - **`architecture_type`** — `hierarchical` (acyclic), `core-periphery`
//! (one dominant cyclic group), or `multi-core` (several comparable
//! ones) — Baldwin, `MacCormack` & Rusnak 2014.
//!
//! All derived in one pass from the shared import-graph kernel (SCC +
//! reachability), so this adds no new query cost beyond building the
//! graph. Accuracy follows the import resolver's language coverage.
//!
//! Four further rows disclose how much of the import surface the graph
//! above actually covers — *coverage* transparency, not defect scores, so
//! that a sparse graph can't read as a clean one. The structural metrics
//! only ever see the resolved edges (`target_path IS NOT NULL`); these
//! query the full `imports` table so a poor resolution rate is visible:
//!
//! - **`import_resolution_rate`** — fraction of all import statements whose
//! target resolved to an in-repo file. External and standard-library
//! imports (`numpy`, `java.util`, `std::fmt`, …) legitimately point
//! outside the repo and count as unresolved, so a repo with many
//! third-party dependencies naturally scores lower — this is expected,
//! not a resolver bug.
//! - **`first_party_import_share`** — fraction of import statements that are
//! first-party by intent: either already resolved, or a *relative* import
//! (`use crate::…`, `from .mod import …`, `./foo`) naming an in-repo path
//! even when the resolver missed it — including a first-party *glob*
//! (`use crate::foo::*;`, bare `use super::*;`), which `imports.kind`
//! tags `wildcard` rather than `relative` (see the "Definition caveat"
//! below) but which is still unambiguously in-repo by its
//! `crate::`/`self::`/`super::` prefix.
//! - **`resolution_rate_first_party`** — of those first-party imports, the
//! fraction that resolved. It drops external imports from the denominator,
//! isolating resolver coverage from third-party-dependency density: a low
//! value points at a genuine resolver gap rather than many external deps.
//! - **`wildcard_import_share`** — fraction of all import statements that
//! are glob imports (`imports.kind = 'wildcard'`), first-party or not.
//! Purely informational (a glob names a module, not a symbol).
//!
//! When an active calibration artifact ([`crate::calibration::load_active_artifact`])
//! carries a `repo_metrics` section (corpus pools populated by `codelore
//! calibrate`), additional rows report where this repo's `propagation_cost`
//! and `cycle_file_share` sit against that corpus, each paired with a Wilson
//! 95% confidence interval (`…:ci_low` / `…:ci_high`) that reflects the finite
//! corpus pool's sampling uncertainty — see [`run_architecture_metrics`].
//! Absent artifact or absent section ⇒ those rows are simply not emitted (the
//! additivity contract this module's tests pin).
use crate;
use crate;
use crateFactsDb;
use crate::;
/// One repo-level architecture metric: `(metric, value)`. The value is a
/// string so the numeric metrics and the textual `architecture_type`
/// label can share one row shape; numeric values are written as bare,
/// parseable numbers (e.g. `0.0607`) so downstream tooling can read them.
/// Share of a cyclic node set the largest cycle must cover to call the
/// architecture "core-periphery" rather than "multi-core".
const CORE_DOMINANCE: f64 = 0.6;
/// Run the `architecture-metrics` analysis. Returns one row per
/// repo-level metric, in a fixed presentation order, plus the
/// import-resolution disclosure rows (see [`import_resolution_rows`]).
/// Returns just those disclosure rows when the repo has import statements
/// but none resolve into the graph (`n == 0`); empty only when the repo
/// has no imports at all.
///
/// # Errors
///
/// Returns [`crate::CodeLoreError::Analysis`] on `DuckDB` query errors
/// (propagated from the import-graph build).
/// The import-resolution disclosure rows, over the already-ingested
/// `imports` fact (query-time, no graph rebuild). Empty when the repo has no
/// imports at all; otherwise up to three `{:.4}`-fraction rows that split one
/// coarse number into a resolver-strength signal and a repo-composition one,
/// so a low headline rate can't be misread as a weak resolver:
///
/// - **`import_resolution_rate`** — resolved ÷ *all* imports. Unchanged
/// semantics (additive-only contract). External crates / stdlib / npm
/// imports legitimately point outside the repo and stay unresolved, so a
/// repo with many third-party deps naturally reads low: this is *coverage
/// of the whole import surface*, NOT a defect score.
/// - **`first_party_import_share`** — first-party candidates ÷ all imports.
/// A *first-party candidate* is an import that could target in-repo code:
/// it either resolved to a tracked file, OR is syntactically repo-relative
/// (`imports.kind = 'relative'` — Rust `crate::`/`self::`/`super::`, Python
/// leading-dot, JS/TS `./`|`../`), the exact imports each per-language
/// resolver *attempts* to resolve in-repo (see `imports::resolver`). An
/// unresolved *absolute* import is presumed external. This is how much of
/// the surface even aims at the repo.
/// - **`resolution_rate_first_party`** — resolved ÷ first-party candidates:
/// the resolver's strength on the imports that actually point in-repo,
/// isolated from the third-party mix that drags the headline rate down.
/// Omitted when there are no first-party candidates (the rate is undefined,
/// and a `0.00` would misread as "resolved none of them").
/// - **`wildcard_import_share`** — glob imports (`kind = 'wildcard'`: Rust
/// `use foo::*`, Java `import foo.*;`) ÷ all
/// imports. Purely informational — a glob names a module, not a symbol, so
/// it is inherently harder for the resolver to check against an in-repo
/// path than a named import.
///
/// Definition caveat (documented, not hidden): an absolute import that fails
/// to resolve is counted as external, so a genuinely first-party absolute
/// import the resolver *missed* is under-counted; and for a language with no
/// syntactic relative marker (Java), first-party candidates reduce to the
/// resolved imports, making `resolution_rate_first_party` optimistic there.
/// A second, narrower instance of the same undercount: `classify` tests for a
/// trailing glob (`ends_with('*')`) BEFORE it tests for a relative root, so a
/// first-party glob (`use crate::foo::*;`, bare `use super::*;`) is tagged
/// `Wildcard`, not `Relative` — `imports.kind` keeps that distinction (Java's
/// `import foo.*;` has no relative form to fall back to, so blending the two
/// kinds would erase real signal there). `first_party_import_share` instead
/// widens its first-party predicate to recognise a `Wildcard` row whose raw
/// `target` carries the same `crate::`/`self::`/`super::` prefix `classify`
/// itself uses for Rust relative imports, counting it as first-party without
/// touching the stored `kind`.
/// The `n` for a repo-level metric's Wilson interval: the count of corpus repos
/// contributing to this pool. Saturates on the impossible >4-billion case
/// rather than wrapping.