Compare commits

..

7 Commits

Author SHA1 Message Date
c1c16bc814 version: 1.0.168 2026-07-04 18:21:36 -04:00
5885552192 database: page carries 'personal' (the personal-scope sibling punc) and sidebar carries 'icon' — previously stripped by the whitelist
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:21:30 -04:00
dc033296d7 version: 1.0.167 2026-07-03 20:04:28 -04:00
03a871bc1a filter synthesis: named value types get their own synthesized, referenced filter
Supersedes the inline-structural approach (0d282cc): inlining erased the value
type's name, so identical nested shapes (weekly_hours at two paths) generated
duplicate leaf types downstream and Dart barrel exports collided. Now a named
non-table value type's filter is synthesized ONCE (like table-backed boundaries)
and property references point at it — mirroring how the entity side generates
one class per named type. Same filter-by-fields capability; laziness also
removes the structural-recursion hazard. A named type with no compilable
structure still gets no filter and is omitted rather than dangled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:04:16 -04:00
8aa15873b0 version: 1.0.166 2026-07-03 19:24:16 -04:00
0d282cc930 filter synthesis: compile named non-table value types structurally
A property typed as a named value type (a schema-only config object like
an operating-hours schedule) previously got a dangling {type}.filter
reference — no filter is ever synthesized for a non-table-backed schema,
so the whole parent filter failed downstream (PROXY_TYPE_RESOLUTION_FAILED;
the punc generator emitted an empty filter type).

Naming a value type is a reuse choice, not a semantics choice: it now
compiles structurally into the parent filter, exactly like an inline
object, recursively (including array items). Table-backed boundaries keep
the lazy {type}.filter reference. A named type with no compilable
structure is omitted instead of dangling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 19:23:54 -04:00
581fc8e0c0 flow update 2026-07-03 01:33:00 -04:00
6 changed files with 142 additions and 9 deletions

View File

@ -87,6 +87,34 @@
"type": "string" "type": "string"
} }
} }
},
"schedule": {
"type": [
"opening_hours",
"null"
]
}
}
},
"season": {
"type": "object",
"properties": {
"label": {
"type": "string"
}
}
},
"opening_hours": {
"type": "object",
"properties": {
"open": {
"type": "string"
},
"seasons": {
"type": "array",
"items": {
"type": "season"
}
} }
} }
} }
@ -262,6 +290,7 @@
"uuid_field", "uuid_field",
"tags", "tags",
"ad_hoc", "ad_hoc",
"schedule",
"$and", "$and",
"$or" "$or"
], ],
@ -277,6 +306,7 @@
"uuid_field", "uuid_field",
"tags", "tags",
"ad_hoc", "ad_hoc",
"schedule",
"$and", "$and",
"$or" "$or"
], ],
@ -298,6 +328,7 @@
"uuid_field", "uuid_field",
"tags", "tags",
"ad_hoc", "ad_hoc",
"schedule",
"$and", "$and",
"$or" "$or"
], ],
@ -366,6 +397,12 @@
"string.condition", "string.condition",
"null" "null"
] ]
},
"schedule": {
"type": [
"opening_hours.filter",
"null"
]
} }
}, },
"type": "filter" "type": "filter"
@ -483,10 +520,66 @@
] ]
} }
} }
} },
"opening_hours": {},
"season": {},
"opening_hours.filter": {
"type": "filter",
"compiledPropertyNames": [
"open",
"seasons",
"$and",
"$or"
],
"properties": {
"open": {
"type": [
"string.condition",
"null"
]
},
"seasons": {
"type": [
"season.filter",
"null"
]
},
"$and": {
"type": [
"array",
"null"
],
"items": {
"type": "opening_hours.filter",
"compiledPropertyNames": [
"open",
"seasons",
"$and",
"$or"
]
}
},
"$or": {
"type": [
"array",
"null"
],
"items": {
"type": "opening_hours.filter",
"compiledPropertyNames": [
"open",
"seasons",
"$and",
"$or"
]
}
}
}
},
"season.filter": {}
} }
} }
} }
] ]
} }
] ]

2
flows

Submodule flows updated: c6af26cece...0d9bd8644e

View File

@ -165,8 +165,20 @@ impl Schema {
} else if db.enums.contains_key(custom) { } else if db.enums.contains_key(custom) {
Some(vec![format!("{}.condition", custom)]) Some(vec![format!("{}.condition", custom)])
} else { } else {
// Assume anything else is a Relational cross-boundary that already has its own .filter dynamically built // A named type gets a reference to its dynamically built .filter — either a
Some(vec![format!("{}.filter", custom)]) // Relational cross-boundary (table-backed) or a named value type, whose filter
// is likewise synthesized (see Database::compile_filters). A named type with no
// compilable structure gets no filter — omit it rather than dangle a reference.
let base = custom.split('.').next_back().unwrap_or(custom);
let has_value_filter = db
.schemas
.get(custom)
.map_or(false, |s| Database::is_value_filter_candidate(custom, s));
if db.types.contains_key(base) || has_value_filter {
Some(vec![format!("{}.filter", custom)])
} else {
None
}
} }
} }
} }

View File

@ -262,15 +262,23 @@ impl Database {
} }
} }
/// Synthesizes Composed Filter References for all table-backed boundaries. /// Synthesizes Composed Filter References for all table-backed boundaries — and for
/// named non-table value types (schema-only objects, e.g. an operating-hours config),
/// so a property reference resolves to ONE named filter instead of inlining anonymous
/// per-path copies (which duplicate identical leaf type names for downstream codegen).
fn compile_filters(&mut self, errors: &mut Vec<crate::drop::Error>) -> Vec<(String, String)> { fn compile_filters(&mut self, errors: &mut Vec<crate::drop::Error>) -> Vec<(String, String)> {
let mut filter_schemas = Vec::new(); let mut filter_schemas = Vec::new();
let mut seen_value_ids = std::collections::HashSet::new();
for (type_name, type_def) in &self.types { for (type_name, type_def) in &self.types {
for (id, schema_arc) in &type_def.schemas { for (id, schema_arc) in &type_def.schemas {
// Only run synthesis on actual structured, table-backed boundaries. Exclude subschemas! // Run synthesis on structured table-backed boundaries and named value types.
// Exclude subschemas!
let base_name = id.split('.').last().unwrap_or(id); let base_name = id.split('.').last().unwrap_or(id);
let is_table_backed = base_name == type_def.name; let is_table_backed = base_name == type_def.name;
if is_table_backed && !id.contains('/') { let is_value_type = !is_table_backed
&& Self::is_value_filter_candidate(id, schema_arc)
&& seen_value_ids.insert(id.clone());
if (is_table_backed || is_value_type) && !id.contains('/') {
if let Some(filter_schema) = schema_arc.compile_filter(self, id, errors) { if let Some(filter_schema) = schema_arc.compile_filter(self, id, errors) {
filter_schemas.push(( filter_schemas.push((
type_name.clone(), type_name.clone(),
@ -293,6 +301,20 @@ impl Database {
filter_ids filter_ids
} }
/// A named non-table value type that earns its own synthesized filter: a bare-named
/// (dotless) schema-only object with compiled properties. The base `filter`/`condition`
/// schemas are infrastructure, not value types.
pub fn is_value_filter_candidate(id: &str, schema: &Arc<Schema>) -> bool {
!id.contains('.')
&& id != "filter"
&& id != "condition"
&& schema
.obj
.compiled_properties
.get()
.map_or(false, |props| !props.is_empty())
}
/// Synthesizes strong Enum Conditions mirroring the string.condition capabilities. /// Synthesizes strong Enum Conditions mirroring the string.condition capabilities.
fn compile_conditions(&mut self) -> Vec<(String, String)> { fn compile_conditions(&mut self) -> Vec<(String, String)> {
let mut enum_conditions = Vec::new(); let mut enum_conditions = Vec::new();

View File

@ -13,6 +13,10 @@ pub struct Page {
pub sidebar: Option<Sidebar>, pub sidebar: Option<Sidebar>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub actions: Option<IndexMap<String, Action>>, pub actions: Option<IndexMap<String, Action>>,
/// The personal-scope sibling punc for this page ("one surface, two scopes"):
/// the caller-scoped read the page issues when the app is in personal scope.
#[serde(skip_serializing_if = "Option::is_none")]
pub personal: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
@ -22,4 +26,6 @@ pub struct Sidebar {
pub category: Option<String>, pub category: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<i32>, pub priority: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
} }

View File

@ -1 +1 @@
1.0.165 1.0.168