ADR-0007: HCL is a third view over FlowDoc, paired by file

Status: accepted, 2026-09-11

Context

Phase A keeps one interchange format, FlowDoc, with two authored views over it: the TypeScript builder (<name>.flow.ts, regenerated by codegen and re-synthesized on edit) and the studio canvas. A document has one companion source file, paired by suffix, and the bridge keeps the two in step within a second. With a Terraform provider that takes action blocks (ADR-0006), HCL becomes a third way to write the same document, and the question is whether it is a rendering target, like the emitter's output, or a view with the same standing as the .flow.ts file.

Decision

A view. A document may have a <name>.flow.tf companion instead of a <name>.flow.ts one, chosen when the document is created and switched only by an explicit flow-cli convert. The studio reads and writes the .flow.tf the way it reads and writes the .flow.ts: a canvas save regenerates it, an edit to it re-syncs the canvas, and the same dirty guard and conflict dialog apply. meta.sourceKind records which companion a document has.

The mapping between HCL and FlowDoc is a contract in this repository, conformance/hcl/README.md, with byte-exact goldens: writing a document produces exactly one resource text, reading that text produces the document again modulo meta, and the text is a terraform fmt fixed point. A pure TypeScript package, @flow-as-code/hcl, implements the reader and writer for the browser and the CLI with no wasm; the Go provider implements the reader over Terraform's evaluated configuration. Neither parses the other's output: the fixtures do.

What a .flow.tf carries that a FlowDoc does not (the refs bindings, instance_id, tags, state, lint.disable and the meta-arguments) is carried across regeneration from the previous file, never rewritten from the document; comments survive only under the @keep marker rule codegen already uses. A reference written as a resource address (aws_connect_queue.x.arn) in a reference field is sugar the TypeScript side normalizes to the key plus a refs entry on the next save; the provider refuses it and says what to write.

Consequences

Every document has exactly one companion, so a directory holding both a .flow.ts and a .flow.tf for one name is an error the watcher reports rather than a choice it makes. flow-cli convert between the two drops what the other side cannot hold: converting to TypeScript loses the bindings, the instance id, the tags and the lint settings; converting to HCL starts with null bindings and var.connect_instance_id.

What does not round-trip is recorded rather than smoothed over. Whether an action is written as a typed sub-block or as generic is not part of the invariant, since both read to the same action, and the catalog's shape, not the builder, decides which (a form the builder leaves generic can still be typed in HCL). A .flow.tf written by hand in a shape the writer would not produce is normalized on its first regeneration.

Round-trip limits

Recorded as building the reader, the writer and the provider found them. Each is held by a fixture or a test named here, so none is a promise made in prose only.