packages feed

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 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",