Sales ordering v3 draft review finds normalization gaps and preserves recovery boundaries
Assignment ADR-0053/sales/002; Sales participant 01a0ff07-d3d4-7553-bf28-5314810de63e, host local; coordinator 01a0e095-f279-7fd0-a8ef-92ce8f9547fc, host local. This is a bounded independent design review, not a formal acknowledgment. ADR-0052 implementation remains held and its clean candidate 62c6c0449d8316f38dd33df2e83033208ea0703c is preserved. No runtime, database, provider, signing, issuance or deployment work was performed.
Reviewed Revenue's proposed ordering 3.0.0/recovery 1.0.0 slice at C:/Users/jallr/.codex/worktrees/revenue-adr0052/revenue/.tmp/adr0053-ordering-draft/, manifest SHA256 2c87e998b13bf2ab84885405a9c1b0c0b0da7c2758b003f4ea75c44f509a48e5. All twelve listed files matched their byte counts/hashes before copying to Sales-only review storage. Proposed document SHA256 621497e1745e91f547acf096afa853dc366635b54aa886f45c4bab82374edc85, schema 2493b1624a5d50f21d303c24edf7026c6aa11fa771a10601a1f6e77f453ebae3, canonical reference aac03686b73ac7cbd4c4a378e28a8f99242e6058cc5b27735167d6839bdb0a69. Manifest source basis is Revenue d24da76a84e7fc72dc909a1d84c748e9d11d8d65 and accepted ADR-0052 package 43cd36bc312cdda3e1e2543215df55a7ace52ab1.
The live draft changed during review: later manifest SHA256 6cb64672fdc42420d0e00a149d9420127ff8d3ea3c4709ea86ea1913df42a33a adds compensation-owner-assessment.md and changes the proposal document's compensation section. This review is pinned to the original copied snapshot. Normalizer/schema/vector hashes remained the same in that later manifest. The later compensation recommendation is not assessed or approved here. Platform's descriptor/trust slice subsequently arrived and is reviewed separately below against its own exact manifest; the two slices are not yet aligned or assembled.
Reproducible pure local evidence
Review storage is C:/Users/jallr/Documents/Sguild/repos/sales/.tmp/adr0053-sales002/. source-artifact-manifest.json retains the original manifest. review.mjs extracts the actual toWireBody function with TypeScript's AST from the preserved Sales close client and executes it in isolation, then compares its resulting wire digest with all 25 valid draft fixtures. It also executes Revenue's actual structural parseCloseRequest with its actual ID regex dependency and compares normalized values with the draft. Both sets of 25 checks pass. Offer arrays are compared by offering identity for value parity because the intentionally changed v3 scalar ordering is independently tested by the fixed byte vectors; v2 locale ordering is not rewritten.
Run from the Sales primary root:
node .tmp/adr0053-sales002/validate-draft.mjs
node .tmp/adr0053-sales002/review.mjs
python .tmp/adr0053-sales002/verify-independent.py
python .tmp/adr0053-sales002/edge-independent.py
node .tmp/adr0053-sales002/platform-snapshot/validation/validate.mjs
node .tmp/adr0053-sales002/cross-slice.mjs
Only the copied validator's Ajv import paths were adapted to installed Revenue dependencies. Its generated reports stay inside Sales review storage; neither Revenue scripts nor drafts were edited. The copied validator reproduces 257 checks, 37 cases, 25 positive digests and 12 negative normalization cases. The supplied Python verifier independently reproduces all 25 valid digests and rejects nine negative fixtures, but exits 1 for the three documented unsupported admission boundaries (mixed Market, foreign tenant, legacy key). Its JSON summary explicitly reports those three accepted-invalid failures. Do not describe that executable as a clean all-vector pass. The extra Sales probes below are recorded in sales-review-report.json and python-edge-report.json. No endpoint, JWT/JWS validation, provider, financial predicates, transaction or concurrency proof is established.
Concrete issues to resolve before accepting the digest/wire package
Trimmed capture source disagrees across prose, source and verifiers. Existing Revenue
schema.ts:142-153callsreadRequiredStringoncapture.source, so" inline "is trimmed and accepted. The Python reference also trims source. Proposedcanonical.mjsbranches and checks the untrimmed source, rejecting the same input asunknown_member; the proposed transport enum also rejects it. The actual Sales mapper emits literalinline/link, so this is an interoperability and normative-boundary discrepancy rather than evidence that the current UI sends this value. Specify whether v3 deliberately forbids surrounding whitespace for source, or normalize it consistently, and add fixed accept/deny vectors. A deliberate v3 tightening must be stated separately from claims of matching current parsed behavior.Currency trim and transport schema disagree. Both the actual Revenue parser and draft normalizer accept
offer.currency: " USD "as USD.$defs/CloseValuesrejects it using the raw^[A-Z]{3}$pattern. Sections 3-4 distinguish transport/schema from semantic validation but do not define a preprocessing order that resolves this conflict. Define whether transport validation precedes normalization and rejects this raw value, or whether the schema describes normalized values. Do not silently switch the digest from parsed values to raw values. Add vectors covering bothOriginalCloseRequestvalidation and normalization, not only their common intersection.Independent Python zero-payment boundary contradicts the proposed minimum. Prose/schema/JS and existing Revenue structural parsing permit nonnegative
payment.amount_cents, including zero, while suppliedverify-independent.pycallsinteger(...,1)and rejects zero. This says nothing about permission to execute a zero-value payment: catalog/amount/provider predicates remain separate. Correct the independent structural bound or explicitly revise the proposed major wire and vectors; add a zero fixture plus negative controls. The 25 valid vectors miss this disagreement.Raw fractional-number policy is under-specified and not enforced as stated. A raw JSON amount
9007199254740991.1is parsed by JS as9007199254740991;parseStrict,normalizeand the schema all accept it. The prose says reject fractional/unsafe values, but does not resolve lexical mathematical values versus already rounded IEEE-754 values. Define the intended rule. If raw fractions or unsafe mathematical values must be rejected, validate number lexemes exactly before lossy parsing and add decimal/exponent/boundary vectors in both languages. If parsed-number semantics are intended, say so explicitly and prove canonical agreement; do not present JSON Schema or Number.isSafeInteger as raw-number proof. This is new-protocol clarification, not a proposed v2 parser change.
The nested recovery identity mismatch and a transient 503 hold_bind_unavailable wrapped as original_terminal both pass the draft shape schema. These are not new draft contradictions: the prose explicitly requires exact identity/receipt digest correspondence and separately durable terminal classification. They demonstrate why shape checks are insufficient. The assembled contract must make these semantic checks executable validation gates and negative vectors; callers must not trust a shape-valid terminal or settlement without them.
Source-backed design conformance and Sales integration conditions
The draft's major-version choice is appropriate: mandatory original protocol header, sclose3:<UUID-v4> key and descriptor/body requirements cannot be described as additive v2.9 requirements. An absent/2.9 header must reject both a v3 namespace and descriptor before effects, while a v3 header must reject old keys. Original route remains POST /api/v1/sales-ordering/close; recovery remains the separate proposed POST. Loss of a version header cannot downgrade execution. Old binaries, privileged writers and direct/provider senders must be drained or fenced before first v3 issuance/settlement; a new registry alone does not stop them. These are proposed rules, not verified enforcement.
Original close continues to use the existing seven-scope sales-production-to-revenue-v1 policy and close scope. Recovery uses a separately issued sole recovery scope. The four new policies do not grant original close; neither token shape may substitute for the other. Sales' current client obtains original authority independently (lib/revenue/sales-ordering-close-client.ts:165-190 and revenueAuthHeaders). It has no implemented v3 header, descriptor or typed recovery client. The stricter proposed recipient pins need Platform's assembled disposition without changing v2 authority by implication.
All 25 valid fixtures preserve actual Sales mapper values for inline, link and no-payment; singular versus one-item array remains distinct, while reversed multi-item arrays have the same v3 digest. Absent/null/blank payer and Lead omission, absent/null payment omission, absent/false historical omission, true historical retention, original payer case/Unicode, amount/credential/Person/Lesson/Organization/Lead/key changes and capture commitment all agree for those fixtures. V3 scalar ordering is deliberately isolated from existing localeCompare v2 normalization. Existing optional non-string values, unknown members, Unicode and integer tightening must remain explicitly confined to the major wire.
The finite 16-KiB descriptor/64-KiB body limits are not proved to cover every existing supported request. Current Sales DTO and Revenue structural parser impose no general capture/offer-array size limit. A finite major-version limit can be reviewed, but requires a documented supported payload budget and pre-send operator behavior. It must not discard or replace an already created soft hold when descriptor/body preparation exceeds the bound. No production payload survey or credential inspection was performed.
Before the first original Revenue send, Sales needs a distinct immutable non-secret close record containing the exact original key, protocol/digest versions, digest, descriptor bytes and descriptor digest, original actor/admission/resource/soft receipt association and original prepared non-secret wire values. Commit it only after the first Lesson and final payer/offer/payment commitment are fixed, then permit fetch. A crash before this commit authorizes no send; a crash after it recovers that same record without resigning, fresh hydration, new key or another capture. Do not persist the inline credential, bearer, cookie or workload proof. The proposal's issuance-after-durable-admission and exact saved-artifact requirements must explicitly include this Sales-side prepare/commit/send transition in the assembled contract.
Current candidate calculates a selection-derived close key and fetches immediately after the first Lesson at close-orchestration.service.ts:459; its soft-create journal is not this record. Its recovery function at lines 656-673 invokes original close again, and completion at scheduling-admission/repo.ts:84-98 accepts only reserved/committed plus Order. Neither proves v3 compatibility. A future dedicated recovery DTO/client must validate echoed original identity, exact descriptor and immutable receipt/result before local reconciliation; the existing 200-flat-close parser cannot consume the new recovery envelope.
| Proposed outcome | Required Sales behavior and blocker consequence |
|---|---|
pending |
Retain the original task, resources and protection; show reconciliation pending. Fresh assessed_at or a reason is not original-result mutation. Disable replacement/payment. No pending admitted attempt becomes never-admitted. |
original_terminal, success |
Retain exact original status/body/IDs/timestamps. Reserved remains reserved even after later funding; payment/handoff is separately assessed. Restore the original invoice correlation without marking SMS sent or payment confirmed. |
original_terminal, definitive failure |
Retain original failure evidence. Do not pass a generic 5xx through the old success completion API or infer quiescence/compensation. Replacement remains blocked until separate owner-approved resource/financial resolution. |
never_admitted |
Validate exact immutable settlement identity/receipt/digest and persist it separately from successful completion. It prevents the original Revenue admission, but does not prove the previously created soft resource expired or was cancelled. Retire only the matching Sales blocker after Delivery-owned resolution predicates hold; keep original history. |
| denial, mismatch, 404, unavailable, malformed or response loss | Retain evidence and uncertainty. Never treat transport absence as noncommit or create a replacement key. Historical authority and descriptor trust rules need the assembled Platform package. |
The draft's admitted/private-original-request rule and stable original provider attempt are consistent with response-loss recovery without Sales credential retention. Permanent admitted versus never-admitted state and original terminal replay are consistent with Sales' first position. Full response-loss/race/writeback/security semantics and exact Sales blocker retirement remain to be specified and proved with owner handlers; this local review supplies none of that runtime proof.
Platform descriptor/trust cross-slice review
Reviewed Platform's ADR-0053/platform/002 slice at manifest SHA256 728885f4f584cc242767eb897d34092cb102f7bbd3fcb91084f7a0d9d74d4987; all thirteen listed artifacts matched hashes and were copied into platform-snapshot/ under Sales review storage. The copied validator's dependency root was adapted to installed Ajv only, with all writes remaining in Sales. Its three strict schemas, twenty definitions and 102 offline vectors reproduce PASS. No RSA verification, signing key, issuance, durable trust watermark or runtime barrier was tested. cross-slice.mjs directly compiles both owners' schemas and records their actual incompatibilities in cross-slice-report.json.
Namespace and admission identity are incompatible today. Platform requires UUIDv7 admission_id, sc3:<same UUIDv7> and that same suffix in its recovery request. Revenue requires sclose3:<UUIDv4>. Each owner's positive recovery request is rejected by the other's schema. Actual Sales buildSchedulingCommand uses crypto.randomUUID() and ssh2:<that UUIDv4>; the accepted journal Admission and ReceiptCapture require UUIDv4. Platform's current payload rejects that original admission UUID. No silent UUID substitution, new human admission or rewritten historical journal is acceptable.
Sales concurs with root's proposed alignment: reference the original valid Sales UUIDv4 admission unchanged; use sclose3:<that same admission UUIDv4> for its single original close and retain ssh2:<same UUIDv4> for the original soft command. This is a draft recommendation, not an accepted contract or retrospective eligibility grant. Newly eligible v3 admissions must be identified prospectively under the approved protocol. Unfinished/old ADR-0052 or v1 commands are not upgraded by adding a descriptor. A close-preparation stage can be a separate immutable record keyed uniquely by the original admission, without another human identity/admission or replacement business key.
Actual single-close intent is source-backed: only the single_booking admitted flow reaches closeOfferWithSchedulingAdmission; one command/continuation is inserted, one successful soft receipt supplies the exact first Lesson, and assertRevenueContinuation requires that Lesson plus original Person/Organization/offer/payment. The repo serializes the Lead and refuses a second unresolved admission; composite, rebook and replacement motions are excluded. However the candidate does not durably enforce one Revenue key/descriptor per admission: the current key is selection-derived and there is no separate close record. The assembled v3 contract must require UNIQUE(original admission) preparation/descriptor/key association and bind Revenue's verified descriptor admission ID to the key suffix. This is an implementation/proof obligation, not a claim that existing source already implements it. No distinct new close business identity is justified for this single-close motion.
Byte bounds differ. Platform caps compact JWS at 8192 bytes, decoded header at 384 and payload at 4096, with RSA signature 256..512 bytes. Revenue caps its JWS transport at 16384 and total JSON body at 65536. A synthetic 9000-byte three-segment transport passes Revenue's shape and fails Platform's. Sales recommends using the stricter common 8192-byte transport plus Platform's decoded bounds in the assembled interface, subject to supported payload budgeting. Total body bounds remain a separate Revenue rule. Do not treat the generic transport regex or zero-signature fixture as a valid descriptor, and do not accept an open placeholder payload to bridge owners.
Digest equivalence language differs. Platform requires distinguishing null/absent/empty values and says every semantic field changes the digest, while Revenue and actual Revenue normalization deliberately equate optional absent/null/blank, absent/false historical and absent/null payment. Sales recommends committing to approved normalized semantic values and explicitly enumerating those equivalence classes, as demonstrated by current source and vectors; preserve meaningful differences such as singular versus offer_items and actual payer/capture/resource/key changes. Raw malformed/unsupported inputs are denied, not assigned a competing digest. The descriptor excludes itself from request_digest; exact original compact bytes retain a separate descriptor digest. ordering_version in claims versus protocol_version on recovery, explicit nullable originating Lead versus normalized body omission, and payment_motion none versus prose "no-payment" need an exact correlation map, not inferred defaults.
Descriptor denial codes differ. Platform proposes 401 descriptor_invalid for signature/namespace/issuer/audience invalidity and 409 descriptor_unverifiable for retired/revoked unseen artifacts. Revenue proposes 400 malformed transport, 403 invalid signed provenance and 503 eligible-history/trust uncertainty. Pick one exact domain taxonomy, independently from fresh workload 401/403 taxonomy, and provide agreed vectors. Sales always keeps the blocker for these denied/unverifiable cases; no transport code is a never-admitted receipt. Known admitted versus unseen trust modes must remain distinct: active trusted key may validate a new eligible artifact; retired/revoked unseen artifacts cannot tombstone, and revoked known admissions permit only independently justified historical reconciliation, not artifact-authorized new preparation/collection/bind.
Original soft receipt is the correct pre-close provenance. Platform's closed soft_receipt_ref uses original ssh2 key, key_digest, request_fingerprint, status/content type and raw-body SHA256, rather than an impossible future bind receipt. Sales actually saves raw body_base64 and its body_sha256, validates them against original key/fingerprint/resources and first Lesson, and obtains key_digest from the original command journal. This supports a direct immutable projection into the proposed reference without reserialization of the body or fabricated Delivery signature. Its 200/201 enum matches accepted $defs/WireReceipt; current Sales candidate transport accepts/captures only 201, so don't broaden deployed behavior just because the reference schema can represent 200. The final reference must correspond exactly to the retained original receipt and command; hash equality is not current resource authority or financial admission.
Platform's dedicated Sales server key, Revenue-local approved public keys, no fifth issuance service, sign-once durable audit, generation-serialized unseen admission/tombstone and exact original-artifact recovery are consistent with the requested bounded design. Its prepared-record and signing steps supply the explicit pre-first-send sequence requested above. They remain proposed. Human admission-evidence serialization, signer permission/custody, exact timestamp conversion from original journal ISO values to integer descriptor seconds, credential commitment security, public-key trust storage/rotation/watermark and owner financial/resource rules still need a closed jointly reviewed disposition. No arbitrary claims, self-asserted timestamp, stale public config or missing row can fill these gaps.
Remaining scope and handback
The previous Sales flow inventory remains controlling: inactive ADR-0052 v2-on candidate denies inline first close, legacy reservation-first close, composite multi-lesson, reschedule, trial rebook, existing-invoice replacement and invoice creation for supported add-on/top-up purchases. Listing/selecting an invoice, logging manual SMS, payment reads and cancellation are not universally denied by the flag. Proposed ordering v3 recovery does not authorize those other flows or justify removing them. Their compatible adapters or separate bounded designs remain cutover gates outside this recovery slice.
Sales withholds formal acknowledgment until these normalization and actual cross-slice mismatches are disposed, descriptor/issuance/history trust and recipient pins are aligned, compensation/resource/blocker semantics are assembled, and affected owners approve the complete package. Root owns publication, indexer and freshness. This participant prepared only the review memo, pinned scratch copies and pure local review evidence in permitted Sales folders.