Review round 2 of #2677 (N1-N12). Every case ends in an explicit unknown,
never in an OOM, a quadratic stall or a wrong image.
Bounds (helm_template.c, pass_k8s.c):
- N1: capture arrays grow by doubling, so n captures cost memory linear in
n. Arena growth since the render began counts against a third budget
(256 MiB) beside steps and output; a trip is logged with its reason.
- N10: poisoned maps are a pointer hash set (merged values a path list), so
a loop poisoning a new dict per iteration keeps reads O(1). Release images
are deduplicated by hash.
Wrong images (helm_template.c):
- N2: a set / merge in a body an UNKNOWN condition skips is judged by its
target whatever it is -- a call's result like `index .Values "image"` is
evaluated in the frame: own, or a known map (poisoned), else it taints.
- N3: putting a value another part holds into an own map revokes own; a
part of a poisoned map is never own.
- N4: a run-time set whose target is an UNKNOWN call result taints.
- N5: an UNKNOWN break / continue spoils the rest of the loop body around
it; a continue is a continue, not a break.
- N6: the scan notes redefinitions and assignments, carries what it saw
across the actions of one branch, and never judges a variable the part
rebinds by its value in the frame.
- N11: a define's "may not change" that leaned on a define still being
scanned (mutual includes) is not kept.
- N12: an include not run because its argument is poisoned or UNKNOWN
spoils that argument (poisoned deep, or a taint).
Releases and charts (pass_k8s.c, pass_terraform.c):
- N8: an empty values.yaml is values {} (Helm's), not a file not read.
- N9: a Terraform set_sensitive whose name overlaps a key an image reads, a
Terraform postrender, or a kustomization with its own transformers
(images, patches, replacements, transformers, components) leaves the
release without an image of its own.
Tests (N7): the symlink probes are POSIX-only (SKIP_PLATFORM on Windows).
One test per item, RED on 4cea54f7 by its assertion; N10's is a correctness
control, its cost measured (linear vs quadratic) outside the suite.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Ordinary attribute lookup walked the bases one at a time, each through its
whole ancestry, so on a diamond self.m() and e.m() reached the shared base
before a later base that overrides it. It now follows the class's C3 order,
the order super() uses, after the class's own members; a hierarchy without
a C3 order keeps the depth-first walk.
Each class is linearized once per file: the orders live in a per-file,
arena-allocated open-addressing cache keyed by class QN, which also replaces
the per-lookup cache of the super() resolver. The registry is complete
before the walk starts, so an entry never goes stale; keys and the class
entry of an order are arena copies. Linearizing a class costs one merge of
its bases' cached orders, so no lookup re-linearizes a deep hierarchy.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The radarr diamond seen from an ordinary attribute: self.write_state() in
RadarrCalendarEntity and e.write_state() on a RadarrCalendarEntity look the
method up along the class's C3 order, which reaches CalendarEntity before
the shared base Entity. Single inheritance as the control; sequential and
parallel.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
super() binds to the enclosing class's method resolution order, Python's C3
linearization, in which a common base follows every class deriving from it.
The super() resolver walked the bases one by one, each through its whole
ancestry, so with RadarrCalendarEntity(RadarrEntity, CalendarEntity) the
first base reached Entity through CoordinatorEntity before CalendarEntity,
which overrides it, was ever looked at.
py_c3_linearize computes the order (memoized per lookup, capped in depth and
length; -1 when the hierarchy has none), and super() takes the first class
after the enclosing one that declares the member. A hierarchy without a C3
order keeps the base-by-base walk, as does the synthesized __init__ of an
undeclared constructor.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The home-assistant radarr shape: RadarrCalendarEntity(RadarrEntity,
CalendarEntity) where RadarrEntity reaches the common base Entity through
CoordinatorEntity and CalendarEntity overrides Entity. In C3 order a common
base comes after every class deriving from it, so super().write_state() and
super().__init__() reach CalendarEntity, not Entity through the first base's
ancestry. Single inheritance as the control; sequential and parallel.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
augment() is awaited inline in 'tool.execute.after', so a hook-augment
child that never exits blocked every grep/glob/read tool result in the
host OpenCode session. The emitted helper now:
- kills the child (SIGKILL) and resolves '' after AUGMENT_TIMEOUT_MS
(10000ms, a backstop above hook-augment's own in-process deadline,
which is clamped to 10000ms, so the child's graceful self-exit and
its hook-augment-timeouts.log breadcrumb always fire first)
- listens for child.stdin 'error' and guards stdin.end(), because stream
errors fire on the stream, not the child, and previously escaped as
unhandled 'error' events (EPIPE with a >64KB buffered write, ENOENT
spawn), crashing the host under Bun 1.4.0
- destroys stdout in the timeout path to release the read end promptly
Fixes#2693
Signed-off-by: Lance Woodson <65194841+woodsonl@users.noreply.github.com>
Python evaluates a class statement's bases in the body that encloses it, so
a bare base of a class nested in a class names an earlier sibling of that
body (sqlalchemy's asyncpg adapter: class IntegrityError(DatabaseError))
before any same-named module or imported class. The base resolvers ran the
registry's name strategies instead, which prefer an import-reachable
candidate, so the sibling lost as soon as the adapter's exc import resolved.
cbm_registry_resolve_python_class_body_base looks the sibling up exactly
(<enclosing class>.<base>, both type-like) and requires it to be defined
earlier in the same file, from the graph-buffer node lines: a later sibling
is not bound yet when the base is evaluated. The sequential and incremental
semantic pass, the parallel resolver and the cross-LSP base resolver ask it
first; a sibling needs no external-base gate.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
sqlalchemy's asyncpg adapter imports its exc module; once that import
resolves, name guesses prefer exc's same-named classes over the earlier
sibling of the class body. The fixture now imports exc as a module too.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The sqlalchemy asyncpg adapter re-declares the DBAPI exception hierarchy as
classes nested in one class body, each based on an earlier sibling spelled
bare (class IntegrityError(DatabaseError)). Python evaluates the bases in the
enclosing class body, so the sibling wins over same-named module classes and
over another adapter's nested namesakes; a sibling defined later is not bound
yet. Sequential and parallel pipeline, each followed by an incremental
re-index.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
main's Go doc-link tests were written against flat Go method QNs, where
`(A) Close` and `(B) Close` shared one node `alpha.Close`. That is the
fused-node defect #1913 removes. The expectations move to the new scheme,
and each changed assertion pins an edge onto, or a count of, the
receiver-qualified node (production: the previous commit and #1913):
- sources: RefM's source is p.pkg.S.M, not p.pkg.M.
- forms: [Widget.Run] lands on alpha.Widget.Run, [beta.Thing.Do] on
beta.Thing.Do.
- plain_text, struct_fields: Run's doc is the source alpha.Widget.Run.
- lookup_order: two alpha.Run sources and beta.Promoted named no node
any more, so they held vacuously. They are now alpha.Widget.Run and
beta.Embedded.Promoted. The alias links land on beta.Thing.Do and
alpha.Deep10.End.
- node_owner, the positive contract that replaces the fused case:
- two Method nodes, alpha.A.Close and alpha.B.Close, and none named
alpha.Close;
- [A.Close] and [B.Close] are edges, not graph_gap rows;
- each method's own [Target] is an edge from its own doc line (9 and
12), with no unresolved row;
- the type Open keeps alpha.Open: [Open] is the type (no graph_gap),
[B.Open] is the method, and UsesOpen has 2 targets.
- scope_blob:
- method records carry w `-`;
- swapping two methods of one name no longer changes the blob;
- a func/type pair swap still does (new case);
- the k1-k3 records follow (a method neither keeps nor contests a
name);
- a new k4 keeps the "two on one line contest it" rule covered with a
var and a const.
- incremental_equals_full:
- [b.T.M], [b.W.Name] and [b.V.Name] all resolve, and U.M's [Target]
is an edge;
- removing T.M breaks only [b.T.M];
- swapping V.Name and W.Name moves nothing and is now an ordinary
closure repair instead of a forced full rebuild.
- scope_delta_rules:
- a method/function swap is LOCAL;
- removing the method of such a pair is LOCAL with the name "Open" (a
member left);
- new func/type cases keep the GLOBAL keeper-change rules covered for
swap and removal.
- workers_agree: [p01.T3.Close] now resolves (exact edge, no row). The
graph_gap reason stays covered by the named Shared assertion: one
function declared by every file of a package, which is still a gap.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
#1913 makes Go method nodes receiver-qualified (<dir>.<Recv>.<Method>).
The Go doc-link model still let every method claim the flat node
<dir>.<name>:
- doclink_go.c go_scan_method pushed a claim record;
- doc_links_go.c go_add_claim put it in the flat table.
So a type, function or value that shares its name with a method in its
directory became a graph_gap ("lost its node to the method"), although
the graph now gives it that node. Base reports the same graph_gap for
this shape, because there the method really did take the node. So this
completes #1913's QN scheme for one of its consumers; it is not a
regression #1913 causes.
- A method record now carries w `-`: it claims no flat name.
- The resolver no longer enters methods into the flat table. Methods are
found through their receiver (the member table), which was already the
first lookup.
- The flat fallback in go_method_node is now unreachable. It stays, with
a comment saying so.
Corpus, before = this branch's tree, par x2, r1 == r2:
- terraform: graph_gap rows 9 -> 0, doc-link MENTIONS 528 -> 536 (+8,
-0). All 8 new edges were judged correct: e.g. [AbsProviderConfig]
now lands on the type, not on the method
RootProviderConfig.AbsProviderConfig.
- vault, prometheus: unchanged.
- prometheus incremental == full for MENTIONS.
- The 3 -> 4 index-format bump already forces a full rebuild, so no
stored scope blob mixes the two meanings.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A PHP file's import evidence is written as complete only when every `use`
either resolved or is proven absent. A global-namespace class use such as
`use Exception;` had no absence proof, so one such line left the whole
file's bindings unknown, and its project imports reached the PHP LSP as
File QNs again (the #2520 path).
An unaliased global class use is now proven absent when no PHP file
declares a class-like symbol of that name (Class, Interface, Trait, Enum,
File or Module; namespaced ones included, so the proof stays
conservative). Aliased global uses keep the old, unknown verdict.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Review round 1 of #2677. Every case ends in an explicit unknown, never in a
hang, an OOM, a partial or a wrong image.
Bounds (helm_template.c):
- A per-render step and output budget (10,000,000 steps, 64 MiB): a range
over a huge int, a huge indent, defines that include each other in a fan
stop the render; every probe of the template is UNKNOWN, also one captured
before, and the trip is logged (helm.render.budget). Counted, never timed.
On the 20 infra corpora no render reaches it.
- go-yaml v2's alias limit: values whose decodes are mostly alias expansion
(a billion laughs) do not parse, as in Helm.
- --set: an index past strvals' MaxIndex (65536) or a key longer than the
parser holds is no value (the release is unknown), never a padded list or
a split key.
Reads (pass_k8s.c, pass_terraform.c): a values file, Chart.yaml, kustomize
valuesFile or Terraform file() named by path is read only when it is a
discovered file of the repository -- never through a symlink out of it --
with the size cap and tree-sitter padding of every other read. A chart file
that is there but not read makes the chart's values unknown, not empty.
Wrong images:
- Terraform number sets reach the chart as Terraform's exact decimal
(1.10 -> "1.1"), not %.17g.
- A release with a value source not read -- Flux valuesFrom, valuesFiles,
postRenderers; kustomize additionalValuesFiles, a valuesMerge other than
override, a valuesFile not read; Terraform dynamic "set" -- has no image of
its own.
- An aliased subchart renders with .Chart.Name its alias.
- An UNKNOWN if / with / range makes what its bodies assign UNKNOWN and
leaves the loop a break in them would; set / unset / merge poison the map
they change (merge: the maps within too). A change that may reach values
another part holds taints every later capture; one of a new value
(fromYaml, deepCopy, an empty dict, a part of one) does not.
Marks and secrets:
- The chart edge reads default_of as JSON (list paths kept), builds its
properties to size, and writes no edge whose mark it could not carry whole.
- Build arguments with a secret (by name or value) are never kept; build
edge properties and values-path marks are never cut.
Tests: one per bound and per wrong-image case, each RED on d21949ef by its
assertion; a Helm chart joins the Kubernetes complexity fixture.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
py_process_class qualified every class under the module, so a class nested
in a class (proj.m.Outer.Inner, the QN extraction gives it) was walked as
proj.m.Inner: its methods' LSP rows carried callers no node has and never
joined. A typed-parameter call in such a method got no edge at all, a self
call fell to unique_name and a constructor to same_module. A nested class,
decorated or not, is now walked under its enclosing class through the same
py_process_class_in the function-local classes use.
Tests: pipeline_python_nested_class_methods_resolve(_parallel).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
RED on F11a alone: py_process_class walks a nested class under the module, so none of its LSP rows join the method nodes.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
push_nested_class_nodes descended into field/template/declaration and conditional wrappers but not into Python's decorated_definition, so a decorated nested class (`@dataclass class Meta:` inside a class) was never extracted: no Class node, no Method nodes, and the calls in it fell to the FILE node.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A route with 70 callers must link all 70 to its handler, a route whose
INFRA_MAPS endpoint has 40 handlers must link its caller to all 40, and
the flow set must not change when the callers' HTTP_CALLS edges are
inserted in reverse order. Before the fix the first 64 callers and the
first 32 infra handlers in insertion order were kept, so all three fail.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
create_data_flows copied a route's callers (HTTP_CALLS + ASYNC_CALLS)
into a 64-slot buffer and its INFRA_MAPS handlers into a 32-slot one,
each keeping the first entries in edge-insertion order. In a parallel
index that order is worker interleaving, so which DATA_FLOWS existed
changed between two indexes of the same tree with the same binary
(thingsboard, 2026-10-17), and every caller past the 64th lost its flow
outright.
The pass now walks the graph buffer's own edge arrays and creates the
full caller x handler product per route. A (caller, handler) pair that
is reached twice keeps cbm_gbuf_insert_edge's order-free property
winner, so the flow set and its properties no longer depend on the
order edges were inserted in.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A definition's properties buffer was sized for its docstring, variants and
variant_key only; every other field had to share the remaining 2 KB. Fields
that do not fit are dropped whole, so a long signature, decorator list or
parameter list silently squeezed out the MinHash fingerprint (fp), the
structural profile (sp) and the body tokens (bt) appended behind them. Those
definitions got no SIMILAR_TO edge and fewer SEMANTICALLY_RELATED edges, and
any newly added property (route_path, route_method) pushed more of them out.
pd_props_buf and pp_props_buf (twins, kept in lockstep) now count the fixed
metrics prefix plus the whole serialized form of every field
build_def_props appends, including the reserve the fp/sp/bt room checks
use. Definitions that fit keep the stack buffer.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A coverage map must cover every function it lists, and five of the seven
variants covered only function 1, so the map failed to parse and the test
stopped at its setup ("map is NULL") with the fix applied or not. The
changed function (0) is now covered by the setup row of a suite no runner
registers, so alpha's own rows stay quiet and each variant still reaches
alpha by exactly one path. RED with the selection fix reverted (alpha
lacks CONDITIONAL), green with it.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The parallel resolver re-picks a multi-candidate registry match through
try_field_type_hint. That helper capitalizes the receiver VARIABLE name
and accepts any candidate whose QN contains it as a substring: `t.Run`
on a *testing.T became `T` and bound a project `TBench.Run`. The
sequential resolver has no such step. The Go selector guard treated
field_type_hint as receiver-aware and kept it, so the same weak pick it
drops in sequential mode survived in parallel mode under a new name.
On caddy + prometheus, parallel and sequential CALLS diverged by 1,684
(this branch) and 5,560 (with receiver-qualified method QNs, which put
type names into more QNs). At base the gap is 84. SCIP precision of Go
field_type_hint is 0.016 at base.
Go no longer takes the hint. The guard keeps field_type_hint on the
shared weak list, and the fixture of both twins now pins the
`t.Run` case. A typed field call (lrp_go_s8, `s.repo.Find()`) resolves
through the Go LSP and is unaffected.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>