Design the app framework — retailer and vendor surfaces #23

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

Question

What are the actual screens, and what is on them? A working design session covering navigation,
the retailer's RFP and evaluation surfaces, the vendor's workspace, and how a single company that
is both a retailer and a vendor moves between the two.

This is a prototype ticket: make something rough and concrete to react to, rather than
deciding in the abstract. Its output feeds the code layout directly — the module structure should
fall out of the surfaces, not be invented alongside them.

Deliberately blocked. The blockers are not bureaucracy; they are the app framework. The
schema determines which objects a screen can show. The tenancy model determines navigation, org
switching, and the dual-role experience. Visibility determines the retailer's core surface, and
the vault determines the vendor's. Designing ahead of these means retrofitting the domain model to
the screens, or discarding the screens.

Also worth having in hand before starting: the artifacts research, for realistic document shapes —
section counts, rubric weightings, pricing-table structures — so the surfaces are drawn against
real documents rather than imagined ones.

## Question What are the actual screens, and what is on them? A working design session covering navigation, the retailer's RFP and evaluation surfaces, the vendor's workspace, and how a single company that is both a retailer and a vendor moves between the two. This is a **prototype** ticket: make something rough and concrete to react to, rather than deciding in the abstract. Its output feeds the code layout directly — the module structure should fall out of the surfaces, not be invented alongside them. **Deliberately blocked.** The blockers are not bureaucracy; they *are* the app framework. The schema determines which objects a screen can show. The tenancy model determines navigation, org switching, and the dual-role experience. Visibility determines the retailer's core surface, and the vault determines the vendor's. Designing ahead of these means retrofitting the domain model to the screens, or discarding the screens. Also worth having in hand before starting: the artifacts research, for realistic document shapes — section counts, rubric weightings, pricing-table structures — so the surfaces are drawn against real documents rather than imagined ones.
christian added the
wayfinder:prototype
wayfinder:ticket
labels 2026-08-02 03:11:53 +00:00
christian added a new dependency 2026-08-02 03:11:53 +00:00
christian added a new dependency 2026-08-02 03:11:54 +00:00
christian added a new dependency 2026-08-02 03:11:54 +00:00
christian added a new dependency 2026-08-02 03:11:54 +00:00
christian added a new dependency 2026-08-02 03:11:54 +00:00
christian self-assigned this 2026-08-03 16:15:06 +00:00
Author
Owner

Resolution

Prototype: Surface map — six app-framework decisions — every screen below, drawn
against the closed resolutions, with the open tickets marked as seams rather than guessed at.

Six decisions, taken in order, each depending on the one before it. The through-line: the UI
boundary is the authorization boundary, drawn in the same place
— and where a surface would have
to invent something the domain model cannot produce, it stops.

1. One app, with a combined org x capability picker

The picker flattens org and capability into a single list — Acme Foods . Bidding — and the nav
beneath it is entirely capability-scoped. Switching context replaces the navigation wholesale.

Why. Tenancy holds that permissions never resolve against an org alone, only against
(org, capability, resource), and that the two contexts cannot see each other. A unified surface
sorted by direction (Incoming / Outgoing) was rejected because app.capability would then have no
UI anchor, and "why can't I see this?" would have no answer the user can point at. Two separate
apps were rejected because they break the one-profile promise experientially and give a retailer
who wants to bid a second product to discover.

What it costs. A dual-role user pays a context switch to move between their own two sides, and
deep links must carry capability or they land in the wrong nav.

Falls out of this, not asked separately: a dual-role org's vendor vault is invisible to its own
issuing context. That is decision 2 of tenancy applied literally, and it is correct — the
issuing side evaluating its own vendor side would be exactly the leak RLS exists to prevent.

2. The vendor's bid surface is requirement-led; uploading is an import path into it

The surface renders the schema directly: one answer per requirement, keyed to requirement_id,
carrying coverage as answered / not answered / indeterminate. Uploading a PDF fills the cells in;
authoring natively fills the same cells. One surface, two entry paths.

Why. Submit from anywhere settled that we accept any document, but never said which
artifact the vendor looks at. A document-led surface (dropzone, background extraction, submit)
was rejected on three counts: native authoring has nowhere to live, so the adoption pull that
decision was counting on never materialises; there is no per-requirement anchor, so the Pro upsell
is homeless; and the vendor never sees what they left unanswered — which the corpus work names as
a first-class case.

What it costs. Extraction failure is loud and in the vendor's face. A bad parse is now the
first thing a paying customer sees.

3. On the retailer's side the document is authoritative; structure is a confirmed overlay

Compose shows the retailer's own RFP beside the requirement overlay extracted from it, provenance
on screen. The overlay governs evaluation but can never silently diverge — editing a row forces
a reissue or becomes a recorded clarification. A retailer with no document authors natively and we
emit the PDF; that is the secondary path.

Why. The schema marks solicitation_document legally authoritative and
the layer decision keeps us out of their system of record. Native-authoring-with-generated-
PDF was rejected because it makes us the system of record for their legal artifact and drags in the
compliance and retention burden that decision explicitly refused — and because a large retailer
will not retype their procurement template. A two-panel both-editable model was rejected because
"legally authoritative" would then have two masters, and a protest asks which artifact governed.

What it costs. Confirmation is real work before every issue, and it sits on the free side.

4. The invitation shows issuer, category and deadline — and nothing else

The full vendor sequence is now invited -> subscribe -> teaser -> acknowledgement -> full RFP:
a billing gate, then a confidentiality gate, doing two different jobs.

Why. Issuer, category and deadline are facts about a relationship the retailer already has with
this vendor offline; scope, volume band and terms are RFP content and stay behind the wall. A bare
"Northgate Market has invited you" was rejected because it reads as phishing, carries no deadline
and therefore no urgency, and leaves the vendor unable to judge whether $1,200/yr is worth it. A
per-RFP toggle was rejected because it puts our conversion rate in the hands of someone with no
stake in it.

What it costs, stated plainly. A forwarded invitation tells a competitor that Northgate is
sourcing Produce. Accepted as the price of legibility.

5. The vendor's home is in-flight work beside eligibility health

Two panels. Deadlines on the left; on the right, what your documents are doing to your invitations
— phrased as consequence, not filing status: not "expires in 11 days" but "you drop out of three
rosters."
The anonymised acceptance count lives here too.

Why. The vault established that expiry removes a vendor from future audience
evaluations
. That is the cruellest mechanic in the system — a supplier stops being invited and
nothing tells them why. An opportunities-only inbox would ship that failure as a default, and would
also make the thing they pay for a nav item.

What it costs. Two things compete for the eye on load, and the right panel is quiet for a
healthy vendor — dead space most of the time, by design.

6. Evaluation is a requirement matrix, vendors as columns

Requirements down, responses across, coverage state in the cells, with mandatory coverage, per-
scenario cost and exceptions-taken as summary rows.

Why. Because both sides confirmed structure against the same requirement_id, the comparison
is a join, not a feature — the vendor's surface and the retailer's are the same data seen from
two directions. A ranked leaderboard was rejected as the lead surface on a hard constraint, not a
preference: the schema holds that scores are raw and never normalised, and one composite
number per vendor is precisely that normalisation. It would also hide non-responsiveness behind a
rank. A per-bid dossier was rejected because comparison is the product, and reading one bid at a
time makes a gap in vendor 2 invisible while you are reading vendor 1.

What it costs. A 200-requirement RFP needs column pinning and horizontal scroll; this is the
hardest surface here to make work small.

Seams — deliberately left open

  • Data release decides what is in the cells at each stage. The spine is fixed; the
    matrix is built to be progressively populated, not populated once.
  • Ranking adds an ordering over the matrix. Whether a composite number is defensible
    is that ticket's call; the spine works either way.
  • Extraction confirmation owns the confirm step, which now has a home on both the
    retailer's overlay column and the vendor's indeterminate rows.
  • Roster-entry claiming sits between the invitation wall and the workspace — the wall
    addresses a roster entry by name, and claiming is what turns it into a vendor org.
  • Catalogue, spine, rate structures fill vocabulary the surfaces
    already render: document types in the eligibility panel, categories in the picker and invitation,
    rate structures in the cost row.
  • The pinned fog — onboarding, trials, sponsorship. The wall in decision 4 is drawn at its
    hardest setting, so anything that softens it is a change to that surface, not a new one.

Constraints handed to the code layout

  • Capability is a routing concern, not a component concern. Two nav trees under one shell;
    every route carries capability so a deep link cannot land in the wrong context.
  • Extraction and confirmation is one module used twice — retailer document to requirement
    overlay, vendor document to answer set. Same shape, same confirm step, one implementation. The
    largest shared seam in the system.
  • The pre-authorization surface lives outside the authenticated shell and needs its own minimal
    entry point: it renders for an actor with no org, no capability and no subscription.
  • Eligibility health is a read model, not a query over documents — recomputed on the clock the
    vault decision already required. The vendor home reads whatever that produced.
  • Locked Pro affordances render from the entitlement assertion helper, so the paid-surface list
    is derived from code rather than maintained beside it. Visible-but-locked is the default
    rendering for a missing entitlement; absent is a bug.

What this makes specifiable

The destination is a code layout, and until now no ticket produced it — four resolutions had
handed structural requirements to an artifact that did not exist. Six surfaces plus five module
boundaries is enough to assemble it. Graduated to its own ticket, blocked on data release since
the permission boundaries are half of what the layout must express.

## Resolution **Prototype: [Surface map — six app-framework decisions](https://claude.ai/code/artifact/91357d1c-96a3-43e0-8a3b-d2da6296b01a)** — every screen below, drawn against the closed resolutions, with the open tickets marked as seams rather than guessed at. Six decisions, taken in order, each depending on the one before it. The through-line: **the UI boundary is the authorization boundary, drawn in the same place** — and where a surface would have to invent something the domain model cannot produce, it stops. ### 1. One app, with a combined org x capability picker The picker flattens org and capability into a single list — `Acme Foods . Bidding` — and the nav beneath it is **entirely capability-scoped**. Switching context replaces the navigation wholesale. **Why.** [Tenancy](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/11) holds that permissions never resolve against an org alone, only against `(org, capability, resource)`, and that the two contexts cannot see each other. A unified surface sorted by direction (Incoming / Outgoing) was rejected because `app.capability` would then have no UI anchor, and "why can't I see this?" would have no answer the user can point at. Two separate apps were rejected because they break the one-profile promise experientially and give a retailer who wants to bid a second product to discover. **What it costs.** A dual-role user pays a context switch to move between their own two sides, and deep links must carry capability or they land in the wrong nav. **Falls out of this, not asked separately:** a dual-role org's vendor vault is invisible to its own issuing context. That is decision 2 of [tenancy](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/11) applied literally, and it is correct — the issuing side evaluating its own vendor side would be exactly the leak RLS exists to prevent. ### 2. The vendor's bid surface is requirement-led; uploading is an import path into it The surface renders the schema directly: one answer per requirement, keyed to `requirement_id`, carrying `coverage` as answered / not answered / indeterminate. Uploading a PDF fills the cells in; authoring natively fills the same cells. **One surface, two entry paths.** **Why.** [Submit from anywhere](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/4) settled that we accept any document, but never said which artifact the vendor *looks at*. A document-led surface (dropzone, background extraction, submit) was rejected on three counts: native authoring has nowhere to live, so the adoption pull that decision was counting on never materialises; there is no per-requirement anchor, so the Pro upsell is homeless; and the vendor never sees what they left unanswered — which the corpus work names as a first-class case. **What it costs.** Extraction failure is loud and in the vendor's face. A bad parse is now the first thing a paying customer sees. ### 3. On the retailer's side the document is authoritative; structure is a confirmed overlay Compose shows the retailer's own RFP beside the requirement overlay extracted from it, provenance on screen. The overlay governs evaluation but **can never silently diverge** — editing a row forces a reissue or becomes a recorded clarification. A retailer with no document authors natively and we emit the PDF; that is the secondary path. **Why.** [The schema](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/10) marks `solicitation_document` legally authoritative and [the layer decision](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/3) keeps us out of their system of record. Native-authoring-with-generated- PDF was rejected because it makes us the system of record for their legal artifact and drags in the compliance and retention burden that decision explicitly refused — and because a large retailer will not retype their procurement template. A two-panel both-editable model was rejected because "legally authoritative" would then have two masters, and a protest asks which artifact governed. **What it costs.** Confirmation is real work before every issue, and it sits on the free side. ### 4. The invitation shows issuer, category and deadline — and nothing else The full vendor sequence is now **invited -> subscribe -> teaser -> acknowledgement -> full RFP**: a billing gate, then a confidentiality gate, doing two different jobs. **Why.** Issuer, category and deadline are facts about a relationship the retailer already has with this vendor offline; scope, volume band and terms are RFP content and stay behind the wall. A bare "Northgate Market has invited you" was rejected because it reads as phishing, carries no deadline and therefore no urgency, and leaves the vendor unable to judge whether $1,200/yr is worth it. A per-RFP toggle was rejected because it puts our conversion rate in the hands of someone with no stake in it. **What it costs, stated plainly.** A forwarded invitation tells a competitor that Northgate is sourcing Produce. Accepted as the price of legibility. ### 5. The vendor's home is in-flight work beside eligibility health Two panels. Deadlines on the left; on the right, what your documents are doing to your invitations — phrased as consequence, not filing status: **not "expires in 11 days" but "you drop out of three rosters."** The anonymised acceptance count lives here too. **Why.** [The vault](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/13) established that expiry *removes a vendor from future audience evaluations*. That is the cruellest mechanic in the system — a supplier stops being invited and nothing tells them why. An opportunities-only inbox would ship that failure as a default, and would also make the thing they pay for a nav item. **What it costs.** Two things compete for the eye on load, and the right panel is quiet for a healthy vendor — dead space most of the time, by design. ### 6. Evaluation is a requirement matrix, vendors as columns Requirements down, responses across, coverage state in the cells, with mandatory coverage, per- scenario cost and exceptions-taken as summary rows. **Why.** Because both sides confirmed structure against the same `requirement_id`, the comparison is **a join, not a feature** — the vendor's surface and the retailer's are the same data seen from two directions. A ranked leaderboard was rejected as the lead surface on a hard constraint, not a preference: [the schema](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/10) holds that scores are raw and never normalised, and one composite number per vendor is precisely that normalisation. It would also hide non-responsiveness behind a rank. A per-bid dossier was rejected because comparison is the product, and reading one bid at a time makes a gap in vendor 2 invisible while you are reading vendor 1. **What it costs.** A 200-requirement RFP needs column pinning and horizontal scroll; this is the hardest surface here to make work small. ### Seams — deliberately left open - **[Data release](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/14)** decides *what is in the cells at each stage*. The spine is fixed; the matrix is built to be progressively populated, not populated once. - **[Ranking](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/15)** adds an ordering *over* the matrix. Whether a composite number is defensible is that ticket's call; the spine works either way. - **[Extraction confirmation](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/16)** owns the confirm step, which now has a home on *both* the retailer's overlay column and the vendor's indeterminate rows. - **[Roster-entry claiming](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/24)** sits between the invitation wall and the workspace — the wall addresses a roster entry by name, and claiming is what turns it into a vendor org. - **[Catalogue](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/27), [spine](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/28), [rate structures](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/29)** fill vocabulary the surfaces already render: document types in the eligibility panel, categories in the picker and invitation, rate structures in the cost row. - **The pinned fog** — onboarding, trials, sponsorship. The wall in decision 4 is drawn at its *hardest* setting, so anything that softens it is a change to that surface, not a new one. ### Constraints handed to the code layout - **Capability is a routing concern, not a component concern.** Two nav trees under one shell; every route carries capability so a deep link cannot land in the wrong context. - **Extraction and confirmation is one module used twice** — retailer document to requirement overlay, vendor document to answer set. Same shape, same confirm step, one implementation. The largest shared seam in the system. - **The pre-authorization surface lives outside the authenticated shell** and needs its own minimal entry point: it renders for an actor with no org, no capability and no subscription. - **Eligibility health is a read model**, not a query over documents — recomputed on the clock the [vault decision](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/13) already required. The vendor home reads whatever that produced. - **Locked Pro affordances render from the entitlement assertion helper**, so the paid-surface list is derived from code rather than maintained beside it. *Visible-but-locked is the default rendering for a missing entitlement; absent is a bug.* ### What this makes specifiable The destination is a code layout, and until now **no ticket produced it** — four resolutions had handed structural requirements to an artifact that did not exist. Six surfaces plus five module boundaries is enough to assemble it. Graduated to its own ticket, blocked on data release since the permission boundaries are half of what the layout must express.
Sign in to join this conversation.
No description provided.