Files
ConfigKit/docs/roadmap/audit.md

30 KiB

Operator Convention Audit: ConfigKit and Ansible

Status: audit complete; no behavior changes proposed or authorized by this document.

Scope and evidence

Reviewed ConfigKit revision: 2baf14d. Comparison date: 2026-10-19. The configuration contract owns ConfigKit behavior; this audit records differences, not an alternative contract or implementation plan.

The closest Ansible counterpart is usually YAML variable data loaded through ansible.builtin.include_vars, not a playbook's task language or ansible.cfg (which is INI). Where useful, comparisons explicitly identify fileglob, find, variable precedence, filters, or task syntax. Those mechanisms have different conventions even within Ansible.

Evidence consists of current official Ansible documentation, the versioned ansible-core v2.19.0 include-vars implementation and YAML constructor, and the PyYAML 6.0.2 scalar resolver. Implementation observations below are identified separately from documented interfaces; they are not promises about every Ansible version or plugin. The /latest/ documentation links are moving references. No Ansible executable is available in the development environment, so this is a documentation/source comparison, not a side-by-side runtime test.

ConfigKit's parser and import-policy implementations were inspected alongside their tests and maintained consumer examples. Focused YAML, import, composition, overlay, inheritance, version, decode, secret, and consumer tests passed on the reviewed tree. No code or existing policy was changed for this audit.

Summary

Area Main operator surprise
YAML values Several accepted Ansible scalar spellings are errors in ConfigKit; dates remain strings.
Discovery Directory recursion, filename filtering, missing-input handling, and wildcard syntax are not drop-in equivalents.
Composition Import overlap is a conflict, not permission for a later file to replace an earlier value.
Precedence ConfigKit recursively combines mappings across explicit tiers; Ansible normally replaces dictionary variables unless merging is requested.
Reuse ConfigKit's post-assembly sibling extends is neither YAML anchor reuse nor Ansible task/role inheritance.
Evaluation YAML is configuration data, not Jinja expressions, lookups, conditionals, or a task program.
Secrets and inspection Typed environment-name references and classified frozen inspection differ from Vault, lookups, and task-output controls.

These differences are not automatically defects. Many follow accepted ConfigKit decisions, especially strict composition, predictable precedence, presence tracking, typed validation, and the complete-on-return lifecycle.

1. YAML parsing and scalar conventions

ConfigKit authority: YAML documents, scalars, and parser policy tests.

Y1 — Boolean and null spellings are narrower

ConfigKit accepts plain lowercase true, false, and null, plus ~ and an empty mapping value for null. Unquoted yes, no, on, off, and case variants such as True or NULL fail rather than becoming strings. Quoting makes them strings, not typed booleans.

Ansible's YAML conventions accept additional boolean spellings; the inspected PyYAML resolver also recognizes mixed-case null forms. An operator copying enabled: yes must change the value to true, not merely quote it when the application expects a boolean. Ansible YAML, resolver source.

Y2 — Numeric spellings and timestamp interpretation differ

ConfigKit uses JSON-number syntax, rejecting forms such as 010, 0x10, 1_000, +1, .5, and .inf. Date/time-looking plain values remain strings. The inspected PyYAML resolver recognizes several non-JSON numeric forms and timestamps; Ansible's constructor retains timestamp construction. Do not assume that accepting the same YAML text produces the same scalar type. Resolver source, Ansible constructor.

ConfigKit also retains exact integer spelling for configured version checks, but ordinary typed and dynamic destinations still enforce their documented ranges. Ansible's Python-backed numeric data is not a guarantee that a value will fit a ConfigKit application's Go field.

Y3 — Duplicate keys are always errors

ConfigKit rejects duplicate decoded mapping keys at every depth. Ansible's documented default is a warning; its constructor retains the last value. Ansible also offers error and ignore settings. A file that loads with an Ansible warning can therefore fail ConfigKit outright. Duplicate-key setting, constructor source.

Y4 — Anchors, aliases, merge keys, and explicit tags are unavailable

ConfigKit rejects anchors, aliases, all explicit YAML tags, and even an ordinary quoted key named <<. Ansible documents anchor/alias/merge reuse and recognizes tags including !unsafe and !vault; its constructor also supports standard tags such as !!str. These files require semantic rewriting, not just moving them into an import directory. Advanced syntax, constructor source.

Y5 — Empty variable files are not empty configuration mappings

ConfigKit requires one nonempty mapping document: {} is valid; an empty file, document-only marker, null root, or sequence root is not. The inspected include_vars implementation turns a null/empty parsed result into an empty dictionary. Explicitly use {} for an intentionally empty ConfigKit fragment. Include-vars implementation.

Mapping roots themselves are not a divergence from normal Ansible variable files. Playbook sequence roots belong to a different interface. Likewise, Ansible has an explicit from_yaml_all filter for streams; that does not mean ordinary include_vars automatically accepts multiple documents. ConfigKit loading has no corresponding stream mode. Variable files, multi-document filter guidance.

Y6 — Mapping keys are data strings, not Ansible variable identifiers

ConfigKit requires string-classified keys, quoting numeric/boolean-looking keys when needed. Literal dots, slashes, spaces, and numeric-looking quoted keys are not split into paths. Ansible distinguishes restricted top-level variable names from nested dictionary keys; its YAML constructor can construct non-string hashable keys. Porting arbitrary nested Ansible dictionary keys may require quoting, while imposing Ansible variable-name rules on every ConfigKit key would be unnecessarily restrictive. Variable naming and dictionary access, constructor source.

2. Import declarations and filesystem discovery

ConfigKit authority: explicit imports, input destinations, and import internals.

Defaults below are ConfigKit's library defaults. Applications can supply load-wide ImportDefaults; an explicit declaration field wins over that application default. Transitive declarations use the same application defaults, not the options authored on the declaration that selected their parent source. An operator must therefore check the consuming application's selected defaults.

ID ConfigKit convention Closest Ansible convention and difference
I1 Enabled documents contain root configkit.imports; each declaration chooses path, dir, or glob. path/dir accept a string or list. include_vars is a task with file or dir, normally one string; fileglob is a separate lookup. Ansible variable data does not recursively execute embedded include declarations. Module, lookup.
I2 Directory discovery is shallow by default. recursive: true enables descendants; depth requires effective recursion. include_vars: dir recurses by default. find instead defaults to recurse: false, with that setting overriding depth rather than making depth invalid. Include-vars, find.
I3 pattern is one or more case-sensitive basename globs; omitted means unrestricted, [] means no matches. include_vars.files_matching is one regular expression. find.patterns is closer, but its documented empty/default pattern selects all rather than none. Include-vars, find.
I4 exclude uses the same basename-glob grammar. include_vars.ignore_files is described as filenames; the inspected implementation actually uses suffix-anchored regex searches. find.excludes accepts glob/regex patterns. Do not translate fields by renaming alone. Module, implementation, find.
I5 There is no extension whitelist: discovery attempts every selected filename, including .txt or extensionless files. Directory include_vars defaults to json, yaml, and yml; unknown extensions fail unless configured to be ignored. This restriction does not mean an explicitly named file has the same filtering. Module, implementation.
I6 Missing imports are optional by default. required: true checks every named path, or requires a nonempty directory/glob result. Direct include_vars missing-file/directory access fails. fileglob may return no matches, and first_found(skip=True) provides a different explicit absence mechanism. Implementation, fileglob, first_found.
I7 hidden and follow default to true for discovered entries; explicit roots retain normal access. find defaults both to false, and automatic host_group_vars ignores hidden/backup entries. The inspected include_vars walk follows directory links and has no equivalent hidden/follow switches; do not call ConfigKit's defaults universally unlike Ansible. Find, vars plugin, implementation.
I8 Import paths are slash-relative to their declaring source's directory, inside its selected namespace/root. Absolute paths and URLs are invalid. Ansible accepts absolute controller paths and uses role/play/task search contexts for relative resource lookup. Copying a role-relative name can select another file or fail in ConfigKit. Search paths, loader source.
I9 A path list is a normalized set union; lexical duplicates coalesce within a declaration. Repeated admitted sources across declarations/routes fail within a strict tier. Repeating include_vars tasks is allowed by the inspected implementation; no equivalent load-wide repeated-source rejection is applied there. first_found lists select the first hit, not all files. Implementation, first_found.
I10 Source acquisition is authored/depth-first, but assembly uses the smallest dependency-ready stable source key within each strict scope. Directory include_vars uses alphabetic file loading, not ConfigKit's import dependency schedule. Prefixing filenames cannot authorize ConfigKit replacement or force arbitrary dependency-violating item order. Module.
I11 Optional whole-declaration empty results retain source-local ImportSelections evidence, not fake sources or warnings. include_vars returns included file paths, rather than this optional-empty declaration record. The inspection models are not interchangeable. Return values.

Depth alignment: direct files are depth 1, and recursive depth 0 means unlimited subject to budgets. That is consistent with the inspected include_vars depth convention; the significant differences are recursion's default and whether depth is independently applicable. Implementation.

Empty-set behavior: ConfigKit path: [] succeeds even when required because no named target is missing. A required dir: [] or pattern: [] search fails because it selects no files. An explicit empty pattern list also overrides an application's nonempty default patterns. These are set meanings, not automatic fallback to unrestricted search.

I12 — Even shell-style wildcard grammars are not identical

ConfigKit uses Go path.Match, not Python fnmatch or regex: character-class negation uses [^a], whereas Python uses [!a]; Go backslashes escape pattern characters. For example, copying [!a]*.yml can change the selected set rather than producing an obvious error. Match full basenames, not regex substrings. Go pattern grammar, Python filename grammar.

ConfigKit's glob can wildcard directory components and explicitly rejects recursive **. Ansible's fileglob contract supports filename patterns in a single directory, not wildcard directory search as an interchangeable feature. Python glob's ordinary wildcard treatment also excludes dot-prefixed names unless requested by the pattern, unlike ConfigKit's default hidden: true. Fileglob, Python glob.

I13 — Discovery errors and resource bounds are load failures

ConfigKit optionality permits absence, not malformed selected YAML, unreadable contributions, containment failures, active traversal cycles, or exhausted source/byte/node/entry budgets. The inspected Ansible directory walker logs os.walk errors through its verbosity callback rather than applying the same fail-the-load rule. Neither ignore_errors nor lookup errors='ignore' is an equivalent of ConfigKit's narrowly scoped required: false. Implementation, first_found behavior.

ConfigKit's fixed budgets have no shared, portable Ansible equivalence established by this audit. This is not a claim that Ansible or its parsers have no limits.

3. Combination, precedence, and inheritance

C1 — Imports compose strictly rather than overriding

Within a main tier or one underlay group, shared mappings recurse, but overlapping non-mapping values conflict even when equal. Ansible variable precedence normally chooses a winning definition; directory include_vars also overwrites colliding keys in its accumulated results. 10-base.yml plus 90-local.yml is therefore not an override scheme for ConfigKit imports. Intentional replacement needs a higher ConfigKit tier. ConfigKit strict composition, Ansible precedence, include-vars implementation.

C2 — Cross-tier mapping combination is recursive by default

ConfigKit retains omitted lower-tier mapping children, including when a higher tier supplies {}; other present values replace, and [] clears a sequence. Ansible's default dictionary-variable behavior is replacement, and its combine filter defaults to nonrecursive merging. ConfigKit's mapping overlay is closer to explicit combine(recursive=true) than ordinary variable reassignment. Ansible's global hash-merge setting is configurable and discouraged upstream; it is not a portable equivalence. ConfigKit overlays, hash behavior, combine.

C3 — Registered sequence composition is not merge-by-identity

ConfigKit appends distinct string-ID records only at application-registered strict-tier paths; repeated IDs conflict. Higher-tier sequences still replace wholesale. Ansible combine offers selectable list policies, while the separate community.general.lists_mergeby filter combines records sharing an attribute. That optional collection filter does not describe ConfigKit's distinct-ID rule or Ansible's universal default. ConfigKit registration, combine, lists_mergeby.

C4 — The precedence model and lifetime are different

ConfigKit loads application defaults, ordered underlays, main, selected overlay, and explicit CLI values into one completed result. It supplies no general environment-variable overlay. Ansible has host/play/task scopes and runtime variable definitions; ordinary command-line options are not generally highest priority, while -e extra variables are. Environment also configures Ansible itself. Do not equate every application flag with Ansible's ordinary CLI category. ConfigKit layered loading, Ansible precedence.

ConfigKit rejects duplicate or overlapping explicit override records rather than providing a last-argument-wins channel. It accepts typed values supplied by the application; it does not define an Ansible-style key=value parser. Ansible extra-vars key=value values are strings; JSON/YAML forms preserve other types. ConfigKit overrides, Ansible extra vars.

C5 — Defaults depend on presence, not truthiness

ConfigKit preserves explicit false, zero, empty text/list, and allowed null; omission retains defaults. Null is not universally legal: application Go scalar and struct fields reject it, while supported nullable fields accept it. Ansible can distinguish undefined from false/null too. Its ordinary Jinja default filter handles undefined, while default(value, true) additionally treats falsey values as needing replacement; omit is a separate task-argument mechanism. That opt-in truthiness behavior has no ConfigKit YAML equivalent. ConfigKit decoding, Ansible default and omit guidance.

C6 — extends is post-assembly sibling inheritance

ConfigKit resolves an exact immediate sibling mapping after all tiers combine; the child recursively overlays its parent. extends: null cancels a relationship, and enclosing inheritance precedes nested destination-local resolution. All effective relationships are resolved, not just an application-selected preset.

Ansible YAML anchors/<< reuse nodes within one document during parsing, and YAML merge does not recursively combine an already-present nested child mapping. Ansible play/block/task keyword inheritance is another distinct mechanism. Neither is a drop-in equivalent of ConfigKit extends; explicit Ansible recursive combine expressions can approximate parts of the value operation. ConfigKit inheritance, Ansible advanced syntax, YAML merge definition, keyword inheritance, combine.

4. Structural controls, evaluation, and schema

S1 — Reserved data keys and destination syntax differ

ConfigKit reserves configkit for root directives and rejects it below the root; with imports disabled it is still not ordinary application data. extends is structural input, and quoting a key does not bypass either control. Ansible variable dictionaries do not give these two names ConfigKit's structural meaning. Do not confuse this with Ansible's own magic-variable and playbook-keyword rules. ConfigKit imports, inheritance, Ansible variable rules.

Import destination: [catalog, entries] means literal mapping segments relative to the immediate importer; [] adds none. placement: sequence-item builds a record in that destination's sequence, with a local helper scope. Ansible's include_vars.name wraps data under one variable, not this relative mount/item model; dots are not a substitute for ConfigKit segment lists. ConfigKit destinations, Ansible include-vars namespace.

S2 — Jinja, conditional includes, and lookups are not evaluated

In ConfigKit, "{{ value }}", "{{ lookup('env', 'TOKEN') }}", and ${TOKEN} are ordinary strings unless application code gives them domain meaning; no generic template pass exists. when, loop, tags, and ignore_errors are not import-declaration controls. Ansible supports Jinja references/lookups and task-level conditionals and loops. Configuration must be translated structurally or handled by deliberate application resolution, not presumed executable. ConfigKit resolution, Ansible variables, task keywords, env lookup.

S3 — Final typed validation and version ownership are library boundaries

ConfigKit strictly decodes the assembled result against the application's Go shape: unknown struct fields and incompatible values fail; dynamic maps leave domain-key validation to the application. Its configured version rule accepts string/integer declarations, can consume non-main headers, and protects the main version path from imports/upper tiers/inheritance. None of those policies should be inferred from the word version alone.

Ansible variable-file loading is not validation against one final application schema; validation occurs at other boundaries, including module/role argument specifications. A variable named version is not by itself a ConfigKit-style protected, consumed header. ConfigKit versions, typed decoding, Ansible role validation, include-vars data loading.

ConfigKit's integer-version-to-string exception is specific to that configured path. It does not establish general Ansible-like filter coercion elsewhere. Ansible type conversion filters.

For a field typed as time.Duration, ConfigKit requires a duration string such as 5s, not an integer count of seconds. Ansible duration arguments are module specific; for example, wait_for.timeout uses integer seconds. Translate the particular field's representation rather than assuming one shared duration type. ConfigKit decoding, Ansible wait_for.

S4 — Overlay selection and module objects are ConfigKit/application conventions

A selected ConfigKit overlay uses exactly overlays/<name>.yml; empty or exact default means no overlay. There is no extension fallback or stack. Ansible's inventory/role variable directories are different selection and precedence mechanisms, not this named overlay convention. ConfigKit overlays, Ansible vars plugin, roles.

The canonical module-object guidance uses an explicit identifier and options mapping; individual field names belong to applications. Ansible tasks instead name an action/module or use action/args, with task keywords alongside it. A generic ConfigKit configuration object is not an Ansible task, and ConfigKit does not provide an Ansible module registry or execute a module-named mapping. ConfigKit module guidance, Ansible task syntax, keywords.

5. Secrets, resources, and inspection

O1 — A typed secret field contains an environment name

For a ConfigKit Secret slot, api_key: SERVICE_TOKEN is a reference to one nonempty effective environment value; ordinary string slots do not gain that behavior. Missing/unset/empty required secrets fail. Ansible uses explicit env lookups or other secret providers; its env lookup defaults to an empty string for undefined names unless configured otherwise. These are different missing-value and evaluation conventions. ConfigKit secrets, Ansible env lookup.

Vault-encrypted files or !vault values are not ConfigKit inputs to decrypt. Ansible Vault protects stored content and supplies decryption through Ansible; ConfigKit's typed reference deliberately keeps credential bytes out of YAML composition/history. Its separate secrets-directory bootstrap is an application startup operation, not another YAML include or a Vault equivalent. ConfigKit bootstrap, Ansible Vault.

O2 — Redaction and serialization are not task no_log

ConfigKit has application-classified paths and typed secret references, with frozen redacted effective/explanation output; there is no YAML no_log control or unredacted renderer option. Ansible no_log controls task output and does not make every debug operation safe. Vault encryption and !unsafe also have different purposes; neither is a synonym for ConfigKit classification. ConfigKit inspection, Ansible logging, advanced syntax.

ConfigKit effective YAML uses a fixed deterministic layout and may not be reloadable after redaction. Ansible to_nice_yaml exposes formatting options such as indentation and key sorting; it is serialization, not ConfigKit's frozen safe-inspection contract. Do not use either output interchangeably without checking the consumer's required representation. Ansible YAML filter.

O3 — Resource paths are passive data, not generic Ansible file lookup

ConfigKit retains the declaring source's resource context, including through inheritance; applications explicitly open resource-valued fields. It does not automatically resolve every string that resembles a path. Ansible file resources are interpreted by their specific module/lookup and controller/remote search context. These differ from ConfigKit import paths as well as from arbitrary application data. ConfigKit resource contexts, Ansible path contexts.

Assessment and follow-up

The most plausible convention-alignment discussion is discovery: recursion, filter names/grammar, filename defaults, absence handling, and hidden/link defaults. First choose the intended comparison (include_vars, fileglob, or find); copying pieces from each would not create one coherent Ansible convention.

Preserve ConfigKit's accepted composition, presence, lifecycle, and provenance decisions unless the maintainer explicitly chooses to change them. YAML scalar and reuse differences likewise reflect an owned parsing policy, not an accidental failure to inherit upstream YAML behavior. Documenting a difference does not authorize adding a compatibility mode, a new restriction, or an implementation.

Immediate operator guidance from the audit is to review imported files for overlapping values, use explicit directory filters/requiredness when intended, translate regex/Python patterns rather than copying them, and replace Ansible tags/templates/anchors with supported application data and ConfigKit mechanisms.

Validation for this documentation pass: focused ConfigKit YAML and engine tests, maintained consumer examples, local documentation links, and whitespace checks. External sources were reviewed online; no Ansible installation or application consumer dependencies were changed. This is not an exhaustive audit of Ansible's third-party plugin ecosystem or a claim that its behavior is uniform across versions and application-specific modules.