@oxy-hq/cli 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +312 -31
  2. package/dist/main.mjs +11236 -2822
  3. package/dist/main.mjs.map +1 -1
  4. package/dist/{mcp-xxWL3_dz.mjs → mcp-B2KBWNOU.mjs} +86 -3
  5. package/dist/mcp-B2KBWNOU.mjs.map +1 -0
  6. package/package.json +6 -5
  7. package/skills/oxy-agents/SKILL.md +318 -0
  8. package/skills/oxy-agents/references/quick-reference.md +222 -0
  9. package/skills/oxy-agents/templates/agentic-template.yml +141 -0
  10. package/skills/oxy-airway-pipelines/SKILL.md +223 -316
  11. package/skills/oxy-automations/SKILL.md +61 -15
  12. package/skills/oxy-cli/SKILL.md +40 -6
  13. package/skills/oxy-custom-apps/SKILL.md +380 -294
  14. package/skills/oxy-custom-apps/references/minimal-starter.md +71 -0
  15. package/skills/oxy-custom-apps/references/performance-and-usage.md +151 -0
  16. package/skills/oxy-custom-apps/references/reading-data.md +106 -0
  17. package/skills/oxy-custom-apps/references/sandbox-agent-loop.md +307 -0
  18. package/skills/oxy-custom-apps/references/verifying-in-the-browser.md +65 -0
  19. package/skills/oxy-data-apps/SKILL.md +362 -0
  20. package/skills/oxy-data-apps/references/examples.md +263 -0
  21. package/skills/oxy-data-apps/references/quick-reference.md +171 -0
  22. package/skills/oxy-data-apps/templates/dashboard-with-controls.app.yml +128 -0
  23. package/skills/oxy-data-apps/templates/simple-dashboard.app.yml +102 -0
  24. package/skills/oxy-data-architecture/SKILL.md +137 -152
  25. package/skills/oxy-evals/SKILL.md +362 -0
  26. package/skills/oxy-evals/references/diagnosis.md +243 -0
  27. package/skills/oxy-evals/references/results-json.md +145 -0
  28. package/skills/oxy-evals/references/writing-expected.md +112 -0
  29. package/skills/oxy-semantic-modeling/SKILL.md +239 -59
  30. package/skills/oxy-semantic-modeling/references/warehouse-casts.md +57 -0
  31. package/skills/oxy-semantic-modeling/templates/topic-template.yml +38 -0
  32. package/skills/oxy-semantic-modeling/templates/view-template.yml +119 -0
  33. package/template/.claude/hooks/oxyc-cloud-session.sh +66 -0
  34. package/template/.claude/settings.json +16 -0
  35. package/template/.github/workflows/publish.yaml +15 -13
  36. package/template/.nvmrc +1 -1
  37. package/template/.oxyc-managed +18 -5
  38. package/template/README.md +21 -10
  39. package/template/package.json +3 -2
  40. package/template/pnpm-workspace.yaml +28 -17
  41. package/dist/mcp-xxWL3_dz.mjs.map +0 -1
@@ -1,6 +1,12 @@
1
1
  ---
2
2
  name: oxy-data-architecture
3
- description: Use when deciding where an aggregate gets materialised — writing an `execute_sql` rollup, chaining one after an `airway` ingest step, choosing rollup vs semantic `pre_aggregations`, or whether raw rows must be retained — and on the symptoms: a dashboard that aggregates at query time, a load that wrote every row yet failed, preagg that cannot bucket to 15 minutes or join two views, or no "bronze"/"silver"/"gold" in the schema. Pipeline internals: `oxy-airway-pipelines`; view syntax: `oxy-semantic-modeling`.
3
+ description: >-
4
+ Use when deciding where an aggregate gets materialised — writing an `execute_sql` rollup,
5
+ chaining one after an `airway` ingest step, choosing rollup vs semantic `pre_aggregations`, or
6
+ whether raw rows must be retained — and on the symptoms: a dashboard that aggregates at query
7
+ time, a load that wrote every row yet failed, preagg that cannot bucket to 15 minutes or join
8
+ two views, or no "bronze"/"silver"/"gold" in the schema. Pipeline internals:
9
+ `oxy-airway-pipelines`; view syntax: `oxy-semantic-modeling`.
4
10
  ---
5
11
 
6
12
  # Oxy data architecture: where results get materialised
@@ -11,97 +17,85 @@ query to tune — it is a rollup that was never built. So the work that produces
11
17
  those tables has to be systematic: one place it runs, one guarantee about when it
12
18
  may read, one rule for what gets kept.
13
19
 
14
- That spans Airway ingest, the semantic layer, and scheduled automations, which is
20
+ That spans Airway ingest, the semantic model, and scheduled automations, which is
15
21
  why it is its own skill rather than a section in one of them — and it does not
16
22
  restate them. Fold mechanics, duplicate rows and the pipeline lease are
17
- **`oxy-airway-pipelines`**; views and measures **`oxy-semantic-modeling`**; task
18
- syntax and schedules **`oxy-automations`**.
19
-
20
- ## The source doc, and why half of it is not teachable
21
-
22
- The frame comes from **`internal-docs/medallion-architecture.md`** in
23
- **`oxy-hq/oxygen-internal`** — a **private** repo (often checked out under a
24
- directory named `oxy-internal`). You cannot fetch it like a URL and it may not be
25
- checked out at all where you are reading this, which is why every stable fact
26
- below is carried here rather than pointed at. Do not copy more of it in: it is
27
- actively maintained there, and a duplicate rots and then argues with the original.
28
-
29
- **Its status is `design`, with an open "Decisions needed" section.** So this
30
- skill splits it: **current state** is taught below as fact, re-verified against
31
- source and cited; the **target tiering, gap list, and open decisions** are named
32
- at the end and deliberately *not* taught. Building against a convention that does
33
- not exist yet is worse than building against no convention at all.
34
-
35
- ## The vocabulary bridge
36
-
37
- **Nothing in the code says bronze, silver, gold, or medallion.** The doc
38
- introduces that vocabulary as a *lens*, not as names you will find. Grep
39
- `oxy-internal` for "bronze" or "silver" and you get Tailwind classes for
40
- leaderboard medal ranks in the observability UI (`bg-rank-bronze`); grep "gold"
41
- and you get golden-snapshot test names (`crates/oxy-compile/src/compile.rs:1042`)
42
- and a restaurant in seed data. **Not one hit is a data tier.** An agent that
43
- greps, finds nothing, and invents a tier structure has invented it.
23
+ **`oxy-airway-pipelines`**; views, measures and `pre_aggregations` syntax are
24
+ **`oxy-semantic-modeling`**; task syntax and schedules are **`oxy-automations`**.
25
+
26
+ ## The frame: bronze → silver → gold, as a lens
27
+
28
+ The medallion words are a useful way to think about how raw source data becomes
29
+ analytics-ready. **But nothing in Oxy — code, UI, schema names — says bronze,
30
+ silver, gold, or medallion.** Search a workspace for those words and you will not
31
+ find a tier. An agent that searches, finds nothing, and invents a tier structure
32
+ has invented it. Map the words onto what actually exists:
44
33
 
45
34
  | medallion word | what is actually there |
46
35
  |---|---|
47
36
  | (transient) bronze buffer | `<schema>_raw.<table>` append buffer — **`replacing` disposition only**, not a tier |
48
- | **silver** | `<schema>.<table>`, the normalized read table — every disposition lands here |
49
- | **gold** | `execute_sql` rollup tables (`*_daily_metrics`) **in the same schema** |
50
- | the promotion | the **fold** (watermark-bounded, latest-wins, merge-on-read) — inline at end of load **and again** on the scheduled vacuum, identical SQL |
51
- | storage | Airhouse = DuckLake on DuckDB (S3 Parquet + catalog) — **for pipelines that land in Airhouse** |
37
+ | **silver** | `<schema>.<table>`, the normalized table a pipeline lands — every disposition ends here |
38
+ | **gold** | `execute_sql` rollup tables (`*_daily_metrics`) built by an automation, **in the same schema** |
39
+ | the promotion | the **fold**: watermark-bounded, latest-wins, merge-on-read — inline at the end of a load **and again** on the scheduled vacuum, identical SQL |
40
+ | storage | Airhouse = DuckLake on DuckDB (Parquet on object storage + a catalog) — **for pipelines that land in Airhouse** |
52
41
 
53
42
  That last row is a default, not a law. A rollup lands wherever its `execute_sql`
54
- step's `database:` points, so gold is not necessarily DuckLake: the canonical
55
- exemplar view in `oxy-semantic-modeling` sits on a gold table
56
- (`…___sales_daily_metrics`) whose `datasource:` is **`clickhouse`**. That view is
57
- where the two skills meet — a view models a rollup an automation built — and it
58
- is also the reminder to read the `database:`/`datasource:` rather than assume the
59
- warehouse.
43
+ step's `database:` points, so gold is not necessarily DuckLake: a gold table can
44
+ sit in a customer's ClickHouse with a semantic view (`datasource: clickhouse`)
45
+ modelled on top of it. That view is where the skills meet — a view models a
46
+ rollup an automation built — and it is the reminder to read the
47
+ `database:`/`datasource:` rather than assume the warehouse.
60
48
 
61
- ## Current state: silver on ingest, gold in place
49
+ ## Today: silver on ingest, gold in place
50
+
51
+ ```
52
+ source (POS, accounting, API, …)
53
+ │ Airway: extract + NORMALIZE on ingest
54
+ │ flatten JSON → parent/child tables · snake_case · _aw_* lineage · type-coerce
55
+ ├─ replacing? ──▶ <schema>_raw.<table> (append buffer, drained by the fold)
56
+ ▼
57
+ <schema>.<table> ◀── SILVER: typed, flattened (the landed table)
58
+ │ automation: type: execute_sql — CREATE OR REPLACE TABLE … AS SELECT …
59
+ ▼
60
+ <schema>.<x>_daily_metrics ◀── GOLD: aggregates, same schema
61
+ ```
62
62
 
63
63
  **Normalization happens at ingest, so the landed table is already silver.** It is
64
64
  not a later stage. Airway flattens nested JSON into parent/child relational
65
- tables, snake_cases every name (`normalizer/mod.rs:23`), coerces types, and
66
- injects dlt-style lineage columns — `_aw_id`, `_aw_load_id`, `_aw_parent_id`,
67
- `_aw_root_id`, `_aw_list_idx` (`normalizer/relational.rs:153`). There is no
68
- separate "raw → typed" step to insert your own logic into.
65
+ tables, snake_cases every name, coerces types, and adds lineage columns —
66
+ `_aw_id`, `_aw_load_id`, `_aw_parent_id`, `_aw_root_id`, `_aw_list_idx`. There is
67
+ no separate "raw → typed" step to insert your own logic into.
69
68
 
70
69
  **The `_raw` buffer is a write-throughput optimization, not a tier.** It exists
71
- only for `write_disposition: replacing`, to keep the write path O(batch) instead
72
- of scanning the target (airway `types.rs:117`). It is drained on fold, so steady
70
+ only for `write_disposition: replacing`, to keep the write path proportional to
71
+ the batch instead of scanning the whole target. It is drained on fold, so steady
73
72
  state is roughly empty — and when a fold fails the rows stay durable there,
74
73
  which is what makes the re-run below safe rather than what makes the failure
75
74
  harmless. **You cannot rebuild silver from it.**
76
75
 
77
76
  > **Location trap.** The buffer lives at **`<schema>_raw.<table>`** — a sibling
78
- > *schema*, where `<schema>` is the pipeline's `dataset_name`, so one landing in
79
- > `toast_pos` buffers to `toast_pos_raw.orders` (airway
80
- > `connector/destinations/airhouse.rs:1003`). Several in-tree comments still say
81
- > `<table>_raw`, oxy's own `crates/agentic/airway/src/source_factory.rs:475`
82
- > among them. That is the pre-`raw_schema_name` location, healed in place by
83
- > `migrate_legacy_raw`; the comments never caught up. **This is not version
84
- > drift** — the migration predates every airway rev oxy has pinned. Follow the
85
- > comment and you look in the wrong schema and conclude the buffer is missing.
77
+ > *schema*, where `<schema>` is the pipeline's `dataset_name`, so a pipeline
78
+ > landing in `toast_pos` buffers to `toast_pos_raw.orders`. Older comments and
79
+ > notes say `<table>_raw` inside the same schema; that was an earlier layout,
80
+ > migrated in place long ago. Follow one of them and you look in the wrong schema
81
+ > and conclude the buffer is missing.
86
82
 
87
83
  **Gold is a `CREATE OR REPLACE TABLE` in an automation**, reading the
88
- Airway-landed tables and writing aggregates into the *same* dataset schema —
89
- `toast_pos.orders_daily_metrics` beside `toast_pos.orders`. No marts schema
90
- exists today. The `execute_sql` task:
84
+ pipeline-landed tables and writing aggregates into the *same* dataset schema —
85
+ `toast_pos.orders_daily_metrics` beside `toast_pos.orders`. No separate marts or
86
+ gold schema exists. The `execute_sql` task:
91
87
  <https://www.oxygen-hq.com/docs/guide/build/automations/task-types>
92
88
 
93
- ## The sequencing guarantee — the mechanism that makes this systematic
94
-
95
- This is the whole answer to "how do we do the background work systematically",
96
- and it is one sentence in the source:
89
+ **What follows from this:** there is no durable, replayable bronze for
90
+ pipeline-fed data. To re-derive silver you **re-extract from the source**, and
91
+ silver and gold sit side by side in one schema.
97
92
 
98
- > A step completes only once the pipeline's end-of-load fold has committed, so a
99
- > following `execute_sql` step reads a queryable table rather than a half-folded
100
- > one.
101
- > — `crates/core/src/config/model.rs:2435`
93
+ ## The sequencing guarantee — what makes this systematic
102
94
 
103
- So ingest → rollup is a plain two-step automation, and the ordering is real
104
- rather than hoped-for:
95
+ An automation's `type: airway` step completes only once the pipeline's
96
+ end-of-load fold has committed, so a following `execute_sql` step reads a
97
+ queryable table rather than a half-folded one. Ingest → rollup is therefore a
98
+ plain two-step automation, and the ordering is real rather than hoped-for:
105
99
 
106
100
  ```yaml
107
101
  tasks:
@@ -110,116 +104,107 @@ tasks:
110
104
  pipeline: pipelines/restaurant_analytics.airway.yml
111
105
  - name: rollup
112
106
  type: execute_sql
113
- database: pokehouse
107
+ database: analytics_warehouse
114
108
  sql_query: |
115
109
  CREATE OR REPLACE TABLE toast_pos.orders_daily_metrics AS
116
110
  SELECT business_date, restaurant_id, sum(net_amount) AS net_sales
117
111
  FROM toast_pos.orders GROUP BY 1, 2
118
112
  ```
119
113
 
120
- Two things to know about that step:
121
-
122
- - **`type: airway` is undocumented.** The task-types page above lists
123
- `execute_sql` thoroughly and mentions airway **zero** times (checked
124
- 2026-08-19). It is also not run by `step_executor` like the other I/O tasks —
125
- it is delegated as a `TaskSpec::Airway` (`model.rs:2428`). Reading the docs
126
- and concluding you must trigger ingest some other way is the expected mistake.
127
- - **A failed fold fails the run — and the fix is to re-run, never to
128
- re-extract.** Every row can be written and the load still fail, because the
129
- fold that promotes them into the public table is a separate step: *"what
130
- distinguishes 'written' from 'queryable'"*
131
- (`crates/agentic/airway/src/events.rs:163`). Airway emits `LoadCompleted`
132
- anyway, with correct counts and the per-table outcome in its `folds` field —
133
- the counts are real — *"and the run reports an error"* (`events.rs:153`). It
134
- then returns `Err`, which is *"what makes the RUN terminal state failed"*
135
- (airway `pipeline/mod.rs:1378`; the returns at `:1376` and `:1881`,
136
- `TaskOutcome::Failed` at `crates/agentic/airway/src/worker.rs:311`). So the
137
- run goes red and the message names the cause outright: *"load wrote every row
138
- but N table(s) failed the end-of-load fold … rows are durable in the staging
139
- buffer but not yet visible in the public schema; re-run to fold them"*
140
- (`pipeline/mod.rs:1291`). Take that literally. Cursors already advanced, so
141
- *"the re-run re-extracts nothing and the fold sweeps the pending buffer"*
142
- (`pipeline/mod.rs:1372`) — a full re-extract, or a Reset schema, is the
143
- expensive wrong move on a run that already landed every row.
114
+ The task-types page documents `type: airway` (and its optional `resources:`
115
+ subset); what it does not say is why this guarantee is the backbone of the whole
116
+ pattern. Schedule the automation, not the pipeline, whenever a rollup depends on
117
+ the load.
118
+
119
+ **A failed fold fails the run — and the fix is to re-run, never to
120
+ re-extract.** Every row can be written and the load still fail, because the fold
121
+ that makes rows visible in the public table is a separate step from writing
122
+ them. The run reports correct row counts, then ends **failed**, with a message
123
+ that names the cause outright: *"load wrote every row but N table(s) failed the
124
+ end-of-load fold … rows are durable in the staging buffer but not yet visible in
125
+ the public schema; re-run to fold them."* Take that literally. Cursors were
126
+ already saved, so the re-run re-extracts nothing and the fold sweeps the pending
127
+ buffer. A full re-extract, or a **Reset schema**, is the expensive wrong move on
128
+ a run that already landed every row. Re-run the whole automation, so the
129
+ rollup rebuilds on the folded table.
144
130
 
145
131
  ## Rollup is not preagg
146
132
 
147
- Asked to make a slow dashboard fast, an agent working in the semantic layer
148
- reaches for `pre_aggregations`. Nothing warns it off: `pre_aggregations` appears
149
- in no docs page, and `oxy-semantic-modeling` does not mention it either — so the
150
- first thing that tells you it is the wrong tool is the SQL failing. It is a
151
- **different mechanism** and it cannot do what a rollup does:
133
+ Asked to make a slow dashboard fast, an agent working in the semantic model
134
+ reaches for `pre_aggregations`. It is a **different mechanism**, and it cannot do
135
+ most of what a rollup does:
152
136
 
153
137
  | | `execute_sql` rollup | semantic `pre_aggregations` |
154
138
  |---|---|---|
155
139
  | lives in | an automation | a `.view.yml` block |
156
- | reads | any SQL — joins, CTEs, window functions | **one view**: `generate_build_sql` takes a single `view.source_sql()` and emits no JOIN (airlayer `engine/preagg.rs:266`) |
157
- | time buckets | anything you can write | a calendar part only — `granularity` is interpolated straight into `date_trunc('{g}', …)` (airlayer `dialect/mod.rs:75`), so `year`/`quarter`/`month`/`week`/`day`/`hour`/`minute`/`second` and nothing between |
140
+ | reads | any SQL — joins, CTEs, window functions | **one view**: a rollup holds only that view's own dimensions and measures, with no join |
141
+ | time buckets | anything you can write (`time_bucket(INTERVAL '15 minutes', ts)`) | a calendar part only — `second`/`minute`/`hour`/`day`/`week`/`month`/`quarter`/`year`, nothing between |
142
+ | measures | anything SQL computes | only re-aggregatable ones; `custom`, `median` and `number` measures never serve from a rollup |
158
143
  | refresh | your automation's schedule | `refresh_key`, by a background worker |
144
+ | consumed by | anything that reads the table — a view, an app, a query | only semantic queries the rollup fully covers; anything else falls back to the warehouse |
159
145
 
160
- So **15-minute buckets and any two-table join must be a rollup.** `RollupSpec`
161
- carries `dimensions`, `measures`, `time_dimension`, `granularity` — there is
162
- nowhere to name a second view. Use preagg for the narrow case it fits: one view,
163
- calendar grain, speeding up a measure that already resolves. Use a rollup for
164
- everything else, which in practice is most gold.
146
+ So **15-minute buckets and any two-table join must be a rollup table.** There is
147
+ nowhere in a `pre_aggregations` entry to name a second view or an arbitrary
148
+ bucket. Use preagg for the narrow case it fits: one view, calendar grain,
149
+ speeding up re-aggregatable measures a dashboard already queries through the
150
+ semantic model. Use a rollup for everything else, which in practice is most gold
151
+ — and model a semantic view on the rollup table so agents and apps query it by
152
+ name.
165
153
 
166
- ## Bronze policy is a decision rule, not a preference
154
+ ## Retaining raw data: a decision rule, not a preference
167
155
 
168
- The right policy is a function of **re-extractability**, and it is chosen per
169
- domain rather than applied uniformly:
156
+ Whether raw data must be kept is a function of **re-extractability**, chosen per
157
+ source rather than applied uniformly:
170
158
 
171
159
  | source | policy | why |
172
160
  |---|---|---|
173
- | **re-extractable** — Toast, QuickBooks, most APIs | transient `_raw` buffer is fine; **the source system is the bronze** | you can always re-pull to rebuild silver; a durable warehouse copy is redundant cost |
174
- | **non-re-extractable** — video events, sensor streams | the raw event table **is durable bronze and must be retained** | you cannot re-pull yesterday's frames; drop the events and the history is gone |
161
+ | **re-extractable** — POS and accounting APIs (Toast, QuickBooks), most APIs | the transient `_raw` buffer is fine; **the source system is the bronze** | you can always re-pull to rebuild silver; a durable warehouse copy is redundant cost |
162
+ | **non-re-extractable** — video or sensor events, files people upload by hand | the raw artifact **is durable bronze and must be retained** — a table when it is an event, object storage plus a metadata row when it is a file | you cannot re-pull yesterday's frames or re-download last quarter's portal export; drop it and the history is gone |
163
+
164
+ Non-re-extractable events take a visibly different shape: written by direct
165
+ append into a plain, immutable table, with no `_raw` buffer and no fold — they
166
+ never go through Airway at all. Before proposing a retention or cleanup job on
167
+ any raw table, run the test above; the answer for event streams is the opposite
168
+ of the answer for an API source.
175
169
 
176
- The video domain is the worked example, and it diverges visibly: `oxy_cam_events`
177
- is written by a direct append (`crates/cameras/src/service/ingest.rs:106`) into a
178
- plain table with no `_raw` buffer and no fold
179
- (`crates/cameras/src/airhouse/schema.rs:16`) — it never goes through Airway at
180
- all. That is the shape durable bronze takes here. Before proposing a retention or
181
- cleanup job on any raw table, run the test above; the answer for cameras is the
182
- opposite of the answer for Toast.
170
+ **A durable bronze of files is not automatically replayable.** Re-parsing needs
171
+ whatever the original parse was *given*, and that is often more than the file: a
172
+ reporting period someone picked in a UI, text a browser extracted, a mapping
173
+ chosen at upload. Keep those inputs alongside the file, or the retained artifact
174
+ is necessary and still not sufficient to rebuild anything.
183
175
 
184
176
  ## Why the shapes are these shapes: DuckLake
185
177
 
186
- `PRIMARY KEY`, `UNIQUE`, and `CREATE INDEX` are all rejected
187
- (`crates/cameras/src/airhouse/schema.rs:6`). That single constraint is why
188
- `merge` stages then `MERGE INTO` and why `replacing` dedups on fold — no
189
- `ON CONFLICT` to lean on, and scan pruning comes from partition/sort, not an
190
- index. **`oxy-airway-pipelines` owns the consequences** (duplicate rows, the
178
+ `PRIMARY KEY`, `UNIQUE`, and `CREATE INDEX` are all rejected by DuckLake, and a
179
+ table declaring one stops the writer. That single constraint is why `merge`
180
+ stages then merges and why `replacing` dedups on fold — there is no
181
+ `ON CONFLICT` to lean on, and scan pruning comes from opt-in partitioning and
182
+ sort order (`SET PARTITIONED BY`, `SET SORTED BY`), not from an index.
183
+ **`oxy-airway-pipelines` owns the consequences** (duplicate rows, the
191
184
  single-flight lease, reset); do not re-derive them here. The one that reaches
192
- gold: DDL in an `execute_sql` rollup must carry no constraints either.
185
+ gold: DDL in an `execute_sql` rollup on Airhouse must carry no constraints either.
193
186
 
194
187
  **A corollary for rollup SQL:** join parent to child on the **propagated
195
188
  business key** (`invoice_id` on `invoices__line`), never on `_aw_parent_id` — it
196
- is regenerated every load (airway `types.rs:114`), so a join on it matches within
197
- one load and silently stops matching across loads.
198
-
199
- ## Proposed, not built — cite, do not build against
200
-
201
- The rest of that doc is a roadmap. Say so out loud if someone asks you to follow
202
- it; none of the following exists:
203
-
204
- - **A conformance layer** — shared `dim_store` / `dim_date` / `dim_camera` and
205
- cross-source id maps, materialised once. Cross-source keys are wired ad hoc
206
- per domain today, mostly hand-maintained `VALUES` tables. One camera dimension
207
- has since shipped in the video domain: proof the gap is real, **not** a
208
- convention to copy.
209
- - **Separating gold from silver** by schema or a `*_metrics` suffix tooling
210
- treats as derived. They co-mingle today, and whether to adopt a convention now
211
- or wait for a second consumer is an **open decision** in the doc.
212
- - **Codifying the re-extractability test as policy** — the rule above is
213
- reasoning that has held, not something enforced anywhere.
214
-
215
- If a task needs one of these, that is a design conversation with the doc's
216
- owners, not a pattern to invent in a customer workspace.
217
-
218
- ## Version anchor
219
-
220
- Checked 2026-08-19 against oxy-internal **0.5.126**, airway **0.1.32**
221
- (`55e2794`), airlayer **0.3.6** (`7e884e77`) — both pinned in `Cargo.lock`.
222
- Unprefixed `src/…` paths above are inside those external crates, not
223
- oxy-internal; `crates/…` paths are oxy-internal. Both engines are fetched by git
224
- rev, so a bump can move this ground with no oxy-internal commit — re-check
225
- against the pin in `Cargo.lock`, not against memory.
189
+ is regenerated every load, so a join on it matches within one load and silently
190
+ stops matching across loads.
191
+
192
+ ## What does not exist — do not build against it
193
+
194
+ These come up as "the obvious next step"; none of them is a convention today,
195
+ and inventing one in a customer workspace creates a second pattern nobody else
196
+ follows:
197
+
198
+ - **A shared conformance layer** — common `dim_store` / `dim_date` dimensions
199
+ and cross-source id maps, materialised once. Cross-source keys are wired per
200
+ domain today, mostly as hand-maintained `VALUES` tables. If you need one, build
201
+ it as a maintained table for that domain and say it is local.
202
+ - **A separate gold schema or naming rule** that tooling treats as derived. Gold
203
+ and silver share a schema; the `*_daily_metrics` suffix is habit, not
204
+ enforcement.
205
+ - **The retention rule as policy.** The re-extractability test above is
206
+ reasoning that has held, not something any tool enforces.
207
+
208
+ Checked in October 2026 against the Airway engine Oxy pins (0.1.51) and the
209
+ semantic engine it ships with. A newer Oxy can move this ground; trust a run's
210
+ own error message over this page.