paradigm

package
v0.2.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 10, 2026 License: Apache-2.0 Imports: 2 Imported by: 0

Documentation

Overview

Package paradigm computes a database schema's analytic-vs-transactional paradigm and each table's warehouse role, as a pure function over the neutral internal/core/db model. It imports ONLY internal/core/db — never a provider, never internal/config, never a sensor (ADR 0015's schema-only simplicity, extended to this second neutral input by ADR 0033).

Detect SEEDS the classification from structural evidence; Resolve applies an explicit developer override on top, honoring the project's innegociable developer-autonomy principle: an explicit config value always wins over detection.

The package also holds the SCHEMA GATE (schemagate.go, ADRs 0035/0036/0037): six schema-wide warehouse signals that ask the paradigm question TOP-DOWN. Detect CONSULTS IT FIRST — a schema that shows none of the three deciding signals is not a warehouse, and no table inside it receives a warehouse role at all. The gate was built inert in stage 1 so its verdict could be selected from a 26-corpus measurement instead of a hunch; stage 2 (ADR 0037) wired it in, and that wiring is test-locked structurally and behaviorally.

Resolve is where developer autonomy meets the gate: an explicit olap/mixed RESTORES roles a closed gate withheld, an explicit oltp restores nothing.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func StripRoleToken

func StripRoleToken(name string) (string, bool)

StripRoleToken removes the recognized warehouse role token from name and returns the REMAINDER, preserving that remainder's original spelling ("D_DATE" → "DATE", "date_dim" → "date", "DimDate" → "Date"). ok is false when the name carries no recognized token, in exactly the cases candidateRole refuses — the two share one implementation, which is the entire point.

WHY THIS IS EXPORTED. A second, parallel name vocabulary lived in internal/core/dwrules (DW-005's time-dimension list: dim_date / dim_time / dim_calendar) and did NOT move when this package's vocabulary widened. The result was measured over 22 real corpora: two warehouses spelling their calendar D_DATE / D_Date went from "no fact table, rule abstains" to a confident "this fact table reaches no time dimension" — over schemas that plainly declare one. Composing on this seam means the next widening here cannot silently re-open that hole; a copied token list would.

It deliberately does NOT return the nominated Role. A caller that wanted the role would be doing name-only classification, and the A5 corroboration gate (structure corroborates a name, it never substitutes for one) is not something this package hands out a bypass for. Callers get the remainder, so they can ask their own question about the rest of the name, and nothing else.

Types

type Classification

type Classification struct {
	Paradigm Paradigm
	Roles    map[string]Role

	// Unprovable names tables whose role could not be DECIDED because the
	// model does not prove their structure complete (db-model-completeness-
	// contract, design SS6). Distinct from an ordinary RoleUnclassified ("the
	// name nominates nothing"): a table demoted to RoleUnclassified is ALSO in
	// Unprovable only when its demotion cause might be a dropped statement
	// rather than a genuine structural absence. Never set for a table whose
	// name carries no recognized warehouse token (that demotion's cause is
	// vocabulary, not structure), and never set for a PROMOTED table
	// (promotion is unconditionally safe — a dropped FK can only undercount
	// fan-out/fan-in, never fabricate it).
	//
	// Membership is therefore the one PUBLIC signal that separates the two
	// demotion causes: candidateRole is unexported, and a name-demoted and a
	// structure-demoted table carry the identical RoleUnclassified. (A caller
	// can also ask StripRoleToken whether a name carries a recognized token at
	// all — but that answers a question about the NAME alone, and says nothing
	// about which of the two causes demoted a given table.)
	//
	// A THIRD demotion cause exists as of ADR 0037 and is deliberately NOT
	// recorded here: a closed schema gate. Gate.Withheld carries those, because
	// their cause is the schema-level verdict rather than anything about the
	// table's own structure, and conflating the two would make Unprovable lie
	// about why.
	Unprovable map[string]bool

	// Gate is the schema-level warehouse verdict that decided whether any role
	// in Roles could be a warehouse role at all (ADR 0037).
	Gate GateVerdict
}

Classification is a schema's computed paradigm plus its per-table role map (keyed by db.Table.Name). Roles always has an entry for every table in the schema Detect ran over.

func Detect

func Detect(s *db.Schema) Classification

Detect computes a Classification as a pure function of s.

TOP-DOWN, as of ADR 0037: the SCHEMA is judged first. WarehouseSignals asks whether schema-wide evidence makes this a warehouse at all, and only inside a schema that qualifies is any warehouse role assigned. When the gate is CLOSED, every table is RoleUnclassified — the roles that would have been assigned are preserved in Gate.Withheld so the decision is reportable, never silent — and the schema folds to oltp.

This is the fix for the hole ADR 0035 documented: the sensor's 3NF suppression reads the PER-TABLE role, so before the gate a single table named dim_status could silence its own DB-002/DB-003 1NF surface inside an otherwise purely transactional schema. It no longer can, because the schema now votes first.

INSIDE a qualifying schema nothing about role assignment changed. Everything the rest of this comment describes is exactly as it was.

Table role is determined by the NAME as the primary signal (locked decision A5) — see candidateRole for the recognized spellings, which cover underscore- delimited leading and trailing tokens and separator-free PascalCase, all case-insensitively. A fact- or dimension-CANDIDATE table is further corroborated by REAL relational structure — a fact candidate needs FK fan-out to factFanOutMin+ distinct tables; a dimension candidate needs to be referenced (fan-in) by at least one other table — and is demoted to unclassified when that structural evidence is absent. A lone single-column (surrogate) primary key is deliberately NOT, by itself, corroboration: almost every ordinary OLTP table has one, so accepting it alone made the corroboration vacuous (CRITICAL C1, fixed post-review — see ADR 0033 decision 2 and the S1 review ledger). Staging/mart candidates need no structural signal in S1 (design §2a). A table whose name nominates nothing is always unclassified, regardless of structure (structure corroborates a name, it never substitutes for one).

The schema-level Paradigm folds from the resulting role mix: any olap-role table (fact/dimension/staging/mart) coexisting with at least one non-olap-role (unclassified) table yields mixed — checked FIRST, since a schema is not purely analytic just because it contains a star shape somewhere; a schema of ONLY olap-role tables with at least one fact AND one dimension yields olap; otherwise oltp.

func Resolve

func Resolve(detected Classification, override Paradigm) Classification

Resolve applies an explicit developer override on top of detected. An empty or "auto" override returns detected unchanged (detection decides). An explicit oltp/olap/mixed override REPLACES the schema-level Paradigm — Roles stay detection-derived, so per-table suppression still works under an override that keeps a mixed reality.

AND IT OUTRANKS THE SCHEMA GATE, in one direction only. This is the project's innegotiable rule (CLAUDE.md, "el developer decide") applied to ADR 0037:

  • explicit olap or mixed is the developer ASSERTING that this is a warehouse. If the gate closed, its withheld roles are RESTORED and the verdict is reopened with ByOverride set. Leaving them withheld would not merely keep 1NF findings — it would hand the whole DW-0xx family an all-unclassified role map and silently run ZERO warehouse rules over a schema the developer just declared to be a warehouse.
  • explicit oltp is the developer asserting the opposite, and it restores NOTHING. Manufacturing a warehouse role there would overrule the developer in the one direction that SILENCES findings — the exact failure the gate exists to close. (The sensor also short-circuits 3NF suppression on explicit oltp; this is the second, independent lock on the same promise.)

The EVIDENCE (Gate.Fired, Gate.Deciding) always survives an override unchanged: it is a measured fact about the schema, and the override changes only what is done with it.

type GateVerdict

type GateVerdict struct {
	// Open is the verdict: this schema may hold warehouse roles.
	Open bool

	// ByOverride distinguishes the two ways Open becomes true. False means the
	// EVIDENCE opened it (Deciding names which signals). True means the
	// DEVELOPER opened it with an explicit database.paradigm: olap/mixed, over
	// a schema whose evidence said otherwise — a distinction the sensor's note
	// must keep, because "codefit judged this a warehouse" and "you told codefit
	// this is a warehouse" are different claims.
	ByOverride bool

	// Fired is every signal that fired, all six evaluated, in the gate's fixed
	// order. Reported whether or not the gate opened: the three excluded signals
	// are still evidence an agent may want, they just do not vote.
	Fired []Signal

	// Deciding is the subset of Fired that opened the gate — empty when the gate
	// is closed, and empty when ByOverride is true (config opened it, no signal
	// did).
	Deciding []Signal

	// Withheld names every table whose bottom-up role the CLOSED gate took away,
	// mapped to the role it would have received. Empty whenever Open is true.
	// This is the raw material for the sensor's audit trace: withholding a role
	// changes what codefit reports, so it is never silent.
	Withheld map[string]Role
}

GateVerdict is the schema gate's decision plus the evidence behind it. It is carried on every Classification so a consumer can always be told WHY — codefit never reports a classification it cannot justify.

type Paradigm

type Paradigm string

Paradigm classifies a schema (or an explicit override) as analytic, transactional, or a mix of both. ParadigmAuto is a valid CONFIG value (meaning "let detection decide") but Detect itself never returns it — detection always resolves to one of oltp/olap/mixed.

const (
	ParadigmOLTP  Paradigm = "oltp"
	ParadigmOLAP  Paradigm = "olap"
	ParadigmMixed Paradigm = "mixed"
	// ParadigmAuto is the config sentinel for "run detection" — never a
	// Detect() result, only a valid Resolve() override input.
	ParadigmAuto Paradigm = "auto"
)

type Role

type Role string

Role classifies a single table's warehouse role: an ordinary OLTP table, or a table whose name carries no recognized warehouse token (see candidateRole) or that lacks structural corroboration, gets the explicit RoleUnclassified value ("unclassified") — NOT the bare Go zero value (""), which Detect never returns. Every entry in a Classification's Roles map is always one of the five named constants below.

const (
	RoleFact         Role = "fact"
	RoleDimension    Role = "dimension"
	RoleStaging      Role = "staging"
	RoleMart         Role = "mart"
	RoleUnclassified Role = "unclassified"
)

type Signal

type Signal string

Signal names one piece of schema-wide warehouse evidence. The six values below are the complete set; the gate reports which of them a schema exhibits and nothing more.

const (
	// SignalCalendarTable: the schema contains a dedicated calendar/date/time
	// table.
	SignalCalendarTable Signal = "calendar_table"
	// SignalSurrogateKeyNames: the schema uses the _sk surrogate-key naming
	// convention as a convention, not incidentally.
	SignalSurrogateKeyNames Signal = "surrogate_key_names"
	// SignalBulkLoadShape: no declared foreign keys anywhere, yet many
	// key-like columns — the shape of a warehouse that drops FK constraints
	// for load speed.
	SignalBulkLoadShape Signal = "bulk_load_shape"
	// SignalNoAuditTimestamps: not one table in the schema carries
	// created_at/updated_at.
	SignalNoAuditTimestamps Signal = "no_audit_timestamps"
	// SignalStarTopology: a hub-and-spoke of depth exactly 1 — a table whose
	// foreign keys reach two or more tables that themselves reference nothing.
	SignalStarTopology Signal = "star_topology"
	// SignalTypeProfileSplit: the schema's COLUMN TYPES split into two poles —
	// a few numeric-dominated tables plus several text-dominated ones — instead
	// of the uniform mix a transactional schema shows.
	SignalTypeProfileSplit Signal = "type_profile_split"
)

type WarehouseEvidence

type WarehouseEvidence struct {
	Fired []Signal
}

WarehouseEvidence is the gate's result: exactly which signals a schema exhibits, in the fixed order of allSignals, plus the verdict those signals imply (Qualifies) and the named subset that produced it (Deciding).

func WarehouseSignals

func WarehouseSignals(s *db.Schema) WarehouseEvidence

WarehouseSignals evaluates all six schema-wide warehouse signals over s as a pure function, and returns the ones that fired.

Detect calls it before it assigns a single role (see paradigm.go). The wiring is test-locked in schemagate_verdict_test.go, structurally and behaviorally.

func (WarehouseEvidence) Count

func (e WarehouseEvidence) Count() int

Count is how many signals fired.

func (WarehouseEvidence) Deciding

func (e WarehouseEvidence) Deciding() []Signal

Deciding returns the fired signals that actually decide the verdict, in allSignals' order. Empty means the gate is closed; a non-empty result is exactly what a caller renders when it has to answer "why did you call this a warehouse".

func (WarehouseEvidence) Has

func (e WarehouseEvidence) Has(sig Signal) bool

Has reports whether sig is among the fired signals.

func (WarehouseEvidence) Qualifies

func (e WarehouseEvidence) Qualifies() bool

Qualifies is THE VERDICT: this schema is a warehouse iff at least one DECIDING signal fired. See decidingSignals for the measurement behind the selection, and for why counting all six is the wrong shape.

A consequence worth stating rather than leaving implicit: WarehouseSignals refuses to evaluate a schema below minJudgeableTables, so a schema of fewer than 3 tables can never qualify. That is the no-vacuous-truths guard reaching its natural conclusion — a two-table model is not enough schema to be evidence of anything — and the developer's explicit database.paradigm override is the escape hatch for the rare case where it is wrong.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL