okf-cli 0.3.0.0 → 0.4.0.0
raw patch · 5 files changed
+142/−39 lines, 5 filesdep ~okf-corePVP ok
version bump matches the API change (PVP)
Dependency ranges changed: okf-core
API changes (from Hackage documentation)
Files
- CHANGELOG.md +19/−0
- help/profiles.md +47/−17
- okf-cli.cabal +3/−3
- src/Okf/Cli.hs +25/−12
- test/Main.hs +48/−7
CHANGELOG.md view
@@ -7,6 +7,25 @@ ## [Unreleased] +## [0.4.0.0] - 2026-07-30++### Added++- `okf profile show` prints `frontmatter.optional` at profile scope, under each+ type rule, and inside `elementFields`, using `(none)` when empty, so all three+ presence lists always print in the same order. Profile JSON emits the matching+ `optional` key in both rule records.+- `okf validate` renders the new `OptionalFieldWithCondition` profile-definition+ error, and the `ConflictingFieldRequirement` message now names all three+ presence lists. The profiles help topic gains an OPTIONAL FIELDS section and+ documents when to prefer `optional` over `recommended`.++### Changed++- Requires `okf-core ^>=0.4.0.0`. That release adds the `optional` field to+ `FrontmatterRules` and `NestedRules`, so a descriptor annotated against okf's+ current `Profile.dhall` by relative path must declare the third list.+ ## [0.3.0.0] - 2026-07-29 ### Added
help/profiles.md view
@@ -21,7 +21,9 @@ exit). --strict Also check profile `recommended` fields. Required- profile fields are checked in both modes.+ profile fields are checked in both modes. Profile+ `optional` fields are never reported when absent, in+ either mode. EXIT CODES @@ -64,7 +66,8 @@ DESCRIPTIONS A profile may document itself: one description for the profile as a whole,- one per required or recommended frontmatter key, and one per type rule.+ one per required, recommended, or optional frontmatter key, and one per type+ rule. Descriptions are prose for humans -- okf never checks one against a bundle and none can produce a deviation. @@ -83,20 +86,22 @@ TYPE-AWARE FRONTMATTER - Each type rule may add its own required and recommended frontmatter fields.- Profile-wide rules apply to every concept; a matching type rule adds to them.- Value constraints merge by key, while presence declarations remain separate;- an applicable required clause wins over a strict recommendation. Unknown- types still receive profile-wide rules.+ Each type rule may add its own required, recommended, and optional+ frontmatter fields. Profile-wide rules apply to every concept; a matching type+ rule adds to them. Value constraints merge by key, while presence declarations+ remain separate; an applicable required clause wins over a strict+ recommendation. Declaring a key optional at one scope does not cancel a+ presence clause the other scope declared. Unknown types still receive+ profile-wide rules. - `okf profile show` prints `frontmatter.required` and- `frontmatter.recommended` beneath each type. New descriptors should use+ `okf profile show` prints `frontmatter.required`, `frontmatter.recommended`,+ and `frontmatter.optional` beneath each type. New descriptors should use `okf.defaults.TypeRule::{ ... }`, whose default supplies empty lists. Before validating a bundle, okf rejects duplicate type rules, repeated keys- in one list, and keys placed in both required and recommended at the same- scope. These are hard profile-definition errors regardless of- `--profile-enforce`.+ in one list, and keys placed in more than one of required, recommended, and+ optional at the same scope. These are hard profile-definition errors+ regardless of `--profile-enforce`. VALUE VOCABULARIES AND CLOSED FIELDS @@ -162,9 +167,9 @@ NESTED RECORD FIELDS - A top-level FieldRule may set elementFields to required and recommended rules- for every record in a list. The public schema is intentionally bounded to one- level: NestedFieldRule has vocabulary, cardinality, and format constraints but+ A top-level FieldRule may set elementFields to required, recommended, and+ optional rules for every record in a list. The public schema is intentionally+ bounded to one level: NestedFieldRule has vocabulary, cardinality, and format constraints but cannot contain another elementFields value. Use field.recordList with NestedFieldRule constructors or record completion.@@ -173,8 +178,9 @@ merge by sibling key just like top-level rules. Each list element must be a record. Required nested keys are always checked;- recommended nested keys only under --strict. Present nested values are checked- in both modes, and diagnostics identify the exact index:+ recommended nested keys only under --strict; optional nested keys never.+ Present nested values are checked in both modes, and diagnostics identify the+ exact index: profile: requests/example: missing profile-required field: reviews[2].outcome profile: requests/example: frontmatter element at reviews[1] must be a record, found: "not-a-record"@@ -206,6 +212,30 @@ Recommended conditions are evaluated only under --strict. profile: decisions/old: missing profile-required field: supersededBy (when status is superseded)++ `when` is rejected on an optional rule: a condition gates a presence clause,+ and an optional rule has none, so the pairing would be silently dead.++OPTIONAL FIELDS++ FrontmatterRules and NestedRules carry a third presence list beside required+ and recommended. An optional field is known to the profile, validated whenever+ present, and never reported when absent -- in permissive and strict modes+ alike. Use it for lifecycle or provenance metadata whose absence is ordinary+ rather than deficient, such as an architecture decision's supersedes.++ , optional = [ field.documented "supersedes" "The decision this replaces." ]++ Optional keys count as declared under allowUnknownFields = False, appear in+ `okf profile show`, and carry their prose into value diagnostics. Every+ vocabulary, cardinality, format, reference, and nested-shape check still runs+ on a present value.++ A field that becomes required under a condition belongs in required with+ `when = Some ...`, not in optional; the two coexist in one scope.++ profile: decisions/accepted: missing profile-recommended field: reviewedBy+ profile: decisions/bad: supersedes references ADR-99, which does not exist in this bundle DOCUMENT REFERENCES
okf-cli.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.4 name: okf-cli-version: 0.3.0.0+version: 0.4.0.0 synopsis: Command-line interface for Open Knowledge Format bundles description: okf-cli provides the @okf@ executable for working with Open Knowledge Format@@ -66,7 +66,7 @@ , generic-lens >=2.2 && <2.4 , githash ^>=0.1 , lens ^>=5.3- , okf-core ^>=0.3.0.0+ , okf-core ^>=0.4.0.0 , optparse-applicative >=0.18 && <0.20 , process >=1.6 && <1.7 , text ^>=2.1@@ -82,7 +82,7 @@ , directory , filepath , okf-cli- , okf-core ^>=0.3.0.0+ , okf-core ^>=0.4.0.0 , optparse-applicative >=0.18 , temporary , text ^>=2.1
src/Okf/Cli.hs view
@@ -64,9 +64,11 @@ FieldFormat (..), FieldPath (..), FieldPathSegment (..),- FrontmatterRules (..),+ -- 'FrontmatterRules' and 'NestedRules' are deliberately absent: importing+ -- their field selectors would make @optional@ ambiguous against+ -- @optparse-applicative@'s, so this module reads all three presence lists+ -- through generic-lens labels instead. HandleReferenceRule (..),- NestedRules (..), ProfileDefinitionError (..), ProfileSpec (..), ProfileViolation (..),@@ -670,7 +672,7 @@ { name, description, okfVersion,- frontmatter = FrontmatterRules {required, recommended},+ frontmatter, allowUnknownTypes, allowUnknownFields, idField,@@ -684,10 +686,17 @@ "allowUnknownFields: " <> renderFlag allowUnknownFields, "idField: " <> renderOptional idField ]- <> renderFieldRules "" "frontmatter.required" required- <> renderFieldRules "" "frontmatter.recommended" recommended+ <> renderPresenceLists "" frontmatter <> concatMap renderTypeRule typeRules where+ -- The three presence lists always print together and in the same order, at+ -- profile scope and under every type rule, so the effective policy for one+ -- key is readable in one place.+ renderPresenceLists indent rules =+ renderFieldRules indent "frontmatter.required" (rules ^. #required)+ <> renderFieldRules indent "frontmatter.recommended" (rules ^. #recommended)+ <> renderFieldRules indent "frontmatter.optional" (rules ^. #optional)+ -- A field's prose cannot share a comma-joined line with its neighbours, so -- a non-empty list becomes a headed block. An empty list keeps the -- single-line @(none)@ form the other optional fields use.@@ -706,10 +715,11 @@ ] <> case rule ^. #elementFields of Nothing -> [indent <> " elementFields: (none)"]- Just NestedRules {required = nestedRequired, recommended = nestedRecommended} ->+ Just nestedRules -> [indent <> " elementFields:"]- <> renderNestedFieldRules (indent <> " ") "required" nestedRequired- <> renderNestedFieldRules (indent <> " ") "recommended" nestedRecommended+ <> renderNestedFieldRules (indent <> " ") "required" (nestedRules ^. #required)+ <> renderNestedFieldRules (indent <> " ") "recommended" (nestedRules ^. #recommended)+ <> renderNestedFieldRules (indent <> " ") "optional" (nestedRules ^. #optional) renderNestedFieldRules indent label [] = [indent <> label <> ": " <> renderList []] renderNestedFieldRules indent label rules =@@ -727,7 +737,7 @@ TypeRule { type_ = ruleType, description = ruleDescription,- frontmatter = FrontmatterRules {required = typeRequired, recommended = typeRecommended},+ frontmatter = typeFrontmatter, pathPattern, resourceScheme, requireSchemaSection,@@ -738,8 +748,7 @@ "type: " <> ruleType, " description: " <> renderOptional ruleDescription ]- <> renderFieldRules " " "frontmatter.required" typeRequired- <> renderFieldRules " " "frontmatter.recommended" typeRecommended+ <> renderPresenceLists " " typeFrontmatter <> [ " pathPattern: " <> renderOptional pathPattern, " resourceScheme: " <> renderOptional resourceScheme, " requireSchemaSection: " <> renderFlag requireSchemaSection,@@ -1286,7 +1295,7 @@ DuplicateFieldRule scope listName key -> renderScope scope <> ": duplicate " <> listName <> " field: " <> key ConflictingFieldRequirement scope key ->- renderScope scope <> ": field appears in required and recommended: " <> key+ renderScope scope <> ": field appears in more than one of required, recommended, and optional: " <> key UnsatisfiableVocabulary scope key profileValues typeValues -> renderScope scope <> ": disjoint allowed values for "@@ -1373,6 +1382,10 @@ <> renderFieldPath target <> " cannot also declare format " <> renderFieldFormat fieldFormat+ OptionalFieldWithCondition scope target ->+ renderScope scope+ <> ": optional field cannot carry a when condition: "+ <> renderFieldPath target where renderScope Nothing = "profile frontmatter" renderScope (Just ctype) = "type " <> ctype <> " frontmatter"
test/Main.hs view
@@ -269,7 +269,8 @@ frontmatter = FrontmatterRules { required = [undocumentedField "type", undocumentedField "title"],- recommended = []+ recommended = [],+ optional = [] }, allowUnknownTypes = False, allowUnknownFields = True,@@ -278,7 +279,7 @@ [ TypeRule { type_ = "PostgreSQL Table", description = Nothing,- frontmatter = FrontmatterRules {required = [], recommended = []},+ frontmatter = FrontmatterRules {required = [], recommended = [], optional = []}, pathPattern = Just "schemas/*/tables/*", resourceScheme = Just "postgresql", requireSchemaSection = True,@@ -309,7 +310,19 @@ }, undocumentedField "title" ],- recommended = []+ recommended = [],+ optional =+ [ FieldRule+ { field = "originatingPlan",+ description = Just "The plan that produced this decision, when one did.",+ allowedValues = [],+ cardinality = Scalar,+ format = Nothing,+ elementFields = Nothing,+ reference = Nothing,+ when = Nothing+ }+ ] }, allowUnknownTypes = False, allowUnknownFields = True,@@ -321,7 +334,8 @@ frontmatter = FrontmatterRules { required = [FieldRule "owner" (Just "Person responsible for the decision.") [] Scalar (Just (DocumentHandle "USR")) Nothing Nothing Nothing],- recommended = [FieldRule "reviewer" Nothing ["Ari", "Bo"] List Nothing Nothing (Just (HandleReferenceRule "ADR" ["mori"] False)) Nothing]+ recommended = [FieldRule "reviewer" Nothing ["Ari", "Bo"] List Nothing Nothing (Just (HandleReferenceRule "ADR" ["mori"] False)) Nothing],+ optional = [FieldRule "supersedes" Nothing [] Scalar Nothing Nothing (Just (HandleReferenceRule "ADR" [] False)) Nothing] }, pathPattern = Just "decisions/*", resourceScheme = Nothing,@@ -353,13 +367,15 @@ ( Just NestedRules { required = [NestedFieldRule "outcome" Nothing ["approved", "rejected"] Any Nothing (Just (FieldCondition "kind" ["model"]))],- recommended = [NestedFieldRule "notes" Nothing [] Scalar Nothing Nothing]+ recommended = [NestedFieldRule "notes" Nothing [] Scalar Nothing Nothing],+ optional = [NestedFieldRule "model" Nothing [] Scalar Nothing Nothing] } ) Nothing Nothing ],- recommended = []+ recommended = [],+ optional = [] }, allowUnknownTypes = True, allowUnknownFields = True,@@ -396,7 +412,14 @@ " cardinality: scalar", " format: (none)", " when: (none)",- "frontmatter.recommended: (none)"+ " optional:",+ " - model: (none)",+ " allowedValues: (any)",+ " cardinality: scalar",+ " format: (none)",+ " when: (none)",+ "frontmatter.recommended: (none)",+ "frontmatter.optional: (none)" ] -- | Every optional field prints, as @(none)@ when absent, so the shape does not@@ -427,6 +450,14 @@ " when: (none)", " elementFields: (none)", "frontmatter.recommended: (none)",+ "frontmatter.optional:",+ " - originatingPlan: The plan that produced this decision, when one did.",+ " allowedValues: (any)",+ " cardinality: scalar",+ " format: (none)",+ " reference: (none)",+ " when: (none)",+ " elementFields: (none)", "", "type: Decision Record", " description: One accepted decision, never edited after acceptance.",@@ -446,6 +477,14 @@ " reference: local-prefix(ADR), external-uri-schemes([mori]), allow-self(false)", " when: (none)", " elementFields: (none)",+ " frontmatter.optional:",+ " - supersedes: (none)",+ " allowedValues: (any)",+ " cardinality: scalar",+ " format: (none)",+ " reference: local-prefix(ADR), external-uri-schemes([]), allow-self(false)",+ " when: (none)",+ " elementFields: (none)", " pathPattern: decisions/*", " resourceScheme: (none)", " requireSchemaSection: false",@@ -481,11 +520,13 @@ " when: (none)", " elementFields: (none)", "frontmatter.recommended: (none)",+ "frontmatter.optional: (none)", "", "type: PostgreSQL Table", " description: (none)", " frontmatter.required: (none)", " frontmatter.recommended: (none)",+ " frontmatter.optional: (none)", " pathPattern: schemas/*/tables/*", " resourceScheme: postgresql", " requireSchemaSection: true",