5. A pattern declares what is vendored#

Status#

Accepted

Context#

ibek pattern hard-coded its vendoring policy: every file in a pattern folder was copied into <instance>/config/ and hashed into runtime-lock.yaml. That is exactly right for a flat StreamDevice pattern and wrong for everything else.

The immediate driver is documentation. We want the useful docs from each source module to live in ibek-runtime-streamdevice alongside the protocol and template files, so the library is browsable and self-explanatory. Those docs must not be vendored into an IOC instance — they are repo context, not runtime files, and config/ is the Kubernetes ConfigMap payload. There was no way to express that.

The second driver is forward compatibility with build-time support management, which needs the same declarative description of “which files go where” against a different library and a different destination.

Decision#

A pattern folder may carry an ibek.manifest.yaml declaring which of its files are vendored and where they land:

version: 1
vendor:
  - src: '.*\.(template|proto|protocol|db|req|ibek\.support\.yaml)$'
    dest: config
  • It is an allow-list. A file matched by no entry is not vendored. That is the mechanism that keeps docs/ out of config/.

  • The list is ordered and first-match-wins. Without that, one file could land in two places under two lock keys, and both check and orphan-pruning become ambiguous about which copy is authoritative.

  • A pattern with no manifest is vendored through a synthesised default (src: '.*', dest: config) rather than through a second code path. One vendoring path, one check path, no branches — and the default is covered by the same validation and the same tests as a hand-written manifest. A pattern gets a manifest if and only if it contains files that must not be vendored.

  • Validation is whole-manifest and fails hard, before a single file is written. A skipped-with-warning entry would mean a file that should be in the IOC silently is not, which is not acceptable in an integrity system. Every message names the offending entry.

  • Lock keys are relative to the destination root, always — not to config/. The synthesised default (a pattern with no manifest) produces config/... keys naturally, so there is no per-pattern base field and no branch in check.

The lock has a version: / patterns: root wrapper, because a bare {pattern_name: entry} mapping has nowhere to put metadata: version: gives the format room to evolve, patterns: holds each vendored pattern by name.

ibek reads and writes exactly this one shape. A lock in any other shape is refused outright, with a generic message naming a standalone conversion script rather than describing what the other shape is or how it arose — a description that would go stale the moment the lock format changes again. Reading such a lock as an empty one is the one thing that must never happen: check would then pass having verified nothing, and the instance would silently become unmanaged.

The lock’s version: is 1, or 2 when any pattern carries a select: (an instance’s own include / exclude adjustment to the manifest). Version 2 is version 1 plus that key, and ibek writes the lowest version that holds the content, so a lock without a selection reads the same to every ibek, while one that could not apply a selection refuses the lock on its version rather than vendoring or checking a different file-set. Unknown top-level keys are refused for the same reason: ignored on read, they would be dropped on the next save.

Consequences#

  • Switching a pattern’s manifest can prune an emptied config/ itself, since orphan pruning now walks up to the destination root. Correct, but startling in a diff.

  • Orphans are computed from the old lock’s key list and new files from the new plan, so update across a version that gains a manifest removes exactly the files the manifest stopped matching.

  • dest may also be a substitution (\1 / \g<name>), expanding to the whole destination-relative path for flattening, renaming or re-rooting. It is kept out of the user-facing tutorial: dest: config is the only form a services repo needs. .. is therefore checked after expansion, since a capture group can introduce one.

  • A vendored *.ibek.support.yaml must land at the config/ root, because ibek pattern schema merges config/*.ibek.support.yaml non-recursively. A misrouted one vendors cleanly, checks cleanly, and is then silently missing from the instance’s editor schema. (ibek runtime generate2 globs config/**/*.ibek.support.yaml and does find a nested one, so the IOC itself still boots — what is lost is editor validation.) The rule is conditional on the file being vendored at all.

  • Two sources mapping to one destination is a hard error, not a last-writer-wins overwrite that check would then pass — and destinations are compared in normal form, so config/x and config/./x are recognised as the one file they are. Likewise one destination cannot be both a file and the folder holding another, which would otherwise fail half way through the write.

  • dest is the only user-controlled placement, and ibek runtime place-files copies *.proto / *.db / *.template from the config/ root only. Nesting those under config/ is for repo organisation; they will not reach the IOC’s runtime search path. Closing that gap belongs to place-files, not here.

Rejected#

  • A lock: key in the manifest. A pattern can never be the authority on where a destination’s lock lives. runtime-lock.yaml at the destination root, always.

  • A stamp: key — moot once the vendored header is dropped (see ADR 4).

  • A per-pattern root: field in the lock, or a “has manifest” flag. Destination-root-relative keys make both unnecessary, and check is offline and cannot see the manifest anyway. The version pin already pins it.

  • Making the lock’s version: a switch ibek branches on to read an older shape. It exists for future evolution and validation only; a shape ibek does not recognise is refused, and converting it is a standalone step outside ibek entirely (scripts/convert-runtime-lock.py).

  • A {pattern} token in dest, a requires: key, dependency resolution, and one lock spanning several destinations. All belong to the build-time support issue, and none of them need a format break — which is why the manifest root is a mapping rather than a bare list.

See ADR 3 for where the authority for a pattern’s content lives, and the pattern manifest reference for the format.