Repository navigation
Release: merge development into beta - #2643
Conversation
…0261009154751 chore(sync): carry beta back into development
…on window
An administrator opens a bounded window per source (POST/DELETE
/api/sources/{id}/body-capture); calls store their bodies only while it is
open, a failure keeps its redacted request for a replay, and the cleanup job
strips expired bodies and keeps the record. Woo row 13.23, decision D5.
call_log gets an admin-only authorization block (Q-integriq-1, decision 137). Integriq's own code reads and writes call records past it: CallRecorder, CallService's early error log and EndpointService's inbound rows, so a call a non-admin triggers is still recorded and CallLogController still serves holders of the call-log permissions. Newman folder 12 probes the refusal live.
Ruben decided on 2026-10-09 that connection health belongs to integriq and certificates to keepiq. A source shows its performance and its keepiq certificate's expiry with a renew link, opens expiry alerts, takes mTLS material by keepiq reference, and accepts pipelinq's BRP settings once. Spec only.
feat: build open OpenSpec changes (integriq, part 2)
fix(call-log): only admins read call records through the generic API (integriq, part 3)
…cuments through typed commands
Microsoft365Adapter::search() posts Graph /search/query for drive items with
the source's application grant and answers REQ-DCC-004 hits with the fetch
handle driveItem:{driveId}:{itemId}; mail and chat answer
delegated-grant-missing until sources-per-user-oauth lands. fetch() hands over
a drive item's name, type and content and keeps no copy.
DocumentSearchRequestedEvent / DocumentFetchRequestedEvent with listeners.
connectors-graph-document-search tasks 1 and 3 (live tenant search owed).
…property names Decision 178 (Q-dossiq-L4b-1): the BRP and KvK providers map source field names (naam, verblijfplaats, handelsnaam) onto the target schema's properties, per target schema slug. The roster records which schemas follow an identity when the subscription goes active, and the poll job posts one update per target schema. Dossiq's brpPerson and kvkCompany refreshes were refused with a 422 before this. Change: registry-update-maps-source-fields (tasks 1-3; 4 is the live pass).
…caller id is declared, not forgotten
creeerZaak, actualiseerZaak, geefZaakdetails, genereerZaakIdentificatie and vrijBericht move from dossiq into integriq's StufZkn as a ZDS client on the StUF-ZKN source: message builder, transmit() on StufZknClient, response reader, circuit breaker, credential resolver, audit log, retry job, and the ADR-041 StufZdsCaseRequestedEvent / StufZdsCaseConcludedEvent seam.
StufZdsOutbound owns audit, transport, classification, retry and the conclusion event (the dossiq StufOutboundTransport half); StufZdsCaseClient keeps the five operations. Long lines wrapped, post() docblock completed.
…field-map feat(registry): post BRP and KvK changes in each target schema's own property names
…iq-graph-document-search
… sensitive Both roster maps in app config are keyed by a followed BSN or KvK number and were stored non-lazy and non-sensitive: Nextcloud loaded them on every request and occ config:list printed them. They are now written lazy and sensitive and read as lazy values, which also finds a value an older release stored eagerly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…analytics Since call_log became admin-only, the product analytics read ran with RBAC on, so a delegated integriq administrator, who passes #[AuthorizedAdminSetting], was shown zero traffic for every product. The rows are read with RBAC and multitenancy off, like the product lookup before it; only aggregates leave the controller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts as closed REQ-OCD-008 limits an investigation window to the instance maximum, but only BodyCaptureController::open() checked it. A bodyCaptureUntil written through the generic source API, a source update or an import opened a window of any length without a reason. isOpen() now also requires the end to lie within maxHours() of the call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…instance maximum Three tests opened a window ending in 2099 or 2999. With a window beyond the 72-hour maximum now counting as closed, two of them failed and the third no longer proved its point (no bodies even inside an open window). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The clear check flagged every element of the new object that carried noValue or xsi:nil, nested relations included. Senders that write each empty optional element that way, integriq's own translators among them, had every W refused. An element now counts as cleared only when it is a direct child of the new object and the old object holds a value for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…outcome The breaker's reset and recordFailure write app config and could still throw after the case system answered: the listener then refused a zaak that existed, or a failed attempt left the row retrying with no job. Both calls now go through breakerRecords(), which logs a failed write. A message concluded as RETRY_NOT_SCHEDULED keeps the last attempt's fout code in its details. Tests cover the breaker failures, an answered question whose row cannot be saved, and a cooldown that cannot be rescheduled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; scrub invalid UTF-8 The full refusal text went into a warning log line, where free text from the owning app can carry identifiers. The log now gets the same bounded line the case system receives. Invalid UTF-8 made preg_replace() return null, so the reason was lost; it is scrubbed first. A receive() test proves the Fo03 carries the bounded line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The requirement said each new confirmation dispatches the conclusion event; the receiver now announces only the first and logs a later one with another code. The requirement says so and names why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ute beyond is closed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; phpcs line length The breaker guard takes the breaker call as a callable instead of branching on a flag, and RETRY_NOT_SCHEDULED keeps the last attempt's fout code in details as is, so the class stays at the phpmd complexity threshold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One branch fewer keeps StufProviderReceiver under the phpmd class complexity limit once #2686's query checks land beside it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bject; refuse clears in a W without one The previous fix compared only direct children by their text, so a cleared relation (heeftAlsInitiator) or a cleared value inside one (inp.bsn) did not count and was answered Bv03. Every nil element of the new object is now compared with the old object at the same path of local names. A W that carries a single object has nothing to compare with, so its empty elements count as clears and it is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…not stranded retry() called the breaker's allows() unguarded. Once a cooldown has passed it resets the breaker, an app-config write; when that threw, the retry job had already been consumed and the row stayed retrying with no job and no conclusion. All breaker calls now go through breakerCall(), which logs the failure and returns a fallback; allows() fails open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hronous confirmation The requirement claimed the owning app never receives two contradicting conclusions, but the synchronous answer at send concludes the row too and the first asynchronous confirmation still overrides it. The text now says so, and a scenario covers a contradicting later confirmation. The test points at this requirement instead of the retry one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wide characters Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s as a coupling Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The clear check ran an ancestor lookup and a path lookup in the old object for every empty element, so a W with thousands of them took seconds per request (5000 took about 6 s), from any source holding the token. Both objects are now walked once, collecting the paths that hold a value and those sent empty, and the clears are their intersection. Tests cover an old relation that held nothing and a W with 5000 empty elements. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(review): #2643 review findings, round 1
fix: second round of review fixes for the development → beta promotion #2643
testARefusedWriteSendsTheRefusalOutAsOneLine checked only the Fo03 body, so logging the reason as the owning app gave it stayed green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
StufZknController::inbound read php://input whole before checking the HMAC signature. It now reads through StufZknRequestBody, at most one byte past the 2 MiB ceiling the provider endpoint uses, and answers 413 for a larger body before the signature or the XML is looked at. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r side reads The zakLk01 creeerZaak carried the zaaktype as <zkn:zaaktype>, the initiator's BSN directly under <zkn:gerelateerde> and no <zkn:parameters>, so integriq's own StufProviderOperationMapper read it without a zaaktype, an initiator or a mutatiesoort. The message now carries parameters/mutatiesoort T with indicatorOvername V, the zaaktype under isVan/gerelateerde (ZAKZKT/ZKT), and each person under <role>/gerelateerde/natuurlijkPersoon. A round-trip test feeds the built message to the mapper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| } | ||
|
|
||
| $this->writeBufferedBodyToSink(response: ($this->callLogResponse(callLog: $callLog) ?? []), sinkPath: $sinkPath); | ||
| $batch = $this->readJsonLinesBatch(path: $sinkPath, skip: (($page - 1) * $batchSize), limit: $batchSize); |
There was a problem hiding this comment.
🟡 Concern — the batch cursor is a record offset into a file that is downloaded again on every tick
Each run re-downloads the whole source (callSourceObject(... sink: $sinkPath)) and then skips ($page - 1) * $batchSize records of that download. Nothing ties batch N to the same file batch 1 read: no ETag, Last-Modified, Content-Length or content check is stored next to currentPage (grep -ci 'etag\|last-modified\|content-length' over fetchJsonLinesBatch…readJsonLines returns 0). Austria's full.jsonl.gz is described in its own fragment as a "continuously-updated" export, and one cycle is 37 batches at one per 5-minute JobTask tick, about three hours.
- Records added or removed before the cursor while the cycle runs move the boundaries, so records are skipped or read twice in that cycle. Reading twice is harmless because the upsert dedups. A skipped record is missing until the next weekly cycle, and only if it does not shift again.
- If the file shrinks so that
skippasses the end, the batch comes back empty andexhausted,resumePagegoes back to 1 and the cycle ends early without any log line. - If a run throws partway through the cycle (a Guzzle connect error out of
callService->call()),SynchronizationActionreturns ERROR andJobServicewaits the weekly interval.currentPagekeeps its value, so a week later the job resumes partway into a new file and skips that file's first N−1 batches. - The cost side is already in the proposal: 37 × 70 MB of downloads per cycle.
Impact: a full pass of a large JSONL source is not a snapshot of any single version of the file. Records drop silently for a cycle, and with no deletion pass (f4) nothing reports the gap.
Suggested fix: pin the cycle to one version of the file. At batch 1, store the response validator (ETag, else Last-Modified + Content-Length) on the synchronization next to currentPage. On batch N > 1, compare it with the new response. If it differs, log it and restart at batch 1. Alternatively, keep the batch-1 download in app data for the cycle and only download again at batch 1, which also removes the 37× download. Apply the same restart when a cycle resumes after an interval-length gap.
Done when:
- a new test in
SynchronizationServiceTest(e.g.testJsonlCycleRestartsWhenTheFileChangesBetweenBatches) serves batch 2 a body with two records prepended and a different ETag. It asserts that the run does not return the shifted records 3–4 and thatresumePage/currentPagerestarts the cycle. Removing the validator comparison turns it red - the REQ-006 delta states what happens when the source changes partway through a cycle
| // reported so the job can come back on the next cron tick. | ||
| if ($isTest === false && ($fetchInfo['resumePage'] ?? null) !== null) { | ||
| $synchronization['currentPage'] = $fetchInfo['resumePage']; | ||
| if ($fetchInfo['resumePage'] > 1) { |
There was a problem hiding this comment.
🟡 Concern — a failed download on batch 2 or later reschedules the job every 5 minutes, with no backoff and no limit
failedJsonLinesBatch() returns resumePage = $page. For $page > 1, line 2722 then sets $result['resume'], SynchronizationAction::withResume() adds resumeSoon, and JobService sets nextRun = now. JobTask runs every 300 s, so an upstream that answers 404/5xx partway through a cycle (a renamed export, an outage) is called again every 5 minutes until it recovers. Each attempt downloads up to the full file and writes a job log at level INFO with "Synchronized 0 successfully". The same failure on batch 1 gives resumePage = 1, no resume, and the weekly interval. Only the position in the file changes the outcome. The job-scheduling delta defines resumeSoon as "a batched source with batches left", not as a retry signal, and REQ-006 only says a failed download "MUST be incomplete and keep its batch".
Impact: pre-flag 3. The job does not starve the others, because JobService::run() takes each due job once per pass. But it keeps rescheduling itself on every tick for as long as the source fails, and hits an external feed 288 times a day.
Suggested fix: report resume only for a batch that actually read records, and let a failed batch keep its cursor and return to the job's interval (or a bounded backoff).
| if ($fetchInfo['resumePage'] > 1) { | |
| if ($fetchInfo['resumePage'] > 1 && $fetchInfo['failureReason'] === 'batch_limit_reached') { |
Done when:
- a new
SynchronizationServiceFetchCompletenessTestcase runs a synchronization atcurrentPage: 4against a 503 and asserts that the result has noresumekey whilecurrentPagestays 4. Removing thefailureReasoncondition turns it red JobServiceTestkeepstestResumeSoonSchedulesTheNextRunOnTheNextTickgreen
| ?bool $usesNextEndpoint = null, | ||
| ?bool $usesPagination = true, | ||
| ): array { | ||
| if (($synchronization['sourceConfig']['format'] ?? null) === 'jsonl') { |
There was a problem hiding this comment.
🟡 Concern — a format: "jsonl" synchronization no longer refuses a .tar.gz/.tar endpoint (regression against REQ-006)
Before this PR, the .tar.gz check in fetchSinglePageData() ran before the JSONL branch (at the parent bb940ae1^1: the isTarGzEndpoint check is at :7901 and the === 'jsonl' branch at :7939). The new branch at line 7088 returns from fetchAllPages() before fetchSinglePageData() runs, and nothing in fetchJsonLinesBatch() / readJsonLinesBatch() / readJsonLines() calls isTarGzEndpoint (0 matches). A JSONL synchronization pointed at a tar archive is now gunzipped by gzopen and read line by line. The 512-byte tar headers are glued onto the first line of each member, so that record fails to decode and is skipped without a log line. For a small archive the run is then reported complete on page 1, and the disappearance pass runs on the mis-parsed set, with only the 10 % ratio guard in the way. The bounded-jsonl-sync REQ-006 delta still says a tar endpoint "MUST short-circuit with a logged warning and an empty result". testTarGzEndpointShortCircuitsWithWarningAndNoObjects stays green only because its synchronization has no format.
No shipped connector combines the two, so this needs an admin-configured source to trigger it. That is why this is 🟡 and not 🔴.
Suggested fix: in the JSONL branch, check isTarGzEndpoint() before downloading, log the same warning, and return an incomplete empty batch (complete: false) that keeps the cursor.
Done when:
- a copy of
testTarGzEndpointShortCircuitsWithWarningAndNoObjectswith'format' => 'jsonl'insourceConfig(warning logged once,[]returned,complete === false) is red on this head and green after the fix
| returns to 1 after the last batch. Only a run that read the whole file from | ||
| its first record is a complete fetch; every other batch MUST be reported | ||
| incomplete with `failureReason: "batch_limit_reached"`, so the deletion | ||
| pass never runs on part of the source (REQ-009). A failed download MUST be |
There was a problem hiding this comment.
🟡 Concern — "never runs on part of the source" means the disappearance pass never runs for any JSONL file larger than one batch
complete is true only for $page === 1 && exhausted (SynchronizationService.php:8390). Once a file has more than pageSize records (Austria: 181,741 / 5,000 = 37 batches), no run in any cycle is complete, so deleteInvalidObjects() returns early with fetch_incomplete on every batch, forever. Each batch also logs deleteInvalidObjects: skipped — preceding fetch did not complete at WARNING and dispatches a deletion-guarded event, which is 36 of each per weekly cycle for an outcome the design makes certain. The spec text and the proposal call this a guard against "treating one batch as the whole source". Neither says that, for a source larger than one batch, deletions upstream are never mirrored and the default disappearance policy has no effect.
Impact: an admin who enables the Austria job gets records that are never removed, and a guard warning stream that reads like a fault.
Suggested fix: decide which of the two it is, and write it down. Either (a) the delta says so: a JSONL source larger than one batch does not run the disappearance pass, and a batch run does not raise the guarded warning/event (a batch_limit_reached incomplete fetch is expected, not guarded). Or (b) the cycle tracks the contracts it saw and runs the pass after the last batch of a cycle that read every batch of one file version (this depends on f1).
Done when:
- (a) REQ-006 has a scenario "a file larger than one batch never runs the disappearance pass", and a test asserts that a
batch_limit_reachedrun logs nofetch_incompletewarning; or (b) a test where the last batch's run deletes a contract absent from every batch of the cycle goes red when the cycle-level pass is removed
| }, | ||
| "interval": 604800, | ||
| "isEnabled": true, | ||
| "isEnabled": false, |
There was a problem hiding this comment.
🟡 Concern — an upgrade does not disable the ten external feeds, and only the proposal says so
The fragment edit changes the ADR-037 signature (+frag.<hash>), so importFromApp runs again. OpenRegister's object import then updates an existing object only when version_compare($importedVersion, $existingVersion, '>') holds (openregister lib/Service/Configuration/ImportHandler.php, line 3612 on development and 3457 on beta). The Job objects stay at "version": "1.0.0" in all ten fragments (unchanged against bb940ae1^1), so every instance that already imported them keeps isEnabled: true. That covers every beta install this release reaches. The PR body and proposal.md:45 say so ("Existing installs keep the enabled state"). The spec deltas do not: the changed scenario at native-data-gathering-provider/spec.md:172 is scoped to "a fresh install". On these instances, Austria's job (CC BY-NC-SA, "internal analysis only" per its own $comment) now changes from crashing to completing. It runs 37 back-to-back batches each week and downloads 70 MB each time, without anyone opting in.
Question for the author: is leaving existing installs enabled the decision? If yes, the spec should carry it. If no, bumping the ten Job objects' version (e.g. 1.0.1) makes the import write isEnabled: false on upgrade. The cost is that it also overwrites an admin's own edits to those Jobs.
Done when:
- either
git grep -n '"version": "1.0.1"' lib/Settings/register.d/*-connector.jsonlists the ten Job objects andWave0GatheringConnectorRegisterFragmentTestasserts the Job version is above1.0.0; or the native-data-gathering-provider (or bounded-jsonl-sync) spec delta has a scenario for an upgraded instance keeping its Jobs' enabled state
| }, | ||
| "interval": 604800, | ||
| "isEnabled": true, | ||
| "isEnabled": false, |
There was a problem hiding this comment.
🟢 Minor — text that still describes the Austria job as enabled, single-request and whole-file
The Job now ships disabled and reads in 5,000-record batches, but these live copies still say otherwise:
austria-datagvat-connector.json:90: Job description "Fetches the completefull.jsonl.gzexport in a single HTTP request each run … Enabled 2026-07-07"austria-datagvat-connector.json:68: Synchronization description "makes the engine gunzip the.gzbody and parse it as line-delimited JSON before the whole-document json_decode/XML fallback"austria-datagvat-connector.json:2:$comment"Needs a comfortable PHP memory_limit for the gunzip+per-line pass … verify headroom … before enabling"tests/Unit/Settings/Wave0GatheringConnectorRegisterFragmentTest.php:153: method name…AndSelfSchedulingJob, while the method now asserts the job is disabledopenspec/changes/native-data-gathering-provider/proposal.md:123: "keyless public feedsisEnabled: true"
Rewording them should come with a version bump on the touched objects only if f5 resolves that way.
| $currentPage = ($synchronization['currentPage'] ?? null) ?? 1; | ||
| // Start with the current page. A JSONL synchronization reads one batch | ||
| // per run, so its page is where the previous run stopped. | ||
| if (($source['rateLimitLimit'] ?? null) !== null || ($sourceConfig['format'] ?? null) === 'jsonl') { |
There was a problem hiding this comment.
🟢 Minor — the currentPage docblock now contradicts this line
SynchronizationService.php:867 still says "getAllObjectsFromApi() reads currentPage back under exactly one condition: the source reports a rate limit". Line 6970 adds a second condition (format === 'jsonl'). The docblock is also orphaned: it sits stacked over exceededMemoryCeiling()'s own docblock, with no method of its own. Update the sentence to name both conditions, or remove the orphan. git grep -n 'under exactly one' -- lib has a single live copy.
| When `Synchronization.sourceConfig.format === "jsonl"`, the engine MUST NOT | ||
| hold the response body in memory. It MUST stream the response to a |
There was a problem hiding this comment.
🟢 Minor — the totality "MUST NOT hold the response body in memory" has a branch where it does not hold
writeBufferedBodyToSink() exists because a source answered in-process (SOAP, case-system) returns its whole body on the call log. That body is held in memory, base64-decoded into a second copy, and only then written to the sink. The same delta's own scenario ("a body a source answered in-process … is read the same way") depends on that branch. The proposal's "the call log records no body for it" (proposal.md:20) is wrong for the same path, as is the method docblock "neither the compressed nor the decompressed body is ever held whole" (SynchronizationService.php:8338). Scope the sentence to streamed responses.
| When `Synchronization.sourceConfig.format === "jsonl"`, the engine MUST NOT | |
| hold the response body in memory. It MUST stream the response to a | |
| When `Synchronization.sourceConfig.format === "jsonl"`, the engine MUST NOT | |
| hold a streamed response body in memory (a body an in-process source returns on | |
| the call log is written to the temporary file first). It MUST stream the response to a |
| * | ||
| * @return void | ||
| * | ||
| * @spec openspec/specs/synchronization-engine/spec.md#requirement-bulk-gzipjsonl-source-ingestion-req-006 |
There was a problem hiding this comment.
🟢 Minor — the scheduling tests point at the ingestion requirement, not the job-scheduling one they cover
testResumeSoonSchedulesTheNextRunOnTheNextTick (here) and SynchronizationActionTest.php:16 both carry @spec …synchronization-engine…req-006. They pin the new job-scheduling requirement "A job with work left runs again on the next tick", which SynchronizationAction::withResume() already cites. A spec-coverage scan will report that requirement as untested.
| * @spec openspec/specs/synchronization-engine/spec.md#requirement-bulk-gzipjsonl-source-ingestion-req-006 | |
| * @spec openspec/changes/bounded-jsonl-sync/specs/job-scheduling/spec.md#requirement-a-job-with-work-left-runs-again-on-the-next-tick |
| - [x] `SynchronizationAction` reports `resumeSoon`; `JobService` schedules that job on the next tick. | ||
| - [x] Ship the ten external gathering connector Jobs disabled. | ||
| - [x] Tests: streaming to a removed temp file, batch and resume, a whole-file run is complete, a failed download keeps its batch, a 24 MB file read under a 2 MB budget with a control, a batch run deletes nothing and persists the next batch, `resumeSoon` scheduling, the fragment test asserts disabled jobs. | ||
| - [ ] Live pass: a fresh install runs cron with no integriq job pulling an external feed; enabling the Austria job imports it in batches without a fatal. |
There was a problem hiding this comment.
🟢 Minor — the one task that exercises the fix end to end is still open, and this release promotes it
"Live pass: … enabling the Austria job imports it in batches without a fatal" is unchecked. The memory figures in the PR body come from replaying the reader against the real file, not from a cron cycle on an instance. Was the live pass run before this release goes to beta? If not, the release notes should say Austria's batched import is unverified on a live instance.
rjzondervan
left a comment
There was a problem hiding this comment.
Verdict: APPROVE (Thorough, round 2)
No blocker: my round-1 findings and @WilcoLouwerse's two blockers are fixed on the head, apart from three 🟡 the fix PRs left to the author; #2690, new since round 1, adds 5 🟡 and 5 🟢. Before a merge: one approval, and every thread resolved — 25 are open (13 mine, 12 of @WilcoLouwerse's), and two of his carry my ✅ for him to resolve.
🔧 Fix PR: #2692 — merged, 6 of 7 verified
#2692 carried 7 of my findings: 6 hold on dafbc43, and the BSN roster one is only partly fixed. #2686 carried @WilcoLouwerse's round-1 findings; of those I verified his two blockers.
| Round | Found | Status |
|---|---|---|
1 (c568b13) |
🟡 6 · 🟢 3 (plus @WilcoLouwerse's 🔴 2 standing) | 6 fixed in #2692, 1 partly; 2 open; his 🔴s fixed in #2686 |
2 (dafbc43) — this review |
🟡 5 · 🟢 5 (all in #2690) | open |
| Finding | Was | Now | Where |
|---|---|---|---|
🔴 The WS-Security password is stored in stuf_message.envelope (@WilcoLouwerse) |
🔴 Wilco | resolved in eef8fb3b (#2686); his to resolve |
thread |
| 🔴 The call-log retention sweep deletes nothing (@WilcoLouwerse) | 🔴 Wilco | resolved in ac08b658 (#2686); his to resolve |
thread |
| 🟡 A 5xx with a non-XML or DOCTYPE body is a permanent failure, never retried | 🟡 round 1 | resolved in 7fcd04fc |
thread |
| 🟡 A sent message can end refused or stuck in 'retrying' | 🟡 round 1 | resolved in 7fcd04fc, 5bb7bb50 |
thread |
| 🟡 A StUF W update cannot clear a field yet is answered Bv03 | 🟡 round 1 | resolved in 3c83bcf4, 4a9e42d8 |
thread |
| 🟢 Owning app's refusal text returned verbatim to the external caller | 🟢 round 1 | resolved in 3c83bcf4, 2041a16a, 79c3aff4 |
thread |
| 🟢 A second, contradicting confirmation flips the row and re-concludes | 🟢 round 1 | resolved in e1eaaa42, 9bebaf44 |
thread |
| 🟢 Product analytics read call_log with RBAC on and silently return zero | 🟢 round 1 (no thread) | resolved in 1b368716 |
ProductSubscriptionsController::fetchProductCallLogRows() |
| 🟡 New BSN→schema roster in appconfig is never pruned | 🟡 round 1 | still open: lazy and sensitive since abe445c3, still never pruned |
thread |
| 🟡 Outbound zakLk01/zakLk02 shape disagrees with integriq's own StUF reader | 🟡 round 1 | still open (#2692: waits for a ZDS 1.2 XSD check) | thread |
| 🟡 The release tree runs on core's Symfony 6.4, which no CI job tests | 🟡 round 1 | still open (#2692: needs a choice) | thread |
🟡 A JSONL synchronization no longer refuses a .tar.gz endpoint |
new | new — 🟡 | comment |
| 🟡 A failed batch past page 1 reschedules every 5 minutes, without limit | new | new — 🟡 | comment |
| 🟡 A JSONL source larger than one batch never runs the disappearance pass | new | new — 🟡 | comment |
| 🟡 The batch cursor indexes a file downloaded again for every batch | new | new — 🟡 | comment |
| 🟡 An upgrade does not disable the ten external feeds | new | new — 🟡 | comment |
| 🟢 Connector text still says enabled, single request, whole file | new | new — 🟢 | comment |
🟢 The currentPage docblock says "under exactly one" condition |
new | new — 🟢 | comment |
| 🟢 "MUST NOT hold the response body" ignores the buffered path | new | new — 🟢 | comment |
🟢 The resumeSoon tests are tagged to REQ-006 |
new | new — 🟢 | comment |
| 🟢 The live-pass task is unticked while this goes to beta | new | new — 🟢 | comment |
Connected issues
| Issue | State | This round |
|---|---|---|
#2684 (WS-Security password in stuf_message) |
open | finished by #2686 on development, verified on the head; can be closed once this reaches beta |
| #2685 (call-log retention sweep) | open | finished by #2686 on development, verified on the head; can be closed once this reaches beta |
Details of this round
- Scope: the 48 commits since
c568b13are the fix PRs #2686 (23) and #2692 (22) and #2690 (3). Both fix PRs' full diffs reverse-apply againstdafbc43, and no merge commit carries a conflict resolution. - Analysed: #2690 only (JSONL streaming in bounded batches, external feeds shipped disabled), Thorough. Memory is bounded: the body streams to a temporary file and is read line by line, one 5,000-record batch at a time. The new findings are about what happens around the batches.
- f5, verified against OpenRegister: the ten Job objects stay at version
1.0.0, andImportHandlerupdates an existing object only when the imported version is strictly higher (lib/Service/Configuration/ImportHandler.php, line 3612 ondevelopment, 3457 onbeta). - ADRs verified: ADR-037. The fragments and
Wave0GatheringConnectorRegisterFragmentTestfollow its deep merge; the fragment signature re-runs the import, but the per-object version gate decides what an upgrade changes (f5). - Not re-verified: @WilcoLouwerse's other 10 open threads.
- CI: every check green on
dafbc43, includingquality / Hydra Gates; E2E (Playwright) was skipped.
Before the next round: each new 🟡 has a Done when in its thread; f3's is a copy of testTarGzEndpointShortCircuitsWithWarningAndNoObjects with 'format' => 'jsonl', red on this head.
🤖 Generated with Claude Code
fix: third round of review fixes for the beta promotion (#2643)
Automated PR to sync development changes to beta for beta release.
Merging this PR will trigger the beta release workflow.
Changelist
Snapshot of
developmentat bb940ae (16 Oct 2026) againstbetaat c235e06: 21 PRs. Thechore(sync)PRs that carry beta back into development are left out. PRs merged to development after this snapshot are not listed yet.The StUF-ZDS client and provider receiver ported from dossiq, BRP, KvK and BAG property sources, call-log changes, a build fix that stops shipping packages Nextcloud core already provides, and JSONL sources synced in bounded batches with external feeds shipped disabled (#2690).
Features (9)
Fixes (4)
Dependencies and security (5)
@types/nodefrom 26.6.3 to 26.6.5@vitejs/plugin-vuefrom 6.0.8 to 6.0.10Maintenance (CI, gates, specs, docs) (3)