* refactor(process): own deadlines as one monotonic type with a finalize budget
Deadline (src/domain/deadline) holds a monotonic work budget and a separate finalize budget that starts when finalization begins. ProviderStartupDeadline is built on it and exposes finalize(), so a failed Ghidra startup drains launcher output under its own budget and the caller's signal instead of the expired startup signal. The six wall-clock deadline sums in src move onto Deadline; a counted guard keeps the one page-clock conversion and rejects new copies.
* refactor(process): share one finalize window across repeated beginFinalize calls
* refactor(application): own the finalization precedence in runOwned
runOwned runs owned work, closes its owner and applies the documented
precedence once: a recorded failure stays primary, cancellation before
commit keeps the completed value as a partial observation, cancellation
after commit keeps the value, and unconfirmed cleanup reports
cleanup_incomplete while keeping the completed value. finishAnalysisCommand
now projects its Finalized outcome; its 31 callers are unchanged.
* fix(application): preserve finalization outcomes
* fix(application): classify finalization by the primary error and keep cancellation through recovered publication
A producer composite whose primary is a cancellation finalized as failed because the kind read only the top-level error class; it now follows primaryAnalysisError. A cancellation that arrived while a recovered value's publication failed was dropped because the signal was read before that last await; it is now attached to the publication failure as a secondary.
* fix(cli): keep the initial failure snapshot whenever close itself succeeded
* refactor(errors): require a reason for every analysis cancellation
Add cancelledBy, the one constructor for AnalysisCancelledError. Its
reason argument is required: an aborted signal, an earlier cancellation,
a reason carried from an inner failure, or an OwnerCancellation when REA
closed the work's owner itself.
Closed adb, apktool, firmware, sqlite, gdb, web-bundle analyzer and
binary-session record owners now report OwnerCancellation.ownerClosed
instead of an absent reason. Firmware's predecessor wait returns its
cancellation as a Result, removing an unreachable reason-less fallback.
An architecture guard rejects new direct constructions, with a counted
per-file baseline that can only shrink.
* refactor(errors): share one aborted-signal predicate with cancelledBy
* refactor(process): return a departing caller's reason from a shared startup wait
* refactor(providers): build decompiler-provider cancellations with a required reason (#10587)
Migrate every AnalysisCancelledError construction under src/ghidra,
src/ida, src/cutter, src/jeb and src/kuna to cancelledBy and lower the
guard baseline from 671 to 572. Closed Kuna target commands report
OwnerCancellation.ownerClosed instead of an absent reason, and Kuna and
JEB close abort their lifetimes with that typed reason.
* fix(errors): read the reason of any signal-shaped cancellation source
* test(ci): cover the counted-baseline comparison behind the architecture guard
* refactor(hopper): build Hopper cancellations with a required reason
Migrate the remaining AnalysisCancelledError constructions under
src/hopper to cancelledBy. Regex-search close aborts its workers with
OwnerCancellation.ownerClosed. HopperCleanup and HopperClient are not
touched.
* test(hopper): cover cancellation during search shutdown
* refactor(parsers): report format failures through one ParseFailure algebra
Add ParseFailure (malformed, truncated, unsupported, limit, with format,
offset and field) and its one carrier, ParseFailureError, in the domain.
BinaryReader now names truncation and the field it read, and its native
format and resource failures are ParseFailureError subclasses.
Providers projected a format failure with their own instanceof ladder,
and some dropped the selected path from the invalid_format message.
parseFailureAnalysisError now owns that projection for the native,
Go, devicetree, V8, Windows Shortcut and Electron fuse providers.
The V8 code cache, Windows Shortcut and Electron fuse format classes
are removed in favour of the algebra.
The architecture guard counts raw Buffer and DataView reads, plain
Error throws in parser folders and plain JSON.parse of parser input
against baselines that can only shrink.
* fix(parsers): stop reading every RangeError as a format failure
Mach-O, Swift and Objective-C metadata parsers threw RangeError for a
truncated or malformed record, so their catch sites treated any
RangeError as a partial decode: a call stack overflow or a host string
limit became an "unresolved" metadata reason, and the IL2CPP Mach-O
path reported it as a malformed container.
Those parsers and the CIL method body reader now throw ParseFailureError.
hostRangeErrorKind is the one interpreter of a host RangeError (stack
overflow, host capacity, raw out-of-bounds read), and
parseFailureFromCause admits only a parse failure or a raw
out-of-bounds read as evidence about the input. Fixup stream catch-alls
rethrow host limits. The architecture guard counts the remaining direct
instanceof RangeError checks.
* fix(parsers): preserve Apple metadata failure ownership
* test(architecture): shrink parser failure baseline
* fix: preserve uncertainty for host range errors
SourceMapFormatFailure and its format/unsupported/limit/section-overlap reasons are replaced by ParseFailureError with format source-map. Limits carry the selected allowance name and maximum; a section-boundary overlap is a malformed failure on the section-boundary field. The codec reply protocol and every public error are unchanged.
* refactor(model): report GGUF and Safetensors format failures through ParseFailure
* fix(model): report malformed model headers as invalid_format through the shared mapping
Malformed and truncated GGUF and Safetensors headers now take the shared ParseFailure mapping (invalid_request, invalid_format on path) instead of the model provider's own BinaryTargetError file_format_rejected. Unsupported forms stay unsupported_target and model allowances keep their resource-constraint recovery.
* refactor(errors): require a reason for every analysis cancellation
Add cancelledBy, the one constructor for AnalysisCancelledError. Its
reason argument is required: an aborted signal, an earlier cancellation,
a reason carried from an inner failure, or an OwnerCancellation when REA
closed the work's owner itself.
Closed adb, apktool, firmware, sqlite, gdb, web-bundle analyzer and
binary-session record owners now report OwnerCancellation.ownerClosed
instead of an absent reason. Firmware's predecessor wait returns its
cancellation as a Result, removing an unreachable reason-less fallback.
An architecture guard rejects new direct constructions, with a counted
per-file baseline that can only shrink.
* refactor(errors): share one aborted-signal predicate with cancelledBy
* refactor(process): return a departing caller's reason from a shared startup wait
* refactor(android): build mobile-provider cancellations with a required reason
Migrate every remaining AnalysisCancelledError construction under
src/adb, src/android, src/apktool, src/apkSignature, src/flutter,
src/il2cpp, src/dotnet and src/frida to cancelledBy and lower the guard
baseline from 671 to 608. A closed adb provider reports
OwnerCancellation.ownerClosed for later probes, and JADX and apktool
shutdown abort with that typed reason.
* refactor(providers): build decompiler-provider cancellations with a required reason (#10587)
Migrate every AnalysisCancelledError construction under src/ghidra,
src/ida, src/cutter, src/jeb and src/kuna to cancelledBy and lower the
guard baseline from 671 to 572. Closed Kuna target commands report
OwnerCancellation.ownerClosed instead of an absent reason, and Kuna and
JEB close abort their lifetimes with that typed reason.
* fix(errors): read the reason of any signal-shaped cancellation source
* test(ci): cover the counted-baseline comparison behind the architecture guard
Hopper and native dispatch metadata re-ranked FAT slices from the target's
architecture name instead of reading the admitted universalSlice. Both now
consume the admitted range and refuse when it is missing or undeclared.
Mach-O load-command codes move to one table with an architecture guard.
* refactor(rust): report cargo-auditable section failures through ParseFailure
RustAuditableFormatFailure restated the malformed/unsupported algebra and
the provider translated it by hand; container BinaryFormatFailures were
re-wrapped and lost their kind. The reader now throws ParseFailureError
(format rust-auditable), container failures propagate unchanged, and the
provider maps both through parseFailureAnalysisError. A counted guard
keeps new per-format failure classes from returning.
* test(rust): pin the container's truncated failure facts through the Rust reader
* refactor(errors): require a reason for every analysis cancellation
Add cancelledBy, the one constructor for AnalysisCancelledError. Its
reason argument is required: an aborted signal, an earlier cancellation,
a reason carried from an inner failure, or an OwnerCancellation when REA
closed the work's owner itself.
Closed adb, apktool, firmware, sqlite, gdb, web-bundle analyzer and
binary-session record owners now report OwnerCancellation.ownerClosed
instead of an absent reason. Firmware's predecessor wait returns its
cancellation as a Result, removing an unreachable reason-less fallback.
An architecture guard rejects new direct constructions, with a counted
per-file baseline that can only shrink.
* refactor(errors): share one aborted-signal predicate with cancelledBy
* refactor(process): return a departing caller's reason from a shared startup wait
* refactor(providers): build decompiler-provider cancellations with a required reason (#10587)
Migrate every AnalysisCancelledError construction under src/ghidra,
src/ida, src/cutter, src/jeb and src/kuna to cancelledBy and lower the
guard baseline from 671 to 572. Closed Kuna target commands report
OwnerCancellation.ownerClosed instead of an absent reason, and Kuna and
JEB close abort their lifetimes with that typed reason.
* fix(errors): read the reason of any signal-shaped cancellation source
* test(ci): cover the counted-baseline comparison behind the architecture guard
* refactor(parsers): report format failures through one ParseFailure algebra
Add ParseFailure (malformed, truncated, unsupported, limit, with format,
offset and field) and its one carrier, ParseFailureError, in the domain.
BinaryReader now names truncation and the field it read, and its native
format and resource failures are ParseFailureError subclasses.
Providers projected a format failure with their own instanceof ladder,
and some dropped the selected path from the invalid_format message.
parseFailureAnalysisError now owns that projection for the native,
Go, devicetree, V8, Windows Shortcut and Electron fuse providers.
The V8 code cache, Windows Shortcut and Electron fuse format classes
are removed in favour of the algebra.
The architecture guard counts raw Buffer and DataView reads, plain
Error throws in parser folders and plain JSON.parse of parser input
against baselines that can only shrink.
* fix(parsers): retain malformed range offsets
* refactor: prune internal seams and preserve configuration ownership
* fix(setup): keep YAML aliases and inherited registration settings on refresh
- Reuse an unchanged YAML collection only while every alias in it still
resolves after the edit; otherwise rebuild it so the alias becomes its
value. A sibling anchor replaced or moved in the same refresh no longer
leaves an unresolved alias in Hermes or Goose output.
- Keep a connection selector whose existing value already chooses stdio
(for example an inherited Hermes transport: stdio) instead of superseding
it; remote selectors are still replaced. Explicit stdio selectors are
therefore no longer rewritten (Kimi expectation updated).
- When the whole Hermes registration is inherited through a merge,
override it with that merge and write only the changed fields.
* fix(setup): edit registration fields whose YAML key is an alias
A key written as an alias to a string scalar (for example *field with
field: &field env) was treated as a source-only key, so any change to
its value was refused in Hermes and Goose. Resolve such keys to their
string, keep the aliased spelling while its anchor survives the edit,
and write the plain key when the same edit rebuilds the anchor.
* test(process): bound cleanup-boundary waits and always release gates
Both cancellation tests awaited a bare started promise and released
their gates only on success, so a missed boundary waited for the outer
deadline and could leave cleanup parked. Use awaitAcknowledgement and
release and join the cleanup in finally.
* fix(setup): repair only aliases an edit leaves unresolved
Replace the pre-edit alias survival guess, which expanded aliases to
sibling anchors that survive unchanged, with an exact repair: record
what each alias in the replaced value resolves to, write the edit, then
replace only aliases left without a preceding anchor by that value.
- Keep an aliased key's source position, so its anchor stays ahead of
later aliases.
- Treat an aliased key as its string only when no other key in the map
resolves to the same string; an ambiguous one stays guarded.
- Override an inherited Hermes registration only when the requested
entry keeps every inherited field, so an explicit replacement can drop
remote selectors.
* fix(setup): repair aliases whose binding an edit changes, keeping comments
An alias whose own anchor was removed could resolve to an earlier anchor
with the same name and silently change value. Record the node each alias
resolved to and repair any alias that no longer resolves to that node.
Carry the alias's comments and spacing onto the repaired value.
* test(setup): split the Hermes anchor and merge refresh cases
* fix(setup): refuse removing an overridden key its merge would restore
The merge-retention check skipped keys the mapping also overrides, so
dropping such a key (for example a remote url during an explicit stdio
replacement) let the retained merge restore it. Check removal before
skipping overridden keys, so the edit is refused with the existing
message instead of producing a registration that cannot be applied.
* fix(verifiers): release fixture roots only after their owners finish
Several real-tool verifiers released a fixture root even when a cleanup
that still used it had failed, so a surviving MCP server, session or
descendant kept running against a deleted directory and the failure
looked like ordinary cleanup.
- scripts/lib/verifier-cleanup.mjs: new releaseAfter(dependents,
release, retained) task. It attempts every dependent cleanup and
releases the resource only if all succeed; otherwise it keeps the
resource and reports it with the dependent failures.
- verify-signal-recording: the root is released after the MCP client
and transport close, and the host-limits read moves before the root
is acquired so its failure cannot leak the root.
- verify-recorded-crash: sessions, the sentinel and the process-lineage
check must all succeed before the root is released. A transport close
failure after a failed connect keeps the connect failure.
- verify-wasm-artifact: both roots are released after the MCP client
and transport close; the residue check and the two directories stay
independent after that.
- verify-native-ui: `open -W` exits only after the app exits, so the
root is removed only once that exit is observed; otherwise it is
retained and reported.
Fixes#8561Fixes#8562Fixes#8564
Refs #8559
Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(verifiers): treat a signaled open as unproven fixture exit
A terminated `open` launcher does not prove the LaunchServices app it
started has exited, so only an `open -W` that exits on its own counts.
Also clear the grace timer so a prompt exit does not hold the process
for the rest of the grace period.
Refs #8559
Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(verifiers): model open -W exiting after the fixture is signaled
The native UI verifier now removes its root only after `open -W` exits
on its own. The authored recording's `open` never exited when the
fixture was signaled and had no signalCode, so the complete case waited
out the grace period and kept the root. The double now behaves like
`open -W`: it exits once the fixture process is signaled.
Refs #8559
Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Retain the original stack merge resolutions for runtime update modes, migrated guard exceptions, deadline ownership and boundary expectations on the merged settings base.
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Retain existing merge resolutions for update modes, source-scan failures and external-reader fixtures while replaying the settings layer on the merged inventory. Preserve parent provenance, candidate counts and presentation-test resource bounds.
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
A numeric parser now reads property and element accesses in its argument, so Number(process.env.REA_ARCHIVE_ENTRY_LIMIT) is a producer without a schema key. Template discovery keeps generated assignments and no longer treats a diagnostic sentence as a read.
New per-call registry entries are rejected. Inventory checks require a table row, and the Electron argv handoff is process protocol rather than a caller setting.
A constructor copy is not a per-call read. Numeric limits also come from a number schema or a numeric parse, and the inventory no longer lists path and provider constants.
Object bindings, in-checks, and object keys that a spread can copy are producers. The architecture guard skips the TypeScript parse until typescript6 is installed, and the inventory links stay inside the published site.
Property access, element access, and helper-call arguments are producers. Comments and array literals are excluded by the tree, so a later argument such as read(environment, "REA_NEW_MAX_BYTES") stays classified.
Discovery no longer treats comments or registration-list entries as producers. The three limits moving on their own PRs must join ANALYSIS_SETTINGS; the exception registry cannot classify them.
Part of #9881. Record every numeric REA_ limit and fail static checks when one is neither a runtime analysis setting nor a documented exception with a reason.
Attempt independent resource cleanup without losing primary failures, retain unresolved owners, and publish completion after release. Bound existing producer commands, preserve helper return values, and settle startup and fixture children on failure.
Validation: 29 focused boundary tests passed; actual helper-value and import-bound filesystem cleanup controls passed. The real inert-child termination control and targeted JavaScript binding checks passed, alongside typecheck, lint and formatting. Live Ghidra, Cutter, Electron GUI and Windows addon qualification was not run.
* docs: organize canonical documentation layout (URGENTE)
Group all existing authored guides by domain and ownership, expose complete navigation, and preserve their public routes. Update physical path consumers and release configuration while retaining historical records. Document domain-based specs and ADR slugs without global numbering.
Co-Authored-By: Codex <noreply@openai.com>
* fix(docs): honor home layout in all-page route verification
* style: format reviewed source changes
* fix(docs): preserve routes and navigation cache inputs
* fix: repair the static-check failures on the default branch (#2895)
* fix: repair the static-check failures on the default branch
The default branch fails its own required `Static checks` job three ways, so
every pull request reports that job as failed regardless of its own change.
- `npm run format:check` reports `src/domain/functionComparison.test.ts` and
`src/ida/IdaOperationRunner.ts`.
- `npm run lint` reports a duplicate `import { join } from "node:path"` in
`tests/boundary/mcp/ghidraInventoryMcp.test.ts` (lines 4 and 17).
- `npm run typecheck` reports `format: "devin"` missing from the client format
union in `src/application/SupportedClients.ts`, and an unchecked index in
`tests/boundary/cli/clientProfileLifecycle.test.ts`, whose pair array is
inferred as `string[][]`.
Apply the pinned formatter, drop the duplicate import, list the `"devin"` format
that its own definition already uses, and mark the pair array `as const`.
Validation: `npm run check:fast` and `npm run format:check` pass.
* fix(setup): keep Devin on the JSON configuration dialect
---------
Co-authored-by: 吴佳浩 <wujiahao@supremind.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
* fix(docs): update links after guide relocation
* docs: correct verified documentation route counts
* fix(docs): update browser capture guide link after move (#2820)
Refresh gettext protected-link comments alongside the authored href.
Co-authored-by: Codex <noreply@openai.com>
* fix(docs): include http3 lab in stable routes
* fix(docs): preserve historical release evidence and links
* fix(ghidra): restore Windows verifier identity import
* docs(wakaru): preserve pinned upstream policy link
* docs(index): retain recently added guide links
* docs(mcp): preserve parent retention clarification
* fix(cli): preserve standalone import guidance
* fix(test): preserve translated guide link drift coverage
---------
Co-authored-by: Matheus Breguêz <matbrgz@gmail.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: morluto <17176810+morluto@users.noreply.github.com>
Co-authored-by: 吴佳浩Alben <35291372+XiaoBinGan@users.noreply.github.com>
Co-authored-by: 吴佳浩 <wujiahao@supremind.com>
Declare logical Evidence sources once and resolve exact retained inputs through
shared application workflows across 20 composition tools. Preserve domain-owned
authentication, source closure, caller choices, coverage and resource admission.
Validation includes public CLI/MCP regressions, local Codex terra/high trials
and fresh-import real-artifact workflows. Measurements, failed attempts and
coverage limits are recorded in docs/verification/evidence-input-composition-2026-10-31.md.
Fixes#8293.
Fixes#8281.
Assisted-by: Suika (gensokyo-skills:suika-scatter-gather-planning)
* feat(artifacts): inventory and extract UF2 and DfuSe regions
open_binary refused UF2 and DfuSe firmware containers as an unsupported
format, so inspect_artifact and extract_artifact could not show which
bytes a drag-and-drop or DFU update writes at which addresses.
UF2 is recognized by a complete first block with all three magic
numbers. Valid main-flash payloads are joined into address-contiguous
regions per familyID, named family-0x<id>/0x<start>-0x<end>.bin, and
known IDs are named from a small table copied from Microsoft's
uf2families.json at a pinned commit. Numbering is counted per familyID
and block count: absent, repeated, out-of-order and out-of-range block
numbers are reported, and absent blocks leave coverage unknown.
Not-main-flash blocks are excluded and reported; file-container blocks
are reported and leave coverage unknown; invalid blocks and a partial
final block are excluded with their position. A rewrite of one address
with different bytes fails closed, naming both block spans.
DfuSe is recognized only by its prefix together with a DFU 1.1a suffix.
Each element becomes one region at its file range with its target's
alternate setting and name. Structural faults fail closed with a byte
offset; overlapping elements stay separate and are reported. A suffix
CRC mismatch follows integrity_policy: fail refuses with a dfu-crc32
integrity error, and record-and-continue records a root contradiction
that extraction also carries.
HEX, UF2 and DfuSe share one retained-region reader and one run
merger, so the HEX decoder and reader lose their private copies.
* docs(artifacts): document UF2 and DfuSe images and their esptool lane
Describe recognition, region names, metadata, numbering, failures, the
DfuSe suffix CRC under integrity_policy, and limits, and link the page
from the CLI and firmware guides and the agent skill reference.
npm run verify:uf2 runs esptool merge-bin --format uf2 on generated
inputs at known addresses, with and without MD5, with 256-byte
payloads, and on a concatenation of two chips' outputs. Inspection and
extraction through the built CLI must return every input at its address
under the familyID esptool itself reports.
* fix(artifacts): accept both DfuSe image size conventions
UM0391 calls DFUImageSize the file length, and dfu-util's dfuse-pack.py
writes that value, while ST's DfuFileMgr, MicroPython's dfu.py and pydfu
leave the 16-byte suffix out. The decoder accepted only the second form,
so it refused dfuse-pack.py output as malformed.
Accept either value and keep the declared one in image_size; any other
value still fails with the suffix offset and the file length. The tests
pin both producers' output for one image, CRC included, from their
build() functions transcribed and run under Python 3.
* fix(artifacts): keep reused UF2 block numbers from hiding missing blocks
esptool numbers every merged input from 0, so two inputs with the same
block count share one numbering group. A block lost from one input left
its number present in the other, and the file reported complete
coverage. Each block number now records the target addresses it was
seen at: equal address counts are complete copies, a resend to the same
address is one block, and unequal counts make coverage unknown with a
limitation and ambiguous_numbering_group_count. sequence_count becomes
numbering_group_count, and region block positions become
first_file_block and last_file_block.
Review follow-ups in the same change:
- compare_artifacts compares a DfuSe element's alternate setting and
target name, so bytes moved to another device memory are changed;
- the shared reader keeps no region bytes outside its map after close;
- one domain type replaces four identical decoder failure types;
- the UF2 decoder returns a typed limit instead of throwing on bytes
beyond its admitted size, and caps retained regions, not runs;
- DfuSe caps elements with bytes only, names empty elements and
element-less targets, drops the constant bVersion and bcdDFU fields,
and says whether the CRC matches when DFUImageSize fits neither
convention;
- classification evidence carries a suffix only when the probe saw the
end, and root classification reads the tail only for a DfuSe prefix;
- the UF2 domain region owns family_name, and the root records the
family registry revision; the table's MIT notice is preserved;
- docs pin the producer revisions (dfu-util ee6b393d and f9a537c9,
MicroPython cb99ca98), give the MD5 structure's real meaning, and
describe container contradictions in the MCP contract guide;
- tests and the esptool lane add the equal-count inputs and a block
lost from one of them, accepted boundaries beside each refusal, a
DfuSe root larger than the classification prefix, and CLI refusals.
* feat(artifacts): name the UF2 families that LibreTiny builds for
LibreTiny writes UF2 for Realtek, Beken and Bouffalo Lab modules. The
Microsoft registry revision REA already cites (cf3bd7f9) lists eight of
those families: RTL8710A, RTL8710B, RTL8720C, RTL8720D, BK7231U, BK7231N,
BK7251 and BL602. Their regions now carry those names. LN882H, which
that revision omits, keeps its familyID with no name.
* fix(artifacts): count DfuSe declarations without bytes instead of listing them
Empty DfuSe elements have no entry, so DFUSE_MAX_ELEMENTS does not cap
them, and the target walk kept a description string for each one. At 8
bytes per element, a file under the 128 MiB ceiling can declare about
16.7 million of them. The walk now counts every declaration without bytes
and builds only the first NAMED_ELEMENTS descriptions, which is all that
the limitation shows. The limitation text and empty_element_count do not
change. The guide now says that both DfuSe limitations name the first four.
On this host, decoding 4,000,000 empty elements (a 32 MB file) ran out of
memory under a 128 MB V8 heap before this change, and it peaked at 1.59 GB
RSS with the default heap. It now decodes in 114 ms with a 220 MB peak.
---------
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>