# Positioning schema — marketing as queryable protocol surface.
#
# Three artefact kinds:
#   - Audience  : a named segment with its primary pain
#   - ValueProp : a claim aimed at one or more audiences, backed by ADRs
#   - Campaign  : a GTM moment bundling value-props for specific channels
#
# Each artefact lives in its own .ncl file under `.ontoref/positioning/`.
# Narrative copy (the prose that sells) is held in markdown files referenced
# via `narrative_path` — NCL holds the verifiable skeleton, markdown holds
# the messaging body. The two split lets the protocol validate claims
# (audience present, evidence_adrs non-empty when Live, launch_criteria pass)
# without trying to be a copywriter.

let status_type = [| 'Draft, 'Validated, 'Live, 'Retired |] in

# Audience validation state — an audience is a Hypothesis until a Proof
# references it (coherence rule 2). Defaults to 'Hypothesis so existing
# audience files remain valid without edits.
let audience_status_type = [| 'Hypothesis, 'Validated |] in

# Launch readiness check — structurally identical to the ConstraintCheck
# type in `.ontoref/adrs/adr-schema.ncl`. Duplicated intentionally as a
# sketch trade-off: a future lift-out into `ontology/schemas/check.ncl`
# (shared by ADR and positioning layers) is tracked in ADR-035's negative
# consequences. Until then, validators in both layers MUST keep these
# variants in sync.
let check_tag_type = [|
  'Grep,
  'NuCmd,
  'ApiCall,
  'FileExists,
|] in

let launch_check_type = {
  tag | check_tag_type,
  ..
} in

# Audience — one file per segment.
let audience_type = {
  id            | String,                       # kebab-case, e.g. "platform-engineer"
  name          | String,                       # human-readable display name
  description   | String,                       # who they are (one paragraph)
  primary_pain  | String,                       # the pain we name and claim to solve
  context       | String        | default = "", # constraints, daily tools, team shape
  channels      | Array String  | default = [], # where they consume content (blog, conf, hn, mastodon)
  evidence      | Array String  | default = [], # observed signals this audience exists & cares
  linked_nodes  | Array String  | default = [], # ontology node ids that this audience engages
  status        | audience_status_type | default = 'Hypothesis, # Validated requires a Proof (rule 2)
} in

# Value-prop localization — the scalar `claim`/`problem_solved` remain the CANONICAL
# (English) source the Rust `ValueProp` struct deserializes; the OPTIONAL `*_i18n`
# siblings carry only TRANSLATIONS. `es` is required (a sibling exists to translate);
# `en` is optional and only overrides the scalar if ever needed. The site generator
# uses the sibling for a lang when that lang key is present, else the scalar — so
# English is never duplicated and the 16 unmigrated vps carry neither field.
let vp_i18n = { es | String, en | String | optional } in

# Value proposition — one file per claim.
let value_prop_type = {
  id              | String,                            # e.g. "vp-001-structure-that-remembers"
  claim           | String,                            # the nuclear one-liner (canonical/English)
  claim_i18n      | vp_i18n             | optional,    # translations of claim; generator prefers this per-lang
  audience_ids    | Array String,                      # references audience_type.id (>=1 required, contract enforced)
  differentiator_ids | Array String     | default = [], # QUÉ×QUIÉN seam: differentiators this claim binds (>=1 when Live)
  problem_solved  | String,                            # the problem this claim addresses (canonical/English)
  problem_solved_i18n | vp_i18n         | optional,    # translations of problem_solved; generator prefers this per-lang
  alternatives    | Array String        | default = [], # competition / status-quo options the user might consider
  unique_angle    | String,                            # why no one else solves it this way
  evidence_adrs   | Array String        | default = [], # ADR ids that materially back this claim (required when status='Live)
  proof_points    | Array String        | default = [], # benchmarks, demos, public metrics
  narrative_path  | String              | optional,    # path to markdown body (relative to project root)
  launch_criteria | Array launch_check_type | default = [],
  status          | status_type,
  retired_at      | String              | optional,    # ISO date (required when status='Retired)
  superseded_by   | String              | optional,    # value-prop id that replaces this one
} in

# Campaign — a GTM moment bundling value-props for specific channels.
let campaign_type = {
  id              | String,                            # e.g. "launch-mcp-surface"
  title           | String,
  audience_ids    | Array String,                      # who this campaign targets
  value_prop_ids  | Array String,                      # value-props in scope
  mechanism_ids   | Array String        | default = [], # difusión mechanisms this campaign coordinates
  goal            | String,                            # what we're trying to achieve
  channels        | Array String        | default = [], # where this campaign runs
  launch_criteria | Array launch_check_type | default = [],
  narrative_path  | String              | optional,
  status          | status_type,
  starts_at       | String              | optional,    # ISO date
  ends_at         | String              | optional,    # ISO date
} in

# ── QUÉ axis ────────────────────────────────────────────────────────────────

# Differentiator — one file per area of differential value. Admissible only
# when anchored: it must name the competitor categories that do NOT do it
# (`vs`) and trace to ontology nodes + Accepted ADRs. Cross-field invariants
# live in `make_differentiator` (defaults).
let differentiator_type = {
  id              | String,                       # e.g. "diff-witness-seam"
  area            | String,                       # short area label
  value_claim     | String,                       # one-line difference in VALUE, not a feature
  vs              | Array String,                 # competitor categories that do not do this
  competitor_ids  | Array String  | default = [], # optional refs to competitor_type.id
  linked_nodes    | Array String  | default = [], # ontology node ids (>=1, contract enforced)
  evidence_adrs   | Array String  | default = [], # ADR ids (>=1 Accepted, contract enforced)
  status          | status_type,
  retired_at      | String        | optional,
} in

# Competitor — one file per competitor or category on the QUÉ axis. Supports
# the differentiators by recording what the competitor does and how ontoref
# differs.
let competitor_type = {
  id              | String,
  name            | String,
  category        | String,                       # "KG consultancy", "developer portal", ...
  what_they_do    | String,
  differs_by      | Array String  | default = [], # how ontoref differs (prose or differentiator ids)
  url             | String        | optional,
  linked_nodes    | Array String  | default = [],
} in

# ── CÓMO axis ───────────────────────────────────────────────────────────────

# DifusionMechanism — one file per vehicle of difusión. Difusión is the
# category; a Campaign is one time-boxed coordination within it. A mechanism is
# persistent (Web, DocsSite, McpServer, Repo) or an event (Content, Talk,
# Event). The service-backed kinds (Newsletter, Forms, Community, Crm) reach an
# audience through a self-hosted execution backend (a libre-wuji component)
# instead of rendering from the ontology — for those, `backend` carries the
# junction to the running service. Admissible only with >=1 channel and >=1
# thing it difunde — enforced by make_difusion_mechanism.
let mechanism_kind_type =
  [| 'Web, 'DocsSite, 'McpServer, 'Repo, 'Content, 'Talk, 'Event,
     'Newsletter, 'Forms, 'Community, 'Crm |] in

# Execution backend for a difusión mechanism — the self-hosted service that
# actually runs the channel. Optional: presentation/content mechanisms (Web,
# DocsSite, Talk) render from the ontology and carry no backend. When present,
# `operational` is the coherence gate: a channel cannot declare status='Live
# while its backend is not configured and serving (enforced by
# make_difusion_mechanism). This is the drift-audit property applied to
# execution — the model refuses to claim a channel is live before it operates.
let mechanism_backend_type = {
  service     | String,                            # libre-wuji component name, e.g. "listmonk"
  endpoint    | String,                            # public URL of the running service
  operational | Bool           | default = false,  # backend configured and serving — gates 'Live
  admin_model | String         | default = "",     # how the admin / first account is obtained
  secret_ref  | String         | optional,         # SOPS basename: infra/libre-wuji/secrets/<ref>.sops.yaml
  guard_ids   | Array String   | default = [],     # cross-cutting guards before public capture (e.g. "cap")
  blockers    | Array String   | default = [],     # what stands between the current state and operational
} in
let difusion_mechanism_type = {
  id                 | String,
  kind               | mechanism_kind_type,
  name               | String,
  description        | String        | default = "",
  persistent         | Bool,                          # persistent surface vs one-off / event
  channels           | Array String  | default = [],  # where it runs (>=1, contract enforced)
  value_prop_ids     | Array String  | default = [],
  differentiator_ids | Array String  | default = [],
  backend            | mechanism_backend_type | optional, # self-hosted executor (service-backed kinds)
  launch_criteria    | Array launch_check_type | default = [],
  status             | status_type,
  retired_at         | String        | optional,
} in

# ── VALIDACIÓN — the seam ────────────────────────────────────────────────────

# Proof — the costura (seam) of the positioning layer. A Proof is the
# convergence of the three axes (a differentiator, an audience, a difusión
# mechanism) signed by a real outcome — "where everything converges really".
# It references the other three; nothing references it. `converges` must carry
# >=1 id on each axis (enforced by make_proof).
let proof_converges_type = {
  differentiator_ids | Array String,
  audience_ids       | Array String,
  mechanism_ids      | Array String,
} in
let proof_kind_type = [| 'Dogfood, 'PersonalInstance, 'ReferenceAdopter, 'Benchmark |] in
let proof_type = {
  id              | String,
  title           | String,
  kind            | proof_kind_type,
  converges       | proof_converges_type,
  outcome         | String,                       # the real, measurable result
  evidence        | Array String  | default = [], # links / paths / metric strings
  evidence_adrs   | Array String  | default = [],
  launch_criteria | Array launch_check_type | default = [],
  status          | status_type,
  retired_at      | String        | optional,
} in

# ── VIABILIDAD — the second seam ──────────────────────────────────────────────

# ViabilityPath — the sustainability costura, parallel to Proof. Where Proof
# answers "is the claim real?" (validation seam over the three axes), a
# ViabilityPath answers "does the claim sustain its maker?" (sustainability seam
# over the same axes). It is NOT a fourth orthogonal axis — compensation is not
# a facet of a claim but a convergence outcome: an audience (PARA QUIÉN) reached
# by a difusión mechanism (CÓMO) monetizing a value-prop (QUÉ×QUIÉN) returns a
# sustaining flow to the maintainer. Self-similar to witness-as-axis-seam and to
# Proof: it references the three axes via `converges`; nothing references it.
#
# `directness` maps to the two poles of the core tension openness-vs-sustainability:
#   'Indirect — the Yin/open pole: visibility, funnel, reputation (the open and
#               personal surface widens the funnel whose paid tail sustains).
#   'Direct   — the Yang/governance pole: metered payment for the witness/audit
#               surface corporate audiences pay for.
#
# A ViabilityPath is 'Draft (a hypothesis about how positioning could sustain the
# project) until a real, measured compensation outcome promotes it — exactly as an
# Audience is 'Hypothesis until a Proof validates it. The gate is one-directional
# (realized status ⇒ realized_outcome present), never a Hard biconditional on the
# Spiral question (ondaod forbidden pattern).
let directness_type = [| 'Direct, 'Indirect |] in
let flow_kind_type = [|
  'HostedService,   # daemon / witness / audit surface run for a fee
  'Support,         # paid support, SLA, priority response
  'Consulting,      # integration / adoption / advisory services
  'DualLicense,     # commercial license of an otherwise-open surface
  'Sponsorship,     # grants, sponsors, patronage of the open protocol
  'Product,         # a consumer artefact (e.g. a native shell) adopted or paid for
  'Funnel,          # indirect: the open/personal surface feeds a paid tail
  'Reputation,      # indirect: talks/demos/content convert to opportunities
|] in
let viability_converges_type = {
  audience_ids   | Array String,                 # PARA QUIÉN the flow draws from (>=1 enforced)
  mechanism_ids  | Array String,                 # CÓMO they are reached — difusión (>=1 enforced)
  value_prop_ids | Array String,                 # QUÉ×QUIÉN claim monetized (>=1 enforced)
} in
let viability_path_type = {
  id               | String,                            # e.g. "via-001-governance-audit-tail"
  title            | String,
  directness       | directness_type,                  # maps to openness-vs-sustainability poles
  flow_kind        | flow_kind_type,
  converges        | viability_converges_type,          # the triple-axis convergence this flow rests on
  hypothesis       | String,                            # the sustaining mechanism stated as a claim
  realized_outcome | String              | optional,    # the REAL measured compensation (required when status='Validated|'Live)
  linked_nodes     | Array String        | default = [], # ontology node ids (>=1 enforced — must engage the viability tension)
  evidence_adrs    | Array String        | default = [], # ADR ids backing the path (required when status='Live)
  launch_criteria  | Array launch_check_type | default = [],
  status           | status_type,                       # Draft until a real outcome → Validated/Live
  retired_at       | String              | optional,
} in

{
  PositioningStatus = status_type,
  AudienceStatus    = audience_status_type,
  MechanismKind     = mechanism_kind_type,
  ProofKind         = proof_kind_type,
  Directness        = directness_type,
  FlowKind          = flow_kind_type,
  LaunchCheckTag    = check_tag_type,
  LaunchCheck       = launch_check_type,
  Audience          = audience_type,
  ValueProp         = value_prop_type,
  Campaign          = campaign_type,
  Differentiator    = differentiator_type,
  Competitor        = competitor_type,
  DifusionMechanism = difusion_mechanism_type,
  MechanismBackend  = mechanism_backend_type,
  Proof             = proof_type,
  ViabilityPath     = viability_path_type,
}
