Skip to content

Product events from annotated spans - #710

Open
Makisuo wants to merge 13 commits into
mainfrom
feat/product-events-from-annotated-spans
Open

Product events from annotated spans#710
Makisuo wants to merge 13 commits into
mainfrom
feat/product-events-from-annotated-spans

Conversation

@Makisuo

@Makisuo Makisuo commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Adds a fourth feed into product_events, alongside browser (session_events MV), server and mobile (POST /v1/events): a span the customer annotated in their own code.

span.setAttributes({
  "maple.product_event.name": "checkout_completed",  // presence is the predicate
  "maple.product_event.user_id": user.id,            // optional identity
})

That becomes a product_events row with Source='trace', carrying its TraceId/SpanId — so it steps in a funnel like any track() call and links back to the request that performed it.

Why an attribute, not a UI action

The first cut of this was going to be "mark a trace as a product event" from the trace view, backed by a Postgres annotations table. That's the wrong shape. A product event has to be emitted by the code path that performed the thing, at the moment it performed it. Marking a trace by hand marks one sampled trace, can't be replayed over history, and puts a mutable user-authored row into an append-only fact table.

An attribute marks every trace the path produces, applies retroactively across the whole traces retention window, and is reviewable in the customer's own diff. There is no new store and no write path from the dashboard: the span is the record, the product event is its projection.

Attribute selection — three tiers, one mechanism

The span's other attributes become the event's properties by default, so nothing has to be declared to get a useful event. Two optional controls narrow or replace that. Both are themselves span attributes, because a materialized view is static SQL per cluster with no per-org config to read.

"maple.product_event.include": "plan,seats"   // ONLY these span keys (whitespace trimmed)
"maple.product_event.prop.plan": "pro"        // explicit prop, merged over the base, wins ties
"maple.product_event.include": ""             // and together: full overwrite
include prop.* Attributes
absent every span attribute
absent set every span attribute, props overriding on collision
"plan,seats" only plan and seats
"" set only the props

include narrows the base, prop.* merges over it, and an empty include narrows the base to nothing so only the props survive. No separate replace flag to get wrong.

Two things are load-bearing here, and both have a test pinning them:

  • include switches on key presence (mapContains), not a non-empty value. A != '' would silently turn the documented overwrite back into copy-everything — the exact opposite of what the caller asked for.
  • mapUpdate(base, props) argument order is the override rule. Swapped, an override gets discarded precisely when the key it meant to correct was already present.

The link

product_events gains TraceId/SpanId (DEFAULT '', appended) plus an idx_trace_id bloom filter. Real columns rather than Attributes keys because both directions filter on them, and a Map lookup on this table reads the whole map per row — the cost product_events was split out of session_events to avoid in the first place.

Direction Query Surface
trace → its product events productEventsForTraceQuery trace detail, under the anatomy strip; clicking selects the annotated span
event → the traces behind it productEventTraceSamplesQuery /analytics, when the event filter is set

Both panels render nothing when empty. Most traces produce no events and a browser track() call has no trace, so silence is the design rather than an empty state on every page.

Schema surfaces

All three, backfilling the trace half from traces (bounded by its 30-day retention against product_events' 365):

  • BYO ClickHouse — migration 0024. requiredForIngest: false: the gateway writes neither new column, so ingest routing is not un-readied over a read-path feature.
  • Managedproduct_events_traces_mv, deployed with the rest of the Tinybird project.
  • Local CLI — schema v13 → v14 with its migration edge.

The projection has one live definition and two frozen copies (0024 and the local edge), deliberately not sharing a constant: a delta migration describes one step in history, and a shared constant would silently rewrite what it did the next time the live projection changes.

Verified

bun typecheck green across 39 packages. Domain 662, query-engine 1366, cli 517, api 2547, web 2339 — all passing. Schema, local-manifest and Tinybird gates up to date; lint and format clean.

The Attributes expression was executed rather than assumed — mapUpdate, trimBoth and the outer-column lambda capture are all things worth checking. Against ClickHouse 26.2:

Scenario Span attributes Result
default http.method, plan=free, seats=5, prop.plan=pro {http.method, seats, plan:'pro'}
include: "plan, seats" + noise {plan:'free', seats:'5'}
include: "" + prop.plan=pro http.method, plan=free {plan:'pro'}

Reviewer notes

The known trade. Copying the whole attribute map by default is the deliberate expensive choice: an annotated span's attributes outlive the span by a factor of twelve (365d vs 30d), and attribute pickers over product events will list the span's full semconv surface until a team sets include. The alternative — opt-in props only — was rejected because it means nothing works until you declare something. include is the lever, and it's a one-line change on the span rather than a schema migration. If this becomes the dominant cost across orgs rather than for one of them, the next lever is a per-org key denylist at the MV. Written up in docs/product-events-funnels.md.

Ingest cost. The MV predicate is one Map value read per incoming span, on the same block every other traces MV already fires on. An MV sees the insert block, not the table, so idx_span_attr_keys doesn't help it. The expensive Attributes expression only evaluates for rows that pass the WHERE, i.e. annotated spans, so its cost is per product event rather than per span.

Rollout. The managed Tinybird side of the original product-events work was never deployed — Tinybird CD is disabled and deploys are a manual operator step. This inherits that; BYO ClickHouse and local mode migrate on their own.

Not in this cut, both noted in the doc:

  • No MCP tool. list_product_events still returns names only and inspect_trace doesn't surface a trace's events, so an agent can't walk the link yet. The queries and routes it would sit on exist.
  • No SDK helper. Teams set the attributes by hand. A markProductEvent(span, name, { props, include }) is a wrapper over setAttributes, and it's where the empty-string overwrite idiom would get a real name instead of being a documented convention.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Makisuo and others added 13 commits September 1, 2026 10:51
Adds a fourth feed into `product_events`, alongside browser (session_events
MV), server and mobile (POST /v1/events): a span the customer annotated in
their own code.

    span.setAttributes({
      "maple.product_event.name": "checkout_completed",
      "maple.product_event.user_id": user.id,
    })

The span's other attributes become the event's properties by default, so
nothing has to be declared to get a useful event. Two optional controls narrow
or replace that: `maple.product_event.include` is a comma-separated allow-list,
`maple.product_event.prop.*` are explicit props merged over the base and
winning ties, and an EMPTY `include` narrows the base to nothing so the props
are all that survive — one mechanism, three tiers, no separate replace flag.
Both are span attributes because a materialized view is static SQL per cluster
with no per-org config to read.

An attribute rather than a UI action because a product event has to be emitted
by the code path that performed the thing, at the moment it performed it.
Marking a trace by hand marks one sampled trace, cannot be replayed over
history, and puts a mutable user-authored row into an append-only fact table.
There is no new store and no write path from the dashboard: the span is the
record, the product event is its projection.

`product_events` gains TraceId/SpanId (DEFAULT '', appended) plus an
idx_trace_id bloom filter. Real columns rather than Attributes keys because
both directions filter on them, and a Map lookup on this table reads the whole
map per row — the cost product_events was split out of session_events to
avoid. That column is the link:

  trace  -> its product events   productEventsForTraceQuery   (trace detail)
  event  -> the traces behind it productEventTraceSamplesQuery (/analytics)

Both panels render nothing when empty. Most traces produce no events and a
browser track() call has no trace, so silence is the design rather than an
empty state on every page.

Shipped across all three schema surfaces: ClickHouse migration 0024 (BYO,
requiredForIngest: false — the gateway writes neither new column, so ingest
routing is not un-readied), product_events_traces_mv for managed orgs, and
local schema v13 -> v14. All three backfill the trace half from `traces`,
bounded by its 30-day retention against product_events' 365.

Known trade, taken deliberately: copying the whole attribute map by default
means an annotated span's attributes outlive the span by a factor of twelve,
and attribute pickers list the span's full semconv surface until a team sets
`include`. Documented in docs/product-events-funnels.md along with the lever.

The Attributes expression was executed against ClickHouse 26.2 rather than
assumed — mapUpdate, trimBoth and the outer-column lambda capture all behave,
and all three tiers produce the intended map.

Not in this cut: no MCP tool (an agent cannot walk the link yet) and no SDK
helper. Both noted in the doc.
Self-review with four adversarial passes (warehouse SQL, migration safety,
API boundary, UI) turned up two bugs that would have shipped.

1. BYO ingest would have dropped every /v1/events batch for unmigrated orgs.
   Widening the product_events datasource regenerated the Rust gateway's insert
   mapping to name TraceId/SpanId, but the readiness gate stayed at schema
   version 21 because 0024 is requiredForIngest: false. A BYO org stamped 21-23
   is therefore still routed to its own cluster, where the INSERT fails on the
   unknown column, retries, trips the breaker and drops the batch. Reproduced
   against a pre-0024 table: Code 16, NO_SUCH_COLUMN_IN_TABLE.

   Fixed at the declaration rather than the flag: TraceId/SpanId now carry no
   jsonPath, so generate-clickhouse-insert-mappings skips them and the gateway
   never names them — the same shape service_usage's MV-only columns already
   use. requiredForIngest: false is honest again, and no BYO org is un-readied
   over columns none of their writers touch. The migration comment now says
   which fact it depends on, since that fact lives in another file.

2. Dropping product_events_mv across the whole trace backfill was a permanent
   hole in page views. The ordering was copied from 0021, where the bracket was
   forced — there the backfill WAS the browser feed. Here the backfill reads
   traces and the view reads session_events, so the bracket bought nothing
   while the chunked backfill ran for up to 400 workflow steps, and every
   navigation row ingested meanwhile was never projected. The view is now
   recreated immediately after its drop; the outage is one statement wide.

Also from the review:

- The idempotency DELETE is now scoped to the backfill's own source window
  (Timestamp >= (SELECT min(Timestamp) FROM traces)) in both 0024 and the local
  edge. Unbounded, a late re-apply cleared 365 days of trace rows and rebuilt
  only the 30 that traces still holds. Verified: 200 rows -> 170 kept, browser
  rows untouched.
- limit is constrained at the HTTP boundary (RowLimit: int, 1..1000). The
  builder inlines a limit into the SQL text, so limit: -1 and limit: 1e21 were
  500s rather than 400s and limit: 1e9 an unbounded scan — the bucket_seconds
  mistake in a second costume, one field over from a comment citing that rule.
- Deleted the two exported row schemas: both were byte-identical to what the
  builder derives and neither was passed to compile, so they were a contract
  nothing enforced. Declared schemas earn their place by narrowing.
- Removed toStringRecord. Rows are decoded before reaching it, so the driver
  quirk it defended against cannot occur.
- The trace panel no longer mounts an atom with empty time bounds before its
  own guard runs — that manufactured a swallowed decode error and exported a
  failure span per render, and only missed the network because TinybirdDateTime
  rejects "". Window resolution now happens before the child that queries.
- Row keys include the index: SpanId is '' on any row that reached the table
  without a span, so two same-named events in one trace collided.
- A row with no span to select is a plain row, not a disabled button, which had
  removed its whole content from the tab order with no visual cue.
- The analytics panel renders an error state instead of silence. It mounts
  because the user asked for it and empty is a meaningful answer there, so
  swallowing a failure answered their question wrongly.
- Docs: the falsified requiredForIngest claim, plus a warning that the managed
  populate is one-shot and overlap-prone (no DELETE step exists on Tinybird, so
  BYO risks a gap where managed risks duplicates).

Unchanged and verified clean by the review: the Attributes expression (merge
direction, substring offset, all three include tiers, key types, lambda
capture), column order, OrgId scoping and time bounds on both queries, and
cache keys — the identity embeds the full payload and is prefixed with orgId.
Dropping the jsonPath from product_events.TraceId/SpanId changed the project
revision, so both local-schema.sql and its v14 snapshot carry a new header
line. The DDL is byte-identical — which is why the local schema identity hash
did not move — but clickhouse:schema:check compares the whole file, and the
regenerated versions were left unstaged in the previous commit.
Carried in from main, which is red at 59db862 for this reason:
IngestAttributeMappingForbiddenError was added to the ingest-attribute-mappings
HTTP schema without rerunning `gen:anticipated-errors`, so the checked-in
literal list (115) no longer matched what reflection derives (116) and
anticipated-errors.test.ts failed.

Not this branch's bug — it is inherited because CI builds the merge commit —
but the merge cannot go green without it. The list is generated, so this is
purely the output of `bun run --cwd packages/domain gen:anticipated-errors`.

Consequence of the gap, for the record: the identifier gates whether a span
failing entirely with that error records as OTLP status Ok rather than Error.
Missing from the list, a plain 403 from that route would have counted as a real
error in error_events_mv.
Second half of main's breakage at 59db862 (the first was the anticipated-
error list). Main is red for exactly `TypeScript (effect-lint)` and
`TypeScript (test-packages)`; CI builds the merge commit, so this branch
inherits both and cannot go green without them. All three sites landed with
#717-#719 and none are this branch's code.

One is a real violation:

- PlanetScaleConnectionService: two consecutive `catchTag` calls collapse into
  a single `catchTags`, which is what the rule asks for and what the rest of
  the repo does.

Two are the heuristic firing on correct code, suppressed with a reason rather
than "fixed" into something worse:

- ElectricClient `shape` is Electric's own domain term — a shape is its unit of
  subscription — and the value is written to the `maple.electric.shape` span
  attribute under that exact name. Renaming it to satisfy
  no-shape-in-symbol-names would make the code describe Electric less
  accurately. Suppressed across the function rather than at one line, since the
  parameter and its use both trip it.
- WarehouseQueryService's two fetch test doubles use `as unknown as typeof
  fetch` because `typeof fetch`'s overload set is not satisfiable by a bare
  async function. The narrowing is local to a test. Same shape as the existing
  anti-slop suppressions in the v2 OpenAPI contract tests.

Directives are placed on the line immediately above the offending code with the
prose above them — an `oxlint-disable-next-line` whose justification wraps onto
a second comment line targets that comment, not the code, and reports as an
unused directive while the original error stands.

Verified: full `bun run lint` clean, apps/api WarehouseQueryService 36 passed,
apps/electric-sync 76 passed, both packages typecheck clean.
Main fixed its own effect-lint breakage in #687, so the three fixes this branch
was carrying to stay green are superseded. Main's versions are better in every
case and win the resolution:

- PlanetScaleConnectionService: main replaced the call with a new
  `deleteManaged(orgId, target.id)` that drops the `allowManaged` flag
  entirely, so the ScrapeTargetValidationError branch no longer exists and
  there is only one catchTag left. The catchTags collapse this branch made is
  moot; took main's refactor whole.
- WarehouseQueryService.test: main typed the fetch doubles as
  `const requestFetch: typeof fetch = …`, removing the chained assertion
  instead of suppressing it. Strictly better than the suppression here; took
  main's.
- ElectricClient: main left `shape` alone but #687 dropped
  no-shape-in-symbol-names from the config, so the suppression became an
  unused directive — its own error. Reverted the file to main's.

No product decision was needed: every conflict was two solutions to one lint
error, and main's is the one that survives.

Verified on the merged tree: full `bun run lint` clean, `bun typecheck` 39/39,
apps/api 2594 passed, packages/domain 698, query-engine 1366, apps/cli 517,
electric-sync 76, plus the ClickHouse schema, local-manifest and Tinybird
generated-artifact gates all up to date.
Main landed #692 (agent-trace span index), which claimed both numbers this
branch had taken: ClickHouse migration 0024 and local schema v14. Version
collisions, not semantic ones — nothing about either change conflicts with the
other, so both are kept and this branch moves up.

- `0024_product_events_from_traces.ts` -> `0025_...`, `version: 24` -> `25`,
  export and every doc reference renamed, including the ones in
  docs/product-events-funnels.md and the "stamped below 24" readiness note.
- `migrations/index.ts` and its test carry both 0024 (ai trace index) and 0025.
  `clickHouseSchemaVersion` stays "21": both are `requiredForIngest: false`.
- The local edge is re-derived rather than renamed. Took main's v14 stack
  whole, deleted the old v13->v14 module, and ran `local-schema:bump` to
  scaffold v15 properly — so `local-schema-history.ts`, `schema-identity.ts`
  and the registry are the tool's own output rather than a hand-edit. The
  module body is ported unchanged apart from version plumbing and main's new
  `cloneStoreForStaging` helper in `prepareTarget`.
- `materializations.ts` keeps both imports; `product_events_traces_mv` and
  `ai_trace_index_mv` coexist. Schema is now 39 tables / 42 MVs.
- Restored the four local-store-migration test assertions that taking main's
  file had reverted (the new MV in the current-manifest and since-v13 deltas,
  `idx_trace_id`, and the appended TraceId/SpanId columns). The v11 delta
  deliberately does NOT list the new MV — main reframed that assertion around
  the frozen v11 manifest, so it pins what v11 introduced, not what exists now.

Verified on the merged tree: `bun typecheck` 39/39, `bun run lint` clean,
ClickHouse schema / local-manifest (v15, 81 objects) / Tinybird gates up to
date, apps/api 2649 passed, apps/web 2365, packages/domain 700,
query-engine 1369, apps/cli 534.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Second version collision in a row. Main landed #749 (commit sha from
vcs.ref.head.revision), which took migration 0025 and local schema v15 — the
slots this branch moved into last merge. Nothing semantic conflicts; both
changes are unrelated and each grabbed the next free number, so this branch
moves up again.

- `0025_product_events_from_traces.ts` -> `0026_...`, `version: 25` -> `26`,
  export and every doc reference renamed, including docs/product-events-funnels.md
  and the "stamped below 25" readiness note.
- `migrations/index.ts` and its test carry 0024 (ai trace index), 0025 (commit
  sha) and 0026 (product events from traces). `clickHouseSchemaVersion` stays
  "21" — all three are `requiredForIngest: false`.
- Local edge re-derived again rather than renamed: took main's v15 stack whole,
  deleted the v14->v15 module, ran `local-schema:bump` for v16, and ported the
  module body unchanged apart from version plumbing.
- Restored the same four local-store-migration test assertions that taking
  main's file reverts each time (the new MV in the current-manifest and
  since-v13 deltas, `idx_trace_id`, the appended TraceId/SpanId columns).

Schema is 39 tables / 42 MVs, local schema v16, 81 objects.

Verified on the merged tree: `bun typecheck` 40/40, `bun run lint` clean,
ClickHouse schema / local-manifest / Tinybird gates up to date, apps/api 2655
passed, apps/web 2365, packages/domain 700, query-engine 1369, apps/cli 534.
`bun.lock` is byte-identical to main's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Third version collision in a row. Main landed #738 (agent-session filter
columns), taking migration 0026 and local schema v16 — the slots this branch
moved into yesterday. As with the previous two, nothing semantic conflicts:
two unrelated changes each took the next free number.

- `0026_product_events_from_traces.ts` -> `0027_...`, `version: 26` -> `27`,
  export and every doc reference renamed, docs/product-events-funnels.md
  included.
- `migrations/index.ts` and its test carry 0024, 0025, 0026 and 0027.
  `clickHouseSchemaVersion` stays "21" — all four are
  `requiredForIngest: false`.
- Local edge re-derived via `local-schema:bump` for v17; the v16->v17 module
  body is the previous one unchanged apart from version plumbing.
- Restored the same four local-store-migration test assertions that taking
  main's copy of that file reverts on every one of these merges.

Schema is 39 tables / 42 MVs, local schema v17, 81 objects.

Verified on the merged tree: `bun typecheck` 41/41, `bun run lint` clean,
ClickHouse schema / local-manifest / Tinybird gates up to date, apps/api 2585
passed, apps/web 2382, packages/domain 703, query-engine 1373, apps/cli 534.
`bun.lock` is byte-identical to main's.

Note for the next merge: main's #750 adds a new `@maple/safe-fetch` workspace,
so a worktree installed before the merge needs `bun install` again afterwards —
without it, `@maple/scraper` typecheck and one ScrapeTargetsService lint rule
fail for missing types rather than for anything in the diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Answering "does this need a Tinybird migration?" — yes, and the assumption in
the original commit ("adding columns with DEFAULT is the compatible case") was
wrong. Verified against a real deploy on the local Tinybird container rather
than reasoned about: main's manifest as a baseline, then this branch's.

Without a forward query, Tinybird satisfies the two added columns by REBUILDING
`product_events` from the datasources that feed it, and both keep 30 days
against its 365:

  it is going to be backfilled using the following datasources which would lead
  to a deleting historical data:
  - 'session_events' has a shorter TTL (30 DAY) than 'product_events' (365 DAY)
  - 'traces' has a shorter TTL (30 DAY) than 'product_events' (365 DAY)

It emits that as a WARNING and proceeds. On this dual-fed table it is worse than
it reads: the server and mobile rows arrive via `POST /v1/events` and have no
source datasource at all, so a rebuild drops them at every age, not merely past
30 days. This is the managed-warehouse sibling of the two data-loss paths the
review already closed on the BYO and local surfaces — a third mechanism, on a
surface I had not checked.

The forward query is the whole fix; with it the deploy reports only that the
table will be backfilled through the SELECT, which moves no data between
datasources and loses nothing.

Two things tested and deliberately NOT done:

- `DEPLOYMENT_METHOD alter` on `product_events_mv` does not substitute for it.
  With alter and no forward query the same data-loss warning returns, because
  it is the DATASOURCE schema change that triggers the source backfill, not the
  view's. (That method is right for the service-operations rollups, where the
  change is additive columns on the MV itself — a different shape.)
- Tinybird then suggests the inverse: "could be applied with ALTER TABLE and no
  data movement at promotion time if you remove the FORWARD_QUERY". Following
  that reintroduces the loss. Per the Tinybird rules the forward query can be
  removed in a LATER deploy, once this one has compacted.

Local ClickHouse schema identity is untouched — a forward query is Tinybird-only
and does not reach the DDL, so no v18 bump.

Verified: `bun typecheck` 41/41, `bun run lint` clean, ClickHouse schema /
local-manifest (v17) / Tinybird manifest gates up to date, packages/domain 703,
query-engine 1373, apps/cli 534.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One conflict, in the generated `tinybird-project-manifest.ts`, resolved the way
a generated file always should be — by regenerating it rather than hand-editing
the markers. `materializations.ts` auto-merged, so both sides' intent is intact
and the regenerated manifest simply reflects it.

Both changes are the same class of fix, arrived at independently:

- main's 0a54892 puts `DEPLOYMENT_METHOD alter` on `ai_trace_index_mv`,
  stopping Tinybird from migrating that target by replaying `traces` through the
  pipe — the backfill that crashed the maple_us deploy.
- this branch puts a `FORWARD_QUERY` on `product_events`, stopping the same
  engine from satisfying two added columns by rebuilding the table from
  `session_events` and `traces`, both of which keep 30 days against its 365.

They are not interchangeable, which is worth recording. `alter` fixes a changed
MV whose target gains columns. It does NOT fix this branch's case: tested
directly, `alter` on `product_events_mv` with no forward query brings the
data-loss warning straight back, because there it is the DATASOURCE schema
change that triggers the source backfill, not the view's.

Re-verified the deploy against the NEW main as baseline, not the old one: the
branch reports only the benign "will be backfilled because it has an explicit
FORWARD_QUERY" notice, no history deletion. (main's own baseline separately
warns about `service_overview_hourly`; that predates this branch.)

No renumber this time — main took no migration or local-schema slot, so this
stays at 0027 / v17.

Verified: `bun typecheck` 40/40, `bun run lint` clean, ClickHouse schema /
local-manifest / Tinybird gates up to date, apps/api 2582, apps/web 2392,
packages/domain 703, query-engine 1373, apps/cli 538. `bun.lock` matches main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four conflicts, all in generated files, all resolved by regenerating rather
than hand-merging the markers: local-inserts.json, local-schema.sql,
clickhouse_insert_mappings.rs and generated/clickhouse-schema.ts. Main's
3cdb22a regenerated the same artifacts for its new project revision, so both
sides were writing the same derived files from different inputs — the only
correct resolution is to rerun the generator over the merged source.

Nothing semantic conflicted. No migration or local-schema slot moved on main,
so this stays at 0027 / v17 and no renumber was needed.

Left `local-schema-v17.sql` alone deliberately. It differs from the regenerated
`local-schema.sql` by exactly one line — the `projectRevision` header — and the
DDL is byte-identical. That file is a FROZEN SNAPSHOT of what v17's schema was,
and the local structural manifest digest is computed over the schema objects
rather than the header comment, so the gate passes with it untouched and
refreshing it would only make a historical record track a revision it did not
ship with.

Verified: `bun typecheck` 40/40, `bun run lint` clean, ClickHouse schema /
local-manifest (v17, 81 objects) / Tinybird manifest gates all up to date,
apps/api 2582, apps/web 2392, packages/domain 703, query-engine 1373,
apps/cli 538.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant