Skip to content

design: is Ansible a lexicon, an Op surface, or neither? #2353

Description

@lex00

What

chant has no Ansible support and no record of considering it. grep -ril ansible across the tree returns nothing, and the only mention anywhere is #2080's preamble, which lists Ansible alongside "Terraform-only controls, live-cluster reads" as out of scope for the audit-rule backlog. That was a scoping note in a survey of 37 linters — ansible-lint is not among them and no Ansible rules were enumerated. This issue is a different question and should not be read as re-opening that one.

The question came up from a concrete want: chaining terraform, Ansible and helm in one Op run. The chaining half is already tracked at #2314 for terraform/helm/k8s. Whether Ansible can join it depends on what Ansible is to chant.

The case that it is a lexicon

There is a real machine-readable surface. Every Ansible module carries a DOCUMENTATION YAML block with typed options:, and ansible-doc --json dumps it. That satisfies what generatePipeline wants on paper: a fetchSchemas returning a map keyed by module name, a parseSchema producing typed options, a NamingStrategy over module names.

The case that it is not

A playbook is not a resource graph, it is an ordered program. Tasks run in sequence, register feeds later tasks, when branches, loop iterates, handlers fire at the end. chant's Declarable model is a graph resolved by reference, and the evaluator has no notion of task order.

Modelling tasks as resources would make declaration order semantically load-bearing in a system that deliberately resolves by dependency instead. That is smuggling control flow through a dependency resolver, and the failure would be quiet: a playbook whose emitted task order differs from the authored one still builds.

Ansible's ordered-steps-with-handlers shape is much closer to something chant already has a model for — an Op: phases, ordered steps, compensation on failure.

The precedent worth copying

The terraform lexicon is exactly this situation already resolved. It has no generated resource surface — one entity per HCL block, read from .tf files that already exist — and lexicons/terraform/src/serializer.ts:23 returns "", with the reason recorded in its own doc comment: emitting HCL "would put a second, generated copy of the estate beside the authored one." Its stated value is "what the entities let the rest of chant do with the root (post-synth checks, chant audit, the Op surface), not a rendered artifact."

Applied to Ansible that shape is:

  • read existing playbooks into entities for lint and chant audit
  • ship ansiblePlaybook / ansibleCheck Op activities, the way terraform ships terraformInit/terraformPlan/terraformApply
  • an AnsibleApplyOp composite alongside TerraformApplyOp
  • serializer is a stub; chant never writes a playbook

What this issue should decide

  1. Wrapper or generated surface. If generated, say what a task's identity is and how order survives, because that is the part that does not obviously work.
  2. Whether the Op activities need a lexicon at all. loadActivities resolves from @intentius/chant-lexicon-<name>/op/activities, so today an activity ships inside a lexicon — but core already has a generic shell activity, so running a playbook is possible now without any of this. Decide what a lexicon buys over shell.
  3. Whether lint/audit over playbooks is wanted, given prior-art: 247 upstream rules with no chant counterpart — the baseline the monthly sweep starts from #2080 put ansible-lint outside the audit backlog. If it is not, the lexicon's value shrinks to the Op activities, and (2) decides the whole thing.
  4. Tier-1 reality check, in full. This is the part that decides whether the wrapper is worth it, so the requirements are listed rather than summarised.

The four lifecycle methods are all required. isLexiconPlugin() rejects a plugin missing any of generate, validate, coverage, package at load, not at first use. A wrapper still has to answer all four, and terraform shows what the answers look like when there is no upstream spec:

  • generate() — writes an empty registry (export {}). Not skipped: validate reads the registry and writeBundleSpec derives dist/meta.json from it. Its own comment calls an empty registry "the honest statement that this lexicon exports no generated resource classes."
  • coverage() — refuses by name: "Coverage analysis not applicable: this lexicon generates no types from an upstream spec".
  • validate() and package() — real, full implementations. No shortcut.

All 23 Tier-1 checks apply, and chant dev check-lexicon exits non-zero on any of them:

Plugin exports a LexiconPlugin; serializer with name, rulePrefix, serialize
Rules lintRules() >= 1; postSynthChecks() >= 1; every rule id under a declared prefix
LSP registers completionProvider and hoverProvider
Docs registers docs(); >= 1 .mdx page; every page reachable from the sidebar; every page names a Diataxis quadrant
Package dist/manifest.json exists and declares a chantVersion; package.json routes exports["."].default at ./src/index.ts and deletes emitted JS in build
Examples >= 1 in examples/; every shipped example builds and passes the lexicon's own post-synth checks
Intrinsics every registered intrinsic exported; isTag matches authoring; foldsAsCall only on plain calls
MCP tools and resources under one well-formed <lexicon>:<verb> namespace
Tests plugin.test.ts and serializer.test.ts both exist
Coverage coverageReport() leaves no upstream kind unaccounted (vacuous if the member is absent)

What the precedent actually cost. The terraform lexicon is the thin wrapper, and it ships 4 examples, 4 doc pages, 2 lint rules and 31 post-synth checks. "Thin" describes the absence of a generated resource surface, not the absence of work. An Ansible wrapper that clears Tier 1 is a comparable build, and the decision should be taken against that number rather than against the two-line serializer stub.

Two more from the authoring docs worth deciding here rather than discovering later. A lexicon that reads its own chant.config.ts namespace (ansible.playbooks, presumably, the way terraform.roots works) should declare a configSchema, or an unknown key inside it is silently ignored — chant dev check-lexicon warns when one is missing. And buildRoots() is the member terraform uses to turn config-declared roots into entities; an Ansible wrapper reading playbook paths from config wants the same seam.

Proof

A written decision on this issue, and either implementation issues filed against it or a recorded rejection with the reason. Refs #2314, #2080.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    designDesign or research note, no deliverable yet

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions