packages feed

okf-cli 0.7.0.0 → 0.8.0.0

raw patch · 8 files changed

+229/−38 lines, 8 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,22 @@  ## [Unreleased] +## [0.8.0.0] - 2026-08-19++### Added++- `okf validate --profile` renders actionable diagnostics for prohibited local+  handles, external URI pattern mismatches, and duplicate record-list member+  values with every zero-based element index.++### Changed++- **Breaking:** requires the coordinated `okf-core` release that adds nested+  reference policies and record-list uniqueness.+- `okf profile show` includes `allow-local`, `external-uri-pattern`, nested+  `reference`, and `uniqueBy`; generated profile documentation exposes the same+  effective constraints.+ ## [0.7.0.0] - 2026-08-18  ### Added
help/bundles.md view
@@ -48,4 +48,5 @@ SEE ALSO    okf help interactive   Choosing an omitted BUNDLE with fzf.+  okf help profiles      The same discovery, for profile descriptors.   okf help ids           The strict document-handle grammar.
help/config.md view
@@ -27,6 +27,11 @@     4. ~/.okf/config.dhall     5. built-in defaults +  profiles.registries is the file-level layer only. A repeatable --registry+  flag, then OKF_PROFILE_REGISTRIES holding a JSON array, then the legacy+  singular OKF_PROFILE_REGISTRY each replace it before the file is consulted.+  Run 'okf profile sources' to see which layer won and why.+   For agent.*, the project file and the global file are both read, and each   setting is resolved separately across them, highest first: 
help/okf.md view
@@ -57,7 +57,9 @@   computations  List the attested computations a bundle declares.   concepts      List the concepts a bundle holds, with optional filters.   id            Allocate and list profile-declared document IDs.-  profile       List and inspect profiles published by a registry.+  profile       List and inspect profiles from registries and local+                descriptors.+  profiles      List local profile descriptor paths without opening a menu.   config        Show and manage okf configuration.   kit           Install and manage agent skills and subagents.   assist        Launch an interactive agent session with installed okf skills.
help/profiles.md view
@@ -317,8 +317,9 @@    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.+  bounded to one level: NestedFieldRule has vocabulary, cardinality, format,+  reference, path, and conditional-presence constraints but cannot contain+  elementFields, objectFields, or uniqueBy.    Use field.recordList with NestedFieldRule constructors or record completion.   Declaring elementFields implies list cardinality; combining it with Scalar is@@ -336,6 +337,27 @@   Extra keys inside a record remain allowed. Nested field-name closure and a   second nested level are not part of this schema. +RECORD-LIST UNIQUENESS++  A FieldRule with elementFields may set uniqueBy to the name of one nested+  member. The member must be declared, unconditionally required, and effectively+  Scalar. Missing, optional, conditionally required, Any, List, and object-only+  keys are hard profile-definition errors.++    field.recordList "acceptanceCriteria" acceptanceCriteriaRules+      // { uniqueBy = Some "id" }++  Values are compared only within that one list on that one concept. Reusing+  AC-1 in another concept is valid. Each duplicated value produces one+  diagnostic with all zero-based element indices, in first-occurrence order:++    profile: requests/duplicate: duplicate value "AC-1" for acceptanceCriteria.id at element indices [0, 1]++  Missing, blank, list, object, and null member values retain their normal+  member diagnostic and are skipped by the duplicate pass. Across profile and+  type scopes, None is the merge identity and equal keys merge; different keys+  conflict.+ CONDITIONAL FIELD PRESENCE    FieldRule and NestedFieldRule may set `when = Some { field, hasValue }` so a@@ -387,35 +409,48 @@  DOCUMENT REFERENCES -  A top-level FieldRule may set reference to a local handle prefix, a list of-  allowed external URI schemes, and an allowSelf policy. The constructors cover-  local-only and explicit external alternatives:+  FieldRule and NestedFieldRule may set reference to a local handle prefix, an+  allowLocal policy, a list of allowed external URI schemes, an optional+  externalUriPattern, and an allowSelf policy. The constructors cover local-only+  and explicit external alternatives:      field.localReference "supersedes" "ADR"     field.localOrExternalReference "supersededBy" "ADR" [ "mori" ] -  The helpers default allowSelf to False. Use-  okf.defaults.HandleReferenceRule record completion to override it.+  The helpers default allowSelf to False, allowLocal to True, and+  externalUriPattern to None. Use okf.defaults.HandleReferenceRule record+  completion to override them.    A canonical handle is checked first. A handle with another prefix is a-  category error; one with the declared prefix must belong to a valid,-  profile-governed concept in this bundle. Duplicate owners still produce the-  existing duplicate-ID deviation but count as present, avoiding a false-  dangling-reference message. Lists are checked element-wise with indexed paths.+  category error. One with the declared prefix is rejected when allowLocal is+  False; otherwise it must belong to a valid, profile-governed concept in this+  bundle. Duplicate owners still produce the existing duplicate-ID deviation+  but count as present, avoiding a false dangling-reference message. Lists are+  checked element-wise with indexed paths.      profile: decisions/current: supersedes[1] references ADR-99, which does not exist in this bundle -  Text that is not a handle must be an absolute URI whose scheme is listed by-  the policy. Scheme comparison is case-insensitive. okf checks syntax and the-  scheme offline; it never resolves an external URI or consults Mori, a registry,-  DNS, or the network.+  Text that is not a handle passes three external checks in order: it must be a+  valid absolute URI; its case-folded scheme must be listed; and, when present,+  externalUriPattern must match the complete original text. The pattern uses+  POSIX extended regular-expression syntax and is a whole-value match, not a+  substring search. Write .* explicitly when surrounding text is intended.+  Invalid patterns are rejected when the profile compiles. +  These are offline syntax checks. okf never resolves the URI, checks whether+  its target exists, or consults Mori, a registry, DNS, or the network.+   The local prefix must use document-handle grammar, be declared by at least one   type idPrefix, and have a profile idField. URI schemes must use RFC 3986 scheme   grammar. A reference field cannot also declare format. Matching profile/type-  policies must use the same local prefix; their external schemes intersect and-  self-reference is allowed only when both permit it. Invalid combinations are-  hard profile-definition errors before any bundle is read.+  policies must use the same local prefix; their external schemes intersect;+  allowLocal and allowSelf combine with logical AND; None is the pattern merge+  identity; equal patterns merge; and unequal patterns conflict. Invalid+  combinations are hard profile-definition errors before any bundle is read.++  Compatibility defaults are inert: older handle policies receive+  allowLocal=True and externalUriPattern=None; older nested rules receive+  reference=None; and older top-level rules receive uniqueBy=None.    A registry reference may be a path to a Dhall file, a directory holding   package.dhall, or a Dhall expression such as a hash-pinned URL. `okf profile
okf-cli.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.4 name:               okf-cli-version:            0.7.0.0+version:            0.8.0.0 synopsis:           Command-line interface for Open Knowledge Format bundles description:   okf-cli provides the @okf@ executable for working with Open Knowledge Format@@ -71,7 +71,7 @@     , generic-lens          >=2.2      && <2.4     , githash               ^>=0.1     , lens                  ^>=5.3-    , okf-core              ^>=0.7.0.0+    , okf-core              ^>=0.8.0.0     , optparse-applicative  >=0.18     && <0.20     , process               >=1.6      && <1.7     , text                  ^>=2.1@@ -88,7 +88,7 @@     , directory     , filepath     , okf-cli-    , okf-core              ^>=0.7.0.0+    , okf-core              ^>=0.8.0.0     , optparse-applicative  >=0.18     , temporary     , text                  ^>=2.1
src/Okf/Cli.hs view
@@ -1904,7 +1904,8 @@           indent <> "    format: " <> maybe "(none)" renderFieldFormat (rule ^. #format),           indent <> "    reference: " <> maybe "(none)" renderHandleReferenceRule (rule ^. #reference),           indent <> "    path: " <> maybe "(none)" renderPathReferenceRule (rule ^. #path),-          indent <> "    when: " <> maybe "(none)" renderCondition (rule ^. #when)+          indent <> "    when: " <> maybe "(none)" renderCondition (rule ^. #when),+          indent <> "    uniqueBy: " <> renderOptional (rule ^. #uniqueBy)         ]           -- The two nested shapes print together and in this order, matching           -- 'Okf.Profile.Documentation.renderFieldRule', which is the other@@ -1933,6 +1934,7 @@           indent <> "    allowedValues: " <> renderVocabulary (rule ^. #allowedValues),           indent <> "    cardinality: " <> renderCardinality (rule ^. #cardinality),           indent <> "    format: " <> maybe "(none)" renderFieldFormat (rule ^. #format),+          indent <> "    reference: " <> maybe "(none)" renderHandleReferenceRule (rule ^. #reference),           indent <> "    path: " <> maybe "(none)" renderPathReferenceRule (rule ^. #path),           indent <> "    when: " <> maybe "(none)" renderCondition (rule ^. #when)         ]@@ -2984,6 +2986,20 @@       <> actualScheme       <> ", allowed schemes: "       <> renderList allowedSchemes+  LocalDocumentReferenceNotAllowed cid fieldPath rawReference ->+    renderConceptId cid+      <> ": local document reference at "+      <> renderFieldPath fieldPath+      <> " is not allowed: "+      <> rawReference+  ExternalReferencePatternMismatch cid fieldPath rawReference patternText ->+    renderConceptId cid+      <> ": external reference at "+      <> renderFieldPath fieldPath+      <> " does not match whole-value pattern "+      <> patternText+      <> ": "+      <> rawReference   SelfDocumentReference cid fieldPath handle ->     renderConceptId cid       <> ": self reference at "@@ -3021,6 +3037,15 @@       <> renderFieldPath fieldPath       <> " must be a record, found: "       <> renderJsonValue actual+  DuplicateNestedFieldValue cid fieldPath duplicateValue elementIndices ->+    renderConceptId cid+      <> ": duplicate value "+      <> renderJsonValue duplicateValue+      <> " for "+      <> renderFieldPath fieldPath+      <> " at element indices ["+      <> Text.intercalate ", " (map (Text.pack . show) (toList elementIndices))+      <> "]"   PathPatternMismatch cid ctype patternText ->     renderConceptId cid <> ": " <> ctype <> " must match path pattern: " <> patternText   MissingResource cid ctype scheme ->@@ -3196,6 +3221,52 @@       <> ": path at "       <> renderFieldPath target       <> " cannot also declare a document reference; a value is resolved as one or the other"+  InvalidExternalUriPattern scope target patternText detail ->+    renderScope scope+      <> ": invalid external URI pattern at "+      <> renderFieldPath target+      <> ": "+      <> patternText+      <> " ("+      <> detail+      <> ")"+  ConflictingExternalUriPatterns ctype target profilePattern typePattern ->+    "type "+      <> ctype+      <> " frontmatter: conflicting external URI patterns for "+      <> renderFieldPath target+      <> " (profile: "+      <> profilePattern+      <> ", type: "+      <> typePattern+      <> ")"+  UniqueByRequiresElementFields scope target key ->+    renderScope scope+      <> ": uniqueBy at "+      <> renderFieldPath target+      <> " names "+      <> key+      <> " but the field has no elementFields"+  UniqueByFieldNotDeclared scope target ->+    renderScope scope <> ": uniqueBy field is not declared at " <> renderFieldPath target+  UniqueByFieldNotUnconditionallyRequired scope target ->+    renderScope scope <> ": uniqueBy field must be unconditionally required at " <> renderFieldPath target+  UniqueByFieldNotScalar scope target actualCardinality ->+    renderScope scope+      <> ": uniqueBy field must be scalar at "+      <> renderFieldPath target+      <> ", found: "+      <> renderCardinality actualCardinality+  ConflictingUniqueBy ctype target profileKey typeKey ->+    "type "+      <> ctype+      <> " frontmatter: conflicting uniqueBy keys for "+      <> renderFieldPath target+      <> " (profile: "+      <> profileKey+      <> ", type: "+      <> typeKey+      <> ")"   -- Each version message names *both* halves of the contradiction. One that said   -- only "field requires OKF 0.2" would leave the author hunting for where the   -- version is declared, which is the other end of the file.@@ -3261,13 +3332,17 @@   Boolean -> "boolean"  renderHandleReferenceRule :: HandleReferenceRule -> Text-renderHandleReferenceRule HandleReferenceRule {localPrefix, externalUriSchemes, allowSelf} =+renderHandleReferenceRule HandleReferenceRule {localPrefix, externalUriSchemes, allowSelf, allowLocal, externalUriPattern} =   "local-prefix("     <> localPrefix     <> "), external-uri-schemes("     <> renderList externalUriSchemes     <> "), allow-self("     <> (if allowSelf then "true" else "false")+    <> "), allow-local("+    <> (if allowLocal then "true" else "false")+    <> "), external-uri-pattern("+    <> fromMaybe "(none)" externalUriPattern     <> ")"   where     renderList xs = "[" <> Text.intercalate ", " xs <> "]"
test/Main.hs view
@@ -580,6 +580,7 @@           profileDocStrictWithTimestamp,           conceptMenuOrdering,           nonAsciiDiagnostics,+          testNestedReferenceDiagnosticRendering,           configDefaults,           configProjectPrecedence,           configEnvPrecedence,@@ -738,6 +739,29 @@ expectedVocabularyLine =   "places/tokyo: frontmatter value at prefecture must be one of [東京都, 京都府], found: \"東京\"" +testNestedReferenceDiagnosticRendering :: Bool+testNestedReferenceDiagnosticRendering =+  case (compileProfile samplePostgresqlProfile, parseConceptId "requests/duplicate") of+    (Right compiled, Right cid) ->+      let referencePath = FieldPath (FieldName "dependencies" :| [ArrayIndex 0, FieldName "ref"])+          uniquePath = FieldPath (FieldName "acceptanceCriteria" :| [FieldName "id"])+          render = renderProfileViolation compiled []+       in [ render (LocalDocumentReferenceNotAllowed cid referencePath "IR-1"),+            render+              ( ExternalReferencePatternMismatch+                  cid+                  referencePath+                  "mori://namespace/project/okf/decisions/concepts/IR-1"+                  "mori://[^/]+/[^/]+/okf/improvement-requests/concepts/IR-[1-9][0-9]*"+              ),+            render (DuplicateNestedFieldValue cid uniquePath (String "AC-1") (0 :| [2]))+          ]+            == [ "requests/duplicate: local document reference at dependencies[0].ref is not allowed: IR-1",+                 "requests/duplicate: external reference at dependencies[0].ref does not match whole-value pattern mori://[^/]+/[^/]+/okf/improvement-requests/concepts/IR-[1-9][0-9]*: mori://namespace/project/okf/decisions/concepts/IR-1",+                 "requests/duplicate: duplicate value \"AC-1\" for acceptanceCriteria.id at element indices [0, 2]"+               ]+    _ -> False+ -- | One root-level entry and one nested entry from differently sized source -- labels, so every positional padder in 'renderRegistryTable' is exercised. sampleRegistryEntries :: [SourcedProfile]@@ -1101,7 +1125,8 @@                     objectFields = Nothing,                     reference = Nothing,                     path = Nothing,-                    when = Nothing+                    when = Nothing,+                    uniqueBy = Nothing                   },                 undocumentedField "title"               ],@@ -1117,7 +1142,8 @@                     objectFields = Nothing,                     reference = Nothing,                     path = Nothing,-                    when = Nothing+                    when = Nothing,+                    uniqueBy = Nothing                   }               ]           },@@ -1131,9 +1157,9 @@               description = Just "One accepted decision, never edited after acceptance.",               frontmatter =                 FrontmatterRules-                  { required = [FieldRule "owner" (Just "Person responsible for the decision.") [] Scalar (Just (DocumentHandle "USR")) Nothing Nothing Nothing Nothing Nothing],-                    recommended = [FieldRule "reviewer" Nothing ["Ari", "Bo"] List Nothing Nothing Nothing (Just (HandleReferenceRule "ADR" ["mori"] False)) Nothing Nothing],-                    optional = [FieldRule "supersedes" Nothing [] Scalar Nothing Nothing Nothing (Just (HandleReferenceRule "ADR" [] False)) Nothing Nothing]+                  { required = [FieldRule "owner" (Just "Person responsible for the decision.") [] Scalar (Just (DocumentHandle "USR")) Nothing Nothing Nothing Nothing Nothing Nothing],+                    recommended = [FieldRule "reviewer" Nothing ["Ari", "Bo"] List Nothing Nothing Nothing (Just (handleReferenceRule "ADR" ["mori"] False)) Nothing Nothing Nothing],+                    optional = [FieldRule "supersedes" Nothing [] Scalar Nothing Nothing Nothing (Just (handleReferenceRule "ADR" [] False)) Nothing Nothing Nothing]                   },               pathPattern = Just "decisions/*",               resourceScheme = Nothing,@@ -1145,8 +1171,16 @@     }  undocumentedField :: Text.Text -> FieldRule-undocumentedField key = FieldRule {field = key, description = Nothing, allowedValues = [], cardinality = Any, format = Nothing, elementFields = Nothing, objectFields = Nothing, reference = Nothing, path = Nothing, when = Nothing}+undocumentedField key = FieldRule {field = key, description = Nothing, allowedValues = [], cardinality = Any, format = Nothing, elementFields = Nothing, objectFields = Nothing, reference = Nothing, path = Nothing, when = Nothing, uniqueBy = Nothing} +handleReferenceRule :: Text.Text -> [Text.Text] -> Bool -> HandleReferenceRule+handleReferenceRule prefix schemes selfAllowed =+  HandleReferenceRule prefix schemes selfAllowed True Nothing++nestedFieldRule :: Text.Text -> Maybe Text.Text -> [Text.Text] -> Cardinality -> Maybe FieldFormat -> Maybe PathReferenceRule -> Maybe FieldCondition -> NestedFieldRule+nestedFieldRule key description allowedValues cardinality format path condition =+  NestedFieldRule key description allowedValues cardinality format path condition Nothing+ sampleNestedProfile :: ProfileSpec sampleNestedProfile =   ProfileSpec@@ -1164,15 +1198,22 @@                   Nothing                   ( Just                       NestedRules-                        { required = [NestedFieldRule "outcome" Nothing ["approved", "rejected"] Any Nothing Nothing (Just (FieldCondition "kind" ["model"]))],-                          recommended = [NestedFieldRule "notes" Nothing [] Scalar Nothing Nothing Nothing],-                          optional = [NestedFieldRule "model" Nothing [] Scalar Nothing Nothing Nothing]+                        { required =+                            [ (nestedFieldRule "outcome" Nothing ["approved", "rejected"] Any Nothing Nothing (Just (FieldCondition "kind" ["model"])))+                                { reference =+                                    Just+                                      (HandleReferenceRule "IR" ["mori"] False False (Just "mori://[^/]+/IR-[1-9][0-9]*"))+                                }+                            ],+                          recommended = [nestedFieldRule "notes" Nothing [] Scalar Nothing Nothing Nothing],+                          optional = [nestedFieldRule "model" Nothing [] Scalar Nothing Nothing Nothing]                         }                   )                   Nothing                   Nothing                   Nothing-                  Nothing,+                  Nothing+                  (Just "outcome"),                 -- The OKF v0.2 §10 executor, as a house convention would express                 -- it: a mapping-valued key whose members are constrained with                 -- `objectFields`, with a path policy on the member that names a@@ -1187,14 +1228,15 @@                   Nothing                   ( Just                       NestedRules-                        { required = [NestedFieldRule "resource" (Just "The skill a runner follows.") [] Scalar Nothing (Just (PathReferenceRule [] False)) Nothing],+                        { required = [nestedFieldRule "resource" (Just "The skill a runner follows.") [] Scalar Nothing (Just (PathReferenceRule [] False)) Nothing],                           recommended = [],-                          optional = [NestedFieldRule "receipt" Nothing [] List Nothing Nothing Nothing]+                          optional = [nestedFieldRule "receipt" Nothing [] List Nothing Nothing Nothing]                         }                   )                   Nothing                   Nothing                   Nothing+                  Nothing               ],             recommended = [],             optional = []@@ -1224,6 +1266,7 @@     "    reference: (none)",     "    path: (none)",     "    when: (none)",+    "    uniqueBy: outcome",     "    objectFields: (none)",     "    elementFields:",     "      required:",@@ -1231,6 +1274,7 @@     "          allowedValues: approved, rejected",     "          cardinality: any",     "          format: (none)",+    "          reference: local-prefix(IR), external-uri-schemes([mori]), allow-self(false), allow-local(false), external-uri-pattern(mori://[^/]+/IR-[1-9][0-9]*)",     "          path: (none)",     "          when: kind in [model]",     "      recommended:",@@ -1238,6 +1282,7 @@     "          allowedValues: (any)",     "          cardinality: scalar",     "          format: (none)",+    "          reference: (none)",     "          path: (none)",     "          when: (none)",     "      optional:",@@ -1245,6 +1290,7 @@     "          allowedValues: (any)",     "          cardinality: scalar",     "          format: (none)",+    "          reference: (none)",     "          path: (none)",     "          when: (none)",     "  - executor: Run instructions and the receipt fields a run must return.",@@ -1254,12 +1300,14 @@     "    reference: (none)",     "    path: (none)",     "    when: (none)",+    "    uniqueBy: (none)",     "    objectFields:",     "      required:",     "        - resource: The skill a runner follows.",     "          allowedValues: (any)",     "          cardinality: scalar",     "          format: (none)",+    "          reference: (none)",     "          path: external-uri-schemes([]), allow-self(false)",     "          when: (none)",     "      recommended: (none)",@@ -1268,6 +1316,7 @@     "          allowedValues: (any)",     "          cardinality: list",     "          format: (none)",+    "          reference: (none)",     "          path: (none)",     "          when: (none)",     "    elementFields: (none)",@@ -1296,6 +1345,7 @@     "    reference: (none)",     "    path: (none)",     "    when: (none)",+    "    uniqueBy: (none)",     "    objectFields: (none)",     "    elementFields: (none)",     "  - title: (none)",@@ -1305,6 +1355,7 @@     "    reference: (none)",     "    path: (none)",     "    when: (none)",+    "    uniqueBy: (none)",     "    objectFields: (none)",     "    elementFields: (none)",     "frontmatter.recommended: (none)",@@ -1316,6 +1367,7 @@     "    reference: (none)",     "    path: (none)",     "    when: (none)",+    "    uniqueBy: (none)",     "    objectFields: (none)",     "    elementFields: (none)",     "",@@ -1329,6 +1381,7 @@     "      reference: (none)",     "      path: (none)",     "      when: (none)",+    "      uniqueBy: (none)",     "      objectFields: (none)",     "      elementFields: (none)",     "  frontmatter.recommended:",@@ -1336,9 +1389,10 @@     "      allowedValues: Ari, Bo",     "      cardinality: list",     "      format: (none)",-    "      reference: local-prefix(ADR), external-uri-schemes([mori]), allow-self(false)",+    "      reference: local-prefix(ADR), external-uri-schemes([mori]), allow-self(false), allow-local(true), external-uri-pattern((none))",     "      path: (none)",     "      when: (none)",+    "      uniqueBy: (none)",     "      objectFields: (none)",     "      elementFields: (none)",     "  frontmatter.optional:",@@ -1346,9 +1400,10 @@     "      allowedValues: (any)",     "      cardinality: scalar",     "      format: (none)",-    "      reference: local-prefix(ADR), external-uri-schemes([]), allow-self(false)",+    "      reference: local-prefix(ADR), external-uri-schemes([]), allow-self(false), allow-local(true), external-uri-pattern((none))",     "      path: (none)",     "      when: (none)",+    "      uniqueBy: (none)",     "      objectFields: (none)",     "      elementFields: (none)",     "  pathPattern: decisions/*",@@ -1379,6 +1434,7 @@     "    reference: (none)",     "    path: (none)",     "    when: (none)",+    "    uniqueBy: (none)",     "    objectFields: (none)",     "    elementFields: (none)",     "  - title: (none)",@@ -1388,6 +1444,7 @@     "    reference: (none)",     "    path: (none)",     "    when: (none)",+    "    uniqueBy: (none)",     "    objectFields: (none)",     "    elementFields: (none)",     "frontmatter.recommended: (none)",