Skip to content

Release: merge development into beta - #2643

Merged
WilcoLouwerse merged 103 commits into
betafrom
development
Oct 16, 2026
Merged

WilcoLouwerse merged 103 commits into
betafrom
development

Conversation

@github-actions

@github-actions github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Automated PR to sync development changes to beta for beta release.

Merging this PR will trigger the beta release workflow.

Reminder: Add a major, minor, or patch label to this PR to control the version bump. Default is patch.

Changelist

Snapshot of development at bb940ae (16 Oct 2026) against beta at c235e06: 21 PRs. The chore(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)

Maintenance (CI, gates, specs, docs) (3)

github-actions Bot and others added 11 commits October 9, 2026 15:47
…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)
rubenvdlinde and others added 11 commits October 10, 2026 09:50
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).
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
WilcoLouwerse and others added 5 commits October 16, 2026 14:33
… 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>
WilcoLouwerse and others added 18 commits October 16, 2026 15:39
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: 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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 skip passes the end, the batch comes back empty and exhausted, resumePage goes 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()), SynchronizationAction returns ERROR and JobService waits the weekly interval. currentPage keeps 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 that resumePage/currentPage restarts 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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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).

Suggested change
if ($fetchInfo['resumePage'] > 1) {
if ($fetchInfo['resumePage'] > 1 && $fetchInfo['failureReason'] === 'batch_limit_reached') {

Done when:

  • a new SynchronizationServiceFetchCompletenessTest case runs a synchronization at currentPage: 4 against a 503 and asserts that the result has no resume key while currentPage stays 4. Removing the failureReason condition turns it red
  • JobServiceTest keeps testResumeSoonSchedulesTheNextRunOnTheNextTick green

?bool $usesNextEndpoint = null,
?bool $usesPagination = true,
): array {
if (($synchronization['sourceConfig']['format'] ?? null) === 'jsonl') {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 testTarGzEndpointShortCircuitsWithWarningAndNoObjects with 'format' => 'jsonl' in sourceConfig (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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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_reached run logs no fetch_incomplete warning; 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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.json lists the ten Job objects and Wave0GatheringConnectorRegisterFragmentTest asserts the Job version is above 1.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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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 complete full.jsonl.gz export in a single HTTP request each run … Enabled 2026-07-07"
  • austria-datagvat-connector.json:68: Synchronization description "makes the engine gunzip the .gz body 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 disabled
  • openspec/changes/native-data-gathering-provider/proposal.md:123: "keyless public feeds isEnabled: 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') {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.

Comment on lines +17 to +18
When `Synchronization.sourceConfig.format === "jsonl"`, the engine MUST NOT
hold the response body in memory. It MUST stream the response to a

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.

Suggested change
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.

Suggested change
* @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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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
rjzondervan previously approved these changes Oct 16, 2026

@rjzondervan rjzondervan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 c568b13 are the fix PRs #2686 (23) and #2692 (22) and #2690 (3). Both fix PRs' full diffs reverse-apply against dafbc43, 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, and ImportHandler updates an existing object only when the imported version is strictly higher (lib/Service/Configuration/ImportHandler.php, line 3612 on development, 3457 on beta).
  • ADRs verified: ADR-037. The fragments and Wave0GatheringConnectorRegisterFragmentTest follow 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, including quality / 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)
@WilcoLouwerse
WilcoLouwerse merged commit 825acf0 into beta Oct 16, 2026
62 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants