Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
ff8a39a
feat(breg): add read-only external views over Adapter projections
jeremi Nov 10, 2026
8bb3c68
refactor(breg): move external view runners out of the examples folder
jeremi Nov 10, 2026
621f62b
fix(breg): keep external projection withdrawal with migration authority
jeremi Nov 10, 2026
83dd5ba
refactor(breg): give external projections one cache lifetime
jeremi Nov 10, 2026
f9fa7cd
fix(breg): rebind external projections on authority changes only
jeremi Nov 10, 2026
69f6025
test(cli-docs): expect the external-source-facts generate target
jeremi Nov 10, 2026
b2ef3b1
feat(bregctl): add external-projection inspect, withdraw and rotate
jeremi Nov 10, 2026
5eb4ba9
fix(breg): answer incomplete external view coverage with its own problem
jeremi Nov 10, 2026
cd4e650
fix(breg): keep external snapshots through transient refresh failures
jeremi Nov 10, 2026
1ed6bcf
fix(breg): keep external view cursors and label sweeps across per-key…
jeremi Nov 10, 2026
702b81c
test(breg): name the store publication helper that skips eligibility
jeremi Nov 10, 2026
29b4197
fix(breg): derive external refresh leases from the page deadline
jeremi Nov 10, 2026
10cd081
fix(breg): record external refresh outcomes one key per transaction
jeremi Nov 10, 2026
b50067b
test(breg): refuse external view cursors replayed under other authority
jeremi Nov 10, 2026
05497f2
fix(breg): render external view date cursors in ISO 8601
jeremi Nov 10, 2026
e881c4e
fix(breg): bind external view cursors to the row boundary operator name
jeremi Nov 10, 2026
0c5c6e8
fix(breg): let the planner choose the external refresh page plan
jeremi Nov 10, 2026
bc451d4
test(breg): prove the indexed no-store refusal through the source bin…
jeremi Nov 10, 2026
8cbbe26
refactor(breg): drop the unreachable no-store retention permission
jeremi Nov 10, 2026
aaa52d0
fix(breg): log external projection refresh and purge outcomes by reason
jeremi Nov 10, 2026
51c0863
feat(bregctl): report each external projection's grant horizon in doctor
jeremi Nov 10, 2026
7f3de7e
docs(breg): match external projection monitoring advice to what is em…
jeremi Nov 10, 2026
99161de
fix(dev): check the opened state file instead of its path a second time
jeremi Nov 10, 2026
e480dbb
refactor(breg): move RetentionFailureReason to the postgres module
jeremi Nov 10, 2026
5b4dd36
test(breg): give harness databases and roles a random suffix
jeremi Nov 10, 2026
c0272f3
fix(breg): start the refresh continuation's base table scan at its bo…
jeremi Nov 10, 2026
213bbd5
test(breg): cover a live external view read over HTTP
jeremi Nov 10, 2026
fb70708
docs(breg): add external projection retention and doctor codes to mon…
jeremi Nov 10, 2026
80b7371
docs(breg): point the load-test guide at the external view scale fixture
jeremi Nov 10, 2026
5d4da56
fix(breg): keep one bad link key from making an indexed view unavailable
jeremi Nov 10, 2026
5bfc433
docs(breg): document projection backups, grant-bounded expiry and lin…
jeremi Nov 10, 2026
8fcb5e2
docs(breg): cite withdrawal proofs and the fresh-install restore mode…
jeremi Nov 10, 2026
0e5b807
docs(breg): list every external-projection bregctl code in BReg troub…
jeremi Nov 10, 2026
9cc7576
fix(breg): bound label acquisition pages to the continuation
jeremi Nov 10, 2026
9069e38
docs(breg): state that indexed page cost follows the authorized popul…
jeremi Nov 10, 2026
4040e21
fix(breg): count invalid label keys instead of dropping them in SQL
jeremi Nov 10, 2026
d9e3061
fix(breg): look up indexed view snapshots by primary key per root
jeremi Nov 10, 2026
2fd1f2c
test(breg): cover an unlinked root in an indexed related collection read
jeremi Nov 10, 2026
e96e04e
fix(bregctl): give external-projection prerequisite failures the shar…
jeremi Nov 10, 2026
6aea4ba
test(breg): name the unlinked-root case type so clippy accepts it
jeremi Nov 10, 2026
639d491
test: register the external view incomplete code in the client write …
jeremi Nov 10, 2026
c561fcb
fix: align the BReg runtime schema and external config with the conve…
jeremi Nov 10, 2026
6c3bffd
docs: add the external view obligations to the BReg security model
jeremi Nov 10, 2026
19b875b
fix: serve external decimal fields as strings on live and indexed paths
jeremi Nov 10, 2026
8b98a62
fix: resume external sort continuations from long sort values
jeremi Nov 10, 2026
2a865df
fix: serve malformed external link keys as no outside value and refus…
jeremi Nov 10, 2026
10698e9
fix: keep the link key rule inside registry-breg without the runtime …
jeremi Nov 10, 2026
fe249cc
test: expect a malformed link key to read as no outside value after r…
jeremi Nov 10, 2026
3c931ed
chore(identifiers): regenerate the catalog for the BReg schema changes
jeremi Nov 10, 2026
698dbdb
fix: observe projection responses after credential recovery and let n…
jeremi Nov 10, 2026
61ac119
fix: bind recovery names before use and call the fixture path helper …
jeremi Nov 10, 2026
6300b0e
fix: honour no-store only on successful responses and fail the projec…
jeremi Nov 10, 2026
4a33bd8
chore: tidy the external views load-test helpers
jeremi Nov 10, 2026
48d35a3
docs: say only a successful source response's no-store removes the ke…
jeremi Nov 10, 2026
ff1a7ac
fix: refuse a malformed link key when a proposal is prepared or an ac…
jeremi Nov 10, 2026
79946d9
test: pin the write-time link key check at the record write choke point
jeremi Nov 10, 2026
472d637
test: drop an unused value from the linked field write test
jeremi Nov 10, 2026
33a69d2
fix: serve exponent-range external decimals as plain strings instead …
jeremi Nov 10, 2026
023aa4a
fix: issue a continuation cursor only for a sort value the next page …
jeremi Nov 10, 2026
83c082f
fix: log a live external read that skips a malformed link key
jeremi Nov 10, 2026
a662acc
test: pin credential failure and no-store handling in projection acqu…
jeremi Nov 10, 2026
dc63134
test(adapter): give the projection-invalid code its bad-corpus member
jeremi Nov 10, 2026
2fa8a0f
fix(breg): audit external view reads with the per-client request context
jeremi Nov 10, 2026
322a624
chore(identifiers): regenerate the catalog after the rebase onto the …
jeremi Nov 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 17 additions & 4 deletions crates/registry-adapter-core/src/checks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1713,11 +1713,24 @@ fn source_route_checks(pack: &AdapterPack) -> Vec<Diagnostic> {
.collect();
for (index, source) in pack.source_routes.iter().enumerate() {
let prefix = format!("/sourceRoutes/{index}");
let ingestion = source.evidence.is_none()
|| source.scan.is_some()
let job_declared = source.scan.is_some()
|| source.next_recovery.is_some()
|| source.edits.is_some()
|| source.rescan_delay_seconds.is_some();
let ingestion = (source.evidence.is_none() && source.projection.is_none()) || job_declared;
if source.projection.as_ref().is_some_and(|projection| {
source.evidence.is_some()
|| job_declared
|| source.read_support.is_some()
|| !projection.identifiers_valid()
}) {
findings.push(finding(
CheckCode::PackProjectionInvalid,
&format!("{prefix}/projection"),
"the projection contract has incompatible route roles or identifiers",
"Use a separate projection route with valid authority and scheme identifiers.",
));
}
if ingestion && source.scan.is_none() {
findings.push(job_finding(
JobCode::SourceScanRequired,
Expand Down Expand Up @@ -2857,11 +2870,11 @@ fn evidence_only_endpoint_ids(pack: &AdapterPack) -> HashSet<String> {
if source.evidence.is_some() {
collect_source_endpoint_ids(&source.steps, &bodies, &mut evidence);
}
let ingestion = source.evidence.is_none()
|| source.scan.is_some()
let job_declared = source.scan.is_some()
|| source.next_recovery.is_some()
|| source.edits.is_some()
|| source.rescan_delay_seconds.is_some();
let ingestion = (source.evidence.is_none() && source.projection.is_none()) || job_declared;
if ingestion {
collect_source_endpoint_ids(&source.steps, &bodies, &mut ordinary);
}
Expand Down
4 changes: 4 additions & 0 deletions crates/registry-adapter-core/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pub enum CheckCode {
PackInvalidHeaderName,
PackMapParameterMisplaced,
PackReadSupportInvalid,
PackProjectionInvalid,
PackReservedHeaderName,
PackScriptUncompilable,
PackSqliteExtractInvalid,
Expand Down Expand Up @@ -207,6 +208,7 @@ impl CheckCode {
Self::PackGraphQlQueryRequired,
Self::PackInvalidHeaderName,
Self::PackMapParameterMisplaced,
Self::PackProjectionInvalid,
Self::PackReadSupportInvalid,
Self::PackReservedHeaderName,
Self::PackScriptUncompilable,
Expand Down Expand Up @@ -363,6 +365,7 @@ impl CheckCode {
Self::PackInvalidHeaderName => "adapter.pack.invalid-header-name",
Self::PackMapParameterMisplaced => "adapter.pack.map-parameter-misplaced",
Self::PackReadSupportInvalid => "adapter.pack.read-support-invalid",
Self::PackProjectionInvalid => "adapter.pack.projection-invalid",
Self::PackWriteSupportInvalid => "adapter.pack.write-support-invalid",
Self::PackReservedHeaderName => "adapter.pack.reserved-header-name",
Self::PackSqliteExtractInvalid => "adapter.pack.sqlite-extract-invalid",
Expand Down Expand Up @@ -583,6 +586,7 @@ impl CheckCode {
| Self::PackInvalidHeaderName
| Self::PackMapParameterMisplaced
| Self::PackReadSupportInvalid
| Self::PackProjectionInvalid
| Self::PackWriteSupportInvalid
| Self::PackReservedHeaderName
| Self::PackScriptUncompilable
Expand Down
36 changes: 33 additions & 3 deletions crates/registry-adapter-core/src/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1278,12 +1278,16 @@ fn is_default_csv_trim(value: &bool) -> bool {
#[must_use]
pub fn check_job(job: &AdapterJob, pack: &AdapterPack) -> Vec<Diagnostic> {
let mut findings = Vec::new();
if pack.source_routes.get(job.source.route.as_str()).is_none() {
if pack
.source_routes
.get(job.source.route.as_str())
.is_none_or(|route| route.projection.is_some())
{
findings.push(job_finding(
JobCode::RouteUnknown,
"/source/route",
"the job names no source route its pinned pack declares",
"Name a sourceRoutes id of the pinned pack.",
"the job names no ingestion source route its pinned pack declares",
"Name an ingestion sourceRoutes id of the pinned pack.",
));
}
findings.extend(profile_consistency_checks(&job.profile));
Expand Down Expand Up @@ -2278,6 +2282,32 @@ mod tests {
assert_eq!(findings[0].artifact.as_deref(), Some(ADAPTER_JOB_KIND));
}

#[test]
fn check_job_cannot_acquire_from_a_projection_route() {
let mut value = serde_json::to_value(pack()).expect("pack serializes");
let source = value["sourceRoutes"][0]
.as_object_mut()
.expect("source route");
for member in ["scan", "edits", "nextRecovery", "rescanDelaySeconds"] {
source.remove(member);
}
source.insert(
"projection".to_owned(),
serde_json::json!({
"operation":"exact", "operationVersion":"1", "authority":"urn:example:registry",
"purpose":"licensing", "scope":"licensing-subjects",
"selectorScheme":"urn:example:id", "recordKeyScheme":"urn:example:id",
"retention":"retain", "absence":"protected", "localDisclosure":"host-profiles",
"fieldsSchema":{"path":"fields.json","digest":DIGEST},
"maximumBatchKeys":1, "maximumPages":1, "maximumRows":1
}),
);
let projection_pack = serde_json::from_value(value).expect("projection pack parses");
let findings = check_job(&job("kobo-submissions"), &projection_pack);
assert_eq!(findings[0].code, JobCode::RouteUnknown.code());
assert_eq!(findings[0].artifact.as_deref(), Some(ADAPTER_JOB_KIND));
}

/// Each profile bound is a typed member: a value outside its
/// 1-to-maximum range fails to parse, so a job that declares one never
/// loads. The edge of each range parses; one past it does not.
Expand Down
2 changes: 2 additions & 0 deletions crates/registry-adapter-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ mod preview_expectations;
mod problem;
mod profile;
mod project;
mod projection;
mod result_schema;
mod route;
mod runtime;
Expand All @@ -66,6 +67,7 @@ pub use preview_expectations::*;
pub use problem::*;
pub use profile::*;
pub use project::*;
pub use projection::*;
pub use result_schema::*;
pub use route::*;
pub use runtime::*;
Expand Down
25 changes: 16 additions & 9 deletions crates/registry-adapter-core/src/pack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,11 +446,10 @@ pub struct EvidenceRouteContract {
pub acquisition_posture: EvidenceAcquisitionPosture,
}

/// One source route of the governed-ingestion mode
/// (`products/adapter/CONCEPT.md` sections 17.1 and 17.2): a route
/// definition's steps plus the item-contract declaration. A source route
/// is an entry point of the job mode, never a subroutine: a facade
/// binding naming it, or a `direct:` send targeting it, refuses
/// One source entry point for an ingestion, Evidence or projection host.
/// The host's declaration supplies the operation contract alongside the
/// steps. A source route is never a subroutine: a facade binding naming it,
/// or a `direct:` send targeting it, refuses
/// (`adapter.job.source-bound-refused`,
/// `adapter.job.source-called-refused`). There is no `from`: the
/// `from: file:` consumer form was rejected with the job's read step
Expand All @@ -459,7 +458,7 @@ pub struct EvidenceRouteContract {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SourceRouteDecl {
/// The source route identifier the job definition names.
/// The identifier the embedding host names.
pub id: LocalId,
/// Whether the scan ends. Mandatory; the checks refuse its absence
/// under `adapter.job.source-scan-required`.
Expand Down Expand Up @@ -492,6 +491,9 @@ pub struct SourceRouteDecl {
/// listener-free, read-only Evidence entry point.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evidence: Option<EvidenceRouteContract>,
/// Separately governed acquisition for a host that may retain a projection.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub projection: Option<crate::ProjectionRouteContract>,
/// The source-neutral read semantics this route explicitly supports. A
/// facade host maps these native inputs and guarantees to its public
/// protocol; it never infers them from a route id or source brand.
Expand Down Expand Up @@ -567,9 +569,8 @@ pub struct AdapterPack {
/// The internal `direct:` routes shared sub-flows call.
#[serde(default)]
pub routes: UniqueIdList<RouteDefinition>,
/// The governed-ingestion source routes the job mode runs
/// (`products/adapter/CONCEPT.md` sections 17.1 and 17.2). Entry
/// points of the job mode; a facade route never reaches one.
/// The source routes of the ingestion, Evidence and projection hosts.
/// A facade route never reaches one.
#[serde(default)]
pub source_routes: UniqueIdList<SourceRouteDecl>,
/// The code maps routes convert values through.
Expand Down Expand Up @@ -614,6 +615,9 @@ impl AdapterPack {
)
.collect();
for source in &self.source_routes {
if let Some(projection) = &source.projection {
files.push(&projection.fields_schema);
}
if let Some(evidence) = &source.evidence {
files.push(&evidence.inputs_schema);
files.push(&evidence.facts_schema);
Expand Down Expand Up @@ -861,6 +865,7 @@ mod tests {
edits: Some("ordered".to_string()),
rescan_delay_seconds: None,
evidence: None,
projection: None,
steps: Vec::new(),
}])
.expect("unique"),
Expand Down Expand Up @@ -948,6 +953,7 @@ mod tests {
edits: None,
rescan_delay_seconds: None,
evidence: None,
projection: None,
steps: Vec::new(),
},
SourceRouteDecl {
Expand All @@ -958,6 +964,7 @@ mod tests {
edits: None,
rescan_delay_seconds: None,
evidence: None,
projection: None,
steps: Vec::new(),
},
])
Expand Down
Loading