Repository navigation
ui takes the root namespace and replaces print and fail - #708
Conversation
…er it
placeModule's root path skipped a refused method with `return`, not `continue`:
for m := range module.Methods() {
snake := op.CamelToSnake(m.Name())
if refused[snake] {
return // <- abandons the rest
}
predeclared[snake] = attr
}
ReceiverType.Methods yields SORTED BY NAME from a slice, so this was not a random
loss -- every method sorting after the refused one vanished, every run. That is worse
than order-dependence rather than better: a flaky wrong answer eventually gets
investigated, a stable one reads as intended behavior.
It had never fired. No provider was RoleModule|RoleRoot, and the only candidate claims
deterministic on all six of its methods, so nothing refused anything. The branch's
sibling assertion was inverted for the same reason until #677 step 6 -- two defects in
the same unexercised code, which is a fair signal about the rest of it.
The test names its fixture methods Alpha, Bravo, Charlie so the refused one sorts
first, making the failure exact rather than dependent on what the iterator reaches
first. Verified in both directions: it fails against `return` and passes against
`continue`.
…rint and fail
Starlark ships two output builtins -- print and fail -- and devlore owned neither. A
script's print resolved to starlark.Universe and went straight to stderr through
starlark-go, escaping --silent, color, program-name prefixing, and the diagnostics
stream of 2.8-eventing-infrastructure.md. The uniformity 3.5.16 claims held for
ui.print(...) and not for a bare print(...).
+devlore:root=true closes it with no new mechanism. The resolver checks predeclared
before universal, and CamelToSnake maps Print->print and Fail->fail, so the methods
REPLACE the builtins -- no Universe mutation, no Thread.Print hook, no interception.
ui now announces RoleModule|RoleAction|RoleRoot, and four names that were never
builtins -- note, warn, error, succeed -- become globals too.
Signatures unchanged, deliberately
The builtin is print(*args, sep=" "). Widening to match fails both available ways.
Printf semantics would re-scan a string starlark's % operator already rendered,
turning "50% off" into "50%!o(MISSING)ff" whenever data contains a %. A variadic
Go-side signature receives natives after toGoInto, rendering True as true and None
as <nil>. One string is the correct contract: the script renders with str() and %,
which is starlark's own rendering, and hands over a finished string.
Tested for a property that is otherwise invisible
A script calling print("x") looks identical either way; only the destination differs.
root_test.go asserts the six globals are installed and that the ui global is NOT --
root placement replaces the provider global rather than adding to it. Proved failing
by removing the directive. Without it, a lost directive would restore the builtins
silently and every script would keep working.
Not asserted: that the OUTPUT reaches the narrator. The test covers registration, not
emission; proving emission needs a runtime execution test with a capturing sink.
Also deleted: testdata/integration.star, which called ui.success(...) -- not a method --
and claimed to exercise "all five messaging methods" when there are six. No Go test ever
loaded it. #706 covers the 14 sibling providers carrying the same orphan.
Root placement REPLACES the ui global rather than adding to it: placeModule installs
each method as its own predeclared entry and never sets predeclared["ui"]. So ui.note
stops resolving and every call site has to move. No name collided -- checked every
.star file for a module-scope assignment, local def, or parameter named error, fail,
note, print, succeed, or warn, and found none.
This lands in a second pass, which is a property of the repo worth knowing
star/extensions/*.star is both the generator's INPUT and a consumer of the API the
generator emits -- generate.star calls these methods. Sweeping before codegen knows
about the root placement fails at:
generate.star:1041:5: undefined: note
because the scripts reach for an API the binary running them does not have yet. The
order that works is: directive in with scripts unchanged, so the old star still
resolves ui.note and the gen file gains RoleRoot; then sweep and regenerate, so star
rebuilds with RoleRoot and the bare names exist. Same shape as #702's rm -f cycle --
an output that is also an input. Recorded in 3.5.16-ui-provider.md, since it applies
to any provider-surface change the generator's own extensions use.
232 sites, not the 231 surveyed: four were in the deleted testdata fixture, and the
rest are references without a trailing paren, in comments, which the survey's regex
required.
3.5.16 and its status companion move from "designed" to "landed", and gain the two-pass property: star/extensions/*.star is both the generator's input and a consumer of the API it emits, so a provider-surface change the generator's own extensions use cannot land in one pass. Corrected in the plan: it said the refused-method defect dropped survivors according to MAP ITERATION ORDER. It did not. ReceiverType.Methods yields sorted by name from a slice, so the loss was deterministic -- everything sorting after the refused method, every run. Worse than described, and the reason the regression test could be made exact.
… narrator
root_test.go proved the six names are INSTALLED as predeclared globals, which given
starlark's predeclared-before-universal resolution means a script's print resolves to
the provider. It did not prove the output arrives anywhere -- and where the bytes go is
the entire reason for taking the root namespace.
Four fixtures run in devlore-test
test_imm_ui.star all five non-fatal methods, print now among them
..._print_replaces_builtin print("a", "b") ERRORS
..._fail fail() halts and reports through the narrator
..._one_string a bool is rejected
The arity fixture is the load-bearing one: it is the only BEHAVIORAL discriminator
between our print and starlark's. The builtin is variadic, ours takes one string, so a
two-argument call failing is proof the replacement is live. Everything else about the
two is identical at the call site.
And a Go test for emission
TestUIAtRoot_PrintReachesTheNarrator executes a script through Runtime.Invoke against
a capturing sink and asserts the narrator saw the text. Proved to fail: with the root
directive removed it reports the narrator holding only the selection report, because
print fell through to the builtin and went to stderr. That is exactly the silent
regression this exists to catch.
Two fixtures I tried and could not keep
A ui.note(...) fixture is unwritable here. That is a RESOLVE error, so it fires before
t.expect_error on line 10 ever executes, no expectation is registered, and the harness
reports a hard runner error rather than a satisfied one. Any resolve-time failure is
untestable through expect_error, which is a property of the harness rather than of ui.
root_test.go covers the property by inspecting predeclared directly.
print(42) does not error -- it prints "*". Dispatch reaches op.Convert before
toGoScalar's starlark.AsString path, and there int64 counts as convertible to string,
yielding the RUNE at that code point. Measured: Convert(int64(65) -> string) = "A".
That is #709, it affects every provider method taking a string, and the fixture pins
the bool rejection while naming the integer gap.
Patterns are pinned to the real messages, not to guesses. The arity one asserts
`ui.print: got 2 arguments, want at most 1` AS IS -- naming a symbol no script can type,
which is #710 and pre-existing. Pinning it means fixing #710 has to come back here.
…ext root placement The design document asserted that print(42) reports `expected string, got int` because toGoScalar uses starlark.AsString with no coercion. That is the wrong code path and the wrong outcome. Dispatch reaches op.Convert first, where reflect.Value.Convert treats int64 as convertible to string and yields the RUNE: print(65) emits "A". Measured, not inferred. Tracked as #709; the commit message and PR body for this branch carry the same error, and the squash merge is where that gets corrected. Two traps recorded in the plan, both the same mistake -- an operation that was safe earlier is not safe once the call sites move: The negative probe deadlocks after the sweep. Removing +devlore:root=true to prove a test fails is fine before the sweep and fatal after: the swept scripts call bare note(), so a star rebuilt without RoleRoot cannot load generate.star, and codegen is the only thing that could restore RoleRoot. Recovery needs a checkout from the pre-sweep commit, since HEAD already carries the swept files. The sweep rewrites prose ABOUT the old name. Re-running it turned "the message names ui.print" into "the message names print", destroying a comment written to explain #710. A regex literal in the same file survived only because its backslash broke the pattern.
The previous commit carried 26 .star files backwards to their pre-sweep form, putting
ui.fail(...) and ui.note(...) back into the extensions. ui is not a global any more, so
every leg of CI failed at load: the extensions cannot resolve the name.
How it happened
Recovering from the codegen deadlock needed the pre-sweep scripts, restored with:
git checkout 3b06d8c -- '*.star'
That form of checkout writes the INDEX as well as the working tree. The scripts were
re-swept in the working tree afterwards, but the index still held the rolled-back
copies, and `git commit` commits the index -- not the paths named in the preceding
`git add`. So the test commit reported 32 files when two were intended, and the extra
30 undid the sweep.
Same shape as the staged deletion earlier on this branch: `git add <paths>` narrows
what is ADDED, never what is COMMITTED.
Nothing about the swept content changed -- this restores what 09fdf78 established, from
the working tree that has been running green locally throughout.
|
Note on the red run in this PR's history — it was self-inflicted, not a defect in the change. Commit
Two additions since the PR was opened, both worth a look: e2e coverage in devlore-test — four fixtures. The load-bearing one is An emission test — One correction to the PR body above: it claims Also filed from writing these tests: #710 — dispatch errors on a root-placed provider name a symbol no script can type ( |
* docs(plans): the plan template asks for a test plan Zero of 118 plans in docs/plans carry one, and the template not providing a section is why. Added between Implementation Phases and Migration Path, so new plans inherit it. The section asks for three things a bare list of tests does not: A "fails when" column per row. A test that has only ever passed has not been shown to be a test. Twice this week writing that column was the thing that caught a problem: an assertion that turned out to be unwritable in the harness at all, and a case that did not fail the way the design document claimed. Failing tests written first. A test authored after its fix has to be re-proved by breaking the fix, which is slower and sometimes impossible -- on #708 that deadlocked the build, because removing the directive broke the tool that would put it back. An explicit "not covered" list. An untested property is a decision, not an oversight, and belongs where a reviewer can weigh it. * docs(plans): number fidelity on reload -- read a number against its field Plan for #711. json has one number type, so LoadGraph's plain json.Unmarshal yields float64 for every number and an authored 0o644 reaches an fs.FileMode as a float. Convert truncates it back, which is why nothing has noticed. The rule: convert from the value we serialize to the type of the field it fills. Decode with UseNumber so the literal survives, and convert at ASSEMBLY -- assembleGraph already resolves each unit's action through the registry, so the declared parameter types are in scope there. Converting later leaves json.Number in the graph's in-memory form, and the canonical form is computed from that: a json.Number renders as a quoted string in yaml where a float64 renders bare, which is the checksum mismatch a prototype produced. Scope is parameters with a DECLARED type. An `any` slot is excluded by ruling (#712): json.Marshal(float64(42)) emits 42, so a float in an `any` slot loses its type at SAVE and no load-side fix can reach it. There the document must store the type. The test plan's row 5 is the experiment rather than a regression guard: verify the checksum does not move, BEFORE writing the conversion. Unchanged means the fix is contained in deserialization; changed means graph identity is in scope and needs a ruling before anything merges. * fix(op): a reloaded number is read against the field it fills json has ONE number type, so unmarshalling into an `any` slot cannot say whether 420 was written as an integer or a float. LoadGraph used plain json.Unmarshal, so every number in a reloaded graph came back as float64 and an authored 0o644 reached an fs.FileMode as a float. Nothing noticed because op.Convert truncated it back -- the conversion layer was compensating for a serialization defect. Two things were already true, before #709 touched anything The codecs disagreed. yaml.v3 decodes an integer as int; json yielded float64. The same graph reloaded to different in-memory values depending on which codec wrote it. float64 cannot represent every int64. A large enough integer in a saved graph was ALREADY corrupted on reload; truncation made the corruption look like a clean number. The rule: convert from the value we serialize to the type of the field it fills LoadGraph decodes with UseNumber, so json.Number keeps the literal text. assembleBindings reads it against the parameter -- assembleNode has already resolved the action through the registry, so the declared types are in scope at exactly the point the value is bound. Yaml goes through the same path. It keeps an integer an integer, but has no notion of the fs.FileMode the field declares, so both codecs now reload to identical values. NUMBERS ONLY, which the first implementation got wrong Reading EVERY value against its field is the honest reading of the rule and it breaks a ruling. Convert has a registered-Resource-construction step, so the URI string a resource-typed slot carries was built into a Resource at load time -- and §5.6 of docs/plans/resource-construction.md says that string stays a KEY until dispatch resolves it through the run catalog. The canonical form is computed from these values, so constructing early moved the checksum: op.LoadGraph: checksum mismatch: document "sha256:8e5bf1a1..." recomputed "sha256:1bcc33d7..." That mismatch was the design refusing a rule violation, not a fragile hash. Narrowed to numbers, checksums are unchanged -- verified, which was the plan's step 5 and the question that decided whether this reached graph identity. It does not. Two tests that passed for the wrong reason, both caught by asking which path ran The range test edited a saved document, so LoadGraph refused it for a CHECKSUM mismatch and the test's early return fired without ever reaching range handling. Moved to Convert, where the behavior lives. The ordering it exposed is worth keeping: integrity is a stronger gate than range and runs first, so a graph-level range test cannot reach the code it claims to cover. Step 8 asked whether this resolves the five save/load failures #709 produced. Running those five here proves nothing -- they pass on develop too, and only fail once #709's rule removes the truncation. Pasting that rule on temporarily: all five pass, and the only remaining failure is #709's own convert_struct_test.go, a literal float64 in a map reaching an int field with no json involved. An `any` parameter is out of scope by ruling (#712): json.Marshal(float64(42)) emits 42, so a float in an `any` slot loses its type at SAVE and no load-side fix can reach it. There the document must carry the type. * docs(plans): number fidelity complete -- what step 5 and step 8 actually measured Step 5 was written as the experiment rather than a regression guard: verify the checksum does not move. It answered yes, but only after the first implementation moved it by violating §5.6's resource-construction ruling -- which is a more useful answer than the one the step was written to get. It says which values may be interpreted at load time at all, not merely whether the hash is stable. Step 8 records why checking #709's five failures on this branch proves nothing, and what was done instead. --------- Co-authored-by: David Noble <[email protected]>
… about it (#717) * docs(op): root applies to every surface, and the surface-to-role mapping is written down Three corrections, all surfaced by ui becoming the first RoleModule|RoleAction|RoleRoot provider (#708) and making differences observable that had never been exercised. RootProviders described a filter that does not exist Its doc said callers filter the returned slice by dispatch zone and named plan.Provider as the example that "filters to RoleAction". plan.Provider does not filter -- buildPromotedBuiltins passes registry.RootProviders() through untouched, so every method of every root provider is promoted into plan.*. The code is correct; the comment described an intent never implemented. Corrected, with a note on why nobody noticed: flow is the only other root provider and it is surface=graph, so the distinction had no observable consequence until ui arrived holding both dispatch bits. RoleRoot now states its intent so it cannot be lost One bit, every surface, deliberately. Placement is orthogonal to dispatch, so a provider holding both dispatch bits surfaces flat in both namespaces from a single +devlore:root=true: note() in a script AND plan.note() in a graph. There is no way to be root in one zone and nested in the other, and nothing a provider could mean by asking -- a name is promoted because it reads better without its qualifier, and that is as true of a graph as of a script. The surface-to-role mapping existed only in generate.star As a switch, with no prose anywhere. The provider-development guide's directive table listed lifetime and bind and omitted BOTH surface and root. Now in the guide and in 3.6-method-classification.md: absent (default) RoleModule|RoleAction + root -> |RoleRoot graph RoleAction + root -> |RoleRoot module RoleModule + root -> |RoleRoot Both documents also carry the root-applies-everywhere rule, so a reader arriving at either one gets it. Related: #715 renames these roles, because surface=graph produces RoleAction -- two vocabularies for one idea -- and the op->workflow rename retires both words. * docs(plans): the rename plan says what is stale about it, and what it lacks The plan presented 2026-05-26 measurements as current and claimed a prerequisite that has been satisfied for months. A reader arriving at it got stale facts with no sign that a re-audit was chartered -- every finding lived in issues the plan did not reference. A banner up front: prerequisite satisfied, numbers stale, do not start a phase from them, with pointers to #716 and #715. Prerequisites corrected in place, with the old blocker struck through rather than deleted. It was true when written, and the record should show that. Staleness (measured 2026-08-27), beside the Current State it corrects: root .go files 62 -> 108, provider files 76 -> 238, qualified op.X references ~2,499 -> 3,880. The subpackage list is wrong in both directions -- sops/ has left, claimcheck/ and server/ arrived. What still holds: all five renames are well-formed and the collision claim survives, Step being declared only in internal/console. New evidence FOR the target, also recorded: plan.Provider now exposes AssembleDefinition, LoadDefinition, and SaveDefinition, all returning *op.Graph, and scripts read plan.assemble_definition(...). The API already calls a graph a definition while the Go type is still Graph. The rename closes a gap that widened on its own. Appendix: Name Mapping -- TO BE WRITTEN. The Rename Mapping section is five rows, the top-level types, and nothing maps the derived identifiers -- 53 exported identifiers contain Graph, 37 contain Subgraph, 18 contain RecoveryStack, and 14 files carry a renamed word in their name. Goal 4 promises reviewers can verify by inspection; without this appendix there is nothing to inspect against. The appendix section works through why it needs judgment rather than derivation. Node is ambiguous by construction: it maps to Step AND is the target of ExecutableUnit, so it appears on both sides. GenerateNodeID is the proof -- flow/helpers.go calls it as GenerateNodeID(string(Subgraph)), so it mints ids for subgraphs and names the ABSTRACT VERTEX, which under the new taxonomy keeps the name Node. A mechanical Node -> Step sweep would rename it GenerateStepID and quietly make it wrong. And the requirement that identifiers KEEPING their name be listed explicitly: a table of only the changes cannot distinguish "correctly unchanged" from "missed". Related Rename: the provider roles, pointing at #715. Status stays draft. Leaving draft is #716's last acceptance item, and should not happen while the numbers are annotated rather than re-measured. --------- Co-authored-by: David Noble <[email protected]>
…lue (#723) Plan for #715, under the op->workflow rename epic (#451). The defect: +devlore:surface=graph produces RoleAction and +devlore:surface=module produces RoleModule -- two vocabularies for one idea, connected only by a switch in generate.star. An author writes graph and the generated code says Action. The rename forces it: Graph becomes Definition, so surface=graph will name a type that no longer exists, and module is starlark's word for a namespace rather than a term in the new taxonomy. Neither current word survives. The target +devlore:surface=script|workflow a list; default both +devlore:placement=promoted one value; default qualified type ProviderFlags uint16 packs both type Surfaces uint8 BIT FIELD -- one or more type Placement uint8 VALUE -- exactly one The zones are not the same kind of thing, and the design says so three ways Plural for the set, singular for the value: flags.Surfaces() returning SurfaceScript|SurfaceWorkflow reads correctly, where flags.Surface() returning two things reads as a contradiction. Someone reading only the accessor signatures learns which zone is which. Distinct types per zone, following reflect, where flag.kind() returns Kind rather than another flag. That makes flags.Placement() == SurfaceScript a compile error rather than nonsense that compiles. C# disagrees -- MethodAttributes packs a MemberAccessMask zone and flag bits in one enum -- and Go's answer was taken for the type safety. PlacementQualified = 0x00 is the zero VALUE of an enumeration, not an absent flag, so nothing can express "promoted and qualified" -- not because a check rejects it, but because the zone holds one value of a type with two. Why ProviderFlags rather than something specific ProviderRole was specific and went stale: #677 changed the zone from invocation mode to surface eligibility, and RoleAction, Dispatch(), and ProviderRole all outlived what they named. A deliberately generic name cannot fail that way, and the accessors carry the meaning. Precedent: 14 *Flag/*Flags types in Go's stdlib, and C#'s BindingFlags. The bootstrap is the hard part, and it has no two-pass escape generate.star emits the constant names into every .gen.go. Renaming them breaks all 58 generated references at once; the tree stops compiling; star cannot rebuild; and codegen -- the only thing that could rewrite those files -- cannot run. Unlike #708 there is no ordering that avoids it, because the constant name and its generated use must change together. The LKG escape hatch is exactly this case: STAR resolves to build/star.lkg when it exists, and build/star: FORCE is never consulted, so a prebuilt star regenerates a tree it could not itself compile. cmd/devlore-inventory does not import pkg/op (verified), so make generate's inventory step survives too. make star-lkg is therefore step 1 and a PRECONDITION, not a convenience: taken after the rename begins it would snapshot a binary built from a tree that no longer compiles. Scope: ~400 references, 58 of them generated, and FOUR authored directive lines -- ui, plan, and flow are the only providers that declare anything. Co-authored-by: David Noble <[email protected]>
…alue (#726) Closes #715. +devlore:surface=graph produced RoleAction and +devlore:surface=module produced RoleModule -- two vocabularies for one idea, connected only by a switch in generate.star. An author wrote graph and the generated code said Action. The op->workflow rename forces the issue: Graph becomes Definition, so surface=graph would name a type that no longer exists, and module is starlark's word for a namespace rather than a term in the new taxonomy. Neither word survives. +devlore:surface=script|workflow a list; default both +devlore:placement=promoted one value; default qualified type ProviderFlags uint16 packs both type Surfaces uint8 BIT FIELD -- SurfaceScript, SurfaceWorkflow type Placement uint8 VALUE -- PlacementQualified, PlacementPromoted The zones are not the same kind of thing, and the design says so three ways PLURAL for the set, singular for the value. flags.Surfaces() returning SurfaceScript|SurfaceWorkflow reads correctly; flags.Surface() returning two things would read as a contradiction. The accessor signatures alone say which zone is which. DISTINCT TYPES per zone, following reflect, where flag.kind() returns Kind rather than another flag. flags.Placement() == SurfaceScript is now a compile error rather than nonsense that compiles. C# disagrees -- MethodAttributes packs a MemberAccessMask zone and flag bits in one enum -- and Go's answer was taken for the type safety. PlacementQualified = 0x00 is the zero VALUE of an enumeration, not an absent flag, so nothing can express "promoted and qualified" -- not because a check rejects it, but because the zone holds one value of a type with two. Why ProviderFlags rather than something specific ProviderRole was specific and went stale: #677 changed the zone from invocation mode to surface eligibility, and RoleAction, Dispatch(), and ProviderRole all outlived what they named. Dispatch() -> Surfaces() for the same reason. A generic name cannot fail that way, and the accessors carry the meaning. Precedent: 14 *Flag/*Flags types in Go's stdlib, and C#'s BindingFlags. The bootstrap had no two-pass escape generate.star emits the constant names into every .gen.go, so renaming them broke all 58 generated references at once, the tree stopped compiling, star could not rebuild, and codegen -- the only thing that could rewrite those files -- could not run. Unlike #708 no ordering avoids it: the constant name and its generated use must change together. make star-lkg first, so a prebuilt star could regenerate a tree it could not itself compile. It took five iterations of generate.star scope errors to get through, and the tree was uncompilable during all of them; each would have been unrecoverable without it. The LKG was then removed and a source-built star reproduced all 159 files identically, proving it was an escape hatch rather than load-bearing. A property that was documented three times and tested zero times #717 established that ui, reaching both surfaces, promotes six names into plan.* as well as installing six globals. That claim lived in three doc comments and no test -- and it is the half of placement this rename routes through new code. TestProvider_ResolveAttr_Tier2_PromotedAcrossBothSurfaces now asserts it, with a negative case proving file.copy stays qualified. Without the negative half it would pass on a change that promoted everything. Also: promoteRootMethods -> promoteMethods and rootNames -> promotedNames, resolving an identifier that carried two words for one idea. Placement test functions renamed; fsroot, run-root, scoped-root, and git-root names deliberately untouched -- a word-level sweep over "root" would break their meaning without breaking the build (#725 tracks the last two artifacts). The accessors mask explicitly rather than relying on the conversion to truncate, which gosec G115 caught: the bare form is correct only while both zones are a byte wide. Co-authored-by: David Noble <[email protected]>
…ed (#728) Closes #710. >>> print("a", "b") ui.print: got 2 arguments, want at most 1 ui.print does not exist. ui is promoted, so its methods ARE the top-level globals and the provider name is not a symbol any script can use -- an author who takes the error at its word and writes ui.print(...) gets `undefined: ui`. The error sent them toward a second, unrelated error, which is worse than a vague message. Pre-existing, not introduced by #708: flow has done this since promotion existed. The rule Where a call site exists, an error names what the author typed; where none exists, it names the action. The two cannot be one name because the action name is a node's identity field -- it is in the serialized document and the checksum -- so tying it to placement would rewrite every saved workflow when a provider's placement changed. print("a", "b") script evaluation -- the call site is right here -> print: got 2 arguments, want at most 1 plan.failed("db down") runs later, possibly from a workflow loaded off disk -> flow.failed executed: db down unchanged; there is no call site to name Three sites, and each already knew its own surface starlarkbridge/go_receiver.go script consults Placement(): bare when promoted plan/provider.go workflow promoted -> plan.<method> plan/adapter.go workflow qualified -> plan.<provider>.<method> Only the first consults placement. The other two ARE the promoted and qualified paths, so the site itself determines the shape. This is what #715 unblocked. Before it, placement was a bit tangled with dispatch and "which name would the author have typed" had no clean answer. flags.Placement() == op.PlacementPromoted is now a one-line question. actionName was a misnomer, and it made the change look risky Both workflow sites named the variable actionName and passed it to dispatchBuiltinBody, which reads as though changing it would move the serialized action name. It does not: that function uses the string ONLY in error text, and a node's action name comes from unit.Action().Name() at marshal. A display string was wearing an identity's name. Renamed to callSiteName at both sites, with a comment saying which it is. Step 1 classified every fixture before anything changed Four expectations name a provider-qualified symbol; exactly ONE moves. test_imm_ui_print_replaces_builtin is script evaluation. The three file.* expectations and the flow.* one are execution errors and stay exactly as they are. "Runs a graph" is the mechanical discriminator -- every execution fixture calls t.run(graph). The change then confirmed the classification rather than merely being consistent with it: those four fixtures passed untouched, because they never reach these sites. Tests assert both directions A promoted provider reports the bare name AND a qualified one keeps its qualifier -- together, because a fix that made every name bare would satisfy the first alone. And flow.choose, flow.gather, and flow.failed must still resolve through BuildAction: a saved workflow says flow.choose and has to load. test_imm_ui_print_replaces_builtin pinned `ui\.print: got 2 arguments` deliberately, with a comment saying the fix to #710 had to come back through it. It did. Co-authored-by: David Noble <[email protected]>
Closes #700. Implements #698.
Starlark ships two output builtins —
printandfail— and devlore owned neither. A script'sprintresolved to
starlark.Universeand went straight to stderr throughstarlark-go, escaping--silent, color,program-name prefixing, and the diagnostics stream of
2.8. The uniformity
3.5.16claims held forui.print(...)and not for a bareprint(...).+devlore:root=truecloses it with no new mechanism. The resolver checks predeclared names beforeuniversal ones, and
CamelToSnakemapsPrint→printandFail→fail, so the methods replace thebuiltins — no
Universemutation, noThread.Printhook, no interception code. Four names that were neverbuiltins —
note,warn,error,succeed— become globals too.Signatures unchanged, deliberately
The builtin is
print(*args, sep=" "). Widening to match fails both available ways:%operator already rendered, turning50% offinto50%!o(MISSING)ffwhenever the data contains a%. Go avoids this by makingPrintandPrintfdifferentfunctions; one function cannot be both.
toGoInto, renderingTrueastrueandNoneas<nil>— Starlark's vocabulary is gone by the time the method sees the arguments.One string is the correct contract: the script renders with
str()and%, which is Starlark's own rendering,and hands over a finished string. The tree already worked this way — 64
printand 65failcall sites, zeromulti-argument, zero non-string.
A defect in the branch this activates
placeModule's root path skipped a refused method withreturnrather thancontinue, abandoning everymethod the loop had not reached.
ReceiverType.Methodsyields sorted by name, so the loss wasdeterministic — everything sorting after the refused method, every run. That is worse than order-dependence,
not better: a flaky wrong answer eventually gets investigated, a stable one reads as intended behavior.
It had never fired: no provider was
RoleModule|RoleRoot, anduiclaimsdeterministicon all six methods,so nothing refuses them. This PR arms it. The branch's sibling assertion was inverted for the same reason until
#677 — two defects in the same unexercised code.
Tested for properties that are otherwise invisible
Both tests were proved to fail before being trusted:
root_test.go+devlore:root=trueis removedTestNewRuntime_ModuleRoot_RefusedMethodDoesNotDropTheRestcontinuereverts toreturnA script calling
print("x")looks identical whichever implementation it reaches — only the destinationdiffers. Without the first test, a lost directive would restore the builtins silently, every script would
keep working, and output would quietly return to stderr.
Not asserted: that the output reaches the narrator. These cover registration, not emission; proving
emission needs a runtime execution test with a capturing sink.
Landing it takes two passes
star/extensions/*.staris both the generator's input and a consumer of the API the generator emits —generate.starcalls these methods. Sweeping before codegen knows about the root placement fails at:The order that works: directive in with scripts unchanged (old
starstill resolvesui.note, gen file gainsRoleRoot), then sweep and regenerate (starrebuilds withRoleRoot, bare names exist). Same shape as#702's
rm -fcycle — an output that is also an input. Recorded in3.5.16-ui-provider.md, since it appliesto any provider-surface change the generator's own extensions use.
The sweep
232 call sites across 27 files. Root placement replaces the
uiglobal rather than adding to it, soui.notestops resolving. No name collided — every.starfile was checked for a module-scope assignment,local def, or parameter named any of the six.
Also deleted:
pkg/op/provider/ui/testdata/integration.star, which calledui.success(...)— not a method —and claimed to exercise "all five messaging methods" when there are six. No Go test ever loaded it. #706
covers the 14 sibling providers carrying the same orphan, 305 lines that nothing executes.
Verified
make check,make test-race,make test-scenario— all pass.