architecture: invariant 1's line between "adds behavior" and "duplicates schema-owned shape" is a judgement, not a check — decide the criterion and hold it
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 9
- Forks
- 0
- Avg merge
- 3h 3m
- Merged PRs (30d)
- 509
Description
Problem
Invariant 1 says boundary-crossing shapes are defined once in Protobuf, and that hand-written Go types "may add behavior, not duplicate schema-owned shape". Which side of that line a given type falls on is currently decided per review, by argument. The argument is usually reconstructible but it is never written down, so the same question gets re-litigated at each new type and the answer depends on who is reading.
The immediate prompt was engine.heldFailure in #1975, and the answer there happens to be clear once the right question is asked — but it took reading three call sites to establish, and nothing records the finding for the next reader.
Evidence
engine.heldFailure is {id string; err error} and v1.HeldFailure has five scalar fields covering the same failure. Superficially a duplicate. It is not one, because three behaviors depend on the Go value's identity rather than on its fields:
temporal.IsCanceledError(err)at the debug drain (engine/execute.go).classifyRunError(engine/workflow.go:441) rewraps any*ErrRunFailedas a Temporal application error, and its own doc comment turns on a cancellation never being one. Flatten the failure at hold time anderrors.Asstarts finding it, so a cancelled run reports FAILED and takes its failure compensations.ErrRunFailed.Unwrap()→cause.classifyRunErrorpasses the whole error asCause, so Temporal's failure converter walks into #1163's retained inner attempt. A held failure raised in the same segment keeps it.recordedOwn *v1.Node_Outputs, the richer transcript entry an exhausted loop or a failed switch files.
v1.HeldFailure is a deliberately lossy projection of that — cause and recordedOwn are dropped on purpose and the narrowing is documented on the field. Using the proto as the in-memory type moves that loss from "across a seam" to "always", which would make a debugged run report differently from an undebugged one: exactly the property #1119 and #1968 exist to protect.
So the criterion that actually decided it was not "do the fields overlap" but "does anything depend on this value's identity, its method set, or a chain the proto cannot hold". That question is answerable mechanically far more often than the shape comparison is, and it is the one worth writing down.
Desired outcome
A stated criterion for invariant 1, and — where it can be made one — a check rather than a reviewer's judgement. A new Go type that mirrors a proto message's fields without adding behavior of that kind should be visible as such, and a type like heldFailure should pass without anyone having to reconstruct the argument.
Acceptance criteria
docs/ARCHITECTURE.md's invariant 1 states the criterion, with one example either side of the line.- An audit of the existing hand-written types that pair with a proto message, each classified against it; the ones that fall on the wrong side become their own issues rather than being fixed here.
- Whatever part of the criterion can be checked is checked, and the part that cannot is named as a judgement with the question a reviewer should ask.
Constraints and dependencies
- This is a criterion plus an audit, not a sweep. #521 forbids applying a mechanical rewrite across the tree as a side effect of a narrow task, and a conversion boundary that exists for a real reason must survive the audit intact.
- A check here shares a shape with #1970's — "walk a call graph, find the sites, let a site declare its own exemption" — and the same open question about
golang.org/x/tools/go/callgraphversus an AST approximation applies. Worth deciding once for both. - The existence of
heldAcross/heldFrom-style conversion pairs is not itself the defect. Proto-first expects a boundary conversion; what invariant 1 forbids is a second place the shape is defined.
Open questions
- Is "something depends on the value's identity, method set, or unwrap chain" the whole criterion, or only the sufficient half? A type that adds no behavior today but exists to keep a package from importing the schema is a different argument, and the audit will say whether any such type exists.
- Should the criterion also cover the reverse direction — a proto message introduced for something that never crosses a boundary?
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with invariant 1 in docs/ARCHITECTURE.md, then inspect engine/execute.go, engine/workflow.go:441, and the hand-written types paired with proto messages. Review #1970's call-graph versus AST-check question and the heldFailure evidence from #1975. Done means the criterion and examples are documented, existing pairs are audited, and any feasible check plus its remaining reviewer judgement are recorded.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- documentation, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100