Skip to content

refactor(compilers/openapi): define the yaml-node builders once - #373

Merged
OmarAlJarrah merged 3 commits into
mainfrom
refactor/openapi-shared-yaml-builders
Aug 13, 2026
Merged

refactor(compilers/openapi): define the yaml-node builders once#373
OmarAlJarrah merged 3 commits into
mainfrom
refactor/openapi-shared-yaml-builders

Conversation

@OmarAlJarrah

@OmarAlJarrah OmarAlJarrah commented Aug 9, 2026

Copy link
Copy Markdown
Member

Summary

internal/nodeview and internal/scan each carried their own copy of the same six
yaml-node builders — yscalar, ymap, yseq, yalias, ymerge, mergeChain — byte for
byte in four cases. mergeChainSpec was a seventh copy, shared between internal/scan and
the compiler package's own tests.

internal/sourceindex landed while this branch was open with a copy of its own — four of
the same builders, byte for byte — so it is folded in here too. Its allowlist is left alone,
unlike nodeview's: archtest reads production sources only, and sourceindex reaches
ynode from an internal test, so listing the edge would claim a dependency the checked graph
does not have.

Direction taken: one definition, not a comment explaining why there are two.

The issue offered both and asked for a decision. What kept the family apart was that
ymerge needs the merge tag and the tag lived in nodeview, so any shared home would have
had to import nodeview — and nodeview's own tests are one of the two callers. That
constraint is real, and I checked it by construction rather than by reading: a probe package
importing nodeview, imported from nodeview's internal test file, fails to build with

imports .../internal/cycleprobe from probe_cycle_internal_test.go
imports .../internal/nodeview from probe.go: import cycle not allowed in test

But the constraint only blocks a home above nodeview. Moving the tag below it removes
the problem entirely, and the same probe inverted — a package nodeview imports, imported
from nodeview's internal test — builds and passes. So the family gets a home:
compilers/openapi/internal/ynode holds MergeTag and the constructors that spell the node
shapes it names, nodeview.IsMergeKey reads the tag back, and both packages' tests build
nodes from the one definition.

MergeTag is a fact about yaml.v3 and speakeasy's yml.IsMergeKey, not about the view, so
it reads no worse one level down. IsMergeKey itself stays in nodeview — it is the view's
predicate, used by the view's expansion — and keeps the comment explaining what the tag
means and why the check is spelled the way it is.

mergeChainSpec moves too, as ynode.MergeChainSpec: it is the same merge-chain fixture in
source form, spelling the same << chain whose tag the package defines.

ynode gets its own entry in internal/archtest's rules map (gopkg.in/yaml.v3 and
nothing else) and nodeview's entry gains it, with the import constraint written down as
the reason the package sits where it does. It also gets its own tests: without -coverpkg a
package is instrumented only by its own test binary, so one with statements and no test
files contributes zero-count blocks to the profile and fails the coverage gate — confirmed
against the probe package, which reported coverage: 0.0% and put a zero-count block in the
profile while its only callers were elsewhere.

Test plan

Every builder is defined exactly once. The issue's own grep now matches nothing:

$ grep -rn '^func \(yscalar\|ymap\|yseq\|yalias\|ymerge\|mergeChain\)(' --include='*_test.go' compilers/openapi
$ grep -rn 'func mergeChainSpec' --include='*.go' .
$ grep -rn '^func \(Scalar\|Map\|Seq\|Alias\|Merge\|MergeChain\|MergeChainSpec\)(' --include='*.go' compilers/openapi
compilers/openapi/internal/ynode/ynode.go:25:func Scalar(v string) *yaml.Node {
compilers/openapi/internal/ynode/ynode.go:31:func Map(pairs ...*yaml.Node) *yaml.Node {
compilers/openapi/internal/ynode/ynode.go:36:func Seq(items ...*yaml.Node) *yaml.Node {
compilers/openapi/internal/ynode/ynode.go:42:func Alias(target *yaml.Node) *yaml.Node {
compilers/openapi/internal/ynode/ynode.go:48:func Merge() *yaml.Node {
compilers/openapi/internal/ynode/ynode.go:56:func MergeChain(levels int) *yaml.Node {
compilers/openapi/internal/ynode/ynode.go:72:func MergeChainSpec(levels int) string {

This is a test-infrastructure change, so the risk is a silently weakened test rather than a
broken one. Three things were checked:

The moved builders produce identical nodes. A throwaway test compiled the pre-move
definitions verbatim from git HEAD alongside the moved ones and compared their output —
cmp.Diff over every scalar value the callers use, a nested tree built by both sets, and
MergeChain/MergeChainSpec at levels 0, 1, 2, 3, 7, 64, 66, 200 and 1600. Green.
MergeChainSpec's body also diffs clean against the original line for line.

No test changed meaning. Comparing go test ./... -list '.*' before and after, the only
difference is additions — 7 new tests, all in the new package, zero removals or renames
(1270 names before, 1277 after).

The new tests were checked against planted defects rather than read:

planted defect result
MergeTag becomes "!!MERGE" RED in three packages — ynode's TestMerge_MatchesTheParsedMergeKey, nodeview's TestIsMergeKey_AgreesWithParsedTags and TestMappingPairs_MergeKeySources, scan's TestDetectCycles_Reproducers
MergeChain's leaf pair becomes {leaf: w} RED — ynode's TestMergeChain_NestsOneMergePerLevelOverALeaf and nodeview's TestNodeView_TruncationIsPerNode
MergeChainSpec stops writing paths: {} initially GREEN everywhere but the throwaway — see below

That first row is the load-bearing one: the mutation reddens production behaviour in
nodeview and scan, not just the new package's own tests, which is what shows the moved
constant is still the one IsMergeKey reads.

The third row is why an assertion was added. Dropping paths: {} from the fixture reddened
nothing in the repository — 3.1 makes paths optional, and the fixture still parsed,
compiled and satisfied both callers. TestMergeChainSpec_AnchorsEveryLevelAndNamesItFromASchema
now asserts the preamble key by key, and re-planting the defect reddens it.

Full gate green, coverage at exactly 100% including the new package. The statement count is elided from the transcript below: the gate's total depends on test-cache warmth (#369), so it would describe the cache rather than the branch.

--- [1/5] gofmt --- OK
--- [2/5] go vet --- OK
--- [3/5] golangci-lint --- 0 issues.
--- [4/5] go build --- OK
--- [5/5] coverage gate ---
ok  github.com/dexpace/morphic/compilers/openapi/internal/ynode  coverage: 100.0% of statements
Coverage gate passed: all statements covered.

Out of scope

pairMap is still a per-package copy in nodeview and scan. It takes []nodeview.Pair,
so a shared home for it would have to import nodeview — the cycle above, and not one
moving a constant can fix. cycleReproducers and readReproducer are also duplicated
between internal/scan and the compiler package's tests; both are unrelated to the merge
tag.

Closes #325

internal/nodeview and internal/scan each carried their own copy of the same
six yaml-node builders, byte for byte in four cases, and mergeChainSpec was a
seventh copy shared between internal/scan and the compiler's own tests.

The copies survived because ymerge needs the merge tag and the tag lived in
nodeview: a home above nodeview would be one nodeview's own internal tests
could not import, since an internal test file cannot import a package that
imports its own package. Rather than split the family across two homes, the
tag moves down instead. compilers/openapi/internal/ynode holds MergeTag and
the constructors that spell the node shapes it names, nodeview imports it for
IsMergeKey, and both packages' tests build nodes from the one definition.

MergeTag is a fact about yaml.v3 and speakeasy rather than about the view, so
it reads no worse one level down, and the predicate that tests for it keeps
the comment explaining what the tag means.

ynode carries its own tests: without -coverpkg a package is instrumented only
by its own test binary, so one with statements and no test files contributes
zero-count blocks to the profile and fails the coverage gate.
sourceindex landed while this branch was open, carrying its own copy of
the four node builders — byte for byte the ones ynode now defines. The
branch consolidated the copies it could see, so merging main left the
count at one rather than none, and "define the builders once" no longer
described the tree.

sourceindex's internal tests build the same nodes for the same reason the
view's and the scan's do, and ynode sits below it as it sits below them,
so the same argument applies unchanged: the definitions go and the call
sites name ynode.

Its allowlist is left alone, unlike nodeview's. archtest reads production
sources only, and only nodeview imports ynode from one; sourceindex reaches
it from an internal test, which the layering check never sees. Listing the
edge there would claim a dependency the checked graph does not have.
@OmarAlJarrah
OmarAlJarrah force-pushed the refactor/openapi-shared-yaml-builders branch from 038ee09 to bf3bf2a Compare August 13, 2026 10:23
@OmarAlJarrah
OmarAlJarrah merged commit 6f18478 into main Aug 13, 2026
1 check passed
@OmarAlJarrah
OmarAlJarrah deleted the refactor/openapi-shared-yaml-builders branch August 13, 2026 10:40
OmarAlJarrah added a commit that referenced this pull request Aug 13, 2026
The branch forked before #328, #356 and #373, so three of the four files
needed more than a textual resolution:

- The test helpers it calls were consolidated while it was open. ymap,
  yscalar, yalias and ymerge became ynode.Map/Scalar/Alias/Merge in #373,
  and yamlNode became openapitest.YAMLNode in #328. The bodies are
  identical, so these are renames.
- TestRawChildNode_IsNotTheMergeAwareView asserted that a repeated key
  resolves to opposite ends in the two readers. #356 made RawChildNode
  take the last pair, as the parser does, so they now agree. The case is
  rewritten to assert the agreement rather than deleted: a reader
  drifting back to first-wins is worth failing on.
- nodeview.go kept main's DocumentPath, walkPointer and tokenless, and
  main's ynode.MergeTag over the local const this branch predates. The
  index is additive to all of it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

openapi: the nodeview/scan yaml-node builders are still a per-package copy

1 participant