okf-cli-0.4.0.0: help/profiles.md
PROFILE-BASED VALIDATION
A profile descriptor declares house conventions layered on top of OKF: which
type strings are allowed, which frontmatter keys are required, which
resource:// schemes are expected, the file layout, and required # Schema
columns. Profiles are written as Dhall descriptors.
USAGE
okf validate BUNDLE --profile PROFILE.dhall
okf validate BUNDLE --profile PROFILE.dhall --profile-enforce
ADVISORY VS ENFORCED
--profile PROFILE Run profile checks after structural validation.
Deviations print to stderr, each line prefixed
"profile:". By default they are advisory and do NOT
change the exit code.
--profile-enforce Make profile deviations fail the command (non-zero
exit).
--strict Also check profile `recommended` fields. Required
profile fields are checked in both modes. Profile
`optional` fields are never reported when absent, in
either mode.
EXIT CODES
- Structural errors always exit non-zero, with or without --profile.
- Profile deviations exit 0 by default (advisory), or non-zero with
--profile-enforce.
- A descriptor that fails to load is always a hard error.
EXAMPLE (ADVISORY)
profile: schemas/sales/tables/bad: type not in profile vocabulary: pg table
OK: 3 concepts
profile: 1 advisory deviation(s) (use --profile-enforce to fail)
DOCUMENT IDS
A profile may name an idField such as "docId" and give selected type rules an
idPrefix such as "ADR". Those concepts must carry canonical handles such as
ADR-7. Use `okf id next BUNDLE ADR --profile PROFILE.dhall` to print the next
handle, `okf id list BUNDLE --profile PROFILE.dhall` to list allocations, and
`okf show BUNDLE ADR-7` to resolve one.
REGISTRIES
You do not have to write a descriptor from scratch. A registry is any Dhall
expression evaluating to a record whose fields -- possibly nested -- are
profile values. okf finds them structurally: it walks the evaluated record
and reports every field that decodes as a profile, under the dotted path it
was found at. That path is the profile's export path.
okf profile list
okf profile list --registry /path/to/okf-profiles
okf profile show postgresql --registry /path/to/okf-profiles
A bare `okf profile` means `okf profile list`. Both subcommands accept
--json. The EXPORT column reads "(root)" when the reference is itself a
profile rather than a record of profiles; the ID FIELD column reads "-" when
the profile declares no idField.
DESCRIPTIONS
A profile may document itself: one description for the profile as a whole,
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.
`okf profile list` shows the profile's own description in a trailing
DESCRIPTION column, reading "-" when it has none. `okf profile show` prints
the profile description under the name, one " - key: prose" line per
frontmatter key, and a description line in each type block, all reading
"(none)" when absent. When a required key is missing from a concept, the
advisory repeats the key's prose in parentheses:
profile: schemas/sales/tables/orders: missing profile-required field: title (Human-readable name of the object.)
Descriptions are optional and additive. A descriptor written before they
existed loads unchanged and simply shows none; nothing needs migrating. See
docs/user/profiles.md for how to add them to a descriptor you already have.
TYPE-AWARE FRONTMATTER
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`, `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 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
A FieldRule may set allowedValues to a list of legal text values. An empty
list means unconstrained. Present strings and lists of strings are checked in
both permissive and strict modes; a type-level vocabulary narrows a
profile-wide vocabulary by intersection. Disjoint vocabularies are rejected
as a profile-definition error before bundle validation.
Set allowUnknownFields = False to reject undeclared top-level frontmatter
keys. The allowed names come from the effective rules for that concept's own
type, plus the core OKF keys and the profile's idField. The default is True,
so existing profiles continue to allow producer extensions.
profile: requests/typo: missing profile-required field: status
profile: requests/typo: frontmatter field not declared by profile: stauts
FIELD CARDINALITY
Every FieldRule has a cardinality: Any, Scalar, or List. Any is the default
and preserves the legacy non-empty-text-or-non-empty-list presence behavior.
Scalar accepts non-blank text, numbers, and booleans. List accepts arrays.
Objects and null fail an explicit cardinality constraint.
Use `field.scalar "title"` or `field.list "tags"`. At profile and type scope,
Any is the identity; contradictory Scalar and List declarations are a hard
profile-definition error. Wrong-shape values are reported even for a
recommended field outside --strict, without a duplicate missing-field or
vocabulary-shape diagnostic.
profile: bad: frontmatter cardinality at title must be scalar, found list: ["One","Two"]
profile: bad: frontmatter cardinality at tags must be list, found scalar: "one"
NAMED FIELD FORMATS
A FieldRule may set format to one of five parser-backed textual contracts:
Rfc3339Utc, Date, Uri, UriWithScheme Text, or DocumentHandle Text. The default
is None, so existing fields remain unconstrained. Formats check present strings
and every string in a list; they do not make an absent field required.
Rfc3339Utc accepts extended timestamps such as 2026-07-29T17:00:00Z and
requires uppercase Z rather than a numeric offset. Date accepts exactly
YYYY-MM-DD and rejects impossible calendar dates. Uri requires an absolute RFC
3986 URI. UriWithScheme additionally requires the named scheme, compared
case-insensitively. DocumentHandle requires the canonical PREFIX-N form and
compares the prefix case-sensitively.
The FieldRule constructors cover the common forms:
field.rfc3339Utc "timestamp"
field.date "published"
field.uri "source"
field.uriWithScheme "originPlan" "mori"
field.documentHandle "decision" "ADR"
Uri at profile scope may be narrowed to UriWithScheme at type scope. Equal
formats merge unchanged; other unequal pairs are a hard profile-definition
error. URI scheme parameters must follow RFC 3986 scheme syntax, and document
prefixes must follow the same grammar as okf document IDs.
profile: bad: frontmatter value at timestamp must match format rfc3339-utc, found: "2026-07-29T17:00:00+01:00"
profile: bad: frontmatter value at originPlan must match format uri-with-scheme(mori), found: "https://example.test"
NESTED RECORD FIELDS
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.
Declaring elementFields implies list cardinality; combining it with Scalar is
a hard profile-definition error. Profile-wide and type-specific nested rules
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; 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"
Extra keys inside a record remain allowed. Nested field-name closure and a
second nested level are not part of this schema.
CONDITIONAL FIELD PRESENCE
FieldRule and NestedFieldRule may set `when = Some { field, hasValue }` so a
required or recommended field applies only when a sibling scalar text field
has one of the listed values. Top-level rules see top-level siblings; nested
rules see only siblings in the same list element. There is no cross-scope
capture.
The source must be explicitly Scalar and have a non-empty allowedValues
vocabulary. hasValue must be non-empty and a subset of that vocabulary. Empty,
self-referential, undeclared, open, non-scalar, and unreachable conditions are
hard profile-definition errors before any bundle is read.
FieldRule::{
, field = "supersededBy"
, when = Some { field = "status", hasValue = [ "superseded" ] }
}
A missing, wrong-shape, or out-of-vocabulary source makes the condition false,
avoiding a second target-field diagnostic. When the target is present, its
vocabulary, cardinality, and format are checked regardless of the condition.
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
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:
field.localReference "supersedes" "ADR"
field.localOrExternalReference "supersededBy" "ADR" [ "mori" ]
The helpers default allowSelf to False. Use
okf.defaults.HandleReferenceRule record completion to override it.
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.
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.
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.
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. Without
--registry, okf uses OKF_PROFILE_REGISTRY, then profiles.registry from
configuration, then the built-in default: the okf-profiles package pinned by
tag and sha256 hash. Because it is pinned, Dhall caches it under
~/.cache/dhall after the first fetch, so later runs are offline. Pass
--registry with a local checkout to be offline throughout.
There is no install step. `okf profile show` closes with the two-line Dhall
snippet that consumes the profile; save it to a file and pass that file to
`okf validate --profile`.
UPGRADING FROM 0.1.x
okf 0.2.0.0 added idField to Profile and idPrefix to TypeRule. Dhall record
types are closed, so descriptors written against 0.1.x fail to load with
"Expression doesn't match annotation", listing the missing fields with a "-".
Set idField = None Text and idPrefix = None Text to keep the old behavior
(no document-ID checks), or adopt record completion via
okf.defaults.Profile::{ ... } so later schema additions do not break the
descriptor again.
Descriptors pinned to a schema URL must bump the tag and the sha256 hash
together. Edit the tag, then run `dhall freeze PROFILE.dhall`, which
rewrites the hash in place even when the old one is stale.
SEE ALSO
okf help validation Structural validation and referential integrity.
The full descriptor schema, and the upgrade steps above in detail, are
documented in docs/user/profiles.md.