ontoref-code/ontology/defaults/positioning.ncl
2026-07-10 01:44:59 +01:00

293 lines
12 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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,
}