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 +16/−0
- help/bundles.md +1/−0
- help/config.md +5/−0
- help/okf.md +3/−1
- help/profiles.md +53/−18
- okf-cli.cabal +3/−3
- src/Okf/Cli.hs +77/−2
- test/Main.hs +71/−14
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)",