signals — Module Spec

1. Purpose

signals is the feature/forecast/outcome store of the agents-v2/v3 build — where the not-yet-built agents module (Phase 5) will read the bitemporal facts a decision needs ("what did we know about this entity, and when did we know it") and write back the measured outcomes of its own past actions. It is Phase 4 of a 6-phase agents-v2/v3 build authorization (Phase 1 reopened platform for the module registry + AI-capacity layer, PROJECT_DECISIONS #62; Phase 2 reopened ai for the model-registry/deployment/prompt/routing layer, PROJECT_DECISIONS #63; Phase 3 built semantics, the shared business ontology, PROJECT_DECISIONS #64) — and is this build's own explicitly-designated highest-risk phase, combining 3 mechanisms no prior phase combined in one schema: table partitioning, a split-authority pattern for an invariant a native partitioned index structurally cannot express, and SECURITY DEFINER tenant-isolation functions.

Like semantics, signals has zero v1 antecedent — confirmed: v1 had no feature-store, forecast, anomaly-detection, or experimentation concept of any kind. Unlike semantics (definitional, low-volume, human-curated), signals is observational, high-volume, bitemporal, append-only, partitioned — the opposite operational profile.

See PROJECT_DECISIONS #65 for the full build record, including the 4 named critical guards, the 2 privilege-mechanics gaps found during the migration-apply pass, and the Section 4 self-audit fix.

2. Ownership

Owns — 11 tables, 111 columns:

Table Cols Tenant-scoped? Partitioned? Role
feature_definition 6 No (global) No Standard A2b catalog — the stable, versioned concept a feature is
feature_version 7 No (global) No A revision of a feature definition — how it's computed
experiment 6 No (global) No Standard A2b catalog — the stable, versioned concept an experiment is
experiment_version 9 No (global) No A revision of an experiment — immutable treatment logic, allows_reentry/washout_days
feature_value 10 Yes Yes (weekly, recorded_at) Bitemporal feature-value store, REVOKE ALL from authenticated — function-only read
forecast 11 Yes Yes (weekly, recorded_at) Bitemporal forecast store, same access shape as feature_value
anomaly_score 10 Yes Yes (weekly, recorded_at) Bitemporal anomaly-score store, same access shape as feature_value
experiment_assignment 19 Yes No Which subject got which treatment arm; RLS-enabled
experiment_exposure_event 5 Yes No Append-only exposure log; RLS-enabled
outcome_observation 19 Yes Yes (monthly, created_at) Append-only, remeasurable outcome ledger; RLS-enabled
outcome_authority 9 Yes No The split-authority pattern — exactly one authoritative observation per scope
Total 111 7 of 11 tenant-scoped 4 of 11 partitioned

Does NOT own:

  • The agents module's own runtime (Phase 5, not yet built) — signals stores facts and outcomes for an agent decision to read/write; it does not itself decide, act, or execute anything.
  • The shared business ontologysemantics.metric_definition/goal_definition/attribution_model_definition (already built, Phase 3). signals.outcome_observation.attribution_model_version_id is a real FK into semantics.attribution_model_version; signals does not duplicate or reinvent that vocabulary.
  • Agent identity/authorityidentity.agent_identity/agent_duty_grant (already built and locked). signals introduces no new authority mechanism of its own (see §8).
  • The decision-context snapshot agents.decision_context_snapshot (Phase 5, not yet built) — signals.lock_experiment_causal_basis() is the function this table's own future trigger will call, but the CREATE TRIGGER ... ON agents.decision_context_snapshot statement itself is deferred to Phase 5, which builds that table.

3. Layer & Dependencies

Foundation layer (per SCHEMA_CONVENTIONS.md's foundation/business/consumer taxonomy) — infrastructure a future agent capability reads and writes, not a nursery-vertical product surface, structurally the same positioning as ai, approvals, and semantics.

Depends on:

  • platformtenant.id (the FK target for all 7 tenant-scoped tables' tenant_id column) and the new platform.current_tenant_id() helper function this phase adds (a thin wrapper over the session GUC every RLS policy and tenantDB()/consumerDB() call already sets — the tenant-scope mechanism the 3 SECURITY DEFINER as-of functions derive their tenant boundary from, instead of a spoofable parameter).
  • semanticsattribution_model_version.id (real FK from outcome_observation.attribution_model_version_id) — the first real consumer of semantics's own attribution-model family, exactly as semantics's own Phase 3 doc predicted this table would do, replacing v2's original free-text attribution_method column.

Depends on NO domain/nursery-vertical module — same reverse-dependency discipline approvals/semantics established for themselves: signals never references crm, inventory, pos, or any other product module directly. feature_value.entity_type/entity_ref and experiment_assignment.subject_type/subject_ref are polymorphic, no-FK pointers that CAN reference any table in the system without signals ever being blocked by, or blocking, that table's own schema evolution.

Will depend onagents.agent_action (Phase 5, not yet built): outcome_observation.agent_action_id/outcome_authority.agent_action_id are disclosed forward-refs, bare uuid, no FK today; and agents.decision_context_snapshot (Phase 5): lock_experiment_causal_basis()'s own CREATE TRIGGER statement is deferred until that table exists.

Depended on by: the not-yet-built agents module (Phase 5) — this is the entire reason signals exists as its own phase, built before agents. agents is expected to write feature_value/forecast/anomaly_score rows (via an elevated service connection, not authenticated), read them back through the 3 SECURITY DEFINER as-of functions once agent_reader (Phase 6) exists, write outcome_observation rows measuring its own past actions' effect, and drive experiment_assignment/experiment_exposure_event for any agent decision that runs as a randomized experiment. Not live today — signals has zero real callers at this phase, by design (see §7).

4. Tables — grouped

Feature/Experiment catalog family (4, standard A2b pattern): feature_definitionfeature_version / experimentexperiment_version (the latter carrying B4's own 2 required decisions: allows_reentry, washout_days).

Bitemporal value family (3, PARTITIONED weekly by recorded_at): feature_value / forecast / anomaly_score — the same shape (polymorphic entity_type/entity_ref, as_of business-truth time, recorded_at ERP-knowledge-time partition key, lineage JSONB), REVOKE ALL from authenticated, readable only through 3 SECURITY DEFINER as-of functions.

Experiment causal-inference family (2, NOT partitioned): experiment_assignment (who got which treatment arm, assigned_at forgery-proofed by a trigger) → experiment_exposure_event (append-only, validated against its own assignment's assigned_at, drives the causal-basis-lock/contamination-flag state machine).

Outcome split-authority family (2): outcome_observation (PARTITIONED monthly by created_at, append-only/remeasurable) + outcome_authority (NOT partitioned, the real "exactly one authoritative observation per scope" enforcement mechanism, maintained by a native atomic upsert).

Full column-level detail (types, nullability, defaults, CHECK constraints, indexes, triggers, partition mechanics) lives in docs/database/schema_docs/signals.md — not restated here.

5. Capabilities

signals defines no user-facing capability and no agent-authored write path of its own in this phase — it is infrastructure a future agent decision reads and writes through, not a screen a human uses.

  • Enforced bitemporal readsget_feature_as_of()/get_forecast_as_of()/get_anomaly_score_as_of() structurally cannot return a fact the caller "shouldn't have known yet" — a fact recorded (recorded_at) after the caller's own knowledge_cutoff parameter is invisible, regardless of how far in the business past (as_of) it describes. This is not a service-layer discipline; it's a DB-level guarantee live-reproduced against exactly this scenario (a fact learned 1 day after a decision was made 30 days earlier).
  • Tenant-safe function-mediated access — the 3 as-of functions derive tenant scope from platform.current_tenant_id() (the session GUC), not a caller-supplied parameter, closing a real spoofing vector a naive SECURITY DEFINER design would have left open.
  • Partition-safe split-authority enforcementoutcome_authority's own PK guarantees exactly one authoritative observation per (tenant, agent_action, outcome_type, measurement_window) scope, maintained via a native, race-free INSERT ... ON CONFLICT upsert, correctly across however many monthly partitions outcome_observation itself has split its history into.
  • Forgery-proof and contamination-aware experimentationassigned_at cannot be backdated (a trigger overwrites it unconditionally); an exposure event cannot predate its own assignment; and a genuinely-earlier exposure arriving after the causal basis is already locked is flagged (contamination_status, outcome_authority.remeasurement_required), never silently absorbed into history.

No AI write path is implemented this phase — schema-only build (Drizzle + migration + tests), no SignalsService. See §6 and §9.

6. Service Contract — SignalsService

No SignalsService exists yet — schema-only this phase, see PROJECT_DECISIONS #65. This matches every phase of the agents-v2/v3 build so far (Phase 1's platform reopen, Phase 2's ai reopen, and Phase 3's semantics build were all schema-only too) — every phase of this build stays schema-only until Phase 5 lands the agents module itself. Downstream code (once it exists) would query signals.* directly via Drizzle for the 4 catalog + 2 experiment-causal-inference tables, and would call the 3 SECURITY DEFINER as-of functions for feature_value/forecast/anomaly_score reads (once agent_reader, Phase 6, is granted EXECUTE — see §11).

The mechanisms that matter most for correctness — the split-authority upsert, the bitemporal-read enforcement, the assigned_at forgery guard, and the retroactive-contamination-race fix — are NOT waiting on that service layer to be correct: they are already live, atomic, and enforced directly by the schema regardless of what future application code does or doesn't do.

7. Data-Flow / Population Model

signals is unusual among this project's modules in the same way ai's agent-runtime cluster and semantics were at their own first pass: it has no first-party UI and is not populated by a user filling out a form in this module directly.

  • feature_value/forecast/anomaly_score are written by backend service jobs (a feature-computation pipeline, a forecasting job, an anomaly-detection job), never directly by tenant-scoped application code — REVOKE ALL from authenticated on all 3 tables (parent and every partition) enforces this structurally, not just by convention. The sole sanctioned read path for a tenant-scoped caller is the 3 SECURITY DEFINER as-of functions, and even those have no GRANT EXECUTE to any role yet (see §11).
  • The 4 catalog tables (feature_definition, feature_version, experiment, experiment_version) are populated by platform/service_roleauthenticated gets SELECT only, with an explicit REVOKE INSERT, UPDATE, DELETE.
  • experiment_assignment/experiment_exposure_event are RLS-enabled and tenant-scoped, but authenticated has no bespoke INSERT grant on either in this migration (only the schema-wide default SELECT, plus experiment_assignment's own scoped-column UPDATE) — consistent with the design's own framing that assignment/exposure recording is a randomization-service responsibility, not a tenant-application write path, mirroring feature_value's own "written by an elevated service connection" convention.
  • outcome_observation/outcome_authority follow the same pattern as experiment_assignment for inserts, with one exception: outcome_authority gets a genuine GRANT SELECT, INSERT, UPDATE to authenticated, since the promotion trigger that maintains it runs under the INSERTING session's own privileges (not SECURITY DEFINER) — but the intended access path is exclusively through promote_authoritative_observation()'s own upsert, a disclosed workflow convention rather than a DB-enforced restriction.
  • agents (deferred, Phase 5) is the expected FIRST REAL READER AND WRITER of this entire schema. Until it's built, every table in signals can be populated (by a future backend service/migration pass) and queried, but nothing in the live system actually consumes it yet — the same deliberate "build the substrate before the speaker" sequencing semantics established for itself.

8. Agent Authority Mapping

signals introduces no new authority mechanism. It is not itself a consumer of identity.agent_duty_grant in this phase either — like semantics, signals defines no agent-authored write path at all in this build (see §5 and §7): the 4 global catalog tables are platform-write-only; feature_value/forecast/anomaly_score are elevated-service-write-only; experiment_assignment/experiment_exposure_event/outcome_observation have no bespoke authenticated INSERT grant either. Once agents (Phase 5) is built and begins writing to this schema, agent actions will be logged the same way every other module's agent actions are — via ai.agent_execution — but that is ai's mechanism, not a new one signals invents.

9. Cross-Module Seams

Cataloged in docs/modules/CROSS_MODULE_CONTRACTS.md (referenced, not restated here). Key relationships:

Direction Contract
Signals → Platform every tenant-scoped table's tenant_id → platform.tenant.id (enforced FK, RLS scope where RLS applies). platform.current_tenant_id() (new this phase) is the tenant-derivation mechanism the 3 SECURITY DEFINER as-of functions call instead of trusting a caller-supplied parameter.
Signals → Semantics outcome_observation.attribution_model_version_id → semantics.attribution_model_version.id (enforced FK, NOT NULL) — the first real consumer of semantics's attribution-model family, replacing v2's original free-text attribution_method column exactly as semantics's own Phase 3 doc predicted.
Signals (internal, disclosed forward-ref) outcome_observation.agent_action_id / outcome_authority.agent_action_id are plain uuid, NO FKagents.agent_action (Phase 5) doesn't exist yet (confirmed live: zero tables in the agents schema as of this phase). Logged in docs/open-items/OPEN_ITEMS.md (signals | FK).
Signals (internal, disclosed deferred GRANT) get_feature_as_of() / get_forecast_as_of() / get_anomaly_score_as_of() have REVOKE EXECUTE FROM PUBLIC but no GRANT EXECUTE to any role — agent_reader (Phase 6) doesn't exist yet. Matches this design's own explicit allowance to "stub the REVOKE ahead of the role's full build." Logged in docs/open-items/OPEN_ITEMS.md (signals | FK).
agents (deferred, Phase 5) → Signals (forward, not yet built) expected to write feature_value/forecast/anomaly_score via an elevated service connection, read them back through the 3 as-of functions once agent_reader exists, write outcome_observation measuring its own past actions, and drive experiment_assignment/experiment_exposure_event for randomized-experiment decisions. agents.decision_context_snapshot's own future CREATE TRIGGER for lock_experiment_causal_basis() also lands here.

10. Design Rationale

  • DR-1 — the split-authority pattern is the load-bearing mechanism for BLOCKER 2. A native unique/partial-unique index enforcing "exactly one authoritative observation per scope" cannot survive PARTITION BY RANGE(created_at) — Postgres requires the partition key in every unique constraint on a partitioned table, which would silently narrow the guarantee to per-partition-month scope. outcome_authority, a genuinely separate unpartitioned table whose own PK is the real enforcement, closes this without weakening the guarantee. See docs/database/schema_docs/signals.md's own "Design Patterns Summary" for the full live-reproduction record.
  • DR-2 — the SECURITY DEFINER as-of functions derive tenant scope from a session GUC, not a parameter, because a parameter is spoofable by construction. No amount of "validate the parameter" logic closes a hole where the parameter itself is attacker-controlled; removing the parameter entirely (deriving scope from platform.current_tenant_id() instead) is the only structural fix (BLOCKER 1).
  • DR-3 — 2 constraints were corrected from the design doc's own literal SQL during the build itself, not by a later audit: anomaly_score's uniqueness (design said (id, tenant_id), corrected to (id, tenant_id, recorded_at)) and outcome_observation's second unique constraint (design omitted created_at, corrected to include it, backstopped by a genuine cross-partition trigger since the corrected constraint alone would silently narrow the invariant to per-partition-month scope). Both are the same structural class of error BLOCKER 2 itself documents for is_authoritative, just not caught by that section's own adversarial pass for these 2 sibling constraints — found live during THIS phase's own migration-apply pass. See docs/database/schema_docs/signals.md for the full disclosure.
  • DR-4 — assigned_at forgery prevention overwrites rather than validates. DEFAULT now() only fires when a column is omitted; it is not a validation rule and does nothing to stop an explicit backdated INSERT. force_assignment_timestamp() unconditionally overwrites the value with clock_timestamp() (the true wall-clock instant, not now(), which is frozen at transaction start and therefore constructible by a caller holding a transaction open) — a structural fix, not a stricter check (BLOCKER 3, Fix 1).
  • DR-5 — the retroactive-exposure-contamination race is closed by row-locking the assignment first, in both functions that touch it. lock_experiment_causal_basis() and update_first_eligible_exposure() both SELECT ... FOR UPDATE the target assignment row before reading or writing its own state — this is what makes the two functions serialize correctly against each other; the contamination-flagging branch logic alone, without the lock, would not have closed the live-reproduced race (BLOCKER 3, Fix 2).
  • DR-6 — subject_type/subject_ref resolve a genuine internal naming inconsistency in the design doc itself, disclosed rather than silently picked: the design's own column-list prose said assignment_unit, its own "Uniqueness" line said subject_type, subject_ref — treated as the same single concept, named per the more specific, typed, structurally-consistent form matching this codebase's established polymorphic-pointer convention.
  • DR-7 — 2 privilege-mechanics gaps were found live during the migration-apply pass, distinct from the design-phase guard verifications: ALTER FUNCTION ... OWNER TO signals_function_owner needed both role-membership and schema-CREATE grants the migration-running role didn't have by default (this Supabase stack's postgres role is not a true superuser); and the SECURITY DEFINER function body's own call to platform.current_tenant_id() needed signals_function_owner to hold USAGE on schema platform, a genuinely separate requirement from EXECUTE on the function itself.

11. Deferred / Future Items

All items tracked in docs/open-items/OPEN_ITEMS.md, attributed to signals (PROJECT_DECISIONS #65). Summary for context:

Item Status Trigger
outcome_observation.agent_action_id / outcome_authority.agent_action_id — bare uuid, no FK open, disclosed forward-ref Wire both as real composite FKs → agents.agent_action(id, tenant_id) when Phase 5 lands that table.
get_feature_as_of() / get_forecast_as_of() / get_anomaly_score_as_of()REVOKE EXECUTE FROM PUBLIC, no GRANT EXECUTE to any role open, disclosed deferred GRANT Wire GRANT EXECUTE ... TO agent_reader on all 3 functions when Phase 6 lands that role.
lock_experiment_causal_basis()'s own CREATE TRIGGER ... ON agents.decision_context_snapshot open, disclosed forward-ref The function itself is built this phase (touches only signals.experiment_assignment); the trigger statement is deferred until Phase 5 builds agents.decision_context_snapshot.
SignalsService open No service layer exists yet for any of the 11 tables — schema-only this phase, matching every phase of this build. Trigger: when a service layer is built, most likely alongside or just before Phase 5's agents module, since agents is this schema's first real read/write consumer.
agents real consumption open signals has zero real callers today — a deliberate build-the-substrate-first sequencing. Trigger: Phase 5 (agents) of the same build authorization.
Last modified: Jul 12, 2026, 3:52 AM PT
On this page
Esc