Repository navigation
feat(engine): atomically settle approval batches for #403 - #337
Conversation
waterbro-8
left a comment
There was a problem hiding this comment.
Review (digital-employee #337, supports roleweave #403)
REQUEST_CHANGES — not a product 验收.
CI test (22) / governance:check failed on this PR body:
- missing required headings: Canonical requirement, Requirement trace, File domains, Scope and non-goals, Validation, Security and compatibility, Known limitations, Risk and rollback, Product review handoff
- canonical Issue must be a full GitHub Issue URL
- consumed revision must match R
- no-auto-close acknowledgement is missing
- requirement trace / validation ledger table headers missing
- exact commands / observed counts / check URLs need copyable evidence or
NOT VERIFIED: <reason> - product handoff needs merge ledger owner, product reviewer, packet URL or
N/A: <reason>
Do not merge while test (22) is red. I did not treat this as product 验收.
After the PR body matches the governance ledger, ping me for a second pass. CODEOWNERS merge still requires @PeterGuy326 / @Bindy-lbb / @waterbro-8 and I cannot merge my own CODEOWNER PRs.
PeterGuy326
left a comment
There was a problem hiding this comment.
Re-review of head eba9e74e. The governance ledger waterbro-8 asked for is now in the body, and the red test (22) in the rollup is the superseded 08:15 run — the 08:32 re-run on this same head is green across the matrix including test (22). The batch gate itself is sound. One blocking finding remains, and it is in the evidence contract rather than the settlement path.
Blocking: batch recovery turns produce evidence that fails the repo's own conformance checker
turn-evidence.ts gains the approvalRefs?: TurnEvidenceApprovalRef[] property, and turn-executor.ts spreads it into every record (...(approvalRefs !== undefined ? { approvalRefs } : {})). But ALLOWED_TOP_LEVEL_KEYS in the same file is not updated, and validateTurnEvidenceRecord fails closed on any unrecognised top-level key — by design, per its own docstring: "the evidence record is a sealed digest-only artefact, and an unrecognised field signals producer drift, not forward compatibility."
So every atomic-batch recovery turn — granted, denied and expired alike — writes a turn-evidence.v1 record that the published #140 conformance checker rejects. Verified by execution against the validator (unchanged by this PR, so main is equivalent):
singular approvalRef -> true []
batch approvalRefs -> false [{"field":"approvalRefs","code":"unknown_field",
"message":"unrecognised top-level field: approvalRefs"}]
This is not a theoretical consumer: scripts/w1-acceptance-harness.mjs runs records through validateTurnEvidenceRecord, and docs/verification.md AC-004b states "all 8 archived records pass validateTurnEvidenceRecord". The PR ships a producer whose output cannot pass. The existing suites stay green only because no test feeds a batch-settled record to the validator — tests/engine/approval-events.test.ts asserts evidenceSink.records[0].approvalRefs directly and never round-trips it through conformance.
Needed:
- Add
"approvalRefs"toALLOWED_TOP_LEVEL_KEYS. - Add a validator branch mirroring the
approvalRefrules — array, 2–32 members, each with a non-emptyapprovalId, optional stringpreviewId, andoutcomein the existing enum — with dotted violation fields (approvalRefs[0].approvalId, …) so the harness can name the offending member. - Add a conformance test for a conformant batch record plus at least one rejecting case, and extend the #403 engine test to pass its settled record through
validateTurnEvidenceRecordso producer and checker cannot drift apart again.
Verified as correct
validateTurnRequestruns atturn-executor.ts:367, long before the approval gate, and it callsvalidateApprovalRequestFields— so the 2–32 bound, theapprovalIduniqueness check and the single-decision homogeneity check are all enforced before the gate readspendingApprovals[0].decision. Reading element 0 for the branch is therefore safe.- Atomicity holds at the boundary: expiry is evaluated with
.some()across the whole set before any event is yielded, so one expired member produces zeroapproval.granted/approval.deniedevents and the model port is never touched. Matches AC-004. - Three-way mutual exclusion (
approvalAction/pendingApproval/pendingApprovals) is enforced in both the engine andparseTurnEnvelope, and the envelope parser re-checks length, duplicate ids and homogeneity independently of the JSON Schema. Fail-closed at both layers. - The batch branches mirror the singular ones exactly — evidence before event,
engine.internal_erroron a sink failure,cancelledterminal with the existingAPPROVAL_EXPIRED_CODE/APPROVAL_DENIED_CODE, no new terminal reasons. - Keeping
pendingApprovalsas a second field rather than overloadingpendingApprovalleaves legacy envelopes byte-identical; the #205 v1 byte-exactness test still covers that.
Non-blocking
- GitHub code search across the org's default branches finds no producer or consumer of
pendingApprovalsoutside this branch, so the engine leg lands ahead of its only caller. Fine for a dependency PR, but #403 is not wired end to end by this merge and should not be reported as such. - The expired branch stamps
outcome: "expired"on every member, including the ones that were still valid. That is the right atomic semantics, but the record then does not say which verdict expired, so diagnosing a batch failure means recomputing expiry from the stored verdicts. Consider carrying the offending id(s). uniqueItems: truein bothturn-envelope.schema.jsonandbuildTurnEnvelopeSchema()compares whole objects, notapprovalId, so two members with the same id but differentreasonpass the Schema layer. The parser catches it; the Schema assertion is just weaker than it looks.
Not product 验收. Merge also stays blocked on waterbro-8 re-reviewing or dismissing his CHANGES_REQUESTED — that review is on this same head, so GitHub still counts it.
|
Addressed the evidence-contract blocker in |
waterbro-8
left a comment
There was a problem hiding this comment.
Review (digital-employee #337, supports roleweave #403)
APPROVE — not a product 验收. Do not merge from this review.
Re-review of head d763d4a. Previous blockers are gone:
- PR body now has Canonical requirement, Requirement trace, and the rest of the governance ledger. Canonical Issue is the full RoleWeave #403 URL; consumed revision is R1; no-auto-close is acknowledged.
- CI is green on this head, including test (20/22/24) (governance:check runs inside test (22)), coverage, commit-metadata, CodeQL.
- Evidence-contract gap from PeterGuy326 / 胡奕舟:
approvalRefsis in ALLOWED_TOP_LEVEL_KEYS, 2–32 members are validated with dotted fields, conformance covers accept + reject, and the #403 producer record round-trips through validateTurnEvidenceRecord.
Atomic batch settlement holds: the complete homogeneous set is validated before any verdict event or model consumption; mixed, duplicate, and expired batches emit no partial approval events.
Not product 验收. I cannot merge my own CODEOWNER PRs.
PeterGuy326
left a comment
There was a problem hiding this comment.
Re-review (digital-employee #337, head d763d4a)
My previous CHANGES_REQUESTED was on head eba9e74, blocking solely on the evidence-contract gap. That finding is resolved, and I am withdrawing the change request. Not product 验收, and no merge judgment from this review — @Bindy-lbb remains merge ledger owner.
What closed it, checked against d763d4a:
approvalRefsadded toALLOWED_TOP_LEVEL_KEYS, so an atomic-batch recovery turn no longer trips the validator's fail-closed unknown-field path — which was the concrete breakage (every granted/denied/expired batch record was being rejected by the published #140 conformance checker).validateTurnEvidenceRecordnow validates the array itself: 2–32 length bound, per-memberobject_required, non-emptyapprovalId,previewIdstring-when-present, andoutcomeagainstAPPROVAL_OUTCOMES— reported with dotted field paths (approvalRefs[0].outcome), matching how the singularapprovalReferrors are shaped.- The producer is pinned, not just the validator:
approval-events.test.tsnow assertsvalidateTurnEvidenceRecord(evidenceSink.records[0]).violationsis[]on the real batch settlement, so this cannot silently regress. turn-evidence-conformance.test.tscovers accept and reject in both directions.
Scope discipline is right: 54 added lines, all in the evidence contract and its tests, nothing touched in the settlement path that was already sound.
State now: 16/16 checks green on this head including test (20/22/24), coverage, commit-metadata, CodeQL; @waterbro-8 approved on d763d4a.
Canonical requirement
Delivery ownership
Requirement trace
packages/engine/src/contracts.ts,turn-executor.ts: sealed plural approval-recovery contracttests/engine/approval-events.test.tsatomically validates the complete homogeneous set before model consumptionapps/cli/turn/*,configs/turn-envelope.schema.json: envelope boundary and recovery hand-offtests/apps/turn-envelope-schema.test.tsrejects mixed and duplicate batch memberspackages/engine/src/turn-evidence.ts: plural approval evidencetests/engine/approval-events.test.tsFile domains
pendingApprovalsboundary.Scope and non-goals
This PR supplies the engine dependency for RoleWeave #403: a recovery turn can settle 2–32 homogeneous approval verdicts as one validated set before execution resumes. It preserves the legacy singular approval path.
It does not provide queue UI behavior, policy classification, external network enforcement, or a broad approval scope; those remain in RoleWeave PR #410 and its policy layer.
Validation
npm run typecheck --silent;npx tsx --test tests/apps/turn-envelope-schema.test.ts tests/engine/approval-events.test.ts;npm run build --silent;node ./scripts/governance-precheck.js --body-file pr-337-body.md --base-sha origin/main --head-sha HEAD --pr-number 337 --repo bytefolk/digital-employeenpx tsx --test tests/engine/approval-events.test.tsnpx tsx --test tests/apps/turn-envelope-schema.test.tsnpm run typecheck --silentnpx tsx --test tests/engine/approval-events.test.tsSecurity and compatibility
pendingApprovalremains supported while plural recovery is opt-in.The protocol is fail-closed: malformed, expired, mixed, or duplicate members never produce a partial set of approval events or reach model consumption.
Known limitations
test (22)result predates this repaired implementation trace; its rerun is required before merge.Risk and rollback
The primary risk is cross-version consumers that do not recognize
pendingApprovals; the sealed schema fails those consumers closed. Roll back by reverting this single commit, returning callers to the legacy singular recovery path. No persistent migration or irreversible data operation is introduced.Product review handoff