Shared-scan graph projection: one scan per contract, declarative rule predicates (LLP 0095/0096)#291
Conversation
…icates (LLP 0095/0096) projectGraph ran every contract rule as an independent full SQL scan with refresh:'always', and the LLP 0026 aux filter prepended `attributes` to each rule and JSON-parsed it per rule per row. At 25 rules that meant 25 full table sweeps per projection: ~35GB read and 45+ minutes for a ~1.3GB source on a production hypaware-server deployment (LLP 0095). - project.js: one shared scan per contract over the union of declarative rules' columns; per-rule where predicates (eq/in/likePrefix, SQL null semantics) evaluated in JS; raw-SQL rules stay supported, run standalone, grouped by identical SQL text. - Contract gains rowFilter, evaluated once per row on both paths; the ai-gateway contract's aux filter moves there. - ai-gateway-graph: 21 of 25 rules migrated to columns/where; the two content_text prefix surfaces (x node+edge) stay raw SQL so the table's largest column keeps its server-side pushdown. - Registry validates the new shapes; equivalence test pins the JS evaluator to the SQL engine's semantics over a fixture hitting every predicate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dual-agent review —
|
| Source | Finding (severity, evidence) | Intersects |
|---|---|---|
| Claude | inline import() types violate CLAUDE.md (minor, project.js:55,110,216,247) |
Targets (project.js), not a Risks-section surface |
Codex review
Fix Validations
Per-rule graph projection scans
- Status: correct
- Evidence: hypaware-core/plugins-workspace/context-graph/src/project.js:84, hypaware-core/plugins-workspace/context-graph/src/project.js:91, hypaware-core/plugins-workspace/context-graph/src/project.js:118, test/plugins/context-graph-project-e2e.test.js:457
- Assessment: Declarative rules now share one union-column scan per contract, while raw SQL is grouped by identical SQL. The new e2e SQL-twin test validates row-identical output for the migrated ai-gateway contract.
LLP 0026 aux exclusion preservation
- Status: correct
- Evidence: hypaware-core/plugins-workspace/ai-gateway-graph/src/graph_contract.js:470, hypaware-core/plugins-workspace/context-graph/src/project.js:100, hypaware-core/plugins-workspace/context-graph/src/project.js:128
- Assessment: The aux filter is now applied before rule fanout on both shared-scan and raw-SQL paths. Current ai-gateway raw rules explicitly project
attributes.
Findings
2) Contract & Interface Fidelity
- Severity: major
- Confidence: high
- Evidence: hypaware-core/plugins-workspace/context-graph/src/contract-registry.js:94, hypaware-core/plugins-workspace/context-graph/src/contract-registry.js:96, test/plugins/context-graph-contract.test.js:158, hypaware-core/plugins-workspace/context-graph/src/project.js:128
- Why it matters: The registry promises raw SQL under a
rowFilteractually selects filter columns, butrule.sql.includes(col)accepts false positives likeWHERE attributes IS NOT NULLorSELECT other_attributes, sokeep(row)can run with a missing column and silently let filtered rows through. - Suggested fix: Validate the projected SELECT list, not the whole SQL string, and add negative tests for
WHERE attributes...andother_attributes; also consider checkingresult.columnsbefore applyingkeep.
9) Test Evidence Quality
- Severity: minor
- Confidence: high
- Evidence: hypaware-core/plugins-workspace/context-graph/src/project.js:233, test/plugins/context-graph-project-e2e.test.js:453, hypaware-core/plugins-workspace/ai-gateway-graph/src/graph_contract.js:219, hypaware-core/plugins-workspace/ai-gateway-graph/src/graph_contract.js:232
- Why it matters: The test comment claims the equivalence fixture exercises
likePrefix, but the ai-gateway prefix surfaces stayed raw SQL, leaving the new JSlikePrefixevaluator without direct coverage. - Suggested fix: Add a small synthetic declarative-rule projection test with
where: { likePrefix: ... }, including null/non-string/non-matching rows.
No Finding
- Behavioral Correctness
- Change Impact / Blast Radius
- Concurrency, Ordering & State Safety
- Error Handling & Resilience
- Security Surface
- Resource Lifecycle & Cleanup
- Release Safety
- Architectural Consistency
- Debuggability & Operability
Evidence Bundle
- Changed hot paths:
projectGraphshared scan and raw grouping at hypaware-core/plugins-workspace/context-graph/src/project.js:57, :84, :118; predicate evaluation at hypaware-core/plugins-workspace/context-graph/src/project.js:221; contract validation at hypaware-core/plugins-workspace/context-graph/src/contract-registry.js:56. - Impacted callers: CLI projection via hypaware-core/plugins-workspace/context-graph/src/command.js:42; contract registration capability via hypaware-core/plugins-workspace/context-graph/src/index.js:62; raw legacy contract shape still present at hypaware-core/plugins-workspace/context-graph-enrich/src/contract.js:54.
- Impacted tests: test/plugins/ai-gateway-graph-contract.test.js:182; test/plugins/context-graph-contract.test.js:137; test/plugins/context-graph-contract.test.js:149; test/plugins/context-graph-project-e2e.test.js:457.
- Unresolved uncertainty: I did not run the suite; review used the supplied diff plus targeted
rg/line checks. External hypaware-server contracts mentioned in the PR are outside this worktree.
Claude review
Claude review
Reviewed head 209e250. Focus: equivalence of the shared single scan vs. the prior per-rule scans (nodes/edges must be identical), totality of the declarative predicates, aux-filter semantics, and the withholding/export-seam invariant.
Equivalence risks checked and cleared:
- Aux-filter semantics identical. Old
if (auxKindOf(r.attributes)) return nullvs newkeep(r){ return auxKindOf(r.attributes) === null }.auxKindOfreturnsstr(claude.aux_kind), andstrmaps''/non-string tonulland never returnsundefined— so it yields exactlynon-empty-string | null.=== nullis therefore identical to the old truthiness test at every value (including empty-string aux_kind and stringifiedattributes). - Predicate<->SQL mapping is exact. Every migrated
WHEREmaps 1:1 toeq/in(part_type/tool_name/role);matchesPredicateuses strict!==foreqandtypeof v !== 'string'guards forin/likePrefix, so a null/absent column never matches — mirroring SQL. Heavycontent_text LIKE 'prefix%'guards (Skill marker/slash surfaces) correctly stay raw SQL for server-side pushdown, and those raw rules now selectattributesthemselves (registry-enforced), so the aux filter still applies to them. - Row-order independence holds. The shared scan changes Map insertion order (row-major instead of rule-major), but
mergeRow/propsValueWinsresolve conflicts by earliestfirst_seenthen stable-JSON tie-break, so the merged node/edge set is order-independent. The newshared-scan is row-identical to per-rule SQLe2e test passes and exercises eq/in/likePrefix + aux + null columns. - Column-union completeness.
columnsunion +predicateColumns(where)+rowFilter.columnscovers every field eachtoRowreads (spot-checked File/touched, Program/invoked, Skill surfaces).toRows read named fields only, so extra union columns in the row are inert. - Withholding/export-seam (LLP 0070/0071) not implicated. Graph projection is a purely local read of
ai_gateway_messagesto build local node/edge tables; it is not the sink/export path, and there are no withholding references in the projection code. Dropping per-ruleWHEREdoes not widen any export.
Inline import('./types.js') type annotations violate the repo type-import rule
- Severity: minor
- Confidence: 85
- Evidence: hypaware-core/plugins-workspace/context-graph/src/project.js:55, 110, 216, 247
- Why it matters: CLAUDE.md states "Never use inline
import('...')types. Declare type imports at the top of the file with@importJSDoc comments, then reference the bare names." The PR adds four inlineimport('./types.js').ContractRule/.RulePredicateannotations, and the file already has an@import { Contract, GraphRow } from './types.js'block at the top that these belong in. - Suggested fix: Extend the existing top-of-file
@importto{ Contract, GraphRow, ContractRule, RulePredicate } from './types.js'and use the bare names at the four sites.
Reports: /Users/phil/workspace/hypaware/.git/worktrees/wt/dual-review/pr-291
|
neutral review — round 1 · verdict: request-changes · adopted PR (LLP 0025, full-heal) Reviewed head Findings — all non-blocking. None is a true blocker (no production defect in the shipped code); handed back for your call.
LLP consistency: good. LLP 0095 (Issue, Active) and 0096 (Decision, Accepted) are well-formed and accurately describe the implementation; in-code Design note (not a finding): Why neutral did not push fixes: you are actively iterating on |
…ral review, PR #291) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
neutral review — round 2 · verdict: clean → approved · adopted PR (LLP 0025, full-heal) Round 1 (at Fix confirmation (verified against the committed tree + local tests):
Checks at this head: typecheck + test green, MERGEABLE. Tests locally: 28/28 (context-graph contract/project/e2e) + 46/46 (ai-gateway-graph, exercising the real Two latent, non-blocking robustness notes on the new guard (both fail closed — reject at registration, never a leak; neither reachable by any current rule, so no action needed):
Mergeable ∧ green ∧ reviewed-clean → approved. Held for you to merge — neutral does not merge or ready adopted PRs. (Reminder: origin is now ahead of your local branch by the fix commit; |
…292) The read-amplification fix (LLP 0095) shipped in #291 via decision LLP 0096; the code @ref'd the decision but not the issue, so neutral's coverage scan (which counts a design/plan @ref or a code @ref to the request itself) read 0095 as an uncovered request. Add the realizing-code @ref LLP 0095 [implements] so the shipped fix is recognized as code-covered. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
projectGraphexecuted every contract rule as an independent full SQL scan of the source dataset, and the LLP 0026 aux filter prependedattributes(19% of table bytes) to every rule's SQL and JSON-parsed it once per rule per row. With the ai-gateway contract's 25 rules, one projection = 25 full table sweeps.Observed on a production hypaware-server deployment: a single org-scoped
graph projectover a ~700k-row / ~1.3GB source read ~35GB from disk and had not finished after 45 minutes (single core pegged) before being aborted. The same corpus projects in seconds locally, which is why this was never noticed: local installs are just earlier on the samerules x table-sizecurve. Full evidence in LLP 0095.How
Settled in LLP 0096:
{ columns, where }instead of raw SQL; the engine scans the source once with the union of declared columns and evaluates each rule's predicate in JS per row beforetoRow.whereis the AND of the three shapes the rules actually use (eq,in,likePrefix), with SQL null semantics.content_text LIKE 'prefix%'Skill surfaces deliberately stay pushed down so the table's largest column never enters the shared scan's materialization.rowFilter, evaluated once per row on both paths (previously: per rule per row). The registry enforces that raw-SQL rules under a rowFilter select its columns themselves.Net effect at server scale: 25 full scans become 1 slim scan + 2 pushed-down prefix scans, and 25x fewer
attributesparses.Tests
context_graph_projects_rowssmoke passes; typecheck clean.Notes
🤖 Generated with Claude Code