db::catalog::set_object_fields intentionally does not enforce required-field completeness — a caller may set any subset. Its doc comment notes that completeness "belongs to the publish gate (when moving to Visibility::Public)". Plan 7 built the stepwise transition machine but deferred this gate to keep scope tight.
What to do
When transitioning a record to Visibility::Public (in set_visibility, or a dedicated publish path), validate that every FieldDefinition with required = true has a present value on the object. Reject the publish with a clear error listing the missing required fields if not.
Open questions
Should the gate live in db::catalog::set_visibility (so every caller is protected) or at the service/API layer?
Required-completeness for the typed inventory minimum vs. flexible required fields — both, presumably.
Plan: docs/plans/2026-06-02-publishing-public-api.md (Notes for follow-on plans)
## Context
`db::catalog::set_object_fields` intentionally does **not** enforce required-field completeness — a caller may set any subset. Its doc comment notes that completeness "belongs to the publish gate (when moving to `Visibility::Public`)". Plan 7 built the stepwise transition machine but deferred this gate to keep scope tight.
## What to do
When transitioning a record to `Visibility::Public` (in `set_visibility`, or a dedicated publish path), validate that every `FieldDefinition` with `required = true` has a present value on the object. Reject the publish with a clear error listing the missing required fields if not.
## Open questions
- Should the gate live in `db::catalog::set_visibility` (so every caller is protected) or at the service/API layer?
- Required-completeness for the typed inventory minimum vs. flexible required fields — both, presumably.
## References
- `crates/db/src/catalog.rs` — `set_object_fields` doc comment; `set_visibility`
- `domain::FieldDefinition { required, .. }`
- Plan: `docs/plans/2026-06-02-publishing-public-api.md` (Notes for follow-on plans)
Implemented and merged to main (e96f74f, no-op fix 8cfcf07).
Decisions (the issue's open questions):
Gate lives in db::catalog::set_visibility — every caller is protected and the check is atomic with the transition (integrity-critical logic stays in db).
Only flexible required fields are checked. The typed inventory-minimum columns (object_number, object_name, number_of_objects) are already NOT NULL in 0003_object.sql; the other typed columns are optional. So the gate covers field definitions with required = true, verifying each key is present and non-null in the object's fields JSONB.
Behaviour:
Transitioning to Public with missing required fields → VisibilityError::MissingRequiredFields(keys); the admin publish endpoint maps it to 422, and the transition is rejected (visibility unchanged, no audit).
The gate fires only on an actual transition into public, so the documented set-to-current idempotent no-op is preserved even if a required field is introduced after publish (regression test added).
Tests:db reject-then-succeed round-trip + the re-set no-op; api real-HTTP 422 with visibility unchanged. Full workspace green.
Note: the response is a bare 422 (consistent with the other admin validation errors, which also don't return a structured body). The db error variant carries the missing-key list; surfacing it in the response body — across all the admin 422 paths — would be a worthwhile separate enhancement. Closing.
Implemented and merged to `main` (`e96f74f`, no-op fix `8cfcf07`).
**Decisions (the issue's open questions):**
- **Gate lives in `db::catalog::set_visibility`** — every caller is protected and the check is atomic with the transition (integrity-critical logic stays in `db`).
- **Only flexible required fields are checked.** The typed inventory-minimum columns (`object_number`, `object_name`, `number_of_objects`) are already `NOT NULL` in `0003_object.sql`; the other typed columns are optional. So the gate covers field definitions with `required = true`, verifying each key is present and non-null in the object's `fields` JSONB.
**Behaviour:**
- Transitioning to `Public` with missing required fields → `VisibilityError::MissingRequiredFields(keys)`; the admin publish endpoint maps it to **422**, and the transition is rejected (visibility unchanged, no audit).
- The gate fires only on an *actual* transition into public, so the documented set-to-current idempotent no-op is preserved even if a required field is introduced after publish (regression test added).
**Tests:** `db` reject-then-succeed round-trip + the re-set no-op; `api` real-HTTP 422 with visibility unchanged. Full workspace green.
**Note:** the response is a bare 422 (consistent with the other admin validation errors, which also don't return a structured body). The db error variant carries the missing-key list; surfacing it in the response body — across all the admin 422 paths — would be a worthwhile separate enhancement. Closing.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Context
db::catalog::set_object_fieldsintentionally does not enforce required-field completeness — a caller may set any subset. Its doc comment notes that completeness "belongs to the publish gate (when moving toVisibility::Public)". Plan 7 built the stepwise transition machine but deferred this gate to keep scope tight.What to do
When transitioning a record to
Visibility::Public(inset_visibility, or a dedicated publish path), validate that everyFieldDefinitionwithrequired = truehas a present value on the object. Reject the publish with a clear error listing the missing required fields if not.Open questions
db::catalog::set_visibility(so every caller is protected) or at the service/API layer?References
crates/db/src/catalog.rs—set_object_fieldsdoc comment;set_visibilitydomain::FieldDefinition { required, .. }docs/plans/2026-06-02-publishing-public-api.md(Notes for follow-on plans)Implemented and merged to
main(e96f74f, no-op fix8cfcf07).Decisions (the issue's open questions):
db::catalog::set_visibility— every caller is protected and the check is atomic with the transition (integrity-critical logic stays indb).object_number,object_name,number_of_objects) are alreadyNOT NULLin0003_object.sql; the other typed columns are optional. So the gate covers field definitions withrequired = true, verifying each key is present and non-null in the object'sfieldsJSONB.Behaviour:
Publicwith missing required fields →VisibilityError::MissingRequiredFields(keys); the admin publish endpoint maps it to 422, and the transition is rejected (visibility unchanged, no audit).Tests:
dbreject-then-succeed round-trip + the re-set no-op;apireal-HTTP 422 with visibility unchanged. Full workspace green.Note: the response is a bare 422 (consistent with the other admin validation errors, which also don't return a structured body). The db error variant carries the missing-key list; surfacing it in the response body — across all the admin 422 paths — would be a worthwhile separate enhancement. Closing.