Audit backlog — findings that need design work, not a FIX¶
Findings surfaced by the code-review audits whose remediation exceeds a one-shot
FIX — they change semantics or a contract, so they need an ADR update and/or
a dedicated SRD rather than a defect patch. Parked here for future research and
development; each entry names the governing docs to amend. A FIX-track item, by
contrast, lands as docs/fix/FIX-NNN.
Graduated: AB-001 (keyless
ParallelEventsstart gate double-instantiates) landed as SRD-033 (2026-07-02) — implementing the already-decided ADR-005 v.4 §2.12.5(7) validation rule, so it needed a landing SRD, not the ADR decision this backlog assumed.
AB-002 — UserTask interactor lifecycle & rendering multiplicity¶
- Source:
docs/audit/code-review-third-pass-2026-06-29.md§2.8, §2.9 (both 🟠 P2, Active). - Code:
pkg/model/activities/user_task.goExec(:170-209),pkg/model/activities/user_task_options.goWithRenderer(:60-81); registrator contract inpkg/model/hinteraction/renv.RuntimeEnvironment.
Two defects in the User Task ↔ interactor boundary. Grouped as one design track because both turn on how the human-interaction subsystem is supposed to behave (rendering multiplicity; task lifecycle on instance teardown), not on a local patch — they belong with a future Service/User-task interactor design pass (cf. [[project_manualtask_decision]], the bundled Service/User-task refactor).
§2.9 — UserTask.Exec ignores context (goroutine leak). Exec takes
_ context.Context (:171) and blocks on for d := range rCh (:194) until
the registrator closes the channel. On instance cancel/timeout the track
goroutine never observes ctx.Done(), so a never-completed human task pins the
goroutine + its scope frame forever. Siblings (ServiceTask, ReceiveTask)
already select on ctx.Done(). The fix is more than a select: the
RenderRegistrator contract has to define what cancellation means for a
half-entered human task — does the registrator get an Unregister/cancel call,
is partial input discarded, who closes rCh? That contract decision is the
design work.
§2.8 — WithRenderer rejects a second renderer of the same implementation
type. The dedup predicate (user_task_options.go:70) rejects when
r2c.Implementation() == r.Implementation(), but Implementation() is a fixed
type marker (e.g. "##consInputRender"), so two distinct console renderers
with different IDs collide and the second is refused. BPMN permits one-or-more
renderings of any kind (hinteraction/rendering.go). The narrow fix is "dedup by
ID only", but the right scope is the rendering-multiplicity contract (may a task
carry N renderers of the same impl? how are they composed/ordered?) — a
hinteraction-design question.
Governing docs to amend. A hinteraction/interactor ADR (none exists yet —
the subsystem is pkg/model/hinteraction + internal/interactor) plus the
landing SRD; fold into the planned Service/User-task interactor refactor.
Status: Resolved by ADR-020
+ SRD-034 (landed on feat/human-interaction-model, 2026-07-03) — the subsystem
got its hinteraction/interactor ADR (the human-interaction execution model).
§2.9 is dissolved by construction: UserTask is now a wait node parking cooperatively
on the loop-owned evtCh (cancellable via ctx/teardown), so there is no
uncancellable blocking loop; §2.8 is fixed by dedup-by-ID()-only. The whole
blocking interactor contract (Registrator/RenderRegistrator) was removed.
Originally parked 2026-06-30 (pulled from the FIX-014 P2 cluster: the other two —
§2.1 GExpression.Evaluate, §2.11 bpmncommon.Error.Structure — were already
resolved by FIX-010, so the cluster yielded no FIX).
AB-003 — Unspecified-direction gateway: enforce merge-or-split?¶
- Source:
docs/audit/code-review-third-pass-2026-06-29.md§3.5 (🟡 P3, Active validation gap). - Code:
pkg/model/gateways/gateway.gotestDirectionFlows, theUnspecifiedcase (:295-298) acceptsinCount >= 1 && outCount >= 1— so a 1-in/1-out gateway passesTestFlows.
Problem. The audit asks to tighten the Unspecified rule to merge-or-split
(inCount >= 2 || outCount >= 2), citing a BPMN mandate that "a Gateway MUST
merge or split". That normative rule is NOT in the vendored spec extract:
docs/bpmn-spec/elements/gateways.md lists gatewayDirection ∈ {Unspecified
(default), Converging, Diverging, Mixed} and docs/bpmn-spec/semantics/
gateways.md gives per-type token rules, but neither forbids a 1-in/1-out
gateway. So the fix rests on an unverified standard-claim.
Why it's not a FIX. Two coupled decisions, neither mechanical:
1. Verify the mandate against the full BPMN 2.0 spec (and the BPMN NotebookLM)
with an actual §-pin — per the project's spec-grounding rule, an
asserted-from-memory standard-claim can't drive a validation change.
2. Policy: tightening rejects processes that validate today (1-in/1-out
Unspecified gateways). The project rule is standard-default + opt-in
relaxation ([[feedback_parametrize_relaxations_default_standard]]) — so if the
mandate holds, the change likely needs a relaxation parameter, not a hard
reject. That is ADR-005 (gateways) work.
Governing docs to amend. ADR-005 (gateways-and-joins) — the
direction-conformance decision + any relaxation knob; docs/bpmn-spec/
elements/semantics gateways as the authority to pin.
Status: Parked 2026-06-30 (pulled from the FIX-014 P3 sweep; the other 11 sweep findings are mechanical and proceed as FIX-014).
AB-004 — depguard core-import rule blocks the runtime server under per-module lint¶
- Source:
docs/audit/code-review-third-pass-2026-06-29.md§3.17 (🟡 P3, Latent — server stub). - Code:
.golangci.yml:39-50(thecore-no-runtime-no-adaptersdepguard rule,filesglobs include**/cmd/**/*.go);runtime/cmd/gobpm-server/ main.go(a stub today, noruntimeimport yet);Makefile:132-139lint-all-modulesrunscd $$dir && golangci-lint run --config=$(CURDIR)/ .golangci.ymlper module.
Problem. The rule denies core importing …/runtime / …/adapters. When
the ADR-004 server is wired, runtime/cmd/gobpm-server must import …/runtime
— and make ci (via lint-all-modules) will deny it, failing lint.
Why it's not a quick fix. lint-all-modules cds into each module and runs
golangci with module-relative paths and the forced root config. Inside
runtime/, the server's path is cmd/gobpm-server/main.go — identical to the
core module's cmd/…. So a path-glob carve-out (!**/runtime/cmd/**) can't
distinguish the two, and a runtime/.golangci.yml is overridden by the forced
--config. The real remedy is a small multi-module lint-config strategy
(give runtime its own config without the core-import deny, and stop forcing the
root config — or a runtime-scoped rule), a decision best made and validated
against a real runtime import, not blind against a stub.
Governing docs to amend. ADR-003 (layering — the core-import rule §4.4)
and ADR-004 (runtime server). Do it when the server actually wires runtime.
Status: Parked 2026-06-30 (pulled from the FIX-015 CI/build-hardening cluster; the other four — §2.12, §3.15, §3.16, §3.18 — are mechanical and proceed as FIX-015).
AB-005 — Structured ItemDefinition composition / decomposition (multi-variable Data{Input,Output}Associations)¶
- Source: surfaced during the SRD-036 Service Task external-worker design (input/output marshalling) — "how does a worker get more than one process variable?".
- Code:
pkg/model/service/operation.goBindInputresolves a single scope variable by the message item's ID (r.GetDataByID(msg.Item().ID())→msg.Item().Structure().Update(...));pkg/model/bpmncommon/message.goMessagecarries a singleitem *data.ItemDefinition;pkg/model/data/item.goItemDefinitionalready supports a compositeStructure()value andisCollection.
Problem. An Operation's input/output is a single Message → a single
ItemDefinition, and gobpm binds it from one scope variable by ID. It does
not (a) assemble several distinct process variables into one structured
input item (a DataInputAssociation with multiple sources), nor (b) spread a
structured output item's fields back out into several process variables (a
DataOutputAssociation with multiple targets). So a ServiceTask/worker that
needs several process variables must have them pre-combined into one
structured variable, and cannot scatter a structured result across variables.
What's needed (bidirectional).
1. Compose — put multiple data.Data values (variables and other data) as
items into a structured ItemDefinition (gather → structure), to build a
worker's single input message from many process variables.
2. Spread — destructure a structured ItemDefinition's fields out into
separate process variables (structure → scatter), to land a worker's single
output message into many process variables.
Why it's not a quick fix. This changes the data-flow semantics (how
ItemDefinitions compose from / decompose to process variables) — a model /
contract change touching the data layer, the association model, and operation
binding — not a defect patch.
Governing docs to amend. ADR-011 (Process Data Flow — the data-binding model), plus a dedicated landing SRD.
Status: Parked 2026-07-06. Job.Input stays a single *data.ItemDefinition
(the operation contract is one message); this composition/spread is a data-layer
concern orthogonal to the SRD-036 job-queue rework.