(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"
# A service-backed mechanism cannot be 'Live while its execution backend is not
# operational. The drift-audit property applied to execution: the positioning
# layer refuses to publish a channel as live before the service that runs it is
# actually configured and serving. Mechanisms without a backend (Web, Talk, …)
# are unaffected — the gate only fires when a backend is present.
let _backend_live_requires_operational = std.contract.custom (
fun label =>
fun value =>
if value.status == 'Live
&& std.record.has_field "backend" value
&& !value.backend.operational then
'Error {
message = "mechanism '%{value.id}': status='Live but backend not operational — %{value.backend.service} at %{value.backend.endpoint} must serve before the channel goes Live"
# 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,