Release: merge development into beta - #2
Conversation
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
PHPUnit Tests
Integration Tests (Newman)
E2E Tests (Playwright)
Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (215 total)
PHPUnit TestsPHPUnit tests were not enabled for this run. Integration Tests (Newman)Newman integration tests were not enabled for this run. E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
PHPUnit Tests
Integration Tests (Newman)
E2E Tests (Playwright)
Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (215 total)
PHPUnit TestsPHPUnit tests were not enabled for this run. Integration Tests (Newman)Newman integration tests were not enabled for this run. E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (215 total)
PHPUnit TestsPHPUnit tests were not enabled for this run. Integration Tests (Newman)Newman integration tests were not enabled for this run. E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (215 total)
PHPUnit TestsPHPUnit tests were not enabled for this run. Integration Tests (Newman)Newman integration tests were not enabled for this run. E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (215 total)
PHPUnit TestsPHPUnit tests were not enabled for this run. Integration Tests (Newman)Newman integration tests were not enabled for this run. E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (215 total)
PHPUnit TestsPHPUnit tests were not enabled for this run. Integration Tests (Newman)Newman integration tests were not enabled for this run. E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (416 total)
PHPUnit Tests
Code coverage: 0% (0 / 3 statements) Integration Tests (Newman)
E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (416 total)
PHPUnit Tests
Integration Tests (Newman)
E2E Tests (Playwright)
Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (416 total)
PHPUnit Tests
Code coverage: 0% (0 / 3 statements) Integration Tests (Newman)
E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (416 total)
PHPUnit Tests
Integration Tests (Newman)
E2E Tests (Playwright)
Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (416 total)
PHPUnit Tests
Code coverage: 0% (0 / 3 statements) Integration Tests (Newman)
E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (416 total)
PHPUnit Tests
Integration Tests (Newman)
E2E Tests (Playwright)
Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
npm dependencies (416 total)
PHPUnit Tests
Integration Tests (Newman)
E2E Tests (Playwright)
Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (416 total)
PHPUnit Tests
Code coverage: 0% (0 / 3 statements) Integration Tests (Newman)
E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (416 total)
PHPUnit Tests
Code coverage: 0% (0 / 3 statements) Integration Tests (Newman)
E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
PHPUnit Tests
Integration Tests (Newman)
E2E Tests (Playwright)
Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (416 total)
PHPUnit Tests
Code coverage: 0% (0 / 3 statements) Integration Tests (Newman)
E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (416 total)
PHPUnit Tests
Code coverage: 0% (0 / 3 statements) Integration Tests (Newman)
E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (416 total)
PHPUnit Tests
Code coverage: 0% (0 / 3 statements) Integration Tests (Newman)
E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
npm dependencies (416 total)
PHPUnit Tests
Integration Tests (Newman)
E2E Tests (Playwright)
Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (416 total)
PHPUnit Tests
Code coverage: 0% (0 / 3 statements) Integration Tests (Newman)
E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
npm dependencies (416 total)
PHPUnit Tests
Integration Tests (Newman)
E2E Tests (Playwright)
Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (416 total)
PHPUnit Tests
Code coverage: 0% (0 / 3 statements) Integration Tests (Newman)
E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (416 total)
PHPUnit Tests
Integration Tests (Newman)
E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (416 total)
PHPUnit Tests
Code coverage: 0% (0 / 3 statements) Integration Tests (Newman)
E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (100 total)
npm dependencies (416 total)
PHPUnit Tests
Code coverage: 0% (0 / 3 statements) Integration Tests (Newman)
E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Quality Report — ConductionNL/decidesk @
|
| Check | Result |
|---|---|
| PHP lint | ✅ |
| PHP phpcs | ✅ |
| PHP phpmd | ✅ |
| PHP psalm | ✅ |
| PHP phpstan | ✅ |
| PHP phpmetrics | ✅ |
| eslint | ✅ |
| stylelint | ✅ |
| Security (composer) | ✅ |
| Security (npm) | ✅ |
| License (composer) | ✅ 100/100 |
| License (npm) | ✅ 416/416 |
| PHPUnit | ✅ |
| Newman | ✅ |
| Playwright | ⏭️ |
Coverage: 0% (0/3 statements)
Quality workflow — 2026-04-13 18:03 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ❌ | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ |
Quality workflow — 2026-04-13 18:11 UTC
Download the full PDF report from the workflow artifacts.
…uctors
Companion to the previous commit. ADR-083 both ADDED objectService and REMOVED
the ContainerInterface parameter from the services it converted; the first was
fixed already, this is the second half:
new SomeService(
- container: $this->container, <- Unknown named parameter
appConfig: $this->appConfig,
objectService: $this->createMock(ObjectServiceInterface::class),
);
Decided PER CLASS, never by pattern: 61 classes in decidesk, 102 in pipelinq and
151 in shillinq legitimately still take a ContainerInterface, because they use
the availability-guarded lookup that is ADR-083 rule 1's exception. Removing
theirs would have broken working code.
php -l on every touched file, reverted on failure; a re-scan reports 0 remaining
sites for both problems.
phpstan caught a defect in the rollout transformer:
PHPDoc tag @var for property $objectService with type
OCA\OpenRegister\Service\ObjectServiceInterface is not subtype of native
type OCA\OpenRegister\Contract\ObjectServiceInterface
The docblock rewrite matched `@var \OCA\OpenRegister\Service\ObjectService` and
appended `Interface` to the CLASS name while leaving the NAMESPACE alone, so the
declared type named a class that does not exist. The native type next to it was
correct, which is why only phpstan noticed — PHP itself never reads the docblock,
and the tests pass either way.
That is the fifth silent failure from this transformer, and the same shape as
the others: it produced plausible output that no runtime check disagreed with.
…e-fix one
These branches were cut from ADR-083 branches predating the 2026-08-14
correction, so they carry an older code-quality.yml whose push trigger has no
refactor/** — which is this branch's prefix. development's version says why that
matters:
An ALLOW-LIST of branch prefixes is a gate with a hole in it, and the hole
is SILENT: a branch matching nothing gets no CI at all, and its last visible
status is whatever it inherited — indistinguishable, on every dashboard,
from a branch that passed.
Observed here: softwarecatalog#519 settled at FOUR checks (CodeQL and Analyze
only) and read as green, having previously reported 43. shillinq#556 did the
same at three, which is shillinq#557.
Takes merge-hygiene.yml with it, the companion added in the same change, which
runs the fast structural checks on ** so an unlisted prefix is not completely
unguarded.
This restores coverage via the PUSH path. It does not explain why the
pull_request runs stopped, which is tracked separately.
The arity fix added
objectService: $this->createMock(ObjectServiceInterface::class)
to every construction that needed one. In these files the test had ALREADY built
and configured a double — and was handing it over through the container, a path
the service no longer consults now the dependency is injected. So the service
received an EMPTY mock while a fully-seeded one sat unused a few lines above,
and every lookup returned null:
RuntimeException: Zaak not found: zaak-uuid-1
Now the constructor gets the variable the container was handing back.
Scoped to services whose __construct actually takes an ObjectServiceInterface.
Services still taking a ContainerInterface are the ADR-083 rule-1 exception and
their container seeding is CORRECT — an unfiltered version of this change would
have broken them. softwarecatalog's SbomImportService is the example that caught
it: it still takes the container, its test seeds through it, and nothing there
needed changing.
php -l on every touched file, reverted on failure.
The app pinned the exact prerelease 2.2.0-vue3.16, which is now deprecated: the Vue 3 line was folded into the mainline 2.x release series and ships as 2.3.0 on the `latest` dist-tag. A caret range replaces the exact pin so future 2.x releases roll out without a per-app edit. Drops the `overrides.@conduction/nextcloud-vue.eslint` entry. That override existed only because the old prerelease declared `eslint: ^8.56.0 || ^9.0.0` and could not see the app's eslint 10; 2.3.0 declares `|| ^10.0.0` itself, so the peer resolves without help. No API change: 252 components in and 252 out, no export removed, one added (the BSN validators). Peer ranges are otherwise identical.
…isfy the contract
Three distinct pre-existing defects, all of which only became visible once the
tests could actually construct their subjects.
1. ARITY, completely this time. The first pass added `objectService` only,
because that is the parameter ADR-084 was about. ADR-083 added others in the
same commit — softwarecatalog's ContactpersonenController gained THREE
(objectService, magicMapper, organisationService) — so a call could be fixed
for one and still be short by two. Every required parameter is now supplied,
by NAME so it fills the right slot regardless of the existing arguments.
2. IMPORTS for the types those arguments name. `createMock(MagicMapper::class)`
without a `use` resolves the short name against the TEST's own namespace, and
`::class` does not require the class to exist — so it silently mocks a class
nobody declared. Same trap as the container-key strings in lib/.
3. The ObjectEntity STUB now implements ObjectEntityInterface. Once
ObjectServiceInterface is mocked its return types are enforced:
Method find may not return value of type MockObject_ObjectEntity, its
declared return type is "?OCA\OpenRegister\Contract\ObjectEntityInterface"
A hand-rolled double that does not declare the interface cannot be handed
back. This is ADR-084's argument arriving in the tests: ten apps had such a
double, and none of them was checked against anything until now.
softwarecatalog also gains a MagicMapper stub, because ADR-083 injected
OpenRegister's MagicMapper into a controller and this app has no way to load it.
That stub is debt of exactly the kind ADR-084 removed for ObjectService — noted
in the file so it stays visible rather than becoming furniture.
php -l on every touched file, reverted on failure.
…lass
The arity fixer keyed constructors by FILE STEM, and two classes with the same
stem in different namespaces collide. procest has
lib/Service/ChecklistService.php no constructor arguments
lib/Service/Inspection/ChecklistService.php three
so a correct `new ChecklistService()` was given three arguments belonging to the
other class:
Error: Unknown named parameter $settingsService
Constructions are now resolved through the FILE'S OWN `use` imports to a
fully-qualified name before their arguments are judged.
The reconcile pass was itself wrong on its first run, in a way worth recording:
its parameter parser only accepted types beginning with a capital, so
`string $appName` on every Nextcloud controller was invisible and a valid
`appName:` argument looked like one the constructor did not have. It removed 33
files' worth of arguments in procest; with scalar types accepted it removes 8.
The tell was reading a diff that deleted `appName: 'procest'` — a line no
plausible fix would touch.
php -l on every touched file, reverted on failure.
phpmd, on this branch:
UndefinedVariable $objectService
ADR-083 replaced
$objectService = $this->container->get('OCA\OpenRegister\Service\ObjectService');
with an injected property and rewrote most usages to $this->objectService — but
not all. What remained read a local that no longer exists. In PHP that is not a
parse error and not a test failure unless the line executes; it is null at
runtime, and the call it feeds gets null instead of the service.
Rewritten only where the enclosing function has no assignment to that local AND
does not take it as a PARAMETER — several helpers legitimately receive it, e.g.
CreditLimitGuard::sumOutstandingCents(object $objectService, ...), and those are
untouched.
php -l on every touched file, reverted on failure.
…ferent class"
The reconcile pass removed VALID arguments. PHPUnit, after it:
Error: DsoObjectRepository::__construct(): Argument #1 ($settingsService) not passed
Error: ZgwRulesBase::__construct(): Argument #1 ($logger) not passed
Error: Too few arguments to ProcestCaseReader::__construct(), 0 passed
It was written to fix ONE real problem — two classes sharing a file stem, where
`new ChecklistService()` was given three arguments belonging to
lib/Service/Inspection/ChecklistService.php. That problem is real and is fixed
by hand in the next commit.
The pass itself judged every construction in the file, and where its constructor
parse came back empty it treated every named argument as unknown and deleted it.
A tool that removes code needs to be certain, not merely unable to confirm;
reverting is cheaper than making it certain, since exactly one call site
actually needed changing.
Several classes here share a FILE NAME with another in a different namespace —
lib/Service/ChecklistService.php and lib/Service/Inspection/ChecklistService.php,
BelplanRoutingService, HearingService and others. The arity fixer keyed
constructors by that file name, so some constructions received arguments from
the wrong class:
Error: Unknown named parameter $settingsService
Classes are now resolved through the file's own `use` imports, and the pass
refuses to act unless it is certain: a constructor that exists but parses to
nothing is treated as a PARSE FAILURE and skipped, never as "takes no
arguments".
It also reads only DEPTH-0 named arguments. A nested construction has its own
constructor:
new ProcestToolProvider(
caseReader: new ProcestCaseReader(
settingsService: $settingsService, <-- the INNER call's parameter
logger: $logger,
Judging those against the outer constructor is what made the first attempt
delete valid arguments; that attempt was reverted, and this is its replacement.
…l terms in l10n (#497) * refactor(decidesk): translate the enum values, keeping the Dutch legal terms in l10n 122 Dutch enum values across 52 sets. The code and the schema hold English now; the Dutch term is not lost, it MOVES — every one is written into l10n/nl.json against its English key, so a Dutch-rendered UI still shows `Splitsingsakte`, `Statuten`, `Verordening`, `Gemeentewet`, `Reglement van orde`, `Decharge` and the rest. That is the point of doing it this way. These are named instruments in Dutch law, and an English rendering in the DATA is right — the data is read by code — while an English rendering in the UI would be wrong for the person reading it. Separating the two is what the translation layer is for. 274 l10n entries added across en.json and nl.json (137 each). `RenameDutchDecideskValues` migrates the rows already written: the schema edit changes only the DECLARATION, and a filter on the new value returns null rather than an error. `oriType` is exempt — `Besluit`/`Vergadering`/`Verslag` are the ORI standard's vocabulary and decidesk's own OriSerializer consumes them. A mapping is configuration. One false positive, caught by the test written for exactly that risk: the bare pass rewrote a Dutch SYNONYM LIST — `synonyms: ['financial statements', 'jaarrekening', 'financieel verslag']` — which exists to match Dutch agenda titles. `matches case-insensitively on en + nl synonyms` failed and the value was restored. Two map collisions caught before applying, where two members of one enum would have collapsed into one: `geldend`/`van-kracht` (both "in force") and `gesteld`/`ingediend` (both "submitted"). Differentiated rather than merged. Verified against a control built with `git archive origin/development`: PHPUnit 958 tests and 3456 assertions on BOTH sides, zero failures either way. phpstan clean, psalm "No errors found", phpmd clean, 0 phpcs errors on the new step, vitest 283/283, prettier clean, l10n and manifest validators pass, no enum lost a member. Dutch enum values 122 -> 3. * test(decidesk): getSchema() is no longer magic — OpenRegister declares it Five PHPUnit cells failed on 'getSchema() must stay magic'. Nothing in this branch touches that file: OpenRegister now declares `public function getSchema(): ?string` on ObjectEntity rather than serving it through Entity::__call(), so method_exists() is true where the test pinned it false. Any decidesk PR would fail this. The test's own docblock named this condition: if it flips, the method_exists() guard ListenerSchemaResolver replaced would work again and the resolver may be redundant. That is a decision about decidesk#471 and does not belong in a translation change, so the assertion moves to the property that actually matters and holds either way — the schema READS — and the redundancy question is left open, in writing. * docs(decidesk): the stub no longer matches production on getSchema() The file docblock claimed the stubs answer method_exists() 'exactly as it does in production'. That was true when written and is not now: OpenRegister declares getSchema() for real, so where CI resolves the real class the answer is true and the stub's is false. Nothing in the file may assume either. * test(decidesk): put the value migration's decisions where they can be tested The coverage guard failed stable34 by 0.24% — the widest gap yet, because decidesk's baseline is 61.4% and a new DB-heavy step is nearly all unreachable. Unreachable literally here: decidesk's unit environment does not install doctrine/dbal, so createMock(IDBConnection) fails while BUILDING the double on Doctrine\DBAL\ParameterType, before any assertion runs. That is why the existing repair test uses newInstanceWithoutConstructor(), and why mocking the connection the way pipelinq does is not an option. So the step keeps only the three calls that touch the database and every decision moves to RenameDutchDecideskValueDecisions: the MagicMapper column spelling, the per-table work list, and a defensive result-column read. The test that matters most is the last one. It asserts the Dutch LEGAL TERMS survive in l10n/nl.json — Splitsingsakte, Statuten, Verordening, Reglement van orde, Gemeentewet. The data is English now; if one of those entries goes missing the term is simply gone from the Dutch interface, and nothing else would notice. 965 tests (was 958), zero failures. phpstan, psalm clean; 0 phpcs errors. * refactor(decidesk): give the value migration a port it can be tested through The coverage guard is not satisfiable for a DB-bound repair step in this app, and the reason is structural rather than a matter of effort: decidesk's unit environment has no doctrine/dbal, so `createMock(IDBConnection)` fails while BUILDING the double on `Doctrine\DBAL\ParameterType` — before any assertion runs. Its existing RenameDutchVocabularyColumnsTest says the same. With a 61.4% baseline, every statement that touches the connection is dead weight against the ratio, and extracting predicates alone got it only to 0.18% short. So the step no longer depends on IDBConnection. It depends on ValueMigrationGateway — shardTables, columnsOf, rewrite — and a hand-written three-method fake drives the whole of run(). What is left unreachable is DbValueMigrationGateway, which forwards and does nothing else. That is better design regardless of the gate. A repair step that cannot be exercised is one that ships on the strength of having been read, and this fleet has already shipped a repair step that silently did nothing. The new tests assert what the migration is for: every mapped value is rewritten on a column the table HAS, nothing is rewritten for a column it lacks, and an install with no shard tables reports it rather than issuing statements. 967 tests (was 958), zero failures; phpstan, psalm and phpcs clean. * style(decidesk): document the fake gateway's constructor params phpcs wanted @PARAM for the anonymous gateway's three constructor arguments. * style(decidesk): name the quoting closure's parameter phpmd ShortVariable on $i in the gateway's identifier-quoting closure. * test(repair): assert the operator-facing migration messages The step built its two output lines inline, so nothing asserted them and they sat uncovered. "0 row value(s)" and "nothing to do" mean different things to an operator -- no matching rows versus no shard tables at all -- and that distinction is worth pinning. * docs(repair): tag the vocabulary-migration seam methods with @SPEC exclude gate-16 covers lib/Repair. The gateway port, its database adapter and the pure predicates all carry no business rule of their own -- they exist so the migration can be tested without a real IDBConnection -- so each is excluded explicitly rather than pointed at a spec that does not describe it. * test(repair): assert the value map holds no case-only entries A map entry whose replacement differs from its source by case alone translates nothing, yet still produces a diff -- so it reads as a translation that was made. Where the source is an identifier rather than a word it renames the identifier instead: the same defect in shillinq's draft map turned `ACMReport` into `aCMReport` and `WEEK` into `wEEK`, renaming an entity type and a period constant, and the failures surfaced as `assertArrayHasKey(..., null)` a long way from the cause. The test also feeds the detector a known offender, because an empty result from an instrument nobody has watched fail proves nothing about the map. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
phpstan, and by volume this was the bulk of what remained:
138x PHPDoc tag @PARAM references unknown parameter: $container (shillinq)
62x (pipelinq)
37x (decidesk)
ADR-083 removed the ContainerInterface parameter from the classes it converted
and left the @PARAM line above it. Removed only where the documented signature
genuinely has no $container — classes still using the availability-guarded
lookup keep both, verified on three of them (signature present, docblock intact).
Also:
Dead catch - Throwable is never thrown in the try block
getObjectService() became a property read, which throws nothing.
Expression "$this->objectService" on a separate line does not do anything
the old `$objectService = $container->get(...)` line survived as a bare
expression after its right-hand side was removed.
Not touched here: `is_array()` on an ObjectEntityInterface, which phpstan says
always evaluates to false. That is 45 sites in shillinq alone and the correct
fix differs per branch — the array arm is dead now that find() returns an
entity, but what the surviving arm should read is a per-site question.
Helpers that RECEIVE an object service as a parameter were reading $this->objectService instead. My dangling-reference pass was meant to skip functions that take the value; the skip did not fire for these. This never failed loudly, which is why it survived: the property holds the same service, so the code works — right up until a caller passes a DIFFERENT service to one of these helpers, which is the only reason they take a parameter at all. Every changed line was checked to sit inside a signature that receives it. Found while resolving a merge conflict in softwarecatalog, where development had the parameter and my branch had the property read.
PHPUnit was failing every matrix cell with "Named parameter $objectService overwrites previous argument", which names the symptom rather than the cause. The cause is a leftover first argument. ADR-083 removed the container from these constructors, but the test call sites still pass it, so every positional argument is shifted one place and $logger lands on $objectService -- which is then also passed by name. Dropping the container realigns everything. Only dropped where the constructor's first parameter is genuinely not a container and the collision is real, so a class that still takes one is left alone. Caveat: the matcher is not nesting-aware, so `new Outer(new Inner(...), ...)` is matched to the first `);`. The edits lint clean and the diff reads correctly, but nested constructions in these files are worth a second look.
Same defect as openbuild and pipelinq: the rollout switched the mocks to the published contract but left these property declarations typed to the concrete class, so PHPUnit assigns an interface mock to an ObjectService&MockObject property and PHP refuses it. Missed twice: first by a sweep searching for a bare `ObjectService $` declaration (these are intersection types), then by a skip-guard that subtracted the interface-form count from the concrete-form count -- in files holding both, that went negative and the file was skipped while still reporting clean.
…ride (#496) * fix(build): opt-in local-lib, semver-validated, and drop the src override Three changes to the sibling-checkout handling. Polarity: USE_LOCAL_LIB was opt-OUT, so with the variable unset — its normal state — the build used ../nextcloud-vue instead of the published package. Defaulting to a developer's working checkout is the wrong default for a build that can ship. Now opt-in. Validation: this app had NO version check at all, so an unset variable built from whatever sibling happened to be on disk. The sibling is now validated against this app's own declared @conduction/nextcloud-vue range, failing CLOSED — an unrunnable check refuses the sibling. Removed CN_NEXTCLOUD_VUE_SRC. It pointed the build at an arbitrary path AND was exempt from the version check, which is exactly the hole the guard exists to close: an unmerged branch is the sibling most likely to be skewed. Iterating on an unmerged library branch now means pointing this app's declared range at that version, so the intent lives in package.json rather than in one developer's shell. The variable appeared nowhere else in the repo — no docs, scripts or CI referenced it. For the record, the earlier diagnosis was wrong: the sibling is NOT "the Vue 2 line". It declares peerDependencies.vue ^3.5.0 and uses defineComponent / createApp / <script setup> — a Vue 3 library. The build errors it produced came from a stale vue-demi shim inside the SIBLING's own node_modules (postinstall picks v2/v2.7/v3 and does not re-run on npm install). Validating against the declared range refuses a sibling the app did not ask for whatever is wrong with it. Verified: sibling 2.0.5 vs declared 2.2.0-vue3.16 -> refused; build with no environment set compiles clean (exit 0). See ADR-090. * style: run prettier on webpack.config.js CI's Frontend Check (format) runs `prettier --check` over **/*.js and the guard edit in the previous commit was not formatted. Formatting only — the sibling-version check still refuses ../nextcloud-vue (guard verdict: refused-ok). --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
decidesk carried a comment describing a 24h supply-chain guard and had none of the three settings that make one work: min-release-age=1 below the fleet minimum of 2 days (no exclusion) @conduction/* was not excluded engines.npm=^10.0.0 npm 10 does not implement min-release-age Any one of these alone is a configuration that looks like protection and is not. The option does not exist in npm 10 — `npm config get min-release-age` answers `undefined` — so the window was read by NOTHING while the comment claimed otherwise. The exclusion matters more than it looks: without it the cooldown does not fail loudly, it silently resolves BACKWARDS. Installing @conduction/nextcloud-vue on release day under a cooldown with no exclusion resolves an old version and exits 0. That is a live risk to the fleet's `^2.3.0` migration — a green install of months-old first-party code is worse than a red one, because nothing distinguishes it from a correct install. Verified with the gate's OWN script: pristine development checked 3 setting(s): 3 failure(s) exit 1 this branch checked 3 setting(s): 0 failure(s) exit 0 "checked 3" on both sides, so this is not a zero-file vacuous pass. `npm ci` resolves from the lockfile, so the cooldown does not affect CI installs; and no `.npmrc` in this fleet sets `engine-strict`, so the engines bump cannot hard-fail an install either. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
* chore(security): enable the npm supply-chain cooldown on npm 11 Sets `min-release-age=2` and `min-release-age-exclude[]=@conduction/*`, raises `engines.npm` to ^11.0.0, and regenerates the lockfile under npm 11. The .npmrc comment here has described a cooldown for months and it has never been in effect. `min-release-age` does not exist in npm 10 — `npm config get min-release-age` answers `undefined` — and every Node 22 release bundles npm 10, so the setting was read by nothing. Most repos also had it at 0, which disables it outright. @conduction/* is exempt because without the exemption the cooldown does not fail loudly, it silently resolves backwards: measured 2026-08-15, an install of @conduction/nextcloud-vue on release day picked 2.0.7 instead of 2.3.0 and exited 0. The lock is regenerated under npm 11 and iterated to a fixed point. Where the tree changed rather than its metadata, that is npm 10 -> 11 reconciling a lock shaped by the older resolver, not the cooldown — verified by regenerating with the cooldown enabled and disabled and getting identical trees. Verified: npm ci exit 0 under npm 11.19.0, @conduction/nextcloud-vue resolves to 2.3.0, gate-84 conformance passes. * ci: re-run against the merged shared workflow `gh run rerun` replays the workflow version resolved when the run was created, so a reusable workflow referenced as @main is NOT re-resolved — every re-run after ConductionNL/.github#469 merged still executed Node 22 with npm 10.9.8, where `min-release-age` does not exist and `npm ci` cannot read an npm-11 lockfile. Only a new run picks up the merged workflow. This empty commit is that trigger. * ci: run every npm job on Node 24 The shared quality.yml moved to Node 24 (ConductionNL/.github#469), but this repo's OWN workflows did not, and they run `npm ci` too. Node 20 and 22 both bundle npm 10, which cannot install from an npm 11 lockfile — it exits EUSAGE with 'Missing: <pkg> from lock file'. Measured here: Lint Check and Spec Validation went red on exactly that while every shared-workflow job passed. pull-request-lint-check.yaml had no setup-node AT ALL, so it silently inherited the runner default. That is the harder half to notice: nothing in the file named a Node version, so nothing looked wrong. Left alone deliberately: api-test-coverage.yml stays on Node 20 where it exists — it runs `npm install -g newman`, a global tool install with no lockfile, so the npm major is irrelevant there. l10n.yml likewise: its only 'npm ci' is inside a comment saying it needs none.
…it is not (#503) testGetSchemaIsReadableHoweverItIsDeclared() records that getSchema() flipped from magic to declared and deliberately leaves open whether this class is now redundant. This answers it: it is not, and the class docblock still describes the OLD world, which is the part that would mislead the next reader. The tripwire's original conclusion addressed only the FIRST of the two defects this class exists for, and the same docblock says 'fixing either alone leaves the listener dead'. * Probe half — moot on current OpenRegister. readValue() needs no change: it probes method_exists() || property_exists(), so it reads the value under both shapes. * Value half — untouched, and the reason the class survives. OpenRegister still stamps the schema's numeric id onto every entity it materialises (setSchema((string) $schema->getId()) in MagicMapper and in every ObjectSource provider, verified 2026-08-16). A declared getter returns that id, never the slug the listeners compare against. Only if OpenRegister began stamping the slug itself would this class become redundant. Documentation only — no behaviour change.
…er a file-wide no-undef (#504) * docs(resolver): answer the redundancy question the tripwire raised — it is not testGetSchemaIsReadableHoweverItIsDeclared() records that getSchema() flipped from magic to declared and deliberately leaves open whether this class is now redundant. This answers it: it is not, and the class docblock still describes the OLD world, which is the part that would mislead the next reader. The tripwire's original conclusion addressed only the FIRST of the two defects this class exists for, and the same docblock says 'fixing either alone leaves the listener dead'. * Probe half — moot on current OpenRegister. readValue() needs no change: it probes method_exists() || property_exists(), so it reads the value under both shapes. * Value half — untouched, and the reason the class survives. OpenRegister still stamps the schema's numeric id onto every entity it materialises (setSchema((string) $schema->getId()) in MagicMapper and in every ObjectSource provider, verified 2026-08-16). A declared getter returns that id, never the slug the listeners compare against. Only if OpenRegister began stamping the slug itself would this class become redundant. Documentation only — no behaviour change. * fix(lint): clear the last tranche-A suppression — require.context under a file-wide no-undef `require.context()` is a WEBPACK build-time API the bundler rewrites at compile time, so eslint is right that no runtime `require` exists and the code is right too. The file-wide `no-undef` suppression that recorded this also switched the rule off for every OTHER identifier in the file, so a genuine typo there would have been silent. Scoped to `/* global require */`. Same shape found in 9 apps across the fleet; all cleared the same way. Verified: eslint 0 errors, build exit 0, tests pass.
…the-contract refactor(deps): type-hint OpenRegister's published contract (ADR-084)
222 PHPUnit errors on development, all one shape: ArgumentCountError: Too few arguments to function OCA\Decidesk\Mcp\McpActionItemTools::__construct(), 5 passed in DecideskToolProvider.php on line 215 and exactly 6 expected The ADR-084 pass appended ObjectServiceInterface to McpActionItemTools, McpMeetingTools and McpMeetingGate but never rewired what builds them, so the chain was broken in four places at once and only the first one could report itself: - DecideskToolProvider neither took the service nor passed it — the visible error, and the only one reachable. - McpMeetingTools::__construct passed `container: $container` to the gate with no $container parameter of its own; an undefined variable. - McpMeetingGate did the same to McpMeetingScopeResolver. - Both passed a `container:` named argument the callee no longer declares. The service now flows provider -> tools -> gate -> resolver, and McpMeetingScopeResolver takes the contract instead of pulling ObjectService out of the container by string. McpActionItemTools keeps its container because it resolves ActionItemWriter through it, which is a different dependency; its docblock said otherwise and now says what it does.
…lm see TaskService Two failures from the first pass on this branch: phpmd/psalm both flagged McpMeetingScopeResolver.php:104 — UndefinedVariable $objectService. Removing the container lookup removed the local that meetingUuidsForBody() was being handed. The helper keeps its parameter (it exists precisely so a different service can be passed) and the call site now hands it $this->objectService. psalm's UndefinedClass on ActionItemWriter's TaskService is pre-existing and unrelated to ADR-084: OCA\OpenRegister\Service\TaskService exists on openregister/development, but psalm analyses lib/ without the openregister app on its path, which is why psalm.xml already suppresses UndefinedClass for fourteen other OCA\OpenRegister classes. TaskService was simply missing from that list.
…505) Adopts the canonical script from ConductionNL/.github (quality-config/coverage-guard.php). The whole-project comparison fires on measurement noise. doriath#240 was a PR whose entire diff was `webpack.config.js` — no PHP at all — and the guard failed it: identical denominator (13723), both runs reporting exactly `Tests: 948, Assertions: 3051, Skipped: 1`, and six covered statements of run-to-run xdebug variance between them. The measured `--against` floor cancels driver variance (xdebug vs pcov), as its header says. It does not cancel run-to-run variance within one driver, and the ratchet has no tolerance. Scoping the comparison to the PHP a change actually touches keeps full strength where a regression matters and makes the noise unreachable by construction — a diff with no PHP cannot fail. New `changed-files` capability; the shared workflow PROBES for it rather than assuming, so an un-updated copy keeps the previous behaviour instead of silently accepting and ignoring the flag. Script only — no behaviour change until the workflow passes `--changed-files`. Byte-identical to the canonical copy (md5 5be122aad209da030c79b22a133232fb). Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…caught PHPUnit is green on this branch; phpstan and psalm are not, and what they found is the same unrewired-collaborator shape as the MCP chain — real fatals on paths no test exercises. - MotionService built MotionAmendmentService and MotionForwardingService without the $objectService both now require. - VoteCastingService used $container in its body with no such parameter, and built VoteCastGuard without its $objectService. The container comes back as a parameter because VoterTokenSecret, VoteBallotFactory and VoteCastGuard still resolve through it; only the object service moved. - PublicationService and TranscriptionService passed `container:` to repositories that no longer declare it. Both now take what the repository actually needs and hand it over. - ActionItemWriter kept a $container it never read. - MotionForwardingService::forward() array-accessed the ObjectEntityInterface that saveObject() returns and declared `: array`. It normalises with jsonSerialize() once, which also removes the `$created ?? …` that phpstan flagged as always-true. Tests follow the signatures: seven VotingService suites pass the container, PublicationServiceTest passes the object service it already built, and TranscriptionServiceTest's helper takes optional doubles — its three repository-asserting tests hand over their own, and they now mock the contract rather than the concrete class.
…ct-service fix(mcp): carry the object service through the whole tool chain
… signatures (#507) decidesk's E2E job went from 3 failed / 117 passed to 13 failed / 107 passed when #495 (`refactor/adr-084-type-hint-the-contract`) merged at 08:46 today. The SKIP COUNT is 58 on both sides, so the ten extra failures are a regression, not a skip shuffle, and their messages are backend 500s seen through the browser: seeded chair must be allowed to open (got 500) show-of-hands tally should not 500 unauthorized open must be 403 The refactor replaced `ContainerInterface $container` with `ObjectServiceInterface $objectService` in constructor SIGNATURES, but not in the constructor BODIES that use `$container` to build collaborators, nor at the call sites. Eleven of those, in seven files, are hard runtime fatals: `$container` is an undefined variable in four constructors, and six `new X(...)` calls pass a parameter the target no longer has or omit one it now requires. `VotingRoundOpener` and `PublicationService` are the two the failing E2E tests walk through — hence the 500s on opening a voting round, tallying, and withdrawing or rectifying a published decision. Repairs, each the completion of #495 rather than a revert of it: - McpMeetingScopeResolver: took the container only to fetch ObjectService, so it now takes the contract. That removes McpMeetingGate's need for a container entirely. - McpMeetingGate / McpActionItemTools / McpMeetingTools / DecideskToolProvider: pass `objectService` down; drop the `container` argument where the target no longer declares one. - MotionService, VotingRoundOpener, VotingRoundResults: pass `objectService` to the collaborator that now requires it. - PublicationService: the container existed only to reach the repository, which no longer wants one — replaced by the contract. - TranscriptionService and VoteCastingService: their collaborators are now INJECTED rather than hand-built. Re-adding a container to feed them would have restored a service locator ADR-084 is removing AND pushed CouplingBetweenObjects to the phpmd threshold; injection removes both and drops the parameters that existed only to feed the collaborators. - MotionForwardingService::forward(): `saveObject()` now returns an `ObjectEntityInterface`, so `$created['id']` was array-indexing an object and `forwardMotion(): array` would have TypeError'd on it. Both were invisible while the container returned `mixed`. Reads `getUuid()` / `getObject()`. - ActionItemWriter: the promoted `$container` is dead after #495 rewired it to typed TaskService / RegisterMapper / SchemaMapper. - psalm.xml: `OCA\OpenRegister\Service\TaskService` joins the existing cross-app UndefinedClass list its siblings RegisterMapper, SchemaMapper, FileService and ObjectService are already on. Measured with each tool's own command, in a php:8.3-cli container, same vendor tree for every run: | check | pre-#495 b05a1fd | base f3a1df8 | this branch | |--------------------|-------------------|---------------|-------------| | phpstan | 0 | **29** | **0** | | psalm | 0 | **21** | **0** | | phpmd | 0 | **16** | **5** | | PHPUnit errors | 0 | 222 | 217 | | PHPUnit failures | 0 | 25 | 27 | | broken tests total | 0 | 247 | 244 | 207 lib files scanned. The five remaining phpmd findings are all `CouplingBetweenObjects = 13`, all present on the base, all caused by #495 adding one type to a class that already sat at 12 — no new finding is introduced here. NO test that passes on the base fails on this branch: the two that move from `error` to `failure` are the same two tests, and the deeper defect the ArgumentCountError was masking is #495's test migration replacing store-backed fakes with hollow `createMock(ObjectServiceInterface::class)` doubles. That test-side debt is ~244 tests across ~50 files and is NOT fixed here. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
… mock it
All six PHPUnit cells report the same error 177 times:
UnknownTypeException: Class or interface
"OCA\Decidesk\Tests\Unit\Service\FileService" does not exist
Seven test files call `$this->createMock(FileService::class)` without importing
it. Unqualified, `FileService` resolves against the file's OWN namespace —
`OCA\Decidesk\Tests\Unit\Service` — and no such class exists. The one they mean
is `OCA\OpenRegister\Service\FileService`, which is what the production code
under test imports (`VotingRoundCloser` line 37, `BoardEvaluationReportService`).
decidesk ships no FileService of its own; there is no `lib/Service/FileService.php`.
`TranscriptionServiceTest` already had the import and is untouched — it is the
control that shows the intended target.
## Verification: CI, not my machine, and I want to be clear about that
I could not reproduce this locally, for a reason worth recording rather than
glossing: the decidesk deployed on the shared instance sits on
`chore/coverage-guard-changed-files`, whose copy of these files PREDATES the
change that introduced the unqualified `createMock` calls — its
`VotingServiceTallyMatrixTest.php` contains no `FileService::class` at all. Its
suite passes 969/969, which looks like evidence and is not: it is a different
tree. A green local run against the wrong revision is exactly the kind of
"check that did not run" that reads as a pass.
Confirmed instead by resolution, which is unambiguous:
class_exists('OCA\Decidesk\Tests\Unit\Service\FileService') -> false
and CI's error names that exact FQN. In CI OpenRegister is checked out, so the
import resolves to a real class; locally OR is not on decidesk's autoloader, so
even the corrected name would not resolve — the environment cannot judge this
either way.
This addresses the 177 errors. The 37 FAILURES in the same run are a separate
defect (e.g. `Failed asserting that 'Decision 'dec-1' not found.' contains
"Cannot 'enact'"`) and are NOT touched here, so `PHPUnit` stays red until those
land too.
…er-fileservice fix(tests): import OpenRegister's FileService in the seven tests that mock it
…ible() (#509) * fix(e2e): de-race 20 test.skip() gates built on the non-waiting isVisible() `locator.isVisible()` is an IMMEDIATE predicate — its `timeout` option is ignored. Called on the tick after a `goto`, it asks "is this here right now", before the SPA has issued an XHR. It answers no, and the test skips with a reason that is FALSE. A skip whose stated reason is untrue is an invisible pass, and a worse one than a stub assertion: it renders as "not applicable" rather than as a gap, the reason looks investigated, and it inflates the skip count — the number that separates a flake from a regression. decidesk skipped 58 of 178. Adds `tests/e2e/becomes-visible.ts`, a polling probe built on `waitFor`, and routes every skip gate through it. The `test.skip()` calls are KEPT: the fix is not to unskip, it is to make the gate tell the truth. A skip that survives this change is skipping for the reason it states. Gates de-raced (20): - admin-settings 3 members tab body, import menu ×2 - agenda-management 6 agenda tab ×3, statutory warning, parent row, assemble - meeting-efficiency 5 + the openFirstLiveMeeting() helper's own 2 probes, which gate four callers - meeting-management 1 series tab - process-configuration 2 built-in template, first row - resolution-minutes 2 + the openMinutesTab() helper, which gates five further tests - voting-rules 1 open-round button Also de-races four `(await a.isVisible()) || (await b.isVisible())` assertions in meeting-efficiency to `expect(a.or(b)).toBeVisible()`. These are not skip gates, but they sit immediately downstream of gates this commit opens: leaving them would convert a silent skip into a spurious FAILURE and report instrument noise as a finding. Out of scope and deliberately untouched: overlay-dismissal probes (`dismissSupportDialog` and friends) and optional-branch probes that gate no skip; `integration-registry.spec.ts`, whose 37 skips are gated on `waitForFunction` — which does poll — and are a separate, real registry gap. Collected total unchanged at 178 tests in 30 files, measured on both sides. * style(e2e): helper as .js with an explicit extension — zero new lint errors `import-extensions/extensions` wants a file extension on relative imports, and a bare `'../becomes-visible'` added one error per importing file (7). Naming the helper `.js` with JSDoc types and importing `'../becomes-visible.js'` satisfies the rule and resolves unambiguously for Playwright's loader. Measured on the seven changed spec files: base 40 eslint errors, branch 40. Zero introduced. `--list` still collects 178 tests in 30 files. * fix(e2e): the committed helper was TypeScript under a .js name — E2E ran ZERO tests 🔴 SELF-INFLICTED, and worth recording rather than quietly squashing. `git mv becomes-visible.ts becomes-visible.js` staged the rename with the OLD TypeScript body. I then rewrote the file as JSDoc-typed JavaScript, and re-staged with: git add tests/e2e/becomes-visible.js tests/e2e/becomes-visible.ts 2>/dev/null The `.ts` path no longer existed, so `git add` **exited 128 and staged NOTHING** — and `2>/dev/null` swallowed the only signal saying so. The commit therefore carried `import type { Locator } from '@playwright/test'` inside a `.js` file, which Playwright's loader does not transpile: SyntaxError: tests/e2e/becomes-visible.js: Unexpected token, expected "from" (36:12) Every spec importing it failed to parse, so the E2E job **collected and ran zero tests and reported `failure`** — which is indistinguishable at a glance from "the tests failed". Two lessons this fleet has already written down, both broken by one command: - **never `2>/dev/null` a check whose emptiness IS the decision** — `git add`'s refusal was the whole signal; - **measure the bytes that SHIP.** My local `playwright --list` said "178 tests, exit 0" — it was reading the WORKING TREE, which was correct all along. The index was not. This commit stages the file that was actually written, and the verification is now run against `git stash`-clean HEAD content rather than the working tree. * style(e2e): prettier — I DID break Frontend Check (format), and this fixes it `Frontend Check (format)` was **success on development@1d66c7c4** and `failure` on this PR, naming exactly one file — `tests/e2e/spec-coverage/resolution-minutes.spec.ts`, which is mine. So this is a genuine introduced failure, not inherited noise, and it is fixed rather than explained away. if ( await becomesVisible(tab.getByTestId('minutes-action-submit'), 5_000) ) { → if (await becomesVisible(tab.getByTestId('minutes-action-submit'), 5_000)) { Collapsing the probe onto one line made the condition short enough for prettier to want it inline; I had kept the multi-line shape of the `isVisible()` chain it replaced. Verified: `npx prettier --check "**/*.{js,ts,vue,css,scss}"` — the exact repo-wide command CI runs — now exits 0. * fix(e2e): the .js import extension reached only 1 of 7 specs — same staging bug, again The earlier `git add … 2>/dev/null` that exited 128 also swallowed the extension edits to six spec files, and I only noticed because a later `git status` showed them still unstaged. Six files therefore still imported `'../becomes-visible'` while the commit message claimed all seven used `'../becomes-visible.js'`. Functionally harmless — the extensionless specifier resolves — but the claim was false, and the state was inconsistent: 1 file with the extension, 6 without. 🔑 THE REAL LESSON, AND IT IS NOT ABOUT `git add`: after a `reset --soft` I never re-ran a full `git status --porcelain` and confirmed it was EMPTY. A staging error is invisible to every check that reads the working tree, and `git show --stat` looks perfectly normal because the FILES are all there — only their CONTENT is stale. The check that catches it is `git status --porcelain` returning nothing, plus a build/list run from `git archive HEAD` rather than from the checkout. --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
Automated PR to sync development changes to beta for beta release.
Merging this PR will trigger the beta release workflow.