packages feed

okf-cli-0.5.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.

GENERATING DOCUMENTATION

  A profile can generate an OKF bundle documenting itself: one page for the
  profile, one page per concept type it declares.

    okf profile document --profile PROFILE.dhall
    okf profile document --profile PROFILE.dhall --out DIR --write

  Without --write the command prints what it would generate and touches
  nothing. With --out DIR --write it writes the pages and the index.md files
  and prints a one-line summary. --write without --out is an error, and so is
  combining --profile with an EXPORT argument or --registry.

  Without --profile the profile comes from a registry export, using the same
  --registry precedence as list and show.

  Each type page shows the EFFECTIVE rules for that type: the profile-wide
  rules and the type's own, already merged. That is the difference from
  `okf profile show`, which shows the two declaration sites separately and
  leaves you to compose them. Recommended keys carry a bullet saying they are
  checked only under --strict.

  The `description` prose you write on the profile, on a type rule, and on
  each key is what fills the generated pages. A profile with no descriptions
  still generates, with synthesized summaries.

  The output is an ordinary OKF bundle, so okf validate, okf graph, and
  okf show all work on it. Generation never reads the clock, so regenerating
  produces the same bytes; committing the result and running

    git diff --exit-code DIR

  after regenerating is a complete CI drift check.

  Every generated page records who produced it:

    generated:
      by: process:okf-profile-document

  so default output passes `okf validate --strict` with no extra flag. Pass
  --generated-by ACTOR to name a different producer -- <producer>/<version>,
  human:<id>, or process:<id> -- and --generated-at RFC3339 to record when.
  Neither is written unless you ask: generation never reads the clock.

  Pass --okf-version 0.2 to declare the OKF version in the generated bundle's
  root index, exactly as `okf index --okf-version` does. Omitting it preserves
  whatever declaration the destination already carries.

  Two things that will otherwise look like bugs:

  A bundle generated with --timestamp or --generated-at has dates but no
  log.md, so do not check generated documentation with --log-enforce.

  --write regenerates index.md for EVERY directory under --out, including
  ones it did not write into, and never deletes a file it did not generate.
  Point --out at a directory dedicated to the generated documentation.

  okf ships docs/profiles/profile-documentation.dhall, a profile describing
  what a generated documentation bundle looks like, and
  examples/postgresql-profile/ as a committed worked example.

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

REQUIRING A BUNDLE VERSION

  A bundle may declare which OKF version it targets, with okf_version: "0.2"
  in the frontmatter of its root index.md. Specification 12 makes that a MAY,
  so okf validate never reports a bundle for omitting it, even with --strict.

  A profile can demand it as a house convention:

    , requireBundleVersion = Some "0.2"

  A bundle that declares nothing, declares an older version, or declares
  something okf cannot parse is then a deviation. A higher version is not:

    profile: bundle does not declare okf_version; this profile requires 0.2 or later
    profile: bundle declares okf_version 0.1; this profile requires 0.2 or later

  Advisory like every other profile deviation, and fatal with
  --profile-enforce. This is the one violation that names no concept, because
  the declaration belongs to the bundle. Fix a failing bundle with

    okf index BUNDLE --write --okf-version 0.2

  requireBundleVersion is distinct from okfVersion. okfVersion says which
  version's rules the profile itself writes; requireBundleVersion says what
  the profile demands of a bundle. okf never infers one from the other. A
  value that is not <major>.<minor> is rejected when the profile compiles,
  before any bundle is read.

  docs/profiles/postgresql.dhall sets it. docs/profiles/okf-v0-2.dhall
  deliberately does not: a format-level profile that demanded what the
  specification only permits would advise against the specification.

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.
  okf help ids          Document IDs: idField, idPrefix, and okf id.
  okf help index        Declaring okf_version to satisfy requireBundleVersion.

  The full descriptor schema, and the upgrade steps above in detail, are
  documented in docs/user/profiles.md, whose "Generating profile
  documentation" section covers `okf profile document` at length.