Task: Address the blueprint review
Table of Contents
This page documents a task in the Rework the market data feeds into a market data service with adapters — the remaining work story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
The curve blueprint screens read a name from the record, treat an absent discount curve as null, and name people with their picture.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Rework the market data feeds into a market data service with adapters — the remaining work |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-10-28 |
3. Acceptance
- ir_curve_bootstrap_config carries a required name, in the model, the SQL and the screens.
- discount_curve_config_id is nullable as std::optional; no code uses a nil sentinel.
- Publishers and requesters draw with the standard person cell, and the check runs on a funding curve.
4. Plan
- Both model changes are one edit to
projects/ores.refdata/modeling/ores.refdata.ir_curve_bootstrap_config.org: a requirednamecolumn that is searchable and sortable, a declared unique index over(tenant_id, party_id, name)for the active rows, anddiscount_curve_config_idasuuid/std::optional<boost::uuids::uuid>/ nullable with the nil-sentinel default and prose removed. compass codegen entity generate ir_curve_bootstrap_configregenerates the domain, entity, mapper, repository, service, protocol, shell commands, SQL create and the web protocol; the hand-written uses follow: the republish service, the publish fixture, the refdata seed, the BFF write schema and the web registry.- The screens follow the model: the list's first cell, the record header and breadcrumb, Details' first fact and the Add and Copy dialogs all draw the name; the Check tab reads the discount curve only when the record names one; the published screens draw the publisher with the standard person cell.
- The grants are seeded in
iam_roles_populate.sqland applied to the Acme tenant with the DDL user. - The environment is rebuilt (
db recreate, Acme provisioning, the evidence seed, the demo publish flow) and the walkthrough runs against the restarted services.
5. Measurements
Every number this task claims about a tree, with the command that produced it
and the commit it was measured at. A number without both is not a measurement,
because a clean-up record describes a tree and trunk moves under a branch: a
consumer count, a gate's component count and a suite's size all change with no
code changing, and the reader of a bare number cannot tell which tree it came
from. Where the raw output matters, put it in an evidence/ file beside the
task and name that file in the Command column.
| What | Value | Command | Commit |
|---|---|---|---|
| Codegen idempotence | two consecutive entity generate runs leave the tree unchanged |
compass codegen entity generate ir_curve_bootstrap_config twice, then git diff --stat |
HEAD of this task's branch |
| refdata api suite | 83 cases, 0 failed | compass test run -- -R ores.refdata.api.tests |
HEAD of this task's branch |
| refdata core suite | 474 cases, 0 failed | compass test run -- -R ores.refdata.core.tests |
HEAD of this task's branch |
| refdata service suite | 10 cases, 0 failed | compass test run -- -R ores.refdata.service.tests |
HEAD of this task's branch |
| marketdata service suite | 202 cases, 0 failed | compass test run -- -R ores.marketdata.service.tests |
HEAD of this task's branch |
| shell refdata suite | 2809 cases, 0 failed | compass test run -- -R ores.shell.refdata.tests |
HEAD of this task's branch |
| web workspace tests | 2318 cases, 0 failed | npm run test --workspace @ores/web in projects/ores.web |
HEAD of this task's branch |
| BFF suite | 1128 cases, 0 failed | npm run test --workspace @ores/bff in projects/ores.web |
HEAD of this task's branch |
| typecheck | clean across every workspace | npm run typecheck in projects/ores.web |
HEAD of this task's branch |
| SQL schema validation | 635 tables, 11 warnings, none new | ./projects/ores.sql/utility/validate_schemas.sh |
HEAD of this task's branch |
| Docs validation | 39 components pass | ./projects/ores.codegen/validate_docs.sh |
HEAD of this task's branch |
| Docs lint | filetags and generator markers valid | compass lint |
HEAD of this task's branch |
| Live walkthrough | see =* Result |
The review fixes are built, verified and merged with PR 4020.
- The curve blueprint carries a required name in the model and the table, searchable and sortable and unique per tenant and party; the list, header, Details and the Add and Copy dialogs draw it, and nothing parses the oresmd URI for a name.
- discount_curve_config_id is a nullable std::optional with no nil sentinel; a FUNDING blueprint names none, Details draws a dash, and the Check runs.
- Published people draw through PersonCell, the account read is granted to the roles that need it, and a request says whether its subject was refused or absent.
- The two-segment method's split tenor may close the first segment, and the market series read is granted to Operations, Support and Viewer, so the screens' series resolution no longer reports a partial refusal.
Evidence: refdata api 83, core 474, service 10, marketdata service 202, shell refdata 2809, web 2318, BFF 1128, typecheck clean, codegen idempotent over two runs, 635 tables with no new warnings, 39 docs components; live named blueprint, null discount, check building 12M 0.9580379383023567 and 2Y 0.9210501013169695, and Mark Reid resolving Henry Jones with his picture.
Caveats: ores.marketdata.official needed its client certificate, now issued; the generated TS filter type for a nullable column spells both no-filter and is-null as string | null | null.
All four fixes are in, the entity regenerates cleanly, and the walkthrough ran against the provisioned Acme tenant with the rebuilt web and BFF and restarted services.
5.1. Fix 1 – the name comes from the database
ir_curve_bootstrap_config carries a required name (text, searchable,
sortable) with a unique index over (tenant_id, party_id, name) for the active
rows. The entity was regenerated and every affected piece follows: the domain,
the repository and its mappers, the service, the protocol, the shell commands,
the SQL create, the refdata seed (USD-SOFR-FOMC), the BFF write schema and the
web protocol. The model's list contract says the name replaces the sentence that
said a blueprint has no name. The web draws the name in the list's first cell,
in the record header and breadcrumb, and as Details' first fact; the output
series stays a property on Details, drawn as its URI and linked; the Add and Copy
dialogs ask for the name first and it is typed, never derived. No screen parses
the URI for a name anywhere.
5.2. Fix 2 and Fix 4 – the discount curve is null, and the check runs
discount_curve_config_id is std::optional<boost::uuids::uuid> and nullable,
with no default; the nil-sentinel shape check became an is null / is not null
pair per role, and the trigger's two-tier guard compares no nil uuid. Every use
follows: the republish service reads a projection's discount curve through the
optional and refuses a projection that names none, the test fixture writes null,
the refdata seed and the evidence seed leave it out, and the BFF write schema
accepts null. On the web the field is optional wherever it is read, Details draws
"—", and the Check tab reads a discount curve only when the record names one, so
it no longer asks the server for the record a nil id names. The recipe's two
discount checks still need a projection, and a funding curve passes them without
a discount being read.
5.3. Fix 3 – people are drawn as people
PersonCell is the shared pairing the history panel already uses – an Avatar
with the picture beside a PersonRef – added to PersonRef.tsx because no
shared component paired them. The published list's Publisher column, the
blueprint's Published sets tab, and the Points and Compare tabs draw
published_by through it, and the published record's Details does too. The
request page keeps AccountPicture and PersonRef and now resolves them,
because the reader holds iam::accounts:read. The subject-missing message on a
request is two messages: the BFF passes publishedSubjectState refused or
missing, and the screen says whether the read was refused or found nothing.
5.4. The live walkthrough, 2026-10-28, against the restarted services
Environment: db recreate, the Acme provisioning recipe, the evidence seed
applied as the DDL user, the three demo publish scripts replayed. The web and BFF
were rebuilt by compass build and the services restarted; the served bundle
carries the new copy.
| Step | What was seen |
|---|---|
GET /api/refdata/ir-curve-bootstrap-configs?sort=name |
one row, name USD-SOFR-ACME, discount_curve_config_id null, sorted on name |
PUT /api/refdata/ir-curve-bootstrap-configs (the Add dialog's write) |
204; the record reads back with name USD-SOFR-ACME-NEW and discount_curve_config_id null |
POST /api/curves/compute on the new blueprint |
curve_republish_service: bootstrap config has no pillars – the honest refusal, and not the nil-uuid record read that failed before |
POST /api/curves/compute on the seeded blueprint |
two points: 12M 0.9580379383023567 and 2Y 0.9210501013169695 |
| Details of the seeded blueprint | name USD-SOFR-ACME, output_series_id c1a2b3c4-... whose URI is oresmd://ir/USD?type=series&instrument=discount"e=rate&curve_id=USD-SOFR-ACME, discount null |
GET /api/curves/published as mark.reid |
one set, published_by henry.jones, state_code published, two points |
GET /api/accounts as mark.reid |
henry.jones reads as Henry Jones with imageId 01a1847c-7a75-b64b-edb8b40f3203 |
GET /api/images/01a1847c-... as mark.reid and as henry.jones |
200, image/jpeg, 101340 bytes – the picture the person cell draws |
GET /api/requests/01a18485-... as mark.reid |
the request raised by Henry Jones, with publishedSubject USD-SOFR-ACME / ACCOUS / 2 points |
GET /market-data/curve-blueprints on the running app |
200, and the served bundle carries the new name and subject-missing copy |
5.5. What is left
The ores.marketdata.official service fails to start in this environment for a
reason that predates this task: its NATS TLS client certificate is missing
(build/keys/nats/ores.marketdata.official.crt). It serves the public fixings
adapter and touches none of these screens.= | the BFF on port 21402, restarted after the build | HEAD of this task's branch |
6. Notes
6.1. The name is a column, and two natural keys are not two indexes
name is a plain required column, not a natural key. The natural-key machinery
emits one index per entity, and the entity already carries output_series_id as
its natural key – exactly one active config, tenant-wide, may claim an output
series. A second natural key would be emitted as one composite index over both
columns, which would drop that rule and let two blueprints claim one series as
long as their names differ. The model therefore states the name's uniqueness as
a declared unique index over (tenant_id, party_id, name) for the active rows,
which is what the model can express: tenant plus party, exactly as the review
asked.
6.2. The grants, and why each role holds the account read
iam::accounts:read is now held by Trading and Viewer in
iam_roles_populate.sql, beside the roles that already held it: Member,
Operations, Support and the service accounts. The set and its reason:
Tradingraises a publish request and reads back the sets it published, and every one of those rows names the account that published it. The maker also reads the request page, where the decider's name and picture are drawn.Vieweris the read-only role that sees the published curves, the points, the comparison and the blueprint's Published sets tab. It sees the same people and could not resolve one.Operationsalready held it, and the seed has no Market Risk role: the demonstration's Head of Market Risk works as an Operations account, so the deciders already resolved a name before this task. The task's report of a decider that could not is about the maker's side of the same page, which the Trading grant answers.- The read is the account read and not a tenant-wide one: row-level security
still narrows it to the parties the reader works in, the same exception
Member's own line states.
The seed is the source and the apply is per tenant, with the DDL user:
select ores_iam_role_permissions_assign_fn( '<tenant id>', 'Trading', 'iam::accounts:read'); select ores_iam_role_permissions_assign_fn( '<tenant id>', 'Viewer', 'iam::accounts:read');
Running it as postgres is refused (Invalid modified_by: postgres): the
function validates its actor against the account table, so the DDL user is the
one that works.
marketdata::curve_bootstrap:compute is now held by Trading as well. The
Check that a curve blueprint builds journey names running the test as needing
it, and only the tenant administrator's wildcard held it, so the Check tab
refused the trader it exists for. Fix 4 read "the check runs"; the missing code
is the other half of why it did not.
6.3. What the model could not express, and what the evidence seed had to fix
- The unique index over
(tenant_id, party_id, name)is declared, so nothing was lost here. The model has no per-column unique flag; the SQL section'sIndexestable is the only way to say it, and it says it. - The generated shell recipe spells the absent discount curve as
-, whichfrom_token<std::optional<...>>maps to nullopt. Nothing else in the generated set needed a hand edit. - The generated TypeScript filter type for a nullable filter column reads
string | null | null, because the C++ filter isstd::optional<std::optional<>>– the outer optional says "no filter", the inner null says "is null". The screen passes the outer one and the double null is the generator's spelling of it, not a second state. - The evidence seed's config id carried variant nibble
7, which the strict uuid reader the compute route uses refuses asInvalid UUID. It is now an RFC 4122 id, and the seed says why. The seed also wrote series straight into the table with no identity row, so no picker offered them; it now writes the identity projection the pickers read, and its config insert is guarded rather thanon conflict(the insert trigger refuses a second write at version zero before the conflict could resolve, so a re-apply failed).
7. Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario)
that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
8. PRs
| PR | Title |
|---|---|
9. Review
| Comment summary | File | Decision | Notes |
|---|---|---|---|