Skip to content

Release: merge development into beta - #4715

Open
github-actions[bot] wants to merge 54 commits into
betafrom
development
Open

github-actions[bot] wants to merge 54 commits into
betafrom
development

Conversation

@github-actions

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.

rubenvdlinde and others added 30 commits October 20, 2026 11:32
An example set seeds the records a demo needs, but a task lives in the
task engine, not in a register, so no fragment could put one on its
objects. A fragment may now declare tasks: a key and the task's fields
(title, description, objectUuid, state, assignee, candidateGroups,
priority, dueAt, kind, and completedAt and completedBy for a finished
one), placeholders allowed.

The check refuses an unknown field, a task without a title or object,
an assignee or completer the set does not create or adopt, a state the
engine does not map and a date that is not one. Install writes each
task once through TaskService::import (the trusted path, so a task may
be born completed), keyed example-set:<app>/<key>, records its uuid
and reports a recorded task that is gone as missing. Remove deletes
the tasks before the objects they are on.

Assisted-by: ClaudeCode:claude-opus-5-5
…0261020094809

chore(sync): carry beta back into development
A macro bound its flow by uuid, but a flow an app ships in
x-openregister-flows gets a uuid minted per installation, so no app
could ship a macro. A binding may now name a flow the same schema
declares: the save checks the declaration (a manual trigger is
required), and a run resolves the name to the stored flow by the
import's identity, name and declaring schema.

Assisted-by: ClaudeCode:claude-opus-5-5
…trail (#4722)

Suspending, reactivating, ending, retaining or archiving an organisation,
or editing its name, description or slug, left no entry on the audit
trail. Each change now writes one sealed entry keyed by the
organisation's uuid in object_uuid, with the acting user:
organisation.status_changed from TenantLifecycleService and
organisation.updated from the organisation update endpoint. Fail-soft,
like the settings and grant auditors. dossiq's organisation History tab
reads them through GET /api/audit-trails?object_uuid=.

Assisted-by: ClaudeCode:claude-opus-5-5
* feat(flow): a macro run's audit entry lists what it changed

The action.macro audit entry named the action, the flow and the run, but
not what the run did, so a page could not say "2 changes: term
suspended, message sent" (dossiq DqZaakAssistent) without opening the
run. The entry's context now carries `changes`: each completed step that
wrote an object, sent a message or created a task, by its own label and
kind, in run order.

Assisted-by: ClaudeCode:claude-opus-5-5

* feat(flow): name a macro's changes by the node's label

A run's log names a step by its node id ("set-term"), which is not what
a page should print. A flow node may carry a `label` written for people
("termijn opgeschort"); the changes on the action.macro audit entry use
it, and fall back to the id.

Assisted-by: ClaudeCode:claude-opus-5-5

* feat(flow): a labelled app step counts as a macro change

OpenRegister cannot tell what another app's node type does, so a
dossiq step such as suspending the term never counted as a change. A
completed step whose node carries a `label` now counts as kind `step`.

Assisted-by: ClaudeCode:claude-opus-5-5

* fix(flow): macro run changes pass phpmd and the controller test

CI on #4721: phpmd flagged the static MacroRunChanges::fromLog call in
the controller and its cyclomatic complexity of 11, and the controller
test still expected the macro audit context without `changes`. The
changes are now an instance method split into labelsOf() and
changeOf(), the resolver composes them (changesOf()), and the test
expects `changes: []`.

Assisted-by: ClaudeCode:claude-opus-5-5
)

PUT/PATCH /api/organisations/{uuid} let any member rename an
organisation or change its description; only a change of who holds
grant_manage asked for more. Ruben decided members only read it. The
update now checks isOrganisationAdmin (a Nextcloud administrator or the
organisation's owner) before anything is written, and answers a member
403. The grant_manage-only guard could no longer be reached and goes.

Assisted-by: ClaudeCode:claude-opus-5-5
…4724)

* feat(example-sets): a fragment may declare saved views

Dossiq's search page draws a saved search with a count alert, and a
saved view lives in the view table, not a register. A fragment may now
declare views (name, description, owner, isPublic, query, presentation,
alert) for the set's own accounts: checked before anything is written,
written once through ViewService::create (which validates presentation
and alert), recorded, proved and deleted on remove.

Tasks and views share one path: ExampleSetEngineRows holds the check
and install loops, ExampleSetEngineKinds runs every kind for the
installer and the remover, and a kind this OpenRegister cannot write
refuses its rows instead of skipping them.

The cast gains Fatma Kaya, regisseur jeugdteam (group Sociaal domein),
the handler of dossiq's jeugdwet example case, and Sanne de Vries lives
at Parkstraat 24, as DqContact draws her.

Assisted-by: ClaudeCode:claude-opus-5-5

* fix(example-sets): keep Sanne de Vries at Lindelaan 14

portaliq's fragment writes her own cases at that address (Dakkapel
Lindelaan 14), so moving her to Parkstraat 24 for the contact board
would contradict them. That move waits for a decision.

Assisted-by: ClaudeCode:claude-opus-5-5

* test(example-sets): declare the engine-row classes the example-set tests run

Every example-set test now builds the installer and remover with the
tasks and views writers, so under beStrictAboutCoverageMetadata each
test that did not list them was risky and its coverage was dropped,
which read as 0% on the changed files in the coverage guard.

Assisted-by: ClaudeCode:claude-opus-5-5
* feat(example-sets): a fragment may declare saved views

Dossiq's search page draws a saved search with a count alert, and a
saved view lives in the view table, not a register. A fragment may now
declare views (name, description, owner, isPublic, query, presentation,
alert) for the set's own accounts: checked before anything is written,
written once through ViewService::create (which validates presentation
and alert), recorded, proved and deleted on remove.

Tasks and views share one path: ExampleSetEngineRows holds the check
and install loops, ExampleSetEngineKinds runs every kind for the
installer and the remover, and a kind this OpenRegister cannot write
refuses its rows instead of skipping them.

The cast gains Fatma Kaya, regisseur jeugdteam (group Sociaal domein),
the handler of dossiq's jeugdwet example case, and Sanne de Vries lives
at Parkstraat 24, as DqContact draws her.

Assisted-by: ClaudeCode:claude-opus-5-5

* fix(example-sets): keep Sanne de Vries at Lindelaan 14

portaliq's fragment writes her own cases at that address (Dakkapel
Lindelaan 14), so moving her to Parkstraat 24 for the contact board
would contradict them. That move waits for a decision.

Assisted-by: ClaudeCode:claude-opus-5-5

* test(example-sets): declare the engine-row classes the example-set tests run

Every example-set test now builds the installer and remover with the
tasks and views writers, so under beStrictAboutCoverageMetadata each
test that did not list them was risky and its coverage was dropped,
which read as 0% on the changed files in the coverage guard.

Assisted-by: ClaudeCode:claude-opus-5-5

* feat(example-sets): Pieter Jansen joins the Zuiddrecht cast

Dossiq's boards draw the cases list as Pieter Jansen, a handler in
Vergunningen, and its saved-view boards open the row menu of a view he
owns. A fragment may only give a view to an account the set creates or
adopts, so he joins the cast with the account id `pieter`, the one the
dossiq examples already name as a participant.

Assisted-by: ClaudeCode:claude-opus-5-5
…#4737)

Spec-only change object-actions-declared-and-run. A declared action
(x-openregister-action) gains inputs as JSON Schema, a precondition,
surfaces and one body (transition, flow or inline steps lowered to the
flow engine). One list endpoint per object merges declared actions with
lifecycle transitions; one run path validates inputs, checks the right
and runs sync or async; every run writes one action.run audit entry.
Includes the migration path for dossiq, pipelinq and learniq verbs and
six open questions with recommendations.

Assisted-by: ClaudeCode:claude-opus-5-5
…nd one body

Generalises the macro binding into ObjectActionDefinition (tasks 1.1-1.5):
label, icon, surfaces, inputs (JSON Schema), when (JSONLogic), confirm,
mode, next, elevate and exactly one body (transition, flow or steps).
A named flow is a body on its own; macro: true stays accepted.

Schema save refuses two bodies, an undeclared transition, non-object
inputs, invalid JSONLogic, unknown surfaces, bulk with @object defaults,
async transitions, async writeOnly inputs, wildcard or ungranted elevate,
and elevate saved by a non-administrator. Register-level actions are
inherited at read time; a schema entry replaces the inherited one whole
and an inherited entry never elevates.

Assisted-by: ClaudeCode:claude-opus-5-5
…engine

Tasks 2.4 and the run-context half of 2.3. FlowDefinitionBuilder::lowerSteps()
turns an action's steps into a straight line: manual trigger, the new
openregister.action-inputs step, the steps, an end node carrying next.
ObjectActionStepsFlow stores that document once per content hash as an
inert system flow (disabled, ownerless) and publishes it, so a steps run
is an ordinary run readable at /api/flow-runs/{uuid}.

Schema save refuses a step whose node is unknown, whose config the node
refuses, or that branches, waits or bounds the flow, and says to bind a
flow instead. writeOnly inputs reach the steps through a request-scoped
holder and never enter the stored run context.

Assisted-by: ClaudeCode:claude-opus-5-5
Tasks 2.1-2.3, 2.5, 2.8, 2.9 and 4.1.

- GET /api/objects/{register}/{schema}/{id}/actions lists the runnable
  declared actions the caller holds the right to (inherited ones too)
  and every lifecycle transition, each with availability and reason;
  a transition a declared action names is listed once under its key.
- GET /api/object-actions/{register}/{schema} lists the bulk-surface
  actions, right checked at schema level.
- POST .../actions/{action} takes {inputs}: 404 unknown, 403 no right,
  409 action-precondition-unmet, 422 invalid inputs (per field) or a
  refusing body. transition goes through TransitionEngine::transition(),
  flow through FlowService::run(), steps through the new
  FlowService::runForAction(). Async answers 202 queued, a suspended sync
  run 202 suspended.
- A selection only runs actions offered on bulk.
- Every run writes one action.run audit entry with the inputs digest;
  writeOnly inputs are left out of the digest and the stored context.
- available-actions keeps its shape and gains a Deprecation header and
  a Link to the new list.

Assisted-by: ClaudeCode:claude-opus-5-5
…bject nodes

Tasks 3.1 and 3.2. openregister.transition takes {action, data?, object?}
and moves the item's object through TransitionEngine::transition() as the
run's identity; a refused move fails the step with the engine's message.

openregister.link-object either creates an object from {register,
schema, data} and writes its uuid into {relation} on the item's object
(appended to a list), or runs the item schema's declared
x-openregister-handoff through HandoffService. An absent implementer
fails the step with not-available instead of succeeding silently.

Both declare their BPMN kind and palette category.

Assisted-by: ClaudeCode:claude-opus-5-5
… action.macro

Task 4.2. GET /api/objects/{register}/{schema}/{id}/actions/history lists
the object's action.run entries and the older action.macro entries as one
kind, newest first, each with its run link, its changes and its outcome.
The caller needs read on the object. Older macro entries are read as they
were written; the hash chain forbids rewriting them.

Assisted-by: ClaudeCode:claude-opus-5-5
…un-action bulk job

Task 2.6. POST /api/object-actions/{register}/{schema}/{action} with more
than 100 ids now answers 202 with a previewed bulk job of the new bulk
action openregister:run-action and the link to commit it, instead of a
400. The preview asks each member the run's own checks (action, right,
when, inputs) through ObjectActionRunner::check(); the commit runs the
action as the person who started the job.

Assisted-by: ClaudeCode:claude-opus-5-5
…tor alone

The object action validator already asks the macro validator, so the
mapper needs one optional collaborator, not two; this keeps its
constructor at nine parameters (phpmd ExcessiveParameterList). Without
a validator wired the shape refusals of both still fire, and the save
warns when runnable actions were saved unverified.

Assisted-by: ClaudeCode:claude-opus-5-5
…ions

Psalm NullableReturnStatement: Register::getConfiguration() may be null.

Assisted-by: ClaudeCode:claude-opus-5-5
…ookup

RecentLensLookupCallSitesTest: an internal find must say whether it is a
lookup; this one reads the object only to write the relation back.

Assisted-by: ClaudeCode:claude-opus-5-5
rubenvdlinde and others added 24 commits October 21, 2026 13:48
…erOf match (#4740)

* test(rbac): the membership set a $memberOf operand resolves to

Assisted-by: ClaudeCode:claude-opus-5-5

* feat(rbac): MemberOfResolver reads the caller's membership set once per request

Reads the caller's rows of a membership schema without RBAC, filters them by
static where conditions and an optional validity window, and returns the
values of the named property. A malformed operand is unresolvable; a failed
read is the empty set. Both deny.

Assisted-by: ClaudeCode:claude-opus-5-5

* test(rbac): resolveOperators turns $memberOf into $in and denies what it cannot resolve

Assisted-by: ClaudeCode:claude-opus-5-5

* feat(rbac): MemberOfResolver::resolveOperators rewrites $memberOf to $in

Keeps the operator logic in the resolver so ConditionMatcher only delegates,
and splits the operand checks to stay under the class complexity limit.

Assisted-by: ClaudeCode:claude-opus-5-5
…ds (#4741)

* test(rbac): the membership set a $memberOf operand resolves to

Assisted-by: ClaudeCode:claude-opus-5-5

* feat(rbac): MemberOfResolver reads the caller's membership set once per request

Reads the caller's rows of a membership schema without RBAC, filters them by
static where conditions and an optional validity window, and returns the
values of the named property. A malformed operand is unresolvable; a failed
read is the empty set. Both deny.

Assisted-by: ClaudeCode:claude-opus-5-5

* test(rbac): resolveOperators turns $memberOf into $in and denies what it cannot resolve

Assisted-by: ClaudeCode:claude-opus-5-5

* feat(rbac): MemberOfResolver::resolveOperators rewrites $memberOf to $in

Keeps the operator logic in the resolver so ConditionMatcher only delegates,
and splits the operand checks to stay under the class complexity limit.

Assisted-by: ClaudeCode:claude-opus-5-5

* test(rbac): $memberOf on the single-object verdict and the list, count and pages

The list test runs the WHERE conditions a list builds, over the real
MagicRbacHandler, ConditionMatcher and MemberOfResolver, against an SQLite
table of three administrations: another administration's rows are excluded,
COUNT(*) is exact and the LIMIT/OFFSET pages add up to the list.

Assisted-by: ClaudeCode:claude-opus-5-5

* feat(rbac): a $memberOf match admits by the caller's membership records

ConditionMatcher::resolveDynamicValue() hands an operator object holding
$memberOf to MemberOfResolver, which returns it with $in over the caller's
set. Both SQL list paths and the PHP verdict resolve through here, so they
read the same set. Without a resolver the property is unresolvable and denies.

Two token comparisons become in_array() to keep the class under the PHPMD
complexity limit.

Assisted-by: ClaudeCode:claude-opus-5-5
* test(rbac): the membership set a $memberOf operand resolves to

Assisted-by: ClaudeCode:claude-opus-5-5

* feat(rbac): MemberOfResolver reads the caller's membership set once per request

Reads the caller's rows of a membership schema without RBAC, filters them by
static where conditions and an optional validity window, and returns the
values of the named property. A malformed operand is unresolvable; a failed
read is the empty set. Both deny.

Assisted-by: ClaudeCode:claude-opus-5-5

* test(rbac): resolveOperators turns $memberOf into $in and denies what it cannot resolve

Assisted-by: ClaudeCode:claude-opus-5-5

* feat(rbac): MemberOfResolver::resolveOperators rewrites $memberOf to $in

Keeps the operator logic in the resolver so ConditionMatcher only delegates,
and splits the operand checks to stay under the class complexity limit.

Assisted-by: ClaudeCode:claude-opus-5-5

* test(rbac): $memberOf on the single-object verdict and the list, count and pages

The list test runs the WHERE conditions a list builds, over the real
MagicRbacHandler, ConditionMatcher and MemberOfResolver, against an SQLite
table of three administrations: another administration's rows are excluded,
COUNT(*) is exact and the LIMIT/OFFSET pages add up to the list.

Assisted-by: ClaudeCode:claude-opus-5-5

* feat(rbac): a $memberOf match admits by the caller's membership records

ConditionMatcher::resolveDynamicValue() hands an operator object holding
$memberOf to MemberOfResolver, which returns it with $in over the caller's
set. Both SQL list paths and the PHP verdict resolve through here, so they
read the same set. Without a resolver the property is unresolvable and denies.

Two token comparisons become in_array() to keep the class under the PHPMD
complexity limit.

Assisted-by: ClaudeCode:claude-opus-5-5

* docs(rbac): document the $memberOf match operator

Assisted-by: ClaudeCode:claude-opus-5-5
…nd one body (#4744)

* feat(object-actions): declared actions carry inputs, when, surfaces and one body

Generalises the macro binding into ObjectActionDefinition (tasks 1.1-1.5):
label, icon, surfaces, inputs (JSON Schema), when (JSONLogic), confirm,
mode, next, elevate and exactly one body (transition, flow or steps).
A named flow is a body on its own; macro: true stays accepted.

Schema save refuses two bodies, an undeclared transition, non-object
inputs, invalid JSONLogic, unknown surfaces, bulk with @object defaults,
async transitions, async writeOnly inputs, wildcard or ungranted elevate,
and elevate saved by a non-administrator. Register-level actions are
inherited at read time; a schema entry replaces the inherited one whole
and an inherited entry never elevates.

Assisted-by: ClaudeCode:claude-opus-5-5

* refactor(object-actions): SchemaMapper takes the object action validator alone

The object action validator already asks the macro validator, so the
mapper needs one optional collaborator, not two; this keeps its
constructor at nine parameters (phpmd ExcessiveParameterList). Without
a validator wired the shape refusals of both still fire, and the save
warns when runnable actions were saved unverified.

Assisted-by: ClaudeCode:claude-opus-5-5

* fix(object-actions): a register without configuration reads as no actions

Psalm NullableReturnStatement: Register::getConfiguration() may be null.

Assisted-by: ClaudeCode:claude-opus-5-5
Restacks after the squash merge of #4744; conflicting hunks keep the branch's content.

Assisted-by: ClaudeCode:claude-opus-5-5
Restacks after the squash merge of #4744; conflicting hunks keep the branch's content.

Assisted-by: ClaudeCode:claude-opus-5-5
…st-run

Assisted-by: ClaudeCode:claude-opus-5-5
Restacks after the squash merge of #4744; conflicting hunks keep the branch's content.

Assisted-by: ClaudeCode:claude-opus-5-5
Restacks after the squash merge of #4744; conflicting hunks keep the branch's content.

Assisted-by: ClaudeCode:claude-opus-5-5
Restacks after the squash merge of #4744; conflicting hunks keep the branch's content.

Assisted-by: ClaudeCode:claude-opus-5-5
…-nodes

Assisted-by: ClaudeCode:claude-opus-5-5
…story

Assisted-by: ClaudeCode:claude-opus-5-5
…bulk

Assisted-by: ClaudeCode:claude-opus-5-5
…teps

feat(object-actions): inline steps lower to a stored flow on the one engine
…ist-run

feat(object-actions): one list and one run path for actions on an object
…odes

feat(object-actions): openregister.transition and openregister.link-object nodes
…istory

feat(object-actions): an object's action history reads action.run and action.macro
feat(object-actions): a selection above 100 becomes an openregister:run-action bulk job
…4753)

POST /api/flows/{id}/dry-run {register, schema, objectId} walks the flow
through the real engine with a preview dispatcher. Steps that declare
IFlowSideEffectFree (set-fields, switch, route, filter, merge, explode,
end, the triggers) run, so branches are decided as in a run; every other
step is described and not performed. object-write previews its fields;
send-email, send-notification and send-talk-message preview their
composed recipients and text (FlowMessagePreview, recipients resolved
through FlowRecipientAddresses, which now owns resolveRecipients). The
marking lives on an unsaved run record, the object is read with the
caller's RBAC and without a read audit, and nothing is sent. Gated by the
flow.update right.

Assisted-by: ClaudeCode:claude-opus-5-5
A fragment's task may declare performerType: external with a
party:<reference> assignee and uploadRules (required, maxFiles,
maxSizeBytes, acceptedTypes). Install writes the rules as
metadata.upload, where the portal task seam reads them, and raises the
task as the fragment's app. The check refuses an external task that is
not addressed to a party, one marked done by completedBy, upload rules
on an internal task and an unknown upload rule.

Assisted-by: ClaudeCode:claude-opus-5-5
The task engine keeps a checklist on a task and the task page draws it,
but ExampleSetTasks refused the field, so no example task could show
one. The check now accepts a list of {id, label, checked} items and
refuses a malformed one before anything is written.

Assisted-by: ClaudeCode:claude-opus-5-5
…its objects (#4760)

A follower is a row in openregister_watchers. The loader had no part for
it, so no example case could show who follows it. A fragment's watchers
rows are now checked, written through WatcherMapper::subscribe, proved
and taken away on remove. The Zuiddrecht cast gains Anouk Bakker, who
follows dossiq's Woo case on the DqZaak board.

Assisted-by: ClaudeCode:claude-opus-5-5
checklistRefusal() pushed ExampleSetTasks over phpmd's class and method
complexity thresholds (53 > 50, cyclomatic 13, NPath 366), failing
check:strict on development. The check moves, unchanged in behaviour,
into ExampleSetTaskChecklist.

Assisted-by: ClaudeCode:claude-opus-5-5
#4762)

The dossiq boards name the team Woo en informatie: Pieter works in it,
Anouk Bakker leads it, and it is the team on the Lindelaan Woo case. The
cast gains the group, Pieter and Thomas join it, and Anouk becomes its
team leader.

Assisted-by: ClaudeCode:claude-opus-5-5

This branch has not been deployed

No deployments
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.

1 participant