Specify the two-party data release rules #14

Closed
opened 2026-08-02 03:05:53 +00:00 by christian · 1 comment
Owner

Question

The original framing: bidirectional data released only when required. This is the trust property the whole system rests on, and it needs to be a specification, not a principle.

Resolve: exactly what the retailer sees of a vendor and when (before bid, at bid, at shortlist, at award, after award); exactly what the vendor sees of the retailer and of the existence, count, or content of competing bids; what is released on rejection; what is retained after an RFP closes; and what each side can export. Write it as rules over the tenancy model.

Added by a skeptical review pass

  • The evaluation scenario is a disclosure decision, not just a schema field.
    The schema publishes the volume basket with the RFP so price is bindable up front;
    visibility names volume commitments as precisely what the acknowledgement gate protects,
    and that gate is optional per RFP. Decide when the scenario is released and whether publishing it
    forces the gate on.
  • The anonymised acceptance count needs a privileged read path. It reads acceptance rows owned
    by other retailers, which no actor context may see. The k-floor therefore lives in privileged
    code, not application code — see the note on tenancy.
  • Release granularity is (response, requirement), per the schema decision — answer.text,
    price_quote.rate_structure, and exception.text are the sensitive fields.
  • Rule out derived cross-RFP vendor metrics explicitly. No cross-RFP score and no global rating
    was carefully held by the schema decision; win rate is that rating in different clothes and
    leaks competitive outcomes. It should be named and excluded rather than left to discipline.

Parent: #1

## Question The original framing: *bidirectional data released only when required*. This is the trust property the whole system rests on, and it needs to be a specification, not a principle. Resolve: exactly what the retailer sees of a vendor and when (before bid, at bid, at shortlist, at award, after award); exactly what the vendor sees of the retailer and of the existence, count, or content of competing bids; what is released on rejection; what is retained after an RFP closes; and what each side can export. Write it as rules over the tenancy model. ## Added by a skeptical review pass - **The evaluation scenario is a disclosure decision, not just a schema field.** [The schema](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/10) publishes the volume basket with the RFP so price is bindable up front; [visibility](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/12) names volume commitments as precisely what the acknowledgement gate protects, and that gate is optional per RFP. Decide when the scenario is released and whether publishing it forces the gate on. - **The anonymised acceptance count needs a privileged read path.** It reads acceptance rows owned by other retailers, which no actor context may see. The k-floor therefore lives in privileged code, not application code — see the note on [tenancy](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/11). - **Release granularity is `(response, requirement)`**, per the schema decision — `answer.text`, `price_quote.rate_structure`, and `exception.text` are the sensitive fields. - **Rule out derived cross-RFP vendor metrics explicitly.** No cross-RFP score and no global rating was carefully held by the schema decision; **win rate** is that rating in different clothes and leaks competitive outcomes. It should be named and excluded rather than left to discipline. --- Parent: #1
christian added the
wayfinder:grilling
wayfinder:ticket
labels 2026-08-02 03:05:53 +00:00
christian added a new dependency 2026-08-02 03:06:25 +00:00
christian added a new dependency 2026-08-02 03:06:25 +00:00
christian added a new dependency 2026-08-02 03:06:25 +00:00
christian added a new dependency 2026-08-02 03:06:26 +00:00
christian added a new dependency 2026-08-02 03:06:26 +00:00
christian added a new dependency 2026-08-03 16:47:07 +00:00
christian self-assigned this 2026-08-03 16:52:13 +00:00
Author
Owner

Resolution

Five decisions plus four consequences that fell out rather than needed asking. The through-line:
release is one rule set, expressed once, enforced in the database and rendered by the UI from the
same source.
Every place this ticket could have grown a second rule set, it refused.

1. A release-level function feeding a versioned class x level matrix

release_level(viewer_org, capability, response) -> enum
    computed from the four inputs https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/12 named:
    audience membership, solicitation stage, participation state, acknowledgement

disclosure_class    the field side: answer text, exception text, price quote,
                    derived cost, vault document contents, participation signal,
                    vendor identity, scoring of one's own response

release_matrix      class x level -> released | withheld     -- versioned

Policies call the function and compare against the matrix. Nothing else encodes lifecycle.

Why not per-table policies with the lifecycle written inline. Eight-plus policies each
restating the same stage logic is the drift the trust property cannot survive: the divergence is
invisible until it is a leak, and there is no single place to review what is released.

Why not materialised disclosure rows. Attractive for audit, and rejected because release is a
continuous function of stage rather than a decision made once — unlike the audience, which #12
materialises for exactly that reason. Writing viewers x classes at every transition churns badly on
a 40-bidder event, a missed write is a silent under-disclosure, and a stale row is a silent leak.

The matrix must be versioned. This is the one thing the function form costs: without a version
history, a past release cannot be reconstructed when an award is protested. Handed to #21.

Bonus the shape buys: the evaluation surface's dimmed cells render from the same matrix, so the
screen and the policy cannot disagree.

2. Sealed until the deadline, then the whole field opens at once

No response content is readable by the retailer before submissions close. At the deadline every bid
opens simultaneously. Before that the retailer sees participation signals only — opened,
acknowledged, intends to bid, has submitted — which is precisely the pre-bid signal #12 promised
them, and it is non-content.

Why. Reading bids as they arrive makes bid shopping a capability we shipped: leveraging one
vendor's price against another. That is the single thing most likely to destroy vendor trust in the
venue, and it is what the paying side is paying to be protected from. A vendor who suspects it will
not put a real price in, which degrades the retailer's outcome too.

Retailer-configurable was rejected outright. The retailer's incentive runs toward seeing early
and the vendor cannot verify the setting was honest. A venue promise that is configurable by the
party it constrains is not a promise.

Two-envelope was seriously considered and deferred, not declined — everything at deadline
except price, which opens only after technical scoring locks, so price cannot anchor technical
judgement. It is the stronger public-procurement pattern and it maps cleanly onto the
criterion / price_line split the schema already has. It needs a real, irreversible scoring-lock
transition to mean anything, which is more machinery than v1 needs. The matrix makes it a
one-row change plus a transition
, which is the point of choosing the matrix.

What it costs. Forty bids on a 200-requirement RFP all arrive for review at the same moment.

3. A vendor sees nothing of the field and a great deal of themselves

Never, at any stage: how many were invited, how many bid, who they are, their prices, any rank
including their own, or who won.

What opens instead, at award: the retailer's scoring of that vendor's own response, their
coverage gaps, their exceptions as read, and the outcome — awarded, not awarded, or cancelled.

Why. The field size is the retailer's sourcing strategy, not ours to hand out. The winner's new
supply relationship is their asset to disclose — the same rule #13 set for acceptance signals,
where disclosure is the vendor's asset to trade rather than ours to give away.

Post-award tabulation was rejected despite being the public-procurement convention: a public
bid opening is a statutory duty of public buyers, which private retailers do not carry and would
not accept. And "your rank" is a cross-response comparison — the normalised score #10 bans.

Field size in the invitation was rejected because the pre-wall surface is the widest audience
the RFP ever has, and it makes the number a lever a retailer could inflate with our publication
behind it.

This is where a losing vendor's feedback comes from, and it is real: everything released is
about their own response, so the paying side gets something for the fee without anyone learning
anything about a competitor.

4. Volume band free, exact quantities behind acknowledgement, gate-off warns hard

The teaser's coarse volume band stays pre-acknowledgement, as #12 already specified.
scenario_quantity — the exact basket — sits behind the gate.

Issuing with declared quantities and the gate off is allowed, and raises a warning the retailer
must dismiss: these volumes will reach every invited supplier, including those who never bid, under
no confidentiality undertaking.

Why not force the gate on. It is the tidier rule and it was rejected on the map's own
through-line: we establish facts, retailers make judgements — the same shape as #13 computing
expiry state while retailers configure its consequence. Forcing it would also fire the gate on
nearly every RFP, and #12 warned the gate irritates vendors on low-stakes events if it is not
genuinely optional. Routine replenishment with known volumes is a real case.

What the warning buys: an accidental leak becomes near-impossible while deliberate disclosure
stays available. The retailer who dismisses it has made a decision on the record.

5. Export mirrors read exactly, and every export is an event

You may export precisely what your release level lets you read. No second rule set. Each export
writes export_event (actor, org, capability, solicitation, classes, row_count, at).

Why not narrower than read. Excluding competitor price detail from bulk export sounds
protective and is mostly friction: a retailer who can read it can screenshot it, so the promise
would be stronger than the mechanism — and it creates the second rule set decision 1 exists to
avoid. It would also block the ERP hand-back #3 says we owe.

Why not defer bulk export. A procurement team that cannot get a comparison into a spreadsheet
will not adopt, and the retailer is the free side we are trying to attract.

Recorded honestly: post-award, the retailer holds a permanent copy of every losing vendor's
pricing, outside every policy on this page. That is unavoidable the moment export exists at all.
The log is the accountability, not the restriction — a retailer who bulk-extracts every losing
price has done so on the record, and that record is available to #21.

Consequences that did not need asking

Release never contracts. #20 already settled that documents released to a retailer on a past
RFP stay released — delivery is not retractable. The same holds for every class here: the matrix
only ever opens with advancing stage. There is no revocation transition, and retention after close
is therefore not a separate rule.

The acceptance count's k-floor, with the subtraction hole closed. The count is a privileged
read path
— it reads acceptance rows owned by other retailers, which no actor context may see —
so the floor lives in privileged code, per #33. The floor is on others, not on the total:
a retailer who has themselves accepted the document knows they are one of the accepters and would
otherwise subtract themselves. So the count shown to viewer R is |accepters \ {R}|, displayed
only when that reaches 3. The number is tunable data; the subtraction rule is structure.

Win rate does not exist as a disclosure class. Named and excluded here rather than left to
discipline, per #33 and the map's standing guardrail. There is no cross-RFP vendor metric to
release at any level, to either side, in any export.

Release granularity is (response, requirement), per #10answer.text,
price_quote.rate_structure and exception.text are the sensitive fields, and they are classed
separately because price opens later than prose under two-envelope.

Constraints handed to other tickets

  • #21 audit — the matrix must be versioned and the release-level function's inputs
    reconstructable, or a protest cannot be answered. export_event is protest surface.
  • #35 layout — the release-level function is the second enumerated privileged boundary after
    the acceptance count and the expiry scheduler. Policies must not reference entitlement at all,
    per #20; billing state is nowhere in the four inputs.
  • #15 ranking — ranking reads at the evaluating level and above. It may not surface any
    cross-response derived metric that the matrix does not release.
  • #23 surfaces (closed) — two additions land on already-decided screens: the issue-time
    warning when quantities are declared without a gate, and the deadline as the moment the
    evaluation matrix populates. Neither changes a surface decision.
  • #18 integration — export paths inherit the matrix and must write export_event. Export can
    never exceed read; that is fixed here so integration cannot quietly widen it.
## Resolution Five decisions plus four consequences that fell out rather than needed asking. The through-line: **release is one rule set, expressed once, enforced in the database and rendered by the UI from the same source.** Every place this ticket could have grown a second rule set, it refused. ### 1. A release-level function feeding a versioned class x level matrix ``` release_level(viewer_org, capability, response) -> enum computed from the four inputs https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/12 named: audience membership, solicitation stage, participation state, acknowledgement disclosure_class the field side: answer text, exception text, price quote, derived cost, vault document contents, participation signal, vendor identity, scoring of one's own response release_matrix class x level -> released | withheld -- versioned ``` Policies call the function and compare against the matrix. Nothing else encodes lifecycle. **Why not per-table policies with the lifecycle written inline.** Eight-plus policies each restating the same stage logic is the drift the trust property cannot survive: the divergence is invisible until it is a leak, and there is no single place to review what is released. **Why not materialised disclosure rows.** Attractive for audit, and rejected because release is a *continuous function of stage* rather than a decision made once — unlike the audience, which https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/12 materialises for exactly that reason. Writing viewers x classes at every transition churns badly on a 40-bidder event, a missed write is a silent under-disclosure, and a stale row is a silent leak. **The matrix must be versioned.** This is the one thing the function form costs: without a version history, a past release cannot be reconstructed when an award is protested. Handed to https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/21. **Bonus the shape buys:** the evaluation surface's dimmed cells render from the same matrix, so the screen and the policy cannot disagree. ### 2. Sealed until the deadline, then the whole field opens at once No response content is readable by the retailer before submissions close. At the deadline every bid opens simultaneously. Before that the retailer sees participation signals only — opened, acknowledged, intends to bid, has submitted — which is precisely the pre-bid signal https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/12 promised them, and it is non-content. **Why.** Reading bids as they arrive makes **bid shopping** a capability we shipped: leveraging one vendor's price against another. That is the single thing most likely to destroy vendor trust in the venue, and it is what the paying side is paying to be protected from. A vendor who suspects it will not put a real price in, which degrades the retailer's outcome too. **Retailer-configurable was rejected outright.** The retailer's incentive runs toward seeing early and the vendor cannot verify the setting was honest. A venue promise that is configurable by the party it constrains is not a promise. **Two-envelope was seriously considered and deferred, not declined** — everything at deadline except price, which opens only after technical scoring locks, so price cannot anchor technical judgement. It is the stronger public-procurement pattern and it maps cleanly onto the `criterion` / `price_line` split the schema already has. It needs a real, irreversible scoring-lock transition to mean anything, which is more machinery than v1 needs. **The matrix makes it a one-row change plus a transition**, which is the point of choosing the matrix. **What it costs.** Forty bids on a 200-requirement RFP all arrive for review at the same moment. ### 3. A vendor sees nothing of the field and a great deal of themselves Never, at any stage: how many were invited, how many bid, who they are, their prices, any rank including their own, or who won. What opens instead, at award: **the retailer's scoring of that vendor's own response**, their coverage gaps, their exceptions as read, and the outcome — awarded, not awarded, or cancelled. **Why.** The field size is the retailer's sourcing strategy, not ours to hand out. The winner's new supply relationship is *their* asset to disclose — the same rule https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/13 set for acceptance signals, where disclosure is the vendor's asset to trade rather than ours to give away. **Post-award tabulation was rejected** despite being the public-procurement convention: a public bid opening is a *statutory duty of public buyers*, which private retailers do not carry and would not accept. And "your rank" is a cross-response comparison — the normalised score https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/10 bans. **Field size in the invitation was rejected** because the pre-wall surface is the widest audience the RFP ever has, and it makes the number a lever a retailer could inflate with our publication behind it. **This is where a losing vendor's feedback comes from**, and it is real: everything released is about their own response, so the paying side gets something for the fee without anyone learning anything about a competitor. ### 4. Volume band free, exact quantities behind acknowledgement, gate-off warns hard The teaser's coarse volume band stays pre-acknowledgement, as https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/12 already specified. `scenario_quantity` — the exact basket — sits behind the gate. Issuing with declared quantities and the gate **off** is allowed, and raises a warning the retailer must dismiss: these volumes will reach every invited supplier, including those who never bid, under no confidentiality undertaking. **Why not force the gate on.** It is the tidier rule and it was rejected on the map's own through-line: *we establish facts, retailers make judgements* — the same shape as https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/13 computing expiry state while retailers configure its consequence. Forcing it would also fire the gate on nearly every RFP, and https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/12 warned the gate irritates vendors on low-stakes events if it is not genuinely optional. Routine replenishment with known volumes is a real case. **What the warning buys:** an accidental leak becomes near-impossible while deliberate disclosure stays available. The retailer who dismisses it has made a decision on the record. ### 5. Export mirrors read exactly, and every export is an event You may export precisely what your release level lets you read. No second rule set. Each export writes `export_event (actor, org, capability, solicitation, classes, row_count, at)`. **Why not narrower than read.** Excluding competitor price detail from bulk export sounds protective and is mostly friction: a retailer who can read it can screenshot it, so the promise would be stronger than the mechanism — and it creates the second rule set decision 1 exists to avoid. It would also block the ERP hand-back https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/3 says we owe. **Why not defer bulk export.** A procurement team that cannot get a comparison into a spreadsheet will not adopt, and the retailer is the free side we are trying to attract. **Recorded honestly:** post-award, the retailer holds a permanent copy of every losing vendor's pricing, outside every policy on this page. That is unavoidable the moment export exists at all. **The log is the accountability, not the restriction** — a retailer who bulk-extracts every losing price has done so on the record, and that record is available to https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/21. ### Consequences that did not need asking **Release never contracts.** https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/20 already settled that documents released to a retailer on a past RFP stay released — delivery is not retractable. The same holds for every class here: the matrix only ever opens with advancing stage. There is no revocation transition, and retention after close is therefore not a separate rule. **The acceptance count's k-floor, with the subtraction hole closed.** The count is a **privileged read path** — it reads acceptance rows owned by other retailers, which no actor context may see — so the floor lives in privileged code, per https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/33. The floor is on **others**, not on the total: a retailer who has themselves accepted the document knows they are one of the accepters and would otherwise subtract themselves. So the count shown to viewer R is `|accepters \ {R}|`, displayed only when that reaches **3**. The number is tunable data; the subtraction rule is structure. **Win rate does not exist as a disclosure class.** Named and excluded here rather than left to discipline, per https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/33 and the map's standing guardrail. There is no cross-RFP vendor metric to release at any level, to either side, in any export. **Release granularity is `(response, requirement)`**, per https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/10 — `answer.text`, `price_quote.rate_structure` and `exception.text` are the sensitive fields, and they are classed separately because price opens later than prose under two-envelope. ### Constraints handed to other tickets - **https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/21 audit** — the matrix must be versioned and the release-level function's inputs reconstructable, or a protest cannot be answered. `export_event` is protest surface. - **https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/35 layout** — the release-level function is the second enumerated privileged boundary after the acceptance count and the expiry scheduler. Policies must not reference entitlement at all, per https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/20; billing state is nowhere in the four inputs. - **https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/15 ranking** — ranking reads at the evaluating level and above. It may not surface any cross-response derived metric that the matrix does not release. - **https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/23 surfaces** (closed) — two additions land on already-decided screens: the issue-time warning when quantities are declared without a gate, and the deadline as the moment the evaluation matrix populates. Neither changes a surface decision. - **https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/18 integration** — export paths inherit the matrix and must write `export_event`. Export can never exceed read; that is fixed here so integration cannot quietly widen it.
Sign in to join this conversation.
No description provided.