@prefix : <https://ontology.inferal.com/modules/advertising-measurement/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix rng: <https://ontology.inferal.com/modules/range/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

:MetricObservationShape
    a sh:NodeShape ;
    rdfs:label "metric observation shape" ;
    rdfs:comment "Requires the qualifiers that determine whether two observations are comparable: subject, grain, metric kind, window, and attribution basis, plus one value." ;
    sh:targetClass :MetricObservation ;
    sh:property [
        sh:path :observationSubject ;
        sh:name "observation subject" ;
        sh:description "Exactly one resource is measured." ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:nodeKind sh:IRI ;
    ] ;
    sh:property [
        sh:path :observationGrain ;
        sh:name "observation grain" ;
        sh:description "Grain is mandatory: summing across grains double-counts, and provider rows do not carry it reliably." ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:class :ObservationGrain ;
    ] ;
    sh:property [
        sh:path :metricKind ;
        sh:name "metric kind" ;
        sh:description "Exactly one phenomenon per observation." ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:class :MetricKind ;
    ] ;
    sh:property [
        sh:path :reportingWindow ;
        sh:name "reporting window" ;
        sh:description "A measured quantity without a window cannot be compared to anything." ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:class :ReportingWindow ;
    ] ;
    sh:property [
        sh:path :observedValue ;
        sh:name "observed value" ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:class :MetricValue ;
    ] ;
    sh:property [
        sh:path :attributionBasis ;
        sh:name "attribution basis" ;
        sh:description "Present exactly when the metric kind is an attributed one, and typed when present. Whether it is required is decided by the conditional below." ;
        sh:maxCount 1 ;
        sh:class :AttributionBasis ;
    ] ;
    # An attribution basis belongs to a figure produced by crediting outcomes
    # under a rule, and to no other. A conversion count without its rule cannot
    # be compared with anything; a spend or impression figure carrying one
    # states a rule that played no part in producing it.
    sh:or (
        [
            sh:property [
                sh:path ( :metricKind rdf:type ) ;
                sh:hasValue :AttributedMetricKind ;
            ] ;
            sh:property [
                sh:path :attributionBasis ;
                sh:name "attribution basis" ;
                sh:description "Required: the figure does not exist until a crediting rule produces it." ;
                sh:minCount 1 ;
            ] ;
        ]
        [
            sh:property [
                sh:path ( :metricKind rdf:type ) ;
                sh:not [ sh:hasValue :AttributedMetricKind ] ;
            ] ;
            sh:property [
                sh:path :attributionBasis ;
                sh:name "attribution basis" ;
                sh:description "Forbidden: the figure is counted directly and means the same under every crediting rule." ;
                sh:maxCount 0 ;
            ] ;
        ]
    ) ;
    sh:property [
        sh:path :observedChannel ;
        sh:name "observed channel" ;
        sh:maxCount 1 ;
        sh:class :Channel ;
    ] ;
    sh:property [
        sh:path :observationBreakdown ;
        sh:name "observation breakdown" ;
        sh:description "Optional and repeatable. Absent means the observation covers the whole population for its subject and window; present means it covers only the named slice." ;
        sh:class :Breakdown ;
    ] .

:BreakdownShape
    a sh:NodeShape ;
    rdfs:label "breakdown shape" ;
    rdfs:comment "A breakdown is a pair and is meaningless as half of one. Requiring both ends is what stops a provider's conflated dimension-and-value string being carried through unparsed." ;
    sh:targetClass :Breakdown ;
    sh:property [
        sh:path :breakdownDimension ;
        sh:name "breakdown dimension" ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:class :BreakdownDimension ;
    ] ;
    sh:property [
        sh:path :breakdownValue ;
        sh:name "breakdown value" ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:nodeKind sh:IRI ;
    ] .

:ReportingWindowShape
    a sh:NodeShape ;
    rdfs:label "reporting window shape" ;
    rdfs:comment "A window must open. It may be unbounded above, which is how lifetime totals are stated." ;
    sh:targetClass :ReportingWindow ;
    sh:property [
        sh:path :windowStart ;
        sh:name "window start" ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:nodeKind sh:IRI ;
    ] ;
    sh:property [
        sh:path :windowEnd ;
        sh:name "window end" ;
        sh:description "Absent for an unbounded, lifetime-to-date window." ;
        sh:maxCount 1 ;
        sh:nodeKind sh:IRI ;
    ] .

:MetricValueShape
    a sh:NodeShape ;
    rdfs:label "metric value shape" ;
    rdfs:comment "Every value carries exactly one canonical decimal magnitude. The decimal datatype is the constraint that stops a provider's wire string or micros integer landing on the shared predicate." ;
    sh:targetClass :MetricValue ;
    sh:property [
        sh:path :canonicalMagnitude ;
        sh:name "canonical magnitude" ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:decimal ;
    ] .

:CountValueShape
    a sh:NodeShape ;
    rdfs:label "count value shape" ;
    rdfs:comment "Counts are non-negative. They may be fractional where a provider apportions an outcome across touchpoints." ;
    sh:targetClass :CountValue ;
    sh:property [
        sh:path :canonicalMagnitude ;
        sh:name "canonical magnitude" ;
        sh:minInclusive 0 ;
        sh:datatype xsd:decimal ;
    ] .

:MonetaryValueShape
    a sh:NodeShape ;
    rdfs:label "monetary value shape" ;
    rdfs:comment "A monetary value must carry its currency. This is the constraint that makes an unsummable cross-currency total detectable rather than silent." ;
    sh:targetClass :MonetaryValue ;
    sh:property [
        sh:path :valueAmount ;
        sh:name "value amount" ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:class :MonetaryAmount ;
    ] .

:RatioValueShape
    a sh:NodeShape ;
    rdfs:label "ratio value shape" ;
    rdfs:comment "A ratio is a dimensionless quotient such as a click-through rate. Forbidding a monetary amount here stops it being typed as currency and summed with actual spend. A rate that is denominated in money is a cost rate rather than a ratio." ;
    sh:targetClass :RatioValue ;
    sh:property [
        sh:path :valueAmount ;
        sh:name "value amount" ;
        sh:description "Forbidden: a ratio is dimensionless. A rate carrying money is a cost rate value." ;
        sh:maxCount 0 ;
    ] .

:CostRateValueShape
    a sh:NodeShape ;
    rdfs:label "cost rate value shape" ;
    rdfs:comment "A cost rate is money per unit, so it carries the currency-bearing amount that a plain ratio must not. Two rates of the same magnitude in different currencies are different quantities, and without the amount they are indistinguishable." ;
    sh:targetClass :CostRateValue ;
    sh:property [
        sh:path :valueAmount ;
        sh:name "value amount" ;
        sh:description "Required and single-valued: the rate is denominated, and the currency lives on the amount." ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:class :MonetaryAmount ;
    ] .

:MonetaryAmountShape
    a sh:NodeShape ;
    rdfs:label "monetary amount shape" ;
    rdfs:comment "Amount and ISO 4217 currency code, both mandatory." ;
    sh:targetClass :MonetaryAmount ;
    sh:property [
        sh:path :amountValue ;
        sh:name "amount value" ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:decimal ;
    ] ;
    sh:property [
        sh:path :currencyCode ;
        sh:name "currency code" ;
        sh:description "Three uppercase letters, per ISO 4217." ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:pattern "^[A-Z]{3}$" ;
    ] .

:AttributionBasisShape
    a sh:NodeShape ;
    rdfs:label "attribution basis shape" ;
    rdfs:comment "A basis states its model. The lookback window is optional because some models are windowless." ;
    sh:targetClass :AttributionBasis ;
    sh:property [
        sh:path :usedAttributionModel ;
        sh:name "used attribution model" ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:class :AttributionModel ;
    ] ;
    sh:property [
        sh:path :creditedInteraction ;
        sh:name "credited interaction" ;
        sh:description "Optional, because some models credit any interaction. When present it is load-bearing: click-attributed and view-attributed figures are not comparable." ;
        sh:maxCount 1 ;
        sh:class :CreditedInteraction ;
    ] ;
    sh:property [
        sh:path :lookbackWindow ;
        sh:name "lookback window" ;
        sh:maxCount 1 ;
        sh:class rng:Range ;
    ] .

:HierarchyMembershipShape
    a sh:NodeShape ;
    rdfs:label "hierarchy membership shape" ;
    rdfs:comment "Containment is only meaningful with both ends and a validity interval. Requiring validity is what stops a membership from silently rewriting the hierarchy that historical observations were measured under." ;
    sh:targetClass :HierarchyMembership ;
    sh:property [
        sh:path :containedResource ;
        sh:name "contained resource" ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:nodeKind sh:IRI ;
    ] ;
    sh:property [
        sh:path :containedIn ;
        sh:name "contained in" ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:nodeKind sh:IRI ;
    ] ;
    sh:property [
        sh:path :validityWindow ;
        sh:name "validity window" ;
        sh:description "Mandatory. Without it, a parent link asserted today silently applies to every past observation." ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:class rng:Range ;
    ] .

# --- Controlled-vocabulary classes -------------------------------------------
#
# These classes have concepts as their instances rather than structured records.
# Their shapes constrain how a concept may be *used* in data: it must be a
# resolvable IRI, never a blank node, so a consumer can dereference it.
#
# Scheme membership and definitions are properties of the vocabulary itself, not
# of any graph that references it. A data graph naming adm:spend must not be
# required to restate the concept, so those constraints live in the module test
# suite where they are checked against the ontology directly.

:CreditedInteractionShape
    a sh:NodeShape ;
    rdfs:label "credited interaction shape" ;
    rdfs:comment "Credited interactions must be resolvable IRIs so a comparability rule branches on identity." ;
    sh:targetClass :CreditedInteraction ;
    sh:nodeKind sh:IRI .

:MetricKindShape
    a sh:NodeShape ;
    rdfs:label "metric kind shape" ;
    rdfs:comment "A metric kind must be a dereferenceable IRI so provider mappings can point at it." ;
    sh:targetClass :MetricKind ;
    sh:nodeKind sh:IRI .

:AttributionModelShape
    a sh:NodeShape ;
    rdfs:label "attribution model shape" ;
    rdfs:comment "Attribution models must be IRIs: a proprietary model has to be identifiable, not an anonymous node." ;
    sh:targetClass :AttributionModel ;
    sh:nodeKind sh:IRI .

:ChannelShape
    a sh:NodeShape ;
    rdfs:label "channel shape" ;
    rdfs:comment "Channels must be IRIs so cross-provider queries resolve against one vocabulary." ;
    sh:targetClass :Channel ;
    sh:nodeKind sh:IRI .

:BreakdownDimensionShape
    a sh:NodeShape ;
    rdfs:label "breakdown dimension shape" ;
    rdfs:comment "Dimensions must be IRIs, otherwise a provider's ad hoc axis is indistinguishable from a governed one." ;
    sh:targetClass :BreakdownDimension ;
    sh:nodeKind sh:IRI .

:PublisherPlatformShape
    a sh:NodeShape ;
    rdfs:label "publisher platform shape" ;
    rdfs:comment "Publisher platforms must be resolvable IRIs. The unknown platform is a real reported value rather than an absence, so it is a concept like any other and must not be dropped." ;
    sh:targetClass :PublisherPlatform ;
    sh:nodeKind sh:IRI .

:PlacementSurfaceShape
    a sh:NodeShape ;
    rdfs:label "placement shape" ;
    rdfs:comment "Placements are open-ended and provider profiles extend the scheme, so being a resolvable IRI is the one requirement that keeps the vocabulary navigable." ;
    sh:targetClass :PlacementSurface ;
    sh:nodeKind sh:IRI .

:ObservationGrainShape
    a sh:NodeShape ;
    rdfs:label "observation grain shape" ;
    rdfs:comment "Grains are a closed set of hierarchy tiers defined by this module. Restricting data to the four named individuals stops a provider inventing a fifth tier that no comparability rule accounts for." ;
    sh:targetClass :ObservationGrain ;
    sh:in ( :AccountGrain :CampaignGrain :AdGroupGrain :AdGrain ) .

:AttributedMetricKindShape
    a sh:NodeShape ;
    rdfs:label "attributed metric kind shape" ;
    rdfs:comment "An attributed metric kind is a metric kind, so it carries the same scheme attachment. It is separated only so an observation shape can require an attribution basis for it and forbid one elsewhere." ;
    sh:targetClass :AttributedMetricKind ;
    sh:nodeKind sh:IRI .

:WireEncodingShape
    a sh:NodeShape ;
    rdfs:label "wire encoding shape" ;
    rdfs:comment "An encoding is one of the module's own named individuals. An unrecognised encoding cannot be lowered, so it is rejected rather than carried." ;
    sh:targetClass :WireEncoding ;
    sh:property [
        sh:path rdfs:label ;
        sh:name "label" ;
        sh:minCount 1 ;
        sh:datatype xsd:string ;
    ] ;
    sh:property [
        sh:path rdfs:comment ;
        sh:name "comment" ;
        sh:minCount 1 ;
        sh:datatype xsd:string ;
    ] ;
    sh:property [
        sh:path :lowersByCurrencyExponent ;
        sh:name "lowers by currency exponent" ;
        sh:maxCount 1 ;
        sh:datatype xsd:boolean ;
    ] ;
    sh:property [
        sh:path :lowersTo ;
        sh:name "lowers to" ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:nodeKind sh:IRI ;
    ] ;
    sh:or (
        [ sh:property [ sh:path :lowersBy ; sh:minCount 1 ; sh:datatype xsd:decimal ] ]
        [ sh:property [ sh:path :lowersByCurrencyExponent ; sh:hasValue true ] ]
    ) .

:SpineComparabilityShape
    a sh:NodeShape ;
    rdfs:label "spine comparability shape" ;
    rdfs:comment "A comparability resource names one shared term and one datatype. A resource naming neither states nothing a reader can apply." ;
    sh:targetClass :SpineComparability ;
    sh:property [
        sh:path :spineTerm ;
        sh:name "spine term" ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:nodeKind sh:IRI ;
    ] ;
    sh:property [
        sh:path :comparableAs ;
        sh:name "comparable as" ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:nodeKind sh:IRI ;
    ] ;
    sh:property [
        sh:path rdfs:label ;
        sh:name "label" ;
        sh:minCount 1 ;
        sh:datatype xsd:string ;
    ] .
