Round 2 parity: the digital post record reaches the register it is written to - #2083
Merged
Merged
Conversation
…ithout a lookup (#2066) A guard against the recursion already existed when this change was picked up, added by outbound-webhooks-activation. It works, and it is not enough. It recognises the event machinery's own rows by resolving the schema ids of `event` and `event_message` from one existing row per schema, then caching the answer in a process static. On an instance whose event table is empty that resolves to nothing. A fresh install is such an instance, and so is the moment after this change's own remediation purge. The empty answer is then cached for the life of the PHP worker, the guard is inert, and nothing at all bounds what follows. Two things that survive that: - A generated-by marker stamped on every CloudEvent this app writes and read back on the way in. It travels with the row, so it needs no lookup and it still works after the register is renamed or the schema is copied into a second register, which is the #2150 collision shape. - A per-request ceiling. If both other guards go quiet, forwarding stops after EventLoopGuard::MAX_CHAIN events rather than never. That is the answer to what a loop does before it is stopped: at most 25 events and a named refusal, instead of the measured 255 CloudEvents per object create and 45,398 self-generated rows. `event_subscription` joins the machinery schemas: writing a subscription is bookkeeping and must not become an event. The decision is a pure class with no IO, so every branch is testable directly. A refusal is always named, because a guard that went inert and a guard that stopped a loop both forward zero events, and a log has to tell those apart. Sections 3 to 6 of the change stay open on purpose: the queued job, the subscription cache, the purge command and the instance verification are each their own change, and none of them is what stops the loop.
… it already dispatches (#2067) The dispatch arm for `action.kind = 'flow'` was already here, calling FlowRunnerService and asserted by a unit test. So was the one for `'mapping'`. Neither could be reached. `event_subscription.action.kind` listed four values, and `flow` was not one of them, so a subscription naming it could not be saved. `action.flowId`, which the arm reads, did not exist as a property at all. A dispatch arm nothing can reach and a dispatch arm nobody uses look identical from the outside, which is why this sat. This opens the enum to `flow` and `mapping`, adds `flowId` and `mappingId`, and adds the test that would have caught it: the kinds the register accepts and the kinds the switch handles are compared, in both directions. A kind with an arm but no enum entry is unreachable; a kind with an enum entry but no arm saves cleanly and then fails once per delivery as a configuration error. Both lists are read from their own files rather than restated here, so an arm added without registering it fails this test rather than passing against a list the test also forgot. The schema edit is additive: existing subscriptions keep validating, and `flowId`/`mappingId` are plain strings with no format, because adding a format to a property is the breaking change, not adding the property. Task 3, the picker, stays open. The modal offers three kinds and has never offered `notificaties` either, and a flow picker needs a flow list the modal cannot currently fetch, so that is its own change.
…now, and the contract half of the applier (#2068) All three claimed changes are already built. This measures that, records the evidence so the ledger stops being wrong, and closes the two coverage gaps the measurement found. The engine already fails closed on an unknown disappearance policy: deleteInvalidObjects reads it through the throwing reader and, on a value it cannot read, skips the whole deletion step, sets guardInfo.reason, logs it and dispatches the guarded event. Nothing asserted that. The value object's refusal was covered, but a value object that throws into a caller which swallows the throw is the same as no refusal at all, and a misspelled markEnded would have deleted the records it was written to preserve while reporting an ordinary deletion count. DisappearanceApplier::applyToContract had no test. Every assertion in this change was about the object, and the contract is the half the engine reads on the next run, so it is what stops a flagged record being re-flagged or a returning record being missed. The policy difference is pinned too: keepAndFlag must not write an end date, because that would claim the record ended, which is a statement about the subject rather than about the source. Skipped with evidence rather than rebuilt: x-openregister-property-source is not in openregister's property vocabulary, measured against a clone rather than asked; and RegistrySubscriptionRequestedEvent appears once, in a docblock, and is never defined or dispatched, so there is no wire shape to bind to.
…cannot widen (#2069) An expression language that can read the environment is a data exfiltration path: an expression is configuration somebody edits in a form, and env:DATABASE_PASSWORD in a template that renders into an email is a credential leaving the building with a 200 beside it. What the allowlist admits: exact keys, one at a time. No wildcard, no prefix pattern, no empty entry, no regular expression, no case-insensitive match. Every one of those is a rule rather than a list, and a rule also admits whatever is added to the environment next year. DB_* looks careful until somebody names a variable DB_ROOT_PASSWORD. An unreadable stored list allows nothing, and a wildcard smuggled into storage is dropped on read too. Who may change it: an instance administrator, enforced twice — the attribute before the controller runs and requireAdmin() in every body, because a guard that lives only in an attribute disappears when somebody adds a route by hand. The list holds keys and provenance, never values, and not even whether a key is set: that is a map of what is worth asking for. A refusal throws rather than answering an empty string, names the key and never the value, and an unregistered prefix does not fall through to another source. env: refuses a write naming the prefix, because the interface this mirrors has a store() half and a caller will try. Checked before building: lib/Expression did not exist. migration-source- adapters is already merged as #2056 and is skipped; objecten-api-facade is genuinely unbuilt and untouched.
…fils it (#2071) Closes the last task of registry-subscription-connector. I measured the wrong repo to conclude this was blocked. On 2026-09-18 I reported it still blocked because the event name appears once in this repo, in a docblock. That is the wrong place to look for a cross-app dependency: OpenRegister owns and emits it, and it has shipped. lib/Event/RegistrySubscriptionRequestedEvent.php exists there and RegistrySubscriptionNotifier dispatches it. So the wire shape is read rather than guessed: five fields and a getPayload() returning exactly those keys. Two of them, registry and identityValue, are spellings SubscriptionRequestHandler already accepted, so the binding really was one line, as the tasks file predicted. The listener reads getPayload() rather than assembling one from the getters, because assembling it here would be a second definition of the wire shape and the two would drift the first time OpenRegister added a field. It swallows its own failures. The event is dispatched inside OpenRegister's own work, so a registry integriq cannot reach must fail as a subscription that did not happen, not as a save that failed for a reason the person saving cannot act on. The stub was diffed against the real class rather than written from the docblock: same five constructor arguments in the same order, same five getters, same five payload keys. A stub that drifts from the real class can only pass.
…#2072) The seam a phone system reaches integriq through: a provider interface, one shared normaliser so every vendor's payload becomes one shape, the typed CallEvent the KCC panel is fed from, and a guard against a PBX's retries popping the same caller three times. The rule worth keeping: a number this cannot place is EMPTY, never guessed. PhoneNumberValidator already normalises to E.164 and the SMS path uses it, so this reuses it, with one deliberate difference. That validator prefixes bare digits with a plus and accepts the result, so 612345678 becomes +612345678, a valid number in another country. For an outbound SMS that is fine, it fails to deliver. For a caller lookup it matches somebody else's partij, and an agent picks up the phone looking at the wrong person's case history with nothing on screen saying the match was a guess. Asserted in both directions so neither side can be tidied into the other without a test saying what it costs. The event also tells a WITHHELD number apart from an UNKNOWN one. Both give caller = null, but only one is worth asking the caller for their number. The deduplicator is a cache, not a ledger, and says so: two nodes in the same millisecond can both pass. That is survivable only because nothing here writes, and the class and the tasks both say where the idempotency that protects the record actually belongs.
…ller refuses (#2070) Tasks 1 and 2 of zgw-connectors-for-dossiq. Six sets, one per ZGW component, each declaring its auth, the apiVersion it speaks, and its synchronizations and mappings BY SLUG per ADR-015. A shipped numeric id points at whatever holds that id on the installing instance, so it resolves, it installs, and it wires the set to somebody else's configuration. The refusal that matters is two sets on one schema. Both synchronizations run, both write the bound schema, and each overwrites what the other just wrote. Nothing errors. The schema holds whichever set ran last, changing every few minutes, and both source pages report a healthy synchronization because from each one's own side it is healthy: it read its store and it wrote its objects. An operator can watch one page forever without seeing the other undoing it. So the second install is refused and NAMES the set that holds the schema. No set names a fleet app, and that is enforced rather than intended. App ids in this fleet move, and the lookups they feed are duck-typed: a name nothing answers to does not error, it returns false, and the integration becomes a silent no-op that every screen reports as configured. Both the current and the retired spellings are refused, because a set written before a rename carries the old one and checking only the new one lets every pre-rename set through. A set installed against no register or schema is refused: that synchronization runs, reads the store and writes nowhere while reporting a good run. Every shipped set is run through the template guard by a test, so the rule is checked against the things it was written for rather than only against fixtures.
… so (#2073) Tasks 1 to 5 of one-off-and-suppressed-recipients. The resolved list alone is not a record. From "these three addresses received it", a recipient deliberately kept off a beschikking, a recipient who was never on the list, and a bug that dropped one all look identical. So all three parts are kept: standing, added and suppressed, with the suppressed recipient still in the record and marked. A suppressed recipient carries no delivery state. Not failed, not pending, and above all not "not reported": that reads as a transport that never answered and sends somebody chasing a provider about a message nobody sent. Three refusals, each stopping the send rather than correcting it, because every available correction is a lie. A suppression with no reason is refused, since defaulting it to "no reason given" makes the record complete and useless. A suppression of a recipient the CALLER marked required is refused and the refusal repeats the caller's own words: "this recipient is required" gives a handler nothing to argue with or escalate, "the applicant must receive the beschikking under Awb 3:41" tells them what to do next. A suppression of somebody not on the list is refused, because it records a decision that changed nothing. Integriq never decides what is required. Only what the caller marked, with the caller's own stated reason. A rule invented here would be integriq overriding a municipality's own legal advice about its own letters. The resolver is pure, which is the strongest available proof that an addition touches nothing outside its message: there is no standing list to update, no party record to amend, no subject to touch. The preview calls the send's resolution rather than reimplementing it. A second implementation drifts, and the first time anybody notices is when a handler has approved a list the message did not go to, which is worse than no preview.
…ise, by name (#2074) Tasks for REQ-SOW-001 and REQ-SOW-003 of signed-outbound-webhooks. Signed is the default. The opposite default is what most outbound webhooks ship with: unsigned unless configured, so every receiver that never got round to verification keeps working and nobody finds out which ones those are until somebody asks. A receiver cannot tell a request from integriq from a request that merely says it is, and neither can integriq's own delivery log. Unsigned without a reason is refused. Turning signing off on a Friday afternoon to make a stubborn receiver work is legitimate; doing it and leaving nothing to say why is how it is still off two years later, when nobody remembers the receiver was supposed to be fixed. The reason, the user and the time are stored. An existing subscription never gains a secret on upgrade. Retrofitting one would start sending a signature to a receiver that was never given the secret: the strict ones begin rejecting deliveries, the lax ones ignore the header, and either way an operator now believes a channel is verified that is not, which is worse than knowing it is unsigned. And a save that does not mention the secret does not lose it, so an ordinary edit of a sink or a filter never silently unsigns a channel. The list marks the unsigned one with its reason. A list showing only "push" beside both leaves an operator to open every subscription in turn to find them, which is the same as not telling them. Every delivery attempt records its signing, retries and operator replays included. A log that records it only on the first try cannot answer the question a receiver asks after a bad night: was the request I got at 03:14 signed? A replay is the attempt most likely to differ, because it happens after somebody has been changing the configuration. The verification recipe is available whether or not the secret is revealed. The person integrating the receiving end is usually not the one who created the subscription and will never see the reveal. Reuses the existing WebhookSignatureService for generation, signing, rotation grace and the whsec_ prefix rather than growing a second scheme.
…uard (#2075) Gap register row 12.3, statutory. This builds the three pieces that decide correctness and no HTTP: no route of either API is served yet, and the PR body says so route by route rather than implying the whole. Each of the three fails invisibly if it is got wrong. Two registers can each hold a schema called melding, so a name-inferred mapping writes into whichever was first and nothing says which — the mapping is declared, the uuid is the published identity that survives a reseed, and a second declaration for one uuid is refused rather than overwriting. The token check is fail-closed in its ORDERING: the token resolves before an objecttype is looked up and before any register is touched, because a lookup that happens first is a lookup an unauthenticated caller caused. The key is resolved by reference and compared with hash_equals, and a declaration carrying a literal key is refused — that is how a key reaches an export. The leak guard is an allowlist, not a blocklist. A blocklist is correct until OpenRegister adds a field, and then it ships that field to every counterparty in the landscape in a release nobody connects to the leak. Named rather than hidden: the 404/403 split lets a valid token enumerate which objecttypes exist. That is the standard's shape, and a test asserts it.
Both standards' routes are served now, and the PR body's tables say route by route what a consumer reaches. Two things are still not true of this facade and are named rather than implied. The geometry search has been run against NO spatial engine. It is a haversine over the candidate set in PHP, deliberately, so it needs no PostGIS — and that is also its limit: on a large register it reads more rows than a spatial index would. The tests exercise the arithmetic against fixed coordinates with no database at all. A write is REFUSED with 503 while the objecten kanaal is unwired. The requirement pairs writing and announcing in one sentence because they are one contract: an object that changed without an announcement is an object the landscape does not know changed. A loud temporary refusal beats a quiet permanent divergence, and it means this cannot ship as an unannounced write by accident. A list with no type is refused rather than answered with everything, because the token carries a permission per objecttype. A malformed data_attrs clause matches nothing rather than being ignored. An unevaluable search refuses rather than widening. count is the total, not the page size. A PATCH merges. And every one of the ten routes runs the token check first, asserted structurally so a route added later fails.
…not be sure of (#2076) Finishes tasks 1 and 2: the two bindings, the source resolver, the intake, the endpoint, and the lookup that turns a ringing number into a person. The lookup is where the wrong-person risk lives, so nearly all of it is refusals. The comparison is exact over full E.164 strings: not a suffix match, not the last nine digits, not a LIKE. +31612345678 and +49612345678 differ only by their prefix and are different people, and an agent reads the panel and says a name out loud before anybody can check it. More than one party on a number is no match either, because a shared landline is common and picking the first is picking at random. When no party matches there are no open cases either: a case list beside a null caller is somebody's cases on screen under unknown caller. The search seam is its own interface, kept off KlantinteractiesProviderInterface, because adding a method there would force the log sandbox and the REST client to answer a question neither was written for. Until a binding implements it every caller reads as unknown, which is true and visible. Two refusals on the way in. A binding that throws during verification has not said yes, so the request is refused. And a source naming a binding this instance does not have is refused rather than falling back to the sandbox, unlike the KISS resolver it otherwise mirrors: a typo would look like a working integration quietly delivering to a log file. A test harness bug found on the way: an arrow function captures by value, so the cache double never saw its own writes and every retry looked like a first sight. The test would have passed for a deduplicator that does nothing.
… ones I had missed (#2079) Three wrappers checked whether OpenRegister's context class existed, elevated if it did, and otherwise called the operation plainly. That fallback is not a graceful degradation: it runs the identical write as whoever is signed in and returns the same value the elevated call would have, so nothing distinguishes it from having worked. The wrong principal is recorded, or a permission check fails somewhere unrelated for a reason nobody traces back to a missing class. SystemWrite is now the one way to say it. The guard stays, because integriq genuinely cannot hard-depend on OpenRegister; the fallback goes, because answering "no" by doing it anyway was what was wrong. The repo-wide test found more than the three. SynchronizationContractService and SynchronizationService called the context UNGUARDED in four places, a fourth shape that does not degrade but would fatal if OpenRegister were absent, and that a scan for class_exists never sees. Seven sites across five files now go through SystemWrite, which prefers assertSystem so the elevation is verified rather than merely entered. Three other files name the context only in comments. The test looks for actual use rather than the word, so that documentation stays. The refusal is split into refuseWhenUnavailable(bool, string) because inside run() the branch is unreachable wherever OpenRegister is installed, which is everywhere the suite runs: mutating the throw away reddened nothing, leaving the most important rule in the class pinned by no test. Taking availability as an argument makes the decision assertable rather than the environment. OPENREGISTER ITSELF MUST NOT GAIN THIS GUARD. It uses the context unguarded in fourteen files because it owns the class: there class_exists is always true and a guard would be dead code. A test records that so nobody aligns them. The integriq create-cascade question is now answerable: a sweep has one idiom to trust. The next lane should re-run the per-call measurement and treat a SystemWrite::run site as system-written and everything else as acting-user.
…d filinq (#2080) * feat(document generation): the vendor seam, and a vendor nobody could reach says so One provider interface with a log binding, SmartDocuments and Xential. Filinq asks for a render with a typed command, integriq tracks it as a job, resolves the credential by reference through the broker, and announces the outcome. Three rules the job holds. It records a hash of the merge data and never the data, so integriq never ends up with a second copy of the case. A vendor nobody could reach is unreachable, not failed: the vendor refused nothing, the render is unfinished, and nothing is announced while nobody knows whether a document exists. And a render is filed as complete only once its document has been fetched whole: reported done with no document, or fetched empty, ends failed with the reason. * feat(document generation): the operator half, the sweep, and the docs Two administrator endpoints: read a vendor's own template list for a source, and activate a source only when it can actually render. A vendor source with no credentialRef is refused there, not at the first render, where the refusal would arrive as a failed beschikking. A five-minute sweep polls every queued and unreachable render, which is what makes unreachable a state rather than a dead end. No bespoke Vue source form: these sources are configured through the surfaces every other integriq source uses, and the tasks file says so.
Kept both route blocks: the CTI events endpoint and the objecten API from the integration branch, and the digital post provider listing from development. The kcc-cti-adapter tasks file and the app version come from the integration branch, which is further along on both.
… clear the new gate and phpcs findings
The merge of development into parity/round2 kept our side of appinfo/info.xml
whole, and our side did not have the two `<job>` lines development added with
the digital post adapter. The classes came across with the merge, their tests
came across, and nothing registered them, so DigitalPostStatusJob and
DigitalPostInboundJob were two TimedJobs that cron would never call: a letter
handed to a provider would have stayed "on its way" for good, and nothing that
arrived would have reached the intake inbox. Both are back where development
had them.
Gate 7 read DELETE /api/v2/objects/{uuid} as an unscoped lookup. The guard is
real - the token verdict runs first and the write handler resolves register and
schema from the objecttype the token was approved for - but the method also
took the caller's uuid straight to the delete while its siblings resolve it
first. It now does what they do: the principal is resolved once instead of
twice, and a uuid belonging to another objecttype meets the read path's 404
rather than a delete that was never authorised for it.
The rest is the code standard on lines this branch wrote: named parameters on
the twenty-two internal calls that were missing them, and twelve ternaries
written out as the conditionals they are. No behaviour moves; the Objecten and
document generation suites, 84 tests, pass unchanged.
Inherited findings are left alone and reported rather than fixed.
…t that can fail, and the spec tags this branch owed Ten routes were #[PublicPage] with no volume ceiling, which ADR-082 exists to stop: anonymous callers, no session, nothing bounding how often they may ask. Reads take 600 a minute and writes 120, which is a supplier's integration rather than a browser. The Objecten controller had no test of its own at all. Its handlers did, and a handler test cannot see the one thing only the controller decides: whether the token verdict is reached before anything opens a register. Ten routes are now asserted to answer 401 without a token, per route rather than once, because a route that forgot the call would pass a single-route test. That test also found the delete was not theoretical. Mutating the uuid resolution away reddens the assertion, not the setup, and the old code answered 204 for an object belonging to an objecttype the token was never approved for. Gate 7 was right. Fifty-five methods carry the @SPEC their class already carried. Two in SystemWrite carry a reason-bearing exclude instead: there is no change to cite. The tag on SystemWriteTest named openspec/changes/three-degrading-system-writes/, which was never written, in this repo or any other, and a citation that resolves to nothing reads as coverage while stopping anyone from looking. It is gone, and the docblock says why. Left alone and reported instead: eleven schemas short of demo data, ten of them identical to development and one ours, because the regenerator rewrites the whole mock register and dropped 7,521 lines of curated data on the first try. Four manifest strings have no Dutch key; this branch touched no manifest.
…son beside every suppression Twenty-nine findings sat on lines this branch added. Four are now gone for real: the haversine term is $haversine rather than $h, the SmartDocuments binding is $smartDocuments, and the two named arguments in the tests moved with it, which is how the rename got caught at all. The rest are suppressed, and every suppression carries the sentence that makes it checkable. RenderOutcome's queued(), rendered(), failed() and unreachable() are that value object's named constructors, so static access to them is how it is built; the alternative is a `new` with four positional arguments and no name for the state being expressed. ZgwSetCatalogue is a catalogue of constants with nothing to instantiate. $argument on a TimedJob is Nextcloud's signature. The boolean flag on refuse() and verdictFor() is there because a token carries read or read-and-write PER objecttype, so one lookup answers both questions and two methods would be two chances to call the wrong one. Ten of the Objecten controller's eleven public methods are the VNG standard's own routes, and splitting the class to get under the threshold would put the single token check in two places. A suppression nobody can argue with is how five analysers came to read zero while a thousand findings sat behind them, so none of these is bare.
…ollaborator nobody asked PHPStan named two things on lines this branch wrote, and both were real. SystemWrite::run() preferred OpenRegister's `assertSystem()` and fell back to `run()` "only for an OpenRegister old enough not to have it". No OpenRegister has it: `assertSystem` appears nowhere in that repository, and the stub the whole suite runs against does not declare it either, so `method_exists()` answered false in production and in the tests alike. The docblock read as a verification the elevation never got. Deleted rather than stubbed, because adding the method to the stub would make the suite assert an API the peer app does not publish, and the remaining path is the one that was always taken. CtiEventIntake took a CallEventNormaliser and never read it. Normalising is the provider's job — `$provider->normalize()` in handle() — and both bindings take their own normaliser and use it. The unread one is gone, along with the argument in the test that constructed it. Also wrapped two @SPEC exclude lines that ran past the 150-character limit.
…ct we already ship (#2081) Parity ledger row 1.9, the Teams third of it. Outlook is already answered by the graph mailbox transport and .msg import, and an Office document by the Files action once it is in Nextcloud. Teams is answered by nothing, and what it needs is one adapter and one signature scheme: Teams signs an outgoing webhook with Authorization: HMAC <base64> and no timestamp, which none of openconnector, stripe or github reads. Names the thing that makes this worth building: nothing in dossiq listens to IntakeMessageRoutedEvent today, so every channel we receive is held. No code in this pass.
The CI format leg is not part of check:strict, so it went unrun all night. It names nineteen files; eighteen are development's and stay that way.
…tops shouting `check:schema-l10n` is a CI leg that check:strict does not run, so nobody had looked at it all night. It counts 284 schema strings with no catalogue key against a baseline of 1111. Measured on an origin/development worktree, that number is 263 there, so twenty-one of them were this branch's, all of them the document generation job's own titles and descriptions. They now have an English identity key and a Dutch translation, and the uncovered count matches development's exactly again. The schema title was "Document Generation Job". Title Case is banned outright by voice.md section 8 and it is the first thing a Dutch reader sees on the register page, so it is "Document generation job" now, sentence case like every other title in the file. While in the same two files, four manifest strings that gate 102 names had no Dutch key at all and rendered English inside a translated form: Adres, Zaak, Ingeschakeld, Reden. They are development's strings, not this branch's, and they are fixed here only because the exception for inherited debt is exactly this shape: already in the file, under a minute. `npm run l10n:build` regenerated en.js and nl.js; `check:l10n-js` agrees.
PHPStan's only `method.notFound` in the app: `IRootFolder::get()` answers a Node, and `getContent()` is declared on File. For a real file it worked, because the concrete class is a File. For a folder it did not, and the catch-all two lines below turned the resulting error into "could not be read" — the sentence written for a corrupt delivery. An operator who typed a directory path was sent to look at the file. The node is checked before it is read, and the refusal names what is wrong. The new clause that rethrows InvalidArgumentException is load-bearing rather than tidy: without it the Throwable catch swallows the specific message and hands back the generic one, which is the thing being fixed. Mutating the guard to `if (false)` reddens the new test with the old sentence, so it pins the behaviour rather than the shape. This file is identical on development. It is fixed here because it is a defect with a one-line cause, not because the sweep is adopting inherited debt: the remaining programme-era findings in it, twelve phpcs errors, are untouched and reported.
…itten to (#2082) `digitalPostMessage` shipped in `components.schemas` and in neither `register.integriq.schemas[]` nor the test's `SCHEMA_SLUGS`. Declaring a schema is not attaching it: the schema existed on the instance and every read and write through `/api/objects/integriq/digitalPostMessage` answered "Schema not found". `DigitalPostService::SCHEMA` is that slug, and the write around it is wrapped in a `catch (Throwable)` that logs a warning, so the record of what was posted as digital post was dropped on every send and nothing errored. The letter still went; the evidence that it went did not. This is the exact shape `sync_item_dead_letter` shipped in, which is why the constant carries a comment about that one. It now carries a second. The guard written to catch it did catch it: `testNoSchemaIsDeclaredOutsideTheSlugList` has been failing, so the unit suite is red. Detaching the schema again reddens `testRegisterDeclaresAllSchemaSlugs`, so the attachment is the fix and not the constant being padded to match. 🔴 THE DEFECT ORIGINATES ON `development`, NOT ON THIS BRANCH. It arrived with 626d8da (#2062, the digital post seam), and both files are byte-identical on `development` today, so `development` is shipping a red unit suite and a silent write. It is repaired here so it is fixed when `parity/round2` lands.
rubenvdlinde
requested review from
Rem-Dam,
SudoThijn,
WilcoLouwerse,
bbrands02,
remko48 and
rjzondervan
as code owners
September 19, 2026 05:48
Contributor
Quality Report — ConductionNL/integriq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ❌ | ||||
| psalm | ❌ | ||||
| phpstan | ❌ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ❌ | ||||
| format | ❌ | ||||
| check-schema-l10n | ❌ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 141/141 | |||
| npm | ✅ | ✅ 545/545 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| lockfile sync | ✅ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it on any branch from the Actions tab, or locally with npx playwright test. |
||||
| Hydra gates | ❌ | ||||
| Hydra gates (axe) | ✅ |
Quality workflow — 2026-09-19 05:54 UTC
Download the full PDF report from the workflow artifacts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this lands
24 commits of the round 2 parity programme, closing with #2082, which attaches the
digitalPostMessageschema to the register it is written to.Before #2082 the schema shipped in
components.schemasand in neitherregister.integriq.schemas[]nor the guard'sSCHEMA_SLUGS.DigitalPostService::SCHEMAaddresses it by that slug and its write sits inside a
catch (Throwable)that only logs awarning, so every record of what was posted as digital post was dropped on send and nothing
errored. The letter went; the evidence that it went did not. The defect arrived on
developmentwith 626d8da and is repaired here.What I verified locally
Range:
origin/development (626d8dab) .. parity/round2 (9d2b6ea8), 24 commits.git rev-list --count parity/round2..origin/developmentis 0, sodevelopmentis a strictancestor and the merge base is the
developmenttip. A file this range never touches isbyte identical to
development, so anything reported in it is inherited by construction.Every number below is measured against a
git archive origin/developmentcontrol tree withthe same vendor, not assumed.
check:no-legacy-typescheck:routeslint(php -l)phpcsphpmdpsalmphpstantest:allFrontend, every leg the caller in
code-quality.ymldeclares: eslint, stylelint, vitest,check:specs,check:l10n-jsand the webpack build all exit 0. Three legs are red and allthree are inherited:
test:l10n4 missing on both,format18 files on both, andcheck:schema-l10n1372 uncovered against 1374 ondevelopment, so the branch is twobetter there.
Hydra gates,
--base origin/development --scope-to-diff: 55 of 55 applicable gates ran,53 pass. Both failures are named below.
The attachment is the binding, not a padded constant
Detaching the schema again, by reverting only
lib/Settings/integriq_register.jsonto9d2b6ea^, reddens
RegisterDescriptorTest::testRegisterDeclaresAllSchemaSlugsat line 228,on the
assertSameitself rather than onsetUp, with exactly one failure of 56.testNoSchemaIsDeclaredOutsideTheSlugListstays green under that mutation, which is thecontrol: the two guards bind different things, so the fix is the register attachment and not
the constant padded to match.
Inherited, and reported rather than adopted
phpcs: 482 errors, every one of them present ondevelopmentat the same file and count.phpmd: 123 findings, the same set asdevelopment; the only difference is two linenumbers shifting by one where this range added a line to
FileMigrationSource.psalm: 2UndefinedClasserrors onOCA\OpenRegister\Event\RegistrySubscriptionRequestedEvent.The class does exist in OpenRegister and a stub ships here; it is simply absent from the
UndefinedClasssuppression list inpsalm.xmlthat carries every other cross appOpenRegister class. Present on
developmenttoo, so inherited.gate-106cross-app-schema-slug: the slug it objects to ismessage, which is ondevelopment. The gate fired only because fix(digital-post): attach the message schema to the register it is written to #2082 touched the descriptor and brought thewhole file back into diff scope. The slug this branch adds,
digitalPostMessage, is notflagged.
gate-101demo-data-coverage: 11 schemas without demo data, 10 of them already ondevelopment.Carried in, and genuinely new
phpcswarnings, all of one kind: a missing@specPHPDoc tag on a class or publicmethod in the 22 files this range adds. No new errors.
gate-101counts one more schema thandevelopmentdoes,digitalPostMessage, and itcounts it because fix(digital-post): attach the message schema to the register it is written to #2082 attached it. While the schema was unattached the gate could not
see it. Left for the demo data sweep rather than regenerated here: the generator rewrites
the whole mock register, and on filinq a
--keeprun discarded 4451 lines of curated data.Not run in this lane, and saying so rather than reporting a green I did not earn: Playwright,
Newman, axe, the coverage guard (neither pcov nor xdebug is loaded in php8.3 on this box) and
the PHP by Nextcloud matrix, all of which need a live instance this lane may not touch.
composer installneeded--ignore-platform-reqfor ext-bcmath, ext-soap and ext-intl,which are absent here;
vendor/matchedcomposer.lockexactly at 141 of 141 packages.🤖 Generated with Claude Code