mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
main
187 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3f714bcc48 |
fix(jsx): a .jsx component links to the file-scope values it reads, as in .js and .tsx (#2561)
Value-reference edges (a reader symbol -> the file-scope const/var it reads, which impact follows) ran for typescript, tsx and javascript but not jsx. A .jsx file is its own language, parsed with the JavaScript grammar, so the same component had no edge to the lazy page it renders (`<LoginPage />` under `const LoginPage = Loadable(lazy(...))`), the route table or the config it maps over as .jsx, and all of them as .js or .tsx. Every other JS-family gate in the extractor lists jsx, and nothing in the history says this one was left out on purpose: the kernel mirrored the gap with an explicit "NOT jsx" check for parity. Add jsx to VALUE_REF_LANGS and drop the kernel's jsx exclusion. On mantis, matx, berry, mastodon and bigbluebutton-html5 the index only gains edges (35, 56, 15, 28 and 266): no node, ref or file changes and no edge removed. Every added edge is a same-file read of a file-scope value that is the only binding of its name in the file. A repo with no .jsx files indexes byte-identically, and kernel-parity.mjs shows 0 diffs over those repos with the same deferrals as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d0bb9be51e |
fix(react): a component a file declares itself as a value is what its JSX renders (#2420)
A PascalCase name that a .tsx/.jsx file declares as a module-level value is
that declaration wherever the file uses it: a JSX tag, a route's element, a
layout or a TSX type. Its initializer decides what it renders, in order:
- the component of the module its lazy loader loads, or the export the
loader picks, in either branch of a conditional;
- the function it wraps (`observer(function X() {…})`,
`forwardRef(Inner) as {…}`);
- else the declaration itself.
Before, the lookup by kind skipped the value and took a lone same-named
component anywhere, often in another app of the repository.
- Pattern 1 asks declaredComponent about every PascalCase reference, not
only route references. layoutComponent and the jsx-render synthesizer's
jsxChild ask it too.
- valueLoader reads a value's loader with loaderValue (#2480).
- defaultExportedName reads `export default observer(Login)`.
- lazyValueModules records the modules a file's lazy values load as
synthesis inputs, so a sync that changes one of them redraws the tag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
9b237093a3 |
fix(sync): a lazy route relinks a page that appears below its barrel (#2560)
A lazy React Router route whose module is a barrel, and whose page sits in a folder below the barrel's, stayed unlinked after `codegraph sync` added the page or gave it its export, though a fresh index linked it. In DigiShop.Frontend, `@/pages` is `src/pages/index.ts`, which imports `./Home`, `src/pages/Home/index.tsx`, and exports it again. While the page is missing, the route's reference fails and waits under the key of the module it names, `module:pages` (#2422). The retry looks failed references up by the keys of the changed files, and a file in `pages/Home/` gives only `module:Home` and its own stem. The limit, which the `export *` barrels of #2436 share, was recorded with #2558. After the failed-ref retry, which makes the barrel's import edge into an added file, sync now walks up the import edges from each file it added, or gave a definition, to the barrels that pass it on: `export … from`, or an import exported again (`import { X } from './x'; export { X }`, `export default X`), as deep as the resolver chases a name. It re-opens, for the sweep, the failed references waiting under those barrels' keys. Only keys a failed reference waits under are looked up, in one DISTINCT over the failed rows parked under module keys, so a sync with none reads nothing, and a body-only edit seeds nothing. The ImportReader that the added-files reopen (#2512) uses is shared, and gains exportedBindings, behind a regex pre-check, since most importers export nothing. defaultExportBinding moves to alias-binding.ts for this walk and for react.ts's exportsBinding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e30a2c164d |
fix(react-router): a lazy route follows a barrel that re-exports an import (#2558)
A lazily loaded folder whose index passes on a page it imports linked
nothing: DigiShop.Frontend's `src/layout/index.ts` does `import { UserLayout }
from './UserLayout'; export { UserLayout }`, and its routes load
`await import('@/layout')`. exportedComponent, the barrel walk behind lazy
routes, React.lazy values and lazy layouts (#2436, #2452, #2480), read the
export clause as naming a declaration in the barrel, which has none: the
name is an import binding. `import Login from './Login'; export default
Login` and `export { Page as Component }` of an imported Page failed the
same way, so all 27 lazy route refs in DigiShop's App.tsx stayed failed.
A binding the module exports but does not declare is now followed through
the module's import of it, a default import as `default` and a named one by
the name it imports, with each file read added to `reads` so lazyModules
still redraws the route. A namespace import or a package's binding links
nothing. `export default withAuth(Page)` hands over a call, whose first
identifier ownExport reads; that was harmless while only same-file
declarations counted, so a default is followed only when the statement is
the binding itself (exportsBinding), or the route would link the HOC.
#2451 (open) adds the same step to the general import walk; #2420 (open)
carried it in a lazyRouteComponent rewrite that exportedComponent superseded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
a9781b0e05 |
fix(react-router): a lazy route links the page its loader picks (#2480)
A data-router `lazy` loader resolves to the route's properties, and many
pick their page from the module they load: luci-go milo/ui's
`lazy: async () => { const { TestTab } = await import('…/tabs'); return
{ Component: TestTab }; }`, `const { default: Component } = await
import('./pages/Login')`, `import('./x').then((m) => ({ Component: m.Page }))`,
`({ Component: (await import('./x')).Page })`, React Router 7's
`lazy: { Component: async () => (await import('./x')).Page }`. scanRoutes
recorded only the first `import('…')` as `lazy-import:<spec>`, and
resolution linked the module's default export, else its `Component`: a
different page, or nothing (milo/ui's tabs barrel has neither).
lazyRouteReference reads the loader: its `Component`, or the page its
`element` shows, followed through the loader's bindings (destructured
`await import`, a module binding, `.then` callbacks, `Promise.all`, the
v7.5 object form), and the one lazily imported export a guard shows
(`<AgeGate><FireworksPage /></AgeGate>`). A loader that returns the module
keeps `lazy-import:<spec>`; one this does not read (a helper's, `.then(convert)`)
keeps its first import, as before; one that hands over only a `loader`
renders nothing. `async lazy() { … }` methods, which scanRoutes skipped as
entries, are read too, and comments no longer hide an import's specifier.
The picked export is named the way Vue Router and Angular name a lazy
component, `import:<path>#<export>`, so sync's module-tail retry (#2422)
parks and retries it unchanged, and it resolves through #2436's
exportedComponent, barrels included. React is registered before Vue Router
and Angular, so it answers only its own routes' references (tsx or jsx,
which theirs never are). lazyModules (#2452) names the files a picked
export is read from, so a sync that moves it redraws the route.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
b994365b12 |
fix(sync): a route rendering a same-file lazy value follows its module, and a new component gets its JSX edges (#2452)
A route that renders `const Docs = lazy(() => import('./pages/Docs'))` binds
to the component that module exports, and to the declaration while the module
is missing or exports none (#2400's declaredComponent). The reference had
resolved, so no failed-ref retry saw it, and CG-33's rebind reached it only
when the module's component shared the declaration's name: a sync that added
the module, gave it its default export or moved that export left the route
on its old answer. A data router's `lazy` route kept a moved default export
the same way.
FrameworkResolver.lazyModules names the modules a route's answer reads,
including the files a barrel forwards it through (#2436); React implements it
for lazy-import references and the same-file lazy values Pattern 1 reads.
After the CG-33 rebind, sync puts back the route references whose modules it
added or changed (reopenRouteModuleReaders), in the order their edges were
written, for the orphan sweep.
The jsx-render refresh read only each changed file's own content, so a new
component without JSX of its own never got the edge from `<Team />` in an
unchanged file. A sync whose definition delta holds a JSX-renderable node now
refreshes synthesis.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
7a79654157 |
fix(cpp): a call written from the global scope links the declaration of exactly that name (#2445)
The resolver's name-existence pre-filter read no leading `::`: its `::` branch wanted the separator past the first character, and the single-`:` loop skips names holding `::`. So `::leveldb::RepairDB(…)`, `::ROCKSDB_NAMESPACE::RepairDB(…)` or protobuf's `::_pbi::…` calls were dropped before any strategy ran, unless a detected framework claimed them. protobuf's got through only because it ships three Swift files: the Swift ↔ Objective-C bridge claims any name with a `:` in it, which is why #2415 left claimsReference() universal. - The pre-filter checks a C or C++ name without its leading `::`. - claimsReference() is asked only of the frameworks that resolve the reference's language (frameworksFor), like resolve() since #2415. - matchByQualifiedName takes a global-qualified C/C++ name as exactly that qualified name. Its suffix match took a namesake nested in another namespace or class: with the pre-filter alone, 23 of the 24 edges it added on leveldb, fmt and rocksdb were wrong, mostly wrappers like fmt's mock `test::open` linked to the `::open(…)` they wrap, and protobuf's `::operator delete` went to `DynamicMessage::operator delete`. A declaration inside a namespace a macro opens (fmt's `struct pipe`) only looks global in the index, so it is skipped; step 5 still reads those. - `::std::…` and `::memset` count as `std::…` and `memset` for the built-in check, and step 5 returns early for a single-segment name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
235387ec42 |
fix(c,cpp): a struct, class, union or enum defined in a variable declaration is indexed (#2446)
`struct Foo { … } foo;`, `class Bar { … } bar;` and `static struct { … } SPT;`
outside any function or class body went to the variable extractor, which
skipped the declaration's children, so the type, its methods and its
enumerators were never indexed in either extraction engine.
Both engines now walk such a type before the variables: in C always, and in
C++ when the tree has no errors or the file is walked in brace scopes. This
converges with #2426's walk for erroring C++ files; a C++ file whose braces
don't balance is still left alone, since error recovery can run a class past
its own `}` there. The declaration's function-as-value scan skips the walked
type, which captured its own. An unnamed struct, union or enum takes the name
of the first variable its declaration declares (also inside function bodies,
where it was `<anonymous>`), as `typedef struct { … } Name;` already takes the
typedef name, and the comment above the declaration is the type's docstring.
cDeclaratorIdentifier moves to languages/c-cpp.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
16344af8ff |
fix(react-router): a lazy route links the page a barrel module forwards (#2436)
A data router's `lazy: () => import('@/pages/invocation_page')` loads the
folder's `index.ts`. When that file only forwards the page, with
`export * from './invocation_page'`, `export { default } from './page'` or
`export { Page as Component } from './page'`, the route linked nothing: the
module was read for an export of its own and none was found. The lookup now
follows the module's re-exports a few hops to the file that declares the
page, the way JavaScript resolves them (`export *` never forwards the
default, a name two `export *` modules both forward is exported by
neither), and reads a module's own `export { Page as Component }` clause.
`React.lazy` values in a route file, which go through the same lookup,
follow such barrels too.
On luci-go's milo/ui, 51 of 91 failed lazy route refs now link; the rest
load a file under `src/build/` (not indexed), export only a `loader` or a
`handle`, or pick a named export in an async loader. Sync links a page
added or edited behind its barrel the way a fresh index does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
d348d301c1 |
fix(sync): a navigation call links its route when the route appears later (#2425)
A router's navigation call (`history.push('/login')`, `navigate('/login')`,
`router.push('/x')`, `goto('/x')`) names its route by path and its reference
by the router's method, so sync never noticed a route that changed in another
file. A synced index then kept answers a fresh index of the same files does
not have (CG-33):
- A call parked as failed while its route was missing never matched the #1240
retry, which keys on the names the synced files define.
- A call bound to a catch-all, a parameter route or the other arm of a
conditional kept that binding.
- A route renamed in place by runPostExtract kept the calls bound to it.
- Template links from a routes file that matches no synthesis trigger were
never redrawn.
When a navigation router is detected, sync now reads the route nodes at its
first file change and again after runPostExtract. For each route on one side
only, it puts back the calls the routers' new `navigation` hook (method tails
and app scope) says that route can answer: the failed ones, and the
`navigates` edges a resolver made. The orchestrator's orphan sweep resolves
them, under the same 500-per-name ceiling, and the sync refreshes synthesis.
This covers re-extracted and removed files, postExtract renames and
cross-file table routes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
9d07864e65 |
fix(cpp): a class keeps its namespaces and enclosing class past code the parser misreads (#2426)
tree-sitter recovers from a construct it can't parse by inserting the token it expected, and inside a class body that is often a `}`: an unknown macro in front of a member (`PROTOBUF_FUTURE_ADD_EARLY_NODISCARD absl::string_view name() const`) or in a class head (`struct ALIGN_AS(64U) HandleImpl`) closed the class there, and every `}` after it closed the scope one level out. Recovery also ran the other way, keeping a scope open past its own `}` once an ERROR swallowed it. The walker scoped declarations by tree nesting, so qualified names lost their namespace and class segments, or gained ones they don't belong to: protobuf's `FieldDescriptor`, `MapFieldBase` and `MutableRepeatedFieldProxyImpl`, fmt's `detail::` buffers (and `fmt::format_int` put in `detail`), rocksdb's clock_cache classes and the `Opts` structs nested in them, gtest's `PrettyUnitTestResultPrinter`. Every such file parses with errors, and the kernel defers those to wasm, so the wasm walker produced all of them; the kernel's walker has the same tree-nested stack and reaches erroring trees only under the CODEGRAPH_KERNEL_CCPP_ERROR_EXTRACT sweep hatch. For a C++ file whose tree has errors, the walker now takes each declaration's namespaces, and at declaration level its enclosing classes, from the source's braces (languages/cpp-brace-scopes.ts: comments, literals, raw strings, digit separators and preprocessor lines skipped, each #if branch read from the braces open at the #if). A class-like node ends at its body's `}`, and a class the tree glued into a namespace-level declaration's type is walked as a class. A file whose braces don't balance, and every file that parses cleanly, keeps the tree's scopes. Ported to the kernel (ccpp/brace_scopes.rs) so the hatch stays parity-true: the same erroring files diverge before and after (UTF-8/UTF-16 recovery), every other one is byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
19f91e2791 |
fix(frameworks): a framework resolves only references written in its own languages (#2415)
Strategy 1 of resolveOneInner ran every detected framework's resolve() on every reference, whatever its language: `languages` gated only extraction, and gateFrameworkLanguage lets every `calls` result through. Express, detected in etcd by its file-content scan, linked Go `*zap.Logger` result types to a method named `Logger`, and in harbor and bitwarden it linked Python and Rust `validate()` / `authenticate()` calls to an unrelated method or to the calling function itself. getResolvingFrameworks() narrows the loop to the frameworks whose `resolveLanguages` (else `languages`) list the reference's language; one declaring neither stays universal. `resolveLanguages` lets a resolver read a language it extracts nothing from without running extract() there: Svelte reads TS/JS ($lib imports, runes in .svelte.ts), ASP.NET reads Razor (a page's `@model` is the PageModel beside it). claimsReference() stays universal: protobuf's leading-`::` C++ calls get past the name pre-filter only on the Swift/ObjC bridge's claim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c54b72692c |
fix(react-router): a JSX index route is the page at its parent's address, inside the layout around it (#2423)
Route objects already read `{ index: true, element }` as the page at the
parent's address and a route object around others as their layout. JSX
routes did neither: `<Route path="/" element={<Layout/>}><Route index
element={<Home/>}/>…</Route>` bound `/` to Layout, the index child was never
read, and Layout's links counted only from `/`.
The JSX branch of scanRoutes now:
- reads `<Route index>` / `index={true}` (and a `path=""` leaf with an
element) as the page at its parent's address, and lets it claim that
address from the layout or the path-only `<Route path>` around it;
- treats a `<Route element>` with `<Route>`s inside as their layout
(`layout:` references), a pathless or `path=""` one included, and threads
those layouts into a table mapped inside it when the table is written in
the same file;
- reads an index route only where its parent's address is written down: an
enclosing route with a readable path, or the router itself
(`createRoutesFromElements(…)`, `<BrowserRouter>`). At the top of a
component's own `<Routes>` the address is wherever another file mounts the
component, and under `path={paths.x}` it is not spelled out;
- reads `path={"agents"}` as a path.
Reading what an element renders: tags inside an attribute are no longer the
page unless the attribute hands one over (`component`, `element`, `page`),
so `<Suspense fallback={<Loader/>}>` no longer binds the spinner; the line
break Prettier writes after `element={` no longer hides the element; a guard
that wraps only `<Outlet/>` is the guard; and `element={<Outlet/>}` renders
nothing of its own.
Validated against the merged #2400 build: 16 control repos byte-identical,
578 of 581 new layout edges name a component of an enclosing <Route> (3
inherit a pre-existing default-import resolution bug), 27 of 27 new
navigations precise, no navigation lost.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
e0bbb6620f |
fix(dead-code): a type that declare module or declare global merges outward is not dead (#2418)
The viewer's Dead code screen listed TypeScript interfaces that augment a
type declared outside the project: chart.js's `PluginOptionsByType` in
`declare module 'chart.js' { … }` (ghostfolio), `interface Window` in
`declare global { … }` (angular-realworld). The library or the runtime
reads them through the type they merge into, so nothing in the repository
names them, and the index recorded them as unexported file-level
interfaces.
TypeScript exports what such a body declares without the keyword. The
binder makes an ambient module body an export context unless it holds an
export declaration (`export {}`, `export =`, `export default x`), and a
namespace nested in one is ambient too. Both extractors now set isExported
that way: isAmbientExport in languages/typescript.ts, shared by TSX and
ArkTS, and is_ambient_export in the kernel's tsjs walker. The exported rule
then drops them, counted under `exported`, and that count's label now
names them. `declare namespace X` is unchanged: in a module file X belongs
to that file.
The resolver sees them as exported too. A file holding `declare global`
was already treated as exporting, so nothing moves there. A `declare
module` member becomes reachable across files by name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
dd7064698c |
fix(react-router): read route tables another file hands the router (#2400)
A route table kept in a file of its own produced no routes: the ASP.NET
Core React template's AppRoutes array, which App.js maps into
<Route {...rest}>, a routes array imported into useRoutes(routes) or
createBrowserRouter(routes), and codedthemes' one-object-per-file
[MainRoutes, LoginRoutes]. Route objects were read only from a file that
itself names a data router, so these apps had no routes and their
navigate()/<Link to> navigation reached nothing.
The table's own file cannot tell it from a menu's { path, element } list,
so the evidence is the file that hands it over: a useRoutes /
create*Router argument, or a .map whose callback renders a <Route> from
the item's own fields ({...route}, {...rest}, path={route.path}). A new
FrameworkResolver.crossFileNodes hook keeps those routes, reconciled in
runPostExtract on every index and sync (removal-only syncs included):
new ones are inserted with pending references, ones no longer wanted are
removed with their incoming resolution edges resurrected, unchanged ones
keep their edges. The React pass starts from the files that import React
Router, follows the import to the table and the tables it names
(...ApiAuthorizationRoutes, children: x), each under its mount, and owns
its routes by id (route:<file>:<line>:table:<path>).
Route objects (tables and data routers alike): an index: true route is
the page at its parent's address; a route object around others is their
layout (a references edge marked layout: true, as Angular's), and is not
a page of its own where a child claims its address. A route renders past
a *Wrapper or *Route guard, and through a same-file
lazy(() => import(...)) to the module it loads, never a same-named
component in another app. useRoutes joins the file-level gate.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
9a515c3e64 |
fix(dead-code): a method a framework calls by decorator or by name is not dead (#2405)
The viewer's Dead code screen listed two kinds of framework-called methods. A method whose decorator resolves outside the project (`@HostListener`, `@Cron`, `@OnEvent`, `@Process`, `@BeforeInsert`, `@InitBinder`) has no `decorates` edge and no `node.decorators`, only the resolver's unresolved `decorates` row. That row now counts as a decorator, the same evidence the opaque-ancestor rule reads for an `extends`/`implements` row, under the existing `decorated` count. A lifecycle hook on a class that skips the `implements` clause (`ngOnInit` on a `@Component` without `implements OnInit`) had no evidence at all. A new `hooks` rule drops a TypeScript method when its class carries a decorator and an interface outside the index carries the name. What the interface carries is read off the classes that write it out: every one has the method, declared or inherited, at least two and more than half declare it, and more than half of them carry a decorator. TypeScript only, because `implements` is erased there and a framework can only be finding the hook by name. The ancestor walk moves out of overrideCandidates into ancestorsOf, shared by both rules. Declarations are read from the candidate list rather than searched by name: a hook is never called from inside the index, and a name some declaration of which is called is the ambiguous-name rule's anyway. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9a274a4b47 |
fix(angular): a binding, interpolation or control-flow block links the member it calls (#2398)
The Angular template pass read own-member calls only in event bindings, so
a member a template calls from `[name]="icon()"`, `{{ label() }}`,
`*ngIf="isOpen()"` or `@if (loading())` had no incoming edge. With signals
that is the usual way a template reads state, and the dead-code derivation
listed such members as unused (CleanArchitecture's ThemeToggleComponent
icon and label).
Every render-time expression is now read: property bindings,
interpolations, structural-directive microsyntax, control-flow blocks and
@let. A call of one of the component's own members becomes an
`angular-binding` edge with the binding as `via` and no trigger. It runs at
render, and a trigger drew computed signals as user actions in Steps. A
read without a call counts only for a method-kind member: a getter is
called, a function handed to a child is a fnRef reference, and a field a
call filled is a plain reference. Property reads link nothing, as in
TypeScript. Pipes, object and microsyntax keys, members of other objects,
template locals, strings and comments link nothing either.
Validated on CleanArchitecture, angular-realworld, ngx-admin and Ghostfolio:
272 edges added, none removed, and every registeredAt line checked. The
proshop_mern and next-saas-starter dumps are byte-identical.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
c513f1472a |
fix(frameworks): detection reads an app's package.json three or more levels down (#2395)
declaredDependencies read the root manifest and the ones one or two directories down. An app whose package.json sits deeper was never read: an ASP.NET solution's src/Web/ClientApp/, or prometheus's web/ui/mantine-ui/ under a workspace root that declares only tooling. Every resolver gated on dependsOn() then stayed off for it: Angular Router and the Angular template pass, React Router's navigation, Vue Router, SvelteKit and TanStack Router. The existing pass is unchanged. When it leaves slots open, a second pass probes the directories three or more levels down that hold indexed JS/TS code, shallowest first and sorted within a level, at most 192 of them, never inside node_modules. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ac1f430d3e |
fix(c,cpp): a pointer, array or function member is not a supertype (#2394)
extractInheritance carried Go's struct-embedding check (a field_declaration with no direct field_identifier names an embedded type) into every grammar with a field_declaration_list. tree-sitter-c/cpp, and Objective-C's C structs, nest a member's field_identifier inside its pointer/array/ function/reference declarator, so `SharedState* shared;`, `jv elements[];` and `virtual Status Put(...) = 0;` all became `extends` edges to the member's type: false supertypes in the type hierarchy, blast radius and codegraph_explore, and false cpp-override call edges built on them (google/leveldb: 246 of 302 extends edges). Gate the branch to Go in the TS extractor, drop the mirrored arm from the C/C++ kernel port (and the "kept for parity" quirk note), and drop the inert copy from the Rust-language port. C++ base_class_clause, Objective-C superclasses and Go embedding are unchanged. Validated on leveldb, jq, llm.c and redis: every removed extends edge is a member site, no base-clause edge is lost; gin, SDWebImage and ripgrep dumps are byte-identical; kernel/wasm parity sweeps show 0 diffs. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
83f54a1853 |
fix(dart): a library links the files its part directives name (#2391)
A Dart `part 'x.g.dart';` now mints an `import` node (name = the URI,
signature = the directive) and an `imports` ref from the library's file
node, in both extractors. The ref resolves through `dartDirectiveFile` by
the rules an import or export follows, so the library's file links the
part's file. A library and its parts are one library, so a change to a
part, generated or hand-written, now reaches the library and what imports
it in `codegraph affected` and the files listed as using the part. Before,
neither `part` nor `part of` left a trace, and the two files were joined
only by whatever symbol edges happened to cross between them: riverpod's
framework.dart, which holds nothing but `part` lines, by none.
`part of` still records nothing: the library's edge already joins the two
files, and a part -> library edge would list every pair on both import
rails of both files and make each a file 2-cycle.
The kernel's directive arm now goes on into the directive's children, as
the TS ladder does. Its `import_or_export` arm used to return first, so an
annotated directive's arguments (`@Tag(f) import 'a.dart';`) were fn-ref
candidates on the wasm path only.
Validation on rrousselGit/riverpod @4ba1be2 and felangel/bloc @b9be1e2
with the kernel loaded, base
|
||
|
|
837a1866f1 |
fix(dart): a call through an import prefix reaches only what that import exports (#2389)
A Dart call written through an import prefix - `http.get(uri)` after `import 'package:http/http.dart' as http;` - was resolved like a method call on a receiver named `http`, so it linked to any project member that fit the name: riverpod's docs' `http.get(...)` / `http.post(...)` calls went to a docs example's `Http::get` / `Http::post` by the receiver's capitalized name, and `fmt.describe(...)` to a class's `describe` method. Now `p.name(...)` calls a top-level declaration a library imported with that prefix exports (export chains, show / hide and parts followed through the library model), or nothing when the prefixed library is a package outside the repository. A constructor call (`p.Widget()`) instantiates the prefixed library's class. The same holds where the prefix heads a chain - `p.Box.create()`, `p.Report.empty()` - and for `p.Box<int>.named(...)`, which arrives as `Box.named`: the type is the one the prefix brings in. A parameter, local or member named like the prefix still hides it, and `const p.Box.named()`, which the extractors record as `p.named`, links nothing. The library model's one prefix reader is `dartImportPrefixes`: Dart annotations (#2390's `matchDartAnnotation`) read prefixes through it too, in place of the `isDartImportPrefix` it had alongside. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d2fb323e51 |
fix(dart): an annotation links the constant or constructor it names, even behind a comment (#2390)
A Dart annotation is a constant expression: a const variable (`@riverpod`, `@meta.immutable`, a type's static constant or enum value) or a call of a const constructor (`@Riverpod(keepAlive: true)`, `@Foo.named(…)`). It was ranked like a Python decorator, which favours a function or method of that name over a class and scores a constant not at all, so riverpod's 506 `@riverpod` annotations went to riverpod_analyzer_utils' extension getter `riverpod` (0.4) instead of riverpod_annotation's `const riverpod = Riverpod();`. matchDartAnnotation (name-matcher.ts) now takes every Dart `decorates` ref before any other strategy, and nothing falls through. It reads the annotation as written from its `@` and links a constant or (called) a class the file's library can see, through the import prefix it is written with (isDartImportPrefix, dart-libraries.ts); a type's own static constant, enum value or (called) named constructor (isDartConstructor); or, on a member, a static constant of the type around it. Never a method, getter or function. The decorator scan also stopped at the first sibling that was not an annotation, so a comment between an annotation and its declaration (`@override` `// ignore: must_call_super` `void f()`, riverpod_lint's `@riverpod` `// expect_lint: …` fixtures) hid the annotation. Dart's decoratorStepOverTypes lists its two comment kinds, which the scan now steps over, and the kernel's extract_decorators_for steps over the same. Validated on rrousselGit/riverpod @4ba1be2 and felangel/bloc @b9be1e2 with the kernel loaded: riverpod's 506 `@riverpod` edges move to `const riverpod` and 16 annotations behind a comment add an edge each, with nothing removed; bloc gains 7 `@override` refs and no edge changes. Kernel and wasm full-index dumps are byte-identical, and the parity sweeps show 0 diffs. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9924684899 |
fix(dart): a member keeps the dartdoc written above its annotations (#2387)
A Dart member's annotations are siblings that stand between it and the
dartdoc written above them:
/// Builds the widget.
@override
Widget build(BuildContext context) { ... }
Both extractors read a docstring from the comments directly before the
declaration (or its `declaration` wrapper) and stopped at the first node
that was not a comment, so the annotation ended the run before the `///`
was reached. Members behind `@override`, `@protected`, `@mustCallSuper`,
`@pragma(...)`, `@internal` and the like lost their documentation, and
searching for words in it didn't find them.
Extraction (TS + kernel, at parity):
- getPrecedingDocstring takes an optional list of sibling types to step
over, and Dart sets `docstringStepOverTypes: ['annotation']`. Stacked,
multi-line and inline annotations are passed, and comments on either
side of one still join (`/// a` `@x` `// b` gives "a" and "b" on two
lines), as adjacent comments already did. Any other node still ends
the walk: the previous member's body, a field, a top-level variable,
an import or another declaration.
- The kernel's shared docstring.rs gains preceding_docstring_stepping_over.
preceding_docstring passes an empty list, so every other language is
unchanged; only Dart's docstring_of passes ["annotation"].
- Class-like declarations needed nothing: a class, mixin, extension,
extension type, enum or typedef node starts at its first annotation,
so the dartdoc above it is already the node's previous sibling.
Validated on felangel/bloc @b9be1e2 and rrousselGit/riverpod @4ba1be2 at
main
|
||
|
|
249d9a860e |
fix(dart): a comment inside a member chain no longer loses a call or a read (#2385)
tree-sitter-dart keeps a comment as a named node wherever it is written, so in a member chain broken over lines (`tester //`, then `.state(…)` on the next line: the empty `//` keeps dart format from joining the lines) the comment, not `tester`, is the sibling right before the `.state` selector. Both extractors (the TS walker and the native kernel) read that sibling as the receiver or the callee: - A call with a comment between the member and its arguments (`box.grow /* by */ (3)`) was lost, and recorded as a read of `box.grow`. - A getter read (`box //` + `.area`) or a static access (`Config //` + `.instance`) written after a comment was lost. - A call after a comment kept only its bare name. Since #2376 the resolver reads such a chain link back past comments, so those calls already linked correctly; they now carry the receiver in the name. Every sibling step along a member chain now skips `comment` and `documentation_comment` nodes: the callee before an argument part, dartReceiverOf / receiver_of, dartCalleeOfArgPart, the type in front of a constructor's type arguments, and the member read's receiver and next selector. The misparsed-generic-call gate keeps reading raw siblings, and now rejects a comment against the `<` (`ref.read /* c */<Repo>(p)`), so that shape stays a comparison instead of losing its read. Validated with the kernel loaded on felangel/bloc @b9be1e2, rrousselGit/riverpod @4ba1be2 and flutter/samples @63411527. No edge is added or removed: the 14 comment-split chains there are all in riverpod, and its 5 calls among them gain their receiver and stay unlinked. Kernel/wasm parity has 0 diffs on all three. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d500e58a52 |
fix(dart): link a call chained on a generic lookup like BlocProvider.of<T>(…) (#750) (#2383)
When a generic call parses as a call (a named argument after a positional
one, as in `(context, listen: false)`, or a type argument like `<A?>`,
`<A, B>` or `<List<A>>`), the Dart extractor records a chain on it as
`BlocProvider.of().increment`. The resolver typed that receiver by what
`BlocProvider.of` is declared to return: `T` for `static T of<T>(…)`,
which names no type, and nothing at all when the lookup comes from a
package, as flutter_bloc and provider do in an app. So
`BlocProvider.of<CounterCubit>(context, listen: false).increment()` and
provider's `Provider.of<Cart>(context, listen: false).add(item)` linked
nothing.
The declared return type is still tried first, so `Foo.create().bar()`
with a concrete return type resolves as before. When it gives no edge,
the chain is read at the call site the way #2376 reads a later link of a
chain: the lookup's own type parameter is the type argument the call
gives, and an outside `of<T>(…)` lookup hands back its T. The edge stays
on the encoded ref, as in Go's fallback (the gin runaway).
The extractors are unchanged. A chain on a call parsed as two
comparisons keeps its bare name (#2375), and now reaches the same
method; the comments that gave the old reason are corrected.
Validated before/after with the native kernel at
|
||
|
|
1b752de2c0 |
fix(dart): a member with no body keeps its dartdoc and annotations (#2382)
A Dart member written without a body - a constructor like `Foo._();` or
`Point.origin() : x = 0;`, a `const` constructor or redirecting factory
such as flutter_bloc's `const BlocProvider.value(...)`, an abstract or
`external` method - parses as a `declaration` wrapping its signature,
and its `///` dartdoc and `@annotation`s are siblings of that wrapper.
Both extractors looked for them before the signature, inside the
wrapper, and found nothing: the docstring was lost, and
`@visibleForTesting Foo._();` emitted no `decorates` reference.
Extraction (TS + kernel, at parity):
- A Dart-only `getDeclarationWrapper` hook returns the `declaration`
when the signature is its first named child. `docstringFor` and
extractDecoratorsFor's preceding-sibling scan start from it; the
kernel's `declaration_wrapper` does the same for every docstring
lookup and for the annotation scan.
- The shared DOCSTRING_WRAPPER_TYPES / `is_wrapper` list is untouched:
C/C++ share it, and their `declaration` wraps declarators. All 35
non-Dart parity fixtures extract byte-identically before and after.
Validated on felangel/bloc @b9be1e2 and rrousselGit/riverpod @4ba1be2
at main
|
||
|
|
3f96f80fcb |
fix(dart): link generic calls the parser reads as two comparisons (#2375)
tree-sitter-dart parses a generic call with arguments as two comparisons about as often as it parses it as a call, depending on the code around it: `ref.read<Repo>(p)` comes out as `relational_expression(relational_expression(ref.read < Repo) > (p))`. Generic constructors (`Provider<int>((ref) => 0)`), named arguments (which then parse as a `record_literal`) and calls after `await` or a prefix operator are hit too. Neither extractor recorded a call for that shape, only a member read of `ref.read` that linked nothing, so providers, repository reads and bloc lookups written with a type argument had no callers. Across felangel/bloc, rrousselGit/riverpod and flutter/samples there are 2,853 such calls, against 2,836 generic calls with arguments that parse as calls. - Both extractors (the TS walker and the native kernel) recover the call at its `<` when the type argument names a type and the code is laid out as a call: `<` against the callee and `(` against the `>`. A comparison such as `a < b` is left alone. The recovered call gets what a parsed one does: the call at the `<`, a reference to the type argument, and no member read of the callee or of a prefixed type argument. - A call chained on a recovered call keeps its bare name. The `BlocProvider.of().increment` encoding a parsed chain gets resolves through what `of` returns, which for a generic factory is its type parameter, so it would drop the edge the bare name finds. - When the grammar ends an arrow function in front of a generic call (`(ref) => ref.watch<int>(p)` as `((ref) => ref).watch<int>(p)`), the receiver is read from the end of the arrow's body, so the call is `ref.watch` rather than a bare `watch`. Validated with the kernel loaded: bloc +85, riverpod +1,580 and flutter/samples +54 edges, none lost; kernel/wasm parity has 0 diffs on all three repos. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
652c36b72a |
fix(dart): index const constructors and redirecting factories; a call through a type stays on that type (#2380)
* fix(dart): index const constructors and redirecting factories; a call through a type stays on that type Both Dart extractors (the TS walker and the native kernel) skipped `const` constructors and redirecting factories (`const Foo.bar(...)`, `const factory Foo.bar() = _Bar;`), which parse as signature kinds of their own. A call such as flutter_bloc's `BlocProvider.value(...)` then found no method on BlocProvider, and the resolver guessed another type's `value`: on felangel/bloc, 89 calls linked to `RepositoryProvider::value`. Extraction (TS + kernel, at parity): - Named const constructors and redirecting factories are methods named by the constructor, returning their class, with a parameters-only signature; the unnamed ones stay unindexed. The name is read only from before the parameter list, so a redirect target (`= _Impl.named`) never names it. - A redirecting factory references the class it redirects to, not the target constructor's name or an import prefix. - A constructor called with type arguments keeps its type: `BlocProvider<CounterCubit>.value(...)` is `BlocProvider.value`, as a statement and as an expression (`constructor_invocation`, which produced no call at all before). Resolver: - A Dart call through a type's name reaches only that type's own constructor, static member or static constant, or nothing: no guess by name, no inherited lookup, no same-file look-alike, no instance member of the same name. SDK calls like `Uri.parse` no longer land on a project `parse` past an `extension on Uri`. - A named constructor is never reached by a bare name, the last link of a chain, a function value or an annotation (riverpod's 210 `@internal` annotations). Validated on felangel/bloc @b9be1e2 and rrousselGit/riverpod @4ba1be2 with the kernel loaded: bloc 18,338 -> 18,495 edges, riverpod 85,606 -> 85,854, every removed edge triaged. Afterwards every edge into a named constructor is written `Owner.ctor`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(dart): read a type's static members one way for calls and chains - A call through a type's name now finds its member with dartStaticMember, the lookup a chain's static link uses since #2376, so both pick the same member: the call site's own file first, then the nearest copy. - isDartStaticMember's constructor test is isDartConstructor, anchored at the declaration: an instance getter declared just above a same-named factory (`Object? get error` over `const factory AsyncValue.error(...)`) read the factory from the next lines and counted as static. - clearNameMatcherMemos drops DART_CONSTRUCTORS with the other source-derived Dart memos. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6421644ef2 |
fix(dart): link calls in top-level and field initializers (#2367)
Both Dart extractors (the TS walker and the native kernel) skipped the code in initializers. A `final`/`const` declaration minted its constant and stopped, and a field's or `var`'s initializer only had its types recorded (#2360). In Riverpod apps, where most wiring lives in provider closures (`final repoProvider = Provider((ref) => Repository(ref.watch(dioProvider)));`), what a provider builds with had no callers, and impact and explore flows stopped at the provider. - A `final`/`const` initializer is walked like a body, for its constant: calls, instantiations, static and member reads, types, closures with block bodies and local functions, function values. - A field's (instance, `static var`, typed, `late`) or a top-level `var`/`late final` initializer is walked for its class or the file, since neither mints a node. A local declaration's second variable (`for (var i = 0, j = n(); ...)`) stays its function's. - Each initializer is walked once: a new `walkInitializer` context method marks it, and the dispatcher's function-as-value scan skips it. Other languages' hooks are unchanged. - Resolver: a Dart call through a type's static constant (`FutureProvider.autoDispose(...)` on `static const autoDispose = ...Builder();`) links that constant, not another type's same-named method. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ed1a89fb0f |
fix(rust): a variant path references the enum that declares it (#2328) (#2354)
Using an enum only through its variants (`mode::Mode::A` in an expression,
`mode::Mode::A => 1` / `Mode::B => ...` in a match, a `Mode::C(x)` or
`Mode::D { .. }` pattern) recorded no reference to the enum, so code that
builds or matches variants was missing from the enum's callers and impact.
Only a type annotation (`fn takes(_m: Mode)`) produced one.
Cause: Rust was not in the static-member pass, which turns `Enum.value` /
`Type::CONST` reads into references for Java, C#, Kotlin, Swift, Scala,
Dart, PHP and C++, and a path to a variant that is not a call or a struct
literal reaches no other extractor.
Fix:
- Extraction (wasm extractor and native kernel, mirrored): a path whose
last segment is capitalized emits a `references` ref named by its
receiver (the segment before it; `Self` read as the impl's type, nothing
in a trait), positioned at the receiver, so a `mode::` prefix scopes it
exactly as it scopes a type annotation. Covers value reads, match arms,
`|` / `if let` patterns, tuple- and struct-variant patterns and a variant
passed as a value. A call's callee (`Mode::C(1)`, `Foo::new()`), a struct
literal, a longer path's prefix, a `use` tree and a lowercase receiver or
member emit nothing.
- Resolution (isRustNameInScope): a reference written `Name::Variant` or
`Self::Variant` lands only on an enum that declares that variant, after
the usual Rust scoping (module path, `use`, glob, prelude, std and
dependency imports). ripgrep's `Match::None` reaches `ignore`'s enum, not
the matcher's `struct Match`; std variants (`Ordering::Less`,
`Option::Some`) never reach a same-named project enum; associated consts
and functions (`Limits::MAX`, `Mode::ALL`, `Foo::new()`) reference no
type. `Self::X` is read as the impl's type, never through a path written
elsewhere on its line.
Adapted from #2330 by @danusha2345: the extraction half (both walkers) and
most of the tests are theirs. Changed here:
- The resolver accepts a declared variant only. #2330 also accepted any type
whose file holds a variable or const of the item's name; impl consts are
file-level variables in the graph, so that cannot tell a type's associated
const from another type's or a top-level one.
- The walkers check the node kind before looking at its parent, and the
body walk hands the parent down: #2330 called `parent()`, which walks down
from the tree's root, for every node of every body, which nearly doubled
the kernel's Rust extraction time on tokio (2.1 s -> 3.9 s); now within
noise of main.
- EXTRACTION_VERSION is not bumped again (already 28 on main).
Verification: __tests__/rust-enum-variant-reference.test.ts (10 tests):
6 fail on main in wasm mode, in kernel mode, and with this TypeScript over
main's kernel binary; 10/10 pass in both modes. The kernel parity case
covers every shape and asserts the refs exist; the kernel-parity sweep over
ripgrep, alacritty and tokio is 1004/1006 files byte-parity, 0 diffs (2
parse-error files defer to wasm by policy).
Validation (native kernel loaded, main -> branch, edges keyed by
QN/kind/file/line): ripgrep +822/-0, alacritty +917/-0, tokio +665/-0, all
`references -> enum`, node counts unchanged. Every added edge's source
reads `Name::Variant` / `Self::Variant` at its column and its target enum
declares that variant; no std enum targets. New symbol->enum pairs:
ripgrep +220, alacritty +253, tokio +254; enums with no referencer before
and one now: 31 / 13 / 31. One wrong target on tokio
(`oneshot::error::TryRecvError::Empty` -> mpsc's `TryRecvError`) comes
from the existing path rule that checks only the last module segment, which
type annotations share. Index time on tokio stays within noise of main
(interleaved runs; resolveOne time 27.7 s vs 31.2 s summed over workers).
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
a192df40f9 |
fix(telemetry): count late index uploads, treat usage as live ingest, purge before catch-up (#2333) (#2356)
Three bugs in the self-hosted telemetry services (telemetry-worker/ and
telemetry-dashboard/), reported against
|
||
|
|
355afc70b1 |
fix(csharp): walk field and property initializers; a constant never names a type (#2337) (#2348)
* fix(csharp): walk field and property initializers as the member Field declarators and property `= initializer` values were skipped by both extractors, so `private readonly ILogger _log = LogManager.GetLogger(typeof(X));` linked nothing, and a method or class used only from an initializer looked unused. - tree-sitter.ts / csharp.rs: walk each C# field declarator whole with its field on the stack, and every property `value:` (not only `=> expr`) with the property on the stack. Calls, instantiations, static reads, lambda bodies and fn-ref candidates (varinit included) belong to the member; the fn-ref scan skips the walked subtrees, so each candidate is captured once. Attributes stay unwalked. - A target-typed `new()` that is the initializer instantiates the declared type (`List<Foo> _items = new();` -> List). In bodies it stays invisible, as before. - name-matcher.ts: a .NET type position never names a `constant` (the kind `const` / `static readonly` fields get). Walking initializers exposed `new Version(5, 18)` binding to a `const string Version` and `static readonly Meter Meter = new(...)` instantiating itself. Kernel/wasm parity: 0 diffs on serilog, Newtonsoft.Json and jellyfin, and full-index dumps byte-identical between the two arms. No EXTRACTION_VERSION bump: #2345 already moved it to 28 this release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changelog): credit the report and contributor behind the constant change The type-position change here is what issue #2337 reports (`new Station { … }` unresolved beside a `private const string Station`) and the same one-line change as @drakeo338's #2339. Verified on the issue's own three-file repro: the `instantiates` edge to `Demo.Core::Station`, missing on main, is present with this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
489b4749c7 |
fix(vbnet,csharp): index every Structure member; walk property bodies and VB initializers (#2345)
VB.NET's grammar tags each member of a `Structure` as its own `body`
field, so extractAggregate's field lookup saw only the first member.
SCrawler's UserMedia indexed none of its members, and a structure that
opened with a nested enum indexed only the enum. Aggregates now resolve
their body the way classes and enums do (resolveBody, which VB maps to
the declaration itself). Objective-C, the only other struct language
with that hook, is unchanged.
Property bodies were never walked: the propertyTypes branch set
skipChildren after extractProperty, so calls, instantiations and reads
in a VB `Get`/`Set` block, `= initializer` or `As New`, and in a C#
accessor body or `=> expr`, were lost.
- propertyBodies() names the parts of a property that run code (VB: the
whole declaration, as its methods are walked; C#: each accessor's body
and an arrow value), walked with the property on the stack. The
candidates-only fn-ref scan skips the walked subtrees by node id, so a
function value is captured once, from the property. C# `= initializer`s
stay unwalked, as C# field initializers are.
- The C# kernel mirrors it (property_bodies, scan skip list). The parity
suite and a new micro pass; a serilog sweep is 209/214 byte-identical
with 0 diffs (5 deferred for parse errors).
- VB field declarators (`= expr`, `As New T`) and Custom Event
AddHandler/RemoveHandler/RaiseEvent blocks are walked as their field or
event.
Before/after (nodes / edges): SCrawler 9,950 -> 10,427 / 14,336 -> 15,092;
staxrip 13,440 -> 13,644 / 24,120 -> 28,002, most of it its
`Property X As New NumParam With {...}` settings; serilog edges
6,642 -> 6,654. All 23 removed edges are the same call sites re-resolved
by VB's name-guessing resolver now that struct members are candidates:
10 wrong edges dropped, 9 correct ones now guessed wrong, 4 wrong either
way. Designer -> Add/Size edge counts are unchanged.
EXTRACTION_VERSION 27 -> 28.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
217190680d |
fix(telemetry): recover from the D1 10 GB outage; dashboard starts on today (#2317)
The telemetry database hit D1's 10 GB cap on 2026-08-11 and refused nearly every write for seven weeks: the dashboard kept showing Aug 9 as the latest day, and its activation query (a cohort join over raw events, ~55 s per week) stalled D1's single query lane until every panel failed. Root cause was the client: usage counters were aggregated per process and appended on exit, so every `serve` launch, CLI command and prompt hook uploaded its own `count: 1` line - 30.3M of the 30.8M event rows. - Client: merge count lines per (day, kind, name, client) on append, stale-claim recovery (which also bypassed the size cap) and send. - Ingest: usage counters ADD into usage_daily, one row per machine x day x tool (migration 0003), so storage no longer depends on upload frequency. - Rollup: reads usage from usage_daily, folds legacy usage rows out of events in 50k-row transactions, maintains machine_first_seen.first_index_day (0002), catches up on days a failed run missed, purges usage_daily with retention. 0004 drops the now-unused events_machine_day index. - Dashboard: ranges end today; rolled-up series return null past the rollup's last day instead of a phantom zero; no panel reads raw events (activation reads first_index_day); min/max lookups stay index-only; one retry on transient 5xx; banners when ingest stalls or the rollup falls behind. - scripts/backfill-rollup.sh re-runs the rollup over a day range. Already applied in production: DB 10 GB -> 467 MB, all usage counts kept, rollups current through 2026-10-02. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1f64e15886 |
fix(react-router): read links through a route-config object's getHref (#2187)
bulletproof-react writes every link as `<Link to={paths.app.discussion.getHref(id)}>`
and every redirect as `navigate(paths.auth.login.getHref(...))`, so none of its
navigation reached a route. The href reader now follows the config object to
where it is declared (through imports and tsconfig aliases), takes the string or
template the helper returns, keeps a whole-segment `${id}` as a parameter and
drops a hole glued onto a segment (a `?redirectTo=` suffix).
The Link tag pattern also stopped at the `>` of an arrow attribute written
before `to` (`onMouseEnter={() => prefetch(id)}`), which hid those links.
bulletproof-react: 0 -> 11 navigates edges; jira_clone, ecommerce-react,
react-redux-realworld, create-t3-turbo and excalidraw unchanged.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
37bcd5c0ae |
fix(react-router): nested, lazy and constant routes; per-app tsconfig aliases (#2165)
- Route tables compose nested paths: a child's `path` is relative to the
path-bearing routes around it (`children` of a data router, nested
`<Route>` elements), absolute ones reset. `lazy: () => import('./x')`
(object and JSX) renders the module's default export (`Component` export
as fallback) via a claimed `lazy-import:` ref. `path: paths.app.root.path`
keeps its parts on the node's signature and is named in postExtract from
the constant's object literal (found through the file's import). The
route keeps its extracted id; `isReactRouterRoute` accepts it. An element
wrapped in parentheses or a guard (`<ProtectedRoute>`, `<Suspense>`,
`*Provider`, `*Guard`) renders the first element inside.
- Import aliases: the tsconfig / jsconfig nearest the importing file that
declares `paths` is tried before the root's, so each monorepo app resolves
its own `@/*` / `~/*`.
A/B (edges removed / added): bulletproof-react -43/+367 (0 → 9 named,
linked routes; cross-app imports fixed), react-native-reusables -103/+193,
kit -184/+218, trpc -45/+71 (examples' `~/…` now their own), create-t3-turbo
-1/+13; excalidraw, halo, obytes, realworld byte-identical. #1348's nested
fixtures now expect composed paths (`/dashboard/settings`, `/data/prefs`).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
cec041d241 |
fix(angular): Nx workspaces, barrels, class-constant paths and root-relative navigate (#2133)
From the router sweep:
- angular-spotify had 12 routes all named `/` and no navigation. Its lazy
routes are `async () => (await import('@ws/home')).HomeModule` through an
Nx library's `src/index.ts` barrel, which the mount pass never looked
through; and every lib is its own `src/`, so each routes file got its own
route table and no template's `routerLink` found the screen it names.
- jira-clone had 3 routes and no navigation: its issue route is
`issue/:${ProjectConst.IssueId}` (a class `static readonly`), its root
redirect lives in `app.routes.ts`, which only mounts, and
`navigate(['project', 'issue', id])` has no leading `/`.
Now: lazy mounts follow `export … from` barrels (and an NgModule's routing
imports); `(await import(x)).M` is read; route constants may be class
statics (strings or objects), enum members, or live behind a barrel, and a
template-literal path takes constant holes; a routes file that only mounts
contributes its redirects at its mount prefix; the route table is keyed by
the `angular.json` / `nx.json` workspace; and `router.navigate` /
`createUrlTree` without `relativeTo` resolve a bare first command from the
root, as Angular does (template `routerLink` stays relative).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
4423891e76 |
fix(react-router): v5 <Redirect to> and styled(Link) wrappers navigate (#2132)
react-boilerplate's header links are `export default styled(Link)`…``, used as `<HeaderLink to="/features">`; takenote's guards render v5's `<Redirect to="/" />`. Neither tag was read, so both apps had routes and no links between their screens. The link synthesizer now reads `<Redirect to>` (navMethod `redirect`) and any styled-components / emotion wrapper of `Link` / `NavLink` — declared in the file, or imported from a file that declares or default-exports one. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b62da73550 |
fix(sveltekit): route names drop (group) folders and parameter matchers (#2131)
A `(group)` directory shares a layout and is never part of the URL, and a
parameter's `=matcher` checks its value. Kept in the name, shadcn-svelte's
`src/routes/(app)/(layout)/blocks/+page.svelte` was `/(app)/(layout)/blocks`,
so no `goto('/blocks')` or `<a href="/blocks">` ever reached it and its
Screens showed no navigation at all.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
54a1698caa |
fix(vue-router): admin-template route tables, children and layouts; Nuxt routes only in Nuxt (#2130)
vue-element-admin, vue-admin-template and vben had no routes: their tables
are named arrays (`export const constantRoutes = [...]`, `const routes:
RouteRecordRaw[] = [...]`) or per-module objects (`const tableRouter = {…}`)
handed to `new Router(...)`, and most of their screens are `children`.
The Vue Router reader now reads those tables in any file that builds a
router, imports vue-router or lives in a router/ directory; joins children
onto their parent's path (a parent is a screen only when no child claims
its address and it doesn't redirect); binds a lazy view by the FILE it
imports (all of vue-element-admin's are `…/index.vue`), through an alias the
resolver can't follow by the one file in the app with that path; links each
screen to its parents' components as layouts; and takes `this.$router.push`.
Nuxt's file routes move into their own `nuxt` resolver, detected only in a
Nuxt app: halo's plain-Vue console got 30 made-up screens from its `pages/`
folders. A Nuxt app at the repository root now gets its routes, and a
top-level `pages/index.vue` is `/`, not `/index`.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
c3efbbf4e7 |
feat(angular): a template's event bindings call the component's methods (#2114)
An Angular handler's only caller is its template: `(click)="toggleFavorite()"`,
`(ngSubmit)="submitForm()"`. With no edge, the method showed no callers,
impact stopped at it, and the Steps picture of a screen reached its
handlers only through class containment, with nothing to say what fires
them.
The template pass now reads each `(event)="…"` binding — not
`[(ngModel)]`'s two-way half — and links the component to every method of
its own the statement calls (`save()`, `this.toggle(x)`; not
`closed.emit()` or `form.reset()`), as a `calls` edge
(`synthesizedBy: 'angular-event'`) carrying the binding as
`metadata.trigger`: `{ kind: 'prop', name: '(click)', of: 'button' }`.
The binding is in the template, so it cannot be read back from the source
at the edge's line; Steps now takes a trigger an edge carries before
reading one at the site.
angular-realworld: 21 binding sites → 17 edges (3 repeats of a method and
event, 1 `delete.emit(true)`); Steps for /article/:slug draws
deleteArticle on (click) · <button> → DELETE and back to /, addComment on
(ngSubmit) · <form>, the favorite button's toggle → POST/DELETE or
/register. Ghostfolio 225 edges of 302 sites, ngx-admin 75 of 100 — the
rest are `$event.stopPropagation()`, output emits, assignments and
repeats. Controls (koel, proshop) byte-identical.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
761611c18e |
feat(angular): routes, navigation and the component tree for Angular apps (#2113)
An Angular app had no route nodes, no navigates edges and no link from a template to what it renders — the viewer's Steps tab said "No screens or endpoints" and there was no Screens tab. Routes (frameworks/angular-router.ts): every `Routes` array — typed `Routes` / `Route[]`, `RouterModule.forRoot/forChild`, `provideRouter`, a routes file's `export default [...]` / `as|satisfies Routes` — walked as objects. `component` binds by `references` (route-roots takes a class as a named handler); a lazy `loadComponent` binds to the file's `@Component` class. `children` join their parent's path; a lazily loaded file's routes are put under the path that loads them in postExtract (following an NgModule `loadChildren` to the routing module it imports), idempotently from the in-file path kept in the qualified name. A route with children is a layout: a screen only where no child claims its address, and linked from each screen inside it (`references`, `layout: true`). Paths written as route constants (`internalRoutes.account.path`, through destructured locals and `const x + '/:id'`) and `$localize` strings are read from the constant object. `redirectTo` aliases an address to the screen it sends the user to; a root `**` redirect answers `/` only. A `matcher` route and a `**` catch-all are not screens. Navigation: `router.navigate([...])`, `navigateByUrl`, a guard's `createUrlTree` / `parseUrl` — a command array (non-literal elements are holes), a static string, a route constant, a component property holding one, a function constant called with arguments, `.concat(id)`. A `relativeTo` navigation, a query-only `navigate([])` and a destination made only of holes (it would match any route of its length) are left unresolved. Templates (angular-template-synthesizer.ts): a component's `templateUrl` file or inline `template:` is read at synthesis time. `<app-foo>` by element selector (same app first; two candidates = none) is a `calls` edge from parent to child (`angular-template`), the edge a navigation in a child component rides to its screen. `routerLink` / `[routerLink]` and a `routerLink:` field written in the class (a tab bar's config) are navigates edges. Gated on an `@angular/core` dependency. Measured: - angular-realworld: 10 routes all bound; 11/11 navigation sites accounted for (10 resolved, 1 query-only); 31/31 routerLink sites resolved; 18 renders. - Ghostfolio: 84 routes all bound, 0 unresolved constant paths; 27 navigate resolved, 23 correctly not (8 relative, 9 query-only, 2 computed, 4 aimed at a matcher route); 162 routerLink edges, every one checked against its site; 170 renders. - ngx-admin (NgModule): 55 routes, 47 bound (8 are @nebular/auth's); no navigation calls in the source. - Controls byte-identical vs main (nodes and edges incl. metadata): koel, IceCubesApp, spring-petclinic, proshop. Ghostfolio index time at parity. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
003305f1e7 |
fix(explore): claim complete source only for sections that are complete (#2077)
* fix(explore): claim complete source only for sections that are complete On the tiers with a completeness signal (>= 500 indexed files) every codegraph_explore response ended with "Complete source for N files is included above — do NOT re-read them", whatever the render had cut. A windowed spine method, a shrunk or dropped cluster and the per-symbol view all went out under that line; the oversize-spine window never even set the trim flag the small tiers key off, which is how a 62-line slice of vscode's 968-line rpcProtocol.ts was called complete. The line also said "Reserve Read for a single specific line range", which explore output must never say. Completeness is now measured at emission from the ranges actually sent (plus back-referenced spans) against the symbols each section set out to deliver. A trimmed response keeps the verbatim / already-Read guarantee, names the trimmed files and the most relevant elided symbols, and steers to another codegraph_explore. The note is offered from most to least specific and fitted to the leftover room (CG-26); a trimmed note yields the pointer list's first entry. The lost-pointer, cut and truncation notes drop "complete" the same way, at no greater length, so the epilogue floor is unchanged. The small tiers' trimmed note now also fires for the cuts the flag missed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(explore): offer an elided method as Owner.member in the completeness note The note offered `as_sql` for django's elided SQLCompiler.as_sql — one of 110 definitions — so agents appended a path and line to disambiguate it, and that query shape returns none of the body. `SQLCompiler.as_sql` alone returns most of it. Methods are now offered as `Owner.member` from the indexed qualified name; everything else keeps its bare name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(explore): fit the completeness note and pointer list in one tested function A trimmed note's optional detail (the files, the elided names) never costs a pointer entry the least specific note would have left. The fit moves out of handleExplore into fitExploreEpilogue so both rules are pinned by tests; the second-try branch it had could never fire and is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(explore): completeness note wording after #2068 flags the spine window Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(explore): record the completeness-note replay and agent A/B Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(explore): address review of the completeness note - Judge completeness AFTER the dedup fold: a remainder too small to fence is sent in neither call, so a section with one is trimmed, not complete. The whole-file render now declares its symbols too (clamped to the lines it prints; the file node is not one), so a fold there registers. - Label trimmed files unique among every path the response can name, the pointer list included. - "1 file", and no "0 files" when every section was held from an earlier call. - The lost-pointer, cut and truncation notes are one exported table, EXPLORE_FALLBACK_NOTES, with the length rule tested; the lost note picks its wording from trimmedShown instead of re-joining the response. - The design doc's follow-up list marks the overclaim fixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
724b5dee0c |
fix(explore): return named bodies from merged spine clusters; let named files use spare budget (#2068)
* fix(explore): return named bodies from merged spine clusters; let named files use spare budget Re-applied onto main after #2062 and #2063 landed in the same render path (originally e31cc9fc on claude/inspiring-grothendieck-8a7b3e): - the merged-spine hold-back and its top-up are dropped: #2062's hold-back (every cluster leaves room for the protected members ranked below it) subsumes them, and the past-the-pin-cap test passes on it; - #2063's "don't window a cluster holding an exact target" becomes the member rule's "don't window a spine method that is or holds one"; - the shrink orders exact targets ahead of spine members, and the exact branch prices member windows (sectionRangesOf) rather than whole bodies; - the final ceiling fit shortens the file header back to its estimate before it cuts source. With an exact target's overshoot (#2063) spending a named file to its funded line, a lower file's header running past its estimate cost it 109 chars of source (922 of 1,031; 995 on main). The valve test now asserts funding in full and delivery to the line, since #2062's exact rendering stops at whole lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(explore): record the agent A/B for the named-concentration fix Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(explore): record landing named concentration on main after #2062/#2063 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
47840a6ed4 |
fix(explore): return a qualified or line-anchored method's body instead of its signature (#2063)
`SQLCompiler.as_sql pre_sql_setup get_select` on django returned get_select and pre_sql_setup in full and SQLCompiler.as_sql -- qualified, step 1 of the Flow, 226 lines -- as one signature line. compiler.py renders as a per-symbol focused view, which picked bodies in source order within a tier, so the unnamed bridge get_qualify_sql and get_select (both above as_sql in the file) took the cap. The follow-ups agents issue (`compiler.py:776`, `compiler.py lines 900-1003`) pinned the file and dropped the line numbers. Every agent run ended in a Read of compiler.py. - query-paths: keep `:776` / `:12-40` / `#L88-L120` suffixes as line anchors and bind prose ranges (`lines 900-1003`, `L900-L1003`, `lines 900 to 1003`) to the nearest single-file path. - EXACT targets: a qualified name resolving to <=3 callables, or the callable enclosing a single-line anchor. Multi-line anchors render as that span. - Focused view: tiers exact > named spine step > unnamed bridge > unique named > family co-named, spine in call order. A tier 0-1 body that does not fit is windowed (head + calls into the question's other symbols) with the explore range that returns each hole, never reduced to a signature. Priced in rendered chars net of the signature lines, so the section keeps its promise. - Cluster path: exact clusters rank first with the spine's bounded overshoot, shrink by exact rendered size, and hold back what lower-ranked exact clusters still owe; files holding one are funded like spine files. - Blast radius leads with exact targets. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
adad8407ff |
fix(explore): pay named members before incidental ones across clusters (#2062)
Clusters of equal max importance rank by density, and density counts every member, so a named function with unrelated helpers merged around it outranked a cluster holding a named function alone. Taken whole, the higher clusters spent the file's reservation on the helpers and the isolated named function rendered nothing (lib/response.ts repro: sendBody 0 of 36 lines). Reordering alone only moves the victim. Two rules, both scoped to incidental members (not named, not an entry point, not on the call path): - a cluster's incidental members may only use what is left once every lower-ranked cluster's protected members are paid; its own protected members are never held back, so named-vs-named stays rank's call; - in a cluster holding a protected member, incidental members are never kept past the render's ceiling in rendered size, so the source-order window back to the ceiling no longer cuts a named body's tail (gin's handleHTTPRequest) on selection or on any re-render. 47 deterministic queries over the 7 README repos plus express vs main: named bodies complete 198 -> 234 (17 responses up, 0 down), named lines +10%, responses over the 25K cap 5 -> 1. One new final-cut truncation (django-bag) comes from owedPayableBelow's MIN_CHARS threshold, recorded in the design doc. On main + the named-concentration branch: 235 -> 279. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8c38b80c5c |
fix(scala): bump the bundled grammar to tree-sitter-scala v0.26.2 (#1823) (#2003)
The vendored grammar (master@0aca5d0a6f, in both the wasm and the kernel's C
sources) parsed `class A(x: X)(implicit y: Y) extends B(x)(y) with C with D`
with an ERROR and cut the extends clause after `B(x)(y)`, so `C` and `D` never
became inheritance edges. Both copies now come from the v0.26.2 release: the
wasm is the release asset, parser.c/scanner.c are the tag's generated sources.
The grammar change is additive over the old pin: no node type or field was
removed.
On two real repos the share of Scala files that fail to parse drops to zero
(lila 85/1565, playframework 83/846), with no file newly failing. Edges from
those files were built from error-recovered trees; playframework, for
example, nested top-level classes inside their neighbours and drew an
`extends` self-loop.
The kernel defer pins used inputs that the new grammar reads cleanly; they
now use inputs that still fail, and the old inputs pin that both paths
extract them in parity.
(cherry picked from commit
|
||
|
|
51116a26cb |
fix(telemetry): honor opt-out across running processes (#1880)
Refresh consent across processes, reset identity on opt-out, discard obsolete buffers and stop subsequent requests/requeues. Preserve environment override precedence and document in-flight semantics. Fixes #1869. |
||
|
|
3ed73bc127 |
feat(ui): the Steps tab lays a screen out in clusters and says a far link in words; a <Card/> is the Card its file imports (#1817)
* fix(ui): a screen's steps read as clusters, and a link too far to follow is said in words The mobile app's /capture came back as a web: 100 boxes in a 1,227x5,588 ribbon, 113 lines drawn at rest crossing each other 652 times, each one running over about five other boxes' names. Measured, not guessed — three separate causes, very unequal. The region grouping had nothing to divide there (98 of 100 boxes take their region from one memoized component), so the picture fell back to a single 719px column. But the region dimension was not the lever. The lever was that `packRegions` packed every step of one distance onto shared rows and wrapped those rows at a fixed 720px, so a box and the thing it fires landed seven lines apart: 70 of the 113 lines joined boxes ONE step apart. That is what the crossings were made of. So a region is now packed as CLUSTERS — a step, then the steps it sets in motion on the line under it, stepped in — while the starting points that fire nothing still share a line, because a screen's handlers are siblings and giving each its own line turned a flat region into a column. A region's line width is earned rather than fixed (sqrt(total * pitch), clamped 720..2600), so a big screen comes out about as wide as it is tall. Clustering makes most links local but not all: a step reached from two places is drawn under whichever reached it first, so the other way in still crosses the picture. Those are now said in WORDS at both ends — `-> resumeInference` under the box that leads there, `<- CaptureView` under the box it arrives at, capped at three with `+N more` — rather than drawn. This is not a hiding: the link is stated, which says more than a line vanishing off the edge of the screen does, and selecting the box draws every one of its real lines exactly as before. It is the one at-rest cut that does not produce the "box that leads somewhere and draws nothing" every earlier cut produced. Also fixed while here, and predicted by the earlier region work: the in-region row relaxation had no cycle guard, so a region holding one loop pushed 65 of its boxes to rows 294-301 while the rest sat at 0-2. `forwardLinks` sets cycle-closing links aside first, as the order reading's `withoutBackEdges` already did. /capture: 1,227x5,588 -> 2,279x4,356, at-rest crossings 652 -> 1, lines-over-boxes 553 -> 26, with half the links still drawn as real lines and every quiet box still one the screen itself fires directly. The order reading is untouched (it keeps every line). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTUFNN5bH3aPw2gi2LqbYD * fix(ui): a stub names its box without the mark the box wears for its kind `← ⇠ onCaptureProgress +2` reads as two arrows arguing: the stub already leads with a direction, and the box's own kind mark was competing with it. Verified in the live canvas. The box keeps its mark, where nothing competes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTUFNN5bH3aPw2gi2LqbYD * fix(ui): the screen's line into each of its parts stops sweeping the picture Audited all 51 screens of the mobile app on this branch. 96 lines — the screen's own stand-in line into each region — were 17% of everything drawn and caused 79% of every crossing left. A screen with ten regions tiles them into bands, so the line into a region two bands down travelled the height of the whole picture. Two causes, both fixed. The entry the line lands on was the walk's first member of the region; clustering moves a step that fires something BELOW the ones that fire nothing, so that box could sit lines down inside the region and the line had to reach past everything above it. It now lands on the box nearest the region's top-left that the screen actually leads to. And the stand-in line is no longer exempt from the stub rule — when the region is still too far to follow, the link is said in words like any other. The rest of the anchor's fan stays quiet as before: it is already stood in for. Across the 51 screens: crossings 47 -> 10, no screen above 10 (worst was 19, now 2); lines-over-boxes 236 -> 182; boxes with neither a line nor a word 150 -> 135. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTUFNN5bH3aPw2gi2LqbYD * fix(ui): a screen's parts fill the canvas instead of squaring off into rows Audited the app's 28 regioned screens: the median canvas was 55% region and 45% nothing, and /home was 44% — 4,860px tall to hold about 2,160px of picture. The cause is that regions were tiled a row at a time with each row as tall as its tallest member, so one short region beside a tall one left the rest of that row blank, and a reader scrolls through the blank. Each region now goes as high as it can and then as far left as it can, over a skyline of what is already placed. Reading order is untouched: regions are still walked in the screen's own source order, so an earlier one is never pushed below a later one — a short one just tucks under another short one rather than waiting for the tall one beside it. Layering now comes from the finished geometry rather than a band counter, since once regions drop independently what a reader sees as one row IS one row. /home 4,860px -> 3,584px, aspect 0.59 -> 0.94. Tallest screen in the app 4,860 -> 4,356. Crossings 10 -> 13 across all 51 screens, still none above 10. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTUFNN5bH3aPw2gi2LqbYD * fix(ui): the width a picture wraps at is tried, not estimated A region's line width came from sqrt(total * pitch) — the width at which total/width lines come out square. That estimate is wrong for how these pictures are drawn: a cluster spends lines on its own structure (a hub gets a line to itself, and what it fires starts another), so it undercounts a region's lines badly and wrapped /capture's 98 boxes into a 4,356px column. Laying a picture out is cheap and exact, so the widths are tried instead: layoutAt runs the whole pack at each of eight widths and the best finished canvas wins (~2ms for the model, all eight included). It has to be scored on the CANVAS, not per region — squaring each region off individually leaves fewer of them side by side, which took /home from 3,584px to 5,624px while every region looked better on its own. Also measured and rejected while here: dropping a region's CLUSTERS side by side the way the regions drop onto the canvas. Total height 42,084 -> 39,756px (-6%), but lines-over-boxes 120 -> 134 and crossings 5 -> 8, because two clusters side by side put each one's lines through the other. Height is cheap to scroll; a crossed line is what made this picture unreadable. The reasoning is recorded in the code so it is not re-tried blindly. Regions differ — they sit far enough apart that few lines run between them. Across the app's 51 screens: tallest picture 4,356 -> 3,796px, total height 45,744 -> 42,084px, lines-over-boxes 184 -> 120, crossings 13 -> 5, and no screen is a tall ribbon any more. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTUFNN5bH3aPw2gi2LqbYD * stuff --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7a23648db9 |
Merge remote-tracking branch 'origin/main'
# Conflicts: # CHANGELOG.md |