Repository navigation
Providers claim what they guarantee, and a type-checked pass holds them to it - #697
Merged
Merged
Conversation
added 8 commits
August 25, 2026 11:45
…er provider's identity
The provider-level access directive is gone from the documentation. Provider design docs lose it from their header lines; the directive tables in 3.1-provider-loading, 3.2-projected-provider-api, and 3-operation-namespaces are removed; and the guides stop instructing developers to write it. Plan documents keep their historical prose with the literal token replaced. New plans, one per defect found while verifying the provider catalog: elevation-provider-rename.md #679 6.1 settled the name; the tree never received it method-claims.md #677 the replacement scheme: claims, not tiers regex-provider-naming.md #678 the code renamed to regex; the docs did not template-purity-claim.md #680 documented pure, reads the host environment method-classification.md becomes method-claims.md: the scheme is a claim namespace over deterministic, idempotent, and sandboxed, not a three-tier classification. The Go and generated changes for the elimination stay uncommitted. The tree is red until #677 lands -- 34 packages failing, 201 tests -- because nothing supplies namespace placement yet.
… code removal Several changesets, kept together because the tree cannot go green until #677 lands and splitting them would leave every intermediate commit red for a different reason. The access directive, code side (#677 supplies the replacement) The +devlore:access directives are gone from the provider sources, generate.star hardcodes the documented default, and every *.gen.go is regenerated from it. This is what makes the tree red: all 19 providers now announce RoleModule only, so the plan.* namespace is empty. 34 packages, 201 tests. The 10 new module.gen_test.go files are generator output for providers that previously had no module side. goast's production_test.go carried the directive as fixture text rather than as an annotation, so it moves to +devlore:root=true. fsroot speaks one word: sandbox (#685) OpenConfined becomes OpenExisting -- both constructors are sandboxed, so the pair names the two lifetimes, the only axis on which they differ. confinedDir and confinedScratchDir become sandboxedDir and sandboxedScratchDir. The package doc said "confined", the Dir doc said "scoped", and the plan title said "scoping"; all three are now "sandboxed", the term Bazel uses for the mechanism. The `// Confinement:` convention becomes `// Unsandboxed:`, which names what those 11 comments actually mark: a deliberate bypass, with its reason. 4.5-fsroot-variants.md is retitled to match, and env-minted-root.md gains a supersession note -- it is marked complete while describing a Mode type and three constructors that appear in zero Go files. The dead nil guards are gone (#669) graphMetadata.origin is typed OriginBase, a concrete value, so Graph.Origin() never returns nil and staticcheck was right about all four SA4023 findings. The guards are removed and the invariant is documented on Origin() itself, so the next reader does not re-add them. The local gate matches CI (#670, partial; #686) go.mod moves to 1.27 and CI follows, so both sides analyse with the same semantics. golangci-lint is pinned to v2.13.1 on both sides -- the skew against CI's v2.12.2 is what hid those four findings from CI. make lint now runs `star lint go`, the gate CI actually carries, rather than a bare golangci-lint. Routing through star exposed #686: its tidy check ran `go mod tidy` and then diffed against HEAD, so it could not tell a change tidy made from one the caller made -- every uncommitted go.mod edit failed, and the check gated the very commit that would clear it. It now compares against a pre-tidy snapshot and restores the originals on mismatch, so it reports without rewriting the tree. make lint and make vet are green. make test is not, and stays that way until #677.
…ntime filter Phases A, B, and C of #677. make check exits 0 -- 103 packages passing, zero failures, down from 34 failing packages and 201 failing tests. The type op.MethodClaims is a bit set over ClaimDeterministic, ClaimIdempotent, and ClaimSandboxed. It rides the rails MethodModifiers already used: codegen emits it onto MethodMetadata, the announcement path stamps it onto the Method. Claims are asserted and checked, never derived -- a derived property flips silently when a helper three calls down grows a dependency, where a false claim fails the build. Method.Mutates() is the deliberate exception: kind == MethodCompensableFunction, covering both *Receipt and *RecoveryStack returns. Sufficient, not necessary -- shell.exec and git.pull mutate without returning either -- and its doc says so. The directive +devlore:claim=a, b, c follows the +devlore:defaults precedent: comma-separated on one line, sorted on the way out so author order never churns a regenerated file. Codegen refuses a deterministic claim on a mutating signature and quotes it back: method Mkdir: claims deterministic but returns "(*Directory, *Receipt, error)", which mutates by construction regex.Match carries the first real claim in the tree. Surface eligibility The access directive was vague and its role assignment could not be reproduced from any property of the code -- four providers with zero mutators were "both", two with zero mutators were "immediate". +devlore:surface= replaces it and maps straight onto the existing dispatch zone: graph is RoleAction alone, module is RoleModule alone, absent is both. RoleModule now means ELIGIBLE FOR a module surface, not "immediate mode". Two providers declare, and they are opposites: flow is graph-only (its methods ARE the graph combinators), plan is module-only (no scenario for planning the planner). The other sixteen are both; mem announces nothing, having no Provider. Consequence worth naming: ui and star's lint provider were immediate-only before and are now eligible for both, so each gains action_names.gen.go and gen/action.gen_test.go. The runtime filter Hermetic() is a RuntimeOption. admits(method) is `!hermetic || claims deterministic` -- per METHOD, never per provider, because a hermetic runtime wants file.join and not file.write_text and no bit on the provider can say that. The filter is consulted at install time rather than through applyDenials, so it reaches both placement paths: a non-root provider is wrapped in a filteredReceiver carrying the refused set, a root provider's install loop skips a refused method. DenyAttributes is untouched -- caller-driven narrowing stays a separate concern. Two zero cases, deliberately different: a provider with no methods is skipped in silence (elevation while dormant, mem), while a provider whose every method is refused fails loudly at construction rather than as a NameError later. Retired access_title and the descriptor's "pure" field, both written and read by nothing. "pure" computed `"error" not in m.returns` -- infallible, not effect-free, the same conflation op.Action's doc comment still carries. Regeneration was verified to have actually run: 103 codegen invocations counted, not inferred from an exit code. Step 15 (wiring lore hermetic) waits on step 14: only regex.Match claims anything today, so lore would trip the loud failure on every provider it selects.
…s holds them to it Steps 14 through 20 and 22 of #677, plus two sandbox escapes the annotation pass found. make check exits 0 -- 104 packages. Closes #687. 43 claims across ten providers deterministic: regex (8), plan's construction surface (8), ui (6), platform (5), json (4), file's path algebra (4), yaml (3), function.call (1). sandboxed: file's 21 I/O methods, archive (2), encryption (2). file reaches complete coverage -- all 25 announced methods carry a claim. Four plan methods are deliberately excluded, and two of them have clean bodies: Spec calls fsroot.OpenExisting to probe the root, and Run builds an executor and runs the graph. Everything they REACH is non-deterministic -- the claim-propagation case arriving early, and proof that a body-only check would pass a false claim. function.call claims deterministic because a Starlark callable is evaluated against the same filtered surface: in a hermetic runtime it cannot reach anything the runtime did not admit. Hermeticity inherited, not inspected. lore plans hermetically starlarkbridge.Hermetic() at builder.go:610. star passes no such option: it is a scripting tool, where effects are the point. The loud-failure rule is reversed A fully refused provider used to fail runtime construction. That made selecting every module impossible under hermeticity -- and select-all is the POINT of a per-method filter, since a caller should not have to curate the list. The provider now installs with every attribute denied, so the author hears about it where they reached for it: shell.exec is not available in this runtime: this runtime is hermetic and the method makes no deterministic claim That names the method. A construction-time failure named only the provider. Two selection reports Runtime.reportSelection narrates what a module surface admitted, refused, and never considered; GraphExecutor.reportGraphReach narrates which providers the graph actually dispatches to. Planning reports what an author could reach, execution reports what the plan reached, and the difference is now visible. Both carry TODO(#507): they are diagnostics by 2.8's own test, and go to the narrator only because the DEBUG/TRACE API does not exist yet. 2.8 back-references them as a first consumer to design against. pkg/op/claimcheck holds the claims to account A claim is an assertion, so something must be able to contradict it. claimcheck loads the provider tree once through x/tools/go/packages and resolves every selector against real type information: method plan.LoadDefinition: claims deterministic but calls os.ReadFile at provider.go:364 Types are the whole point. os.Getenv and os.FileMode are one shape to a parser, and os.FileMode(mode) is a CALL despite being a conversion. Ten claiming methods reference os.FileMode, os.O_CREATE, os.O_TRUNC, os.ErrNotExist, or os.ModeSymlink; a syntactic scan flags every one, and the type checker correctly sees a type, three constants, and a sentinel. file.Copy carries four and passes clean. It runs as a conformance test, so make check gates it with no new command and no Makefile change -- and one load (6s) rather than one per provider during codegen. The checker is a library with a test on top, so a lint command can call it later without a rewrite. Two sandbox escapes, found by annotating and fixed here file.Glob used filepath.Glob, which honored an absolute pattern verbatim, let `..` survive Join's clean, and followed a symlink out. It now matches over fs.Glob(root.FS()), where patterns are slash-form and root-relative by construction. TestGlob_RefusesPatternsOutsideTheRoot pins all three shapes. walkDir fell back to filepath.WalkDir whenever no root existed, so file.Find and file.WalkTree were confined only when a root happened to be there. The fallback is gone; it was not load-bearing. Dependency golang.org/x/tools becomes direct (go.sum already carried it transitively). The upgrade pulled a newer x/net, which deprecates h2c -- suppressed with a reason at pkg/op/server/router.go and filed as #689, because the documented fix needs an http.Server that nothing constructs yet.
Step 20 of #677. A claiming method's own body was never the whole story: plan.Run has a clean body and builds an executor that runs everything. What it follows Direct calls into in-module functions, transitively, reporting the hop chain that led to a capability: method plan.AssembleDefinition: claims deterministic but reaches time.Now at graph.go:152, through op.NewGraph That found a real determinism break on its first run (#690), and AssembleDefinition's claim is withdrawn here until it is fixed. Two boundaries, both deliberate pkg/fsroot is a declared trust boundary and is never walked through. It IS the sandbox, so its own body calls os.OpenRoot; walking it would report the sandbox as an escape from itself. Methods on values are not acquisitions. f.Stat() on an *os.File obtained through the root is already confined -- what would escape is OBTAINING the handle, which is a package-level call like os.Open. types.Signature.Recv() distinguishes them. Without those two, the first run produced ten violations and every one was noise. What it cannot do Interface dispatch is not followed and cannot be. action.Do(...) resolves to an interface method whose implementation dispatches by reflection, so no static walk reaches what it runs. Propagation catches a helper that reaches a capability and never one reached through dispatch. So a claim stays an assertion the author is accountable for, not a proof. plan.Run is safe because its claim was withheld, not because anything verified it. Worth knowing before reading a green gate as a guarantee. Shape inspectBody became a walker -- walk / record / follow -- because the recursive closure it replaced scored 32 on cognitive complexity. Traversal, classification, and recursion are now separate and each is small.
Closes #690. make check exits 0 -- 104 packages. The break NewGraph stamped time.Now(), canonicalGraph carried Timestamp, and the checksum is taken over exactly that. Two runs of an identical plan produced different checksums with no input having changed -- the thing 2.4 says must not happen. Identity: the timestamp leaves canonical content canonicalGraph has always omitted Signature, because a signature is taken OVER the content and cannot be IN it. A build time has the identical relationship: it says when this graph was made, not what it is. Timestamp joins Signature in being absent. The alternative was to call the timestamp an acceptable deviation from hermeticity. Rejected: an exception has no natural boundary -- if wall-clock is acceptable, so is hostname, so is the planning user -- and "hermetic except X" cannot be checked mechanically where "hermetic" can. Provenance: the clock is read once, where it is visible RuntimeEnvironmentSpec.ConceivedAt is stamped by NewRuntimeEnvironmentSpec and carried to the environment; GraphSpec.WithTimestamp threads it into every graph a session assembles. NewGraph no longer reads the clock at all -- it is a pure function of its spec. Time of conception, not time of birth. Every graph from one planning session shares a timestamp and stays comparable, where a per-graph construction instant made each one unique for no reason. Six call sites supply it: plan.AssembleDefinition, lore's builder, and writ's deploy, decommission, secret, and upgrade. That is what lets plan.AssembleDefinition claim +devlore:claim=deterministic with a straight face. It does again, and #677's propagation check holds it. Verified in both directions TestGraphChecksum_IdenticalAcrossConstructionTimes builds the same graph with two timestamps two hundred million seconds apart, asserts they differ so identity cannot pass by coincidence, then asserts the checksums match. Reverting the canonical-content change makes it fail. It sits beside TestGraphChecksum_IdenticalAcrossJSONAndYAMLDocuments, which pins the other axis. Together: a graph's identity depends on neither when it was built nor how it was written down.
Phases E and F of #677, which completes it. make check exits 0 -- 104 packages. 3.6 is replaced, not amended Its subject was gone: the title read "Method Classification -- Pure, Query, Action" and the tiers were rejected on 2026-08-26. The filename stays so inbound links resolve; the document is now "Method Claims", written against the implementation rather than ahead of it. §8 records why the tiers were rejected, because they are the obvious first idea and someone will propose them again. The reason is structural: the tiers were exclusive where the properties are independent. file.mkdir is sandboxed, mutating, and probably idempotent -- three facts, and a tier holds one. `idempotent` is the proof, since under tiers it could not exist at all: it applies to mutators, which a tier labels `action` and stops describing. §8 also records what is NOT a reason. It was argued that the tiers misclassified platform and ui. They did not -- I did, by assuming platform.os() read the host without opening provider.go:71, which delegates to a supplied field. That is written down because a rejection record crediting a design flaw for an analyst's mistake teaches the wrong lesson: distrust taxonomies, rather than read the implementation before classifying it. op.Action's doc comments were describing effects and measuring fallibility Action claimed "no side effects" while holding platform's five and ui's six. FallibleAction claimed "has side effects" while holding regex's eight and json/yaml's seven. Both corrected. CompensableAction keeps its effects language, where it is accurate -- a compensator exists to reverse something, which is why mutation is the one property derived from a signature rather than claimed. Every claim is verified against every platform, from one machine Step 29 asked for make check under GOOS=linux. Locally that only compiles: the cross-built test binary will not execute on the host. claimcheck.CheckGOOS loads with a chosen target instead, so the conformance test runs on the host and checks host, darwin, linux, and windows in 5.3s. Build tags select different bodies, so a claim genuinely can hold on one platform and fail on another; that is now checked rather than left to a CI leg that tests one. This verifies CLAIMS under those build tags. It does not verify the code builds and passes tests on those platforms -- that needs real machines (#671). The catalog reports the code 3.5-provider-catalog.md's Role column is regenerated from the announced values and gains a Claims column, which is what a hermetic runtime filters on. elevator reads "not announced", the truth until #679 renames it. Also A testdata fixture pins every violation shape the checker must catch, including the one a call-only scan misses: os.Getenv stored as a function value, which is #683's exact shape. It also pins the shape it must NOT catch -- types, constants, and conversions -- because reporting those would fail ten real methods.
added 2 commits
August 26, 2026 15:24
One conflict, in pkg/op/provider/function/provider.go. Both sides were right about different things: develop carried #676's sealing of function.Resource, and this branch had added +devlore:claim=deterministic to Call afterward. The claim is kept; develop had nothing at that position, having been squashed before it existed. Everything else auto-merged, including the two files most likely to break quietly: .github/workflows/ci.yaml #682's arm64 build-and-test matrix alongside this branch's go-version 1.27 and golangci-lint v2.13.1. All three go-version sites read 1.27, including the matrix jobs #682 added. Makefile #682's build-system work alongside this branch's GOLANGCI_LINT_VERSION pin, the golangci-lint-pinned prerequisite, and lint routing through star. Verified rather than assumed: make check exits 0 -- 104 packages, no lint issues -- and every claim still holds against host, darwin, linux, and windows build tags.
…o move The darwin CI leg on #697 failed in codegen, not in tests: generate.star:357 in validate_action_name_consts make: *** [Makefile:647: cmd/star/provider/goast/gen/provider.gen.go] Error 1 goast declared LoadSourceFile twice: as a *Provider method, announced to starlark and called by lint-go-style.star as goast.load_source_file, and as a package-level function. Action-name consts live in the package ROOT, so emitting one for the method collides with the function and will not compile. validate_action_name_consts exists to catch exactly that. Latent, and surfaced by this branch The validation sits behind the branch step 8 of #677 rewrote: `if access in ["planned", "both"]` became `if surface != "module"`. Under the old scheme goast was immediate, so the check never ran for it. Under the new one goast is eligible for both surfaces, so it runs -- and caught a defect that predates this work. Same class as ui and star's lint provider gaining action_names.gen.go, which the earlier commit noted; for goast the consequence was fatal rather than cosmetic. Linux did not pass -- it never ran the validation Neither declaration is build-tagged, so the collision exists on every platform. make generate regenerates only when a grouped target is stale, and on a fresh CI checkout mtimes are near-identical, so which legs regenerate goast is luck. Darwin's did. This is the grouped-target staleness that produced a false negative on 2026-08-24, so rather than trust it again, staleness was forced across every provider: 158 codegen invocations, no other collisions. The fix names each for what it does The package-level function becomes parseSourceFile, and unexported -- it is used only inside the package. The METHOD takes a path and loads it; the FUNCTION takes content and parses it. Sharing a name was always inaccurate and only became a build failure once the const was emitted. Verified on three legs, two of which make check does not run make check 104 packages, no lint issues make test-race 104 packages, no races make test-scenario green Worth recording: make check runs `test`, not `test-race`, and no scenarios at all. Every "green" reported on this branch before now was green on a strict subset of what CI checks. That gap belongs on #670, which so far only covers lint.
added 3 commits
August 26, 2026 16:46
quality-gate failed on #697 without running lint at all: ERROR: golangci-lint 2.13.1 on PATH, expected v2.13.1. make: *** [Makefile:375: golangci-lint-pinned] Error 1 `golangci-lint version --short` prints 2.13.1. The pin is written v2.13.1, which is the form the install URL and the install command in the same recipe both require. The comparison was string equality between the two, so the correct binary was rejected and the gate failed closed on a leading character. Both sides now have a leading v stripped before comparison, rather than the variable being rewritten -- the variable still has to carry the v for the two install lines, and normalizing at the comparison survives golangci-lint changing which form it reports.
test-race timed out on darwin-arm64 and windows-amd64:
panic: test timed out after 2m0s
running tests:
TestEveryClaimHolds (2m0s)
TestEveryClaimHolds/linux (41s)
FAIL github.com/NobleFactor/devlore-cli/pkg/op/claimcheck 120.416s
The test swept "", darwin, linux, and windows, and each sweep entry is a full
type-checked load of the module -- about 40s apiece under -race. Four sequential loads
does not fit a 120s ceiling. Linux passed on faster runners, which is the only reason
this looked platform-specific.
The sweep was redundant, not just slow
CI runs five legs and each ran the same four configurations, so twenty loads answered
twelve questions. The matrix is darwin-arm64, linux-amd64, linux-arm64, windows-amd64,
and windows-arm64: each leg checking its OWN host covers darwin, linux, and windows
between them, at one load apiece. That is what makes the matrix load-bearing rather
than decorative, and it is a quarter of the work.
Why not the alternatives
Restricting the sweep to non-race builds looked right and is not: the legs run
`make test-race` OR `make test`, never both, and only windows/arm64 takes the non-race
path -- the gate would have run on one leg.
Running the four in parallel trades wall clock for four concurrent type-check graphs,
and packages.Load already saturates the cores it is given, so the speedup is small and
the memory is not.
Raising the timeout alone leaves the redundancy in place and makes a genuinely hung
test take five minutes to report.
The local cost, and the way out of it
`make test` on a mac now checks darwin and lets CI find a linux-only violation. Set
DEVLORE_CLAIMCHECK_ALL_PLATFORMS to sweep all four from one machine before pushing.
…s, not a gap
Starlark ships two output builtins -- print and fail -- and devlore owns neither. A
script's print resolves to starlark.Universe and goes straight to stderr, so it escapes
--silent, escapes color and program-name prefixing, and would escape the diagnostics
stream of 2.8. The uniformity 3.5.16 claims holds for ui.print(...) and not for a bare
print(...). Adding op.RoleRoot closes that: the resolver checks predeclared before
universal, and CamelToSnake maps Print->print and Fail->fail, so the methods REPLACE the
builtins with no Universe mutation, no Thread.Print hook, and no interception code.
That raised the obvious question -- whether ui.Print(msg string) should widen to match
the builtin's print(*args, sep=" "). It should not, and the reason is worth recording
because both widenings look reasonable until you follow them through.
fmt.Printf semantics double-interpret the string
Starlark's % is an OPERATOR (interpolate, in starlark/eval.go) that runs before the
call, so print("%s" % name) arrives already rendered. Printf semantics would re-scan
that finished string for verbs, turning "50% off" into "50%!o(MISSING)ff" whenever the
DATA contains a %. Go dodges this by making Print and Printf different functions; one
function cannot be both. The everyday form is print("100% done").
Variadic space-join arrives after the types are gone
fillVariadicSlot runs toGoInto before dispatch, so a Go-side args ...any receives Go
natives: True as bool, None as nil, a list as []any. Rendering those Go-side gives
true, <nil>, [a b] where Starlark says True, None, ["a"]. Faithful output would mean
reimplementing writeValue on the far side of a conversion that already discarded what
it needs.
So one string is not a compromise short of the builtin -- it is the only contract that
can be correct. The script renders with str() and %, which is Starlark's own rendering,
deterministic by construction, and hands over a finished string. Requiring an explicit
str() is a feature in a configuration language: nothing is stringified by a rule the
author cannot see.
The tree already works this way without exception. Surveyed every call site:
print( is 64 sites, fail( is 65, and ZERO of either pass multiple arguments or a
non-string. Each builds one string with %, +, or str().
Residual cost, recorded rather than hidden
print("a", "b") and print(42) are legal against the builtin and are errors against
these methods. Nothing in the tree does either, but a Python reflex produces one. The
errors are legible -- toGoScalar uses starlark.AsString with no coercion, so print(42)
reports `expected string, got int`.
Also corrected: the Thesis said op.RoleModule; the generated roles are
op.RoleModule|op.RoleAction.
Root placement itself is designed, not landed -- #698, task #700, plan
docs/plans/ui-at-root.md. Landing it carries two consequences both recorded on #700:
231 ui.* call sites move to bare names, and placeModule's root path has a latent
return-for-continue defect that putting ui at root would arm.
Closed
4 tasks
David-Noble-at-work
added a commit
that referenced
this pull request
Aug 27, 2026
…705) * docs(claimcheck): the CheckGOOS comment still argued for the sweep that was removed Narrowing the test to one platform per CI leg left the doc comment on CheckGOOS making the case for the four-platform sweep it replaced: Verifying only the developer's platform would leave the others to CI, and CI runs one of them. That is now both stale and an argument against the design around it -- a reader looking for why the sweep is gone finds a paragraph explaining why it should not be. It also misdescribes CI, which runs five legs spanning three platforms rather than one of them. Rewritten to say where the coverage actually comes from: each leg checks its own host, the legs span darwin, linux, and windows between them, and an explicit goos is for sweeping the rest from one machine. * feat(make): regenerate forces every codegen rule, ignoring mtimes Codegen is mtime-gated, and a fresh clone stamps every file at checkout -- so whether a provider looks stale is decided by write order, not by its source. Measured on CI run 33024633546, ONE commit across six jobs: test (darwin-arm64) 159 files 30 providers quality-gate 9 files 1 provider test (linux-amd64) 9 files 1 provider test (linux-arm64) 0 files 0 providers test (windows-amd64) 0 files 0 providers test (windows-arm64) 0 files 0 providers Nothing varied but which runner took the job. Half of them ran no provider generator at all, and every generator validation rides along with that -- so which defects get caught was luck. #697 has the receipt: darwin regenerated goast, caught a duplicate declaration through validate_action_name_consts, and failed; linux skipped it and passed. Neither declaration was build-tagged. Touching sources, not deleting outputs The obvious lever is `rm -f $(NEW_OP_INVENTORY)` -- one output per provider, each the head of a grouped rule, list already maintained. It cannot work. The generated files hold the op.Announce*() call sites devlore-inventory scans, inventory is a prerequisite of the star binary, and `build/star: FORCE` rebuilds star every run. THE OUTPUTS ARE ALSO INPUTS, so deleting them destroys the bootstrap that would regenerate them. Tried it: inventory found 8 packages instead of 19, then failed with `no op.Announce*() call sites found`. Deletion would be safe with an LKG present -- STAR resolves to build/star.lkg when it exists and build/star: FORCE is never consulted. Not taken: CI has no LKG, and a check that depends on an artifact CI lacks is not a check. `make -B generate` also forces the rules and forces far more than the question needs, propagating into the build/star sub-make. Verified make regenerate 159 files, 30 providers -- matches the darwin leg exactly make generate 0 files -- the mtime gate still skips unchanged providers check / test-race / test-scenario all pass * ci: assert the committed generated files equal what the generator produces Generated files are committed, CI never ran `generate` explicitly, and no step asserted that generation produced no diff. Two consequences, both live until now: A stale committed .gen.go passed CI. Edit a provider.go, forget to regenerate, commit -- if the mtimes fell the wrong way, nothing noticed. When codegen DID run, CI tested the regenerated output rather than the committed output. The artifact under test was not the artifact in the repository, and the rewrite happened inside the build where nobody saw it. The fix already existed in this file, applied to go.mod: run the tool, then assert nothing changed. `make regenerate` forces every rule; `git diff --exit-code` holds the committed tree to it. One leg is the right scope Generated output is platform-independent -- it has to be, since the repository holds one copy. Confirmed rather than assumed: regenerating all 159 files on darwin reproduced the committed tree byte for byte. Forcing regeneration on quality-gate makes the generator validations fire deterministically there, and the diff makes every other leg's mtime-gated build safe, because the committed files are then known current. A deletion the generator no longer emits also fails this check -- the #572 class of bug, caught for free. Proved it can fail before trusting it A check that has only ever passed is not a check. Removing one +devlore:claim directive from ui.Note changed pkg/op/provider/ui/gen/provider.gen.go, which the diff catches. Restored byte-identical afterward. Worth recording: the first probe edited a method doc comment and changed NOTHING -- doc comments do not reach the generated artifacts. Stopping there would have concluded the check was broken. * docs(plans): deterministic codegen, closes #702 --------- Co-authored-by: David Noble <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #677. Also closes #669, #686, #687, #690.
make checkexits 0 — 104 packages, no lint issues. Every claim verified against host, darwin, linux, andwindows build tags.
What this is
A provider method now states the guarantees it makes, and two checks hold it to them. The work started as
"remove the
+devlore:access=directive" and became "what should have replaced it," because the directive wasvague in a way that could not be repaired: its role assignment cannot be reproduced from any property of the
code. Four providers with zero mutators were
both; two with zero mutators wereimmediate. Nothingdistinguished them but a judgment nobody wrote down.
// +devlore:claim=deterministic, idempotent, sandboxeddeterministicpkg/op/claimcheck, against the call graphidempotentsandboxedfsroot.Dirpkg/op/claimcheck, against the call graph38 claims across ten providers.
filereaches complete coverage of its 25 announced methods.Mutation is the one property NOT claimed: a method returning a receipt mutates by construction, and a signature
cannot drift without changing. Everything else is asserted, because a derived property flips silently when a
helper three calls down grows a dependency, where a false claim fails the build and names the method.
The commits, in order
ecf1fde6memandfunctionsealed;functionstops borrowing another provider's identity (#662)361260a3eeadf3f5fsrootspeaks one word (sandbox); the local lint gate matches CI; the directive leaves the code7b05b38cop.MethodClaims,+devlore:surface=, and the hermetic runtime filterb202640fpkg/op/claimcheck— one type-checked load of the module, gated as a conformance test54ade79f684e9d1aop.Action's doc comments corrected, the catalog regeneratedThe branch name predates all of this. It describes
ecf1fde6, the smallest of the eight.Why the claims earned their keep
Every one of these was invisible until something asserted a property that could be contradicted.
file.Globusedfilepath.Glob— absolute patterns honored verbatim,..survivingJoin's clean, symlinks followed out of the treewalkDirfell back tofilepath.WalkDirwhenever no root existed, soFindandWalkTreewere confined only when a root happened to be thereapplyOwnershipcalledos.Chownwhilefsroot.Dirhas one — the only file mutation leaving the sandboxtime.Now(), so no two identical plans agreedtemplate.Envreads the ambient environmentstar lint goreported "not tidy" for any uncommittedgo.modedit, gating the commit that would clear itTwo things a reviewer should know
Interface dispatch is not followed and cannot be.
action.Do(...)resolves to an interface method whoseimplementation dispatches by reflection, so no static walk reaches what it runs. Propagation catches a helper
that reaches a capability and never one reached through dispatch. A claim remains an assertion the author is
accountable for, not a proof —
plan.Runcarries no claim because it was withheld, not because anythingverified it.
Two trust boundaries are declared and never walked through.
pkg/fsrootIS the sandbox, so its own bodycalls
os.OpenRoot; walking it would report the sandbox as escaping itself.pkg/assertreachesruntime.Callersonly while raising — the abort path of a failed assertion.Notable non-obvious choices
A denylist, never an allowlist. Eleven capability packages, enumerating the ways a Go program reaches outside
its own process — which the OS interface fixes, so the list is not ours to maintain. An allowlist of safe
packages would need ~26 entries today and grow with every import.
Types, not syntax.
os.Getenvandos.FileModeare one shape to a parser, andos.FileMode(mode)is acall despite being a conversion. Ten methods in
filereferenceos.FileMode,os.O_CREATE,os.O_TRUNC,os.ErrNotExist, oros.ModeSymlink; a syntactic scan flags every one.file.Copycarries four and passesclean.
A separate pass, not a codegen step. One type-checked load (~6s) rather than one per provider, and it runs
where verification belongs rather than where metadata is generated.
Dependency
golang.org/x/toolsbecomes direct —go.sumalready carried it transitively.go.modmoves to 1.27 and CIfollows, so both sides analyze with the same semantics. The upgrade pulled a newer
x/net, which deprecatesh2c; suppressed with a reason and filed as #689, because the documented fix needs anhttp.Serverthatnothing constructs yet.
Follow-ups filed
#683 · #689 · #691 · #694 — and #679, #678, #680 from the catalog audit that started this.