# Positioning defaults — make_* helpers that apply cross-field contracts # on top of the raw schema in `../schemas/positioning.ncl`. # # Same pattern as adr-defaults.ncl: schema declares record shapes, defaults # applies the cross-field invariants (multi-field reads) on the merged record. let s = import "../schemas/positioning.ncl" in # value-prop: audience_ids must not be empty — a claim without a target is # positioning into the void. let _non_empty_audience_ids = std.contract.custom ( fun label => fun value => if std.array.length value.audience_ids == 0 then 'Error { message = "value-prop '%{value.id}': audience_ids must not be empty — a claim without a target audience is positioning into the void" } else 'Ok value ) in # value-prop: status='Live requires non-empty evidence_adrs — a Live claim # that cites no architectural decision as its backing is marketing without # substance. Drafts may be unbacked (the ADRs may not exist yet). let _live_requires_evidence_adrs = std.contract.custom ( fun label => fun value => if value.status == 'Live && std.array.length value.evidence_adrs == 0 then 'Error { message = "value-prop '%{value.id}': status='Live but evidence_adrs is empty — Live claims must cite at least one ADR as architectural backing" } else 'Ok value ) in # value-prop / campaign: status='Retired requires retired_at (audit trail). let _retired_has_date = std.contract.custom ( fun label => fun value => if value.status == 'Retired && !(std.record.has_field "retired_at" value) then 'Error { message = "'%{value.id}': status='Retired requires retired_at (ISO date) for audit trail" } else 'Ok value ) in # value-prop: if superseded_by is set, status must be 'Retired (forbidden to # claim supersession while remaining Live). let _superseded_implies_retired = std.contract.custom ( fun label => fun value => if std.record.has_field "superseded_by" value && value.status != 'Retired then 'Error { message = "value-prop '%{value.id}': superseded_by is set but status is not 'Retired — supersession implies retirement" } else 'Ok value ) in # launch_check: per-tag well-formedness. Mirrors EachCheckWellFormed in # adr-constraints.ncl. Kept in sync manually until ADR-035's lift-out lands. let _each_launch_check_well_formed = std.contract.custom ( fun label => fun checks => let validate = fun c => let tag = c.tag in let needs = fun field => !(std.record.has_field field c) in if tag == 'Grep then if needs "pattern" || needs "paths" || needs "must_be_empty" then "Grep check requires 'pattern', 'paths', 'must_be_empty'" else "" else if tag == 'NuCmd then if needs "cmd" || needs "expect_exit" then "NuCmd check requires 'cmd' and 'expect_exit'" else "" else if tag == 'ApiCall then if needs "endpoint" || needs "json_path" || needs "expected" then "ApiCall check requires 'endpoint', 'json_path', 'expected'" else "" else if tag == 'FileExists then if needs "path" || needs "present" then "FileExists check requires 'path' and 'present'" else "" else "unknown launch_check tag '%{std.to_str tag}'" in let first_err = std.array.fold_left (fun acc c => if acc != "" then acc else validate c) "" checks in if first_err == "" then 'Ok checks else 'Error { message = first_err } ) in # value-prop: status='Live requires >=1 differentiator — the QUÉ×QUIÉN seam. # A Live claim aimed at an audience with no differential value behind it is # positioning without substance (coherence rule 3, local half). let _live_requires_differentiator = std.contract.custom ( fun label => fun value => if value.status == 'Live && std.array.length value.differentiator_ids == 0 then 'Error { message = "value-prop '%{value.id}': status='Live but differentiator_ids is empty — a Live claim must bind to >=1 differentiator (the QUÉ axis)" } else 'Ok value ) in # differentiator: must be anchored — >=1 linked_node AND >=1 evidence_adr. # An unanchored differentiator is a feature claim, not a value difference. let _diff_requires_anchor = std.contract.custom ( fun label => fun value => if std.array.length value.linked_nodes == 0 || std.array.length value.evidence_adrs == 0 then 'Error { message = "differentiator '%{value.id}': must cite >=1 linked_node and >=1 evidence_adr — an unanchored differentiator is a feature, not a value difference" } else 'Ok value ) in # differentiator: `vs` must name >=1 competitor category it beats. let _diff_requires_vs = std.contract.custom ( fun label => fun value => if std.array.length value.vs == 0 then 'Error { message = "differentiator '%{value.id}': vs must name >=1 competitor category that does NOT do this" } else 'Ok value ) in # difusion mechanism: >=1 channel AND >=1 thing it difunde (value-prop or # differentiator) — difusión with no channel or no payload is empty. let _mechanism_non_empty = std.contract.custom ( fun label => fun value => if std.array.length value.channels == 0 then 'Error { message = "mechanism '%{value.id}': channels must not be empty — difusión needs a channel" } else if std.array.length value.value_prop_ids == 0 && std.array.length value.differentiator_ids == 0 then 'Error { message = "mechanism '%{value.id}': must difundir >=1 value-prop or differentiator" } else 'Ok value ) in # proof: converges must carry >=1 id on each axis — the triple seam. A proof # is the convergence of all three or it is not a proof. let _proof_converges_all = std.contract.custom ( fun label => fun value => let c = value.converges in if std.array.length c.differentiator_ids == 0 || std.array.length c.audience_ids == 0 || std.array.length c.mechanism_ids == 0 then 'Error { message = "proof '%{value.id}': converges must reference >=1 differentiator, >=1 audience and >=1 mechanism — a proof is the triple convergence or it is not one" } else 'Ok value ) in # viability-path: converges must carry >=1 audience, >=1 mechanism and >=1 # value-prop — a sustaining flow that does not draw from an audience reached by a # difusión mechanism monetizing a claim is not a path, it is a wish. let _viability_converges_all = std.contract.custom ( fun label => fun value => let c = value.converges in if std.array.length c.audience_ids == 0 || std.array.length c.mechanism_ids == 0 || std.array.length c.value_prop_ids == 0 then 'Error { message = "viability-path '%{value.id}': converges must reference >=1 audience, >=1 mechanism and >=1 value-prop — a sustaining flow is the convergence of PARA QUIÉN × CÓMO × (QUÉ×QUIÉN) or it is not a path" } else 'Ok value ) in # viability-path: must be anchored to >=1 ontology node — a viability path is # meant to engage the openness-vs-sustainability tension explicitly, not float # free of the substrate. Mirrors _diff_requires_anchor's linked_nodes half. let _viability_anchored = std.contract.custom ( fun label => fun value => if std.array.length value.linked_nodes == 0 then 'Error { message = "viability-path '%{value.id}': must cite >=1 linked_node — a viability path that engages no ontology node (typically openness-vs-sustainability) is reasoned from outside the graph" } else 'Ok value ) in # viability-path: a realized status (Validated|Live) requires realized_outcome — # you cannot claim a flow sustains the project without a real, measured result. # One-directional gate (realized ⇒ outcome present), NOT a Hard biconditional on # the Spiral question: Draft paths are hypotheses and may carry no outcome. let _realized_requires_outcome = std.contract.custom ( fun label => fun value => if (value.status == 'Validated || value.status == 'Live) && !(std.record.has_field "realized_outcome" value) then 'Error { message = "viability-path '%{value.id}': status='%{std.to_str value.status} but realized_outcome is absent — a realized path must carry the real, measured compensation outcome; Draft paths may omit it" } else 'Ok value ) in # viability-path: a Live path requires >=1 evidence_adr — a flow declared live as # architectural commitment must cite the decision that froze it. Drafts may be # unbacked (the ADR may not exist yet). Mirrors _live_requires_evidence_adrs. let _viability_live_requires_adr = std.contract.custom ( fun label => fun value => if value.status == 'Live && std.array.length value.evidence_adrs == 0 then 'Error { message = "viability-path '%{value.id}': status='Live but evidence_adrs is empty — a Live viability path must cite >=1 ADR as architectural backing" } else 'Ok value ) in { make_audience = fun data => s.Audience & data, make_value_prop = fun data => let r1 | _non_empty_audience_ids = s.ValueProp & data in let r2 | _live_requires_evidence_adrs = r1 in let r3 | _live_requires_differentiator = r2 in let r4 | _retired_has_date = r3 in let r5 | _superseded_implies_retired = r4 in let _ | _each_launch_check_well_formed = r5.launch_criteria in r5, make_campaign = fun data => let r1 | _retired_has_date = s.Campaign & data in let _ | _each_launch_check_well_formed = r1.launch_criteria in r1, make_differentiator = fun data => let r1 | _diff_requires_anchor = s.Differentiator & data in let r2 | _diff_requires_vs = r1 in let _ | _retired_has_date = r2 in r2, make_competitor = fun data => s.Competitor & data, make_difusion_mechanism = fun data => let r1 | _mechanism_non_empty = s.DifusionMechanism & data in let r2 | _retired_has_date = r1 in let _ | _each_launch_check_well_formed = r2.launch_criteria in r2, make_proof = fun data => let r1 | _proof_converges_all = s.Proof & data in let r2 | _retired_has_date = r1 in let _ | _each_launch_check_well_formed = r2.launch_criteria in r2, make_viability_path = fun data => let r1 | _viability_converges_all = s.ViabilityPath & data in let r2 | _viability_anchored = r1 in let r3 | _realized_requires_outcome = r2 in let r4 | _viability_live_requires_adr = r3 in let r5 | _retired_has_date = r4 in let _ | _each_launch_check_well_formed = r5.launch_criteria in r5, Audience = s.Audience, ValueProp = s.ValueProp, Campaign = s.Campaign, Differentiator = s.Differentiator, Competitor = s.Competitor, DifusionMechanism = s.DifusionMechanism, Proof = s.Proof, ViabilityPath = s.ViabilityPath, LaunchCheck = s.LaunchCheck, LaunchCheckTag = s.LaunchCheckTag, PositioningStatus = s.PositioningStatus, AudienceStatus = s.AudienceStatus, MechanismKind = s.MechanismKind, ProofKind = s.ProofKind, Directness = s.Directness, FlowKind = s.FlowKind, }