/** * UPG Entity Type Catalog. * https://unifiedproductgraph.org/spec | MIT */ /** * The union of every active entity type, across the flat domain registry. * Count is computed at module init as `UPG_TYPES.length` (see `UPG_ENTITY_COUNT`); * no hard-coded total is kept here, so the comment can't drift from the catalog. * * @see registry/domains.ts for domain groupings * @see registry/entity-meta.ts for type IDs and maturity * @see grammar/hierarchy.ts for parent-child rules */ type UPGEntityType = 'specification' | 'primitive' | 'operating_lifecycle' | 'operating_stage' | 'product' | 'outcome' | 'objective' | 'key_result' | 'metric' | 'metric_quality_assessment' | 'vision' | 'mission' | 'strategic_theme' | 'initiative' | 'capability' | 'value_stream' | 'strategic_pillar' | 'assumption' | 'decision' | 'constraint' | 'strategic_question' | 'persona' | 'job' | 'need' | 'desired_outcome' | 'job_step' | 'switching_cost' | 'opportunity' | 'solution' | 'feasibility_study' | 'design_sprint' | 'hypothesis' | 'hypothesis_evidence' | 'experiment' | 'experiment_plan' | 'experiment_run' | 'learning' | 'test_plan' | 'evidence' | 'research_plan' | 'competitor' | 'competitor_feature' | 'competitor_signal' | 'market_trend' | 'market_segment' | 'competitive_analysis' | 'classification_axis' | 'classification_value' | 'research_study' | 'insight' | 'participant' | 'observation' | 'quote' | 'affinity_cluster' | 'research_question' | 'interview_guide' | 'survey_response' | 'user_journey' | 'journey_step' | 'journey_phase' | 'journey_action' | 'design_question' | 'design_concept' | 'prototype' | 'wireframe' | 'user_flow' | 'screen' | 'screen_state' | 'surface' | 'annotation' | 'interaction_spec' | 'design_component' | 'design_token' | 'design_system' | 'design_pattern' | 'design_guideline' | 'brand_identity' | 'brand_colour' | 'brand_typography' | 'brand_voice' | 'brand_logo' | 'brand_imagery' | 'feature_area' | 'feature' | 'epic' | 'user_story' | 'story_statement' | 'story_task' | 'acceptance_criterion' | 'release' | 'task' | 'bug' | 'roadmap' | 'roadmap_item' | 'theme' | 'roadmap_theme' | 'changelog' | 'planning_cycle' | 'configuration_axis' | 'bounded_context' | 'service' | 'domain_event' | 'api_contract' | 'technical_debt_item' | 'feature_flag' | 'deployment' | 'aggregate' | 'domain_entity' | 'value_object' | 'command' | 'read_model' | 'api_endpoint' | 'database_schema' | 'queue_topic' | 'build_artifact' | 'code_repository' | 'library_dependency' | 'integration_pattern' | 'external_api' | 'data_flow' | 'investigation' | 'root_cause' | 'symptom' | 'fix' | 'funnel' | 'funnel_step' | 'acquisition_channel' | 'growth_campaign' | 'cohort' | 'behavioral_segment' | 'growth_loop' | 'variant' | 'attribution_model' | 'business_model' | 'value_proposition' | 'revenue_stream' | 'pricing_tier' | 'cost_structure' | 'unit_economics' | 'partnership' | 'key_resource' | 'key_activity' | 'customer_relationship' | 'distribution_channel' | 'gtm_strategy' | 'ideal_customer_profile' | 'positioning' | 'messaging' | 'launch' | 'content_strategy' | 'sales_motion' | 'competitive_battle_card' | 'demand_gen_program' | 'territory' | 'objection' | 'rebuttal' | 'proof_point' | 'team' | 'role' | 'stakeholder' | 'person' | 'team_okr' | 'retrospective' | 'dependency' | 'department' | 'skill' | 'ceremony' | 'capacity_plan' | 'data_source' | 'event_schema' | 'dashboard' | 'data_model' | 'data_quality_rule' | 'data_product' | 'data_pipeline' | 'data_lineage' | 'glossary_term' | 'data_domain' | 'report' | 'content_piece' | 'knowledge_base_article' | 'brand_asset' | 'prompt_template' | 'content_calendar' | 'content_theme' | 'documentation_template' | 'document' | 'legal_entity' | 'ip_asset' | 'contract' | 'contract_clause' | 'privacy_policy' | 'compliance_requirement' | 'risk' | 'data_contract' | 'audit_log_policy' | 'compliance_framework' | 'security_audit' | 'service_level_indicator' | 'service_level_objective' | 'error_budget' | 'incident' | 'postmortem' | 'runbook' | 'monitor' | 'alert_rule' | 'ci_pipeline' | 'release_strategy' | 'on_call_rotation' | 'infrastructure_component' | 'threat_model' | 'threat' | 'vulnerability' | 'security_control' | 'security_policy' | 'penetration_test' | 'security_review' | 'data_classification' | 'access_policy' | 'a11y_standard' | 'a11y_guideline' | 'a11y_audit' | 'a11y_issue' | 'a11y_annotation' | 'test_suite' | 'test_case' | 'qa_session' | 'regression_test' | 'test_coverage_report' | 'test_environment' | 'test_result' | 'feedback_program' | 'feature_request' | 'feedback_vote' | 'nps_campaign' | 'user_advisory_board' | 'beta_program' | 'feedback_theme' | 'pricing_strategy' | 'discount_strategy' | 'trial_config' | 'paywall' | 'ai_model' | 'prompt_version' | 'eval_benchmark' | 'eval_run' | 'ai_cost_tracker' | 'hallucination_report' | 'ai_guardrail' | 'model_comparison' | 'ai_experiment' | 'ai_dataset' | 'ai_trace' | 'workflow_template' | 'workflow_run' | 'agent_definition' | 'agent_session' | 'review_gate' | 'approval_record' | 'agent_skill' | 'agent_hook' | 'workflow_artifact' | 'agent_task' | 'organization' | 'portfolio' | 'product_area' | 'workspace' | 'framework_exercise' | 'composition' | 'capture' | 'account' | 'contact' | 'lead' | 'deal' | 'pipeline_sales' | 'pipeline_stage' | 'quote_document' | 'subscription' | 'invoice' | 'forecast' | 'program' | 'project' | 'milestone' | 'risk_register' | 'change_request' | 'deliverable' | 'resource_allocation' | 'status_report' | 'marketing_strategy' | 'marketing_channel' | 'marketing_campaign_plan' | 'email_sequence' | 'social_post' | 'seo_keyword' | 'ad_creative' | 'press_release' | 'event' | 'community_initiative' | 'support_ticket' | 'customer_feedback' | 'churn_reason' | 'customer_health_score' | 'playbook' | 'service_level_agreement' | 'customer_journey_stage' | 'touchpoint' | 'success_milestone' | 'service_blueprint' | 'locale' | 'translation_key' | 'translation_bundle' | 'locale_config' | 'cultural_adaptation' | 'regional_pricing' | 'education_program' | 'tutorial' | 'walkthrough' | 'webinar' | 'certification' | 'help_video' | 'learning_path' | 'partner_program' | 'partner_tier' | 'api_ecosystem' | 'marketplace_listing' | 'developer_portal' | 'integration_partner' | 'partner_revenue_share'; /** * No entity types are currently deprecated; this union is `never`. The * deprecated-name bridge, which previously widened this union to early * aliases (`jtbd`, `pain_point`, `kpi`, …), is closed, so runtime callers no * longer need an alias-aware type. * * Narrowed to `never` so any code that still references this union for * widening cascades a type error and gets cleaned up. Migration runtime * (`migrateNode`, `UPG_MIGRATIONS`, `getReplacementType`) keeps its own * internal list of historical names; it does NOT depend on this union. * * @see ../grammar/migrations.ts for runtime migration helpers * @see scripts/check-no-deprecated-symbols.ts for the string-literal guard */ type DeprecatedUPGEntityType = never; /** All entity types. `DeprecatedUPGEntityType` is `never`, so this equals `UPGEntityType`. */ type AnyUPGEntityType = UPGEntityType | DeprecatedUPGEntityType; /** * UPG Assessment Scales. Spec-defined 5-point scales + `UPGAssessment`. * https://unifiedproductgraph.org/spec | MIT */ /** An assessment is a human judgment mapped to a numeric scale. * It carries both the qualitative meaning (label) and the numeric * encoding (value) so UIs can display labels and formulas can compute * scores. */ interface UPGAssessment { /** The numeric value, used for computation. Must fall within the referenced * scale's `min`..`max`. This is the ONLY field that carries cross-tool meaning. */ value: number; /** The qualitative label: what the assessor actually meant. * * FREE TEXT BY CONTRACT (docket wave 2 item 7; clarification STAGED, release * number assigned at release prep). This is the * assessor's own word and is NOT required to equal the `UPGScalePoint.label` of * the matching point. The two are different vocabularies serving different jobs: * `UPGScalePoint.label` is the scale's display name for a point (what a picker * offers); `UPGAssessment.label` is the judgment as it was actually authored * (what a human wrote). `{ value: 5, label: 'critical' }` on a `severity_5` * property is CONFORMANT even though point 5 displays as "Blocker" — indeed * several property descriptions instruct writers to "carry the old word in * `label`" when migrating a legacy enum. * * Consequently: never reconcile stored labels against scale point labels, and * never widen a scale's `points` to admit authored words. Compare on `value`. * * The one exception is `friendly_aliases`. An alias (`low`/`medium`/`high`) is a * writer-side shorthand, not a judgment — expanding it MUST yield both the * canonical `value` and the canonical point label, so an unexpanded alias left * sitting in `label` is a writer bug. */ label: string; /** Which assessment scale this was rated on. * References a scale definition in the spec or in the document's * scale_extensions. If omitted, the spec default scale for this * property is assumed. */ scale_id?: string; /** Normalized 0-1 value, for cross-tool comparison when scales differ. * Computed as (value - min) / (max - min). */ normalized?: number; } /** A scale definition provides the vocabulary for assessments */ interface UPGScaleDefinition { /** Unique scale identifier */ id: string; /** Human-readable name */ label: string; /** What this scale measures */ description: string; /** Minimum value */ min: number; /** Maximum value */ max: number; /** Number of discrete points (undefined = continuous) */ steps?: number; /** Each point on the scale */ points: UPGScalePoint[]; /** * Friendly-word aliases that resolve to a canonical point `value` (UPG 0.11.1). * The single, introspectable source of truth so every writer that accepts a * friendly confidence word expands it to the SAME `confidence_5` value (and the * canonical point label), instead of each tool inventing its own mapping. e.g. * `{ low: 2, medium: 3, high: 4 }` — `high` is "Confident" (value 4), reserving * "Data-backed" (5) for genuinely quantified claims. Surfaced via `get_scale`. */ friendly_aliases?: Record; } /** A single point on an assessment scale */ interface UPGScalePoint { /** The numeric value */ value: number; /** The qualitative label the user sees */ label: string; /** Longer description */ description: string; } /** * All spec-defined assessment scales, keyed by scale_id. * * These are the canonical scales for UPG-native tools. External tools may * declare additional scales in the document's scale_extensions field. */ declare const UPG_SCALES: Record; /** * Look up a scale definition by its ID. * * Returns undefined for unknown IDs; callers should handle this case * gracefully (e.g. fall back to displaying raw value). * * @example * const scale = getScale('reach_5') * const point = scale?.points.find(p => p.value === assessment.value) */ declare function getScale(scaleId: string): UPGScaleDefinition | undefined; /** * Resolve a friendly word (e.g. `high`) to a canonical assessment on a scale, * using the scale's pinned `friendly_aliases` (UPG 0.11.1). The single source of * truth every writer must use, so `high` is always the same `confidence_5` value * with the canonical point label — no per-tool drift. Returns `null` when the * scale has no aliases or the word is not aliased. * * @example * friendlyToAssessment('confidence_5', 'high') * // => { value: 4, label: 'Confident', scale_id: 'confidence_5' } */ declare function friendlyToAssessment(scaleId: string, word: string): { value: number; label: string; scale_id: string; } | null; /** * Per-property scale overrides. * * Maps a property name to the scale that best captures its semantics. * Properties absent from this map fall back to `'scale_5'`, a generic * 1–5 ordinal. The mapping is keyed by property name only; the `entityType` * parameter on `getPropertyDefaultScale` is reserved for future per-entity * disambiguation. * * Sources: all `UPGAssessment`-typed properties across * `packages/upg-spec/src/properties/domains/` (audited at v0.4.0). */ declare const PROPERTY_SCALE_MAP: Record; /** * Per-ENTITY scale overrides (0.35.0). Beats `PROPERTY_SCALE_MAP` for the one * entity named, and only that entity. * * This activates the per-entity disambiguation `getPropertyDefaultScale` has * reserved since v0.4.0. It exists for a narrow, real class: a property name * that means one thing on most entities and its opposite on one. * * `risk.impact` is that case. `impact_5` is benefit-framed ("Minimal → * Transformative") and reads high-is-good, which on discovery and market * entities is correct — a high-impact opportunity is good news. On a `risk`, * the same word means severity of consequences, and a catastrophic risk was * rendering GREEN. `severity_5` ("Mild inconvenience → Blocker") is the * risk-shaped ladder and reads low-is-good, so the traffic-light story arrives * from the existing polarity map with no bespoke bucketing. * * `risk.probability` is the second entry, and it is the same instrument used * for the opposite purpose (Captain-ratified 2026-08-22). `probability` is * DEPRECATED on `risk` in favour of `likelihood`, and the name-level map holds * it at `confidence_5` for `forecast.probability`, which is a bare sales * percentage and genuinely belongs there. Leaving `risk.probability` on that * name-level entry meant the deprecated field and its replacement rendered on * DIFFERENT ladders for the whole deprecation window: the same stored 4 reading * "Confident" through the old name and "Likely" through the new one, on one * card, from one graph. A staged deprecation exists so a reader can migrate * WITHOUT their data changing meaning, and a ladder swap at the rename is * exactly that meaning changing. So the override pins the legacy spelling to * `likelihood_5` — the ladder the field always should have had — and the two * names agree until 1.0.0 drops the old one. * * Note what this does NOT do: `forecast.probability` is untouched and still * resolves to `confidence_5` through the name-level map. Being able to correct * ONE entity without disturbing the other is the entire reason this layer * exists, and this is the first time it has been used for that rather than for * a polarity fix. * * Keep this layer SMALL. A name that needs an override on three entities is a * name that should be split, not overridden; renaming `risk.probability` to * `likelihood` in the same release is that lesson applied. The two entries here * are both `risk`, both from one audit, and both close when 1.0.0 lands. */ declare const PROPERTY_SCALE_MAP_BY_ENTITY: Record>; /** * Return the default scale ID for a given entity-type / property-name pair. * * Resolution order (0.35.0 — the entity layer is new; the other two are not): * 1. `PROPERTY_SCALE_MAP_BY_ENTITY[entityType][propertyName]` — a per-entity * override, for a name that means something different on one entity. * 2. `PROPERTY_SCALE_MAP[propertyName]` — the property-level default. * 3. `'scale_5'` (generic 1–5 ordinal). * * Adding layer 1 is non-breaking: every pair with no entity override resolves * exactly as before. * * @param entityType - The UPG entity type string (e.g. `'problem_statement'`). * @param propertyName - The property name on that entity (e.g. `'severity'`). * @returns A scale ID string (always a key of `UPG_SCALES` or `'scale_5'`). * * @example * getPropertyDefaultScale('problem_statement', 'reach') // → 'reach_5' * getPropertyDefaultScale('problem_statement', 'frequency') // → 'frequency_5' * getPropertyDefaultScale('problem_statement', 'severity') // → 'severity_5' * getPropertyDefaultScale('risk', 'risk_level') // → 'severity_5' * getPropertyDefaultScale('risk', 'likelihood') // → 'likelihood_5' * getPropertyDefaultScale('risk', 'impact') // → 'severity_5' (entity override) * getPropertyDefaultScale('risk', 'probability') // → 'likelihood_5' (entity override; deprecated name, same ladder as `likelihood`) * getPropertyDefaultScale('forecast', 'probability') // → 'confidence_5' (unchanged) * getPropertyDefaultScale('opportunity', 'impact') // → 'impact_5' (unchanged) * getPropertyDefaultScale('anything', 'unknown_property') // → 'scale_5' */ declare function getPropertyDefaultScale(entityType: string, propertyName: string): string; /** * Inverse of `PROPERTY_SCALE_MAP`: the canonical property names that default to * a given scale, in declaration order. Useful for documentation surfaces that * want to show "where is this scale used". * * Returns an empty array for scales no property defaults to (e.g. the generic * `'scale_5'` fallback, which is never an explicit entry). * * Scope: the NAME-level map only. Per-entity overrides * (`PROPERTY_SCALE_MAP_BY_ENTITY`, 0.35.0) are deliberately not folded in, * because a bare property name cannot express "impact, but only on risk" and a * documentation surface that listed `impact` under both ladders would be * telling the reader less than it does now. Read that map directly when the * question is per-entity. * * @example * getPropertiesForScale('effort_5') // → ['effort', 'effort_estimate', 'effort_to_fix'] */ declare function getPropertiesForScale(scaleId: string): string[]; /** * Shared property primitives. Scalar aliases and string-literal unions used * across every domain property interface. Each type below carries its own * one-line declaration. * https://unifiedproductgraph.org/spec | MIT */ /** ISO 8601 date (e.g. "2026-06-01"). Communicates date-only intent. */ type ISODate = string; /** ISO 8601 datetime with timezone (e.g. "2026-06-01T09:00:00Z"). Communicates timestamp intent. */ type ISODateTime = string; /** * ISO 8601 duration string. * * Format: `PnYnMnDTnHnMnS` (year/month/day prefix P, time prefix T). * Common examples: * `P14D` : 14 days * `P3M` : 3 months * `PT2H30M` : 2 hours 30 minutes * `P1Y6M` : 1 year 6 months * * Used for retention periods, notice periods, agreement terms, lead times, * lookback windows, sprint durations, and any other measured time span. */ type Duration = string; /** * 5- or 6-field cron expression. * * Standard 5-field form: `minute hour day-of-month month day-of-week` * Extended 6-field form: `second minute hour day-of-month month day-of-week` * * Examples: * `'0 9 * * 1'` : every Monday at 09:00 * `'0 0 1 * *'` : first day of every month at midnight * `'*\/15 * * * *'` : every 15 minutes * * Used for scheduled data pipelines, recurring jobs, and automation triggers. */ type Cron = string; /** * Flexible, human-readable timeframe label. * * Accepted forms (not exhaustive; this is an open-shape string): * `'Q1 2026'` : calendar quarter * `'2026-H1'` : calendar half-year * `'FY2027 Q2'` : fiscal quarter * `'12-18 months'` : relative range * `'now'` / `'ongoing'` : open-ended * * Used to unify `timeline`, `time_horizon`, `timeframe`, `period`, and * `report_period` properties that share the same human-calendar semantics * but are too coarse for a precise ISO interval. */ type Timeframe = string; /** * ISO 4217 three-character alphabetic currency code. * * Examples: `'USD'`, `'EUR'`, `'GBP'`, `'JPY'`. * All characters must be ASCII uppercase letters (A–Z). * * Used wherever a property JSDoc says "ISO 4217": pricing tiers, contracts, * sales quotes, growth campaigns, and localisation settings. */ type ISO4217 = string; /** * Semantic version string following semver.org 2.0.0. * * Format: `MAJOR.MINOR.PATCH[-prerelease][+build]` * Examples: * `'1.0.0'` * `'0.3.1'` * `'2.0.0-beta.1'` * `'1.4.0+build.42'` * * Used for software version fields across `engineering.ts`, `ai.ts`, * `content.ts`, `legal.ts`, `product-spec.ts`, and `accessibility.ts`. */ type Semver = string; /** Task or strategic priority level */ type Priority = 'urgent' | 'high' | 'medium' | 'low' | 'none'; /** Traffic-light health status for initiatives, features, and delivery */ type HealthStatus = 'on_track' | 'at_risk' | 'off_track'; /** * How strict a rule is. Used by constraint entities, design guidelines, * security policies, and accessibility standards to classify their * imperative force. Pairs with `UPG_ENUM_SCALES.RuleStrength` for * per-value labels and descriptions. * * Members (strict → permissive): * `'must'` : hard requirement, violation blocks * `'must_not'` : hard prohibition, violation blocks * `'exception'`: documented carve-out from a must / must_not * `'warning'` : soft signal, should consider, can override with justification * `'guideline'`: recommendation, encouraged, not enforced */ type RuleStrength = 'must' | 'must_not' | 'exception' | 'warning' | 'guideline'; /** * Confidence level for assumptions, evidence, and feasibility. * * @deprecated since v0.8.0. Confidence is now expressed as a structured * `UPGAssessment` on the numeric `confidence_5` scale (UPG-579 Option C), so * the qualitative label and a computable/comparable value travel together. * Migrate `confidence: Confidence` to `confidence: UPGAssessment` with * `scale_id: 'confidence_5'` (suggested value map: `high` -> 5, `medium` -> 3, * `low` -> 1). The enum is retained only for back-compat reads. */ type Confidence = 'high' | 'medium' | 'low'; /** * Three-point ordinal scale: low / medium / high. * * @deprecated since v0.8.0. Alias of the deprecated `Confidence`; was used by * zero properties. For magnitude/degree judgments use a named enum scale or a * `UPGAssessment`. Retained only for back-compat. */ type LowMedHigh = Confidence; /** * Maturity of a root-cause determination during incident debugging. * * Used by `engineering.ts` (`root_cause.cause_confidence`). An epistemic * *maturity ladder*, distinct from `Confidence`'s level vocabulary. Pairs * with `UPG_ENUM_SCALES.CauseConfidence` for per-value labels. * * Members (tentative → certain): * `'hypothesised'` : a proposed cause, not yet tested * `'likely'` : supported by evidence but not conclusively proven * `'confirmed'` : verified as the cause */ type CauseConfidence = 'hypothesised' | 'likely' | 'confirmed'; /** * A person's comfort level with a tool, technology, or practice. * * Used by `users.ts` (`persona.tech_comfort`). Pairs with * `UPG_ENUM_SCALES.ComfortLevel` for per-value labels and descriptions. * * Members (low → expert, plus an escape hatch): * `'low'` : little or no familiarity * `'medium'` : functional, everyday competence * `'high'` : confident, fluent use * `'expert'` : deep mastery; can teach or extend * `'other'` : a comfort profile not captured by the above tiers */ type ComfortLevel = 'low' | 'medium' | 'high' | 'expert' | 'other'; /** * Operational log / alert verbosity level. Distinct from user-impact * `severity_5` (UPG-579 Option 0): this classifies how loud a signal should * be, not how bad an outcome is for the user. * * Used by `devops.ts` (`monitor.severity`). Pairs with * `UPG_ENUM_SCALES.LogLevel` for per-value labels and descriptions. * * Members (loudest → quietest): * `'critical'` : page someone now; service-affecting * `'warning'` : needs attention soon, not yet service-affecting * `'info'` : informational; no action required */ type LogLevel = 'critical' | 'warning' | 'info'; /** * Incident severity tier (paging classification). Distinct from user-impact * `severity_5` and from `LogLevel` (UPG-579 Option 0): this is the ops-side * blast-radius tier used to drive escalation and response process. * * Used by `devops.ts` (`incident.severity_level`). Pairs with * `UPG_ENUM_SCALES.IncidentSeverity` for per-value labels and descriptions. * * Members (most severe -> least): * `'sev1'` : critical outage; full response, exec-visible * `'sev2'` : major degradation; urgent response * `'sev3'` : minor/partial impact; handled in hours * `'sev4'` : negligible impact; routine handling */ type IncidentSeverity = 'sev1' | 'sev2' | 'sev3' | 'sev4'; /** * Urgency level of an inbound signal (customer, support, or market). * * Used across `feedback.ts` and `customer-success.ts` wherever * `signal_urgency` is declared. Extracted to eliminate four identical * inline redeclarations. */ type SignalUrgency = 'low' | 'medium' | 'high' | 'critical'; /** * Sentiment polarity of an inbound signal. * * Used across `feedback.ts` and `customer-success.ts` wherever * `signal_sentiment` is declared. Member order: positive → neutral → * negative → mixed (valence-first). */ type SignalSentiment = 'positive' | 'neutral' | 'negative' | 'mixed'; /** * Capability/process maturity on the CMMI ladder. Used by `strategy.ts` * (`maturity_level`, `target_maturity`). Pairs with `UPG_ENUM_SCALES.MaturityLevel`. */ type MaturityLevel = 'initial' | 'developing' | 'defined' | 'managed' | 'optimizing'; /** * WCAG conformance level. Used by `accessibility.ts` (`conformance_level`, * `level`). Pairs with `UPG_ENUM_SCALES.ConformanceLevel`. */ type ConformanceLevel = 'A' | 'AA' | 'AAA'; /** * Data classification / sensitivity tier. Used by `data.ts` (`sensitivity`) * and `security.ts` (`level`). Pairs with `UPG_ENUM_SCALES.DataSensitivity`. */ type DataSensitivity = 'public' | 'internal' | 'confidential' | 'restricted'; /** * Learning difficulty tier. Used by `education.ts` (`difficulty`, * `path_difficulty`). Pairs with `UPG_ENUM_SCALES.DifficultyLevel`. */ type DifficultyLevel = 'beginner' | 'intermediate' | 'advanced'; /** * Qualitative frequency rating (distinct from the numeric `frequency_5` scale * and from `Cadence`'s calendar tiers). Used by `frequency_rating` across * gtm/business-model/engineering/content/customer-success. Pairs with * `UPG_ENUM_SCALES.FrequencyRating`. */ type FrequencyRating = 'constant' | 'regular' | 'occasional' | 'rare' | 'other'; /** * Direction of evidence relative to a claim (supports / refutes / neutral). * Distinct from `confidence_impact` (strengthens/weakens) per the UPG-579 * polysemy verdicts. Used by `validation.ts` (`direction`, `result_direction`). * Pairs with `UPG_ENUM_SCALES.EvidenceDirection`. */ type EvidenceDirection = 'supports' | 'refutes' | 'neutral'; /** * Channel through which an inbound signal was received. * * Current call sites declare `signal_channel` as plain `string`; this enum * provides the canonical closed set for migration. * * Members: * `'analytics_event'` : first-party product analytics event (PostHog, Amplitude, etc.) * `'community'` : forum, Discord, Slack community, etc. * `'email'` : direct email or newsletter reply * `'in_app'` : in-product feedback widget, NPS prompt, or beta-feedback panel * `'interview'` : user or sales interview * `'other'` : catch-all for unlisted channels * `'review'` : app store or G2/Capterra review * `'sales_call'` : sales demo or discovery call * `'social'` : social media mention or DM * `'support_ticket'` : helpdesk or in-app support ticket * `'survey'` : NPS, CSAT, or custom survey * `'usage_session'` : observed product session (telemetry-derived friction signal) */ type SignalChannel = 'analytics_event' | 'community' | 'email' | 'in_app' | 'interview' | 'other' | 'review' | 'sales_call' | 'social' | 'support_ticket' | 'survey' | 'usage_session'; /** * Canonical cadence primitive for recurring activities, publications, and * measurement schedules. * * Closed enum on the qualitative axis "how often does this recur?". Pairs * with the `frequency_count` + `frequency_period` (ISO-8601 `Duration`) * numeric pair when authors need an exact rate ("3 times per `P7D`") * rather than a labelled tier. * * Members (coarse → fine): * `'continuous'` : always running, no discrete recurrence * `'hourly'` : recurs every hour * `'daily'` : recurs every day * `'weekly'` : recurs every week * `'monthly'` : recurs every month * `'quarterly'` : recurs every calendar quarter * `'yearly'` : recurs every year * `'on_demand'` : triggered by event, not schedule * `'other'` : recurs on a cadence not captured by the above tiers * * Introduced in v0.4.0 as the canonical replacement for ad-hoc * `frequency?: string` and `*cadence?: string` properties (Concern 2 axis * canonicalisation). Supersedes the domain-local `MetricFrequency` * type in `domains/metrics.ts` which is now an alias. */ type Cadence = 'continuous' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly' | 'on_demand' | 'other'; /** * Communication protocol for API contracts and data flows. * * Normalised to lowercase. Supersedes two divergent inline enums in * `engineering.ts`: * - `ApiContract.protocol` used mixed-case: `'REST' | 'GraphQL' | 'gRPC' | 'AsyncAPI' | 'SOAP' | 'WebSocket' | 'MQTT' | 'other'` * - `DataFlow.protocol` used lowercase: `'rest' | 'graphql' | 'grpc' | 'event' | 'webhook' | 'file'` * * This primitive is the lowercase superset of both; call-site migration * will normalise casing in a follow-up pass. * * Members (alphabetical): * `'asyncapi'` : AsyncAPI / event-driven API spec * `'event'` : generic event/message bus * `'file'` : file-based transfer (SFTP, S3, etc.) * `'graphql'` : GraphQL over HTTP * `'grpc'` : gRPC / Protocol Buffers * `'mqtt'` : MQTT (IoT / lightweight pub-sub) * `'other'` : unlisted protocol * `'rest'` : REST over HTTP * `'soap'` : SOAP / XML web services * `'webhook'` : HTTP webhook (push callback) * `'websocket'` : WebSocket bidirectional stream */ type Protocol = 'asyncapi' | 'event' | 'file' | 'graphql' | 'grpc' | 'mqtt' | 'other' | 'rest' | 'soap' | 'webhook' | 'websocket'; /** * Marketing and advertising platform identifier. * * Supersedes two overlapping inline enums in `marketing.ts`: * - `SocialPost.platform`: `'twitter' | 'linkedin' | 'instagram' | 'youtube' | 'tiktok' | 'other'` * - `AdCreative.platform`: `'google' | 'meta' | 'linkedin' | 'twitter' | 'other'` * * This primitive is the alphabetical superset of both. * * Members: * `'discord'` : Discord community / server * `'email'` : newsletter / email marketing platform * `'google'` : Google Ads / Search / Display * `'instagram'` : Instagram organic or paid * `'linkedin'` : LinkedIn organic or paid * `'meta'` : Meta Ads (Facebook/Instagram ad platform) * `'other'` : unlisted platform * `'podcast'` : podcast advertising or distribution * `'reddit'` : Reddit organic or paid * `'tiktok'` : TikTok organic or paid * `'twitter'` : Twitter / X organic or paid * `'youtube'` : YouTube organic or paid */ type MarketingPlatform = 'discord' | 'email' | 'google' | 'instagram' | 'linkedin' | 'meta' | 'other' | 'podcast' | 'reddit' | 'tiktok' | 'twitter' | 'youtube'; /** * UPG Base Node and shared primitives. Every node extends `UPGBaseNode`. * https://unifiedproductgraph.org/spec | MIT */ /** Confidence level for a type mapping when importing from an external tool. * - `high`: unambiguous match (e.g. exact type string match) * - `medium`: probable match (e.g. semantic similarity) * - `low`: speculative match, human review recommended * - `manual`: mapping was set explicitly by a human */ type UPGMappingConfidence = 'high' | 'medium' | 'low' | 'manual'; /** One external link beyond the canonical artifact. * * Generalises the `ServiceProperties.links` shape, which was the only * list-of-links declaration in the spec before 0.33.0, up to the base node so * that any node can hold more than one outward pointer. */ interface UPGExternalLink { /** URI. Same posture as `UPGBaseNode.external_ref`: https:// for cloud tools, * file:// or a relative path for local files. */ url: string; /** Human label for the link. */ label?: string; /** What kind of thing is on the other end, e.g. "pull_request", "design", * "runbook". A free string with a documented convention rather than an enum: * the set is open by nature and a closed one would be wrong within a release. */ kind?: string; } /** The structural base shared by every node in a product graph. * * All entity types extend this interface, either directly (using `properties` * as `Record`) or via the typed `UPGNode` wrapper, which * narrows `properties` to the correct interface from `UPGPropertyMap`. * * Core identity fields (`id`, `type`, `title`) are required. * Everything else is optional to keep the format lightweight. * * @example * // A minimal persona node, only required fields populated. * const persona: UPGBaseNode = { * id: 'n_persona_1', * type: 'persona', * title: 'Head of Product at a B2B SaaS scale-up', * } * * @example * // An imported node with mapping metadata + type-specific properties. * const importedPersona: UPGBaseNode = { * id: 'n_persona_2', * type: 'persona', * title: 'Solo founder, non-technical', * description: 'Operator who ships with AI and needs thinking tools to keep up.', * tags: ['primary', 'launch-audience'], * source_id: 'notion_page_abc123', * source_type: 'customer_archetype', * mapping_confidence: 'high', * external_tool: 'notion', * external_ref: 'https://notion.so/acme/abc123', * properties: { * is_primary: true, * experience_level: 'intermediate', * }, * } */ interface UPGBaseNode { /** Unique identifier within the graph */ id: string; /** The UPG entity type (must be a value from UPGEntityType) */ type: UPGEntityType; /** Human-readable title */ title: string; /** * Stable, human-readable handle for inline `[[type:slug]]` chips in * `.upg.md` documents. Auto-generated from `title` when omitted; unique * within `(product_id, type)`. The `id` field remains the canonical * identifier for adapters, MCP tools, and cross-product edges. Resolvers * MUST accept either form (UUID `id` OR slug) when matching chips. */ slug?: string; /** * Past values of `slug`, retained when the slug is renamed so existing * `.upg.md` chips that reference the old slug still resolve. The set * (slug ∪ aliases) is unique within `(product_id, type)`. Order is * preservation-only; resolvers treat aliases as a flat lookup set. */ aliases?: string[]; /** * Stable, human-citable key minted for this node (e.g. `"LTN-311"`). Unique * within the product ACROSS entity types, immutable once assigned, and never * reused. Distinct from `slug`: a key is minted, not derived from the title. * * @remarks * WHY NOT `slug`. A slug is unique within `(product_id, type)` and is * auto-generated from `title` when omitted. A citable key is neither: under * slug's scope a `task` and a `bug` could each legally hold `LTN-311`, and a * key that followed a retitle would break every citation that made it worth * having. The two fields answer different questions and both are optional. * * WHY NOT `external_id`. That field records the identifier a node had in the * tool it came FROM. It is the right home for an imported key's provenance * and is structurally incapable of naming the next node, because there is no * external tool to mint it. A graph that outlives its source tool needs a key * of its own. * * MINTING. The next number is `max(existing) + 1`, derived from the graph. No * counter is serialised, because a counter is store state rather than a fact * about the thing (the same cut that keeps `composition.rev`, which is a fact, * and excludes a concurrency token, which is not). * * WHICH PREFIX (normative; ladder stated as built in 0.33.1, candidate set * narrowed in 0.34.0). Three rungs, tried in order. * * 1. A create NAMES its prefix explicitly. * 2. Otherwise the CANDIDATE SET is consulted. It is the UNION of the prefixes * DECLARED by the product's teams (`team.key_prefix`) and the prefixes * OBSERVED on the product's existing keys. One candidate resolves; more * than one, and the create surface asks. `product.key_prefix` is consulted * only when no team declares one, and is `@deprecated` for that reason. * 3. A product with an empty candidate set and no explicit prefix mints no * keys. * * THE OBSERVED RUNG IS A REAL MINT PATH, and 0.33.0 published two sentences that * disagreed about whether it existed. This paragraph used to end "A product where * nothing declares a prefix mints no keys", which deletes it; the two paragraphs * below both name a prefix INFERRED from existing keys, and `team.key_prefix` * rule 3 presupposes it by construction. The field settles it: the only keyed * graph in the estate carries 1,032 keys minted by inference in a product that * declares no prefix at all, and under the deleted rung that graph could not * exist. Corrected in the 0.33.1 patch and restated here. * * A DECLARATION ADDS A CANDIDATE; IT DOES NOT REMOVE ONE (0.34.0). Declaring a * team prefix suppresses `product.key_prefix` and nothing else. It never * suppresses an OBSERVED prefix, which is evidence of a namespace already in * use. The full argument, the measurement behind it and the reason it ships as a * change rather than as a patch are on `team.key_prefix`. * * Key uniqueness is scoped to the product across entity types; a prefix names a * team within that scope, not a separate number line. * * ENFORCED WITHIN THE PRODUCT (0.34.1). A create naming a key another node in * this product already holds is REFUSED, across entity types, with a typed * `DuplicateNodeKeyError`. Immutability was already enforced on the update * path from 0.33.0, through the typed field and through `properties` both. * * THE REFUSAL IS AN ERROR AND NOT A SILENT NO-KEY, and the distinction is the * one the minting rules draw two paragraphs down. A portfolio-shared team's * prefix mints nothing in a second product, and there the refusal IS no-key * and never an exception — because nobody asked for that key; it was inferred. * A duplicate arrives the other way round: the caller NAMED it. Returning * success for a node whose citation was quietly dropped is the failure mode * the 0.32.2 field guards exist to end. * * ENFORCED ON CREATE, NOT ON OPEN. A graph minted before the check existed may * hold a collision, and a store that refuses to load it leaves its owner no * way to repair it. The index is therefore built first-wins at load and the * invariant is held at the only moment it can still be honoured. * * THE PRECEDING TWO PARAGRAPHS SHIPPED AS AN OVERCLAIM AND ARE CORRECTED HERE. * 0.33.0 wrote "uniqueness is enforced per product (the store index is * `(product_id, key)`)" as settled fact, and 0.34.0 restated the within-product * case as stated-but-unchecked without reconciling the two. Neither was * describing anything that ran: no index existed and no check fired, on any * release through 0.34.0. It was measured on the only keyed graph in the * estate — 1,032 keys, and a second node claiming one of them was accepted * without a warning. This matters more than a wrong sentence usually would, * because the portfolio detector below names the per-product invariant as its * premise: a portfolio check that assumes each product is internally clean was * resting on nothing. 0.34.1 makes the sentence true rather than deleting it. * * PORTFOLIO-WIDE UNIQUENESS IS DEFINED AND UNENFORCED (0.33.0). Within a * portfolio, one `(prefix, number)` pair should identify one node. Uniqueness * is enforced per product (the store index is `(product_id, key)`, and from * 0.34.1 that index exists), so two products minting under one prefix produce * the same citation for two different things. This is measured rather than * feared: a fixture reproduces the * collision through the ordinary create path, including the quiet case where * nobody declares a prefix and it is inferred from existing keys. No PER-PRODUCT * check can ever see it, because each product reports its own key as valid — * which is why the detector that ships in 0.34.0 is a PORTFOLIO-scope * anti-pattern (`duplicate-key-across-products`) rather than a graph validation. * It is ENFORCED from the day it ships; what was staged is the corpus, and the * distinction matters because there is no defined-then-enforced mechanic and * inventing one would have been worse than either real option. A detector that is * registered but declines to fire is a detector nobody can reason about. * * MINTING IS PRODUCT-SCOPED (normative, 0.33.0). The `(product_id, key)` index * is the enforced invariant and the scope of a key sequence is the product. * `team` is `portfolio_shared`, so one team can be referenced from two * products; minting does not travel with it. A portfolio-shared team's prefix * is NOT a minting candidate in a second product, refusal is NO-KEY and never * an exception, and the rule applies on the MINT path including a prefix that * was INFERRED from existing keys rather than requested by any caller. * Portfolio-shared team minting is deferred until a supra-product uniqueness * design exists. See `team.key_prefix` for the rule in full, including the three * mechanics 0.34.0 made normative that this rule needs and 0.33.0 left open: HOW * a minter decides which product a prefix belongs to (evidence, not a stored * marker), what happens when nothing can decide (NEITHER mints, no invented * tiebreak), and what "in scope" means (engine-defined, with a floor at every * product the engine can enumerate for this caller and a ceiling at every product * the caller could not otherwise read). * * WHERE THE BEHAVIOUR LIVES, because this is a contract and not an * implementation. Nothing in this package mints a key. Deriving the next number * requires reading every keyed node in the product, and that read must page * explicitly, so the minter is the graph service and the agent surface accepts * a caller-supplied key on create without ever deriving one. * * @example "LTN-311" */ key?: string; /** Optional narrative description */ description?: string; /** Freeform tags for filtering and grouping */ tags?: string[]; /** Current lifecycle phase (must be a phase ID from getLifecycleForType()). * E.g., for hypothesis: 'untested' | 'testing' | 'resolved'. * Entity types without a lifecycle definition should omit this field. * Validated at runtime against UPG_ALL_PHASES_SET. */ status?: string; /** * Swept out of default views. ORTHOGONAL to `status`: a node can be done and * live, or done and archived, and those are different facts. Archived nodes * remain fully queryable. * * @remarks * WHY THIS IS NOT A LIFECYCLE PHASE. Several lifecycles carry an `archived` * phase, and in every one of them it is `status_category: 'completed'` — so in * the six-bucket read, archived and done are indistinguishable. Field data * settles it: a real 1,032-issue tracker held 559 archived-Done items * alongside 18 live-Done ones. One field cannot carry two facts, and a bucket * system that collapses them cannot answer the question it exists for. * * THE DEFAULT-READ CONVENTION, documented and NOT enforced: archived nodes are * excluded from default views and included on request (`UPGViewQuery * .include_archived`). A consumer that shows them by default is doing * something unusual, not something wrong, so no check fires on it. * * Generalised at 0.32.0 from `WorkspaceProperties.archived`, which shipped the * same pair for one type and is now `@deprecated` in favour of this field. * The existing `archived` LIFECYCLE PHASES are deliberately untouched; * reconciling them is its own cycle. */ archived?: boolean; /** ISO timestamp archived. Pairs with `archived === true`. */ archived_at?: ISODateTime; /** Original ID in the source tool (for round-trip fidelity) */ source_id?: string; /** Original type name in the source tool */ source_type?: string; /** Confidence level of the type mapping */ mapping_confidence?: UPGMappingConfidence; /** External tool that holds the canonical artifact (e.g. "figma", "linear", "notion") */ external_tool?: string; /** URI to the canonical artifact: https:// for cloud tools, file:// or relative path for local files */ external_ref?: string; /** Identifier in the external tool's system (for sync / round-trip) */ external_id?: string; /** * Additional external links. `external_ref` names THE canonical artifact and * stays the single answer to "where does this live"; this list holds * everything else that points outward. A node may carry this with no * `external_ref` when no single link is canonical. * * @remarks * WHY A LIST AND NOT AN ENTITY. A measured tracker import produced 703 * attachments across 1,032 issues: 524 fitted on `external_ref` and 162 * overflowed into a vendor-namespaced property bag key with nowhere declared to * go. A link has no lifecycle, no owner, no description and no independent * identity, so minting a `link` type for it would pay roughly fourteen * registration points for a scalar fact about a node. * * WHY NOT `external_refs`. That name is one character from `external_ref` and * means the opposite thing (all the others, versus the canonical one). Two * fields whose names differ by a plural and whose semantics differ by * canonicality is a misreading waiting to happen in every consumer. * * MIGRATION NOTE, CORRECTED 0.34.0 ON TWO FIGURES. Both were carried forward * from the 0.33.0 pass and both were wrong, and the second would have broken * this field's own contract on its first real population. * * THE RESIDUE IS 179, NOT 162. Seventeen further attachments sat in a second * vendor bag key and were equally homeless. They were never counted because * the census read one key. * * THE CANONICAL URL MUST BE SUBTRACTED BEFORE THE COPY. 524 of the 528 nodes * holding a parked bag DUPLICATE their own `external_ref` inside it. A * straight copy of the bag into this list would therefore put the canonical * artifact into the NON-canonical list on 99% of the field's first real * population, breaking the exact invariant this field was minted to * establish. A migration reads the bag, removes the entry matching * `external_ref`, and copies what remains. * * The correction is recorded here rather than in a plan because this is where a * migration author will be standing when they need it. * * @example * [{ url: 'https://github.com/acme/api/pull/812', label: 'PR 812', kind: 'pull_request' }] */ external_links?: UPGExternalLink[]; /** * When the node was created. Store metadata lifted into the spec at 0.33.0 so * that a declared view query can express a window over it. * * @volatile * @remarks * TAGGED `@volatile` DELIBERATELY. These two timestamps are maintained by * whatever store holds the graph rather than authored, so they are not design * knowledge and a reader must not treat them as stable facts about the thing. * They are declared anyway because `UPGViewClause` can open a `date` window and * two of the six date dimensions a real board filters on are these; leaving them * undeclared would ship a query language that cannot say what the surface most * often says, which is the failure the clause list exists to fix. */ created_at?: ISODateTime; /** * When the node was last modified. Store metadata, same posture as * `created_at`. * * @volatile */ updated_at?: ISODateTime; /** * Type-specific properties. * * @remarks * NAMESPACED EXTENSION KEYS (the 0.31.0 rule, extended to this bag at 0.33.0). * A key that a tool owns and the spec does not declare is written * `:`, with a colon. An underscore key such as * `linear_state_history` is indistinguishable from a misspelled spec property: * no migration can target it and no validator can tell it apart from a typo, * and that undetectability is the whole reason the rule exists. The rule was * written for `WorkspaceCanvas` and applies here for the same reason. * * ONE UNDECLARED EXTENSION IS KNOWN, MEASURED AND DELIBERATELY LEFT UNDECLARED. * A tracker import carries raw ordered state-transition history for every issue * (1,032 issues, 2,862 transitions in the measured corpus) under a vendor-owned * key. It has no declared shape because nothing reads it: declaring a shape for * records nobody queries is dead schema, and it would freeze one vendor's * transition model into the format before a second source has been seen. The * pull-forward condition is the first reader. Silence and a decision look * identical six months later, so this is written down as a decision. * * THAT KEY IS NOT ACTUALLY NAMESPACED, AND THE PARAGRAPH ABOVE USED TO CLAIM IT * WAS (corrected 0.34.1). The rule two paragraphs up mandates `:` * with a COLON. The import writes `linear_state_history`, with an underscore — * the precise shape the rule was written to forbid, cited here as the example * of compliance. It went unnoticed because nothing could see it: no drift class * examined undeclared bag keys until `undeclared_property_drift` shipped in * 0.34.1, which measured 5,779 such keys across eight `linear_*` shapes on the * one imported graph in the estate. * * Recorded rather than migrated, because renaming a key on 1,032 nodes is a * migration with its own cycle and this paragraph is where its author will be * standing. What changes here is the claim, not the data. */ properties?: Record; } /** * Every top-level field declared by `UPGBaseNode`, as runtime data. * * This is the SINGLE SOURCE for "what is a base-node field". Consumers that * classify top-level keys (drift detectors, patch mergers, serialisers) must * derive from this rather than enumerate their own list. * * Order follows the interface declaration order and is not significant; callers * that need a canonical serialisation order use `NODE_KEY_ORDER` in * `format/canonical.ts`, which is deliberately a SUPERSET (it also orders * tolerated non-base keys such as `lifecycle_status` and `sort_order`). */ declare const UPG_BASE_NODE_FIELDS: readonly (keyof UPGBaseNode)[]; /** `UPG_BASE_NODE_FIELDS` as a lookup set, for membership tests. */ declare const UPG_BASE_NODE_FIELD_SET: ReadonlySet; /** * Base-node fields that `id` aside are NOT freely mergeable by a generic * shallow patch, because each needs its own handling: * * - `id` identity; changing it would orphan every edge. * - `type` narrowed to `UPGEntityType`, and a type change carries a * property migration, so it routes through a migration path. * - `slug` a change rotates the old value into `aliases` (see * `rotateSlug`), so it is never a plain assignment. * - `aliases` replaced outright when patched directly, which must happen * AFTER the slug rotation that would otherwise append to it. * - `properties` deep-merged rather than replaced. * * Exported so a merger can state the exclusions once and derive the rest. */ declare const UPG_BASE_NODE_SPECIAL_MERGE_FIELDS: ReadonlySet; /** * UPG Property Registry: Runtime schema for entity types * * AUTO-GENERATED by scripts/generate-property-registry.ts * Do not edit manually. Run: npx tsx scripts/generate-property-registry.ts * * Sources: property types / enums / descriptions come from the domain * interfaces in src/properties/domains/*.ts (their JSDoc); the `modifier` * provenance annotations (derived / snapshot / volatile, property-fit audit) * come from the curated src/properties/property-modifier-overlay.ts. Both are * re-emitted here, so a regeneration reproduces this file exactly (no * wall-clock timestamp: generation is deterministic). This file is a GATED * artifact — check:generated re-runs the generator and fails on any drift, so * edit the sources above, never this file. * Entity types with properties: 327 */ interface PropertyDefinition { type: 'string' | 'number' | 'boolean' | 'string[]' | 'object' | 'object[]' | 'assessment'; /** * The CONTRACT: what the property means and the semantics a caller must * not get wrong. Two or three sentences. This is what a hover card, a * tooltip and a token-conscious agent read. */ description?: string; /** * The LONGFORM half (`@remarks` in the source JSDoc): rationale, edge * cases, workflow recipes, design history. The reference page renders it * behind a disclosure; hovers and schema summaries leave it out. * * Semantics MOVE here, they are never deleted. A field that stops being * findable is worse than a field that is long, which is the lesson a * reporter taught by concluding a capability did not exist when it was * documented somewhere they did not read. */ notes?: string; enum?: string[]; /** For 'assessment'-typed fields: the canonical UPG scale this property is rated on (e.g. 'confidence_5'). */ scale_id?: string; /** For object/assessment fields: nested property shapes. */ properties?: Record; /** For object/assessment fields: required keys within `properties`. */ required?: string[]; /** * Provenance / volatility modifier (property-fit audit, 2026-06-16). Marks a property * whose value is not authored-and-stable, so tooling (`validate_graph`, renderers, export) * can treat it accordingly: * - `'derived'` — computed from edges/children at read-time; never hand-authored. * `validate_graph` flags a stored value that contradicts the graph. * - `'snapshot'` — a stale-stamped cache of a live reading; SHOULD pair with a `*_as_of` * timestamp. Definition entities carry live state only as a snapshot. * - `'volatile'` — an environment-specific pointer (URL / path / id) that may rot or be * stripped on export; not portable modeling knowledge (open-standard * data-boundary ADR). Enforcement lands in `validate_graph` (mcp-server). * Sourced from property-modifier-overlay.ts. */ modifier?: 'derived' | 'snapshot' | 'volatile'; } type PropertySchema = Record; /** * Runtime property schemas for all entity types with typed properties. * Entity types not listed here have no typed properties (just title/description/status/tags). */ declare const UPG_PROPERTY_SCHEMA: Record; /** * Get the property schema for an entity type. * Returns undefined if the type has no typed properties. */ declare function getPropertySchema(entityType: string): PropertySchema | undefined; /** * UPG Edge Catalog. Maps each canonical edge type to its verb pair and classification. * https://unifiedproductgraph.org/spec | MIT */ /** * Wildcard sentinel for `source_type` / `target_type` on polymorphic edges. * Matches any node. Polymorphic edges must be registered in * `UPG_POLYMORPHIC_EDGE_KEYS`. See `src/ARCHITECTURE.md`, "Polymorphic Edges". */ declare const UPG_WILDCARD_ENDPOINT: "node"; type UPGWildcardEndpoint = typeof UPG_WILDCARD_ENDPOINT; /** * The shape of a single canonical edge in `UPG_EDGE_CATALOG`. * * @example * const personaPursuesJob: UPGEdgeDefinition = { * forward_verb: 'pursues', * reverse_verb: 'pursued_by', * classification: 'semantic', * source_type: 'persona', * target_type: 'job', * } */ interface UPGEdgeDefinition { /** Active voice verb: "source [forward_verb] target". */ forward_verb: string; /** Reverse reading: "target [reverse_verb] source". */ reverse_verb: string; /** Structural classification of this edge */ classification: 'hierarchy' | 'causal' | 'semantic' | 'cross-domain'; /** Source entity type. `UPGEntityType` or `'node'` (polymorphic wildcard). */ source_type: string; /** Target entity type. `UPGEntityType` or `'node'` (polymorphic wildcard). */ target_type: string; /** * Opt-in to the gated edge-property model. When `true`, instances of this * edge type MAY carry `properties` (validated against the relevant schema); * when absent/false, validators reject any `properties` on the edge. This * keeps plain semantic edges payload-free while letting a small, deliberate * set of edges (currently `framework_exercise_includes_node`) hold a value * that belongs to the relationship rather than to either endpoint. */ carries_properties?: boolean; /** * For `carries_properties` edges: the typed shape of the `properties` bag, * keyed by property name (same `PropertyDefinition` shape entity properties * use). When present, the writers reject unknown property keys and validators * range-check typed values (e.g. an `assessment` against its `scale_id`); * when absent, a `carries_properties` edge accepts an unvalidated bag (the * pre-0.10.4 behaviour, still used by `feature_rivals_competitor_feature`). * Discoverable via `get_edge_type`. */ property_schema?: PropertySchema; /** * Dual-registration marker (0.17.3). When `true`, this within-graph catalog edge * is ALSO a valid cross-product edge: its endpoints can legitimately live in * DIFFERENT graphs within a portfolio (a connective, rollup-laddering, reference, * org-ownership, or competitive-intel relationship), so `batch_create_cross_product_edges` * admits it across files. This is the single source of truth for the cross-product * whitelist: `UPGCrossEdgeType` (the union) and `UPG_CROSS_EDGE_TYPES` (the runtime * list) both derive the dual-registered half from this flag, so flagging one edge * here is the only edit needed to admit it. NOT for product-spine or org-spine * containment, whose endpoints must co-reside in one graph. Portfolio-native edges * that only ever exist across products (shares_*, depends_on_product, instance_of, * rolls_up_to, the area and foundation edges) have no within-graph catalog entry and * live in `UPG_CROSS_ONLY_EDGE_TYPES` instead. Discoverable via `get_edge_type`. */ cross_product_eligible?: true; /** * Deliberate-only marker (0.17.4). When `true`, this edge carries a meaning that * is a deliberate authoring act, NOT a relationship that can be inferred from a * source's hierarchy or a generic parent nesting. `objective_defers_feature` / * `objective_defers_capability` mean an objective explicitly PARKS a * feature/capability out of scope, the opposite of the "this child contributes to * this parent" link a nesting implies. Generic-inference chokepoints * (`inferEdgeTypeWithTier` in auto-nest mode, the adapter parentage resolvers) * skip these, declining to auto-materialise them so the write path falls back to a * `node_informs_node` link or a decline+warn. Explicit resolution * (`resolve_edge_for_pair`, `create_edge`) is UNAFFECTED — the edge is authored on * request. Single source of truth: `UPG_DELIBERATE_ONLY_EDGE_TYPES` (the runtime * list) and `isDeliberateOnlyEdge` derive from this flag, so flagging one edge here * is the only edit needed. Discoverable via `get_edge_type`. */ deliberate_only?: true; } /** * Property schema carried by the two classification cross-edges * (`competitor_classified_as_classification_value` and its polymorphic sibling * `node_classified_as_classification_value`). Identical for both: the metadata a * classification carries does not depend on what is being classified. All keys * are optional (back-compat: the 218 existing classification edges carry none); * the only conditional requirement is internal to `confidence` (value + label), * mirroring the `competitor.confidence` entity precedent. 0.10.4. */ declare const CLASSIFICATION_EDGE_PROPERTY_SCHEMA: PropertySchema; /** * Property schema carried by the two defer edges (`objective_defers_feature` * and `objective_defers_capability`, 0.17.4). A single freeform `deferred_to` * key names the period the parked work is deferred to. Freeform on purpose, * mirroring `strategic_theme.time_horizon` ("Q1 2026", "FY26"): a deferral * target is a planning label, not a typed date, and can name a quarter, a * release, or a coarser horizon. Optional (a defer edge is meaningful without * a stated period); the writers reject any other key. */ declare const DEFER_EDGE_PROPERTY_SCHEMA: PropertySchema; /** * Property schema carried by `surface_varies_by_configuration_axis` (0.30.0). * * CONDITIONAL EXISTENCE, expressed as an edge. The surface exists in the stored * graph regardless; this edge says which members of the configuration family it * appears in. No edge at all means invariant, which is why every graph written * before this existed keeps meaning exactly what it meant. */ declare const CONFIGURATION_VARIANCE_EDGE_PROPERTY_SCHEMA: PropertySchema; /** * Property schema carried by the two surface-composition edges * (`surface_contains_surface` and `feature_occupies_surface`), 0.30.0. * * A single `active_when` key qualifies the relationship by a configuration * axis: the edge holds only under the named values, and is absent from every * other projection. * * SCOPE IS THE POINT, NOT AN OVERSIGHT. These two edges are the whole legal * surface for the qualifier, and `validate_graph` rejects `active_when` on any * other edge type. UPG is not adding a general modality system on one field * report; it is answering the composition question that report actually asked. * Any further edge earns the qualifier on field evidence, one at a time. * * A QUALIFIER CANNOT EXPRESS NON-EXISTENCE. If the surface itself is absent * under a value, say so on the node with `surface_varies_by_configuration_axis`; * the projection then drops this edge as dangling and the qualifier is * redundant. Reach for `active_when` only when BOTH endpoints exist and the * RELATIONSHIP is what changes: the occupant that moves to a different row, the * feature that occupies a different place under the flag. * * ONE AXIS PER QUALIFIER. An edge conditional on two axes at once is a stated * non-goal; where it is genuinely needed, the honest model is one axis whose * values are the combinations that actually occur, which is the same discipline * as "one axis per semantic lever". */ declare const CONFIGURATION_QUALIFIER_EDGE_PROPERTY_SCHEMA: PropertySchema; /** * Property schema carried by `workspace_arranges_node`: where a node sits on a * canvas, and how its card is presented there. * * A PLACEMENT IS A FACT ABOUT THE RELATIONSHIP, not about either endpoint. The * same persona can sit at different coordinates on five different canvases, and * none of those coordinates is a property of the persona. This is the same * principle `framework_exercise_includes_node` applies to per-entity results. * * `selected` IS DELIBERATELY ABSENT. Selection is ephemeral UI state, and * persisting it means reloading a canvas restores someone's month-old * selection and every click dirties the file. The spec does not bless it. * * AT MOST ONE ARRANGEMENT PER (WORKSPACE, NODE) PAIR: a card appears once on a * canvas. The write amplification is real and named rather than hidden: * dragging a card rewrites edge properties and so changes the body checksum, * which is diff noise on a shared graph. It is bounded by the tool writing only * the canvases a user explicitly keeps, and by viewport panning touching the * opaque `canvas` bag rather than any edge. * * `x` and `y` are expected on every instance. Requiredness here is convention * rather than enforcement: `PropertyDefinition.required` names required keys * WITHIN a nested object, and the edge-property validator has no top-level * required-key concept, so the contract is stated in each description. */ declare const WORKSPACE_ARRANGEMENT_EDGE_PROPERTY_SCHEMA: PropertySchema; declare const UPG_EDGE_CATALOG: { product_targets_persona: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; persona_pursues_job: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; persona_experiences_need: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; persona_aspires_to_desired_outcome: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; persona_delegates_to_persona: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; cross_product_eligible: true; }; persona_incurs_switching_cost: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; job_surfaces_need: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; job_motivates_desired_outcome: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; job_decomposes_into_job_step: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; outcome_reveals_opportunity: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; opportunity_drives_solution: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; opportunity_explores_via_design_concept: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; opportunity_assessed_by_feasibility_study: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; metric_assessed_by_metric_quality_assessment: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; opportunity_investigated_via_design_sprint: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; opportunity_addresses_need: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; opportunity_pursues_outcome: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; opportunity_contextualises_job: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; solution_proposes_hypothesis: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; solution_materialises_as_prototype: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; solution_becomes_feature: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; hypothesis_requires_experiment_plan: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; hypothesis_tested_by_experiment: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; experiment_validates_hypothesis: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; hypothesis_investigated_via_research_plan: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; insight_generates_hypothesis: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; research_plan_conducted_as_research_study: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; experiment_run_produces_learning: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; experiment_run_yields_evidence: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; experiment_run_tested_via_experiment_run: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; learning_updates_hypothesis: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; assumption_becomes_hypothesis: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; experiment_plan_ran_as_experiment_run: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; experiment_run_validates_hypothesis: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; hypothesis_has_evidence: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; experiment_run_produced_insight_insight: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; experiment_run_informed_decision_decision: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; experiment_plan_targets_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; experiment_plan_designs_experiment: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; experiment_executed_as_experiment_run: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; experiment_produces_learning: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; experiment_produces_evidence: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; product_contains_research_study: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; research_study_enrolls_participant: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; research_study_captures_observation: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; research_study_captures_quote: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; research_study_clusters_into_affinity_cluster: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; research_study_produces_insight: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; research_study_investigates_research_question: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; research_study_follows_interview_guide: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; research_study_collects_survey_response: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; observation_evidenced_by_quote: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; affinity_cluster_synthesises_insight: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; insight_informs_opportunity: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; deliberate_only: true; }; product_contains_competitive_analysis: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; competitor_offers_competitor_feature: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; feature_rivals_competitor_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; carries_properties: true; cross_product_eligible: true; }; competitor_emits_competitor_signal: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; competitor_signal_maps_to_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; cross_product_eligible: true; }; competitor_signal_surfaces_opportunity: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; cross_product_eligible: true; }; competitive_analysis_analyses_competitor: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; competitive_analysis_identifies_market_trend: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; competitive_analysis_scopes_market_segment: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; market_trend_influences_outcome: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; market_trend_creates_opportunity: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; competitor_feature_inspires_solution: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; competitor_competes_for_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; competitor_competes_in_territory: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; competitor_yields_learning: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; competitor_offers_capability: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; competitive_analysis_dimensioned_by_classification_axis: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_dimensioned_by_classification_axis: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; classification_axis_includes_classification_value: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; competitor_classified_as_classification_value: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; carries_properties: true; property_schema: PropertySchema; cross_product_eligible: true; }; node_classified_as_classification_value: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; carries_properties: true; property_schema: PropertySchema; cross_product_eligible: true; }; persona_anti_fit_for_classification_value: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; persona_anti_fit_for_product: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; persona_anti_fit_for_competitor: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; classification_value_evolves_from_classification_value: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; classification_value_opposite_of_classification_value: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; classification_value_sibling_of_classification_value: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; classification_value_derives_from_classification_value: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; classification_value_compatible_with_classification_value: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; classification_value_incompatible_with_classification_value: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; persona_pursues_classification_value: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; product_tracks_market_trend: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; product_has_feedback_program: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_has_user_advisory_board: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_runs_beta_program: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; feedback_program_collects_feature_request: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; feature_request_voted_on_by_feedback_vote: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; feedback_program_runs_nps_campaign: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; feedback_program_identifies_feedback_theme: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; feature_request_creates_opportunity: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feedback_theme_validates_need: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; nps_campaign_tracks_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; beta_program_runs_experiment_run: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; user_advisory_board_includes_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feedback_vote_prioritises_roadmap_item: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feedback_program_hosts_user_advisory_board: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; feedback_program_has_beta_program: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_pursues_outcome: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; cross_product_eligible: true; }; product_targets_objective: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; cross_product_eligible: true; }; product_guided_by_vision: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; cross_product_eligible: true; }; product_fulfils_mission: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; cross_product_eligible: true; }; product_organises_around_strategic_theme: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; cross_product_eligible: true; }; product_stands_on_strategic_pillar: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; cross_product_eligible: true; }; product_invests_in_initiative: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; cross_product_eligible: true; }; product_develops_capability: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; product_delivers_through_value_stream: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; product_holds_assumption: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; product_measures_with_metric: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; cross_product_eligible: true; }; outcome_measured_by_metric: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; cross_product_eligible: true; }; objective_achieved_through_key_result: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; cross_product_eligible: true; }; objective_measured_by_metric: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; cross_product_eligible: true; }; key_result_quantified_by_metric: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; cross_product_eligible: true; }; vision_realised_through_mission: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; mission_supported_by_strategic_pillar: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; strategic_pillar_organises_strategic_theme: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; strategic_pillar_enables_capability: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; strategic_pillar_delivers_value_stream: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; strategic_pillar_decided_via_decision: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; strategic_pillar_measured_by_metric: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; cross_product_eligible: true; }; strategic_theme_pursues_initiative: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; cross_product_eligible: true; }; strategic_theme_realised_by_roadmap_theme: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; strategic_theme_delivers_outcome: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; cross_product_eligible: true; }; strategic_theme_measured_by_key_result: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; cross_product_eligible: true; }; strategic_theme_contains_objective: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; cross_product_eligible: true; }; initiative_assumes_assumption: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; initiative_advances_key_result: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; cross_product_eligible: true; }; initiative_delivered_via_roadmap_theme: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; initiative_drives_outcome: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; cross_product_eligible: true; }; capability_enables_value_stream: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; need_fulfilled_by_capability: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; capability_depends_on_capability: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; capability_implemented_by_feature: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; vision_guides_objective: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; metric_decomposes_into_metric: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; metric_guards_metric: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; metric_segmented_by_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; metric_drives_outcome: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; product_organises_into_feature_area: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_builds_feature: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_ships_via_release: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_plans_via_roadmap: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_runs_planning_cycle: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_categorises_by_roadmap_theme: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; feature_area_contains_feature: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; feature_area_contains_feature_area: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; feature_area_groups_design_component: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; outcome_delivered_by_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; outcome_delivered_via_feature_area: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feature_decomposed_into_epic: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; epic_specified_by_user_story: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; feature_specified_by_user_story: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; user_story_verified_by_acceptance_criterion: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; task_implements_user_story: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feature_affected_by_bug: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; release_contains_feature: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; release_contains_bug: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; release_documented_in_changelog: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; roadmap_contains_roadmap_item: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; roadmap_categorised_by_roadmap_theme: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; roadmap_schedules_release: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; roadmap_theme_groups_feature: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; roadmap_theme_spans_feature_area: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; bug_affects_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; roadmap_item_references_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feature_decomposes_into_task: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; epic_decomposes_into_task: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; cross_product_eligible: true; }; epic_affected_by_bug: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; cross_product_eligible: true; }; task_has_subtask: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; planning_cycle_contains_planning_cycle: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; planning_cycle_schedules_work_item: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; deliberate_only: true; }; objective_scoped_to_planning_cycle: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; strategic_theme_scoped_to_planning_cycle: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; work_item_blocks_work_item: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; deliberate_only: true; }; work_item_relates_to_work_item: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; deliberate_only: true; }; work_item_duplicates_work_item: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; deliberate_only: true; }; product_owned_by_legal_entity: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; legal_entity_protects_ip_asset: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; legal_entity_bound_by_contract: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; contract_contains_contract_clause: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_governed_by_privacy_policy: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; contract_governs_partnership: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_maps_experience_via_user_journey: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_navigated_via_user_flow: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_sketched_in_wireframe: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_contains_screen: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; user_journey_contains_journey_step: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; journey_step_precedes_journey_step: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; user_flow_walks_journey_step: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; user_flow_routes_through_screen: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; screen_renders_as_screen_state: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; need_reframed_as_design_question: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; design_question_answered_by_design_concept: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; design_concept_realised_as_prototype: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; design_concept_sketched_in_wireframe: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; persona_experiences_user_journey: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; user_journey_maps_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; user_journey_addresses_job: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; user_flow_targets_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; journey_step_reveals_need: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; need_occurs_in_journey_step: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; journey_step_realised_by_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; opportunity_improves_user_journey: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; user_journey_passes_through_journey_phase: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; journey_phase_spans_journey_step: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; journey_step_has_action: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; journey_action_surfaces_need: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; journey_action_realised_by_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; surface_contains_surface: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; carries_properties: true; property_schema: PropertySchema; }; surface_serves_job: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; surface_governed_by_design_guideline: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; cross_product_eligible: true; }; surface_renders_design_component: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; cross_product_eligible: true; }; surface_measured_by_metric: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; cross_product_eligible: true; }; surface_supersedes_surface: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; surface_deviates_via_technical_debt_item: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_defines_configuration_axis: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; feature_flag_drives_configuration_axis: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; surface_varies_by_configuration_axis: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; carries_properties: true; property_schema: PropertySchema; }; surface_alternates_with_surface: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; feature_occupies_surface: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; carries_properties: true; property_schema: PropertySchema; }; screen_renders_surface: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; decision_affects_surface: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; journey_step_occurs_on_surface: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; product_systematised_in_design_system: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_built_with_design_component: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; design_system_contains_design_component: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; design_system_defines_design_token: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; design_system_codified_in_design_guideline: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; design_system_expresses_brand_identity: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_expresses_brand_identity: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; cross_product_eligible: true; }; design_system_encompasses_user_journey: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; design_system_encompasses_user_flow: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; design_system_informed_by_insight: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; design_system_decided_via_decision: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; design_component_styled_by_design_token: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; design_token_derives_from_design_token: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; cross_product_eligible: true; }; design_component_follows_design_pattern: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; design_component_governed_by_design_guideline: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; design_component_specified_by_interaction_spec: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; design_component_composes_design_component: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; cross_product_eligible: true; }; prototype_annotated_with_annotation: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; screen_renders_design_component: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; cross_product_eligible: true; }; screen_markets_product: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; cross_product_eligible: true; }; screen_targets_competitor: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; cross_product_eligible: true; }; feature_surfaces_product: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; cross_product_eligible: true; }; feature_uses_design_component: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; cross_product_eligible: true; }; product_implements_design_system: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; cross_product_eligible: true; }; screen_navigates_to_screen: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; screen_surfaces_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; screen_wireframed_as_wireframe: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; wireframe_specifies_screen: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_branded_as_brand_identity: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; brand_identity_coloured_with_brand_colour: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; brand_identity_typeset_with_brand_typography: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; brand_identity_speaks_with_brand_voice: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; brand_identity_expressed_in_brand_asset: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; brand_identity_expressed_through_brand_imagery: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_bounded_by_bounded_context: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_decided_via_decision: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_bounded_by_constraint: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_stored_in_code_repository: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_integrates_via_integration_pattern: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_connects_to_external_api: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_flows_through_data_flow: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; bounded_context_deploys_service: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; bounded_context_emits_domain_event: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; bounded_context_decided_via_decision: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; bounded_context_modelled_as_aggregate: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; bounded_context_projected_as_read_model: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; bounded_context_persisted_in_data_model: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; bounded_context_stored_in_code_repository: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; bounded_context_integrates_via_integration_pattern: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; bounded_context_connects_to_external_api: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; bounded_context_flows_through_data_flow: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; bounded_context_contains_feature_area: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; service_exposes_api_contract: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; service_carries_technical_debt_item: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; service_toggles_feature_flag: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; service_deployed_as_deployment: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; deployment_triggers_incident: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; service_serves_api_endpoint: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; api_contract_contains_api_endpoint: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; service_persisted_in_database_schema: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; service_publishes_to_queue_topic: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; service_produces_build_artifact: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; service_depends_on_library_dependency: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; service_powers_feature_area: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; service_powers_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; decision_incurs_technical_debt_item: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; aggregate_contains_domain_entity: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; aggregate_contains_value_object: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; aggregate_handles_command: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; aggregate_emits_domain_event: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; command_produces_domain_event: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; domain_event_projected_to_read_model: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; root_cause_causes_symptom: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; root_cause_causes_bug: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; investigation_revealed_bug: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; investigation_revealed_root_cause: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; fix_resolved_bug: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; fix_resolved_root_cause: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; fix_derived_from_investigation: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; root_cause_shares_cause_with_root_cause: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; root_cause_manifests_as_technical_debt_item: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; service_investigated_via_investigation: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; service_affected_by_root_cause: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; bug_affects_service: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; root_cause_affects_service: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; root_cause_affects_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; investigation_surfaces_symptom: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; root_cause_resolved_by_fix: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; bounded_context_contains_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; technical_debt_item_blocks_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; api_endpoint_serves_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; design_component_implements_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; design_component_consumes_service: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; prototype_validates_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; wireframe_specifies_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; user_flow_requires_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_commits_to_service_level_objective: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; service_level_objective_measured_by_service_level_indicator: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; service_level_objective_budgets_as_error_budget: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_experiences_incident: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; incident_analysed_in_postmortem: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_documented_in_runbook: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_monitored_by_monitor: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; monitor_triggers_via_alert_rule: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_built_by_ci_pipeline: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_released_via_release_strategy: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_covered_by_on_call_rotation: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_runs_on_infrastructure_component: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; ci_pipeline_produces_build_artifact: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; infrastructure_component_committed_to_service_level_objective: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; infrastructure_component_monitored_by_monitor: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; infrastructure_component_built_by_ci_pipeline: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; infrastructure_component_experiences_incident: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; infrastructure_component_documented_in_runbook: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; infrastructure_component_released_via_release_strategy: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; infrastructure_component_covered_by_on_call_rotation: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; service_level_objective_tracks_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; service_level_objective_satisfies_service_level_agreement: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; incident_triggers_postmortem: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; incident_breaches_service_level_objective: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; incident_caused_by_root_cause: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; incident_exploits_vulnerability: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; monitor_watches_service: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; monitor_measures_service_level_indicator: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; ci_pipeline_deploys_service: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; alert_rule_triggers_runbook: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; runbook_mitigates_incident: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; postmortem_identifies_root_cause: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; postmortem_produces_runbook: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; product_models_threats_with_threat_model: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; threat_model_identifies_threat: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; threat_model_surfaces_vulnerability: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_enforces_security_control: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_governed_by_security_policy: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_tested_by_penetration_test: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_reviewed_by_security_review: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_classifies_data_with_data_classification: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_restricts_access_with_access_policy: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; security_policy_mandates_security_control: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; security_policy_defines_access_policy: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; security_policy_establishes_data_classification: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; security_policy_requires_threat_model: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; security_policy_schedules_security_review: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; security_review_commissions_penetration_test: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; security_policy_governs_incident: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; threat_targets_service: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; vulnerability_affects_service: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; security_control_mitigates_threat: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; penetration_test_assesses_service: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; security_control_protects_service: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; access_policy_governs_service: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; vulnerability_discovered_by_penetration_test: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; data_classification_applies_to_data_source: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_plans_qa_via_test_plan: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; test_plan_executed_by_test_suite: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; test_plan_specifies_test_environment: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_maintains_test_suite: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; test_suite_contains_test_case: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_undergoes_qa_session: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; test_suite_includes_regression_test: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_measured_by_test_coverage_report: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_provisioned_in_test_environment: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; qa_session_discovers_bug: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; test_suite_tested_via_qa_session: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; test_suite_measured_by_test_coverage_report: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; test_suite_deployed_in_test_environment: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; test_case_validates_acceptance_criterion: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; test_suite_covers_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; test_environment_mirrors_deployment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; regression_test_guards_release: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; test_case_covers_user_story: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; qa_session_targets_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; test_coverage_report_covers_service: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; test_suite_produces_test_result: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; test_case_produces_test_result: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; product_conforms_to_a11y_standard: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; a11y_standard_contains_a11y_guideline: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_audited_by_a11y_audit: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; a11y_audit_discovers_a11y_issue: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_annotated_with_a11y_annotation: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; a11y_standard_verified_by_a11y_audit: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; a11y_standard_annotated_with_a11y_annotation: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; a11y_issue_affects_design_component: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; a11y_audit_covers_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_powered_by_ai_model: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; ai_model_defines_prompt_template: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; prompt_template_contains_prompt_version: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; ai_model_benchmarked_by_eval_benchmark: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; eval_benchmark_executed_as_eval_run: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; ai_model_costed_by_ai_cost_tracker: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; ai_model_flagged_by_hallucination_report: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; ai_model_constrained_by_ai_guardrail: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_compared_via_model_comparison: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; ai_model_compared_in_model_comparison: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; prompt_version_supersedes_prompt_version: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; eval_benchmark_measures_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; eval_benchmark_measures_node: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; eval_benchmark_draws_cases_from_ai_dataset: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; ai_guardrail_enforces_security_policy: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; model_comparison_informs_decision: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; ai_cost_tracker_feeds_cost_structure: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; ai_model_evaluated_through_ai_experiment: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; ai_model_trained_on_ai_dataset: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; ai_model_produces_ai_trace: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; eval_run_evaluates_ai_model: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; eval_run_scores_prompt_version: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; hallucination_report_traces_to_ai_trace: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; hallucination_report_caused_by_root_cause: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; ai_trace_executed_prompt_version: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; ai_dataset_sourced_from_data_source: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_automated_via_workflow_template: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; workflow_template_executed_as_workflow_run: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_assisted_by_agent_definition: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; agent_definition_runs_agent_session: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; workflow_template_gated_by_review_gate: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; review_gate_approved_via_approval_record: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; agent_definition_capable_of_agent_skill: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; agent_definition_triggered_via_agent_hook: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; workflow_run_produces_workflow_artifact: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; agent_definition_orchestrates_workflow_template: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; workflow_run_implements_initiative: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; agent_session_creates_decision: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; review_gate_blocks_release: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; agent_skill_extends_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; workflow_artifact_references_deliverable: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; agent_hook_triggers_ci_pipeline: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; workflow_template_defines_agent_task: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; agent_definition_spawns_agent_task: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; agent_task_executes_task: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_measures_funnel: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_acquires_via_acquisition_channel: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_segments_into_cohort: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_segments_into_behavioral_segment: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_grows_via_growth_loop: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_attributed_via_attribution_model: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; attribution_model_credits_acquisition_channel: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; product_guided_by_metric: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; funnel_contains_funnel_step: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; acquisition_channel_runs_growth_campaign: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; growth_campaign_tests_variant: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; growth_campaign_tests_via_experiment_plan: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; experiment_run_tests_variant: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; metric_drives_metric: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; funnel_step_reveals_need: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; funnel_step_tracks_event_schema: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; marketing_channel_drives_funnel: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; metric_measures_key_result: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; behavioral_segment_maps_to_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; cohort_exposed_to_experiment_run: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; acquisition_channel_drives_outcome: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; growth_campaign_targets_behavioral_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; growth_loop_drives_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; growth_loop_fuels_acquisition_channel: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; variant_tests_hypothesis: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; experiment_plan_targets_behavioral_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; funnel_maps_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; cohort_represents_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; acquisition_channel_targets_behavioral_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; acquisition_channel_reaches_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_monetised_via_business_model: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; business_model_delivers_value_proposition: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; business_model_earns_via_revenue_stream: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; business_model_costs_via_cost_structure: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; business_model_measured_by_unit_economics: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; business_model_partnered_via_partnership: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; business_model_requires_key_resource: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; business_model_performs_key_activity: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; business_model_targets_market_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; business_model_maintains_customer_relationship: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; business_model_distributes_via_distribution_channel: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; revenue_stream_tiered_as_pricing_tier: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; revenue_stream_measured_by_metric: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; cost_structure_measured_by_metric: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; value_proposition_targets_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; value_proposition_delivers_outcome: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; value_proposition_addresses_job: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; value_proposition_solves_need: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; revenue_stream_drives_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; funnel_step_maps_to_journey_step: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; customer_journey_stage_contains_funnel_step: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; customer_journey_stage_spans_journey_step: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; dependency_blocks_team: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; dependency_depends_on_team: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; program_implements_initiative: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; playbook_triggered_by_customer_health_score: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; partnership_with_integration_partner: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; market_segment_includes_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; persona_belongs_to_market_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; revenue_stream_priced_by_pricing_strategy: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; revenue_stream_drives_outcome: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; pricing_tier_targets_behavioral_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; key_resource_enables_value_proposition: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_goes_to_market_via_gtm_strategy: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; gtm_strategy_targets_ideal_customer_profile: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; gtm_strategy_positions_via_positioning: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; gtm_strategy_launches_via_launch: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; gtm_strategy_educates_via_content_strategy: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; gtm_strategy_sells_via_sales_motion: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; gtm_strategy_arms_with_competitive_battle_card: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; gtm_strategy_generates_demand_via_demand_gen_program: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; gtm_strategy_operates_in_territory: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; positioning_communicated_via_messaging: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; positioning_challenged_by_objection: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; positioning_evidenced_by_proof_point: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; value_proposition_challenged_by_objection: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; value_proposition_evidenced_by_proof_point: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; competitive_battle_card_addresses_objection: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; objection_countered_by_rebuttal: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; rebuttal_evidenced_by_proof_point: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; positioning_references_competitor: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; positioning_resonates_with_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; positioning_differentiates_from_competitor: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; ideal_customer_profile_maps_to_behavioral_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; ideal_customer_profile_maps_to_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; ideal_customer_profile_targets_behavioral_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; launch_ships_with_release: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; launch_amplified_by_growth_campaign: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; launch_ships_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; launch_announces_release: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; messaging_targets_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; competitive_battle_card_references_competitor: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; competitor_feature_inspires_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; territory_maps_to_behavioral_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; positioning_differentiates_via_value_proposition: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; positioning_within_market_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; ideal_customer_profile_targets_market_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; launch_amplified_by_marketing_channel: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; sales_motion_qualifies_via_funnel_step: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; objection_sourced_from_quote: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; proof_point_derived_from_evidence: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; proof_point_derived_from_insight: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_priced_via_pricing_strategy: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; pricing_strategy_tests_experiment_plan: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; pricing_strategy_offers_pricing_tier: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; pricing_strategy_discounts_via_discount_strategy: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; pricing_strategy_trials_via_trial_config: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; pricing_strategy_gates_via_paywall: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; pricing_tier_includes_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; pricing_tier_gated_by_paywall: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; pricing_tier_trialed_via_trial_config: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; pricing_tier_discounted_by_discount_strategy: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; experiment_run_tests_pricing_tier: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; trial_config_unlocks_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; trial_config_drives_funnel: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; paywall_gates_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; discount_strategy_targets_behavioral_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; pricing_tier_localised_as_regional_pricing: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_sold_via_pipeline_sales: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; pipeline_sales_contains_pipeline_stage: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_serves_account: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; account_contains_contact: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; account_negotiates_deal: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_attracts_lead: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; deal_quoted_via_quote_document: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_subscribed_via_subscription: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; subscription_billed_via_invoice: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_forecasted_via_forecast: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; pipeline_sales_qualifies_lead: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; pipeline_sales_manages_account: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; pipeline_sales_projected_via_forecast: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; pipeline_sales_converts_to_subscription: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; deal_references_ideal_customer_profile: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; lead_becomes_account: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; subscription_drives_revenue_stream: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; forecast_predicts_revenue_stream: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; forecast_projects_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_markets_through_marketing_strategy: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; marketing_strategy_activates_marketing_channel: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; marketing_strategy_targets_seo_keyword: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; marketing_strategy_publishes_press_release: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; marketing_strategy_hosts_event: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; marketing_strategy_builds_community_via_community_initiative: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; marketing_channel_runs_marketing_campaign_plan: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; marketing_campaign_plan_sends_email_sequence: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; marketing_campaign_plan_publishes_social_post: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; marketing_campaign_plan_runs_ad_creative: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_announced_via_press_release: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_hosts_event: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_engages_via_community_initiative: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; marketing_campaign_plan_targets_behavioral_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; event_generates_lead: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; seo_keyword_drives_content_piece: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; ad_creative_references_messaging: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; community_initiative_surfaces_insight_about_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_supports_via_support_ticket: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_listens_via_customer_feedback: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_loses_because_churn_reason: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_onboards_via_user_flow: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_health_scored_via_customer_health_score: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_operated_via_playbook: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_guarantees_via_service_level_agreement: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_celebrates_via_success_milestone: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_blueprinted_via_service_blueprint: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_measured_by_nps_campaign: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; service_blueprint_contains_user_flow: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; service_blueprint_contains_playbook: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; service_blueprint_contains_service_level_agreement: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; service_blueprint_contains_customer_health_score: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; service_blueprint_contains_support_ticket: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; service_blueprint_contains_customer_feedback: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; customer_health_score_tracked_by_nps_campaign: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; customer_health_score_contains_success_milestone: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; customer_feedback_reveals_churn_reason: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; user_flow_contains_customer_journey_stage: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; customer_journey_stage_contains_touchpoint: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; support_ticket_reveals_need: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; customer_feedback_creates_observation: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; churn_reason_generates_hypothesis: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; user_flow_maps_user_journey: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; service_level_agreement_governs_service: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; touchpoint_occurs_in_journey_step: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; nps_campaign_tracks_customer_health_score: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; playbook_targets_customer_journey_stage: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; success_milestone_validates_outcome: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; customer_health_score_informs_playbook: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; customer_feedback_becomes_feature_request: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; support_ticket_reports_bug: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; churn_reason_reveals_need: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_publishes_content_piece: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_documents_in_knowledge_base_article: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_expressed_via_brand_asset: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_documented_in_document: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_templated_via_documentation_template: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_records_in_document: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; content_strategy_scheduled_in_content_calendar: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; content_strategy_themed_by_content_theme: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; content_calendar_contains_content_theme: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; content_calendar_schedules_content_piece: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; content_calendar_schedules_knowledge_base_article: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; content_calendar_schedules_brand_asset: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; content_calendar_schedules_document: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; content_calendar_schedules_documentation_template: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; content_piece_supports_messaging: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; content_piece_part_of_growth_campaign: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; knowledge_base_article_documents_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; prompt_template_powers_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; content_theme_targets_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; content_theme_organizes_content_piece: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; document_describes_feature: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; document_describes_vision: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; document_describes_persona: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; document_describes_competitor: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; document_describes_strategic_pillar: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; document_describes_market_segment: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; document_describes_revenue_stream: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; document_describes_positioning: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; document_describes_decision: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; document_contains_insight: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; document_contains_quote: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; document_contains_observation: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; /** * A document EMBEDS this node's live value at a position in its prose. * * @remarks * NOT `describes`. `document_describes_persona` says the document is ABOUT the * persona. Transclusion says the document RENDERS that node where the prose * sits, so the value a reader sees is the node's current value rather than a * copy taken when the sentence was written. A PRD that transcludes a metric is * not "about" the metric, and collapsing the two would lose the only property * transclusion is bought for: current by construction. The nine-member * `document_describes_*` family is untouched and keeps its meaning. * * POLYMORPHIC because the target set is genuinely open. Anything renderable can * be embedded, and the nine enumerated describe-targets are none of `metric`, * `research_study`, `specification` or `architecture_decision`, which is what * 65% of measured documents carrying zero outbound edges were reaching for. * * THE ANCHOR BINDING (normative). `@unified-product-graph/markdown` already * parses an inline reference form and records the source line of every * occurrence: `[[type:id]]`, `[[type:id|label]]`, `[[type:id|k:v|...]]`, * `[[+type:id]]` for creation, and `[[type:id@product]]` across products. The * grammar is published as Appendix F of the specification paper. The rule that * was missing, and is stated here: an anchor appearing in the body of a * `document` IS a transclusion anchor, and a conformant parser WRITES this edge * beside it. Anchor and edge are one fact recorded twice, so they must be * written together or they drift. * * NO POSITION PROPERTY, deliberately. The obvious `anchor_line` is the most * volatile value a text document has: every insertion above moves every anchor * below, so a stored line number is wrong after the next paragraph and nothing * reports the drift. The anchor IS the position, and it lives in the prose * where ordinary editing moves it for free. What the law asks is that the edge * be written BESIDE the anchor, which is a write-time discipline rather than a * stored field. * * `deliberate_only`, and the 0.33.0 trap is why that is safe here. Flagging a * widened edge `deliberate_only` silently switched five adapters off in the * last release, two of them under a green suite. The rule that came out of it: * an adapter reading an explicit field the SOURCE stores carries an authored * fact rather than an inferred one and must emit deliberate-only edges * EXPLICITLY. A `[[type:id]]` anchor is as explicit as a source gets, because a * person typed it, so the markdown emitter keys on the ANCHOR and never on the * `node` wildcard. No generic pair-resolution path may ever produce one. */ document_transcludes_node: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; deliberate_only: true; }; product_educates_via_education_program: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; education_program_teaches_via_tutorial: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; education_program_guides_via_walkthrough: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; education_program_presents_via_webinar: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; education_program_certifies_via_certification: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; education_program_demonstrates_via_help_video: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; education_program_structures_via_learning_path: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; learning_path_contains_tutorial: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; learning_path_includes_certification: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; tutorial_explains_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; walkthrough_maps_user_flow: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; certification_validates_skill: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; help_video_documents_screen: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; tutorial_references_knowledge_base_article: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; webinar_generates_content_piece: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_staffed_by_team: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_influenced_by_stakeholder: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_organised_into_department: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; department_contains_team: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; team_contains_team: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; department_includes_stakeholder: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; person_member_of_team: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; person_reports_to_person: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; person_holds_role: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; team_staffed_with_role: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; team_targets_team_okr: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; team_reflects_in_retrospective: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; team_depends_on_dependency: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; team_skilled_in_skill: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; team_practices_ceremony: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; team_planned_via_capacity_plan: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; team_decides_decision: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; decision_references_decision: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; stakeholder_maps_to_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; stakeholder_invested_in_outcome: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; team_okr_aligns_with_objective: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; team_okr_aligns_with_key_result: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; ceremony_involves_team: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_managed_via_program: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; program_contains_project: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; project_targets_milestone: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_targets_milestone: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; project_produces_deliverable: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; program_tracked_via_risk_register: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; risk_register_contains_risk: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; program_changed_via_change_request: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; program_resourced_via_resource_allocation: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; program_reported_via_status_report: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; program_contains_epic: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; project_implements_initiative: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; project_delivers_work_item: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; deliberate_only: true; }; project_contains_epic: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; project_contains_feature: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; project_contains_user_story: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; project_contains_task: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; project_contains_bug: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; project_advances_key_result: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; project_drives_outcome: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; project_assumes_assumption: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; constraint_constrains_project: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; strategic_theme_pursues_project: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; milestone_gates_release: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; milestone_triggers_release: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; deliverable_ships_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_constrained_by_compliance_requirement: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_exposed_to_risk: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_bound_by_data_contract: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_audited_via_audit_log_policy: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_governed_by_compliance_framework: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; compliance_framework_mandates_compliance_requirement: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; compliance_framework_verified_by_security_audit: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; compliance_framework_requires_privacy_policy: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; compliance_framework_requires_audit_log_policy: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; compliance_framework_identifies_risk: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; compliance_framework_governs_data_contract: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; compliance_framework_applies_to_legal_entity: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; compliance_requirement_constrains_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; compliance_requirement_constrains_decision: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; risk_manifests_as_technical_debt_item: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; risk_threatens_node: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; risk_mitigated_by_node: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; data_contract_governs_data_source: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; compliance_framework_requires_security_control: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; security_audit_validates_compliance_framework: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_localised_in_locale: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_configured_via_locale_config: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; locale_translated_via_translation_bundle: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; locale_adapted_via_cultural_adaptation: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; locale_priced_in_regional_pricing: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; locale_configured_via_locale_config: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; translation_bundle_contains_translation_key: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; screen_translated_via_translation_bundle: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; cultural_adaptation_targets_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_partnered_via_partner_program: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; partner_program_tiers_as_partner_tier: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_exposed_via_api_ecosystem: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; api_ecosystem_lists_marketplace_listing: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_documented_via_developer_portal: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; partner_program_includes_integration_partner: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; partner_program_shares_revenue_via_partner_revenue_share: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; partner_program_exposes_api_ecosystem: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; partner_program_documents_developer_portal: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; api_ecosystem_exposes_api_endpoint: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; marketplace_listing_extends_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; integration_partner_connects_external_api: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; developer_portal_documents_api_contract: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; partner_tier_qualifies_integration_partner: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; partner_revenue_share_governs_partner_tier: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; marketplace_listing_references_help_video: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_ingests_from_data_source: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_tracks_via_event_schema: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_visualised_in_dashboard: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_organised_into_data_domain: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_defined_by_glossary_term: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; data_source_defines_metric: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; data_source_processed_via_data_pipeline: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; data_source_traced_via_data_lineage: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; data_source_emits_event_schema: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; metric_validated_by_data_quality_rule: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; data_domain_produces_data_product: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; data_domain_contains_data_source: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; data_domain_defines_glossary_term: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; data_domain_modelled_in_data_model: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; data_domain_visualised_in_dashboard: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; dashboard_contains_report: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; dashboard_contains_experiment_run: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; metric_measures_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; event_schema_tracks_funnel_step: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; dashboard_tracks_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; data_product_serves_dashboard: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; data_pipeline_feeds_data_product: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; organization_invests_via_portfolio: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; organization_organised_into_product_area: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; portfolio_contains_product: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_area_contains_product: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; portfolio_contains_portfolio: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_area_contains_product_area: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_area_contains_feature: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_has_competitor: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_addresses_market_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_has_positioning: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_has_feature_request: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_shares_persona_with_product: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; product_shares_competitor_with_product: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; product_shares_metric_with_product: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; product_depends_on_product: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; product_cannibalises_product: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; product_succeeds_product: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; organization_thinks_in_workspace: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_area_thinks_in_workspace: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; product_thinks_in_workspace: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; workspace_produced_node: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; workspace_arranges_node: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; carries_properties: true; property_schema: PropertySchema; }; composition_focuses_node: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; capture_renders_node: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feature_addresses_job: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feature_drives_key_result: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; quote_relates_to_job: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; changelog_includes_feature: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; ai_trace_spawns_ai_trace: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; business_model_guided_by_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; vision_anchored_by_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; metric_fed_by_data_source: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; hallucination_report_has_root_cause: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; model_comparison_compares_ai_model: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; launch_measured_by_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; wireframe_depicts_screen: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; service_level_indicator_measures_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; agent_definition_uses_ai_model: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; privacy_policy_governs_compliance_requirement: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; a11y_annotation_targets_design_component: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; metric_quality_assessment_considers_proxy_metric: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; ai_experiment_based_on_ai_model: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; rebuttal_supported_by_evidence: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; bug_observed_in_release: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; vulnerability_affects_library_dependency: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; a11y_issue_found_in_screen: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; team_okr_cascades_from_team_okr: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; agent_session_invoked_agent_skill: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; agent_task_uses_agent_skill: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; participant_belongs_to_behavioral_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; classification_axis_owned_by_product: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_expressed_as_design_component: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_manifests_in_design_component: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; product_enables_flow_user_flow: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; design_component_surfaces_insight_insight: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; design_component_specifies_for_service: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; design_token_tokenised_as_service: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; design_component_requires_data_from_service: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; service_implements_design_component: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; service_constrains_interaction_design_component: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; service_enables_pattern_design_component: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; service_constrains_scope_product: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; node_informs_node: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; node_constrains_node: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; node_inspires_node: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; decision_influences_node: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; decision_constrained_by_node: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; decision_superseded_by_decision: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; decision_produces_node: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; decision_affects_design_component: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; decision_affects_screen: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; decision_informs_decision: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; constraint_constrains_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; constraint_constrains_initiative: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; constraint_constrains_metric: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; constraint_owned_by_team: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; insight_validates_need: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; insight_reveals_desired_outcome: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; insight_informs_job: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; insight_characterises_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; observation_reveals_need: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; observation_characterises_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; quote_evidences_need: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; quote_evidences_job: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; insight_enriches_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; insight_validates_value_proposition: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; insight_validates_strategic_pillar: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; insight_surfaces_opportunity: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; insight_informs_solution: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; insight_inspires_design_question: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; insight_inspires_design_concept: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; observation_yields_insight: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; insight_refines_into_insight: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; insight_validates_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; learning_validates_opportunity: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; learning_validates_solution: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; learning_refines_hypothesis: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; learning_validates_need: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; learning_validates_job: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; learning_informs_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; evidence_supports_opportunity: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; experiment_run_tests_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; experiment_run_measures_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; experiment_run_guards_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; experiment_run_measured_by_metric: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; solution_measured_by_metric: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; node_owned_by_team: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; cross_product_eligible: true; }; node_owned_by_role: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; node_owned_by_stakeholder: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; node_owned_by_department: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; cross_product_eligible: true; }; node_owned_by_person: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; node_assigned_to_person: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; node_belongs_to_bounded_context: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; framework_exercise_includes_node: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; carries_properties: true; }; marketing_strategy_pursues_outcome: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; marketing_channel_feeds_acquisition_channel: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; deal_at_pipeline_stage: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; subscription_subscribes_to_pricing_tier: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; lead_sourced_from_acquisition_channel: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; account_partners_via_partnership: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; customer_health_score_composed_of_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; service_level_agreement_measures_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; education_program_targets_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; milestone_gates_deliverable: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; data_lineage_sourced_from_data_source: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; data_lineage_feeds_data_source: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; data_pipeline_reads_from_data_source: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; data_pipeline_writes_to_data_source: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; audit_log_policy_tracks_event_schema: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; cultural_adaptation_targets_market_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feature_request_from_behavioral_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feature_request_in_feature_area: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feedback_vote_from_behavioral_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; ai_experiment_uses_ai_model: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feature_tests_hypothesis: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; journey_step_shown_on_screen: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; prototype_simulates_screen: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; prototype_tests_hypothesis: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; design_token_reflects_brand_colour: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; brand_identity_signed_with_brand_logo: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; messaging_aligns_with_brand_voice: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; privacy_policy_governs_data_source: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; monitor_detects_symptom: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; symptom_triggers_incident: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; /** Replaces `LearningProperties.metric: string`. Links a learning to the metric it was observed on. */ learning_observed_on_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; /** Replaces `ModelComparisonProperties.winner: string`. Links a model comparison to the ai_model that won. */ model_comparison_winner_is_ai_model: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; /** Replaces `DataProductProperties.consumers: string`. Links a data product to each consuming service (per JSDoc example values like `analytics-service`, `search-indexer`). */ data_product_consumed_by_service: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; /** Replaces `ReportProperties.recipients: string`. Links a report to each receiving team (per JSDoc example values like `exec-team`, `product-leads`). */ report_distributed_to_team: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; /** Replaces `ServiceLevelAgreementProperties.customer: string`. Links an SLA to the account it covers (the existing JSDoc already hinted this was the canonical shape). */ service_level_agreement_covers_account: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; regression_test_addresses_bug: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; incident_affects_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; release_strategy_used_by_deployment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; incident_generates_support_ticket: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; participant_voiced_quote: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; participant_represents_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; research_question_addressed_by_insight: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; survey_response_evidences_insight: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feature_flag_gates_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; data_model_persisted_in_database_schema: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; read_model_projects_aggregate: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; key_activity_delivers_value_proposition: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; key_activity_uses_key_resource: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; key_resource_enables_key_activity: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; partnership_performs_key_activity: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; partnership_provides_key_resource: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; customer_relationship_with_market_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; distribution_channel_reaches_market_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; distribution_channel_delivers_value_proposition: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; value_proposition_addresses_market_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; revenue_stream_captured_from_market_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; cost_structure_driven_by_key_activity: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; cost_structure_driven_by_key_resource: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; value_proposition_yields_revenue_stream: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; customer_relationship_supports_value_proposition: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; partnership_supports_value_proposition: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; solution_addresses_need: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; capability_enables_value_proposition: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; competitor_addresses_need: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; ideal_customer_profile_informs_positioning: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; ideal_customer_profile_shapes_messaging: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; ideal_customer_profile_shapes_sales_motion: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; messaging_used_in_launch: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; messaging_enables_sales_motion: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; evidence_interpreted_as_learning: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; learning_informs_decision: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; assumption_concerns_need: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; assumption_concerns_persona: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; assumption_concerns_solution: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; hypothesis_targets_outcome: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; hypothesis_concerns_persona: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; feature_addresses_need: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; experiment_run_measures_outcome: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; persona_pursues_outcome: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; assumption_concerns_outcome: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; assumption_concerns_feature: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; quote_voices_persona: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; need_measured_by_desired_outcome: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; observation_reveals_job: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; design_question_resolved_by_decision: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; decision_selects_design_concept: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; user_flow_validated_by_observation: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; design_concept_realised_as_user_flow: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; observation_informs_decision: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; bounded_context_publishes_api_contract: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; domain_event_triggers_command: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; eval_run_produces_metric: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; eval_benchmark_defines_metric: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; api_endpoint_references_domain_entity: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; api_contract_records_decision: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; data_flow_transports_domain_entity: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; api_endpoint_participates_in_data_flow: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; agent_definition_produces_workflow_artifact: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; agent_hook_fires_during_workflow_run: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; workflow_run_passes_through_review_gate: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; vision_guides_strategic_theme: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; strategic_theme_requires_capability: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; strategic_theme_flows_through_value_stream: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; initiative_enters_market_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; initiative_realises_value_proposition: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; initiative_unlocks_revenue_stream: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; distribution_channel_generates_revenue_stream: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; review_gate_vets_insight: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; research_plan_recruits_participant: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; insight_informs_design_guideline: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; research_question_generates_task: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; cohort_defined_by_behavioral_segment: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; cohort_measured_by_metric: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; behavioral_segment_measured_by_metric: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; user_advisory_board_convenes_as_ceremony: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; user_advisory_board_surfaces_research_question: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; user_advisory_board_shapes_initiative: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feedback_program_measured_by_metric: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; metric_segmented_by_behavioral_segment: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feedback_theme_surfaces_insight: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; insight_evidenced_by_quote: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; journey_step_yields_observation: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; data_classification_governed_by_privacy_policy: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; design_component_conforms_to_a11y_standard: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; design_component_conforms_to_a11y_guideline: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feature_request_becomes_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; objective_advances_outcome: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; cross_product_eligible: true; }; desired_outcome_reveals_opportunity: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; desired_outcome_quantified_by_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feasibility_study_informs_decision: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feasibility_study_recommends_solution: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; feasibility_study_produces_learning: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; design_sprint_informs_decision: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; design_sprint_recommends_solution: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; design_sprint_produces_learning: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; affinity_cluster_groups_observation: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; feedback_theme_reveals_opportunity: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; competitor_feature_benchmarks_capability: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; switching_cost_locks_in_competitor: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; test_result_reports_bug: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; content_piece_repurposed_as_social_post: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; translation_key_localises_content_piece: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; translation_key_targets_locale: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; partner_revenue_share_feeds_revenue_stream: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; retrospective_produces_learning: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; retrospective_yields_decision: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; status_report_reports_on_milestone: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; quote_document_advances_deal: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; data_quality_rule_governs_data_source: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; press_release_announces_launch: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; specification_extends_specification: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; specification_competes_with_specification: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; primitive_defined_by_specification: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; primitive_composes_primitive: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; operating_lifecycle_defined_by_specification: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; operating_lifecycle_contains_operating_stage: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; specification_governed_by_organization: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; journey_phase_realises_operating_stage: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; cross_product_eligible: true; }; operating_stage_measured_by_metric: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; objective_depends_on_dependency: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; cross_product_eligible: true; }; dependency_blocks_objective: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; cross_product_eligible: true; }; dependency_resolved_by_objective: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; cross_product_eligible: true; }; objective_raises_strategic_question: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; initiative_raises_strategic_question: { forward_verb: string; reverse_verb: string; classification: "hierarchy"; source_type: string; target_type: string; }; objective_defers_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; carries_properties: true; property_schema: PropertySchema; cross_product_eligible: true; deliberate_only: true; }; objective_defers_capability: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; carries_properties: true; property_schema: PropertySchema; cross_product_eligible: true; deliberate_only: true; }; deal_involves_contact: { forward_verb: string; reverse_verb: string; classification: "semantic"; source_type: string; target_type: string; }; deal_challenged_by_objection: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; deal_armed_with_competitive_battle_card: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; deal_lost_to_competitor: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; deal_closed_via_contract: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; deal_blocked_by_feature: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; cross_product_eligible: true; }; research_study_analyzes_deal: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; cross_product_eligible: true; }; account_raises_support_ticket: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; account_health_scored_via_customer_health_score: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; account_lost_because_churn_reason: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; account_implements_via_project: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; subscription_renews_via_deal: { forward_verb: string; reverse_verb: string; classification: "causal"; source_type: string; target_type: string; }; account_participates_in_beta_program: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; deal_gated_by_security_review: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; feature_communicated_via_messaging: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; launch_coordinated_via_project: { forward_verb: string; reverse_verb: string; classification: "cross-domain"; source_type: string; target_type: string; }; }; /** The `UPGEdgeType` union derived from the registry above. Declared here (not * in `shapes/edges.ts`) so the polymorphic list below can reference it * without creating a cyclic import. */ type _UPGEdgeTypeLocal = keyof typeof UPG_EDGE_CATALOG; /** * The dual-registered cross-product edges, as a value-filtered union derived from * the `cross_product_eligible` flag (0.17.3). Because `UPG_EDGE_CATALOG` is declared * with `satisfies`, the literal `true` is preserved, so this conditional-type filter * stays exact as edges are flagged — flag one catalog entry and it appears here with * no other edit. This is the dual-registered half of `UPGCrossEdgeType`; the * portfolio-native half (edges with no within-graph catalog entry) is the explicit * `UPG_CROSS_ONLY_EDGE_TYPES` in `shapes/document.ts`. */ type CrossProductEligibleEdgeType = { [K in keyof typeof UPG_EDGE_CATALOG]: typeof UPG_EDGE_CATALOG[K] extends { cross_product_eligible: true; } ? K : never; }[keyof typeof UPG_EDGE_CATALOG]; /** * Runtime list of catalog edges flagged `cross_product_eligible`, derived from the * catalog in declaration order. The runtime mirror of `CrossProductEligibleEdgeType`, * composed with `UPG_CROSS_ONLY_EDGE_TYPES` to build `UPG_CROSS_EDGE_TYPES`. */ declare const UPG_CROSS_ELIGIBLE_CATALOG_EDGE_TYPES: readonly CrossProductEligibleEdgeType[]; /** * True if this catalog edge is dual-registered as cross-product-eligible: a * within-graph edge whose endpoints may also live in different graphs within a * portfolio (0.17.3). Accepts any string for ergonomic call sites. * * @example * isCrossProductEligible('strategic_theme_contains_objective') // → true * isCrossProductEligible('feature_area_contains_feature') // → false */ declare function isCrossProductEligible(type: string): boolean; /** * The deliberate-only edges, as a value-filtered union derived from the * `deliberate_only` flag (0.17.4). Same `satisfies`-preserved pattern as * `CrossProductEligibleEdgeType`: flag one catalog entry and it appears here with * no other edit. These edges must never be inferred from a generic parent nesting; * generic-inference chokepoints skip them. */ type DeliberateOnlyEdgeType = { [K in keyof typeof UPG_EDGE_CATALOG]: typeof UPG_EDGE_CATALOG[K] extends { deliberate_only: true; } ? K : never; }[keyof typeof UPG_EDGE_CATALOG]; /** * Runtime list of catalog edges flagged `deliberate_only`, derived from the catalog * in declaration order. The single source of truth the SDK's auto-nest inference and * the import adapters' parentage resolvers consume, so a deliberate-only edge * self-excludes from every generic-inference path with one flag. */ declare const UPG_DELIBERATE_ONLY_EDGE_TYPES: readonly DeliberateOnlyEdgeType[]; /** * True if this catalog edge is deliberate-only: it must be authored explicitly and * is never inferred from a generic parent nesting (0.17.4). Accepts any string for * ergonomic call sites. * * @example * isDeliberateOnlyEdge('objective_defers_feature') // → true * isDeliberateOnlyEdge('feature_area_contains_feature') // → false */ declare function isDeliberateOnlyEdge(type: string): boolean; /** * Canonical allow-list of edges that use the `'node'` wildcard endpoint. * * Sixteen semantic families are sanctioned. The count and the partition are * asserted in `spec-integrity.test.ts`, so this list cannot silently fall out * of step with the array below the way it did between 0.28.0 and 0.31.0: * * 1. **Universal semantic verbs**: any node can inform / constrain / inspire * any other node. The meaning is deliberately abstract; consumers render * them as plain relational signals. * 2. **Decision-to-anything**: a decision can influence, be constrained by, * or produce any kind of node. Decisions cut across domains; binding the * target would force a combinatorial explosion. * 3. **Universal ownership**: any node can be owned by a team, role, * stakeholder, department, or person. Ownership is not per-entity-type. * 4. **Universal architecture references**: any node can belong to a bounded * context (DDD building blocks span the type system). * 5. **Framework exercises**: a framework_exercise can include any entity type * it scores. The technique is type-agnostic, so binding the target would * re-weld frameworks to one entity type — the limitation this edge removes. * 6. **Universal classification**: any node can be classified against a * classification_value. Classification schemes are type-agnostic. * 7. **Work-item issue links**: blocks/relates/duplicates spans the work-item * set {feature, epic, user_story, task, bug} — endpoint-polymorphic over a * bounded family, not the full type universe. * 8. **Workspace provenance** (WS3, 2026-07-05): a workspace's commit loop can * produce any entity type arranged in it (decision, feature, persona, ...). * Widened from the single-target `workspace_produced_decision`; see * `UPG_EDGE_MIGRATIONS` for the rename rule. * 9. **Canvas arrangement and published-view focus**: any entity can be dragged * onto a canvas or shown in a published view, and neither placement carries * a structural role, so both collapse polymorphic rather than enumerating * the type universe twice. Both are `cross-domain`, so neither enters * `UPG_VALID_CHILDREN` and neither can be walked as containment. * 10. **Benchmark subject** (0.31.0): an eval measures a tool, a document, a * check, an importer, a feature. Registered as OVER-WIDE by admission, * because a benchmark measures a measurable thing rather than literally any * node; the width is what it costs to avoid answering "what is a tool in the * graph" as a side effect of building an eval harness. Reach for the typed * `eval_benchmark_measures_feature` when the subject IS a feature. * 11. **Universal assignment** (0.32.0): any node can be assigned to a person, * and assignment is deliberately NOT the ownership family above. Ownership is * durable accountability; assignment is who is working on it now, with an * interval and an exclusivity ownership does not have. A board that runs 85% * unassigned while everything is owned cannot say so with one edge. * 12. **Capture subject** (0.32.0): anything in the graph can be rendered into a * dated, hashed file. Being captured carries no structural role, so the * endpoint collapses rather than enumerating a list that would never finish. * 13. **Cadence scheduling** (0.32.0): a planning_cycle schedules work across the * same bounded {feature, epic, user_story, task, bug} family as family 7, * and for the same reason. Widened from `planning_cycle_schedules_user_story`, * whose story-only endpoint could not hold the `task` a tracker import * actually produces; see `UPG_EDGE_MIGRATIONS` for the rename rule. * 14. **Project membership** (0.33.0): a project delivers work across the same * bounded {feature, epic, user_story, task, bug} family as families 7 and 13. * Widened from `project_delivers_epic`, whose epic-only endpoint left 651 * measured project memberships stranded on a vendor property because the * type a tracker import actually produces is `task`. The verb stays * `delivers` and NOT `contains`, because a containment verb would oblige a * `UPG_VALID_CHILDREN` pair that a wildcard endpoint can never supply; the * parent axis is containment and the project axis is a reference. * 15. **Document transclusion** (0.34.0): a document embeds a node's live value * at a position in its prose. The only family whose target set is open by * NATURE rather than by a bounded enumeration nobody wanted to write out: * anything renderable can be embedded. Deliberately NOT merged with the * nine-member `document_describes_*` family, which says the document is ABOUT * a thing rather than that it renders it. * 16. **Risk exposure** (0.35.0): what a risk puts at stake, and what mitigates * it. Deliberately mirrors family 2 ("decision-to-anything") with `risk` in * the source slot, for the same reason: both target sets are open (outcome, * key_result, release, service, contract, launch on one side; decision, * feature, experiment, security_control on the other) and neither endpoint * carries a structural role. Both `cross-domain`, so the containment tree is * untouched. The typed `security_control_mitigates_threat` is NOT absorbed: * it stays the security-domain edge, and this is the product generalisation. * * Adding a new polymorphic edge requires extending this array AND naming the * family it joins in `UPG_POLYMORPHIC_EDGE_FAMILIES` below, which the * spec-integrity regression test asserts against. That forces a conscious * decision and keeps consumers (MCP, Entopo, audit tools, the editorial gate) * able to enumerate the full set and attribute a wildcard edge to real types. */ declare const UPG_POLYMORPHIC_EDGE_KEYS: readonly _UPGEdgeTypeLocal[]; /** * The partition of `UPG_POLYMORPHIC_EDGE_KEYS` into named families. * * @remarks * WHY THIS IS AN EXPORT AND NOT A TEST LITERAL. It lived inside * `spec-integrity.test.ts` until 0.34.0, which made it unreachable by every * consumer that needs it, and there is now a real one: `check:editorial` matches * `source_type === type` and `target_type === type` LITERALLY when it derives an * entity's attached-edge set, so an edge whose endpoint is the `node` wildcard is * attributed to nobody. `project_delivers_work_item` moved zero fingerprints on * `task`, `bug`, `user_story` or `feature`. A gate cannot fix that without a map * from a wildcard key to the types the family actually ranges over, and duplicating * the map in the gate would recreate the drift the test was written to catch. * * THE MEMBER LIST IS THE KEYS, NOT THE TYPES. A family names which polymorphic * KEYS belong together. What a consumer does with that — which concrete entity * types to attribute a wildcard edge to — is its own decision, made against the * catalog. This export does not pretend to answer it. * * The prose list in the JSDoc above drifted once already: it still read "Eight * semantic families" after the ninth and tenth had shipped. `spec-integrity` * asserts this object against `UPG_POLYMORPHIC_EDGE_KEYS` in both directions, so * a new key fails until its author names the family it joins. */ declare const UPG_POLYMORPHIC_EDGE_FAMILIES: Readonly>; /** * True if the edge uses the `'node'` wildcard at either endpoint. Derived * dynamically from `source_type`/`target_type`, not from the allow-list, * so an accidentally-added polymorphic edge still returns true and surfaces * via the invariant test rather than silently passing. * * @example * isPolymorphicEdge('node_owned_by_team') // → true (source is 'node' wildcard) * isPolymorphicEdge('persona_pursues_job') // → false (both endpoints typed) */ declare function isPolymorphicEdge(key: _UPGEdgeTypeLocal): boolean; /** * True if the edge is in the registered polymorphic allow-list. * * @example * isRegisteredPolymorphicEdge('node_owned_by_team') // → true * isRegisteredPolymorphicEdge('persona_pursues_job') // → false */ declare function isRegisteredPolymorphicEdge(key: _UPGEdgeTypeLocal): boolean; /** * True if this edge type opts into the gated edge-property model * (`carries_properties: true` in its catalog definition). Only such edges may * carry `properties` on their instances; validators reject `properties` on any * other edge, keeping plain semantic edges payload-free. * * Accepts any string for ergonomic call sites (unknown types return false). * * @example * edgeCarriesProperties('framework_exercise_includes_node') // → true * edgeCarriesProperties('persona_pursues_job') // → false */ declare function edgeCarriesProperties(type: string): boolean; /** * The typed property schema for a `carries_properties` edge, or `undefined` if * the edge type declares none (it then accepts an unvalidated `properties` bag). * Writers use this to reject unknown keys; validators use it to range-check * typed values. 0.10.4. */ declare function getEdgePropertySchema(type: string): PropertySchema | undefined; /** * UPG Edge type union and `UPGEdge` interface. * `UPGEdgeType` is derived from `UPG_EDGE_CATALOG` via `keyof typeof`. * https://unifiedproductgraph.org/spec | MIT */ /** Union of all edge type keys from the canonical edge registry */ type UPGEdgeType = keyof typeof UPG_EDGE_CATALOG; /** * Where an edge came from, when it was not authored directly. * * @remarks * WHY UNGATED, and the precedent settles it. The obvious objection is that this * belongs behind `carries_properties`. It does not: `mapping_confidence` is * ALREADY an ungated provenance field on every edge. The `carries_properties` * gate exists to keep semantic edges free of domain PAYLOAD — a MoSCoW bucket, a * RICE score, a canvas slot, facts about the relationship's content. Provenance * is not content; it is a fact about the record, in the same class as the field * already sitting ungated beside it. Gating it would mean an edge has to opt in * to having a history, which is the wrong shape for a universal fact and would * make an emission pass check whether the edge it is writing is allowed to * remember anything. * * WHY NESTED, and it is a collision rather than a preference. `UPGEdge.source` * already means the source NODE. A flat `source_id` copied from the node side * would sit one character from it and mean something entirely different — the * exact misreading generator `external_refs` was rejected for at 0.33.0. Nesting * removes the collision completely and costs one field. * * WHY IT EXISTS AT ALL. The node side has carried `source_id`, `source_type`, * `mapping_confidence`, `external_tool`, `external_ref`, `external_id` and, since * 0.33.0, `created_at` / `updated_at`. A node knows where it came from; an edge * knew only how sure the importer was. A bulk emission of 651 derived edges had * nowhere ON THE EDGE to record derived-by-what, from-which-fact, or when, so the * record lived in a sidecar ledger outside the graph. That is the sibling of the * question documents-as-entities exists to answer: provenance belongs to the * thing, not beside it. * * NOT a substitute for `mapping_confidence`, which answers a different question * (how sure an import was that this edge TYPE was right) and stays where it is. */ interface UPGEdgeProvenance { /** The tool or pass that derived it. Same posture as `UPGBaseNode.external_tool`. */ tool?: string; /** The fact it was derived FROM: a source field, a property key, a ledger id. */ from?: string; /** ISO timestamp of the derivation. */ at?: ISODateTime; } /** Interface for a semantic relationship between two nodes */ interface UPGEdge { /** Unique identifier within the graph */ id: string; /** Source node ID */ source: string; /** Target node ID */ target: string; /** The semantic relationship type. Must be a key from UPG_EDGE_CATALOG. */ type: UPGEdgeType; /** Confidence level if this edge type was inferred during import */ mapping_confidence?: UPGMappingConfidence; /** * Edge-scoped properties. Permitted ONLY on edge types whose catalog * definition sets `carries_properties: true`. `framework_exercise_includes_node` * is the archetype, storing a framework's per-entity result here (a MoSCoW * bucket, a RICE score, a canvas slot, a funnel stage). A value that exists * only within a specific exercise of a framework is a fact about the * *relationship*, not about either endpoint, so it belongs on the edge. Plain * semantic edges stay payload-free; validators reject `properties` on edges * that do not opt in. * * @remarks * CORRECTED 0.34.0. This said the gate applies "currently the * `framework_exercise_includes_node` edge". ELEVEN catalog edges set * `carries_properties: true`. One word — "currently" — was doing the work of a * gate nobody wired, and the sentence had been wrong for ten mints. The set is * derivable from the catalog and is not restated here, for the reason this * correction exists: a hand-maintained census in a comment is a claim with * nothing recomputing it. * * NOT the home for provenance. See `provenance` below: a fact about the record * is not domain payload, and requiring an opt-in to have a history is the wrong * shape. */ properties?: Record; /** Where this edge came from, when it was not authored directly. */ provenance?: UPGEdgeProvenance; } /** * UPG Document format. `UPGDocument` is the portable interchange shape for a product's knowledge graph. * https://unifiedproductgraph.org/spec | MIT */ /** * Identifies the tool that produced a `UPGDocument`. Every exporter stamps * one of these into the document's `source` field so round-trips stay * auditable. * * @example * const source: UPGSource = { * tool: 'entopo', * tool_version: '0.3.1', * workspace_id: 'ws_acme_main', * } */ interface UPGSource { /** The tool that exported this document */ tool: string; /** Optional tool version */ tool_version?: string; /** Optional workspace or project identifier in the source tool */ workspace_id?: string; } /** * The root product described by a `UPGDocument`. All nodes in the document * belong to this product; portfolio documents carry an array of these. * * @example * const product: UPGProduct = { * id: 'entopo', * title: 'Entopo', * description: 'The product creation tool: canvas + AI + graph, built on UPG.', * stage: 'beta', * } */ interface UPGProduct { /** Unique identifier for the product within the document */ id: string; /** Human-readable name of the product */ title: string; /** Optional longer description of what the product does */ description?: string; /** Product lifecycle stage. Where this product is in its journey. */ stage?: UPGProductStage; } /** Product lifecycle stages. Covers the full arc from napkin idea to end-of-life. */ type UPGProductStage = 'concept' | 'validation' | 'build' | 'beta' | 'launch' | 'growth' | 'mature' | 'maintenance' | 'sunset'; /** * Portfolio-native cross-product edge types (0.17.3): relationships that exist ONLY * across products within a portfolio and have NO within-graph catalog entry, so they * cannot derive from a catalog flag the way the dual-registered set does. This is the * stable half of the whitelist (these change rarely); the growing, dual-registered * half is derived from the `cross_product_eligible` catalog flag — see * `CrossProductEligibleEdgeType` and `UPG_CROSS_ELIGIBLE_CATALOG_EDGE_TYPES`. * * @remarks * DECLARED AND UNENFORCED (recorded 0.34.0, exhaustively). No WRITER reads this * array. Not the SDK, not the local MCP server, not the cloud server, not the * graph service. Outside `node_modules` and `dist` it is referenced by its own * definition, two prose comments, one spec test, `check-editorial.mjs`, one * generated mirror, and documents. The tier is a declared vocabulary that nothing * validates at write time, so a graph can carry one of these types anywhere and no * gate objects. * * That is worth stating rather than leaving to be rediscovered, because it changes * what a "non-colliding key" would buy. A within-graph edge minted under a key that * does not collide with one of these would not be safe BECAUSE nothing collides at * runtime; it would be safe by discipline. Discipline and a guarantee should not be * confused in a comment that reads like a contract. * * This is the third artifact damaged by reading one of the two edge registries in * isolation, after the vocabulary check (fixed 0.33.0) and the withdrawn 0.33.0 * Item G. The fingerprint half of `check:editorial` was the fourth and is fixed in * 0.34.0. */ declare const UPG_CROSS_ONLY_EDGE_TYPES: readonly ["shares_persona", "shares_competitor", "shares_metric", "shares_job", "shares_need", "depends_on_product", "cannibalises", "succeeds", "hosts", "contributes_to", "rolls_up_to", "instance_of", "area_serves_persona", "area_targets_market_segment", "product_implements_specification", "product_exposes_specification", "feature_conforms_to_specification", "api_contract_speaks_specification", "product_exposes_primitive", "feature_manipulates_primitive", "primitive_stored_as_data_type"]; /** One portfolio-native cross-product edge type (see `UPG_CROSS_ONLY_EDGE_TYPES`). */ type UPGCrossOnlyEdgeType = (typeof UPG_CROSS_ONLY_EDGE_TYPES)[number]; /** * The set of valid cross-product relationship types. The union of two tiers: * - `UPGCrossOnlyEdgeType` — portfolio-native edges with no within-graph form * (the stable half, listed above). * - `CrossProductEligibleEdgeType` — within-graph catalog edges dual-registered * across graphs via the `cross_product_eligible` flag (the derived, self- * maintaining half: flag a catalog entry and it joins this union with no edit * here). Includes the competitive-intel edges, the design-system / brand and * marketing references, org ownership (`node_owned_by_*`), and the strategy / * OKR / measurement laddering that spans the rollup and its product graphs. */ type UPGCrossEdgeType = UPGCrossOnlyEdgeType | CrossProductEligibleEdgeType; /** * Runtime-checkable list of valid cross-product edge types, for validators and * writers that test `edge.type` against the whitelist at runtime. Composed from the * two tiers so a newly-flagged catalog edge flows in with no edit here. Order: * portfolio-native first, then the catalog-derived set in catalog declaration order. */ declare const UPG_CROSS_EDGE_TYPES: readonly UPGCrossEdgeType[]; /** * Reserved pseudo product-id for the portfolio registry tier. Canonical * registry entities are addressed in qualified-id references (and `instance_of` * cross-edge targets) as `registry/{node_id}`. No real product may claim this * id; product creation rejects it. The registry itself lives in the * `registry` section of the portfolio document (`UPGPortfolioDocument.registry`), * not in a product file. */ declare const REGISTRY_PRODUCT_ID: "registry"; /** * A cross-product edge links entities across different products within a portfolio. * The source/target use qualified IDs: `{product_id}/{node_id}`. */ interface UPGCrossEdge { /** Unique identifier within the portfolio document */ id: string; /** Qualified source: `{product_id}/{node_id}` */ source: string; /** Qualified target: `{product_id}/{node_id}` */ target: string; /** Cross-product relationship type */ type: UPGCrossEdgeType; /** Optional source product ID (denormalised for convenience) */ source_product_id?: string; /** Optional target product ID (denormalised for convenience) */ target_product_id?: string; /** Confidence level if this edge was inferred during import */ mapping_confidence?: UPGMappingConfidence; /** * Edge metadata, for cross-edge types declared `carries_properties` in the * edge catalogue (0.10.0, #38). A `feature_rivals_competitor_feature` cross-edge * carries the parity assessment here: `parity_status` / `quality` / `is_gap` / * `assessed_on` / `evidence` / `confidence`. Cross-edge types NOT declared * `carries_properties` reject properties at the write surface. */ properties?: Record; /** * Sanctioned divergence marker (`instance_of` only). When true, registry drift * detection treats an instance title that differs from its canonical as * intentional (an informative product-local name, e.g. "Vercel Platform / SDK" * vs canonical "Vercel"), excluding it from the `title_divergence` count so * `clean` reflects only un-sanctioned drift. */ alias?: boolean; /** * Audience relevance for `area_serves_persona` / `area_targets_market_segment`: * whether the audience is a primary or secondary focus of this area. The * primary-vs-secondary distinction is the core value of the area-to-audience matrix. */ relevance?: 'primary' | 'secondary'; /** * Audience role in this area's context (`area_serves_persona`). Mirrors * `PersonaProperties.audience_role`: the same persona can be a `buyer` for one * area and a `user` for another. */ audience_role?: 'buyer' | 'user' | 'champion' | 'influencer' | 'partner'; } /** * A product area within a portfolio. The organisational axis (who owns * what). Groups products by team or org structure, independent of strategic * theme. Areas may nest via `parent_area_id`. * * @example * const platformArea: UPGProductArea = { * id: 'area_platform', * title: 'Platform', * description: 'Core infrastructure shared across all customer-facing products.', * strategic_priority: 'high', * products: ['entopo', 'upg-cli'], * } * * @example * // Nested sub-area that rolls up to a parent area. * const billingArea: UPGProductArea = { * id: 'area_platform_billing', * title: 'Billing', * parent_area_id: 'area_platform', * strategic_priority: 'medium', * products: ['billing-service'], * } */ interface UPGProductArea { /** Unique identifier for the product area */ id: string; /** Human-readable name of the product area */ title: string; /** Optional longer description of the area's scope and ownership */ description?: string; /** Parent area ID for nesting (sub-areas) */ parent_area_id?: string | null; /** Strategic priority (mirrors the canonical `Priority` scale) */ strategic_priority?: 'urgent' | 'high' | 'medium' | 'low' | 'none'; /** Person or team that owns this area */ owner?: string; /** Product IDs that belong to this area */ products?: string[]; } /** * A portfolio grouping. The strategic axis (where we invest). Groups * products by thesis or bet, independent of ownership. Portfolios may nest * via `parent_portfolio_id`. * * @example * const growthPortfolio: UPGPortfolio = { * id: 'pf_growth', * title: 'Growth Bets', * description: 'New product investments aimed at new market expansion.', * hierarchy_model: 'flat', * products: ['entopo', 'upg-cli'], * } */ interface UPGPortfolio { /** Unique identifier for the portfolio */ id: string; /** Human-readable name of the portfolio */ title: string; /** Optional longer description of the portfolio's strategic focus */ description?: string; /** Parent portfolio ID for nesting (sub-portfolios) */ parent_portfolio_id?: string | null; /** How products are structured within this portfolio */ hierarchy_model?: 'flat' | 'nested' | 'matrix'; /** * Investment posture / grouping (UPG 0.9.27; extended 0.17.x). `owned` = * products we build and manage (the default: coverage, health, and * product-spine anti-patterns apply). `watched` = an externally-monitored * landscape such as competitor intelligence graphs, which must NOT be judged * by product-management expectations or drag portfolio health. `strategic`, * `internal`, and `gtm` are owned-side groupings (e.g. a Go-to-Market * portfolio of revenue operating_functions, an Internal portfolio of support * functions); they classify the portfolio but, like `owned`, do not relax * product grading. Only `watched` does. Absent is treated as `owned`. */ kind?: UPGPortfolioKind; /** Product IDs that belong to this portfolio */ products?: string[]; } /** * Closed set of portfolio kinds (0.17.x extends the 0.9.27 owned/watched pair, * closing gap G2 / spec-issue #39). Only `watched` relaxes product grading; * `strategic` / `internal` / `gtm` are owned-side classifications. */ declare const UPG_PORTFOLIO_KINDS: readonly ["owned", "watched", "strategic", "internal", "gtm"]; type UPGPortfolioKind = (typeof UPG_PORTFOLIO_KINDS)[number]; /** * An organisation that owns portfolios and product areas. The top of the * portfolio-document hierarchy; a portfolio document has exactly one. * * @example * const org: UPGOrganization = { * id: 'org_arkheiev', * title: 'Arkheiev UG', * description: 'Builds The Product Creator brand ecosystem.', * logo_url: 'https://theproductcreator.com/logo.svg', * industry: 'Developer Tools', * } */ interface UPGOrganization { /** Unique identifier for the organisation */ id: string; /** Legal or trading name of the organisation */ title: string; /** Optional longer description of what the organisation does */ description?: string; /** URL of the organisation's logo. Used in portfolio and org-level rendering. */ logo_url?: string; /** Industry sector the organisation operates in */ industry?: string; } /** * The canonical shared-entity registry: the portfolio's shared vocabulary tier. * * Entities shared across products (personas, metrics, competitors, * market_segments, ...) are defined ONCE here as authoritative nodes; each * product's local instance links to the canonical via an `instance_of` * cross-edge (`registry/{node_id}` target). This is a third conceptual tier * above products: product graphs hold instances, the portfolio holds org * structure, and `registry` holds the shared vocabulary. * * A canonical entity is just a normal `UPGBaseNode` — canonical-ness is * conferred by living in the registry, not by a new type or flag. `edges` is * reserved for future canonical-internal structure (e.g. a canonical persona * pursuing a canonical job) and is optional; v1 tooling operates on `nodes`. * * @example * const registry: UPGRegistry = { * nodes: [ * { id: 'persona_developer', type: 'persona', title: 'Developer', * properties: { audience_role: 'user' } }, * ], * } */ interface UPGRegistry { /** Canonical shared entities. Each is a normal node addressed as `registry/{id}`. */ nodes: UPGBaseNode[]; /** Reserved: canonical-internal relationships. Optional; unused by v1 tooling. */ edges?: UPGEdge[]; } /** * Checksum-backed tamper-evidence metadata stamped into a `UPGDocument` at * save time by the MCP server. Consumers re-hash on load and compare. * * @example * const integrity: UPGIntegrity = { * checksum: 'a3f1c9e2b7d4806f1a5c3b2e8d9f4c17', * verified_at: '2026-04-17T09:42:11.004Z', * verified_by: 'upg-mcp-server@0.2.0', * } */ interface UPGIntegrity { /** SHA-256 checksum of nodes + edges content (hex, first 32 chars) */ checksum: string; /** ISO 8601 timestamp when checksum was computed */ verified_at: string; /** Tool that computed the checksum */ verified_by: string; } /** * A UPGDocument is a portable, versioned snapshot of a product's knowledge graph. * * How the format works: * * Every node carries its source identity (source_id + source_type + source.tool), * so the original record stays traceable and round-trips work. * * Every mapped entity carries `mapping_confidence`. When a source type doesn't * map cleanly, the confidence value records how uncertain the mapping is. * * `UPGEntityType` is a closed union. Adapters map unknown source types to the * nearest UPG type and preserve the original in `source_type` for auditing. * * Extra properties pass through in `properties` as they were received. * * `upg_version` records the spec version. Tools use it for forward compatibility. */ interface UPGDocument { /** Spec version (semver string, e.g. "0.1"). */ upg_version: string; /** ISO 8601 timestamp of export */ exported_at: string; /** The tool that produced this document */ source: UPGSource; /** The root product */ product: UPGProduct; /** All nodes in the graph */ nodes: UPGBaseNode[]; /** All edges connecting nodes */ edges: UPGEdge[]; /** * Workspace member kind (0.10.0, #45). `product` (default / absent) = a product * under management; `org_rollup` = the company umbrella graph (org-level vision * and OKRs, not a shippable product); `watched` = an externally monitored * intelligence graph (e.g. a competitor); `operating_function` = a function a * team operates (revenue / success / finance / people / marketing) rather than a * product it ships — no product spine, graded on a function validation profile * (0.17.0). Serialised to `$upg.member_kind`; cached in workspace.json + the * portfolio registry for enumeration and counts. */ member_kind?: 'product' | 'org_rollup' | 'watched' | 'operating_function'; /** Integrity checksum. Set by the MCP server on save, verified on load. */ _integrity?: UPGIntegrity; } /** * A UPGPortfolioDocument is a portable, versioned snapshot of a multi-product portfolio. * * It extends the single-product format with: * - An organisation root * - Product areas (the organisational axis: who owns what) * - Portfolios (the strategic axis: where we invest) * - Multiple products, each with their own nodes and edges * - Cross-product edges that link entities across products * * The format is additive. Single-product `.upg` files (UPGDocument) remain valid. */ interface UPGPortfolioDocument { /** Spec version (semver string, e.g. "0.2"). */ upg_version: string; /** Document type discriminator */ type: 'portfolio'; /** ISO 8601 timestamp of export */ exported_at: string; /** The tool that produced this document */ source: UPGSource; /** The organisation that owns this portfolio */ organization: UPGOrganization; /** Product areas (the organisational axis). */ product_areas: UPGProductArea[]; /** Portfolios (the strategic axis). */ portfolios: UPGPortfolio[]; /** All products in the portfolio, each with their own nodes and edges */ products: Array; /** Cross-product edges linking entities across products */ cross_edges: UPGCrossEdge[]; /** * The canonical shared-entity registry (shared-vocabulary tier). Optional and * additive: portfolio documents without a registry remain valid, and an empty * registry is omitted rather than serialised. Product instances reference * canonical nodes here via `instance_of` cross-edges (`registry/{node_id}`). */ registry?: UPGRegistry; /** * Append-only classification-history stream (UPG 0.11.0). Each entry is a * `competitor_signal` node with `signal_type: 'reclassification'`, auto-emitted * at the classify-write chokepoint when a competitor moves between * classification cells on an axis. Kept here (a portfolio-scoped collection) * rather than in `registry.nodes` so it never pollutes the canonical-vocabulary * tier or the landscape/tree reads, and rather than in a product graph so the * emit stays atomic with the portfolio cross-edge write. Read by * `diff_classification`. Optional and additive: omitted when empty; portfolio * documents without it remain valid. */ signals?: UPGBaseNode[]; } /** * Legacy product-stage aliases (UPG-509 Part 2). * * Existing graphs (`entopo.upg`, `nimbus.upg`, `maximum-minimum.upg`, * `notion-saturated.upg`, etc.) carry `product.stage` values that pre-date * the canonical `UPGProductStage` enum * (`concept | validation | build | beta | launch | growth | mature | * maintenance | sunset`). The `create_product` write path rejects these as * legacy; this module surfaces the migration as a pure, reusable helper so * loaders can canonicalise them at the read boundary. * * The richer `coerceProductStage` lives in `intelligence/product-stage-coercion.ts` * and returns a structured `{ canonical, wasCoerced, wasUnknown }` shape for * load-time warnings. `migrateProductStage` is the simpler shape: take a * stage string, return its canonical form (or the input verbatim if it is * already canonical and not a known legacy alias). * * Mapping rationale: * - `idea → concept`: pre-canonical alias from early v0.1 product nodes; * matches the v0.2.13 `properties.stage` migration `value_map` and the * `UPG_PRODUCT_STAGE_COERCION_MAP` documented mapping. * * Future legacy aliases (`mvp → build`, `production → launch`, etc.) should * be added here as authoritative one-to-one mappings. Anything fuzzier (case * normalisation, multi-source coercion targets) belongs in the * `coerceProductStage` helper. * * @module catalog/legacy-product-stages */ /** * Authoritative legacy → canonical `UPGProductStage` map. Append-only; * removing an entry breaks read-side compatibility with existing graphs. * * Keys are lower-case legacy values; canonical values are the current * `UPGProductStage` literals. */ declare const LEGACY_PRODUCT_STAGES: Readonly>; /** * Canonicalise a stage value via `LEGACY_PRODUCT_STAGES`. * * - Returns the canonical stage when `stage` is a known legacy alias. * - Returns `stage` verbatim when it is not a known legacy alias (the input * may itself already be canonical, or genuinely unknown. The strict * write path is responsible for rejecting genuinely-unknown values). * - Returns `undefined` for `undefined` / `null` inputs. * * Lookups are case-insensitive on the input. * * @example * migrateProductStage('idea') // → 'concept' * migrateProductStage('IDEA') // → 'concept' * migrateProductStage('concept') // → 'concept' (already canonical, passthrough) * migrateProductStage('launch') // → 'launch' (already canonical, passthrough) * migrateProductStage('xyz') // → 'xyz' (unknown, passthrough; callers decide) * migrateProductStage(undefined) // → undefined */ declare function migrateProductStage(stage: T): T extends string ? UPGProductStage | string : undefined; /** * True when `stage` is a known legacy alias (would migrate to a different * canonical value). Useful for loaders that want to warn-and-rewrite on * legacy data. * * @example * isLegacyProductStage('idea') // → true * isLegacyProductStage('concept') // → false */ declare function isLegacyProductStage(stage: unknown): stage is string; /** * The authored one-line description of every entity type. * * WHY THIS LIVES IN THE SPEC PACKAGE (0.34.1). It used to live only in the * documentation site's generator, which is an application. That put the one * sentence explaining what a type IS somewhere no consumer of the format could * reach: `get_entity_schema` returned `type, domain, expected_properties, * edges_out, edges_in, domain_guide` and no description, and * `get_catalog_entry({ kind: 'entity_meta' })` returned `name, type_id, * maturity, since, domain_id` and no description. The MCP server's own * instructions say "Before creating an entity or edge, call `get_entity_schema` * for the type", and the sentence that answers the question the caller actually * has was the one thing that call did not return. * * It also meant 0.32.1's editorial gate — which turns a release red until every * type has an authored description, after 0.32.0 shipped `capture` with the * generator's fallback text — protected a surface no agent could see. * * PULLED FORWARD BY NECESSITY, and worth saying plainly: promoting these to a * public export was a banked item, not a planned part of this patch. There is no * other way for an agent surface to return an authored description, because the * only authored copy was in an app. The alternative was a second copy in this * package, and a second copy of a hand-maintained string table is the exact * defect the derived base-node field list exists to prevent. * * SINGLE SOURCE. The site generator now READS this table instead of holding its * own; `check:generated` proves the emitted site data is unchanged by the move. * A new entity type without an entry here is caught by `check:editorial`. * * Descriptions are prose for a human reader and carry no contract. The * normative statements about a type are its properties, its edges and its * lifecycle. */ declare const UPG_ENTITY_DESCRIPTIONS: Readonly>; /** * The authored description for an entity type, or `undefined`. * * A lookup rather than direct indexing so a caller handed a runtime string does * not have to assert the key exists. */ declare function getEntityDescription(type: string): string | undefined; /** * UPG Entity Type Metadata. Immutable `type_id`, human-readable `name`, * `maturity` (draft → proposed → stable → deprecated → removed), and version * tracking (`since`, `deprecated_in`, `replacement`). * * ## Maturity Promotion Rubric * * Promote `proposed → stable` when all of: * * 1. Referenced by ≥2 framework slots in `src/frameworks/definitions/`. * 2. Carries ≥3 properties in `UPG_PROPERTY_SCHEMA`. * 3. Has a lifecycle in `UPG_LIFECYCLES` or is documented lifecycle-free. * 4. No rename, merge, or restructure ticket open for 30+ days. * * Demote `proposed → deprecated` when: * * - 2 minor versions pass without a framework reference. * - Canonical overlap with a newer type. * * `scripts/audit-proposed-promotion.ts` checks the rubric. * * https://unifiedproductgraph.org/spec | MIT */ type UPGEntityTypeMaturity = 'draft' | 'proposed' | 'stable' | 'deprecated' | 'removed'; /** * Identity + lifecycle metadata for a single UPG entity type. * * @example * const personaMeta: EntityTypeMeta = { * name: 'persona', * type_id: 'ent_016', * maturity: 'stable', * since: '0.1.0', * } * * @example * // Deprecated type: readable in .upg files, migrates on write. * const painPointMeta: EntityTypeMeta = { * name: 'pain_point', * type_id: 'ent_018', * maturity: 'deprecated', * since: '0.1.0', * deprecated_in: '0.1.0', * replacement: 'need', * } */ interface EntityTypeMeta { /** Human-readable type name (e.g. 'need'). May change across versions. */ name: string; /** Immutable identifier (e.g. 'ent_313'). Never changes, even if name changes. */ type_id: string; /** Current maturity level */ maturity: UPGEntityTypeMaturity; /** UPG version when this type was introduced */ since: string; /** UPG version when this type was deprecated (if applicable) */ deprecated_in?: string; /** UPG version when this type was removed (if applicable) */ removed_in?: string; /** Canonical replacement type (if deprecated) */ replacement?: string; /** * Frameworks usually applied to this type, by `UPGFramework.id`. A * declarative, type-level affordance ("these lenses are usually applied to * me"). Runtimes may offer them as default scoring/structuring exercises on * creation. The score lives on the framework application (a `framework_exercise` * includes-edge), never on the entity. Not field-absorption: this is a pointer, * not embedded columns. */ default_frameworks?: string[]; /** * Portfolio-shared tier (0.18.0). True for the ~26 canonical / above-product / * registry-hostable entity types a *second* product graph can legitimately point * at — the strategy spine + measurement (outcome, objective, key_result, metric, * vision, mission, strategic_theme, strategic_pillar, initiative, capability, * dependency), competitive-intel canon (competitor, competitor_feature, * competitor_signal, classification_value, classification_axis, market_segment), * design/brand foundation atoms (design_system, design_component, brand_identity), * cross-team org (team, department), and the registry / foundations (specification, * primitive, operating_lifecycle, operating_stage). * * This is a NEW axis, orthogonal to region/domain/classification — none of those * encode "shareable across product graphs". It is the derived guardrail for * cross-product edge eligibility: an edge is *cross-capable* iff ≥1 endpoint type * is `portfolio_shared` (see `isCrossCapable` / `crossProductScope`). Product-local * types (persona, job, need, assumption, decision, role, stakeholder, `product` * itself as a graph root) are deliberately NOT shared, so their internal * decomposition edges stay resident (hard-rejected cross-product). */ portfolio_shared?: boolean; } declare const UPG_ENTITY_META: readonly EntityTypeMeta[]; /** * Proposed entity types deliberately HELD at `proposed` by an open contract or * ADR, even where the mechanical promotion rubric (≥2 framework refs, ≥3 * properties, lifecycle-or-free) would otherwise pass them. The freeze is a * decision, not an oversight: graduating one means deciding its contract is * settled enough to lift. Single source of truth for both the graduation test * (`maturity-graduation.test.ts`) and the promotion auditor * (`scripts/audit-proposed-promotion.ts`), so the rubric checker never invites a * graduation a contract forbids. Maps each type to its governing decision. */ declare const DEFERRED_PROPOSED_BY_CONTRACT: Readonly>; /** O(1) lookup: type name → metadata */ declare const UPG_ENTITY_META_BY_NAME: ReadonlyMap; /** O(1) lookup: type_id → metadata */ declare const UPG_ENTITY_META_BY_ID: ReadonlyMap; /** All active (non-deprecated, non-removed) type names */ declare const UPG_ACTIVE_TYPES: readonly string[]; /** All deprecated type names */ declare const UPG_DEPRECATED_TYPES: readonly string[]; /** * The portfolio-shared entity types (0.18.0), derived from the `portfolio_shared` * flag on the records above — single source of truth, same derive-from-records * pattern as `UPG_ACTIVE_TYPES`. The 26 canonical / above-product types a second * product graph can reference. Snapshot-guarded (`cross-product-edges.test.ts`) so * adding or removing a shared tag is a deliberate, reviewed change. */ declare const UPG_PORTFOLIO_SHARED_TYPES: readonly string[]; /** * Check if an entity type is portfolio-shared (0.18.0): a canonical / above-product * type a second product graph can point at. The necessary condition for a * cross-product edge — see `isCrossCapable` / `crossProductScope`. * * @example * isPortfolioSharedType('metric') // → true * isPortfolioSharedType('persona') // → false (echoable via shares_persona, not resident) * isPortfolioSharedType('product') // → false (a graph root, not a shared reference target) */ declare function isPortfolioSharedType(name: string): boolean; /** * Check if a type name is deprecated. * * @example * isDeprecatedType('pain_point') // → true (replaced by 'need') * isDeprecatedType('package') // → true (replaced by 'pricing_tier') * isDeprecatedType('persona') // → false * isDeprecatedType('not_a_type') // → false (unknown types are not "deprecated") */ declare function isDeprecatedType(name: string): boolean; /** * Get the replacement type for a deprecated type. * * @example * getReplacementType('pain_point') // → 'need' * getReplacementType('package') // → 'pricing_tier' * getReplacementType('persona') // → undefined (still canonical) */ declare function getReplacementType(name: string): string | undefined; /** * Resolve a type name to its type_id (stable across renames). * * @example * getTypeId('persona') // → 'ent_016' * getTypeId('package') // → 'ent_228' (id survives the rename to 'pricing_tier') * getTypeId('not_a_type') // → undefined */ declare function getTypeId(name: string): string | undefined; /** * Resolve a type_id back to its current name. * * @example * getTypeName('ent_016') // → 'persona' * getTypeName('ent_018') // → 'pain_point' (deprecated; see `getReplacementType`) * getTypeName('ent_9999') // → undefined */ declare function getTypeName(typeId: string): string | undefined; /** * UPG Domains. 36 flat semantic groupings of entity types. * https://unifiedproductgraph.org/spec | MIT */ /** * A semantic domain: a flat grouping of related entity types. * * @example * const userDomain: UPGDomain = { * id: 'user', * label: 'User', * description: 'Who your users are and what drives them.', * types: ['persona', 'job', 'need', 'desired_outcome', 'job_step', 'switching_cost'], * } */ interface UPGDomain { /** Machine-readable domain identifier */ id: string; /** Human-readable domain name */ label: string; /** Short description of what this domain covers */ description: string; /** Entity types in this domain */ types: readonly string[]; } declare const UPG_DOMAINS: readonly [{ readonly id: "strategy"; readonly label: "Strategy"; readonly description: "The high-level direction of your product. Tracks the product itself, vision, mission, strategic themes, strategic pillars, initiatives, and capabilities that define what to build. Outcomes, objectives, and key results (OKRs) measure progress. Metrics quantify success. Assumptions and decisions record the reasoning. Value streams map how value flows. Connects upward to Portfolio and downward to Product Specification and Discovery."; readonly types: readonly ["product", "outcome", "objective", "key_result", "metric", "metric_quality_assessment", "vision", "mission", "strategic_theme", "initiative", "capability", "value_stream", "strategic_pillar", "assumption", "decision", "constraint", "strategic_question"]; }, { readonly id: "user"; readonly label: "User"; readonly description: "Who your users are and what drives them. Personas represent user archetypes. Jobs capture what users are trying to accomplish, broken into job steps. Needs are the gaps users experience. Desired outcomes define what success looks like. Switching costs capture barriers to change. Feeds into Discovery, Experience Design, and Strategy."; readonly types: readonly ["persona", "job", "need", "desired_outcome", "job_step", "switching_cost"]; }, { readonly id: "discovery"; readonly label: "Discovery"; readonly description: "Finding and shaping what to build next. Opportunities capture unmet needs worth pursuing. Solutions are candidate responses to opportunities. Feasibility studies assess viability. Design sprints are time-boxed exploration cycles. Bridges User (unmet needs) and Validation (testing ideas) to produce candidates for Product Specification."; readonly types: readonly ["opportunity", "solution", "feasibility_study", "design_sprint"]; }, { readonly id: "validation"; readonly label: "Validation"; readonly description: "Testing ideas before committing to build them. Hypotheses state testable beliefs. Experiment plans structure the validation design. Experiments run the tests, with experiment runs capturing replication. Evidence captures what was observed. Learnings distill what was understood. Research plans coordinate broader investigation. Consumes opportunities from Discovery and insights from User Research, producing evidence that informs Strategy and Product Specification."; readonly types: readonly ["hypothesis", "experiment", "experiment_plan", "experiment_run", "learning", "evidence", "research_plan"]; }, { readonly id: "market_intelligence"; readonly label: "Market Intelligence"; readonly description: "The competitive landscape your product operates in. Competitors and their competitor features map the field. Market trends track industry shifts. Market segments define addressable audiences. Competitive analyses synthesize the full picture. Classification axes and values express the dimensional structure of the landscape (e.g. CMS Architecture × Editing Paradigm). Informs Strategy (positioning), Go-To-Market (battle cards), and Business Model (differentiation)."; readonly types: readonly ["competitor", "competitor_feature", "competitor_signal", "market_trend", "market_segment", "competitive_analysis", "classification_axis", "classification_value"]; }, { readonly id: "user_research"; readonly label: "User Research"; readonly description: "Primary research with real users. Research studies are the container. Participants are who you talk to. Interview guides structure conversations. Observations capture what you see. Quotes preserve exact words. Survey responses collect structured input. Affinity clusters group patterns. Research questions frame what you want to learn. Insights synthesize findings. Feeds User (persona refinement), Discovery (opportunities), and Validation (hypotheses)."; readonly types: readonly ["research_study", "insight", "participant", "observation", "quote", "affinity_cluster", "research_question", "interview_guide", "survey_response"]; }, { readonly id: "ux_design"; readonly label: "Experience Design"; readonly description: "How users experience and interact with your product. User journeys map end-to-end experiences, broken into journey steps. User flows chart navigation paths. Screens and screen states define what users see. Surfaces name the places inside a screen, who may occupy them, and the rule that settles contention. Design questions frame open problems. Design concepts explore possible solutions. Prototypes and wireframes make ideas tangible. Connects User (who) to Product Specification (what) through Design System (how)."; readonly types: readonly ["user_journey", "journey_step", "journey_phase", "journey_action", "user_flow", "screen", "screen_state", "surface", "design_question", "design_concept", "prototype", "wireframe"]; }, { readonly id: "design_system"; readonly label: "Design System"; readonly description: "The reusable building blocks of your product UI. The design system entity anchors the collection. Design components are the atoms. Design tokens encode colour, spacing, and typography values. Design patterns document recurring solutions. Design guidelines codify usage rules. Annotations mark up designs with notes. Interaction specs define behaviour contracts. Ensures consistency across Experience Design and Engineering. Referenced by Accessibility."; readonly types: readonly ["design_component", "design_token", "design_system", "design_pattern", "design_guideline", "annotation", "interaction_spec"]; }, { readonly id: "brand"; readonly label: "Brand Identity"; readonly description: "Your product's visual and verbal identity. Brand identity is the root entity. Brand colour, brand typography, brand voice, brand logo, and brand imagery define the palette, type system, tone, mark, and visual language. Design System implements these at the component level. Go-To-Market applies them in external communications."; readonly types: readonly ["brand_identity", "brand_colour", "brand_typography", "brand_voice", "brand_logo", "brand_imagery", "brand_asset"]; }, { readonly id: "product_spec"; readonly label: "Product Specification"; readonly description: "What you are building and shipping. Feature areas group related capabilities. Features, epics, and user stories break work down. Acceptance criteria define done. Tasks and bugs track execution. Releases and changelogs mark what shipped. Roadmaps and roadmap items plan what comes next. Roadmap themes group roadmap work around the customer problem it solves, one level down from the strategic themes in Strategy. Planning cycles are the cadence axis: the named, dated, self-nesting intervals (sprint, iteration, quarter, program increment, cooldown) that work is scheduled through. Translates Strategy into Engineering and tracks delivery through Program Management."; readonly types: readonly ["feature", "feature_area", "epic", "user_story", "acceptance_criterion", "release", "task", "bug", "roadmap", "roadmap_item", "roadmap_theme", "changelog", "planning_cycle", "configuration_axis"]; }, { readonly id: "engineering"; readonly label: "Engineering"; readonly description: "The technical architecture and implementation. Bounded contexts, aggregates, domain entities, value objects, commands, read models, and domain events model the domain. Services, API contracts, API endpoints, and database schemas define interfaces. Queue topics and integration patterns connect systems. External APIs and library dependencies track what you consume. Data flows map how information moves. Code repositories and build artifacts track source and output. Feature flags gate rollout. Deployments mark releases. Technical debt items track shortcuts. Investigations, root causes, symptoms, and fixes handle incidents. Relies on DevOps and Security."; readonly types: readonly ["bounded_context", "service", "domain_event", "api_contract", "technical_debt_item", "feature_flag", "deployment", "aggregate", "domain_entity", "value_object", "command", "read_model", "api_endpoint", "database_schema", "queue_topic", "build_artifact", "code_repository", "library_dependency", "integration_pattern", "external_api", "data_flow", "investigation", "root_cause", "symptom", "fix"]; }, { readonly id: "growth"; readonly label: "Growth"; readonly description: "How your product acquires, activates, and retains users. Funnels and funnel steps model conversion paths. Acquisition channels track where users come from. Growth campaigns run targeted experiments. Cohorts group users by behaviour or timing. Behavioral segments slice the user base. Growth loops model self-reinforcing cycles. Variants track A/B test alternatives. Attribution models assign credit across touchpoints. Connects to Data & Analytics, Marketing, and Business Model."; readonly types: readonly ["funnel", "funnel_step", "acquisition_channel", "growth_campaign", "cohort", "behavioral_segment", "growth_loop", "variant", "attribution_model"]; }, { readonly id: "business_model"; readonly label: "Business Model"; readonly description: "How your product creates and captures value. The business model entity anchors the canvas. Value propositions define why customers buy. Revenue streams and pricing tiers model income. Cost structures and unit economics track spend. Partnerships and key resources identify what you need. Key activities define what you do. Target customer segments specify who you serve. Customer relationships describe how you engage. Distribution channels map how you deliver. Connects Strategy to Pricing & Packaging and Sales."; readonly types: readonly ["business_model", "value_proposition", "revenue_stream", "cost_structure", "unit_economics", "partnership", "key_resource", "key_activity", "customer_relationship", "distribution_channel"]; }, { readonly id: "go_to_market"; readonly label: "Go-To-Market"; readonly description: "Your plan to bring the product to market. GTM strategies set the overall approach. Ideal customer profiles define who to target. Positioning and messaging frame how you talk about it. Launches coordinate market entry. Content strategies plan thought leadership. Sales motions define how you sell. Competitive battle cards arm the team. Demand gen programs drive pipeline. Territories segment the market. Objections, rebuttals, and proof points handle resistance. Translates Market Intelligence and Business Model into Marketing and Sales."; readonly types: readonly ["gtm_strategy", "ideal_customer_profile", "positioning", "messaging", "launch", "content_strategy", "sales_motion", "competitive_battle_card", "demand_gen_program", "territory", "objection", "rebuttal", "proof_point"]; }, { readonly id: "team_org"; readonly label: "Team & Organisation"; readonly description: "The people and structure behind the product. Teams are the units. Roles define responsibilities. Stakeholders track who has influence. People name accountable individuals. Team OKRs set team-level goals. Retrospectives capture team learnings. Dependencies map cross-team blockers. Departments structure the org. Skills track capabilities. Ceremonies define recurring rituals. Capacity plans model available effort. Connects to Program Management and Product Specification."; readonly types: readonly ["team", "role", "stakeholder", "person", "team_okr", "retrospective", "dependency", "department", "skill", "ceremony", "capacity_plan"]; }, { readonly id: "data_analytics"; readonly label: "Data & Analytics"; readonly description: "How you measure and understand your product. Data sources define where data comes from. Event schemas standardize tracking. Data models structure the warehouse. Data pipelines move data between systems. Data lineage traces provenance. Data quality rules enforce standards. Data products package data for consumption. Data domains organize ownership. Dashboards and reports visualize insights. Glossary terms align vocabulary. Provides the measurement layer for Growth, Strategy, and Quality Assurance."; readonly types: readonly ["data_source", "event_schema", "dashboard", "data_model", "data_quality_rule", "data_product", "data_pipeline", "data_lineage", "glossary_term", "data_domain", "report"]; }, { readonly id: "content"; readonly label: "Content & Knowledge"; readonly description: "All content your product team creates and manages. Content pieces are individual assets. Knowledge base articles serve users. Brand assets store visual collateral. Internal docs capture team knowledge. Prompt templates standardize AI interactions. Content calendars plan publication. Content themes group editorial focus. Documentation templates ensure consistency. Documents are general-purpose containers. Supports Marketing, Customer Education, and Customer Success."; readonly types: readonly ["content_piece", "knowledge_base_article", "content_calendar", "content_theme", "documentation_template", "document"]; }, { readonly id: "legal"; readonly label: "Legal"; readonly description: "Legal structure and intellectual property protection. Legal entities define corporate structure. IP assets track patents, trademarks, and copyrights. Contracts and contract clauses manage agreements. Privacy policies govern data handling. Connects to Compliance (regulatory requirements), Security (data classification), and Business Model (partnership agreements)."; readonly types: readonly ["legal_entity", "ip_asset", "contract", "contract_clause", "privacy_policy"]; }, { readonly id: "devops"; readonly label: "DevOps & Platform"; readonly description: "The reliability and infrastructure layer. Service level indicators (SLIs) and service level objectives (SLOs) define targets. Error budgets track risk tolerance. Incidents and postmortems handle failures. Runbooks document response procedures. Monitors and alert rules detect problems. CI pipelines automate builds. Release strategies govern rollout. On-call rotations assign responsibility. Infrastructure components model the platform. Supports Engineering and connects to Security and Quality Assurance."; readonly types: readonly ["service_level_indicator", "service_level_objective", "error_budget", "incident", "postmortem", "runbook", "monitor", "alert_rule", "ci_pipeline", "release_strategy", "on_call_rotation", "infrastructure_component"]; }, { readonly id: "security"; readonly label: "Security"; readonly description: "Protecting your product and its users. Threat models map attack surfaces. Threats identify specific risks. Vulnerabilities track known weaknesses. Security controls are the mitigations. Security policies set rules. Penetration tests verify defences. Security reviews assess posture. Data classifications label sensitivity. Access policies govern who can reach what. Connects to Engineering, Compliance, and DevOps."; readonly types: readonly ["threat_model", "threat", "vulnerability", "security_control", "security_policy", "penetration_test", "security_review", "data_classification", "access_policy"]; }, { readonly id: "accessibility"; readonly label: "Accessibility"; readonly description: "Ensuring your product is usable by everyone. A11y standards define the bar (WCAG, etc.). A11y guidelines translate standards into actionable rules. A11y audits assess compliance. A11y issues track violations. A11y annotations mark up designs with accessibility notes. Connects to Design System (component compliance), Experience Design (journey inclusion), and Quality Assurance (testing coverage)."; readonly types: readonly ["a11y_standard", "a11y_guideline", "a11y_audit", "a11y_issue", "a11y_annotation"]; }, { readonly id: "testing"; readonly label: "Quality Assurance"; readonly description: "Verifying your product works correctly. Test plans define the verification approach (scope, environments, pass criteria). Test suites group related tests. Test cases define individual checks. QA sessions capture exploratory testing. Regression tests guard against regressions. Test coverage reports measure completeness. Test environments define where tests run. Test results record outcomes. Validates Engineering (code quality) and Product Specification (acceptance criteria). Feeds DevOps (release confidence)."; readonly types: readonly ["test_plan", "test_suite", "test_case", "qa_session", "regression_test", "test_coverage_report", "test_environment", "test_result"]; }, { readonly id: "feedback"; readonly label: "Customer Feedback"; readonly description: "The voice of your customers after they use your product. Feedback programs are the containers. Feature requests capture what customers want. Feedback votes quantify demand. NPS campaigns measure satisfaction. User advisory boards provide structured input. Beta programs test with early adopters. Feedback themes group recurring patterns. Feeds User Research (patterns), Discovery (opportunities), and Strategy (priorities)."; readonly types: readonly ["feedback_program", "feature_request", "feedback_vote", "nps_campaign", "user_advisory_board", "beta_program", "feedback_theme"]; }, { readonly id: "pricing"; readonly label: "Pricing & Packaging"; readonly description: "How you package and price your product. Pricing strategies set the overall approach. Pricing tiers define what customers buy, bundling features, trials, gates, and discounts. Discount strategies manage promotions. Trial configs define free-to-paid conversion mechanics. Paywalls gate premium features. Connects Business Model (revenue streams) to Growth (conversion optimization) and Sales (deal structure)."; readonly types: readonly ["pricing_strategy", "pricing_tier", "discount_strategy", "trial_config", "paywall"]; }, { readonly id: "ai"; readonly label: "AI & Machine Learning"; readonly description: "AI and machine learning capabilities within your product. AI models track deployed models. Prompt templates define reusable prompts; prompt versions manage their evolution. Eval benchmarks and eval runs measure quality. AI cost trackers monitor spend. Hallucination reports flag reliability issues. AI guardrails set safety boundaries. Model comparisons evaluate alternatives. AI experiments test new approaches. AI datasets track training data. AI traces log inference chains. Connects to Engineering, Data & Analytics, and Product Specification."; readonly types: readonly ["ai_model", "prompt_version", "eval_benchmark", "eval_run", "ai_cost_tracker", "hallucination_report", "ai_guardrail", "model_comparison", "ai_experiment", "ai_dataset", "ai_trace", "prompt_template"]; }, { readonly id: "automation"; readonly label: "Workflows & Agents"; readonly description: "Automated processes and AI agents that operate on your product graph. Workflow templates define reusable processes. Workflow runs are executions. Workflow artifacts are outputs. Agent definitions describe autonomous agents. Agent sessions track their work. Agent skills define capabilities. Agent hooks wire triggers. Agent tasks are discrete units of agent work. Review gates enforce human checkpoints. Approval records log decisions. Extends Engineering and AI to reduce manual work across all domains."; readonly types: readonly ["workflow_template", "workflow_run", "agent_definition", "agent_session", "review_gate", "approval_record", "agent_skill", "agent_hook", "workflow_artifact", "agent_task"]; }, { readonly id: "portfolio"; readonly label: "Portfolio"; readonly description: "Multi-product management and organisational hierarchy. Organizations are the top-level entity. Portfolios group products by strategic axis (where you invest). Product areas group products by organisational axis (who owns what). Provides the container for Strategy (per-product direction) and enables cross-product edges that connect shared users, features, and infrastructure."; readonly types: readonly ["organization", "portfolio", "product_area"]; }, { readonly id: "sales"; readonly label: "Sales & Revenue"; readonly description: "Revenue operations from lead to invoice. Accounts represent companies. Contacts are people within accounts. Leads track inbound interest. Deals are active opportunities. Pipeline sales and pipeline stages model the sales funnel. Quote documents formalize offers. Subscriptions track recurring revenue. Invoices record billing. Forecasts project revenue. Connects Go-To-Market and Pricing & Packaging to Business Model."; readonly types: readonly ["account", "contact", "lead", "deal", "pipeline_sales", "pipeline_stage", "quote_document", "subscription", "invoice", "forecast"]; }, { readonly id: "program_mgmt"; readonly label: "Program Management"; readonly description: "Coordinating delivery across teams and timelines. Programs are the highest container. Projects break programs down. Milestones mark key dates, and hang from either a project or, where a product owns the date outright with no program above it, from the product itself. Risk registers track threats to delivery. Change requests manage scope changes. Deliverables define what ships. Resource allocations assign effort. Status reports communicate progress. Connects Product Specification (what to deliver) to Team & Organisation (who delivers)."; readonly types: readonly ["program", "project", "milestone", "risk_register", "change_request", "deliverable", "resource_allocation", "status_report"]; }, { readonly id: "marketing"; readonly label: "Marketing"; readonly description: "Executing campaigns that reach your audience. Marketing strategies set direction. Marketing channels define where you reach people. Marketing campaign plans coordinate execution. Email sequences nurture leads. Social posts engage audiences. SEO keywords target search. Ad creatives drive paid acquisition. Press releases announce news. Events create in-person touchpoints. Community initiatives build grassroots engagement. Implements Go-To-Market and feeds Growth."; readonly types: readonly ["marketing_strategy", "marketing_channel", "marketing_campaign_plan", "email_sequence", "social_post", "seo_keyword", "ad_creative", "press_release", "event", "community_initiative"]; }, { readonly id: "customer_success"; readonly label: "Customer Success"; readonly description: "Keeping customers healthy and reducing churn. Support tickets track issues. Customer feedback captures post-sale voice. Churn reasons explain why customers leave. Customer health scores quantify account risk. Playbooks codify response patterns. Service level agreements set expectations. Customer journey stages map the post-sale arc. Touchpoints track every interaction. Success milestones mark key achievements. Service blueprints model the full service delivery. Connects Customer Feedback to Growth and Product Specification."; readonly types: readonly ["support_ticket", "customer_feedback", "churn_reason", "customer_health_score", "playbook", "service_level_agreement", "customer_journey_stage", "touchpoint", "success_milestone", "service_blueprint"]; }, { readonly id: "localisation"; readonly label: "Localisation"; readonly description: "Adapting your product for global audiences. Locales define supported languages and regions. Translation keys are individual translatable strings. Translation bundles group keys for deployment. Locale configs store per-locale settings. Cultural adaptations track region-specific adjustments beyond language. Regional pricing models location-based pricing. Connects to Content & Knowledge, Pricing & Packaging, and Experience Design."; readonly types: readonly ["locale", "translation_key", "translation_bundle", "locale_config", "cultural_adaptation", "regional_pricing"]; }, { readonly id: "education"; readonly label: "Customer Education"; readonly description: "Teaching users how to succeed with your product. Education programs are the containers. Tutorials provide step-by-step instruction. Walkthroughs guide users through features. Webinars deliver live education. Certifications validate mastery. Help videos offer visual guidance. Learning paths sequence content into curricula. Supports Customer Success (onboarding), Content & Knowledge (educational material), and Growth (activation)."; readonly types: readonly ["education_program", "tutorial", "walkthrough", "webinar", "certification", "help_video", "learning_path"]; }, { readonly id: "ecosystem"; readonly label: "Partners & Ecosystem"; readonly description: "The network of partners and integrations around your product. Partner programs define the structure. Partner tiers segment partners by value. API ecosystems track integration surfaces. Marketplace listings manage distribution. Developer portals serve external builders. Integration partners are specific collaborators. Partner revenue shares model economics. Extends Business Model (partnership value) and Engineering (API ecosystem)."; readonly types: readonly ["partner_program", "partner_tier", "api_ecosystem", "marketplace_listing", "developer_portal", "integration_partner", "partner_revenue_share"]; }, { readonly id: "compliance"; readonly label: "Compliance"; readonly description: "Meeting regulatory and governance requirements. Compliance frameworks define which standards apply (SOC 2, GDPR, etc.). Compliance requirements are individual mandates. Risks track exposure. Data contracts formalize data-sharing agreements. Audit log policies govern what gets logged. Security audits assess compliance posture. Connects to Legal, Security, and Data & Analytics."; readonly types: readonly ["compliance_requirement", "risk", "data_contract", "audit_log_policy", "compliance_framework", "security_audit"]; }, { readonly id: "workspace"; readonly label: "Workspace"; readonly description: "Spatial thinking spaces for arranging entities, debating decisions, and committing to the graph. Workspaces are transient canvases that sit alongside all other domains, letting you compose and explore relationships before they become permanent graph structure. A framework exercise is a structured workspace: one run of a framework (MoSCoW, RICE, Kano, …) applied to a chosen set of entities, with each entity's result recorded on the exercise-to-entity edge rather than the entity itself. A composition is the durable other half: a named, published view assembled from a canvas, with a stable slug people link to and a revision history, whose frozen arrangement holds pointers rather than copied content."; readonly types: readonly ["workspace", "framework_exercise", "composition", "capture"]; }, { readonly id: "foundations"; readonly label: "Foundations"; readonly description: "The shared specifications and foundational primitives a product organisation stewards or implements everywhere. Specifications are governed specs (query languages, protocols, data formats, encodings) like NQL or Structured Text; primitives are the compositional units those specs define (a block, a reference, a query value). Both are registry-hostable canonicals that products implement, expose, or conform to. Distinct from a product feature: a specification has no single owner, no P&L, and no buyer; it is the rulebook many products point at."; readonly types: readonly ["specification", "primitive", "operating_lifecycle", "operating_stage"]; }]; /** * Union of every canonical domain identifier, derived from UPG_DOMAINS. * * Adding, removing, or renaming a domain updates this type automatically. * Do NOT maintain a separate list here. */ type UPGDomainId = typeof UPG_DOMAINS[number]['id']; /** * Canonical entity-type → domain-id lookup. O(1) access, derived from * `UPG_DOMAINS` at module init. Never maintained by hand. * * Prefer this over walking `UPG_DOMAINS` in downstream packages: the repeated * `UPG_DOMAINS.find(d => d.types.includes(t))?.id` pattern is a drift risk * (subtle `includes` vs `indexOf` semantics, missing null-guards, etc.). */ declare const UPG_ENTITY_TO_DOMAIN: Readonly>; /** * Get all entity types across all domains. * * @example * const types = getTypes() * types.includes('persona') // → true * types.includes('feature') // → true * types.length // → 300+ (all active types) */ declare function getTypes(): string[]; /** * Look up which domain an entity type belongs to. * * @example * const d = getDomainForType('persona') * // d?.id === 'user' * // d?.name === 'User' * * @example * getDomainForType('not_a_type') // → undefined */ declare function getDomainForType(entityType: string): UPGDomain | undefined; /** * Look up the canonical domain id for a typed entity type. O(1). * * Returns `undefined` only for the degenerate case where an entity type is * absent from every domain, which spec-integrity tests guarantee never * happens for active types. Callers with a `UPGEntityType` can treat the * result as non-null, but the return type keeps the escape hatch for * defensive string-typed call sites. * * @example * getDomainIdForType('persona') // → 'user' * getDomainIdForType('feature') // → 'product_spec' * getDomainIdForType('competitor') // → 'market_intelligence' */ declare function getDomainIdForType(entityType: UPGEntityType): UPGDomainId | undefined; /** * Result of resolving a (possibly deprecated) entity-type input. * - `canonical`: a canonical `UPG_TYPES` member (the input unchanged * when it was already canonical). * - `alias`: set when the input was a deprecated synonym. Carries the * `from → to` trail so callers can surface a warning. */ interface EntityTypeResolution { canonical: string; alias?: { from: string; to: string; }; } /** * Thrown when the input type is neither canonical nor a known alias. * Carries up to 5 Levenshtein-1 suggestions drawn from `UPG_TYPES`. */ declare class UnknownEntityTypeError extends Error { readonly suggestions: string[]; readonly rawType: string; constructor(rawType: string, suggestions: string[]); } /** * Validate a raw entity-type string from a caller. * * Two-tier behaviour: * 1. Already canonical: return `{ canonical }` unchanged. * 2. Deprecated synonym with a known replacement: return * `{ canonical, alias: { from, to } }` so the caller can warn. * 3. Otherwise: throw `UnknownEntityTypeError` with up to 5 * Levenshtein-1 suggestions. * * Every UPG consumer resolves raw caller input through this helper before * touching the catalog so deprecated synonyms get the same warning * treatment everywhere. */ declare function resolveEntityType(rawType: unknown): EntityTypeResolution; /** * UPG Hierarchy. `UPG_VALID_CHILDREN` declares which types are permitted as * direct children of each parent. Drives add menus, validation, and traversal. * Ordering reflects the suggested creation sequence. * https://unifiedproductgraph.org/spec | MIT */ /** The canonical parent → children map. Keys are parent types, values are ordered child types. */ declare const UPG_VALID_CHILDREN: Record; /** * Returns the list of valid child entity types for the given parent type. * Returns an empty array for any type not present in the hierarchy. * * @example * getValidChildren('persona') * // → ['job', 'need', 'switching_cost', 'desired_outcome'] * * @example * getValidChildren('unknown_type') * // → [] */ declare function getValidChildren(entityType: string): readonly string[]; /** * Returns `true` if `childType` is a permitted direct child of `parentType` * according to the UPG hierarchy. * * @example * canBeChildOf('job', 'persona') // → true * canBeChildOf('persona', 'product') // → true * canBeChildOf('product', 'persona') // → false (reversed hierarchy) */ declare function canBeChildOf(childType: string, parentType: string): boolean; /** * Containment-free entity types: types that exist in the graph but are * *referenced* by other nodes (via edges) rather than *contained* by * structural parents. They appear in no `UPG_VALID_CHILDREN` list and * have no hierarchy edges into them. * * This is parallel to `UPG_LIFECYCLE_FREE_TYPES` in `grammar/lifecycles.ts`: * both describe what a type *does not have*. Lifecycle-free types lack a * status progression; containment-free types lack a structural parent. * A type may be one, both, or neither: `person` is both; `theme` is * lifecycle-free but structurally contained; `feature` is neither. * * The G2b hierarchy audit treats absence from `UPG_VALID_CHILDREN` as a * defect *unless* the type is in this set. Use this category for types * that are orthogonal to product structure: identities, references, * cross-cutting concerns that any node might point at. * * `person` is the first explicit member of the set. Additional candidates * (`theme`, `glossary_term`, `learning`, `insight`, `risk`, * `classification_value`) will be audited and added one at a time with * deliberate justification rather than retrofitted in bulk. * * @example * isContainmentFreeType('person') // → true * isContainmentFreeType('feature') // → false (nested under product) * isContainmentFreeType('roadmap_theme') // → false (not yet ratified into the set) */ declare const UPG_CONTAINMENT_FREE_TYPES: ReadonlySet; /** * Returns `true` if `entityType` is containment-free, i.e. it deliberately * has no structural parent in `UPG_VALID_CHILDREN`. See * `UPG_CONTAINMENT_FREE_TYPES` for the rationale and roster. */ declare function isContainmentFreeType(entityType: string): boolean; /** * UPG Configuration Projection (0.30.0). * * The stored graph is the UNION of a configuration family. A single * configuration is a PROJECTION of it, and this is the operator that takes one. * * π(G, C) drops what a named configuration does not contain and leaves * everything else exactly as it was. Facts that carry no configuration * qualification are invariant: they belong to every member of the family, which * is why a graph written before this existed projects to itself under every C * and needs no migration. * * PURE, AND DELIBERATELY STORE-FREE. It takes node and edge arrays and returns * node and edge arrays, so the local file store, the cloud SQL store and a * synthetic store built for validation preview all reuse one definition of what * a projection IS. A projection implemented twice is two projections. * * READ-ONLY. Nothing writes a projected graph back. The union is the file. * * https://unifiedproductgraph.org/spec | MIT */ /** The minimum a node must expose to be projected. */ interface ProjectableNode { id: string; type?: string; properties?: Record | undefined; } /** The minimum an edge must expose to be projected. */ interface ProjectableEdge { id?: string; source: string; target: string; type?: string; properties?: Record | undefined; } /** * A configuration: axis node id to the single value that holds on it. * * PARTIAL BY DESIGN. An axis absent from the map is not applied, so every fact * qualified on it is retained. Projecting on nothing returns the union, which * makes a configuration a strictly narrowing filter an agent can apply * incrementally rather than an all-or-nothing mode. */ type Configuration = Readonly>; /** The edge that declares conditional existence. */ declare const VARIES_BY_EDGE = "surface_varies_by_configuration_axis"; /** The property carried by that edge. */ declare const PRESENT_UNDER_PROPERTY = "present_under"; /** The qualifier property carried by the two composition edges. */ declare const ACTIVE_WHEN_PROPERTY = "active_when"; /** * The only edge types on which `active_when` is legal (D3). * * Scope is enforced, not merely documented: `validate_graph`'s * `configuration_drift` scope reports the qualifier on any other edge type as * an error. The projection operator itself is deliberately permissive here (it * honours a qualifier wherever it finds one) so that a graph carrying an * illegal qualifier still projects predictably while the validator names the * problem. Silently ignoring it would make the drift invisible in the one view * where its effect shows. */ declare const QUALIFIABLE_EDGE_TYPES: readonly string[]; /** Result of a projection: the surviving nodes and edges, plus what it dropped. */ interface ProjectionResult { nodes: N[]; edges: E[]; /** Node ids dropped because the configuration excluded them. */ excluded_node_ids: string[]; /** Count of edges dropped because their own qualifier excluded them. */ deactivated_edge_count: number; /** Count of edges dropped because an endpoint was excluded. */ dangling_edge_count: number; } /** * Read a string array property defensively; anything else reads as absent. * * Exported so the drift checker and the read-tool view share one reading of * what a list-valued configuration property is. Three copies of this drifting * apart is how a value the validator accepts becomes a value the projection * ignores. */ declare function readStringArray(properties: Record | undefined, key: string): string[] | undefined; /** * Read an `active_when` qualifier, or undefined when the edge carries none or * carries something malformed. * * A malformed qualifier reads as ABSENT rather than as "excludes everything". * Absence means the relationship is invariant, so a broken qualifier leaves the * edge in every projection: the graph shows too much rather than too little, * and `configuration_drift` reports the malformation by name. The alternative * would let one bad property silently delete structure from every view. */ declare function readActiveWhen(edge: ProjectableEdge): { axis: string; values: string[]; } | undefined; /** * Project a graph onto one configuration. * * Applied in order, ONCE. The operator is a filter, not a solver: * * 1. NODE EXCLUSION. A node is dropped when it declares variance on an axis * named in `configuration` and the chosen value is not in its * `present_under`. A node that says nothing about a named axis is retained, * because silence means invariant. * 2. EDGE DEACTIVATION. An edge is dropped when its own `active_when` names an * axis in `configuration` and the chosen value is not among its values. * 3. DANGLING REMOVAL. Any surviving edge with an endpoint dropped in step 1 * goes too. * * NO CASCADE, AND THIS IS THE SUBTLE PART. A child surface is NOT dropped * because its parent was. The motivating field case is precisely a surface * whose PARENT changes under the flag (a navigation row splits in two and an * occupant moves into the new row), so cascading would delete a surface that is * genuinely present. A surface that survives with no containment parent in that * projection is a modelling gap for the validator to report, not a deletion for * the operator to guess at. * * COMMUTATIVE ACROSS AXES. Each axis's predicate reads only that axis, so * projecting on two axes in either order, or both at once, gives the same * result. Multi-axis projection therefore needs no ordering rule. * * @example * // A graph where the inspector exists only under the split-nav flag. * projectGraph(nodes, edges, { axis_nav: 'legacy_nav' }).nodes * // → every node except the split-nav-only surfaces */ declare function projectGraph(nodes: N[], edges: E[], configuration: Configuration): ProjectionResult; /** * Every configuration this graph can be projected onto, one axis at a time. * * PER-AXIS, NOT CARTESIAN. The list is the union (an empty configuration) plus * one entry per declared value of each axis: `1 + Σ|values|`, linear in the * declarations. The cartesian product across axes is combinatorial and buys * nothing here, because every v1 qualifier reads a single axis and every check * that consumes a projection is surface-local. Cross-axis interaction is a * stated non-goal; when a detector needs it, this is the function that grows. */ declare function enumerateProjections(nodes: ProjectableNode[]): Array<{ axis?: string; value?: string; configuration: Configuration; }>; /** * UPG Configuration Drift (0.30.0). * * Structural checking of the configuration declarations themselves: that an * axis is well formed, that every value named anywhere is a value the axis * actually declares, that the qualifier appears only where it is legal, and * that a declared alternation is consistent with what the declarations imply. * * DRIFT, NOT AN ANTI-PATTERN. These are contradictions inside the model, the * same family as an unknown entity type or a status outside its lifecycle: a * graph carrying one is saying something it cannot mean. Anti-patterns are the * other thing, judgements about a graph that is internally consistent, and * 0.30.0 deliberately mints none of those (a detector with no field evidence * behind it is how a check family gets noisy). * * Runs on the UNION. The declarations are facts about the whole family, so they * are checked once, not once per projection. The single exception is * `orphaned_under_projection`, which by definition can only be seen by taking * one. * * https://unifiedproductgraph.org/spec | MIT */ /** The kinds of configuration drift the validator reports. */ type ConfigurationDriftKind = 'axis_values_empty' | 'axis_default_not_a_value' | 'present_under_empty' | 'present_under_unknown_value' | 'qualifier_axis_unresolved' | 'qualifier_values_empty' | 'qualifier_unknown_value' | 'qualifier_on_illegal_edge' | 'alternation_axis_mismatch' | 'alternation_overlap' | 'orphaned_under_projection'; /** One configuration-drift finding. */ interface ConfigurationDriftFinding { kind: ConfigurationDriftKind; /** * `error` means the graph contradicts itself and a projection of it cannot be * trusted. `warning` means the graph is coherent but a projection of it has a * gap worth looking at. */ severity: 'error' | 'warning'; /** The node this finding is about, when it is about a node. */ node_id?: string; /** The edge this finding is about, when it is about an edge. */ edge_id?: string; /** The axis involved, where one is identifiable. */ axis_id?: string; /** Human-readable statement of what is wrong. */ message: string; } /** * Check every configuration declaration in a graph. * * @param nodes All nodes in the union. * @param edges All edges in the union. * @returns Findings, in a stable order: axis checks, then variance, then * qualifiers, then alternation, then the per-projection orphan warning. */ declare function checkConfigurationDrift(nodes: ProjectableNode[], edges: ProjectableEdge[]): ConfigurationDriftFinding[]; /** * UPG Lifecycles. Phase-and-state journeys for entity types with meaningful status. * * Phase is the universal vocabulary fixed by the spec. State carries granular * meaning within a phase. Tools add their own states via `lifecycle_extensions` * on the UPG document. Static types (persona, metric, quote) have no lifecycle. */ /** A lifecycle defines the valid phase-and-state journey for an entity type. */ interface UPGLifecycle { /** Which entity type this lifecycle governs. */ entity_type: string; /** * Identifier of the reusable template this lifecycle was generated from * (e.g. `'PUBLISHING'`, `'OPERATIONAL'`), if any. * * Hand-authored lifecycles leave this `undefined`. Template-derived * lifecycles set it via `fromTemplate()` so render and audit tooling can * group, label, and link templates without re-detecting them structurally. */ template_id?: string; /** The universal phases, fixed by the spec and understood by all tools. */ phases: LifecyclePhase[]; /** Which phase a new entity of this type starts in. */ initial_phase: string; /** * Phases representing completion: the end points of normal forward progression. * * A terminal phase MAY still declare `transitions_to` entries, which represent * late-state transitions (e.g., `archived → draft` for republishing, `approved * → deprecated` for late retirement, `parked → open` for reopen). These are * legitimate domain moves and are NOT forward progression. * * Consumers treating terminals as "complete": * - Dashboards / health scores: treat terminal phases as done for counting. * - Status nudges: stop prompting once terminal is reached. * - Transition gates: allow terminal → `transitions_to` targets as explicit * late-state moves; forward-progression gates should compare against the * authoring layer (e.g., "moving a `done` task back to `todo` requires * reopening"). */ terminal_phases: string[]; } /** * The near-universal PM-tool status bucket a phase belongs to (0.25.1 feedback * 1ee70102) — the same six-bucket system Linear, Jira, Asana, and GitHub * Projects converge on. It exists for one job: mapping a graph entity's phases * onto an external tool's workflow-state categories WITHOUT re-deriving the * categorisation by trial and error inside the external tool's UI. * * Bucket semantics (assignment rules used across the catalog): * - `triage` — raised/identified/proposed; awaiting an acceptance decision. * - `backlog` — accepted or set aside deliberately; not scheduled (parked, * paused, deferred, vacant, off). * - `unstarted` — committed/scheduled/ready; the work itself has not begun. * - `started` — any in-flight phase, AND any ongoing steady-state phase * (active, live, production, mature): the entity is alive. * - `completed` — reached an end through the process itself, even when the * outcome is negative (done, shipped, missed, failed, * closed_lost, expired): the outcome is recorded, the flow ran. * - `cancelled` — deliberately ended without/before completion (won't-do, * duplicate, abandoned, rejected, terminated). * * Spelling note: the spec uses `cancelled` (its established phase-id spelling); * Linear's API spells the equivalent bucket `canceled`. Adapters map trivially. */ type StatusCategory = 'triage' | 'backlog' | 'unstarted' | 'started' | 'completed' | 'cancelled'; /** A phase is one broad stage in the entity's lifecycle. */ interface LifecyclePhase { /** Machine-readable phase ID: the universal vocabulary. * @example "in_progress" */ id: string; /** Which of the six near-universal PM-tool buckets this phase maps onto. * See {@link StatusCategory} for the assignment rules. * @example "started" */ status_category: StatusCategory; /** Human-readable label. * @example "In Progress" */ label: string; /** What this phase means: guidance for agents and documentation. * @example "The solution is actively being built or tested." */ description: string; /** * Which phases can follow this one: valid transitions at the phase level. * * On non-terminal phases: forward progression targets. * On terminal phases: late-state / reopen / revive paths (optional; often empty). * * @example ["shipped", "deferred"] */ transitions_to: string[]; /** Core states defined by the spec, always available in this phase. * If omitted, the phase itself is the only state. No nesting needed. */ core_states?: LifecycleState[]; } /** A state is a specific position within a phase. */ interface LifecycleState { /** Machine-readable state ID. * @example "in_review" */ id: string; /** Human-readable label. * @example "In Review" */ label: string; /** What this state means within the phase. * @example "Code complete, awaiting peer review before merge." */ description: string; /** Optional: which states within this phase can follow this one. * If not defined, transition to any state in the next phase is valid. */ transitions_to?: string[]; } declare const UPG_LIFECYCLES: readonly UPGLifecycle[]; /** * Returns the lifecycle definition for a given entity type, or `undefined` * if the type does not have a lifecycle. * * @example * const lifecycle = getLifecycleForType('hypothesis') * // → VALIDATION template (untested → testing → validated | invalidated) * * const noLifecycle = getLifecycleForType('persona') * // → undefined */ declare function getLifecycleForType(entityType: string): UPGLifecycle | undefined; /** * Entity types that are deliberately lifecycle-free. * * Every active UPGEntityType must be either in UPG_LIFECYCLES or in this * set; the `10-lifecycle-coverage` audit enforces it. The coverage check * is the successor to the earlier ad-hoc "barren entity" info log. * * Categories represented here: * * • **Reference / catalog data**: vocabulary definitions that don't * transition (persona, skill, competitor, glossary_term). * • **Structural primitives**: atoms of a larger composite (journey_step, * funnel_step, aggregate, domain_entity, contract_clause). * • **Data points / snapshots**: immutable observations (quote, * observation, evidence, survey_response, test_result). * • **Configuration**: static settings (paywall, trial_config, * attribution_model, design_token). * • **Historical records**: append-only ledger entries (changelog, * approval_record, workflow_artifact, ai_trace, ai_cost_tracker). * • **Profile / directory entries**: people and orgs (contact, * stakeholder, organization). * • **Computed / derived values**: not workflow-bearing (cohort, * customer_health_score, forecast, error_budget). * • **DDD modelling primitives**: aggregates / value objects / commands; * they describe the domain model, not processes within it. * * Some of these will gain lifecycles in v0.3 if Entopo usage surfaces real * states. The allow-list is therefore *intentional today*, not * *locked forever*. * * Keep this list organised by domain for review clarity. When moving a * type into UPG_LIFECYCLES, remove it from here. */ declare const UPG_LIFECYCLE_FREE_TYPES: ReadonlySet; /** * Returns `true` if an entity type is intentionally lifecycle-free (see * {@link UPG_LIFECYCLE_FREE_TYPES} for the full list and rationale). * * @example * isLifecycleFreeType('tag') // true (tags are reference data, not stateful artefacts) * isLifecycleFreeType('feature') // false (features carry a development lifecycle) */ declare function isLifecycleFreeType(entityType: string): boolean; /** * Entity types that are **triaged but not yet implemented**; each has a * lifecycle planned for a later phase of. This set exists so that * the coverage audit (`10-lifecycle-coverage`) distinguishes * "actively deferred" from "untriaged"; moving a type here is a commitment * to enrich it, not a punt. * * Phase breakdown (committed 2026-04-17): * * • **Phase B**: high-value hand-authored lifecycles (NL) + two new * templates (SALES_DEAL, RISK_ITEM). ~30 types: outcome, feature, epic, * user_story, bug, deal, threat, risk, compliance_requirement, insight, * hypothesis-adjacent artefacts, workflow artefacts, etc. * • **Phase C**: expand existing-template coverage to the PUBLISHING / * OPERATIONAL / APPROVAL / MATURITY / WORK_ITEM buckets via * `fromTemplate()`. ~60 types. * • **Phase D**: residual new lifecycles for types that don't fit a * template cleanly but didn't make Phase B's cut. ~10 types. * * As phases land, types migrate out of this set into UPG_LIFECYCLES. * The `10-lifecycle-coverage` audit surfaces the remaining count so the * closure trajectory is visible. */ declare const UPG_LIFECYCLE_PLANNED_TYPES: ReadonlySet; /** * Returns `true` if an entity type has a lifecycle planned for a later phase * (see {@link UPG_LIFECYCLE_PLANNED_TYPES}). * * @example * isLifecyclePlannedType('outcome') // true (planned for a later phase) * isLifecyclePlannedType('feature') // false (feature lifecycle is already implemented) */ declare function isLifecyclePlannedType(entityType: string): boolean; /** A render-ready lifecycle state (one phase of the underlying lifecycle). */ interface LifecycleRenderState { /** Phase id (e.g. `'draft'`, `'in_progress'`, `'archived'`). */ id: string; /** Human-readable label. */ label: string; /** What this state means. */ description: string; /** True if this state is in the lifecycle's `terminal_phases`. */ terminal: boolean; } /** A render-ready transition between two lifecycle states. */ interface LifecycleRenderTransition { /** Source state id. */ from: string; /** Target state id. */ to: string; /** * Classification derived from the lifecycle's terminal set and authoring * order: * * - `'reopen'`: source is terminal, target is not (e.g. `archived → draft`). * - `'terminal'`: target is terminal, source is not (forward to completion). * - `'backward'`: target appears before source in `phases[]` and is not a reopen. * - `'forward'`: everything else. * * The classification is a render hint, not a validation rule: any transition * declared in the source lifecycle is valid by definition. */ kind: 'forward' | 'backward' | 'terminal' | 'reopen'; } /** A flattened, render-ready shape derived from a single `UPGLifecycle`. */ interface LifecycleRenderShape { /** Entity type this shape was derived from. */ entity_type: string; /** * Identifier of the source template, if the underlying lifecycle was * generated from one (e.g. `'PUBLISHING'`). Hand-authored lifecycles leave * this `undefined`. */ template_id?: string; /** Render states, in the authoring order of the source `phases[]`. */ states: LifecycleRenderState[]; /** Render transitions, classified by `kind`. */ transitions: LifecycleRenderTransition[]; /** Initial state id; equals the source `initial_phase`. */ initial_state: string; } /** * Returns a render-ready `{ states, transitions }` shape for an entity type's * lifecycle, or `null` if the type is intentionally lifecycle-free * (see {@link UPG_LIFECYCLE_FREE_TYPES}). * * The shape is flattened to one level; each phase becomes one render state. * Optional finer-grained `core_states` are not surfaced here; consumers that * need them should read the source lifecycle directly via * {@link getLifecycleForType}. * * Transitions are classified into one of `'forward' | 'backward' | 'terminal' * | 'reopen'` so visualisers can style them without re-deriving the * classification. * * Returns `null` for: * - Types in {@link UPG_LIFECYCLE_FREE_TYPES} (e.g. `persona`, `metric`). * - Types not yet in the registry (planned types per * {@link UPG_LIFECYCLE_PLANNED_TYPES}, or unknown types). * * @example * const shape = getLifecycleRenderShape('document') * // → { entity_type: 'document', template_id: 'PUBLISHING', * // states: [{ id: 'draft', terminal: false, … }, …], * // transitions: [{ from: 'archived', to: 'draft', kind: 'reopen' }, …], * // initial_state: 'draft' } * * @example * getLifecycleRenderShape('persona') // → null (intentionally lifecycle-free) */ declare function getLifecycleRenderShape(entityType: string): LifecycleRenderShape | null; /** * UPG Enum Scales. Runtime metadata for the closed-enum primitives in * `properties/primitives.ts`. Per-value labels and descriptions for UI hover. * Parallel to `UPG_SCALES` (numeric assessment scales). * https://unifiedproductgraph.org/spec | MIT */ /** A single value in a closed-enum scale */ interface UPGEnumScaleValue { /** The TypeScript string literal (e.g. `'at_risk'`) */ value: string; /** Human-readable display label (e.g. `'At risk'`) */ label: string; /** One-sentence definition shown on hover */ description: string; } /** * Runtime metadata for a closed-enum primitive type. * * `values` is ordered semantically (e.g. low → high for ordinal scales; * strict → permissive for rule strength). Array index is the canonical * position (no separate `position` field). */ interface UPGEnumScaleDefinition { /** Matches the TypeScript type name (e.g. `'HealthStatus'`) */ id: string; /** Human-readable scale name (e.g. `'Health status'`) */ label: string; /** What the scale classifies or measures */ description: string; /** Per-value metadata in semantic order */ values: UPGEnumScaleValue[]; /** * Optional rendering hint: which end of the scale is desirable. * Parallel to `SCALE_TONE_DIRECTION` for numeric scales. * * `'high-is-good'`: first value is best (e.g. on_track, high confidence) * `'low-is-good'`: last value in natural order is worst (e.g. critical urgency is bad) * `'neutral'`: no inherent good/bad direction (e.g. Cadence, Priority) */ tone_direction?: 'high-is-good' | 'low-is-good' | 'neutral'; } declare const UPG_ENUM_SCALES: Record; /** Look up a full enum scale definition by its type name. */ declare function getEnumScale(name: string): UPGEnumScaleDefinition | undefined; /** Look up per-value metadata within a named enum scale. */ declare function getEnumValueMeta(scaleName: string, value: string): UPGEnumScaleValue | undefined; /** * UPG Schema Migrations. Version-scoped maps for type renames, merges, and deprecations. * https://unifiedproductgraph.org/spec | MIT */ interface UPGTypeMigration { /** The old type name being retired */ from: string; /** The new canonical type name */ to: string; /** Default designation/property values to set on migrated nodes */ defaults?: Record; /** Human-readable explanation of why this migration exists */ reason: string; } /** * Version-scoped migration definitions. * Key is the version that INTRODUCES the migration (target version). * Each entry describes how an old type maps to a new canonical type. */ declare const UPG_MIGRATIONS: Record; /** * Compare two semver-style version strings of the form `MAJOR.MINOR.PATCH`. * * Returns a negative number if `a < b`, zero if `a === b`, positive if `a > b`. * Components are compared numerically (e.g. `0.2.11` is greater than `0.2.8`, * not less; naive string comparison gets this backwards because `'11' < '8'` * lexicographically). * * Used by every range-filtered migration helper below * (`getMigrationMap`, `getAllMigrations`, `getPropertyMigrations`, * `getSplitMigrations`, `getUPGEdgeMigrations`, `migrateNodeProperties`). * * Previously, those helpers used raw string comparison and silently dropped * every v0.2.7+v0.2.8 rule once `UPG_VERSION` crossed `0.2.10` fix. * * @example * compareVersions('0.2.8', '0.2.11') // → negative (0.2.8 < 0.2.11) * compareVersions('0.2.11', '0.2.8') // → positive (0.2.11 > 0.2.8) * compareVersions('0.2.0', '0.2.0') // → 0 */ declare function compareVersions(a: string, b: string): number; /** * Build a flat old→new type name map for a specific version upgrade. * Used by TYPE_ALIASES and .upg file readers. * * @example * // Upgrading from v0 (pre-UPG) to v0.2.0: collect every type rename. * const map = getMigrationMap('0.0.0', '0.2.0') * // map.pain_point === 'need' * // map.jtbd === 'job' * // map.package === 'pricing_tier' */ declare function getMigrationMap(fromVersion: string, toVersion: string): Record; /** * Apply migrations to a single node, converting its type and * merging default properties. * * `T` is the caller's node shape, typically `UPGBaseNode` or a narrower * app-specific node type. The constraint (`{ type: string; properties?: ... }`) * keeps the function node-library-agnostic so adapters, CLI tools, and test * fixtures can all reuse it without coupling to `UPGBaseNode` directly. * * @example * // Legacy deprecated type migrates to its canonical replacement. * const legacy = { id: 'n1', type: 'pain_point', properties: { description: 'Onboarding is slow' } } * const migrated = migrateNode(legacy, '0.0.0', '0.1.0') * // migrated.type === 'need' * // migrated.properties.valence === 'pain' // merged default * // migrated.properties.description === 'Onboarding is slow' // original preserved * * @example * // Non-deprecated type passes through; return shape preserves T. * const healthy = { id: 'n2', type: 'persona', properties: { is_primary: true } } * const same = migrateNode(healthy, '0.0.0', '0.1.0') * // same === healthy (referentially unchanged) */ declare function migrateNode; }>(node: T, fromVersion: string, toVersion: string): T; /** * Get all deprecated type names (across all versions) as a flat set. * Useful for validation warnings. * * @example * const deprecated = getDeprecatedTypes() * deprecated.has('pain_point') // → true (consolidated into 'need') * deprecated.has('jtbd') // → true (renamed to 'job') * deprecated.has('persona') // → false (still canonical) */ declare function getDeprecatedTypes(): Set; /** * A property-level migration applied at load time. * * Discriminated by `kind`: * * - `'drop_props'`: remove keys from `node.properties`. The original * shape; the existing v0.2.2 + v0.2.8 entries use this. * - `'rename_top_level'`: rename a top-level `UPGBaseNode` field to a * different top-level field, optionally remapping its value. Used when a * pre-canonical custom field (e.g. `lifecycle_status`) shifts to its * canonical slot (`status`). * - `'lift_property_to_top_level'`: move a value from `node.properties` to a * top-level field, optionally remapping. Used when a slot that was * pragmatically stuffed in `properties` graduates to a top-level * `UPGBaseNode` slot (e.g. `properties.stage` → top-level `status`). * - `'drop_when_self_referential'`: drop top-level fields whose value * equals the node's own `id` or `type` (or another configured * self-reference). Used to clean up redundant round-trip metadata that's * only meaningful when pointing OUT of the node, not at itself. */ type UPGPropertyMigration = { kind: 'drop_props'; /** The entity type whose properties are being migrated. */ type: string; /** Property keys to drop from `type.properties`. */ drop_props: string[]; /** Human-readable explanation surfaced in load-time warnings. */ reason: string; } | { kind: 'rename_top_level'; /** The entity type this rule applies to (or `'*'` for all types). */ type: string; /** Pre-canonical top-level field name. */ from: string; /** Canonical top-level field name. */ to: string; /** * Optional value remap from old → new. If unset, the value is copied * verbatim from `from` to `to`. If set, only entries in the map are * remapped; values absent from the map pass through unchanged. */ value_map?: Record; /** Human-readable explanation surfaced in load-time warnings. */ reason: string; } | { kind: 'lift_property_to_top_level'; /** The entity type this rule applies to (or `'*'` for all types). */ type: string; /** Key inside `properties` that should move to top-level. */ from_property: string; /** Top-level field that receives the lifted value. */ to: string; /** Optional value remap from old → new (same semantics as `rename_top_level`). */ value_map?: Record; /** Human-readable explanation surfaced in load-time warnings. */ reason: string; } | { kind: 'drop_when_self_referential'; /** The entity type this rule applies to (or `'*'` for all types). */ type: string; /** * Top-level fields to inspect. Each is dropped only when its value * equals the node's `id` (for fields named like `*_id`) or `type` * (for fields named like `*_type`); otherwise preserved. */ fields: string[]; /** Human-readable explanation surfaced in load-time warnings. */ reason: string; } | { kind: 'remap_property_value'; /** The entity type this rule applies to (or `'*'` for all types). */ type: string; /** Key inside `properties` whose value is being remapped (stays in the bag). */ property: string; /** * Old to new value remap. Only entries in the map are touched; values * absent from the map pass through unchanged. */ value_map: Record; /** * Optional sibling property to receive the mapped value (a split). When * set, the mapped value is written to `to_property` and `property` is * reset to `reset_value`. Used to separate a conflated axis, e.g. lift a * data_flow orientation value out of `direction` into `orientation`. */ to_property?: string; /** Value written back to `property` after a split. Required when `to_property` is set. */ reset_value?: string; /** Human-readable explanation surfaced in load-time warnings. */ reason: string; } | { /** * Reshape a bare numeric property into a `UPGAssessment` object * (`{ value, label, scale_id }`). For the v0.10.0 property-registry * correction (#43): interfaces that always declared `UPGAssessment` were * generated loosely, so pre-0.10.0 graphs stored a bare number (e.g. * `market_trend.impact: 4`). This wraps that number on its canonical scale, * deriving the qualitative `label` from the scale's points. Idempotent: a * value that is already an object (assessment) passes through untouched. */ kind: 'reshape_value_to_assessment'; /** The entity type this rule applies to (or `'*'` for all types). */ type: string; /** Key inside `properties` whose bare number is wrapped (stays in the bag). */ property: string; /** The assessment scale to label the value on (e.g. `impact_5`, `importance_5`). */ scale_id: string; /** Human-readable explanation surfaced in load-time warnings. */ reason: string; }; /** * Version-scoped property migrations. Same convention as `UPG_MIGRATIONS`: * the key is the version that introduces the migration. */ declare const UPG_PROPERTY_MIGRATIONS: Record; /** * A single property-migration change applied during `migrateNodeProperties`. * Surfaced for one-warning-per-file logging and structured load-time reports. */ type UPGPropertyMigrationChange = { kind: 'dropped'; key: string; } | { kind: 'renamed_top_level'; from: string; to: string; value_changed: boolean; } | { kind: 'lifted_to_top_level'; from_property: string; to: string; value_changed: boolean; } | { kind: 'self_ref_dropped'; field: string; } | { kind: 'remapped_property_value'; property: string; to_property?: string; value_changed: boolean; } | { kind: 'reshaped_to_assessment'; property: string; scale_id: string; value: number; label: string; }; /** * Apply property migrations to a single node. Returns the (possibly new) node * along with a structured list of changes so callers can emit warnings, * generate audit reports, or skip nodes that don't need rewrites. * * Operates on top-level `UPGBaseNode` fields (`status`, `lifecycle_status`, * `source_id`, `source_type`, …) AND on intra-`properties` keys, dispatching * by rule `kind`. The four kinds are orthogonal; rules apply in registry * order within a version, and across versions in `(fromVersion, toVersion]`. * * Backward-compat helper: callers that only care about dropped property keys * can derive the old `dropped: string[]` shape via * `changes.filter(c => c.kind === 'dropped').map(c => c.key)`. * * @example * // drop_props (unchanged behaviour): * const m = { type: 'metric', properties: { quality_score: 4, current_value: 100 } } * const { node, changes } = migrateNodeProperties(m, '0.2.0', '0.2.2') * // node.properties === { current_value: 100 } * // changes[0].kind === 'dropped'; changes[0].key === 'quality_score' * * @example * // lift_property_to_top_level: * const p = { id: 'p1', type: 'product', properties: { stage: 'idea' } } * const { node } = migrateNodeProperties(p, '0.2.12', '0.2.13') * // node.status === 'concept' (lifted + remapped) * // node.properties.stage === undefined (removed from properties) * * @example * // drop_when_self_referential (wildcard type): * const x = { id: 'x1', type: 'product', source_id: 'x1', source_type: 'product' } * const { changes } = migrateNodeProperties(x, '0.2.12', '0.2.13') * // changes contains { kind: 'self_ref_dropped', field: 'source_id' } * // changes contains { kind: 'self_ref_dropped', field: 'source_type' } */ declare function migrateNodeProperties; [key: string]: unknown; }>(node: T, fromVersion: string, toVersion: string): { node: T; changes: UPGPropertyMigrationChange[]; }; /** * Returns every property migration entry between two versions; useful for * load-time warning generation and audit reports. */ declare function getPropertyMigrations(fromVersion: string, toVersion: string): UPGPropertyMigration[]; /** A single produced target within a 1→N split. */ interface UPGSplitTarget { /** Local name (referenced by routing + edges within this rule). */ ref: string; /** Canonical entity type to spawn. */ type: string; /** Property keys copied from source.properties. Use `'__id'` to inherit source id. */ keep_props?: readonly string[]; /** Defaults merged after keep_props (route-specific defaults win). */ defaults?: Record; } /** Per-target route plan: what to spawn and what overrides apply. */ interface UPGSplitRouteTarget { /** Route-specific defaults merged on top of UPGSplitTarget.defaults. */ defaults?: Record; } /** A single routing rule keyed by source status value. */ interface UPGSplitRoute { /** Which target refs to spawn for this status. */ spawn: readonly string[]; /** * Per-target route overrides. Keys must match `produces[].ref` for * targets in `spawn`. */ [targetRef: string]: UPGSplitRouteTarget | readonly string[] | undefined; } /** A canonical edge to emit between spawned targets, gated by a condition. */ interface UPGSplitEdge { /** Source target ref (from `produces`). */ source_ref: string; /** Target target ref (from `produces`). */ target_ref: string; /** Edge type key (must exist in UPG_EDGE_CATALOG). */ type: string; /** * When to emit the edge. * * - `'both_spawned'`: only emit if both source_ref and target_ref were * spawned by the routing rule. * - `'always'`: emit unconditionally (rare; only valid if both refs * appear in every routing entry's `spawn`). */ when: 'both_spawned' | 'always'; } /** A status-routed 1→N split migration rule. */ interface UPGSplitMigration { /** Discriminator. Only one kind today. */ kind: 'status_routed'; /** The deprecated source type being decomposed. */ from: string; /** Property name on source.properties whose value drives routing. */ status_property: string; /** The N canonical types this rule produces. */ produces: readonly UPGSplitTarget[]; /** Per-status routing: keys are values of source[status_property]. */ routing: Record; /** Edges to emit between spawned targets. */ edges: readonly UPGSplitEdge[]; /** Human-readable explanation surfaced in load-time warnings. */ reason: string; } /** * Version-scoped 1→N split migrations. Same convention as `UPG_MIGRATIONS`: * the key is the version that introduces the migration. */ declare const UPG_SPLIT_MIGRATIONS: Record; /** * Get all 1→N split migrations between two versions. * * @example * const splits = getSplitMigrations('0.2.5', '0.2.6') * // splits[0].from === 'experiment' * // splits[0].produces.length === 2 */ declare function getSplitMigrations(fromVersion: string, toVersion: string): UPGSplitMigration[]; /** * Promote a scalar property that names a first-class entity into a canonical edge. * Graph-level (mints/links nodes) — NOT applied by `migrateNodeProperties`. */ interface UPGScalarToEdgeMigration { /** Source entity type carrying the scalar. */ from_type: string; /** Property key holding the entity identity (inside `properties`, or top-level if `top_level`). */ scalar_property: string; /** Whether the scalar lives at top-level instead of inside `properties`. */ top_level?: boolean; /** Canonical target entity type to find-or-create. */ target_type: string; /** Canonical edge to create. Must exist in `UPG_EDGE_CATALOG`. */ edge_type: string; /** * Edge orientation. Default `false`: the `from_type` node is the edge SOURCE * (`from → target`). `true`: the resolved target entity is the edge source * (`target → from`) — use when an EXISTING edge's canonical direction runs * target_type → from_type (e.g. `acquisition_channel_runs_growth_campaign`, * whose scalar lives on `growth_campaign`). */ reverse?: boolean; /** How to resolve an existing target before minting: normalized title (default) or exact id. */ target_match?: 'title' | 'id'; /** Default properties merged onto a freshly-minted target (e.g. `{ designation: 'north_star' }`). */ target_defaults?: Record; /** `string[]` scalars (e.g. `success_metrics`) → one edge per element. */ multi?: boolean; /** Drop the scalar after linking. `true` for orphans/shadows; `false` to keep an actor display-cache. */ drop_scalar: boolean; /** Human-readable explanation surfaced in load-time warnings + changelog. */ reason: string; } /** * Version-scoped scalar→edge promotions. Same convention as `UPG_MIGRATIONS` / * `UPG_SPLIT_MIGRATIONS`: the key is the version that introduces the rule. */ declare const UPG_SCALAR_TO_EDGE_MIGRATIONS: Record; /** * Get all scalar→edge promotions between two versions. Mirrors `getSplitMigrations`. * * @example * const rules = getScalarToEdgeMigrations('0.11.6', '0.12.0') * // rules[0].from_type === 'business_model' */ declare function getScalarToEdgeMigrations(fromVersion: string, toVersion: string): UPGScalarToEdgeMigration[]; /** * A single edge-key migration rule. Discriminated by `kind`. * * `rename` retargets `from` to `to` (optionally swapping endpoints when * `flip` is true) and may gate on endpoint identity via * `requires_source_type` / `requires_target_type`. * * `drop` removes the edge entirely (no replacement key); used when a * legacy edge has been superseded by a structurally different canonical * edge whose endpoints don't match the legacy rule's `from`. */ type UPGEdgeMigration = { kind: 'rename'; /** The old edge type key. */ from: string; /** The new canonical edge type key. */ to: string; /** When true, swap source/target on each migrated edge. */ flip?: boolean; /** Required source-node type (post-migration) for this rule to fire. */ requires_source_type?: string; /** Required target-node type (post-migration) for this rule to fire. */ requires_target_type?: string; /** Human-readable reason quoting the originating CHANGELOG section. */ reason: string; } | { kind: 'drop'; /** The old edge type key being removed without replacement. */ from: string; /** Human-readable reason quoting the originating CHANGELOG section. */ reason: string; }; /** * Version-scoped edge migration registry. * Key is the version that INTRODUCES the migration (target version). */ declare const UPG_EDGE_MIGRATIONS: Record; /** * Get all edge migration rules between two versions, in version order. * * @example * // v0.2.0 backfill: six jtbd→job edge renames. * const rules = getUPGEdgeMigrations('0.0.0', '0.2.0') * rules.length // → 6 * rules.every(r => r.kind === 'rename') // → true * * @example * // Full v0.2.x range: every rule from v0.2.0 + v0.2.7 + v0.2.8. * getUPGEdgeMigrations('0.0.0', '0.2.8').filter(r => r.kind === 'drop').length // → 4 */ declare function getUPGEdgeMigrations(fromVersion: string, toVersion: string): UPGEdgeMigration[]; /** * Endpoint context for `migrateEdge` guard evaluation. * * Both `sourceType` and `targetType` should be the *post-migration* node * types (i.e. after `migrateNode` / `applySplit` has run on the endpoints). * When omitted, rules with `requires_source_type` / `requires_target_type` * guards are skipped (safer default; a guard that cannot be evaluated * does not fire). */ interface UPGEdgeMigrationEndpoints { /** Post-migration source-node type (after `migrateNode` / `applySplit`). */ sourceType?: string; /** Post-migration target-node type (after `migrateNode` / `applySplit`). */ targetType?: string; } /** * Walk the edge-migration chain for `edge_type` until reaching a value that * exists in `UPG_EDGE_CATALOG` (the current canonical name) or the chain * dead-ends. * * **Why this exists.** `UPG_EDGE_MIGRATIONS` accumulates rules across the * spec's history. A single edge can be renamed multiple times, and the * direction can reverse (e.g. v0.2.8 renamed `solution_proposes_hypothesis` * → `solution_proposes_hypothesis_claim`, then v0.4.0 renamed the latter * back to the former). Naively picking "the first rule whose `from` matches" * surfaces a stale migration target. The validator's `edge_drift` * suggestions need to land on the *current* canonical name, not whatever * intermediate the chain hop happens to be next. * * **Resolution strategy.** Process all rules across all versions, sorted * latest-version first per `from` key. From a given starting key, follow * the highest-version `rename` rule whose `from` matches; if the result is * in `UPG_EDGE_CATALOG`, return it (canonical). Otherwise re-enter with the * new key and continue. Cycles are detected and broken to return a `cycle` * outcome rather than loop forever. * * **Return values.** * - `{ kind: 'canonical', to }`: the walk landed on a key present in * `UPG_EDGE_CATALOG`. Callers should suggest `to` as the migration target. * - `{ kind: 'drop' }`: the walk encountered a `drop` rule. The edge has * no canonical replacement. * - `{ kind: 'dead_end', last }`: no rule matched and `last` is not in * `UPG_EDGE_CATALOG`. The edge is non-canonical with no known migration * target. * - `{ kind: 'cycle', visited }`: a cycle was detected. Returned so * callers can degrade gracefully. Should not happen in practice given * the version-ordered structure of `UPG_EDGE_MIGRATIONS`. * * **Caller note.** Callers that already know `edge_type` is canonical * (i.e. `edge_type in UPG_EDGE_CATALOG`) should NOT call this helper; * there's nothing to suggest. The helper is for the case where the edge * type is deprecated and the caller needs to find its canonical successor. * * @example * // Single-hop walk: deprecated → canonical in one step. * walkMigrationChainToCanonical('solution_proposes_hypothesis_claim', UPG_EDGE_CATALOG) * // → { kind: 'canonical', to: 'solution_proposes_hypothesis' } * * @example * // Drop rule short-circuits the walk. * walkMigrationChainToCanonical('experiment_tests_hypothesis', UPG_EDGE_CATALOG) * // → { kind: 'drop' } * * @example * // Already-canonical edge type: returns dead_end because the helper is * // intended for non-canonical callers. The 'canonical' branch fires only * // when the chain ENDS on a catalog entry, not when it STARTS on one. * // (In practice the validator skips this case before calling.) */ type WalkMigrationChainResult = { kind: 'canonical'; to: string; } | { kind: 'drop'; } | { kind: 'dead_end'; last: string; } | { kind: 'cycle'; visited: readonly string[]; }; declare function walkMigrationChainToCanonical(edge_type: string, catalog: Readonly>): WalkMigrationChainResult; /** * Apply edge migrations to a single edge. * * Returns: * - the original edge (un-shaped, referentially equal) if no rule matched; * - a new edge with retyped `type` (and possibly swapped `source`/`target` * when the rule sets `flip: true`) if a `rename` rule matched; * - `null` if a `drop` rule matched; caller should remove the edge. * * Endpoint guards (`requires_source_type` / `requires_target_type`) check * the *post-migration* endpoint types provided via `endpoints`. When * `endpoints` is omitted, guarded rules are skipped; callers running edge * migration in isolation (without endpoint type context) get only the * un-guarded rules. * * `T` is the caller's edge shape; only `type` is required. `source` and * `target` are touched only when `flip: true` and are otherwise preserved * verbatim. * * @example * // No rule matches; edge passes through unchanged. * const edge = { id: 'e1', type: 'persona_pursues_job' } * migrateEdge(edge, '0.2.0', '0.2.8') === edge // → true * * @example * // Rename rule matches with endpoint guards satisfied. * const legacy = { id: 'e2', source: 'p1', target: 'j1', type: 'persona_has_jtbd' } * const migrated = migrateEdge(legacy, '0.0.0', '0.2.0', { sourceType: 'persona', targetType: 'job' }) * migrated?.type // → 'persona_pursues_job' * * @example * // Drop rule matches; null signals "remove this edge". * const dropped = { id: 'e3', type: 'experiment_tests_hypothesis' } * migrateEdge(dropped, '0.0.0', '0.2.7') // → null */ declare function migrateEdge(edge: T, fromVersion: string, toVersion: string, endpoints?: UPGEdgeMigrationEndpoints): T | null; /** * UPG Status Migrations. Maps legacy status values to canonical lifecycle * phases per entity type, surfacing automated cleanup for the largest single * drift class in real product graphs. * * Sibling to `migrations.ts`. The other migration maps (`UPG_MIGRATIONS`, * `UPG_PROPERTY_MIGRATIONS`, `UPG_EDGE_MIGRATIONS`, `UPG_SPLIT_MIGRATIONS`) * cover entity-type renames, property-shape evolution, edge-key retargeting, * and 1→N splits respectively. This map fills the remaining axis: when a * type's lifecycle exists but the graph carries pre-canonical status values * the lifecycle never had. * * Source: live drift in `.upg/entopo.upg`, `.upg/nimbus.upg`, and * `.upg/inkling.upg` surveyed during the v0.5 launch train (UPG-527). * Highest single drift class: 173 `service` nodes with `status: "active"` * against the canonical `[development, staging, production, deprecated]` * lifecycle. * * https://unifiedproductgraph.org/spec | MIT */ /** * Per-entity-type map of legacy status values to canonical lifecycle phases. * * Each top-level key is an entity type; each inner key is a legacy status * value observed in real graphs; the value is the canonical phase id from * that type's lifecycle. Entity types absent from this map have no * registered status migration; `migrateStatusValue` returns `null` for * them, signalling "no automated fix; surface to operator". * * **Population strategy.** Only mappings that are unambiguous from observed * usage are included. Where a legacy value could plausibly map to two * different canonical phases, the type is left empty rather than guessing. * * **Why this is a map of maps rather than a list of rules.** Status values * are entity-type-scoped: `"active"` means different things on `service`, * `feature`, and `hypothesis`. A flat `[{ from, to, type }]` list would * force every lookup to filter; a map of maps gives O(1) `[type][value]` * lookup and makes the per-type registry obvious to readers. */ declare const UPG_STATUS_MIGRATIONS: Partial>>; /** * Look up the canonical replacement for a legacy status value on the given * entity type. Returns `null` when no migration is registered. * * Caller contract: only invoke when the current status is known-invalid * for the entity's lifecycle. `null` signals "no automated fix is on * file; surface to the operator". The presence of a mapping does NOT * imply the current status is invalid; checking validity is the caller's * job (typically via `getLifecycleForType(entityType).phases`). * * @example * migrateStatusValue('service', 'active') // → 'production' * migrateStatusValue('service', 'unknown_val') // → null * migrateStatusValue('persona', 'active') // → null (no map registered) */ declare function migrateStatusValue(entityType: string, currentStatus: string): string | null; /** * Return true when the (entityType, currentStatus) pair has a registered * canonical replacement that ALSO differs from the current value. * * Useful for filter predicates: callers usually only care about migrations * that would actually mutate the node. An identity mapping * (`active → active` on `feature_area`) is a registered migration but * shouldn't trigger a rewrite. */ declare function hasStatusMigration(entityType: string, currentStatus: string): boolean; /** * Audit helper: enumerate every (entityType, legacyStatus, canonicalStatus) * triple in the registry. Useful for changelogs, doc generation, and * spec-coverage tests. */ declare function listStatusMigrations(): Array<{ entity_type: string; from: string; to: string; }>; /** * Spec-coherence helper: returns the entity types in * `UPG_STATUS_MIGRATIONS` whose registered canonical replacements are NOT * valid phases in the type's lifecycle. Used by the spec-integrity test to * catch drift between the migration map and the lifecycle catalog. * * An empty array means every replacement target resolves to a real phase. * Entries here mean either: * * - the lifecycle changed but the migration map was not updated, OR * - the lifecycle is missing entirely (template-generated types whose * template id moved). * * Both cases are spec defects worth surfacing. */ declare function findInvalidStatusMigrationTargets(): Array<{ entity_type: string; from: string; to: string; reason: 'no_lifecycle' | 'unknown_phase'; }>; /** * UPG slug generation + collision handling. * * `slug` is the human-readable handle used in inline `[[type:slug]]` chips * inside `.upg.md` documents (v0.2.2). Each node carries an * optional `slug` plus an `aliases[]` array of past slugs. * * Rules: * - Lowercase ASCII only; accents stripped via NFKD; emojis and non-ASCII * word chars dropped. * - Whitespace + underscores collapse to single `-`. * - Punctuation removed; runs of `-` collapsed to one. * - Leading / trailing `-` trimmed. * - Empty result (e.g. emoji-only title) → fallback to `untitled`. * - Uniqueness is scoped `(product_id, type)`; resolveSlugCollision appends * `-2`, `-3`, … against an existing-slug set. * * The set used for collision detection MUST include both current `slug` * values AND every `aliases[]` value within the same `(product_id, type)`, * so a renamed slug never collides with a still-resolvable alias. */ /** * Generate a slug from a title. Pure function, no collision check. * * @example * generateSlug('Tree navigation outcome') // => 'tree-navigation-outcome' * generateSlug('Café résumé') // => 'cafe-resume' * generateSlug(' hello___world ') // => 'hello-world' * generateSlug('🎉') // => 'untitled' */ declare function generateSlug(title: string): string; /** * Resolve a base slug against an existing-slug set, appending `-2`, `-3`, * etc. as needed. Returns the original slug if it does not collide. * * The `existing` set MUST cover both current `slug` values and every * `aliases[]` value within the same `(product_id, type)`. * * @example * resolveSlugCollision('foo', new Set()) // => 'foo' * resolveSlugCollision('foo', new Set(['foo'])) // => 'foo-2' * resolveSlugCollision('foo', new Set(['foo', 'foo-2'])) // => 'foo-3' */ declare function resolveSlugCollision(base: string, existing: ReadonlySet): string; /** * Build the existing-slug set for a `(type)` cohort within a single product. * Pass the nodes of one product; the helper picks the ones with the given * type and accumulates their `slug` and `aliases` values. */ declare function collectSlugsForType(nodes: readonly Pick[], type: string): Set; /** * Backfill `slug` on a node if it's missing. Mutates and returns the node. * Pass the existing-slug set for the node's type so collisions resolve. * * Idempotent: if `node.slug` is already set, returns the node unchanged * and does not touch `existing`. */ declare function backfillSlug>(node: T, existing: Set): T; /** * Rotate a slug rename: push the old slug into `aliases[]` (deduped) and * set the new slug. No-op if `next` equals the current slug. */ declare function rotateSlug>(node: T, next: string): T; /** * Cross-product edge scope — the derived 3-state model (0.18.0). Layer 2: it * bridges the cross-product facts that already live split across lower layers — * `UPG_CROSS_EDGE_TYPES` (shapes/document), the catalog `cross_product_eligible` * flags (catalog/edge-catalog), and `portfolio_shared` (registry/entity-meta). * * Cross-product eligibility is NOT cleanly derivable from a single per-edge signal: * endpoint entity-TIER is necessary but not sufficient (a "≥1 shared endpoint ⇒ * eligible" rule over-admits ~5×, because portfolio-shared types also anchor rich * within-graph decomposition). So eligibility is a two-layer model: * * - **curated** — the blessed canonical set (`UPG_CROSS_EDGE_TYPES`, 61 types: * 21 portfolio-native + 40 catalog-flagged `cross_product_eligible`). * Hard-allow at the write surface, no warning. * - **provisional** — an unflagged catalog edge that PASSES the shared-tier gate * (≥1 endpoint `portfolio_shared`). Allowed at the write surface * WITH a warning, and never added to the canonical set — it only * materialises as a warning if actually authored. This is what * removes the per-edge catalog-PR friction: modelling a genuinely * cross relationship no longer needs a spec change mid-session. * The warning is WRITE-TIME only — `portfolio_validate` does not * re-surface it and is not an eligibility backstop. * - **resident** — no shared endpoint and not curated. Hard-rejected cross-product; * the containment guardrail (persona↔job↔need decompositions, * product-local reasoning) that must stay in-graph. * * WILDCARD ENDPOINTS, STATED PLAINLY (0.41.0, field report `7aaa2f7e`). The * mechanism is described on `isCrossCapable` below, but its consequence was * never written down where a reader looks for it, so a reporter reasonably * read a `decision_*_node` edge's absent scope as unfinished work and proposed * declaring it `provisional`. It cannot be declared: scope is DERIVED, never * authored. And `node` is not a type, so it can never be `portfolio_shared` * and never carries the gate on its own. Therefore: * * **An edge with a `node` wildcard endpoint is `resident` unless its OTHER, * concrete endpoint is a portfolio-shared type, or the edge type is curated * into `UPG_CROSS_EDGE_TYPES` by name.** * * That is why `node_owned_by_team` is cross-capable (team is shared) while * `decision_influences_node`, `decision_produces_node`, * `decision_constrained_by_node` and `risk_threatens_node` are resident: * `decision` and `risk` are not in the shared tier. Only three wildcard edges * are cross-capable today, all of them curated BY NAME. * * Widening one is a two-option decision and BOTH amend a ratified list, which * is why it is not a spec-writer's call: curate the edge type by name, or * promote the concrete endpoint type into the portfolio-shared tier, which * moves every other edge touching that type at the same time. * * The gate (`isCrossCapable`) is DERIVED from `EntityTypeMeta.portfolio_shared`, so * the guardrail is self-maintaining: a new persona/job/need internal edge is * auto-rejected with no list to touch. The gate LOGIC itself never changes the * canonical `UPG_CROSS_EDGE_TYPES` snapshot (adapters / portfolio_query / * list_cross_edge_types keep reading whatever it currently is, 61 as of 0.20.1); * only a new `cross_product_eligible` flag or portfolio-native type grows it. * `crossProductScope` is a separate predicate the write + read surfaces consult. */ /** The three cross-product states a directed relationship can occupy. */ type CrossProductScope = 'curated' | 'provisional' | 'resident'; /** * The shared-tier GATE: is a directed edge between these endpoint types authorable * across product graphs at all? True iff ≥1 endpoint type is `portfolio_shared`. * A `node`-wildcard endpoint never qualifies on its own (it is not shared), so the * decision rides on the concrete other endpoint — exactly the polymorphic * `node_owned_by_team` / `node_classified_as_classification_value` case. * * This is a NECESSARY condition, not sufficient: a gate-pass edge that is not curated * is `provisional`, not eligible. Failing the gate is the hard guardrail. * * @example * isCrossCapable('objective', 'metric') // → true (both shared) * isCrossCapable('node', 'team') // → true (team shared) * isCrossCapable('persona', 'job') // → false (neither shared → resident) */ declare function isCrossCapable(sourceType: string, targetType: string): boolean; /** * True if this cross-edge type is in the curated canonical set (`UPG_CROSS_EDGE_TYPES`, * the 61). Distinct from the catalog's `isCrossProductEligible` (the 40 dual-registered * flags only): this also covers the 21 portfolio-native cross-only types. * * @example * isCuratedCrossEligible('shares_persona') // → true (portfolio-native) * isCuratedCrossEligible('strategic_theme_contains_objective') // → true (flagged) * isCuratedCrossEligible('experiment_run_measures_metric') // → false (provisional, not curated) */ declare function isCuratedCrossEligible(edgeType: string): boolean; /** * Classify an edge TYPE into the 3-state cross-product model. The single classifier * consulted by the write surfaces (curated → allow, provisional → allow+warn, * resident → reject) and the read surfaces (`resolve_edge_for_pair`, * `get_entity_schema`), so model-time guidance and write-time enforcement agree. * * Type-based (not instance-based): for a catalog edge the gate reads the DECLARED * endpoint types, matching how the write handlers gate on `edge.type`. A portfolio- * native cross-only type (no catalog def) is curated by membership. * * @example * crossProductScope('product_pursues_outcome') // → 'curated' * crossProductScope('experiment_run_measures_metric') // → 'provisional' (metric shared) * crossProductScope('persona_pursues_job') // → 'resident' */ declare function crossProductScope(edgeType: string): CrossProductScope; /** * UPG Document Validator. Validates a `UPGDocument` against the spec. Zero dependencies. * https://unifiedproductgraph.org/spec | MIT */ interface UPGValidationError { /** JSON-path location of the field that failed validation (e.g. `$.nodes[2].type`) */ path: string; /** Human-readable description of the validation failure */ message: string; } interface UPGValidationResult { /** Whether the document passed all mandatory spec checks */ valid: boolean; /** Spec violations: any entry here means `valid` is false */ errors: UPGValidationError[]; /** Best-practice notices: present even when `valid` is true */ warnings: UPGValidationWarning[]; } interface UPGValidationWarning { /** JSON-path location of the field that triggered the warning */ path: string; /** Human-readable description of the best-practice notice */ message: string; /** * Stable machine-readable category for the warning, when it belongs to one * of the content-depth checks. Lets consumers (e.g. the CLI `verify` * command) select a specific class of finding without parsing `message`. * * These are surfaced as warnings (not errors) on purpose: the document * LOAD path throws on any `errors[]` entry, and real graphs already carry * known drift (non-canonical enum values, primitive-where-assessment). * Promoting them to errors would brick those graphs on load. `verify` * re-runs the validator and re-classifies these as policy violations so a * CI gate still fails, without making the parser refuse to read the file. */ rule?: 'property-type' | 'property-enum' | 'self-loop' | 'framework-score'; } /** * Warning `rule` codes for the content-depth checks. * `verify`/`check` treat a document carrying any of these as a policy * violation (exit 2) even though they never block the load path. * * - `property-type` / `property-enum` / `self-loop` — UPG-632. * - `framework-score` — UPG-638. A `framework_exercise`'s persisted per-entity * result (carried on its `framework_exercise_includes_node` edge properties) * that violates the framework's own input spec: an invalid enum bucket, a * non-numeric / out-of-scale / negative score, or a zero in a divisor input * (e.g. RICE `effort`, WSJF `job_size`). A WARNING, not an error: a drifted * exercise still LOADS, consistent with the severity discipline above. See * `validateFrameworkScores`. */ declare const CONTENT_DEPTH_WARNING_RULES: ReadonlySet>; /** * Validates a UPGDocument against the UPG v0.2 specification. * * Returns a result object with `valid`, `errors`, and `warnings`. * Errors are spec violations. Warnings are best-practice notices. * Unknown node types and edge types produce warnings, not errors. * * @example * const result = validateUPGDocument({ * upg_version: '0.2.0', * exported_at: '2026-04-17T10:00:00Z', * source: { tool: 'entopo', tool_version: '0.1.0' }, * product: { id: 'p1', title: 'My Product' }, * nodes: [{ id: 'n1', type: 'persona', title: 'Creator' }], * edges: [], * }) * // result.valid === true * // result.errors === [] * // result.warnings === [] // unless an unknown type slipped in */ declare function validateUPGDocument(doc: unknown): UPGValidationResult; /** * Type guard: returns true if the document is a valid UPGDocument. * Use `validateUPGDocument` for detailed error reporting. * * @example * const raw: unknown = JSON.parse(fileContents) * if (isUPGDocument(raw)) { * // raw is now typed as UPGDocument * console.log(`Loaded product "${raw.product.title}" with ${raw.nodes.length} nodes`) * } */ declare function isUPGDocument(doc: unknown): doc is UPGDocument; /** * UPG Property Schemas: Portfolio Domain * * Organization, Portfolio, and ProductArea entity properties. * * Part of the Unified Product Graph specification. * https://unifiedproductgraph.org/spec * License: MIT */ /** Organization entity. * * @example * const properties: OrganizationProperties = { * logo_url: 'https://arkheiev.com/logo.svg', * industry: 'Developer Tools', * } */ interface OrganizationProperties { /** URL of the organisation's logo */ logo_url?: string; /** Industry vertical the organisation operates in */ industry?: string; } /** Portfolio entity. * * @example * const properties: PortfolioProperties = { * hierarchy_model: 'nested', * strategy_type: 'horizon-2-bets', * explore_exploit_target: { explore: 30, exploit: 70 }, * } */ interface PortfolioProperties { /** How products are structured within the portfolio */ hierarchy_model?: 'flat' | 'nested' | 'matrix'; /** High-level strategy archetype */ strategy_type?: string; /** * Explore vs exploit investment target (percentages, should sum to 100). * @example { explore: 30, exploit: 70 } */ explore_exploit_target?: { explore: number; exploit: number; }; } /** ProductArea entity. * * @example * const properties: ProductAreaProperties = { * strategic_priority: 'high', * description: 'Shared platform services consumed by every customer-facing product.', * } */ interface ProductAreaProperties { /** Number of products within this area */ /** Strategic priority assigned to this area */ strategic_priority?: Priority; /** Narrative description of what this area covers */ description?: string; /** Person or team that owns this area. Promote to a `node_owned_by_team` edge if ownership must be queryable. */ owner?: string; } /** * UPG Property Schemas: Strategy Domain. * Product (root), Vision, Mission, StrategicTheme, Initiative, Capability, * ValueStream, StrategicPillar, Assumption, Decision, Constraint. * https://unifiedproductgraph.org/spec | MIT */ /** The product being created. Root of the graph. * * Identity and lifecycle only. Users, features, business model, metrics live * in child entities and edges. * * @example * const properties: ProductProperties = { * stage: 'beta', * health_status: 'on_track', * url: 'https://entopo.app', * } */ interface ProductProperties { /** Lifecycle stage */ stage?: UPGProductStage; /** Overall health */ health_status?: HealthStatus; /** Where the product lives. Marketing site, app store URL, etc. */ url?: string; /** Logo or icon URL. Used to render product cards and lists. */ logo_url?: string; /** When the product became generally available (ISO 8601) */ launched_at?: ISODateTime; /** * The product configuration this graph describes, named in plain language. * A label for readers: nothing reads it to gate, filter, or alter how a tool * interprets the graph. * * @example "Enterprise plan, split-navigation flag on" * * @remarks * A product's composition often differs by feature flag, plan tier, * permission level or beta programme: surfaces appear, disappear, or are * replaced by different surfaces with different occupants. A graph that * models one of those without saying which is qualified by a condition nobody * wrote down, and every fact in it inherits that silence. * * It makes no claim about which configuration most customers are on. Where a * product genuinely ships several and the differences matter, declare a * `configuration_axis` instead and let each fact say which values it holds * under. */ described_configuration?: string; /** * Prefix for keys minted onto this product's nodes (e.g. `"LTN"`, giving * `LTN-1`, `LTN-2`, ...). Retained as the single-team unasked default. * @deprecated since 0.33.0, removeIn 1.0.0. Use `team.key_prefix`. This field's own summary called the prefix "the namespace a tracker calls a team", and a single string cannot express a product with two of them: a two-team product has never been expressible, which is a defect rather than an unused feature. Ignored whenever any team in the product declares a prefix. Deprecated rather than removed because removal would strand every graph that has a product prefix and no `team` nodes. * * @remarks * Pairs with `UPGBaseNode.key`, which holds the minted value and states the * resolution order normatively. Only the prefix is serialised: the next number * is `max(existing) + 1`, derived from the graph, because a counter is a fact * about a store rather than about the product. * * WHY THIS IS NOT A DEFAULT WITH `team.key_prefix` AS AN OVERRIDE. A default * and an override describe one fact at two scopes. These describe different * facts, and this one is correct only when the product has exactly one team, * which is an accident of arity rather than a scope. Keeping it live as a * fallback would also reintroduce the defect: a prefix that still resolves wins * on ladder order, so a legacy-prefixed product with two teams would offer no * choice at all and mint everything under the legacy prefix silently, with * migration order deciding the behaviour per graph. * * @example "LTN" */ key_prefix?: string; } /** Vision entity. * * @example * const properties: VisionProperties = { * timeframe: '12-18 months', * north_star: 'Active weekly graph-edits per product team', * success_looks_like: 'success looks like', * } */ interface VisionProperties { /** * Target timeframe. * @example "3 years" */ timeframe?: string; /** * The north-star statement: a prose slogan of the future the product steers * toward. Legitimately free-text (graph-vs-prose). To link the *metric* a * vision optimises for, use the `vision_anchored_by_metric` edge (P14 0.12.0). */ north_star?: string; /** Narrative of what success looks like */ success_looks_like?: string; } /** Mission entity. * * @example * const properties: MissionProperties = { * target_audience: 'Product managers and founders at 10–200-person B2B SaaS companies.', * core_value: 'Clarity beats velocity: decisions you can justify compound faster than code.', * differentiation: 'Open interchange format + works where you already think.', * } */ interface MissionProperties { /** Who the mission serves */ target_audience: string; /** Core value proposition */ core_value: string; /** Differentiation from alternatives */ differentiation?: string; } /** StrategicTheme entity. * * @example * const properties: StrategicThemeProperties = { * owner: 'sam.patel@arkheiev.com', * time_horizon: 'Q2 2026', * description: 'Short narrative describing the entity and why it exists.', * } */ interface StrategicThemeProperties { /** Owning person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; /** * Bounded period this theme is active (a theme is time-bound, within a pillar). * @deprecated since 0.20.0, removeIn 1.0.0. Promote the period to a `planning_cycle` node and link it with the `strategic_theme_scoped_to_planning_cycle` edge, which points at a shared, dated, nestable interval instead of a drifting per-theme string. Kept (not removed) for back-compat. The removal version is stated at 0.33.0 because an undated deprecation is how this field survived twelve minors; the promotion is documented rather than automated and no `drop_props` migration ships. `strategic_pillar.time_horizon` stays as-is (a durable pillar horizon is genuinely open-ended, not a dated cycle). * @example "Q1 2026", "FY26" */ time_horizon?: string; /** Short narrative of this time-bound thrust within its pillar */ description?: string; /** What the theme explicitly includes or excludes */ scope?: string; } /** Initiative entity. * * @example * const properties: InitiativeProperties = { * start_date: '2026-04-01', * end_date: '2026-09-30', * budget: 50000, * } */ interface InitiativeProperties { /** * ISO start date. * @example "2026-04-01" */ start_date?: ISODate; /** * ISO end date. * @example "2026-09-30" */ end_date?: ISODate; /** Budget allocated (base currency units) */ budget?: number; /** Owning person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; } /** Capability entity. * * @example * const properties: CapabilityProperties = { * maturity_level: 'initial', * target_maturity: 'initial', * gap: 'No lifecycle coverage for external-api nodes.', * evolution_stage: 'product', * visibility: 0.8, * } */ interface CapabilityProperties { /** Current maturity */ maturity_level?: MaturityLevel; /** Target maturity */ target_maturity?: MaturityLevel; /** Gap between current and target */ gap?: string; /** * Wardley evolution axis: where this capability sits on the * genesis → custom → product → commodity spectrum. Used by frameworks * like `wardley-map`; generally useful as a maturity-of-the-domain signal * independent of `maturity_level` (which measures the team's internal * capability practice). * * @example 'product' */ evolution_stage?: 'genesis' | 'custom' | 'product' | 'commodity'; /** * Position on the visibility axis from `0.0` (deepest dependency, infra * the user never sees) to `1.0` (user-visible anchor). Used by * `wardley-map` for the y-axis position of each capability in the * value chain. * * @example 0.8 */ visibility?: number; } /** ValueStream entity. * * @example * const properties: ValueStreamProperties = { * stream_stage: 'growth', * lead_time: '14d', * throughput: '250 req/s at p95 60ms', * } */ interface ValueStreamProperties { /** Current stage in the value delivery pipeline (UPG-579 Option B). */ stream_stage?: 'discovery' | 'definition' | 'build' | 'delivery' | 'operation' | 'other'; /** * End-to-end lead time. * @example "2 weeks" */ lead_time?: string; /** * Throughput measure. * @example "5 features/sprint" */ throughput?: string; } /** StrategicPillar entity. Durable multi-year direction the product commits to. * Pillars frame what themes hang under and what initiatives serve. * * @example * const properties: StrategicPillarProperties = { * owner: 'sam.patel@arkheiev.com', * description: 'Short narrative describing the entity and why it exists.', * scope: 'Covers in-product onboarding; excludes lifecycle email.', * } */ interface StrategicPillarProperties { /** Owning person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; /** Narrative of the pillar's intent as a durable strategic area */ description?: string; /** The standing organisational area this pillar owns */ scope?: string; /** * Standing / multi-year horizon, often open-ended (a pillar is durable). * @example "3 years", "2026-2028", "ongoing" */ time_horizon?: string; /** How the business knows this durable pillar is on track. Narrative, not a metric edge. A strategic_theme deliberately has no success_indicator: it is measured through its child objectives, not on its own. */ success_indicator?: string; } /** Assumption entity. * * @example * const properties: AssumptionProperties = { * confidence: 'medium', * validation_method: 'Prototype + 5 task-based interviews', * risk_level: 4, * } */ interface AssumptionProperties { /** Confidence before testing (UPGAssessment on `confidence_5`). Independent of whether the assumption is validated (tracked in lifecycle). */ confidence?: UPGAssessment; /** Validation method, planned or used */ validation_method?: string; /** Exposure if the assumption turns out wrong */ risk_level?: UPGAssessment; /** Observation that would prove this assumption false */ falsifiability?: string; } /** StrategicQuestion entity. * * An open coordination or ownership question a plan is exposed to: who owns a * capability across teams, where a boundary falls after a reorg. The * strategy-domain sibling of `research_question` and `design_question`. * * Distinct from `assumption`. An assumption is a premise the plan is built on * and resolves by being tested (`assumption_becomes_hypothesis`). A * strategic_question is not a testable premise; it is an unresolved question * the plan is exposed to and carries until someone answers it. `status` is the * node lifecycle (open → resolved); the answer lands in `resolution`. * * @example * const properties: StrategicQuestionProperties = { * question: 'Who owns Variants delivery across MCP, Content Agent, and SDK?', * context: 'Studio does not own the full platform deployment path.', * priority: 'high', * } */ interface StrategicQuestionProperties { /** The question itself. Primary content. */ question?: string; /** Context that surfaced the question: the reorg, the boundary, the unowned area. */ context?: string; /** The answer, captured when the question moves to `resolved`. */ resolution?: string; /** Importance to resolve relative to other open questions. */ priority?: Priority; } /** Decision record. Strategic, product, engineering, or design. * * @example * const properties: DecisionProperties = { * layer: 'strategic', * options_considered: ['buy SaaS', 'build in-house', 'extend open-source'], * context: 'Leads a 12-person product team at a mid-size B2B SaaS (50–200 employees).', * } */ interface DecisionProperties { /** * Domain layer. * `engineering` = Architecture Decision Record (ADR). * `design` = Design Decision Record. * The layer field replaces separate `architecture_decision` and `design_decision` types. */ layer: 'strategic' | 'product' | 'engineering' | 'design' | 'business' | 'other'; /** Background and problem statement that prompted the decision */ context?: string; /** Required. Evaluated alternatives. A decision without considered alternatives is incomplete. */ options_considered: string[]; /** Why the chosen option was selected over the alternatives */ rationale?: string; /** ISO date the decision was made or last meaningfully updated. */ date?: string; /** Outcome text. What was decided. Separate from `rationale` (which explains why). */ decision_outcome?: string; /** Known positive and negative consequences. Mirrors MADR's "Consequences" section. */ consequences?: string; /** People who made the decision. Mirrors MADR's "Deciders" field. Promote to `node_owned_by_person` edges (one per name) if ownership must be queryable. */ decision_makers?: string[]; /** * Forces, constraints, and goals that shaped the decision. Mirrors MADR's * "Decision Drivers" section. * @example ["must work offline", "team has no Go expertise", "cost < $500/mo"] */ decision_drivers?: string[]; } /** * Kind of constraint. Category of limitation. * Aligns with Theory of Constraints / Wardley value-chain framings. * `resource` = budget, headcount, or capacity bound. * `technical` = platform ceiling, performance, architecture bound. * `regulatory` = law or jurisdictional rule (GDPR, CCPA, regional). * `temporal` = deadline, time-window, sequencing dependency. * `compliance` = contractual or audit-framework requirement. * `other` = falls outside the above; describe via `source`. */ type ConstraintKind = 'resource' | 'technical' | 'regulatory' | 'temporal' | 'compliance' | 'other'; /** * Whether a constraint is currently binding. * `binding` = actively limiting; cannot be exceeded today. * `advisory` = recommended boundary; can be deliberately accepted. * `lifted` = no longer in effect (kept for historical record). */ type ConstraintStatus = 'binding' | 'advisory' | 'lifted'; /** * Where a constraint comes from (0.17.2). The provenance axis, orthogonal to * `constraint_kind` (category), `constraint_status` (in-effect state), and * `rule_strength` (enforcement strictness). * `internal` = self-imposed: a principle or operating tenet the team commits to * (e.g. "ship weekly", "no feature without a metric"). The home for the * qualitative guardrails `constraint` models in place of a separate principle * type. * `external` = imposed on us: a limit, requirement, or ceiling coming from * outside (a regulation, a budget cap, a platform bound, a contractual rule). */ type ConstraintOrigin = 'internal' | 'external'; /** * Constraint: a named limitation or boundary on product creation. * * **Edge-defined**: semantic identity comes from what it constrains * (feature, initiative, metric, team) via `constraint_constrains_*` and * `constraint_owned_by_*` edges. Properties here are intentionally thin. * * Reuses `RuleStrength` (see `primitives.ts`) so constraint-enforcement * vocabulary matches the broader rule/governance system. * * @example * const properties: ConstraintProperties = { * constraint_kind: 'regulatory', * constraint_status: 'binding', * rule_strength: 'must', * source: 'EU AI Act Article 6', * review_date: '2026-12-01T00:00:00Z', * } */ interface ConstraintProperties { /** Limitation category */ constraint_kind?: ConstraintKind; /** Provenance: a self-imposed tenet (`internal`) or an imposed-on-us limit/requirement (`external`). */ constraint_origin?: ConstraintOrigin; /** Whether binding, advisory, or lifted */ constraint_status?: ConstraintStatus; /** Enforcement strictness. Reuses the governance/guideline rule vocabulary. */ rule_strength?: RuleStrength; /** Free-text origin: policy document, regulation, stakeholder, technical doc. */ source?: string; /** Re-evaluation date (ISO-8601). Useful for regulatory or temporal constraints with sunset clauses. */ review_date?: ISODateTime; } /** * UPG Property Schemas: Users & Needs Domain. * Persona, Job (JTBD), Need, DesiredOutcome, JobStep, SwitchingCost. * https://unifiedproductgraph.org/spec | MIT */ /** User archetype representing a distinct group of users. * * @example * const properties: PersonaProperties = { * context: 'Leads a 12-person product team at a mid-size B2B SaaS (50–200 employees).', * is_primary: true, * experience_level: 'beginner', * } */ interface PersonaProperties { /** Free-text description of the persona's situation and environment * @example "Leads 12-person team at mid-size B2B SaaS (50-200 employees)" */ context?: string; /** Whether this is the primary/target persona for this product */ is_primary?: boolean; /** How experienced this persona is in their domain */ experience_level?: 'beginner' | 'intermediate' | 'advanced' | 'varies'; /** Primary motivation or driving need * @example "Making confident, evidence-based decisions" */ motivation?: string; /** * Tech comfort. Closed set so personas across products compare on the same axis. * Free-text colour belongs in `context` or `motivation`. */ tech_comfort?: ComfortLevel; /** Industry or domain knowledge this persona brings * @example "10+ years SaaS experience", "New to healthcare IT" */ domain_expertise?: string; /** * Role in the buying or adoption decision: who signs (`buyer`), who uses * (`user`), who advocates internally (`champion`), who shapes the choice * (`influencer`), and who delivers it (`partner`). A closed set, so roles * compare across products. * * @example "buyer" * * @remarks * A portfolio must separate the economic buyer from the practitioner user: * they are distinct personas with distinct jobs, and collapsing them is how a * product ends up built for whoever was easiest to interview. */ audience_role?: 'buyer' | 'user' | 'champion' | 'influencer' | 'partner'; /** * Who performs the work: a human archetype, an autonomous AI agent, or a * non-agentic platform service. Absent means `human`, so existing personas * need no migration. * * @example "agent" * * @remarks * An `agent` persona is a first-class actor: it participates in journeys * through the same persona machinery, and humans delegate to it via * `persona_delegates_to_persona`. Human-coverage and segmentation metrics * count `human` only, so agent and system personas are opt-in rather than * silently inflating a coverage number. */ actor_kind?: 'human' | 'agent' | 'system'; } /** Job-to-be-Done: the underlying goal a user is trying to accomplish. * * @example * const properties: JobProperties = { * statement: 'When I am reviewing a sprint, I want a one-page health summary so I can decide what to escalate.', * job_type: 'functional', * importance: 4, * } */ interface JobProperties { /** Job statement: "When I... I want to... So I can..." */ statement?: string; /** Classification by motivation dimension */ job_type?: 'functional' | 'emotional' | 'social' | 'supporting'; /** Importance to the user (1 = low, 5 = critical) */ importance?: UPGAssessment; /** Current satisfaction with how this job gets done (1 = very unsatisfied, 5 = fully satisfied) */ current_satisfaction?: UPGAssessment; /** Persona role in the value exchange */ supporting_role?: 'buyer_of_value' | 'co_creator_of_value' | 'transferrer_of_value'; } /** What kind of experience a need represents */ type NeedValence = 'pain' | 'gap' | 'constraint'; /** How mature a need is in our understanding */ type NeedMaturity = 'raw' | 'validated' | 'prioritized'; /** Unified need. Replaces pain_point + user_need. Framework labels provide context-specific display names. * * @example * const properties: NeedProperties = { * statement: 'When I am reviewing a sprint, I want a one-page health summary so I can decide what to escalate.', * valence: 'pain', * maturity: 'validated', * } */ interface NeedProperties { /** The need expressed as a clear, user-facing statement */ statement?: string; /** What kind of experience: pain (friction), gap (unmet), constraint (limitation) */ valence?: NeedValence; /** How mature is this need in our understanding */ maturity?: NeedMaturity; /** How often the user encounters this need (1 = rarely, 5 = constantly) */ frequency?: UPGAssessment; /** How painful or disruptive the need is when unaddressed (1 = minor, 5 = critical) */ severity?: UPGAssessment; /** How important resolving this need is to the user (1 = low, 5 = critical) */ importance?: UPGAssessment; /** Motivation dimension. Inherited from parent job if not set. */ motivation?: 'functional' | 'emotional' | 'social'; } /** DesiredOutcome entity. * * @example * const properties: DesiredOutcomeProperties = { * importance: 4, * current_satisfaction: 4, * statement: 'When I am reviewing a sprint, I want a one-page health summary so I can decide what to escalate.', * } */ interface DesiredOutcomeProperties { /** Outcome statement in the user's words */ statement?: string; /** How important this outcome is to the user (1 = low, 5 = critical) */ importance: UPGAssessment; /** How satisfied the user currently is with this outcome (1 = very unsatisfied, 5 = fully satisfied) */ current_satisfaction: UPGAssessment; } /** JobStep entity. * * @example * const properties: JobStepProperties = { * step_order: 1, * step_type: 'core', * tools_used: 'Notion for capture, Linear for tracking, Figma for design.', * } */ interface JobStepProperties { /** Order of this step within the parent job */ step_order?: number; /** Classification of the step */ step_type?: 'core' | 'supporting' | 'emotional'; /** Tools or products currently used for this step */ tools_used?: string; } /** SwitchingCost entity. * * @example * const properties: SwitchingCostProperties = { * cost_type: 'financial', * magnitude: { value: 4, label: 'high' }, * barrier_description: 'Extensive Notion workspace with years of history; migration feels daunting.', * } */ interface SwitchingCostProperties { /** Type of switching cost */ cost_type?: 'financial' | 'learning' | 'data' | 'relationship' | 'procedural'; /** How large the barrier is (UPGAssessment on the `severity_5` scale). */ magnitude?: UPGAssessment; /** Free-text description of the barrier */ barrier_description?: string; } /** * UPG Property Schemas: Discovery Domain * * Opportunity, Solution, FeasibilityStudy, DesignSprint entity properties. * * Part of the Unified Product Graph specification. * https://unifiedproductgraph.org/spec * License: MIT */ /** A problem worth solving, grounded in user need and business value * * @example * const properties: OpportunityProperties = { * reach: 4, * frequency: 'monthly', * pain: 4, * } */ /** * Opportunity carries no intrinsic scalar properties: it is defined by its * title/description and its edges (to the problems/needs it addresses and the * solutions proposed against it). Its former scoring inputs (`reach`, * `frequency`, `pain`, `opportunity_score`) were framework-scoped from 0.9.0 — * they live on the `framework_exercise` includes-edge of the `opportunity-sizing` * framework, not on the entity. Removed from the schema in 0.14.0 (UPG-574). */ interface OpportunityProperties { } /** A proposed response to an opportunity * * @example * const properties: SolutionProperties = { * timeline: 'Kickoff 2026-04-22, results by 2026-05-15.', * } * * RICE scoring inputs (`reach`, `impact`, `confidence`, `effort`) and the * computed `rice_score` were framework-scoped from 0.9.0 and removed from the * schema in 0.14.0 (UPG-574): apply the `rice-scoring` framework and read the * score off the `framework_exercise` includes-edge, not the entity. */ interface SolutionProperties { /** Estimated delivery or target timeline */ timeline?: string; } /** FeasibilityStudy entity. * * @example * const properties: FeasibilityStudyProperties = { * study_type: 'technical', * conclusion: 'feasible', * confidence: 'medium', * } */ interface FeasibilityStudyProperties { /** * Type of feasibility being assessed. * @example "technical" for assessing engineering viability */ study_type?: 'technical' | 'business' | 'market' | 'resource'; /** Outcome conclusion of the study */ conclusion?: 'feasible' | 'not_feasible' | 'conditional' | 'needs_more_data'; /** Confidence in the conclusion (UPGAssessment on `confidence_5`). */ confidence?: UPGAssessment; } /** DesignSprint entity. * * @example * const properties: DesignSprintProperties = { * duration: '5 days', * challenge: 'Cut time-to-first-committed-decision from 7 days to 2.', * } */ interface DesignSprintProperties { /** * Duration of the sprint. * @example "5 days" */ duration?: string; /** The core challenge the sprint addresses */ challenge?: string; } /** * UPG Property Schemas: Validation Domain. * Hypothesis, ExperimentPlan, Experiment, ExperimentRun, Learning, Evidence, * ResearchPlan. (`test_plan` re-homed to the QA/testing domain — UPG-678.) * https://unifiedproductgraph.org/spec | MIT */ /** A testable belief. The canonical design artefact for validating product assumptions. * * **Lifecycle-bearing**: progresses through * `drafted → active → validated | invalidated | archived`. The belief either holds, * fails, or is abandoned. * * Evidence attaches via the `hypothesis_has_evidence` edge (neutral direction; * `evidence.direction` carries supports/refutes/neutral polarity). A single * hypothesis can have N evidence nodes; `current_confidence` aggregates the * weighted signal. * * @example * const h: HypothesisProperties = { * we_believe: 'We believe that linking statements to evidence', * will_result_in: 'will result in higher decision confidence', * we_know_when: 'we know when 60% of decisions cite evidence', * risk_if_wrong: 'Wasted dev cycles wiring evidence panels users ignore', * current_confidence: { value: 3, scale: 'confidence_5', label: 'Medium' }, * } */ interface HypothesisProperties { /** The belief being tested. The "if" clause. */ we_believe?: string; /** The expected result. The "then" clause. */ will_result_in?: string; /** The measurable signal that confirms or refutes the claim. */ we_know_when?: string; /** Risk surface for prioritisation if the claim turns out wrong. */ risk_if_wrong?: string; /** * Team confidence at the current point in time. Derived from the weighted * sum of attached `hypothesis_evidence` rows (formula spec'd separately). * Authors may set explicitly; loaders may overwrite from derivation. */ current_confidence?: UPGAssessment; } /** * @deprecated since v0.4.0. Use `EvidenceProperties`. `evidence` is the * canonical entity for all evidence: carries both `evidence_rigor` and * `evidence_source` axes, plus `weight`, `summary`, and `observed_at`. * Attach to a hypothesis via the `hypothesis_has_evidence` edge. * `hypothesis_evidence` will be removed in v0.5.0. */ interface HypothesisEvidenceProperties { /** * Kind of evidence. Drives renderer + filter UI. The provenance edge * (`derived_from_*`) carries the actual source node reference per P14; * this enum is typing/UI metadata. */ evidence_type?: 'experiment_run' | 'observation' | 'quote' | 'metric_change' | 'market_data' | 'interview'; /** * Direction relative to the parent claim: `supports` and `refutes` pair with * the edges of the same name, `neutral` means the data is insufficient or * noisy. * * BREAKING in v0.4.0: `confirms`, `disconfirms` and `inconclusive` no longer * type-check. Migration is one-to-one: confirms to supports, disconfirms to * refutes, inconclusive to neutral. * * @remarks * Aligned to `Evidence.direction` so every direction-of-evidence property in * the spec shares one vocabulary. */ direction?: EvidenceDirection; /** Strength of the evidence (UPGAssessment, scale `scale_5`). */ weight?: UPGAssessment; /** Plain-English summary of what the evidence shows. */ summary?: string; /** ISO date observed. */ observed_at?: ISODate; } /** A structured test designed to validate a hypothesis. The canonical unit of * a test. * * `experiment` is canonical-stable and the spine of the validation chain * `hypothesis → experiment_plan → experiment → experiment_run`: the * `experiment_plan` is the validation design, the `experiment` is the * structured test it produces, and the optional `experiment_run` child captures * the multi-run / replication case (longitudinal or replicated tests). For a * single everyday A/B test, `experiment` alone (with its plan) suffices. * * @example * const properties: ExperimentProperties = { * method: 'A/B test', * start_date: '2026-04-01', * end_date: '2026-09-30', * } */ interface ExperimentProperties { /** Experimental method (e.g. "A/B test", "usability study", "smoke test") */ method?: string; /** ISO start date */ start_date?: ISODate; /** ISO end date */ end_date?: ISODate; /** Targeted participants or observations */ sample_size?: number; /** Expected change in the primary metric */ expected_lift?: number; /** Unit of `expected_lift` */ expected_lift_unit?: 'percentage' | 'absolute' | 'ratio'; /** Observed change in the primary metric */ actual_lift?: number; } /** The validation PLAN: the design for a structured test of a hypothesis * (UCS pattern P4: work-unit). * * `experiment_plan` is the canonical validation plan type (graduated * `proposed → stable` in UPG-664). It carries the plan-shape fields — method, * success criteria, sample size, projected reach/impact, intended dates — and * designs the `experiment` it produces (`experiment_plan_designs_experiment`). * It absorbed `test_plan`'s planning properties when `test_plan` re-homed to * the QA/testing domain (UPG-678): `test_plan` is now the QA * verification-procedure plan, not a validation-planning artefact. * * @example * const plan: ExperimentPlanProperties = { * method: 'a_b_test', * success_criteria: 'Day-7 activation rate +5% lift, p<0.05', * sample_size: 4000, * projected_reach: { value: 4, scale: 'reach_5', evidence: 'Cohort sizing' }, * confidence: { value: 3, scale: 'confidence_5' }, * planned_start_date: '2026-05-01', * planned_end_date: '2026-05-21', * } */ interface ExperimentPlanProperties { /** Experimental method. Drives renderer and analysis tooling. */ method?: 'a_b_test' | 'multivariate' | 'qual_interview' | 'prototype_test' | 'fake_door' | 'wizard_of_oz' | 'longitudinal'; /** Plain-English description of "passing" */ success_criteria?: string; /** * Targeted participants or observations for the planned test. Absorbed from * `test_plan` (UPG-678) when it re-homed to QA; the planning sample size now * lives on the validation plan. */ sample_size?: number; /** Projected reach: how many people the run is expected to touch (UPGAssessment) */ projected_reach?: UPGAssessment; /** Projected impact on the target metric (UPGAssessment) */ projected_impact?: UPGAssessment; /** Team confidence at plan-time (UPGAssessment, scale `confidence_5`) */ confidence?: UPGAssessment; /** Cost estimate at plan-time (UPGAssessment) */ cost_estimate?: UPGAssessment; /** Planned start date */ planned_start_date?: ISODate; /** Planned end date */ planned_end_date?: ISODate; } /** Execution evidence for a structured test of a hypothesis (UCS pattern P6: event-occurrence). * * The optional multi-run / replication child of `experiment` (UPG-664). A * single `experiment` produces one or more runs; each run is its own event * with its own dates, observed reach, outcome summary, severity of finding, * learning, and disposition. Use runs only when longitudinal or replicated * execution matters — an everyday single test needs only the `experiment`. * * @example * const run: ExperimentRunProperties = { * actual_start_date: '2026-05-01', * actual_end_date: '2026-05-22', * actual_reach: 12450, * outcome_summary: 'Variant B uplift +6.3% on day-7 activation, p=0.03.', * severity_of_finding: { value: 4, scale: 'severity_5' }, * learning: 'Onboarding tooltip placement materially shifts activation; productize.', * disposition: 'confirmed', * } */ interface ExperimentRunProperties { /** * What sort of experiment this run is: `ab_test` is a controlled split * between variants, `growth` an acquisition or activation experiment, * `pricing` a packaging or willingness-to-pay one. Distinct from * `disposition`, which is the outcome axis. * * @remarks * Three retired types (`ab_test`, `growth_experiment`, `pricing_experiment`) * collapsed into `experiment_run` precisely along this axis, and the * migration defaults stamp it so the distinction survives consolidation. * * Extensible: further kinds may be added in a minor release. */ experiment_type?: 'ab_test' | 'growth' | 'pricing'; /** ISO actual start date (may differ from the plan's `planned_start_date`) */ actual_start_date?: ISODate; /** ISO actual end date */ actual_end_date?: ISODate; /** Observed reach: how many people the run actually touched */ actual_reach?: number; /** Plain-English outcome */ outcome_summary?: string; /** Severity / strength of the finding (UPGAssessment) */ severity_of_finding?: UPGAssessment; /** What the team learned (rich text) */ learning?: string; /** * Resolution against the parent plan's success criteria. * `confirmed` = evidence supports the parent hypothesis_claim. * `disconfirmed` = evidence refutes the parent hypothesis_claim. * `inconclusive` = data insufficient or noisy. * `aborted` = run terminated early. */ disposition?: 'confirmed' | 'disconfirmed' | 'inconclusive' | 'aborted'; } /** Result of an experiment. Evidence that updates a hypothesis. * * @example * const properties: LearningProperties = { * result: 'Activation rate rose to 72% after tooltip change.', * result_value: 0.72, * result_unit: '%', * } */ interface LearningProperties { /** Summary */ result?: string; /** Measured value of the result */ result_value?: number; /** Unit (e.g. "%" or "ms") */ result_unit?: string; /** * Direction relative to the parent hypothesis. Canonical direction axis. * * BREAKING in v0.4.0: legacy `'positive'`, `'negative'`, `'neutral'` are * replaced by `'supports'`, `'refutes'`, `'neutral'` to align with * `Evidence.direction` and `HypothesisEvidence.direction`. Migration: * `positive → supports`, `negative → refutes`, `neutral → neutral`. */ result_direction?: EvidenceDirection; /** Confidence impact on the parent hypothesis */ confidence_impact?: 'strengthens' | 'weakens' | 'neutral'; } /** ResearchPlan entity. * * @example * const properties: ResearchPlanProperties = { * research_question: 'Which activation step loses the most new users in week one?', * suggested_methods: ['5-minute interview', 'async transcript review'], * evidence_threshold: '3 independent sources', * } */ interface ResearchPlanProperties { /** Primary research question */ research_question?: string; /** Suggested methods */ suggested_methods?: string[]; /** Minimum evidence bar */ evidence_threshold?: string; /** * Suggested completion deadline. * @example "2026-06-30" */ deadline?: string; } /** Evidence supporting or refuting a hypothesis (P2: lifecycle-free snapshot). * * Unified evidence entity that absorbed `hypothesis_evidence` in v0.4.0. * Carries two orthogonal classification axes: * `evidence_rigor` = epistemological rigour (how the data was gathered). * `evidence_source` = origin type (what artefact the evidence came from). * * Attach to a hypothesis via `hypothesis_has_evidence`. `direction` on this * node carries the supports/refutes/neutral polarity. * * @example * const e: EvidenceProperties = { * evidence_rigor: 'quantitative', * evidence_source: 'experiment_run', * direction: 'supports', * weight: { value: 4, scale: 'scale_5', label: 'Strong' }, * summary: 'Variant B uplift +6.3% on day-7 activation, p=0.03 across n=12,450.', * observed_at: '2026-05-22', * } */ interface EvidenceProperties { /** Epistemological rigour. How the data was gathered. */ evidence_rigor?: 'quantitative' | 'qualitative' | 'anecdotal' | 'expert_opinion'; /** * Origin type. Drives renderer + filter UI; the provenance edge * (`derived_from_*`) carries the actual source node reference. */ evidence_source?: 'experiment_run' | 'observation' | 'quote' | 'metric_change' | 'market_data' | 'interview'; /** Direction relative to the parent hypothesis. */ direction?: EvidenceDirection; /** Strength (UPGAssessment, scale `scale_5`). */ weight?: UPGAssessment; /** Plain-English summary. */ summary?: string; /** ISO date observed. */ observed_at?: ISODate; /** Free-text provenance note */ source?: string; } /** * UPG Property Schemas: Market Intelligence Domain. * Competitor, CompetitorFeature, MarketTrend, MarketSegment, CompetitiveAnalysis. * https://unifiedproductgraph.org/spec | MIT */ /** A product or approach competing for the same user need. * * @example * const properties: CompetitorProperties = { * positioning: 'For product teams drowning in AI output.', * pricing_model: 'per-seat SaaS', * website: 'https://entopo.app', * strengths: ['Real-time multiplayer canvas', 'Generous free tier'], * weaknesses: ['Weak API query capabilities', 'No mobile companion'], * } */ interface CompetitorProperties { /** Market positioning */ positioning?: string; /** Pricing model (e.g. "freemium", "per-seat SaaS", "usage-based") */ pricing_model?: string; /** Public website URL */ website?: string; /** * Bulleted factual strengths. Each item is a short statement, not prose. * @example ['Real-time multiplayer canvas', 'Generous free tier'] */ strengths?: string[]; /** * Bulleted factual weaknesses: gaps, friction, or capabilities materially below market. * @example ['Weak API query capabilities', 'No mobile companion'] */ weaknesses?: string[]; /** * Provenance: ISO date-time this record was last observed or refreshed. * Lets a stale record be told apart from a fresh one. * @example "2026-06-13" */ last_updated?: string; /** * Provenance: where this was observed. A changelog, pricing, or docs URL, * an analyst report, or a research note. * @example "https://docs.larch.example/pricing/" */ source?: string; /** * Provenance: how sure we are, on the canonical confidence_5 scale. * Carries both a numeric value and a high / medium / low label. */ confidence?: UPGAssessment; /** * Provenance: agent or routine id that last wrote this. Absent when * hand-authored, the signal that a human (not a poller) is the last writer. * @example "competitor-watch-agent" */ observed_by?: string; } /** Competitor feature or capability. * * @example * const properties: CompetitorFeatureProperties = { * is_gap: true, * our_equivalent: 'Canvas workspace', * quality: 'better', * } */ interface CompetitorFeatureProperties { /** * Our equivalent feature, if any. Leave empty when we offer nothing equivalent. * @example "Canvas collaboration" */ our_equivalent?: string; /** Gap in our offering. True when `our_equivalent` is absent or materially inferior. */ is_gap: boolean; /** * Quality comparison. * `better` = ours is meaningfully superior. `same` = roughly equivalent. * `worse` = theirs is meaningfully superior. `missing` = we have no equivalent. */ quality?: 'better' | 'same' | 'worse' | 'missing'; /** * Parity. More granular than `quality`; captures whether the gap is offensive or defensive. * `ahead` = we lead. `behind` = they lead. `parity` = equivalent. * `unique_to_us` / `unique_to_them` = only one side offers it. */ parity_status?: 'ahead' | 'behind' | 'parity' | 'unique_to_us' | 'unique_to_them'; /** * ISO date this assessment was last updated. Competitor feature landscapes change quickly. * @example "2026-02-15" */ last_updated?: string; /** * Provenance: where this was observed. A changelog, pricing, or docs URL, * an analyst report, or a research note. * @example "https://docs.larch.example/changelog/" */ source?: string; /** * Provenance: how sure we are, on the canonical confidence_5 scale. * Carries both a numeric value and a high / medium / low label. */ confidence?: UPGAssessment; /** * Provenance: agent or routine id that last wrote this. Absent when * hand-authored, the signal that a human (not a poller) is the last writer. * @example "competitor-watch-agent" */ observed_by?: string; } /** Market trend. * * @example * const properties: MarketTrendProperties = { * relevance: 4, * timeframe: '12-18 months', * impact: 4, * } */ interface MarketTrendProperties { /** Relevance to our product (1 = low, 5 = critical). */ relevance?: UPGAssessment; /** * Expected peak or mainstream window. * @example "12-18 months", "2027" */ timeframe?: string; /** Expected impact on our market or category. */ impact?: UPGAssessment; /** * Source of the trend data: analyst report, research firm, observed behaviour. * @example "Gartner Hype Cycle 2025", "Observed in user interviews Q1 2026" */ source?: string; /** * Provenance: ISO date-time this record was last observed or refreshed. * Lets a stale record be told apart from a fresh one. * @example "2026-06-13" */ last_updated?: string; /** * Provenance: how sure we are, on the canonical confidence_5 scale. * Carries both a numeric value and a high / medium / low label. */ confidence?: UPGAssessment; /** * Provenance: agent or routine id that last wrote this. Absent when * hand-authored, the signal that a human (not a poller) is the last writer. * @example "competitor-watch-agent" */ observed_by?: string; } /** MarketSegment entity. * * @example * const properties: MarketSegmentProperties = { * segment_size: 12000, * growth_rate: 0.15, * tam: 42, * } */ interface MarketSegmentProperties { /** Potential customers in this segment */ segment_size?: number; /** * YoY growth rate as a decimal. * @example 0.15 represents 15% */ growth_rate?: number; /** Total Addressable Market (currency units) */ tam?: number; /** Serviceable Addressable Market (currency units) */ sam?: number; } /** Competitive analysis exercise or snapshot. * * @example * const properties: CompetitiveAnalysisProperties = { * analysis_type: 'feature_comparison', * analysis_date: '2026-03-15', * framework_id: 'lean-canvas', * } */ interface CompetitiveAnalysisProperties { /** * Type of analysis. * `feature_comparison` = side-by-side matrix. * `positioning` = competitor positioning relative to each other. * `swot` = strengths, weaknesses, opportunities, threats. * `pricing` = pricing structure comparison. */ analysis_type?: 'feature_comparison' | 'positioning' | 'swot' | 'pricing'; /** * ISO date conducted. Competitive intelligence decays quickly; track snapshot age. * @example "2026-03-15" */ analysis_date?: ISODate; /** * Framework ID (references `UPGFramework.id`). * @example "porter-five-forces", "swot-analysis", "competitive-matrix" */ framework_id?: string; /** * Empty cells in a two-axis classification matrix that earn explicit * commentary, and only the strategically interesting ones. Opportunity-kind * cells are the most valuable: they identify unoccupied strategic space. * * @remarks * Each entry references two `classification_value` nodes by id, one from each * `classification_axis` child of this `competitive_analysis`. * `validate_graph` enforces that the refs resolve to `classification_value` * nodes whose parents are distinct `classification_axis` instances, so a cell * cannot name two values from the same axis. * * See `ClassificationValueProperties.commitments` for the inverse * "occupied cell" structural-definition shape. * * A worked pair, one of each rationale kind: * [ * { axis_a_value_ref: 'val-git-based', axis_b_value_ref: 'val-structured-text', * rationale_kind: 'opportunity', * rationale_md: 'No technical reason; only adoption inertia. Watching brief.' }, * { axis_a_value_ref: 'val-composable', axis_b_value_ref: 'val-wysiwyg', * rationale_kind: 'structural', * rationale_md: 'Composable rejects HTML blobs; WYSIWYG requires them.' }, * ] */ empty_cells?: Array; /** * Provenance: ISO date-time this record was last observed or refreshed. * Distinct from `analysis_date` (when the analysis was conducted). * @example "2026-06-13" */ last_updated?: string; /** * Provenance: where this was observed. A changelog, pricing, or docs URL, * an analyst report, or a research note. * @example "https://docs.larch.example/changelog/" */ source?: string; /** * Provenance: how sure we are, on the canonical confidence_5 scale. * Carries both a numeric value and a high / medium / low label. */ confidence?: UPGAssessment; /** * Provenance: agent or routine id that last wrote this. Absent when * hand-authored, the signal that a human (not a poller) is the last writer. * @example "competitor-watch-agent" */ observed_by?: string; } /** Competitor signal: a dated competitor move mapped onto our portfolio. * * An append-only event (feature launch, pricing change, acquisition, partnership, * market entry) emitted by a competitor. Distinct from `market_trend` (macro, * no single actor) and `launch` (our own ship event). * * @example * const properties: CompetitorSignalProperties = { * observed_at: '2026-06-10', * signal_type: 'feature_launch', * summary: 'Shipped Visual Editor AI Assist', * impact: 'high', * } */ interface CompetitorSignalProperties { /** * ISO date-time the move was observed. * @example "2026-06-10" */ observed_at?: ISODateTime; /** * Kind of move: a shipped `feature_launch`, a `pricing_change`, the strategic * `acquisition` / `partnership` / `market_entry`, or a `reclassification` * when the competitor moves between classification cells on an axis. * * @remarks * `reclassification` is auto-emitted at the classify-write chokepoint rather * than authored, and carries `axis`, `from_value`, `to_value` and * `competitor` so the move is reconstructable without diffing two snapshots. */ signal_type?: 'feature_launch' | 'pricing_change' | 'acquisition' | 'partnership' | 'market_entry' | 'reclassification'; /** One-line factual summary of the move (what shipped, not marketing copy). */ summary?: string; /** Expected impact on our position. */ impact?: 'high' | 'medium' | 'low'; /** * Reclassification only. The qualified id of the competitor that moved, as the * classify cross-edge source (e.g. `p_rival/n_acme`). Identifies both the * subject and its owning product, so `diff_classification({ product })` can * filter the history stream. */ competitor?: string; /** * Reclassification only. The `classification_axis` id the move is on (e.g. * `ca_ai_maturity`). Mirrors the axis the superseded and new classify edges * share. */ axis?: string; /** * Reclassification only. The prior `classification_value` id the competitor * was classified as before this move (the superseded cell). Absent for a * first-time classification (nothing was superseded). */ from_value?: string; /** * Reclassification only. The new `classification_value` id the competitor is * classified as after this move (the cell the new classify edge points at). */ to_value?: string; /** * Provenance: ISO date-time this record was last observed or refreshed. * @example "2026-06-13" */ last_updated?: string; /** * Provenance: where this was observed. A changelog, pricing, or docs URL, * an analyst report, or a research note. * @example "https://docs.larch.example/changelog/" */ source?: string; /** * Provenance: how sure we are, on the canonical confidence_5 scale. * Carries both a numeric value and a high / medium / low label. */ confidence?: UPGAssessment; /** * Provenance: agent or routine id that last wrote this. Absent when * hand-authored, the signal that a human (not a poller) is the last writer. * @example "competitor-watch-agent" */ observed_by?: string; } /** * Defining architectural commitment of a `classification_value`. * * The N (typically 2–4) load-bearing axes that define the category. Removing * any one collapses the category into a different paradigm: "take typing away * from composable and you have headless, not composable." * * Commitments describe the *load-bearing definition*; `ClassificationCapability` * entries describe the *surface capabilities*. */ interface ClassificationCommitment { /** Short structured label. What the commitment is (e.g. "Typed content graph"). */ name: string; /** One paragraph: why this commitment is load-bearing. */ description: string; } /** * The six canonical capability surfaces of a `classification_value`. * * Enum-narrowed because competitive-landscape exercises treat these as fixed columns. * Propose an addition rather than free-stringing a 7th surface. */ type ClassificationCapabilitySurface = 'delivery' | 'extensibility' | 'collaboration' | 'preview' | 'localization' | 'developer_experience'; /** * A capability surface and its bullet list on a `classification_value`. * * Collapses six identically-shaped dossier sections (delivery / extensibility / * collaboration / preview / localization / DX) into one structured field. * Each surface appears at most once per `classification_value`. * * Capabilities describe the *surface offering*; companion `ClassificationCommitment` * entries describe the *load-bearing definition*. */ interface ClassificationCapability { /** Canonical surface this group describes */ surface: ClassificationCapabilitySurface; /** Ordered short factual bullets */ bullets: string[]; } /** * Reason a (axis_a × axis_b) cell in a 2-axis classification matrix is unoccupied. * * Three flavours observed in competitive-landscape work: * `structural` = structurally incompatible; physics says no. * `ideological` = culturally implausible; values/incentives say no. * `opportunity` = nobody has done it yet; watching brief, strategic option. * * Opportunity-kind cells are the most analytically valuable: they identify * unoccupied strategic space. */ type EmptyCellRationaleKind = 'structural' | 'ideological' | 'opportunity'; /** * An empty (axis_a_value × axis_b_value) cell with rationale for non-occupancy. * * Meaningful on `competitive_analysis` nodes with exactly two * `classification_axis` children. `validate_graph` advises when this shape * appears on a non-2-axis analysis. * * `axis_a_value_ref` and `axis_b_value_ref` are string ids of * `classification_value` nodes (refs by id, not embedded snapshots, so the shape * survives value-rename). Each ref must be a child of a distinct * `classification_axis` (one from each axis). */ interface EmptyCell { /** Id of the axis-A `classification_value` */ axis_a_value_ref: string; /** Id of the axis-B `classification_value` */ axis_b_value_ref: string; /** Reason for non-occupancy */ rationale_kind: EmptyCellRationaleKind; /** Markdown rationale. One paragraph. Opportunity cells deserve `research_question` children. */ rationale_md: string; } /** ClassificationAxis: a dimension along which subjects are classified. * * A top-level taxonomy axis, hosted by either a `competitive_analysis` (via * `competitive_analysis_dimensioned_by_classification_axis`) or a `product` (via * `product_dimensioned_by_classification_axis`, added at 0.32.0 so a product can * carry its own label taxonomy without inventing a `tag` entity). Common shapes: * "CMS Architecture", "Editing Paradigm", "Database × Workload Type", * "IDE × Language Ecosystem". * * @example * const properties: ClassificationAxisProperties = { * axis_kind: 'categorical', * } * * @example * // Canonical governance axis `change_blast_radius` (ordinal): grade a change / * // feature / decision by how much risk it carries, so review ceremony and * // agent-autonomy gating scale with reach. Four ordered values — * // Trivial(0) < Standard(1) < Significant(2) < Critical(3) — classified via the * // polymorphic `node_classified_as_classification_value` edge. Report metrics * // WITHIN tier (pooling across tiers is Simpson's-paradox bait). See * // decisions/2026-07-15-change-blast-radius-axis.md. * const properties: ClassificationAxisProperties = { * axis_kind: 'ordinal', * } */ interface ClassificationAxisProperties { /** * Structural kind of values on this axis. * `categorical` = discrete, unordered (most common; CMS architectures). * `ordinal` = discrete, ordered (maturity tiers, T-shirt sizes). * `continuous` = numeric range (latency budget, price points). */ axis_kind?: 'categorical' | 'ordinal' | 'continuous'; /** * How many values a subject may hold on this axis at once. `single` (the * default) means re-classifying SUPERSEDES the prior value; `multi` means it * ADDS one. * * @remarks * Under `single` the classify writer retires the old same-axis edge and * records the move in the reclassification history, so the change is * traceable rather than silent. `multi` suits an axis like "supported * frameworks", where holding several values at once is the truth. * * A separate axis from `axis_kind`: an axis can be `categorical` (unordered) * yet single-select, or `categorical` yet multi-select. The two answer * different questions and neither implies the other. */ cardinality?: 'single' | 'multi'; } /** ClassificationValue: a value on a classification axis. * * One position on a `classification_axis`: "Composable Structured-Content", * "Headless API-First", "Git-Based Repo-as-Database". Reusable across the row * dossier and the column dossier of a 2-axis matrix. * * @example * const properties: ClassificationValueProperties = { * rationale: 'Composable extends headless with three load-bearing commitments.', * exemplars: ['Nimbus', 'Larch', 'Prism'], * commitments: [ * { name: 'Typed content graph', description: 'Schemas in code; references as edges.' }, * { name: 'Real-time backend', description: 'Live queries, CRDT collaboration.' }, * { name: 'Embeddable studio', description: 'Editor is a library hosted inside the team app.' }, * ], * capabilities: [ * { surface: 'delivery', bullets: ['NQL', 'GraphQL', 'REST', 'Live Sync API'] }, * ], * } */ interface ClassificationValueProperties { /** * Short paragraph: why this value earns its own row/column. * Longer narrative belongs in `summary_md` or attached `content_piece` nodes. */ rationale?: string; /** * Free-text examples of products occupying this value. * For queryable occupancy, prefer `competitor` nodes with `classified_as` edges. * @example ['Nimbus', 'Larch', 'Prism'] */ exemplars?: string[]; /** * The load-bearing commitments that define this category, typically two to * four. Each is a structural axis: removing it changes the category. * * @remarks * Pairs with `capabilities`, and the distinction matters when deciding where * a bullet belongs: commitments are the load-bearing DEFINITION, capabilities * are the surface OFFERING. A capability can be dropped without the category * changing; a commitment cannot. * * A worked set: * [ * { name: 'Typed content graph', description: 'Schemas in code; references as edges; content is structured data.' }, * { name: 'Real-time backend', description: 'Live queries, CRDT collaboration, sub-second propagation.' }, * { name: 'Embeddable studio', description: 'Editor is a library hosted inside the team app, not an external portal.' }, * ] */ commitments?: Array; /** * Structured capability bullets across the six canonical surfaces. Each * surface appears at most once per `classification_value`. * * @remarks * Pairs with `commitments`, and the distinction decides where a bullet * belongs: capabilities describe what the category OFFERS, commitments * describe what DEFINES it. Dropping a capability leaves the category intact; * dropping a commitment does not. * * @example * [ * { surface: 'delivery', bullets: ['NQL', 'GraphQL', 'REST', 'Live Sync API', 'Asset CDN'] }, * { surface: 'extensibility', bullets: ['Custom input components', 'editor plugins'] }, * ] */ capabilities?: Array; } /** * UPG Property Schemas: User Research Domain. * ResearchStudy, Observation, Quote, InsightProperties, AffinityCluster, * ResearchQuestion, InterviewGuide, SurveyResponse, Participant. * https://unifiedproductgraph.org/spec | MIT */ /** Structured user research activity. * * @example * const properties: ResearchStudyProperties = { * method: 'interview', * participant_count: 8, * start_date: '2026-04-01', * } */ interface ResearchStudyProperties { /** Research method used to conduct the study */ method?: 'interview' | 'usability' | 'survey' | 'diary' | 'analytics'; /** Number of participants recruited or observed */ participant_count?: number; /** ISO date when the study starts */ start_date?: ISODate; /** ISO date when the study ends */ end_date?: ISODate; } /** Research participant. * * @example * const properties: ParticipantProperties = { * alias: 'primary-alias', * segment: 'power-users', * recruit_source: 'Existing beta waitlist', * } */ interface ParticipantProperties { /** Anonymous alias for privacy (e.g. "P01") */ alias?: string; /** How the participant was recruited */ recruit_source?: string; /** Current consent status for data usage */ consent_status?: 'pending' | 'given' | 'withdrawn'; /** * Stable deep-link to the exact moment this participant appears in the * originating recording or transcript. A per-moment locator, not a * study-level link. Rot-prone external pointer; treat as `volatile`. */ source_url?: string; } /** Discrete observation captured during research. Absorbed from the deprecated Highlight entity. * * @example * const properties: ObservationProperties = { * content: 'The full body text of this entity, written as the user would read it.', * source_type: 'quote', * session_ref: 'sess_01HXYZ123456', * } */ interface ObservationProperties { /** Note or highlight text. */ content?: string; /** Producing research method. */ source_type?: 'quote' | 'behavior' | 'metric'; /** * Capturing session reference. Convenience field; the canonical * relationship to study/session is an edge per P14. Retained as a * lightweight context anchor for AI inference. */ session_ref?: string; /** Flagged as a highlight. Absorbed from the deprecated Highlight entity. */ is_highlighted?: boolean; /** * Free-form highlight type tag. * @example "pain", "delight", "behaviour", "moment of clarity" */ highlight_tag?: string; /** * Structured sentiment. Tools like Dovetail and EnjoyHQ converge on these * four values. More precise than `highlight_tag` for aggregation. */ sentiment?: SignalSentiment; /** * Stable deep-link to the exact moment this observation was captured in the * originating recording or transcript. A per-moment locator, not a * study-level link. Distinct from `session_ref`, which holds an opaque * session ID for AI inference. Rot-prone external pointer; treat as `volatile`. */ source_url?: string; } /** Verbatim quote from a participant. * Link to the participant via an edge, not a speaker property. * * @example * const properties: QuoteProperties = { * text: 'text', * timestamp: '2026-04-17T09:00:00Z', * } */ interface QuoteProperties { /** Quoted text */ text: string; /** When said (ISO timestamp or session offset) */ timestamp?: string; /** * Stable deep-link to the exact moment this quote was spoken in the * originating recording or transcript. A per-moment locator, not a * study-level link. Rot-prone external pointer; treat as `volatile`. */ source_url?: string; } /** Unified insight, synthesised from evidence and observations. * * @example * const properties: InsightProperties = { * insight_level: 'pattern', * confidence: 'medium', * evidence_count: 42, * } */ interface InsightProperties { /** * Maturity level. * `pattern` = recurring observation, not yet interpreted. * `finding` = interpreted pattern with a clear meaning. * `actionable` = finding with a clear next step. * `strategic` = finding that affects product direction. */ insight_level?: 'pattern' | 'finding' | 'actionable' | 'strategic'; /** Confidence (UPGAssessment on `confidence_5`). Reflects the strength and diversity of supporting evidence. */ confidence?: UPGAssessment; /** Supporting observations, quotes, or evidence items. Higher counts increase confidence. */ evidence_count?: number; /** * Novelty against existing knowledge. * `known` = confirms what we already believed. * `surprising` = challenges or extends our understanding. * `contradictory` = directly conflicts with a prior assumption. */ novelty?: 'known' | 'surprising' | 'contradictory'; /** * Current actionability. * `immediate` = clear action, no further research needed. * `needs_validation` = promising but requires more evidence. * `informational` = important context, no direct action. */ actionability?: 'immediate' | 'needs_validation' | 'informational'; /** * Producing research method. * @example "usability_study", "interview_series", "survey" */ source_method?: string; /** * Which discipline the insight came out of. The second provenance axis, * symmetrical with `source_method`: that one is HOW the insight was produced, * this one is WHICH PRACTICE produced it, and neither substitutes for the * other. * * @example "ux", "support", "sales", "data_science" * * @remarks * Declared in v0.26.0 so the retired `ux_insight` type's migration default * (`source_domain: 'ux'`) lands in a typed field rather than being lost. */ source_domain?: string; /** * Insight statement in plain language. Write as an active, present-tense assertion. * @example "Users consistently skip the tutorial because they trust their ability to explore independently." */ statement?: string; /** Product implications. The so-what. */ implications?: string; } /** Affinity cluster grouping observations. * * @example * const properties: AffinityClusterProperties = { * theme: 'activation', * child_observation_count: 42, * confidence: 'medium', * } */ interface AffinityClusterProperties { /** Emergent theme label */ theme?: string; /** Observations in this cluster */ child_observation_count?: number; /** Confidence in the theme's validity (UPGAssessment on `confidence_5`). */ confidence?: UPGAssessment; } /** Research question guiding a study. * * @example * const properties: ResearchQuestionProperties = { * question_type: 'exploratory', * priority: 'high', * } */ interface ResearchQuestionProperties { /** Question classification */ question_type?: 'exploratory' | 'evaluative' | 'generative'; /** Importance to answer */ priority?: Priority; } /** Interview guide document. * * @example * const properties: InterviewGuideProperties = { * guide_type: 'structured', * question_count: 42, * duration_minutes: 45, * } */ interface InterviewGuideProperties { /** Format structure */ guide_type?: 'structured' | 'semi_structured' | 'unstructured'; /** Total questions */ question_count?: number; /** Expected length (minutes) */ duration_minutes?: number; } /** Aggregated survey response data. * * @example * const properties: SurveyResponseProperties = { * response_count: 128, * completion_rate: 0.72, * method: 'email', * } */ interface SurveyResponseProperties { /** Total responses */ response_count?: number; /** Completion (0–1) */ completion_rate?: number; /** Distribution method */ method?: 'email' | 'in_app' | 'phone' | 'other'; } /** * UPG Property Schemas: UX Design Domain. * UserJourney, JourneyStep, DesignQuestion, DesignConcept, Prototype, * Wireframe, UserFlow, Screen, ScreenState, Surface, Annotation, InteractionSpec. * https://unifiedproductgraph.org/spec | MIT */ /** Whether a journey maps current or future state. * * BREAKING in v0.9.9 (UPG-675): the `'service_blueprint'` value is deprecated * and removed from the union. A service blueprint is a first-class * `service_blueprint` *entity* (in the customer_success domain), not a *type of* * `user_journey`. Migration: a `user_journey` carrying * `journey_type: 'service_blueprint'` should be re-modelled as a * `service_blueprint` node; drop the journey_type value (it no longer * type-checks). `current_state` / `future_state` / `day_in_the_life` are kept. */ type JourneyType = 'current_state' | 'future_state' | 'day_in_the_life'; /** User journey map. * * @example * const properties: UserJourneyProperties = { * scope: 'Covers in-product onboarding; excludes lifecycle email.', * scenario: 'First-time user lands in an empty workspace.', * } */ interface UserJourneyProperties { /** Scope (e.g. "end-to-end onboarding") */ scope?: string; /** Maps current or future state */ journey_type?: JourneyType; /** Scenario context */ scenario?: string; } /** Phase within a user journey. A temporal BAND over the journey's step * timeline, not a container that owns steps (UPG-663). A journey map renders * phases as a horizontal band above one timeline; a phase spans a range of * that timeline. Steps belong to the journey, not the phase. * * Per UPG principle P14, structural relationships are edges: * parent journey: `user_journey_passes_through_journey_phase` * (the journey carries the phase as a non-owning band overlay) * spanned steps: `journey_phase_spans_journey_step` (non-owning; the steps * are owned by `user_journey_contains_journey_step`, mirroring the * marketing precedent `customer_journey_stage_spans_journey_step`) * phase ordering: the `phase_order` scalar below (the convention shared * with `journey_step.step_order` and `journey_action.action_order`) * * @example * const properties: JourneyPhaseProperties = { * phase_order: 1, * label: 'Onboarding', * goal: 'Cut time-to-first-value from 7 days to 2.', * emotion_arc: 'rising', * entry_trigger: 'User completes signup and lands in an empty workspace', * exit_trigger: 'User commits their first decision node', * key_questions: ['What can I do here?', 'Will this help my team?', 'How do I get started?'], * timeframe: 'days 1–3', * } */ interface JourneyPhaseProperties { /** Display order within the journey (0-indexed) */ phase_order?: number; /** * Short human-readable name. * @example "Discovery", "Onboarding", "Activation" */ label?: string; /** What the user is trying to accomplish */ goal?: string; /** Directional shape of user emotion. Spots design opportunities at dips and payoff points at peaks. */ emotion_arc?: 'rising' | 'steady' | 'falling' | 'mixed'; /** Event or signal marking entry into this phase */ entry_trigger?: string; /** Event or signal marking exit. Pairs with the next phase's `entry_trigger`. */ exit_trigger?: string; /** Open questions the user asks themselves. Fuel for design and content priorities. */ key_questions?: string[]; /** * Typical time window. * @example "first 30 seconds", "days 1–7", "onboarding week" */ timeframe?: string; } /** Single step within a user journey. A user-moment on the journey's single * step timeline. Steps belong to the journey via * `user_journey_contains_journey_step` (the stable 0.1.0 spine); a * `journey_phase` spans a range of them but does not own them (UPG-663). * * @example * const properties: JourneyStepProperties = { * step_order: 1, * touchpoint: 'in-product', * channel: 'in-product', * emotion_score: 4, * } */ interface JourneyStepProperties { /** * Display order within the journey's step timeline (0-indexed). The scalar * ordering convention shared with `journey_phase.phase_order` and * `journey_action.action_order` (UPG-663). For branching journeys, the * explicit `journey_step_precedes_journey_step` edge captures the chain. */ step_order?: number; /** Channel (e.g. "web", "email", "in-store") */ channel?: string; /** User emotion (1 = very negative, 5 = very positive) */ emotion_score?: UPGAssessment; /** Friction (1 = effortless, 5 = very painful) */ friction_score?: UPGAssessment; /** What the user is thinking */ thought?: string; /** Responsible owner. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; } /** Discrete action at a journey step, classified by service layer. * The finest blueprint layer (a service-blueprint row within a moment). * Enables service blueprint rendering and cross-domain linking. * * Per UPG principle P14, structural relationships are edges: * parent step: `journey_step_has_action` (the step owns its actions) * downstream need: `journey_action_surfaces_need`. Opportunity discovery * routes through `need`, which reaches `opportunity` via * `opportunity_addresses_need`; the `pain_score` / `opportunity_score` * scalars below are blueprint-cell diagnostics that fuel that discovery. * realising feature: `journey_action_realised_by_feature` * * The `system` property is a display label naming the performing system; when * a `service` entity exists, model the relationship structurally rather than * relying on the label. * * @example * const properties: JourneyActionProperties = { * action_order: 0, * layer: 'user', * action_description: 'User pastes a meeting transcript into the empty canvas', * channel: 'in-app', * pain_score: 4, * opportunity_score: 5, * evidence: 'Pasted transcript appears as a single text node', * system: 'canvas-paste-handler', * notes: 'Most users hesitate before pasting; too much trust required.', * } */ interface JourneyActionProperties { /** * Display order of this action within its step (0-indexed). The scalar * ordering convention shared with `journey_phase.phase_order` and * `journey_step.step_order` (UPG-663). Orders the service-blueprint rows * within a single moment. */ action_order?: number; /** Service layer */ layer: 'user' | 'frontstage' | 'backstage' | 'support'; /** Plain-language description. Primary content of the action. */ action_description?: string; /** Channel or surface. Keeps service-blueprint columns consistent across the journey. */ channel?: 'in-app' | 'email' | 'web' | 'mobile' | 'phone' | 'in-person' | 'sms' | 'social' | 'other'; /** Pain (1 = effortless, 5 = very painful). Drives opportunity discovery. */ pain_score?: UPGAssessment; /** Opportunity (1 = low leverage, 5 = high leverage). Pairs with `pain_score` to rank investment. */ opportunity_score?: UPGAssessment; /** Physical or digital evidence visible at this point */ evidence?: string; /** Performing system or service */ system?: string; /** Free-text notes, observations, or follow-up questions */ notes?: string; } /** Design question framing an open problem. * * Seed of a design exploration. Sits upstream of `design_concept` (proposed * solutions) and downstream of `need` and `insight` (surfacing signals). * Lifecycle (open → researching → answered → parked / archived) is governed * by the canonical `DISCOVERY_TEMPLATE`. * * Per UPG principle P14, structural relationships are edges: * upstream signal: `insight_inspires_design_question` * upstream need: `need_reframed_as_design_question` * downstream answer: `design_question_answered_by_design_concept` * * @example * const properties: DesignQuestionProperties = { * question: 'How might we help first-time users feel productive within their first session?', * problem_context: 'Teams have rich AI output but no place to see how pieces connect.', * hypothesis: 'A guided first-canvas template will lift day-1 activation by 8pp.', * target_domain: 'ux', * framing: 'how_might_we', * priority: 'high', * confidence: 'medium', * assumptions: [ * 'Empty-state friction is the dominant drop-off cause', * 'Users tolerate one guided template before exploring freely', * ], * validation_method: 'usability_test', * } */ interface DesignQuestionProperties { /** The question itself ("How might we…?", "What if…?"). Primary content. */ question?: string; /** Context that prompted the question */ problem_context?: string; /** Working hypothesis. Captured up-front so research can confirm or disconfirm. */ hypothesis?: string; /** Target design discipline */ target_domain?: 'ux' | 'visual' | 'interaction' | 'content' | 'accessibility' | 'other'; /** Question framing template */ framing?: 'how_might_we' | 'what_if' | 'why_do' | 'how_do' | 'what_prevents' | 'other'; /** Importance against other backlog questions */ priority?: Priority; /** Confidence the question is well-framed (UPGAssessment on `confidence_5`). Distinct from confidence in any answer. */ confidence?: UPGAssessment; /** Underlying assumptions. Surfaced explicitly so they can be challenged or validated. */ assumptions?: string[]; /** Primary validation method */ validation_method?: 'interview' | 'survey' | 'usability_test' | 'analytics' | 'a_b_test' | 'prototype_test' | 'literature_review' | 'other'; } /** Design concept being explored. * * @example * const properties: DesignConceptProperties = { * sketch_url: 'https://figma.com/file/abc/sketch', * rationale: 'Reduces support burden and lifts activation, both priorities this quarter.', * concept_status: 'exploring', * } */ interface DesignConceptProperties { /** URL of the sketch or visual */ sketch_url?: string; /** Selection or rejection rationale */ rationale?: string; /** Current selection status */ concept_status?: 'exploring' | 'validated' | 'selected' | 'rejected'; /** Development stage, from rough idea to presentation-ready */ maturity?: 'sketch' | 'refined' | 'final'; /** Shepherding designer or researcher. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; } /** Prototype. * * @example * const properties: PrototypeProperties = { * fidelity: 'low', * tool: 'entopo', * } */ interface PrototypeProperties { /** Detail level */ fidelity?: 'low' | 'medium' | 'high'; /** Authoring tool */ tool?: string; } /** Wireframe. * * @example * const properties: WireframeProperties = { * fidelity: 'low', * screen_name: 'OnboardingChecklist', * version: '0.3.1', * } */ interface WireframeProperties { /** Detail level */ fidelity?: 'low' | 'medium' | 'high'; /** Version or iteration (e.g. "v2", "2026-04-B") */ version?: string; /** * Authoring tool. * @example "Figma", "Balsamiq", "pen and paper" */ tool?: string; /** Review gate status */ review_status?: 'draft' | 'in_review' | 'approved' | 'rejected'; /** URL of the corresponding interactive prototype */ linked_prototype_url?: string; } /** User flow. * * @example * const properties: UserFlowProperties = { * steps: ['Open the workspace', 'Pick a persona', 'Commit a decision'], * trigger: 'User opens the third restricted feature in a session.', * success_state: 'Dashboard shows a populated graph with at least one committed decision.', * } */ interface UserFlowProperties { /** * What sort of flow this is. Free-form rather than a closed enum, because * flow kinds are product-specific and the spec has no complete vocabulary * for them. * * @example "onboarding", "checkout", "recovery", "upgrade" * * @remarks * `user_flow` otherwise carries only structure (`trigger`, `steps`, * `success_state`, `failure_state`) and no classification. The retired * `onboarding_flow` type collapsed into `user_flow` along this axis, and its * migration default stamps the value so the distinction survives. */ flow_type?: string; /** * Display order of this flow among sibling flows (0-indexed). The scalar * ordering convention shared with `journey_step.step_order` and * `journey_action.action_order` (UPG-663 / UPG-674). The free-text `steps` * array below still captures the within-flow narrative; this scalar makes the * flow itself a deterministically orderable sibling. */ flow_order?: number; /** Initiating event */ trigger?: string; /** Ordered steps */ steps: string[]; /** Successful completion */ success_state?: string; /** Failed completion */ failure_state?: string; } /** Screen in the product. * * @example * const properties: ScreenProperties = { * route: '/workspace/:slug', * viewport: 'mobile', * access_level: 'public', * } */ interface ScreenProperties { /** * Application route. * @example "/dashboard", "/settings/billing" */ route?: string; /** Primary target viewport */ viewport?: 'mobile' | 'tablet' | 'desktop' | 'tv' | 'watch' | 'responsive'; /** Reach */ access_level?: 'public' | 'authenticated' | 'admin' | 'internal'; /** One-line purpose */ purpose?: string; } /** Specific state of a screen. * * @example * const properties: ScreenStateProperties = { * state_name: 'empty', * trigger: 'User opens the third restricted feature in a session.', * condition: 'personas.length > 0 && opportunities.length === 0', * } */ interface ScreenStateProperties { /** * Display order of this state within its parent screen (0-indexed). The scalar * ordering convention shared with `journey_step.step_order` and * `journey_action.action_order` (UPG-663 / UPG-674). Orders the states a * screen moves through (e.g. skeleton, loading, populated). */ state_order?: number; /** State */ state_name: 'empty' | 'loading' | 'error' | 'populated' | 'skeleton' | 'partial'; /** Cause for entering this state */ trigger?: string; /** Data or environmental condition the state represents */ condition?: string; /** User-visible copy */ message?: string; } /** Structural kind of a surface. Determines what may legally nest inside it * (see `UPG_VALID_CHILDREN.surface` and `surface_contains_surface`). * * - `shell` — the outermost frame a product renders into (window chrome, app shell). * - `tool` — a self-contained working environment hosted by the shell (a canvas, an editor). * - `pane` — a resizable division of a tool or shell (sidebar, inspector, split view). * - `region` — a named zone inside a pane with its own layout rules (header, body, footer). * - `slot` — a single addressable insertion point that holds one occupant at a time. * - `gutter` — a narrow margin rail alongside content (line numbers, field affordances). * - `action_bar` — a strip that collects invocable controls (toolbar, footer bar, command row). * - `overlay` — a surface drawn above the stack, temporarily taking focus (modal, popover, sheet). * - `ambient` — a non-focus-taking surface that reports state (toast rail, status line, presence layer). */ type SurfaceKind = 'shell' | 'tool' | 'pane' | 'region' | 'slot' | 'gutter' | 'action_bar' | 'overlay' | 'ambient'; /** How reliably a surface is present. * * - `always` — rendered in every state of its parent. * - `conditional` — rendered when `visibility_condition` holds. * - `on_demand` — rendered only after the user invokes it. * - `transient` — appears and self-dismisses without user action. */ type SurfacePersistence = 'always' | 'conditional' | 'on_demand' | 'transient'; /** Who may add occupants to a surface. * * - `closed` — the occupant set is fixed by the product team. * - `plugin_registerable` — extensions may register occupants through a published contract. * - `user_configurable` — the end user chooses what occupies it. * * @deprecated since 0.36.0, removeIn 1.0.0. Use `extension_mechanism` / * `extension_audience` / `extension_scope` / `extension_point`. Found * modelling 154 real surfaces of a public content-editing Studio: 71% came * out `closed`, 27 of those falsely (they were overridable via documented * API) — this single enum cannot separate "has no registration list of its * own" from "cannot be customized", so the modeller was forced to assert the * stronger, wrong one. See `SurfaceExtensionMechanism`. */ type SurfaceExtensibility = 'closed' | 'plugin_registerable' | 'user_configurable'; /** * HOW a surface is customized — the mechanism half of what `extensibility` * used to conflate with audience and scope. * * - `none` — not customizable by this mechanism (the surface may still be * customizable by a different mechanism; declare each that applies). * - `component_wrap` — the override receives a `renderDefault` callback and * wraps the default rather than replacing it. * - `component_replace` — the override fully replaces the default. * - `list_resolve` — config supplies an array, or a resolver that receives * the previous list and may filter/reorder/append. * - `register` — registration into a named collection (tools, asset sources). * - `config_flag` — a boolean config flag turns the surface on or off. * - `render_callback` — a bare render-prop/callback with no default to wrap. */ type SurfaceExtensionMechanism = 'none' | 'component_wrap' | 'component_replace' | 'list_resolve' | 'register' | 'config_flag' | 'render_callback'; /** How many instances of the surface exist, in UML multiplicity notation. * * `capacity` counts OCCUPANTS INSIDE one instance; `cardinality` counts the * INSTANCES themselves. A slot with `capacity: 1` still says nothing about * whether the product renders one such slot or forty. * * - `'1'` — exactly one instance, always present. * - `'0..1'` — at most one; may not be rendered at all. * - `'1..n'` — at least one, and more may be rendered. * - `'0..n'` — any number, including none. * * Spelled in UML multiplicity rather than snake_case words because the notation * is the lingua franca for the question and reads unambiguously in a diff. The * registry already carries non-identifier enum values where the domain has its * own notation (`headcount_band: '1-10'`, `openapi_version: 'v3.1'`). */ type SurfaceCardinality = '1' | '0..1' | '1..n' | '0..n'; /** What an instance of the surface is scoped to. * * Read with `cardinality`: together they say whether "the product has an * inspector panel" is a true sentence, or whether the truth is "every document * pane has its own inspector panel, and three can show different content at * once". Without this, a graph asserts the former when it means the latter. * * - `global` — one instance serves the whole product; its state is shared. * - `per_parent` — each containing surface owns its own instance with its own * state. "Parent" is the `surface_contains_surface` parent (or the * `screen_renders_surface` host when the surface sits at the top of a screen). */ type SurfaceInstanceScope = 'global' | 'per_parent'; /** How the occupants of a surface relate to one another. * * The three modes need genuinely different governance, and conflating them is * what makes a contention check misfire: * * - `exclusive` — one occupant wins and the others are not rendered. This is * contention in the strict sense, and it is the mode that needs an * `arbitration_rule`. * - `additive` — all occupants coexist and are rendered together. Nobody is * suppressed, so the open question is ORDER rather than victory; record the * ordering in `arbitration_rule`. * - `chained` — each occupant WRAPS the next, typically by invoking a * render-default callback it is handed. Not contention but a trust model: an * occupant that never calls the callback silently deletes everything * downstream of it. Many occupants and no arbitration rule is the DESIGNED * shape here, not a defect, so `chained` is exempt from * `contended-surface-without-arbitration`. * * EXEMPTION IS DECLARE-TO-EARN. A surface that leaves `composition_mode` unset * keeps firing the contention check: the default posture stays suspicious, and * silence has to be claimed deliberately. Declaring `chained` is a factual * claim about how the code composes, and claiming it falsely to quiet the check * is the same category of error as filling `arbitration_rule` with a * placeholder. */ type SurfaceCompositionMode = 'exclusive' | 'additive' | 'chained'; /** Whether the arbitration answer is enforced, written down, both, or neither. * * `arbitration_rule` is free text and its absence is meaningful, but absence * alone conflates three very different situations with three very different * remediations. This property separates them: * * - `enforced_documented` — the code enforces a rule and `arbitration_rule` * transcribes it. The healthy state. * - `enforced_undocumented` — the code enforces a rule that was never written * down. Remediation is ten minutes of transcription, not a design decision. * - `safe_by_coincidence` — nothing enforces anything, and the surface behaves * only because the occupants happen not to collide (disjoint enum values, * mutually exclusive conditions). The most dangerous state, because it looks * settled from the outside and breaks the first time an occupant is added. * - `none` — nobody ever decided. Remediation is a design meeting. * - `no_contention_by_design` — there is no arbitration question to answer, * because the occupants never compete. The exact opposite of * `safe_by_coincidence`: designed absence of the question rather than * accidental absence of a collision. * * THIS IS NOT AN ESCAPE HATCH. Declaring `enforced_documented` does not by * itself silence `contended-surface-without-arbitration`: the detector still * requires a non-empty `arbitration_rule`, so claiming the rule is documented * without documenting it changes nothing. `safe_by_coincidence` and `none` * make the check fire whatever else is recorded, because both are admissions. * `no_contention_by_design` silences nothing that is not already silent: the * contention branch is a separate check that exempts only on * `composition_mode: 'chained'`, so a surface that claims this state while * genuinely over capacity, ruleless and not chained is still caught there. * * WHY ABSENCE COULD NOT CARRY THIS. An unset `arbitration_state` already means * "unassessed", and a surface whose occupants wrap rather than compete was * previously forced to use that same emptiness to say something quite * different. Certainty and ignorance became indistinguishable in one field. * `no_contention_by_design` makes a positive design claim, which is what makes * it declare-to-earn rather than an "I don't know". A generic `not_applicable` * was considered and rejected for exactly this reason: it reads as * "unassessed", collapses back into absence, and buys nothing. * * LEGALITY IS CONVENTION, NOT ENFORCEMENT. This value belongs on surfaces * whose `composition_mode` is `chained`, and nothing checks that pairing. UPG * has no cross-property conditional-enum mechanism, and a check must never * fire on legal modelling. Enforce only if field drift appears. */ type SurfaceArbitrationState = 'enforced_documented' | 'enforced_undocumented' | 'safe_by_coincidence' | 'none' | 'no_contention_by_design'; /** A place in the UI, its occupants, and the rule that arbitrates between them. * * `screen` answers *which route is this?*; `feature_area` answers *who owns * this?*; `bounded_context` answers *what architecture is this?*. None of them * answer *what else occupies the same place, and who wins when two things want * it?* A `surface` is that place: the shell, tool, pane, slot, gutter, action * bar, or overlay that features compete to occupy. Contention over UI places is * decided constantly and, when the decision lives only in prose, rediscovered * forever. Recording the place as an entity makes the guest list queryable and * the arbitration rule durable. * * Per UPG principle P14, structural relationships are edges: * nesting: `surface_contains_surface` (the spine; legality is constrained by * `surface_kind` via `UPG_VALID_CHILDREN`) * the guest list: `feature_occupies_surface` (inbound; the most important edge) * the host screen: `screen_renders_surface` (inbound) * purpose: `surface_serves_job` * the governing rule: `surface_governed_by_design_guideline` * what it draws with: `surface_renders_design_component` * how it is judged: `surface_measured_by_metric` * replacement: `surface_supersedes_surface` * intent-versus-reality: `surface_deviates_via_technical_debt_item` * * @example * const properties: SurfaceProperties = { * surface_kind: 'pane', * persistence: 'conditional', * visibility_condition: 'A node is selected and the inspector is not collapsed.', * capacity: 1, * cardinality: '0..n', * instance_scope: 'per_parent', * composition_mode: 'exclusive', * arbitration_rule: 'The most recently selected node wins. A pinned inspector outranks selection until unpinned.', * arbitration_state: 'enforced_documented', * extensibility: 'plugin_registerable', * mutates_content: true, * dimensional_constraint: '292px wide, fixed', * } */ interface SurfaceProperties { /** Structural kind. Determines what may legally nest inside this surface. */ surface_kind?: SurfaceKind; /** How reliably the surface is present. */ persistence?: SurfacePersistence; /** * When the surface appears, in plain language. Pairs with * `persistence: 'conditional'`, which states *that* it is conditional; this * states *what* the condition is. * @example "A node is selected", "Only for workspace admins" */ visibility_condition?: string; /** * How many occupants ONE INSTANCE of the surface holds at once, as a * non-negative count. Absent means unbounded, `0` means a reserved place * nothing may occupy. Always the cap the design INTENDS, never what was * observed rendering. Count the instances themselves with `cardinality`. * * @remarks * Intent, not observation: a banner region declared `capacity: 1` keeps * saying 1 even after someone finds it rendering four. Reality that has * drifted from the declared intent belongs on * `surface_deviates_via_technical_debt_item` as a trackable, assignable debt * item. Editing this number up to match the bug would destroy the only record * that a gap exists. * * Why absence rather than a sentinel: UPG has no union-typed property * primitive, so `integer | unbounded` is expressed as an optional number * whose absence carries the "no cap" reading, instead of a magic value every * consumer would have to special-case. * * Three states, all different. ABSENT is unbounded, no cap stated. `0` is a * reserved place. `null` is neither, and nothing this field defines: it is * what an explicit null write leaves behind, and no consumer reads it as a * cap. To return the property to absent, remove the key with `update_node`'s * or `batch_update_nodes`' `unset_properties`, since a property merge * preserves anything omitted. * * Unbounded is not an exemption from scrutiny. The contention detector reads * an absent capacity as a threshold of 1, because a surface that states no * limit has stated no answer either, so several occupants is exactly the * unrecorded decision worth naming. Declaring a real capacity is the way to * quiet the check honestly, and the only way that also records something true. */ capacity?: number; /** * How many instances of this surface exist. `capacity` counts occupants * within one instance; this counts the instances. */ cardinality?: SurfaceCardinality; /** * What an instance is scoped to: one shared instance for the product, or one * per containing surface. Decides whether "the product has this surface" is * a true sentence or a per-parent one. */ instance_scope?: SurfaceInstanceScope; /** * How the occupants relate: one wins (`exclusive`), all coexist * (`additive`), or each wraps the next (`chained`). Declaring `chained` * exempts the surface from `contended-surface-without-arbitration`; leaving * this unset does not. */ composition_mode?: SurfaceCompositionMode; /** * Who wins when more occupants want the surface than `capacity` allows, and * why. Absence is meaningful: on a contested surface it means nobody decided. * * @example "Highest priority wins; ties break to the most recently updated." * * @remarks * An empty or absent rule on a contested surface is exactly what * `contended-surface-without-arbitration` detects. Do not fill it in with a * placeholder to silence the check: the check exists to find the unrecorded * decision, and a placeholder hides it without settling anything. * * The field is overloaded across composition modes, which is worth knowing * before writing one. On an `exclusive` surface it records DISPLACEMENT (who * is not rendered). On an `additive` surface everyone fits, so what it * records is ORDER. Only the displacement reading is what the contention * detector reads. */ arbitration_rule?: string; /** * Whether the arbitration answer is enforced, written down, both, neither, or * not owed at all. Leaving the field unset means unassessed, and always did. * * @remarks * The separations are the whole point of the field, because absence alone * conflates four situations with four different remediations: "enforced in * code but never transcribed" is ten minutes of typing, "never decided" is a * design meeting, "safe only because nothing has collided yet" is the * dangerous one, and "nothing to decide, the occupants never compete" is a * chained surface, which owes no arbitration rule at all. */ arbitration_state?: SurfaceArbitrationState; /** * Who may add occupants to the surface. @deprecated since 0.36.0, * removeIn 1.0.0. Use `extension_mechanism` / `extension_audience` / * `extension_scope` / `extension_point`: this single enum collapses four * independent facts (mechanism, audience, scope, and the named API entry * point) into one value, which forces false `closed` readings on surfaces * that are overridable through a mechanism the enum has no room to name. * STAGED, not renamed: the field is kept and still read, no stored bytes * change. There is no `PROPERTY_SCALE_MAP_BY_ENTITY` entry for this one: * unlike `risk.probability` → `likelihood_5`, this is a structural split * with no scale to remap onto, so no machine migration ships; re-modelling * a surface's real mechanism/audience/scope is a judgement call, same as * promoting `objective.timeframe` to a `planning_cycle` node. */ extensibility?: SurfaceExtensibility; /** * The mechanism by which this surface is customized: e.g. `component_wrap` * for a `renderDefault`-style override, `list_resolve` for a filter/append * resolver, `register` for named-collection registration. Independent of * `extension_audience` and `extension_scope`. `none` means not * customizable by this mechanism, not unassessed. * * @remarks * A surface can be `component_wrap`-able by a schema author globally and * `register`-able by a plugin author per type at the same time, which is * exactly the shape `extensibility` could not hold: it named only a * mechanism-or-audience, never both. */ extension_mechanism?: SurfaceExtensionMechanism; /** * WHO may customize the surface through the declared `extension_mechanism`: * the studio config author, the schema author, a plugin author, or the end * user. Multi-valued because the same surface is often overridable by more * than one audience through different entry points (e.g. globally by a * config author, per-type by a schema author). */ extension_audience?: Array<'config_author' | 'schema_author' | 'plugin_author' | 'end_user'>; /** * AT WHAT SCOPE the customization applies: the whole surface, one type, one * field, or one instance. Multi-valued: a form field surface can be * overridable globally and per type at once. * * @remarks * This is the fact `extensibility: 'closed'` got wrong most often: "has no * registration list of its own" (no per-instance entry, e.g. * `form.components.field` has none) does not mean "cannot be customized" * (it is still overridable globally and per type via a schema-level * `components` declaration). */ extension_scope?: Array<'global' | 'per_type' | 'per_field' | 'per_instance'>; /** * The named API entry point through which the customization happens. * @example "form.components.field", "document.actions", "studio.tools" * * @remarks * A durable, checkable link from the model to the code, in the same spirit * as recording a DOM selector: it makes the customization fact verifiable * against the real API rather than asserted from memory. */ extension_point?: string; /** * Whether occupying this surface can change the underlying content, as * opposed to only selecting or revealing it. The selector-versus-mutator * distinction: a gutter that toggles a value is a mutator, a gutter that * jumps the cursor is not. */ mutates_content?: boolean; /** * The hard spatial budget the surface imposes on its occupants, in whatever * unit the design system speaks. * @example "292px wide", "25px per field", "two grid columns" */ dimensional_constraint?: string; } /** Design annotation on a screen. * * @example * const properties: AnnotationProperties = { * annotation_type: 'spec', * target_element: '[data-testid="onboarding-cta"]', * note: 'Rechecked on 2026-04-10 after the onboarding rewrite.', * } */ interface AnnotationProperties { /** Annotation type */ annotation_type?: 'spec' | 'interaction' | 'content' | 'accessibility'; /** Annotated element */ target_element?: string; /** Note text */ note?: string; } /** Interaction specification. * * @example * const properties: InteractionSpecProperties = { * trigger: 'User opens the third restricted feature in a session.', * animation_type: 'ease-in-out 240ms', * duration_ms: 42, * } */ interface InteractionSpecProperties { /** Triggering event */ trigger?: string; /** Animation or transition kind */ animation_type?: string; /** Duration in ms */ duration_ms?: number; /** Easing (e.g. "ease-in-out", "spring") */ easing?: string; } /** * UPG Property Schemas: Design System Domain. * DesignComponent, DesignToken, DesignSystem, DesignPattern, DesignGuideline. * https://unifiedproductgraph.org/spec | MIT */ /** Design system component. * * @example * const properties: DesignComponentProperties = { * atomic_level: 'atom', * code_url: 'https://github.com/arkheiev/entopo/tree/main/packages/ui/src/Button', * documentation_url: 'https://entopo.app/docs/components/button', * } */ interface DesignComponentProperties { /** Atomic design level */ atomic_level?: 'atom' | 'molecule' | 'organism' | 'template'; /** Code implementation URL */ code_url?: string; /** Documentation / usage page URL */ documentation_url?: string; /** Component version (independent of the wider design system version) */ component_version?: string; /** Distribution readiness */ component_status?: 'draft' | 'beta' | 'stable' | 'deprecated'; /** Component-specific accessibility guidance */ accessibility_notes?: string; } /** Design token. * * @example * const properties: DesignTokenProperties = { * value: '65%', * category: 'color', * css_variable: '--color-primary-500', * } */ interface DesignTokenProperties { /** * What kind of value this token holds. Aligned to the Design Tokens * Community Group `$type` vocabulary so a DTCG export maps across without * translation; the five original UPG values are kept, `color` and `spacing` * being the ones DTCG spells the same and `typography`, `radius`, `motion` * being groupings a design system names in its own terms. * * @remarks * DTCG mapping for the values that are not one-to-one: a `radius` token is a * DTCG `dimension`, a `motion` token is usually a `duration` or a * `cubicBezier`, and a `typography` token is a composite over `fontFamily` / * `fontWeight` / `dimension`. Both spellings are accepted: an estate that * speaks DTCG writes the `$type` name, one that speaks UPG's original five * keeps writing them, and neither has to translate at the boundary. * * Extended at 0.39.0 from a measured estate (163 DTCG primitives) whose * `shadow`, `dimension` and `opacity` tokens had no category to land on. The * enum was documented but unenforced then; `validate_graph`'s * `property_enum_drift` class (same release) is what makes it real, which is * why the vocabulary had to be right before the check arrived. */ category?: 'color' | 'spacing' | 'typography' | 'radius' | 'motion' | 'dimension' | 'fontFamily' | 'fontWeight' | 'duration' | 'cubicBezier' | 'number' | 'shadow' | 'gradient' | 'border' | 'transition' | 'strokeStyle' | 'opacity'; /** Resolved value */ value: string; /** CSS custom property name */ css_variable?: string; } /** Design system as a whole. * * @example * const properties: DesignSystemProperties = { * version: '0.3.1', * repo_path: 'packages/ui/src/Button', * maintainer: 'platform-team', * } */ interface DesignSystemProperties { /** Current version */ version?: string; /** Code repository or package path */ repo_path?: string; /** Maintaining person or team. Promote to a `node_owned_by_team` edge if ownership must be queryable. */ maintainer?: string; /** Open-source license, if public */ license?: string; /** Documentation homepage. Public-facing entry point. */ homepage_url?: string; } /** Reusable design pattern. * * @example * const properties: DesignPatternProperties = { * pattern_category: 'navigation', * usage_context: 'Primary call-to-action in signup flow', * pattern_status: 'proposed', * } */ interface DesignPatternProperties { /** Category */ pattern_category?: 'navigation' | 'input' | 'display' | 'feedback' | 'layout'; /** When and where to use */ usage_context?: string; /** Adoption status inside the system */ pattern_status?: 'proposed' | 'adopted' | 'experimental' | 'deprecated'; /** Common mis-applications or lookalikes to avoid */ anti_patterns?: string; /** Real-world usages: screens, components, flows */ examples?: string; } /** Design guideline. * * @example * const properties: DesignGuidelineProperties = { * guideline_category: 'spacing', * applies_to: 'All customer-facing surfaces', * rationale: 'Reduces support burden and lifts activation, both priorities this quarter.', * } */ interface DesignGuidelineProperties { /** Category covered */ guideline_category?: 'spacing' | 'color' | 'typography' | 'layout' | 'interaction' | 'content'; /** Applicable elements or contexts */ applies_to?: string; /** Reasoning. Why this rule exists. */ rationale?: string; /** Imperative force. (Superseded the removed `strictness` field in 0.14.0, UPG-574.) */ rule_strength?: RuleStrength; /** Exception request or documentation process */ exception_policy?: string; } /** * UPG Property Schemas: Brand Identity Domain. * BrandIdentity, BrandColour, BrandTypography, BrandVoice, BrandLogo, BrandImagery. * https://unifiedproductgraph.org/spec | MIT */ /** Brand identity. Root entity for a brand's visual and verbal identity. * * @example * const properties: BrandIdentityProperties = { * personality_traits: ['pragmatic', 'curious', 'rigorous'], * tagline: 'Think clearer. Ship surer.', * mission_statement: 'Give every product team the tools to think clearly and ship decisions that compound.', * } */ interface BrandIdentityProperties { /** Core personality traits of the brand (e.g. "bold", "approachable", "innovative") */ personality_traits?: string[]; /** Brand tagline or slogan (e.g. "Just Do It", "Think Different") */ tagline?: string; /** Mission statement: what the brand exists to do. */ mission_statement?: string; /** * Core brand values. The principles the brand stands for. * @example ["innovation", "transparency", "sustainability"] */ brand_values?: string[]; /** Brand origin story or narrative. Free-form text, may be multiple paragraphs. */ brand_story?: string; /** * Target audience description. Who the brand speaks to. * @example "Solo founders and small product teams building their first product." */ target_audience_description?: string; /** * Brand personality archetype (e.g. "The Creator", "The Explorer", "The Sage"). * Based on the 12 Jungian brand archetypes model used in brand strategy. */ brand_personality_archetype?: string; } /** Brand colour. A single colour in the brand palette. * * @example * const properties: BrandColourProperties = { * hex: '#0A84FF', * role: 'primary', * contrast_pair: 'background-primary vs text-primary, AA-compliant', * } */ interface BrandColourProperties { /** Hex colour value including hash (e.g. "#1A2B3C") */ hex: string; /** Role this colour plays in the brand palette */ role?: 'primary' | 'secondary' | 'accent' | 'neutral' | 'semantic'; /** * Hex of the contrast-paired colour for accessibility. * Used to verify WCAG AA/AAA contrast ratios. */ contrast_pair?: string; /** Where this colour should be used (e.g. "hero backgrounds", "CTA buttons", "body text") */ usage_context?: string; /** * Human-readable colour name within the brand palette. * @example "Midnight Blue", "Sunrise Orange" */ color_name?: string; /** * RGB value as a comma-separated string. * @example "26, 43, 60" */ rgb?: string; /** * CMYK value for print contexts, as a comma-separated string. * @example "57, 28, 0, 76" */ cmyk?: string; /** * Pantone code for physical brand materials (print, packaging, signage). * @example "Pantone 289 C" */ pantone?: string; } /** Brand typography. A font family and its usage rules. * * @example * const properties: BrandTypographyProperties = { * font_family: 'Inter, -apple-system, sans-serif', * category: 'heading', * weight_range: '400–700', * } */ interface BrandTypographyProperties { /** Font family name (e.g. "Inter", "Playfair Display", "JetBrains Mono") */ font_family: string; /** Typographic role within the brand's type system */ category?: 'heading' | 'body' | 'mono' | 'display' | 'accent'; /** Available weight range as a string (e.g. "400-700", "100-900") */ weight_range?: string; /** Example text to preview the font (e.g. "The quick brown fox") */ sample_text?: string; /** * Where the font is sourced from. * @example "Google Fonts", "Adobe Fonts", "self-hosted", "custom" */ font_source?: string; /** * Recommended line height as a unitless ratio or CSS value. * @example "1.5", "1.75", "24px" */ line_height?: string; /** * Recommended letter spacing / tracking as a CSS value. * @example "0.02em", "-0.01em", "normal" */ letter_spacing?: string; /** * Fallback font stack for web rendering (CSS font-family fallbacks). * @example "system-ui, -apple-system, sans-serif" */ fallback_font?: string; } /** Brand voice guidelines. How the brand sounds in written and spoken communication. * * @example * const properties: BrandVoiceProperties = { * tone_attributes: ['warm', 'direct', 'expert-but-approachable'], * do_examples: ['Use sentence case', 'Prefer active voice'], * dont_examples: ['Avoid jargon', 'Never use ALL CAPS for emphasis'], * } */ interface BrandVoiceProperties { /** * Tone descriptors defining how the brand sounds. * @example ["confident", "warm", "precise", "never condescending"] */ tone_attributes?: string[]; /** * Examples of correct brand voice. "This is how we write." * @example ["We make complex things simple.", "Let's figure this out together."] */ do_examples?: string[]; /** * Examples to avoid. "We never write like this." * @example ["Click here to leverage our synergies.", "Dear valued customer..."] */ dont_examples?: string[]; /** * Core writing principles that govern all brand communication. * @example ["Lead with clarity", "Be specific, not vague", "Write for humans first"] */ writing_principles?: string[]; /** * Preferred and avoided vocabulary. * @example "Say 'product creator', not 'entrepreneur'" */ vocabulary_preferences?: string; /** * How voice adapts by audience segment. * @example "Enterprise: more formal, data-led. Solo founders: conversational, encouraging." */ audience_adaptation?: string; /** * Voice variation by communication channel. * @example "Social: punchy and casual. Docs: precise and thorough. Email: warm and direct." */ channel_guidelines?: string; } /** Brand logo variant. A specific version of the brand's logo. * * @example * const properties: BrandLogoProperties = { * variant: 'primary', * min_size_px: 42, * clear_space_ratio: 42, * } */ interface BrandLogoProperties { /** * Which variant of the logo this represents. * A brand typically has multiple logo variants for different contexts. */ variant?: 'primary' | 'secondary' | 'icon' | 'wordmark' | 'monochrome' | 'reversed' | 'other'; /** * Minimum display size in pixels to maintain legibility. * @example 32 * @minimum 1 */ min_size_px?: number; /** * Minimum padding around the logo as a fraction of its height * (0.25 = 25% on each side). * @example 0.25 */ clear_space_ratio?: number; /** * Background colours the logo is approved for use on. * @example ["#FFFFFF", "#1A1A1A", "#F5F5F5"] */ approved_backgrounds?: string[]; /** * Background colours or contexts where the logo must NOT be placed. * @example ["busy photography", "low-contrast surfaces"] */ forbidden_backgrounds?: string[]; /** * File formats this logo variant is available in. * @example ["svg", "png", "eps", "pdf"] */ file_formats?: string[]; /** * URL to the logo asset file or asset library entry. */ asset_url?: string; } /** Brand imagery guidelines. Photography, illustration, and visual mood. * * @example * const properties: BrandImageryProperties = { * style: 'photography', * mood_keywords: ['calm', 'confident', 'clear'], * approved_filters: ['status', 'owner', 'priority'], * } */ interface BrandImageryProperties { /** * Primary visual style category. * Determines whether the brand uses photography, illustrations, or a mix. */ style?: 'photography' | 'illustration' | 'mixed' | 'abstract' | 'other'; /** * Keywords describing the visual mood and feeling of brand imagery. * @example ["warm", "authentic", "natural light", "diverse", "in-context"] */ mood_keywords?: string[]; /** * Approved image treatments and filters. * @example ["warm colour grade", "subtle desaturation", "no heavy vignettes"] */ approved_filters?: string[]; /** * Guidelines for stock photography selection or commissioned shoots. * @example "Use candid, in-context shots. Avoid staged corporate handshakes." */ stock_guidelines?: string; /** * Rules for visual composition in brand imagery. * @example "Subject off-centre. Generous negative space on left for text overlay." */ composition_rules?: string; /** * Illustration style guidelines (if applicable). * @example "Flat vector with rounded corners. Limited to brand palette. No gradients." */ illustration_style?: string; } /** * UPG Property Schemas: Product Specification Domain. * Feature, FeatureArea, Epic, UserStory, AcceptanceCriterion, Release, * Task, Bug, Roadmap, RoadmapItem, Theme, Changelog. * Also: OutcomeProperties, ObjectiveProperties, KeyResultProperties. * https://unifiedproductgraph.org/spec | MIT */ /** A measurable change in user or business state the product drives. * The "O" in OKR when paired with objectives. Answers *what changed for the user*, * not *what we built*. * * @example * const properties: OutcomeProperties = { * timeline: 'Kickoff 2026-04-22, results by 2026-05-15.', * owner: 'sam.patel@arkheiev.com', * success_criteria: 'Week-one activation rises from 38% to ≥45%.', * } */ interface OutcomeProperties { /** Target timeframe (e.g. "Q2 2026", "12 months") */ timeline?: string; /** Accountable person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; /** * What "achieved" looks like, concretely. Pairs with `measurement_method`. * @example "30-day retention above 40% for new signups" */ success_criteria?: string; /** * Assessment approach. * `quantitative` = metrics drive the call. * `qualitative` = observation / interviews. * `mixed` = both, weighted case-by-case. */ measurement_method?: 'quantitative' | 'qualitative' | 'mixed'; /** Baseline or latest read */ current_state?: string; /** Evidence gathered so far (quotes, metrics, studies) */ evidence_summary?: string; /** Confidence this is the right outcome to pursue */ confidence?: UPGAssessment; } /** A high-level strategic goal. The O in OKR. * * @example * const properties: ObjectiveProperties = { * timeframe: '12-18 months', * progress: 42, * } */ interface ObjectiveProperties { /** * Planning timeframe (e.g. "Q1 2026", "H1 2026"). * @deprecated since 0.33.0, removeIn 1.0.0. Promote the period to a `planning_cycle` node and link it with the `objective_scoped_to_planning_cycle` edge, which has existed since 0.20.0 and points at a shared, dated, nestable interval instead of a drifting per-objective string. Until promoted, the value is a display label and nothing schedules on it. The promotion is documented rather than automated and no `drop_props` migration ships: which cycle a free-text string means, and whether one exists yet, is a judgement, and dropping the string before the cycle exists destroys the only record of the intent. * @example "Q1 2026", "H1 2026" */ timeframe?: string; /** Overall progress (0–100) */ progress?: number; } /** A measurable result under an objective. The KR in OKR. * * @example * const properties: KeyResultProperties = { * current_value: 42, * target_value: 42, * unit: 'days', * } */ interface KeyResultProperties { /** Most recent observed value */ current_value?: number; /** Value for full achievement */ target_value?: number; /** Display unit (e.g. "%", "users", "£") */ unit?: string; } /** A structural grouping of related features within a product. * * A semantic container (e.g. "Canvas & Visualisation", "AI & Intelligence", * "Onboarding") that gathers features under a shared product surface, owning * team, or capability. Lifecycle (planned → active → deprecated) is governed * by the base-node `status` field (the former `area_status` property was * collapsed into it in 0.15.0, UPG-689 Pattern D). * * Per UPG principle P14, structural relationships are edges: * parent product: `product_organised_by_feature_area` * contained features: `feature_area_contains_feature` * owning team: `team_owns_feature_area` (the `owning_team` field below is * at-a-glance display only; the edge is canonical when a `team` entity exists) * related capability: `feature_area_realises_capability` * * @example * const properties: FeatureAreaProperties = { * scope_summary: 'Everything users see and touch on the canvas: graph rendering, layout, zoom, multi-select.', * owning_team: 'platform-canvas', * feature_count: 14, * priority: 'high', * maturity: 'mature', * owner: 'sam.patel@arkheiev.com', * } */ interface FeatureAreaProperties { /** One-line scope description. Disambiguates from sibling areas at a glance. */ scope_summary?: string; /** Team identifier or slug. Free-form display. Canonical relationship is the `team_owns_feature_area` edge. */ owning_team?: string; /** Approximate feature count under this area. Snapshot; `feature_area_contains_feature` edges are the source of truth. */ feature_count?: number; /** Area owner (handle or email). Promote to a `node_owned_by_team` edge if ownership must be queryable. */ owner?: string; /** Importance to the product overall */ priority?: Priority; /** * Maturity. * `nascent` = newly-formed grouping. `mature` = established surface. * `legacy` = being phased out for a successor. */ maturity?: 'nascent' | 'growing' | 'mature' | 'legacy'; } /** A discrete, user-facing capability of the product. * * @example * const properties: FeatureProperties = { * priority: 'high', * owner: 'sam.patel@arkheiev.com', * start_date: '2026-04-01', * } */ interface FeatureProperties { /** Task-level priority */ priority?: Priority; /** Responsible person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; /** ISO date work begins */ start_date?: ISODate; /** ISO date work completes */ target_date?: ISODate; /** Delivery health */ health?: HealthStatus; /** The source tool's raw custom workflow state, verbatim and opaque (e.g. "In Review", "QA", "Needs Triage"). Non-canonical and never reasoned over: it exists to round-trip an import losslessly. Map it onto a canonical bucket with `workflow_state_category`; canonical `status` stays the sole reasoning axis. */ workflow_state?: string; /** * Canonical bucket the raw `workflow_state` maps onto for reasoning: a source * "In Review" and a source "QA" may both map to a verification phase. * Optional companion to `workflow_state`; canonical `status` remains the sole * reasoning axis. * * @remarks * It exists so a graph can reason over an imported custom workflow WITHOUT * promoting the source's raw label to `status`. The raw label keeps its own * field and stays verbatim; this one says what that label means in the * six-bucket vocabulary every major tracker converges on. * * NARROWED FROM `string` AT 0.32.0. The field exists to carry exactly the * {@link StatusCategory} vocabulary, and typing it as an open string meant * nothing enforced the one thing it was for; an importer could write any word * here and no consumer would know it had. A graph carrying a free string now * fails to type-check rather than failing to be understood. */ workflow_state_category?: StatusCategory; } /** A collection of related user stories that delivers a feature or capability. * * @example * const properties: EpicProperties = { * effort: '3 person-weeks', * priority: 'high', * owner: 'sam.patel@arkheiev.com', * } */ interface EpicProperties { /** Effort estimate (e.g. "2h", "1d", "3 points"). Use a consistent unit within your team. Canonical name for the work-item size family (`epic`, `user_story`, `task`). */ effort?: string; /** * Rough size estimate (e.g. "3 sprints", "L", "13 points"). * @deprecated STAGED, release number assigned at release prep (docket Track "Docket wave 2"). * Use `effort`, the family-uniform name already carried by * `user_story` and `task`. `estimate` was epic's lone divergent spelling of the same * concept and it caused real misreads (the app read `estimate` on `user_story`, where * only `effort` is declared). Kept (not removed) for back-compat; removal is a later * major. Writers: emit `effort`. Readers: prefer `effort`, fall back to `estimate`. */ estimate?: string; /** Task-level priority */ priority?: Priority; /** Responsible person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; /** ISO date work begins */ start_date?: ISODate; /** ISO date work completes */ target_date?: ISODate; /** The source tool's raw custom workflow state, verbatim and opaque (e.g. "In Review", "QA", "Needs Triage"). Non-canonical and never reasoned over: it exists to round-trip an import losslessly. Map it onto a canonical bucket with `workflow_state_category`; canonical `status` stays the sole reasoning axis. */ workflow_state?: string; /** * Canonical bucket the raw `workflow_state` maps onto for reasoning: a source * "In Review" and a source "QA" may both map to a verification phase. * Optional companion to `workflow_state`; canonical `status` remains the sole * reasoning axis. * * @remarks * It exists so a graph can reason over an imported custom workflow WITHOUT * promoting the source's raw label to `status`. The raw label keeps its own * field and stays verbatim; this one says what that label means in the * six-bucket vocabulary every major tracker converges on. * * NARROWED FROM `string` AT 0.32.0. The field exists to carry exactly the * {@link StatusCategory} vocabulary, and typing it as an open string meant * nothing enforced the one thing it was for; an importer could write any word * here and no consumer would know it had. A graph carrying a free string now * fails to type-check rather than failing to be understood. */ workflow_state_category?: StatusCategory; } /** "As X, I want Y so Z" templated promise on a user story (UCS pattern P5). * * The stable design artefact: a product–engineering contract for what the * persona will be able to do. Lifecycle-free: a statement either still * describes a real promise, or is superseded by a new one when the promise changes. * * Pairs with `task` (UCS pattern P4, story_task collapsed into task) via `task_implements_user_story`. * One statement can have many tasks (re-implementation, per-platform variants); * each task carries its own lifecycle. * * @example * const statement: UserStoryProperties = { * as_a: 'product manager', * i_want_to: 'compare two metrics side-by-side', * so_that: 'I can prioritise this week with confidence', * text: 'As a product manager, I want to compare two metrics side-by-side so that I can prioritise this week with confidence.', * } */ interface UserStoryProperties { /** "As a [persona], …". Free-text persona name or slug ref. */ as_a?: string; /** Capability or action the persona wants. */ i_want_to?: string; /** Benefit or outcome the persona expects. */ so_that?: string; /** Free-form story text. Used as a single-line rendered view. */ text?: string; /** Assigned person. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ assignee?: string; /** Effort estimate (e.g. "2h", "1d", "3 points"). Use a consistent unit within your team. */ effort?: string; /** Relative importance against other stories. Lifted onto user_story (0.20.0) so the story is a first-class plannable unit alongside task, matching Jira/Linear where the story/issue is the estimated-and-assigned atom. */ priority?: Priority; /** ISO date due. Typically bounded by the release or planning_cycle the story is scheduled into. */ due_date?: ISODate; /** The source tool's raw custom workflow state, verbatim and opaque (e.g. "In Review", "QA", "Needs Triage"). Non-canonical and never reasoned over: it exists to round-trip an import losslessly. Map it onto a canonical bucket with `workflow_state_category`; canonical `status` stays the sole reasoning axis. */ workflow_state?: string; /** * Canonical bucket the raw `workflow_state` maps onto for reasoning: a source * "In Review" and a source "QA" may both map to a verification phase. * Optional companion to `workflow_state`; canonical `status` remains the sole * reasoning axis. * * @remarks * It exists so a graph can reason over an imported custom workflow WITHOUT * promoting the source's raw label to `status`. The raw label keeps its own * field and stays verbatim; this one says what that label means in the * six-bucket vocabulary every major tracker converges on. * * NARROWED FROM `string` AT 0.32.0. The field exists to carry exactly the * {@link StatusCategory} vocabulary, and typing it as an open string meant * nothing enforced the one thing it was for; an importer could write any word * here and no consumer would know it had. A graph carrying a free string now * fails to type-check rather than failing to be understood. */ workflow_state_category?: StatusCategory; } /** * @deprecated since v0.4.0. Use `TaskProperties`. `story_task` collapsed into * canonical `task`. `task_implements_user_story` edge expresses the story * relationship. The former `estimate` / `effort` / `priority` fields were * removed in v0.8.0 (UPG-574); their values live on the canonical `task` * (`TaskProperties.estimate` / `.effort` / `.priority`). * Migration: `UPG_MIGRATIONS['0.4.0']` renames story_task nodes to task; * `UPG_PROPERTY_MIGRATIONS['0.8.0']` drops the removed property residue. */ type StoryTaskProperties = Record; /** Acceptance criterion on a story or feature. * * @example * const properties: AcceptanceCriterionProperties = { * condition: 'personas.length > 0 && opportunities.length === 0', * test_type: 'manual', * pass_status: 'untested', * } */ interface AcceptanceCriterionProperties { /** Required condition (Given/When/Then or plain text) */ condition: string; /** Test mode */ test_type?: 'manual' | 'automated'; /** * Current verification state of this criterion. `untested` means never * attempted; `blocked` means attempted but not verifiable for an * environmental reason; `regressed` means previously passing, now failing. * * @remarks * `blocked` is a distinct state from `untested` (a missing credential or an * unreachable dependency is not the same as nobody having tried), which the * earlier three-value enum conflated into a silent gap. * * `regressed` is a criterion-level state and is NOT derivable here, because * `acceptance_criterion` stores current state only and keeps no result * history. The history lives on the `test_case` to `test_result` series, * where a single execution's outcome is `TestResultProperties.result_status` * and can never itself be "regressed". */ pass_status?: 'untested' | 'pass' | 'fail' | 'regressed' | 'blocked'; } /** A shipped version or milestone of the product. * * @example * const properties: ReleaseProperties = { * release_date: '2026-04-01', * version: '0.3.1', * start_date: '2026-04-01', * } */ interface ReleaseProperties { /** Scheduled or actual release date (ISO) */ release_date?: ISODate; /** Semver or named version (e.g. "v2.1.0", "Beta 3") */ version?: string; /** ISO date work begins */ start_date?: ISODate; /** Responsible person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; } /** Task: a discrete unit of work, smaller than a story. * * @example * const properties: TaskProperties = { * assignee: 'sam.patel@arkheiev.com', * effort: '3 person-weeks', * } */ interface TaskProperties { /** Assigned person. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ assignee?: string; /** Effort estimate (e.g. "2h", "1d", "3 points"). Use a consistent unit within your team. */ effort?: string; /** Relative importance against other tasks */ priority?: Priority; /** ISO date due. Typically bounded by the containing story's due date. */ due_date?: ISODate; /** * Free-form classification tags. * @deprecated since 0.32.0. Use base-node `tags` for ungrouped labels, and * `classification_axis` + `classification_value` + * `node_classified_as_classification_value` when the labels belong to a named * group. This field duplicated `tags` and had no consumers; three parallel * label surfaces (base `tags`, per-type `tags`, per-type `labels`) were two * too many. `UPG_PROPERTY_MIGRATIONS['0.32.0']` drops it. */ labels?: string[]; /** The source tool's raw custom workflow state, verbatim and opaque (e.g. "In Review", "QA", "Needs Triage"). Non-canonical and never reasoned over: it exists to round-trip an import losslessly. Map it onto a canonical bucket with `workflow_state_category`; canonical `status` stays the sole reasoning axis. */ workflow_state?: string; /** * Canonical bucket the raw `workflow_state` maps onto for reasoning: a source * "In Review" and a source "QA" may both map to a verification phase. * Optional companion to `workflow_state`; canonical `status` remains the sole * reasoning axis. * * @remarks * It exists so a graph can reason over an imported custom workflow WITHOUT * promoting the source's raw label to `status`. The raw label keeps its own * field and stays verbatim; this one says what that label means in the * six-bucket vocabulary every major tracker converges on. * * NARROWED FROM `string` AT 0.32.0. The field exists to carry exactly the * {@link StatusCategory} vocabulary, and typing it as an open string meant * nothing enforced the one thing it was for; an importer could write any word * here and no consumer would know it had. A graph carrying a free string now * fails to type-check rather than failing to be understood. */ workflow_state_category?: StatusCategory; } /** Bug report. * * @example * const properties: BugProperties = { * bug_severity: { value: 5, scale: 'severity_5', label: 'critical' }, * steps_to_reproduce: 'steps to reproduce', * } */ interface BugProperties { /** * Impact severity (UPGAssessment on the `severity_5` scale). Independent of * priority (which governs when it gets fixed). Migrated from the inline * `critical|major|minor|trivial` enum (UPG-579 Option C): map * `critical` -> 5, `major` -> 4, `minor` -> 2, `trivial` -> 1; carry the old * word in `label`. */ bug_severity?: UPGAssessment; /** Step-by-step reproduction */ steps_to_reproduce?: string; /** Observed environment (e.g. "prod", "staging", "iOS 17.4") */ environment?: string; /** Urgency relative to other work. Independent of `bug_severity` (a critical bug can have low priority if rare). */ priority?: Priority; /** Assigned person. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ assignee?: string; /** ISO date due. Often tied to a release gate or SLA. */ due_date?: ISODate; /** * Free-form classification tags. * @deprecated since 0.32.0. Use base-node `tags` for ungrouped labels, and * `classification_axis` + `classification_value` + * `node_classified_as_classification_value` when the labels belong to a named * group. This field duplicated `tags` and had no consumers; three parallel * label surfaces (base `tags`, per-type `tags`, per-type `labels`) were two * too many. `UPG_PROPERTY_MIGRATIONS['0.32.0']` drops it. */ labels?: string[]; /** The source tool's raw custom workflow state, verbatim and opaque (e.g. "In Review", "QA", "Needs Triage"). Non-canonical and never reasoned over: it exists to round-trip an import losslessly. Map it onto a canonical bucket with `workflow_state_category`; canonical `status` stays the sole reasoning axis. */ workflow_state?: string; /** * Canonical bucket the raw `workflow_state` maps onto for reasoning: a source * "In Review" and a source "QA" may both map to a verification phase. * Optional companion to `workflow_state`; canonical `status` remains the sole * reasoning axis. * * @remarks * It exists so a graph can reason over an imported custom workflow WITHOUT * promoting the source's raw label to `status`. The raw label keeps its own * field and stays verbatim; this one says what that label means in the * six-bucket vocabulary every major tracker converges on. * * NARROWED FROM `string` AT 0.32.0. The field exists to carry exactly the * {@link StatusCategory} vocabulary, and typing it as an open string meant * nothing enforced the one thing it was for; an importer could write any word * here and no consumer would know it had. A graph carrying a free string now * fails to type-check rather than failing to be understood. */ workflow_state_category?: StatusCategory; } /** Product roadmap. * * @example * const properties: RoadmapProperties = { * roadmap_type: 'now_next_later', * timeframe: '12-18 months', * owner: 'sam.patel@arkheiev.com', * } */ interface RoadmapProperties { /** Structure */ roadmap_type?: 'now_next_later' | 'quarterly' | 'release_based' | 'theme_based'; /** Covered timeframe */ timeframe?: string; /** Owning person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; } /** Roadmap item. * * @example * const properties: RoadmapItemProperties = { * quarter: '2026-Q2', * priority: 'high', * } */ interface RoadmapItemProperties { /** Planning quarter (e.g. "Q2 2026"). Pair with `start_date`/`target_date` for precise scheduling. */ quarter?: string; /** Importance against other items */ priority?: Priority; /** Delivery confidence within the planned period (UPGAssessment on `confidence_5`). */ confidence?: UPGAssessment; /** ISO date work begins. More precise than `quarter` for continuous planning. */ start_date?: ISODate; /** ISO date completion is expected. For shipped items, the actual completion date. */ target_date?: ISODate; } /** Thematic grouping of roadmap work, around the customer problem it solves. * * @example * const properties: RoadmapThemeProperties = { * theme_scope: 'Week-one activation across all customer-facing surfaces.', * priority: 'high', * } */ interface RoadmapThemeProperties { /** Scope description */ theme_scope?: string; /** Priority */ priority?: Priority; } /** Changelog entry. * * @example * const properties: ChangelogProperties = { * version: '0.3.1', * date: '2026-04-01', * change_type: 'feature', * } */ interface ChangelogProperties { /** Version (e.g. "1.2.0") */ version?: string; /** ISO date */ date?: string; /** Change type */ change_type?: 'feature' | 'improvement' | 'bugfix' | 'breaking' | 'deprecation'; } /** Planning cycle: a named, dated interval work flows through, which nests. * * The cadence axis of the delivery region. One self-nesting type spans every * delivery methodology (Scrum sprint, Kanban cadence, Shape Up cycle + cooldown, * SAFe program increment, quarterly OKR cycle) because they are all variants of * one primitive: a named, dated interval that work is assigned to and which can * contain finer intervals. `cadence_kind` discriminates the granularity instead * of minting a type per methodology; `planning_cycle_contains_planning_cycle` * handles the granularity ladder (a PI contains iterations; a cycle contains its * cooldown). Concretely dated (`starts_on` / `ends_on`), which is the deliberate * contrast with the freeform `strategic_theme.time_horizon` label a theme carries. * * @remarks * THE ACTIVE CYCLE IS THE ONE WHOSE STATUS IS `active`. There is no `is_active` * property and there will not be one: the lifecycle already holds that fact, and * a boolean beside it would be a second source of truth for one thing, which is * a pair that drifts rather than a pair that agrees. * * INVARIANT, documented and deliberately UNENFORCED: at most one `active` cycle * per parent per `cadence_kind`. No check ships for it. The near-miss is real * rather than hypothetical, which is the reason: a portfolio legitimately runs * two products' iterations concurrently, so the rule holds per parent and a * checker would need a graded corpus to tell a violation from that arrangement. * Under the labeled-fixture doctrine a check without such a corpus is one nobody * can defend, so the rule lives here, where an implementer meets it, instead of * in a validator that would be wrong about real graphs. * * ROLLING A CYCLE FORWARD: set cycle N to `closed`, and RE-POINT the unfinished * work's `planning_cycle_schedules_work_item` edges at N+1. A rolled item ends * with ONE edge, not two. The edge means IS SCHEDULED IN, present tense, so a * second edge would assert the item is in both cycles at once. * * The rejected alternative was keeping both edges plus a `rolled_from` edge * property. It builds the deferred scheduling-provenance layer through a side * door, and the corpus shows what it would actually record: six issues * auto-rolled across five cycles with zero completions, which is five edges per * issue documenting that nothing happened. * * Whether the HISTORY of past scheduling is retained is deliberately still open, * deferred alongside the same question for canvas keys rather than answered * twice in two places with two different answers. * * PLANNING ENTITIES RELATE TO TIME THROUGH AN EDGE, NOT A FREE-TEXT FIELD * (0.33.0, ruled). A free-text period on a planning entity is a display label * that nothing can schedule on, join across, or nest; the edge to a * `planning_cycle` is the queryable form and both edges the ruling needs already * exist (`objective_scoped_to_planning_cycle` and * `strategic_theme_scoped_to_planning_cycle`, both shipped 0.20.0). Accordingly * `objective.timeframe` and `strategic_theme.time_horizon` are `@deprecated` with * `removeIn` 1.0.0. * * THE CLASS HAS A BOUNDARY, AND IT IS A TEST RATHER THAN A SLOGAN. The class is * the entities that can be SCHEDULED IN a planning cycle: the ones that have, or * could have, a `*_scoped_to_planning_cycle` or `planning_cycle_schedules_*` * edge. That is `objective` and `strategic_theme`, and nothing else in the * census. NAMED EXEMPTIONS, because a broader reading would wrongly capture all * five: `strategic_pillar.time_horizon` (a durable pillar horizon is genuinely * open-ended, not a dated cycle, and its own JSDoc already says so), * `vision.timeframe` (a vision is not scheduled in a cycle), * `roadmap.timeframe` (a roadmap is a plan OF cycles, not a thing inside one), * `roadmap_item.quarter` (pairs with real `start_date` and `target_date` * ISODates), `journey_phase.timeframe` (not a planning entity at all), and * `market_trend.timeframe` (a forecast window for something the org does not * schedule, which is the clearest case of all: nothing can be scoped to a cycle * that the org does not control). * * @example * const properties: PlanningCycleProperties = { * cadence_kind: 'iteration', * cadence_label: 'sprint', * starts_on: '2026-07-06', * ends_on: '2026-07-17', * sequence: 47, * goal: 'Ship the AI autofill beta to the design-partner cohort.', * } */ interface PlanningCycleProperties { /** Methodology-neutral granularity of this interval. `period` is a coarse container (quarter / PI / OKR-cycle scale); `iteration` is a fine execution box (sprint / cycle); `buffer` is between-box slack (cooldown). Required: it is the discriminator that lets one type stand in for every methodology. */ cadence_kind: 'period' | 'iteration' | 'buffer'; /** The source methodology term verbatim ("sprint", "cycle", "PI", "quarter", "cooldown"). The dual-band label: `cadence_kind` is the canonical granularity reasoned over; `cadence_label` preserves what the team actually calls it. */ cadence_label?: string; /** ISO date the interval opens. A cycle is concretely dated, unlike a coarse `time_horizon` label. */ starts_on?: ISODate; /** ISO date the interval closes. */ ends_on?: ISODate; /** The cycle / iteration number (e.g. Sprint 47, PI 3). */ sequence?: number; /** The interval's goal or focus: what this cadence box is for. */ goal?: string; /** Shape Up appetite: the fixed time budget a cycle is willing to spend on a bet (e.g. "6 weeks", "2 weeks"). */ appetite?: string; } /** What kind of lever this axis is. Names the mechanism family so a reader * knows what changing the value would mean operationally. * * - `feature_flag` — a runtime toggle owned by the product team. * - `plan_tier` — what the customer bought. * - `permission_level` — what this user is allowed to see. * - `beta_program` — enrolment in an early-access cohort. * - `other` — a lever that is none of these and is described in the node's * description rather than forced into a category that fits badly. */ type ConfigurationAxisKind = 'feature_flag' | 'plan_tier' | 'permission_level' | 'beta_program' | 'other'; /** A named dimension along which the product's composition differs. * * The stored graph is the UNION of a configuration family; an axis names one * dimension of that family, and a projection picks one value on it. A graph * that declares no axis describes a product with one configuration, which is * what every graph written before this entity existed already meant. That is * the zero-migration guarantee: silence still means "true everywhere". * * ONE AXIS PER SEMANTIC LEVER, NOT PER CODE FLAG. The unit is the decision a * reader makes ("are we on the new navigation or the old one?"), not the * boolean a deploy system stores. Two flags that cannot be set independently * — one forcing the other on — are ONE axis with two values, and modelling * them as two axes would assert a four-configuration family, three members of * which do not exist. Link each driving flag with * `feature_flag_drives_configuration_axis`: the mechanism lives in * engineering, the lever lives here. * * VALUES ARE CLOSED. Every `present_under` and every `active_when.values` * entry must name a member of this list, and `validate_graph`'s * `configuration_drift` scope says so. An axis whose values are open cannot * support a projection, because nothing bounds the family being projected * from. * * NOT `classification_axis`. That instrument sorts SUBJECTS into cells: a * subject may sit in several at once, each placement carries evidence and * confidence, and the axis describes a landscape. This one selects WHICH * GRAPH YOU ARE READING: exactly one value holds at a time, placements carry * no evidence, and changing the value changes what exists. The shapes rhyme; * the questions do not. * * Per UPG principle P14, structural relationships are edges: * the product that defines it: `product_defines_configuration_axis` (inbound) * the driving mechanism: `feature_flag_drives_configuration_axis` (inbound) * what varies along it: `surface_varies_by_configuration_axis` (inbound) * * @example * const properties: ConfigurationAxisProperties = { * values: ['legacy_nav', 'split_nav'], * default_value: 'legacy_nav', * kind: 'feature_flag', * } */ interface ConfigurationAxisProperties { /** * The closed set of values this axis can take. Required: an axis with no * values selects nothing and cannot be projected along. Every `present_under` * and `active_when.values` entry must name one of them. * * @remarks * Two values is the common case and three is not unusual (a plan ladder). * Order is not significant: the axis is categorical, not ordinal. Where the * ordering does matter (an entitlement ladder in which each tier includes the * one below), that is a classification question, and `classification_axis` * with `axis_kind: 'ordinal'` is the instrument for it. */ values: string[]; /** * The value this axis is understood to sit at when nobody says otherwise. * Must be a member of `values`. NOTHING APPLIES IT AUTOMATICALLY: an * unqualified read returns the union, not this projection. * * @remarks * The union is the honest answer to an unqualified question, because it is * every configuration at once; silently substituting one of them would hide * the others from a reader who did not know to ask. What the field does carry * is the declaration convention for `surface_alternates_with_surface` * (declare the edge from the surface present under the default) and a * documented anchor for tools that later want to offer a starting * configuration. * * It is a claim about the model, not about deployment: it says which value * most of the graph was written against, not which configuration most * customers are on. */ default_value?: string; /** What kind of lever this is. Names the mechanism family. */ kind?: ConfigurationAxisKind; } /** * UPG Property Schemas: Metrics. Unified measurement across the product. * https://unifiedproductgraph.org/spec | MIT */ /** Role a metric plays in the measurement system */ type MetricDesignation = 'north_star' | 'kpi' | 'driver' | 'input' | 'guardrail' | 'proxy' | 'health' | 'vanity' | 'metric'; /** Aggregation function applied to raw data */ type MetricStatisticalFunction = 'average' | 'total' | 'count' | 'median' | 'rate' | 'ratio' | 'percentage' | 'score' | 'min' | 'max' | 'p95' | 'p99' | 'growth_rate' | 'conversion_rate' | 'retention_rate' | 'churn_rate' | 'custom'; /** Where a metric sits in the impact hierarchy */ type MetricImpactLevel = 'impact' | 'outcome' | 'output'; /** Whether a metric leads or lags the behaviour it measures */ type MetricIndicatorDirection = 'leading' | 'lagging'; /** AARRR or HEART category */ type MetricCategory = 'acquisition' | 'activation' | 'retention' | 'referral' | 'revenue' | 'engagement' | 'happiness' | 'task_success' | 'adoption' | 'other'; /** * How often a metric is measured or updated. * * @deprecated since v0.4.0. Removed in v0.5.0. Replaced by the spec-wide * `Cadence` primitive. `'realtime'` maps to `'continuous'`; all other values * map 1:1. Authors should set `MetricProperties.cadence` (typed `Cadence`) on * new graphs. */ /** * Universal metric health rollup. Applies to every metric regardless of * `designation`. Orthogonal to lifecycle (metric has no `UPGBaseNode.status`) * and to `GuardrailStatus` (which stays breach-specific for guardrails). * * Renderers read `metric_health` for the lifecycle-dot signal across all * metric types; `guardrail_status` remains an overlay for guardrails. */ type MetricHealth = 'healthy' | 'at_risk' | 'unhealthy' | 'unknown'; /** Current guardrail health state. Breach-specific; meaningful when * `designation === 'guardrail'`. For a universal health signal across all * metric types, see `MetricHealth`. */ type GuardrailStatus = 'safe' | 'warning' | 'breached'; /** Type of audience or group a metric segment represents */ type MetricSegmentType = 'persona' | 'cohort' | 'channel' | 'geography' | 'device' | 'plan' | 'custom'; /** Why a metric is used as a proxy */ type ProxyReason = 'qualitative' | 'no_direct_measure' | 'not_yet_instrumented' | 'too_expensive'; /** * How strongly a proxy metric predicts the direct measure. Pairs with * `UPG_ENUM_SCALES.ProxyConfidence` for per-value labels and descriptions. */ type ProxyConfidence = 'strong' | 'moderate' | 'weak'; /** A time-series reading for a metric. One data point in the metric's history. */ interface MetricDataPoint { /** Unique data point identifier */ id: string; /** The recorded metric value */ value: number; /** Optional annotation for this reading */ note?: string; /** When the measurement was taken (ISO 8601) */ recorded_at: ISODateTime; /** When this record was created (ISO 8601) */ created_at: ISODateTime; } /** Unified metric. Measures progress, health, or behaviour across the product. * * @example * const properties: MetricProperties = { * designation: 'guardrail', * action: 'Send welcome email', * unit_of_analysis: 'unit of analysis', * } * * Structural refs go via edges (v0.4.0): * guards which metric → `metric_guards_metric` (replaces deprecated `guardrail_for` property) */ interface MetricProperties { /** Role this metric plays in the measurement system */ designation?: MetricDesignation; /** The action or behaviour this metric measures (e.g. "users who activated within 7 days") */ action?: string; /** The unit being counted or measured (e.g. "users", "sessions", "£") */ unit_of_analysis?: string; /** Aggregation function applied to raw data */ statistical_function?: MetricStatisticalFunction; /** Calculation formula or expression */ formula?: string; /** Where this metric sits in the impact hierarchy */ impact_level?: MetricImpactLevel; /** Whether this metric leads or lags the behaviour it measures */ indicator_direction?: MetricIndicatorDirection; /** AARRR or HEART category this metric belongs to */ metric_category?: MetricCategory; /** Most recent observed value */ current_value?: number; /** Value we are aiming to reach */ target_value?: number; /** Display unit for the metric value (e.g. "%", "ms", "£") */ unit?: string; /** Minimum expected or baseline value */ range_min?: number; /** Maximum expected or ceiling value */ range_max?: number; /** * Measurement cadence. Canonical `Cadence` since v0.4.0. * `'realtime'` migrates to `'continuous'`; all other values 1:1. */ cadence?: Cadence; /** Person or team responsible for tracking this metric. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; /** * Universal health rollup, applying to every metric regardless of * `designation`: `healthy` is inside range, `at_risk` is drifting toward a * breach or shortfall, `unhealthy` has missed or breached, `unknown` has no * current reading. * * @remarks * Orthogonal to lifecycle and to `guardrail_status`. For guardrails * specifically, `guardrail_status` remains the breach-specific signal * (`safe` / `warning` / `breached`), so a guardrail carries both: one says * how it is trending, the other says whether it has been crossed. */ metric_health?: MetricHealth; /** Lower bound for guardrail safety (below this = breach) */ guardrail_threshold_min?: number; /** Upper bound for guardrail safety (above this = breach) */ guardrail_threshold_max?: number; /** Current guardrail health state */ guardrail_status?: GuardrailStatus; } /** * A point-in-time review of a metric's quality and fitness for purpose. * * Quality assessments are made BY someone, AT a moment in time. Splitting * them out of `metric` cleanly separates "what the metric is" (intrinsic * definition) from "how good a measurement we currently think it is" * (assessment). A single metric can carry multiple assessments over its * lifetime, one per review. * * Linked to its target metric via the `metric_assessed_by_metric_quality_assessment` * edge. * * @example * const properties: MetricQualityAssessmentProperties = { * assessed_at: '2026-04-27T10:00:00Z', * assessor: 'product-team', * quality_correlated: true, * quality_actionable: true, * quality_score: 4, * } */ interface MetricQualityAssessmentProperties { /** ISO 8601 timestamp of when this assessment was made */ assessed_at?: ISODateTime; /** Free-text assessor label (person, team, or role) */ assessor?: string; /** Quality signal: metric correlates with outcomes we care about */ quality_correlated?: boolean; /** Quality signal: team can take action based on this metric */ quality_actionable?: boolean; /** Quality signal: metric changes when behaviour changes */ quality_sensitive?: boolean; /** Quality signal: metric can be compared across cohorts or time */ quality_comparative?: boolean; /** Quality signal: metric relates to other key metrics in the system */ quality_related?: boolean; /** Computed quality score across all quality signals (0–5) */ quality_score?: number; /** Why this metric is used as a proxy instead of measuring directly */ proxy_reason?: ProxyReason; /** How strongly this metric predicts the direct measure */ proxy_confidence?: ProxyConfidence; } /** * UPG Property Schemas: Engineering Domain. * Service, BoundedContext, DomainEvent, ApiContract, TechnicalDebtItem, * FeatureFlag, Deployment, DDD building blocks, Investigation, RootCause, * Symptom, Fix. * https://unifiedproductgraph.org/spec | MIT */ /** DDD bounded context. * * @example * const properties: BoundedContextProperties = { * team_owner: 'platform-team', * tech_stack: ['TypeScript', 'Postgres', 'Redis'], * ubiquitous_language: 'Order, Customer, LineItem, Fulfilment, Invoice.', * } */ interface BoundedContextProperties { /** Owning team. Promote to a `node_owned_by_team` edge if ownership must be queryable. */ team_owner?: string; /** Technologies used within this context */ tech_stack?: string[]; /** Key terms and their definitions */ ubiquitous_language?: string; } /** Service or microservice. * * @example * const properties: ServiceProperties = { * service_type: 'web', * tech_stack: ['TypeScript', 'Postgres', 'Redis'], * } */ interface ServiceProperties { /** Functional classification. Expanded from Backstage's component type vocabulary. `cli` added in 0.9.12. */ service_type?: 'web' | 'api' | 'worker' | 'db' | 'queue' | 'library' | 'mobile' | 'docs' | 'lambda' | 'cli'; /** Technologies used (e.g. ["TypeScript", "Postgres", "Redis"]) */ tech_stack?: string[]; /** Owning person or team. Backstage marks this required; strongly recommended. Promote to a `node_owned_by_team` edge if ownership must be queryable. */ owner?: string; /** * Service maturity. Answers "how mature is it?". * `experimental` = early-stage. `production` = battle-tested. * `deprecated` = being phased out. */ lifecycle?: 'experimental' | 'production' | 'deprecated'; /** Free-form filter tags (e.g. ["payments", "critical-path", "team-alpha"]). */ tags?: string[]; /** Named URLs for documentation, dashboards, runbooks. */ links?: Array<{ title: string; url: string; }>; } /** Domain event in an event-driven architecture. * * @example * const properties: DomainEventProperties = { * event_name: 'node.created', * payload_schema: 'https://schemas.entopo.app/events/node-created.v1.json', * triggered_by: 'user-signup', * } */ interface DomainEventProperties { /** Event name (e.g. "OrderPlaced") */ event_name?: string; /** Payload schema or shape */ payload_schema?: string; /** What triggers this event */ triggered_by?: string; } /** API contract. * * @example * const properties: ApiContractProperties = { * spec_url: 'https://unifiedproductgraph.org/spec/v0.2', * protocol: 'REST', * version: '0.3.1', * } */ interface ApiContractProperties { /** URL of the specification document */ spec_url?: string; /** Communication protocol. `SSE` (server-sent events) added in 0.9.12. */ protocol?: 'REST' | 'GraphQL' | 'gRPC' | 'AsyncAPI' | 'SOAP' | 'WebSocket' | 'MQTT' | 'SSE' | 'other'; /** API version */ version?: string; /** Maintaining person or team. Promote to a `node_owned_by_team` edge if ownership must be queryable. */ owner?: string; } /** Technical debt item. * * @example * const properties: TechnicalDebtItemProperties = { * debt_type: 'code', * severity: 4, * effort_to_fix: 4, * } */ interface TechnicalDebtItemProperties { /** * Type of debt. * `code` = quality issues. `architecture` = structural problems. * `security` = unpatched vulnerabilities. `test` = missing/flaky tests. * `docs` = missing or stale documentation. `dependency` = outdated packages. */ debt_type?: 'code' | 'architecture' | 'security' | 'test' | 'docs' | 'dependency'; /** Severity on system or team. Requires human evaluation. */ severity?: UPGAssessment; /** Estimated effort to resolve. Requires team knowledge of the codebase. */ effort_to_fix?: UPGAssessment; /** Owning person or team responsible for paydown. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; /** * Codebase location, service, or module. * @example "apps/graph/src/canvas/", "UserService", "auth module" */ affected_area?: string; /** * Ongoing cost of leaving it unresolved (the "interest" in the financial metaphor). * @example "~2h/sprint of workarounds", "blocks type-safe refactor of checkout" */ interest?: string; /** * Origin of the debt. * `deliberate` = conscious decision to ship something imperfect (prudent or reckless). * `inadvertent` = discovered after the fact. * Based on Fowler's Technical Debt Quadrant. */ intentionality?: 'deliberate' | 'inadvertent'; } /** Feature flag. * * @example * const properties: FeatureFlagProperties = { * key: 'cta.primary.signup', * rollout_pct: 42, * } */ interface FeatureFlagProperties { /** Required. Stable flag key used in code (e.g. "new-checkout-flow"). */ key: string; /** Percentage enabled (0–100). Meaningful when the flag's `status === 'rollout'`. */ rollout_pct?: number; /** Human-readable targeting rules. Full rule evaluation happens in the flag service. */ targeting_rules?: string; /** Owning person or team responsible for the flag's lifecycle. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; /** * Lifecycle classification. * `temporary` = should be removed after rollout (kill switch, gradual rollout). * `permanent` = long-lived feature gate (entitlement flag). * `experiment` = A/B test with a defined end condition. */ flag_type?: 'temporary' | 'permanent' | 'experiment'; /** * ISO date after which this flag should be cleaned up. * A temporary flag without an `expiry_date` is a code smell. */ expiry_date?: ISODate; /** Creation date. Useful for flag age and stale-flag detection. */ created_date?: ISODate; } /** Deployment. * * @example * const properties: DeploymentProperties = { * environment: 'prod', * timestamp: '2026-04-17T09:00:00Z', * } */ interface DeploymentProperties { /** Target environment */ environment?: 'dev' | 'staging' | 'prod'; /** ISO timestamp */ timestamp?: string; /** Git SHA of the deployed commit */ sha?: string; /** Wall-clock duration in seconds. Tracks deployment speed trends. */ duration_seconds?: number; /** Triggering person or system. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ deployer?: string; } /** DDD aggregate. * * @example * const properties: AggregateProperties = { * aggregate_root: 'Order', * invariants: 'Order total never negative; line items non-empty.', * } */ interface AggregateProperties { /** Root entity */ aggregate_root?: string; /** Enforced business rules */ invariants?: string; } /** DDD domain entity. * * @example * const properties: DomainEntityProperties = { * entity_identity: 'aggregate-root', * lifecycle: 'untested → testing → resolved', * } */ interface DomainEntityProperties { /** Identifier shape (e.g. "UUID", "email") */ entity_identity?: string; /** Lifecycle description */ lifecycle?: string; } /** DDD value object. * * @example * const properties: ValueObjectProperties = { * immutable: true, * equality_fields: ['id', 'version'], * } */ interface ValueObjectProperties { /** Whether immutable */ immutable?: boolean; /** Fields used for equality */ equality_fields?: string; } /** CQRS command. * * @example * const properties: CommandProperties = { * command_handler: 'OrderService.placeOrder', * validation_rules: 'title required; description max 2000 chars; tags must be lower-kebab.', * } */ interface CommandProperties { /** Processing handler */ command_handler?: string; /** Pre-execution validation rules */ validation_rules?: string; } /** CQRS read model / projection. * * @example * const properties: ReadModelProperties = { * projection_source: 'order_events', * refresh_strategy: 'sync', * } */ interface ReadModelProperties { /** Source event or aggregate */ projection_source?: string; /** How the model stays current */ refresh_strategy?: 'sync' | 'async' | 'cron' | 'on_demand'; } /** API endpoint. * * @example * const properties: ApiEndpointProperties = { * http_method: 'GET', * path: '/docs/spec/v0.2', * auth_required: true, * } */ interface ApiEndpointProperties { /** HTTP method */ http_method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; /** URL path (e.g. "/api/v1/products/:id") */ path?: string; /** Whether authentication is required */ auth_required?: boolean; /** Rate limit description (e.g. "100/min") */ rate_limit?: string; } /** Database schema. * * @example * const properties: DatabaseSchemaProperties = { * db_type: 'postgres', * schema_version: '1.3.0', * } */ interface DatabaseSchemaProperties { /** Engine */ db_type?: 'postgres' | 'mysql' | 'mongodb' | 'redis' | 'other'; /** Current schema version */ schema_version?: string; /** Owning person or team responsible for design and migrations. Promote to a `node_owned_by_team` edge if ownership must be queryable. */ owner?: string; /** Tables or collections in this schema. Useful for migration scope estimation. */ table_count?: number; } /** Message queue or topic. * * @example * const properties: QueueTopicProperties = { * queue_type: 'sqs', * retention_hours: 42, * consumer_groups: ['analytics', 'search-indexer'], * } */ interface QueueTopicProperties { /** Technology */ queue_type?: 'sqs' | 'kafka' | 'rabbitmq' | 'pubsub' | 'other'; /** Message retention in hours */ retention_hours?: number; /** Consumer group names */ consumer_groups?: string; /** * Whether a dead-letter queue is configured. Absent or `false` flags a message-loss risk. * A DLQ is critical for debugging failed message processing. */ has_dead_letter_queue?: boolean; } /** Build artifact. * * @example * const properties: BuildArtifactProperties = { * artifact_type: 'docker_image', * version: '0.3.1', * size: 120, * } */ interface BuildArtifactProperties { /** Type */ artifact_type?: 'docker_image' | 'npm_package' | 'binary' | 'static_assets' | 'other'; /** Version */ version?: string; /** Human-readable size (e.g. "12.4 MB") */ size?: string; /** Registry or storage location */ registry?: string; /** URL of the producing CI/CD build run (GitHub Actions, CircleCI, etc.). */ build_url?: string; } /** Code repository. * * @example * const properties: CodeRepositoryProperties = { * repo_url: 'https://github.com/arkheiev/entopo', * default_branch: 'main', * language: 'en-GB', * } */ interface CodeRepositoryProperties { /** URL */ repo_url?: string; /** Default branch */ default_branch?: string; /** Primary programming language */ language?: string; /** Current CI status */ ci_status?: 'passing' | 'failing' | 'unknown'; /** * Visibility. * `internal` = visible within the organisation only (GitHub internal repos). */ visibility?: 'public' | 'private' | 'internal'; } /** Library or package dependency. * * @example * const properties: LibraryDependencyProperties = { * dep_version: '^0.3.1', * dep_type: 'runtime', * license: 'MIT', * } */ interface LibraryDependencyProperties { /** Installed version */ dep_version?: string; /** Dependency classification */ dep_type?: 'runtime' | 'dev' | 'peer' | 'optional'; /** SPDX license identifier */ license?: string; /** Whether a newer version is available */ is_outdated?: boolean; /** Known vulnerabilities in the installed version. Populated from npm audit, Snyk, etc. */ vulnerability_count?: number; } /** Integration pattern between systems. * * @example * const properties: IntegrationPatternProperties = { * pattern_type: 'api', * protocol: 'https', * } */ interface IntegrationPatternProperties { /** Type. `adapter`, `client_library`, `host_embedding`, `pipes_and_filters`, `data_sync` added in 0.9.12. */ pattern_type?: 'api' | 'event' | 'file' | 'database' | 'webhook' | 'adapter' | 'client_library' | 'host_embedding' | 'pipes_and_filters' | 'data_sync'; /** Communication protocol */ protocol?: string; } /** External API dependency. * * @example * const properties: ExternalApiProperties = { * provider: 'anthropic', * base_url: 'https://api.entopo.app/v1', * auth_type: 'api_key', * } */ interface ExternalApiProperties { /** Provider */ provider?: string; /** Base URL */ base_url?: string; /** Authentication method */ auth_type?: 'api_key' | 'oauth2' | 'jwt' | 'basic' | 'none'; /** Rate limit description */ rate_limits?: string; } /** Data flow between services. * * @example * const properties: DataFlowProperties = { * trigger: 'User opens the third restricted feature in a session.', * data_type: 'string', * direction: 'unidirectional', * } */ interface DataFlowProperties { /** What triggers the flow */ trigger?: string; /** Type of data transferred */ data_type?: string; /** Direction (cardinality of the link). */ direction?: 'unidirectional' | 'bidirectional'; /** * Flow orientation relative to the system or component, a separate axis from * `direction` (which is cardinality). A flow can be `unidirectional` AND * `inbound`. Added in 0.9.12 to split the orientation axis that authors were * conflating into `direction` (inbound/outbound/internal values). */ orientation?: 'inbound' | 'outbound' | 'internal'; /** Communication protocol */ protocol?: 'rest' | 'graphql' | 'grpc' | 'event' | 'webhook' | 'file'; } /** Active thread of inquiry: debugging, architecture exploration, RCA. * * Container for root-cause analysis. Binds symptoms (observable behaviour), * root causes (structural reasons), and fixes (remediations). Lifecycle is * tracked via `UPGBaseNode.status` (canonical lifecycle slot). * * Per UPG principle P14, structural relationships are edges: * observed signals: `investigation_observes_symptom` * identified cause: `investigation_identifies_root_cause` * planned remediation: `investigation_resolves_via_fix` * parent incident: `incident_triggers_investigation` * * @example * const properties: InvestigationProperties = { * severity: 4, * hypothesis: 'Removing the manual setup step lifts week-one activation by 15%.', * findings: 'Three out of five participants gave up before reaching the canvas.', * started_at: '2026-04-22T09:15:00Z', * resolved_at: '2026-04-26T17:30:00Z', * lead_investigator: 'sam.patel@arkheiev.com', * category: 'activation', * } */ interface InvestigationProperties { /** * Believed severity of the underlying issue. Drives prioritisation across investigations. * * Canonicalised in v0.4.0: the ad-hoc `'low' | 'medium' | 'high' | 'critical'` * shape was replaced by `UPGAssessment` so every "severity" property reports * on the same axis. Migration: `low → 2`, `medium → 3`, `high → 4`, `critical → 5`. */ severity?: UPGAssessment; /** Working hypothesis about the root cause */ hypothesis?: string; /** Findings discovered so far */ findings?: string; /** ISO timestamp the investigation began */ started_at?: ISODateTime; /** ISO timestamp the investigation was concluded. Pairs with `status === 'resolved' | 'abandoned'`. */ resolved_at?: ISODateTime; /** Lead investigator (email or handle). Distinct from the team owning the affected service. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ lead_investigator?: string; /** * Kind of issue under investigation. Distinct from `RootCauseProperties.cause_category`, * which captures *why something went wrong*. */ category?: 'performance' | 'security' | 'data_quality' | 'reliability' | 'cost' | 'compliance' | 'other'; } /** Underlying architectural or systemic issue. * * The structural reason a `symptom` was observable. Captured during an * `investigation` and resolved by one or more `fix` entities. * * Per UPG principle P14, structural relationships are edges: * parent investigation: `investigation_identifies_root_cause` * explained symptoms: `root_cause_causes_symptom` * resolving fixes: `fix_addresses_root_cause` * * @example * const properties: RootCauseProperties = { * cause_category: 'code', * severity: 4, * cause_confidence: 'confirmed', * evidence_summary: 'Trace shows retry loop on 4xx responses; reproduced in staging with synthetic 401.', * affected_area: 'Billing settlement service', * } */ interface RootCauseProperties { /** Severity (1 = minor, 5 = critical) */ severity?: UPGAssessment; /** * Closed-enum cause category for RCA reporting and dashboards. * Distinct from the legacy free-form `category`. */ cause_category?: 'code' | 'config' | 'process' | 'dependency' | 'data' | 'infrastructure' | 'human_error' | 'other'; /** * Team certainty about this cause. * `hypothesised` = educated guess. `likely` = evidence points here. * `confirmed` = reproduced. * * Renamed from `confidence` in v0.4.0 to disambiguate from the entity-wide * `UPGAssessment`-typed epistemic confidence used elsewhere. The 3-tier shape * stays as a discrete RCA-lifecycle marker. */ cause_confidence?: CauseConfidence; /** One-paragraph evidence summary. Log lines, traces, repro steps. Detailed artefacts go on linked `evidence` nodes. */ evidence_summary?: string; /** Affected area of the system */ affected_area?: string; } /** Observable behaviour produced by a root cause. * * The visible side of an issue. Upstream of `root_cause` (which explains it) * and orthogonal to `bug` (a tracked work item; a symptom is a raw observation). * * Per UPG principle P14, structural relationships are edges: * observed in: `investigation_observes_symptom` * explained by: `root_cause_causes_symptom` * reported via: `support_ticket_reports_symptom` * * @example * const properties: SymptomProperties = { * symptom_description: 'Users see a blank canvas after refreshing during an unsaved edit.', * first_observed_at: '2026-04-22T14:08:00Z', * severity: 4, * frequency_rating: 'occasional', * affected_users_estimate: 120, * reproducibility: 'intermittent', * steps_to_reproduce: '1. Open canvas. 2. Edit a node. 3. Hard-refresh before autosave fires.', * } */ interface SymptomProperties { /** Plain-language description of observed behaviour. Primary content of the entity. */ symptom_description?: string; /** ISO timestamp first observed in the wild. Pairs with `frequency_rating` and `reproducibility` for triage. */ first_observed_at?: ISODateTime; /** * Severity for affected users. Independent of how widespread the symptom is. * * Canonicalised in v0.4.0: the ad-hoc `'low' | 'medium' | 'high' | 'critical'` * shape was replaced by `UPGAssessment`. */ severity?: UPGAssessment; /** Exact observation count in the period. Pairs with `frequency_period` for a precise rate. */ frequency_count?: number; /** * Recurrence period (ISO-8601 `Duration`). * @example 'P7D' (per week), 'P1D' (per day), 'PT1H' (per hour) */ frequency_period?: Duration; /** * Qualitative frequency tier. Canonical replacement for the legacy * `'once' | 'sporadic' | 'frequent' | 'constant' | string` shape. * Use when an exact rate is unknown. * Migration: `once → rare`, `sporadic → occasional`, * `frequent → regular`, `constant → constant`. */ frequency_rating?: FrequencyRating; /** Approximate count of users affected. Snapshot estimate. */ affected_users_estimate?: number; /** Reproduction reliability */ reproducibility?: 'always' | 'frequent' | 'intermittent' | 'rare' | 'once'; /** Steps to reproduce */ steps_to_reproduce?: string; } /** Specific change that resolved an issue. * * Remediation side of an RCA. The concrete change (code, config, process) * that resolved or mitigated a `root_cause`. * * Per UPG principle P14, structural relationships are edges: * addresses cause: `fix_addresses_root_cause` * relates to investigation: `investigation_resolves_via_fix` * delivered by: `deployment_delivers_fix` * validated by: `test_case_validates_fix` / `regression_test_guards_fix` * * @example * const properties: FixProperties = { * fix_type: 'permanent', * commit: 'a3f1c9e', * files_changed: ['packages/upg-spec/src/shapes/document.ts'], * deployed_at: '2026-04-26T18:02:00Z', * fixed_date: '2026-04-26', * verified: true, * verified_by_test: true, * } */ interface FixProperties { /** * Kind of fix. * `hotfix` = urgent patch. `permanent` = proper structural fix. * `workaround` = mitigates symptom without addressing root cause; should track a follow-up. */ fix_type?: 'hotfix' | 'permanent' | 'workaround' | 'configuration' | 'process_change'; /** Git commit SHA */ commit?: string; /** Files changed */ files_changed?: string[]; /** ISO timestamp landed in the target environment. */ deployed_at?: ISODateTime; /** Application date (ISO date). Coarser-grained complement to `deployed_at`. */ fixed_date?: ISODate; /** Verified in production */ verified?: boolean; /** Validated by an automated regression or integration test. Distinct from `verified` (human judgment). */ verified_by_test?: boolean; } /** * UPG Property Schemas: DevOps & Platform Domain. * SLI, SLO, ErrorBudget, Incident, Postmortem, Runbook, Monitor, * AlertRule, CiPipeline, ReleaseStrategy, OnCallRotation, * InfrastructureComponent. * https://unifiedproductgraph.org/spec | MIT */ /** Service Level Indicator. * * @example * const properties: ServiceLevelIndicatorProperties = { * metric_name: 'weekly_active_teams', * threshold: 10, * current_value: 42, * } */ interface ServiceLevelIndicatorProperties { /** * Threshold that defines a "good" event. * @example 200 (ms latency), 0.01 (1% error rate), 99.9 (% availability) */ threshold?: number; /** * Current observed value. Compared against `threshold` for SLO compliance. * @example 150 (ms), 0.003 (0.3% error rate) */ current_value?: number; /** * Unit of measurement. Required to interpret `threshold` and `current_value`. * @example "ms", "%", "req/s", "errors/min" */ unit?: string; /** * Aggregation over the evaluation window. p99 and avg tell different stories. * @example "p99" for tail latency, "avg" for mean throughput, "count" for total events */ aggregation?: 'avg' | 'sum' | 'max' | 'min' | 'p50' | 'p95' | 'p99' | 'count'; /** * Query expression that produces `current_value`. Free-form to fit PromQL, * Datadog query strings, SQL, or vendor-specific DSLs. * @example 'sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))' */ measurement_query?: string; /** * Historical baseline. Pairs with `current_value` to indicate drift. * @example 99.85 */ baseline_value?: number; } /** Service Level Objective. * * @example * const properties: ServiceLevelObjectiveProperties = { * target_percentage: 42, * window: 'rolling-30-days', * current_percentage: 42, * } */ interface ServiceLevelObjectiveProperties { /** * Target percentage. The reliability commitment. * @example 99.9 (three nines), 99.95, 99.99 (four nines) */ target_percentage?: number; /** * Evaluation window. * @example "30 days", "rolling 28 days", "calendar quarter" */ window?: string; /** * Current achieved percentage. Compared against `target_percentage` for health. * @example 99.92 (above a 99.9 target) */ current_percentage?: number; /** * Measurement mechanism. * `metric` = ratio (good/total). `monitor` = monitor-based. `time_slice` = uptime windows. */ slo_type?: 'metric' | 'monitor' | 'time_slice'; /** * Soft alert threshold before the target breaches. Gives teams time to act. * @example 99.95 (warn at 99.95% when target is 99.9%) */ warning_threshold?: number; } /** Error budget. * * @example * const properties: ErrorBudgetProperties = { * budget_remaining: 42, * burn_rate: 42, * policy: 'least-privilege', * } */ interface ErrorBudgetProperties { /** * Remaining budget percentage (0–100). * @example 45.2 (45.2% remaining, 54.8% used) */ budget_remaining?: number; /** * Consumption rate as a multiplier against sustainable burn. * 1.0 = on track. 2.5 = consuming 2.5x faster than sustainable. */ burn_rate?: number; /** * Policy when the budget hits 0%. * @example "Freeze all non-reliability deploys", "Page engineering lead immediately" */ policy?: string; /** * Budget window. Defines reset cadence. * @example "30 days", "rolling 28 days" */ budget_window?: string; } /** Incident. * * @example * const properties: IncidentProperties = { * incident_type: 'operational', * severity_level: 'sev1', * urgency: 'high', * } */ interface IncidentProperties { /** * Discriminator. Absorbs the deprecated `security_incident` type. * When `incident_type === 'security'`, this node replaces the former `security_incident`. * @example "security" for a data breach, "operational" for a service outage, "performance" for degradation */ incident_type?: 'operational' | 'security' | 'data_breach' | 'performance' | 'dependency' | 'other'; /** * Incident severity tier (paging classification). `sev1` = critical/system * down. `sev2` = major impact. `sev3` = minor impact. `sev4` = minimal. * Uses the `IncidentSeverity` scale; distinct from user-impact `severity_5`. * @example "sev1" for complete service unavailability */ severity_level?: IncidentSeverity; /** * Notification urgency. Independent of severity. Uses the shared * `SignalUrgency` scale (`low` | `medium` | `high` | `critical`); * higher tiers escalate the notification channel. */ urgency?: SignalUrgency; /** ISO timestamp the incident started or was first detected. */ started_at?: ISODateTime; /** ISO timestamp first acknowledged by a responder. Used to compute time-to-acknowledge. */ acknowledged_at?: ISODateTime; /** * ISO timestamp contained. Blast radius limited, bleeding stopped. * Containment precedes full resolution, especially for security incidents. */ contained_at?: ISODateTime; /** ISO timestamp fully resolved. */ resolved_at?: ISODateTime; /** * Customer or service impact. * @example "Users unable to log in", "Payment processing delayed by 30+ seconds for 15% of users" */ impact_summary?: string; } /** Postmortem. * * @example * const properties: PostmortemProperties = { * timeline: 'Kickoff 2026-04-22, results by 2026-05-15.', * action_items: 'Book the kickoff; draft the research brief; recruit 5 participants.', * detection_method: 'monitoring', * } */ interface PostmortemProperties { /** * Chronological timeline. Events with timestamps in order. * @example "03:15 Alert fired. 03:20 On-call acknowledged. 03:45 Root cause identified. 06:30 Service restored." */ timeline?: string; /** * Follow-up actions with owners and due dates. * @example "1. Add circuit breaker to auth service (owner: Platform, due: 2026-04-12). 2. Update runbook for DB failover." */ action_items?: string; /** * Detection source. Key learning for improving detection coverage. * @example "alert" if monitoring caught it, "customer_report" if a user reported first */ detection_method?: 'monitoring' | 'alert' | 'customer_report' | 'internal_report' | 'automated'; } /** Runbook. * * @example * const properties: RunbookProperties = { * trigger: 'User opens the third restricted feature in a session.', * steps: ['Open the workspace', 'Pick a persona', 'Commit a decision'], * last_tested: '2026-04-12', * } */ interface RunbookProperties { /** * Triggering event or alert. * @example "Error rate exceeds 5% for 5 minutes", "Database connection pool exhausted" */ trigger?: string; /** * Ordered steps, one action per element. * @example ["Check Grafana dashboard X", "SSH into affected node", "Restart service Y"] */ steps?: string[]; /** * ISO date last tested or rehearsed. Runbooks degrade if untested. * @example "2026-03-15" */ last_tested?: string; /** * Operational maturity. Manual runbooks are candidates for automation investment. * `semi_automated` = some steps scripted; human judgment still required. */ automation_level?: 'manual' | 'semi_automated' | 'fully_automated'; } /** Monitor. * * @example * const properties: MonitorProperties = { * monitor_type: 'uptime', * target: 'Week-one activation ≥ 45%.', * threshold: 10, * } */ interface MonitorProperties { /** * Measurement kind. * `uptime` = availability. `latency` = response time. `error_rate` = failure ratio. * `throughput` = req/sec. `log` = log-based. `event` = event-driven. * `synthetic` = scripted user-journey tests. `slo_burn` = tracks SLO error budget. * @example "synthetic" for a scripted checkout flow test */ monitor_type?: 'uptime' | 'latency' | 'error_rate' | 'throughput' | 'log' | 'event' | 'synthetic' | 'slo_burn' | 'custom'; /** * Service, endpoint, or resource monitored. * @example "graph-api /health", "PostgreSQL connection pool", "CDN edge latency" */ target?: string; /** * Alert condition, expressed as a condition rather than a bare number. * @example "> 500ms p99", "< 99.9% uptime over 5 minutes", "> 1% error rate" */ threshold?: string; /** * Alert destination on threshold breach. * @example "slack:#ops-alerts", "pagerduty:on-call-graph" */ alert_channel?: string; /** * Operational state. * `ok` = all clear. `warn` = approaching threshold. `alert` = threshold breached. * `no_data` = nothing received (may indicate monitor or service failure). `muted` = silenced. */ monitor_status?: 'ok' | 'warn' | 'alert' | 'no_data' | 'muted'; /** Currently silenced. Typical during planned maintenance windows. */ muted?: boolean; } /** Alert rule. * * @example * const properties: AlertRuleProperties = { * condition: 'personas.length > 0 && opportunities.length === 0', * severity: 'critical', * notification_channel: '#alerts-product', * } */ interface AlertRuleProperties { /** * Triggering query or expression. * @example "avg(rate(http_errors_total[5m])) > 0.05" */ condition?: string; /** * Routes notification and escalation. Uses the `LogLevel` scale (operational * verbosity, distinct from user-impact `severity_5`). * `critical` = page immediately. `warning` = notify, don't page. `info` = log only. */ severity?: LogLevel; /** * Notification destination. * @example "pagerduty:sev1-rotation", "slack:#alerts-low" */ notification_channel?: string; /** * Required duration the condition holds before firing. Prevents flapping on transient spikes. * @example "5m", "15m", "1h" */ evaluation_window?: string; /** * Escalation behaviour on unacknowledged alerts. * @example "Escalate to engineering lead after 10 minutes" */ escalation_policy?: string; } /** CI pipeline. * * @example * const properties: CiPipelineProperties = { * pipeline_type: 'build', * trigger: 'User opens the third restricted feature in a session.', * avg_duration: '45m', * } */ interface CiPipelineProperties { /** * Pipeline scope, narrowest to broadest. * `build` = compile only. `test` = test only. `deploy` = ship to environment. * `release` = create release artifact. `full` = commit-to-deploy. */ pipeline_type?: 'build' | 'test' | 'deploy' | 'release' | 'full'; /** * Triggering event. * @example "Push to main", "PR merge", "Nightly schedule at 02:00 UTC", "Manual dispatch" */ trigger?: string; /** * Average run duration across recent executions. * @example "4m 30s", "12 minutes" */ avg_duration?: string; /** Result of the most recent run. Current health indicator. */ last_run_status?: 'success' | 'failure' | 'cancelled' | 'skipped' | 'in_progress'; /** * Primary branch. The branch that triggers production deployments. * @example "main", "release/*" */ target_branch?: string; /** * Total runs since creation. Indicates activity level. * @example 1452 */ run_count?: number; /** * Reliability metric for the pipeline itself: % of runs that succeed. * @example 94.3 */ success_rate?: number; } /** Release strategy. * * @example * const properties: ReleaseStrategyProperties = { * strategy_type: 'blue_green', * canary_percentage: 42, * rollback_criteria: 'If week-one activation drops by 3 percentage points over 48h, roll back.', * } */ interface ReleaseStrategyProperties { /** * How a new version reaches production. * * @example "canary" for gradual rollout, "blue_green" for instant switch with instant rollback * * @remarks * `blue_green` switches instantly between two identical environments. * `canary` rolls out by percentage. `rolling` replaces instances * incrementally. `recreate` takes the old down before bringing the new up. * `feature_flag` ships the code with the features gated. */ strategy_type?: 'blue_green' | 'canary' | 'rolling' | 'recreate' | 'feature_flag'; /** * Traffic routed to canary. Applies when `strategy_type === 'canary'`. * @example 5 (5% initial canary before full rollout) */ canary_percentage?: number; /** * Rollback triggers, automatic or manual. * @example "Error rate > 1% over 5 minutes", "Latency p99 > 2x baseline" */ rollback_criteria?: string; /** * Soak time before promoting to full production. * @example "30m", "2h", "24h" */ bake_time?: string; /** Whether the system rolls back on threshold breach without human intervention. */ auto_rollback?: boolean; } /** On-call rotation. * * @example * const properties: OnCallRotationProperties = { * schedule: '0 2 * * *', * escalation_policy: 'pagerduty-primary', * rotation_cadence: 'daily', * } */ interface OnCallRotationProperties { /** * Human-readable schedule of who is on call when. * @example "Weekly rotation, Monday 09:00 UTC handoff", "Follow-the-sun (US, EU, APAC)" */ schedule?: string; /** * Escalation when the primary doesn't respond. * @example "5 min to respond, then escalate to secondary. 10 min to secondary, then page engineering lead." */ escalation_policy?: string; /** * Cycle cadence. * `weekly` for standard team rotations. `daily` for high-incident-volume teams. */ rotation_cadence?: 'daily' | 'weekly' | 'biweekly' | 'custom'; /** * Shift handoff time. Affects team coordination and sleep. * @example "09:00 UTC", "17:00 local" */ handoff_time?: string; } /** Infrastructure component. * * @example * const properties: InfrastructureComponentProperties = { * component_type: 'compute', * provider: 'anthropic', * region: 'eu-west-1', * } */ interface InfrastructureComponentProperties { /** * Resource category. * `compute` = VMs or containers. `storage` = object/block/file. * `network` = VPC, load balancer, DNS. `database` = managed DB services. * `cdn` = content delivery. `queue` = message broker or event bus. * @example "database" for a managed PostgreSQL instance */ component_type?: 'compute' | 'storage' | 'network' | 'database' | 'cdn' | 'queue' | 'other'; /** * Cloud or infrastructure provider. * @example "AWS", "Vercel", "Cloudflare", "Supabase", "Fly.io" */ provider?: string; /** * Geographic deployment region. * @example "us-east-1", "eu-west-1", "global" (CDN or multi-region) */ region?: string; /** * Monthly cost in base currency (USD). * @example 250.00 */ cost_monthly?: number; /** * Environment. * `production` = live traffic. `staging` = pre-release. * `development` = developer sandbox. `shared` = cross-environment services (e.g. logging). */ environment?: 'production' | 'staging' | 'development' | 'shared'; /** * Operational status. * `healthy` = normal. `degraded` = partial impairment. `down` = unavailable. * `maintenance` = intentionally offline. */ component_status?: 'healthy' | 'degraded' | 'down' | 'maintenance'; } /** * UPG Property Schemas: QA & Testing Domain. * TestPlan, TestSuite, TestCase, QaSession, RegressionTest, TestCoverageReport, * TestEnvironment, TestResult. * https://unifiedproductgraph.org/spec | MIT */ /** Test plan: the QA verification procedure for a product or release. * * Re-homed validation → QA in UPG-678. A `test_plan` defines HOW the team * verifies the software works — the scope under test, the environments it runs * in, the entry/exit (pass) criteria — and groups the `test_suite`s that carry * it out. Its former validation-planning role (designing an experiment for a * hypothesis) is now carried by `experiment_plan`. * * @example * const properties: TestPlanProperties = { * test_scope: 'Checkout, billing settlement, and refund flows.', * plan_type: 'release', * environments: ['staging', 'production_mirror'], * entry_criteria: 'Build deployed to staging; seed data loaded.', * pass_criteria: 'All P0/P1 suites green; no open blocker bugs.', * } */ interface TestPlanProperties { /** What the plan covers (e.g. "checkout flow", "auth module", "release 2.4"). */ test_scope?: string; /** Kind of verification effort this plan governs. */ plan_type?: 'release' | 'regression' | 'integration' | 'acceptance' | 'smoke' | 'exploratory'; /** Environments the plan exercises (mirrors `TestEnvironmentProperties.env_type`). */ environments?: Array<'local' | 'ci' | 'staging' | 'sandbox' | 'production_mirror'>; /** Conditions that must hold before execution may begin. */ entry_criteria?: string; /** Exit / pass criteria determining whether the plan succeeds. */ pass_criteria?: string; } /** Test suite. * * @example * const properties: TestSuiteProperties = { * suite_type: 'unit', * pass_rate: 42, * last_run: '2026-04-10T02:00:00Z', * } */ interface TestSuiteProperties { /** * Category of test suite. The single canonical verification-method vocabulary * in the spec: do not mint a second one at criterion level. * * @remarks * `visual` covers visual-regression and screenshot-diff suites, whose * evidence is a rendered-output comparison rather than an assertion over * behaviour. Before it existed, producers mapped visual suites onto * `integration` as the least-wrong stock value, which made them * indistinguishable from genuine integration coverage. */ suite_type?: 'unit' | 'integration' | 'e2e' | 'performance' | 'security' | 'accessibility' | 'visual'; /** Number of tests in the suite */ test_count?: number; /** Percentage of tests passing (0-100) */ pass_rate?: number; /** ISO date of last execution */ last_run?: string; /** Total execution time for the last run, in milliseconds */ total_duration_ms?: number; /** Number of tests that failed in the last run */ failed_count?: number; /** Number of tests that were skipped in the last run */ skipped_count?: number; /** Number of tests that passed only on retry (flaky) in the last run */ flaky_count?: number; } /** Test case. * * @example * const properties: TestCaseProperties = { * execution_type: 'manual', * priority: 'high', * preconditions: ['User is logged in', 'At least one product exists in the workspace'], * steps: ['Open the product', 'Click the delete button', 'Confirm the dialog'], * } */ interface TestCaseProperties { /** How the test is executed */ execution_type?: 'manual' | 'automated' | 'exploratory'; /** Test importance */ priority?: Priority; /** Conditions required before running the test */ preconditions?: string[]; /** Ordered steps to execute the test */ steps?: string[]; /** What a passing result looks like */ expected_result?: string; /** Result of the most recent execution */ last_result?: 'not_run' | 'pass' | 'fail' | 'blocked' | 'skipped'; /** Hierarchical grouping within the parent suite (e.g. "Checkout / Payment") */ section?: string; /** Test case template applied (e.g. "BDD", "Given-When-Then", "exploratory charter") */ template?: string; /** Links to requirements, tickets, or external documentation */ references?: string[]; /** * Automation status. Closed set so coverage dashboards can group test cases * by automation maturity. Pair with `automation_tool` for the specific * framework (e.g. Playwright, Cypress) when automated. */ automation_status?: 'manual' | 'automated' | 'partially_automated' | 'planned' | 'other'; /** * Automation tool or framework (e.g. `'Playwright'`, `'Cypress'`, `'Jest'`). * Free-text; the universe of tools is open. Pairs with `automation_status` * per audit recommendation (#40). */ automation_tool?: string; } /** QA session. * * @example * const properties: QaSessionProperties = { * session_type: 'exploratory', * duration_minutes: 45, * bugs_found: 42, * } */ interface QaSessionProperties { /** Type of QA session */ session_type?: 'exploratory' | 'regression' | 'smoke' | 'uat'; /** Duration of the session in minutes */ duration_minutes?: number; /** Number of bugs found during the session */ bugs_found?: number; /** * Environment the session was run against. Single-valued, unlike * `TestPlanProperties.environments` (a plan spans several); the enum is the * same one, mirroring `TestEnvironmentProperties.env_type`. */ environment?: 'local' | 'ci' | 'staging' | 'sandbox' | 'production_mirror'; /** * Plain-English outcome of the session: what it established, not how many * bugs it counted (`bugs_found`). Named `outcome_summary`, matching * `ExperimentRunProperties.outcome_summary`, because a bare `outcome` would * collide with the `outcome` entity type. */ outcome_summary?: string; /** ISO timestamp of when the session was run. Mirrors `TestResultProperties.executed_at`. */ executed_at?: ISODateTime; } /** Regression test. * * @example * const properties: RegressionTestProperties = { * regression_scope: 'Core auth flows and billing settlement.', * automated: true, * last_pass: '2026-04-10', * } */ interface RegressionTestProperties { /** Scope of regression coverage (e.g. "checkout flow", "auth module") */ regression_scope?: string; /** Whether the regression test is automated */ automated?: boolean; /** ISO date of last passing run */ last_pass?: string; /** Number of failures in recent runs */ recent_failures?: number; } /** Test coverage report. * * @example * const properties: TestCoverageReportProperties = { * line_coverage: 42, * branch_coverage: 42, * function_coverage: 42, * } */ interface TestCoverageReportProperties { /** Percentage of lines covered (0-100) */ line_coverage?: number; /** Percentage of branches covered (0-100) */ branch_coverage?: number; /** Percentage of functions covered (0-100) */ function_coverage?: number; /** Percentage of statements covered (0-100) */ statement_coverage?: number; /** Coverage target threshold set by the team (0-100) */ target_coverage?: number; /** Number of lines not covered by any test */ uncovered_lines?: number; /** ISO date when the report was generated */ report_date?: ISODate; } /** Test environment. * * @example * const properties: TestEnvironmentProperties = { * env_type: 'local', * env_status: 'available', * config: '{ retries: 3, timeout_ms: 15000 }', * } */ interface TestEnvironmentProperties { /** Type of environment */ env_type?: 'local' | 'ci' | 'staging' | 'sandbox' | 'production_mirror'; /** Current availability status of the environment */ env_status?: 'available' | 'in_use' | 'maintenance' | 'unavailable'; /** Configuration details (e.g. OS, browser version, database seed) */ config?: string; } /** Single test execution result. * * @example * const properties: TestResultProperties = { * result_status: 'passed', * duration_ms: 42, * retry_index: 42, * } */ interface TestResultProperties { /** * Outcome of this execution. * passed = all assertions met; failed = one or more assertions failed; * timed_out = execution exceeded the timeout; skipped = test was not run; * interrupted = test was stopped mid-run. */ result_status: 'passed' | 'failed' | 'timed_out' | 'skipped' | 'interrupted'; /** Duration of this execution in milliseconds */ duration_ms?: number; /** Retry index. 0 = first attempt, 1 = first retry, etc. */ retry_index?: number; /** Error message if the test failed */ error_message?: string; /** Version of the product or build under test */ version_tested?: string; /** * ISO timestamp of the execution. * @example "2026-04-05T14:30:00Z" */ executed_at?: ISODateTime; /** Comma-separated list of attachment names or URLs (screenshots, logs, traces) */ attachments?: string; /** Notes or commentary about this result */ comment?: string; } /** * UPG Property Schemas: Security Domain. * ThreatModel, Threat, Vulnerability, SecurityControl, SecurityPolicy, * PenetrationTest, SecurityReview, DataClassification, AccessPolicy. * https://unifiedproductgraph.org/spec | MIT */ /** Threat model. * * @example * const properties: ThreatModelProperties = { * methodology: 'stride', * scope: 'Covers in-product onboarding; excludes lifecycle email.', * last_reviewed: '2026-02-15', * } */ interface ThreatModelProperties { /** * Threat-modelling methodology: `stride`, `dread`, `pasta`, or `attack_tree`. * * @example "stride" is the most widely used methodology for web applications * * @remarks * `stride` is the OWASP standard (Spoofing, Tampering, Repudiation, * Information Disclosure, Denial of Service, Elevation of Privilege). * `dread` is numeric scoring (Damage, Reproducibility, Exploitability, * Affected users, Discoverability). `pasta` is the seven-stage Process for * Attack Simulation and Threat Analysis. `attack_tree` is a hierarchical tree * of attack paths. */ methodology?: 'stride' | 'dread' | 'pasta' | 'attack_tree' | 'other'; /** * What system, feature, or data flow is being analysed. * @example "user authentication", "payment processing", "admin API" */ scope?: string; /** * ISO date last reviewed. Models become stale as systems evolve. * @example "2026-03-01" */ last_reviewed?: string; /** * Participants in the exercise. Promote individuals to `node_owned_by_person` edges if participation must be queryable. * @example "Alice Chen (security lead), Bob Park (backend engineer), Carol Liu (architect)" */ participants?: string; /** * Threats identified. Key metric for scope and completeness. * @example 12 */ threat_count?: number; } /** Threat. * * @example * const properties: ThreatProperties = { * category: 'activation', * likelihood: 4, * impact: 4, * } */ interface ThreatProperties { /** * Attack or threat scenario. * @example "injection", "misconfiguration", "social engineering", "supply chain" */ category?: string; /** * Likelihood (1 = theoretical, 5 = actively exploited). * @example value 5 for a known, exploitable, commonly targeted pattern */ likelihood?: UPGAssessment; /** * Impact (1 = minimal, 5 = catastrophic). * @example value 5 for threats that expose PII or cause complete service compromise */ impact?: UPGAssessment; /** * STRIDE classification. Which security property is violated. * `spoofing` = identity. `tampering` = integrity. `repudiation` = non-repudiability. * `info_disclosure` = confidentiality. `denial_of_service` = availability. * `elevation_of_privilege` = authorisation. */ stride_type?: 'spoofing' | 'tampering' | 'repudiation' | 'info_disclosure' | 'denial_of_service' | 'elevation_of_privilege'; /** * Threat actor or source. * @example "external attacker", "malicious insider", "compromised dependency", "misconfigured service" */ threat_agent?: string; /** * Mitigation status. * @example "accepted" = the risk has been formally acknowledged and no action will be taken */ mitigation_status?: 'open' | 'mitigated' | 'accepted' | 'transferred' | 'eliminated'; /** * Violated security property. Maps STRIDE to the CIA+ model. * @example "confidentiality" for information disclosure threats */ violated_property?: 'authentication' | 'integrity' | 'non_repudiation' | 'confidentiality' | 'availability' | 'authorisation'; } /** Vulnerability. * * @example * const properties: VulnerabilityProperties = { * cve_id: 'CVE-2026-12345', * cvss_score: 42, * severity: 'critical', * } */ interface VulnerabilityProperties { /** * CVE identifier from the National Vulnerability Database. * @example "CVE-2024-1234" */ cve_id?: string; /** * CVSS numeric score (0.0–10.0). Computed from the CVSS vector. Distinct from the categorical `severity`. * @example 9.8 (critical), 6.5 (medium), 3.7 (low) */ cvss_score?: number; /** * Categorical severity derived from CVSS, as a UPGAssessment on the * `severity_5` scale. Score alone is insufficient for triage: severity is * what drives filtering and prioritisation. * * @example value 5, label 'Critical', scale_id 'severity_5' (a remotely exploitable, no-auth vuln) * * @remarks * Migrated from the inline `critical|high|medium|low|informational` enum * (UPG-579 Option C): map `critical` to 5, `high` to 4, `medium` to 3, `low` * to 2, `informational` to 1, carrying the old word in `label` so the * original vocabulary survives the move. */ severity?: UPGAssessment; /** * CVSS scoring version. v3.1 and v4.0 differ significantly for the same vulnerability. * @example "v4.0" for vulnerabilities scored after the v4.0 release in 2023 */ cvss_version?: 'v3.1' | 'v4.0'; /** * Affected component, library, or system. * @example "lodash", "openssl", "login service" */ affected_component?: string; /** * Exploit maturity. Primary prioritisation factor after severity. * `no_known_exploit` = theoretical. `proof_of_concept` = exploit code exists, not weaponised. * `functional_exploit` = working exploit available. `active_exploitation` = active in the wild. * @example "active_exploitation" demands immediate response regardless of severity score */ exploit_maturity?: 'no_known_exploit' | 'proof_of_concept' | 'functional_exploit' | 'active_exploitation'; /** * Patch availability. Common triage question after severity. * @example false for a zero-day with no available patch */ fix_available?: boolean; /** * ISO date publicly disclosed. Time since disclosure matters for SLA. * @example "2024-03-15" */ disclosed_at?: ISODateTime; /** * ISO date discovered in this system. * @example "2026-04-01" */ discovered_at?: ISODateTime; /** * ISO date resolved or accepted. Closes the remediation timeline. * @example "2026-04-10" */ remediated_at?: ISODateTime; } /** Security control. * * @example * const properties: SecurityControlProperties = { * control_type: 'preventive', * effectiveness: 4, * control_family: 'access-control', * } */ interface SecurityControlProperties { /** * Functional role. * `preventive` = stops attacks (MFA, input validation). * `detective` = identifies attacks in progress (intrusion detection, audit logs). * `corrective` = reduces impact after an attack (incident response, backup restore). * `compensating` = alternative when primary isn't feasible. */ control_type?: 'preventive' | 'detective' | 'corrective' | 'compensating'; /** Mitigation effectiveness (1 = minimal, 5 = fully effective) */ effectiveness?: UPGAssessment; /** Control family (e.g. "access control", "network security") */ control_family?: string; /** Framework reference (e.g. "CC6.1", "A.9.2.3") */ framework_ref?: string; /** ISO date last tested */ last_tested?: string; } /** Security policy. * * @example * const properties: SecurityPolicyProperties = { * scope: 'Covers in-product onboarding; excludes lifecycle email.', * review_cadence: 'quarterly', * version: '0.3.1', * } */ interface SecurityPolicyProperties { /** Systems or processes covered */ scope?: string; /** Review cadence (e.g. `yearly`, `quarterly`). Uses the shared `Cadence` scale. */ review_cadence?: Cadence; /** Version */ version?: string; /** ISO effective date */ effective_date?: ISODate; /** Policy document URL */ url?: string; /** Lifecycle status */ policy_status?: 'draft' | 'active' | 'under_review' | 'retired'; /** Imperative force */ rule_strength?: RuleStrength; /** Owning person or team accountable for the policy. Promote to a `node_owned_by_team` edge if ownership must be queryable. */ owner?: string; } /** Penetration test. * * @example * const properties: PenetrationTestProperties = { * category: 'external', * scope: 'Covers in-product onboarding; excludes lifecycle email.', * findings_count: 42, * } */ interface PenetrationTestProperties { /** Test type */ category?: 'external' | 'internal' | 'web_app' | 'api' | 'mobile'; /** Systems or features in scope */ scope?: string; /** Total findings */ findings_count?: number; /** Critical-severity findings */ critical_count?: number; /** ISO start date */ start_date?: ISODate; /** ISO end date */ end_date?: ISODate; /** Full report URL */ report_url?: string; /** Methodology (e.g. "OWASP", "PTES") */ methodology?: string; } /** Security review. * * @example * const properties: SecurityReviewProperties = { * category: 'code', * findings: 'Three out of five participants gave up before reaching the canvas.', * review_date: '2026-04-01', * } */ interface SecurityReviewProperties { /** Reviewed artifact */ category?: 'code' | 'design' | 'architecture' | 'vendor'; /** Findings summary */ findings?: string; /** ISO review date */ review_date?: ISODate; /** Final outcome */ outcome?: 'approved' | 'approved_with_conditions' | 'rejected' | 'needs_rework'; } /** Data classification. * * @example * const properties: DataClassificationProperties = { * level: 'public', * handling_requirements: 'Encrypt at rest; audit every read.', * examples: ['Notion doc dump', 'Loom recording', 'Figma board'], * } */ interface DataClassificationProperties { /** Sensitivity level */ level?: DataSensitivity; /** Handling rules */ handling_requirements?: string; /** Example data covered */ examples?: string[]; /** Retention period */ retention_period?: string; /** Whether encryption is mandatory */ encryption_required?: boolean; } /** Access policy. * * @example * const properties: AccessPolicyProperties = { * resource: 'billing-database-primary', * principal: 'service-account:billing', * permission_level: 'read', * } */ interface AccessPolicyProperties { /** Covered resource or system */ resource?: string; /** User, role, or group granted */ principal?: string; /** Access level */ permission_level?: 'read' | 'write' | 'admin' | 'custom'; /** Conditions (e.g. "VPN only", "business hours") */ condition?: string; } /** * UPG Property Schemas: Accessibility Domain. * A11yStandard, A11yGuideline, A11yAudit, A11yIssue, A11yAnnotation. * https://unifiedproductgraph.org/spec | MIT */ /** Accessibility standard. * * @example * const properties: A11yStandardProperties = { * version: '0.3.1', * conformance_level: 'A', * } */ interface A11yStandardProperties { /** Name of the standard (e.g. "WCAG", "Section 508") */ /** Version of the standard (e.g. "2.1", "2.2") */ version?: string; /** Target conformance level */ conformance_level?: ConformanceLevel; } /** Accessibility guideline. * * @example * const properties: A11yGuidelineProperties = { * principle: 'perceivable', * guideline_number: 'DS-014', * level: 'A', * } */ interface A11yGuidelineProperties { /** WCAG principle this guideline falls under */ principle?: 'perceivable' | 'operable' | 'understandable' | 'robust'; /** Guideline reference number (e.g. "1.1", "2.4") */ guideline_number?: string; /** WCAG conformance level required */ level?: ConformanceLevel; /** Imperative force of this guideline. */ rule_strength?: RuleStrength; } /** Accessibility audit. * * @example * const properties: A11yAuditProperties = { * method: 'automated', * scope: 'Covers in-product onboarding; excludes lifecycle email.', * conformance_result: 'pass', * } */ interface A11yAuditProperties { /** How the audit was conducted */ method?: 'automated' | 'manual' | 'assistive_tech' | 'expert'; /** What was audited (e.g. "homepage", "checkout flow") */ scope?: string; /** Overall conformance result */ conformance_result?: 'pass' | 'partial' | 'fail'; /** Number of accessibility violations found */ violations_count?: number; /** Number of checks that passed */ passes_count?: number; /** Number of checks that could not be completed */ incomplete_count?: number; /** Aggregate accessibility score (0-100) */ score?: number; /** Tool used to perform the audit */ tool?: 'axe-core' | 'lighthouse' | 'wave' | 'manual' | 'other'; /** Version of the audit tool */ tool_version?: string; /** URL of the page tested */ url_tested?: string; /** Date the audit was conducted (ISO format) */ audit_date?: ISODate; } /** Accessibility issue. * * @example * const properties: A11yIssueProperties = { * severity: 'minor', * wcag_criterion: '1.4.3: Contrast (Minimum)', * rule_id: 'dqr_nodes_no_null_title', * } */ interface A11yIssueProperties { /** * Impact severity (UPGAssessment on the `severity_5` scale). Migrated from * the inline axe-core 4-level enum (`minor|moderate|serious|critical`) * (UPG-579 Option C): map `critical` -> 5, `serious` -> 4, `moderate` -> 3, * `minor` -> 2; carry the old word in `label`. */ severity?: UPGAssessment; /** WCAG success criterion violated (e.g. "1.1.1", "2.4.7") */ wcag_criterion?: string; /** Identifier of the rule that flagged the issue */ rule_id?: string; /** URL to documentation explaining the issue and how to fix it */ help_url?: string; /** Description of the affected UI element */ affected_element?: string; /** CSS selector targeting the affected element */ css_selector?: string; /** HTML snippet containing the violation */ html_snippet?: string; /** Tags from the audit tool (e.g. "wcag2aa", "cat.color") */ tags?: string[]; /** Recommended fix for the issue */ remediation?: string; /** Description of the user impact */ impact_description?: string; } /** Accessibility annotation. * * @example * const properties: A11yAnnotationProperties = { * category: 'focus_order', * target_component: 'OnboardingChecklist', * requirement: 'All customer data must be encrypted at rest and in transit.', * } */ interface A11yAnnotationProperties { /** * Category of accessibility annotation. * Expanded from the original 5-value enum to cover the full range of a11y annotation types. */ category?: 'focus_order' | 'alt_text' | 'heading_level' | 'aria_label' | 'colour_contrast' | 'keyboard_nav' | 'screen_reader' | 'motion_preference' | 'other'; /** Specific accessibility requirement described */ requirement?: string; /** WCAG success criterion this annotation relates to */ wcag_criterion?: string; /** Priority for implementing this annotation */ annotation_priority?: Priority; /** Whether the annotation has been implemented */ applied?: boolean; } /** * UPG Property Schemas: Customer Feedback Domain. * FeedbackProgram, FeatureRequest, FeedbackVote, NpsCampaign, * UserAdvisoryBoard, BetaProgram, FeedbackTheme. * https://unifiedproductgraph.org/spec | MIT */ /** Feedback program. * * @example * const properties: FeedbackProgramProperties = { * program_type: 'continuous', * collection_method: 'Segment events + Stripe webhooks', * } */ interface FeedbackProgramProperties { /** How frequently feedback is collected */ program_type?: 'continuous' | 'periodic' | 'event_triggered'; /** Method used to collect feedback (e.g. "survey", "interview", "widget") */ collection_method?: string; } /** Feature request raised by customer, prospect, or internal stakeholder. * * @example * const properties: FeatureRequestProperties = { * request_source: 'customer', * vote_count: 42, * signal_sentiment: 'positive', * } */ interface FeatureRequestProperties { /** Where the request originated */ request_source?: 'customer' | 'internal' | 'prospect' | 'support' | 'community'; /** Number of votes or upvotes from users */ vote_count?: number; /** Detected sentiment of the request */ signal_sentiment?: SignalSentiment; /** Channel through which the request was received */ signal_channel?: string; /** Perceived urgency of the request */ signal_urgency?: 'low' | 'medium' | 'high' | 'critical'; /** Estimated revenue impact if implemented (1-5) */ revenue_impact?: UPGAssessment; /** Estimated implementation effort (1-5) */ effort_estimate?: UPGAssessment; /** Computed impact score combining reach, revenue, and effort */ impact_score?: number; /** Estimated delivery date (ISO format) */ eta?: string; } /** Feedback vote. * * @example * const properties: FeedbackVoteProperties = { * vote_count: 42, * mrr_impact: 42, * } */ interface FeedbackVoteProperties { /** Total number of votes cast */ vote_count?: number; /** Combined MRR of voting accounts */ mrr_impact?: number; } /** NPS (Net Promoter Score) measurement campaign. * * @example * const properties: NpsCampaignProperties = { * campaign_type: 'relationship', * send_date: '2026-04-01', * response_count: 42, * } */ interface NpsCampaignProperties { /** What triggers the NPS survey */ campaign_type?: 'relationship' | 'transactional' | 'feature'; /** Date the survey was sent (ISO format) */ send_date?: ISODate; /** Number of responses received */ response_count?: number; /** Percentage of recipients who responded */ response_rate?: number; /** Net Promoter Score (-100 to 100) */ score?: number; /** Percentage of respondents who are promoters (9-10) */ promoters_pct?: number; /** Percentage of respondents who are detractors (0-6) */ detractors_pct?: number; } /** User advisory board. * * @example * const properties: UserAdvisoryBoardProperties = { * member_count: 42, * meeting_cadence: 'bi-weekly', * board_focus: 'Weekly discovery prioritisation', * } */ interface UserAdvisoryBoardProperties { /** Number of members on the board */ member_count?: number; /** How often the board meets. Uses the shared `Cadence` scale. */ meeting_cadence?: Cadence; /** Primary topic or area the board advises on */ board_focus?: string; } /** Beta program. * * @example * const properties: BetaProgramProperties = { * beta_type: 'closed', * participant_count: 42, * } */ interface BetaProgramProperties { /** Access model for the beta */ beta_type?: 'closed' | 'open' | 'invite_only'; /** Number of users participating in the beta */ participant_count?: number; } /** Recurring theme identified across feedback signals. * * @example * const properties: FeedbackThemeProperties = { * sentiment: 'positive', * actionable: true, * frequency: 'monthly', * } */ interface FeedbackThemeProperties { /** Display name of the theme */ /** Overall sentiment across mentions */ sentiment?: SignalSentiment; /** Whether this theme can be acted upon */ actionable?: boolean; /** How often the theme is mentioned (1-5) */ frequency?: UPGAssessment; /** ISO date when the theme was first identified */ first_seen_date?: ISODate; /** ISO date of the most recent mention */ last_seen_date?: ISODate; /** Whether mentions are increasing or decreasing */ trend_direction?: 'growing' | 'stable' | 'declining'; } /** * UPG Property Schemas: Data & Analytics Domain. * DataSource, EventSchema, Dashboard, DataModel, DataQualityRule, * DataProduct, DataPipeline, DataLineage, GlossaryTerm, DataDomain, Report. * https://unifiedproductgraph.org/spec | MIT */ /** DataSource entity. * * @example * const properties: DataSourceProperties = { * source_type: 'database', * connection_status: 'connected', * refresh_cadence: 'hourly', * } */ interface DataSourceProperties { /** Kind of data source */ source_type?: 'database' | 'api' | 'event_stream' | 'warehouse'; /** Current connection health */ connection_status?: 'connected' | 'disconnected' | 'error'; /** How often the data is refreshed. Uses the shared `Cadence` scale. */ refresh_cadence?: Cadence; } /** EventSchema entity. * * @example * const properties: EventSchemaProperties = { * event_name: 'node.created', * properties: ['title', 'description', 'status'], * trigger_description: 'Fires when a node transitions to the committed state.', * } */ interface EventSchemaProperties { /** Name of the analytics or tracking event */ event_name: string; /** Property names included in the event payload */ properties?: string[]; /** Description of what triggers this event */ trigger_description?: string; } /** Dashboard entity. * * @example * const properties: DashboardProperties = { * tool: 'looker', * url: 'https://example.com/doc', * audience: 'product-managers', * } */ interface DashboardProperties { /** Analytics tool hosting this dashboard */ tool?: 'looker' | 'amplitude' | 'mixpanel' | 'posthog' | 'omni' | 'custom'; /** URL to the live dashboard */ url?: string; /** Intended audience for this dashboard */ audience?: string; /** Number of widgets or panels on the dashboard */ element_count?: number; /** How often the dashboard data refreshes. Uses the shared `Cadence` scale. */ refresh_cadence?: Cadence; /** Number of user-configurable filters */ filter_count?: number; } /** DataModel entity. * * @example * const properties: DataModelProperties = { * schema_name: 'public', * database_name: 'entopo_graph', * table_count: 42, * } */ interface DataModelProperties { /** Name of the schema this model belongs to */ schema_name?: string; /** Name of the database containing this model */ database_name?: string; /** Number of tables in the model */ table_count?: number; /** Total number of columns across all tables */ column_count?: number; /** Number of data tests defined for this model */ test_count?: number; /** Database paradigm used */ model_type?: 'relational' | 'document' | 'graph' | 'time_series'; /** How the model is materialised in the warehouse */ materialization?: 'view' | 'table' | 'incremental' | 'ephemeral' | 'materialized_view'; /** Arbitrary metadata key-value pairs */ meta?: Record; /** Free-form categorisation tags */ tags?: string[]; } /** DataQualityRule entity. * * @example * const properties: DataQualityRuleProperties = { * rule_type: 'completeness', * test_type: 'unique', * column_ref: 'public.nodes.id', * } */ interface DataQualityRuleProperties { /** Quality dimension this rule validates */ rule_type?: 'completeness' | 'accuracy' | 'freshness' | 'uniqueness' | 'consistency'; /** Specific test implementation */ test_type?: 'unique' | 'not_null' | 'accepted_values' | 'relationships' | 'custom'; /** Column or field this rule applies to */ column_ref?: string; /** Acceptable threshold value for the rule */ threshold?: string; /** Whether to send an alert when the rule is breached */ alert_on_breach?: boolean; /** Result of the most recent run */ last_run_status?: 'pass' | 'fail' | 'error' | 'not_run'; /** ISO date of the most recent run */ last_run_date?: ISODate; } /** DataProduct entity. * * @example * const properties: DataProductProperties = { * data_product_type: 'data product type', * sla_freshness: 'under 5 minutes', * consumers: ['analytics-service', 'search-indexer'], * } */ interface DataProductProperties { /** Classification of the data product (UPG-579 Option B). */ data_product_type?: 'report' | 'dataset' | 'stream' | 'api' | 'ml_feature' | 'other'; /** Freshness SLA commitment (e.g. "< 1 hour") */ sla_freshness?: string; /** Owning person or team. Sibling of `data_domain.steward`: use `owner` for the accountable party of a single data product, `steward` for a whole domain. Promote to a `node_owned_by_team` edge if ownership must be queryable. */ owner?: string; } /** DataPipeline entity. * * @example * const properties: DataPipelineProperties = { * schedule: '0 2 * * *', * avg_runtime: '3m 20s', * } */ interface DataPipelineProperties { /** Cron or scheduling expression */ schedule?: string; /** Average wall-clock runtime per execution */ avg_runtime?: string; /** Orchestration tool (e.g. "Airflow", "Dagster", "dbt Cloud") */ orchestrator?: string; /** Number of automatic retries on failure */ retry_count?: number; /** Delay between retries in seconds */ retry_delay_seconds?: number; /** Maximum allowed runtime in seconds before timeout */ timeout_seconds?: number; /** Rule that determines when this pipeline triggers */ trigger_rule?: string; /** Resource pool this pipeline runs in */ pool?: string; } /** DataLineage entity. * * @example * const properties: DataLineageProperties = { * transformation: 'lowercase + trim', * } */ interface DataLineageProperties { /** Description of how the data is transformed */ transformation?: string; } /** GlossaryTerm entity. * * @example * const properties: GlossaryTermProperties = { * term_definition: 'A measurable result a team commits to deliver within a time-boxed period.', * synonyms: ['OKR', 'objective and key result'], * } */ interface GlossaryTermProperties { /** Plain-language definition of the term */ term_definition?: string; /** Alternative names or abbreviations for this term */ synonyms?: string[]; /** Person or team accountable for keeping the definition current. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; } /** DataDomain. A coarse-grained grouping of related data assets (sources, * pipelines, products, glossary terms) under a single stewardship boundary. * Structural relationships go via hierarchy; only stewardship + classification live here. * * @example * const properties: DataDomainProperties = { * steward: 'steward', * domain_type: 'master', * sensitivity: 'public', * } */ interface DataDomainProperties { /** Accountable person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ steward?: string; /** * Domain contents classification. * `master` = canonical entity data (customers, products). * `operational` = live transactional data. * `analytical` = warehouse / BI data. * `reference` = slow-changing lookup data. */ domain_type?: 'master' | 'operational' | 'analytical' | 'reference'; /** Sensitivity band applied across the domain */ sensitivity?: DataSensitivity; } /** Report entity. * * @example * const properties: ReportProperties = { * report_type: 'weekly-exec', * schedule: '0 2 * * *', * recipients: ['exec-team', 'product-leads'], * } */ interface ReportProperties { /** Classification of the report (e.g. "weekly metrics", "ad hoc") */ report_type?: string; /** How often the report is generated */ schedule?: string; /** Tool used to generate the report */ tool?: string; } /** * UPG Property Schemas: Content & Knowledge Domain. * ContentPiece, KnowledgeBaseArticle, BrandAsset, InternalDoc, * ContentCalendar, ContentTheme, DocumentationTemplate, Document. * (`PromptTemplate` moved to the AI domain file — UPG-665.) * https://unifiedproductgraph.org/spec | MIT */ /** Content piece. * * @example * const properties: ContentPieceProperties = { * content_type: 'blog', * url: 'https://example.com/doc', * } */ interface ContentPieceProperties { /** Format of the content */ content_type?: 'blog' | 'video' | 'podcast' | 'whitepaper' | 'case_study' | 'other'; /** URL where the content is published */ url?: string; } /** Knowledge base article. * * @example * const properties: KnowledgeBaseArticleProperties = { * audience: 'customer', * url: 'https://example.com/doc', * } */ interface KnowledgeBaseArticleProperties { /** Who this article is intended for */ audience?: 'customer' | 'internal' | 'developer' | 'admin'; /** URL of the published article */ url?: string; } /** Brand asset. * * @example * const properties: BrandAssetProperties = { * asset_type: 'logo', * url: 'https://example.com/doc', * usage_rights: 'Internal use only. Do not redistribute without written approval.', * } */ interface BrandAssetProperties { /** Category of the brand asset */ asset_type?: 'logo' | 'icon' | 'illustration' | 'photo' | 'video' | 'template'; /** URL or path to the asset file */ url?: string; /** Usage rights or licensing restrictions */ usage_rights?: string; } /** Internal document. * * @deprecated since 0.2.0. Use `DocumentProperties` (`document` with an internal * audience). `internal_doc` is absent from the entity-catalog union, so no * conformant graph can hold a node of this type — these properties are * unreachable in practice. Migration is lossless: `doc_type` → `document_type` * (its enum already contains rfc/runbook/guide/spec/onboarding), `url` → * `source_url`. Retained only because this interface is part of the published * type surface; RETIREMENT SCHEDULED for the next MAJOR (docket wave 2, item 3). * * @example * const properties: InternalDocProperties = { * doc_type: 'rfc', * url: 'https://example.com/doc', * } */ interface InternalDocProperties { /** Classification of the document. @deprecated use `document.document_type` */ doc_type?: 'rfc' | 'runbook' | 'guide' | 'spec' | 'onboarding' | 'other'; /** URL or path to the document. @deprecated use `document.source_url` */ url?: string; } /** Document. Provenance container linking to source files. * * Container for any narrative artefact (spec, RFC, brief, report, runbook, * case-study, session note) that the rest of the graph cites or describes. * Lifecycle (draft → review → published → archived) is governed by the * canonical `PUBLISHING_TEMPLATE`. * * Per UPG principle P14, "what this document is about" is expressed as edges: * parent product: `product_documented_in_document` * subject coverage: `document_describes_*` (feature, vision, persona, * competitor, strategic_pillar, market_segment, revenue_stream, * positioning, decision, …) * contained insights: `document_contains_insight` * scheduling: `content_calendar_schedules_document` * * @example * const properties: DocumentProperties = { * path: '/docs/spec/v0.2/upg-core.md', * source_url: 'https://github.com/unified-product-graph/core/blob/main/spec/v0.2/upg-core.md', * platform: 'markdown', * document_type: 'spec', * last_updated: '2026-04-28T13:27:08Z', * word_count: 2480, * content_summary: 'Defines the v0.2 entity catalogue, edge catalogue, and migration adapters.', * } */ interface DocumentProperties { /** File path or workspace-relative location. Use `source_url` for off-platform links. */ path?: string; /** Canonical retrievable URL */ source_url?: string; /** Hosting platform */ platform?: 'markdown' | 'notion' | 'figma' | 'google_docs' | 'confluence' | 'github' | 'linear' | 'other'; /** Purpose classification */ document_type?: 'vision' | 'plan' | 'decision' | 'research' | 'spec' | 'audit' | 'session' | 'feedback' | 'case-study' | 'narrative' | 'bug-report' | 'archive-collection' | 'rfc' | 'runbook' | 'guide' | 'onboarding' | 'brief' | 'report' | 'reference'; /** ISO 8601 last-meaningful-update */ last_updated?: ISODateTime; /** Approximate word count. Useful for planning, indexing, summarisation. */ word_count?: number; /** 1–3 sentence summary. Drives previews, search snippets, embedding context. */ content_summary?: string; /** Primary language (BCP 47 tag, e.g. "en", "en-GB", "fr") */ language?: string; } /** Content calendar. * * @example * const properties: ContentCalendarProperties = { * calendar_period: '2026-Q2', * publish_cadence: 'weekly', * } */ interface ContentCalendarProperties { /** Covered period (e.g. "Q2 2026") */ calendar_period?: string; /** * Publishing cadence (canonical `Cadence` since v0.4.0). * Retyped from the legacy free-form `publish_cadence: string`. For exact * rates ("3 per week"), set `frequency_count` + `frequency_period`. * * BREAKING in v0.4.0: previous string values like `"3x/week"` no longer * type-check. Map to `'weekly'` + `frequency_count: 3` + `frequency_period: 'P7D'`. */ publish_cadence?: Cadence; /** Exact count in the period. Pairs with `frequency_period`. */ frequency_count?: number; /** Recurrence period (ISO-8601 `Duration`, e.g. `'P7D'`) */ frequency_period?: Duration; /** Qualitative rate tier when an exact rate is unknown */ frequency_rating?: FrequencyRating; } /** Content theme. * * @example * const properties: ContentThemeProperties = { * theme_category: 'activation', * } */ interface ContentThemeProperties { /** Category or topic area of the theme */ theme_category?: string; } /** Documentation template. * * @example * const properties: DocumentationTemplateProperties = { * template_type: 'weekly-review', * sections: ['Summary', 'Evidence', 'Recommendation'], * version: '0.3.1', * } */ interface DocumentationTemplateProperties { /** Kind of document this template produces */ template_type?: string; /** Sections or outline of the template */ sections?: string[]; /** Version identifier of the template */ version?: string; } /** * UPG Property Schemas: Legal Domain. * LegalEntity, IpAsset, Contract, ContractClause, PrivacyPolicy. * https://unifiedproductgraph.org/spec | MIT */ /** Legal entity. * * @example * const properties: LegalEntityProperties = { * entity_type: 'corporation', * jurisdiction: 'Germany (DE)', * ip_ownership: 'Assigned to Arkheiev UG under standard IP assignment clause.', * } */ interface LegalEntityProperties { /** Legal structure of the entity */ entity_type?: 'corporation' | 'llc' | 'partnership' | 'sole_proprietor' | 'nonprofit'; /** Jurisdiction where the entity is registered */ jurisdiction?: string; /** Description of intellectual property ownership */ ip_ownership?: string; /** Date the entity was incorporated (ISO format) */ date_incorporated?: string; } /** Intellectual property asset. * * @example * const properties: IpAssetProperties = { * asset_type: 'patent', * jurisdiction: 'Germany (DE)', * filing_date: '2026-04-01', * } */ interface IpAssetProperties { /** Category of intellectual property */ asset_type?: 'patent' | 'trademark' | 'copyright' | 'trade_secret' | 'design' | 'domain_name'; /** Jurisdiction where the IP is protected */ jurisdiction?: string; /** Date the application was filed (ISO format) */ filing_date?: ISODate; /** Official registration or patent number */ registration_number?: string; /** Date the protection expires (ISO format) */ expiry_date?: ISODate; /** Priority date for patent claims (ISO format) */ priority_date?: ISODate; } /** Contract. * * @example * const properties: ContractProperties = { * contract_type: 'service', * contract_status: 'draft', * start_date: '2026-04-01', * } */ interface ContractProperties { /** Classification of the contract */ contract_type?: 'service' | 'employment' | 'nda' | 'license' | 'partnership' | 'other'; /** Contract effective start date (ISO format) */ start_date?: ISODate; /** Contract end or expiration date (ISO format) */ end_date?: ISODate; /** Total monetary value of the contract */ value?: number; /** Currency code for the contract value */ currency?: string; /** How the contract renews at expiration */ auto_renewal?: 'auto_renew' | 'optional_extension' | 'none'; /** Duration of each renewal period */ renewal_term?: string; /** Notice period required for termination or non-renewal */ notice_period?: string; /** Jurisdiction whose laws govern the contract */ governing_law?: string; } /** Contract clause. * * @example * const properties: ContractClauseProperties = { * clause_type: 'termination', * clause_category: 'protective', * clause_text: 'Either party may terminate with 30 days written notice if the other materially breaches this agreement.', * } */ interface ContractClauseProperties { /** * Specific clause. Closed set covering canonical commercial clause families. * Pairs with `clause_category` (`'protective' | 'operational' | 'financial' | * 'boilerplate'`), the functional grouping. `clause_type` is the named clause. */ clause_type?: 'indemnity' | 'liability_cap' | 'termination' | 'confidentiality' | 'ip_assignment' | 'non_compete' | 'warranty' | 'governing_law' | 'other'; /** Functional category of the clause */ clause_category?: 'protective' | 'operational' | 'financial' | 'boilerplate'; /** Full text of the clause */ clause_text?: string; /** Whether this clause is open to negotiation */ is_negotiable?: boolean; /** Risk level if the clause is accepted as-is (1 = negligible, 5 = severe exposure) */ risk_level?: UPGAssessment; } /** Privacy policy. * * @example * const properties: PrivacyPolicyProperties = { * version: '0.3.1', * last_updated: '2026-02-15', * effective_date: '2026-04-01', * } */ interface PrivacyPolicyProperties { /** Version identifier of the policy */ version?: string; /** Date the policy was last updated (ISO format) */ last_updated?: string; /** Date the policy takes effect (ISO format) */ effective_date?: ISODate; /** URL where the policy is published */ url?: string; } /** * UPG Property Schemas: Compliance Domain. * ComplianceRequirement, Risk, DataContract, AuditLogPolicy, * ComplianceFramework, SecurityAudit. * https://unifiedproductgraph.org/spec | MIT */ /** Compliance requirement. * * @example * const properties: ComplianceRequirementProperties = { * regulation: 'gdpr', * compliance_status: 'compliant', * } */ interface ComplianceRequirementProperties { /** Regulation or standard this requirement derives from */ regulation?: 'gdpr' | 'ccpa' | 'hipaa' | 'soc2' | 'iso27001' | 'pci_dss' | 'other'; /** Current compliance posture */ compliance_status?: 'compliant' | 'non_compliant' | 'in_progress' | 'not_applicable'; /** Accountable person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; } /** Risk. * * @example * const properties: RiskProperties = { * risk_type: 'technical', * likelihood: 4, * impact: 4, * } */ interface RiskProperties { /** * Domain the risk belongs to. The single kind axis for `risk`: there is no * second classification vocabulary. * * `program` added in v0.26.0 so the retired `risk_item` (Program Management) * type has a home on the canonical `risk` after consolidation, rather than a * parallel `risk_domain` field free to drift from this one. */ risk_type?: 'technical' | 'business' | 'legal' | 'security' | 'operational' | 'program'; /** * How likely this risk is to materialise. Rated on `likelihood_5` * (Rare → Almost certain). * @remarks * Canonical name since 0.35.0, superseding `probability` below. Three * reasons, in order of weight. (1) `probability` was one name for two * incompatible types (`UPGAssessment` here, a bare `number` on * `forecast.probability`), and `PROPERTY_SCALE_MAP` is keyed by name alone, * so a sales percentage and a risk judgment resolved to the same ladder. * (2) `likelihood` is already the spec's own word: the RISK_ITEM lifecycle * prose says "Likelihood and impact have been evaluated", and * `threat.likelihood` has been a `UPGAssessment` all along: one name, one * type, one ladder. (3) ISO 31000 says likelihood. * * The ladder moved with the name: `likelihood_5` is new in 0.35.0 because no * probability ladder existed and `confidence_5` is epistemic. It says how * sure the assessor is, not how likely the event is. */ likelihood?: UPGAssessment; /** * How likely this risk is to materialise (1 = unlikely, 5 = near certain). * @deprecated since="0.35.0" removeIn="1.0.0". Use `likelihood`, which is the * spec's own word for this in the risk lifecycle and on `threat`. * * Both names resolve to the same `likelihood_5` ladder for the length of the * deprecation window (`PROPERTY_SCALE_MAP_BY_ENTITY.risk.probability`, * Captain-ratified 2026-08-22): a deprecated field that renders on a * DIFFERENT ladder from its replacement would make one stored 4 read * "Confident" under the old name and "Likely" under the new one, which is the * data changing meaning at the rename, which is the exact thing staging * exists to prevent. `forecast.probability` is untouched and stays on `confidence_5`. * * STAGED, not renamed. The field is KEPT and still read: 0.35.0 changes what * writers emit and what readers prefer, and changes NO stored bytes. A graph * written before 0.35.0 carries `probability` and no `likelihood`, and reads * correctly, which is the whole point of staging it. * * Writers: emit `likelihood`. Readers: prefer `likelihood`, fall back to * `probability`. The fallback is a CONTRACT on consumers, not executable * spec machinery, exactly as it is for `epic.estimate` → `effort`. * * `removeIn="1.0.0"` is a deadline, not a wish: at 1.0.0 this field is * dropped by a `drop_props` rule in `UPG_PROPERTY_MIGRATIONS`, the same * two-step the `removeIn="0.5.0"` properties followed (declared at 0.4.0, * dropped by the 0.5.0 rules). Until then there is deliberately no * executable rule: see the `'0.35.0'` block in `grammar/migrations.ts`. */ probability?: UPGAssessment; /** * Severity of consequences if the risk materialises. Rated on `severity_5` * (Mild inconvenience → Blocker), NOT the benefit-framed `impact_5`. * @remarks * The ladder is set by `PROPERTY_SCALE_MAP_BY_ENTITY.risk.impact` (0.35.0), * the per-entity override layer. `impact` legitimately means magnitude of * BENEFIT on discovery and market entities, where high is good; on a risk it * means severity of harm, where high is bad. Sharing `impact_5` rendered a * catastrophic risk green. */ impact?: UPGAssessment; /** * Planned or implemented mitigation strategy, as prose. * @remarks * Prose only. A structured list of mitigating ACTIONS is a set of edges: * `risk_mitigated_by_node` (0.35.0), pointing at the decisions, features and * experiments that actually do the mitigating; a string array of them is * unqueryable by construction. Likewise, what the risk puts at stake is * `risk_threatens_node`, not a scope-list property. */ mitigation?: string; } /** Data contract. * * @example * const properties: DataContractProperties = { * retention_period: '365 days', * deletion_policy: 'retain-90-days', * third_party_sharing: true, * } */ interface DataContractProperties { /** How long data is retained before deletion */ retention_period?: string; /** Policy governing data deletion */ deletion_policy?: string; /** Whether data is shared with third parties */ third_party_sharing?: boolean; /** Owning person or team accountable for the contract. Promote to a `node_owned_by_team` edge if ownership must be queryable. */ owner?: string; } /** Audit log policy. * * @example * const properties: AuditLogPolicyProperties = { * scope: 'Covers in-product onboarding; excludes lifecycle email.', * retention_days: 42, * } */ interface AuditLogPolicyProperties { /** What systems or actions are covered by the audit log */ scope?: string; /** Number of days audit logs are retained */ retention_days?: number; /** Types of events being logged */ event_types?: string[]; } /** Compliance framework. * * @example * const properties: ComplianceFrameworkProperties = { * audit_date: '2026-04-01', * next_audit: '2026-10-15', * } */ interface ComplianceFrameworkProperties { /** Name of the framework (e.g. "SOC 2 Type II", "ISO 27001") */ framework_name?: string; /** Date of the last audit (ISO format) */ audit_date?: ISODate; /** Date of the next scheduled audit (ISO format) */ next_audit?: string; } /** Security audit. * * @example * const properties: SecurityAuditProperties = { * audit_scope: 'All customer-facing APIs and the billing service.', * findings_count: 42, * } */ interface SecurityAuditProperties { /** Systems or processes covered by the audit */ audit_scope?: string; /** Total number of findings */ findings_count?: number; /** Number of critical-severity findings */ critical_findings?: number; } /** * UPG Property Schemas: Team & Organisation Domain. * Team, Role, Stakeholder, TeamOkr, Retrospective, Dependency, * Department, Skill, Ceremony, CapacityPlan. * https://unifiedproductgraph.org/spec | MIT */ /** Team entity. * * @example * const properties: TeamProperties = { * team_type: 'product', * size: 120, * mission: 'Give every product team the tools to think clearly and ship decisions that compound.', * } */ interface TeamProperties { /** Functional area of the team */ team_type?: 'product' | 'engineering' | 'design' | 'growth' | 'customer_success'; /** Number of people on the team */ size?: number; /** Team's mission statement */ mission?: string; /** * Prefix this team mints node keys with (e.g. `"ENTP"`, giving `ENTP-1`, * `ENTP-2`, ...). * * @remarks * SUPERSEDES `product.key_prefix` FOR THIS PRODUCT, AND NOTHING ELSE * (normative, narrowed in 0.34.0). The moment any team declares a prefix, the * PRODUCT-LEVEL prefix stops being consulted. A product-level prefix that still * resolved would win on order alone, and a multi-team product would then * silently mint everything under it, which is the defect this field exists to * end. * * THE CANDIDATE SET IS THE UNION OF DECLARED AND OBSERVED PREFIXES. A * declaration adds a candidate; it does not remove one. A prefix stops being * offered only when something claims it or when nothing has ever minted under * it, and while more than one candidate stands the create surface keeps asking. * * WHY THIS WAS NARROWED, stated because 0.33.0 shipped the wider reading and an * implementer built against it. The paragraph above made ANY declaration replace * the candidate set outright, and its own stated rationale is entirely about * `product.key_prefix`: a SINGLE STRING, declared once, that cannot represent * two teams. An OBSERVED prefix is not that. It is evidence of a namespace * already in active use. Suppressing it reproduces the precise defect this field * exists to end, inside one product, and does so silently. Measured on the only * keyed graph in the estate: it carries two observed prefixes, 370 keys under * one and 662 under the other, and declares neither. Under the wider reading, * one team declaring the smaller prefix collapses the candidate set to it, the * picker disappears, and every later create mints under it, including the 662 * keys' worth of work belonging to the other namespace. The sentence over-reached * beyond its own reason, and this narrows it back to that reason. * * Shipped as a CHANGE in 0.34.0 rather than as a patch correction. Read as a * correction it is defensible, but implementers had already built to the wider * text, and moving a contract under them in a patch is how a patch becomes a * surprise. * * A DECLARED PREFIX IS A CANDIDATE BEFORE IT IS OBSERVED. Candidates derived * only from keys that already exist cannot see a team's first create, which is * the one that most needs asking about. That is why declaration ADDS to the * candidate set; it is not a reason for it to subtract. * * UNIQUENESS IS PRODUCT-SCOPED, AND A TEAM PREFIX DOES NOT WIDEN IT. The key * sequence runs per product across entity types, so two teams in one product * share one number line and never collide with each other. A team prefix names * a team WITHIN that scope; it does not create a sequence that spans products. * * MINTING IS PRODUCT-SCOPED (normative, 0.33.0). `team` is `portfolio_shared`, * so one team node can be referenced from two products. Minting is not * portfolio-scoped with it. The rule, and it is a requirement rather than a * recommendation: * * 1. A portfolio-shared team's prefix is NOT a minting candidate in a second * product. The first product a team mints under is the only product that * prefix mints in. * 2. Refusal is NO-KEY. The node is created without a key. Refusal MUST NOT be * an exception: a keyless create is a legal outcome everywhere else in the * ladder, and throwing here would break creates that succeed today on every * surface. Throwing stays reserved for a broken invariant, not for policy. * 3. The rule applies on the MINT path, not the picker path, and it covers the * INFERRED case. A prefix that was never requested by anyone, and was * derived from keys that already exist in the second product, is refused on * the same terms as one a caller named. A guard that only inspects an * explicitly requested prefix misses the quiet path, and the quiet path is * the one a fixture reproduces. * * WHY THE SCOPE STOPS AT THE PRODUCT. Key uniqueness is enforced by a * `(product_id, key)` index, which permits the same key under two product ids * by construction. A prefix that minted in two products would therefore run two * independent sequences under one name and hand two different nodes the same * citation, with nothing objecting. Portfolio-shared team minting is DEFERRED * until a supra-product uniqueness design exists, rather than approximated. * * HOW RULE 1 IS DECIDED: THE EVIDENCE RULE (normative, 0.34.0). Rule 1 names a * first product without saying how a minter knows which one it is. It is decided * by EVIDENCE, not by a stored marker: at mint, if any other IN-SCOPE product * already holds a key under the prefix, refuse. Evidence is derivable from the * graphs themselves, needs no migration, cannot go stale, and cannot disagree * with the keys. A durable home marker could do all three, and would mint state * for a fact the graph already carries. The cost is stated rather than hidden: * evidence is SCOPE-DEPENDENT, which is why the scope is ruled below in the same * breath rather than left open. * * THE UNDECIDABLE CASE: NEITHER MINTS (normative, 0.34.0). When two products * already hold keys under one prefix and nothing establishes which was first, * NEITHER mints. No tiebreak is invented. Creation order is not recorded, and * `max(existing)` measures import volume rather than precedence, so any tiebreak * would be a guess wearing a rule, and a silent one, since it would attribute a * namespace to a product with nothing to say it was wrong. Refusing both is the * NO-KEY outcome rule 2 already sanctions, and it is recoverable: once either * product declares the prefix, the other is unambiguous and a backfill can run. * * WHAT "IN SCOPE" MEANS: ENGINE-DEFINED, WITH A FLOOR AND A CEILING (normative, * 0.34.0). The scope over which the evidence rule looks is defined by the * engine, bounded on both sides. * * FLOOR. It MUST include every product the engine can enumerate for this * caller. An engine that looks at fewer products than it can see is choosing * not to notice a collision it could have seen. * * CEILING. It MUST NEVER include a product the caller could not otherwise * read. A wider read is a cross-tenant information channel: refusing a mint * because of a key in a graph the caller cannot see leaks that the graph * exists and what is in it. This half is a security constraint and is not * negotiable. * * PORTFOLIO ALTITUDE IS THE WRONG NORMATIVE ALTITUDE, and it was measured rather * than argued: deleting the portfolio document changes nothing about minting, * because no minter consults the portfolio seam. Stating the invariant there * states it where nobody looks. In practice the local engine's scope is every * graph in the workspace folder and the cloud engine's is the caller's own * product list. * * THE HONEST CONSEQUENCE, which belongs in the text rather than in a later * surprise: the invariant is SCOPE-RELATIVE. Two engines can legitimately * disagree about whether one mint is safe, because they can legitimately see * different sets of products. That is a real limitation of the evidence rule and * the price of the ceiling. * * IMMUTABLE ONCE ANYTHING HAS MINTED UNDER IT, PER PRODUCT (normative, 0.34.0). * Once any key exists under this prefix IN A GIVEN PRODUCT, the declaration * cannot be edited for that product. Refusal-shaped and NO-KEY, matching rule 2: * never an exception, because a keyless create is legal everywhere else in the * ladder. * * SCOPED PER PRODUCT, NOT PER TEAM, and the reason is that `team` is * `portfolio_shared`: a team that has never minted in product B must still be * free to declare there. A per-team global lock would strand it. * * WHAT IT PREVENTS. Without it, a team edits its prefix after four hundred * mints and one product silently carries two number lines under two names, * with nothing recording that they were ever one sequence. * * RENAMING STAYS POSSIBLE BY THE HONEST ROUTE: a migration that rewrites the * existing keys. That is a deliberate act with a visible cost, which is the * difference between renaming a namespace and forking it by accident. * * THE PICKER MAY OFFER WHAT THE MINT REFUSES, AND THE SURFACE OWNS THE * DIVERGENCE (0.34.0). Rule 3 applies on the MINT path and not the picker path, * which is deliberate: the picker cannot cheaply know the answer, since knowing * it requires reading other products. The consequence is that a surface can * present a choice that then fails. The owner is named here rather than left * implicit: A CREATE SURFACE THAT OFFERS A PREFIX THE MINT MAY REFUSE MUST BE * ABLE TO REPORT THE REFUSAL, AND MUST BE ABLE TO PRESENT NO-KEY AS AN OUTCOME * RATHER THAN AS AN ERROR. This is a design obligation on the surface, not a * spec mechanic: the spec cannot fix a UX gap and should not pretend to. The * union ruling above shrinks the divergence considerably, because the picker now * keeps asking in exactly the case that would otherwise resolve wrongly. * * This rule states the CONTRACT. The behaviour belongs to whatever mints keys, * which is not this package: see `UPGBaseNode.key`. * * @example "ENTP" */ key_prefix?: string; } /** Role entity. * * @example * const properties: RoleProperties = { * responsibilities: ['Own delivery timelines', 'Run weekly reviews'], * seniority_range: 'intern', * required_skills: ['product-strategy', 'user-research', 'data-analysis'], * } */ interface RoleProperties { /** Key responsibilities of the role */ responsibilities?: string[]; /** Seniority band this role sits in */ seniority_range?: 'intern' | 'junior' | 'mid' | 'senior' | 'staff' | 'principal' | 'director' | 'executive'; /** Skills expected for the role (structural refs to Skill entities go via edges) */ required_skills?: string[]; /** Role this one reports to (name or role id) */ reporting_line?: string; } /** Person entity. A named, accountable individual. * * Distinct from `stakeholder` (an interested party: internal/external/investor/regulator) * and from `role` (a responsibility slot that may be filled by one or more people). * * Containment-free: `person` is not nested under `product` / `department` / * `team` in the structural hierarchy. People are *referenced* via * `node_owned_by_person`, not *contained*. See `UPG_CONTAINMENT_FREE_TYPES` * in `grammar/hierarchy.ts`. * * Properties are minimal: identity, addressability, coordination. HR-shaped * fields (`seniority`, `employment_type`, `start_date`) are intentionally * out of scope; they encode org-design opinions that vary wildly across * companies and have no bearing on product-graph reasoning. UPG is not * an HRIS. * * @example * const properties: PersonProperties = { * role_title: 'Product Engineer', * time_zone: 'Europe/Berlin', * } */ interface PersonProperties { /** Free-text job title. Distinct from the structured `role` entity. */ role_title?: string; /** IANA time zone (e.g. "Europe/Berlin"). Useful for capacity / on-call planning. */ time_zone?: string; } /** * Which way a stakeholder leans. Pairs with `UPG_ENUM_SCALES.EngagementPosture` * for per-value labels and descriptions. */ type EngagementPosture = 'champion' | 'supporter' | 'neutral' | 'skeptic' | 'blocker'; /** Stakeholder entity. * * @example * const properties: StakeholderProperties = { * stakeholder_type: 'internal', * influence: 4, * interest: 4, * engagement_posture: 'skeptic', * engagement_cadence: 'monthly', * } */ interface StakeholderProperties { /** Relationship of the stakeholder to the organisation */ stakeholder_type?: 'internal' | 'external' | 'investor' | 'regulator'; /** How much influence this stakeholder has over decisions (1 = minimal, 5 = decisive) */ influence?: UPGAssessment; /** How much interest this stakeholder has in the outcome (1 = passive, 5 = deeply invested) */ interest?: UPGAssessment; /** * Which way this stakeholder leans: actively for, actively against, or * neither. The third axis of the stakeholder model (0.35.0), beside the * power/interest grid `influence` and `interest` describe. * @remarks * `influence` and `interest` are magnitudes and carry no direction: a * high-influence, high-interest stakeholder can be the strongest champion or * the one who kills it, and the grid renders them identically. This closed * enum is what makes "who blocks this?" a query rather than a reading * exercise. Pairs with `UPG_ENUM_SCALES.EngagementPosture`. Posture is about * the person's stance; it is NOT delivery health, which belongs to work * items, and NOT the relationship's operational state. */ engagement_posture?: EngagementPosture; /** * How often this stakeholder is engaged. Uses the shared `Cadence` scale. * @remarks * Typed as `Cadence` rather than free text on purpose: v0.4.0 introduced that * enum precisely to retire strings like `"2x/week"`, and a cadence that * cannot be compared across stakeholders cannot answer "who have we not * spoken to this quarter?". WHERE you meet them (the channel) is an * app-level concern and is deliberately not modelled here. */ engagement_cadence?: Cadence; } /** TeamOkr entity. * * @example * const properties: TeamOkrProperties = { * period: '2026-Q2', * progress: 42, * objective_statement: 'Reach 1,000 weekly active graphs by end of Q3.', * } */ interface TeamOkrProperties { /** Time period for the OKR (e.g. "Q2 2026") */ period?: string; /** Overall progress toward the objective (0-100%) */ progress?: number; /** The team-level objective statement (key results live in child entities) */ objective_statement?: string; } /** Retrospective entity. * * @example * const properties: RetrospectiveProperties = { * format: 'start_stop_continue', * period: '2026-Q2', * key_learnings: ['Week-one retention correlates with first committed decision'], * } */ interface RetrospectiveProperties { /** * Closed-set retro format covering established retrospective patterns. * Use `'other'` for novel formats; raise a spec proposal if `'other'` recurs. */ format?: 'start_stop_continue' | 'four_ls' | 'mad_sad_glad' | 'sailboat' | 'plus_delta' | 'lean_coffee' | 'other'; /** Sprint or time period being reflected on */ period?: string; /** Key learnings from the retrospective */ key_learnings?: string[]; /** Action items agreed upon */ action_items?: string[]; } /** Dependency entity. * * @example * const properties: DependencyProperties = { * dependency_type: 'blocks', * resolution: 'Disable the retry loop on 4xx responses.', * criticality: 'high', * } */ interface DependencyProperties { /** Nature of the dependency relationship */ dependency_type?: 'blocks' | 'enables' | 'informs'; /** How the dependency was or will be resolved */ resolution?: string; /** How urgent the dependency is to resolve */ criticality?: Priority; /** Date by which resolution is needed (ISO 8601) */ target_date?: ISODate; /** Whether a workaround exists if the dependency is not resolved in time */ workaround_available?: boolean; } /** Department entity. A department is the single org tier above a team. * Division, org unit and business unit are alternative LABELS for this type, not * separate types. * * @remarks * THE LABEL RULING, STATED HERE BECAUSE ONE LINE IN A LABEL ARRAY WAS TOO QUIET. * `division`, `org unit` and `business unit` have been `alt_labels` on this type * since 0.1.0 and the ruling is right: a synonym is not a tier. It was also * findable only from the labels file, and a pre-scan for org modelling duly * reported `division` and `org_unit` as entity types. Two phantom types reached a * commissioning brief before a census caught them. * * THE BOUNDARY AGAINST `organization`, because one phrase reaching two types is * the confusion this ruling exists to end. "Business unit" names a structural * tier INSIDE a company and belongs here. Its alt_labels on `organization` are * org, company, enterprise and organisation, deliberately not this one. * * THE TEST IS INCORPORATION, AND THE NODE IS THE OPERATING COMPANY. A separately * incorporated operating company is an `organization`; a division inside one * company is a `department`. Do not read the incorporation test as "one node per * legal entity", which is a different and wrong rule: an `organization` is the * operating company as people experience it, and one organisation can span * several legal entities wherever structure follows tax or jurisdiction rather * than how the work is run. `legal_entity` is `organization`'s own declared * neighbour for exactly that reason. The test picks the boundary; it does not * pick the noun. * * DEPARTMENTS ARE FLAT, AND THAT IS THE CONSTRAINT THE RULING RAISES. * `team_contains_team` exists and `department_contains_department` does not, so a * three-level org chart is expressed as nested TEAMS under one department. That * is a deliberate constraint rather than a gap, and it is what makes a separate * division tier unnecessary rather than merely undesirable. * * @example * const properties: DepartmentProperties = { * headcount: 42, * budget: 50000, * department_mission: 'Keep the product reliable, secure, and fast.', * } */ interface DepartmentProperties { /** Total number of people in the department */ headcount?: number; /** Annual budget allocated to the department */ budget?: number; /** Charter / purpose statement for the department */ department_mission?: string; /** Department leader (person or role reference). Promote to a `node_owned_by_person` edge if ownership must be queryable. */ leader?: string; /** Fiscal year the headcount / budget numbers apply to */ fiscal_year?: string; } /** Skill entity. * * @example * const properties: SkillProperties = { * skill_category: 'research', * proficiency_levels: ['novice', 'competent', 'expert'], * domain: 'user', * } */ interface SkillProperties { /** Category of the skill (UPG-579 Option B). */ skill_category?: 'technical' | 'leadership' | 'design' | 'product' | 'business' | 'operations' | 'other'; /** Description of proficiency levels for this skill */ proficiency_levels?: string[]; /** Problem domain the skill applies to (e.g. "payments", "accessibility") */ domain?: string; /** How scarce this skill is in the labour market this team hires from */ rarity?: UPGAssessment; /** Typical hours of deliberate practice to reach working proficiency */ hours_to_proficiency?: number; } /** Ceremony entity. * * @example * const properties: CeremonyProperties = { * ceremony_type: 'standup', * cadence: 'quarterly', * duration_minutes: 45, * } */ interface CeremonyProperties { /** Kind of recurring meeting */ ceremony_type?: 'standup' | 'planning' | 'review' | 'retro' | 'sync' | 'demo' | 'other'; /** How often the ceremony occurs. Uses the shared `Cadence` scale. */ cadence?: Cadence; /** Typical duration of the meeting in minutes */ duration_minutes?: number; /** People or roles who attend. Promote individuals to `node_owned_by_person` edges if participation must be queryable. */ participants?: string; } /** CapacityPlan entity. * * @example * const properties: CapacityPlanProperties = { * plan_period: '2026-Q2', * total_capacity: 42, * allocated: 42, * } */ interface CapacityPlanProperties { /** Time period the plan covers (e.g. "Sprint 14", "Q2 2026") */ plan_period?: string; /** Total available capacity in person-days or story points */ total_capacity?: number; /** Capacity already allocated to work */ allocated?: number; /** Remaining unallocated capacity */ available?: number; } /** * UPG Property Schemas: Program Management Domain. * Program, Project, Milestone, RiskRegister, ChangeRequest, * Deliverable, ResourceAllocation, StatusReport. * https://unifiedproductgraph.org/spec | MIT */ /** Program. * * @example * const properties: ProgramProperties = { * start_date: '2026-04-01', * end_date: '2026-09-30', * budget: 50000, * } */ interface ProgramProperties { /** Program start date (ISO format) */ start_date?: ISODate; /** Program end date (ISO format) */ end_date?: ISODate; /** Total budget allocated to the program */ budget?: number; } /** Project. * * @example * const properties: ProjectProperties = { * start_date: '2026-04-01', * end_date: '2026-09-30', * methodology: 'agile', * } */ interface ProjectProperties { /** Project start date (ISO format) */ start_date?: ISODate; /** Project end date (ISO format) */ end_date?: ISODate; /** Development methodology used */ methodology?: 'agile' | 'waterfall' | 'kanban' | 'hybrid'; } /** Milestone. * * @example * const properties: MilestoneProperties = { * due_date: '2026-06-15', * } */ interface MilestoneProperties { /** * Display order of this milestone within its parent (0-indexed). The scalar * ordering convention shared with `journey_step.step_order` and * `journey_action.action_order` (UPG-663 / UPG-674). Orders the delivery * milestones a parent moves through, independent of `due_date`. * * @remarks * The parent is whichever of the two milestone parents the graph actually * uses: a `project` via `project_targets_milestone`, or a `product` directly * via `product_targets_milestone` (Portfolio Phase 2, for milestones a * product owns outright with no program or project above them). The order is * scoped to that one parent, never global. */ milestone_order?: number; /** Target due date (ISO format) */ due_date?: ISODate; /** Whether the milestone was met on time */ met_on_time?: boolean; } /** Risk register. * * @example * const properties: RiskRegisterProperties = { * last_reviewed: '2026-02-15', * } */ interface RiskRegisterProperties { /** Date the register was last reviewed (ISO format) */ last_reviewed?: string; } /** Change request. * * @example * const properties: ChangeRequestProperties = { * change_type: 'scope', * priority: 'high', * impact_assessment: 'Medium: affects onboarding success metric.', * } */ interface ChangeRequestProperties { /** What aspect of the project is being changed */ change_type?: 'scope' | 'schedule' | 'budget' | 'resource' | 'requirements'; /** Current approval status of the request */ approval_status?: 'pending' | 'approved' | 'rejected' | 'deferred'; /** Priority of the change request */ priority?: Priority; /** Description of the change's impact on the project */ impact_assessment?: string; } /** Deliverable. * * @example * const properties: DeliverableProperties = { * deliverable_type: 'document', * due_date: '2026-06-15', * acceptance_criteria: 'Given a new signup, when they complete onboarding, then the welcome email fires within 60 seconds.', * } */ interface DeliverableProperties { /** Kind of deliverable (UPG-579 Option B). */ deliverable_type?: 'document' | 'prototype' | 'release' | 'design' | 'report' | 'other'; /** Due date for the deliverable (ISO format) */ due_date?: ISODate; /** Criteria that must be met for the deliverable to be accepted */ acceptance_criteria?: string; } /** Resource allocation. * * @example * const properties: ResourceAllocationProperties = { * resource_type: 'person', * allocation_percentage: 42, * start_date: '2026-04-01', * } */ interface ResourceAllocationProperties { /** Kind of resource being allocated */ resource_type?: 'person' | 'team' | 'budget' | 'tool'; /** Percentage of the resource allocated (0-100) */ allocation_percentage?: number; /** Start date of the allocation (ISO format) */ start_date?: ISODate; /** End date of the allocation (ISO format) */ end_date?: ISODate; } /** Status report. * * @example * const properties: StatusReportProperties = { * report_period: '2026-Q2', * overall_status: 'green', * risks_flagged: 42, * } */ interface StatusReportProperties { /** Time period the report covers */ report_period?: string; /** Overall Red/Amber/Green health status */ overall_status?: HealthStatus; /** Number of risks flagged in this report */ risks_flagged?: number; /** Description of current blockers */ blockers?: string; } /** * UPG Property Schemas: AI & ML Domain. * AiModel, PromptTemplate, PromptVersion, EvalBenchmark, EvalRun, * AiCostTracker, HallucinationReport, AiGuardrail, ModelComparison, * AiExperiment, AiDataset, AiTrace. * Creation order: ai_model → prompt_template → prompt_version (UPG-665). * https://unifiedproductgraph.org/spec | MIT */ /** AI model. * * @example * const properties: AiModelProperties = { * model_provider: 'anthropic', * model_id: 'claude-sonnet-4-6', * model_version: '2026-04-01', * } */ interface AiModelProperties { /** Provider or vendor */ model_provider?: 'anthropic' | 'openai' | 'google' | 'meta' | 'mistral' | 'custom'; /** Unique model identifier (e.g. "claude-sonnet-4-20250514") */ model_id?: string; /** Specific version */ model_version?: string; /** Intended use case */ model_purpose?: string; /** Maximum context window (tokens) */ context_window?: number; /** Median latency (p50, ms) */ latency_p50_ms?: number; /** Tail latency (p99, ms) */ latency_p99_ms?: number; /** Expected input format or schema */ input_schema?: string; /** Expected output format or schema */ output_schema?: string; /** Alternative names */ aliases?: string[]; /** Free-form classification tags */ tags?: string[]; } /** Prompt template. * * The canonical parent of `prompt_version` (UPG-665): a template contains its * versions the way a file contains its commits. Lives in the AI domain, owned * by the `ai_model` that defines it (ai_model → prompt_template → prompt_version). * * @example * const properties: PromptTemplateProperties = { * use_case: 'Kick off a discovery sprint with a fresh persona set.', * variables: ['user_name', 'workspace_slug', 'cta_url'], * version: '0.3.1', * } */ interface PromptTemplateProperties { /** Intended use case for the prompt */ use_case?: string; /** Variable names expected by the template */ variables?: string[]; /** Version identifier of the template */ version?: string; } /** Prompt version. * * @example * const properties: PromptVersionProperties = { * version_number: '0.3.1', * template: 'weekly-exec-review', * system_prompt: 'You are a product-research copilot. Extract opportunities from the transcript below.', * } */ interface PromptVersionProperties { /** Semantic version */ version_number?: string; /** Template body with variable placeholders */ template?: string; /** System prompt prepended to every call */ system_prompt?: string; /** Expected template variable names */ variables?: string[]; /** Sampling temperature (0 = deterministic, 1 = creative) */ temperature?: number; /** Max tokens to generate */ max_tokens?: number; /** Estimated input tokens per invocation */ input_token_estimate?: number; /** Aggregate quality score from evaluations */ performance_score?: number; } /** Evaluation benchmark. * * @example * const properties: EvalBenchmarkProperties = { * benchmark_type: 'accuracy', * test_case_count: 42, * passing_threshold: 42, * } */ interface EvalBenchmarkProperties { /** * Measured dimension. * * @remarks * Widened in 0.31.0 with the three dimensions an eval suite actually reports * and this set could not express: `precision_recall` (two numbers, not one * accuracy figure, and the pair is the point, because a detector that fires on * everything scores perfectly on recall alone), `task_success` (did the agent * achieve the goal), and `coherence` (rubric-graded, for multi-step runs). * * THIS ENUM IS A DIMENSION AXIS, AND IT SHOULD STAY ONE. An eval suite's * families are a SUBJECT axis (what is under test), and a dimension cannot * separate subjects, which is why several families legitimately share * `task_success`. * * The subject is expressed on the EDGE, not here: `eval_benchmark_measures_node` * names what a benchmark measures by pointing at it. So "which benchmarks cover * the importer" is a traversal, and this property stays a clean answer to a * different question, "what dimension does this benchmark report". The * commissioning brief hoped the enum would carry both; splitting them across the * enum and the edge is the better answer, and it needs no third mechanism. * * Corollary worth stating, since it is load-bearing: do NOT add subject-shaped * values here. `tool_use` or `documentation` would encode on this axis what the * edge already carries, and the two would drift the first time a benchmark * measured something its enum value did not admit. */ benchmark_type?: 'accuracy' | 'latency' | 'cost' | 'safety' | 'precision_recall' | 'task_success' | 'coherence' | 'custom'; /** Test cases in the suite */ test_case_count?: number; /** Minimum passing score */ passing_threshold?: number; /** ISO date of the most recent run */ last_run?: ISODate; } /** One metric's score inside `EvalRunProperties.metric_scores`. * * Every field is required, including `sample_size`. A score without its * denominator is not a smaller measurement, it is not a measurement, and an * optional denominator is how a run quietly stops reporting one. * * @example * const scores: MetricScore[] = [ * { metric: 'precision', value: 0.94, sample_size: 250 }, * { metric: 'recall', value: 0.71, sample_size: 250 }, * ] */ interface MetricScore { /** What was measured, named as the metric names itself: `precision`, `recall`, `faithfulness`. */ metric: string; /** * The score, normalized to 0 through 1, where 1 is the best result. * * @remarks * Normalization is the contract rather than a suggestion, because comparing * runs is the whole reason this array exists and two runs on different raw * scales do not compare. A rating collected on 1 through 5 is divided before * it lands here, and the raw scale belongs in the metric name if a reader * needs it (`faithfulness_of_5`). * * Measurements that are not rates do not belong here at all. Wall-clock time, * token counts, and cost already have typed fields on `eval_run`, so a metric * that cannot be expressed as a rate is a sign the value wanted one of those. */ value: number; /** * How many cases the score was computed over. * * @remarks * Required so a comparison across runs can never quietly compare different * sample sizes. It is the one field an author is most tempted to omit and the * one whose absence is least visible afterwards: a precision of 0.94 reads the * same whether it was measured over 250 cases or 4. */ sample_size: number; } /** Evaluation run. * * @example * const properties: EvalRunProperties = { * run_date: '2026-04-01', * score: 42, * passed: true, * } */ interface EvalRunProperties { /** ISO date executed */ run_date?: ISODate; /** Aggregate score */ score?: number; /** Whether the passing threshold was met */ passed?: boolean; /** Wall-clock duration (ms) */ duration_ms?: number; /** Total tokens consumed */ token_count?: number; /** Input tokens */ input_token_count?: number; /** Output tokens */ output_token_count?: number; /** Total run cost */ cost?: number; /** Percentage of test cases that errored */ error_rate?: number; /** Feedback score summary (human or automated) */ feedback_scores?: string; /** * Per-metric scores, one entry per measured metric. * * @remarks * Added in 0.31.0 because `score` is a single aggregate and the forcing case * needs two numbers: precision and recall, reported separately. An aggregate * hides the only interesting failure mode, which is a detector that catches * everything by firing on everything. * * `sample_size` is not optional decoration, and as of 0.31.0 the type says so: * it is a REQUIRED member of `MetricScore`. A sampled run reports its sample * size beside its score, always, so a comparison across runs can never quietly * compare different sample sizes. A number without its denominator is not a * smaller measurement, it is not a measurement, and a remark saying `always` * over a field typed optional is a contract only the careful reader honours. * * The entry shape is the exported `MetricScore` interface rather than an * anonymous literal, so a consumer can name the thing it is building. This is * one of two `object[]` properties in the spec; the other, `composition.members`, * already exports `CompositionMember`. * * The rejected alternative was two runs per benchmark, which needs no spec * change and makes every question about a benchmark's quality a join, encoding * a measurement artifact as graph structure. */ metric_scores?: MetricScore[]; } /** AI cost tracker. * * @example * const properties: AiCostTrackerProperties = { * period: '2026-Q2', * total_cost: 42, * total_requests: 42, * } */ interface AiCostTrackerProperties { /** Tracked period (e.g. "2026-Q1", "2026-04") */ period?: string; /** Total spend across all models */ total_cost?: number; /** Total API requests */ total_requests?: number; /** Average cost per request */ avg_cost_per_request?: number; /** Total input tokens */ input_tokens?: number; /** Total output tokens */ output_tokens?: number; /** Cost breakdown by model (name → USD) */ cost_by_model?: Record; /** Spend ceiling for the period */ budget_limit?: number; /** Alert threshold (spend percentage) */ budget_alert_threshold?: number; } /** Hallucination report. * * @example * const properties: HallucinationReportProperties = { * report_type: 'factual', * severity: 4, * user_facing: true, * } */ interface HallucinationReportProperties { /** Classification */ report_type?: 'factual' | 'logical' | 'fabrication' | 'inconsistency'; /** Impact severity (1 = trivial, 5 = dangerous misinformation) */ severity?: UPGAssessment; /** Visible to end users */ user_facing?: boolean; /** Remediation steps */ remediation?: string; } /** AI guardrail. * * @example * const properties: AiGuardrailProperties = { * guardrail_type: 'content_filter', * enforcement: 'block', * trigger_count: 42, * } */ interface AiGuardrailProperties { /** Protection category */ guardrail_type?: 'content_filter' | 'rate_limit' | 'token_limit' | 'safety' | 'custom'; /** Action when triggered */ enforcement?: 'block' | 'warn' | 'log'; /** Times triggered */ trigger_count?: number; } /** Model comparison. * * @example * const properties: ModelComparisonProperties = { * comparison_criteria: ['pricing', 'integrations', 'time-to-value'], * winner: 'variant-B', * comparison_date: '2026-04-01', * } */ interface ModelComparisonProperties { /** Comparison dimensions (e.g. "accuracy", "cost", "latency") */ comparison_criteria?: string[]; /** ISO conduct date */ comparison_date?: ISODate; } /** AI training or fine-tuning experiment. * * @example * const properties: AiExperimentProperties = { * project: 'q2-activation-uplift', * run_name: 'regression-nightly-2026-04-17', * config: '{ retries: 3, timeout_ms: 15000 }', * } */ interface AiExperimentProperties { /** Parent project or experiment group */ project?: string; /** Human-readable run name */ run_name?: string; /** Serialised hyperparameters and config */ config?: string; /** Key-metric summary */ summary_metrics?: string; /** ISO timestamp started */ started_at?: ISODateTime; /** ISO timestamp completed */ completed_at?: ISODateTime; /** Training steps or epochs completed */ training_steps?: number; /** Produced artifact URI */ artifact_uri?: string; /** Free-text notes */ notes?: string; /** Free-form classification tags */ tags?: string[]; } /** Versioned AI training or evaluation dataset. * * @example * const properties: AiDatasetProperties = { * dataset_type: 'training', * version: '0.3.1', * record_count: 42, * } */ interface AiDatasetProperties { /** Purpose */ dataset_type?: 'training' | 'evaluation' | 'fine_tuning' | 'rlhf' | 'synthetic'; /** Version */ version?: string; /** Records */ record_count?: number; /** Format (e.g. "jsonl", "csv", "parquet") */ format?: string; /** Storage URI */ storage_uri?: string; /** Integrity hash */ checksum?: string; /** Origin */ provenance?: 'human_labelled' | 'synthetic' | 'scraped' | 'converted' | 'mixed'; /** SPDX license identifier */ license?: string; /** Free-form classification tags */ tags?: string[]; } /** Single LLM call or chain execution trace. * * @example * const properties: AiTraceProperties = { * inputs: ['conversation-transcript', 'existing-graph'], * outputs: ['ranked-opportunities', 'supporting-evidence'], * called_at: '2026-04-01T00:00:00Z', * } */ interface AiTraceProperties { /** Serialised input */ inputs?: string; /** Serialised output */ outputs?: string; /** ISO timestamp called */ called_at?: ISODateTime; /** Round-trip latency (ms) */ latency_ms?: number; /** Input tokens */ input_tokens?: number; /** Output tokens */ output_tokens?: number; /** Monetary cost */ cost?: number; /** Error message on failure */ error?: string; /** HTTP or API status */ status_code?: number; /** Human or automated quality score */ feedback_score?: number; /** Free-form classification tags */ tags?: string[]; } /** * UPG Property Schemas: Automation & Agentic Workflows Domain. * WorkflowTemplate, WorkflowRun, AgentDefinition, AgentSession, ReviewGate, * ApprovalRecord, AgentSkill, AgentHook, WorkflowArtifact, AgentTask. * https://unifiedproductgraph.org/spec | MIT */ /** Workflow template. * * @example * const properties: WorkflowTemplateProperties = { * template_type: 'sequential', * step_count: 42, * agent_count: 42, * } */ interface WorkflowTemplateProperties { /** Execution pattern for the workflow steps */ template_type?: 'sequential' | 'parallel' | 'conditional' | 'loop'; /** Number of steps in the workflow */ step_count?: number; /** Number of agents involved in the workflow */ agent_count?: number; /** Estimated wall-clock duration of a full run */ estimated_duration?: string; /** Schema describing the workflow's state object */ state_schema?: string; /** Whether the workflow supports checkpointing for recovery */ checkpoint_enabled?: boolean; /** Whether a human approval step is required */ human_in_loop?: boolean; /** Version label for this workflow template (e.g. "2.1") */ version?: string; } /** Workflow run. * * @example * const properties: WorkflowRunProperties = { * started_at: '2026-04-01T00:00:00Z', * completed_at: '2026-04-01T00:00:00Z', * run_status: 'pending', * } */ interface WorkflowRunProperties { /** ISO timestamp when the run started */ started_at?: ISODateTime; /** ISO timestamp when the run completed */ completed_at?: ISODateTime; /** Current execution status of the run */ run_status?: 'pending' | 'running' | 'completed' | 'failed' | 'canceled'; /** Event or action that triggered this run */ triggering_event?: string; /** Number of steps executed in this run */ step_count?: number; /** Total tokens consumed across all steps */ total_tokens?: number; /** Total monetary cost of the run */ total_cost?: number; /** Error message if the run failed */ error_message?: string; } /** Agent definition. * * @example * const properties: AgentDefinitionProperties = { * agent_role: 'research-copilot', * agent_scope: 'Persona interviews within the growth domain.', * goal: 'Cut time-to-first-value from 7 days to 2.', * } */ interface AgentDefinitionProperties { /** Role the agent plays within a workflow */ agent_role?: string; /** Boundaries of what this agent can act on */ agent_scope?: string; /** Primary objective the agent is trying to achieve */ goal?: string; /** Context or persona narrative for the agent */ backstory?: string; /** Whether the agent can delegate tasks to other agents */ allow_delegation?: boolean; /** Whether the agent retains memory across sessions */ memory_enabled?: boolean; /** Maximum number of reasoning iterations allowed */ max_iterations?: number; /** Hard timeout for agent execution in seconds */ max_execution_time_seconds?: number; /** Whether the agent can execute generated code */ allow_code_execution?: boolean; /** Whether the agent can process images and other media */ multimodal?: boolean; /** Version label for this agent definition (e.g. "1.4.2") */ version?: string; } /** Agent session. * * @example * const properties: AgentSessionProperties = { * session_start: '2026-04-17T09:00:00Z', * session_end: '2026-04-17T09:48:00Z', * turns: 42, * } */ interface AgentSessionProperties { /** ISO timestamp when the session began */ session_start?: string; /** ISO timestamp when the session ended */ session_end?: string; /** Number of conversational turns in the session */ turns?: number; /** Total tokens consumed during the session */ tokens_used?: number; /** Total monetary cost of the session */ cost?: number; /** Number of errors encountered during the session */ error_count?: number; /** Current status of the session */ session_status?: 'active' | 'completed' | 'errored' | 'timed_out'; /** Brief summary of the session's output */ output_summary?: string; } /** Agent task. A discrete task assigned to an agent. * * @example * const properties: AgentTaskProperties = { * description: 'Short narrative describing the entity and why it exists.', * expected_output: 'A ranked list of opportunities with confidence scores.', * context: 'Leads a 12-person product team at a mid-size B2B SaaS (50–200 employees).', * } */ interface AgentTaskProperties { /** What the agent should accomplish */ description: string; /** Description of the expected output format or content */ expected_output?: string; /** Additional context provided to the agent for this task */ context?: string; /** File path where the agent should write output */ output_file?: string; /** Whether this task blocks downstream tasks */ blocking?: boolean; /** Relative priority of this task */ priority?: Priority; } /** Review gate. * * @example * const properties: ReviewGateProperties = { * gate_type: 'human_review', * required_approvers: ['eng-lead', 'security-reviewer'], * } */ interface ReviewGateProperties { /** Kind of review required at this gate */ gate_type?: 'human_review' | 'automated_check' | 'approval'; /** People or roles that must approve. Promote to `node_owned_by_person` edges (one per name) if ownership must be queryable. */ required_approvers?: string[]; } /** Approval record. * * @example * const properties: ApprovalRecordProperties = { * approved: true, * comment: 'Revisit once we have Q2 interview data.', * approved_at: '2026-04-01T00:00:00Z', * } */ interface ApprovalRecordProperties { /** Whether the item was approved or rejected */ approved?: boolean; /** Reviewer's comment or rationale */ comment?: string; /** ISO timestamp when the approval was given */ approved_at?: ISODateTime; } /** Agent skill. * * @example * const properties: AgentSkillProperties = { * skill_trigger: '/upg-new-research', * skill_description: 'Turns a transcript into a ranked list of opportunities.', * invocation_count: 42, * } */ interface AgentSkillProperties { /** Event or command that activates this skill */ skill_trigger?: string; /** Human-readable description of what the skill does */ skill_description?: string; /** Number of times this skill has been invoked */ invocation_count?: number; } /** Agent hook. * * @example * const properties: AgentHookProperties = { * hook_event: 'node.committed', * hook_action: 'post-to-slack', * hook_status: 'active', * } */ interface AgentHookProperties { /** Event that triggers this hook */ hook_event?: string; /** Action performed when the hook fires */ hook_action?: string; /** * Operational status of the hook. * @deprecated since 0.33.0, removeIn 1.0.0. Use the base `status` field. `agent_hook` moved from the WORK_ITEM lifecycle to OPERATIONAL at 0.33.0, whose phases carry this axis directly: `active` maps to `active` and `disabled` maps to `paused`. A `*_status` property beside the base `status` is the shadow that Pattern D collapsed fourteen times at 0.15.0, and a type that models its own status in a property is a type whose lifecycle did not fit. `error` maps to `paused`: OPERATIONAL has no failure phase, and runtime health is store or telemetry state rather than a fact about the thing, which is the same cut that keeps `composition.rev` and excludes a concurrency token. Put the failure detail in `description` or a diagnostic property. */ hook_status?: 'active' | 'disabled' | 'error'; /** Number of times this hook has fired */ execution_count?: number; } /** Workflow artifact. * * @example * const properties: WorkflowArtifactProperties = { * artifact_type: 'document', * artifact_url: 'https://builds.entopo.app/artifacts/entopo-0.3.1.tgz', * produced_at: '2026-04-01T00:00:00Z', * } */ interface WorkflowArtifactProperties { /** Kind of output produced by the workflow */ artifact_type?: 'document' | 'code' | 'data' | 'report' | 'other'; /** URL or path to the artifact */ artifact_url?: string; /** ISO timestamp when the artifact was produced */ produced_at?: ISODateTime; } /** * UPG Property Schemas: Business Model Domain. * BusinessModel, ValueProposition, RevenueStream, PricingTier, CostStructure, * UnitEconomics, Partnership, KeyResource, KeyActivity, TargetCustomerSegment, * CustomerRelationship, DistributionChannel. * https://unifiedproductgraph.org/spec | MIT */ /** BusinessModel: root of a BMC-style model. * * Structural atoms (segments, channels, propositions) live in child entities * connected via edges. This entity holds the top-level framing. * * @example * const properties: BusinessModelProperties = { * framework_id: 'lean-canvas', * stage: 'draft', * pattern: 'saas', * } */ interface BusinessModelProperties { /** Framework ID (references `UPGFramework.id`) */ framework_id?: string; /** Maturity */ stage?: 'draft' | 'validated' | 'active'; /** Canonical pattern. Useful for benchmarking against peer businesses with the same shape. */ pattern?: 'freemium' | 'marketplace' | 'saas' | 'subscription' | 'transactional' | 'advertising' | 'licensing' | 'hybrid'; } /** ValueProposition. Most relationships expressed as edges. * * @example * const properties: ValuePropositionProperties = { * validation_state: 'hypothesis', * offering_type: 'product', * unique_selling_point: 'integration', * } * * Structural refs go via edges (v0.4.0): * jobs addressed → `value_proposition_addresses_job` * pains relieved → `value_proposition_solves_need` (a pain is a `need` with valence='pain') * gains created → `value_proposition_delivers_outcome` */ interface ValuePropositionProperties { /** * Validation maturity. Where it sits on the "is this real?" journey. * Renamed from `confidence` because these values describe validation state, * not subjective confidence. Use `Confidence` or `UPGAssessment` from primitives * for per-rater confidence. */ validation_state?: 'hypothesis' | 'tested' | 'validated'; /** Offering shape */ offering_type?: 'product' | 'service' | 'platform' | 'experience' | 'hybrid'; /** * Differentiation axis. Closed set so dashboards group propositions by * differentiation strategy. For the narrative form, use `unique_selling_point_statement`. */ unique_selling_point?: 'category_definition' | 'price' | 'speed' | 'quality' | 'integration' | 'experience' | 'other'; /** * Narrative differentiator copy. Pairs with `unique_selling_point` for * messaging when the team needs the rhetorical sentence. */ unique_selling_point_statement?: string; } /** RevenueStream. * * @example * const properties: RevenueStreamProperties = { * stream_type: 'subscription', * recurring_revenue: 42, * billing_model: 'subscription', * } */ interface RevenueStreamProperties { /** How revenue is generated */ stream_type?: 'subscription' | 'transaction' | 'licensing' | 'advertising' | 'freemium' | 'other'; /** Monthly or annual recurring revenue from this stream */ recurring_revenue?: number; /** Billing mechanics. May differ from `stream_type`. */ billing_model?: 'subscription' | 'usage' | 'one_time' | 'tiered' | 'freemium' | 'other'; /** Accounting basis for revenue recognition */ recognition_basis?: 'accrual' | 'cash' | 'deferred'; /** Share of total ARR contributed (0–100) */ arr_contribution_pct?: number; /** Free-text forecast or projection */ forecast?: string; } /** PricingTier: the central pricing concept (the plan a customer buys). * * @example * const properties: PricingTierProperties = { * price: 49, * billing_period: 'monthly', * currency: 'USD', * } */ interface PricingTierProperties { /** Price per billing period */ price?: number; /** Billing cadence */ billing_period?: 'monthly' | 'yearly' | 'one_time'; /** ISO 4217 currency (e.g. "USD", "EUR") */ currency?: string; /** Display ordering (1 = first tier shown) */ tier_order?: number; /** Highlighted as recommended / most popular */ is_highlighted?: boolean; } /** CostStructure. * * @example * const properties: CostStructureProperties = { * cost_type: 'fixed', * amount: 1200, * period: 'monthly', * } */ interface CostStructureProperties { /** Classification */ cost_type?: 'fixed' | 'variable' | 'cogs' | 'opex'; /** Monetary amount */ amount?: number; /** Recurrence */ period?: 'monthly' | 'yearly' | 'one_time'; } /** UnitEconomics. * * @example * const properties: UnitEconomicsProperties = { * lifetime_value: 42, * customer_acquisition_cost: 42, * payback_period_months: 42, * } */ interface UnitEconomicsProperties { /** Customer lifetime value */ lifetime_value?: number; /** Customer acquisition cost */ customer_acquisition_cost?: number; /** Months to recover CAC from revenue */ payback_period_months?: number; /** Gross margin (0–100) */ gross_margin?: number; } /** Partnership. * * @example * const properties: PartnershipProperties = { * partner_type: 'technology', * value_exchange: 'We provide distribution; partner provides fulfilment at cost.', * partnership_tier: 'strategic', * } */ interface PartnershipProperties { /** Partnership nature */ partner_type?: 'technology' | 'distribution' | 'content' | 'strategic'; /** What each party gives and receives */ value_exchange?: string; /** Commercial significance. Drives attention and exec sponsorship. */ partnership_tier?: 'strategic' | 'preferred' | 'standard' | 'trial'; /** Exposure if the partnership fails (concentration risk, IP risk, etc.) */ risk_level?: UPGAssessment; /** Internal owner. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; /** Date the partnership became effective (ISO 8601) */ start_date?: ISODate; } /** KeyResource. * * @example * const properties: KeyResourceProperties = { * resource_type: 'physical', * criticality: 'high', * owner: 'sam.patel@arkheiev.com', * } */ interface KeyResourceProperties { /** Category */ resource_type?: 'physical' | 'intellectual' | 'human' | 'financial'; /** Criticality to the business model */ criticality?: Priority; /** Accountable person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; /** Replacement difficulty */ scarcity_risk?: UPGAssessment; /** Substitutability if lost */ substitutability?: 'high' | 'medium' | 'low'; } /** KeyActivity. * * @example * const properties: KeyActivityProperties = { * activity_type: 'production', * cadence: 'monthly', * operational_owner: 'platform-team', * } */ interface KeyActivityProperties { /** Activity nature */ activity_type?: 'production' | 'problem_solving' | 'platform' | 'network'; /** * Canonical `Cadence`. Replaces the legacy free-form `frequency: string` in v0.4.0. * For exact rates (e.g. "3 times per week") set `frequency_count` + `frequency_period`. * For qualitative tiers ("rare" → "constant") use `frequency_rating`. */ cadence?: Cadence; /** Exact count of runs in the period. Pairs with `frequency_period`. */ frequency_count?: number; /** Recurrence period (ISO-8601 `Duration`, e.g. `'P7D'`) */ frequency_period?: Duration; /** Qualitative tier. Use when an exact rate is unknown. */ frequency_rating?: FrequencyRating; /** Operationally accountable team or individual. Promote to a `node_owned_by_team` edge if ownership must be queryable. */ operational_owner?: string; /** Bottleneck or scaling constraint */ capacity_constraint?: string; /** How much runs without human intervention */ automation_level?: 'manual' | 'assisted' | 'automated'; } /** TargetCustomerSegment. * * @example * const properties: TargetCustomerSegmentProperties = { * segment_type: 'mass', * segment_size: 12000, * willingness_to_pay: 'willingness to pay', * } */ interface TargetCustomerSegmentProperties { /** Segmentation strategy */ segment_type?: 'mass' | 'niche' | 'segmented' | 'diversified' | 'multi_sided'; /** Estimated potential customers */ segment_size?: number; /** Price sensitivity and willingness-to-pay description */ willingness_to_pay?: string; } /** CustomerRelationship. * * @example * const properties: CustomerRelationshipProperties = { * relationship_type: 'personal', * acquisition_role: 'first-touch', * retention_role: 'Proactive check-ins at day 14, 30, and 60.', * } */ interface CustomerRelationshipProperties { /** Interaction shape */ relationship_type?: 'personal' | 'self_service' | 'automated' | 'community' | 'co_creation'; /** Acquisition funnel stage primarily served */ acquisition_role?: 'first_touch' | 'nurture' | 'demo' | 'close' | 'other'; /** Retention lever primarily pulled in the customer lifecycle */ retention_role?: 'onboarding' | 'check_in' | 'expansion' | 'renewal' | 'reactivation' | 'other'; } /** DistributionChannel. * * @example * const properties: DistributionChannelProperties = { * channel_type: 'direct', * owned_or_partner: 'owned', * phase: 'awareness', * } */ interface DistributionChannelProperties { /** How the product reaches customers */ channel_type?: 'direct' | 'retail' | 'wholesale' | 'marketplace' | 'oem'; /** Owned or partner-operated */ owned_or_partner?: 'owned' | 'partner'; /** Customer journey phase served */ phase?: 'awareness' | 'purchase' | 'delivery' | 'support'; } /** * UPG Property Schemas: Growth Domain. * Funnel, FunnelStep, AcquisitionChannel, GrowthCampaign, Cohort, BehavioralSegment, * GrowthLoop, Variant, AttributionModel. * https://unifiedproductgraph.org/spec | MIT */ /** Funnel entity. * * @example * const properties: FunnelProperties = { * funnel_type: 'acquisition', * step_count: 42, * overall_conversion_rate: 42, * } */ interface FunnelProperties { /** Which stage of the customer lifecycle this funnel measures */ funnel_type?: 'acquisition' | 'activation' | 'retention' | 'revenue' | 'referral' | 'custom'; /** Number of steps in the funnel */ step_count?: number; /** End-to-end conversion rate through the funnel (0-1) */ overall_conversion_rate?: number; } /** FunnelStep entity. * * @example * const properties: FunnelStepProperties = { * step_index: 42, * conversion_rate: 0.08, * drop_off_rate: 42, * } */ interface FunnelStepProperties { /** Position of this step in the funnel (0-indexed) */ step_index?: number; /** Percentage of users who advance from this step */ conversion_rate?: number; /** Percentage of users who leave at this step */ drop_off_rate?: number; } /** AcquisitionChannel entity. * * @example * const properties: AcquisitionChannelProperties = { * channel_type: 'seo', * customer_acquisition_cost: 42, * monthly_volume: 42, * } */ interface AcquisitionChannelProperties { /** Category of the acquisition channel */ channel_type?: 'seo' | 'paid' | 'social' | 'referral' | 'direct' | 'content'; /** Customer acquisition cost for this channel */ customer_acquisition_cost?: number; /** Monthly volume of new users from this channel */ monthly_volume?: number; } /** GrowthCampaign entity. * * A bounded, time-boxed promotional or acquisition push run across one or * more channels with measurable goals. Campaign lifecycle (drafted -> * planning → live → completed/paused) is governed by the canonical * `GROWTH_CAMPAIGN_LIFECYCLE`. * * Per UPG principle P14, structural relationships are edges: * parent acquisition_channel: `acquisition_channel_runs_growth_campaign` * target audience: `growth_campaign_targets_behavioral_segment` * tested via: `growth_campaign_tests_via_experiment_plan` * amplified launches: `launch_amplified_by_growth_campaign` * included content: `content_piece_part_of_growth_campaign` * * @example * const properties: GrowthCampaignProperties = { * campaign_type: 'paid_acquisition', * start_date: '2026-04-01', * end_date: '2026-09-30', * budget_amount: 50000, * budget_currency: 'USD', * channels_targeted: ['google_ads', 'linkedin', 'partner_newsletter'], * primary_kpi: 'qualified_signups', * kpi_target: 1500, * utm_parameters: 'utm_source=google&utm_medium=cpc&utm_campaign=q2-launch', * } */ interface GrowthCampaignProperties { /** Strategic shape. Drives channel mix, budget pattern, and measurement style. */ campaign_type?: 'paid_acquisition' | 'content' | 'referral' | 'partnership' | 'event' | 'viral' | 'lifecycle' | 'other'; /** ISO start date */ start_date?: ISODate; /** ISO end date */ end_date?: ISODate; /** Total allocated budget. Use with `budget_currency`. */ budget_amount?: number; /** ISO 4217 currency code (e.g. "USD", "EUR", "GBP") */ budget_currency?: string; /** Primary KPI optimised for (e.g. "qualified_signups", "MQL_volume", "activated_teams") */ primary_kpi?: string; /** Numeric target for `primary_kpi` over the campaign window */ kpi_target?: number; /** UTM tracking parameters for this campaign */ utm_parameters?: string; /** Hypothesis or rationale this campaign is testing (free-form, complements `experiment_plan` linkage) */ hypothesis?: string; } /** Cohort entity. * * @example * const properties: CohortProperties = { * definition: 'A measurable result a team commits to deliver in a time-boxed period.', * acquisition_start: '2026-01-01', * acquisition_end: '2026-03-31', * } */ interface CohortProperties { /** How this cohort is defined */ definition?: string; /** Start of the acquisition window (ISO format) */ acquisition_start?: string; /** End of the acquisition window (ISO format) */ acquisition_end?: string; /** Number of users in the cohort */ size?: number; /** 7-day retention rate (0-1) */ retention_day_7?: number; /** 30-day retention rate (0-1) */ retention_day_30?: number; } /** BehavioralSegment entity. * * A definition of a user cohort grouped by observed or qualifying behaviour * (distinct from `cohort`, a time-bounded acquisition window, and from * `market_segment`, which segments the addressable market). Behavioral * segments are computed snapshots: no lifecycle status, just a defining rule, * a measured size, and a validity window over which the snapshot was taken. * * Per UPG principle P14, mappings to other entities are edges: * persona overlay: `behavioral_segment_maps_to_persona` * parent product: `product_segments_into_behavioral_segment` * ICP overlay: `ideal_customer_profile_maps_to_behavioral_segment` * campaign / experiment / pricing targeting: `*_targets_behavioral_segment` * * @example * const properties: BehavioralSegmentProperties = { * definition: 'Users who created ≥ 3 nodes within their first 24 hours.', * criteria: 'event:node_created count >= 3 AND days_since_signup < 1', * segment_type: 'behavioral', * source: 'clickstream', * included_behaviors: ['created_node', 'connected_nodes', 'opened_canvas'], * excluded_behaviors: ['account_deleted', 'support_flagged'], * size_estimate: 1240, * validity_period_start: '2026-04-01', * validity_period_end: '2026-04-30', * } */ interface BehavioralSegmentProperties { /** Plain-language description. Pairs with the machine-readable `criteria`. */ definition?: string; /** Machine-readable or semi-structured membership rule (event-query DSL, SQL fragment, segmentation tool clause). */ criteria?: string; /** Classification. `behavioral` is the default; the wider enum allows reclassification without changing entity type. */ segment_type?: 'behavioral' | 'demographic' | 'firmographic' | 'custom'; /** Membership data source. Drives confidence weighting and refresh policy. */ source?: 'clickstream' | 'survey' | 'interview' | 'mixed' | 'inferred' | 'imported'; /** Qualifying behaviours (positive criteria, free-text labels) */ included_behaviors?: string[]; /** Excluding behaviours (negative criteria, free-text labels) */ excluded_behaviors?: string[]; /** Estimated current size. Snapshot value. (`size`, the v0.2 alias, removed in 0.14.0, UPG-574.) */ size_estimate?: number; /** ISO start of the validity window. Behavioural definitions decay; outside the window the snapshot should be re-computed. */ validity_period_start?: ISODate; /** End of the window over which the segment definition is considered valid (ISO format) */ validity_period_end?: ISODate; } /** GrowthLoop entity. * * @example * const properties: GrowthLoopProperties = { * loop_type: 'viral', * trigger: 'User opens the third restricted feature in a session.', * action: 'Send welcome email', * } */ interface GrowthLoopProperties { /** Mechanism that drives the loop */ loop_type?: 'viral' | 'content' | 'paid' | 'product' | 'network_effect'; /** Event that initiates the loop cycle */ trigger?: string; /** Action the user takes within the loop */ action?: string; /** Outcome that reinforces the next cycle */ reward?: string; } /** Variant entity. * * @example * const properties: VariantProperties = { * variant_name: 'B: simplified onboarding', * traffic_percentage: 42, * variant_status: 'active', * } */ interface VariantProperties { /** Display name of the experiment variant */ variant_name?: string; /** Percentage of traffic allocated to this variant */ traffic_percentage?: number; /** Outcome status of the variant */ variant_status?: 'active' | 'winner' | 'loser' | 'inactive'; } /** AttributionModel entity. * * @example * const properties: AttributionModelProperties = { * model_type: 'first_touch', * lookback_window: '90d', * } */ interface AttributionModelProperties { /** How credit is distributed across touchpoints */ model_type?: 'first_touch' | 'last_touch' | 'linear' | 'time_decay' | 'custom'; /** Time window for attributing conversions */ lookback_window?: string; } /** * UPG Property Schemas: Go-To-Market Domain. * GtmStrategy, IdealCustomerProfile, Positioning, Messaging, Launch, * ContentStrategy, SalesMotion, CompetitiveBattleCard, DemandGenProgram, * Territory, Objection, Rebuttal, ProofPoint. * https://unifiedproductgraph.org/spec | MIT */ /** GtmStrategy entity. * * @example * const properties: GtmStrategyProperties = { * primary_motion: 'product_led', * launch_date: '2026-04-01', * } */ interface GtmStrategyProperties { /** Primary go-to-market motion */ primary_motion?: 'product_led' | 'sales_led' | 'channel'; /** Planned launch date (ISO format) */ launch_date?: ISODate; } /** IdealCustomerProfile entity. * * @example * const properties: IdealCustomerProfileProperties = { * company_size: '51-200', * industry: 'Developer Tools', * budget_range: '$50K – $150K ACV', * } */ interface IdealCustomerProfileProperties { /** Target company size. Standard B2B segmentation buckets so ICPs across products are comparable. */ company_size?: '1-10' | '11-50' | '51-200' | '201-1000' | '1001-5000' | '5000+' | 'other'; /** Target industry vertical */ industry?: string; /** Expected budget range for the solution */ budget_range?: string; /** Events that signal buying readiness */ trigger_events?: string[]; /** Technologies the ideal customer already uses */ tools_used?: string[]; } /** Positioning entity. Structural facts expressed via edges (P20 hub). * * Only the statement itself lives here (textual, no natural edge target). * Structured fields go via edges: * Differentiator → `positioning_differentiates_via_value_proposition` * Category → `positioning_within_market_segment` * Competitors → `competitor` edges * Tagline → `brand_identity.tagline` * * @example * const properties: PositioningProperties = { * positioning_statement: 'For product leaders drowning in AI output, Entopo is the structured thinking space that turns scattered work into compound knowledge.', * target_summary: 'Product-led B2B SaaS teams of 10–200 people.', * } */ interface PositioningProperties { /** * Full positioning statement, typically in Geoffrey Moore's form: * "For {target audience} who {problem or need}, {product} is the {category} * that {unique benefit}. Unlike {competitor or alternative}, we {differentiator}." * Edges carry the structured atoms; this field preserves the rhetorical whole. */ positioning_statement?: string; /** * One-line audience summary. Shortcut when an edge to the full * `ideal_customer_profile` or `persona` isn't yet in place. * @example "Solo product creators drowning in AI-generated artefacts" */ target_summary?: string; } /** Messaging entity. Each instance is a channel/stage variant. * * @example * const properties: MessagingProperties = { * channel: 'landing_page', * funnel_stage: 'awareness', * headline: 'Stop losing AI output in tabs.', * } */ interface MessagingProperties { /** Channel this messaging variant is crafted for. `enablement` is the internal * field-ops channel (SE/PMM/SA enablement one-pagers, battlecards' narrative), * distinct from `pitch` (external, customer-facing). */ channel?: 'landing_page' | 'email' | 'social' | 'ad' | 'pitch' | 'press' | 'in_product' | 'enablement' | 'other'; /** Marketing funnel stage this variant targets */ funnel_stage?: 'awareness' | 'consideration' | 'conversion' | 'retention'; /** Primary headline or hook */ headline?: string; /** Full message body copy */ body?: string; /** Call-to-action text */ call_to_action?: string; /** Voice and tone for this variant (e.g. "conversational", "authoritative", "playful") */ tone?: string; } /** Launch entity. * * @example * const properties: LaunchProperties = { * launch_type: 'soft', * target_date: '2026-06-15', * success_metrics: ['Week-one activation', 'Decisions committed per team'], * } */ interface LaunchProperties { /** Scale and audience of the launch */ launch_type?: 'soft' | 'beta' | 'public' | 'feature'; /** Planned launch date (ISO format) */ target_date?: ISODate; } /** ContentStrategy entity. * * @example * const properties: ContentStrategyProperties = { * funnel_stage: 'top_of_funnel', * content_types: ['blog-post', 'case-study', 'tutorial'], * distribution_channels: ['self-serve signup', 'sales-assisted', 'partner-resold'], * } */ interface ContentStrategyProperties { /** Funnel stage this content targets */ funnel_stage?: 'top_of_funnel' | 'middle_of_funnel' | 'bottom_of_funnel'; /** Types of content to produce */ content_types?: string[]; /** Channels where content will be distributed */ distribution_channels?: string[]; /** * Publishing cadence (canonical `Cadence` since v0.4.0). * Retyped from the legacy free-form `cadence: string` (e.g. "2x/week"). For * exact rates, set `frequency_count` + `frequency_period`. * * BREAKING in v0.4.0: previous string values like `"2x/week"` no longer * type-check. Map to `'weekly'` + `frequency_count: 2` + `frequency_period: 'P7D'`. */ cadence?: Cadence; /** Exact count of publications in the period. Pairs with `frequency_period`. */ frequency_count?: number; /** Recurrence period (ISO-8601 `Duration`, e.g. `'P7D'`) */ frequency_period?: Duration; /** Qualitative rate tier when an exact rate is unknown */ frequency_rating?: FrequencyRating; } /** SalesMotion entity. * * @example * const properties: SalesMotionProperties = { * motion_type: 'self_serve', * qualification_criteria: 'Team of 5+ product people; uses at least two AI tools weekly.', * avg_deal_cycle: 'P30D', * } */ interface SalesMotionProperties { /** Level of human involvement in the sales process */ motion_type?: 'self_serve' | 'assisted' | 'enterprise'; /** Narrative qualification rule: which funnel steps and conditions define 'qualified'. */ qualification_criteria?: string; /** * Average time from first touch to closed deal. ISO-8601 duration * (e.g. `'P30D'`, `'P3M'`). Typed as `Duration` so units survive round-trip. */ avg_deal_cycle?: Duration; } /** Objection entity. * * @example * const properties: ObjectionProperties = { * statement: 'When I am reviewing a sprint, I want a one-page health summary so I can decide what to escalate.', * source_type: 'prospect', * severity: 4, * } */ interface ObjectionProperties { /** The objection as stated by the source */ statement?: string; /** Where this objection originated */ source_type?: 'prospect' | 'competitor' | 'internal' | 'market'; /** How frequently or strongly this objection comes up (1-5) */ severity?: UPGAssessment; /** Whether this objection has been addressed */ resolution?: 'open' | 'addressed' | 'invalidated'; } /** Rebuttal entity. * * @example * const properties: RebuttalProperties = { * statement: 'When I am reviewing a sprint, I want a one-page health summary so I can decide what to escalate.', * strength: 4, * evidence_refs: ['interview_007', 'interview_011'], * } */ interface RebuttalProperties { /** The counter-argument or response to an objection */ statement?: string; /** How convincing this rebuttal is (1-5) */ strength?: UPGAssessment; /** References to supporting evidence (e.g. interview or observation ids). */ evidence_refs?: string[]; } /** ProofPoint entity. * * @example * const properties: ProofPointProperties = { * statement: 'When I am reviewing a sprint, I want a one-page health summary so I can decide what to escalate.', * evidence_type: 'case_study', * source: 'Onboarding interview series, Q1 2026', * } */ interface ProofPointProperties { /** The claim or evidence statement */ statement?: string; /** Kind of evidence this proof point represents */ evidence_type?: 'case_study' | 'statistic' | 'testimonial' | 'certification' | 'award'; /** Origin of the evidence (e.g. customer name, study URL) */ source?: string; } /** CompetitiveBattleCard entity. * * @example * const properties: CompetitiveBattleCardProperties = { * win_rate: 42, * key_differentiators: 'Structured thinking primitives; open interchange format; works in your editor.', * } */ interface CompetitiveBattleCardProperties { /** Historical win rate against this competitor (0-100%) */ win_rate?: number; /** Summary of key differentiators versus this competitor */ key_differentiators?: string; } /** DemandGenProgram entity. * * @example * const properties: DemandGenProgramProperties = { * program_type: 'beta', * budget: 50000, * target_leads: 42, * } */ interface DemandGenProgramProperties { /** * Closed-set classification so demand-gen mix can be reported on a stable axis. * Use `'other'` for novel program shapes; raise a spec proposal if `'other'` recurs. */ program_type?: 'webinar' | 'content_syndication' | 'event' | 'paid_media' | 'aba' | 'beta' | 'other'; /** Allocated budget for this program */ budget?: number; /** Target number of leads to generate */ target_leads?: number; } /** Territory entity. * * @example * const properties: TerritoryProperties = { * territory_type: 'geographic', * region: 'eu-west-1', * quota: 42, * } */ interface TerritoryProperties { /** How the territory is defined */ territory_type?: 'geographic' | 'vertical' | 'account_based' | 'named'; /** Geographic region or market covered */ region?: string; /** Revenue quota for this territory */ quota?: number; } /** * UPG Property Schemas: Pricing & Packaging Domain. * PricingStrategy, DiscountStrategy, TrialConfig, Paywall. * * Note: `package` was consolidated into `pricing_tier` in v0.2.0 and moved to * `DeprecatedUPGEntityType` for string-level migration support (`migrations.ts`). * Use `PricingTierProperties` for the canonical shape. * * https://unifiedproductgraph.org/spec | MIT */ /** Pricing strategy. * * @example * const properties: PricingStrategyProperties = { * strategy_type: 'value_based', * review_cadence: 'quarterly', * last_change: '2026-02-15', * } */ interface PricingStrategyProperties { /** Pricing methodology used */ strategy_type?: 'value_based' | 'cost_plus' | 'competitor_based' | 'penetration' | 'freemium'; /** How often pricing is reviewed. Uses the shared `Cadence` scale. */ review_cadence?: Cadence; /** Date of the last pricing change (ISO format) */ last_change?: string; } /** Discount strategy. * * @example * const properties: DiscountStrategyProperties = { * discount_type: 'percentage', * discount_percentage: 25, * valid_until: '2026-12-31', * } */ interface DiscountStrategyProperties { /** How the discount is applied */ discount_type?: 'percentage' | 'fixed' | 'tiered' | 'bundle'; /** Discount amount as a percentage (0-100) */ discount_percentage?: number; /** Expiration date of the discount (ISO format) */ valid_until?: string; /** Number of times this discount has been redeemed */ redemption_count?: number; } /** Trial configuration. * * @example * const properties: TrialConfigProperties = { * trial_type: 'time_limited', * duration_days: 14, * conversion_rate: 0.08, * } */ interface TrialConfigProperties { /** How the trial is limited */ trial_type?: 'time_limited' | 'feature_limited' | 'usage_limited' | 'reverse'; /** Length of the trial period in days */ duration_days?: number; /** Percentage of trial users who convert to paid */ conversion_rate?: number; } /** Paywall. * * @example * const properties: PaywallProperties = { * paywall_type: 'hard', * trigger: 'User opens the third restricted feature in a session.', * conversion_rate: 0.08, * } */ interface PaywallProperties { /** How restrictive the paywall is */ paywall_type?: 'hard' | 'soft' | 'metered' | 'freemium_gate'; /** User action or threshold that triggers the paywall */ trigger?: string; /** Percentage of users who convert at this paywall */ conversion_rate?: number; } /** * UPG Property Schemas: Sales & Revenue Domain. * Account, Contact, Lead, Deal, PipelineSales, PipelineStage, * QuoteDocument, Subscription, Invoice, Forecast. * https://unifiedproductgraph.org/spec | MIT */ /** Sales account. * * @example * const properties: AccountProperties = { * account_type: 'prospect', * industry: 'Developer Tools', * employee_count: 42, * } */ interface AccountProperties { /** Relationship status of this account */ account_type?: 'prospect' | 'customer' | 'partner' | 'churned'; /** Industry vertical the account operates in */ industry?: string; /** Number of employees at the account */ employee_count?: number; /** * Go-to-market tier this account is served at. A segmentation *decision* * (how the field org treats the account), distinct from `employee_count` * (a raw fact) and aligned with but not identical to the ideal customer * profile's `company_size` buckets. `strategic` is the tier that escalates * to a dedicated account-plan graph (Tier 3 client model). */ segment?: 'smb' | 'mid_market' | 'enterprise' | 'strategic'; /** * Annual contract value in the account's billing currency. The single most * common enterprise account-tiering input; a snapshot that changes on * expansion or renewal. */ annual_contract_value?: number; /** * Geographic region the account is managed in (e.g. "EMEA", "NA-West"). * Free-form: territory taxonomies vary by org and are modelled structurally * via the `territory` entity when they need to be queryable. */ region?: string; } /** Contact. * * @example * const properties: ContactProperties = { * contact_role: 'champion', * } */ interface ContactProperties { /** Job title or role within the account */ contact_role?: string; /** Whether this person has purchasing authority */ is_decision_maker?: boolean; /** * Role this contact plays in the buying committee, the decision-making unit. * * @example 'economic_buyer' * * @remarks * The substrate of enterprise multi-threading and of qualification frameworks * such as MEDDICC and SPICED: a deal with no `champion` and no * `economic_buyer` mapped is single-threaded and at risk, which is a fact the * graph can answer rather than a judgement someone has to make. * * `is_decision_maker` becomes largely derivable from this * (`buying_role = economic_buyer`) but is kept for back-compat. */ buying_role?: 'champion' | 'economic_buyer' | 'technical_evaluator' | 'end_user' | 'detractor' | 'influencer' | 'procurement' | 'legal' | 'security'; } /** Lead. * * @example * const properties: LeadProperties = { * lead_score: 42, * qualification_status: 'marketing_qualified', * } */ interface LeadProperties { /** How this lead was acquired (e.g. "website", "referral", "event") */ lead_source?: string; /** Numeric scoring of lead quality */ lead_score?: number; /** Current progression status of the lead */ lead_status?: 'new' | 'contacted' | 'nurturing' | 'converted' | 'disqualified'; /** Marketing or sales qualification level */ qualification_status?: 'marketing_qualified' | 'sales_qualified' | 'product_qualified' | 'unqualified'; } /** Deal. * * @example * const properties: DealProperties = { * deal_value: 42, * close_date: '2026-04-01', * probability: 42, * } */ interface DealProperties { /** Monetary value of the deal */ deal_value?: number; /** Expected close date (ISO format) */ close_date?: ISODate; /** Likelihood of closing (0-100%) */ probability?: number; /** * Terminal result of the deal: the win or loss verdict a closed deal carries. * * @example 'won' * * @remarks * An Event-axis outcome (the verdict on a one-time event) and NOT a lifecycle * phase, so it is `deal_outcome` rather than `deal_status`, per * status-convention Rule 3. Lifecycle (open, won and lost as phases) belongs * on the base `status` slot. This field is what gives * `deal_lost_to_competitor` and win-loss study derivations their anchor. */ deal_outcome?: 'won' | 'lost' | 'no_decision'; /** * Motion this deal belongs to. Mirrors `PipelineSales.pipeline_type` at the * deal grain; `renewal` is the value `subscription_renews_via_deal` points at. * @example 'expansion' */ deal_type?: 'new_business' | 'expansion' | 'renewal'; /** * The single most-used CRM field: the next concrete action to move the deal. * Free text on purpose (a coordination note, not a structured task). * @example 'Send security questionnaire to procurement' */ next_step?: string; /** When the `next_step` is due (ISO format). */ next_step_date?: ISODate; /** * Qualification framework this deal is scored against. Names the rubric so the * `qualification_score` is interpretable; per-pillar booleans (MEDDICC's Metrics / * Economic buyer / Decision criteria / …) are deferred until `custom` recurrence * proves the demand. * @example 'meddicc' */ qualification_framework?: 'meddicc' | 'bant' | 'spiced' | 'custom'; /** * How well-qualified the deal is, on the canonical `confidence_5` scale * (UPGAssessment: numeric value plus a high/medium/low label). Coarse by * design; the granular per-pillar breakdown is deferred (see * `qualification_framework`). */ qualification_score?: UPGAssessment; /** Accountable person or team carrying the deal. Promote to a `node_owned_by_person` edge if ownership must be queryable. */ owner?: string; } /** Sales pipeline. * * @example * const properties: PipelineSalesProperties = { * pipeline_type: 'ci', * avg_cycle_days: 42, * } */ interface PipelineSalesProperties { /** Classification of the pipeline (UPG-579 Option B). */ pipeline_type?: 'new_business' | 'expansion' | 'renewal' | 'partner' | 'other'; /** Average days from opportunity creation to close */ avg_cycle_days?: number; } /** Pipeline stage. * * @example * const properties: PipelineStageProperties = { * stage_order: 42, * conversion_rate: 0.08, * avg_days_in_stage: 42, * } */ interface PipelineStageProperties { /** Position of this stage in the pipeline sequence */ stage_order?: number; /** Percentage of deals that advance from this stage */ conversion_rate?: number; /** Average number of days deals spend in this stage */ avg_days_in_stage?: number; } /** Quote document. * * @example * const properties: QuoteDocumentProperties = { * total_amount: 42, * valid_until: '2026-12-31', * currency: 'USD', * } */ interface QuoteDocumentProperties { /** Current status of the quote */ quote_status?: 'draft' | 'sent' | 'accepted' | 'rejected' | 'expired'; /** Total monetary amount of the quote */ total_amount?: number; /** Expiration date of the quote (ISO format) */ valid_until?: string; /** Currency code (e.g. "USD", "EUR") */ currency?: string; } /** Subscription. * * @example * const properties: SubscriptionProperties = { * monthly_recurring_revenue: 42, * start_date: '2026-04-01', * renewal_date: '2026-04-01', * } */ interface SubscriptionProperties { /** Monthly recurring revenue from this subscription */ monthly_recurring_revenue?: number; /** Subscription start date (ISO format) */ start_date?: ISODate; /** Next renewal date (ISO format) */ renewal_date?: ISODate; /** Current status of the subscription */ subscription_status?: 'active' | 'trialing' | 'past_due' | 'cancelled' | 'paused'; } /** Invoice. * * @example * const properties: InvoiceProperties = { * amount: 1200, * due_date: '2026-06-15', * currency: 'USD', * } */ interface InvoiceProperties { /** Current payment status of the invoice */ invoice_status?: 'draft' | 'sent' | 'paid' | 'overdue' | 'voided'; /** Total amount billed */ amount?: number; /** Payment due date (ISO format) */ due_date?: ISODate; /** Currency code (e.g. "USD", "EUR") */ currency?: string; } /** Revenue forecast. * * @example * const properties: ForecastProperties = { * forecast_period: '2026-H2', * predicted_revenue: 42, * confidence: 4, * } */ interface ForecastProperties { /** Time period the forecast covers (e.g. "Q2 2026") */ forecast_period?: string; /** Predicted revenue amount */ predicted_revenue?: number; /** Confidence in the prediction (1 = speculative, 5 = high conviction) */ confidence?: UPGAssessment; /** Forecasting methodology used */ methodology?: string; } /** * UPG Property Schemas: Marketing Operations Domain. * MarketingStrategy, MarketingChannel, MarketingCampaignPlan, EmailSequence, * SocialPost, SeoKeyword, AdCreative, PressRelease, Event, CommunityInitiative. * https://unifiedproductgraph.org/spec | MIT */ /** Marketing strategy. * * @example * const properties: MarketingStrategyProperties = { * approach: 'inbound', * annual_budget: 42, * } */ interface MarketingStrategyProperties { /** Overall marketing approach */ approach?: 'inbound' | 'outbound' | 'product_led' | 'community' | 'hybrid'; /** Total annual marketing budget */ annual_budget?: number; /** Primary objective for the marketing strategy */ objective?: string; } /** Marketing channel. * * @example * const properties: MarketingChannelProperties = { * channel_type: 'social', * monthly_budget: 42, * roi: 42, * } */ interface MarketingChannelProperties { /** Category of marketing channel */ channel_type?: 'social' | 'email' | 'seo' | 'sem' | 'content' | 'events' | 'other'; /** Monthly spend allocated to this channel */ monthly_budget?: number; /** Return on investment ratio */ roi?: number; } /** Marketing campaign plan. * * @example * const properties: MarketingCampaignPlanProperties = { * brief: 'One-page research brief for the Q2 activation study.', * budget: 50000, * start_date: '2026-04-01', * } */ interface MarketingCampaignPlanProperties { /** Campaign brief summarising objectives and approach */ brief?: string; /** Allocated budget for this campaign */ budget?: number; /** Campaign start date (ISO format) */ start_date?: ISODate; /** Campaign end date (ISO format) */ end_date?: ISODate; /** Audience segment the campaign targets */ target_segment?: string; } /** Email sequence. * * @example * const properties: EmailSequenceProperties = { * sequence_type: 'onboarding', * email_count: 42, * open_rate: 42, * } */ interface EmailSequenceProperties { /** Purpose of the email sequence */ sequence_type?: 'onboarding' | 'nurture' | 're_engagement' | 'sales' | 'other'; /** Number of emails in the sequence */ email_count?: number; /** Average open rate across the sequence (0-1) */ open_rate?: number; /** Average click-through rate across the sequence (0-1) */ click_rate?: number; } /** Social media post. * * @example * const properties: SocialPostProperties = { * platform: 'twitter', * post_type: 'text', * scheduled_date: '2026-04-01', * } */ interface SocialPostProperties { /** Social media platform */ platform?: 'twitter' | 'linkedin' | 'instagram' | 'youtube' | 'tiktok' | 'other'; /** Format of the post */ post_type?: 'text' | 'image' | 'video' | 'carousel' | 'story'; /** Date the post is scheduled to publish (ISO format) */ scheduled_date?: ISODate; } /** SEO keyword. * * @example * const properties: SeoKeywordProperties = { * keyword: 'product graph', * search_volume: 42, * difficulty: 42, * } */ interface SeoKeywordProperties { /** The keyword or phrase being targeted */ keyword: string; /** Estimated monthly search volume */ search_volume?: number; /** Keyword difficulty score (0-100) */ difficulty?: number; /** Search intent behind the keyword */ intent?: 'informational' | 'navigational' | 'commercial' | 'transactional'; /** Current SERP ranking position */ current_rank?: number; /** Desired ranking position */ target_rank?: number; } /** Ad creative. * * @example * const properties: AdCreativeProperties = { * platform: 'google', * ad_format: 'search', * headline: 'Stop losing AI output in tabs.', * } */ interface AdCreativeProperties { /** Advertising platform */ platform?: 'google' | 'meta' | 'linkedin' | 'twitter' | 'other'; /** Format of the ad unit */ ad_format?: 'search' | 'display' | 'video' | 'native' | 'social'; /** Primary headline text */ headline?: string; /** Call-to-action text */ call_to_action?: string; /** Total spend on this creative */ spend?: number; /** Total number of impressions served */ impressions?: number; /** Total number of clicks received */ clicks?: number; } /** Press release. * * @example * const properties: PressReleaseProperties = { * pr_type: 'product_launch', * publish_date: '2026-04-01', * outlets: ['TechCrunch', 'The Information', 'HN Launch'], * } */ interface PressReleaseProperties { /** Category of the press release */ pr_type?: 'product_launch' | 'partnership' | 'funding' | 'milestone' | 'other'; /** Date the release was or will be published (ISO format) */ publish_date?: ISODate; /** Media outlets targeted for distribution */ outlets?: string[]; } /** Event. * * @example * const properties: EventProperties = { * event_type: 'conference', * event_date: '2026-04-01', * location: 'Berlin, DE', * } */ interface EventProperties { /** Format of the event */ event_type?: 'conference' | 'webinar' | 'meetup' | 'workshop' | 'trade_show' | 'other'; /** Date of the event (ISO format) */ event_date?: ISODate; /** Venue or virtual platform */ location?: string; /** Number of attendees */ attendee_count?: number; } /** Community initiative. * * @example * const properties: CommunityInitiativeProperties = { * initiative_type: 'forum', * member_count: 42, * engagement_rate: 42, * } */ interface CommunityInitiativeProperties { /** Platform or format for the community */ initiative_type?: 'forum' | 'discord' | 'slack' | 'meetup' | 'ambassador' | 'other'; /** Current number of community members */ member_count?: number; /** Percentage of members actively participating */ engagement_rate?: number; } /** * UPG Property Schemas: Customer Success Domain. * SupportTicket, CustomerFeedback, ChurnReason, CustomerHealthScore, * Playbook, ServiceLevelAgreement, CustomerJourneyStage, Touchpoint, * SuccessMilestone, ServiceBlueprint. * https://unifiedproductgraph.org/spec | MIT */ /** Support ticket. * * @example * const properties: SupportTicketProperties = { * ticket_type: 'bug', * severity: 4, * resolution: 'Disable the retry loop on 4xx responses.', * } */ interface SupportTicketProperties { /** Classification of the ticket */ ticket_type?: 'bug' | 'question' | 'feature_request'; /** Impact severity of the issue (1 = cosmetic, 5 = service down) */ severity?: UPGAssessment; /** Description of how the ticket was resolved */ resolution?: string; /** Where the ticket originated (e.g. "email", "in-app", "chat") */ source?: string; /** Detected sentiment of the customer's message */ signal_sentiment?: SignalSentiment; /** Channel through which the signal was received */ signal_channel?: string; /** Perceived urgency of the customer's request */ signal_urgency?: 'low' | 'medium' | 'high' | 'critical'; } /** Customer feedback. * * @example * const properties: CustomerFeedbackProperties = { * feedback_type: 'survey', * sentiment: 'positive', * verbatim: 'I just paste everything into a doc and hope I find it again.', * } */ interface CustomerFeedbackProperties { /** How the feedback was collected */ feedback_type?: 'survey' | 'interview' | 'review' | 'nps'; /** Overall sentiment of the feedback */ sentiment?: SignalSentiment; /** Exact words from the customer */ verbatim?: string; /** Detected sentiment of the underlying signal */ signal_sentiment?: SignalSentiment; /** Channel through which the signal was received */ signal_channel?: string; /** Perceived urgency of the feedback */ signal_urgency?: 'low' | 'medium' | 'high' | 'critical'; } /** Churn reason. * * @example * const properties: ChurnReasonProperties = { * category: 'activation', * frequency_count: 12, * frequency_period: 'P30D', * contributing_factors: ['recent login flow change', 'cache invalidation bug'], * } */ interface ChurnReasonProperties { /** High-level category of the churn reason */ category?: string; /** Exact count of times this reason has been cited in `frequency_period` */ frequency_count?: number; /** The recurrence period the count is measured over (ISO-8601 `Duration`, e.g. `'P30D'`) */ frequency_period?: Duration; /** Qualitative frequency tier when an exact count is not known */ frequency_rating?: FrequencyRating; /** Other factors that contributed to the churn */ contributing_factors?: string[]; /** Detected sentiment of the churn signal */ signal_sentiment?: SignalSentiment; /** Channel through which the churn signal was received */ signal_channel?: string; /** Urgency of the churn risk */ signal_urgency?: 'low' | 'medium' | 'high' | 'critical'; } /** Customer health score. * * @example * const properties: CustomerHealthScoreProperties = { * overall_score: 42, * risk_level: 'healthy', * trend: 'improving', * } */ interface CustomerHealthScoreProperties { /** Individual metrics that compose the health score */ metrics?: Record; /** Aggregate health score (0-100) */ overall_score?: number; /** Current risk classification */ risk_level?: 'healthy' | 'at_risk' | 'red'; /** Direction the health score is moving */ trend?: 'improving' | 'stable' | 'declining'; } /** Customer success playbook. * * @example * const properties: PlaybookProperties = { * playbook_type: 'onboarding', * trigger: 'User opens the third restricted feature in a session.', * playbook_steps: ['Open ticket', 'Verify logs', 'Page on-call', 'Write postmortem'], * } */ interface PlaybookProperties { /** Scenario this playbook addresses */ playbook_type?: 'onboarding' | 'expansion' | 'renewal' | 'rescue' | 'other'; /** Condition that activates this playbook */ trigger?: string; /** Ordered list of steps to execute */ playbook_steps?: string[]; } /** Service-level agreement (SLA). The concrete obligations a service commits * to deliver against. Renamed from `SlaProperties` so the generated * property-schema key matches the canonical entity name `service_level_agreement`. * * @example * const properties: ServiceLevelAgreementProperties = { * target: 'Week-one activation ≥ 45%.', * measurement_window: 'rolling-30-days', * coverage_hours: '24x7', * } */ interface ServiceLevelAgreementProperties { /** Target value for the primary metric (e.g. "99.9%", "< 200ms p95") */ target?: string; /** Time period over which `target` is measured (e.g. "monthly", "quarterly") */ measurement_window?: string; /** Hours during which the SLA applies (e.g. "24/7", "business hours", "follow-the-sun") */ coverage_hours?: string; /** Target time to first acknowledgement of an incident (e.g. "15 minutes") */ response_time_target?: string; /** Target time to incident resolution (e.g. "4 hours" for sev-1) */ resolution_time_target?: string; /** Effective term of the agreement (e.g. "12 months", "auto-renewing annual") */ agreement_term?: string; /** ISO date effective */ effective_date?: ISODate; /** ISO date expires. Pairs with `agreement_term` for renewal logic. */ expiry_date?: ISODate; /** Party accountable on the service provider side. Promote to a `node_owned_by_team` edge if ownership must be queryable. */ owner?: string; /** What happens if the SLA is breached (credits, penalties, escalation path) */ consequence_of_breach?: string; } /** Customer journey stage. * * @example * const properties: CustomerJourneyStageProperties = { * stage_type: 'awareness', * stage_order: 0, * avg_duration: '45m', * conversion_rate: 0.08, * } */ interface CustomerJourneyStageProperties { /** Which pirate metric (AAARRR) this stage maps to */ stage_type?: 'awareness' | 'acquisition' | 'activation' | 'retention' | 'revenue' | 'referral'; /** * Display order along the lifecycle timeline (0-indexed). The `*_order` * convention shared with `journey_phase.phase_order` and * `journey_step.step_order` (UPG-675 / CS-8), so two same-`stage_type` stages * are orderable. */ stage_order?: number; /** Average time a customer spends in this stage */ avg_duration?: string; /** Percentage of customers who advance to the next stage */ conversion_rate?: number; } /** Customer touchpoint. * * @example * const properties: TouchpointProperties = { * touchpoint_channel: 'in_app', * touchpoint_type: 'reactive', * satisfaction_score: 42, * } */ interface TouchpointProperties { /** * Medium (modality) of the interaction: how it physically happens. Distinct * from a go-to-market *channel* (`marketing_channel`/`acquisition_channel`/ * `distribution_channel`), which are market routes, not interaction media. (UPG-679) */ touchpoint_channel?: 'in_app' | 'email' | 'phone' | 'chat' | 'sms' | 'in_person' | 'mail'; /** Whether the touchpoint is initiated by the customer, CSM, or system */ touchpoint_type?: 'reactive' | 'proactive' | 'automated'; /** Customer satisfaction score for this touchpoint */ satisfaction_score?: number; } /** Customer success milestone. * * @example * const properties: SuccessMilestoneProperties = { * milestone_type: 'adoption', * target_date: '2026-06-15', * } */ interface SuccessMilestoneProperties { /** Phase of the customer lifecycle this milestone tracks */ milestone_type?: 'adoption' | 'expansion' | 'renewal' | 'advocacy'; /** Target date for achieving this milestone (ISO format) */ target_date?: ISODate; /** Whether the milestone has been reached */ achieved?: boolean; } /** Service blueprint. * * @example * const properties: ServiceBlueprintProperties = { * blueprint_scope: 'End-to-end onboarding from signup to first committed decision.', * frontstage_steps: 42, * backstage_steps: 42, * } */ interface ServiceBlueprintProperties { /** What part of the service this blueprint covers */ blueprint_scope?: string; /** Number of customer-visible steps */ frontstage_steps?: number; /** Number of internal operational steps */ backstage_steps?: number; } /** * UPG Property Schemas: Localisation & i18n Domain. * Locale, TranslationKey, TranslationBundle, LocaleConfig, * CulturalAdaptation, RegionalPricing. * https://unifiedproductgraph.org/spec | MIT */ /** Locale. * * @example * const properties: LocaleProperties = { * language_code: 'en-GB', * region_code: 'eu-west-1', * is_default: true, * } */ interface LocaleProperties { /** ISO 639-1 language code (e.g. "en", "de", "ja") */ language_code: string; /** ISO 3166-1 region code (e.g. "US", "GB", "DE") */ region_code?: string; /** Whether this is the default/fallback locale */ is_default?: boolean; /** Rollout status of this locale */ /** Percentage of strings translated (0-100) */ translation_coverage?: number; } /** Translation key. * * @example * const properties: TranslationKeyProperties = { * key_path: 'billing.plan.tier', * source_text: 'I just paste everything into a doc and hope I find it again.', * context_hint: 'Hover over any node to see its outbound relationships.', * } */ interface TranslationKeyProperties { /** Dot-separated key path (e.g. "onboarding.welcome.title") */ key_path: string; /** Original source-language text */ source_text?: string; /** Contextual hint for translators */ context_hint?: string; /** Maximum character length for the translated string */ max_length?: number; } /** Translation bundle. * * @example * const properties: TranslationBundleProperties = { * bundle_scope: 'core', * last_synced: '2026-04-17T08:00:00Z', * } */ interface TranslationBundleProperties { /** * Bundle scope. Closed set of common translation bundle groupings. * Use `'other'` for product-specific module bundles. */ bundle_scope?: 'core' | 'onboarding' | 'settings' | 'errors' | 'marketing' | 'help' | 'legal' | 'other'; /** Translation completion percentage (0-100) */ /** ISO timestamp of the last sync with the translation service */ last_synced?: string; } /** Locale configuration. * * @example * const properties: LocaleConfigProperties = { * date_format: 'YYYY-MM-DD', * number_format: '0,0.00', * currency: 'USD', * } */ interface LocaleConfigProperties { /** Date formatting pattern (e.g. "MM/DD/YYYY", "DD.MM.YYYY") */ date_format?: string; /** Number formatting convention (e.g. "1,000.00", "1.000,00") */ number_format?: string; /** Default currency code for this locale */ currency?: string; /** Text direction for the locale's script */ text_direction?: 'ltr' | 'rtl'; /** Default timezone (e.g. "Europe/Berlin", "America/New_York") */ timezone?: string; } /** Cultural adaptation. * * @example * const properties: CulturalAdaptationProperties = { * adaptation_type: 'content', * rationale: 'Reduces support burden and lifts activation, both priorities this quarter.', * } */ interface CulturalAdaptationProperties { /** Aspect of the product being adapted */ adaptation_type?: 'content' | 'imagery' | 'ux' | 'legal' | 'payment'; /** Market or region the adaptation targets */ /** Reason for the cultural adaptation */ rationale?: string; } /** Regional pricing. * * @example * const properties: RegionalPricingProperties = { * currency: 'USD', * price_override: 42, * ppp_factor: 42, * } */ interface RegionalPricingProperties { /** Local currency code */ currency?: string; /** Overridden price for this region */ price_override?: number; /** Purchasing power parity adjustment factor */ ppp_factor?: number; /** Date the regional pricing takes effect (ISO format) */ effective_date?: ISODate; } /** * UPG Property Schemas: Customer Education Domain. * EducationProgram, Tutorial, Walkthrough, Webinar, Certification, * HelpVideo, LearningPath. * https://unifiedproductgraph.org/spec | MIT */ /** Education program. * * @example * const properties: EducationProgramProperties = { * program_type: 'onboarding', * } */ interface EducationProgramProperties { /** Purpose of the education program */ program_type?: 'onboarding' | 'certification' | 'ongoing' | 'partner'; } /** Tutorial. * * @example * const properties: TutorialProperties = { * tutorial_format: 'written', * difficulty: 'beginner', * duration_minutes: 45, * } */ interface TutorialProperties { /** Delivery format of the tutorial */ tutorial_format?: 'written' | 'video' | 'interactive' | 'code_along'; /** Skill level required to follow the tutorial */ difficulty?: DifficultyLevel; /** Estimated time to complete in minutes */ duration_minutes?: number; /** Percentage of users who complete the tutorial */ completion_rate?: number; } /** Product walkthrough. * * @example * const properties: WalkthroughProperties = { * walkthrough_type: 'product_tour', * step_count: 42, * trigger: 'User opens the third restricted feature in a session.', * } */ interface WalkthroughProperties { /** Format of the in-product walkthrough */ walkthrough_type?: 'product_tour' | 'feature_intro' | 'tooltip_sequence' | 'checklist'; /** Number of steps in the walkthrough */ step_count?: number; /** User action or event that starts the walkthrough */ trigger?: string; /** Percentage of users who complete the walkthrough */ completion_rate?: number; } /** Webinar. * * @example * const properties: WebinarProperties = { * webinar_type: 'live', * scheduled_date: '2026-04-01', * duration_minutes: 45, * } */ interface WebinarProperties { /** Delivery format of the webinar */ webinar_type?: 'live' | 'recorded' | 'hybrid'; /** Date the webinar is scheduled (ISO format) */ scheduled_date?: ISODate; /** Duration of the webinar in minutes */ duration_minutes?: number; /** Number of people who registered */ registrations?: number; /** Number of people who attended */ attendance?: number; } /** Certification. * * @example * const properties: CertificationProperties = { * cert_level: 'foundation', * requirements: ['Encrypt at rest', 'Audit every access'], * validity_months: 42, * } */ interface CertificationProperties { /** Difficulty level of the certification */ cert_level?: 'foundation' | 'practitioner' | 'expert'; /** Prerequisites or requirements to earn the certification */ requirements?: string[]; /** How long the certification is valid in months */ validity_months?: number; /** Number of people currently holding this certification */ holders?: number; } /** Help video. * * @example * const properties: HelpVideoProperties = { * video_type: 'how_to', * duration_seconds: 42, * views: 42, * } */ interface HelpVideoProperties { /** Purpose of the video */ video_type?: 'how_to' | 'overview' | 'troubleshooting' | 'best_practice'; /** Duration of the video in seconds */ duration_seconds?: number; /** Total number of views */ views?: number; /** URL where the video is hosted */ url?: string; } /** Learning path. * * @example * const properties: LearningPathProperties = { * path_difficulty: 'beginner', * item_count: 42, * estimated_hours: 42, * } */ interface LearningPathProperties { /** * Display order of this path within a curriculum or program (0-indexed). The * scalar ordering convention shared with `journey_step.step_order` and * `journey_action.action_order` (UPG-663 / UPG-674). Makes a learning_path a * deterministically orderable sequence among sibling paths rather than a star. */ path_order?: number; /** Overall difficulty level of the learning path */ path_difficulty?: DifficultyLevel; /** Number of items (tutorials, videos, etc.) in the path */ item_count?: number; /** Estimated total hours to complete the path */ estimated_hours?: number; /** Percentage of users who complete the full path */ completion_rate?: number; } /** * UPG Property Schemas: Partners & Ecosystem Domain. * PartnerProgram, PartnerTier, ApiEcosystem, MarketplaceListing, * DeveloperPortal, IntegrationPartner, PartnerRevenueShare. * https://unifiedproductgraph.org/spec | MIT */ /** Partner program. * * @example * const properties: PartnerProgramProperties = { * program_type: 'referral', * } */ interface PartnerProgramProperties { /** Category of the partner program */ program_type?: 'referral' | 'reseller' | 'technology' | 'consulting' | 'marketplace'; } /** Partner tier. * * @example * const properties: PartnerTierProperties = { * tier_level: 42, * requirements: ['Encrypt at rest', 'Audit every access'], * benefits: ['Single source of truth', 'Lower onboarding cost'], * } */ interface PartnerTierProperties { /** * Display order of this tier among sibling partner tiers (1 = first tier * shown). Mirrors `pricing_tier.tier_order`; part of the spec-wide `*_order` * sequence convention (UPG-663 / UPG-674). Distinct from `tier_level`, which * is a prestige rank, not a presentation order. */ tier_order?: number; /** Numeric rank of the tier (higher = more prestigious) */ tier_level?: number; /** What a partner must achieve to reach this tier */ requirements?: string[]; /** Benefits granted at this tier level */ benefits?: string[]; } /** API ecosystem. * * @example * const properties: ApiEcosystemProperties = { * api_style: 'rest', * developer_count: 42, * app_count: 42, * } */ interface ApiEcosystemProperties { /** Primary API architecture style */ api_style?: 'rest' | 'graphql' | 'grpc' | 'webhook' | 'mixed'; /** Number of registered developers */ developer_count?: number; /** Number of apps built on the API */ app_count?: number; } /** Marketplace listing. * * @example * const properties: MarketplaceListingProperties = { * listing_type: 'app', * installs: 42, * rating: 42, * } */ interface MarketplaceListingProperties { /** Category of the marketplace listing */ listing_type?: 'app' | 'integration' | 'template' | 'plugin'; /** Number of installations */ installs?: number; /** Average user rating (e.g. 1-5 stars) */ rating?: number; } /** Developer portal. * * @example * const properties: DeveloperPortalProperties = { * portal_url: 'https://partners.entopo.app', * doc_count: 42, * sandbox_available: true, * } */ interface DeveloperPortalProperties { /** URL of the developer portal */ portal_url?: string; /** Number of documentation pages */ doc_count?: number; /** Whether a sandbox environment is available */ sandbox_available?: boolean; } /** Integration partner. * * @example * const properties: IntegrationPartnerProperties = { * integration_type: 'native', * } */ interface IntegrationPartnerProperties { /** Name of the integration partner */ /** How the integration is implemented */ integration_type?: 'native' | 'webhook' | 'api' | 'embedded'; } /** Partner revenue share. * * @example * const properties: PartnerRevenueShareProperties = { * share_model: 'percentage', * share_percentage: 42, * annual_revenue: 42, * } */ interface PartnerRevenueShareProperties { /** How revenue is split with the partner */ share_model?: 'percentage' | 'flat_fee' | 'tiered' | 'hybrid'; /** Partner's share as a percentage (0-100) */ share_percentage?: number; /** Annual revenue generated through this partner */ annual_revenue?: number; } /** * UPG v0.2 Workspace Properties. * https://unifiedproductgraph.org/spec | MIT */ /** Workspace: a spatial thinking space for arranging entities. * * A transient or durable canvas where a team arranges, relates, and debates * entities before committing them to the wider product graph. Distinct from * `feature_area` (which structures shipped product) and `team` (the people * unit): a workspace is the *space* a team thinks in. * * Per UPG principle P14, structural relationships are edges: * parent anchor: `product_thinks_in_workspace` (or its `organization_` / * `product_area_` siblings, one per altitude) * members: `team_works_in_workspace` / `persona_collaborates_in_workspace` * (the `member_count` and `owner` properties are display-time aggregates; * canonical membership is the edge set) * contained entities: `workspace_arranges_node`, whose edge `properties` * carry the spatial placement (`x`, `y`, `expanded`, `frame_id`) * * Two meanings of "workspace" travel together in shipped tools and are * unrelated. THIS is the CANVAS workspace: a thinking space, an entity in the * graph. The FILE workspace is a folder of `.upg` products, addressed by the * CLI and MCP surfaces (`init_workspace`, `switch_product`, `workspace.json`). * A third, `UPGSource.workspace_id`, is the source tool's own project id. * Neither is renamed: all three are entrenched, and the ambiguity is smaller * than the rename would be. * * @example * const properties: WorkspaceProperties = { * visibility: 'shared', * purpose: 'Arrange personas, jobs, and opportunities for the Q2 discovery sprint.', * workspace_purpose: 'discovery', * owner: 'sam.patel@arkheiev.com', * member_count: 5, * archived: false, * archived_at: undefined, * icon: 'compass', * } */ interface WorkspaceProperties extends UPGQueryDrivenLayer { /** Who can see this workspace */ visibility?: 'private' | 'shared' | 'public'; /** Free-text description. Pairs with the closed-enum `workspace_purpose`. */ purpose?: string; /** * What the workspace is for. Drives template suggestions and surfaces in * workspace browsers. * * @remarks * `discovery` is persona, job and opportunity exploration. `planning` covers * roadmap and decision sessions. `retrospective` is reflection on shipped * work. `design` is experience or UI exploration. `research` organises study * data and synthesis. `strategy` is high-level direction setting. `general` * is the catch-all. */ workspace_purpose?: 'discovery' | 'planning' | 'retrospective' | 'design' | 'research' | 'strategy' | 'general'; /** Workspace owner (handle or email). Display label; canonical owner is `team_owns_workspace` or `persona_owns_workspace`. */ owner?: string; /** Snapshot count. `team_works_in_workspace` edges are the source of truth. */ member_count?: number; /** * Archived. Archived workspaces remain queryable but hidden from default views. * @deprecated since 0.32.0. Use `UPGBaseNode.archived`, which generalises this * pair to every entity type. Field data showed the archived/status split is not * a workspace peculiarity: a tracker held 559 archived-completed items beside 18 * live-completed ones, and one status field has nowhere to put the difference. * `UPG_PROPERTY_MIGRATIONS['0.32.0']` lifts this value to the top level. */ archived?: boolean; /** * ISO timestamp archived. Pairs with `archived === true`. * @deprecated since 0.32.0. Use `UPGBaseNode.archived_at`. Lifted by * `UPG_PROPERTY_MIGRATIONS['0.32.0']`. */ archived_at?: ISODateTime; /** Display icon (emoji or icon name) */ icon?: string; /** * Retention intent. Absent means `transient`. * * @remarks * A workspace is a free-form thinking space and is transient by default, so * the spec makes both intents EXPRESSIBLE and takes no position on which * reaches the file. Whether to write a transient canvas at all is a tool * decision, and the recommended posture is not to: a scratch canvas nobody * named has no business in a shared, git-tracked graph, where it lands in * everyone's diff. Writing only `durable` workspaces is what keeps a * canonical graph entity comfortable with a transient-by-default object. */ retention?: 'transient' | 'durable'; /** * Opaque canvas furniture: the parts of a canvas with no graph referent. * Preserved verbatim on round-trip and never interpreted. Tool extension keys * are namespaced `:` with a colon (the rule since 0.31.0), and no * consumer interprets a key it does not own. * * @remarks * The cut here is UPG principle P14 applied literally. Anything that is a * REFERENCE TO A GRAPH NODE is an edge, which is why placed entities ride * `workspace_arranges_node` and not this bag: a node id held as a scalar * inside a blob is a foreign key in disguise, and a deleted node would leave * a stale reference nothing can detect. Anything that is pure UI chrome with * no graph referent stays here, because minting `annotation` and `frame` * entity types would add catalog surface that is meaningless to every * consumer outside the tool that drew it. * * WHY THE COLON, and why the rule is in the summary rather than buried here. * An underscore key is indistinguishable from an ordinary property name, so a * migration that targets namespaced keys cannot match it and no validator can * detect one that should have been namespaced. That undetectability is why * enforcement lives in the type and the documentation instead of in a check: * a check would report clean on a bag full of underscore keys, which is worse * than no check. Two conventions were already coexisting when this was ruled * (`entopo_views` in a live writer, `entopo:view_blocks` in the contract test * certifying preservation), which is how a cheap rule becomes a migration. * * PRESERVATION IS NOT PERMISSION TO RENDER. Preserving every byte says nothing * about meaning: a field-measured canvas carried `excluded: true` tombstones, * and a consumer that preserved them faithfully while rendering every entry * showed the user images they had deleted. */ canvas?: WorkspaceCanvas; } /** The opaque half of a canvas: furniture with no graph referent. * * Carried on `WorkspaceProperties.canvas`. A consumer MUST preserve this object * verbatim and MUST NOT act on it. That preservation is already guaranteed by * the canonical serialiser, which passes `properties` through as an open bag, * so honouring the rule costs nothing; it just needs saying. * * @example * const canvas: WorkspaceCanvas = { * canvas_version: 1, * viewport: { x: 0, y: 0, zoom: 1 }, * } */ interface WorkspaceCanvas { /** Schema version of this bag, versioned by the tool that writes it and independent of the spec version. */ canvas_version: number; /** Free-text note cards placed on the canvas. */ annotations?: Array<{ id: string; text: string; color?: string; x: number; y: number; }>; /** Visual grouping containers. `children` holds the ids of arranged nodes inside the frame. */ frames?: Array<{ id: string; label: string; x: number; y: number; width: number; height: number; children?: string[]; }>; /** * Lightweight visual associations drawn between two cards. * * @remarks * Called RIDGES, not edges, and the rename is the point. Inside a `.upg` file * the word "edge" already means a graph edge, and a ridge is emphatically not * one: `promoted` exists precisely to mark the moment a ridge has BECOME a * real edge. Two different things under one word in the file format would be * a genuine ambiguity, and removing it costs nothing. */ ridges?: Array<{ id: string; source: string; target: string; label?: string; promoted?: boolean; }>; /** Last camera position on the canvas. */ viewport?: { x: number; y: number; zoom: number; }; /** * Canvas cards that are not yet entities in the graph. * * @remarks * A draft stays here rather than being materialised as a `status: 'draft'` * node because a draft is BY DEFINITION not in the graph yet. That is what * committing means, and `workspace_produced_node` exists to record the moment * it enters. Auto-materialising would empty that edge of meaning and litter * graphs with abandoned drafts. */ drafts?: Array<{ id: string; type: string; title: string; description?: string; x: number; y: number; }>; /** Seed provenance, when the canvas was opened from a gap prompt rather than blank. */ gap_context?: { scope_label: string; region_slug?: string; note?: string; gaps: Array<{ type: string; label: string; count?: number; detail?: string; }>; }; /** * Tool-namespaced extension keys, written by a tool and preserved by everyone. * * The doc above has always promised an open bag; until 0.31.0 this interface * closed, so every doc-honouring consumer widened locally (`graph-service` * shipped `WorkspaceCanvas & Record` as its boundary type). A * type that forces every conforming consumer to work around it is describing * the wrong thing, so the type now says what the doc always did. * * THE RULE FOR THESE KEYS (0.31.0; the one-release read tolerance for the old * underscore form was retired at 0.32.0, which is a different fact about a * different release and is why the two dates are both correct): * - Syntax is `:`, with a COLON. An underscore key is indistinguishable * from an ordinary property name, so it cannot be reliably matched, which is * exactly what makes a future migration miss half of what it targets. * - Every consumer PRESERVES unknown keys byte for byte. * - No consumer INTERPRETS a key it does not own. Preservation is a storage * guarantee that says nothing about meaning: a field-measured canvas carried * `excluded: true` tombstones, and a consumer that preserved every byte while * rendering every entry showed the user images they had deleted. * - The prefix is the owner. No registry at present scale. * * The declared members above keep their exact types, so the enumeration guard * is unaffected: adding a known layer still breaks exhaustive call sites at * compile time. * * THE COLON IS IN THE TYPE, and this is the only place the syntax rule is * enforceable at all. No runtime check can catch a key that should have been * namespaced and was not, because an underscore key is indistinguishable from * an ordinary property by construction, and a write-time rejection would * contradict the preservation guarantee the bag is built on. A bare * `[key: string]: unknown` would have accepted every form the rule forbids, so * the pattern index signature is what makes the rule cost anything. * * The declared members are exempt because none of them matches the pattern, * which is the mechanism working rather than a hole in it: they are the keys * the spec owns, and the pattern governs the keys it does not. * * The cost paid for the openness is narrower than the old closure but real: a * misspelled namespaced key resolves to `unknown` and surfaces at first use * rather than at the typo. A misspelled DECLARED member (`canvas.anotations`) * is still a compile error, because it does not match the pattern either. */ [namespacedKey: `${string}:${string}`]: unknown; } /** Composition: a named, published view assembled from a canvas. * * The durable counterpart to `workspace`. Where a workspace is a free-form * thinking space with an audience of one, a composition is a stable artifact * with a slug people link to, a publisher, and a revision history. Its node * `id` IS the slug: `.upg` ids are slugs by convention, and minting a separate * `slug` property would give one thing two identities. * * SCOPE OF THAT RULE, stated because assuming it generalised produced a * `not_found` refusal in the field (the invariant working, the documentation * failing): id-is-slug holds for `composition` and `workspace`. It does NOT hold * for `product`, which carries a uuid id and a separate slug. A rule whose scope * is undocumented gets over-generalised by the next reader. * * THE ARRANGEMENT IS FROZEN, THE CONTENT IS NOT. `members` captures layout and * pointers at publish time and never resolved data. Each member's `href` is * still a QUERY, re-resolved against current graph data at render, which is * what makes a composition a live UPG surface rather than a stale screenshot. * A consumer must not treat `members` as cached content. * * Provenance reuses `workspace_produced_node`: which canvas a composition was * published from is already expressible, so nothing new is minted for it. What * a composition SHOWS is `composition_focuses_node`, which is what makes * "which published views show this persona?" answerable to a tool that cannot * parse the publishing tool's URLs. * * @example * const properties: CompositionProperties = { * members: [ * { id: 'blk_1', href: '/view/personas?segment=smb', title: 'SMB personas', x: 0, y: 0, width: 6, height: 4 }, * ], * rev: 3, * published_at: '2026-08-17T09:00:00Z', * published_by: 'sam.patel@arkheiev.com', * } */ /** One predicate over a type-specific property. * * @example * const p: UPGViewPredicate = { property: 'priority', in: ['urgent', 'high'] } */ interface UPGViewPredicate { /** Property name, resolved against the node's `properties` bag. */ property: string; /** Admitted values. A node matches when its value is one of these. */ in?: string[]; /** Match on presence rather than value. `true` admits nodes that carry the * property at all; `false` admits those that do not. */ present?: boolean; } /** The axis a clause selects on. */ type UPGViewDimension = 'type' | 'status' | 'status_category' | 'tag' | 'classification' | 'property' | 'date' | 'edge'; /** A relative or absolute time window, DECLARED rather than resolved. * * @remarks * A window is evaluated at READ time in the reader's session, never frozen at * save. A saved view that says "this quarter" must mean this quarter to whoever * opens it, which an absolute range captured at save cannot do. * * THE CALENDAR AND THE CADENCE ARE NAMED SEPARATELY AND DELIBERATELY. `calendar` * is the wall clock. A team's own cadence is NOT a window at all: it resolves * through the ACTIVE-CYCLE DESIGNATION, which is an edge clause with * `target_status: ['active']`, not a date bracket. Field data settles why: of 19 * cycles in a real tracker every one is dateless and exactly one carries status * `active`, so a date-bracketed reading of "current cycle" resolves to nothing on * the only graph that has real cycle data. Conflating them makes the filter lie. * * @example * const w: UPGTimeWindow = { kind: 'calendar', anchor: 'current', unit: 'quarter' } */ type UPGTimeWindow = { kind: 'calendar'; anchor: 'current' | 'previous' | 'next'; unit: 'week' | 'month' | 'quarter' | 'year'; } | { kind: 'rolling'; anchor: 'last_n' | 'next_n'; unit: 'day' | 'week' | 'month' | 'quarter'; count: number; } | { kind: 'absolute'; from?: ISODate; to?: ISODate; }; /** A condition on a node's edges: the gap 0.32.0 opened by shipping assignment * and cadence AS EDGES alongside a query that could not mention one. * * @remarks * `UPGViewQuery.from_focus` does not cover this. It walks from the composition's * focused set, so it can say "everything under this epic" and cannot say * "assigned to anyone" or "in the active cycle". * * TWO AXES NAME A MOVING TARGET WITHOUT HOLDING AN ID, and they are deliberately * parallel. On the CADENCE axis it is `target_status`: the active cycle is the * one whose status is `active` (the invariant stated on * `PlanningCycleProperties`), so an edge clause over * `planning_cycle_schedules_work_item` with `target_status: ['active']` selects * the current cycle portably. On the PERSON axis it is `target_designation`: * `'viewer'` selects whoever is reading, resolved in the reader's session. * * `target_ids` is the exception and is admitted knowingly, on the same terms as * `UPGViewQuery.classified_as`. * * @example * const cycle: UPGViewEdgeClause = { * edge_type: 'planning_cycle_schedules_work_item', * direction: 'in', * target_status: ['active'], * } * * @example * const mine: UPGViewEdgeClause = { * edge_type: 'node_assigned_to_person', * direction: 'out', * target_designation: 'viewer', * } */ interface UPGViewEdgeClause { /** Canonical edge type. */ edge_type: string; /** Which way to walk it from the candidate node. */ direction: 'out' | 'in' | 'both'; /** Admitted endpoint ids. Omitted means any edge of this type satisfies it. */ target_ids?: string[]; /** Admitted endpoint phase ids. The designation form on the cadence axis. */ target_status?: string[]; /** * Selects the endpoint by ROLE rather than by identity. `'viewer'` is whoever * is reading the view, resolved in the reader's session at READ time. * * @remarks * WHY THIS IS NOT AN ID, and it is the whole point of the field. A saved view * that means "assigned to me" cannot store an id, because the id it stores is * one particular person and the view is then permanently about a colleague. A * shipped surface reached for the sentinel `'@me'` inside `target_ids`, which * round-trips and whose MEANING does not travel: a consumer that has not * agreed to that sentinel reads it as a node id and resolves nothing. A * sentinel in an id field is a private protocol wearing a public shape. * * THE PARALLEL IS DELIBERATE. This is the person axis' answer to the same * question `target_status: ['active']` answers on the cadence axis: name the * moving target by what it IS to the reader, not by which row it happens to be * today. Both resolve at read time, both are portable, and neither holds a * foreign key in a scalar. * * DESIGNATION, NOT SERIALISATION. A tool may serialise this however it likes * at its own boundary, `'@me'` included; the spec stores the designation. A * closed union rather than a free string, because an open one is how the * sentinel arose in the first place. */ target_designation?: 'viewer'; } /** One clause of a selection: the faithful form. * * It can express negation, declared time windows and edge conditions, none of * which the named fields on {@link UPGViewQuery} can hold. * * @remarks * A named-field shape cannot carry per-clause negation without a `not_*` twin * for every field, which is combinatorial and grows with every dimension added, * and which still could not express "not (A and B)" as distinct from "(not A) * and (not B)". A uniform clause list carries it with one flag. * * A DISCRIMINATED UNION ON `dimension` SINCE 0.34.0, and the reason is that the * two tiers disagreed about types. `values` was `string[]` while the shorthand * `UPGViewQuery.types` is `UPGEntityType[]`, so the FAITHFUL tier admitted a * value the SHORTHAND tier could not describe, and a round-trip through the * shorthand could silently narrow what the clause list said. The shipped example * `{ dimension: 'type', values: ['task', 'bug'] }` puts both halves on one line. * The field evidence is a cast at exactly that boundary in the first consumer, * carrying the comment "the clause list is the authority" — a cast is the tell * that the author knew the tiers disagreed and had to assert past it. * * THIS IS A COMPILE-TIME BREAK AND CALLING IT ADDITIVE WOULD BE DISHONEST. The * WIRE FORMAT does not change at all: the JSON is identical, so there is no * migration, no fixture and no canonical-format entry. What changes is that a * TypeScript consumer writing a plain `string[]` on a `type` clause stops * compiling. Two measured facts make that acceptable: the field shipped ONE * release ago, and the only known consumer already casts at this exact boundary, * so the break lands where someone has already written down why the tiers * disagree. A stated break with one known site is better than a silent narrowing * with none, which is what shipping nothing preserves. * * THE ROUND-TRIP NARROWING RULE (normative, and it holds regardless of the * union, because a union constrains AUTHORS and not JSON arriving from a file). * The clause list is AUTHORITATIVE. The named shorthand fields on * {@link UPGViewQuery} are a positive-only PROJECTION of it. A consumer that * rewrites clauses into shorthand and back MUST NOT narrow the admitted set, and * MUST REFUSE rather than narrow when it cannot represent a clause. Silently * dropping what it cannot express is the one behaviour this rule forbids. * * @example * const c: UPGViewClause = { dimension: 'tag', values: ['spike'], negate: true } * * @example * const t: UPGViewClause = { dimension: 'type', values: ['task', 'bug'] } */ interface UPGViewClauseBase { /** Property name when `dimension` is `property`; the date field when `date`. */ field?: string; /** Present exactly when `dimension` is `date`. */ window?: UPGTimeWindow; /** Present exactly when `dimension` is `edge`. */ edge?: UPGViewEdgeClause; /** Negates this clause and only this clause. */ negate?: boolean; } /** A clause on the `type` axis. `values` are entity types, not free strings. */ interface UPGViewTypeClause extends UPGViewClauseBase { dimension: 'type'; /** Admitted entity types. */ values?: UPGEntityType[]; } /** A clause on any axis other than `type`, whose admitted values are strings. */ interface UPGViewGenericClause extends UPGViewClauseBase { dimension: Exclude; /** Admitted values. */ values?: string[]; } type UPGViewClause = UPGViewTypeClause | UPGViewGenericClause; /** A declarative, portable selection over the graph. * * Selection ONLY. What a surface should look like once the nodes are chosen is * {@link UPGViewPresentation}, and a consumer may ignore all of it. * * @remarks * WHY THIS IS IN THE SPEC AND NOT IN A NAMESPACED BAG KEY. The 0.31.0 rule says * no consumer interprets a key it does not own. A driving query living in * `sometool:gallery_query` would therefore be readable by exactly one tool, so * the layer it drives could be rendered by exactly one tool. A query-driven * layer whose query nothing else can read is a feature, not a format — and * "views are queries" would stop being a property of the standard and become a * property of one application. * * P14 AND THE ONE PLACE THIS DESIGN TOUCHES THE LINE. A predicate over types, * phases, buckets and tag strings holds no node references, so it is a * predicate rather than a foreign key. `from_focus` keeps it that way for * relative selections: the anchor set is the `composition_focuses_node` EDGE, * and the query only says how to walk from it — which is how "everything under * this epic" is expressed without a node id in a scalar. `classified_as` is the * exception and is admitted knowingly: it holds `classification_value` ids, * because the alternative is matching a taxonomy by title, which is the thing * P14 exists to stop. Those ids are resolvable references that * `repair_dangling_edges` does not cover, and a consumer meeting one that no * longer resolves should drop the clause rather than the view. * * @example * const q: UPGViewQuery = { * types: ['task', 'bug'], * status_category: ['unstarted', 'started'], * properties: [{ property: 'priority', in: ['urgent', 'high'] }], * } */ interface UPGViewQuery { /** Entity types admitted. Omitted means every type. */ types?: UPGEntityType[]; /** Canonical phase ids admitted (e.g. `['todo', 'in_progress']`). */ status?: string[]; /** Six-bucket categories admitted. The portable form when the phase ids * differ per type but the reading is the same. */ status_category?: StatusCategory[]; /** Freeform tags. `match` governs all-of versus any-of. */ tags?: string[]; /** Ids of `classification_value` nodes admitted: the grouped-label clause. */ classified_as?: string[]; /** Predicates over type-specific properties. */ properties?: UPGViewPredicate[]; /** Whether archived nodes are admitted. Absent means false, which is the * documented default read for `UPGBaseNode.archived`. */ include_archived?: boolean; /** How the clauses combine. Absent means `all`. */ match?: 'all' | 'any'; /** Walk from the composition's focused nodes, for a selection that is * relative rather than absolute. The anchor is the * `composition_focuses_node` edge set, never an id held here. */ from_focus?: { /** Canonical edge types to traverse. */ edge_types: string[]; /** Which way to walk them. */ direction: 'out' | 'in' | 'both'; /** * Hops. Absent means 1. `'unbounded'` walks the relation transitively until * it stops producing new nodes. * * @remarks * WHY A NAMED ARM RATHER THAN A LARGE NUMBER. A relative selection over a * tree ("everything under this epic") is transitive by nature and has no * correct finite depth: the right answer is a property of the data, not of * the query. A surface that wants transitivity and has only a number picks a * big one, and a shipped one picked 64. That is a sentinel, it is * indistinguishable from a caller who genuinely meant 64, and it silently * truncates the first graph deeper than the guess. * * A consumer that cannot walk transitively should refuse the clause rather * than substitute a depth of its own choosing, which would quietly return a * different answer to the question asked. */ depth?: number | 'unbounded'; }; /** * The faithful representation of the selection: every clause, including the * negations, declared windows and edge conditions the named fields above * cannot hold. * * @remarks * PRECEDENCE. A reader that finds `clauses` uses it and IGNORES the named * fields. A reader that finds only named fields lifts them into clauses. A * writer emitting both keeps the named fields positive-only, because they have * nowhere to put a negation. * * THE NAMED FIELDS ARE NOT DEPRECATED. They are the readable form of the common * case and most selections never need a clause. This is one canonical form plus * a positive-only shorthand with a stated precedence rule, which is the shape a * field application arrived at independently for the same reason. * * A `date` clause over `created_at` or `updated_at` reads store metadata, which * is declared on `UPGBaseNode` and tagged `@volatile`. Such a window is * portable but the values it reads are maintained by the store rather than * authored. * * @example * [ * { dimension: 'type', values: ['task', 'bug'] }, * { dimension: 'tag', values: ['spike'], negate: true }, * { dimension: 'edge', edge: { edge_type: 'node_assigned_to_person', direction: 'out' } }, * ] */ clauses?: UPGViewClause[]; } /** One grouping axis: a value partition, or an edge dimension. * * The string form is a property name, base field, or `status_category`, * partitioning members on a value. The object form lanes members by an EDGE: * each lane is a far-end neighbour of the named edge type ("board laned by * assignee" is `{ dimension: 'edge', edge_type: 'node_assigned_to_person' }`). * Lane identity is the neighbour node's id and its label is that node's title. * * @remarks * The object form mirrors the edge grammar the QUERY side already speaks * (`UPGViewClause`'s `dimension: 'edge'`) rather than minting a second one. * `direction` is which way the edge is walked FROM THE MEMBER: `'out'` (absent * means `'out'`) lanes a member by the targets of its outgoing edges, `'in'` * by the sources of its incoming ones. Direction is explicit in the object * form from day one, never guessed, which is the lesson `nest_by`'s bare-name * orientation rule taught (F-7). The failure case is stated rather than left * to the renderer: a member with NO such edge lands in a `none` lane, never * dropped, for the same reason `orphan_disposition` defaults to `root`. */ type UPGViewAxis = string | { dimension: 'edge'; /** Canonical edge type whose far-end neighbour identifies the lane. */ edge_type: string; /** Which way the edge is walked from the member. Absent means `'out'`. */ direction?: 'out' | 'in'; }; /** One `nest_by` entry: a bare edge-type name, or the explicit-orientation form. * * @remarks * A bare name binds in catalog-declared orientation, source = parent (the * normative rule, F-7). The object form exists for the intent a bare name * cannot state: nesting children under the edge's TARGET end. `parent` names * which declared endpoint of the edge is the tree parent; a bare string is * exactly `{ parent: 'source' }`. */ type UPGViewNestEntry = string | { edge_type: string; parent: 'source' | 'target'; }; /** Where a tree roots: the product node, every member of a type, or the focus set. * * @remarks * Root choice is PRESENTATION, not scope (ruled 2026-09-01, F-8): it never * changes the selected set, only which already-selected members are drawn as * the tree's tops, and `orphan_disposition`'s absent-means-`root` default is * what keeps it safely ignorable. `'focus'` roots at the composition's * `composition_focuses_node` edge set, the same portable anchor `from_focus` * uses. Absent means `'product'`, which states the fallback consumers already * applied. A portable root is also what makes an authored * `orphan_disposition: 'hide'` safe to honour: measured on the registry, 8 of * 57 saved trees collapsed to a single node on bare read-back precisely * because they hid orphans while their non-product root went unstated. */ type UPGViewTreeRoot = { kind: 'product'; } | { kind: 'type'; type: string; } | { kind: 'focus'; }; /** Advisory rendering intent. A consumer MAY ignore every field here. * * @remarks * Kept separate from {@link UPGViewQuery} so that "views are queries" stays * literally true. A selection is a fact about the graph and travels; a lane * arrangement is a preference of the tool that drew it and does not. Splitting * them means a consumer that cannot honour the presentation still renders the * right nodes, which is the failure mode worth designing for. * * @example * const p: UPGViewPresentation = { group_by: 'status_category', layout: 'board' } */ interface UPGViewPresentation { /** The lane (column) axis. A property/base-field/`status_category` string, * or an edge dimension (see {@link UPGViewAxis}). */ group_by?: UPGViewAxis; /** * The row axis, making the board a two-axis grid (`rows_by` × `group_by`). * Absent means a one-axis board, which is exactly what every pre-0.37.0 * view already meant. * * @remarks * A named second slot rather than an axes array, deliberately: a grid is * two-dimensional, the generality of an array is unearned, and an array * would put the first axis in two homes beside `group_by`, which is the * shadow-pair shape the property-fit cleanup exists to kill. Dropping this * field degrades `priority × status` into a flat status board: same nodes, * but the dimension the author was REASONING in is gone, which is why the * field is portable rather than app-local. */ rows_by?: UPGViewAxis; /** Sort keys in precedence order. */ sort?: Array<{ key: string; direction: 'asc' | 'desc'; }>; /** Requested layout family. Advisory. */ layout?: 'board' | 'table' | 'list' | 'cards' | 'timeline' | 'gallery' | 'tree'; /** * Lane keys in display order, leftmost first. Lane keys are the `group_by` * values (node ids when the axis is an edge dimension). * * @remarks * The presentation split's own charter names this case: a lane arrangement * is a preference of the tool that drew it. Same class as `sort`. The * failure case is stated: lanes not listed here follow the listed ones in * natural order, so a new value never vanishes for being unlisted. */ lane_order?: string[]; /** * Lane keys drawn collapsed. Advisory; a consumer that ignores this renders * every lane expanded, which is the safe direction (it shows more, never * hides). * * @remarks * Not ephemeral-UI territory: `workspace_arranges_node`'s `expanded` already * blesses persisted collapse state as presentation that survives a reload, * unlike selection. This is the same cut at lane level. */ collapsed_lanes?: string[]; /** Row keys drawn collapsed, when `rows_by` is set. Same contract as * `collapsed_lanes`, on the row axis. */ collapsed_rows?: string[]; /** * Where the tree roots, when `layout` is `'tree'`. Absent means * `{ kind: 'product' }`. See {@link UPGViewTreeRoot}. */ root?: UPGViewTreeRoot; /** * Edge types to nest by, outermost first, when `layout` is `'tree'`. Advisory, * like everything else here. * * @remarks * `group_by` partitions a flat set on a value and cannot express nesting, * because a tree's levels are EDGES rather than property values. Naming the * edge types keeps the nesting portable: a consumer that does not know a * layout family can still read which relation the author meant to nest on. * * A consumer may ignore this entirely, render flat, and remain conformant. * * ORIENTATION IS NORMATIVE (ruled 2026-09-01, F-7): a bare name binds in its * catalog-declared orientation — the edge's `source_type` is the PARENT and * its `target_type` is the CHILD. That is the convention every hierarchy edge * in the catalog already follows (`user_journey_contains_journey_step`: * source = journey = parent), and it is what makes a bare name deterministic: * two conformant consumers reading the same `nest_by` must build the same * tree. Guessing the orientation, or walking an edge in its converse * direction on a bare name, is non-conformant. Intent whose parent sits at * the edge's TARGET end (e.g. children under their * `opportunity_pursues_outcome` target) takes the explicit object form, * `{ edge_type, parent: 'target' }` (0.37.0); a bare string is exactly * `{ parent: 'source' }`. Direction is semantics, never a rendering * preference: the catalog legally holds distinct edge types between the same * endpoints in opposite directions (`outcome_reveals_opportunity` is * provenance, `opportunity_pursues_outcome` is intent), so no consumer may * rewrite one into the other to make a tree connect. */ nest_by?: UPGViewNestEntry[]; /** * What to do with a selected member the nest relation does not reach. * Absent means `'root'`. * * @remarks * THE ASYMMETRY THIS CLOSES. 0.33.0's `nest_by` made NESTING portable and left * the FAILURE of nesting unportable. A member the scope selected that the nest * relation cannot reach may be drawn as an additional root or silently omitted, * both conformant, and the spec could not say which the author meant. * * ABSENT MEANS `'root'`, deliberately, and this is the whole safety argument. A * consumer that ignores this field must never silently DROP a node the scope * admitted. Measured on a real imported tracker graph of 1,118 nodes and 3,685 * edges: one tree selects 218 members and renders ONE card under `hide`; three * more go 185 to 1, and one goes 11 to 1. Under absent-means-hide a conformant * consumer renders a blank tree over 218 matching nodes with no way to tell the * author anything is missing. Under absent-means-root, "everything the scope * admits is visible somewhere" becomes a checkable invariant, and the majority * (51 of 57 registry trees prefer hiding) pays for hiding EXPLICITLY — which is * the right way round for a default that decides whether data disappears. * * The orphans ARE the dataset rather than an edge case, and the cause is * structural: tracker-imported work wires through cycle and project relations, * which are reference-axis edges, never nesting, so the containment edges these * trees traverse do not reach it. * * WHY THIS IS SPEC AND NOT APPLICATION CHASSIS, which the filing author first * concluded and measurement overturned. Running the two dispositions changes * WHICH NODES APPEAR, not where pixels go. Presentation is advisory precisely * because a consumer may ignore it and stay conformant, and a field that * decides membership visibility cannot be ignored safely. Selection-class facts * belong in the spec; that is the line, and the measurement is what located it. * * PRESENTATION STAYS ADVISORY OVERALL. This field does not break that: a * consumer may ignore it and remain conformant, because the default it then * applies is the safe one. That property is exactly what the absent-means-root * cut buys. */ orphan_disposition?: 'root' | 'hide'; } /** A layer whose membership is produced by a query rather than by placement. * * Extended by both `CompositionProperties` and `WorkspaceProperties` (0.33.0). * * @remarks * WHY BOTH HALVES CARRY IT. `composition` is the durable published print and * `workspace` is the transient canvas it is published from. A layer is * query-driven while it is being worked on, not only after it is published, so * declaring the query only on the published half makes the fact something * invented at publish time rather than something recorded. * * WHY ONE INTERFACE RATHER THAN TWO DECLARATIONS. The alternative is two copies * of the same JSDoc that must stay identical, which is how a shipped type summary * came to cite an edge that does not exist. Note that the lift is not free at the * runtime layer: `UPG_PROPERTY_SCHEMA` is a flat per-type map, so `workspace` * gets its own entries there regardless. What is shared is the definition. */ interface UPGQueryDrivenLayer { /** * When present, membership is DERIVED: members are produced by running this * query rather than authored by placement. The clause list is authoritative and * the named fields are a positive-only projection of it; since 0.34.0 a clause * is a discriminated union on `dimension`, so the `type` axis carries entity * types rather than free strings. * * @remarks * This is the portable statement of what the layer shows. On a composition, * `CompositionMember.href` remains the publishing tool's own resolved route * and stays opaque to everyone else; a member may carry both, and then the * href is a fast path while the query is the meaning. A consumer that cannot * parse the href can still render the layer, which is the whole reason the * declaration is here rather than in a tool-namespaced bag key. * * A layer with no `member_query` is authored, which is what every composition * written before 0.32.0 is. * * DECLARED ON BOTH HALVES OF THE PAIR since 0.33.0. A layer is query-driven * while it is being worked on, not only once it is published, so declaring the * query only on the durable composition would make it a fact invented at * publish time rather than one recorded. */ member_query?: UPGViewQuery; /** * Advisory rendering intent for the layer as a whole: `group_by`, `sort`, * `layout`, `nest_by`, and `orphan_disposition` (0.34.0, absent means * `'root'`). A consumer may ignore it entirely and still be conformant, * because every default it then applies is the safe one. * * @remarks * THE DESCRIPTION LISTS THE FIELDS ON PURPOSE. This property is `object` in the * runtime property registry, so an agent reading `get_entity_schema` gets an * opaque blob and this sentence. For an object-typed property the description IS * the declared shape, which is why `check:editorial` hashes it (E.4, 0.34.0) and * why a field added to `UPGViewPresentation` without a word here would be * invisible to every gate and every agent at once. */ presentation?: UPGViewPresentation; } interface CompositionMember { /** Stable id of the block within this composition. */ id: string; /** Tool-namespaced view reference, opaque to every other tool and preserved verbatim. */ href: string; /** Display title captured at publish, used as a fallback while the target resolves. */ title: string; /** Horizontal position of the block in the frozen arrangement. */ x: number; /** Vertical position of the block in the frozen arrangement. */ y: number; /** Width of the block in the frozen arrangement. */ width: number; /** Height of the block in the frozen arrangement. */ height: number; /** Whether the block is drawn collapsed. */ collapsed?: boolean; /** * True when this member arrived by running the composition's `member_query` * rather than by a person placing it. * * @remarks * MEMBERSHIP IS DERIVED, POSITION IS AUTHORED, and both are serialised because * both are real. A field pilot measured the shape: blocks were auto-admitted * by a query, then dragged into an order somebody chose. * * DERIVED MEMBERS ARE NOT AUTHORED CONTENT. Any signature meant to answer "did * a person change this" must exclude them. The measured failure is precise: a * content baseline captured at creation, before the query first ran, moved the * moment the query admitted its first block — so a viewer who only glanced at * a gallery would have written to it. */ derived?: boolean; } interface CompositionProperties extends UPGQueryDrivenLayer { /** * The frozen member arrangement, captured at publish. Layout and pointers * only, never resolved content. */ members: CompositionMember[]; /** * Monotonic revision, bumped on each republish of the same slug. * * @remarks * A published revision is a fact about the composition, user-visible as a new * print from the same plate, so it is serialised. Worth distinguishing from a * store's concurrency token, which shares the name in some backends but is a * fact about the table rather than about the thing, and is not spec data. */ rev: number; /** ISO timestamp of the most recent publish or republish. */ published_at: ISODateTime; /** Publisher handle or email. Display scalar, same posture as `WorkspaceProperties.owner`. */ published_by?: string; } /** Capture: a dated, hashed rendition of something already in the graph. * * A screenshot of a surface, a PDF of a report, an export of a canvas. The * subject is a graph node and stays one; a capture says "here is what that * looked like, at this moment, and here is how to tell whether it still does." * Lifecycle-free (a capture is a fact, not a workflow) and containment-free * (it anchors to what it RENDERS via `capture_renders_node`, the same posture * as `composition` and `framework_exercise`). * * @remarks * WHY IT IS NOT CALLED `artifact`. The word is already spent twice over: * `workflow_artifact` is an output produced BY a workflow run, and shipped * tools commonly have an "artifacts" store of generated documents holding * inline content. A third meaning of one word is how a vocabulary stops being * one, so this type is named for what it does. * * WHY NOT EXTEND `document`. Three ways apart, which is a split by the spec's * own dual-shape gate: `document` carries the PUBLISHING lifecycle and a * capture has no lifecycle; `document` has neither a content hash nor a capture * moment; and `document_describes_*` is a family of typed edges about subject * matter, not a rendition relation. * * WHY NOT EXTEND `workflow_artifact`. Its only structural parent is * `workflow_run`, so every captured file would need a workflow run invented to * hold it — dead schema created to satisfy a hierarchy. * * WHY THE HASH IS THE SIGNAL. Regenerating a capture must change what viewers * see without moving anything they arranged. Modification time lies on a * byte-identical re-run and size misses a same-size repaint, so a content hash * is the only value that means THIS RENDITION IS DIFFERENT. * * WHERE THE BYTES LIVE IS A TOOL DECISION. `capture_uri` is a URI on the same * footing as `UPGBaseNode.external_ref`: `https://` for hosted, `file://` or a * relative path for local. The spec takes no position on a sibling directory * beside the `.upg` file. The earlier no-sidecar ruling governed the `.upg` * FILE BODY and reading it wider than it was made would be inventing a decision * nobody took. * * @example * const properties: CaptureProperties = { * capture_uri: './captures/checkout-panel.png', * content_hash: '9f2b7c1a4e6d8035bb1c2f9a7e4d6058c3b1a9f27e4d60583c1b9a2f7e4d6058', * captured_at: '2026-08-20T21:14:00Z', * media_type: 'image/png', * fidelity: 'exact', * capture_status: 'captured', * } */ interface CaptureProperties { /** Where the bytes are. `https://` for hosted, `file://` or a relative path * for local. Required: a capture with no location renders nothing. */ capture_uri: string; /** Content hash of the bytes. The regeneration signal; see the type's * `@remarks` for why mtime and size are not. */ content_hash?: string; /** Hash algorithm. Absent means `sha256`. */ hash_algorithm?: 'sha256' | 'sha1' | 'md5'; /** ISO timestamp the capture was taken. */ captured_at?: ISODateTime; /** IANA media type of the bytes. @example "image/png" */ media_type?: string; /** How faithfully this rendition represents its subject. `approximate` covers * a capture taken in a stand-in state or at the wrong viewport. */ fidelity?: 'exact' | 'approximate' | 'not_applicable'; /** * Whether the capture succeeded. * * @remarks * A `blocked` capture is a real record rather than a missing one: it says the * subject exists and could not be rendered, which is what stops the next run * rediscovering the same obstacle. `skipped` is the deliberate exclusion. */ capture_status?: 'captured' | 'blocked' | 'skipped'; } /** Framework exercise: one run of a framework over a set of entities. * * A framework_exercise is a structured workspace — applying a single framework * (MoSCoW, RICE, Kano, …) to a chosen set of entities. It is the persistent * home for that run's answers: each entity it scores is linked by a * `framework_exercise_includes_node` edge whose `properties` carry the result * for that entity (a MoSCoW bucket, a RICE score, a canvas slot, a funnel * stage). Because the value lives on the edge, the same entity can appear in * many exercises with different results, and any entity type can be scored — * not just `feature`. See ADR 2026-06-02-framework-exercises. * * `title` (the exercise's human label) and `status` (draft / active / * archived) are base-node fields; the fields below are its own intrinsic data. * * @example * const properties: FrameworkExerciseProperties = { * framework_id: 'moscow', * } */ interface FrameworkExerciseProperties { /** Which framework this exercise runs: a framework id (e.g. 'moscow', * 'rice-scoring', 'kano-model'). Resolves against the framework catalog. */ framework_id: string; /** * Optional frozen copy of the framework's input spec at apply time, so a * historical exercise still renders correctly if the framework definition * later evolves (inputs added, removed, or rescaled). */ inputs_snapshot?: Record; /** * Relative multiplier per framework input for this run, keyed by input id. * * @remarks * A weight belongs to the RUN, not to any entity it scores: one weight per * input, shared by every scored entity, which is why it sits here and not on * the `framework_exercise_includes_node` edge alongside the per-entity result. * * Named `input_weights` rather than a bare `weight` deliberately, and the * collision is live rather than hypothetical: `getPropertyDefaultScale` keys * on property NAME alone and ignores entity type, and `PROPERTY_SCALE_MAP` * already maps `weight` to the `importance_5` ordinal. A bare `weight` here * would silently resolve to a 1-5 assessment scale, which is the wrong type, * range and meaning for a multiplier. The plural also matches the spec's own * vocabulary: a framework declares *inputs*, not dimensions. * * @example * const properties: FrameworkExerciseProperties = { * framework_id: 'rice-scoring', * input_weights: { reach: 1, impact: 2, confidence: 1, effort: 1.5 }, * } */ input_weights?: Record; } /** * UPG Property Schemas: Foundations Domain. * Specification (governed specs: query languages, protocols, formats, * encodings) and Primitive (the compositional units a specification defines). * Both are registry-hostable canonicals. Added in 0.9.12. * https://unifiedproductgraph.org/spec | MIT */ /** A governed specification: a query language, protocol, data format, encoding, * or interface contract that one or more products implement, expose, or conform * to. The genus type; whether it is also a ratified `standard` is carried by * `governance`, not the type. * * @remarks * THE THREE RELATIONSHIPS ARE REAL, AND THEY ARE PORTFOLIO CROSS-EDGES RATHER * THAN CATALOG EDGES. This paragraph replaces one that said the opposite. The * verbs in the summary above resolve to `product_implements_specification`, * `product_exposes_specification` and `feature_conforms_to_specification`, all * three of which have existed since 0.9.12 in `UPG_CROSS_ONLY_EDGE_TYPES`. They * are portfolio-native by design: a governed specification is a registry * canonical, so a product links to it at `registry/{node_id}`, the same shape as * `instance_of`. Nothing in the within-graph edge catalog points from `product` * or `feature` at `specification`, and that is deliberate rather than missing. * * WHY THIS CORRECTION MATTERS TO ANYONE READING A DOC THAT CITES THEM. An * editorial sweep flagged those three names in a published entity doc as * fabricated, on the strength of a catalog-only search. They are not fabricated. * A citation check that reads only the edge catalog will report every legitimate * cross-only edge as a phantom, which is a false positive with the same shape as * a real one. Check both registries before calling a name invented. * * THE GAP THAT DOES REMAIN, stated rather than closed. A `specification` node * held inside a SINGLE product graph, with no portfolio around it, has no * conformance edge available, because the cross-only tier needs a registry * target. A claim can ride the universal `node_constrains_node`, which is honest * and unqueryable as conformance. Closing that case is banked, with its condition * the first single-product graph that models a specification it conforms to. * * @example * const properties: SpecificationProperties = { * kind: 'language', * language_flavor: 'query', * governance: 'open_spec_stewarded', * steward: 'Nimbus', * openness: 'open', * spec_url: 'https://nimbus.example/docs/nql', * current_version: '1.0', * since: '2019', * } */ interface SpecificationProperties { /** What the artifact fundamentally is, independent of how it is governed. */ kind?: 'language' | 'protocol' | 'data_format' | 'encoding' | 'interface_contract' | 'object_model'; /** Set only when `kind` is `language`: the kind of language. */ language_flavor?: 'query' | 'programming' | 'markup' | 'styling' | 'schema' | 'template'; /** * How the specification is governed (its ratification status, and whether it * counts as a `standard`). `open_standard_consortium` is a formal standard * (W3C, IETF, ISO); `de_facto` and `internal_primitive` never became one. */ governance?: 'open_spec_stewarded' | 'open_standard_consortium' | 'proprietary_open' | 'internal_primitive' | 'de_facto'; /** The governing body or organisation (may later become an `organization` ref). */ steward?: string; /** Whether the specification itself is open or proprietary. */ openness?: 'open' | 'proprietary'; /** URL of the published specification. */ spec_url?: string; /** Latest published version string. */ current_version?: string; /** Year or version the specification was introduced. */ since?: string; /** How conformance is tested (test suite, certification program). Optional. */ conformance?: string; } /** A foundational compositional unit a specification defines: the noun products * pass around and compose with (a Structured Text block, a typed reference, a * Git commit, a Stripe PaymentIntent). Lifecycle-free, like `metric`. * * @example * const properties: PrimitiveProperties = { * primitive_kind: 'block', * defined_by: 'specification/structured_text', * since: '2018', * } */ interface PrimitiveProperties { /** The shape of the thing products pass around. */ primitive_kind?: 'data_type' | 'object' | 'block' | 'unit'; /** The `specification/` this primitive comes from; nullable for spec-less internal primitives. Mirrors the `primitive_defined_by_specification` edge for quick lookup. */ defined_by?: string; /** Year or version the primitive was introduced. */ since?: string; } /** An operating_lifecycle: a canonical, ordered (often cyclic) operating process * that many products' journey phases map onto — e.g. Nimbus's content-operations * lifecycle. Registry-hostable like a `specification`; the cross-product join key * that turns per-surface journeys into one operation. Distinct from the per-entity * status `lifecycle` grammar concept (this is an entity, not a state machine). * * @example * const properties: OperatingLifecycleProperties = { * cyclic: true, * source: 'a canonical 5-stage content-ops lifecycle', * } */ interface OperatingLifecycleProperties { /** True if the process loops (e.g. Analyze → Extend → Plan). The sequence is fully expressed by the stages' `stage_order`; `cyclic` adds the wrap from the last stage back to the first. */ cyclic?: boolean; /** Origin of the canonical model (e.g. "a published content-ops lifecycle"). Optional provenance; promote to an edge if it names a real `specification`/`document`. */ source?: string; } /** An operating_stage: one ordered stage of an `operating_lifecycle`. A product's * `journey_phase` resolves to a stage via `journey_phase_realises_operating_stage`, * so the end-to-end operation is derived from the join key rather than stored. * * @example * const properties: OperatingStageProperties = { * stage_order: 1, * goal: 'Model, create, and review content', * } */ interface OperatingStageProperties { /** Ordered position within the lifecycle, 0-indexed. The source of truth for sequence (pairs with `phase_order`/`step_order`/`action_order`). */ stage_order?: number; /** What this stage accomplishes. */ goal?: string; /** Role that owns the stage (free-text role label). Optional; promote to a `node_owned_by_role` edge if ownership must be queryable across stages. */ owner_role?: string; } /** * UPG Property Map. Type-safe lookup from entity type to property interface. * Use `UPGNode` for narrowed `properties` typing at call sites. * https://unifiedproductgraph.org/spec | MIT */ /** Type-safe lookup from entity type string to its property interface. * Every active entity type has an entry. */ interface UPGPropertyMap { /** The product being built: root node of every product graph */ product: ProductProperties; /** A measurable outcome the product is working toward */ outcome: OutcomeProperties; /** A specific objective tied to a strategic theme or OKR */ objective: ObjectiveProperties; /** A quantifiable key result used to measure an objective */ key_result: KeyResultProperties; /** A tracked metric, present when a quantitative measure is defined */ metric: MetricProperties; /** A point-in-time review of a metric's quality and fitness for purpose. */ metric_quality_assessment: MetricQualityAssessmentProperties; /** The long-horizon vision statement for the product */ vision: VisionProperties; /** The mission statement: who is served and what core value is delivered */ mission: MissionProperties; /** A strategic theme grouping related initiatives under a common direction */ strategic_theme: StrategicThemeProperties; /** A time-boxed initiative delivering against a strategic theme */ initiative: InitiativeProperties; /** An organisational or technical capability being developed or maintained */ capability: CapabilityProperties; /** A value stream mapping the end-to-end flow of value delivery */ value_stream: ValueStreamProperties; /** A durable strategic pillar spanning multi-year horizons */ strategic_pillar: StrategicPillarProperties; /** An assumption that underpins strategy or design, present when validation is being tracked */ assumption: AssumptionProperties; /** A decision record (strategic, product, engineering, or design) */ decision: DecisionProperties; /** A named limitation or boundary (resource, technical, regulatory, * temporal, or compliance) that bounds product creation. Edge-defined. */ constraint: ConstraintProperties; /** An open coordination or ownership question a plan is exposed to (strategy sibling of research_question / design_question) */ strategic_question: StrategicQuestionProperties; /** A user persona representing a segment of people the product serves */ persona: PersonaProperties; /** A job-to-be-done: the progress a user is trying to make */ job: JobProperties; /** A user need derived from research or synthesis */ need: NeedProperties; /** A desired outcome: the specific result a user wants from a job step */ desired_outcome: DesiredOutcomeProperties; /** A discrete step within a job-to-be-done */ job_step: JobStepProperties; /** A switching cost that prevents a user from adopting the product */ switching_cost: SwitchingCostProperties; /** A product opportunity identified from user needs or market gaps */ opportunity: OpportunityProperties; /** A proposed solution to an opportunity, present before commitment */ solution: SolutionProperties; /** A feasibility study assessing viability of a solution */ feasibility_study: FeasibilityStudyProperties; /** A design sprint run to explore and test a solution space */ design_sprint: DesignSprintProperties; /** A testable belief: the canonical validation design artefact. */ hypothesis: HypothesisProperties; /** @deprecated since v0.4.0. Use `evidence` + `hypothesis_has_evidence` edge instead. */ hypothesis_evidence: HypothesisEvidenceProperties; /** A general-purpose experiment entity (canonical-stable). Use `experiment_plan` + `experiment_run` for fine-grained plan/run separation. */ experiment: ExperimentProperties; /** Planning intent for a structured test of a hypothesis (UCS pattern P4, work-unit). Pairs with `experiment_run`. */ experiment_plan: ExperimentPlanProperties; /** Execution evidence for a structured test of a hypothesis (UCS pattern P6, event-occurrence). Pairs with `experiment_plan`. */ experiment_run: ExperimentRunProperties; /** A learning captured from an experiment or research activity */ learning: LearningProperties; /** A test plan defining scope and method for a validation activity */ test_plan: TestPlanProperties; /** A piece of evidence supporting or refuting a hypothesis */ evidence: EvidenceProperties; /** A research plan outlining goals, methods, and participant criteria */ research_plan: ResearchPlanProperties; /** A competitor in the market, present when competitive tracking is active */ competitor: CompetitorProperties; /** A specific feature offered by a competitor */ competitor_feature: CompetitorFeatureProperties; /** A dated competitor move (feature launch, pricing change, ...) mapped onto our portfolio */ competitor_signal: CompetitorSignalProperties; /** A market trend that may affect product strategy */ market_trend: MarketTrendProperties; /** A segment of the addressable market being targeted */ market_segment: MarketSegmentProperties; /** A structured competitive analysis comparing product position */ competitive_analysis: CompetitiveAnalysisProperties; /** A dimension along which subjects are classified */ classification_axis: ClassificationAxisProperties; /** A value on a classification axis: one position in a taxonomy */ classification_value: ClassificationValueProperties; /** A formal user research study (interview series, survey, usability test) */ research_study: ResearchStudyProperties; /** An insight synthesised from research: a meaningful pattern or finding */ insight: InsightProperties; /** A research participant, present when participant tracking is needed */ participant: ParticipantProperties; /** A raw observation recorded during research */ observation: ObservationProperties; /** A verbatim or paraphrased quote from a participant */ quote: QuoteProperties; /** A cluster of affinity-mapped observations sharing a common theme */ affinity_cluster: AffinityClusterProperties; /** A research question guiding a study or sprint */ research_question: ResearchQuestionProperties; /** An interview guide structuring a qualitative session */ interview_guide: InterviewGuideProperties; /** A single survey response from a participant */ survey_response: SurveyResponseProperties; /** An open design question framing a problem to be solved */ design_question: DesignQuestionProperties; /** A user journey map visualising experience across touchpoints */ user_journey: UserJourneyProperties; /** A discrete action at a journey step, classified by service layer */ journey_action: JourneyActionProperties; /** A single step within a user journey */ journey_step: JourneyStepProperties; /** A phase grouping journey steps into a named stage */ journey_phase: JourneyPhaseProperties; /** A design concept being explored or evaluated */ design_concept: DesignConceptProperties; /** A prototype built to test a design concept */ prototype: PrototypeProperties; /** A wireframe representing a screen layout at varying fidelity */ wireframe: WireframeProperties; /** A user flow showing task-level paths through the product */ user_flow: UserFlowProperties; /** A screen in the product UI, present when screen mapping is tracked */ screen: ScreenProperties; /** A specific UI state of a screen (empty, loading, error, etc.) */ screen_state: ScreenStateProperties; /** A place in the UI, its occupants, and the rule that arbitrates between them */ surface: SurfaceProperties; /** A design annotation on a screen or component */ annotation: AnnotationProperties; /** An interaction specification defining animation and transition behaviour */ interaction_spec: InteractionSpecProperties; /** A reusable UI component in the design system */ design_component: DesignComponentProperties; /** A design token: a named value for colour, spacing, typography, etc. */ design_token: DesignTokenProperties; /** The design system itself, covering governance, tooling, and coverage metadata */ design_system: DesignSystemProperties; /** A recurring interaction or layout pattern documented in the design system */ design_pattern: DesignPatternProperties; /** A design guideline stating how and when to apply a pattern or token */ design_guideline: DesignGuidelineProperties; /** The brand identity, covering values, personality, and positioning */ brand_identity: BrandIdentityProperties; /** A specific brand colour with usage guidance */ brand_colour: BrandColourProperties; /** A brand typography pairing (typeface, scale, and usage rules) */ brand_typography: BrandTypographyProperties; /** The brand voice: tone, register, and writing principles */ brand_voice: BrandVoiceProperties; /** A brand logo variant (full, icon, wordmark, etc.) */ brand_logo: BrandLogoProperties; /** Brand imagery guidelines covering photography and illustration style */ brand_imagery: BrandImageryProperties; /** A feature area grouping related features under a product section */ feature_area: FeatureAreaProperties; /** A product feature: a discrete unit of user-facing functionality */ feature: FeatureProperties; /** An epic grouping related user stories under a deliverable theme */ epic: EpicProperties; /** The "As X, I want Y so Z" templated promise: a user story (UCS pattern P5, templated-statement). Implemented by `task` via `task_implements_user_story`. */ user_story: UserStoryProperties; /** * @deprecated since v0.4.0. story_task collapsed into task. * Use the `task` entry below. */ story_task: StoryTaskProperties; /** An acceptance criterion defining when a story or feature is complete */ acceptance_criterion: AcceptanceCriterionProperties; /** A product release, present when versioned delivery is tracked */ release: ReleaseProperties; /** A discrete task assigned to a person or team */ task: TaskProperties; /** A bug report, present when a defect is being tracked */ bug: BugProperties; /** A product roadmap grouping features and themes over a planning horizon */ roadmap: RoadmapProperties; /** A single item on a roadmap: a feature, epic, or theme with timing */ roadmap_item: RoadmapItemProperties; /** A thematic grouping used to cluster work on the roadmap, around the customer problem */ roadmap_theme: RoadmapThemeProperties; /** A changelog entry recording what shipped in a release */ changelog: ChangelogProperties; /** A named, dated, self-nesting planning interval (sprint / iteration / quarter / PI / cooldown) work is scheduled through: the cadence axis */ planning_cycle: PlanningCycleProperties; configuration_axis: ConfigurationAxisProperties; /** A governed specification (query language, protocol, format) products implement, expose, or conform to */ specification: SpecificationProperties; /** A foundational compositional unit a specification defines (a block, a reference, a query value) */ primitive: PrimitiveProperties; /** A canonical ordered (often cyclic) operating process products' journey phases map onto */ operating_lifecycle: OperatingLifecycleProperties; /** One ordered stage of an operating_lifecycle */ operating_stage: OperatingStageProperties; /** A bounded context defining the scope of a domain model */ bounded_context: BoundedContextProperties; /** A deployable service within the system architecture */ service: ServiceProperties; /** A domain event signalling something meaningful happened in the system */ domain_event: DomainEventProperties; /** An API contract specifying the interface between services or clients */ api_contract: ApiContractProperties; /** A technical debt item, present when debt is being tracked for remediation */ technical_debt_item: TechnicalDebtItemProperties; /** A feature flag controlling runtime behaviour without deployment */ feature_flag: FeatureFlagProperties; /** A deployment record, present when a specific release was deployed */ deployment: DeploymentProperties; /** A DDD aggregate: a cluster of domain objects with a consistency boundary */ aggregate: AggregateProperties; /** A domain entity: an object with identity that changes over time */ domain_entity: DomainEntityProperties; /** A value object: an immutable domain concept identified by its attributes */ value_object: ValueObjectProperties; /** A command representing an intent to change system state */ command: CommandProperties; /** A read model: a query-optimised projection of domain state */ read_model: ReadModelProperties; /** A specific API endpoint, present when endpoint-level tracking is needed */ api_endpoint: ApiEndpointProperties; /** A database schema (table structure, constraints, and migration status) */ database_schema: DatabaseSchemaProperties; /** A message queue topic or channel */ queue_topic: QueueTopicProperties; /** A build artifact produced by the CI pipeline */ build_artifact: BuildArtifactProperties; /** A code repository, present when repo-level tracking is needed */ code_repository: CodeRepositoryProperties; /** A library dependency tracked for version and licence compliance */ library_dependency: LibraryDependencyProperties; /** An integration pattern documenting how two systems communicate */ integration_pattern: IntegrationPatternProperties; /** An external API consumed by the product */ external_api: ExternalApiProperties; /** A data flow tracing how data moves between components */ data_flow: DataFlowProperties; /** An investigation into a production issue or unexpected behaviour */ investigation: InvestigationProperties; /** The root cause identified during an incident investigation */ root_cause: RootCauseProperties; /** A symptom observed during an incident, present before root cause is known */ symptom: SymptomProperties; /** A fix applied to resolve a bug or incident */ fix: FixProperties; /** A conversion funnel tracking user progression through a key flow */ funnel: FunnelProperties; /** A single step within a conversion funnel */ funnel_step: FunnelStepProperties; /** An acquisition channel bringing new users or leads into the product */ acquisition_channel: AcquisitionChannelProperties; /** A growth campaign driving acquisition, activation, or retention */ growth_campaign: GrowthCampaignProperties; /** A user cohort grouped by shared behaviour or acquisition date */ cohort: CohortProperties; /** A behavioural segment defined by in-product actions */ behavioral_segment: BehavioralSegmentProperties; /** A growth loop: a self-reinforcing cycle that compounds user or revenue growth */ growth_loop: GrowthLoopProperties; /** A variant in an A/B or multivariate experiment */ variant: VariantProperties; /** An attribution model defining how credit is assigned to acquisition channels */ attribution_model: AttributionModelProperties; /** The overall business model, present when BMC-level structure is tracked */ business_model: BusinessModelProperties; /** A value proposition: the core promise made to a customer segment */ value_proposition: ValuePropositionProperties; /** A revenue stream: a mechanism through which the product earns money */ revenue_stream: RevenueStreamProperties; /** A pricing tier defining what a customer gets at a given price point */ pricing_tier: PricingTierProperties; /** The cost structure (fixed and variable costs underpinning the business model) */ cost_structure: CostStructureProperties; /** Unit economics: per-unit revenue, cost, and margin metrics */ unit_economics: UnitEconomicsProperties; /** A partnership: a strategic relationship with an external organisation */ partnership: PartnershipProperties; /** A key resource required to deliver value (people, IP, infrastructure) */ key_resource: KeyResourceProperties; /** A key activity the business must perform to deliver its value proposition */ key_activity: KeyActivityProperties; /** A customer relationship type: how the business interacts with a segment */ customer_relationship: CustomerRelationshipProperties; /** A distribution channel through which value reaches the customer */ distribution_channel: DistributionChannelProperties; /** The GTM strategy, present when go-to-market planning is formalised */ gtm_strategy: GtmStrategyProperties; /** An ideal customer profile defining the best-fit buyer */ ideal_customer_profile: IdealCustomerProfileProperties; /** A positioning statement defining where the product sits in the market */ positioning: PositioningProperties; /** A messaging framework (headlines, value pillars, and proof points) */ messaging: MessagingProperties; /** A product launch, present when a coordinated launch is being planned */ launch: LaunchProperties; /** A content strategy defining themes, formats, and channels */ content_strategy: ContentStrategyProperties; /** A sales motion: the repeatable process used to close deals */ sales_motion: SalesMotionProperties; /** A competitive battle card for use by the sales team */ competitive_battle_card: CompetitiveBattleCardProperties; /** A demand generation programme driving pipeline */ demand_gen_program: DemandGenProgramProperties; /** A sales territory defining geographic or segment-based coverage */ territory: TerritoryProperties; /** A common sales objection raised by prospects */ objection: ObjectionProperties; /** A rebuttal to a specific sales objection */ rebuttal: RebuttalProperties; /** A proof point substantiating a claim in messaging or sales */ proof_point: ProofPointProperties; /** A team: a group of people working on a shared scope */ team: TeamProperties; /** A role: a function or responsibility within the team or organisation */ role: RoleProperties; /** A stakeholder with interest in or influence over the product */ stakeholder: StakeholderProperties; /** A named, accountable individual, distinct from `stakeholder` (interested party) and `role` (responsibility slot) */ person: PersonProperties; /** An OKR set at the team level */ team_okr: TeamOkrProperties; /** A retrospective, present when team reflections are being tracked */ retrospective: RetrospectiveProperties; /** A cross-team dependency that may block delivery */ dependency: DependencyProperties; /** A department or organisational unit */ department: DepartmentProperties; /** A skill tracked for hiring, development, or capacity planning */ skill: SkillProperties; /** A recurring team ceremony (standup, sprint review, planning, etc.) */ ceremony: CeremonyProperties; /** A capacity plan mapping team bandwidth to upcoming work */ capacity_plan: CapacityPlanProperties; /** A data source feeding the analytics or data platform */ data_source: DataSourceProperties; /** An event schema defining the structure of a tracked event */ event_schema: EventSchemaProperties; /** A dashboard, present when a reporting surface is being tracked */ dashboard: DashboardProperties; /** A data model covering entity relationships and schema structure */ data_model: DataModelProperties; /** A data quality rule defining expectations on a dataset or field */ data_quality_rule: DataQualityRuleProperties; /** A data product: a curated, discoverable dataset made available to consumers */ data_product: DataProductProperties; /** A data pipeline moving or transforming data between systems */ data_pipeline: DataPipelineProperties; /** A data lineage record tracing data from source to destination */ data_lineage: DataLineageProperties; /** A glossary term defining a business or technical concept */ glossary_term: GlossaryTermProperties; /** A data domain grouping related data assets under a common owner */ data_domain: DataDomainProperties; /** A report: a scheduled or on-demand analytical output */ report: ReportProperties; /** A content piece (blog post, video, podcast, whitepaper, or case study) */ content_piece: ContentPieceProperties; /** A knowledge base article for customers, developers, or internal teams */ knowledge_base_article: KnowledgeBaseArticleProperties; /** A brand asset (logo, illustration, photo, or video) */ brand_asset: BrandAssetProperties; /** A prompt template used for AI-assisted workflows */ prompt_template: PromptTemplateProperties; /** A content calendar organising publishing activity over a period */ content_calendar: ContentCalendarProperties; /** A content theme grouping related content under a strategic topic */ content_theme: ContentThemeProperties; /** A documentation template defining structure for a class of documents */ documentation_template: DocumentationTemplateProperties; /** A document entity: provenance container linking to a source file */ document: DocumentProperties; /** A legal entity (company, subsidiary, or contracting party) */ legal_entity: LegalEntityProperties; /** An intellectual property asset (patent, trademark, copyright, or trade secret) */ ip_asset: IpAssetProperties; /** A contract between the company and an external party */ contract: ContractProperties; /** A specific clause within a contract, present when clause-level tracking is needed */ contract_clause: ContractClauseProperties; /** A privacy policy document, present when policy tracking is active */ privacy_policy: PrivacyPolicyProperties; /** A compliance requirement from a regulation, standard, or customer contract */ compliance_requirement: ComplianceRequirementProperties; /** A risk: a potential event with negative impact on the product or business */ risk: RiskProperties; /** A data contract defining agreed schema and SLAs between producer and consumer */ data_contract: DataContractProperties; /** An audit log policy defining retention and access rules */ audit_log_policy: AuditLogPolicyProperties; /** A compliance framework being adhered to (SOC 2, ISO 27001, GDPR, etc.) */ compliance_framework: ComplianceFrameworkProperties; /** A security audit, present when a formal review has been conducted */ security_audit: SecurityAuditProperties; /** A service level indicator: a specific measurement of service behaviour */ service_level_indicator: ServiceLevelIndicatorProperties; /** A service level objective: a target range for an SLI */ service_level_objective: ServiceLevelObjectiveProperties; /** An error budget derived from an SLO, present when reliability is tracked */ error_budget: ErrorBudgetProperties; /** An incident: a production event requiring response */ incident: IncidentProperties; /** A postmortem written after an incident is resolved */ postmortem: PostmortemProperties; /** A runbook documenting how to respond to an operational scenario */ runbook: RunbookProperties; /** A monitor watching a system metric or log pattern */ monitor: MonitorProperties; /** An alert rule defining when a monitor fires a notification */ alert_rule: AlertRuleProperties; /** A CI pipeline configuration (build, test, and deploy stages) */ ci_pipeline: CiPipelineProperties; /** A release strategy defining how changes are deployed (blue-green, canary, etc.) */ release_strategy: ReleaseStrategyProperties; /** An on-call rotation assigning responders to incidents by schedule */ on_call_rotation: OnCallRotationProperties; /** An infrastructure component (server, container, network resource, etc.) */ infrastructure_component: InfrastructureComponentProperties; /** A threat model documenting attack surfaces and adversary assumptions */ threat_model: ThreatModelProperties; /** A specific threat identified within a threat model */ threat: ThreatProperties; /** A vulnerability: a known weakness that could be exploited */ vulnerability: VulnerabilityProperties; /** A security control mitigating one or more threats */ security_control: SecurityControlProperties; /** A security policy governing how the system or team handles a security domain */ security_policy: SecurityPolicyProperties; /** A penetration test, present when external security testing is tracked */ penetration_test: PenetrationTestProperties; /** A security review of a feature, service, or architecture change */ security_review: SecurityReviewProperties; /** A data classification label applied to a dataset or field */ data_classification: DataClassificationProperties; /** An access policy defining who can do what on which resource */ access_policy: AccessPolicyProperties; /** An accessibility standard the product is conforming to (WCAG 2.1 AA, etc.) */ a11y_standard: A11yStandardProperties; /** A specific accessibility guideline derived from a standard */ a11y_guideline: A11yGuidelineProperties; /** An accessibility audit, present when a formal review has been run */ a11y_audit: A11yAuditProperties; /** An accessibility issue found during audit or user research */ a11y_issue: A11yIssueProperties; /** An accessibility annotation on a design or component */ a11y_annotation: A11yAnnotationProperties; /** A test suite grouping related test cases */ test_suite: TestSuiteProperties; /** A test case: a single scenario with expected inputs and outputs */ test_case: TestCaseProperties; /** A QA session, present when exploratory testing is being tracked */ qa_session: QaSessionProperties; /** A regression test ensuring previously fixed behaviour does not break */ regression_test: RegressionTestProperties; /** A test coverage report measuring how much code is exercised by tests */ test_coverage_report: TestCoverageReportProperties; /** A test environment configuration used to run a test suite */ test_environment: TestEnvironmentProperties; /** The result of a test run (pass, fail, or flaky) */ test_result: TestResultProperties; /** A feedback programme, present when structured customer feedback is collected */ feedback_program: FeedbackProgramProperties; /** A feature request submitted by a customer or prospect */ feature_request: FeatureRequestProperties; /** A vote on a feature request, present when voting is tracked */ feedback_vote: FeedbackVoteProperties; /** An NPS campaign, present when Net Promoter Score is being measured */ nps_campaign: NpsCampaignProperties; /** A user advisory board: a formal group of customers shaping product direction */ user_advisory_board: UserAdvisoryBoardProperties; /** A beta programme, present when pre-release access is being managed */ beta_program: BetaProgramProperties; /** A feedback theme clustering related requests or complaints */ feedback_theme: FeedbackThemeProperties; /** The pricing strategy, present when pricing approach is formalised */ pricing_strategy: PricingStrategyProperties; /** A discount strategy defining when and how discounts are applied */ discount_strategy: DiscountStrategyProperties; /** A trial configuration, present when a free trial or pilot is offered */ trial_config: TrialConfigProperties; /** A paywall: the gate between free and paid access */ paywall: PaywallProperties; /** An AI model used by or integrated into the product */ ai_model: AiModelProperties; /** A versioned prompt, present when prompt engineering is tracked */ prompt_version: PromptVersionProperties; /** An evaluation benchmark defining pass/fail criteria for a model */ eval_benchmark: EvalBenchmarkProperties; /** A single evaluation run against a benchmark */ eval_run: EvalRunProperties; /** An AI cost tracker monitoring token usage and spend */ ai_cost_tracker: AiCostTrackerProperties; /** A hallucination report, present when model output accuracy is audited */ hallucination_report: HallucinationReportProperties; /** An AI guardrail: a safety or quality constraint applied to model output */ ai_guardrail: AiGuardrailProperties; /** A model comparison evaluating two or more models on the same benchmark */ model_comparison: ModelComparisonProperties; /** An AI experiment testing a model, prompt, or pipeline change */ ai_experiment: AiExperimentProperties; /** A dataset used to train, fine-tune, or evaluate a model */ ai_dataset: AiDatasetProperties; /** A trace capturing inputs, outputs, and latency for a model invocation */ ai_trace: AiTraceProperties; /** A workflow template defining a reusable multi-step process */ workflow_template: WorkflowTemplateProperties; /** A workflow run: a live execution of a workflow template */ workflow_run: WorkflowRunProperties; /** An agent definition: the configuration and capabilities of an AI agent */ agent_definition: AgentDefinitionProperties; /** An agent session: a single interaction run by an agent */ agent_session: AgentSessionProperties; /** A review gate requiring human approval before a workflow continues */ review_gate: ReviewGateProperties; /** An approval record: the decision made at a review gate */ approval_record: ApprovalRecordProperties; /** A skill available to an agent: a named, callable capability */ agent_skill: AgentSkillProperties; /** A hook executed before or after a workflow step */ agent_hook: AgentHookProperties; /** An artifact produced by a workflow run */ workflow_artifact: WorkflowArtifactProperties; /** A task assigned to an agent within a workflow run */ agent_task: AgentTaskProperties; /** An organisation: the company or entity that owns a portfolio of products */ organization: OrganizationProperties; /** A portfolio grouping multiple products under a common strategy */ portfolio: PortfolioProperties; /** A product area: a named section of the product with its own ownership */ product_area: ProductAreaProperties; /** A workspace: the collaboration boundary for a team or organisation */ workspace: WorkspaceProperties; /** A framework exercise: one run of a framework over a set of entities */ framework_exercise: FrameworkExerciseProperties; /** A composition: a named, published view assembled from a canvas */ composition: CompositionProperties; /** A dated, hashed rendition of a node already in the graph: a screenshot, an export, a PDF */ capture: CaptureProperties; /** An account: a company or organisation in the CRM */ account: AccountProperties; /** A contact: an individual person at an account */ contact: ContactProperties; /** A lead: an unqualified prospect entering the sales funnel */ lead: LeadProperties; /** A deal: a qualified sales opportunity being pursued */ deal: DealProperties; /** The sales pipeline, present when pipeline-level metadata is tracked */ pipeline_sales: PipelineSalesProperties; /** A stage within the sales pipeline */ pipeline_stage: PipelineStageProperties; /** A quote document sent to a prospect */ quote_document: QuoteDocumentProperties; /** A subscription: a recurring revenue relationship with a customer */ subscription: SubscriptionProperties; /** An invoice issued to a customer */ invoice: InvoiceProperties; /** A sales forecast, present when revenue prediction is tracked */ forecast: ForecastProperties; /** A programme grouping related projects under a strategic objective */ program: ProgramProperties; /** A project: a time-boxed initiative with a defined scope */ project: ProjectProperties; /** A milestone marking a significant point in a project or release */ milestone: MilestoneProperties; /** A risk register capturing identified risks and mitigations */ risk_register: RiskRegisterProperties; /** A change request, present when a formal change process is followed */ change_request: ChangeRequestProperties; /** A deliverable: a tangible output committed to a stakeholder */ deliverable: DeliverableProperties; /** A resource allocation mapping team capacity to projects */ resource_allocation: ResourceAllocationProperties; /** A status report summarising progress against plan */ status_report: StatusReportProperties; /** The marketing strategy, present when marketing is formalised at the programme level */ marketing_strategy: MarketingStrategyProperties; /** A marketing channel: a medium used to reach the target audience */ marketing_channel: MarketingChannelProperties; /** A marketing campaign plan, present when a campaign is being planned */ marketing_campaign_plan: MarketingCampaignPlanProperties; /** An email sequence: a series of automated emails sent to a segment */ email_sequence: EmailSequenceProperties; /** A social post, present when social content is being planned or tracked */ social_post: SocialPostProperties; /** An SEO keyword being targeted in content or paid search */ seo_keyword: SeoKeywordProperties; /** An ad creative: copy and visual used in a paid media placement */ ad_creative: AdCreativeProperties; /** A press release, present when media outreach is being tracked */ press_release: PressReleaseProperties; /** An event (physical or virtual) being planned or tracked */ event: EventProperties; /** A community initiative building engagement with users or prospects */ community_initiative: CommunityInitiativeProperties; /** A support ticket raised by a customer */ support_ticket: SupportTicketProperties; /** Customer feedback collected outside a formal research study */ customer_feedback: CustomerFeedbackProperties; /** A churn reason explaining why a customer cancelled */ churn_reason: ChurnReasonProperties; /** A customer health score, present when account health is tracked */ customer_health_score: CustomerHealthScoreProperties; /** A playbook: a documented response to a customer scenario */ playbook: PlaybookProperties; /** A service level agreement defining commitments to a customer */ service_level_agreement: ServiceLevelAgreementProperties; /** A stage in the customer journey post-purchase */ customer_journey_stage: CustomerJourneyStageProperties; /** A touchpoint where the customer interacts with the product or team */ touchpoint: TouchpointProperties; /** A success milestone marking a meaningful achievement in the customer lifecycle */ success_milestone: SuccessMilestoneProperties; /** A service blueprint mapping frontstage and backstage activities */ service_blueprint: ServiceBlueprintProperties; /** A locale: a language and region combination supported by the product */ locale: LocaleProperties; /** A translation key mapping a string identifier to localised text */ translation_key: TranslationKeyProperties; /** A translation bundle: a collection of keys for a single locale */ translation_bundle: TranslationBundleProperties; /** A locale configuration defining date, number, and currency formatting */ locale_config: LocaleConfigProperties; /** A cultural adaptation: a locale-specific change beyond string translation */ cultural_adaptation: CulturalAdaptationProperties; /** A regional pricing configuration adjusting price for a locale */ regional_pricing: RegionalPricingProperties; /** An education programme: a structured curriculum for customer learning */ education_program: EducationProgramProperties; /** A tutorial walking a user through a feature or workflow */ tutorial: TutorialProperties; /** A walkthrough: an in-product guided tour */ walkthrough: WalkthroughProperties; /** A webinar: a live or recorded educational session */ webinar: WebinarProperties; /** A certification awarded upon completing a learning path */ certification: CertificationProperties; /** A help video: a short screencast explaining a feature */ help_video: HelpVideoProperties; /** A learning path: a sequenced collection of tutorials, videos, and assessments */ learning_path: LearningPathProperties; /** A partner programme defining tiers, benefits, and requirements */ partner_program: PartnerProgramProperties; /** A partner tier within a programme (Bronze, Silver, Gold, etc.) */ partner_tier: PartnerTierProperties; /** The API ecosystem: developer community and integration surface */ api_ecosystem: ApiEcosystemProperties; /** A marketplace listing, present when the product is listed on a platform */ marketplace_listing: MarketplaceListingProperties; /** A developer portal providing documentation and sandbox access */ developer_portal: DeveloperPortalProperties; /** An integration partner building on top of the product API */ integration_partner: IntegrationPartnerProperties; /** A partner revenue share arrangement defining commercial terms */ partner_revenue_share: PartnerRevenueShareProperties; } /** A fully typed UPG node. `UPGBaseNode` narrowed to a specific entity type. * * `T` is an entity-type literal from `UPGPropertyMap`. When supplied, it * constrains `type` to that literal and narrows `properties` to the matching * `*Properties` interface. When omitted, `T` defaults to the full union: the * node accepts any entity type and `properties` widens to the union of all * domain property shapes. * * @example * // Narrowed by T: `type` is locked to 'feature' and `properties` to * // FeatureProperties. * const featureNode: UPGNode<'feature'> = { * id: 'feat-1', * type: 'feature', * title: 'Dark mode', * properties: { priority: 'high' }, * } * * @example * // A different instantiation of T: the generic lets a single helper * // signature return the correctly typed node for any entity type. * const personaNode: UPGNode<'persona'> = { * id: 'p-1', * type: 'persona', * title: 'Solo founder', * properties: { is_primary: true, experience_level: 'intermediate' }, * } */ type UPGNode = UPGBaseNode & { type: T; properties?: UPGPropertyMap[T]; }; /** * Property modifiers: the queryable surface (UPG-684 / 0.13.0 Wave 0). * * The `modifier` field on `PropertyDefinition` (`'derived' | 'snapshot' | * 'volatile'`, shipped in 0.11.6 off the 2026-06-16 property-fit audit) marks a * property whose value is NOT authored-and-stable. This module turns that field * from "a key on a raw schema dump" into a real, machine-checkable surface: * accessors to enumerate modified properties, the record-vs-definition * governance line, and the shape detectors that the spec guardrails (and, later, * `validate_graph`) consume. * * It carries no I/O and no graph instance — pure catalog introspection over * `UPG_PROPERTY_SCHEMA`. The guardrails in `__tests__/spec-guardrails.test.ts` * (T1.2 stored-aggregate, T1.3 runtime-state-on-definition-entity) read from * here so there is exactly one definition of each smell. * * https://unifiedproductgraph.org/spec | MIT */ /** The three property-modifier values. Single source — derived from the type. */ type PropertyModifier = NonNullable; /** All modifier values, in canonical order. */ declare const PROPERTY_MODIFIERS: readonly ["derived", "snapshot", "volatile"]; /** * Human-readable semantics for each modifier — the same prose the * `PropertyDefinition` doc comment carries, exposed as data so renderers, * docs, and tooling describe the modifiers consistently. */ declare const PROPERTY_MODIFIER_SEMANTICS: Record; /** One modified property, located by entity type + top-level property name. */ interface ModifiedProperty { /** Entity type id (key into `UPG_PROPERTY_SCHEMA`). */ type: string; /** Top-level property name. */ property: string; /** The modifier carried by the property. */ modifier: PropertyModifier; } /** * Enumerate every property in the catalog that carries a modifier, optionally * filtered to one modifier kind. Top-level properties only — modifiers are a * property-level provenance signal and are not applied to nested object keys * anywhere in the catalog today. */ declare function listPropertiesByModifier(modifier?: PropertyModifier): ModifiedProperty[]; /** The modifier on a single property, or `undefined` if it is plain/authored. */ declare function getPropertyModifier(entityType: string, property: string): PropertyModifier | undefined; /** * The modifiers an entity type carries, grouped by kind — `undefined` when the * entity has no modified properties. Powers the `property_modifiers` summary on * `get_entity_schema`. */ declare function getEntityModifierSummary(entityType: string): Record | undefined; declare const RECORD_ENTITY_TYPES: ReadonlySet; /** Whether an entity legitimately holds live/aggregate values (a record, not a definition). */ declare function isRecordEntity(entityType: string): boolean; /** * Aggregate shape (Pattern B): a numeric count/rollup derivable from edges or * children. Tight signal: a `number`-typed property whose name ends in `_count` * (or is `headcount`). Such a value should be `@derived`, not stored. */ declare function isAggregateShapedProperty(name: string, def: PropertyDefinition): boolean; /** * Runtime-state shape (Pattern A): a numeric live reading — a rate, percentage, * current value, remaining budget, latency percentile, monthly figure, or * per-unit rate. On a definition entity such a value should live on a `metric` * node (by edge) or be marked `@snapshot`. `*_status` enums are intentionally * left to the T1.1 status-shadows-phase guardrail; this detector owns the * numeric live readings. */ declare function isRuntimeStateShapedProperty(name: string, def: PropertyDefinition): boolean; /** * Validate an edge's `properties` bag against its catalog `property_schema`. * Returns human-readable error strings (empty array = valid). Unknown keys are * rejected; typed values are checked; an `assessment` is range-checked against * its scale and must carry its required keys. */ declare function validateEdgeProperties(edgeType: string, properties: Record | undefined): string[]; /** * UPG Labels: framework-vocabulary Rosetta Stone. Each entity type maps to a * canonical label, alt labels, and framework-specific labels. Answers "what * does framework X call this concept?" * * Registry-driven: derived from `UPG_ACTIVE_TYPES` and `UPG_MIGRATIONS`. */ interface UPGTypeLabel { /** Matches the NodeType string (canonical, post-consolidation) */ id: string; /** Default display name */ canonical_label: string; /** * One distinct, relevant emoji glyph for the type (the canonical, Captain-reviewed * set in `entity-emoji.ts`). Always resolves: a type without an explicit glyph * falls back to `DEFAULT_ENTITY_EMOJI`. This is THE live emoji source, surfaced * by `get_type_label` + `list_type_labels`, so renderers stop hardcoding. */ emoji: string; /** All known synonyms across frameworks + common usage (lowercase for matching) */ alt_labels: string[]; /** Framework-specific labels: { framework_id: "what that framework calls it" } */ framework_labels: Record; /** Only for types that use the designation pattern */ designations?: Record; } /** * The complete Rosetta Stone: one UPGTypeLabel for every active type. * * Assembly logic: * 1. If a type has a PRIORITY_LABELS entry → use it (richest framework_labels) * 2. Else if a type has a STANDARD_LABELS entry → build from that * 3. Else → auto-generate from the type name (canonical_label only) * * In all cases, migration aliases are merged into alt_labels automatically. */ declare const UPG_TYPE_LABELS: UPGTypeLabel[]; /** O(1) lookup by entity type id */ declare const UPG_TYPE_LABELS_MAP: ReadonlyMap; /** * Resolve the display label for an entity type, with optional framework context. * * Priority: * 1. If frameworkId provided and a framework_labels entry exists → use it * 2. If designation provided and a designations entry exists → use it * 3. Fall back to canonical_label * 4. Fall back to Title Case of the id * * @example * resolveLabel('persona') // → 'Persona' (canonical) * resolveLabel('need', 'lean_canvas') // → 'Problem' (framework-specific) * resolveLabel('need', undefined, 'pain') // → 'Pain Point' (designation-specific) * resolveLabel('not_a_type') // → 'Not A Type' (Title Case fallback) */ declare function resolveLabel(entityType: string, frameworkId?: string, designation?: string): string; /** * Build the TYPE_ALIASES map from alt_labels. * * Replaces the hand-maintained TYPE_ALIASES in validation.ts. * Maps every alt_label (and its snake_case variant) → canonical entity type id. * * Collision rule: first entry wins (entries earlier in UPG_TYPE_LABELS take priority). * * @example * const aliases = buildTypeAliases() * aliases['jtbd'] // → 'job' (JTBD alt-label → canonical) * aliases['job_to_be_done'] // → 'job' * aliases['problem'] // → 'need' (Lean Canvas label → canonical) */ declare function buildTypeAliases(): Record; /** Pre-built alias map. Import this instead of calling buildTypeAliases() repeatedly. */ declare const UPG_TYPE_ALIASES: Record; /** * UPG entity emoji glyphs: one distinct, relevant glyph per active entity type. * * The canonical, Captain-reviewed emoji set (icon-uniqueness pass). Single source * of truth, surfaced live via `get_type_label({type}).emoji` and * `list_type_labels()`, and consumed by the UPG site docs generator. Keep this * the ONLY place an entity emoji is authored, so renderers stop hardcoding. * * A type without an entry falls back to `DEFAULT_ENTITY_EMOJI` (the builder in * labels.ts applies the fallback), so the `emoji` field always resolves. * * https://unifiedproductgraph.org | MIT */ /** Fallback glyph for any active type without an explicit entry. */ declare const DEFAULT_ENTITY_EMOJI: "\uD83D\uDCE6"; /** Canonical emoji per entity type id (snake_case). */ declare const ENTITY_EMOJI: Record; /** * UPG canonical tree patterns: the named, server-owned shapes the `get_tree` * tool assembles (OST, OKR, user, product, validation, strategy, feature areas, * delivery, architecture, journey, design system, commercial, north star, org) — * one per tree-shaped region (or, for `architecture`/`commercial`/`org`, the * tree-shaped containment subset of a DAG/multi-hub region). * * A tree pattern is anchor + a TYPE-DRIVEN child map, NOT a list of edge names. * `get_tree` roots at `anchor_type` (falling back through `fallback_anchors` when * the anchor has no nodes or yields a childless tree) and walks the live graph: * at a node of type T, a neighbour whose type is in `child_map[T]` becomes a * child, whatever edge wired them. This is deliberate: the `/upg-show-tree` skill * drifted precisely because it hardcoded edge names (e.g. `vision_guides_strategic_theme`) * that a real graph did not use (its bets anchored on the product). Following to * the next TYPE, not a named edge, is drift-proof: a chain refinement in the edge * catalogue cannot rot the pattern. Polymorphic parentage is native: a child type * is simply listed under every parent type that can hold it (a `strategic_theme` * appears under `vision`, `product`, and `strategic_pillar`). * * Each child carries a `required` flag. Only a MISSING required child produces a * `gap`; optional children render when present and are silent when absent. This * is what separates a real structural hole (a bet with no initiative) from noise * (a feature with no epic, where the epic tier is optional). * * The chains here were authored by resolving every (parent, child) pair against * the live catalogue (2026-06-11), and corrected against a post-ship report from * field-testing on a real 304-node graph (G1-G7). * * https://unifiedproductgraph.org | MIT */ /** One child slot in a pattern: a child entity type and whether it is gap-worthy. */ interface UPGTreeChild { /** The child entity type. */ type: string; /** * When true, a parent of this slot's owning type that has NO child of `type` * is reported as a structural gap. Optional (default false) children render * when present and are silent when absent. */ required?: boolean; /** * The node property `get_tree` sorts this slot's children by (ascending, * nodes lacking it last). Ordering is a property of the DATA, not the viewer, * so the server returns children pre-sorted rather than leaving every client * to re-sort (UPG-663 sequence scalars: `phase_order`, `step_order`, * `action_order`, `state_order`). Omitted -> children keep declared slot order. */ order_by?: string; /** * Spine resolution for a DAG. When this slot reaches the SAME node redundantly * with a path through a sibling type (e.g. `user_journey -> journey_step` * directly AND `user_journey -> journey_phase -> journey_step`), name that * sibling type here. A child also reachable as a grandchild through a * `prefer_via`-typed child renders under that spine, NOT here -- collapsing the * redundant path so the node is neither silently dropped (G5) nor * double-counted (J1). A child not on the spine still renders here, so the * direct path remains the fallback when the grouping layer is absent. */ prefer_via?: string; } /** How a pattern decides what counts as a structural gap. */ type UPGTreeGapPolicy = 'required-children-only' | 'all-optional'; /** A canonical tree shape for `get_tree`. */ interface UPGTreePattern { /** Stable id (the `pattern` argument to get_tree). */ id: string; /** Display name. */ label: string; /** One-line description of what the tree shows. */ description: string; /** The framework this pattern realises, where one maps (else undefined). */ framework_id?: string; /** * The canonical region this pattern is the tree view of (ties pattern -> * region -> its `shape`). A region may afford several patterns (e.g. * `product_delivery` has product, feature_areas, and delivery). */ region: string; /** * `required-children-only`: a node missing a required child type is a gap. * `all-optional`: a browse view; nothing is gap-flagged (heterogeneous wiring * where gap-flagging would be noise). Descriptive; the assembler already * derives gaps from the per-child `required` flags. */ gap_policy: UPGTreeGapPolicy; /** Canonical root entity type. */ anchor_type: string; /** * Root types tried, in order, when `anchor_type` has no nodes OR the assembled * tree reaches no descendants (the "wrong root, empty tree" case). The anchor * actually used is reported back in the get_tree metadata. */ fallback_anchors: string[]; /** * Type-driven adjacency: for a node of type `parent_type`, a graph neighbour * whose type is one of `child_map[parent_type]`'s slots is a child. Omitted * parent types are leaves. Branching is expressed by listing several slots; a * child type listed under several parents is polymorphically parented. */ child_map: Record; /** The pattern's natural rendering depth (default for get_tree's `depth`). */ natural_depth: number; } /** * The canonical tree patterns. Every type referenced is an active UPG entity * type; integrity tests assert that and that each pattern is reachable from its * anchor. Append-only by convention (ids are a public surface). */ declare const UPG_TREE_PATTERNS: readonly UPGTreePattern[]; /** O(1) lookup by pattern id. */ declare const UPG_TREE_PATTERNS_BY_ID: Record; /** Look up a tree pattern by id. */ declare function getTreePattern(id: string): UPGTreePattern | undefined; /** * One resolved edge in a pattern's child map: the (parent -> child) pair plus * the canonical edge that wires it, resolved LIVE from the edge catalogue (not * stored on the pattern). `via`/`kind` are null only if the grammar has no edge * for the pair, which the drift-check forbids. A reader gets the real edge * without reverse-engineering it from behaviour. */ interface UPGTreePatternEdge { parent: string; child: string; /** Canonical edge type wiring parent -> child, or null if ungrounded. */ via: string | null; /** The edge's classification (hierarchy, semantic, cross-domain, ...), or null. */ kind: string | null; required: boolean; /** Node scalar this slot's children are sorted by, when declared (J2). */ order_by?: string; /** Sibling type whose path is the canonical spine for this child (J1), when declared. */ prefer_via?: string; } /** A pattern with its child map resolved to concrete edges (for introspection). */ interface UPGTreePatternDetail extends UPGTreePattern { /** The child_map flattened to (parent, child, via, kind, required) rows. */ edges: UPGTreePatternEdge[]; } /** A pattern summary row (no child_map) for list_tree_patterns. */ interface UPGTreePatternSummary { id: string; label: string; description: string; framework_id?: string; region: string; anchor_type: string; fallback_anchors: string[]; natural_depth: number; gap_policy: UPGTreeGapPolicy; /** Number of (parent -> child) slots in the child map. */ slot_count: number; } /** Flatten a pattern's child_map to resolved (parent, child, via, kind) edges. */ declare function resolveTreePatternEdges(pattern: UPGTreePattern): UPGTreePatternEdge[]; /** * The full declarative record for one pattern: the pattern plus its child_map * resolved to concrete edges. The `via`/`kind` are derived from the live edge * catalogue at call time, so they cannot drift from the grammar. */ declare function describeTreePattern(id: string): UPGTreePatternDetail | undefined; /** Every pattern as a summary row (the list_tree_patterns surface). */ declare function listTreePatternSummaries(): UPGTreePatternSummary[]; /** * UPG area-taxonomy cross-walk (0.9.16). Three overlapping "area" groupings ship * across the stack, and skills repeatedly conflated them, computing coverage * against a stale denominator: * * 1. `get_graph_digest.coverage` -> 10 stage-oriented coverage keys * (identity, understanding, discovery, validation, reaching, converting, * building, sustaining, learning, operations). Source of truth: * `BUSINESS_AREAS` in the SDK's lib/tools.ts. * 2. `list_regions` -> 11 canonical super-domain regions (this package's * regions/catalog.ts: strategy_outcomes ... foundations). * 3. the shared docs' "8 business areas" (identity ... learning). Source of * truth: `BusinessArea` / `BUSINESS_AREA_META` in the SDK's classification.ts. * * This table is the documented, introspectable correspondence between them, so a * skill can translate any direction instead of guessing. It is keyed by the 10 * `digest.coverage` keys (the denominator skills hit most): each entry names the * matching "8 business area" (null for `validation` and `operations`, which the * 8-area grouping folds into discovery / omits) and the canonical region ids the * key's entities live in (primary first). * * Drift guard: a test in the SDK pins `coverage_key` against the live * `BUSINESS_AREAS` keys and `business_area` against `BUSINESS_AREA_META`, so this * table cannot silently fall out of step with the runtime denominators. Region * ids are validated against `UPG_REGIONS` by the test in this package. * * https://unifiedproductgraph.org/spec | MIT */ /** One row of the area-taxonomy cross-walk, keyed by a `digest.coverage` key. */ interface UPGAreaTaxonomyEntry { /** The `get_graph_digest.coverage` key (one of the 10). */ coverage_key: string; /** Human label. */ label: string; /** * The matching "8 business area" id, or null when the 8-area grouping has no * direct equivalent (`validation` folds into `discovery`; `operations` is not * one of the 8). */ business_area: string | null; /** Canonical region ids whose entities this coverage key draws from (primary first). */ regions: string[]; } /** * The cross-walk. Append/edit in lockstep with the SDK `BUSINESS_AREAS` (coverage * keys) and `classification.ts` (business areas); the SDK drift test enforces it. */ declare const UPG_AREA_TAXONOMY: readonly UPGAreaTaxonomyEntry[]; /** O(1) lookup by `digest.coverage` key. */ declare const UPG_AREA_TAXONOMY_BY_COVERAGE_KEY: Readonly>; /** The cross-walk row for a `digest.coverage` key, or undefined. */ declare function getAreaTaxonomyEntry(coverageKey: string): UPGAreaTaxonomyEntry | undefined; /** The `digest.coverage` keys whose entities live (partly) in a given region. */ declare function getCoverageKeysForRegion(regionId: string): string[]; /** The "8 business area" ids that map to a given region (deduped). */ declare function getBusinessAreasForRegion(regionId: string): string[]; /** * UPG Intelligence Conditions: structured, machine-evaluable graph predicates. * * Check types: `EntityCheck` (count by criteria), `RelationshipCheck` * (edge existence/count), `BenchmarkCheck` (compare against stage benchmark), * `EdgeCountVsPropertyCheck` (per-node: edge count against the node's own * numeric property), plus the graph-shape counts (total / domain / orphan). * Composable via `and` / `or` operators. * * All but `EdgeCountVsPropertyCheck` are AGGREGATE checks — whole-graph tallies * compared against constants. That is why most detectors here are whole-graph * approximations of per-node rules, and why they can say a graph has a problem * without saying where. `EdgeCountVsPropertyCheck` (0.29.0) is the exception, * and it is what lets a violation name the nodes it is about. */ /** Count entities matching criteria */ interface EntityCheck { /** Discriminator, always `'entity_count'` for this check type */ type: 'entity_count'; /** The UPG entity type to count */ entity_type: UPGEntityType; /** * Optional filter narrowing which entities of `entity_type` are counted. * Three recognised forms, checked in this order: * * 1. `{ property, present }` (0.27.0) — count entities whose `property` * carries a non-empty value (`present: true`) or does NOT * (`present: false`). The absence form is what lets a pattern key on a * field nobody filled in, e.g. a `surface` with no `arbitration_rule`. * A value-keyed filter cannot express absence, because the collector only * indexes values that exist. * * NON-EMPTY VALUE HERE MEANS NON-EMPTY *STRING*. The aggregate indexes * backing this form record string values only, so a numeric `0` or a * boolean `false` counts as ABSENT to this filter. That is a known wart, * documented rather than fixed in 0.29.0: broadening it would change which * graphs existing detectors fire on, which is not a change to make in a * release already carrying correctness fixes. It costs nothing today (no * catalog filter uses this form since the contention branch moved to * `EdgeCountVsPropertyCheck`), and the broadening question is banked as * a tracked follow-up. A detector that needs to see a declared * zero should use `EdgeCountVsPropertyCheck.node_filter`, whose presence * test is deliberately broader. * 2. `{ property, value }` (0.17.0) — count entities whose `property` equals * `value`, e.g. `metric` where `designation === 'north_star'`. * 3. `{ status }` — count entities in a given lifecycle status. * 4. `{ property, present, except_property, except_value }` (0.28.0) — form 1, * but entities whose `except_property` equals `except_value` are removed * from the counted population first. This is what lets a detector carry a * declared exemption: `surface` counts as missing an `arbitration_rule` * unless it has declared `composition_mode: 'chained'`, for which having * no rule is the designed shape rather than an omission. The exclusion is * an intersection, not a marginal, so it cannot be derived from forms 1-3; * collectors compute it from `UPG_PRESENCE_EXCEPT_SPECS`, which is derived * from these filters, and store it under `presenceExceptKey(...)`. */ filter?: Record; /** Comparison operator */ comparison: 'eq' | 'gt' | 'lt' | 'gte' | 'lte' | 'zero' | 'nonzero'; /** Threshold value (not needed for 'zero' / 'nonzero') */ threshold?: number; } /** Check relationship existence or count */ interface RelationshipCheck { /** Discriminator, always `'relationship'` for this check type */ type: 'relationship'; /** Source entity type */ source_type: UPGEntityType; /** Edge type to check */ edge_type: UPGEdgeType; /** * Target entity type, or the wildcard `'node'` for a polymorphic edge. * * @remarks * THE WILDCARD IS NOT COSMETIC. The presence index a check reads is keyed by * the CONCRETE endpoint types of each edge instance, so a check declaring a * concrete target only sees edges that landed on that type. For a polymorphic * edge the concrete target could be any type in the semantic set, and a check * naming one of them would miss the rest — while a check naming `'node'` * against a concretely-keyed index would match nothing at all, which for a * `not_exists` comparison means firing on every graph including correct ones. * * The collector therefore records BOTH keys for a polymorphic edge: the * concrete `source|edge|target` and the wildcard `source|edge|node`. Declare * `'node'` here when, and only when, the edge is registered in * `UPG_POLYMORPHIC_EDGE_KEYS`. */ target_type: UPGEntityType | UPGWildcardEndpoint; /** Comparison */ comparison: 'exists' | 'not_exists' | 'count_gt' | 'count_lt'; /** Threshold for count comparisons */ threshold?: number; } /** Compare against stage-appropriate benchmark */ interface BenchmarkCheck { /** Discriminator, always `'benchmark'` for this check type */ type: 'benchmark'; /** The entity type to benchmark */ entity_type: UPGEntityType; /** How current count compares to the benchmark for the product's stage */ comparison: 'below_min' | 'above_max' | 'within_range' | 'missing'; } /** * Graph-wide entity-count check, ignoring entity type. Used by the Full lens * to detect "the graph is nearly empty" / "the graph is large" without naming * a specific type. */ interface TotalEntityCountCheck { /** Discriminator, always `'total_entity_count'` for this check type */ type: 'total_entity_count'; /** Comparison operator */ comparison: 'eq' | 'gt' | 'lt' | 'gte' | 'lte' | 'zero' | 'nonzero'; /** Threshold value (not needed for 'zero' / 'nonzero') */ threshold?: number; } /** * Count how many distinct UPG domains the graph populates. Used by the Full * lens to flag thin-coverage graphs (e.g. all entities in one or two domains). */ interface DomainCountCheck { /** Discriminator, always `'domain_count'` for this check type */ type: 'domain_count'; /** Comparison operator */ comparison: 'eq' | 'gt' | 'lt' | 'gte' | 'lte'; /** Threshold (number of distinct domains with at least one entity) */ threshold: number; } /** * Check whether a specific domain has any entities. Used by the Full lens to * detect "building without validating" patterns (validation domain empty * while product-spec domain is populated). */ interface DomainPopulationCheck { /** Discriminator, always `'domain_population'` for this check type */ type: 'domain_population'; /** Domain id (matches `UPGDomainId`) */ domain_id: string; /** Comparison operator */ comparison: 'zero' | 'nonzero' | 'gt' | 'lt'; /** Threshold for `'gt'` / `'lt'` comparisons */ threshold?: number; } /** * Count entities that have no incoming or outgoing edges. Used by the Full * lens to surface "loose thoughts waiting to be placed" in graph-health * prompts. */ interface OrphanCheck { /** Discriminator, always `'orphan_count'` for this check type */ type: 'orphan_count'; /** Comparison operator */ comparison: 'eq' | 'gt' | 'lt' | 'gte' | 'lte' | 'zero' | 'nonzero'; /** Threshold value (not needed for 'zero' / 'nonzero') */ threshold?: number; } /** * Count entities whose EDGE COUNT stands in a stated relation to one of their * own NUMERIC PROPERTIES (0.29.0). * * The first genuinely per-node check form. Every other check in this file is an * aggregate: it compares a whole-graph tally against a constant. This one * compares two values that both live on the same node, so it cannot be * expressed by any composition of the others — a graph-wide edge count says * nothing about which node holds which capacity, and the presence filters key * on whether a property EXISTS, never on what it says relative to anything else. * * The motivating case (feedback `af9ae4c2`, measured on a 43-surface field * graph): `contended-surface-without-arbitration` counted * `feature_occupies_surface` edges and never read `capacity`, so a header row * declaring room for four occupants and holding exactly four was flagged as * contended alongside a capacity-1 panel holding three. Ten surfaces flagged, * three of them wrongly, and all three wrong for this one reason. * * DELIBERATELY GENERAL. The shape is "edge count versus numeric property", not * "occupancy versus capacity". A surface's guest list against its capacity is * the first instance, but the form fits any place the graph states a numeric * intent and the edges record what actually arrived. * * COLLECTORS ARE DRIVEN BY DECLARED SPECS, never by speculative indexing. * `UPG_EDGE_COUNT_SPECS` is derived from the conditions in this catalog, so a * collector computes exactly the per-node tallies some detector asked for, and * seeds every declared spec before walking so that "nothing matched" and "this * collector is stale" stay distinguishable. The evaluator treats the second as * a worst case and over-reports, which is the safe failure for a detector whose * job is noticing omissions. */ interface EdgeCountVsPropertyCheck { /** Discriminator, always `'edge_count_vs_property'` for this check type */ type: 'edge_count_vs_property'; /** The entity type whose nodes are evaluated one at a time. */ entity_type: UPGEntityType; /** The edge type counted against each node of `entity_type`. */ edge_type: UPGEdgeType; /** * Which end of `edge_type` the evaluated node sits on. `inbound` (the * default) counts edges that POINT AT the node, which is the occupancy * reading: `feature_occupies_surface` runs feature → surface, so a surface's * guest list is its inbound count. */ direction?: 'inbound' | 'outbound'; /** The node's own numeric property the count is compared against. */ property: string; /** * What to compare against when the node does not carry `property` at all. * * ABSENCE IS A READING, NOT A HOLE. On `surface.capacity` absence means * unbounded, and an unbounded surface is not thereby exempt: it has stated no * limit, so it has stated no answer, and more than one occupant is exactly * the situation worth naming. Setting this to `1` encodes that — an * unqualified place holding two things has an unrecorded decision in it. */ property_absent_default: number; /** * How a node's edge count must relate to the property value for that node to * be counted. `gt` reads "more arrived than the design said would fit". */ node_comparison: 'gt' | 'gte' | 'lt' | 'lte' | 'eq'; /** * Optional additional per-node requirement: the node must carry * (`present: true`) or must not carry (`present: false`) a value for * `property`. * * PRESENT MEANS ANY VALUE, NOT ANY NON-EMPTY STRING. Only `undefined`, * `null` and the empty string read as absent here. `capacity: 0` is a real * cap (a reserved place nothing may occupy) and `mutates_content: false` is a * real answer, so treating either as an omission would be wrong: this check * form exists to read NUMERIC intents, and a form that could not see a * declared zero would misread the surfaces it was built for. * * THIS DELIBERATELY DIFFERS FROM `EntityCheck.filter`'s presence form, which * is string-only because the aggregate indexes behind it only hold strings. * The asymmetry is a real difference in what the two can see, not a style * choice, so this field does not borrow that vocabulary. Collectors express * the split as two named predicates (`isPropertyPresent` here, * `isStringPropertyPresent` there) rather than one helper with a comment. * * THIS IS ALSO WHY THE FORM IS PER-NODE RATHER THAN TWO ANDED AGGREGATES. * "Some surface is over capacity" AND "some surface has no arbitration rule" * are both true of a graph where those are DIFFERENT surfaces, so composing * two aggregate checks would keep firing on exactly the graphs this release * exists to stop firing on. Both halves have to be asked of the same node. */ node_filter?: { property: string; present: boolean; }; /** * Optional exemption, mirroring the 0.28.0 `except_property` / * `except_value` form on `EntityCheck`. A node declaring the exemption is * removed from the evaluated population entirely — it is not counted, and it * is not attributed. */ except_property?: string; /** Value of `except_property` that triggers the exemption. */ except_value?: string; /** Aggregate comparison applied to the number of nodes that matched. */ comparison: 'eq' | 'gt' | 'lt' | 'gte' | 'lte' | 'zero' | 'nonzero'; /** Threshold value (not needed for 'zero' / 'nonzero') */ threshold?: number; } /** * A structured, machine-evaluable condition for intelligence prompts. * * Modelled as a discriminated union. Every condition is either a single * leaf check or a compound node combining child conditions with `and` or `or`. * This prevents constructing nonsensical shapes (e.g. an `operator` with a * `check` at the same level). * * @example Leaf: { check: { type: 'entity_count', entity_type: 'persona', comparison: 'zero' } } * @example Compound: { operator: 'and', checks: [ * { check: { type: 'entity_count', entity_type: 'feature', comparison: 'nonzero' } }, * { check: { type: 'entity_count', entity_type: 'hypothesis', comparison: 'zero' } } * ] } */ type IntelligenceCondition = { check: EntityCheck | RelationshipCheck | BenchmarkCheck | TotalEntityCountCheck | DomainCountCheck | DomainPopulationCheck | OrphanCheck | EdgeCountVsPropertyCheck; } | { operator: 'and' | 'or'; checks: IntelligenceCondition[]; }; /** * Step sequence machinery shared by `UPGPlaybook` and `UPGApproach`. * * A `sub_sequence` step references either a playbook or an approach by * namespace-prefixed id (`playbook:*` / `approach:*`). */ /** * Discriminator for step behaviour. * * - `domain_guide`: resolve creation sequence from `DomainUsageGuide[domain_id]` at runtime * - `framework`: apply a structured framework (BMC, RICE, OST) * - `entity_sequence`: explicit list of entity types to create * - `sub_sequence`: nest another playbook or approach at this step * * The `sub_sequence` kind references either a playbook or an approach by * namespace-prefixed id (`playbook:*` / `approach:*`). */ type StepKind = 'domain_guide' | 'framework' | 'entity_sequence' | 'sub_sequence'; /** How a playbook or technique is entered. */ type EntryMode = 'domain' | 'stage' | 'gap' | 'framework'; /** Fields every step carries, independent of kind. */ interface StepBase { /** Position in the sequence (1-based) */ order: number; /** The phase label this step belongs to (e.g. "Discovery", "Validation") */ phase: string; /** Human-readable label for this step */ name?: string; /** Optional prompt shown to the user at this step. Structure only, no UI hints. */ prompt_hint?: string; /** Machine-evaluable condition that must hold before advancing to the next step */ transition_condition?: IntelligenceCondition; /** Sequence to chain into when a gap is detected at this step */ next_sequence_on_gap?: string; } /** Step that defers to a domain's `DomainUsageGuide.creation_sequence`. */ interface DomainGuideStep extends StepBase { kind: 'domain_guide'; /** Domain whose `DomainUsageGuide.creation_sequence` the runtime reads at execution time */ domain_id: string; } /** Step that applies a named framework. */ interface FrameworkInvocationStep extends StepBase { kind: 'framework'; /** ID of the framework to apply (matches an entry in UPG_FRAMEWORKS) */ framework_id: string; } /** Step that creates entities of an explicit, fixed list of types. */ interface EntitySequenceStep extends StepBase { kind: 'entity_sequence'; /** Explicit list of entity types to create at this step */ entity_types: readonly string[]; } /** Step that chains into a nested playbook or technique. */ interface SubSequenceStep extends StepBase { kind: 'sub_sequence'; /** ID of the nested sequence: `playbook:*` or `technique:*` */ sub_sequence_id: string; } /** * A single step in a playbook or technique, discriminated by `kind`. * * @example { kind: 'domain_guide', order: 1, phase: 'Discovery', domain_id: 'user' } * @example { kind: 'framework', order: 2, phase: 'Prioritisation', framework_id: 'rice-scoring' } * @example { kind: 'entity_sequence', order: 3, phase: 'Personas', entity_types: ['persona', 'job'] } * @example { kind: 'sub_sequence', order: 4, phase: 'Discovery', sub_sequence_id: 'playbook:users-needs' } */ type Step = DomainGuideStep | FrameworkInvocationStep | EntitySequenceStep | SubSequenceStep; /** * Surface identifier. Known surfaces are typed; runtimes may register their own * identifiers (string fallback) without touching the spec. */ type SurfaceId = 'cli' | 'entopo' | 'mcp_tool' | (string & {}); /** Runtime context handed to `startRun`. Open-ended; each surface adds its own. */ interface RunContext { /** Path to the `.upg` file, for file-backed runtimes */ graph_path?: string; /** Product identifier for cloud-backed runtimes */ product_id?: string; /** Active user (if authenticated) */ user_id?: string; /** Runtime-specific session identifier */ session_id?: string; /** Surface-specific extensions */ [key: string]: unknown; } /** What a step produced when recorded. */ type StepOutputKind = 'entities_created' | 'entities_updated' | 'response' | 'skipped'; /** The runtime record of what a single step produced. */ interface StepOutput { /** What category of output the step produced */ kind: StepOutputKind; /** IDs of entities created or updated by this step */ entity_ids?: readonly string[]; /** Free-form user response captured at this step */ response_text?: string; /** Surface-specific metadata (e.g. timing, confidence) */ metadata?: Record; } declare function isDomainGuideStep(step: Step): step is DomainGuideStep; declare function isFrameworkInvocationStep(step: Step): step is FrameworkInvocationStep; declare function isEntitySequenceStep(step: Step): step is EntitySequenceStep; declare function isSubSequenceStep(step: Step): step is SubSequenceStep; /** * UPG Region types (topology only). 10 super-domain rollups over the 36 * atomic domains. Each region carries an edge topology, anchor entity, and * shape archetype. Rendering concerns live in consumer codebases. * * https://unifiedproductgraph.org/spec | MIT */ /** * Shape archetype: the structural character of a super-domain. * * Pure topological names: each value describes how edges flow within the * region, independent of any rendering choice. Consumers may map these to * their own rendering systems (Entopo maps them to UCS pattern families). */ type UPGRegionShape = /** Top-down tree, source of strategy, closed upstream. Example: Strategy & Outcomes */ 'cascade' /** Central node pulled on by many domains; low outbound, high inbound. Example: Users & Needs */ | 'convergent' /** Directed graph with feedback cycles and self-nesting. Example: Discovery/Research/Validation */ | 'directed-cyclic' /** Mostly outbound, reads external reality and emits pressure. Example: Market & Competitive */ | 'tributary' /** Multiple parallel sub-hierarchies coexisting. Example: Experience/Design/Brand */ | 'multi-hierarchy' /** Hierarchical work breakdown with lifecycle at every node. Example: Product & Delivery */ | 'work-breakdown' /** Directed acyclic graph with heavy cross-node dependencies. Example: Engineering & Platform */ | 'dag' /** Multiple sub-hubs sharing users and messages. Example: Business/GTM/Growth */ | 'multi-hub' /** Small roster but referenced by many; the graph's shared measurement plane. Example: Analytics & Data */ | 'polymorphic-target' /** Event-triggered and policy-governed guardrail concerns. Example: Operations & Quality */ | 'event-driven'; /** * The graph role an entity plays *within* its super-domain. * * Purely structural, independent of any rendering concept. */ type UPGRegionEntityRole = 'anchor' | 'root' | 'hub' | 'container' | 'leaf'; /** A single entity's membership in a super-domain. */ interface UPGRegionEntityMembership { type: string; role: UPGRegionEntityRole; /** Optional: structural notes (e.g. "sideways-resolved", "dual-role"). */ notes?: string; } /** Direction of a boundary edge relative to the super-domain. */ type UPGBoundaryDirection = 'import' | 'export' | 'sideways'; /** * A boundary edge connecting the super-domain to another super-domain. * * Edge references are keys in `UPG_EDGE_CATALOG` (see `../catalog/edge-catalog.ts`). */ interface UPGBoundaryEdge { direction: UPGBoundaryDirection; edge_id: string; crosses_into: string; } /** * Anchor entity deep-dive within a region. * * The anchor is the single entity that best represents the super-domain's * structural problem. Consumers may use it to seed default views or treat it * as the region's primary citizen. */ interface UPGAnchorEntity { type: string; rationale: string; outbound_cross_edge_count: number; inbound_cross_edge_count: number; } /** * A super-domain region: a coherent rollup of 1-N atomic domains. * * Topology only: regions describe what is connected to what, not how it * renders. Rendering concerns live in Entopo (`apps/entopo/src/regions/`). * * @see {@link UPG_REGIONS} for the canonical catalog * @see MENTAL-MODEL.md for regions as the static "place" primitive */ interface UPGRegion { /** Machine-readable region id (snake_case) */ id: string; /** Human-readable name */ label: string; /** Numeric order, matches prose file prefix (01-10) */ order: number; /** Shape archetype */ shape: UPGRegionShape; /** 1-sentence mental model */ mental_model: string; /** Typical operators / personas working in this domain */ operators: readonly string[]; /** Atomic domain ids from UPG_DOMAINS that this super-domain composes */ composes_atomic_domains: readonly string[]; /** All entity memberships */ entities: readonly UPGRegionEntityMembership[]; /** The anchor entity for this region */ anchor: UPGAnchorEntity; /** Intra-domain edges (by UPG_EDGE_CATALOG id) */ intra_edges: readonly string[]; /** Boundary edges to other super-domains */ boundary_edges: readonly UPGBoundaryEdge[]; } /** The canonical list of super-domain region ids. */ type UPGRegionId = 'strategy_outcomes' | 'users_needs' | 'discovery_research_validation' | 'market_competitive' | 'experience_design_brand' | 'product_delivery' | 'engineering_platform' | 'business_gtm_growth' | 'analytics_data' | 'operations_quality' | 'foundations'; /** * UPG Playbook primitive. Bootstraps a region with an ordered creation * sequence. Region-scoped: one canonical playbook per region, plus optional * framework-anchored specialised playbooks. * * @see {@link UPG_PLAYBOOKS} for the catalog */ /** * The canonical playbook primitive. Structure only. Surface-specific * presentation lives in per-surface bindings. */ interface UPGPlaybook { /** Unique identifier. Namespace-prefixed: `playbook:[-variant]`. */ id: string; /** Human-readable name. Shown in CLI, Entopo, and MCP responses. */ name: string; /** Semver version of this playbook definition */ version: string; /** One-sentence description */ description: string; /** REQUIRED: the region this playbook anchors */ region: UPGRegionId; /** * Marks the canonical playbook for this region. Exactly one canonical per * region (W1 restated). Specialised playbooks omit this field or set false. */ is_canonical?: boolean; /** * Optional: the framework this playbook is anchored on. References * `UPGFramework.id`. Specialised playbooks are typically framework-anchored; * the canonical playbook usually is not. */ framework_id?: string; /** * Optional: frameworks relevant to this playbook's region, surfaced as * "related reading" without implying they are invoked as steps (unlike * `framework_id` / framework-kind steps). Each references a `UPGFramework.id`. * Lets consumers show a region's signature frameworks even when the * creation sequence is pure `entity_sequence`. (UPG-580.) */ related_framework_ids?: readonly string[]; /** * The single named outcome entity this playbook produces. Defaults to the * region's anchor entity if omitted. */ target_anchor_entity?: string; /** * Ordered creation sequence, typically one `DomainGuideStep` per atomic * domain composed by the region, OR a framework-driven sequence when * `framework_id` is set. */ creation_sequence: readonly Step[]; } /** Per-surface experience binding for a `UPGPlaybook`. */ interface PlaybookBinding { /** The `UPGPlaybook.id` this binding renders */ playbook_id: string; /** Surface this binding targets */ surface: SurfaceId; /** Identifier the runtime maps to a component or handler */ renderer: string; /** Per-step renderer overrides, keyed by `Step.order` */ step_renderers?: Record; /** Surface-specific step kinds the runtime handles beyond the canonical ones */ custom_step_kinds?: readonly string[]; /** Lifecycle hook id: runtime-resolved, fires when a run starts */ on_start?: string; /** Lifecycle hook id: runtime-resolved, fires after each step */ on_step_complete?: string; /** Lifecycle hook id: runtime-resolved, fires when a run completes */ on_run_complete?: string; } /** * Narrowing filter for `listPlaybooks`. All fields AND together. */ interface PlaybookFilter { /** Filter to playbooks anchored at a specific region */ region?: UPGRegionId; /** Filter to playbooks marked canonical (or specialised when false) */ is_canonical?: boolean; /** Filter to playbooks anchored on a specific framework */ framework_id?: string; /** Filter to playbooks reachable via a specific entry mode (always 'domain' for playbooks) */ entry_mode?: EntryMode; /** Filter to playbooks producing a specific anchor entity type */ target_anchor_entity?: string; } /** A concrete execution of a `UPGPlaybook`. */ interface PlaybookRun { /** Unique identifier for this run */ id: string; /** The `UPGPlaybook.id` this run is executing */ playbook_id: string; /** Version the run was started against */ playbook_version: string; /** ISO 8601 datetime */ started_at: string; /** ISO 8601 datetime, set when the run completes */ completed_at?: string; /** Order of the step currently in progress, if any */ current_step_order?: number; /** Runtime context passed when the run was started */ context: RunContext; } /** * The minimum interface a playbook runtime must expose. CLI, Entopo, and any * future surface implement these methods with identical signatures. */ interface PlaybookRuntime { /** Return all playbooks matching an optional filter */ listPlaybooks(filter?: PlaybookFilter): readonly UPGPlaybook[]; /** Return a single playbook by ID, or null if not found */ getPlaybook(id: string): UPGPlaybook | null; /** Return the single canonical playbook for a region (the "start here" path) */ getCanonicalPlaybookForRegion(region: UPGRegionId): UPGPlaybook | null; /** Return all playbooks (canonical + specialised) anchored at a region */ getPlaybooksForRegion(region: UPGRegionId): readonly UPGPlaybook[]; /** Start a new run of a playbook, returning the in-progress `PlaybookRun` */ startRun(playbook_id: string, context: RunContext): PlaybookRun; /** Record the output of a completed step against an in-progress run */ recordStep(run_id: string, step_order: number, output: StepOutput): void; } /** * UPG Lenses: role-specific projections combining vocabulary, visibility, * workflow, and intelligence. Applied on read. * https://unifiedproductgraph.org/spec | MIT */ /** * A single context-sensitive nudge belonging to a lens's intelligence * layer. Surfaces a message when a trigger condition evaluates to `true` * over the current product graph. * * Runtimes evaluate `structured_condition`, the machine-readable form. * `condition` is a human-readable shadow kept for documentation, prompt * engineering, debug output, and grep-ability. The two should describe * the same intent; when they diverge (rare), `structured_condition` is * the source of truth. * * @example * // A simple "missing thing" nudge that fires when the product has no outcomes. * const noOutcomes: UPGLensIntelligencePrompt = { * condition: 'outcomes.length === 0', * structured_condition: { * check: { type: 'entity_count', entity_type: 'outcome', comparison: 'zero' }, * }, * message: 'No outcomes defined yet. Start with what success looks like: what measurable result should this product drive?', * } * * @example * // A compound nudge: features exist but no hypotheses validate them. * const featuresWithoutHypotheses: UPGLensIntelligencePrompt = { * condition: 'features.length > 0 && hypotheses.length === 0', * structured_condition: { * operator: 'and', * checks: [ * { check: { type: 'entity_count', entity_type: 'feature', comparison: 'nonzero' } }, * { check: { type: 'entity_count', entity_type: 'hypothesis', comparison: 'zero' } }, * ], * }, * message: 'Features without hypotheses means you are building on assumptions. What needs to be true for these features to matter?', * } */ interface UPGLensIntelligencePrompt { /** * Human-readable shadow of `structured_condition`. Kept as documentary reference, * useful for prompt engineering, debug output, and search. Not evaluated * at runtime; if the two ever diverge, `structured_condition` wins. */ condition: string; /** Machine-evaluable condition: when to surface this prompt. */ structured_condition: IntelligenceCondition; /** The message to show, in the lens's voice */ message: string; } /** * A contextual projection of the product graph for a specific role, * framework, or mode of thinking. * * A lens combines four orthogonal layers: vocabulary (which labels to * use), visibility (which domains to show), workflow (which guided * sequence to follow), and intelligence (which nudges to surface). The * `.upg` file format is lens-unaware; lenses apply on read only. * * @example * const productLens: UPGLens = { * id: 'product', * name: 'Product', * description: 'Full graph, PM vocabulary, outcome-driven workflow', * icon: 'target', * framework_id: 'ost', * visible_domains: [], // empty = all visible * playbook_id: 'playbook:product-delivery', * benchmark_domains: ['strategy', 'user', 'discovery', 'validation'], * intelligence_prompts: [ * { * condition: 'outcomes.length === 0', * structured_condition: { * check: { type: 'entity_count', entity_type: 'outcome', comparison: 'zero' }, * }, * message: 'No outcomes defined yet. Start with what success looks like.', * }, * ], * audience: 'Product managers and founders making strategic decisions', * perspective: 'Outcome-driven, evidence-aware, strategically oriented.', * } */ interface UPGLens { /** Unique identifier (e.g. 'product', 'ux_design', 'engineering') */ id: string; /** Human-readable name */ name: string; /** One-sentence description of what this lens shows */ description: string; /** Lucide icon name for UI rendering */ icon: string; /** Framework ID for label resolution (maps to type-labels.ts framework_labels) */ framework_id?: string; /** Custom label overrides (entity type → display label) for cases where no framework covers the translation */ label_overrides?: Record; /** Domain IDs to show (from domains.ts). Empty array = show all. */ visible_domains: string[]; /** Specific entity types to always show even if their domain is hidden */ always_show_types?: string[]; /** Specific entity types to always hide even if their domain is visible */ always_hide_types?: string[]; /** * Optional ID of the canonical `UPGPlaybook` that best matches this lens's * mental model. Structure lives in the playbook registry; the lens owns * only presentation (labels, visibility, intelligence). * * Lenses without a 1:1 region playbook (e.g. `product`, `full`, both * cross-region) leave this field unset. Resolved via `getLensPlaybook`. * * Renamed from `workflow_id` (workflows → playbooks + techniques). */ playbook_id?: string; /** Which benchmark domain IDs to check */ benchmark_domains: string[]; /** Custom intelligence prompts scoped to this lens's perspective */ intelligence_prompts: UPGLensIntelligencePrompt[]; /** Who this lens is designed for */ audience: string; /** Plain language description of the perspective this lens provides */ perspective: string; } declare const UPG_LENSES: readonly UPGLens[]; /** * Get a lens by its id. Returns undefined if not found. * * @example * const lens = getLens('product') * // lens?.id === 'product' * // lens?.name === 'Product' * getLens('not_a_lens') // → undefined */ declare function getLens(id: string): UPGLens | undefined; /** * Get all lenses that include a given domain in their visible_domains. * Lenses with an empty `visible_domains` list match every domain. * * @example * const lenses = getLensesForDomain('user') * // includes 'product', 'research', and 'full' (the latter via empty-visible-domains rule) */ declare function getLensesForDomain(domainId: string): UPGLens[]; /** * Get all entity types visible through a given lens. * * Resolves the lens's visible_domains to concrete entity types from the * domain registry, then applies always_show_types and always_hide_types. * * If visible_domains is empty (show all), returns all types from all domains * minus always_hide_types. * * @example * const productLens = getLens('product')! * const types = getVisibleTypes(productLens) * // types.includes('persona') → true * // types.includes('feature') → true * // types.includes('api_endpoint') → false (filtered out for the product lens) */ declare function getVisibleTypes(lens: UPGLens): string[]; /** * Get the default lens (Full). * * @example * const lens = getDefaultLens() * // lens.id === 'full' * // lens.name === 'Full' // shows every type across every domain */ declare function getDefaultLens(): UPGLens; /** * Get all lens IDs. * * @example * getLensIds() * // → ['product', 'ux_design', 'engineering', 'growth', 'business', 'research', 'marketing', 'full'] */ declare function getLensIds(): string[]; /** * Resolve a lens to its associated `UPGPlaybook`. Returns undefined if the * lens has no `playbook_id` (cross-region lenses like `product` and `full`), * or if the id does not resolve in the canonical playbook registry. * * Renamed from `getLensWorkflow` (workflows → playbooks + techniques). * * @example * const lens = getLens('design')! * const playbook = getLensPlaybook(lens) * // playbook?.id matches lens.playbook_id, the bootstrap path for the * // experience_design_brand region. */ declare function getLensPlaybook(lens: UPGLens): UPGPlaybook | undefined; /** * UPG Domain Rings: 7 concentric groupings of the 36 UPG domains. * * Defines the ring assignment for every domain and the canonical ring metadata. * * The rings radiate outward from the product nucleus: * Nucleus → Understand → Define → Build → Grow → Operate → Extend * * This is the single source of truth for the OUTWARD ORDER and grouping of the * domains across every UPG surface (e.g. the /docs domain grid). The flattened * ring order — `ringOrderedDomainIds()` — replaces any hand-maintained domain * sequence. A module-init invariant asserts ring membership exactly covers the * canonical `UPG_DOMAINS` set, so a missing or extra domain fails loudly here * rather than silently dropping out of a downstream grid. */ interface UPGDomainRing { /** Machine-readable ring identifier */ id: string; /** Human-readable label */ label: string; /** The question this ring answers */ description: string; /** Domain IDs that belong to this ring */ domain_ids: readonly string[]; } declare const UPG_DOMAIN_RINGS: readonly UPGDomainRing[]; /** * Look up which ring a domain belongs to. * * @example * const ring = getRingForDomain('user') * // ring?.id === 'understand' * // ring?.label === 'Understand' * * @example * getRingForDomain('engineering')?.id // → 'build' * getRingForDomain('not_a_domain') // → undefined */ declare function getRingForDomain(domainId: string): UPGDomainRing | undefined; /** * Get all domain IDs in a given ring. * * @example * getDomainsInRing('understand') * // → ['user', 'user_research', 'market_intelligence', 'discovery', 'validation', 'feedback'] * * @example * getDomainsInRing('not_a_ring') // → [] */ declare function getDomainsInRing(ringId: string): string[]; /** * Every domain id in canonical ring order (nucleus outward, then within-ring * order). The single source of truth for the OUTWARD ORDER of the domains — * use this instead of a hand-maintained sequence. The coverage invariant above * guarantees this is a permutation of `UPG_DOMAINS` with no gaps or duplicates. * * @example * ringOrderedDomainIds().slice(0, 3) // → ['portfolio', 'workspace', 'user'] */ declare function ringOrderedDomainIds(): string[]; /** * UPG Domain Usage Guides: operational knowledge for MCP agents. * * Each guide gives a domain's anchor entity, creation sequence, named * patterns (entity + edge chains), required cross-domain bridges, and * common mistakes. Surfaced via `get_entity_schema` and `get_product_context`. */ interface UPGDomainPattern { /** Human-readable pattern name (no jargon, spell out acronyms) */ name: string; /** What this pattern accomplishes */ description: string; /** Entity types involved */ entity_types: UPGEntityType[]; /** Edge chain that connects them (in creation order) */ edge_chain: UPGEdgeType[]; } interface UPGDomainBridge { /** The edge type that crosses domain boundaries */ edge_type: UPGEdgeType; /** The target domain this edge connects to */ target_domain: UPGDomainId; /** When this bridge should be created */ when: string; } /** * A common mistake agents make in a domain. Structured so MCP * consumers can proactively surface violations (e.g. "you created a feature * without a persona, that's an anti-pattern") instead of only rendering * prose. Migrated from `anti_patterns: string[]`; existing copy lives in * `description`, and `name` / `affected_entity` / `remediation` are * optional for backward migration but preferred for new entries. */ interface UPGAntiPattern { /** Short title. Enables compact display and cross-guide search. */ name?: string; /** The anti-pattern itself: prose explanation of the mistake. */ description: string; /** Which entity type is typically involved in this anti-pattern. */ affected_entity?: UPGEntityType; /** What the agent should do instead. */ remediation?: string; } interface UPGDomainUsageGuide { /** Domain ID this guide covers */ domain_id: UPGDomainId; /** The entity you create first. Everything else hangs from it. */ anchor_entity: UPGEntityType; /** Recommended creation sequence */ creation_sequence: UPGEntityType[]; /** Named patterns within this domain */ patterns: UPGDomainPattern[]; /** Cross-domain edges that should always be created */ required_bridges: UPGDomainBridge[]; /** Common mistakes agents make in this domain */ anti_patterns: UPGAntiPattern[]; } declare const UPG_DOMAIN_GUIDES: readonly UPGDomainUsageGuide[]; /** * Look up the usage guide for a domain. * * @example * const guide = getGuideForDomain('user') * // guide?.anchor_entity === 'persona' * // guide?.creation_sequence === ['persona', 'job', 'need', ...] * * @example * getGuideForDomain('not_a_domain') // → undefined */ declare function getGuideForDomain(domainId: UPGDomainId | string): UPGDomainUsageGuide | undefined; /** * Get the anchor entity for a domain (the entity you create first). * * @example * getAnchorEntity('user') // → 'persona' * getAnchorEntity('market_intelligence') // → 'competitive_analysis' * getAnchorEntity('not_a_domain') // → undefined */ declare function getAnchorEntity(domainId: UPGDomainId | string): UPGEntityType | undefined; /** * Get all anti-patterns across all domains, flattened and tagged by domain. * * @example * const all = getAntiPatterns() * // all[0].domain === 'user' * // all[0].anti_pattern.name === 'persona_without_jobs' (example) * // all.length // one entry per anti-pattern across all guides */ declare function getAntiPatterns(): Array<{ domain: UPGDomainId; anti_pattern: UPGAntiPattern; }>; /** * UPG Benchmark type definitions. */ /** * Where a benchmark's range or expectation comes from. * * Structured so consumers can do "show me every Lean Startup benchmark" or * "show me every industry-standard DevOps expectation" without regex-matching * against free-form strings. Each variant carries enough metadata to render * the citation inline without further lookup. * * @example { kind: 'book', citation: 'The Lean Startup, Eric Ries (2011), ch. 7' } * @example { kind: 'practitioner', attribution: 'Teresa Torres, Continuous Discovery Habits' } * @example { kind: 'industry_practice', category: 'devops' } * @example { kind: 'fundamental' } */ type UPGBenchmarkSource = /** Cited from a named book or published work. */ { kind: 'book'; citation: string; } /** Attributed to a specific practitioner or method author. */ | { kind: 'practitioner'; attribution: string; } /** * Generally accepted industry practice rather than one citation. * `category` groups the benchmark by discipline (agile, devops, security, * voice_of_customer, etc.) so consumers can filter by space. */ | { kind: 'industry_practice'; category: string; } /** Definitional: true by the spec's own construction, not externally sourced. */ | { kind: 'fundamental'; }; /** Ordered stages from earliest to latest (canonical 9-stage model) */ declare const UPG_PRODUCT_STAGES: readonly ["concept", "validation", "build", "beta", "launch", "growth", "mature", "maintenance", "sunset"]; type StageRange = { min: number; max: number; } | null; interface CountBenchmark { /** The entity type this benchmark applies to */ type: UPGEntityType; /** The domain this entity type belongs to */ domain: UPGDomainId; /** Expected range per stage. null = not expected at this stage */ concept: StageRange; /** Expected count range at the validation stage */ validation: StageRange; /** Expected count range at the build stage */ build: StageRange; /** Expected count range at the beta stage */ beta: StageRange; /** Expected count range at the launch stage */ launch: StageRange; /** Expected count range at the growth stage */ growth: StageRange; /** Expected count range at the mature stage */ mature: StageRange; /** Expected count range at the maintenance stage */ maintenance: StageRange; /** Expected count range at the sunset stage */ sunset: StageRange; /** Attribution for this benchmark's expected ranges */ source: UPGBenchmarkSource; /** Why this benchmark exists and how the ranges were determined */ rationale: string; } interface RelationshipBenchmark { /** The parent entity type in the relationship */ parent_type: UPGEntityType; /** The child entity type connected to the parent */ child_type: UPGEntityType; /** Minimum children per parent */ min_per_parent: number; /** Stages where this relationship is expected */ stages: UPGProductStage[]; /** Attribution for this benchmark's expected relationship counts */ source: UPGBenchmarkSource; /** Why this relationship benchmark exists and how the threshold was set */ rationale: string; } interface RatioBenchmark { /** Human-readable identifier for this ratio (e.g. "hypothesis-to-experiment") */ name: string; /** Entity type(s) forming the numerator of the ratio */ numerator_type: UPGEntityType | UPGEntityType[]; /** Entity type(s) forming the denominator of the ratio */ denominator_type: UPGEntityType | UPGEntityType[]; /** Minimum acceptable ratio value. Below this signals an imbalance. */ expected_min: number; /** Stages where this ratio is meaningful and should be evaluated */ stages: UPGProductStage[]; /** Attribution for this ratio benchmark */ source: UPGBenchmarkSource; /** Why this ratio matters and how the minimum was determined */ rationale: string; } interface DomainActivation { /** The domain being activated */ domain_id: UPGDomainId; /** At which stage should this domain have at least 1 entity? */ expected_from: UPGProductStage; /** At which stage should this domain be well-populated? */ expected_mature: UPGProductStage; /** Attribution for this domain activation expectation */ source: UPGBenchmarkSource; /** Why this domain activation timing is expected */ rationale: string; } /** * Per-entity-type expected counts across the canonical 9-stage product journey. * * Each row binds an entity type (+ its domain) to a `StageRange` for every * stage from `concept` → `sunset`. A `null` range marks "not expected at this * stage". Ranges are sourced from product-management literature (JTBD, OST, * Lean Startup, BMC, etc.); the `source` column on the interface attributes * each row. * * Consumers: * - Intelligence layer (`intelligence.ts`) → health scoring, gap detection * - Graph audits → flag entities missing at a given stage * * Benchmark shape: see `CountBenchmark` in `./types.ts`. * * @see ./types.ts `CountBenchmark`, `StageRange`, `UPG_PRODUCT_STAGES` * @see ../intelligence.ts health scoring consumer * https://unifiedproductgraph.org/spec | MIT */ declare const UPG_COUNT_BENCHMARKS: CountBenchmark[]; /** * Expected parent → child relationship benchmarks (minimum connections that a * parent type should have to a given child type at the listed stages). * * Encodes wisdom like "each persona should have at least 2 jobs" (JTBD) or * "each hypothesis should produce at least one learning" (Lean Startup). * Relationship benchmarks are stage-scoped: a persona doesn't need 2 jobs * at `concept`, but does by `build`. * * Consumers: * - Intelligence layer → "thin parent" gap detection (e.g. personas missing * jobs); flags shallow modelling during audits * * Benchmark shape: see `RelationshipBenchmark` in `./types.ts`. * * @see ./types.ts `RelationshipBenchmark` * @see ../intelligence.ts audit consumer * https://unifiedproductgraph.org/spec | MIT */ declare const UPG_RELATIONSHIP_BENCHMARKS: RelationshipBenchmark[]; /** * Expected ratios between entity-type counts at given stages. * * Encodes relationships like "learnings / hypotheses ≥ 1" (every hypothesis * should yield at least one learning) or "evidence per insight ≥ 1" (no * untested insights). * * Numerator and denominator can be a single type or a union (e.g. the * "evidence" ratio counts `learning | observation | research_finding` against * insights). Ratios are stage-scoped because early stages should not be * penalised for not yet having collected evidence. * * Consumers: * - Intelligence layer → ratio-based health checks, "untested assumption" * warnings * * Benchmark shape: see `RatioBenchmark` in `./types.ts`. * * @see ./types.ts `RatioBenchmark` * @see ../intelligence.ts ratio audit consumer * https://unifiedproductgraph.org/spec | MIT */ declare const UPG_RATIO_BENCHMARKS: RatioBenchmark[]; /** * When each UPG domain is expected to "turn on" across the product journey. * * Each row binds a domain (see `registry/domains.ts`) to the earliest stage * at which domain activity is expected (`expected_from`) and the stage by * which it should be fully active (`expected_mature`). E.g. `strategy` should * be active from `concept`; `sales` typically activates at `launch`. * * Consumers: * - Intelligence layer → "domain asleep at this stage" warnings, stage * readiness checks * * Benchmark shape: see `DomainActivation` in `./types.ts`. * * @see ./types.ts `DomainActivation` * @see ../../registry/domains.ts canonical domain registry * @see ../intelligence.ts domain-activation audit consumer * https://unifiedproductgraph.org/spec | MIT */ declare const UPG_DOMAIN_ACTIVATION: DomainActivation[]; /** * benchmarks/ Product management wisdom as structured data. * * Encodes expected entity counts, relationships, ratios, and domain activation * thresholds per product stage. All 9 stages populated. */ /** * Look up the expected range for a type at a given stage. * * Returns `null` when the type is unknown OR when the type exists but is not * expected at that stage (e.g. `sales` activity at `concept`). * * @example * getBenchmark('product', 'concept') // → { min: 1, max: 1 } * getBenchmark('persona', 'build') // → { min: 2, max: 5 } (example range) * getBenchmark('not_a_type', 'growth') // → null */ declare function getBenchmark(entityType: UPGEntityType | string, stage: UPGProductStage): StageRange; /** * Get all benchmarks for a given domain. * * @example * const userBenchmarks = getBenchmarksByDomain('user') * // userBenchmarks.map(b => b.type) * // → ['persona', 'job', 'need', 'desired_outcome', ...] */ declare function getBenchmarksByDomain(domain: UPGDomainId | string): CountBenchmark[]; /** * UPG Curated Anti-Patterns: cross-domain reference set. * * Each entry pairs a memorable name with a machine-evaluable * `IntelligenceCondition`, the stages it fires in, a "why it matters" line, * and a remediation hint. * * Distinct from `UPGAntiPattern` in `domain-guides.ts`: * - `UPGAntiPattern` (per-domain): guidance for MCP agents working *inside* a domain. * - `UPGCuratedAntiPattern` (this file): cross-cutting patterns evaluated against the *whole* graph. * * Adding one: append to `UPG_ANTI_PATTERNS`. Integrity tests validate id * uniqueness, condition well-formedness, and stage/severity vocab. Cite * sources via `UPGBenchmarkSource` where applicable. * * https://unifiedproductgraph.org | MIT */ /** * Severity tier for a curated anti-pattern. * * - `'high'`: blocks downstream work or surfaces a missing causal chain * (e.g. features without hypotheses, building-without-validating). * - `'medium'`: quality or coverage gap that degrades the graph as a * reasoning surface (e.g. orphan-loose-thoughts, single-domain-graph). * - `'low'`: informational. Signal worth surfacing but not urgent. */ type UPGAntiPatternSeverity = 'high' | 'medium' | 'low'; /** * A curated, cross-domain anti-pattern with a machine-evaluable detector. * * @example * { * id: 'features-without-hypotheses', * name: 'Features without hypotheses', * description: 'The graph has features but no hypotheses. Work is being scoped without a stated belief about why it should work.', * structured_condition: { operator: 'and', checks: [ * { check: { type: 'entity_count', entity_type: 'feature', comparison: 'nonzero' } }, * { check: { type: 'entity_count', entity_type: 'hypothesis', comparison: 'zero' } }, * ] }, * why_it_matters: 'Features built without hypotheses ship as opinion; learnings from delivery cannot validate or refute anything because no claim was made.', * remediation: 'For each in-flight feature, draft one hypothesis it tests; link via feature_tests_hypothesis.', * stages: ['validation', 'build', 'beta', 'launch', 'growth'], * severity: 'high', * } */ interface UPGCuratedAntiPattern { /** * Stable slug: kebab-case, unique within `UPG_ANTI_PATTERNS`. * Surfaced as URL fragment on the `/intelligence` site page; never rename * once published (rename = breaking link surface). */ id: string; /** Short, memorable display title (≤ 6 words). */ name: string; /** * 2–3 sentence plain-English explanation. Read by a product * practitioner, not a graph engineer. */ description: string; /** * Detection scope. * - `'graph'` (default, omitted): evaluated against a single product graph by * the `evaluateAntiPatterns` chokepoint (validate_graph, get_anti_pattern_violations_for). * - `'portfolio'`: evaluated across products + the shared registry by * `portfolio_validate`. The single-graph evaluator SKIPS these (a portfolio * pattern can never flip a single graph invalid), and they carry no * `structured_condition` because the cross-product detector is not expressible * as an `IntelligenceCondition` over one graph. */ scope?: 'graph' | 'portfolio'; /** * Machine-evaluable detector. Composes `EntityCheck`, * `RelationshipCheck`, `BenchmarkCheck`, etc. via `and` / `or`. * Consumers (Entopo, MCP, the site) evaluate this against a graph. * * Required for graph-scoped patterns; OMITTED for `scope: 'portfolio'` * patterns, whose detector lives in `portfolio_validate` instead. */ structured_condition?: IntelligenceCondition; /** One sentence on the product impact when this anti-pattern fires. */ why_it_matters: string; /** * One sentence pointing at the fix. Where useful, names a * canonical edge type, entity type, or workflow / skill. */ remediation: string; /** * Product stages this anti-pattern can meaningfully trigger in. * Surface for stage-aware filtering (e.g. don't show * "competitors-missing" warnings during `concept`). */ stages: readonly UPGProductStage[]; /** Severity tier. See `UPGAntiPatternSeverity`. */ severity: UPGAntiPatternSeverity; /** * Optional citation for the pattern's origin (a book, practitioner, * industry practice, or fundamental). Reuses the same controlled * vocabulary as `CountBenchmark.source` so consumers render * citations uniformly. */ source?: UPGBenchmarkSource; /** * UPG version that introduced this anti-pattern (e.g. `'0.9.7'`). Lets * `get_spec_version` surface "new anti-patterns in this version" so a graph * authored clean under an earlier version is not silently flipped invalid on * upgrade with no heads-up (batch-6 #36). Omitted on baseline patterns that * predate this tracking (treated as "always present"). */ since?: string; } /** * The curated anti-pattern reference set. Append-only; existing ids are * stable URL fragments and content surfaces. */ declare const UPG_ANTI_PATTERNS: readonly UPGCuratedAntiPattern[]; /** * One `{ property, present, except_property, except_value }` filter declared * somewhere in `UPG_ANTI_PATTERNS`, flattened for collectors. * * @see UPG_PRESENCE_EXCEPT_SPECS */ interface UPGPresenceExceptSpec { /** Entity type the count is taken over. */ entity_type: string; /** Property whose presence is being counted. */ property: string; /** Property that removes an entity from the counted population. */ except_property: string; /** Value of `except_property` that triggers the exclusion. */ except_value: string; } /** * Every except-qualified presence filter the catalog declares (0.28.0). * * A collector cannot compute joint property counts speculatively: indexing * every (property, other-property, other-value) triple is quadratic in * properties-per-node and would tax every `validate_graph` call to serve one * detector. Nor can it derive the count arithmetically from the existing * indexes, because those record marginals and the question is an intersection. * * So the catalog declares what it needs and collectors compute exactly that * and no more. It stays correct without maintenance because it is derived from * the conditions themselves. * * EMPTY SINCE 0.29.0, AND THAT IS NOT A REGRESSION. Its one declarer was the * arbitration branch of `contended-surface-without-arbitration`, which moved to * the per-node `edge_count_vs_property` form because the aggregate could not * ask "over capacity" and "no rule" of the SAME surface. The mechanism stays * supported and tested: an except-qualified presence count is still the right * instrument for a detector that needs an intersection of two marginals and no * per-node arithmetic, and a future pattern declaring one gets it for free. * * @example * // The shape, as the 0.28.0 contention branch declared it: * // { entity_type: 'surface', property: 'arbitration_rule', * // except_property: 'composition_mode', except_value: 'chained' } */ declare const UPG_PRESENCE_EXCEPT_SPECS: readonly UPGPresenceExceptSpec[]; /** * Canonical key for an except-qualified presence count, shared by the * collectors that build `countsByTypeAndPropertyPresenceExcept` and the * evaluator that reads it. Both sides must agree, so neither spells it inline. * * @example * presenceExceptKey('arbitration_rule', 'composition_mode', 'chained') * // → "arbitration_rule!composition_mode=chained" */ declare function presenceExceptKey(property: string, exceptProperty: string, exceptValue: string): string; /** * One `edge_count_vs_property` check declared somewhere in * `UPG_ANTI_PATTERNS`, flattened for collectors (0.29.0). * * @see UPG_EDGE_COUNT_SPECS */ interface UPGEdgeCountSpec { /** Entity type whose nodes are evaluated one at a time. */ entity_type: string; /** Edge type counted against each node. */ edge_type: string; /** Which end of the edge the evaluated node sits on. */ direction: 'inbound' | 'outbound'; /** The node's own numeric property the count is compared against. */ property: string; /** Value used when the node does not carry `property` at all. */ property_absent_default: number; /** How the count must relate to the property for the node to match. */ node_comparison: 'gt' | 'gte' | 'lt' | 'lte' | 'eq'; /** Optional extra per-node presence requirement. */ node_filter?: { property: string; present: boolean; }; /** Property that removes a node from the evaluated population. */ except_property?: string; /** Value of `except_property` that triggers the exclusion. */ except_value?: string; } /** * Canonical key for one edge-count spec, shared by the collectors that build * `nodesByEdgeCountSpec` and the evaluator that reads it. Both sides must * agree, so neither spells it inline. * * Every discriminating field is in the key. Two detectors asking about the same * edge and property but with different thresholds, filters or exemptions are * different questions and must not collide on one tally. * * @example * edgeCountSpecKey({ entity_type: 'surface', edge_type: 'feature_occupies_surface', * direction: 'inbound', property: 'capacity', property_absent_default: 1, * node_comparison: 'gt', node_filter: { property: 'arbitration_rule', present: false }, * except_property: 'composition_mode', except_value: 'chained' }) * // → "surface|feature_occupies_surface|inbound|capacity|1|gt|arbitration_rule=absent|composition_mode=chained" */ declare function edgeCountSpecKey(spec: UPGEdgeCountSpec): string; /** * Canonical key for one `entity_count` filter, shared by the collectors that * build `nodesByEntityFilter` and the evaluator that reads it (0.29.0). * * Covers all four filter forms in `EntityCheck.filter`. The key encodes the * filter as the detector wrote it, so a `present: false` filter and a * `present: true` filter over the same property are different keys holding * different node sets, and neither has to be derived from the other by * subtraction. * * @example * entityFilterKey('surface', { property: 'arbitration_state', value: 'none' }) * // → "surface|arbitration_state=none" * entityFilterKey('hypothesis', { status: 'drafted' }) * // → "hypothesis|status=drafted" */ declare function entityFilterKey(entityType: string, filter: Record): string; /** * Which of the recognised `EntityCheck.filter` shapes this is. * * The presence form deliberately does NOT branch on `except_property` / * `except_value`. Those belong to the aggregate * `countsByTypeAndPropertyPresenceExcept` mechanism, which is still supported * for counting; ATTRIBUTION for that shape has no declarer, no collector path * and no test, and shipping an unexercised parallel path is how the next drift * starts. When a detector declares one, its attribution lands with it and with * a test. Until then such a filter classifies as `presence` and is attributed * on the presence predicate alone, which is a superset and therefore never * names a node the detector did not implicate. */ declare function classifyEntityFilter(filter: Record): 'presence' | 'value' | 'status' | 'unrecognized'; /** * One `entity_count` filter declared somewhere in `UPG_ANTI_PATTERNS`, * flattened for collectors (0.29.0). Attribution only: the counts these * filters drive are unchanged and still read the aggregate tallies. * * @see UPG_ENTITY_FILTER_SPECS */ interface UPGEntityFilterSpec { /** Entity type the filter is applied to. */ entity_type: string; /** The filter exactly as the detector declared it. */ filter: Record; /** * Which recognised shape this filter is, resolved once at derivation. * * Carrying it means a collector switches exhaustively on a closed set rather * than re-sniffing the shape and quietly skipping anything it was not taught. * A silent skip there produces a forever-empty match list: attribution dies * with no error and no failing test, which is the worst possible failure for * a feature whose whole job is to name things. */ kind: 'presence' | 'value' | 'status'; } /** * Every `entity_count` filter the catalog declares (0.29.0), so collectors can * record which nodes matched each one without indexing the whole graph. * * The catalog declares FOUR filters in total (one status form, three value * forms), which is what makes per-node attribution affordable here: a handful * of predicate evaluations per node, rather than an id list for every (type, * property, value) triple that happens to exist in the data. * * Note there is no `present` form among them since 0.29.0, when the contention * branch moved to `EdgeCountVsPropertyCheck`. The only `present: false` left in * the catalog is that check's `node_filter`, which this walk correctly does not * collect: it is a per-node clause on a different check type, tallied through * `UPG_EDGE_COUNT_SPECS` instead. The count is asserted in the tests rather * than trusted to this comment. */ declare const UPG_ENTITY_FILTER_SPECS: readonly UPGEntityFilterSpec[]; /** * Normalise one `edge_count_vs_property` check into its spec. * * THE ONLY PLACE THIS SHAPE IS CONSTRUCTED. The spec has nine fields and every * one of them is keyed into `edgeCountSpecKey`, so a second construction site * that forgot to apply the `direction` default (or added a field) would produce * a key that silently misses the collector's entry, and the check would read as * "no node matched" rather than failing. Collector, evaluator and attribution * all route through here. */ declare function checkToEdgeCountSpec(check: EdgeCountVsPropertyCheck): UPGEdgeCountSpec; /** * Every per-node edge-count check the catalog declares (0.29.0). * * Same contract as `UPG_PRESENCE_EXCEPT_SPECS`: the catalog states what it * needs, collectors compute exactly that and nothing more. Indexing every * (type, edge type, property) triple speculatively would tax every * `validate_graph` call to serve one detector, and unlike the aggregate counts * these tallies cannot be derived from each other. * * Today the list holds one entry (surface occupancy against `capacity`), and it * stays correct without maintenance because it is derived from the conditions * themselves. */ declare const UPG_EDGE_COUNT_SPECS: readonly UPGEdgeCountSpec[]; /** * Look up a curated anti-pattern by its slug id. * * @example * getAntiPatternById('features-without-hypotheses')?.severity // → 'high' * getAntiPatternById('not-a-real-pattern') // → undefined */ declare function getAntiPatternById(id: string): UPGCuratedAntiPattern | undefined; /** * Filter the curated set to anti-patterns relevant at a given product stage. * * @example * const concept = getAntiPatternsForStage('concept') * concept.every(ap => ap.stages.includes('concept')) // → true */ declare function getAntiPatternsForStage(stage: UPGProductStage): readonly UPGCuratedAntiPattern[]; /** * Filter the curated set by severity tier. * * @example * getAntiPatternsBySeverity('high').length >= 1 // → true */ declare function getAntiPatternsBySeverity(severity: UPGAntiPatternSeverity): readonly UPGCuratedAntiPattern[]; /** * Per-member-kind validation profiles (0.17.0). * * A workspace member graph is graded against a profile chosen by its * `member_kind`. Before 0.17.0 the only kind-aware behaviour was a single * hard-coded `member_kind === 'watched'` branch in the MCP `validate_graph` * tool that demoted ALL anti-pattern violations wholesale; `org_rollup` got no * relaxation, and the cloud server had no equivalent. This module lifts that into * the spec core as a table so every consumer (local MCP, cloud MCP, the site) * grades each kind the same way, and a new kind is a row, not a bespoke branch. * * Two axes per profile: * - `evaluate_concerns` — which anti-pattern concern families are RUN + reported. * - `gating_concerns` — the subset whose fired violations flip `valid` (the rest * are advisory: reported, non-gating). * * The concern of each curated anti-pattern is looked up by id from * `UPG_ANTI_PATTERN_CONCERNS` (kept here rather than inline on each pattern so the * whole classification reads in one place). Unlisted ids default to * `product_spine`. Portfolio-scoped patterns are unlisted — the single-graph * evaluator skips them regardless. * * https://unifiedproductgraph.org | MIT */ /** * The concern family an anti-pattern belongs to — the axis a per-member-kind * profile switches on. * - `product_spine` — presupposes a shippable product (personas/jobs, features/ * hypotheses, roadmap→outcome, competitors, journeys). A category error for a * non-product graph. * - `universal` — kind-independent: graph hygiene (orphans, single-domain) and * strategy discipline (objectives→key results). Applies to every kind. * - `operating` — operating_function expectation (a metric to operate toward, * real operating content). Only meaningful for that kind. */ type UPGAntiPatternConcern = 'product_spine' | 'universal' | 'operating'; /** Member-kind keys for the profile table. Mirrors the `member_kind` union on * `UPGDocument`; kept as a local literal because the SDK's `UPG_MEMBER_KINDS` * const is downstream of the spec core. */ type UPGMemberKindKey = 'product' | 'org_rollup' | 'watched' | 'operating_function'; /** * Concern family per curated anti-pattern id. Unlisted ids default to * `product_spine` (the conservative classification: gated for product, suppressed * for operating_function). The single-graph evaluator skips portfolio-scoped * patterns, so they are intentionally absent here. */ declare const UPG_ANTI_PATTERN_CONCERNS: Readonly>; /** The concern family for a curated anti-pattern id (default `product_spine`). */ declare function concernFor(antiPatternId: string): UPGAntiPatternConcern; interface UPGValidationProfile { /** Concern families whose patterns are EVALUATED (run + reported) for this kind. */ evaluate_concerns: readonly UPGAntiPatternConcern[]; /** Concern families whose fired violations GATE `valid` (a subset of `evaluate_concerns`). */ gating_concerns: readonly UPGAntiPatternConcern[]; } /** * The per-member-kind validation profile table. * * - `product` — the default; evaluates and gates the full product set. Identical * to pre-0.17.0 behaviour. * - `watched` — a monitored competitor-intelligence graph: product-thinking * patterns are category errors, so everything is advisory (gates nothing). * Reproduces the old hard-coded watched suppression, now in the core. * - `org_rollup` — the company umbrella: product-spine is a category error * (advisory) but universal hygiene still gates. (Pre-0.17.0 it incorrectly * gated product-spine too.) * - `operating_function` — a function a team operates: product-spine is not even * evaluated (no noise); universal hygiene and the operating spine gate. */ declare const UPG_VALIDATION_PROFILES: Readonly>; /** Resolve a member kind to its profile; unknown/absent kinds fall back to * `product` (gate-everything) for back-compat safety. */ declare function validationProfileFor(kind: string | undefined): UPGValidationProfile; /** Is an anti-pattern of this concern EVALUATED (run + reported) for this kind? */ declare function concernEvaluatedFor(kind: string | undefined, concern: UPGAntiPatternConcern): boolean; /** Does a fired violation of this concern GATE `valid` for this kind? */ declare function concernGatesFor(kind: string | undefined, concern: UPGAntiPatternConcern): boolean; /** * Coverage / benchmark anti-patterns that presuppose a graph has grown enough to * expect breadth (multiple domains, several personas, a competitor set). On a * brand-new thin graph they fire as false alarms — a 3-node stub is not the same * as a drifted product. Below `THIN_GRAPH_THRESHOLD` total entities these are * demoted to advisory (reported, non-gating) for every member kind, so an * intentionally-thin stub is not indistinguishable from drift. */ declare const COVERAGE_ANTI_PATTERNS: ReadonlySet; /** A graph with fewer than this many entities is treated as too thin to grade on * coverage breadth. */ declare const THIN_GRAPH_THRESHOLD = 8; /** True when a fired coverage anti-pattern should be advisory (not gating) because * the graph is still too thin to expect breadth. */ declare function isThinCoverageAdvisory(antiPatternId: string, totalEntityCount: number): boolean; /** * UPG Anti-Pattern Evaluator. Pure function over pre-computed graph stats. * Walks `UPG_ANTI_PATTERNS`, evaluates each `structured_condition` against * `AntiPatternInputs`, returns the violations. * * Synchronous. Collectors live outside this package (`packages/upg-sdk/src/lib/anti-pattern-inputs.ts`). * Covers every leaf check type in `IntelligenceCondition`; composes recursively. * * https://unifiedproductgraph.org | MIT */ /** * Pre-computed graph statistics consumed by the evaluator. * * Per-server collectors derive this from their own store: in-memory walks for * the local mcp-server, SQL queries for the cloud server. The evaluator * doesn't care which. * * Severity / id filters live on the evaluator's `options` arg, not here, so * callers can re-filter the same inputs without recollecting. */ interface AntiPatternInputs { /** Per-type entity counts. Example: `{ persona: 4, job: 2, feature: 0 }` */ countsByType: Record; /** * Per-type counts filtered by `status`. Only required for anti-patterns with * a `filter.status` clause (currently 1: `untested-hypothesis-pile-up`). * Example: `{ hypothesis: { drafted: 5, active: 2 } }`. */ countsByTypeAndStatus?: Record>; /** * Per-type counts filtered by a property value (0.17.0). Only required for * anti-patterns with a `filter: { property, value }` clause (e.g. * `operating-function-without-north-star` counts `metric` where * `designation === 'north_star'`). Shape: type → property key → value → count. */ countsByTypeAndProperty?: Record>>; /** * Per-type counts of entities that CARRY a non-empty value for a property * (0.27.0). Only required for anti-patterns with a * `filter: { property, present }` clause (e.g. * `contended-surface-without-arbitration` counts surfaces with NO * `arbitration_rule`). Shape: type → property key → count of entities of that * type with a value. The evaluator derives the "absent" count by subtracting * from `countsByType`, so a collector never has to enumerate absences. * Absent input reads as zero, so a stale collector degrades to "nothing * carries this property" rather than crashing. */ countsByTypeAndPropertyPresence?: Record>; /** * As `countsByTypeAndPropertyPresence`, but counted over a population that * EXCLUDES entities carrying a declared exemption (0.28.0). Only required for * anti-patterns with a `filter: { property, present, except_property, * except_value }` clause (currently 1: * `contended-surface-without-arbitration`, which exempts surfaces that have * declared `composition_mode: 'chained'`). * * Shape: type → `presenceExceptKey(property, except_property, except_value)` * → count of entities of that type that carry a value for `property` AND are * NOT exempt. The evaluator derives the absent-and-not-exempt count from this * plus `countsByType` and `countsByTypeAndProperty`, so collectors still * never enumerate absences. * * Collectors build it by walking `UPG_PRESENCE_EXCEPT_SPECS` rather than * indexing property pairs speculatively, which would be quadratic in * properties-per-node for the benefit of one detector. Absent input reads as * zero, which makes a stale collector OVER-report (every non-exempt entity * reads as missing the property) rather than silently under-report — the safe * direction for a check whose job is to notice omissions. */ countsByTypeAndPropertyPresenceExcept?: Record>; /** * Node ids matching each declared per-node edge-count check (0.29.0). Shape: * `edgeCountSpecKey(spec)` → the ids of the nodes that matched. * * The COUNT is the array length, so this input carries both halves of the * check: what fired, and which nodes it fired about. Collectors build it by * walking `UPG_EDGE_COUNT_SPECS`, never speculatively. * * A SEEDED-BUT-EMPTY entry and a MISSING entry mean different things, and * collectors must keep them distinct. Empty means "nothing matched" and the * check clears. Missing means "this collector predates the spec", and the * evaluator falls back to assuming every node of the type matched, preserving * the 0.28.0 property that a stale collector over-reports rather than * silently retiring the detector. Attribution stays empty on that path: a * fabricated node id is worse than an unattributed violation. */ nodesByEdgeCountSpec?: Record; /** * Node ids matching each declared `entity_count` filter (0.29.0), for * ATTRIBUTION ONLY. Shape: `entityFilterKey(entity_type, filter)` → ids. * * Counts are unaffected: every `entity_count` comparison still reads the * aggregate tallies above, exactly as it did before this input existed. This * runs alongside purely so a fired violation can name nodes, which means a * collector that omits it loses attribution and changes no verdict. * * Bounded by declaration, like every other spec-driven input here: the whole * catalog declares four filters, so this is a handful of predicate * evaluations per node rather than an index over every property. */ nodesByEntityFilter?: Record; /** * Boolean presence per `(source_type, edge_type, target_type)` tuple. * Key format: `${source_type}|${edge_type}|${target_type}`. * `true` iff at least one edge of that exact shape exists in the graph. */ edgePresence: Record; /** * Per-domain population. `true` iff the domain has at least one entity. * Example: `{ product_spec: true, validation: false, ... }`. */ domainPopulation: Record; /** Total node count in the graph. */ totalEntityCount: number; /** Number of distinct domains with at least one entity. */ domainCount: number; /** Nodes with zero in-edges AND zero out-edges. */ orphanCount: number; /** * Active product stage. Used to filter `UPG_ANTI_PATTERNS[i].stages[]`. * If undefined, the evaluator runs all patterns regardless of stage gating * (safer default: surface everything when stage is unknown). */ productStage?: UPGProductStage; /** * Workspace member kind (0.17.0). Selects the validation profile that decides * which anti-pattern concern families are evaluated for this graph. Absent = * `product` (evaluate the full product set; back-compat). */ memberKind?: string; } /** * One fired anti-pattern, lifted from the catalog with prose attached. * * `target_entities` is filled from the catalog's referenced entity-type * strings. Phase 1 keeps these as types; Phase 1.x will promote to specific * entity ids once the input collector tracks them. */ interface AntiPatternViolation { anti_pattern_id: string; name: string; severity: UPGAntiPatternSeverity; /** The concern family this pattern belongs to (0.17.0). Lets callers partition * fired violations into gating vs advisory per the member-kind profile. */ concern: UPGAntiPatternConcern; /** Entity-type strings the catalog references. Phase 1: types, not ids. */ target_entities: string[]; /** * The specific nodes this violation is about (0.29.0), where the fired * condition could name them. Sorted, deduplicated, and drawn only from the * branches that actually contributed to the fire. * * ABSENT MEANS "THIS DETECTOR CANNOT NAME NODES", NOT "NO NODES". Most * patterns here are whole-graph approximations of per-node rules: they * compare aggregate tallies against constants, so they can say a graph has a * problem without knowing where it lives. Only checks that evaluate nodes one * at a time attribute, plus the declared `entity_count` filters. * * ATTRIBUTION IS PARTIAL, AND PARTIAL PER TYPE. A violation may name nodes of * one type while saying nothing about another type in `target_entities`: the * contention detector names surfaces and never the features occupying them, * though both types appear there. So a consumer must NOT read a non-empty * list as "these are the only implicated entities". * * The contract for a reverse lookup is: this list is authoritative for the * types it actually covers, and silent about every other type, which must * keep resolving through `target_entities`. Reading it as globally * authoritative makes entities of the uncovered types unreachable, which is * a reachability regression dressed up as precision. * * Optional so every existing consumer keeps compiling and behaving as before. */ target_node_ids?: string[]; description: string; why_it_matters: string; remediation: string; source?: UPGCuratedAntiPattern['source']; } interface EvaluateAntiPatternsOptions { /** Filter to one severity tier. */ severity?: UPGAntiPatternSeverity; /** Restrict evaluation to a subset of anti-pattern ids. */ anti_pattern_ids?: string[]; } /** * Evaluate the curated anti-pattern catalog against a graph's pre-computed * stats. Returns the violations, sorted high → medium → low, then by id asc. * * @param inputs The pre-computed graph stats (see `AntiPatternInputs`). * @param options Optional filters: `severity`, `anti_pattern_ids` subset. * * @example * const violations = evaluateAntiPatterns(inputs) * const highOnly = evaluateAntiPatterns(inputs, { severity: 'high' }) * const subset = evaluateAntiPatterns(inputs, { * anti_pattern_ids: ['features-without-hypotheses', 'orphan-loose-thoughts'], * }) */ declare function evaluateAntiPatterns(inputs: AntiPatternInputs, options?: EvaluateAntiPatternsOptions): AntiPatternViolation[]; /** * Walk the condition and return the unique entity-type strings it references. * * This is what fills `target_entities`, so it is the type half of every * consumer's reachability. A check form missing from the walk produces a * violation nothing can find by type, which is why the walk is exported: it is * testable in isolation, against conditions built to defeat the masking that * hides an omission inside a real multi-check pattern. */ declare function collectTargetEntities(cond: IntelligenceCondition | undefined): string[]; /** * `UPGProductStage` validation and soft-coercion. * * Strategy: soft-coerce on read, strict on write. Existing `.upg` files * carrying legacy stage values (`idea`, `discovery`, `mvp`) load with an * in-memory coercion to the nearest canonical equivalent and a deprecation * warning. New writes via `create_product` and `update_node` reject * non-canonical values. * * @module intelligence/product-stage-coercion */ /** * Documented mapping from known-bad legacy stage values to the closest * canonical UPGProductStage. Keys are lowercased. Matching is * case-insensitive at the call boundary. * * Mapping rationale: * - `idea` → `concept`: pre-canonical alias from early v0.1 product nodes. * Matches the v0.2.13 `properties.stage` migration value_map. * - `discovery` → `validation`: "discovery" was used pre-v0.2 to mean the * pre-build research / customer-discovery phase. UPGProductStage does not * have a separate "discovery" phase. The closest canonical equivalent is * `validation` (testing demand, talking to users: the discovery activity). * - `mvp` → `build`: minimum viable product, actively building v1. * - `production` → `launch`: generally available shipped product. * - `draft` → `concept`: mirrors the v0.2.13 `lifecycle_status` migration. * - `active` → `launch`: mirrors the v0.2.13 `lifecycle_status` migration. * - `archived`, `retired`, `deprecated` → `sunset`: winding down or done. */ declare const UPG_PRODUCT_STAGE_COERCION_MAP: Readonly>; /** * True when `value` is a canonical UPGProductStage. * * Use this as the strict guard on the write path. Readers should prefer * `coerceProductStage` so legacy values still resolve. */ declare function isCanonicalProductStage(value: unknown): value is UPGProductStage; /** * Result of a soft-coercion attempt on a product `stage` value. * * - `canonical`: the canonical UPGProductStage to use, or `undefined` when * the input was unrecognised (no entry in the coercion map AND not already * canonical). Callers can choose to fall back to a default (typically * `'concept'`) or surface the unknown value. * - `originalValue`: the raw input, for warning messages and audit trails. * - `wasCoerced`: `true` if the input was a known legacy value that was * mapped to a canonical equivalent. `false` if the input was already * canonical or unrecognised. * - `wasUnknown`: `true` if the input was non-canonical AND not in the * coercion map. Callers should fall back to a default and log loudly. */ interface ProductStageCoercion { canonical: UPGProductStage | undefined; originalValue: unknown; wasCoerced: boolean; wasUnknown: boolean; } /** * Soft-coerce a stage value to canonical UPGProductStage, falling back to * the documented mapping for known legacy values. Used at `.upg` load time * so existing graphs keep working. * * @example * coerceProductStage('idea') * // → { canonical: 'concept', originalValue: 'idea', wasCoerced: true, wasUnknown: false } * * @example * coerceProductStage('concept') * // → { canonical: 'concept', originalValue: 'concept', wasCoerced: false, wasUnknown: false } * * @example * coerceProductStage('xyz') * // → { canonical: undefined, originalValue: 'xyz', wasCoerced: false, wasUnknown: true } */ declare function coerceProductStage(value: unknown): ProductStageCoercion; /** * Strict validator for the write path. Returns `null` when `value` is a * canonical UPGProductStage, otherwise a structured error message * including the canonical set and any documented coercion target. * * Intended for `create_product` and `update_node({ type: 'product', ... })`. * Callers should reject the operation when this returns non-null. * * @example * validateProductStageStrict('idea') * // → "Invalid product stage: \"idea\". Canonical UPGProductStage values: ..." */ declare function validateProductStageStrict(value: unknown): string | null; /** * Canonical playbook definitions. * * 12 playbooks across 10 regions: * - 10 canonical (one per region; W1 invariant). * - 2 specialised (business-growth-metric-driven, business-marketing-audience-first). * * UPG-585: no playbook is framework-anchored anymore; framework anchors now * live on the canonical playbooks' related_framework_ids. * * Every existing v0.2.x workflow maps here; cross-region lens workflows * (`product-journey`, `full-product-journey`) were dropped; their content * is already covered by the 10 canonical region playbooks. */ declare const STRATEGY_OUTCOMES_PLAYBOOK: UPGPlaybook; /** * Canonical users_needs playbook: net-new authored content per Q.C of the * decision doc. Persona has 25 inbound cross-edges; the spec's gravitational * centre. Skeleton-only is not acceptable here. */ declare const USERS_NEEDS_PLAYBOOK: UPGPlaybook; declare const DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK: UPGPlaybook; declare const MARKET_COMPETITIVE_PLAYBOOK: UPGPlaybook; declare const EXPERIENCE_DESIGN_BRAND_PLAYBOOK: UPGPlaybook; declare const PRODUCT_DELIVERY_PLAYBOOK: UPGPlaybook; declare const ENGINEERING_PLATFORM_PLAYBOOK: UPGPlaybook; declare const BUSINESS_GTM_GROWTH_PLAYBOOK: UPGPlaybook; declare const BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK: UPGPlaybook; declare const BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK: UPGPlaybook; declare const ANALYTICS_DATA_PLAYBOOK: UPGPlaybook; declare const OPERATIONS_QUALITY_PLAYBOOK: UPGPlaybook; /** * Every canonical playbook shipped with `@unified-product-graph/core`. * * Spans the ten canonical regions: one canonical playbook per region (the * W1 invariant) plus zero or more specialised playbooks per region, three * of which are framework-anchored (BMC, AARRR, build-measure-learn). * * Order: by region (1 → 10), canonical first within each region. */ declare const FOUNDATIONS_PLAYBOOK: UPGPlaybook; declare const UPG_PLAYBOOKS: readonly UPGPlaybook[]; /** * playbooks/: UPG Playbook public API. * * Exports: `UPGPlaybook`, `PlaybookRuntime`, `PlaybookFilter`, `PlaybookRun`, * `PlaybookBinding`, `UPG_PLAYBOOKS`, and the lookup helpers * (`getCanonicalPlaybookForRegion`, `getPlaybooksForRegion`, `getPlaybookById`). */ /** * Look up a canonical playbook shipped with `@unified-product-graph/core` by id. * Returns `undefined` when the id is unknown (or namespaces a technique). */ declare function getPlaybookById(id: string): UPGPlaybook | undefined; /** * Return the single canonical playbook for a region (the "start here" path). * Returns `null` when the region has no canonical playbook (W1 invariant * violation, caught by `audit-playbook-coverage.ts`). */ declare function getCanonicalPlaybookForRegion(region: UPGRegionId): UPGPlaybook | null; /** * Return every playbook (canonical + specialised) anchored at a region. * Order is the canonical `UPG_PLAYBOOKS` order, canonical entry first by * convention (catalog authoring discipline). */ declare function getPlaybooksForRegion(region: UPGRegionId): readonly UPGPlaybook[]; /** * Framework categories and structure patterns. `FrameworkCategory` is the * discipline a framework belongs to. `StructurePattern` is the visual * topology of its output. */ /** The broad domain a framework belongs to */ type FrameworkCategory = 'prioritization' | 'strategy' | 'discovery' | 'business_model' | 'metrics' | 'validation' | 'planning' | 'competitive' | 'design' | 'ux_research' | 'user_understanding' | 'research' | 'accessibility' | 'feedback_voc' | 'engineering' | 'devops' | 'security' | 'qa_testing' | 'ai_ml' | 'agentic' | 'growth' | 'marketing' | 'go_to_market' | 'sales' | 'pricing' | 'data_analytics' | 'legal_compliance' | 'customer_success' | 'team_process' | 'program_mgmt' | 'content' | 'education' | 'partnerships' | 'localisation' | 'portfolio'; /** All valid framework categories as a runtime array */ declare const UPG_FRAMEWORK_CATEGORIES: readonly FrameworkCategory[]; /** The visual / topological shape a framework's structure takes */ type StructurePattern = 'tree' | 'table' | 'matrix' | 'funnel' | 'collection' | 'quadrant' | 'flow'; /** All valid structure patterns as a runtime array */ declare const UPG_STRUCTURE_PATTERNS: readonly StructurePattern[]; /** * UPG Framework type definitions. * * Frameworks are declarative methods that structure graph data into known * product-management patterns (RICE, Lean Canvas, Opportunity Solution Tree). * Each framework describes four layers: data, structure, presentation, education. * * https://unifiedproductgraph.org/spec | MIT */ /** * One declared, deterministic test over a single property. Atoms combine into a * slot's membership rule; an atom is never a membership rule by itself. * * `scope` says whose property is read, reusing the discriminator that already * exists on `PropertyRequirementLike` rather than inventing a parallel axis: * `'entity'` reads a property of the entity itself (an assumption's * `risk_level`), `'framework'` reads a measured property held on the exercise * edge (Kano's `functional_response`). * * Semantics a consumer must honour — each exists because its violation is a way * a grid could silently lie: * * - **Pure and total.** A function of the entity's and the exercise edge's * properties, and nothing else. No clock, no random, no ambient read. * - **Mutually exclusive.** No entity may satisfy two sibling predicates. * Overlapping predicates make a framework invalid; that is an authoring bug to * reject, never a tie for the renderer to break by declaration order. * - **Not exhaustive.** The complement of the union is the legitimate, expected * set of unplaced entities. Do not add a catch-all. * - **Absence is not a value.** A missing property, a null, or an assessment * without a value fails every predicate. It never compares as zero and never * sorts into a corner cell. * - **Thresholds are declared, never inferred.** Every boundary is a literal * here; the renderer contributes no default of its own. * * Ruling and rationale (including Amendment 1, which set the arity): * the zone-predicate hook ruling (internal decision record, 2026-08-07) */ type FrameworkSlotPredicateAtom = { /** Whose property is being read: the entity's own, or the framework exercise's */ scope: 'entity' | 'framework'; /** The property name being tested */ property: string; } & ({ op: 'eq'; value: string | number | boolean; } | { op: 'in'; value: ReadonlyArray; } | { op: 'gte'; value: number; } | { op: 'lt'; value: number; } /** Half-open interval `[min, max)` — min inclusive, max exclusive */ | { op: 'band'; value: readonly [number, number]; }); /** * A slot's membership rule: a conjunction. An entity belongs iff EVERY atom * holds; absence of any atom's property fails the whole conjunction. * * A list rather than a single atom because every real quadrant framework * partitions on two properties at once — an assumption map on risk AND * confidence, Kano on functional AND dysfunctional response. A one-atom rule * cannot tell "test first" from "monitor", and declaring both as "risk is high" * would make them overlap, which is invalid. * * Always a list, even for one atom, so consumers never branch on arity. * Conjunction only: express a disjunction as two slots, which gives each * disjunct its own visible cell. Empty is invalid — it would match everything, * which is a catch-all cell in disguise. */ type FrameworkSlotPredicate = readonly FrameworkSlotPredicateAtom[]; /** A named position within a framework's visual structure, populated by an entity type */ interface FrameworkSlot { /** Display label for this slot (e.g. "Key Partners", "Problem", "Reach") */ label: string; /** The UPG entity type that fills this slot */ entityTypeId: string; /** * The semantic role this slot plays in the framework, as a stable * machine-readable id (e.g. "pain_reliever", "accountable", "must_have"). * * Distinct from `entityTypeId`: many frameworks fill several slots with the * SAME entity type (a Value Proposition Canvas has six `feature` slots; a RACI * matrix four `role` slots), and the slot role is what disambiguates them so a * consumer can address "the pain-reliever features" rather than just "the * features". Framework-local vocabulary, not a global UPG type. * * Additive and optional. It is also distinct from the COARSE structural * `FrameworkEntityTypeSpec.role` (item/bucket/scored_item/root/leaf/branch), * which describes an entity type's structural place; this is the FINE semantic * part a specific slot plays. Neither is a validation signal — scoring is keyed * off `scope: 'framework'` properties, not roles. */ role?: string; /** * The membership rule for this slot, when membership is decided by a property * value rather than by entity type or framework role. * * Carrying it here — beside `role` — is what keeps the zone class computable * from the spec record alone: a slot with a `predicate` is a PREDICATE-zone. * The two are not alternatives. On a predicate zone, `role` stops being the * membership channel and becomes the manual-override channel. * * Additive and optional. A slot without one is unaffected. */ predicate?: FrameworkSlotPredicate; /** Explanation of what this slot represents in the framework */ description?: string; } /** Where the framework came from: attribution and licensing */ interface FrameworkOrigin { /** Whether the framework is from academia, a practitioner, the community, or original to UPG */ type: 'academic' | 'practitioner' | 'community' | 'custom'; /** Human-readable attribution (e.g. "Sean Ellis", "Marty Cagan", "Teresa Torres") */ attribution?: string; /** Provenance narrative: how the framework came about (e.g. "Published in Business Model Generation") */ description?: string; /** URL to the original source or publication */ url?: string; /** Year the framework was first published or popularised */ year?: number; /** License under which the framework definition is shared */ license?: string; } /** Declares which UPG entity type plays which role within a framework */ interface FrameworkEntityTypeSpec { /** The UPG entity type (must be a valid UPGEntityType) */ type: string; /** The role this entity plays in the framework (e.g. "root", "item", "branch", "leaf", "bucket") */ role: string; /** Minimum number of entities of this type required */ min_count?: number; /** Maximum number of entities of this type allowed */ max_count?: number; /** Whether to auto-create placeholder entities when the framework is applied */ auto_scaffold?: boolean; } /** A property that the framework requires on an entity */ interface FrameworkPropertyRequirement { /** The property key on the entity's properties object */ property: string; /** The data type of the property value */ type: 'number' | 'string' | 'enum' | 'boolean' | 'assessment'; /** Whether the property must be filled for the framework to function */ required: boolean; /** * Where this property lives (UPG-595, Captain-ratified). * * - `'entity'` (default): the property is intrinsic to the entity type and * MUST exist in `UPG_PROPERTY_SCHEMA[]`. The framework-shape * audit gates this: an `'entity'`-scoped requirement that the entity schema * does not define is a real referential-integrity bug. * - `'framework'`: the property is a **framework-scoped scoring input**. It is * declared BY the framework FOR the framework (RICE's reach/impact/ * confidence/effort; ICE's impact/confidence/ease; WSJF's cost-of-delay/ * job-size; MoSCoW's `moscow` bucket; Kano's functional/dysfunctional * response; Wardley's evolution_stage/visibility). It is NOT asserted as an * intrinsic entity property, so the spec stays noise-free and the audit * exempts it from the entity-schema check by design. * * A saved score does NOT live on an entity property — it would live on a * framework-application edge / instance (a persistent annotation store, which * is deferred). At call time, `executePrioritise` sources these inputs from * `node.properties` directly and returns clean `type_mismatch` / * `missing_properties` hints; it never depends on the entity schema. * * @default 'entity' */ scope?: 'entity' | 'framework'; /** Default value to use when the property is not set */ default_value?: unknown; /** Valid values when type is 'enum' */ enum_values?: string[]; /** * The assessment scale this property is rated on (only when type is * 'assessment'). References a scale id in `UPG_SCALES` (e.g. 'reach_5', * 'impact_5'). Lets renderers show the scale's points (1=Minimal … 5=Massive) * and formulas compute on a known range. See `grammar/scales.ts`. */ scale_id?: string; /** Human-readable label for the property (shown in UI) */ label?: string; /** Explanation of what this property represents */ description?: string; } /** A property whose value is computed from other properties via a math DSL */ interface FrameworkComputedProperty { /** The property key that will hold the computed result */ property: string; /** A simple math expression referencing other properties (e.g. "(reach * impact * confidence) / effort") */ expression: string; /** The entity type this computed property applies to */ entity_type: string; /** Human-readable label for the computed property */ label?: string; /** How to format the computed value in the UI */ format?: 'number' | 'percentage' | 'currency'; } /** * A scoring method declared ONCE and applied to several entity types. * * Many scoring frameworks (RICE, ICE, WSJF, cost-of-delay) apply the same * inputs and formula across more than one entity type. Authored verbatim, that * means repeating the identical `required_properties` array and computed * formula once per type — the per-type duplication the framework-shape audit's * `COMPUTED_EXPRESSION_UNDEFINED_VARIABLE` rule otherwise mandates. A method * states the inputs and computed once and lists the types it `applies_to`; a * build-time expander (`expandFramework`) derives the fully-expanded * `required_properties`/`computed_properties` from it, so the public surface * (`canonical.ts`, `@unified-product-graph/core`) stays fully expanded and * every consumer is unaffected. * * Scope: the DATA-layer scoring duplication only. `entity_types` (which may * interleave scored types with `role: 'item'` context types) and `slots` * (framework-specific and irregular) stay hand-authored alongside the method. */ interface FrameworkScoringMethod { /** * The scoring inputs, declared once. The expander sets * `required_properties[type] = inputs` for every `applies_to` type, so the * inputs must be valid for each (in practice they are framework-scoped, * `scope: 'framework'`). */ inputs: FrameworkPropertyRequirement[]; /** * Computed scores, declared once WITHOUT `entity_type`. The expander emits * one copy per `applies_to` type with `entity_type` filled in. */ computed?: Omit[]; /** * The entity types this method scores. Each must also appear in * `data.entity_types` with `role: 'scored_item'`. The order here is the order * `required_properties` keys and `computed_properties` entries are emitted in. */ applies_to: string[]; } /** A fixed entity that the framework scaffolds automatically (e.g. quadrant labels, funnel stages) */ interface FrameworkConstant { /** The UPG entity type for this constant */ type: string; /** Display title for the constant */ title: string; /** Predefined properties for the constant entity */ properties: Record; } /** * Everything the framework needs from the graph's data layer: which entity * types play which roles, which properties each type must carry under the * method, and any computed or scaffolded constants. * * @example * // RICE: scores a feature on reach, impact, confidence, effort. * const riceData: FrameworkDataSpec = { * entity_types: [ * { type: 'feature', role: 'item', min_count: 1 }, * ], * required_properties: { * feature: [ * { property: 'reach', type: 'number', required: true, label: 'Reach' }, * { property: 'impact', type: 'number', required: true, label: 'Impact' }, * { property: 'confidence', type: 'number', required: true, label: 'Confidence (%)' }, * { property: 'effort', type: 'number', required: true, label: 'Effort (person-weeks)' }, * ], * }, * computed_properties: [ * { * property: 'rice_score', * entity_type: 'feature', * expression: '(reach * impact * confidence) / effort', * label: 'RICE', * format: 'number', * }, * ], * } */ interface FrameworkDataSpec { /** The entity types that participate in this framework and their roles */ entity_types: FrameworkEntityTypeSpec[]; /** * Properties required on each entity type, keyed by entity type string. * * **Two scopes (UPG-595).** Each requirement carries a `scope`: * - `scope: 'entity'` (default) — an intrinsic property of the entity type. * It MUST exist in `UPG_PROPERTY_SCHEMA[]`; the framework-shape * audit treats an entity-scoped requirement absent from the schema as a * referential-integrity bug. * - `scope: 'framework'` — a **framework-scoped scoring input**. The framework * declares it FOR itself (RICE's `reach`/`impact`/`confidence`/`effort`, * ICE's `impact`/`confidence`/`ease`, WSJF's `cost_of_delay`/`job_size`, * MoSCoW's `moscow`, Kano's `functional_response`/`dysfunctional_response`, * Wardley's `evolution_stage`/`visibility`). It is NOT asserted as an * intrinsic entity property, so the entity schema stays noise-free. The * audit exempts it from the entity-schema check by design. * * Consumers must read framework-scoped inputs from this spec, not from * `UPG_PROPERTY_SCHEMA`. Renderers merge both when displaying an entity under * a framework. A saved score lives on a framework-application edge/instance, * never on the entity (annotation store deferred). See `src/ARCHITECTURE.md`, * "Framework Properties: Method-Scoped Fields". */ required_properties: Record; /** Properties that are derived from other properties via expressions */ computed_properties?: FrameworkComputedProperty[]; /** Fixed entities that the framework creates automatically */ constants?: FrameworkConstant[]; /** * A scoring method applied to several entity types, declared once. When present * on an AUTHORED definition, a build-time expander (`expandFramework`, run at * the `definitions/` aggregation boundary) derives `required_properties` and * `computed_properties` for every `applies_to` type from it — so authored * definitions stay DRY while the expanded public surface carries the full * fields plus this method. Additive and optional: a consumer that ignores it * sees the same fully-expanded `required_properties`/`computed_properties` as * before. See `FrameworkScoringMethod` and `expandFramework`. */ scoring_method?: FrameworkScoringMethod; } /** One level in a tree-structured framework */ interface FrameworkLevel { /** Zero-based depth in the tree (0 = root) */ depth: number; /** Human-readable name for this level (e.g. "Outcome", "Opportunity") */ label: string; /** Which UPG entity types can appear at this level */ entity_types: string[]; /** Explanation of what this level represents */ description: string; /** The edge type connecting entities at this level to their parent */ edge_from_parent: string; } /** A cell in a matrix-structured framework */ interface MatrixSlot { /** Unique identifier for this slot */ id: string; /** Display label for the slot */ label: string; /** The UPG entity type placed in this slot */ entity_type: string; /** Position within the matrix grid */ position: { /** Zero-based row index */ row: number; /** Zero-based column index */ col: number; /** Number of rows this slot spans */ rowSpan?: number; /** Number of columns this slot spans */ colSpan?: number; }; } /** A stage in a funnel-structured framework */ interface FunnelStage { /** Unique identifier for this stage */ id: string; /** Display label for the stage */ label: string; /** Position in the funnel (0 = top / widest) */ order: number; /** The UPG entity type associated with this stage */ entity_type?: string; /** The metric tracked at this stage (e.g. "visitors", "signups") */ metric_name?: string; } /** A logical grouping of entities within a collection-structured framework */ interface NamedGroup { /** Unique identifier for this group */ id: string; /** Display label for the group */ label: string; /** Explanation of what this group contains */ description: string; /** Which UPG entity types belong to this group */ entity_types: string[]; } /** How entities are topologically organised within the framework */ interface FrameworkStructureSpec { /** The visual / topological pattern (tree, table, matrix, etc.) */ pattern: StructurePattern; /** Tree levels (only used when pattern is 'tree') */ levels?: FrameworkLevel[]; /** Edge types used to connect entities in this framework */ edge_types?: string[]; /** Matrix slots (only used when pattern is 'matrix') */ slots?: MatrixSlot[]; /** Funnel stages (only used when pattern is 'funnel') */ stages?: FunnelStage[]; /** Named groups (only used when pattern is 'collection') */ groups?: NamedGroup[]; } /** A column definition for table-layout frameworks */ interface TableColumn { /** The property key to display in this column */ property: string; /** Column header label */ label: string; /** Column width in pixels (optional) */ width?: number; /** Whether the column can be sorted */ sortable?: boolean; /** How to render the cell value */ format?: 'number' | 'bar' | 'badge' | 'score_pill'; } /** * Discriminated union of all supported layout types. * Each layout carries its own configuration fields. * * @example { type: 'tree', direction: 'TB', engine: 'elk' } * @example { type: 'table', columns: [{ property: 'reach', label: 'Reach', format: 'score_pill' }] } * @example { type: 'matrix', rows: 2, cols: 2, template: 'eisenhower' } * @example { type: 'funnel', orientation: 'vertical' } * @example { type: 'kanban', columns: ['backlog', 'in_progress', 'done'] } * @example { type: 'quadrant', x_axis: 'impact', y_axis: 'effort', x_label: 'Impact', y_label: 'Effort' } * @example { type: 'grid', groupBy: 'domain' } * @example { type: 'flow', direction: 'LR' } */ type FrameworkLayout = { type: 'tree'; /** Layout direction */ direction: 'TB' | 'LR'; /** Layout engine */ engine?: 'dagre' | 'elk'; } | { type: 'table'; /** Column definitions */ columns: TableColumn[]; } | { type: 'matrix'; /** Number of rows */ rows: number; /** Number of columns */ cols: number; /** Optional named template */ template?: string; } | { type: 'funnel'; /** Funnel orientation */ orientation: 'vertical' | 'horizontal'; } | { type: 'kanban'; /** Column identifiers */ columns: string[]; } | { type: 'quadrant'; /** X-axis property */ x_axis: string; /** Y-axis property */ y_axis: string; /** X-axis label */ x_label?: string; /** Y-axis label */ y_label?: string; } | { type: 'grid'; /** Property to group entities by */ groupBy: string; } | { type: 'flow'; /** Flow direction */ direction: 'LR' | 'TB'; }; /** How the framework should be rendered in a UI */ interface FrameworkPresentationSpec { /** The layout strategy for rendering */ layout: FrameworkLayout; /** Default sort order for entities */ sort_by?: { property: string; /** Sort direction */ direction: 'asc' | 'desc'; }; /** Which dimension to use for colour coding */ colour_by?: 'type' | 'status' | 'score' | 'group' | 'custom'; /** Which properties to show on entity cards */ card_fields?: string[]; /** Whether tree branches can be collapsed */ collapsible?: boolean; /** Custom colour map: keys are values of the colour_by dimension, values are CSS colours */ colour_map?: Record; } /** * One traversal step along a declared edge path. * * `direction` is EXPLICIT and never inferred. Inferring it from endpoint types * fails on self-edges — the experiment family has one * (`experiment_run_tested_via_experiment_run`), where source and target are the * same type and the endpoint types cannot disambiguate the reading. * * `forward` walks source → target as the catalog declares it; `reverse` walks * target → source. */ interface RelationalEdgeStep { /** Canonical edge type id from the UPG edge catalog */ edge: string; /** Which way this step traverses the edge. Never inferred. */ direction: 'forward' | 'reverse'; } /** * A column rendering a property of the ROW entity itself. * * A field column declares NO entity type, deliberately. Its entity is the spine * by definition, and asking what type it "contains" is a category error. This is * the difference that dissolves the Status trap: under `slots`, a column showing * a property of the row had to name an `entityTypeId`, which made it look like a * second zone holding the same type as the first — something a partition forbids. * A field column has no such artefact to explain away. */ interface RelationalFieldColumn { kind: 'field'; /** Stable machine-readable column id (snake_case), unique within the surface */ id: string; /** Display header */ label: string; /** Property key on the spine entity */ property: string; /** Whether the column can be sorted */ sortable?: boolean; } /** * A column rendering a declared expression over the ROW entity's own properties. * * Every operand must be a property the spine entity actually declares. An * expression reaching a property held by another entity on a path is INVALID — * it would evaluate to nothing for every row forever, which is a column that is * 100% honest gap, i.e. a lie about what the framework measures. */ interface RelationalComputedColumn { kind: 'computed'; /** Stable machine-readable column id (snake_case), unique within the surface */ id: string; /** Display header */ label: string; /** Arithmetic expression over spine properties */ expression: string; /** How to render the computed value */ format?: 'number' | 'percentage' | 'currency' | 'duration'; /** Whether the column can be sorted */ sortable?: boolean; } /** * A column rendering the entities reached from the row along a declared edge path. * * A PROJECTION IS NOT A PARTITION, and the distinction is load-bearing rather * than stylistic. A partition claims exclusive membership and is a function of * the entity alone; a projection reports a relationship and is a function of the * (row, path) pair. The same entity legitimately appears in several columns, and * in the same column of several rows — that is the edge structure being reported * accurately, not the "rendering illusion" that property-partitioned frameworks * suffer from. Nothing lands in a projection cell by index, by order, or by * count: the edge exists in the graph or it does not. * * Consequently, and this is a rule rather than an observation: **sibling columns * are NOT required to be mutually exclusive, and must not be validated as if * they were.** Mutual exclusivity is a predicate-zone rule. Enforcing it on a * join would forbid the join. * * EVERY PROJECTION IS MANY-VALUED. The edge catalog declares no cardinality on * any edge, so a projection resolves to a set of size 0..n and must be rendered * as one. Taking the first member because "a run has one hypothesis" asserts a * cardinality the spec does not declare, and selecting from an unordered set by * position is the offset-windowing defect at a different arity. * * ABSENCE IS NOT A VALUE. An empty projection renders as a typed, labelled gap, * never as a blank cell — three states must stay distinguishable: no edge along * the path, an edge to an entity whose rendered field is empty, and not yet * loaded. A blank cell collapses all three into one shrug. */ interface RelationalProjectionColumn { kind: 'projection'; /** Stable machine-readable column id (snake_case), unique within the surface */ id: string; /** Display header */ label: string; /** * The traversal from the spine to the projected entities. A LIST, not a single * edge. * * Multi-hop is required, not a generality. Reaching a `hypothesis` from an * `experiment_run` is a two-hop reverse traversal, and the spec has twice * REFUSED to mint the shortcut edge that would flatten it, because doing so * would dissolve the plan/run split made in ENTP-322/323. A single-hop-only * declaration would leave authors needing that column with no legal way to * declare it — and the illegal way is the edge the spec already rejected. */ path: RelationalEdgeStep[]; /** Property keys to render for each projected entity */ fields: string[]; /** * Maximum entities to RENDER in a cell. A declared literal; the renderer * contributes no default. Truncates rendering only, never membership, and a * truncated cell must report the full count. */ limit?: number; /** Whether the column can be sorted */ sortable?: boolean; } /** Any column on a relational surface */ type RelationalColumn = RelationalFieldColumn | RelationalComputedColumn | RelationalProjectionColumn; /** * A relational / join surface: rows are the instances of one declared spine * type, columns are fields of the row or projections along declared edge paths. * * Additive and optional. A framework without this block is unaffected; a * consumer that does not understand it ignores it. It exists because the zoned * grid partitions ONE set into cells and cannot express a surface that JOINS * several sets along the relationships between them — the shape of the `table` * pattern, the second-largest structural family in this spec. * * Rules a consumer must honour, stated here because they are contract, not * rendering preference: * * 1. **The spine is the population.** Row count is the number of spine entities * in scope. No column may filter the row set; a column that removes rows is a * partition wearing a projection's clothes. * 2. **Counts are entity-distinct per column** — the number of DISTINCT entities * appearing anywhere in the column, never the sum of cell sizes. Where the two * differ, both may be shown only if labelled ("7 learnings across 9 rows"). * Reporting the sum as a population is this surface's characteristic lie. * 3. **Unreferenced entities must be reported.** An entity of a type appearing in * any declared path — endpoint OR intermediate — that no row reaches is * invisible by construction. It must be surfaced, always, with a count and its * type, even at zero. Otherwise the surface silently under-reports the graph. * 4. **Determinism.** A projection is a function of the graph's edges and the * projected entities' properties, and nothing else. No clock, no random, no * ambient read. Same graph, same table, every machine. */ interface RelationalSurfaceSpec { /** The UPG entity type whose instances are the rows */ spine: string; /** Ordered columns; at least one */ columns: RelationalColumn[]; /** Declared row order. Never inferred; the renderer contributes no default. */ sort: { /** `id` of a column in `columns` */ column: string; /** Sort direction */ direction: 'asc' | 'desc'; }; } /** One step in a guided walkthrough of how to use a framework */ interface FrameworkStep { /** Step order (1-based) */ order: number; /** Human-readable instruction for this step */ instruction: string; /** The property this step asks the user to fill */ property?: string; /** The entity type this step focuses on */ entity_type?: string; } /** Educational context that helps users understand and apply the framework */ interface FrameworkEducation { /** A one-sentence explanation of what the framework does */ purpose: string; /** The core question the framework helps answer */ core_question: string; /** Situations where this framework is a good fit */ when_to_use: string[]; /** Situations where this framework is a poor fit */ when_not_to_use: string[]; /** URL to further reading about the framework */ learn_more_url?: string; /** Step-by-step guided walkthrough */ steps?: FrameworkStep[]; } /** * A UPG Framework is a declarative, config-driven method that structures * UPG graph data into a well-known product management pattern. * * Frameworks are pure data; no code. The rendering engine reads the * framework definition and produces the appropriate UI. * * @example * // Abbreviated RICE definition, showing the layer split (data / structure * // / presentation / education). Real definitions live in * // `src/frameworks/definitions/`. * const riceFramework: UPGFramework = { * id: 'rice-scoring', * name: 'RICE Scoring', * version: '0.1.0', * description: 'Score features on Reach, Impact, Confidence, Effort to prioritise.', * category: 'prioritisation', * origin: { * type: 'practitioner', * attribution: 'Sean McBride (Intercom)', * year: 2016, * license: 'MIT', * }, * tags: ['prioritisation', 'scoring'], * data: { * entity_types: [{ type: 'feature', role: 'item', min_count: 1 }], * required_properties: { * feature: [ * { property: 'reach', type: 'number', required: true }, * { property: 'impact', type: 'number', required: true }, * { property: 'confidence', type: 'number', required: true }, * { property: 'effort', type: 'number', required: true }, * ], * }, * }, * structure: { pattern: 'table' }, * presentation: { * layout: { * type: 'table', * columns: [ * { property: 'title', label: 'Feature' }, * { property: 'rice_score', label: 'RICE', format: 'score_pill' }, * ], * }, * sort_by: { property: 'rice_score', direction: 'desc' }, * }, * education: { * purpose: 'Rank features by a simple weighted score.', * core_question: 'Which feature gives the most value for the least effort?', * when_to_use: ['A large backlog needs triage', 'Stakeholders argue by intuition'], * when_not_to_use: ['Strategic bets where effort is the wrong denominator'], * }, * } */ interface UPGFramework { /** Unique identifier (kebab-case, e.g. "rice-scoring", "lean-canvas") */ id: string; /** Human-readable name (e.g. "RICE Scoring", "Lean Canvas") */ name: string; /** Semver version of this framework definition */ version: string; /** One-sentence description of the framework */ description: string; /** The broad domain this framework belongs to */ category: FrameworkCategory; /** Attribution and licensing */ origin: FrameworkOrigin; /** Freeform tags for filtering and discovery */ tags: string[]; /** Named positions within the framework's visual structure */ slots?: FrameworkSlot[]; /** What data the framework needs from the graph */ data: FrameworkDataSpec; /** How entities are topologically organised */ structure: FrameworkStructureSpec; /** How the framework should be rendered */ presentation: FrameworkPresentationSpec; /** * A relational / join surface, for frameworks whose columns are projections * along edges rather than a partition of one set. * * Additive and optional. When present it is the authoritative column * declaration and `presentation.layout` describes only chrome — the two must * not be read as competing rosters. `slots` should be omitted on a framework * that declares this block: a slot cannot express an edge path, and forcing a * table into slots is what produced the duplicate-`entityTypeId` artefact this * block removes. */ relational?: RelationalSurfaceSpec; /** Educational context and guided walkthrough */ education: FrameworkEducation; /** IDs of other frameworks this one can be composed with */ composable_with?: string[]; /** ID of a parent framework this one extends */ extends?: string; /** * The approaches this framework serves (many-to-many). Each value is a * `UPGApproachId` (`'plan' | 'inspect' | 'prioritise' | 'trace' | 'reflect'`). * Optional: partial coverage is by design; adding tags later is additive. * * Type-erased to `string[]` to avoid a circular import with * `approaches/types.ts`. Conceptually `readonly UPGApproachId[]`. * * @see MENTAL-MODEL.md, approach × framework relationship */ approach_ids?: readonly string[]; } /** * Lightweight framework metadata: the identity + origin subset of UPGFramework. * * Use this type for catalogue listings, site data, and anywhere that needs * framework identity without the full data/structure/presentation/education layers. * A UPGFrameworkMeta can be progressively enriched into a full UPGFramework. */ interface UPGFrameworkMeta { /** Unique identifier (kebab-case, e.g. "rice-scoring", "lean-canvas") */ id: string; /** Human-readable name (e.g. "RICE Scoring", "Lean Canvas") */ name: string; /** One-sentence description of the framework */ description: string; /** The broad domain this framework belongs to */ category: FrameworkCategory; /** The visual / topological shape (tree, table, matrix, funnel, etc.) */ structure_pattern: StructurePattern; /** Attribution and licensing */ origin?: FrameworkOrigin; /** Named positions within the framework's visual structure */ slots?: FrameworkSlot[]; /** The UPG entity types that participate in this framework */ entity_type_ids?: string[]; /** Freeform tags for filtering and discovery */ tags?: string[]; } /** * UPG Approach primitive: the cognitive *path of arrival* to a region. * * Five canonical approaches: Plan, Inspect, Prioritise, Trace, Reflect. * The catalog is closed. New techniques land as frameworks under existing * approaches. * * @see {@link UPG_APPROACHES} for the canonical five * @see {@link UPGFramework.approach_ids} for the bridge to frameworks */ /** * The five canonical approach ids. Closed catalog: adding a sixth is a * coordinated breaking-shape change. * * Source of truth for `UPGFramework.approach_ids` and the MCP tool dispatch * keys (`plan`, `inspect`, `prioritise`, `trace`, `reflect`). */ type UPGApproachId = 'plan' | 'inspect' | 'prioritise' | 'trace' | 'reflect'; /** * A definition record describing a cognitive engagement category exposed as * a verb-led MCP tool (`plan` / `inspect` / `prioritise` / `trace` / `reflect`). * * Today these ship as definition lookups: the MCP handler returns the * approach record + invocation parameters; the LLM is the executor. * Structured execution is forward-declared (see `ApproachRuntime`) and is * a forthcoming follow-up. * * @example * // The Prioritise approach: "what's most important?" * const prioritise: UPGApproach = { * id: 'prioritise', * label: 'Prioritise', * description: 'Rank a candidate set by an explicit framework: RICE, ICE, Kano, Cost of Delay.', * question_answered: "what's most important?", * signature_hint: '({ candidates: entity_ids[], framework_id }) → { ranked, framework_used }', * framework_id_examples: ['rice-scoring', 'ice-scoring', 'kano-model', 'cost-of-delay'], * } */ interface UPGApproach { /** * Unique identifier: bare verb, matches the MCP tool name. One of * `'plan' | 'inspect' | 'prioritise' | 'trace' | 'reflect'`. */ id: UPGApproachId; /** Human-readable label (Title Case): `'Plan'`, `'Inspect'`, etc. */ label: string; /** One-paragraph description of the cognitive engagement category. */ description: string; /** * The single question this approach answers, read as the user's intent in * plain language. Drives natural-language → MCP-tool routing. */ question_answered: string; /** * Compact signature reminder: `(args) → return-shape`. Documents the * structured-execution shape; today the MCP handler returns the approach * record + invocation parameters (definition lookup). */ signature_hint: string; /** * 3-5 canonical framework ids inside this approach (`UPGFramework.id`), * a discoverability surface, not exhaustive coverage. Full reverse-lookup * is via `UPGFramework.approach_ids`. */ framework_id_examples?: readonly string[]; } /** * Canonical reflect modes: the 4 nouns the `reflect` approach accepts as an * optional `mode` parameter. Absence of `mode` is open reflection. * * Locked vocabulary; agent-facing. Users speak natural language; the LLM * translates `"what assumptions are we making?"` → `mode: 'assumptions'`. */ type ReflectMode = 'assumptions' | 'alternatives' | 'blind-spots' | 'load-bearing'; /** Closed list, useful for tool input-schema enums. */ declare const REFLECT_MODES: readonly ReflectMode[]; /** * Shared envelope every approach handler returns. The handler-specific * payload spreads into `...payload`; see each approach's `signature_hint` * for the per-id shape. */ interface UPGApproachEnvelope { /** The approach id this envelope is wrapping. */ approach_id: UPGApproachId; /** * Approach-specific scope: a region id (Plan, Inspect, Reflect), an * anchor entity id (Trace), an entity id array (Prioritise candidates), * or `null` (open invocation). Typed `unknown` because the shape varies * by approach. */ scope: unknown; /** ISO-8601 datetime when the handler produced the envelope. */ generated_at: string; } /** Per-surface experience binding for a `UPGApproach`. Forward-declared. */ interface ApproachBinding { /** The `UPGApproach.id` this binding renders */ approach_id: UPGApproachId; /** Surface this binding targets */ surface: SurfaceId; /** Identifier the runtime maps to a component or handler */ renderer: string; /** Per-step renderer overrides, keyed by `Step.order` */ step_renderers?: Record; /** Surface-specific step kinds the runtime handles */ custom_step_kinds?: readonly string[]; /** Lifecycle hook id: runtime-resolved, fires when an invocation starts */ on_start?: string; /** Lifecycle hook id: runtime-resolved, fires after each step */ on_step_complete?: string; /** Lifecycle hook id: runtime-resolved, fires when an invocation completes */ on_run_complete?: string; } /** Narrowing filter for `listApproaches`. All fields AND together. */ interface ApproachFilter { /** Filter to approaches whose framework_id_examples include this id */ framework_id?: string; /** Filter to approaches relevant to a specific region (forward-compat; all five are cross-region today) */ region?: UPGRegionId; /** Filter to approaches reachable via a specific entry mode (forward-compat) */ entry_mode?: EntryMode; } /** * A concrete invocation of a `UPGApproach`. Forward-declared: the MCP * handlers are stateless definition lookups today; structured execution * with run tracking is a forthcoming follow-up. */ interface ApproachRun { /** Unique identifier for this run */ id: string; /** The `UPGApproach.id` this run is executing */ approach_id: UPGApproachId; /** ISO 8601 datetime */ started_at: string; /** ISO 8601 datetime, set when the run completes */ completed_at?: string; /** Order of the step currently in progress, if any */ current_step_order?: number; /** Runtime context passed when the run was started */ context: RunContext; } /** * Forward-declared interface for a future structured-execution runtime. No * current surface implements it; the MCP tools ship as definition lookups. */ interface ApproachRuntime { /** Return all approaches matching an optional filter */ listApproaches(filter?: ApproachFilter): readonly UPGApproach[]; /** Return a single approach by id, or null if not found */ getApproach(id: UPGApproachId): UPGApproach | null; /** Start a new run of an approach, returning the in-progress `ApproachRun` */ startRun(approach_id: UPGApproachId, context: RunContext): ApproachRun; /** Record the output of a completed step against an in-progress run */ recordStep(run_id: string, step_order: number, output: StepOutput): void; } /** * approaches/definitions/: the five canonical UPGApproach records. * * Each record is a definition lookup: id, label, description (cartographic * framing), question_answered, signature_hint, framework_id_examples. * Structured execution semantics are a forthcoming follow-up; the LLM is the * executor today. * * Order is stable: Plan / Inspect / Prioritise / Trace / Reflect. Matches the * cognitive flow: decide what to build, check what's broken, rank what's * most important, walk a path through what exists, question what you're * assuming. * * See `../types.ts` for the cartographic-framing JSDoc that anchors the * "approach" naming. Read that before touching this file. */ /** * The five canonical approaches. Order is the cognitive flow. * * `as const` keeps the array length and ids in the type system so consumers * that pin against `UPG_APPROACHES.length === 5` get a compile-time guarantee. */ declare const UPG_APPROACHES: readonly [UPGApproach, UPGApproach, UPGApproach, UPGApproach, UPGApproach]; /** O(1) lookup by id. */ declare const UPG_APPROACHES_BY_ID: Record; /** * UPG Region catalog (topology only). The 10 canonical super-domain regions: * entities, edges, anchors, shape archetype, atomic-domain composition. * https://unifiedproductgraph.org/spec | MIT */ declare const UPG_REGIONS: readonly UPGRegion[]; declare const UPG_REGION_MAP: Readonly>; declare function getRegion(id: string): UPGRegion | undefined; declare function getRegionForEntityType(entityType: string): UPGRegion | undefined; declare const UPG_REGION_COUNT: number; /** * Framework validation: validates UPGFramework objects against the spec. */ /** Result of validating a UPGFramework object */ interface FrameworkValidationResult { /** Whether the framework passed all required checks */ valid: boolean; /** Spec violations that must be fixed */ errors: string[]; /** Best-practice notices that should be reviewed */ warnings: string[]; } /** * Validates a UPGFramework object against the spec. * * Checks: * - Required top-level fields (id, name, version, category, data, structure, presentation, education) * - data.entity_types is a non-empty array * - structure.pattern is a valid StructurePattern * - computed_properties expressions are syntactically valid (balanced parens, valid tokens) * - education has required fields (purpose, core_question, when_to_use, when_not_to_use) * - category is a valid FrameworkCategory * * Returns a result with `valid`, `errors`, and `warnings`. * * @example * const result = validateUPGFramework({ * id: 'lean_canvas', * name: 'Lean Canvas', * version: '1.0.0', * description: 'One-page business model canvas by Ash Maurya', * category: 'business_model', * origin: { type: 'published', author: 'Ash Maurya', year: 2010 }, * structure: { pattern: 'canvas' }, * education: { * purpose: 'Validate early-stage business models', * core_question: 'Is this problem worth solving?', * when_to_use: ['pre-launch', 'pivot analysis'], * when_not_to_use: ['mature product optimisation'], * }, * }) * // result.valid === true * // result.errors === [] */ declare function validateUPGFramework(framework: unknown): FrameworkValidationResult; /** * Whether two sibling predicates are PROVABLY disjoint — the test that decides * whether a framework's zones are mutually exclusive. * * Two conjunctions cannot both hold if they share a `(scope, property)` on * which their atoms cannot both hold: one such property is enough, because an * entity would have to satisfy both atoms on it simultaneously. * * Note the direction: this returns `true` only when disjointness is PROVED. * An unprovable pair is rejected by the validator, not accepted. Over-rejection * costs the author a loud error they fix by adding a discriminating atom; * accepted overlap costs a silent misclassification at render time. See * Amendment 1 §B of the zone-predicate-hook decision. * * Exported so the renderer resolves membership with the same rule the validator * enforced, rather than a second implementation that can drift from it. */ declare function predicatesProvablyDisjoint(a: FrameworkSlotPredicate, b: FrameworkSlotPredicate): boolean; /** * Canonical Framework Library: v1 public surface. * * The famous, battle-tested product frameworks that anchor the public * Unified Product Graph framework catalog. Curated for editorial confidence * over breadth: every name here is universally recognised and actively * taught in product education. * * The fuller research catalog (~182 additional definitions) lives in the * `definitions/` directory and is promoted into this canonical set * incrementally as each framework is reviewed and validated. * * THIS FILE IS GENERATED. See scripts/regen-canonical-frameworks.ts. */ declare const UPG_FRAMEWORKS: UPGFramework[]; /** Framework lookup by ID */ declare const UPG_FRAMEWORKS_BY_ID: Record; /** Frameworks grouped by category */ declare const UPG_FRAMEWORKS_BY_CATEGORY: Record; /** * Relational path resolution: the shared type-composition logic for the * `relational` block's edge paths. * * One implementation, two consumers — the framework validator (which reports * malformed paths as authoring errors) and the shape audit (which checks that * every type a path reaches is declared in `data.entity_types`). They must agree * on what a path means, so they resolve it with the same function rather than * two that drift. * * https://unifiedproductgraph.org/spec | MIT */ type EdgeCatalog = Record; /** One resolved hop: where the traversal was, and where the step lands it. */ interface ResolvedStep { /** Entity type the traversal is at BEFORE this step */ from: string; /** Entity type the traversal is at AFTER this step */ to: string; } /** Why a path failed to resolve. `null` `reason` means it resolved cleanly. */ interface PathResolution { /** The type each step lands on, in order */ steps: ResolvedStep[]; /** Entity type the whole path lands on, or `null` if it did not resolve */ endpoint: string | null; /** Human-readable failure, or `null` on success */ reason: string | null; /** Index of the step that failed, or `-1` */ failedAt: number; } /** * Walk a declared path from a starting entity type. * * `direction` is read, never inferred: `forward` goes source → target as the * catalog declares the edge, `reverse` goes target → source. A step whose * starting type does not match the end it claims to enter is a TYPE-COMPOSITION * failure and is reported with both types named, because "invalid path" without * the mismatch is not actionable. */ declare function resolveRelationalPath(spine: string, path: RelationalEdgeStep[], catalog?: EdgeCatalog): PathResolution; /** * Every entity type a framework's relational block touches: the spine, plus each * type any path passes through or lands on. * * INTERMEDIATES COUNT. A type that a path merely passes through is still a type * the framework depends on, and an instance of it that no row reaches is exactly * as invisible as an unreferenced endpoint — an experiment plan with no runs is * the motivating case. Excluding intermediates would let a framework depend on a * type it never declares. * * Unresolvable paths contribute the types they reached before failing; the * validator reports the failure itself. */ declare function relationalCoveredEntityTypes(fw: UPGFramework): Set; /** * Canonical `.upg` serialisation, the `upg fmt` reference implementation. * * The same logical graph always serialises to byte-identical output, regardless * of which tool wrote it. This is the linchpin of UPG-577: every writer (MCP * server, CLI, SDK, cloud export, AI agents via MCP) calls this one serialiser, * so git diffs reflect MEANING, not formatting. * * Anchored on RFC 8785 (JSON Canonicalization Scheme) for the * object-internal rules, with two deliberate deviations for the git-review * lifecycle: (1) pretty-print (2-space, one element per line, LF) rather than * JCS's compact single line; (2) semantic sort of the set-like arrays * (`nodes`, `edges`, `cross_edges`, `tags`) rather than JCS's preserve-as-is. * * https://unifiedproductgraph.org/spec | MIT */ /** * The on-disk canonical serialisation version, written to `$upg.format_version`. * Distinct from `UPG_VERSION` (the catalogue/spec version, written to * `$upg.spec_version`): the *serialisation* can evolve independently of the * *schema*. Bumped to 1.0.0 for the canonical-form + `$upg` header release * (UPG-577), the first format version actually written to disk. */ declare const UPG_CANONICAL_FORMAT_VERSION: "1.0.0"; /** Provenance block inside the `$upg` header. Holds the volatile fields. */ interface UPGHeaderProvenance { /** The tool that produced this document */ tool: string; /** Optional tool version */ tool_version?: string; /** ISO 8601 timestamp of export (volatile, excluded from the integrity body) */ exported_at?: string; /** Optional workspace/project identifier in the source tool */ workspace_id?: string; } /** Integrity block inside the `$upg` header, checksum of the canonical BODY. */ interface UPGHeaderIntegrity { /** Hash algorithm label, e.g. "sha256-128" (SHA-256 truncated to 128 bits) */ algorithm: string; /** Hex checksum of the canonical serialisation of the body (product + nodes + edges) */ body: string; } /** * The reserved `$upg` header object. Consolidates the previously-scattered * metadata (`upg_version`, `source`, `exported_at`, `_integrity`) into one * leading object so a reader gets instant orientation and tools can read * metadata without parsing the whole graph. */ interface UPGHeader { /** Serialisation/format version (this serialiser's contract) */ format_version: string; /** Spec (catalogue) version the graph conforms to */ spec_version: string; /** "portfolio" for portfolio documents; omitted for single-product */ kind?: 'portfolio'; /** * Workspace member kind for single-product graphs (0.10.0, #45): `org_rollup` * (company umbrella), `watched` (monitored intelligence graph), or * `operating_function` (a function a team operates, not a product it ships; * 0.17.0). Omitted for ordinary products (the default). Distinct from `kind`, * which is the portfolio-vs-product document discriminator. */ member_kind?: 'org_rollup' | 'watched' | 'operating_function'; /** Summary mirror of the root product (single-product docs) */ product?: { id: string; title: string; stage?: string; }; /** Summary mirror of the organisation (portfolio docs) */ organization?: { id: string; title: string; }; /** One-line description, mirrored from the product/org for at-a-glance orientation */ summary?: string; /** Element counts, for cheap size reads without walking the graph */ counts: Record; /** Provenance, who/what last wrote this, and when (volatile) */ provenance: UPGHeaderProvenance; /** Tamper-evidence over the canonical body */ integrity: UPGHeaderIntegrity; } /** * Canonical serialisation order for a node's top-level keys. * * Deliberately a SUPERSET of `UPG_BASE_NODE_FIELDS`: it also orders tolerated * non-base keys (`lifecycle_status`, `sort_order`) so that a graph carrying one * still serialises deterministically. `base-node-fields.test.ts` asserts the * subset relation in the other direction, which is the one that can regress: a * base field declared and never added here would silently drop out of canonical * ordering. Never assert equality, which would fail on the two tolerated keys * and push the next author to delete them. * * Exported at 0.33.0 so that assertion can be written; consumers that need to * classify base fields use `UPG_BASE_NODE_FIELDS`, not this. */ declare const NODE_KEY_ORDER: string[]; /** * Canonical key order for a within-graph edge. * * @remarks * EXPORTED AT 0.34.0 so the subset assertion can be written, exactly as * `NODE_KEY_ORDER` was exported at 0.33.0 and for the same reason. K1 of the * 0.33.0 bundle fixed this class for nodes and left both edge twins as * hand-maintained, unexported, unasserted consts — and the very next release * added a field to `UPGEdge`, which is the regression the node-side assertion * exists to prevent. `canonical-format.test.ts` asserts every declared `UPGEdge` * key appears here. A SUBSET, never equality, for the reason the node comment * gives: an equality check would fail on tolerated keys and push the next author * to delete them. */ declare const EDGE_KEY_ORDER: string[]; /** * Canonical key order for a portfolio cross-product edge. The third instance of * the class `NODE_KEY_ORDER` fixed at 0.33.0, exported at 0.34.0 with the same * subset assertion. * * @remarks * NO `provenance` ENTRY, deliberately. 0.34.0 added `provenance?` to `UPGEdge` * and NOT to `UPGCrossEdge`, so an entry here would order a key that cannot be * written. The assertion is the point of touching this const at all: it is the * direction that can regress, and the next field added to `UPGCrossEdge` now * cannot go unordered. * * `properties` WAS MISSING UNTIL 0.34.0, and the new assertion found it on its * first run. It has been declared on `UPGCrossEdge` since 0.10.0 (the parity * assessment a `feature_rivals_competitor_feature` cross-edge carries), and it * fell through to the sorted-tail pass instead: deterministic, so nothing was * corrupt, but positioned alphabetically among unlisted keys rather than in the * slot the shape implies. That is precisely the regression the node twin got a * guard for at 0.33.0, sitting unnoticed in the edge twin for eleven minors. * * Measured before changing it, because a canonical-order edit rewrites bytes: * across all 61 cross-edges in the estate, ZERO carry `properties`. So no file * re-serialises differently and the fix is free today, which it would not have * been after the first parity assessment landed. */ declare const CROSS_EDGE_KEY_ORDER: string[]; /** Element counts for a single-product document, as stamped into `$upg.counts`. */ declare function deriveSingleCounts(doc: UPGDocument): Record; /** * Element counts for a portfolio document, as stamped into `$upg.counts`. * * `products` counts only `product`-kind members (0.10.0, #45): a watched * competitor-intelligence graph, the org_rollup umbrella graph, or an * operating_function graph (0.17.0) is registered for reference but is not a * product under management. Members carry `member_kind` (absent = product, * back-compat); the non-product kinds are surfaced separately so the breakdown * stays legible, and are OMITTED when zero so existing portfolio files without * them stay byte-identical. */ declare function derivePortfolioCounts(doc: UPGPortfolioDocument): Record; /** Dispatching form of the two count derivations above. */ declare function deriveCounts(doc: UPGDocument | UPGPortfolioDocument): Record; /** * Deterministic checksum of the canonical body (volatile fields excluded by * construction, the body has no timestamps). Hex, first 32 chars. */ declare function computeBodyChecksum(doc: UPGDocument | UPGPortfolioDocument): string; declare function isPortfolio(doc: UPGDocument | UPGPortfolioDocument): doc is UPGPortfolioDocument; interface SerializeOptions { /** Override the export timestamp (default: omitted; writers set provenance.exported_at). */ exportedAt?: string; /** Who/what is writing (maps to provenance). Falls back to the doc's `source`. */ source?: { tool: string; tool_version?: string; workspace_id?: string; }; } /** * Serialise a UPG document to its canonical on-disk form (the `$upg` header * envelope). Idempotent and writer-agnostic: the same logical graph always * yields byte-identical output. Always ends with a single trailing newline. */ declare function serializeCanonical(doc: UPGDocument | UPGPortfolioDocument, opts?: SerializeOptions): string; /** * Parse a `.upg` file's text into the in-memory `UPGDocument` / * `UPGPortfolioDocument` (flat) shape, accepting BOTH the canonical `$upg` * envelope and the legacy flat envelope. Drift (A.6) is repaired on the way in, * so a parse → serialise round-trip is clean. This is the one read path. */ declare function parseUpg(text: string): UPGDocument | UPGPortfolioDocument; /** * Normalise an already-parsed object (canonical `$upg` envelope OR legacy flat) * into the flat in-memory document shape, repairing drift. */ declare function normalizeDocument(obj: unknown): UPGDocument | UPGPortfolioDocument; /** * The `upg fmt` operation on raw text: parse (either envelope) → re-serialise * canonical. Idempotent: `formatUpgText(formatUpgText(x)) === formatUpgText(x)`. */ declare function formatUpgText(text: string, opts?: SerializeOptions): string; /** True iff `text` is already in canonical form (used by `upg fmt --check`). */ declare function isCanonical(text: string): boolean; /** One `$upg.counts` field whose declared value disagrees with the body. */ interface UPGHeaderCountsMismatch { /** The counts key, e.g. `nodes`, `edges`, `products`, `cross_edges`. */ field: string; /** What `$upg.counts` claims (0 when the key is absent from the header). */ declared: number; /** What the body actually holds (0 when the key is not derived for this doc). */ actual: number; } /** The `$upg.integrity.body` seal disagreeing with a recomputation over the body. */ interface UPGHeaderIntegrityMismatch { /** The algorithm label the header declares, e.g. `sha256-128`. */ algorithm: string; /** The checksum recorded in `$upg.integrity.body`. */ declared: string; /** The checksum recomputed from the body as it stands. */ computed: string; } /** Verdict of `checkHeaderSeal`: is a `.upg` file's header true to its own body? */ interface UPGHeaderSealReport { /** * False for a legacy flat file with no `$upg` block. Nothing was declared, so * nothing can be stale: both drift arrays are empty and both `*_checked` * flags are false. Never treat a headerless file as drifted. */ header_present: boolean; /** True when `$upg.counts` was compared against the body. */ counts_checked: boolean; /** True when `$upg.integrity.body` was recomputed and compared. */ integrity_checked: boolean; /** * Why a check was skipped despite a header being present — an unrecognised * `format_version` (the body layout the seal was computed over may differ from * this serialiser's), an unrecognised `integrity.algorithm`, or a missing * block. Absent when everything applicable was checked. */ skipped_reason?: string; /** Per-field counts disagreements. Empty when the counts are true. */ counts_drift: UPGHeaderCountsMismatch[]; /** At most one entry: the body seal is either intact or it is not. */ integrity_drift: UPGHeaderIntegrityMismatch[]; } /** * Compare a parsed `.upg` object's `$upg` header against its own body. * * Both checks are gated on `format_version` matching this serialiser's * {@link UPG_CANONICAL_FORMAT_VERSION}: the counts keys and the canonical body * layout are contracts OF a format version, so a file written under a different * one must not be judged by this one's rules. A future format bump therefore * degrades to "not checked" rather than to a wall of false positives. * * Pure and read-only — it neither repairs the document nor touches the file. * `upg fmt` is the repair: it recomputes both fields from the body it writes. * * @param obj An already-parsed `.upg` document (canonical `$upg` envelope or * legacy flat). Pass the RAW parse, not a normalised document — normalisation * is what discards the header this function exists to read. */ declare function checkHeaderSeal(obj: unknown): UPGHeaderSealReport; /** * Text-level form of {@link checkHeaderSeal}: strips a leading UTF-8 BOM, parses, * and checks. The BOM strip mirrors the load path — editors that prepend one * would otherwise make `JSON.parse` throw on a file that is otherwise valid. * * @throws SyntaxError when `text` is not valid JSON. */ declare function checkHeaderSealText(text: string): UPGHeaderSealReport; /** * @unified-product-graph/core: Unified Product Graph Specification * * The open specification and TypeScript SDK for product knowledge graphs. * * https://unifiedproductgraph.org * License: MIT */ /** * The current spec version implemented by this package. * MUST stay in lockstep with the package.json version of the publish train — * it stamps the `upg_version` field of every `.upg` file written by the SDK. * The `check:version-lockstep` gate enforces this at release time. */ declare const UPG_VERSION: "0.41.0"; /** * The `.upg` JSON document format version. Written to the `upg_version` field. * Evolves independently from `UPG_VERSION` (the catalogue version). */ declare const UPG_FORMAT_VERSION: "0.4.0"; /** * The `.upg.md` format version. Reference: `spec/UPG-MARKDOWN-v0.1.md`. * Reference implementation: `@unified-product-graph/markdown`. */ declare const MARKDOWN_FORMAT_VERSION: "0.1"; /** Every active entity type in the spec. Computed from domains, so it never drifts. */ declare const UPG_TYPES: readonly string[]; /** O(1) lookup set for validation and filtering */ declare const UPG_TYPES_SET: ReadonlySet; /** Human-readable display names: snake_case → Title Case. Handles known abbreviations. */ declare const UPG_TYPE_NAMES: Record; /** Every edge type key in the spec. Computed from the edge catalog. */ declare const UPG_EDGE_TYPES: readonly UPGEdgeType[]; /** * Lookup map: `"source_type:target_type"` → ordered list of canonical edge keys. * * Computed from the edge catalog. Multiple edges may share a `(source, target)` * pair, e.g. `learning_updates_hypothesis` (causal) and `learning_refines_hypothesis` * (cross-domain) both connect `learning → hypothesis`. Prior to v0.4.1 this map * was a `Record` populated by `Object.fromEntries`, which * silently dropped every collision but the last (35 pairs). * * The value is now a list, ordered as edges appear in `UPG_EDGE_CATALOG`. Use * `pickCanonicalEdge` for a single deterministic answer and `resolveAllEdges` * for the full candidate set. */ declare const UPG_EDGE_PAIR_MAP: Record; /** * Edge classification used as the canonical-pick hint and policy axis. * Mirrors `UPGEdgeDefinition.classification` in `catalog/edge-catalog.ts`. */ type UPGEdgePickHint = 'hierarchy' | 'causal' | 'semantic' | 'cross-domain'; /** * Return every catalogued edge for the given `(source, target)` pair. * * Order matches `UPG_EDGE_CATALOG` declaration order. Returns `[]` when the * pair is not in the catalogue. * * @example * resolveAllEdges('learning', 'hypothesis') * // → ['learning_updates_hypothesis', 'learning_refines_hypothesis'] */ declare function resolveAllEdges(sourceType: string, targetType: string): UPGEdgeType[]; /** * Pick the canonical `UPGEdgeType` for a `(source, target)` pair under an * explicit policy. * * **Policy:** * 1. If `hint` is provided and an edge with that `classification` exists for * the pair, return it (first declared wins for sub-collisions inside a * single classification, declaration order is the canonical tiebreaker). * 2. Otherwise, return the highest-ranked classification edge available, * using `CLASSIFICATION_RANK` (hierarchy ≻ causal ≻ semantic ≻ cross-domain). * 3. If the pair has no catalogued edges, return `null`. * * **Determinism:** for any `(source, target)` the picked edge is stable across * runs and never changes unless the catalog is edited. This is the v0.4.1 fix: * pair collisions are no longer last-wins. * * @example * pickCanonicalEdge('product', 'decision', 'hierarchy') * // → 'product_decided_via_decision' (the hierarchy-class edge) * * pickCanonicalEdge('learning', 'hypothesis') * // → 'learning_updates_hypothesis' (causal beats cross-domain) */ declare function pickCanonicalEdge(sourceType: string, targetType: string, hint?: UPGEdgePickHint): UPGEdgeType | null; /** * Resolve the canonical `UPGEdgeType` for a containment relationship. * * Import adapters need to emit edges like "epic contains user_story" but * cannot safely construct raw `${parent}_contains_${child}` template strings, * because that union is closed and most pairs are not registered. This function looks * up the canonical edge for the given parent→child pair using * `pickCanonicalEdge` with the `'hierarchy'` hint, falling back through the * standard precedence when no hierarchy-class edge exists for the pair. * * **v0.4.1 contract change:** * - If the pair has any catalogued edge, a deterministic canonical pick is * returned (hierarchy-class preferred; otherwise causal ≻ semantic ≻ * cross-domain). All 35 collision pairs from the v0.4.0 audit now return * non-null, fixing silent last-wins behaviour. * - If the pair has no catalogued edge at all, `null` is returned so the * caller can fall back to `node_informs_node` or skip the edge entirely. * * **Design note:** the `hint` argument is omitted from this function for * back-compat. All in-tree callers (Markdown, Notion, Linear, GitHub * adapters) are containment-only. Callers that need a different classification * should call `pickCanonicalEdge(source, target, hint)` directly. * * @param parentType - Source entity type string (e.g. `'epic'`) * @param childType - Target entity type string (e.g. `'user_story'`) * @returns Canonical `UPGEdgeType` key, or `null` if the pair has no edges. * * @example * resolveContainmentEdge('feature_area', 'feature') // → 'feature_area_contains_feature' * resolveContainmentEdge('release', 'persona') // → null (no edge for pair) * resolveContainmentEdge('product', 'decision') // → 'product_decided_via_decision' (hierarchy) */ declare function resolveContainmentEdge(parentType: string, childType: string): UPGEdgeType | null; /** * Total number of ACTIVE (non-deprecated) entity types. * Filter: types present in `UPG_DOMAINS` via `getTypes()`. Excludes deprecated * aliases. This is the canonical "creatable types" count (`get_spec_version`, * `list_type_labels`). */ declare const UPG_ENTITY_COUNT: number; /** Total number of semantic domains */ declare const UPG_DOMAIN_COUNT: 37; /** Total number of edge types */ declare const UPG_EDGE_COUNT: number; /** * Total number of entity-type entries in the meta registry, INCLUDING * deprecated aliases. Filter: every `UPG_ENTITY_META` row, no exclusions. This * is the `list_entity_types` count and is strictly ≥ `UPG_ENTITY_COUNT`; the * gap is the deprecated-alias set. */ declare const UPG_META_COUNT: number; /** * How much lifecycle vocabulary the spec defines: the number of DISTINCT * (template, phase) pairs across `UPG_LIFECYCLES`. * * @remarks * WHAT IT COUNTS, and the definition IS the decision. A lifecycle is either * generated from a reusable template (`template_id` set) or hand-authored for one * entity type. Two entity types sharing the `OPERATIONAL` template have not * defined its phases twice, so the pair is keyed on the TEMPLATE where there is * one and on the ENTITY TYPE where there is not. * * WHY NOT THE OTHER TWO CANDIDATES, both of which were live when this was wired: * * 869 — every phase row across all 193 lifecycles. The literal size of the * grammar, and it double-counts a template reused by nine entity types. The * number appears in prose as a claim about how much vocabulary the spec HAS, * which is not what 869 measures. * * 334 — carried in planning documents since 0.32.0 and never computed by * anything. It does not reproduce under either real computation. It was * transcribed forward, which is exactly the failure `check:count-drift` exists * to end, and it is recorded here so nobody restores it. * * WIRED INTO `check:count-drift` AT 0.34.0. The condition was declared met in the * 0.33.0 CHANGELOG and deferred on the ground that adding a check would change * the shape of a release ratified as adding none. That ground is gone. Until now * this was the one figure in the truth line that no check asserted, which is why * it is also the one that drifted. * * Derived, never a literal: the count and any docket or CHANGELOG row quoting it * come from this computation, so two numbers cannot be derived twice. */ declare const UPG_PHASE_COUNT: number; export { type A11yAnnotationProperties, type A11yAuditProperties, type A11yGuidelineProperties, type A11yIssueProperties, type A11yStandardProperties, ACTIVE_WHEN_PROPERTY, ANALYTICS_DATA_PLAYBOOK, type AcceptanceCriterionProperties, type AccessPolicyProperties, type AccountProperties, type AcquisitionChannelProperties, type AdCreativeProperties, type AffinityClusterProperties, type AgentDefinitionProperties, type AgentHookProperties, type AgentSessionProperties, type AgentSkillProperties, type AgentTaskProperties, type AggregateProperties, type AiCostTrackerProperties, type AiDatasetProperties, type AiExperimentProperties, type AiGuardrailProperties, type AiModelProperties, type AiTraceProperties, type AlertRuleProperties, type AnnotationProperties, type AntiPatternInputs, type AntiPatternViolation, type AnyUPGEntityType, type ApiContractProperties, type ApiEcosystemProperties, type ApiEndpointProperties, type ApproachBinding, type ApproachFilter, type ApproachRun, type ApproachRuntime, type ApprovalRecordProperties, type AssumptionProperties, type AttributionModelProperties, type AuditLogPolicyProperties, BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK, BUSINESS_GTM_GROWTH_PLAYBOOK, BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK, type BehavioralSegmentProperties, type BenchmarkCheck, type BetaProgramProperties, type BoundedContextProperties, type BrandAssetProperties, type BrandColourProperties, type BrandIdentityProperties, type BrandImageryProperties, type BrandLogoProperties, type BrandTypographyProperties, type BrandVoiceProperties, type BugProperties, type BuildArtifactProperties, type BusinessModelProperties, CLASSIFICATION_EDGE_PROPERTY_SCHEMA, CONFIGURATION_QUALIFIER_EDGE_PROPERTY_SCHEMA, CONFIGURATION_VARIANCE_EDGE_PROPERTY_SCHEMA, CONTENT_DEPTH_WARNING_RULES, COVERAGE_ANTI_PATTERNS, CROSS_EDGE_KEY_ORDER, type Cadence, type CapabilityProperties, type CapacityPlanProperties, type CaptureProperties, type CauseConfidence, type CeremonyProperties, type CertificationProperties, type ChangeRequestProperties, type ChangelogProperties, type ChurnReasonProperties, type CiPipelineProperties, type ClassificationAxisProperties, type ClassificationCapability, type ClassificationCapabilitySurface, type ClassificationCommitment, type ClassificationValueProperties, type CodeRepositoryProperties, type CohortProperties, type ComfortLevel, type CommandProperties, type CommunityInitiativeProperties, type CompetitiveAnalysisProperties, type CompetitiveBattleCardProperties, type CompetitorFeatureProperties, type CompetitorProperties, type CompetitorSignalProperties, type ComplianceFrameworkProperties, type ComplianceRequirementProperties, type CompositionMember, type CompositionProperties, type Confidence, type Configuration, type ConfigurationAxisKind, type ConfigurationAxisProperties, type ConfigurationDriftFinding, type ConfigurationDriftKind, type ConformanceLevel, type ConstraintKind, type ConstraintOrigin, type ConstraintProperties, type ConstraintStatus, type ContactProperties, type ContentCalendarProperties, type ContentPieceProperties, type ContentStrategyProperties, type ContentThemeProperties, type ContractClauseProperties, type ContractProperties, type CostStructureProperties, type CountBenchmark, type Cron, type CrossProductEligibleEdgeType, type CrossProductScope, type CulturalAdaptationProperties, type CustomerFeedbackProperties, type CustomerHealthScoreProperties, type CustomerJourneyStageProperties, type CustomerRelationshipProperties, DEFAULT_ENTITY_EMOJI, DEFERRED_PROPOSED_BY_CONTRACT, DEFER_EDGE_PROPERTY_SCHEMA, DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK, type DashboardProperties, type DataClassificationProperties, type DataContractProperties, type DataDomainProperties, type DataFlowProperties, type DataLineageProperties, type DataModelProperties, type DataPipelineProperties, type DataProductProperties, type DataQualityRuleProperties, type DataSensitivity, type DataSourceProperties, type DatabaseSchemaProperties, type DealProperties, type DecisionProperties, type DeliberateOnlyEdgeType, type DeliverableProperties, type DemandGenProgramProperties, type DepartmentProperties, type DependencyProperties, type DeploymentProperties, type DeprecatedUPGEntityType, type DesignComponentProperties, type DesignConceptProperties, type DesignGuidelineProperties, type DesignPatternProperties, type DesignQuestionProperties, type DesignSprintProperties, type DesignSystemProperties, type DesignTokenProperties, type DesiredOutcomeProperties, type DeveloperPortalProperties, type DifficultyLevel, type DiscountStrategyProperties, type DistributionChannelProperties, type DocumentProperties, type DocumentationTemplateProperties, type DomainActivation, type DomainCountCheck, type DomainEntityProperties, type DomainEventProperties, type DomainGuideStep, type DomainPopulationCheck, type Duration, EDGE_KEY_ORDER, ENGINEERING_PLATFORM_PLAYBOOK, ENTITY_EMOJI, EXPERIENCE_DESIGN_BRAND_PLAYBOOK, type EdgeCountVsPropertyCheck, type EducationProgramProperties, type EmailSequenceProperties, type EmptyCell, type EmptyCellRationaleKind, type EngagementPosture, type EntityCheck, type EntitySequenceStep, type EntityTypeMeta, type EntityTypeResolution, type EntryMode, type EpicProperties, type ErrorBudgetProperties, type EvalBenchmarkProperties, type EvalRunProperties, type EvaluateAntiPatternsOptions, type EventProperties, type EventSchemaProperties, type EvidenceDirection, type EvidenceProperties, type ExperimentPlanProperties, type ExperimentProperties, type ExperimentRunProperties, type ExternalApiProperties, FOUNDATIONS_PLAYBOOK, type FeasibilityStudyProperties, type FeatureAreaProperties, type FeatureFlagProperties, type FeatureProperties, type FeatureRequestProperties, type FeedbackProgramProperties, type FeedbackThemeProperties, type FeedbackVoteProperties, type FixProperties, type ForecastProperties, type FrameworkCategory, type FrameworkComputedProperty, type FrameworkConstant, type FrameworkDataSpec, type FrameworkEducation, type FrameworkEntityTypeSpec, type FrameworkExerciseProperties, type FrameworkInvocationStep, type FrameworkLayout, type FrameworkLevel, type FrameworkOrigin, type FrameworkPresentationSpec, type FrameworkPropertyRequirement, type FrameworkScoringMethod, type FrameworkSlot, type FrameworkSlotPredicate, type FrameworkSlotPredicateAtom, type FrameworkStep, type FrameworkStructureSpec, type FrameworkValidationResult, type FrequencyRating, type FunnelProperties, type FunnelStage, type FunnelStepProperties, type GlossaryTermProperties, type GrowthCampaignProperties, type GrowthLoopProperties, type GtmStrategyProperties, type GuardrailStatus, type HallucinationReportProperties, type HealthStatus, type HelpVideoProperties, type HypothesisEvidenceProperties, type HypothesisProperties, type ISO4217, type ISODate, type ISODateTime, type IdealCustomerProfileProperties, type IncidentProperties, type IncidentSeverity, type InfrastructureComponentProperties, type InitiativeProperties, type InsightProperties, type IntegrationPartnerProperties, type IntegrationPatternProperties, type IntelligenceCondition, type InteractionSpecProperties, type InternalDocProperties, type InterviewGuideProperties, type InvestigationProperties, type InvoiceProperties, type IpAssetProperties, type JobProperties, type JobStepProperties, type JourneyActionProperties, type JourneyPhaseProperties, type JourneyStepProperties, type JourneyType, type KeyActivityProperties, type KeyResourceProperties, type KeyResultProperties, type KnowledgeBaseArticleProperties, LEGACY_PRODUCT_STAGES, type LaunchProperties, type LeadProperties, type LearningPathProperties, type LearningProperties, type LegalEntityProperties, type LibraryDependencyProperties, type LifecyclePhase, type LifecycleRenderShape, type LifecycleRenderState, type LifecycleRenderTransition, type LifecycleState, type LocaleConfigProperties, type LocaleProperties, type LogLevel, type LowMedHigh, MARKDOWN_FORMAT_VERSION, MARKET_COMPETITIVE_PLAYBOOK, type MarketSegmentProperties, type MarketTrendProperties, type MarketingCampaignPlanProperties, type MarketingChannelProperties, type MarketingPlatform, type MarketingStrategyProperties, type MarketplaceListingProperties, type MatrixSlot, type MaturityLevel, type MessagingProperties, type MetricCategory, type MetricDataPoint, type MetricDesignation, type MetricHealth, type MetricImpactLevel, type MetricIndicatorDirection, type MetricProperties, type MetricQualityAssessmentProperties, type MetricScore, type MetricSegmentType, type MetricStatisticalFunction, type MilestoneProperties, type MissionProperties, type ModelComparisonProperties, type ModifiedProperty, type MonitorProperties, NODE_KEY_ORDER, type NamedGroup, type NeedMaturity, type NeedProperties, type NeedValence, type NpsCampaignProperties, OPERATIONS_QUALITY_PLAYBOOK, type ObjectionProperties, type ObjectiveProperties, type ObservationProperties, type OnCallRotationProperties, type OperatingLifecycleProperties, type OperatingStageProperties, type OpportunityProperties, type OrganizationProperties, type OrphanCheck, type OutcomeProperties, PRESENT_UNDER_PROPERTY, PRODUCT_DELIVERY_PLAYBOOK, PROPERTY_MODIFIERS, PROPERTY_MODIFIER_SEMANTICS, PROPERTY_SCALE_MAP, PROPERTY_SCALE_MAP_BY_ENTITY, type ParticipantProperties, type PartnerProgramProperties, type PartnerRevenueShareProperties, type PartnerTierProperties, type PartnershipProperties, type PathResolution, type PaywallProperties, type PenetrationTestProperties, type PersonProperties, type PersonaProperties, type PipelineSalesProperties, type PipelineStageProperties, type PlanningCycleProperties, type PlaybookBinding, type PlaybookFilter, type PlaybookProperties, type PlaybookRun, type PlaybookRuntime, type PortfolioProperties, type PositioningProperties, type PostmortemProperties, type PressReleaseProperties, type PricingStrategyProperties, type PricingTierProperties, type PrimitiveProperties, type Priority, type PrivacyPolicyProperties, type ProductAreaProperties, type ProductProperties, type ProductStageCoercion, type ProgramProperties, type ProjectProperties, type ProjectableEdge, type ProjectableNode, type ProjectionResult, type PromptTemplateProperties, type PromptVersionProperties, type ProofPointProperties, type PropertyDefinition, type PropertyModifier, type PropertySchema, type Protocol, type PrototypeProperties, type ProxyConfidence, type ProxyReason, QUALIFIABLE_EDGE_TYPES, type QaSessionProperties, type QueueTopicProperties, type QuoteDocumentProperties, type QuoteProperties, RECORD_ENTITY_TYPES, REFLECT_MODES, REGISTRY_PRODUCT_ID, type RatioBenchmark, type ReadModelProperties, type RebuttalProperties, type ReflectMode, type RegionalPricingProperties, type RegressionTestProperties, type RelationalColumn, type RelationalComputedColumn, type RelationalEdgeStep, type RelationalFieldColumn, type RelationalProjectionColumn, type RelationalSurfaceSpec, type RelationshipBenchmark, type RelationshipCheck, type ReleaseProperties, type ReleaseStrategyProperties, type ReportProperties, type ResearchPlanProperties, type ResearchQuestionProperties, type ResearchStudyProperties, type ResolvedStep, type ResourceAllocationProperties, type RetrospectiveProperties, type RevenueStreamProperties, type ReviewGateProperties, type RiskProperties, type RiskRegisterProperties, type RoadmapItemProperties, type RoadmapProperties, type RoadmapThemeProperties, type RoleProperties, type RootCauseProperties, type RuleStrength, type RunContext, type RunbookProperties, STRATEGY_OUTCOMES_PLAYBOOK, type SalesMotionProperties, type ScreenProperties, type ScreenStateProperties, type SecurityAuditProperties, type SecurityControlProperties, type SecurityPolicyProperties, type SecurityReviewProperties, type Semver, type SeoKeywordProperties, type SerializeOptions, type ServiceBlueprintProperties, type ServiceLevelAgreementProperties, type ServiceLevelIndicatorProperties, type ServiceLevelObjectiveProperties, type ServiceProperties, type SignalChannel, type SignalSentiment, type SignalUrgency, type SkillProperties, type SocialPostProperties, type SolutionProperties, type SpecificationProperties, type StageRange, type StakeholderProperties, type StatusCategory, type StatusReportProperties, type Step, type StepKind, type StepOutput, type StepOutputKind, type StoryTaskProperties, type StrategicPillarProperties, type StrategicQuestionProperties, type StrategicThemeProperties, type StructurePattern, type SubSequenceStep, type SubscriptionProperties, type SuccessMilestoneProperties, type SupportTicketProperties, type SurfaceArbitrationState, type SurfaceCardinality, type SurfaceCompositionMode, type SurfaceExtensibility, type SurfaceExtensionMechanism, type SurfaceId, type SurfaceInstanceScope, type SurfaceKind, type SurfacePersistence, type SurfaceProperties, type SurveyResponseProperties, type SwitchingCostProperties, type SymptomProperties, THIN_GRAPH_THRESHOLD, type TableColumn, type TargetCustomerSegmentProperties, type TaskProperties, type TeamOkrProperties, type TeamProperties, type TechnicalDebtItemProperties, type TerritoryProperties, type TestCaseProperties, type TestCoverageReportProperties, type TestEnvironmentProperties, type TestPlanProperties, type TestResultProperties, type TestSuiteProperties, type ThreatModelProperties, type ThreatProperties, type Timeframe, type TotalEntityCountCheck, type TouchpointProperties, type TranslationBundleProperties, type TranslationKeyProperties, type TrialConfigProperties, type TutorialProperties, type UPGAnchorEntity, type UPGAntiPattern, type UPGAntiPatternConcern, type UPGAntiPatternSeverity, type UPGApproach, type UPGApproachEnvelope, type UPGApproachId, type UPGAreaTaxonomyEntry, type UPGAssessment, type UPGBaseNode, type UPGBenchmarkSource, type UPGBoundaryDirection, type UPGBoundaryEdge, type UPGCrossEdge, type UPGCrossEdgeType, type UPGCrossOnlyEdgeType, type UPGCuratedAntiPattern, type UPGDocument, type UPGDomain, type UPGDomainBridge, type UPGDomainId, type UPGDomainPattern, type UPGDomainRing, type UPGDomainUsageGuide, type UPGEdge, type UPGEdgeCountSpec, type UPGEdgeDefinition, type UPGEdgeMigration, type UPGEdgeMigrationEndpoints, type UPGEdgePickHint, type UPGEdgeProvenance, type UPGEdgeType, type UPGEntityFilterSpec, type UPGEntityType, type UPGEntityTypeMaturity, type UPGEnumScaleDefinition, type UPGEnumScaleValue, type UPGExternalLink, type UPGFramework, type UPGFrameworkMeta, type UPGHeader, type UPGHeaderCountsMismatch, type UPGHeaderIntegrity, type UPGHeaderIntegrityMismatch, type UPGHeaderProvenance, type UPGHeaderSealReport, type UPGIntegrity, type UPGLens, type UPGLensIntelligencePrompt, type UPGLifecycle, type UPGMappingConfidence, type UPGMemberKindKey, type UPGNode, type UPGOrganization, type UPGPlaybook, type UPGPortfolio, type UPGPortfolioDocument, type UPGPortfolioKind, type UPGPresenceExceptSpec, type UPGProduct, type UPGProductArea, type UPGProductStage, type UPGPropertyMap, type UPGPropertyMigration, type UPGPropertyMigrationChange, type UPGQueryDrivenLayer, type UPGRegion, type UPGRegionEntityMembership, type UPGRegionEntityRole, type UPGRegionId, type UPGRegionShape, type UPGRegistry, type UPGScalarToEdgeMigration, type UPGScaleDefinition, type UPGScalePoint, type UPGSource, type UPGSplitEdge, type UPGSplitMigration, type UPGSplitRoute, type UPGSplitRouteTarget, type UPGSplitTarget, type UPGTimeWindow, type UPGTreeChild, type UPGTreeGapPolicy, type UPGTreePattern, type UPGTreePatternDetail, type UPGTreePatternEdge, type UPGTreePatternSummary, type UPGTypeLabel, type UPGTypeMigration, type UPGValidationError, type UPGValidationProfile, type UPGValidationResult, type UPGValidationWarning, type UPGViewAxis, type UPGViewClause, type UPGViewDimension, type UPGViewEdgeClause, type UPGViewGenericClause, type UPGViewNestEntry, type UPGViewPredicate, type UPGViewPresentation, type UPGViewQuery, type UPGViewTreeRoot, type UPGViewTypeClause, type UPGWildcardEndpoint, UPG_ACTIVE_TYPES, UPG_ANTI_PATTERNS, UPG_ANTI_PATTERN_CONCERNS, UPG_APPROACHES, UPG_APPROACHES_BY_ID, UPG_AREA_TAXONOMY, UPG_AREA_TAXONOMY_BY_COVERAGE_KEY, UPG_BASE_NODE_FIELDS, UPG_BASE_NODE_FIELD_SET, UPG_BASE_NODE_SPECIAL_MERGE_FIELDS, UPG_CANONICAL_FORMAT_VERSION, UPG_CONTAINMENT_FREE_TYPES, UPG_COUNT_BENCHMARKS, UPG_CROSS_EDGE_TYPES, UPG_CROSS_ELIGIBLE_CATALOG_EDGE_TYPES, UPG_CROSS_ONLY_EDGE_TYPES, UPG_DELIBERATE_ONLY_EDGE_TYPES, UPG_DEPRECATED_TYPES, UPG_DOMAINS, UPG_DOMAIN_ACTIVATION, UPG_DOMAIN_COUNT, UPG_DOMAIN_GUIDES, UPG_DOMAIN_RINGS, UPG_EDGE_CATALOG, UPG_EDGE_COUNT, UPG_EDGE_COUNT_SPECS, UPG_EDGE_MIGRATIONS, UPG_EDGE_PAIR_MAP, UPG_EDGE_TYPES, UPG_ENTITY_COUNT, UPG_ENTITY_DESCRIPTIONS, UPG_ENTITY_FILTER_SPECS, UPG_ENTITY_META, UPG_ENTITY_META_BY_ID, UPG_ENTITY_META_BY_NAME, UPG_ENTITY_TO_DOMAIN, UPG_ENUM_SCALES, UPG_FORMAT_VERSION, UPG_FRAMEWORKS, UPG_FRAMEWORKS_BY_CATEGORY, UPG_FRAMEWORKS_BY_ID, UPG_FRAMEWORK_CATEGORIES, UPG_LENSES, UPG_LIFECYCLES, UPG_LIFECYCLE_FREE_TYPES, UPG_LIFECYCLE_PLANNED_TYPES, UPG_META_COUNT, UPG_MIGRATIONS, UPG_PHASE_COUNT, UPG_PLAYBOOKS, UPG_POLYMORPHIC_EDGE_FAMILIES, UPG_POLYMORPHIC_EDGE_KEYS, UPG_PORTFOLIO_KINDS, UPG_PORTFOLIO_SHARED_TYPES, UPG_PRESENCE_EXCEPT_SPECS, UPG_PRODUCT_STAGES, UPG_PRODUCT_STAGE_COERCION_MAP, UPG_PROPERTY_MIGRATIONS, UPG_PROPERTY_SCHEMA, UPG_RATIO_BENCHMARKS, UPG_REGIONS, UPG_REGION_COUNT, UPG_REGION_MAP, UPG_RELATIONSHIP_BENCHMARKS, UPG_SCALAR_TO_EDGE_MIGRATIONS, UPG_SCALES, UPG_SPLIT_MIGRATIONS, UPG_STATUS_MIGRATIONS, UPG_STRUCTURE_PATTERNS, UPG_TREE_PATTERNS, UPG_TREE_PATTERNS_BY_ID, UPG_TYPES, UPG_TYPES_SET, UPG_TYPE_ALIASES, UPG_TYPE_LABELS, UPG_TYPE_LABELS_MAP, UPG_TYPE_NAMES, UPG_VALIDATION_PROFILES, UPG_VALID_CHILDREN, UPG_VERSION, UPG_WILDCARD_ENDPOINT, USERS_NEEDS_PLAYBOOK, type UnitEconomicsProperties, UnknownEntityTypeError, type UserAdvisoryBoardProperties, type UserFlowProperties, type UserJourneyProperties, type UserStoryProperties, VARIES_BY_EDGE, type ValueObjectProperties, type ValuePropositionProperties, type ValueStreamProperties, type VariantProperties, type VisionProperties, type VulnerabilityProperties, WORKSPACE_ARRANGEMENT_EDGE_PROPERTY_SCHEMA, type WalkMigrationChainResult, type WalkthroughProperties, type WebinarProperties, type WireframeProperties, type WorkflowArtifactProperties, type WorkflowRunProperties, type WorkflowTemplateProperties, type WorkspaceCanvas, type WorkspaceProperties, backfillSlug, buildTypeAliases, canBeChildOf, checkConfigurationDrift, checkHeaderSeal, checkHeaderSealText, checkToEdgeCountSpec, classifyEntityFilter, coerceProductStage, collectSlugsForType, collectTargetEntities, compareVersions, computeBodyChecksum, concernEvaluatedFor, concernFor, concernGatesFor, crossProductScope, deriveCounts, derivePortfolioCounts, deriveSingleCounts, describeTreePattern, edgeCarriesProperties, edgeCountSpecKey, entityFilterKey, enumerateProjections, evaluateAntiPatterns, findInvalidStatusMigrationTargets, formatUpgText, friendlyToAssessment, generateSlug, getAnchorEntity, getAntiPatternById, getAntiPatterns, getAntiPatternsBySeverity, getAntiPatternsForStage, getAreaTaxonomyEntry, getBenchmark, getBenchmarksByDomain, getBusinessAreasForRegion, getCanonicalPlaybookForRegion, getCoverageKeysForRegion, getDefaultLens, getDeprecatedTypes, getDomainForType, getDomainIdForType, getDomainsInRing, getEdgePropertySchema, getEntityDescription, getEntityModifierSummary, getEnumScale, getEnumValueMeta, getGuideForDomain, getLens, getLensIds, getLensPlaybook, getLensesForDomain, getLifecycleForType, getLifecycleRenderShape, getMigrationMap, getPlaybookById, getPlaybooksForRegion, getPropertiesForScale, getPropertyDefaultScale, getPropertyMigrations, getPropertyModifier, getPropertySchema, getRegion, getRegionForEntityType, getReplacementType, getRingForDomain, getScalarToEdgeMigrations, getScale, getSplitMigrations, getTreePattern, getTypeId, getTypeName, getTypes, getUPGEdgeMigrations, getValidChildren, getVisibleTypes, hasStatusMigration, isAggregateShapedProperty, isCanonical, isCanonicalProductStage, isContainmentFreeType, isCrossCapable, isCrossProductEligible, isCuratedCrossEligible, isDeliberateOnlyEdge, isDeprecatedType, isDomainGuideStep, isEntitySequenceStep, isFrameworkInvocationStep, isLegacyProductStage, isLifecycleFreeType, isLifecyclePlannedType, isPolymorphicEdge, isPortfolio, isPortfolioSharedType, isRecordEntity, isRegisteredPolymorphicEdge, isRuntimeStateShapedProperty, isSubSequenceStep, isThinCoverageAdvisory, isUPGDocument, listPropertiesByModifier, listStatusMigrations, listTreePatternSummaries, migrateEdge, migrateNode, migrateNodeProperties, migrateProductStage, migrateStatusValue, normalizeDocument, parseUpg, pickCanonicalEdge, predicatesProvablyDisjoint, presenceExceptKey, projectGraph, readActiveWhen, readStringArray, relationalCoveredEntityTypes, resolveAllEdges, resolveContainmentEdge, resolveEntityType, resolveLabel, resolveRelationalPath, resolveSlugCollision, resolveTreePatternEdges, ringOrderedDomainIds, rotateSlug, serializeCanonical, validateEdgeProperties, validateProductStageStrict, validateUPGDocument, validateUPGFramework, validationProfileFor, walkMigrationChainToCanonical };