Repository navigation
Share one RelationshipPath compiled to SQL between queries and plays, and type play rules (closes #3449) - #3481
Conversation
Pragmatic Code Review: PR #3481Review Type: Initial Review (no prior reviews or comments on the PR; all commits on the branch reviewed) Summary. The design is sound and the PR is a clear net improvement: three name resolvers collapse into Two things should be fixed before merge: a sort regression caused by the new subtype matching (finding 1), and the open-ended walk, which does not terminate on a cycle the way its comment, its test and the docs say (finding 2). Severity labels: Critical, Important, Suggestion. Each finding says whether it is verified (and how) or suspected. Requirements Check (issue #3449)
Code Review Findings1. Important: sorting a base-type query mis-orders subtype rows (regression from subtype matching)
_ => {
let val_a = a.properties.get(&a.node_type).and_then(|ns| ns.get(field));
let val_b = b.properties.get(&b.node_type).and_then(|ns| ns.get(field));The type filter now matches subtypes, so a
2. Important: an open-ended walk over a cycle laps until the depth cap; comment, test and docs say it does not
"{walker}({columns}, depth) AS (\
SELECT {carry_p}j.value, 1 FROM {previous} p, json_each(({first})) j \
UNION \
SELECT {carry_w}j.value, w.depth + 1 FROM {walker} w, json_each(({again})) j \
WHERE w.depth < {OPEN_ENDED_DEPTH_CAP})",
3. Important: the scan's "one query" is 1 + N statements
let node_ids = self.execute_query_for_ids(&built).await?;
...
for id in node_ids {
if let Some(node) = self.store.get_node(&id).await? {
4. Important: a selector's filters accept unknown keys and use a different key casing from the rest of the rule
#[serde(rename_all = "camelCase")]
pub struct QueryFilter {
5. Important: unused public code
6. Suggestion: scan-time resolution of condition paths (staleness trade-off)
I checked the claim in the PR description: action bindings and
7. Suggestion: a reverse name shared by an untyped declaration and another declaration resolves differently than before
if let Some((source_type, rel)) = inbound.iter().find(|(_, rel)| rel.reverse_name == name) {The old play resolver went through
8. Suggestion: the frontend live-append gate still compares types exactly
if (gate.targetType !== '*' && node.nodeType !== gate.targetType) return false;The backend now returns subtypes for a typed query; this gate declines a newly created
9. Suggestion: the model-facing filter change has had no eval
Nits
What I checked and found sound
Recommendation: REQUEST CHANGESRequired before merge: finding 1 (sort regression) and finding 2 (open-ended walk on cycles, plus the test and docs that describe it). Findings 3 to 5 are small and worth folding into the same pass. The rest can follow. |
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: REQUEST CHANGES (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
Review addressed (commit
|
Pragmatic Code Review: PR #3481Review Type: Re-Review (scope: commit Previous Review Summary
What I ran: How each previous finding stands
Removing the depth cap is not a regression. The walk is bounded by roots times reachable nodes, and a hierarchy deeper than 100 is now walked in full instead of being cut off silently. FindingsA. Important: the docs give a stored filter spelling that the code now rejects
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct QueryFilter {A stored filter is written by
B. Suggestion: no test pins the new strictness
Nothing fails if
C. Suggestion: the updated live-append comment overstates what the gate declines
* the gate. The same goes for a node of a subtype of the target type: the
* backend's query returns it, but the type's `extends` chain is not known
* here, so it appears on the next load.Two details are off.
Nits
Recommendation: APPROVEBoth blocking findings from the first review are fixed, and the fixes are checked by tests that fail without them. Finding A is a docs correction in nodespace-docs and should land with the merge. B and C are optional. |
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (re-review): APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
Re-review addressed (commit
|
|
Merge queue: #3481 was taken out of the queue — it conflicts with main in: packages/core/src/behaviors/mod.rs, packages/core/src/playbook/cron_runner.rs, packages/core/src/playbook/graph_resolver.rs, packages/core/src/playbook/validation.rs, packages/core/src/services/node_service/schema.rs, packages/daemon/src/services/node_service.rs, packages/desktop-app/app-lib/src/types.rs, packages/desktop-app/src/lib/types/generated/index.ts, packages/desktop-app/src/lib/types/generated/typed-core-fields.ts, packages/nodespace-types/src/convert.rs, packages/nodespace-types/src/core_type.rs, packages/skill/references/jira-playbook.md. Rebase onto origin/main, push, and re-run |
91268e1 to
7e9823a
Compare
… and type play rules (closes #3449) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7e9823a to
580f5ba
Compare
Closes #3449. Implements ADR-086 §11 and §4 ("Structured") for
playandquery.What changed
One
RelationshipPath, compiled to SQL.nodespace-typesgainsRelationshipPath(a list of hops: a relationship name, built-in, schema-declared or reverse, optionally open-ended). Query relationship filters, a play'sfor_eachand the dot-paths in play conditions all use it.ops::path_opsresolves a path's names against the schemas (one resolver, same order the CLI's relationship reads use).db/sqlite_store/relationship_paths.rsis the one compiler: a CTE per hop, each a correlated subquery that looks edges up by endpoint (never a JOIN againstrelationship),WITH RECURSIVEfor open-ended hops, rows keyed by root, all roots bound as one JSON array. Query-plan tests pin the endpoint-index form.GraphResolverno longer walks hop by hop: it hands whole runs of hops to the store, for every root at once. A scheduled scan resolves each condition path for all selected nodes in one statement and passes the result to the work items it enqueues.relationship/relatedfilters takepathand compile through the same compiler, walking backward from the anchor so the subquery runs once per statement. They now accept schema-declared and reverse names, several hops, and open-ended hops.Typed play rules.
RuleDefinition, a taggedTrigger, a taggedActionwith one params struct per action (deny_unknown_fields),Selector,PlayFields,PlayNodeandPlayNodeUpdateare defined once innodespace-types; core'sParsedRuleis the compiled form. A rule with an unknown trigger, action or param does not decode, so it does not save. Open JSON remains only forpropertiesandedge_data.Selectors. A trigger's
selectreplacesnode_type: a query'starget_typeandfiltersinline, or{ "query_id" }naming a saved query. A scheduled scan runs it as one query throughQueryService.Typed update, end to end.
update_play_nodein core →UpdatePlayNodeproto → daemon → Tauri → dev-proxy → adapters → store, lowering into the shared update pipeline. TypeScript is generated.Node type sequence
play: structured core type, nowWireShape::Typed { update: true }. Fields unchanged (rules,description);rulesis nowRuleDefinition[]with the typed shapes above. Typed update:rules(replaced whole),description(tri-state).query: structured.QueryFilterlosesrelationshipType/relationshipNameand gainspath(RelationshipPath). No new schema fields.40193d3(architecture/relationship-paths.md,playbook-system.md,playbook-engine.md,components/query-node.md,node-types.md,typed-relationships.md).Decisions worth a reviewer's eye
graph_eventselector is a bare type. Filters and saved-query selectors are rejected on agraph_eventtrigger at save time: events are matched in memory by type, and invariant rules inside the triggering transaction, so there is no query to run. The issue scopes query selection to scheduled triggers; the docs say so explicitly.node.child_of.project) is a second statement for the whole set, not one per node.QueryServicenow matches subtypes oftarget_typethrough the ancestry table (asquery-node.mdalready said it did). Needed so a selector overtaskstill scansissuenodes, as the old type scan did. This also changesnodespace query --type task.relationship_type(parent/children/…) andrelationship_nameare replaced bypathin the agent tool schema, the CLI's--filters, and the skill guidance. The prompt golden is regenerated. No model eval was run for this.for_eachstill resolve live.id,content, …) as the end of a path on any node, matching what the resolver already does;update_nodeno longer takeslifecycle_status(typed params; ADR-087).Acceptance criteria
RelationshipPathinnodespace-types; query filters,for_eachand CEL dot-paths use itWITH RECURSIVEfor open-ended; a scan resolves a path for all roots in one statement (query-plan and statement-count tests)GraphResolverno longer callsget_related_nodesnode-types.mdplay and query sheets updatedbun run test:changedpasses;bun run quality:fixcleanTesting
bun run test:changedpassed on the merged tree (frontend, scripts, skill, browser, generated-types drift, Rust workspace). New tests cover the SQL compiler (including plans, cycles, archived nodes), path resolution, batched resolution and root keying, selectors through the cron runner and the save gate, the typed play update (write, clear, conflict, wrong type), and one end-to-end scheduled play selecting through a saved query.🤖 Generated with Claude Code