Product rules

Decisions

Spec labels like D21 are shorthand for locked product rules. This page states what each one means in plain language. Full rationale: planning/01-decisions.md.

Id In one sentence
D17 Agents use Query IR and catalog SQL — not GraphQL as the qLLM API.
D18 Demo Compose/fixtures never become silent defaults for a real deploy.
D19 Redis/Kafka (and similar) are read-only and non-destructive.
D20 fromFilter rebuilds a REST key the API omitted from the body.
D21 Row scope comes from the credential, not from a field the model can rewrite.
D22 json cells are parsed objects/arrays; shape is a hint only.
D17

Agent surface is Query IR + catalog SQL

HTTP exposes Query IR and catalog SQL. MCP tools run catalog SQL. There is no GraphQL agent API, tool, or IR↔GraphQL translation. An optional experimental type: graphql source talks to an upstream GraphQL HTTP endpoint as data — that is unrelated to how agents call qLLM.

D18

Harness world stays separate

Compose, seeds, and fixtures/ prove the runtime. The binary only sees the YAML you point at with --config-dir (or preset/catalog/project/CWD). No valid project files → CONFIG_ERROR, never a silent fallback to demo hosts or entities.

D19

Key/stream sources are read-only

Experimental redis and kafka only use non-mutating reads (no delete/set/pop, no consumer-group commit, no produce). Queries without the required key/partition access path return UNSUPPORTED. Brokers/ACLs remain the real safety boundary; the runtime is a second line.

Separately: Oracle, BigQuery, Snowflake, Elasticsearch, and S3-as-table are simply not source types today — that is a connector matrix gap, not D19.

D20

REST omitted field → fromFilter

When a REST API accepts a filter (for example warehouse_id) but does not echo it in the JSON body, mark the catalog field fromFilter: true. qLLM copies the value from a top-level eq so joins and GROUP BY still see the column. No matching eq → INVALID_IR. Body value disagrees with the filter → SOURCE_ERROR.

D21

Scope on the credential

qllm.access.yaml lists app types, not end users. Row identity lives in a derived Bearer key (or static scope), and the catalog entities[].scope forces an equality filter on that column. The model cannot swap user_id in the query to see another tenant’s rows.

D22

Parsed json cells + typed columns

A catalog field with type: json returns a parsed object or array in the response envelope (not a JSON string). Optional shape is free text for the LLM ({aisle, bin}, string[]); the runtime does not validate it. Column types in SQL results follow the source/DuckDB type. Integers above 253 should be catalogued as string because number is DOUBLE.

Key-conditioned sources (DynamoDB, Cassandra, ksql, Redis, Kafka) still need the documented accessPath equalities or they return UNSUPPORTED — see Connectors. That requirement is separate from the json cell rule, even though both show up when you design catalogs carefully.