Commit graph

47 commits

Author SHA1 Message Date
Mick Cooney
fdbb64c920 fix(r): refuse a rename of a function that a roxygen2 @export tag exports
roxygen2 rebuilds NAMESPACE from @export* tags, so renaming a tagged function
changes the package's public API on the next document() even when the NAMESPACE on
disk and the persisted isExported say it is private. checkSafety now refuses a
top-level function whose directly preceding #' block has an @export* tag. Documents
that a successful rename leaves .Rmd/.qmd/.Rd/pkgdown/roxygen cross-reference
consumers unchanged; the rename result has no provider-owned note field.
2026-10-18 15:53:42 +01:00
Mick Cooney
1f7f7135f5 fix(r): load the R grammar lazily so a native load failure disables only R
parse-worker.ts and languages/r/query.ts imported @eagleoutice/tree-sitter-r
statically. A binding that cannot load (no prebuild, ABI mismatch) therefore
crashed every worker and every module importing the R provider, failing analyze
for all languages. The worker now loads it behind the same guard as the vendored
grammars, through the shared grammar loader, and the query module resolves it on
first use as the Kotlin one does. With the binding removed, analyze indexes the
other languages and skips R files with one logged error.
2026-10-18 15:51:58 +01:00
Mick Cooney
86ec528225 fix(r): make rename refuse when the NAMESPACE reader may have hidden an export
The NAMESPACE scanner skipped conditional (if/else) bodies, dropped raw-string and
multi-line arguments, and discarded a malformed directive together with everything up
to the next column-0 directive, all silently. Rename treated 'not in the export lists'
as proof of privacy, so it could rename a function whose export() entry the reader had
never seen and leave NAMESPACE naming a missing function.

The scanner now reports when a skipped or discarded region names an export-family
directive, and parseRNamespaceExports also flags an argument of one that it cannot
read. loadRPackageConfig stores that as RNamespaceInfo.exportsUnreadable and rename
refuses with incomplete_semantics. An ordinary NAMESPACE, including conditional
imports, does not set it. The indexed isExported is unchanged. r-provider.md updated.
2026-10-18 15:45:51 +01:00
Mick Cooney
7eaaec0384 fix(r): make rename re-read NAMESPACE and fail closed on a stale export flag
After an edit of NAMESPACE alone, the unchanged R/ files are not
re-processed, so the persisted isExported of a newly exported function
stays false and rename read that as proof of privacy. The rename guard now
also uses the NAMESPACE the planner has just loaded from disk (recorded by
the R workspace hook against each parsed file): it refuses when NAMESPACE
exports the name by export(), exportPattern() or S3method(), and fails
closed (incomplete_semantics) when NAMESPACE is gone or unreadable, when
an exportPattern() was dropped or ran out of budget, or when no config was
recorded. The persisted flag remains a second, additive check. The
incremental test now asserts the refusal on the stale index and after
--force; unit cases cover each way NAMESPACE can change. Docs updated.
2026-10-18 15:08:46 +01:00
Mick Cooney
342523965f fix(r): keep a repeated library() package at its first attachment
R leaves an already attached package where it is, so library(a);
library(b); library(a) still lets b mask a. The merger counted the last
call as a fresh attachment, and because the pipeline folds the bindings of
a name in one at a time, a package masked at one step was forgotten at the
next. Remember each package's first attachment, masked ones included, on
the surviving bindings, and flip the expectations that pinned the other
direction (A/B/A -> B, B/A/B -> A) in the unit and pipeline tests.
2026-10-18 14:39:28 +01:00
Mick Cooney
3cfd55a340 fix(r): say what is wrong with a rejected rename name
validateName let ... and ..N through to a misleading name_collision. Reject
them as reserved, say in the message that new names are ASCII-only by design,
and report an old name made only of non-ASCII characters as an unsupported
identifier rather than an infix function or operator.
2026-10-18 13:20:03 +01:00
Mick Cooney
587bf6ce85 fix(r): read a NULL default as a formal that may hold a function
A formal defaulting to NULL is the usual placeholder of an optional
callback, so classing it as constant let a rename edit the call of the
parameter and drew a false edge to a same-named top-level function. Treat it
as 'maybe': the rename refuses the call and the scope publishes the formal
as a lexical name. Update the provider guide and pin both paths.
2026-10-18 13:18:37 +01:00
Mick Cooney
af4ee3d9b4 fix(r): keep a matched export-pattern answer after the work budget is spent
rExportPatternMatches answered false for every name once the budget was
spent, so a duplicate definition of a name that had been exported earlier
flipped to private. Remember the names matched while the budget lasted and
answer them from that set; undecided names still read as unexported, which
the documentation now says.
2026-10-18 13:17:17 +01:00
Mick Cooney
e7fd13d364 docs(r): say which class owns a setMethod() written with a multi-class signature 2026-10-17 18:00:26 +01:00
Mick Cooney
d9f7ab87ac docs(r): list the call-resolution and class-handling gaps that are left as is
Each statement was reproduced against the code before it was written: self$m()
without a type binding, RefClass sibling calls, calls to generics and
generators, source() suffix matching, the library() edge fan-out, same-named
helpers in two packages, METHOD_OVERRIDES on parameter counts, calls inside
setMethod bodies, inherit = pkg::Base, string-named R6 members, nested function
ids, the TRE differences in export patterns and conditional NAMESPACE bodies.
Also says how a class name shared by several classes is owned and reported.
2026-10-17 18:00:12 +01:00
Mick Cooney
35edbb3515 fix(r): stop a call through a formal or loop variable at that name
A formal or a for variable bound no name in the scope model, so
function(data, transform) transform(data) was credited with a call of a
top-level transform() at 0.85, and the unique-name fallback made the same guess
across files.

Each function scope now publishes the names its formals and for loops bind as
lexical names, the shadowing barrier the scope model already has for a
parameter. A formal with a constant default that the body never assigns is left
out, because R skips a value that is not a function when it looks up a callee
(the rename provider reads it the same way).
2026-10-17 17:53:56 +01:00
Mick Cooney
32fb2973d8 fix(r): let the package attached last mask a name two library() calls define
Two library() calls that attach packages defining the same name bound the call
to whichever package file sorted first by id, so a script that attached lp2
after lp reached lp's function while R reaches lp2's.

rMergeBindings now keeps, within the wildcard tier, only the bindings of the
library() call that comes last in the file. A package of several files that
defines the name twice stays ambiguous, and a binding with no call position
leaves the tier as it was.
2026-10-17 17:43:52 +01:00
Mick Cooney
d3e06d373d fix(r): refuse a closure-local function as the unique-name fallback target
A function bound inside another function lives in that call's frame, so no
other function in its file or another can call it by its bare name. The
unique-name fallback ignored that: when the name was unique in the repository,
a bare call elsewhere bound to the closure-local function at 0.5.

The R fallback veto now refuses a function or class that the file's scope
model holds below the top level. A candidate the scope model does not hold, and
every top-level definition, are judged as before.
2026-10-17 17:39:52 +01:00
Mick Cooney
055466b2ed fix(r): keep installed libraries and ignored trees out of package discovery
Discovery read the disk on its own and skipped only renv, packrat, revdep and
.Rcheck, so an installed library under lib/, library/, .Rlibs/, vendor/ or
build/ registered its packages as repository packages (their qualified calls
then fell to the name-based guess instead of reading as external), and a tree
of ignored directories spent the 5,000-directory hidden-package budget and
made discovery read as incomplete.

Discovery now skips dot-directories and the directories the indexer itself
ignores (isHardcodedIgnoredDirectory), and a DESCRIPTION with a Built: field,
which only an installed package has, registers nothing and hides nothing. The
depth-3 and 200-directory bounds are pinned with their boundary values.
2026-10-17 17:30:04 +01:00
Mick Cooney
f52571aac8 fix(r): name an S3method() of another package's generic after the generic
S3method(pkg::generic, class), the delayed-registration form roxygen2 writes
for @exportS3Method pkg::generic, registered no method at all when written
bare and the nonexistent "pkg::generic.class" when quoted, so the function
generic.class read as not exported and an importFrom() of it did not bind.

The generic is the text after the last :: or ::: outside quotes and
backticks, so the method is generic.class as R registers it.
2026-10-17 17:27:35 +01:00
Mick Cooney
1ce05069e9 fix(r): charge export patterns that are refused after their NFA was built
A pattern that expanded past the state cap was refused only after building up
to 16,384 states, yet it reserved no states, charged no work and did not close
the allowance, so the next such pattern started from scratch: 98 KB of
exportPattern("(a|b){8000}...") lines took 4.2 s while the budget counted
nothing, against the documented bound of about a second.

A refusal is now charged for what was built (eight visits per state or build
step), and a spent work budget refuses later patterns before they are parsed.
The guide also says that budget exhaustion while resolving importFrom() names
is not reported, because the scope resolver loads its own configuration.
2026-10-17 17:26:24 +01:00
Mick Cooney
9944a7d969 fix(r): charge a remembered export-pattern verdict to the work budget
A matcher answered a name it had already decided from its memo without any
charge, so asking thousands of matchers about the same few names (initialize,
print, show across many classes) cost time that no budget counted: 5,000
repeat calls over 12,000 matchers took 612 ms and charged nothing.

A memo hit now costs one visit, so the repeat is bounded by the shared budget
like every other cost, and the verdict itself is unchanged.
2026-10-17 17:26:00 +01:00
Mick Cooney
7808a1bdb4 fix(r): make the export-pattern nesting limit independent of JIT warm-up
MAX_NESTING was 2048, beyond what a cold process can recurse: a deeply nested
pattern failed with a stack overflow in a fresh process and compiled after a
warm-up, so whether a NAMESPACE pattern was in effect depended on the machine
and on what ran before it, and the reason given was misleading.

The limit is now 256, which still compiles on a quarter of the default stack,
so the verdict is the same cold or warm and carries the right reason.
2026-10-17 17:25:25 +01:00
Mick Cooney
7730f6df3d docs(r): list the final rename refusals and the PDG answers R cannot give
The rename section now names each refusal the provider makes (NAMESPACE export
read from the indexed flag, infix functions and operators, replacement functions,
new-name collisions and value reads, group and common generics). The control-flow
section records that pdg_query flows, explain and impact --mode pdg return empty
answers for R without saying that R has no def/use harvest; the shared tools carry
no per-language capability signal, so this is documented rather than patched.
2026-10-17 17:03:59 +01:00
Mick Cooney
06ae82c4e3 docs(r): say that NAMESPACE refines graph nodes, not scope definitions
refineRExportStatus flips isExported on graph nodes only. The scope
definitions that finalization reads carry no export verdict for R
(undefined means unknown), which is deliberate: library() attaches every
top-level name, as devtools::load_all() does. State that in the provider
guide and pin it with a test, so a later change that stamps a verdict
onto scope definitions is a conscious one.
2026-10-17 14:55:58 +01:00
Mick Cooney
67bf23580f docs(r): point parser-change guidance at the automatic cache identity 2026-10-17 12:22:14 +01:00
Mick Cooney
8997970f2c test(r): pin the cross-file and R6 method rename refusals
The guide documents ambiguous_reference for an unqualified call into
another file and a refusal for R6 methods, but no fixture covered
either. Add both. The R6 method is refused by the provider as a class
member (unsupported_occurrence); the guide wrongly listed it among the
shared engine's stale_graph refusals, which now name only dotted
functions and S4 classes.
2026-10-16 17:56:12 +01:00
Mick Cooney
b25af72364 docs: count 19 providers in the local import scoping guide
The guide said 18-provider registry. The providers table in
languages/index.ts has 19 entries with R, one per SupportedLanguages
value (counted from the built registry: Object.keys(providers).length).
2026-10-16 17:50:58 +01:00
Mick Cooney
755278a040 fix(r): rename a call that a non-function parameter of the same name does not shadow
R skips a binding that is not a function when it looks up the callee of
f(x), but the rename provider claimed every parameter, so a call under
function(x, helper = 2) was left unrenamed and failed after the rename.

A parameter whose default is a constant and which the body never assigns
is no longer claimed, so the call is renamed with the function it
reaches. A parameter that may hold a function (a function or computed
default, or an assignment to it in the body) is claimed as before, and
a call of the name under it is refused as ambiguous_reference, since it
may reach either. A parameter without a default that is called stays a
function-valued parameter and its call is left alone.
2026-10-16 17:48:25 +01:00
Mick Cooney
6355538947 fix(r): refuse renaming a function that is also assigned with <<-
A <<- inside a function is a global re-definition that the scope graph
keeps as a second declaration of the same name. A rename dropped the
selected function and its calls but left the superassignment, so after
the rename the function wrote a new global of the old name instead of
replacing the renamed one. The top-level form was already refused by
the engine; checkSafety now refuses any <<- or ->> to the name, in or
out of a function, with the same ambiguous_reference code.
2026-10-16 17:43:08 +01:00
Mick Cooney
85fb8aeb55 fix(r): refuse a rename target name that registers an S3 method at any dot
The guard looked only at the text before the first dot, so
to.json.foo (a method of the dotted generic to.json) passed, and base
generics such as print, format, summary and plot were never checked
unless the sources defined them. Every dot-separated prefix of the new
name is now tested against the functions the indexed sources define and
a small list of base generics.
2026-10-16 17:41:01 +01:00
Mick Cooney
154b7d4ebb fix(r): bind importFrom() of a backtick-quoted definition
A function written in backticks keeps them in its binding key and on its
node, while NAMESPACE entries and importFrom() carry the bare symbol, so
importFrom(pkg, `%||%`) found no defining file and the call degraded to
the name-based guess, which cannot tell two packages' `%||%` apart.

rFileTopLevel now keys the top-level names by symbol (rBareName) and
records the spellings it saw. resolveRNamedImport looks files up by
symbol, populateRNamespaceImports synthesises the import with the
definition's own spelling (one per spelling the provider uses) and lets
a backtick-defined function of the caller package mask its import. The
global-name-fallback veto and the pkg::name() lookups compare symbols.
The dotted-tail guard still compares the name as written, which is how
finalize indexes it. Names without backticks answer exactly as before,
pinned by a characterisation snapshot recorded before the change.
2026-10-16 17:36:39 +01:00
Mick Cooney
28ae726097 docs(r): state how backtick-quoted names resolve
A call and its definition must spell a backticked name alike to be linked,
and importFrom() of a backtick-defined function does not bind. NAMESPACE
exports and rename match the symbol either way.
2026-10-16 17:21:02 +01:00
Mick Cooney
33d411c50c docs(r): list exportClassPattern() as not read
The directive is not tokenised, so a class exported only through it reads as
not exported. Say so under the NAMESPACE limits.
2026-10-16 17:17:49 +01:00
Mick Cooney
e45fa2e080 fix(r): read slots and fields given as c(...) vectors
The slot pattern matched only list(...) and representation(...), although
the field extractor accepts any call, so setClass("A", slots = c(a =
"numeric")) and setRefClass("R", fields = c(...)) created no Property nodes.

Accept c(...) for both. An unnamed string in slots = c("a", "b") or
fields = c("a", "b") names an untyped member, as in R; an unnamed string in
representation(...) or list(...) is still not a member. The new pattern
captures its callee as @_vecOwner, so the class-naming capture filter does
not mistake these strings for the class name.
2026-10-16 15:52:26 +01:00
Mick Cooney
fc32e53967 fix(r): qualify R6, RefClass and S4 member ids with their class
Member ids carried no owner, so a second class in one file that shared a
member name with an earlier one (push, items, a setMethod generic, a slot)
minted the same id as the first and lost the member, along with its
HAS_METHOD or HAS_PROPERTY edge.

Register resolveDetachedOwnerName for R. It names the class declared by an
ancestor R6Class assignment, setClass or setRefClass call, or the signature
class of a setMethod call, so ids become Method:<file>:Stack.push#n and
Property:<file>:Stack.items. The owner edge is still attached by postParse
through ownerNameHint. The change is R-only: the hook is optional and
undefined for every other language.

The r-packages characterisation counts now include Child.greet, the member
the id collision used to drop.
2026-10-16 15:43:34 +01:00
Mick Cooney
c6c86a247e docs(r): correct the R provider guide and the type-resolution matrix
r-provider.md: state that an empty or missing if arm is the header's seq
edge (control dependence labels it as the complement of the other arm);
extend the bare-callee limit to library(), require() and source(); say that
NAMESPACE raw strings and strings spanning lines are dropped without a
warning, which the logged-warning sentence did not cover; name the language
registrations that do mention R; give the devtools::load_all() rationale for
the unfiltered library() wildcard.

type-resolution-system.md: R binds no declaration, call-result or
method-call-result types (extractDeclaration is empty and there is no
extractPendingAssignment), and its cross-file propagation is partial, with a
test cited. Drop R from the footnote about synthesizeWildcardImportBindings(),
which does not exist.
2026-10-16 15:03:54 +01:00
Mick Cooney
abcc165088 fix(r): decode string literals in the rename dynamic-lookup guard
The R rename guard compared the raw text of each `string_content` node with the
old and new name. An escaped spelling (`"f\u006fo"`, `'f\x6fo'`, octal) keeps
its escape inside string_content, and a raw string (`r"(foo)"`) has no
string_content node at all, so renaming `foo` proceeded while a get() or
do.call() on the escaped or raw spelling still resolved the old name.

Reproduction: 10 cases in the new test/unit/r-rename-string-guard.test.ts
returned 'ok' instead of unsupported_occurrence on the unfixed provider, and the
two new fixtures string-lookup-escaped and string-lookup-raw (rename-provider-r)
renamed successfully on the unfixed build. The guard now decodes each `string`
node (escapes, octal, raw strings) and compares the value.

docs/languages/r-provider.md said "spelled as a string literal"; it now states
the whole-value rule and what is still not detected (substring, paste0(),
non-indexed files).
2026-10-15 20:42:05 +01:00
Mick Cooney
9c2c3ed595 docs(r): document that namespace-qualified S4 constructors are not recognised
Review finding: `methods::setClass(...)` is ignored by the S4 field detector.
Confirmed by test, and wider than fields: every S4/RefClass pattern
(setClass, setRefClass, setGeneric, setMethod in R_QUERIES, plus the field and
method extractors) requires a bare identifier callee, so a namespace_operator
callee creates no class, slot, generic or method. R6::R6Class is supported.

Widening touches nine files (queries, naming-argument, captures, owner hooks,
post-parse, scope resolver, both extractors) and the qualifier-veto logic, so
it is not a trivial change. Documented as a known limit in
docs/languages/r-provider.md and pinned by characterisation tests in
r-definition-anchors.test.ts (each paired with its unqualified control).
2026-10-15 20:33:36 +01:00
Mick Cooney
3dbf1017ce feat(r): add conservative semantic rename provider
Plain R functions rename through the scope graph; S3, string-spelled,
S4, R6 and NAMESPACE-exported names are refused. Adds fixtures, the
rename-provider-r test and its cross-platform shard entries.
2026-10-15 15:43:32 +01:00
Mick Cooney
56d697256c docs(r): document the R control-flow visitor and its coverage gaps 2026-10-15 15:43:15 +01:00
Mick Cooney
b191eb5dd4 docs(r): document leading-] bracket expression limit 2026-10-12 10:53:26 +01:00
Mick Cooney
75eaac750c fix(r): skip importFrom() imports that can never bind
populateRNamespaceImports synthesised a named import for every
importFrom(localpkg, name). Names the provider does not export, does not
define at top level, spells with a dot, or defines beside a dotted S3
method of the same tail never resolve, yet the unresolved import counted as
an explicit name claim and suppressed the documented 0.5-confidence
global-name-fallback edge.

Skip the import when resolveRNamedImport returns null, so such names create
no phantom claim and their calls keep the fallback.
2026-10-11 19:49:26 +01:00
Mick Cooney
0d4e2b36ca Merge upstream/main into feat/r-language-support
Upstream took SCHEMA_BUMP 133 to 137, so the R provider bump moves to 138.
The pin test and the taken-versions list are updated to match. The import-target
benchmark keeps both the R and Rust arms in the heap-budgeted and context
language lists, and the tree-sitter readiness tests keep the R grammar checks
against the reworked report (R is now a peer-patch grammar and the held-grammar
counts changed).
2026-10-11 08:50:04 +01:00
Gergő Magyar
82347f0f07
fix(scope): resolve local imports by lexical ownership (#3532) 2026-10-10 08:13:55 +03:00
Mick Cooney
ec3c6737e8 docs(r): record partial argument names, positional members and named S3method method as known gaps 2026-10-08 18:19:55 +01:00
Mick Cooney
3f786ef94a fix(r): export the function named by the third S3method() argument
S3method(generic, class, method) registers method, not generic.class, as the S3
method. parseRNamespaceExports ignored the third argument and so marked a
function called generic.class public while the registered method stayed
non-exported. Use the third argument when it is a plain name, else generic.class.
2026-10-08 17:56:20 +01:00
Mick Cooney
4dcc13e168 fix(r): let a later Class= take the formal from an earlier unnamed argument
R matches named arguments first, so in setClass(a = "Q", "B", Class = "A") the
class is A. The naming-argument scan stopped at the first unnamed argument and
named both B and A. Keep scanning the list, once and cached, and drop the choice
when an argument spelled with the formal appears. Replaces the pinned known-limit
test and the known-limit wording in the file header and the provider guide.
2026-10-08 17:55:51 +01:00
Mick Cooney
95eee83e54 docs(r): correct the manifest-edit staleness note and describe the export-pattern work bounds 2026-10-04 02:22:03 +01:00
Mick Cooney
51ed2c2a33 docs(r): move the R provider guide to docs/languages/r-provider.md 2026-10-03 23:02:40 +01:00
Gergő Magyar
274ec3df6e
feat: index Jupyter notebooks as Python (#3381) 2026-09-27 05:54:37 +00:00
mengkaka
4154b63131
feat(indexing): add Objective-C semantic indexing support (#3179)
* docs: add Objective-C fork provider notes

* feat(objective-c): add deterministic provider and grammar

* feat(objective-c): finalize provider MVP

* fix(objective-c): harden provider integration

* fix(objective-c): normalize bare macro markers

* docs(objective-c): integrate provider documentation

* fix(objective-c): harden resolution and header classification

* fix(objective-c): complete provider follow-ups

* fix: address Objective-C review follow-ups

* chore: format Objective-C grammar sources

* fix(objective-c): harden review follow-ups

* Address PR review feedback (#3179)

Keep Objective-C chunking and macro recovery aligned with the grammar, and stop Community MEMBER_OF edges from leaking into symbol context.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address follow-up review on ObjC chunking and language fallback.

Keep preprocessor directive text from changing file-scope brace depth, group real ivar nodes, skip header modifiers, and restore Rakefile/Gemfile detection through getLanguageFromFilename.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Parse Objective-C headers with the objc grammar in embeddings.

ensureAndParse and structural extraction now use the same content classifier as ingest, including method snippets from .h files, so Protocol/Category/Class chunks are not re-parsed as C++.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Keep file-scope macro elision off C line splices and @interface/@protocol/@implementation bodies, and attach ivar attributes to the following instance variable when chunking.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(bench): rebaseline Objective-C CSV emit

* feat(objective-c): add workspace resolution and linear emit benches

Plain .h files are classified as C++, so the ObjC pass could not
resolve #import of those headers. Load a C/C#-style workspace once
per pass, and keep protocol-candidate USES linear.

Refs #3179

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

- Compare LadybugDB labels() as a scalar when excluding Community MEMBER_OF edges.
- Walk superclass members, skip file-static C sibling defs, and ignore comments in ObjC header/macro scans.

Note: pre-existing failure in objective-c-provider integration (worker-pool ready timeout) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Emit Objective-C declaration captures so compilation-unit siblings can share
header/implementation bindings, and keep class vs protocol visibility groups
distinct.

Note: pre-existing failure in worker-pool startup (GITNEXUS_WORKER_READY_TIMEOUT_MS) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Emit every comma-separated property/ivar declarator, and count @interface
after a multiline block comment closes so in-declaration macros stay intact.

Note: pre-existing failure in worker-pool startup (GITNEXUS_WORKER_READY_TIMEOUT_MS) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: ximengkai <ximengkai@soyoung.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 09:40:21 +00:00