okf-core 0.3.0.0 → 0.4.0.0
raw patch · 28 files changed
+1233/−125 lines, 28 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Okf.Profile: OptionalFieldWithCondition :: Maybe Text -> FieldPath -> ProfileDefinitionError
+ Okf.Profile: [optional] :: NestedRules -> ![NestedFieldRule]
+ Okf.Profile: instance Dhall.Marshal.Decode.FromDhall Okf.Profile.ReferenceProfileFieldRule
+ Okf.Profile: instance Dhall.Marshal.Decode.FromDhall Okf.Profile.ReferenceProfileFrontmatterRules
+ Okf.Profile: instance Dhall.Marshal.Decode.FromDhall Okf.Profile.ReferenceProfileNestedFieldRule
+ Okf.Profile: instance Dhall.Marshal.Decode.FromDhall Okf.Profile.ReferenceProfileNestedRules
+ Okf.Profile: instance Dhall.Marshal.Decode.FromDhall Okf.Profile.ReferenceProfileSpec
+ Okf.Profile: instance Dhall.Marshal.Decode.FromDhall Okf.Profile.ReferenceProfileTypeRule
+ Okf.Profile: instance GHC.Classes.Eq Okf.Profile.ReferenceProfileFieldRule
+ Okf.Profile: instance GHC.Classes.Eq Okf.Profile.ReferenceProfileFrontmatterRules
+ Okf.Profile: instance GHC.Classes.Eq Okf.Profile.ReferenceProfileNestedFieldRule
+ Okf.Profile: instance GHC.Classes.Eq Okf.Profile.ReferenceProfileNestedRules
+ Okf.Profile: instance GHC.Classes.Eq Okf.Profile.ReferenceProfileSpec
+ Okf.Profile: instance GHC.Classes.Eq Okf.Profile.ReferenceProfileTypeRule
+ Okf.Profile: instance GHC.Internal.Generics.Generic Okf.Profile.ReferenceProfileFieldRule
+ Okf.Profile: instance GHC.Internal.Generics.Generic Okf.Profile.ReferenceProfileFrontmatterRules
+ Okf.Profile: instance GHC.Internal.Generics.Generic Okf.Profile.ReferenceProfileNestedFieldRule
+ Okf.Profile: instance GHC.Internal.Generics.Generic Okf.Profile.ReferenceProfileNestedRules
+ Okf.Profile: instance GHC.Internal.Generics.Generic Okf.Profile.ReferenceProfileSpec
+ Okf.Profile: instance GHC.Internal.Generics.Generic Okf.Profile.ReferenceProfileTypeRule
+ Okf.Profile: instance GHC.Internal.Show.Show Okf.Profile.ReferenceProfileFieldRule
+ Okf.Profile: instance GHC.Internal.Show.Show Okf.Profile.ReferenceProfileFrontmatterRules
+ Okf.Profile: instance GHC.Internal.Show.Show Okf.Profile.ReferenceProfileNestedFieldRule
+ Okf.Profile: instance GHC.Internal.Show.Show Okf.Profile.ReferenceProfileNestedRules
+ Okf.Profile: instance GHC.Internal.Show.Show Okf.Profile.ReferenceProfileSpec
+ Okf.Profile: instance GHC.Internal.Show.Show Okf.Profile.ReferenceProfileTypeRule
- Okf.Profile: FrontmatterRules :: ![FieldRule] -> ![FieldRule] -> FrontmatterRules
+ Okf.Profile: FrontmatterRules :: ![FieldRule] -> ![FieldRule] -> ![FieldRule] -> FrontmatterRules
- Okf.Profile: NestedRules :: ![NestedFieldRule] -> ![NestedFieldRule] -> NestedRules
+ Okf.Profile: NestedRules :: ![NestedFieldRule] -> ![NestedFieldRule] -> ![NestedFieldRule] -> NestedRules
Files
- CHANGELOG.md +30/−0
- dhall/FrontmatterRules.dhall +13/−1
- dhall/NestedRules.dhall +8/−1
- dhall/defaults/FrontmatterRules.dhall +4/−1
- dhall/defaults/NestedRules.dhall +3/−1
- okf-core.cabal +1/−1
- src/Okf/Profile.hs +299/−81
- test/Main.hs +403/−36
- test/fixtures/profile-optional-fields/decisions/accepted.md +13/−0
- test/fixtures/profile-optional-fields/decisions/bad-supersedes.md +20/−0
- test/fixtures/profile-optional-fields/decisions/superseded.md +14/−0
- test/fixtures/profile-optional-fields/decisions/superseding.md +21/−0
- test/fixtures/profile-optional-fields/log.md +4/−0
- test/fixtures/profiles/cardinality.dhall +2/−0
- test/fixtures/profiles/closed-fields.dhall +3/−0
- test/fixtures/profiles/conditional-fields-invalid.dhall +1/−0
- test/fixtures/profiles/conditional-fields.dhall +5/−0
- test/fixtures/profiles/decisions.dhall +5/−0
- test/fixtures/profiles/document-references-ep3.dhall +149/−0
- test/fixtures/profiles/document-references-invalid.dhall +2/−0
- test/fixtures/profiles/document-references.dhall +1/−0
- test/fixtures/profiles/formats.dhall +2/−0
- test/fixtures/profiles/nested-reviews.dhall +2/−0
- test/fixtures/profiles/optional-collision-invalid.dhall +33/−0
- test/fixtures/profiles/optional-conditional-invalid.dhall +68/−0
- test/fixtures/profiles/optional-fields.dhall +109/−0
- test/fixtures/profiles/postgresql.dhall +16/−3
- test/fixtures/profiles/type-frontmatter.dhall +2/−0
CHANGELOG.md view
@@ -7,6 +7,36 @@ ## [Unreleased] +## [0.4.0.0] - 2026-07-30++### Added++- An `optional` presence list on `FrontmatterRules` and `NestedRules`, defaulted+ to empty. Optional rules compile to an `EffectiveFieldRule` with no presence+ clauses, so `applicablePresenceClause` can never report their absence in either+ `ValidationProfile`, while every vocabulary, cardinality, format, reference,+ and nested-shape check still runs on a present value. Optional keys are part of+ the compiled rule map, so they count as declared for `allowUnknownFields =+ False` and carry their prose into `profileFieldDescriptionForType`.+ `profileFieldDescription` searches the third list after the first two.+- `ProfileDefinitionError` gains `OptionalFieldWithCondition`, raised when an+ optional rule carries `when` at either top level or nested level.+ `ConflictingFieldRequirement` now also covers a key declared in `optional` plus+ either other list at the same scope. An optional declaration at one scope does+ not cancel a presence clause declared at the other.+- The complete reference-aware descriptor generation is frozen as a private+ compatibility decoder and upgrades with `optional = []` at both levels, so+ descriptors written against the previous published schema keep loading.++### Changed++- **Breaking for exhaustive consumers.** Matches on `ProfileDefinitionError`+ must handle `OptionalFieldWithCondition`, and code constructing+ `FrontmatterRules` or `NestedRules` must supply the `optional` field. A+ descriptor that annotates itself against okf's current `Profile.dhall` by+ relative path must add `optional`; one pinned to an older schema URL, or with+ no annotation at all, is unaffected.+ ## [0.3.0.0] - 2026-07-29 ### Added
dhall/FrontmatterRules.dhall view
@@ -5,6 +5,18 @@ -- Each entry is a `FieldRule`: the frontmatter key, plus an optional description -- explaining what the key is for. The description is documentary only — it is -- never checked against a bundle and can never produce a profile violation.+--+-- The three lists are the profile's presence classifications. A `required` key+-- must be present on every applicable concept. A `recommended` key must be+-- present only under `--strict`. An `optional` key is never reported when+-- absent, in any validation mode, while every constraint it declares still+-- applies whenever it is present — for lifecycle or provenance metadata whose+-- absence is ordinary rather than deficient. A key may appear in at most one of+-- the three lists at one scope, and an `optional` rule may not carry a `when`+-- condition, because a condition gates only presence. let FieldRule = ./FieldRule.dhall -in { required : List FieldRule, recommended : List FieldRule }+in { required : List FieldRule+ , recommended : List FieldRule+ , optional : List FieldRule+ }
dhall/NestedRules.dhall view
@@ -1,6 +1,13 @@---| Required and recommended fields inside each record of a list-valued field.+--| Required, recommended, and optional fields inside each record of a+-- list-valued field.+--+-- The three lists mean the same thing they mean at the top level: `required` is+-- always checked, `recommended` only under `--strict`, and an `optional` member+-- is never reported when absent while its declared constraints still apply+-- whenever it is present. let NestedFieldRule = ./NestedFieldRule.dhall in { required : List NestedFieldRule , recommended : List NestedFieldRule+ , optional : List NestedFieldRule }
dhall/defaults/FrontmatterRules.dhall view
@@ -5,5 +5,8 @@ in { Type = FrontmatterRulesType , default =- { required = [] : List FieldRule, recommended = [] : List FieldRule }+ { required = [] : List FieldRule+ , recommended = [] : List FieldRule+ , optional = [] : List FieldRule+ } }
dhall/defaults/NestedRules.dhall view
@@ -1,4 +1,5 @@---| Record-completion defaults for nested required and recommended fields.+--| Record-completion defaults for nested required, recommended, and optional+-- fields. let NestedRulesType = ../NestedRules.dhall let NestedFieldRule = ../NestedFieldRule.dhall@@ -7,5 +8,6 @@ , default = { required = [] : List NestedFieldRule , recommended = [] : List NestedFieldRule+ , optional = [] : List NestedFieldRule } }
okf-core.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.4 name: okf-core-version: 0.3.0.0+version: 0.4.0.0 synopsis: Read, validate, index, and traverse Open Knowledge Format bundles
src/Okf/Profile.hs view
@@ -99,10 +99,14 @@ deriving stock (Generic, Eq, Show) deriving anyclass (FromDhall) --- | Frontmatter keys the profile expects on every concept.+-- | Frontmatter keys the profile expects on every concept. @required@ is always+-- checked, @recommended@ only under 'StrictAuthoring', and @optional@ never:+-- an optional key is fully validated whenever it is present and its absence is+-- never a deviation in any mode. data FrontmatterRules = FrontmatterRules { required :: ![FieldRule],- recommended :: ![FieldRule]+ recommended :: ![FieldRule],+ optional :: ![FieldRule] } deriving stock (Generic, Eq, Show) deriving anyclass (FromDhall)@@ -147,7 +151,8 @@ -- descriptor depth-bounded rather than recursive. data NestedRules = NestedRules { required :: ![NestedFieldRule],- recommended :: ![NestedFieldRule]+ recommended :: ![NestedFieldRule],+ optional :: ![NestedFieldRule] } deriving stock (Generic, Eq, Show) deriving anyclass (FromDhall)@@ -220,10 +225,11 @@ ] instance ToJSON FrontmatterRules where- toJSON FrontmatterRules {required, recommended} =+ toJSON FrontmatterRules {required, recommended, optional} = object [ "required" .= required,- "recommended" .= recommended+ "recommended" .= recommended,+ "optional" .= optional ] instance ToJSON FieldCondition where@@ -255,10 +261,11 @@ ] instance ToJSON NestedRules where- toJSON NestedRules {required, recommended} =+ toJSON NestedRules {required, recommended, optional} = object [ "required" .= required,- "recommended" .= recommended+ "recommended" .= recommended,+ "optional" .= optional ] instance ToJSON NestedFieldRule where@@ -357,9 +364,85 @@ stripTrailingUnderscore fieldName = fromMaybe fieldName (Text.stripSuffix "_" fieldName) +-- | The complete reference-aware descriptor generation, frozen before the+-- @optional@ presence list was added to 'FrontmatterRules' and 'NestedRules'.+-- This is the immediately preceding public descriptor generation: it matches+-- today's shape exactly apart from that third list, so every descriptor written+-- as a record literal against the published schema keeps loading. Exercised by+-- @okf-core\/test\/fixtures\/profiles\/document-references-ep3.dhall@.+data ReferenceProfileFieldRule = ReferenceProfileFieldRule+ { field :: !Text,+ description :: !(Maybe Text),+ allowedValues :: ![Text],+ cardinality :: !Cardinality,+ format :: !(Maybe FieldFormat),+ elementFields :: !(Maybe ReferenceProfileNestedRules),+ reference :: !(Maybe HandleReferenceRule),+ when :: !(Maybe FieldCondition)+ }+ deriving stock (Generic, Eq, Show)+ deriving anyclass (FromDhall)++data ReferenceProfileNestedRules = ReferenceProfileNestedRules+ { required :: ![ReferenceProfileNestedFieldRule],+ recommended :: ![ReferenceProfileNestedFieldRule]+ }+ deriving stock (Generic, Eq, Show)+ deriving anyclass (FromDhall)++data ReferenceProfileNestedFieldRule = ReferenceProfileNestedFieldRule+ { field :: !Text,+ description :: !(Maybe Text),+ allowedValues :: ![Text],+ cardinality :: !Cardinality,+ format :: !(Maybe FieldFormat),+ when :: !(Maybe FieldCondition)+ }+ deriving stock (Generic, Eq, Show)+ deriving anyclass (FromDhall)++data ReferenceProfileFrontmatterRules = ReferenceProfileFrontmatterRules+ { required :: ![ReferenceProfileFieldRule],+ recommended :: ![ReferenceProfileFieldRule]+ }+ deriving stock (Generic, Eq, Show)+ deriving anyclass (FromDhall)++data ReferenceProfileSpec = ReferenceProfileSpec+ { name :: !Text,+ description :: !(Maybe Text),+ okfVersion :: !Text,+ frontmatter :: !ReferenceProfileFrontmatterRules,+ allowUnknownTypes :: !Bool,+ allowUnknownFields :: !Bool,+ idField :: !(Maybe Text),+ types :: ![ReferenceProfileTypeRule]+ }+ deriving stock (Generic, Eq, Show)+ deriving anyclass (FromDhall)++data ReferenceProfileTypeRule = ReferenceProfileTypeRule+ { type_ :: !Text,+ description :: !(Maybe Text),+ frontmatter :: !ReferenceProfileFrontmatterRules,+ pathPattern :: !(Maybe Text),+ resourceScheme :: !(Maybe Text),+ requireSchemaSection :: !Bool,+ schemaColumns :: ![Text],+ idPrefix :: !(Maybe Text)+ }+ deriving stock (Generic, Eq, Show)++instance FromDhall ReferenceProfileTypeRule where+ autoWith _normalizer =+ genericAutoWith+ (Dhall.defaultInterpretOptions {Dhall.fieldModifier = stripTrailingUnderscore})+ where+ stripTrailingUnderscore fieldName =+ fromMaybe fieldName (Text.stripSuffix "_" fieldName)+ -- | The complete condition-aware descriptor generation, frozen before--- top-level document-reference policies were added. This is the immediately--- preceding public descriptor generation.+-- top-level document-reference policies were added. data ConditionalProfileFieldRule = ConditionalProfileFieldRule { field :: !Text, description :: !(Maybe Text),@@ -739,13 +822,49 @@ fromMaybe fieldName (Text.stripSuffix "_" fieldName) emptyFrontmatterRules :: FrontmatterRules-emptyFrontmatterRules = FrontmatterRules {required = [], recommended = []}+emptyFrontmatterRules = FrontmatterRules {required = [], recommended = [], optional = []} +upgradeReferenceProfileFrontmatter :: ReferenceProfileFrontmatterRules -> FrontmatterRules+upgradeReferenceProfileFrontmatter previous =+ FrontmatterRules+ { required = map upgradeField (previous ^. #required),+ recommended = map upgradeField (previous ^. #recommended),+ optional = []+ }+ where+ upgradeField rule =+ FieldRule+ { field = rule ^. #field,+ description = rule ^. #description,+ allowedValues = rule ^. #allowedValues,+ cardinality = rule ^. #cardinality,+ format = rule ^. #format,+ elementFields = upgradeNestedRules <$> rule ^. #elementFields,+ reference = rule ^. #reference,+ when = rule ^. #when+ }+ upgradeNestedRules rules =+ NestedRules+ { required = map upgradeNestedField (rules ^. #required),+ recommended = map upgradeNestedField (rules ^. #recommended),+ optional = []+ }+ upgradeNestedField rule =+ NestedFieldRule+ { field = rule ^. #field,+ description = rule ^. #description,+ allowedValues = rule ^. #allowedValues,+ cardinality = rule ^. #cardinality,+ format = rule ^. #format,+ when = rule ^. #when+ }+ upgradePreviousFrontmatter :: PreviousFrontmatterRules -> FrontmatterRules upgradePreviousFrontmatter previous = FrontmatterRules { required = map upgradeField (previous ^. #required),- recommended = map upgradeField (previous ^. #recommended)+ recommended = map upgradeField (previous ^. #recommended),+ optional = [] } where upgradeField rule =@@ -764,7 +883,8 @@ upgradeConditionalProfileFrontmatter previous = FrontmatterRules { required = map upgradeField (previous ^. #required),- recommended = map upgradeField (previous ^. #recommended)+ recommended = map upgradeField (previous ^. #recommended),+ optional = [] } where upgradeField rule =@@ -781,7 +901,8 @@ upgradeNestedRules rules = NestedRules { required = map upgradeNestedField (rules ^. #required),- recommended = map upgradeNestedField (rules ^. #recommended)+ recommended = map upgradeNestedField (rules ^. #recommended),+ optional = [] } upgradeNestedField rule = NestedFieldRule@@ -797,7 +918,8 @@ upgradeNestedProfileFrontmatter previous = FrontmatterRules { required = map upgradeField (previous ^. #required),- recommended = map upgradeField (previous ^. #recommended)+ recommended = map upgradeField (previous ^. #recommended),+ optional = [] } where upgradeField rule =@@ -814,7 +936,8 @@ upgradeNestedProfileRules rules = NestedRules { required = map upgradeNestedField (rules ^. #required),- recommended = map upgradeNestedField (rules ^. #recommended)+ recommended = map upgradeNestedField (rules ^. #recommended),+ optional = [] } upgradeNestedField rule = NestedFieldRule@@ -830,7 +953,8 @@ upgradeFormatFrontmatter previous = FrontmatterRules { required = map upgradeField (previous ^. #required),- recommended = map upgradeField (previous ^. #recommended)+ recommended = map upgradeField (previous ^. #recommended),+ optional = [] } where upgradeField rule =@@ -849,7 +973,8 @@ upgradeCardinalityFrontmatter previous = FrontmatterRules { required = map upgradeField (previous ^. #required),- recommended = map upgradeField (previous ^. #recommended)+ recommended = map upgradeField (previous ^. #recommended),+ optional = [] } where upgradeField rule =@@ -868,7 +993,8 @@ upgradeVocabularyFrontmatter previous = FrontmatterRules { required = map upgradeField (previous ^. #required),- recommended = map upgradeField (previous ^. #recommended)+ recommended = map upgradeField (previous ^. #recommended),+ optional = [] } where upgradeField rule =@@ -883,6 +1009,31 @@ when = Nothing } +upgradeReferenceProfile :: ReferenceProfileSpec -> ProfileSpec+upgradeReferenceProfile previous =+ ProfileSpec+ { name = previous ^. #name,+ description = previous ^. #description,+ okfVersion = previous ^. #okfVersion,+ frontmatter = upgradeReferenceProfileFrontmatter (previous ^. #frontmatter),+ allowUnknownTypes = previous ^. #allowUnknownTypes,+ allowUnknownFields = previous ^. #allowUnknownFields,+ idField = previous ^. #idField,+ types = map upgradeRule (previous ^. #types)+ }+ where+ upgradeRule rule =+ TypeRule+ { type_ = rule ^. #type_,+ description = rule ^. #description,+ frontmatter = upgradeReferenceProfileFrontmatter (rule ^. #frontmatter),+ pathPattern = rule ^. #pathPattern,+ resourceScheme = rule ^. #resourceScheme,+ requireSchemaSection = rule ^. #requireSchemaSection,+ schemaColumns = rule ^. #schemaColumns,+ idPrefix = rule ^. #idPrefix+ }+ upgradeConditionalProfile :: ConditionalProfileSpec -> ProfileSpec upgradeConditionalProfile previous = ProfileSpec@@ -1069,7 +1220,8 @@ frontmatter = FrontmatterRules { required = map undocumented (legacy ^. #frontmatter . #required),- recommended = map undocumented (legacy ^. #frontmatter . #recommended)+ recommended = map undocumented (legacy ^. #frontmatter . #recommended),+ optional = [] }, allowUnknownTypes = legacy ^. #allowUnknownTypes, allowUnknownFields = True,@@ -1093,9 +1245,10 @@ -- | Load and decode a Dhall profile descriptor from a file path. Any evaluation -- or decoding failure is captured as a human-readable 'Left'. ----- The condition-aware shape, bounded-nested shape, EP-4 format shape, EP-3--- cardinality shape, EP-2 vocabulary shape, type-aware EP-1 shape,--- self-documenting shape, and okf 0.2.x shape are accepted by frozen fallback decoders and upgraded+-- The reference-aware shape, condition-aware shape, bounded-nested shape, EP-4+-- format shape, EP-3 cardinality shape, EP-2 vocabulary shape, type-aware EP-1+-- shape, self-documenting shape, and okf 0.2.x shape are accepted by frozen+-- fallback decoders and upgraded -- with their no-op defaults. When every decoder fails, the /current/ decoder's -- error is reported. loadProfileFile :: FilePath -> IO (Either Text ProfileSpec)@@ -1104,40 +1257,44 @@ case current of Right spec -> pure (Right spec) Left currentError -> do- conditional <- tryDecode (Dhall.inputFile auto path)- case conditional of- Right conditionalSpec -> pure (Right (upgradeConditionalProfile conditionalSpec))- Left _conditionalError -> do- nested <- tryDecode (Dhall.inputFile auto path)- case nested of- Right nestedSpec -> pure (Right (upgradeNestedProfile nestedSpec))- Left _nestedError -> do- formatted <- tryDecode (Dhall.inputFile auto path)- case formatted of- Right formatSpec -> pure (Right (upgradeFormatProfile formatSpec))- Left _formatError -> do- cardinality <- tryDecode (Dhall.inputFile auto path)- case cardinality of- Right cardinalitySpec -> pure (Right (upgradeCardinalityProfile cardinalitySpec))- Left _cardinalityError -> do- vocabulary <- tryDecode (Dhall.inputFile auto path)- case vocabulary of- Right vocabularySpec -> pure (Right (upgradeVocabularyProfile vocabularySpec))- Left _vocabularyError -> do- typeAware <- tryDecode (Dhall.inputFile auto path)- case typeAware of- Right typeAwareSpec -> pure (Right (upgradeTypeAwareProfile typeAwareSpec))- Left _typeAwareError -> do- described <- tryDecode (Dhall.inputFile auto path)- case described of- Right describedSpec -> pure (Right (upgradeDescribedProfile describedSpec))- Left _describedError -> do- legacy <- tryDecode (Dhall.inputFile auto path)- pure $ case legacy of- Right legacySpec -> Right (upgradeLegacyProfile legacySpec)- Left _legacyError -> Left currentError+ referenceAware <- tryDecode (Dhall.inputFile auto path)+ case referenceAware of+ Right referenceSpec -> pure (Right (upgradeReferenceProfile referenceSpec))+ Left _referenceError -> do+ conditional <- tryDecode (Dhall.inputFile auto path)+ case conditional of+ Right conditionalSpec -> pure (Right (upgradeConditionalProfile conditionalSpec))+ Left _conditionalError -> do+ nested <- tryDecode (Dhall.inputFile auto path)+ case nested of+ Right nestedSpec -> pure (Right (upgradeNestedProfile nestedSpec))+ Left _nestedError -> do+ formatted <- tryDecode (Dhall.inputFile auto path)+ case formatted of+ Right formatSpec -> pure (Right (upgradeFormatProfile formatSpec))+ Left _formatError -> do+ cardinality <- tryDecode (Dhall.inputFile auto path)+ case cardinality of+ Right cardinalitySpec -> pure (Right (upgradeCardinalityProfile cardinalitySpec))+ Left _cardinalityError -> do+ vocabulary <- tryDecode (Dhall.inputFile auto path)+ case vocabulary of+ Right vocabularySpec -> pure (Right (upgradeVocabularyProfile vocabularySpec))+ Left _vocabularyError -> do+ typeAware <- tryDecode (Dhall.inputFile auto path)+ case typeAware of+ Right typeAwareSpec -> pure (Right (upgradeTypeAwareProfile typeAwareSpec))+ Left _typeAwareError -> do+ described <- tryDecode (Dhall.inputFile auto path)+ case described of+ Right describedSpec -> pure (Right (upgradeDescribedProfile describedSpec))+ Left _describedError -> do+ legacy <- tryDecode (Dhall.inputFile auto path)+ pure $ case legacy of+ Right legacySpec -> Right (upgradeLegacyProfile legacySpec)+ Left _legacyError -> Left currentError where- -- The nine calls look identical but are inferred at distinct result types;+ -- The ten calls look identical but are inferred at distinct result types; -- @auto@ picks the corresponding current or frozen decoder. tryDecode :: IO a -> IO (Either Text a) tryDecode action =@@ -1145,14 +1302,15 @@ `catch` \(exception :: SomeException) -> pure (Left (Text.pack (show exception))) -- | Does an already-evaluated Dhall expression decode as a profile? Tries the--- current schema, then the condition-aware, bounded-nested, EP-4, EP-3, EP-2, EP-1,--- self-documenting, and okf 0.2.x schemas,+-- current schema, then the reference-aware, condition-aware, bounded-nested,+-- EP-4, EP-3, EP-2, EP-1, self-documenting, and okf 0.2.x schemas, -- so the published @okf-profiles@ package still enumerates. Uses -- 'Dhall.rawInput', which normalizes and runs the decoder's extractor without -- throwing, which is what lets registry enumeration be pure. decodeProfileExpr :: Expr Src Void -> Maybe ProfileSpec decodeProfileExpr expression = Dhall.rawInput Dhall.auto expression+ <|> fmap upgradeReferenceProfile (Dhall.rawInput Dhall.auto expression) <|> fmap upgradeConditionalProfile (Dhall.rawInput Dhall.auto expression) <|> fmap upgradeNestedProfile (Dhall.rawInput Dhall.auto expression) <|> fmap upgradeFormatProfile (Dhall.rawInput Dhall.auto expression)@@ -1163,19 +1321,22 @@ <|> fmap upgradeLegacyProfile (Dhall.rawInput Dhall.auto expression) -- | The description a profile attaches to a frontmatter key, looking in--- @required@ first and then @recommended@. 'Nothing' when the key is--- undocumented or absent from the profile entirely.+-- @required@ first, then @recommended@, then @optional@. 'Nothing' when the key+-- is undocumented or absent from the profile entirely. profileFieldDescription :: ProfileSpec -> Text -> Maybe Text profileFieldDescription spec key = case [rule | rule <- rules, rule ^. #field == key] of (rule : _) -> rule ^. #description [] -> Nothing where- rules = spec ^. #frontmatter . #required <> spec ^. #frontmatter . #recommended+ rules =+ spec ^. #frontmatter . #required+ <> spec ^. #frontmatter . #recommended+ <> spec ^. #frontmatter . #optional -- | A malformed profile definition. The optional type is absent for profile--- scope and present for a type-specific scope. The list name is @required@ or--- @recommended@.+-- scope and present for a type-specific scope. The list name is @required@,+-- @recommended@, or @optional@. data ProfileDefinitionError = DuplicateTypeRule Text | DuplicateFieldRule (Maybe Text) Text Text@@ -1197,6 +1358,9 @@ | InvalidExternalReferenceScheme (Maybe Text) FieldPath Text | ConflictingReferencePrefix Text FieldPath Text Text | ReferenceWithFormat (Maybe Text) FieldPath FieldFormat+ | -- | an @optional@ rule carries a @when@ condition, which gates only presence+ -- and is therefore dead: an optional rule has no presence check at all+ OptionalFieldWithCondition (Maybe Text) FieldPath deriving stock (Generic, Eq, Ord, Show) data FieldRequirement = RecommendedField | RequiredField@@ -1309,6 +1473,9 @@ ConflictingReferencePrefix ctype target profilePrefix typePrefix -> (1, ctype, 17, renderFieldPathKey target <> ":" <> profilePrefix, Text.length typePrefix) ReferenceWithFormat scope target fieldFormat -> referenceErrorKey scope target 18 (Text.pack (show fieldFormat))+ OptionalFieldWithCondition scope target ->+ let (scopeRank, typeName) = scopeKey scope+ in (scopeRank, typeName, 19, renderFieldPathKey target, 0) scopeKey Nothing = (0, "") scopeKey (Just ctype) = (1, ctype)@@ -1321,25 +1488,37 @@ let (scopeRank, typeName) = scopeKey scope in (scopeRank, typeName, rank, renderFieldPathKey target <> ":" <> detail, 0) - scopeErrors scope FrontmatterRules {required, recommended} =+ scopeErrors scope FrontmatterRules {required, recommended, optional} = [DuplicateFieldRule scope "required" key | key <- duplicates (map (^. #field) required)] <> [DuplicateFieldRule scope "recommended" key | key <- duplicates (map (^. #field) recommended)]+ <> [DuplicateFieldRule scope "optional" key | key <- duplicates (map (^. #field) optional)] <> [ ConflictingFieldRequirement scope key- | key <- List.nub (List.sort (List.intersect (map (^. #field) required) (map (^. #field) recommended)))+ | key <- presenceListCollisions (map (^. #field) required) (map (^. #field) recommended) (map (^. #field) optional) ]- <> concatMap (nestedScopeErrors scope) (required <> recommended)+ <> concatMap (nestedScopeErrors scope) (required <> recommended <> optional) nestedScopeErrors scope parentRule = case parentRule ^. #elementFields of Nothing -> []- Just NestedRules {required, recommended} ->+ Just NestedRules {required, recommended, optional} -> let qualify key = parentRule ^. #field <> "." <> key in [DuplicateFieldRule scope "nested required" (qualify key) | key <- duplicates (map (^. #field) required)] <> [DuplicateFieldRule scope "nested recommended" (qualify key) | key <- duplicates (map (^. #field) recommended)]+ <> [DuplicateFieldRule scope "nested optional" (qualify key) | key <- duplicates (map (^. #field) optional)] <> [ ConflictingFieldRequirement scope (qualify key)- | key <- List.nub (List.sort (List.intersect (map (^. #field) required) (map (^. #field) recommended)))+ | key <- presenceListCollisions (map (^. #field) required) (map (^. #field) recommended) (map (^. #field) optional) ] + -- One key classified by more than one presence list at a single scope. The+ -- profile cannot say both "always check for this" and "never check for+ -- this", so any pairing is a definition error rather than a precedence rule.+ presenceListCollisions requiredKeys recommendedKeys optionalKeys =+ List.nub . List.sort . concat $+ [ List.intersect requiredKeys recommendedKeys,+ List.intersect requiredKeys optionalKeys,+ List.intersect recommendedKeys optionalKeys+ ]+ duplicates = mapMaybe duplicateHead . List.group . List.sort duplicateHead (candidate : _ : _) = Just candidate duplicateHead _ = Nothing@@ -1397,7 +1576,7 @@ nestedCardinalityErrors = [ ElementFieldsRequireList scope (topLevelFieldPath (fieldRule ^. #field)) Scalar | (scope, rules) <- (Nothing, rawSpec ^. #frontmatter) : [(Just (rule ^. #type_), rule ^. #frontmatter) | rule <- rawSpec ^. #types],- fieldRule <- rules ^. #required <> rules ^. #recommended,+ fieldRule <- rules ^. #required <> rules ^. #recommended <> rules ^. #optional, isJust (fieldRule ^. #elementFields), fieldRule ^. #cardinality == Scalar ]@@ -1405,14 +1584,14 @@ formatParameterErrors = [ InvalidFormatParameter (topLevelFieldPath (rule ^. #field)) fieldFormat parameter | rules <- (rawSpec ^. #frontmatter) : map (^. #frontmatter) (rawSpec ^. #types),- rule <- rules ^. #required <> rules ^. #recommended,+ rule <- rules ^. #required <> rules ^. #recommended <> rules ^. #optional, Just (fieldFormat, parameter) <- [invalidFormatParameter =<< rule ^. #format] ] <> [ InvalidFormatParameter (nestedDefinitionPath (rule ^. #field) (nestedRule ^. #field)) fieldFormat parameter | rules <- (rawSpec ^. #frontmatter) : map (^. #frontmatter) (rawSpec ^. #types),- rule <- rules ^. #required <> rules ^. #recommended,+ rule <- rules ^. #required <> rules ^. #recommended <> rules ^. #optional, Just nestedRules <- [rule ^. #elementFields],- nestedRule <- nestedRules ^. #required <> nestedRules ^. #recommended,+ nestedRule <- nestedRules ^. #required <> nestedRules ^. #recommended <> nestedRules ^. #optional, Just (fieldFormat, parameter) <- [invalidFormatParameter =<< nestedRule ^. #format] ] @@ -1448,16 +1627,31 @@ conditionErrors scope parent rules effectiveRules = concatMap (fieldConditionErrors scope parent effectiveRules) (rules ^. #required <> rules ^. #recommended)+ <> [ deadCondition scope parent (rawRule ^. #field)+ | rawRule <- rules ^. #optional,+ isJust (rawRule ^. #when)+ ] <> concat [ case (rawRule ^. #elementFields, Map.lookup (rawRule ^. #field) effectiveRules >>= (^. #elementFields)) of (Just nestedRules, Just effectiveNestedRules) -> concatMap (fieldConditionErrors scope (Just (rawRule ^. #field)) effectiveNestedRules) (nestedRules ^. #required <> nestedRules ^. #recommended)+ <> [ deadCondition scope (Just (rawRule ^. #field)) (nestedRule ^. #field)+ | nestedRule <- nestedRules ^. #optional,+ isJust (nestedRule ^. #when)+ ] _ -> []- | rawRule <- rules ^. #required <> rules ^. #recommended+ | rawRule <- rules ^. #required <> rules ^. #recommended <> rules ^. #optional ] + -- A condition gates presence, and an optional rule has no presence clause to+ -- gate, so the pairing is dead in the descriptor rather than a weaker rule.+ deadCondition scope parent key =+ OptionalFieldWithCondition+ scope+ (maybe (topLevelFieldPath key) (\parentKey -> nestedDefinitionPath parentKey key) parent)+ fieldConditionErrors scope parent effectiveRules rawRule = case rawRule ^. #when of Nothing -> []@@ -1496,7 +1690,7 @@ : [(Just (rule ^. #type_), rule ^. #frontmatter) | rule <- rawSpec ^. #types] rawReferenceErrors (scope, rules) =- concatMap (fieldReferenceErrors scope) (rules ^. #required <> rules ^. #recommended)+ concatMap (fieldReferenceErrors scope) (rules ^. #required <> rules ^. #recommended <> rules ^. #optional) fieldReferenceErrors scope rule = case rule ^. #reference of@@ -1533,7 +1727,7 @@ renderSegment (ArrayIndex elementIndex) = Text.pack (show elementIndex) compileRules :: FrontmatterRules -> Map Text EffectiveFieldRule-compileRules FrontmatterRules {required, recommended} =+compileRules FrontmatterRules {required, recommended, optional} = Map.fromList ( [ (rule ^. #field, compileFieldRule RequiredField rule) | rule <- required@@ -1541,12 +1735,20 @@ <> [ (rule ^. #field, compileFieldRule RecommendedField rule) | rule <- recommended ]+ <> [ (rule ^. #field, compileOptionalFieldRule rule)+ | rule <- optional+ ] ) -compileFieldRule :: FieldRequirement -> FieldRule -> EffectiveFieldRule-compileFieldRule requirement rule =+-- | Compile a rule the profile documents but never demands. It carries no+-- presence clause, so 'applicablePresenceClause' can never find one to report in+-- either validation mode, while every value check still runs from the+-- present-value branch. This is also the value constraints a required or+-- recommended rule is built from.+compileOptionalFieldRule :: FieldRule -> EffectiveFieldRule+compileOptionalFieldRule rule = EffectiveFieldRule- { presenceClauses = [PresenceClause requirement (compileCondition <$> rule ^. #when)],+ { presenceClauses = [], description = rule ^. #description, allowedValues = deduplicate (rule ^. #allowedValues), cardinality =@@ -1558,8 +1760,14 @@ reference = compileReferenceRule <$> rule ^. #reference } +compileFieldRule :: FieldRequirement -> FieldRule -> EffectiveFieldRule+compileFieldRule requirement rule =+ (compileOptionalFieldRule rule)+ { presenceClauses = [PresenceClause requirement (compileCondition <$> rule ^. #when)]+ }+ compileNestedRules :: NestedRules -> Map Text EffectiveFieldRule-compileNestedRules NestedRules {required, recommended} =+compileNestedRules NestedRules {required, recommended, optional} = Map.fromList ( [ (rule ^. #field, compileNestedFieldRule RequiredField rule) | rule <- required@@ -1567,18 +1775,28 @@ <> [ (rule ^. #field, compileNestedFieldRule RecommendedField rule) | rule <- recommended ]+ <> [ (rule ^. #field, compileOptionalNestedFieldRule rule)+ | rule <- optional+ ] ) -compileNestedFieldRule :: FieldRequirement -> NestedFieldRule -> EffectiveFieldRule-compileNestedFieldRule requirement rule =+-- | The nested counterpart of 'compileOptionalFieldRule'.+compileOptionalNestedFieldRule :: NestedFieldRule -> EffectiveFieldRule+compileOptionalNestedFieldRule rule = EffectiveFieldRule- { presenceClauses = [PresenceClause requirement (compileCondition <$> rule ^. #when)],+ { presenceClauses = [], description = rule ^. #description, allowedValues = deduplicate (rule ^. #allowedValues), cardinality = rule ^. #cardinality, format = rule ^. #format, elementFields = Nothing, reference = Nothing+ }++compileNestedFieldRule :: FieldRequirement -> NestedFieldRule -> EffectiveFieldRule+compileNestedFieldRule requirement rule =+ (compileOptionalNestedFieldRule rule)+ { presenceClauses = [PresenceClause requirement (compileCondition <$> rule ^. #when)] } mergeRules :: Map Text EffectiveFieldRule -> Map Text EffectiveFieldRule -> Map Text EffectiveFieldRule
test/Main.hs view
@@ -92,8 +92,10 @@ testIO "loadProfileFile preserves the frozen bounded-nested schema" testLoadNestedCompatibilityFixture, testIO "loadProfileFile decodes same-scope conditions" testLoadConditionalFieldsProfileFixture, testIO "loadProfileFile preserves the frozen condition-aware schema" testLoadConditionalCompatibilityFixture,+ testIO "loadProfileFile preserves the frozen reference-aware schema" testLoadReferenceCompatibilityFixture, testIO "loadProfileFile still accepts an okf 0.2.x descriptor" testLoadLegacyProfileFixture, testIO "profileFieldDescription finds required and recommended prose" testProfileFieldDescription,+ testIO "profileFieldDescription finds optional prose" testOptionalFieldDescription, testIO "profile JSON encoding emits type, not type_" testProfileJsonShape, test "field condition JSON encoding is stable" testFieldConditionJsonShape, test "handle reference JSON encoding is stable" testHandleReferenceJsonShape,@@ -124,6 +126,12 @@ test "nested conditions use siblings and avoid cascading diagnostics" testNestedConditionalPresence, test "compileProfile rejects invalid document reference policies" testReferenceDefinitionErrors, test "document references resolve local handles and explicit external URIs" testDocumentReferenceValidation,+ test "optional fields are never missing but are fully value-checked" testOptionalFieldPresence,+ test "optional reference fields resolve handles when present" testOptionalReferenceValidation,+ test "optional nested fields are never missing inside records" testOptionalNestedFieldPresence,+ test "optional fields count as declared under closed field names" testOptionalFieldClosure,+ test "optional at one scope does not cancel the other scope's clause" testOptionalDoesNotCancelOtherScope,+ test "compileProfile rejects optional collisions and dead conditions" testOptionalDefinitionErrors, test "closed profiles reject unknown fields and isolate type fields" testClosedFieldValidation, test "profile rules apply to unknown concept types" testProfileRulesApplyToUnknownTypes, test "strict profile validation checks recommendations" testStrictProfileRecommendations,@@ -148,7 +156,8 @@ testIO "format fixture reports parser-backed mismatches" testFormatsFixture, testIO "nested review fixture validates records with indexed diagnostics" testNestedReviewsFixture, testIO "conditional fixture covers ADR, PostgreSQL, and review scopes" testConditionalFieldsFixture,- testIO "document reference fixture covers local, external, self, and duplicate targets" testDocumentReferencesFixture+ testIO "document reference fixture covers local, external, self, and duplicate targets" testDocumentReferencesFixture,+ testIO "optional-field fixture reports only the recommendation and bad values" testOptionalFieldsFixture ] unless (and results) exitFailure @@ -964,6 +973,33 @@ _ -> Left "expected frozen nested conditions to survive the compatibility upgrade" _ -> Left "expected three frozen condition-aware top-level rules" +-- | The immediately preceding generation: a descriptor that spells out the+-- reference-aware record types with no @optional@ list anywhere still loads,+-- keeps every field it did declare, and behaves as though each optional list+-- were empty.+testLoadReferenceCompatibilityFixture :: IO (Either Text ())+testLoadReferenceCompatibilityFixture = do+ path <- fixtureFilePath "profiles/document-references-ep3.dhall"+ result <- loadProfileFile path+ pure $ case result of+ Left err -> Left ("failed to load frozen reference-aware profile: " <> err)+ Right spec -> do+ assertEqual [] (spec ^. #frontmatter . #optional)+ assertEqual [[]] (map (^. #frontmatter . #optional) (spec ^. #types))+ case spec ^. #frontmatter . #recommended of+ [referenceRule, conditionRule, reviewsRule] -> do+ assertEqual+ (Just (HandleReferenceRule "ADR" ["mori"] False))+ (referenceRule ^. #reference)+ assertEqual (Just (FieldCondition "status" ["superseded"])) (conditionRule ^. #when)+ case reviewsRule ^. #elementFields of+ Just NestedRules {required = [kindRule], recommended = [notesRule], optional = nestedOptional} -> do+ assertEqual "kind" (kindRule ^. #field)+ assertEqual "notes" (notesRule ^. #field)+ assertEqual [] (map (^. #field) nestedOptional)+ _ -> Left "expected the frozen nested rules to survive with an empty optional list"+ _ -> Left "expected three frozen reference-aware recommended rules"+ -- | The backwards-compatibility guarantee: a descriptor frozen in the okf 0.2.x -- shape — bare-string frontmatter keys, no descriptions anywhere — still loads, -- via the legacy fallback decoder, with every description absent.@@ -1000,6 +1036,20 @@ assertEqual Nothing (profileFieldDescription spec "timestamp") assertEqual Nothing (profileFieldDescription spec "nope") +-- | Prose declared on an optional rule is as discoverable as prose on a required+-- or recommended one; the third list is searched last, after the two that can+-- produce a missing-field diagnostic.+testOptionalFieldDescription :: IO (Either Text ())+testOptionalFieldDescription = do+ path <- fixtureFilePath "profiles/decisions.dhall"+ result <- loadProfileFile path+ pure $ case result of+ Left err -> Left ("failed to load decisions profile: " <> err)+ Right spec ->+ assertEqual+ (Just "The decision this one replaces, when it replaces one.")+ (profileFieldDescription spec "supersedes")+ -- | The JSON encoding is pinned field by field, so a future refactor cannot -- silently rename a key. The @type@ key matters most: the Haskell field is -- @type_@, and consumers must never see that.@@ -1055,6 +1105,19 @@ "reference" .= (Nothing :: Maybe HandleReferenceRule), "when" .= (Nothing :: Maybe FieldCondition) ]+ ],+ "optional"+ .= [ object+ [ "field" .= ("supersedes" :: Text),+ "description"+ .= ("The decision this one replaces, when it replaces one." :: Text),+ "allowedValues" .= ([] :: [Text]),+ "cardinality" .= ("any" :: Text),+ "format" .= (Nothing :: Maybe Text),+ "elementFields" .= (Nothing :: Maybe Value),+ "reference" .= (Nothing :: Maybe HandleReferenceRule),+ "when" .= (Nothing :: Maybe FieldCondition)+ ] ] ], "types"@@ -1065,7 +1128,8 @@ "frontmatter" .= object [ "required" .= ([] :: [FieldRule]),- "recommended" .= ([] :: [FieldRule])+ "recommended" .= ([] :: [FieldRule]),+ "optional" .= ([] :: [FieldRule]) ], "pathPattern" .= ("decisions/*" :: Text), "resourceScheme" .= (Nothing :: Maybe Text),@@ -1244,7 +1308,8 @@ frontmatter = FrontmatterRules { required = [requiredField "type", requiredField "title"],- recommended = []+ recommended = [],+ optional = [] }, allowUnknownTypes = False, allowUnknownFields = True,@@ -1272,7 +1337,8 @@ frontmatter = FrontmatterRules { required = [requiredField "type", requiredField "title"],- recommended = []+ recommended = [],+ optional = [] }, allowUnknownTypes = False, allowUnknownFields = True,@@ -1292,7 +1358,7 @@ } emptyTestFrontmatterRules :: FrontmatterRules-emptyTestFrontmatterRules = FrontmatterRules {required = [], recommended = []}+emptyTestFrontmatterRules = FrontmatterRules {required = [], recommended = [], optional = []} typeAwareProfileSpec :: ProfileSpec typeAwareProfileSpec =@@ -1303,7 +1369,8 @@ frontmatter = FrontmatterRules { required = [FieldRule "type" Nothing [] Any Nothing Nothing Nothing Nothing, FieldRule "title" (Just "Global title.") [] Any Nothing Nothing Nothing Nothing],- recommended = [FieldRule "owner" (Just "Profile-level owner.") [] Any Nothing Nothing Nothing Nothing]+ recommended = [FieldRule "owner" (Just "Profile-level owner.") [] Any Nothing Nothing Nothing Nothing],+ optional = [] }, allowUnknownTypes = True, allowUnknownFields = True,@@ -1315,7 +1382,8 @@ frontmatter = FrontmatterRules { required = [FieldRule "owner" (Just "Responsible person.") [] Any Nothing Nothing Nothing Nothing],- recommended = [FieldRule "reviewer" (Just "Second pair of eyes.") [] Any Nothing Nothing Nothing Nothing, FieldRule "title" (Just "Type title.") [] Any Nothing Nothing Nothing Nothing]+ recommended = [FieldRule "reviewer" (Just "Second pair of eyes.") [] Any Nothing Nothing Nothing Nothing, FieldRule "title" (Just "Type title.") [] Any Nothing Nothing Nothing Nothing],+ optional = [] }, pathPattern = Nothing, resourceScheme = Nothing,@@ -1334,7 +1402,8 @@ { frontmatter = FrontmatterRules { required = [duplicateField, duplicateField],- recommended = [duplicateField]+ recommended = [duplicateField],+ optional = [] }, types = (typeAwareProfileSpec ^. #types) <> (typeAwareProfileSpec ^. #types) }@@ -1366,13 +1435,15 @@ { frontmatter = FrontmatterRules { required = [FieldRule "type" Nothing [] Any Nothing Nothing Nothing Nothing],- recommended = [FieldRule "status" Nothing ["draft", "approved", "approved"] Any Nothing Nothing Nothing Nothing]+ recommended = [FieldRule "status" Nothing ["draft", "approved", "approved"] Any Nothing Nothing Nothing Nothing],+ optional = [] }, types = [ withTypeFrontmatter FrontmatterRules { required = [FieldRule "status" Nothing ["approved", "archived"] Any Nothing Nothing Nothing Nothing],- recommended = []+ recommended = [],+ optional = [] } (firstTypeRule typeAwareProfileSpec) ]@@ -1398,7 +1469,8 @@ [ withTypeFrontmatter FrontmatterRules { required = [FieldRule "status" Nothing ["closed"] Any Nothing Nothing Nothing Nothing],- recommended = []+ recommended = [],+ optional = [] } (firstTypeRule vocabularyProfileSpec) ]@@ -1412,12 +1484,14 @@ let profileRules = FrontmatterRules { required = [FieldRule "type" Nothing [] Any Nothing Nothing Nothing Nothing],- recommended = [FieldRule "status" Nothing [] Scalar Nothing Nothing Nothing Nothing]+ recommended = [FieldRule "status" Nothing [] Scalar Nothing Nothing Nothing Nothing],+ optional = [] } typeRules cardinality = FrontmatterRules { required = [FieldRule "status" Nothing [] cardinality Nothing Nothing Nothing Nothing],- recommended = []+ recommended = [],+ optional = [] } baseType = firstTypeRule typeAwareProfileSpec compatible =@@ -1445,7 +1519,8 @@ { frontmatter = FrontmatterRules { required = [FieldRule "type" Nothing [] Any Nothing Nothing Nothing Nothing],- recommended = [FieldRule "status" Nothing ["draft", "approved"] Any Nothing Nothing Nothing Nothing]+ recommended = [FieldRule "status" Nothing ["draft", "approved"] Any Nothing Nothing Nothing Nothing],+ optional = [] }, types = [] }@@ -1523,13 +1598,15 @@ { frontmatter = FrontmatterRules { required = [FieldRule "type" Nothing [] Any Nothing Nothing Nothing Nothing, FieldRule "homepage" Nothing [] Any (Just profileFormat) Nothing Nothing Nothing],- recommended = []+ recommended = [],+ optional = [] }, types = [ withTypeFrontmatter FrontmatterRules { required = [FieldRule "homepage" Nothing [] Any (Just typeFormat) Nothing Nothing Nothing],- recommended = []+ recommended = [],+ optional = [] } baseType ]@@ -1557,7 +1634,8 @@ FieldRule "handle" Nothing [] Any (Just (DocumentHandle "1ADR")) Nothing Nothing Nothing, FieldRule "source" Nothing [] Any (Just (UriWithScheme "https_")) Nothing Nothing Nothing ],- recommended = []+ recommended = [],+ optional = [] }, types = [] }@@ -1614,7 +1692,8 @@ { frontmatter = FrontmatterRules { required = [FieldRule "type" Nothing [] Any Nothing Nothing Nothing Nothing],- recommended = [FieldRule "value" Nothing [] cardinality (Just fieldFormat) Nothing Nothing Nothing]+ recommended = [FieldRule "value" Nothing [] cardinality (Just fieldFormat) Nothing Nothing Nothing],+ optional = [] }, allowUnknownTypes = True, types = []@@ -1626,7 +1705,8 @@ { frontmatter = FrontmatterRules { required = [FieldRule "type" Nothing [] Any Nothing Nothing Nothing Nothing] <> [rule | isRequired],- recommended = [rule | not isRequired]+ recommended = [rule | not isRequired],+ optional = [] }, allowUnknownTypes = True, types = []@@ -1639,7 +1719,8 @@ let profileRules = NestedRules { required = [NestedFieldRule "kind" Nothing ["decision", "implementation"] Any Nothing Nothing],- recommended = [NestedFieldRule "notes" Nothing [] Scalar Nothing Nothing]+ recommended = [NestedFieldRule "notes" Nothing [] Scalar Nothing Nothing],+ optional = [] } typeRules = NestedRules@@ -1647,7 +1728,8 @@ [ NestedFieldRule "kind" Nothing ["implementation", "operations"] Any Nothing Nothing, NestedFieldRule "outcome" Nothing ["approved", "rejected"] Any Nothing Nothing ],- recommended = []+ recommended = [],+ optional = [] } base = nestedProfileWithRules Any profileRules (Just typeRules) compiled <- firstShow (compileProfile base)@@ -1740,7 +1822,8 @@ [ requiredField "type", FieldRule "reviews" Nothing [] outerCardinality Nothing (Just profileNested) Nothing Nothing ],- recommended = []+ recommended = [],+ optional = [] }, allowUnknownTypes = False, allowUnknownFields = True,@@ -1752,7 +1835,8 @@ frontmatter = FrontmatterRules { required = maybe [] (\rules -> [FieldRule "reviews" Nothing [] Any Nothing (Just rules) Nothing Nothing]) typeNested,- recommended = []+ recommended = [],+ optional = [] }, pathPattern = Nothing, resourceScheme = Nothing,@@ -1778,7 +1862,8 @@ NestedFieldRule "outcome" Nothing ["approved", "changes-requested", "commented"] Any Nothing Nothing, NestedFieldRule "context" Nothing [] Scalar Nothing Nothing ],- recommended = [NestedFieldRule "notes" Nothing [] Scalar Nothing Nothing]+ recommended = [NestedFieldRule "notes" Nothing [] Scalar Nothing Nothing],+ optional = [] } nestedTestPath :: Int -> Text -> FieldPath@@ -1797,7 +1882,7 @@ compileWith rules = compileProfile typeAwareProfileSpec- { frontmatter = FrontmatterRules {required = rules, recommended = []},+ { frontmatter = FrontmatterRules {required = rules, recommended = [], optional = []}, allowUnknownTypes = True, types = [] }@@ -1826,7 +1911,8 @@ [ NestedFieldRule "kind" Nothing ["human", "model"] Scalar Nothing Nothing, NestedFieldRule "provider" Nothing [] Scalar Nothing (Just (FieldCondition "status" ["active"])) ],- recommended = []+ recommended = [],+ optional = [] } crossScopeProfile = typeAwareProfileSpec@@ -1836,7 +1922,8 @@ [ source "status" ["active"] Scalar, FieldRule "reviews" Nothing [] List Nothing (Just nestedCrossScope) Nothing Nothing ],- recommended = []+ recommended = [],+ optional = [] }, allowUnknownTypes = True, types = []@@ -1857,11 +1944,11 @@ FieldRule "supersededBy" Nothing ["ADR-1"] Scalar Nothing Nothing Nothing (Just (FieldCondition "status" ["superseded"])) base = typeAwareProfileSpec- { frontmatter = FrontmatterRules {required = [requiredField "type", statusRule], recommended = [recommendedTarget]},+ { frontmatter = FrontmatterRules {required = [requiredField "type", statusRule], recommended = [recommendedTarget], optional = []}, allowUnknownTypes = True, types = [ withTypeFrontmatter- FrontmatterRules {required = [requiredTarget], recommended = []}+ FrontmatterRules {required = [requiredTarget], recommended = [], optional = []} (firstTypeRule typeAwareProfileSpec) ] }@@ -1907,7 +1994,8 @@ NestedFieldRule "provider" Nothing [] Scalar Nothing (Just (FieldCondition "kind" ["model"])) ], recommended =- [NestedFieldRule "notes" Nothing [] Scalar Nothing (Just (FieldCondition "kind" ["human"]))]+ [NestedFieldRule "notes" Nothing [] Scalar Nothing (Just (FieldCondition "kind" ["human"]))],+ optional = [] } spec = nestedProfileWithRules List nestedRules Nothing compiled <- firstShow (compileProfile spec)@@ -1944,7 +2032,7 @@ baseType = firstTypeRule testDocumentIdProfileSpec specWith profileIdField typeRules profileRules = testDocumentIdProfileSpec- { frontmatter = FrontmatterRules {required = profileRules, recommended = []},+ { frontmatter = FrontmatterRules {required = profileRules, recommended = [], optional = []}, idField = profileIdField, types = typeRules }@@ -1957,7 +2045,7 @@ formatSpec = specWith (Just "docId") [baseType] [referenceRule "supersedes" "ADR" [] (Just (DocumentHandle "ADR"))] typeReference = referenceRule "supersedes" "RFC" [] Nothing conflictingType :: TypeRule- conflictingType = baseType & #frontmatter .~ FrontmatterRules {required = [typeReference], recommended = []}+ conflictingType = baseType & #frontmatter .~ FrontmatterRules {required = [typeReference], recommended = [], optional = []} rfcType = baseType {type_ = "RFC", idPrefix = Just "RFC", pathPattern = Nothing} conflictSpec = specWith (Just "docId") [conflictingType, rfcType] [referenceRule "supersedes" "ADR" [] Nothing] assertEqual@@ -1993,7 +2081,8 @@ & #frontmatter .~ FrontmatterRules { required = [requiredField "type", requiredField "title"],- recommended = referenceRules+ recommended = referenceRules,+ optional = [] } compiled <- firstShow (compileProfile spec) duplicateA <- decisionConcept "decisions/duplicate-a" "Duplicate A" "ADR-3" []@@ -2040,20 +2129,298 @@ ("# " <> title <> "\n") indexedPath key elementIndex = FieldPath (FieldName key :| [ArrayIndex elementIndex]) +-- | The whole point of the third presence list: absence is silent in both+-- validation modes, while a present value is checked exactly as hard as it would+-- be under @required@.+testOptionalFieldPresence :: Either Text ()+testOptionalFieldPresence = do+ let optionalRules =+ [ FieldRule "supersedes" Nothing ["ADR-1", "ADR-2"] Scalar Nothing Nothing Nothing Nothing,+ FieldRule "reviewedAt" Nothing [] Any (Just Rfc3339Utc) Nothing Nothing Nothing,+ FieldRule "tags" Nothing [] List Nothing Nothing Nothing Nothing+ ]+ spec =+ typeAwareProfileSpec+ { frontmatter =+ FrontmatterRules+ { required = [requiredField "type"],+ recommended = [requiredField "owner"],+ optional = optionalRules+ },+ allowUnknownTypes = True,+ types = []+ }+ compiled <- firstShow (compileProfile spec)+ absent <- profileConcept "optional/absent" [("type", String "Extension"), ("owner", String "Ari")] "# Absent\n"+ -- A correctly shaped empty value counts as absent, so it is as silent as a key+ -- that was never written. (A blank value on a field that also declares a+ -- vocabulary or format still fails that check; presence and value are+ -- independent, which is exactly what this feature relies on.)+ emptied <-+ profileConcept+ "optional/emptied"+ [ ("type", String "Extension"),+ ("owner", String "Ari"),+ ("tags", toJSON ([] :: [Text]))+ ]+ "# Emptied\n"+ valid <-+ profileConcept+ "optional/valid"+ [ ("type", String "Extension"),+ ("owner", String "Ari"),+ ("supersedes", String "ADR-1"),+ ("reviewedAt", String "2026-07-30T00:00:00Z"),+ ("tags", toJSON (["profiles"] :: [Text]))+ ]+ "# Valid\n"+ invalid <-+ profileConcept+ "optional/invalid"+ [ ("type", String "Extension"),+ ("owner", String "Ari"),+ ("supersedes", String "ADR-9"),+ ("reviewedAt", String "2026-13-45T99:99:99Z"),+ ("tags", String "profiles")+ ]+ "# Invalid\n"+ invalidId <- parseTestConceptId "optional/invalid"+ for_ [PermissiveConformance, StrictAuthoring] $ \validationProfile -> do+ assertEqual [] (validateProfile validationProfile compiled [absent])+ assertEqual [] (validateProfile validationProfile compiled [emptied])+ assertEqual [] (validateProfile validationProfile compiled [valid])+ assertEqual+ [ ValueFormatMismatch invalidId (fieldPath "reviewedAt") Rfc3339Utc (String "2026-13-45T99:99:99Z"),+ ValueNotInVocabulary invalidId (fieldPath "supersedes") ["ADR-1", "ADR-2"] (String "ADR-9"),+ CardinalityMismatch invalidId (fieldPath "tags") List (String "profiles")+ ]+ (validateProfile validationProfile compiled [invalid])++-- | An optional field carrying a document-reference policy resolves handles the+-- same way a required one does; only the absence check differs.+testOptionalReferenceValidation :: Either Text ()+testOptionalReferenceValidation = do+ let spec :: ProfileSpec+ spec =+ testDocumentIdProfileSpec+ & #frontmatter+ .~ FrontmatterRules+ { required = [requiredField "type", requiredField "title"],+ recommended = [],+ optional = [FieldRule "supersedes" Nothing [] Scalar Nothing Nothing (Just (HandleReferenceRule "ADR" [] False)) Nothing]+ }+ compiled <- firstShow (compileProfile spec)+ target <- decisionTestConcept "decisions/target" "Target" "ADR-1" []+ silent <- decisionTestConcept "decisions/silent" "Silent" "ADR-2" []+ dangling <- decisionTestConcept "decisions/dangling" "Dangling" "ADR-3" [("supersedes", String "ADR-99")]+ danglingId <- parseTestConceptId "decisions/dangling"+ for_ [PermissiveConformance, StrictAuthoring] $ \validationProfile -> do+ assertEqual [] (validateProfile validationProfile compiled [target, silent])+ assertEqual+ [DanglingHandleReference danglingId (fieldPath "supersedes") "ADR-99"]+ (validateProfile validationProfile compiled [target, dangling])++-- | Optional members of a list-element record behave the same way inside every+-- record: never missing, always checked when present.+testOptionalNestedFieldPresence :: Either Text ()+testOptionalNestedFieldPresence = do+ let nestedRules =+ NestedRules+ { required = [NestedFieldRule "kind" Nothing ["human", "model"] Scalar Nothing Nothing],+ recommended = [NestedFieldRule "notes" Nothing [] Scalar Nothing Nothing],+ optional = [NestedFieldRule "model" Nothing ["opus", "sonnet"] Scalar Nothing Nothing]+ }+ compiled <- firstShow (compileProfile (nestedProfileWithRules Any nestedRules Nothing))+ concept <-+ profileConcept+ "reviewed/optional"+ [ ("type", String "Reviewed Concept"),+ ( "reviews",+ toJSON+ [ object ["kind" .= ("human" :: Text), "notes" .= ("looks good" :: Text)],+ object ["kind" .= ("model" :: Text), "notes" .= ("ran it" :: Text), "model" .= ("opus" :: Text)],+ object ["kind" .= ("model" :: Text), "notes" .= ("ran it" :: Text), "model" .= ("gpt" :: Text)]+ ]+ )+ ]+ "# Optional\n"+ cid <- parseTestConceptId "reviewed/optional"+ for_ [PermissiveConformance, StrictAuthoring] $ \validationProfile ->+ assertEqual+ [ValueNotInVocabulary cid (nestedTestPath 2 "model") ["opus", "sonnet"] (String "gpt")]+ (validateProfile validationProfile compiled [concept])++-- | An optional key is declared for the purposes of field-name closure, so a+-- closed profile accepts it and still catches a misspelling of it.+testOptionalFieldClosure :: Either Text ()+testOptionalFieldClosure = do+ let closed =+ typeAwareProfileSpec+ { frontmatter =+ FrontmatterRules+ { required = [requiredField "type"],+ recommended = [],+ optional = [requiredField "supersedes"]+ },+ allowUnknownTypes = True,+ allowUnknownFields = False,+ types = []+ }+ compiled <- firstShow (compileProfile closed)+ declared <- profileConcept "closed/declared" [("type", String "Extension"), ("supersedes", String "ADR-1")] "# Declared\n"+ typo <- profileConcept "closed/typo" [("type", String "Extension"), ("supersedse", String "ADR-1")] "# Typo\n"+ typoId <- parseTestConceptId "closed/typo"+ assertEqual [] (validateProfile PermissiveConformance compiled [declared])+ assertEqual+ [FieldNotInProfile typoId "supersedse"]+ (validateProfile PermissiveConformance compiled [typo])++-- | Declaring a key optional at one scope does not cancel the other scope's+-- presence clause. Merging accumulates clauses precisely so a type rule can+-- narrow but never silently weaken a profile-wide expectation.+testOptionalDoesNotCancelOtherScope :: Either Text ()+testOptionalDoesNotCancelOtherScope = do+ let ownerRule = requiredField "owner"+ specWith profileRules typeRules =+ typeAwareProfileSpec+ { frontmatter = profileRules,+ allowUnknownTypes = True,+ types = [withTypeFrontmatter typeRules (firstTypeRule typeAwareProfileSpec)]+ }+ recommendedThenOptional =+ specWith+ FrontmatterRules {required = [requiredField "type"], recommended = [ownerRule], optional = []}+ FrontmatterRules {required = [], recommended = [], optional = [ownerRule]}+ optionalThenRecommended =+ specWith+ FrontmatterRules {required = [requiredField "type"], recommended = [], optional = [ownerRule]}+ FrontmatterRules {required = [], recommended = [ownerRule], optional = []}+ concept <- profileConcept "owned/one" [("type", String "Owned Concept")] "# One\n"+ cid <- parseTestConceptId "owned/one"+ for_ [recommendedThenOptional, optionalThenRecommended] $ \spec -> do+ compiled <- firstShow (compileProfile spec)+ assertEqual [] (validateProfile PermissiveConformance compiled [concept])+ assertEqual+ [MissingRecommendedProfileField cid "owner" Nothing]+ (validateProfile StrictAuthoring compiled [concept])++-- | Compilation rejects the two contradictions the third list makes possible: a+-- key classified twice at one scope, and a condition on a rule that has no+-- presence check for it to gate.+testOptionalDefinitionErrors :: Either Text ()+testOptionalDefinitionErrors = do+ let key name = requiredField name+ specWith rules =+ typeAwareProfileSpec {frontmatter = rules, allowUnknownTypes = True, types = []}+ conditioned name sourceKey =+ FieldRule name Nothing [] Any Nothing Nothing Nothing (Just (FieldCondition sourceKey ["active"]))+ statusRule = FieldRule "status" Nothing ["active"] Scalar Nothing Nothing Nothing Nothing+ assertEqual+ (Left (ConflictingFieldRequirement Nothing "owner" :| []))+ (compileProfile (specWith FrontmatterRules {required = [key "type", key "owner"], recommended = [], optional = [key "owner"]}))+ assertEqual+ (Left (ConflictingFieldRequirement Nothing "owner" :| []))+ (compileProfile (specWith FrontmatterRules {required = [key "type"], recommended = [key "owner"], optional = [key "owner"]}))+ assertEqual+ (Left (DuplicateFieldRule Nothing "optional" "owner" :| []))+ (compileProfile (specWith FrontmatterRules {required = [key "type"], recommended = [], optional = [key "owner", key "owner"]}))+ assertEqual+ (Left (OptionalFieldWithCondition Nothing (fieldPath "supersededBy") :| []))+ ( compileProfile+ (specWith FrontmatterRules {required = [key "type", statusRule], recommended = [], optional = [conditioned "supersededBy" "status"]})+ )+ let nestedRules =+ NestedRules+ { required = [NestedFieldRule "kind" Nothing ["model"] Scalar Nothing Nothing],+ recommended = [],+ optional = [NestedFieldRule "model" Nothing [] Scalar Nothing (Just (FieldCondition "kind" ["model"]))]+ }+ assertEqual+ (Left (OptionalFieldWithCondition Nothing (FieldPath (FieldName "reviews" :| [FieldName "model"])) :| []))+ (compileProfile (nestedProfileWithRules Any nestedRules Nothing))+ let optionalParent =+ specWith+ FrontmatterRules+ { required = [key "type"],+ recommended = [],+ optional = [FieldRule "reviews" Nothing [] List Nothing (Just nestedRules) Nothing Nothing]+ }+ assertEqual+ (Left (OptionalFieldWithCondition Nothing (FieldPath (FieldName "reviews" :| [FieldName "model"])) :| []))+ (compileProfile optionalParent)++decisionTestConcept :: Text -> Text -> Text -> [(Text, Value)] -> Either Text Concept+decisionTestConcept cid title documentId extraFields =+ profileConcept+ cid+ ([("type", String "Decision Record"), ("title", String title), ("docId", String documentId)] <> extraFields)+ ("# " <> title <> "\n")++-- | The end-to-end proof, run against the fixture bundle a reader can also run+-- from the command line. Absence of the three optional keys is silent in both+-- modes; the one genuine recommendation still fails under strict authoring; the+-- conditional requirement in the same type still fires; and every optional key+-- that /is/ present is checked as hard as a required one.+testOptionalFieldsFixture :: IO (Either Text ())+testOptionalFieldsFixture = do+ descriptorPath <- fixtureFilePath "profiles/optional-fields.dhall"+ conditionPath <- fixtureFilePath "profiles/optional-conditional-invalid.dhall"+ collisionPath <- fixtureFilePath "profiles/optional-collision-invalid.dhall"+ loaded <- loadProfileFile descriptorPath+ conditionLoaded <- loadProfileFile conditionPath+ collisionLoaded <- loadProfileFile collisionPath+ root <- fixturePath "profile-optional-fields"+ concepts <- readBundle root+ pure $ do+ spec <- first ("failed to load optional-fields profile: " <>) loaded+ compiled <- firstShow (compileProfile spec)+ conditionSpec <- first ("failed to load invalid optional-condition profile: " <>) conditionLoaded+ collisionSpec <- first ("failed to load invalid optional-collision profile: " <>) collisionLoaded+ assertEqual+ ( Left+ ( OptionalFieldWithCondition (Just "Decision Record") (FieldPath (FieldName "reviews" :| [FieldName "model"]))+ :| [OptionalFieldWithCondition (Just "Decision Record") (fieldPath "supersededBy")]+ )+ )+ (compileProfile conditionSpec)+ assertEqual+ ( Left+ ( ConflictingFieldRequirement Nothing "reviewedBy"+ :| [ConflictingFieldRequirement (Just "Decision Record") "owner"]+ )+ )+ (compileProfile collisionSpec)+ accepted <- parseTestConceptId "decisions/accepted"+ badSupersedes <- parseTestConceptId "decisions/bad-supersedes"+ superseded <- parseTestConceptId "decisions/superseded"+ let valueViolations =+ [ ValueFormatMismatch badSupersedes (fieldPath "decidedAt") Rfc3339Utc (String "not a timestamp"),+ ValueNotInVocabulary badSupersedes (nestedReviewPath 0 "model") ["opus", "sonnet"] (String "gpt"),+ DanglingHandleReference badSupersedes (fieldPath "supersedes") "ADR-99",+ MissingProfileField superseded "supersededBy" (Just (FieldCondition "status" ["superseded"]))+ ]+ assertEqual valueViolations (validateProfile PermissiveConformance compiled concepts)+ assertEqual+ (MissingRecommendedProfileField accepted "reviewedBy" Nothing : valueViolations)+ (validateProfile StrictAuthoring compiled concepts)+ where+ nestedReviewPath elementIndex key =+ FieldPath (FieldName "reviews" :| [ArrayIndex elementIndex, FieldName key])+ testClosedFieldValidation :: Either Text () testClosedFieldValidation = do let ownedRule :: TypeRule ownedRule = withTypeFrontmatter- FrontmatterRules {required = [requiredField "owner"], recommended = []}+ FrontmatterRules {required = [requiredField "owner"], recommended = [], optional = []} (firstTypeRule typeAwareProfileSpec) reviewRule = withTypeName "Review"- (withTypeFrontmatter FrontmatterRules {required = [requiredField "reviewer"], recommended = []} ownedRule)+ (withTypeFrontmatter FrontmatterRules {required = [requiredField "reviewer"], recommended = [], optional = []} ownedRule) closed = typeAwareProfileSpec- { frontmatter = FrontmatterRules {required = [requiredField "type", requiredField "status"], recommended = []},+ { frontmatter = FrontmatterRules {required = [requiredField "type", requiredField "status"], recommended = [], optional = []}, allowUnknownFields = False, idField = Just "requestId", types = [ownedRule, reviewRule]
+ test/fixtures/profile-optional-fields/decisions/accepted.md view
@@ -0,0 +1,13 @@+---+type: Decision Record+description: A live decision that replaces nothing.+timestamp: "2026-07-30T00:00:00Z"+title: Accepted+docId: ADR-1+status: accepted+---++# Accepted++A live decision that replaces nothing, came from no plan, and has no successor.+Every optional key is absent, and only the genuine recommendation is reported.
+ test/fixtures/profile-optional-fields/decisions/bad-supersedes.md view
@@ -0,0 +1,20 @@+---+type: Decision Record+description: A decision whose optional values are wrong.+timestamp: "2026-07-30T02:00:00Z"+title: Bad Supersedes+docId: ADR-3+status: accepted+reviewedBy: Bo+supersedes: ADR-99+decidedAt: not a timestamp+reviews:+ - kind: model+ model: gpt+---++# Bad Supersedes++Optional keys are present but wrong: a dangling handle, a malformed timestamp,+and an out-of-vocabulary nested member. Absence stops being an error without+value checking being switched off.
+ test/fixtures/profile-optional-fields/decisions/superseded.md view
@@ -0,0 +1,14 @@+---+type: Decision Record+description: A decision replaced by a later one.+timestamp: "2026-07-30T03:00:00Z"+title: Superseded+docId: ADR-4+status: superseded+reviewedBy: Ari+---++# Superseded++The conditional requirement bites here: `supersededBy` is required because+`status` is `superseded`, even though optional keys in the same type stay silent.
+ test/fixtures/profile-optional-fields/decisions/superseding.md view
@@ -0,0 +1,21 @@+---+type: Decision Record+description: A decision carrying every optional key.+timestamp: "2026-07-30T01:00:00Z"+title: Superseding+docId: ADR-2+status: accepted+reviewedBy: Ari+supersedes: ADR-1+decidedAt: 2026-07-30T00:00:00Z+originatingPlan: docs/plans/32-add-optional-profile-field-rules.md+reviews:+ - kind: model+ model: opus+ - kind: human+---++# Superseding++Every optional key is present and valid, so this concept reports nothing in+either validation mode.
+ test/fixtures/profile-optional-fields/log.md view
@@ -0,0 +1,4 @@+# Bundle Update Log++## 2026-07-30+* **Add**: Created the optional-field validation fixture.
test/fixtures/profiles/cardinality.dhall view
@@ -17,6 +17,7 @@ , field.scalar "domain" ] , recommended = [] : List FieldRule+ , optional = [] : List FieldRule } , allowUnknownTypes = False , allowUnknownFields = True@@ -27,6 +28,7 @@ , frontmatter = { required = [ field.scalar "score" ] , recommended = [] : List FieldRule+ , optional = [] : List FieldRule } } ]
test/fixtures/profiles/closed-fields.dhall view
@@ -15,6 +15,7 @@ , field.enum "status" [ "proposed", "accepted", "closed" ] ] , recommended = [] : List FieldRule+ , optional = [] : List FieldRule } , allowUnknownTypes = False , allowUnknownFields = False@@ -28,6 +29,7 @@ , field.plain "owner" ] , recommended = [] : List FieldRule+ , optional = [] : List FieldRule } } , TypeRule::{@@ -35,6 +37,7 @@ , frontmatter = { required = [ field.plain "reviewer" ] , recommended = [] : List FieldRule+ , optional = [] : List FieldRule } } ]
test/fixtures/profiles/conditional-fields-invalid.dhall view
@@ -21,5 +21,6 @@ } ] , recommended = [] : List FieldRule.Type+ , optional = [] : List FieldRule.Type } }
test/fixtures/profiles/conditional-fields.dhall view
@@ -29,6 +29,7 @@ } ] , recommended = [] : List FieldRule.Type+ , optional = [] : List FieldRule.Type } } @@ -55,6 +56,7 @@ , when = Some (condition "derivationKind" [ "operational" ]) } ]+ , optional = [] : List FieldRule.Type } } @@ -93,9 +95,11 @@ } ] , recommended = [] : List NestedFieldRule.Type+ , optional = [] : List NestedFieldRule.Type } ] , recommended = [] : List FieldRule.Type+ , optional = [] : List FieldRule.Type } } @@ -105,6 +109,7 @@ , frontmatter = { required = [ okf.mk.FieldRule.plain "type" ] , recommended = [] : List FieldRule.Type+ , optional = [] : List FieldRule.Type } , types = [ decision, postgresql, reviewed ] }
test/fixtures/profiles/decisions.dhall view
@@ -21,6 +21,11 @@ ] , recommended = [ field.documented "status" "One of: proposed, accepted, superseded." ]+ , optional =+ [ field.documented+ "supersedes"+ "The decision this one replaces, when it replaces one."+ ] } , allowUnknownTypes = False , allowUnknownFields = True
+ test/fixtures/profiles/document-references-ep3.dhall view
@@ -0,0 +1,149 @@+--| Frozen reference-aware descriptor generation from MasterPlan 5 EP-3.+-- It deliberately has no `optional` list in either rule record, at either+-- scope, so the compatibility decoder must preserve every other field —+-- including `reference`, `when`, and one level of `elementFields` — while+-- adding an empty optional list at both levels.+--+-- The record types are spelled out rather than imported precisely so this file+-- keeps typechecking after the published schema moves on.+let Cardinality = ../../../dhall/Cardinality.dhall++let FieldFormat = ../../../dhall/FieldFormat.dhall++let FieldCondition = { field : Text, hasValue : List Text }++let HandleReferenceRule =+ { localPrefix : Text+ , externalUriSchemes : List Text+ , allowSelf : Bool+ }++let NestedFieldRule =+ { field : Text+ , description : Optional Text+ , allowedValues : List Text+ , cardinality : Cardinality+ , format : Optional FieldFormat+ , when : Optional FieldCondition+ }++let NestedRules =+ { required : List NestedFieldRule+ , recommended : List NestedFieldRule+ }++let FieldRule =+ { field : Text+ , description : Optional Text+ , allowedValues : List Text+ , cardinality : Cardinality+ , format : Optional FieldFormat+ , elementFields : Optional NestedRules+ , reference : Optional HandleReferenceRule+ , when : Optional FieldCondition+ }++let FrontmatterRules =+ { required : List FieldRule, recommended : List FieldRule }++let TypeRule =+ { type : Text+ , description : Optional Text+ , frontmatter : FrontmatterRules+ , pathPattern : Optional Text+ , resourceScheme : Optional Text+ , requireSchemaSection : Bool+ , schemaColumns : List Text+ , idPrefix : Optional Text+ }++let Profile =+ { name : Text+ , description : Optional Text+ , okfVersion : Text+ , frontmatter : FrontmatterRules+ , allowUnknownTypes : Bool+ , allowUnknownFields : Bool+ , idField : Optional Text+ , types : List TypeRule+ }++let plain =+ \(field : Text) ->+ { field+ , description = None Text+ , allowedValues = [] : List Text+ , cardinality = Cardinality.Any+ , format = None FieldFormat+ , elementFields = None NestedRules+ , reference = None HandleReferenceRule+ , when = None FieldCondition+ }++in { name = "document-references-ep3"+ , description = None Text+ , okfVersion = "0.1"+ , frontmatter =+ { required = [ plain "type", plain "title" ]+ , recommended =+ [ plain "supersedes"+ // { cardinality = Cardinality.Scalar+ , reference = Some+ { localPrefix = "ADR"+ , externalUriSchemes = [ "mori" ]+ , allowSelf = False+ }+ }+ , plain "supersededBy"+ // { cardinality = Cardinality.Scalar+ , when = Some { field = "status", hasValue = [ "superseded" ] }+ }+ , plain "reviews"+ // { cardinality = Cardinality.List+ , elementFields = Some+ { required =+ [ { field = "kind"+ , description = None Text+ , allowedValues = [ "human", "model" ]+ , cardinality = Cardinality.Scalar+ , format = None FieldFormat+ , when = None FieldCondition+ }+ ]+ , recommended =+ [ { field = "notes"+ , description = None Text+ , allowedValues = [] : List Text+ , cardinality = Cardinality.Scalar+ , format = None FieldFormat+ , when = None FieldCondition+ }+ ]+ }+ }+ ]+ }+ , allowUnknownTypes = False+ , allowUnknownFields = True+ , idField = Some "docId"+ , types =+ [ { type = "Decision Record"+ , description = None Text+ , frontmatter =+ { required =+ [ plain "status"+ // { allowedValues = [ "accepted", "superseded" ]+ , cardinality = Cardinality.Scalar+ }+ ]+ , recommended = [] : List FieldRule+ }+ , pathPattern = Some "decisions/*"+ , resourceScheme = None Text+ , requireSchemaSection = False+ , schemaColumns = [] : List Text+ , idPrefix = Some "ADR"+ }+ ]+ }+ : Profile
test/fixtures/profiles/document-references-invalid.dhall view
@@ -37,6 +37,7 @@ , rule "conflict" "ADR" ([] : List Text) ] , recommended = [] : List FieldRule.Type+ , optional = [] : List FieldRule.Type } , allowUnknownTypes = True , allowUnknownFields = True@@ -47,6 +48,7 @@ , frontmatter = { required = [ rule "conflict" "RFC" ([] : List Text) ] , recommended = [] : List FieldRule.Type+ , optional = [] : List FieldRule.Type } , idPrefix = Some "ADR" }
test/fixtures/profiles/document-references.dhall view
@@ -30,6 +30,7 @@ , frontmatter = { required = [ field.plain "type", field.plain "title" ] , recommended = [ references, selfReference ]+ , optional = [] : List FieldRule.Type } , allowUnknownTypes = False , allowUnknownFields = True
test/fixtures/profiles/formats.dhall view
@@ -27,6 +27,7 @@ } ] , recommended = [] : List FieldRule.Type+ , optional = [] : List FieldRule.Type } , allowUnknownTypes = False , allowUnknownFields = True@@ -37,6 +38,7 @@ , frontmatter = { required = [ field.uriWithScheme "homepage" "https" ] , recommended = [] : List FieldRule.Type+ , optional = [] : List FieldRule.Type } } ]
test/fixtures/profiles/nested-reviews.dhall view
@@ -52,9 +52,11 @@ ] , recommended = [ NestedFieldRule::{ field = "notes", cardinality = Cardinality.Scalar } ]+ , optional = [] : List NestedFieldRule.Type } ] , recommended = [] : List FieldRule.Type+ , optional = [] : List FieldRule.Type } , allowUnknownTypes = False , allowUnknownFields = True
+ test/fixtures/profiles/optional-collision-invalid.dhall view
@@ -0,0 +1,33 @@+--| One key classified by two presence lists at one scope. The profile cannot+-- say both "check for this under --strict" and "never check for this", so+-- compileProfile rejects it with ConflictingFieldRequirement rather than picking+-- a winner. `reviewedBy` collides at profile scope; `owner` collides at type+-- scope.+let okf = ../../../dhall/package.dhall++let Profile = okf.defaults.Profile++let TypeRule = okf.defaults.TypeRule++let FieldRule = okf.defaults.FieldRule++let field = okf.mk.FieldRule++in Profile::{+ , name = "invalid-optional-collisions"+ , frontmatter =+ { required = [ field.plain "type" ]+ , recommended = [ field.plain "reviewedBy" ]+ , optional = [ field.plain "reviewedBy" ]+ }+ , types =+ [ TypeRule::{+ , type = "Decision Record"+ , frontmatter =+ { required = [ field.plain "owner" ]+ , recommended = [] : List FieldRule.Type+ , optional = [ field.plain "owner" ]+ }+ }+ ]+ }
+ test/fixtures/profiles/optional-conditional-invalid.dhall view
@@ -0,0 +1,68 @@+--| A condition on an optional rule is dead: `when` gates a presence clause, and+-- an optional rule has none. Dhall accepts the shape; compileProfile rejects it+-- with OptionalFieldWithCondition before any concept is read.+--+-- The nested declaration is rejected for the same reason at `reviews.model`.+let okf = ../../../dhall/package.dhall++let Profile = okf.defaults.Profile++let TypeRule = okf.defaults.TypeRule++let FieldRule = okf.defaults.FieldRule++let NestedFieldRule = okf.defaults.NestedFieldRule++let Cardinality = okf.Cardinality++in Profile::{+ , name = "invalid-optional-conditions"+ , frontmatter =+ { required = [ okf.mk.FieldRule.plain "type" ]+ , recommended = [] : List FieldRule.Type+ , optional = [] : List FieldRule.Type+ }+ , types =+ [ TypeRule::{+ , type = "Decision Record"+ , frontmatter =+ { required =+ [ FieldRule::{+ , field = "status"+ , allowedValues = [ "accepted", "superseded" ]+ , cardinality = Cardinality.Scalar+ }+ , FieldRule::{+ , field = "reviews"+ , cardinality = Cardinality.List+ , elementFields = Some+ { required =+ [ NestedFieldRule::{+ , field = "kind"+ , allowedValues = [ "human", "model" ]+ , cardinality = Cardinality.Scalar+ }+ ]+ , recommended = [] : List NestedFieldRule.Type+ , optional =+ [ NestedFieldRule::{+ , field = "model"+ , cardinality = Cardinality.Scalar+ , when = Some { field = "kind", hasValue = [ "model" ] }+ }+ ]+ }+ }+ ]+ , recommended = [] : List FieldRule.Type+ , optional =+ [ FieldRule::{+ , field = "supersededBy"+ , cardinality = Cardinality.Scalar+ , when = Some { field = "status", hasValue = [ "superseded" ] }+ }+ ]+ }+ }+ ]+ }
+ test/fixtures/profiles/optional-fields.dhall view
@@ -0,0 +1,109 @@+--| Exercises the third presence classification end to end.+--+-- `supersedes` and `originatingPlan` are lifecycle and provenance metadata: a+-- decision that replaces nothing has no `supersedes` value to give, and not+-- every decision came from a plan. They are declared, fully constrained, and+-- never reported when absent. `reviewedBy` is a genuine authoring+-- recommendation, so `--strict` must still demand it. `supersededBy` shows the+-- IR-5 coexistence: a conditional *requirement* in the same type as the optional+-- declarations, required only once `status` says the decision was superseded.+--+-- `allowUnknownFields = False` closes the vocabulary, so this fixture also+-- proves an optional key counts as declared.+let okf = ../../../dhall/package.dhall++let Profile = okf.defaults.Profile++let TypeRule = okf.defaults.TypeRule++let FieldRule = okf.defaults.FieldRule++let NestedFieldRule = okf.defaults.NestedFieldRule++let Cardinality = okf.Cardinality++let FieldFormat = okf.FieldFormat++let HandleReferenceRule = okf.defaults.HandleReferenceRule++let field = okf.mk.FieldRule++let decision =+ TypeRule::{+ , type = "Decision Record"+ , pathPattern = Some "decisions/*"+ , idPrefix = Some "ADR"+ , frontmatter =+ { required =+ [ FieldRule::{+ , field = "status"+ , description = Some "Where the decision is in its lifecycle."+ , allowedValues = [ "accepted", "superseded" ]+ , cardinality = Cardinality.Scalar+ }+ , FieldRule::{+ , field = "supersededBy"+ , description = Some "The decision that replaced this one."+ , cardinality = Cardinality.Scalar+ , reference = Some HandleReferenceRule::{ localPrefix = "ADR" }+ , when = Some { field = "status", hasValue = [ "superseded" ] }+ }+ ]+ , recommended =+ [ field.documented "reviewedBy" "Who signed off on the decision." ]+ , optional =+ [ FieldRule::{+ , field = "supersedes"+ , description = Some "The decision this one replaces, if any."+ , cardinality = Cardinality.Scalar+ , reference = Some HandleReferenceRule::{ localPrefix = "ADR" }+ }+ , FieldRule::{+ , field = "decidedAt"+ , description = Some "When the decision was accepted."+ , format = Some FieldFormat.Rfc3339Utc+ }+ , FieldRule::{+ , field = "reviews"+ , description = Some "Structured review records, when any were made."+ , cardinality = Cardinality.List+ , elementFields = Some+ { required =+ [ NestedFieldRule::{+ , field = "kind"+ , allowedValues = [ "human", "model" ]+ , cardinality = Cardinality.Scalar+ }+ ]+ , recommended = [] : List NestedFieldRule.Type+ , optional =+ [ NestedFieldRule::{+ , field = "model"+ , allowedValues = [ "opus", "sonnet" ]+ , cardinality = Cardinality.Scalar+ }+ ]+ }+ }+ ]+ }+ }++in Profile::{+ , name = "optional-fields"+ , description = Some+ "Distinguishes optional lifecycle metadata from authoring recommendations."+ , allowUnknownTypes = False+ , allowUnknownFields = False+ , idField = Some "docId"+ , frontmatter =+ { required = [ field.plain "type", field.plain "title" ]+ , recommended = [] : List FieldRule.Type+ , optional =+ [ field.documented+ "originatingPlan"+ "The plan that produced this decision, when one did."+ ]+ }+ , types = [ decision ]+ }
test/fixtures/profiles/postgresql.dhall view
@@ -43,6 +43,7 @@ , format = Some (FieldFormat.UriWithScheme "postgresql") } ]+ , optional = [] : List FieldRule.Type } , allowUnknownTypes = False , allowUnknownFields = True@@ -50,7 +51,11 @@ , types = [ { type = "PostgreSQL Schema" , description = Some "One namespace grouping tables and views."- , frontmatter = { required = [] : List FieldRule.Type, recommended = [] : List FieldRule.Type }+ , frontmatter =+ { required = [] : List FieldRule.Type+ , recommended = [] : List FieldRule.Type+ , optional = [] : List FieldRule.Type+ } , pathPattern = Some "schemas/*" , resourceScheme = Some "postgresql" , requireSchemaSection = False@@ -60,7 +65,11 @@ , { type = "PostgreSQL Table" , description = Some "One physical table in a schema, including its column list."- , frontmatter = { required = [] : List FieldRule.Type, recommended = [] : List FieldRule.Type }+ , frontmatter =+ { required = [] : List FieldRule.Type+ , recommended = [] : List FieldRule.Type+ , optional = [] : List FieldRule.Type+ } , pathPattern = Some "schemas/*/tables/*" , resourceScheme = Some "postgresql" , requireSchemaSection = True@@ -69,7 +78,11 @@ } , { type = "PostgreSQL View" , description = Some "One view, including the columns it projects."- , frontmatter = { required = [] : List FieldRule.Type, recommended = [] : List FieldRule.Type }+ , frontmatter =+ { required = [] : List FieldRule.Type+ , recommended = [] : List FieldRule.Type+ , optional = [] : List FieldRule.Type+ } , pathPattern = Some "schemas/*/views/*" , resourceScheme = Some "postgresql" , requireSchemaSection = True
test/fixtures/profiles/type-frontmatter.dhall view
@@ -15,6 +15,7 @@ , field.documented "title" "Human-readable concept title." ] , recommended = [] : List FieldRule+ , optional = [] : List FieldRule } , allowUnknownTypes = False , allowUnknownFields = True@@ -27,6 +28,7 @@ [ field.documented "owner" "Person responsible for the concept." ] , recommended = [ field.documented "reviewer" "Person who independently reviewed it." ]+ , optional = [] : List FieldRule } } , TypeRule::{ type = "Open Concept" }