4. Data model
4.1 Spine
Firm ─┬─ Practice ─── MethodologyPack ─── PackVersion ─┬─ Dimension ─── Practice(mgmt) ─── Item
│ ├─ BandScheme ─── Interpretation
│ ├─ Recommendation ─── ServiceOffering
│ └─ Form (census | short | frontline | 360 | interview guide)
├─ Client ─── Engagement ─── ClientWorkspace ─┬─ Population ─── Person ─── HierarchyNode
│ ├─ Wave ─── Instrument ─── Invitation ─── Response
│ │ └── ResponseItem | TextAnswer | VideoAnswer
│ ├─ Interview · Document · StructureSnapshot
│ ├─ Scorecard ─── ScoreCell
│ ├─ Theme · Quote · Clip
│ ├─ InsightObject ─── EvidenceRef
│ ├─ Deliverable
│ └─ ActionPortfolio ─── Initiative ─── ItemLink
├─ NormSet ─── NormCell (firm book; external references)
└─ User ─── RoleAssignment · AuditEvent
Note on naming: Practice is overloaded. In code, the firm's business unit is FirmPractice; the management practice being scored is PracticeConstruct. This document uses "Practice area" and "Practice" respectively.
4.2 Tenancy and encryption boundaries
| Boundary | Contains | Isolation mechanism |
|---|---|---|
| Firm tenant | Users, packs, library, offerings, brand, norm sets, billing | Separate schema per firm; firm-level KMS key |
| Client workspace | Population, persons, responses, text, video, interviews, documents, scorecards, insights, actions | Separate schema per workspace within the firm's residency region; workspace-level data encryption key (DEK) wrapped by firm key; video in a separate bucket with its own key and lifecycle policy |
| Book pool | Aggregated NormCells only; never row-level | Written by an aggregation job that reads k-safe Scorecards from opted-in workspaces; no reverse link to workspace IDs beyond a salted contributor hash |
Rules:
- No query path joins across client workspaces. Cross-client reads happen only through the Book pool.
- Every row in a workspace schema carries
workspace_id; row-level security enforces it even inside the schema (defence in depth). - Contractors hold
RoleAssignments scoped to namedengagement_ids with a mandatoryexpires_at. - Workspace purge = destroy DEK + delete objects + write tombstone AuditEvent. Crypto-shredding makes backups unreadable.
4.3 Entities and critical fields
Types: id = UUIDv7. ts = UTC timestamp. enum(...) = closed set. → = foreign key.
Firm layer
Firm
| Field | Type | Notes |
|---|---|---|
| id, name, legal_name | ||
| residency_default | enum(US, EU, UK, AU) | Workspaces inherit unless overridden |
| custom_domains[] | string | e.g. diagnostics.firm.com |
| brand_kit_id | → BrandKit | |
| ai_processing_opt_in | bool | Firm-level switch for use of client content in model improvement. Default false. Distinct from runtime AI features. |
| sso_config, scim_enabled | ||
| plan, credits_balance |
FirmPractice — id, firm_id, name (e.g., "Org & Transformation"), lead_user_id.
MethodologyPack — id, firm_id, firm_practice_id, name, pack_type enum(health, change_readiness, operating_model, strategy_cascade, psych_safety, manager_360, ways_of_working, capability, customer_centricity, risk_conduct, ai_readiness, pmi, frontline_pulse, custom), forked_from_pack_version_id, owner_user_id.
PackVersion
| Field | Type | Notes |
|---|---|---|
| id, pack_id, semver | 3.2.0 | |
| state | enum(draft, in_review, locked, retired) | Locked = immutable |
| comparability_group | string | Versions sharing a group are comparable on shared item versions |
| primary_metric | enum(mean_0_100, pct_favorable, frequency_pct) | |
| index_formula | json | Dimension weights, required dimensions |
| scoring_engine_min_version | string | |
| reviewers[], approved_by, approved_at | ||
| changelog | text |
Dimension — id, pack_version_id, code (e.g., DIR), name, definition, weight, required_for_index (bool), order.
PracticeConstruct — id, dimension_id, code (DIR-P2), name, definition, weight, min_items_answered (int, default 2 or 60%), order.
Item
| Field | Type | Notes |
|---|---|---|
| id, lineage_id | lineage_id stable across forks and wording edits | |
| item_version | int | Increment on any wording change |
| comparable_to[] | item_version ids | Explicit; wording edits are not comparable by default |
| practice_id | → PracticeConstruct | Nullable for demographic/outcome items |
| text_key | → TranslationKey | |
| format | enum(likert5, likert7, agreement5, frequency5, forced_rank, max_diff, matrix, nps_0_10, single, multi, numeric, open_text, video) | |
| reverse_coded | bool | |
| weight | decimal | Default 1.0 |
| required | bool | |
| construct_tags[] | string | Used by text/video coding |
| reading_level | decimal | Flesch-Kincaid grade, computed |
| sensitivity | enum(none, sensitive, special_category) | Special category = GDPR Art. 9 adjacency; forces extra review and higher text-k |
| validation_status | enum(orgdiagnostic_authored_pilot, published_scale, licensed, firm_custom, firm_validated) | Plain label shown in Studio and appendix |
| probe_rule | json | e.g., show probe P-17 if practice score in bottom band |
| na_allowed | bool |
BandScheme — id, pack_version_id, kind enum(absolute, norm_relative), bands[] {code, label, lower, upper}, norm_set_version_id (required when norm_relative).
Interpretation — id, band_scheme_id, target (dimension_id | practice_id | index), band_code, text, author_type enum(human, ai_draft), author_id, approved_by, approved_at. Unapproved interpretations cannot ship in a locked version.
Recommendation — id, pack_version_id, code (MGR-B2), title, move (short text), rationale, triggers[] {practice_id, band_codes[]}, service_offering_ids[], typical_duration, evidence_base (text).
ServiceOffering — id, firm_id, name, description, price_band, owner_practice_id.
Form — id, pack_version_id, kind enum(census, exec_short, frontline, pulse, rater_360, interview_exec, interview_middle, interview_frontline), item_ids[], block_layout, est_minutes_desktop, est_minutes_mobile, reportable_practices[] (computed).
NormSet — id, firm_id, layer enum(book, external), name, version, source (for external: publisher, licence ref, expiry), filters {industry, size_band, region, growth_stage, unionized, deskless_share_band}, eligibility {min_clients, min_respondents}, built_at.
NormCell — norm_set_id, target (item lineage | practice code | dimension code | index), stats {n_clients, n_resp, mean, sd, p25, p50, p75, p90}.
Engagement layer
Client — id, firm_id, name, industry (NAICS), size_band, region, contract_flags {book_opt_in, ai_improvement_opt_in, residency, retention_overrides, works_council}.
Engagement — id, client_id, firm_practice_id, package enum(rapid, full_census, transformation_pulse, portfolio, custom), stage enum(proposed, contracted, setup, fielding, analysis, readout, retainer, closed), partner_id, em_id, team[], fee_band, steering_dates[], crm_ref.
ClientWorkspace — id, engagement_id (1:1; a retainer re-uses the workspace across waves), residency, dek_ref, brand_mode enum(firm, co_brand), subdomain, anonymity_policy_id.
AnonymityPolicy
| Field | Default | Works-council default |
|---|---|---|
| k_scores | 5 | 8 (configurable to 10) |
| k_text | 10 | 15 |
| k_video_shareable | explicit consent + 10 | explicit consent + 15 |
| rater_group_k (360) | 3 | 5 |
| manager_view_enabled | true | false until council sign-off |
| max_filter_depth_client | 2 | 1 |
| text_export_to_client_managers | false | false |
| complementary_suppression | on | on |
Population — id, workspace_id, snapshot_at, source enum(csv, workday, successfactors, bamboohr, rippling, personio, adp), hygiene_report {duplicates, leavers, missing_manager, cycles}.
Person — id, population_id, external_ref_hash (salted), contact (encrypted, purged at wave close + N days), hierarchy_node_id, attributes {level, function, location, tenure_band, union_flag, deskless_flag, language, employment_type, custom{}}. No name stored unless 360 or interviews require it.
HierarchyNode — id, population_id, parent_id, name, node_type, headcount, span_of_control, layer, cost_band (optional), maps_to_prior[] {prior_node_id, relation enum(same, merged_into, split_from, renamed), weight}.
Wave — id, workspace_id, seq, kind enum(baseline, pulse, post_intervention, annual, legacy_import), pack_version_id, open_at, close_at, status, comparable_to_wave_id, language_set[], target_response_rate.
Instrument — id, wave_id, form_id, audience_filter, channels[] enum(email, sms, slack, teams, whatsapp, qr, kiosk, sso, magic_link), additive_modules[].
Invitation — id, instrument_id, person_id, token_hash, channel, sent_at, opened_at, completed_at. Stored separately from responses; the link table is dropped at close unless the engagement is longitudinal at person level (off by default).
Response — id, instrument_id, respondent_key (opaque; not person_id), segment_snapshot {hierarchy_node_id, attributes at time of response}, started_at, submitted_at, duration_s, device, language, completeness.
ResponseItem — response_id, item_id, item_version, value_raw, value_scored (0–100 after reverse/normalize), is_na.
TextAnswer — id, response_id, item_id, text_original, text_redacted, language, translation, pii_flags[], construct_codes[], emergent_theme_ids[], quote_permission enum(internal, client_aggregate, client_shareable).
VideoAnswer — id, response_id, item_id, storage_ref, duration_s, transcript_redacted, speaker_segments, consent_tier enum(transcript_only, internal_video, client_shareable_video), retention_until, purged_at.
Interview — id, workspace_id, guide_form_id, interviewee_role_band, layer, date, notes, transcript_ref, consent_tier, coded_segments[].
Document — id, workspace_id, kind enum(strategy_deck, org_chart, prior_survey, policy, other), storage_ref, extracted_text_ref, coded_segments[].
StructureSnapshot — id, workspace_id, as_of, spans/layers stats per node, cost bands (optional), source.
Analysis layer
Scorecard
| Field | Notes |
|---|---|
| id, wave_id, cut_definition | e.g. {hierarchy_node: X} or {level: "Supervisor", site: "Plant C"} |
| engine_version, pack_version_id, weighting_scheme | Deterministic replay requires all three |
| input_hash | Hash of the response set used |
| n_respondents, suppressed (bool), suppression_reason | |
| computed_at |
ScoreCell — scorecard_id, target (index | dimension | practice | item), value, pct_favorable, sd, ci_low, ci_high, n, band_code, band_borderline (bool), delta_prev, delta_sig, effect_size, norm_percentile_book, norm_percentile_external.
Theme — id, wave_id, kind enum(construct, emergent), construct_code, label, definition, prevalence, intensity, segment_skew[], confidence enum(high, medium, low), model_provenance.
Quote — id, source_type enum(text, video, interview, document), source_id, span, construct_codes[], theme_ids[], segment_label (k-safe), permission_tier.
Clip — id, video_answer_id | interview_id, t_start, t_end, transcript_span, construct_codes[], consent_tier.
InsightObject — see §6.
Deliverable — id, engagement_id, kind enum(deck, pdf, excel_appendix, clip_reel, manager_pack, portal_publish), template_id, insight_ids[], generated_at, generated_by, k_check_passed, file_ref.
ActionPortfolio / Initiative — id, workspace_id, title, owner (client user), due_date, status, leading_indicator {item_lineage_id | practice_code, target_delta}, linked_insight_ids[], service_offering_id (optional).
Governance layer
User — id, firm_id | client_id, email, sso_subject, status.
RoleAssignment — user_id, role enum(firm_admin, partner, practice_lead, em, analyst, contractor, client_sponsor, client_admin, client_viewer, client_manager), scope {firm | engagement_id | workspace_id | hierarchy_node_id}, expires_at.
AuditEvent — id, ts, actor_id, action enum(view_aggregate, view_identifiable, export, publish, approve, permission_change, purge, reopen_wave, ai_run), object_ref, purpose_text (required for view_identifiable and export), ip, result. Append-only; hash-chained per workspace.
4.4 Invariants the engine enforces
- A Wave's
pack_version_idmust belocked. - ResponseItems store
item_version; scores are never recomputed against a newer item version. - Any ScoreCell with
n < k_scoresis persisted as suppressed and never serialized to a client-facing surface. - An InsightObject cannot be published unless every EvidenceRef resolves and passes k at publish time and render time.
- Book NormCells are built only from workspaces whose client has
book_opt_in = trueand only oncemin_clientsandmin_respondentsare met. - Video past
retention_untilis purged by job;purged_atis set and clips referencing it degrade to transcript-only.