ontoref-code/ontology/defaults/positioning.ncl

294 lines
12 KiB
Text
Raw Normal View History

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