diff --git a/docs/roadmap/audit.md b/docs/roadmap/audit.md new file mode 100644 index 0000000..c0a086e --- /dev/null +++ b/docs/roadmap/audit.md @@ -0,0 +1,444 @@ +# 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](../config.md) 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][include-source] and +[YAML constructor][constructor], and the +[PyYAML 6.0.2 scalar resolver][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](../config.md#yaml-documents), +[scalars](../config.md#scalars), and +[parser policy tests](../../internal/yaml/parse_test.go). + +### 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][ansible-yaml], +[resolver source][resolver]. + +### 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][resolver], [Ansible constructor][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][duplicate-setting], +[constructor source][constructor]. + +### 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][advanced-syntax], +[constructor source][constructor]. + +### 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][include-source]. + +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][variables], +[multi-document filter guidance][filters]. + +### 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][variables], +[constructor source][constructor]. + +## 2. Import declarations and filesystem discovery + +ConfigKit authority: [explicit imports](../config.md#explicit-imports), +[input destinations](../config.md#input-destinations), and +[import internals](../internal/imports.md). + +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][include-vars], [lookup][fileglob]. | +| 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][include-vars], [find][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][include-vars], [find][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][include-vars], [implementation][include-source], [find][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][include-vars], [implementation][include-source]. | +| 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][include-source], [fileglob][fileglob], [first_found][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][find], [vars plugin][host-group-vars], [implementation][include-source]. | +| 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][paths], [loader source][data-loader]. | +| 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][include-source], [first_found][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][include-vars]. | +| 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][include-vars]. | + +**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][include-source]. + +**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][go-match], [Python filename grammar][python-fnmatch]. + +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][fileglob], [Python glob][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][include-source], [first_found behavior][first-found]. + +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](../config.md#strict-document-composition), +[Ansible precedence][precedence], [include-vars implementation][include-source]. + +### 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](../config.md#selected-overlays), +[hash behavior][hash-setting], [combine][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](../config.md#strict-document-composition), +[combine][combine], [lists_mergeby][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](../config.md#layered-loading), +[Ansible precedence][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](../config.md#explicit-cli-overrides), +[Ansible extra vars][variables]. + +### 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](../config.md#defaults-and-typed-decoding), +[Ansible default and omit guidance][filters]. + +### 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](../config.md#sibling-inheritance), +[Ansible advanced syntax][advanced-syntax], [YAML merge definition][yaml-merge], +[keyword inheritance][precedence], [combine][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](../config.md#explicit-imports), +[inheritance](../config.md#sibling-inheritance), [Ansible variable rules][variables]. + +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](../config.md#input-destinations), +[Ansible include-vars namespace][include-vars]. + +### 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](../config.md#recorded-application-resolution), +[Ansible variables][variables], [task keywords][keywords], [env lookup][env]. + +### 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](../config.md#shared-source-version-checks), +[typed decoding](../config.md#defaults-and-typed-decoding), +[Ansible role validation][roles], [include-vars data loading][include-source]. + +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][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](../config.md#defaults-and-typed-decoding), +[Ansible wait_for][wait-for]. + +### S4 — Overlay selection and module objects are ConfigKit/application conventions + +A selected ConfigKit overlay uses exactly `overlays/.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](../config.md#selected-overlays), [Ansible vars plugin][host-group-vars], +[roles][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](../config.md#canonical-module-options), +[Ansible task syntax][playbooks], [keywords][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](../config.md#typed-secret-references), +[Ansible env lookup][env]. + +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](../config.md#explicit-secrets-directory-bootstrap), +[Ansible Vault][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](../config.md#secret-classification-and-effective-rendering), +[Ansible logging][logging], [advanced syntax][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][nice-yaml]. + +### 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](../config.md#source-resource-contexts), +[Ansible path contexts][paths]. + +## 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. + +[ansible-yaml]: https://docs.ansible.com/projects/ansible/latest/reference_appendices/YAMLSyntax.html +[advanced-syntax]: https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_advanced_syntax.html +[include-vars]: https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/include_vars_module.html +[include-source]: https://github.com/ansible/ansible/blob/v2.19.0/lib/ansible/plugins/action/include_vars.py +[constructor]: https://github.com/ansible/ansible/blob/v2.19.0/lib/ansible/_internal/_yaml/_constructor.py +[resolver]: https://github.com/yaml/pyyaml/blob/6.0.2/lib/yaml/resolver.py +[data-loader]: https://github.com/ansible/ansible/blob/v2.19.0/lib/ansible/parsing/dataloader.py +[duplicate-setting]: https://docs.ansible.com/projects/ansible/latest/reference_appendices/config.html#duplicate-yaml-dict-key +[hash-setting]: https://docs.ansible.com/projects/ansible/latest/reference_appendices/config.html#default-hash-behaviour +[fileglob]: https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/fileglob_lookup.html +[first-found]: https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/first_found_lookup.html +[find]: https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/find_module.html +[host-group-vars]: https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/host_group_vars_vars.html +[paths]: https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbook_pathing.html +[go-match]: https://pkg.go.dev/path#Match +[python-fnmatch]: https://docs.python.org/3/library/fnmatch.html +[python-glob]: https://docs.python.org/3/library/glob.html +[precedence]: https://docs.ansible.com/projects/ansible/latest/reference_appendices/general_precedence.html +[variables]: https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_variables.html +[filters]: https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_filters.html +[combine]: https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/combine_filter.html +[lists-mergeby]: https://docs.ansible.com/projects/ansible/latest/collections/community/general/lists_mergeby_filter.html +[yaml-merge]: https://yaml.org/type/merge.html +[roles]: https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_reuse_roles.html +[keywords]: https://docs.ansible.com/projects/ansible/latest/reference_appendices/playbooks_keywords.html +[playbooks]: https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_intro.html +[env]: https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/env_lookup.html +[vault]: https://docs.ansible.com/projects/ansible/latest/vault_guide/vault_encrypting_content.html +[logging]: https://docs.ansible.com/projects/ansible/latest/reference_appendices/logging.html +[nice-yaml]: https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/to_nice_yaml_filter.html +[wait-for]: https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/wait_for_module.html