Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@ Decisions and their rationale — including what was rejected — live in

### Changed

- **JavaScript and TypeScript now emit dotted member reads as
`FieldAccess`.** The outermost static selector is emitted once: namespace
imports can reach exported definitions, lexical locals remain
`LocalBinding`, and expression/receiver roots retain the existing honest
`NeedsExpressionType` / `NeedsReceiverType` reasons. Call and construction
targets keep their more precise kinds, plain assignment targets are writes
rather than reads, and computed `a[b]` access still emits no invented name.
Type-position members also keep their existing `TypeUse` rows instead of
gaining a duplicate field-access row.

The Fastify denominator share is measured at **14.2% -> 14.3%**
(4,435 / 31,136 -> 5,395 / 37,700); D adds 960 denominator occurrences and
6,564 total occurrences. The card's >=40% figure is a D+J program forecast:
D's complete 6,565-site syntax allocation has a 29.2% arithmetic ceiling
even if every site were incorrectly placed in the denominator. Stream J owns
the separate 21,542-local-binding resolution work that the forecast includes.
This change preserves the honest `LocalBinding` partition instead of gaming
the percentage with false or duplicate rows.
- Java now retries typed applicability only for a legacy
`AmbiguousOverload` whose complete file-local argument vector is known.
Legacy resolved, external, local, and other unresolved rows keep their
Expand Down
48 changes: 24 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ reasons are where that shows. The buckets that need a type environment —
`NeedsExpressionType`, `NeedsReceiverType`, `NeedsTypeInference`,
`AmbiguousOverload`, `UnindexedSupertype`, `DynamicDispatch` — are the majority
of what tier 1 leaves unlinked on nine of the ten real corpora, from 51.9% on
`flask` to 100.0% on both Go corpora. The tenth is `vue-core` at 42.1%, where
`flask` to 100.0% on both Go corpora. The tenth is `vue-core` at 46.6%, where
the largest single bucket is instead the 14,618 names a declared test runner
injects into the global scope.

Expand Down Expand Up @@ -255,23 +255,20 @@ rate without the share invites reading the first number as the second.
| Java | `commons-lang` `598dfc1` | 38,222 | 63,385 | 15,162 | 12,274 | **75.7%** | 39.1% |
| Java | `gson` `3ff35d6` | 13,306 | 16,737 | 6,706 | 5,684 | **70.1%** | 44.8% |
| Java | `probes` `e4dc880` | 13 | 7 | 1 | 1 | **92.9%** † | 63.6% |
| JavaScript | `fastify` `94bcbcc` | 2,795 | 5,159 | 21,542 | 1,640 | **63.0%** | 14.2% |
| JavaScript | `express` `dbac741` | 2,267 | 702 | 3,039 | 5,552 | **29.0%** | 67.6% |
| JavaScript | `probes` `e4dc880` | 6 | 0 | 1 | 2 | **75.0%** † | 88.9% |
| TypeScript | `vue-core` `fa2885d` | 26,297 | 3,694 | 9,564 | 27,945 | **48.5%** | 80.4% |
| TypeScript | `zod` `1fb56a5` | 17,080 | 1,952 | 8,143 | 19,784 | **46.3%** | 78.5% |
| TypeScript | `probes` `e4dc880` | 12 | 0 | 1 | 3 | **80.0%** † | 93.8% |
| JavaScript | `fastify` `00708f3fae0cf8134e718fc8ec35e436d4f481b7` | 2,865 | 5,348 | 26,957 | 2,530 | **53.1%** | 14.3% |
| JavaScript | `express` `00708f3fae0cf8134e718fc8ec35e436d4f481b7` | 2,272 | 748 | 3,642 | 5,910 | **27.8%** | 65.1% |
| JavaScript | `probes` `00708f3fae0cf8134e718fc8ec35e436d4f481b7` | 6 | 0 | 1 | 2 | **75.0%** † | 88.9% |
| TypeScript | `vue-core` `00708f3fae0cf8134e718fc8ec35e436d4f481b7` | 29,763 | 3,817 | 20,551 | 30,557 | **49.3%** | 71.2% |
| TypeScript | `zod` `00708f3fae0cf8134e718fc8ec35e436d4f481b7` | 17,530 | 2,059 | 14,456 | 21,773 | **44.6%** | 70.4% |
| TypeScript | `probes` `00708f3fae0cf8134e718fc8ec35e436d4f481b7` | 12 | 0 | 1 | 3 | **80.0%** † | 93.8% |
| Python | `django` `af67523` | 19,103 | 13,326 | 8,405 | 6,185 | **75.5%** | 53.8% |
| Python | `flask` `22d9247` | 1,185 | 2,317 | 2,146 | 877 | **57.5%** | 31.6% |
| Python | `probes` `e4dc880` | 5 | 0 | 2 | 1 | **83.3%** † | 75.0% |

Ten real corpora and five synthetic probe pins. Since the last published table
four rows are new, three rates moved, one row changed without its rate moving,
and every other cell is byte-identical. A rate moves for more than one reason
and only one of those reasons is "it got better", so each movement below is
attributed per row in [`CHANGELOG.md`](CHANGELOG.md) — by a whole-row join
against a binary built from the previous commit — rather than inferred from
these totals.
Ten real corpora and five synthetic probe pins. The table is a direct rendering
of the committed baselines. Its six ECMA rows include the static member-read
capability; their movement is attributed by a whole-row join in the Stream D
decision record rather than inferred from these totals.

1. **Both Go rates fell, and that is coverage.** The extractor now emits the
two member sites it never had: a selector *read* (`pkg.Name`, `t.field`)
Expand All @@ -297,16 +294,19 @@ these totals.
the `local_binding` column in [`baselines/`](baselines), which is gated for
drift exactly so this cannot be done quietly.

`express` is the row that changed without its rate moving: `XMLHttpRequest` was
`express` previously changed without its rate moving: `XMLHttpRequest` was
missing from the host global list while `WebSocket`, `AbortController` and
`fetch` were all in it, so `external` went 701 → 702 and the rate reads 28.99%
on both sides. The same change reclassified the 1,728 express and 13,833
vue-core references that a declared test runner injects — `it`, `describe`,
`expect` — out of `NoMatchingDefinition`, a bucket whose contract is that it
means *arthron's* bug. They became `Unresolved(UnknownPackage)` rather than
`External` deliberately: `Unresolved` keeps them inside both of the rate's
terms, where `External` would take them out of both and raise the rate without
linking anything. No gated number moved.
`fetch` were all in it, so `external` went 701 → 702 and the rate read 28.99%
on both sides. Its current baseline is 2,272 resolved, 748 external, 3,642
local-binding and 5,910 unresolved: a 27.8% rate after static member reads
entered the denominator. The same earlier change reclassified the 1,728
express and 13,833 vue-core references that a declared test runner injects —
`it`, `describe`, `expect` — out of `NoMatchingDefinition`, a bucket whose
contract is that it means *arthron's* bug. They became
`Unresolved(UnknownPackage)` rather than `External` deliberately:
`Unresolved` keeps them inside both of the rate's terms, where `External` would
take them out of both and raise the rate without linking anything. No gated
number moved in that earlier change.

What the third change costs is *edges*, and the counts are in
[`CHANGELOG.md`](CHANGELOG.md): 5,374 commons-lang and 3,189 gson occurrences
Expand Down Expand Up @@ -395,7 +395,7 @@ in either.

**The rate's denominator is published beside it, every time.** Excluding those
two is correct and it also makes the denominator smaller than the surface the
scan read — on `fastify` the rate covers 14.2% of the references JavaScript
scan read — on `fastify` the rate covers 14.3% of the references JavaScript
emitted. A high rate over a small share is a real measurement and a partial
one, and the only way to read it as the first without the second is for the
second not to be there. So `arthron scan` prints it under every language line,
Expand Down
10 changes: 5 additions & 5 deletions baselines/javascript-express.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
# drift in either fails the gate and must be re-based deliberately.
format = 1
corpus = "corpus/javascript/express"
commit = "dbac741"
commit = "00708f3fae0cf8134e718fc8ec35e436d4f481b7"
language = "javascript"
resolved = 2267
external = 702
local_binding = 3039
unresolved = 5552
resolved = 2272
external = 748
local_binding = 3642
unresolved = 5910
10 changes: 5 additions & 5 deletions baselines/javascript-fastify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
# drift in either fails the gate and must be re-based deliberately.
format = 1
corpus = "corpus/javascript/fastify"
commit = "94bcbcc"
commit = "00708f3fae0cf8134e718fc8ec35e436d4f481b7"
language = "javascript"
resolved = 2795
external = 5159
local_binding = 21542
unresolved = 1640
resolved = 2865
external = 5348
local_binding = 26957
unresolved = 2530
2 changes: 1 addition & 1 deletion baselines/javascript-probes.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# drift in either fails the gate and must be re-based deliberately.
format = 1
corpus = "corpus/javascript/probes"
commit = "e4dc880"
commit = "00708f3fae0cf8134e718fc8ec35e436d4f481b7"
language = "javascript"
resolved = 6
external = 0
Expand Down
2 changes: 1 addition & 1 deletion baselines/typescript-probes.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# drift in either fails the gate and must be re-based deliberately.
format = 1
corpus = "corpus/typescript/probes"
commit = "e4dc880"
commit = "00708f3fae0cf8134e718fc8ec35e436d4f481b7"
language = "typescript"
resolved = 12
external = 0
Expand Down
10 changes: 5 additions & 5 deletions baselines/typescript-vue-core.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
# drift in either fails the gate and must be re-based deliberately.
format = 1
corpus = "corpus/typescript/vue-core"
commit = "fa2885d"
commit = "00708f3fae0cf8134e718fc8ec35e436d4f481b7"
language = "typescript"
resolved = 26297
external = 3694
local_binding = 9564
unresolved = 27945
resolved = 29763
external = 3817
local_binding = 20551
unresolved = 30557
10 changes: 5 additions & 5 deletions baselines/typescript-zod.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
# drift in either fails the gate and must be re-based deliberately.
format = 1
corpus = "corpus/typescript/zod"
commit = "1fb56a5"
commit = "00708f3fae0cf8134e718fc8ec35e436d4f481b7"
language = "typescript"
resolved = 17080
external = 1952
local_binding = 8143
unresolved = 19784
resolved = 17530
external = 2059
local_binding = 14456
unresolved = 21773
127 changes: 127 additions & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,133 @@ store language semantics.* The resolver remains the only authority; the store
owns persistence and tallying of the verdict.

---
## 2026-07-29 — static ECMA member reads enter the graph once

The JavaScript and TypeScript extractors emitted calls, imports, type uses and
heritage, but omitted a dotted property when the property was read as a value.
That was an invisible denominator, not an honest miss: `imported.member` could
name an exported repository definition and there was no row to resolve.

Measured with the pinned ast-grep 0.44.1 parser and the product's own file
walker over all four ECMA corpora, the syntax cut is **33,731** outermost
non-call/non-construction member reads after excluding plain assignment
targets:

| corpus | import binding | `this` / `super` | name root | complex expression | total |
|---|---:|---:|---:|---:|---:|
| fastify | 171 | 221 | 5,631 | 542 | 6,565 |
| express | 0 | 253 | 716 | 43 | 1,012 |
| vue-core | 3,045 | 741 | 11,918 | 1,498 | 17,202 |
| zod | 570 | 212 | 6,766 | 1,404 | 8,952 |
| **total** | **3,786** | **1,427** | **25,031** | **3,487** | **33,731** |

`name root` is syntactic, not a claim that every such name is local:
23,359 are bound by a lexical environment that is not a node, 544 are module
bindings that are nodes, and 1,128 are unbound or global names. Keeping those
classes separate is what prevents the large local term from hiding module
edges.

The syntax count is not the new-row count. **106 TypeScript sites already have
the more precise `TypeUse` kind** (13 in vue-core, 93 in zod). Emitting them
again as `FieldAccess` produced a second, false `NeedsExpressionType` beside a
resolved import-type row in the fixture suite. They are excluded, not lost.
The resulting extraction delta is **33,625 `FieldAccess` occurrences**:
6,565 fastify, 1,012 express, 17,189 vue-core and 8,859 zod. Store
deduplication means these occurrence counts are not predictions of committed
row movement; the corpus join and re-base own that measurement.

The Fastify whole-row join measures **2,865 resolved, 5,348 external, 26,957
local-binding and 2,530 unresolved** occurrences: a rate denominator of
**5,395 of 37,700 references (14.3%)**, from **4,435 of 31,136 (14.2%)**.
The member-read change adds 960 denominator occurrences but 6,564 total
occurrences, and its rate is consequently 63.0% -> 53.1% (2,795 / 4,435 ->
2,865 / 5,395). This is an accounting result, not a reason to relabel local
names: the shape audit finds 1,889 appeared lexical name-root rows marked
`locally_bound`, which policy correctly keeps outside both terms.

The card's Fastify >=40% denominator-share expectation is a repository
program forecast, not a Stream D-only acceptance. D owns only 6,565 authorized
Fastify syntax occurrences; even the invalidly optimistic allocation that put
every one in `Resolved + Unresolved` could reach only
**(4,435 + 6,565) / (31,136 + 6,565) = 11,000 / 37,701 = 29.2%**. The later
Stream J owns resolving the existing 21,542 local bindings that the forecast
also needs. The measured D result is 14.3%; it must not be inflated by
reclassifying locals, emitting computed or write-only names, or duplicating
nested, call, construction, or type-position rows.

The final pre-rebase join uses the complete reference-row key
(`file + kind + declaration space + enclosing FQN + raw target + argc +
locally_bound`) against fresh release builds of `1ea3c87` and `00708f3`.
Every pre-existing row holds byte-for-byte in all six ECMA corpora: zero
vanished rows, zero changed outcomes or occurrence counts, zero duplicate
keys. Every appeared row is the new `FieldAccess` kind. Rows and occurrences
are separate units:

| corpus | appeared rows / occurrences | in-repo resolved rows / occurrences | new `External` rows / occurrences | `LocalBinding` rows / occurrences | other unresolved rows / occurrences |
|---|---:|---:|---:|---:|---:|
| fastify | 2,452 / 6,564 | 41 / 70 | 85 / 189 | 1,906 / 5,415 | 420 / 890 |
| express | 489 / 1,012 | 5 / 5 | 38 / 46 | 306 / 603 | 140 / 358 |
| vue-core | 8,589 / 17,188 | 1,883 / 3,466 | 81 / 123 | 5,186 / 10,987 | 1,439 / 2,612 |
| zod | 5,173 / 8,859 | 346 / 450 | 50 / 107 | 3,218 / 6,313 | 1,559 / 1,989 |
| JavaScript probes | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 |
| TypeScript probes | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 |

The unresolved side introduces no taxonomy. Its full per-reason attribution,
again as rows / occurrences, is:

| corpus | per-reason appeared rows / occurrences |
|---|---|
| fastify | `NoMatchingDefinition` 32 / 100; `NeedsTypeInference` 38 / 60; `LocalBinding` 1,906 / 5,415; `NeedsReceiverType` 66 / 189; `NeedsExpressionType` 284 / 541 |
| express | `UnknownPackage` 1 / 1; `NeedsTypeInference` 56 / 61; `LocalBinding` 306 / 603; `NeedsReceiverType` 42 / 253; `NeedsExpressionType` 41 / 43 |
| vue-core | `NoMatchingDefinition` 101 / 134; `NeedsTypeInference` 250 / 619; `LocalBinding` 5,186 / 10,987; `NeedsReceiverType` 153 / 359; `NeedsExpressionType` 930 / 1,495; `UnindexedSupertype` 5 / 5 |
| zod | `NoMatchingDefinition` 137 / 179; `NeedsTypeInference` 289 / 392; `LocalBinding` 3,218 / 6,313; `NeedsReceiverType` 6 / 12; `NeedsExpressionType` 1,125 / 1,404; `UnindexedSupertype` 2 / 2 |
| JavaScript / TypeScript probes | none |

The six baselines were written only by `arthron gate --rebase --commit
00708f3fae0cf8134e718fc8ec35e436d4f481b7`:

| corpus | before `(resolved, external, local, unresolved)` | after `(resolved, external, local, unresolved)` |
|---|---|---|
| fastify | 2,795 / 5,159 / 21,542 / 1,640 | 2,865 / 5,348 / 26,957 / 2,530 |
| express | 2,267 / 702 / 3,039 / 5,552 | 2,272 / 748 / 3,642 / 5,910 |
| JavaScript probes | 6 / 0 / 1 / 2 | 6 / 0 / 1 / 2 |
| vue-core | 26,297 / 3,694 / 9,564 / 27,945 | 29,763 / 3,817 / 20,551 / 30,557 |
| zod | 17,080 / 1,952 / 8,143 / 19,784 | 17,530 / 2,059 / 14,456 / 21,773 |
| TypeScript probes | 12 / 0 / 1 / 3 | 12 / 0 / 1 / 3 |

Target pins were written only by `arthron pin --write` from the same binary.
Old pins held with zero vanished and zero moved targets: fastify
1,045 held / 41 appeared, express 496 / 5, vue-core 10,573 / 1,883,
zod 10,289 / 346, JavaScript probes 6 / 0, and TypeScript probes 12 / 0.
A fresh comparison against the generated pins then held every row with zero
appeared, vanished, or moved.

**Decided: emit the outermost static `member_expression` once as
`FieldAccess`.** It reuses the same target roots, binding verdict and resolver
paths as call position: an imported namespace member can resolve, a lexical
name remains `LocalBinding`, `this` / `super` retain receiver lookup, and an
expression root remains `NeedsExpressionType`. No unresolved reason changes.

**A plain assignment target (`a.b = x`) is a write without a read and is not
emitted by this read rule.** An augmented assignment (`a.b += x`) reads the
old value and is emitted. A member used as the callee of a call or constructor
keeps its `Call` / `New` row and is not duplicated as a field access.

*Rejected: emitting nested selectors separately.* `a.b.c` is one read target;
an extra `a.b` row double-counts syntax that exists only as the outer
expression's operand.

*Rejected: emitting computed `a[b]` access.* The key is a runtime value. Using
its source text as a member name would invent a target.

*Rejected: emitting type-position members again.* Their existing `TypeUse` row
is both more precise and already in the measurement.

*Rejected: changing outcomes to satisfy the D-only percentage.* `LocalBinding`
means a lexical name is not a node, and the extractor's shape tests separately
guard its partition from imported, expression, receiver, computed, write-only
and call/new sites. A percentage is not authority to create a wrong edge or a
second reference.

## 2026-07-29 — the walk keeps a file's declarations and forgets its references

Expand Down
Loading
Loading