Skip to content

feat(bregctl): derive a registry project from PublicSchema with init --from - #918

Merged
jeremi merged 33 commits into
mainfrom
feat/bregctl-init-from-publicschema
Sep 7, 2026
Merged

feat(bregctl): derive a registry project from PublicSchema with init --from#918
jeremi merged 33 commits into
mainfrom
feat/bregctl-init-from-publicschema

Conversation

@jeremi

@jeremi jeremi commented Sep 7, 2026

Copy link
Copy Markdown
Member

Summary

bregctl init DEST --from publicschema derives a registry project from a selection over the PublicSchema reference model instead of writing the domain-neutral example. Plain bregctl init DEST is unchanged. The selection comes from one of three sources that converge on a single document (kind: ModelSelection, echoed into the project at model/selection.yaml):

  • --starter household, a selection shipped with the model (Person, Household, GroupMembership);
  • --selection FILE, a selection document the adopter edits and passes back;
  • an interactive wizard at a terminal, eight prompts, output on stderr; without a terminal the command refuses with init.selection.missing.

The derived project is ordinary source that check, dev, and build read like any other. The derivation is import only. Locked rules: an enumeration of at most 300 values becomes a closed vocabulary, a larger one a bounded code; a property whose value is another selected concept becomes a reference; a many-valued reference is refused with guidance to model it as its own entity; a property the model marks sensitive becomes a restricted field, so derived projects report access.profile.higher_classification and the README explains why rather than hiding it.

Commits are one per owning area:

  • crates/: the registry-linkml crate (a LinkML reader and the vendored PublicSchema 0.3.0 snapshot with PIN.yaml and sync-snapshot.sh), the household starter, init --from in registry-bregctl, and the wizard;
  • docs/site/: the regenerated CLI reference and the tutorial tutorials/derive-a-registry-from-publicschema;
  • products/breg/: a decision-log bullet;
  • external/: the snapshot recorded as a vendored input.

Checks

  • cargo fmt --check, cargo clippy --locked --profile ci --all-targets -p registry-bregctl -- -D warnings, cargo test --locked --profile ci -p registry-bregctl (213 lib tests + 87 CLI tests) and -p registry-linkml.
  • products/breg/scripts/check_source_neutrality.py and products/breg/scripts/check-contracts.sh.
  • cargo deny check: the duplicate-version warnings are the ones already on main.
  • docs/site: npm test (545) and the full npm run check chain (26227 internal links).
  • Boot proof: the derived household project started under bregctl dev on PostgreSQL 17, its schema-test rehearsal replayed the first-records journey, live reads with a reader token answered, session removed with dev stop --remove.
  • The wizard was driven end to end through a pseudo-terminal with expect; the result passed bregctl check.
  • Tests added with the change: reader and snapshot tests in registry-linkml; selection parsing, resolver, renderer, and wizard unit tests in registry-bregctl; CLI tests for --starter, --selection (symlink and size refusals), the model mismatch, and the no-terminal refusal; docs adoption-path and replay-gate coverage updated for the tutorial.

Notes

  • Dependency review: the vendored PublicSchema snapshot pins upstream commit ca9861a13391aa8c20f8b2a1d4b470a99d01ee7d (2026-05-19, version 0.3.0, CC-BY-4.0). That commit is not yet on the upstream repository's main. external/README.md records it as a current vendored input; please review it as a product and dependency decision.
  • inquire is added to registry-bregctl for the wizard; Cargo.lock changes accordingly.
  • Any change to the clap definitions regenerates all 129 CLI reference pages because of the embedded catalogue digest; that is the bulk of the docs diff.
  • Derived projects intentionally report access.profile.higher_classification for sensitive properties read by the operator profile. The README of a derived project explains the finding and how to narrow it.
  • The tutorial is status: current, matching the other BReg tutorials that describe unreleased bregctl behaviour.
  • No security-sensitive surface is touched: no authentication, authorization, signing, audit, or release code changes.

DCO

  • Every commit includes a Signed-off-by trailer.
  • I reviewed the submitted changes and am responsible for the contribution.

bregctl will offer a registry project drawn from the PublicSchema reference model. The model is LinkML, and the Base Registry Engine source-neutrality gate keeps domain vocabulary out of the breg crates, so the reader and the vendored snapshot live in their own crate: a bundle of LinkML files in, one resolved model out, with the LinkML features it does not model refused rather than dropped. The snapshot is pinned in PIN.yaml and refreshed by sync-snapshot.sh.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Embed named starter selections beside the PublicSchema snapshot so adopter
tooling can derive a first project without asking the adopter to write a
selection document. The first starter covers a person, a household, and the
membership that links them.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Plain init keeps writing the domain-neutral example. With --from, init
derives the project from the embedded PublicSchema snapshot instead: a
selection document (--selection) or a shipped starter (--starter) names the
concepts and properties, and the resolver turns them into entities, fields,
closed vocabularies, references, and structured values, with sensitive
properties classified restricted. The written project carries an operator
and a reader profile, dependency-ordered journeys, dev clients, a README
that explains each derivation and the findings check reports, and an echo
of the selection to derive again from. The interactive path refuses without
a terminal and is not written yet.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Generated by scripts/generate-cli-reference.mjs; the catalog digest changes
in every page, and the init page gains the --from, --selection, and
--starter options with their constraints.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Without a selection file or a starter, `init --from` now gathers the
selection through prompts: registry identifier and title, the concepts,
the properties of each, the target of any reference that fits several
chosen concepts, the identifiers and routes, the code lists to list in
full, and a final review of the document before anything is written.

The prompts offer only what the resolver accepts and validate identifiers
with its grammar, so an answer taken at a prompt is never refused after
the project is derived. The resolver's range-fit, per-property support,
and identifier rules are factored out so the wizard shares them instead
of restating them. Every decision beneath the prompts is a pure function
with tests, and a sweep test derives every concrete concept from the
answers the wizard can produce.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The init --from publicschema path had no page a reader could follow.
The tutorial derives the household starter, reads the attribution and
the field tables init wrote, checks and starts the project, edits the
selection and derives again, and describes the eight wizard prompts in
the order the binary asks them. Every transcript comes from the binary.

The page sits beside the module tutorial in the sidebar and in the
adoption-path test, and is listed with the BReg tutorials the two replay
gates exclude, with the reason, so the coverage check keeps passing.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
init --from <MODEL> is import only, PublicSchema is the first model, and
the snapshot is a pinned external input; the decision log did not say so.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The README lists every vendored input and how it is refreshed; the
PublicSchema snapshot under crates/registry-linkml was missing from it.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-07T17:23:08.598949Z 09bd3a9 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5bacf3fcfb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/registry-bregctl/src/init_from_model/resolve.rs Outdated
Comment thread crates/registry-bregctl/src/init_from_model/resolve.rs Outdated
Comment thread crates/registry-bregctl/src/init_from_model/mod.rs Outdated
Comment thread crates/registry-linkml/publicschema/sync-snapshot.sh
Comment thread crates/registry-bregctl/src/init_from_model/render.rs
Comment thread crates/registry-bregctl/src/init_from_model/resolve.rs Outdated
Comment thread crates/registry-bregctl/src/init_from_model/render.rs Outdated
Comment thread crates/registry-bregctl/src/init_from_model/render.rs Outdated
The bregctl text rendering was one flat line per record, so a run of
findings sharing a code repeated the same sentence against every path it
was reported against, the artifact count sat between the findings and the
steps, and neither the severity nor the count a reader has to act on
appeared at all. relayctl already answers this in its own report module,
so this brings the same discipline over: a lead sentence carrying the
counts, aligned label and value detail, blank-line separated sections,
and a closing count.

It adds two things that discipline did not need. Findings sharing a
severity and a code state their sentence once and list every path under
it, because a registry project applies one access rule across every
entity and so reports one mistake against six paths. And every line is
written with the ANSI attributes anstyle names, which main_entry lets
anstream keep for a terminal and strip for a pipe, a file, or a test
harness, so a captured transcript stays the plain ASCII it was.

Authored names are escaped before they are styled, so the only escape
sequences in a finished rendering are the ones the renderer wrote.

anstream and anstyle were already in the lockfile behind clap, so this
adds two dependency edges and no new package.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…ncing the snapshot

The attribution CC-BY-4.0 asks for must carry a link to the licence, so
PIN.yaml records it and Pin reads it. The sync script copies the working
tree while recording HEAD, so a checkout with edits or untracked files
under the copied paths is refused before anything is written.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
A structured field carrying an unselected concept inline is classified
for the most protected property it holds, so a reader profile no longer
reads a protected value through it. A vocabulary override applies to a
property holding many values too, as the echoed selection says it does.
A bounded code is sized for the longest code of its enumeration, within
the compiler's bound, so no code of the model is refused. The registry
identifier leaves room for the names derived from it, so the refusal
comes before the destination exists rather than from the compiler after.
The wizard gate checks the streams the prompts use, standard input and
standard error, so a piped standard output still runs it. The README
carries the licence link and marks the project as a modified form of
the model, and the identifier field is described as supplied by the
caller, which is what the journeys do.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The remaining renderers still wrote flat `label: value` lines, so a
reader had no severity, no counts, and no column to scan. They now build
a report::Lines like the converted ones do.

Two of them needed the module to grow a shape it did not have. The
access explanation is a list of long sentences, which reads as one
paragraph once the sentences wrap, so `listed` hangs a continuation the
way `steps` already does. The synthetic admission preview exists to
answer one question, so `verdict` puts the attribute on the answer
rather than the heading, and a refusal reads as an answer rather than an
error, because that is what it is.

`counted` pluralized by appending an `s`, which named "8 dependencys"
and "2 deliverys". It now applies the consonant-before-`y` rule, leaving
"journeys" alone.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ca4db8b8b2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/registry-bregctl/src/init_from_model/resolve.rs
Comment thread crates/registry-bregctl/src/init_from_model/render.rs Outdated
Comment thread crates/registry-bregctl/src/init_from_model/render.rs
Comment thread crates/registry-linkml/src/reader.rs Outdated
Comment thread crates/registry-linkml/src/reader.rs Outdated
The renderer labelled the non-fatal severity `warning`, which is the
word relayctl uses. bregctl's own word is `finding`: it is what the JSON
`severity` carries and what the configuration reference defines, and a
reader who has both open should not have to translate between them.

A grouped code now counts the paths it collapsed rather than repeating
the finding count the closing line already gives.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
A reader scanning the dev report for one client's credential paths finds
the block by its blank line above; the first client had none, so it ran
into the session detail directly above it.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…at stays unlinked

The model links its concepts through concepts of their own rather than
through direct properties, so a selection of several concepts derives a
registry of unrelated collections without anyone saying so. The resolver
now finds the concepts not selected that refer to the selected ones at
least twice, the wizard offers them after the concepts are chosen and
ticks a reference to a chosen concept by default, and the entities no
field connects are named in the review, in the README beside the
concepts that would connect them, and in the report's next steps. A
registry of unrelated collections stays a legitimate answer.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Every transcript on these pages quoted the flat `label: value` output the
tool no longer writes, and the prose around them named lines that no
longer exist: a `next:` line, an `artifacts:` count, a first line reading
`synthetic profile admission: allowed (...)`. Each block here is the
current rendering, captured from a real run where the command needs
neither a database nor a network.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The tutorial showed the flat `label: value` transcript and named a
`next:` line and an `artifacts` count that the report no longer prints, so
a reader following it could not match what the terminal shows to what the
page says.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Only the JSON rendering of `init --from publicschema` was asserted, so
nothing held the derived project's text report to the shape the tutorial
quotes: the lead sentence, the grouped finding line, the closing count,
and the numbered next steps.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4c3c74f5d2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/registry-bregctl/src/init_from_model/resolve.rs
Comment thread crates/registry-bregctl/src/init_from_model/resolve.rs Outdated
Comment thread crates/registry-bregctl/src/init_from_model/render.rs Outdated
Comment thread crates/registry-bregctl/src/init_from_model/resolve.rs
Comment thread crates/registry-bregctl/src/init_from_model/render.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 33c14bcf3d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/registry-bregctl/src/report.rs Outdated
Comment thread crates/registry-bregctl/src/init_from_model/resolve.rs
…ommit

The previous pin sat on a branch that was squash-merged upstream, so it
was no longer reachable from main. The merged commit moves the CRVS and
social-protection concept IRIs under their own namespaces and changes
no definition the household starter uses.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The page explained the local clients file field by field, restated the
project files the README already lists, and re-explained response
mechanics at every step. The clients explanation now lives on the
configured-registry page, each step keeps one line of context, and no
paragraph runs past sixty words. Commands, checked output, and every
linked anchor are unchanged.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Routing the renderers through the report module dropped the three
migration bounds from the human explain output, closed a findings-only
refusal with a claim of zero errors, rendered a rebaseline caveat as a
step, and lost the entity and profile nouns of an access explanation.
Each is restored and pinned by a test, labels are escaped like values,
an unchanged project lock no longer reports zero artifacts, and a
process test proves an authored claim name cannot forge a report line.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The change-review tutorial carried a transcript that was never captured:
it claimed eighteen findings under two codes where the tool prints
twenty-six under four. The refusal line and the access explanation
blocks follow the renderer's current wording.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…ules out

The tutorial restated the derivation rules the generated README already
carries and walked all nine wizard prompts inline, which made it read
as reference. The prompts now have a reference page, the rules an
explanation page, and the tutorial keeps the commands, the output the
reader checks, and one line of context per step. The create request
body is corrected to the camel-case names the API accepts, and the
troubleshooting table names the runtime version mismatch that fails a
local start.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c77e263995

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/registry-bregctl/src/init_from_model/resolve.rs
Comment thread crates/registry-bregctl/src/init_from_model/resolve.rs Outdated
Comment thread crates/registry-bregctl/src/init_from_model/resolve.rs Outdated
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…cal URIs through

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
… before writing

Inline vocabulary overrides above the codelist bound, identifier fields the
compiler keeps for itself, field identifiers that normalize to one API or SQL
name, and targets on properties that hold no records are refused during
resolution so init leaves no destination behind. Connectors must reach two
distinct selected concepts, a self-reference no longer counts as a link, and
generated geometry and inlined temporal values keep their shapes.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…what elevates a field

Split a generated journey that would exceed the fixture's step limit, keep
step and capture identifiers within the fixture grammar, tell a field the
model marks sensitive apart from one elevated only by its public entity in
the README and the next steps, and state that a structured field's value is
not projected to the manifest.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
… reserved-name rule

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…nifest-core

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
@jeremi
jeremi merged commit 0ecca30 into main Sep 7, 2026
50 checks passed
@jeremi
jeremi deleted the feat/bregctl-init-from-publicschema branch September 7, 2026 17:21

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09bd3a967a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +661 to +664
let folded: String = name
.chars()
.map(|character| if character == '_' { '-' } else { character })
.collect();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep folded fixture identifiers unique

When a valid selection contains entity identifiers that differ only by _ versus -, such as person_record and person-record, this normalization produces identical create/get step and capture identifiers. Resolution considers the original IDs distinct, but fixture validation rejects the generated journey for duplicate identifiers when bregctl test or dev reads it. Detect collisions after fixture normalization or include the original identifier in a digest even when the folded value is within the length bound.

Useful? React with 👍 / 👎.

Comment on lines +919 to +923
if required.is_empty() {
if let Some((name, property)) = properties.iter().find(|(_, property)| {
property.get("type") == Some(&json!("string")) && property.get("enum").is_none()
}) {
object.insert(name.clone(), example_of_schema(property));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Generate examples that satisfy temporal patterns

When an unselected concept is carried inline and its first eligible string property is temporal—for example, FunctioningProfile.respondent inlines Person, whose first such property is date_of_birth—this branch selects that property and example_of_schema writes "example". The newly added date pattern rejects that value, so the generated tests/journeys.yaml cannot be validated by bregctl test or dev. Either leave the optional object empty or generate a value that honors pattern and the rest of the selected property's schema.

Useful? React with 👍 / 👎.

Comment on lines +752 to +755
"fields: {{{}: {}}}",
scalar(&entity.identifier.id),
scalar(&identifier_value)
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep expected identifiers equal to truncated request values

When a hand-written selection uses an entity ID longer than 62 characters together with a short custom identifierField, identifier_value exceeds the generated field's 64-character bound. example_value truncates the create request, but this expectation still uses the untruncated value, so the generated journey is rejected or fails when the returned record is compared. Derive one bounded identifier value and reuse it in both the request and expectation.

Useful? React with 👍 / 👎.

Comment on lines +763 to +765
ResolvedKind::internal(FieldKind::Reference {
target: entry.id.clone(),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Raise references to restricted target classifications

When a selection classifies a referenced entity as restricted but leaves the carrying entity readable—for example, a restricted Person targeted by an internal FunctioningProfile.respondent—this assigns the reference only its ordinary internal floor. The generated reader profile filters on the reference field's classification, not the target entity's, so it can return the restricted record's identifier and disclose the relationship even though it cannot read the target collection. Promote a reference to at least its selected target's classification and add a focused negative access test.

AGENTS.md reference: products/breg/AGENTS.md:L150-L157

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant