Repository navigation
Phase 8: the workflow-engine foundation for devlore operations - #288
Merged
David-Noble-at-work merged 720 commits intoJul 25, 2026
Merged
Conversation
added 30 commits
June 21, 2026 14:10
…ave/load/restart Captures the step-28 execution-core design worked out this session. - Chained recovery stacks (option C) -- the resolution to the activation.Stack regression: every executor owns its stack, and the per-subgraph stacks form one tree linked two ways -- chain UP (parent pointer) so ResultByUnitID walks to an ancestor stack for promise resolution, and nest DOWN (PushNested) so Unwind cascades compensation. activation.Stack is simply the executor's own stack; input resolution walks the chain up, so there is no conflict. Save/ restore rule: the nesting is durable (serialized in the Trace), the parent pointer is transient (re-derived on load) -- which is what makes resume's promise resolution and skip-completed work. New "Chained recovery stacks" section in the step doc (with diagram); a principle bullet in 2.3. - Save / load / restart sequence diagram (RUN -> PAUSE -> SAVE -> LOAD -> RESTART) with proof points. - Control plane -- the executor's bidirectional command/event surface: commands in (ExecutionControl: pause/stop/step, replacing the pause bool, polled at a control-point switch) and events out (HookRegistry + Status + Result), including the finding that Status/Result are misfiled on RuntimeEnvironment; ownership on the GraphExecutor with the two-axis context-object taxonomy. Scoped as plan step 33. - Combinator design finalized: every combinator IS a subgraph; non-Subgraph combinators delegate to flow.Provider.Subgraph; Choose does NOT select; every combinator keeps its compensate companion. - phase-8.md: step-28 row updated to implementation-in-progress with option (C); steps 31/32/33 in place. Docs only; the step-28 implementation code is uncommitted pending the option (C) fix.
… step-28 design records Implementation (step 28 item (a), green): - RecoveryStack gains a transient `parent` pointer; ResultByUnitID now walks UP the parent chain (this stack -> parent -> ...) for promise resolution. Constructor newRecoveryStack(parent) added. - newChildExecutor(parentStack) chains the child stack to the parent; subgraph.Execute passes the dispatch's stack, so the chain mirrors the subgraph tree. - flow.Subgraph binds kwargs -> subgraph.Parameters() and reads activation.Stack (the executor's own stack) instead of minting one. - This is option (C): activation.Stack is the executor's own stack, and input resolution walks the chain up -- resolving the activation.Stack overload that had broken TestChoose_NotExists / TestChoose_Predicates / TestSubgraph_ReturnsRecoveryStack. The tree is back to the known baseline. Test renames (TestSubject_Scenario standard): - TestShellCompletionPath -> TestShellCompletionPath_PerShell (cmd/star/cli, internal/cli); TestWalkTreePlanned -> TestWalkTree_Planned (devloretest). Design records (step-28 doc + 2.3 + phase-8.md): - Chained recovery stacks (up for resolution, down for unwind; nesting durable / parent pointer transient -- re-derived on Trace load). - Compensation gates on the complement, not a resource (closing the open issue): the named Compensate companion is the live undo path; fix the base-op latent bug where RecoveryStack.Push gates on receipt.Resource() != nil, proven by file.Provider.WalkTree (returns a *op.RecoveryStack complement + CompensateWalkTree, dead code today). Gate on Complement(), env from the executor, route through the action's Undo companion. - Save/load/restart sequence diagram; combinator design (Choose does NOT select; every combinator keeps its companion); control plane (commands in / ExecutionControl + events out / HookRegistry+Status+Result, with the Status/Result migration); plan steps 31/32/33. make test: pkg/op + flow + plan ok; only the 4 pre-existing baseline failures remain (TestBackup_DefaultSuffix, TestCompensation, TestShellCompletionPath_PerShell [the pwsh/powershell issue, step 25 -- now under its renamed name], TestWalkTree_Planned [step 22]). Zero new failures.
…companion Compensation was keyed on receipt.Resource(): RecoveryStack.Push wired an entry for compensation only when the receipt carried a single Resource, and invokeCompensateForReceipt read the RuntimeEnvironment off that resource. So a resource-less complement -- a combinator or file.Provider.WalkTree recovery stack -- was silently demoted to audit-only, its named Compensate companion dead code, and compensation rode an implicit nested auto-unwind instead. Gather's forthcoming []*op.RecoveryStack slice would have been dropped outright. Gate on the complement, supply the env from the executor, and make the named companion the live undo path: - RecoveryStack.Push takes the executor's RuntimeEnvironment and gates on Complement() != nil (undo state of any shape -- a resource action's receipt, a recovery stack, or a slice). - invokeCompensateForReceipt is env-parameterized (no resource lookup) and hands Receipt.Complement() to Method.Undo. Equivalent on the resource path: receipt.Complement() == receipt for a resource action (method.go:545). - pushAuditReceipt drops the PushNested branch; a *op.RecoveryStack complement now rides the audit receipt via Commit, exactly like a resource *Receipt. - RecoveryStack.Receipts descends into a receipt's *RecoveryStack complement so Trace.Summarize still observes subgraph / WalkTree children. - buildSubStackFromReceiptSlice threads activation.RuntimeEnvironment. - Stale nest/PushNested doc comments corrected (subgraph.go, graph_executor.go). CompensateWalkTree / CompensateSubgraph / CompensateChoose / CompensateGather are now invoked, uniform with resource-action compensation and registry-resolvable (survives a Trace load). make test: pkg/op + flow + file compensation green; zero new failures. The 4 standing baseline failures are unchanged -- TestCompensation remains a pre-existing failure->unwind integration gap (step 28 item (b)), proven not this change. Build-failed consumers (writ/lore/docgen) are pre-existing WIP (Assemble rename / op.ImmediateOf removal), untouched here.
…nd 6.1 - 3.2: brokers defined like resources -- self-announce globally (op.AnnounceBroker ~ op.AnnounceResource), own contract + config schema on a common op.BrokerBase, wired by providers (op.WireBrokers), fresh instance per use - configuration.md: elevator case study aligned to broker-owned config + wiring - 6.1: broker-registry section repointed to the resource-mirrored model; the config-shape open item resolved - phase-8 plan: foundation-types item notes the recursive tree; elevator item aligned to the broker seam
- configuration.md: typed sections project via a lazy reflection adapter as the sealed Mapping, uniform across Config / typed sections / ConfigBase / DataSection; struct-valued settings cross as goReceivers through the existing reflection framework (marshalReflect) -- not flattened dicts (fixes the projection wording) - phase-8 plan: star-unification item notes the settled travel form
- configuration.md: the per-setting origin record is a path-keyed override chain -- SetBy(path) gives the winning layer, History(path) the full chain; stamped by appending during the merge; the word "provenance" is gone from the doc - phase-8 plan: loader item and landed-status reflect the SetBy/History reshaping
The failure->unwind cascade never fired: a failed subgraph's recovery state was
thrown away before it could compensate. Two execution-core defects, both masked
until the compensation gate made combinator receipts compensable.
1. Method.Invoke discarded the complement on ANY dispatch error
(`if err != nil { return nil, nil, err }`), so the recovery stack a
compensable forward call returns on failure never reached its audit receipt.
Now the complement is committed and returned ALONGSIDE dispatchErr, so the
receipt is compensable and Run's top-level e.stack.Unwind() cascades through
CompensateSubgraph into the children.
2. invokeCompensateForReceipt resolved the companion via ActionByPath, which
keys on the Go-qualified ActionName (github.com/.../flow.Provider.Subgraph),
but a unit that binds its action by name -- the graph root and every
combinator -- records the dotted name (flow.subgraph) as its action path, so
the lookup missed. It now falls back to RuntimeEnvironment.ActionByName (the
same resolver dispatch uses) on a miss.
Both pinned with a live stack dump at the undo seam (instrumentation since
removed; receiver_registry.go is net-unchanged and not staged).
make test: TestCompensation green (was a standing baseline failure), pkg/op
green, zero new failures. Remaining baseline failures unchanged
(TestBackup_DefaultSuffix, TestWalkTree_Planned, TestShellCompletionPath_PerShell);
writ/lore/docgen build failures are pre-existing WIP.
Resume re-entry is settled as a side-effect-free "pseudo replay": Run re-descends the graph from the root and the restored trace's receipts dictate the descent -- skip any unit with a successful receipt (return its cached result, prune the completed subtree) and descend only the unfinished spine to the frontier. The key distinction captured: "do nothing" is not literally passive. The descent must (1) adopt each subgraph's restored child stack -- option (C) mints a fresh one otherwise, which would re-run the completed children -- and (2) re-resolve the per-subgraph variable frames, the one bit of execution state the Trace does not carry (pure recomputation against the restored stacks). It is a re-walk rather than a literal jump because ErrPaused unwound the Go call stack back to Run; only the recovery stack is restorable data. Scope is increment (X): Run's preamble becomes state-driven (accept RunStatePaused, keep trace.Stack, use trace.Variables); a per-unit skip/adopt/fresh guard in node.Execute / subgraph.Execute; pause-receipt supersession. No replay-map, no work-list rewrite, no frame persistence; the larger resumable-dispatch-loop rewrite (Y) is deferred. - step-28 step doc: new "Resume re-entry -- pseudo replay" section; refreshed the implementation-status block (option C + compensation gate + failure->unwind now landed and committed; (b) designed/ready, (c) pending). - 2.3 architecture model: resume bullet. - phase-8.md step-28 row: resume-design-settled note. Design record only -- no code; implementation of (b) awaits sign-off on this plan.
… correction Two design-doc updates for step 28, each reflected in the architecture doc (2.3), the step doc, and the plan row. 1. Resume re-entry (b) settled as a side-effect-free "pseudo replay": Run re-descends the graph from the root and the restored trace's receipts dictate the descent -- skip any unit with a successful receipt (return its cached result, prune the completed subtree), descend only the unfinished spine to the frontier. "Do nothing" is not literally passive: the descent must adopt each subgraph's restored child stack (option C mints a fresh one otherwise, which would re-run completed children) and re-resolve the per-subgraph variable frames (the one bit of state the Trace does not carry). Re-walk rather than a literal jump because ErrPaused unwound the Go call stack back to Run. Scope is increment (X); the resumable-dispatch-loop rewrite (Y) is deferred. 2. Chained-stacks model corrected to match the compensation-gate fix: a subgraph's child stack rides DOWN on its parent's audit receipt as that receipt's *complement* (committed via Commit), not a separate PushNested entry. Unwind cascades by invoking each receipt's Compensate companion, which unwinds the complement child stack. PushNested now serves only Gather's internal per-item grouping. Updated the chained-stacks bullet (2.3), the step-28 chained-stacks section (bullet, ASCII diagram, failure-unwind walk, save/restore subsection), and the plan row. Design records only -- no code. Implementation of (b) awaits sign-off.
Resume a paused run by re-descending the graph and replaying what the trace already records -- side-effect-free, no re-dispatch of completed work. - Run is state-driven: it accepts a resumed (RunStatePaused) executor, keeps the restored trace.Stack instead of resetting it, and re-publishes trace.Variables onto the fresh environment instead of re-binding. - RecoveryStack.receiptByUnitID + supersede: single-stack receipt lookup and removal (the parent chain is for resolution; a unit's own receipt is local). - node.Execute / subgraph.Execute carry a per-unit guard. A unit already holding a successful receipt is replayed (cached result returned, not re-dispatched). A subgraph holding an ErrPaused receipt is the in-progress spine: it adopts its restored child stack (re-parented to the enclosing stack) so completed children are present and the descent resumes at the frontier, and supersedes the stale receipt so the fresh completion replaces it. Otherwise it runs fresh. On a fresh run no prior receipt exists, so the guard is a no-op. - newChildExecutor takes the caller-built child stack -- newRecoveryStack(parent) for a fresh dispatch, or the adopted restored stack on resume. Tests (lifecycle_api_test.go): TestGraphPauseResume_ViaPublicAPI (flat) and TestGraphPauseResumeNested_ViaPublicAPI (recursive adopt across a nested flow.subgraph) -- pause mid-graph via a lifecycle hook, resume from the Trace on a fresh executor, both side effects present, file.mkdir completed == 2 (no node re-dispatched). Scope: this is the IN-PROCESS increment (pause -> Trace() -> ResumeExecutor -> resume, same process). Still required to close step 28: the full save->load->resume serialize round-trip (a deserialized receipt complement is untyped today; adopt detection moves to Err() != nil), Catalog capture/restore in Trace (c), and Gather resume (N-dispatch). See the step doc's "Remaining for step 28" section. make test: pkg/op + pkg/op/provider/plan green; zero new failures (only the standing baseline TestBackup_DefaultSuffix / TestWalkTree_Planned / TestShellCompletionPath_PerShell, plus pre-existing writ/lore/docgen build breaks). Plan + step doc updated.
A paused run's Trace now survives save -> reload -> resume on disk. The recovery stack owns a provider-agnostic execution-state envelope (receiptEnvelope) read off the Receipt interface, so a reloaded receipt restores what resume needs -- unit_id, action, result, status, and a reconstructed *RecoveryStack complement -- regardless of which provider produced it. This closes a gap where leaf provider receipts (e.g. file.Receipt) serialized only their compensation URIs and dropped unit_id/result/status, so a reloaded trace could neither identify a completed unit (skip) nor return its result (promise). Putting the execution state on the stack keeps the burden off provider authors: a new or maintained provider cannot forget to encode resume state. The envelope carries the dotted action (e.g. "file.mkdir"), not the Go-qualified action_path -- compensation resolves the companion from action via the ActionByName fallback, keeping the long Go path out of the document. - RecoveryStack.MarshalJSON / MarshalYAML emit the envelope; UnmarshalJSON reconstructs the tree recursively (nested sub + receipt envelopes), rebuilding a stack-shaped complement as *RecoveryStack. - Adopt detection moves from errors.Is(Err(), ErrPaused) to Err() != nil -- serialize-safe, since a reloaded error is a plain message and a resumable trace carries only pause-spine error receipts. Test (lifecycle_api_test.go): TestGraphSaveLoadResume_ViaPublicAPI -- pause mid-graph, write the Trace to disk, reload via document.ReadFile, resume on a fresh executor -> completes, no node re-dispatched. In-process resume tests stay green. Scope: happy-path save->load->resume. Still required to close step 28: YAML trace deserialization, compensation-after-resume (reconstruct each provider's concrete receipt + resource), cross-pause promise fidelity, Catalog in Trace (c), Gather resume, and the Starlark variant. See the step doc's "Remaining for step 28". make test: pkg/op + pkg/op/provider/plan green; zero new failures (only the standing baseline + pre-existing writ/lore/docgen build breaks). Plan + step doc updated.
…reference by URI Resume must reconstruct the live ResourceCatalog whatever the case; the open fork was how. Option B (confirmed for full fidelity): the Trace serializes the ResourceCatalog and the recovery stack references ledger entries by URI; resume rebuilds the live catalog from the saved ledger and resolves each receipt's result/boundary/source against it. This unifies two formerly-separate remaining items -- compensation-after-resume and "(c) catalog capture/restore" -- into one ledger-centric mechanism. A file.Receipt is already a reference record: its compensation state is three *Resource references (the produced resource, boundary, source) plus the scalars recoveryID/recoveryDigest. boundary/source can be discovered, not produced (a Backup's source is the pre-existing original, with no production receipt), so rebuilding the ledger from production receipts is lossy in exactly the entries compensation needs -- saving the ledger is full-fidelity. The reference form is the URI: what result already carries, self-describing in the document, resolved via the namespace. Two gen-registered factories at the op<->provider seam: resource-from-URI reuses the AnnounceResource constructor (typeID-keyed, no new registry); receipt-from-references is a new AnnounceReceipt, resolved by action -> provider. Docs only -- design settled, implementation pending: - step-28 doc: new "The resource ledger across resume" section; the Remaining list unifies compensation-after-resume + catalog capture under Option B; the (c) proof point marked resolved by the design. - phase-8.md: step-28 row records the settled Option B design + reworked Remaining list.
…ences by id
Shadowing makes a URI a non-unique key. The catalog is an append-only ledger and
Shadow re-catalogs an existing URI through catalogLocked (revival after Gone, a
producer shadowing a discovery, re-observation), minting a fresh id per
generation. byID distinguishes every generation; the URI->id namespace tracks
only the current one, so Discover(uri)/Current(uri) resolve to the current
generation and a superseded one is reachable solely by Lookup(id). Referencing a
receipt's resource by URI would yield the wrong generation when that resource was
shadowed before the pause -- only the id pins it, and only the full saved ledger
holds every generation.
So Option B is corrected: the Trace serializes the ResourceCatalog as all
generations keyed by resource id (each entry {id, uri, producerID, state} +
nextID; the URI rebuilds the Resource object, the id is restamped as identity,
byID/ns/states replay in append order), and the recovery-stack envelope
references result/boundary/source by id, resolved on resume via Lookup(id).
This also corrects an earlier framing: file.Receipt.hydrate reconstructs
boundary/source from their encoded URIs, so the discovered-not-produced case did
not by itself force the full ledger -- shadowing does.
Docs only -- design settled, implementation pending:
- step-28 doc: "The resource ledger across resume" section rewritten onto id
references + the shadowing rationale; (c) proof point + Remaining list + anchors
updated.
- phase-8.md: step-28 row's ledger-in-Trace design corrected to id references.
…drate
A paused run's resource ledger now serializes into the Trace and rebuilds on
resume with every generation keyed by its catalog id -- the foundation for
referencing receipts by id (B3) so shadowed generations resolve to the exact
resource the receipt captured, not the current one.
B1 -- ledger save:
- ResourceCatalog.Snapshot() projects every generation in append order
({id, uri, producerID, state}) plus the observation index and nextID into a
serializable ResourceLedgerSnapshot (SUPPORTING TYPES).
- Trace gains Catalog *ResourceLedgerSnapshot.
- GraphExecutor snapshots the ledger in Run's teardown when the run pauses (the
environment is nil'd after Run, so Trace() -- called by the host afterward --
would otherwise have nothing to read); Trace() emits the live snapshot during a
run and the captured one after. ResumeExecutor seeds it from trace.Catalog.
B2 -- id-preserving rehydrate:
- receiverRegistry.ResourceConstructorByTypeID resolves a resource constructor
from the Go type id carried in a URI fragment, so op rebuilds a provider's
Resource without importing the provider.
- ResourceLedgerSnapshot.Rehydrate rebuilds the live catalog: each generation is
reconstructed from its URI (ExtractTagSpecific splits off the `specific` part
the constructor accepts -- buildCandidate rejects the full tag URI), with
interning disabled so ResourceCatalog.restoreEntry stamps the saved id rather
than minting a fresh one. Append order reproduces the namespace's
current-generation pointer.
- Run's resume branch rehydrates trace.Catalog and installs it as the env's
catalog (a fresh clone would lose superseded shadow generations).
Tests (lifecycle_api_test.go): TestResourceLedgerRehydrate_PreservesIDs (snapshot
-> rehydrate -> Lookup(savedID) returns the same URI); TestGraphSaveLoadResume
now asserts the ledger snapshot round-trips and resumes with rehydration active.
make test: pkg/op + pkg/op/provider/plan green; zero new failures (only the
standing baseline + pre-existing writ/lore/docgen build breaks). Plan + step doc
record B1+B2; remaining for step 28 narrowed to B3.
…eEncoded A resumed-then-failed run now rolls back its pre-pause work. A reloaded receipt is reconstructed into its concrete provider type, its resource references resolved by id against the rehydrated ledger, and its compensation re-armed -- so unwinding the resumed stack compensates work the pause-time run completed. Receipt.RestoreEncoded(env, bytes) is a method on the Receipt interface: the ReceiptBase default restores base execution state + the *RecoveryStack complement (subsuming the former unmarshalReceiptEnvelope free function so every receipt inherits it via embedding), and file.Receipt overrides it to resolve its resource/boundary/source ids via ResourceCatalog.Lookup. The env arrives as a parameter, matching how pkg/op injects the runtime environment everywhere (always a constructor parameter, never a setter); it replaces file.Receipt's old UnmarshalJSON/UnmarshalYAML/hydrate, which required a pre-seeded env-bearing resource and resolved by URI (a shadowed generation shares its URI, so resolution must be by id). No receipt registry. op cannot construct a *file.Receipt, and json.Unmarshal cannot decode into an interface, so it reflect.News the concrete type first -- read off the Compensate companion op already resolves by action. A complement is one of three shapes (isLegalCompensableComplement): a Receipt, a []Receipt, or a *RecoveryStack. B3 reconstructs the single-Receipt shape (file.*/archive.Extract/service.*), where the forward method returns (result, receipt, error) and the companion takes that receipt -- so the receipt is its own complement; MarshalYAML emits the `receipt` sub-field only for that self-complement case. []Receipt complements (pkg.Install/Remove/Upgrade) are a follow-up: carrying no sub-field, such a trace resumes without that receipt's compensation rather than failing. *RecoveryStack complements (file.WalkTree, flow.*) ride the complement field, restored by the base. - file.Receipt: id-based MarshalYAML (resource_id/boundary_id/source_id + recovery key/digest); RestoreEncoded override; lookupResource helper. - recovery_stack: envelope gains a `receipt` sub-field for resource receipts; recoveryEntry retains the raw envelope at load; a resume-time rearm pass reconstructs concrete receipts, reinstates the self-complement framework-level (a resource receipt is its own complement -- Commit sets it on the produce path), and binds compensation; reconstructReceipt + receiptTypeForAction. - method.Method.complementType reads the companion's last parameter type. - graph_executor: Run's resume branch calls rearm after Rehydrate. Test (lifecycle_api_test.go): TestGraphResumeThenFail_RollsBack_ViaPublicAPI -- pause after a mkdir, save, reload, resume, fail the un-run frontier, assert the pre-pause directory is rolled back. End-to-end lifecycle (lifecycle_e2e_test.go): TestLifecycle_ViaGoAPI drives the Go executor through run-to-completion, pause+resume, and fail+rollback; TestLifecycle_ViaStarlark drives execution from a .star via plan.run through run-to-completion and fail+rollback -- a failure inside plan.run unwinds and compensates automatically (the same Run() path). Pausing a live run is the pending out-of-process eventing API, not a synchronous-script concern, so pause+resume is exercised only on the Go side. make test: pkg/op + pkg/op/provider/plan green; zero new failures (only the standing baseline + pre-existing writ/lore/docgen build breaks). Step doc + plan record B3 as built and the verified companion-is-the-receipt identity.
…ng items The step-28 doc and the phase-8 plan now record the committed end-to-end lifecycle suite (lifecycle_e2e_test.go) and reframe the remaining work in light of the eventing-API model: - Implemented/coverage: TestLifecycle_ViaGoAPI drives the Go executor through run-to-completion, pause+resume, and fail+rollback; TestLifecycle_ViaStarlark builds, saves, loads, and runs via plan.run through run-to-completion and fail+rollback -- a failure inside plan.run unwinds and compensates on the same Run() path, so rollback holds whether the run is launched from Go or Starlark. - Remaining item reframed: Starlark build/save/load/execute + rollback is now done (the e2e suite), and Starlark-driven pause/resume is the eventing API (step 33), not a synchronous-script variant -- so the old "Starlark resume variant" collapses to replacing TestSubgraph_ReturnsRecoveryStack. The []Receipt-complement compensation-after-resume follow-up is also recorded in the plan's remaining list. Docs only; no code change.
…ction Record the corrected design for sub-step 10. B3 made receipt reconstruction JSON-byte-bound (Receipt.RestoreEncoded([]byte) + json.Unmarshal; raw-bytes entry.encoded), which can reconstruct only a JSON-decoded trace. That is out of step with the recorded requirement (graph-signing.md): the graph AND its execution trace serialize to three formats (JSON, YAML, Protobuf), and verify decodes a trace from any format, re-canonicalizes, and verifies -- one signature verifies in any format. The step-28 doc gains a "Format-neutral trace reconstruction" section: reconstruction consumes a decoded value (not bytes); the codec owns field decode while the receipt keeps a format-neutral id-resolution hook; type discovery stays registry-free (action -> Compensate companion), which is exactly what lets Protobuf reconstruct without google.protobuf.Any. Remaining item (1) is reframed from "YAML trace deserialization" to this redesign in both the step doc and the phase-8 plan. Also brings the step-28 doc and the phase-8 plan into sync (status + remaining list). Docs only; design settled, implementation not started.
…mes decoded values Reconstruction consumed JSON bytes (Receipt.RestoreEncoded([]byte) + json.Unmarshal; raw-bytes entry.encoded), so only a JSON-decoded trace could rebuild its receipts. That is out of step with the recorded requirement (graph-signing.md): the graph and its execution trace serialize across JSON, YAML, and Protobuf, and verify decodes a trace from any format, re-canonicalizes, and verifies. This makes reconstruction format-neutral -- the read-side mirror of the already-neutral write side (MarshalYAML builds a both-tagged shape, MarshalJSON wraps it). Receipt.RestoreEncoded(env, base ReceiptData, fields map[string]any) now consumes codec-decoded values, never bytes: the recovery stack decodes the envelope through whichever codec read the trace, the base execution state arrives as a ReceiptData, and the receipt's id-reference sub-field as a format-neutral map. - recovery_stack: a codec-decoded recoveryEntryData shape; UnmarshalJSON and the new UnmarshalYAML both decode into it and share one fromEntries builder; entry.encoded ([]byte) becomes entry.restore (*receiptRestore, the decoded envelope retained for re-arm); rearm + reconstructReceipt take the decoded base + fields. - receipt: ReceiptBase.RestoreEncoded copies the decoded base across (no json); the interface signature carries ReceiptData + map[string]any. - file.Receipt: RestoreEncoded reads its ids from the decoded map (stringField) and Lookups them; no json.Unmarshal. Type discovery stays registry-free (action -> Compensate companion), which is exactly what lets Protobuf reconstruct without google.protobuf.Any -- the concrete type comes from the action name every codec carries, not a type-URL registry. Test: TestGraphResumeThenFail_RollsBack_ViaPublicAPI runs the resume-then-fail rollback through both JSON and YAML traces (a YAML-loaded trace must reconstruct its recovery stack and receipts identically and still roll back). make test: pkg/op + plan green; zero new failures (only the standing baseline + pre-existing writ/lore/docgen build breaks). Step doc + phase-8 plan record sub-step 10 as implemented and bring the step-28 status + remaining list current.
…pe-id design A post-resume consumer that depends on a pre-pause producer's result must receive it at the concrete type its parameter needs. After format-neutral reload that result is the producer's reloaded (untyped) value -- a resource comes back as its tag-URI string. This lands the resource case and records the full design for all result types. Resource case: Convert step 6 (tryConstructResource) resolves a value that is a known resource URI through the rehydrated catalog (Current(uri) -> Lookup(id)), returning the full restored generation, before any fresh construction -- and the file constructor would anyway reject the tag URI. Path strings / unknown URIs miss the catalog and construct as before, so live runs (whose resource slots are already typed and never reach step 6) are unaffected. Test: TestGraphResumePromiseFidelity_ViaPublicAPI -- a mkdir producer -> file.exists consumer with a promise across the pause -- passes through both JSON and YAML. Design (step-28 doc, "Cross-pause promise fidelity" section): full fidelity for ALL result types via a produced-type-id. The source result type is authoritative (it survives an any/interface slot); a combinator's static return is any, so capture reflect.TypeOf(product) at Commit and record a canonical type-id string (the resource URI id scheme generalized to composites) in the receipt. rearm resolves the id to a reflect.Type via an index over method product-return types and Converts the reloaded value into it; a struct<->map hydration step in Convert is the conversion engine. Design settled, implementation not started. make test: pkg/op + plan green; zero new failures (only the standing baseline + pre-existing writ/lore/docgen build breaks).
…1 engine) Convert reconstructed only scalars, resources, and same-kind containers (slice->slice, map->map); a struct reloaded as map[string]any hit the error step, and so did any slice/map whose elements were structs. This adds the conversion engine the produced-type-id design depends on: two fallback steps before the error step. - Step 9, struct hydration: a map source into a struct target -- allocate the struct, fill each exported field from the map by its json/yaml tag (or field name), recursing every field value through Convert. Because steps 3/4 already recurse slices and maps, []Struct, map[string]Struct, *Struct, nested structs, and struct-with-Resource-fields all compose. - Step 8, text unmarshal: a string source into a target (or its pointer) implementing encoding.TextUnmarshaler -- time.Time and friends reconstruct from their reloaded RFC 3339 string. Both are fallbacks reached only after every existing step declines, so nothing that previously converted changes -- they convert only what used to error. Tests (convert_struct_test.go, framework-level, no provider dependency): map->struct (nested, int-from-float64, slices/maps of structs), []struct, map[string]struct, *struct, and time.Time from its reloaded string. This is Slice A of the produced-type-id design (step-28 doc, "Cross-pause promise fidelity"); the trace wiring (Slice B: canonicalID, capture at Commit, the result_type envelope field, the resolver index over method product types, rearm retyping) remains. make test: pkg/op green; zero new failures (only the standing baseline + pre-existing writ/lore/docgen build breaks).
…d Go type
Slice B of the produced-type-id design (Slice A landed the Convert engine). A reloaded
result is untyped -- the codec drops the Go type -- so restore retypes it to the
concrete type it was produced as, authoritative even when a combinator's static return
is `any`.
- Capture: ReceiptBase gains resultType; Commit stamps canonicalIDOf(result).
canonicalID (resource.go) generalizes typeIDOf to recurse on */[]/map/array, using
full import paths (reflect.Type.String()'s package basename would collide).
- Serialize: a result_type string on ReceiptData, the marshal envelope, and the decode
shape; threaded through Snapshot/Restore/RestoreEncoded and file.Receipt's override;
a ResultType() accessor on the Receipt interface (inherited from ReceiptBase).
- Resolve: receiverRegistry.ProductTypeByID builds {canonicalID(method.ResultType()):
type} once over every action method's product type -- complete by construction.
- Retype: rearm's retypeResult resolves the recorded id and Converts the reloaded
result in place, eager, so the stack is type-faithful before anything reads it.
Scoped to struct/scalar/resource: retypeResult leaves a result it cannot reconstruct
as-is rather than failing the resume. A content-addressable observation
(file.Observation embeds op.ResourceBase; its URI is sha256 over its observed state) is
deliberately NOT retyped -- it round-trips by re-observe-and-verify, recorded as its
own resume-correctness slice (step-28 doc): resume re-runs each observation at its
resumption point(s) -- 1+, N for a paused gather -- and asserts the content-hash URI
matches, surfacing drift; on deserialize the observed resource must be in the catalog.
Tests: canonicalID (named/pointer/slice/map/composites); struct hydration via the
Convert engine (Slice A); the produced-type-id round trip via the resource
promise-fidelity test. A struct-result end-to-end is gated on a test action (no
provider yields a non-resource struct).
make test: pkg/op + plan green; zero new failures (only the standing baseline +
pre-existing writ/lore/docgen build breaks).
…tection Re-observe-and-verify (resumption-point drift detection) is not step-28 work. It is a drift-detection capability of the reconciliation framework, so the step-28 doc keeps the design but flips its status to deferred, and phase-8's remaining list drops it. Step 28's remaining work is now gather resume, the TestSubgraph_ReturnsRecoveryStack replacement, and []Receipt-complement compensation-after-resume. reconciliation.md gains a note: op's content-addressable observations -- observe(resource) returns an *Observation whose URI is sha256 over the observed state -- are a re-execution- free drift primitive (re-observe, compare the fresh content-hash URI to a stored observation; a mismatch is drift), complementing receipt-backed ReconcileX and the filesystem-based path. For now observations are treated as ordinary structs: the produced-type-id's struct conversion covers them structurally and retypeResult leaves their serialized URI as-is. Observations are serializable artifacts in their own right and are expected to gain dedicated JSON/YAML/Protobuf serialization interfaces -- the same format-neutral requirement as graphs and traces (graph-signing.md). Docs-only; no code change.
…ent-shape decisions New design doc, file-mutation-receipts.md. file.write_text, file.write_bytes, and archive.extract are one operation -- write bytes to a target (create or update) -- and delete is its mirror. One forward core (WriteFile/DeleteFile/MakeDir/RemoveDir, streaming via io.Copy for zero-copy), one self-describing receipt that always names its own undo (CompensateFileMutation), one undo that dispatches on the receipt's kind. This resolves archive's no-op rollback: archive.extract stops hand-building receipts and loops the file provider's WriteFile/MakeDir, whose receipts self-declare their compensation companion -- so stack.Unwind() compensates them and resume rebuilds them as *file.Receipt regardless of dispatcher. CompensateExtract collapses to stack.Unwind(). Settled decisions: - Explicit create|update|delete (file) / create|delete (dir) kind on the receipt. - remove/remove_all/unlink fold in: single mutation -> one receipt; batch -> *RecoveryStack (remove_all loops DeleteFile/RemoveDir, the same shape as archive.extract). - Directories are first-class (MakeDir/RemoveDir) for archive's and remove_all's standalone/empty dirs; the boundary still handles dirs incidental to a file write. - The compensation action is always set by the operation, with no dispatch-derived default and no override -- collapsing CompensateWriteText/WriteBytes/Remove/RemoveAll/Unlink/Mkdir into the one CompensateFileMutation. Plan/design updates: - step-28 doc: Complement-shape restriction section (decided 2026-06-26) -- complements restrict to *Receipt | *RecoveryStack, the []Receipt shape removed, and flow.Gather's []*op.RecoveryStack collapses to a single nested *RecoveryStack (this work is the run-up to Gather resume); archive's per-file-identity open question resolved via the file-mutation design; open question 3 (Push's env parameter) resolved -- deferred to Unwind; cross-pause promise fidelity (sub-step 11) synced to its Implemented bullet. - phase-8.md: row 28 remaining reframed as the run-up to flow.gather (the complement-shape restriction plus the file-mutation/archive groundwork feed Gather resume); new step 34 queued -- RecoveryStack.Unwind takes the runtime environment, Push/PushNested drop it. - 5.2-recovery-serialization.md: complement-restriction + receipt-names-its-own-undo notes. Docs-only; no code change.
…ism, slice 1 partial) First verified slice of the unified file-mutation receipts mechanism (see docs/plans/extract-starlark-from-op/phase-8/file-mutation-receipts.md). - Streaming write core: file.Provider.write takes io.Reader and copies via io.Copy (constant memory, and the kernel copy_file_range/sendfile fast path for file-to-file); WriteText/WriteBytes wrap their content in strings.NewReader. - file.Receipt gains an explicit kind (MutationKind: create/update/delete file, create/delete dir) with Kind()/SetKind(), serialized as `kind` in MarshalYAML and read in RestoreEncoded. - prepareWrite stamps MutationCreateFile / MutationUpdateFile on every write's receipt, so write/Copy/Move writes carry their kind. Verified in the worktree: `make build` compiles lore+star (pkg/op + file clean), only the standing cmd/writ break remains; `make test` shows pkg/op + file green except the standing TestBackup_DefaultSuffix. No new failures. Remaining (see the doc's Implementation status section): the exported WriteFile/DeleteFile/MakeDir/RemoveDir surface and CompensateFileMutation close slice 1; then the compensation-action seam (slice 2, cross-provider) and the archive rewrite (slice 3). archive/provider.go WIP is intentionally not in this commit. Docs: file-mutation-receipts.md gains an Implementation status / handoff section (verification model + done/next/remaining); phase-8.md notes slice 1 in progress.
…l, methods land with consumers Records the design decisions settled this session for the unified file-mutation receipts mechanism (docs/plans/extract-starlark-from-op/phase-8/file-mutation-receipts.md). - Decision 5: no +devlore:internal flag. Step 24's invariant — announced iff activation-record-first — is the announce discriminator; the four mutation methods carry no *op.ActivationRecord (they are mechanism, not dispatchable actions), so they are excluded by absence, not a bespoke flag. The discriminator is gated on step 24 giving the ~17 existing activation-record-free file actions their leading activation record. - Decision 6: env comes from the resource, not the provider (step 26 shape). The methods take a *Resource (CompensateFileMutation a receipt that holds one) and source env from it; until step 26 they ride the still-present p.RuntimeEnvironment() helpers (option 1 — functionally identical today). - Slice 1 closes at items 1-3 (committed). Items 4-5 are re-sliced to land with their consumers: CompensateFileMutation -> slice 2 (the seam's routing is its first caller); the four exported mutation methods -> slice 3 (with the archive rewrite that consumes them, sequenced after step 24). phase-8.md step 28 row updated to match.
…sationAction Records the settled design for the file-mutation compensation seam (docs/plans/extract-starlark-from-op/phase-8/file-mutation-receipts.md). - Two-field split on op.ReceiptBase: forwardAction (the provider method that dispatched -- audit / Trace tally) and compensationAction (identifies the compensator -- the undo). Retires the action/actionPath short-vs-canonical framing; the rename makes the two roles explicit. - The receipt's CONSTRUCTOR stamps compensationAction, because the type knows its undo (a *file.Receipt is always undone by the file provider), so archive-built file receipts name file's undo for free. Commit stamps forwardAction from the dispatching unit and falls back to compensationAction only when the constructor left it empty -- a transitional fallback (correct where dispatcher == creator), dropped at slice 2b. - Compensation resolves via a compensator-name index (every Compensate* method by its dotted name); the lookup resolves compensationAction through it, falling back to the existing forward->.undo path. File's per-action Compensate companions collapse into CompensateFileMutation (dispatching on Kind), and the "compensable forward requires a Compensate<Name> companion" registration check relaxes. - Slicing: slice 2 = file + framework; slice 2b finishes the cross-provider migration and drops the fallback; slice 3 = archive rewrite consuming the exported mutation methods.
…atingAction Slice-2 step 1 of the file-mutation compensation seam (foundation only -- no behavior change). See docs/plans/extract-starlark-from-op/phase-8/file-mutation-receipts.md. The single action/actionPath pair carried two unrelated jobs; split the names to the two roles, using the saga literature's forward / compensating terminology (Garcia-Molina & Salem, "Sagas", 1987): - ReceiptBase.action -> forwardAction: the provider method that dispatched (audit / Trace.Summarize tally). - ReceiptBase.actionPath -> compensatingAction: identifies the compensator that undoes the receipt. Renamed across the Receipt interface, the ReceiptBase getters (Action()/ActionPath() -> ForwardAction()/CompensatingAction()), the ReceiptData wire shape, and the recovery-stack envelope (receiptEnvelope/recoveryEntryData). Wire keys: action -> forward_action, action_path -> compensating_action. The five provider receipts' op.ReceiptData literals follow; their standalone flat-marshaler `action` keys are left as-is for now (a separate format, addressed later). Behavior is unchanged: Commit still stamps both from the dispatching unit, and the compensation lookup still keys on compensatingAction (the renamed actionPath). make build clean (cmd/writ standing break only); make test shows no new failures.
… independently Slice-2 step 2 of the file-mutation compensation seam. Behavior-preserving groundwork for the constructor-stamped compensator identity (slice-2 step 4). See docs/plans/extract-starlark-from-op/phase-8/file-mutation-receipts.md. - Commit: forwardAction is always stamped from the dispatching unit (the audit identity); compensatingAction is stamped from the unit only when the constructor left it empty -- a transitional fallback for not-yet-migrated providers (whose dispatcher is their own creator), removed at slice 2b. No behavior change yet: no constructor stamps compensatingAction until slice 4, so the fallback always fires and both fields equal the dispatch action, as before. - The recovery-stack envelope (receiptEnvelope / recoveryEntryData) gains a compensating_action key. MarshalYAML writes it, fromEntries restores it independently (no longer aliased to forward_action), and reconstructReceipt reads compensatingAction. compensatingAction now round-trips on its own wire field. make build clean (cmd/writ standing break only); make test shows no new failures (pkg/op + plan lifecycle/resume suites pass).
added 25 commits
July 22, 2026 22:06
…e tree Both docs inventoried a deleted layout -- internal/execution (with flow/ and provider/ under it), internal/writ + five subpackages, internal/lore, internal/starlark, internal/host, internal/shell -- and package-reference.md additionally listed per-symbol elements (public/private functions per package), a maintenance mode that had rotted wholesale. Rewritten from a full enumeration of the live tree (pkg/internal/cmd package docs greped, not recalled): - package-hierarchy.md: the three-layer map (cmd -> internal -> pkg) with the dependency rule (pkg never imports internal/cmd; op is the hub with the op-free capability packages beneath it) and the current tree -- writ's twelve command subpackages, star's eleven tool providers, the 14 pkg/ capability packages; - package-reference.md: a one-line-per-package roster (58 packages across three grouped tables), cross-linked to the owning architecture docs; symbol-level reference is explicitly delegated to go doc. Decision (c) settled: compact and HAND-MAINTAINED; a generator is a possible future charter if churn warrants (recorded in both banners and the step doc). Step-51 charter + phase-8 row advanced (slices 1-7 landed; slice 8 -- 8-rust-migration, decision (b) -- remains).
…rical Decision (b) settled (the 3.3 precedent): the port draft argues from the pre-op world -- Tombstone, action_reflect.go, ActionRegistry, Result/ Compensator as bare any aliases -- and several of its motivating complaints were since answered IN GO: typed receipts + the Compensator interface (steps 40/42), reflect-once baked adapters (step 43), the sealed graph and compiler-checked action names (step 32). Since a port's premise is "describe the system to port," any real Rust effort must re-ground against the landed architecture; the banner says so and the body is retained unedited as the record of the original reasoning. En route: its plan-doc link fixed (../plans/rust-migration.md -- the old target never existed). Index entry -> Historical. With this, ALL 8 STEP-51 SLICES ARE LANDED and all three chartered decisions are settled (3.3 archived; package docs compact hand-maintained with generation deferred; 8 historical). What remains of step 51 is its exit gate: the architecture coherence verification plus the in-flight design-document inventory. Step-51 charter + phase-8 row updated.
…d; COMPLETE
Three scripted passes over all 96 docs/architecture files:
1. Links + anchors (GitHub-slug-accurate): one real break -- 3.1's three
fragments pointed at 3.2's renamed "Provider Registration" heading --
fixed to #registration.
2. Stale-vocabulary census (full pre-op pattern set; archived/historical
exempt): all rewritten-doc hits are deliberate history mentions. Two docs
OUTSIDE the original debt inventory caught:
- 3.1-provider-loading: the module-loading half is current (tree-verified
@devlore///With()/loader cache) but the lifetime taxonomy
(LifetimeStateless/Phase/Session: zero hits) and descriptor Registration
Examples never landed -- status banner applied naming the landed model
(environment-scoped construction; announce-with-metadata); the full
rewrite recorded as step-51 residual debt;
- 2.4-hermeticity: the command-layer to-do targeted deleted
internal/writ/commands.go -- supersession note maps each of its four
items to where the concern landed (steps 33/47/48: run index replacing
receipts+StateView, deploy/snapshot packages).
3. Path-rot check (every pkg/internal/cmd path in the 16 not-rewritten docs):
star-extensions' file table cited the folded-away cmd/star/extension/
package -- fixed (application.go owns discovery + the registry); 6.1's one
dead citation is already self-flagged in place and left as its own record.
Gate deliverable 3 recorded in the step doc: the in-flight design inventory,
14 rows (eventing 2.8, the control plane's events half, elevation + brokers,
distributed/planner visions, both unimplemented appendices, the sidecar,
3.1's lifetime proposal, the loop construct, deferred compensation shapes,
the Merkle digest, star's planned items) with summary, status, and design +
plan links.
STEP 51 COMPLETE 2026-07-22. Residual debt: the 3.1 lifetime-half rewrite
(unchartered; charter on demand). Phase-8 row updated.
Frame elevation's problem space as an opening section of 6.1, grounded in the real provider surfaces: - Substrate 1, command execution -- shell.Provider.Exec, powershell.Provider.Exec, pkg.Provider.Install/Remove/Upgrade, service.Provider.Start/Stop/Enable/... . The "sometimes elevated, sometimes not" is already encoded as per-backend NeedsSudo() (systemd true, launchd user-agent false, MacPorts true), and pkg/platform's runShellCommand inlines `sudo -n` under a standing TODO(elevation) this design closes. Poses two questions: how do we manage these processes, and how do we elevate when we must. - Substrate 2, network access -- appnet.Provider.Download (today bare http.DefaultClient). Any provider method that needs one can be passed an elevation token; elevation.Provider is the token source (mint/lease a SecurityToken from a broker, bridge by-value, audit, release). - The bridge: IdentityAssumption is shared across both substrates; HostEscalation serves only command execution. Elevation acquires a capability; the substrate consumes it. Step-38 charter and the phase-8 parent row record the framing and flip step 38 to in-progress. Design only -- implementation is a later step.
Structure-only reorganization -- every function body byte-identical; zero behavior change. Verified: make build clean, full make test green, vet clean, gofmt clean. Per the section-by-section audit against noblefactor-ops/docs/guides/go-style-guidelines.md: - SS1: interface guards added (Compensator -- the relationship that was invisible without one -- plus json.Marshaler/Unmarshaler); supporting types (receiptRestore, recoveryEntry + its methods, recoveryEntryData + its codecs) collected into a SUPPORTING TYPES region at the end; single-file free functions (errStatus, receiptTypeForAction, reconstructReceipt, retypeResult) into a HELPER FUNCTIONS region after it; - SS2/SS3: the full region hierarchy imposed -- EXPORTED (State management: Err/Len/Result/Stamp/UnitID; Behaviors: Compensate..Unwind) then UNEXPORTED (fromEntries..supersede), alphabetical within each group, exported and unexported no longer interleaved; - SS4: Returns sections added to Len/MarshalJSON/MarshalYAML; errStatus given a full block; Returns keys back-ticked (toCompensate/receiptOrNil/ recoveryStackOrNil/Receipts); - SS8: the three over-120 lines rewrapped (Push doc, the reconstructReceipt call in rearm, the NestedStackByUnitID doc); - SS10: the two multi-file free functions moved to helpers.go -- invokeCompensateForReceipt (also called from receipt.go; its stale "RecoveryStack.Push's pre-bound closure" doc corrected to the real caller, ReceiptBase.Compensate) with ignoreNotCompensable (its only dependent), and newRecoveryStack (called from graph_executor.go + subgraph.go); helpers.go slots them alphabetically and gains the errors import. Pre-existing and untouched: rearm's gocognit 23 (body unchanged) and the IDE whitespace lint on the SS8-mandated blank-after-signature pattern, which the whole repo carries.
Structure-only reorganization -- every function body byte-identical; zero behavior change. Verified: make build clean, full make test green, vet clean, gofmt clean. Per the section-by-section audit against noblefactor-ops/docs/guides/go-style-guidelines.md: - SS1: interface guards added (Compensator -- the relationship that was invisible without one -- plus json.Marshaler/Unmarshaler); supporting types (receiptRestore, recoveryEntry + its methods, recoveryEntryData + its codecs) collected into a SUPPORTING TYPES region at the end; single-file free functions (errStatus, receiptTypeForAction, reconstructReceipt, retypeResult) into a HELPER FUNCTIONS region after it; - SS2/SS3: the full region hierarchy imposed -- EXPORTED (State management: Err/Len/Result/Stamp/UnitID; Behaviors: Compensate..Unwind) then UNEXPORTED (fromEntries..supersede), alphabetical within each group, exported and unexported no longer interleaved; - SS4: Returns sections added to Len/MarshalJSON/MarshalYAML; errStatus given a full block; Returns keys back-ticked (toCompensate/receiptOrNil/ recoveryStackOrNil/Receipts); - SS8: the three over-120 lines rewrapped (Push doc, the reconstructReceipt call in rearm, the NestedStackByUnitID doc); - SS10: the two multi-file free functions moved to helpers.go -- invokeCompensateForReceipt (also called from receipt.go; its stale "RecoveryStack.Push's pre-bound closure" doc corrected to the real caller, ReceiptBase.Compensate) with ignoreNotCompensable (its only dependent), and newRecoveryStack (called from graph_executor.go + subgraph.go); helpers.go slots them alphabetically and gains the errors import. Pre-existing and untouched: rearm's gocognit 23 (body unchanged) and the IDE whitespace lint on the SS8-mandated blank-after-signature pattern, which the whole repo carries.
Six summaries wrapped onto a second line, violating SS4's one-line-title rule. Each is split into a one-line title plus an elaboration paragraph, preserving every word of meaning: - ResultByUnitID (the parent-chain search moves to the elaboration) - supersede (the receipt-or-substack detail moves down) - recoveryEntry.toCompensate (the compensator-selection detail moves down) - recoveryEntryData.UnmarshalJSON (the structural-discrimination detail moves down) - recoveryEntryData.UnmarshalYAML (the entries-key detail moves down) - helpers.go ignoreNotCompensable (the declines-to-compensate rationale moves down) Verified by a mechanical sweep (first doc line followed by a text line = violation): both files now report clean. gofmt clean; make build clean. Comment-only -- zero behavior change.
Cleanliness pass over 6.1-privilege-elevation.md; prose/structure only, no design change: - :210 -- "provenance/integrity" -> "integrity + authenticity" (drops the banned term "provenance"); paragraph rewrapped (was 141 cols); - :270-271 -- "env-specific" -> "environment-specific", "env-config" -> "environment-config" (banned "env" abbreviation); rewrapped (was 121); - :280-281 -- rewrapped (was 129); - :140 -- code comment "env vars" -> "environment variables"; - :184 -- YAML comment "<env>" -> "<environment>". No non-table line over 120; no residual "provenance" or "env" abbreviations.
…gn resumes post-PR
Fold the configuration-foundation dependency into the step-38 charter and set
the step aside; its design completes after phase 8's PR to develop.
- New "Configuration dependency" section: elevation.ProviderConfig assumes the
recursive-tree config foundation, which is designed but largely unimplemented
-- pkg/devconfig still carries the flat predecessor (Config = map[string]Section,
config.go:45; no ConfigBase/Path), no loader/overlay/${} converter; the
recovery is configuration plan item 1 (unstarted). elevator/config.go is a
freestanding stub carrying the environment-keyed shape 6.1 supersedes. So the
design proceeds now (against the settled config design); the implementation
(a later step) is gated on the config reshape + loader.
- Status flipped in-progress -> deferred in the charter (frontmatter + body) and
the phase-8 parent row; a set-aside note added to both.
Design only; no code change.
Design-only (implementation deferred to a post-PR step). The five open decisions in 2.8 are settled, worked interactively and corroborated by an external review: 1. slog is the primary logging API; status.Narrator / result.Pipeline sit above it as a tee handler (local render + otelslog copy to OTel). 2. Categories = narrations, diagnostics, operational events; operational events bracket the other two (span brackets its correlated log records). Attribute names frozen OTel-native (verified vs semconv 1.43.0): event.name, log.iostream, code.function.name, instrumentation scope, trace_id/span_id -- dropping the invented event.category / stream / log.classification. 3. Eventing = OpenTelemetry; the Collector is the router (no hand-built sink registry), with a bounded in-process tap for terminal narration and the live control-plane Subscribe() watch. 4. Metrics via the Collector connector by default; in-app Metrics API reserved. 5. Hook reconciliation: keep OnNode*/OnSubgraph*, add OnRunStatusChanged (run-level, every phase/condition transition); nested subgraphs via OnSubgraph*; sub-events as event.name records; inline emits route through the callback. Plus a Filesystem-persistence section (Collector fileexporter default; stdoutlog->file fallback; the JSON exporter MUST target a separate file, never the program's real stdout/stderr). 2.2: added the run-state-machine mermaid diagram (the machine was documented but undiagrammed). A RunStatus conflict surfaced during the work was resolved by reverting the session's PreflightFailed proposal -- the settled + landed model (pre-flight failure -> execution_failed + ReasonPreflightFailed) stands. Status companion, step-50 charter, and the phase-8 row flipped to design-solidified; implementation deferred.
…e comment
Two steps the status table flagged as "remaining" were stale metadata, not work:
- Step 51: the phase-8 row's status cell opened with "in-progress" while its own
body declared "STEP 51 COMPLETE 2026-07-22" (and the charter frontmatter reads
COMPLETE) -- leading label corrected to complete.
- Step 23: the Merkle-root directory digest ALREADY landed 2026-07-18
(file.Directory.Digest, directory.go:105; merkleRoot at :316). The charter
frontmatter's "slices 3-4 pending" contradicted its own body ("all four slices
landed; suite green") -- aligned to COMPLETE; the phase-8 row's stale
"remains a must-land deliverable before phase-8 closes" -> landed, with refs.
Plus file/resource.go: the base file.Resource.Digest doc comment still said the
Merkle scheme was "deferred until that split (step 23)" -- the split shipped and
file.Directory implements it, so the base erroring for directories is correct by
design, not a deferral. Also Link -> SymbolicLink and the orphaned link ref
removed. Comment-only; gofmt clean.
…ixtures Direct method-level tests to the step-24 bar, filling the gaps the step-39 matrices surfaced. Full `make test` green, 0 FAIL. Go units: - plan (1-3): Plan error door (no-dot / unknown-provider / unknown-method / malformed non-string kwarg), Origin scope-stamping, Clear ledger reset. - file (4-7): WalkTree/CompensateWalkTree fold + compensation, Name/Parent/Root edges, Move forward-path, RemoveAll subtree; (8) observe measurement fields (Size/Mode/ModTime) beyond the existing Exists-only unit. - git (14-15): Checkout/Pull (argv via the dry-run narration seam -- they have no argv hook) + Observe state facts. - encryption (17): CompensateEncryptFile x3 mirroring the decrypt analogs. - function (18): Call arg/kwarg passing, error propagation, non-callable. - powershell (11): Exec success/failure/empty-command, pwsh-gated via requirePwsh. Fixtures (run via explicit Go runners in devloretest/backfill_test.go -- fixtures are not glob-discovered): file.find (9), template.render_bytes (13). Re-dispositioned (the .star harness cannot stage these; reasons in the step doc): observe fixture (Entry-typed input), wait_until edges (10, proven at executor level; match-after-N needs mutable per-poll state), powershell (12) / appnet (16) fixtures (no fixture-level skip for pwsh/network). Charter + phase-8 row record the disposition; the per-provider status-doc gap-cell sweep is the final Exit step (pending).
…s; COMPLETE The final step-52 Exit item: every per-provider status companion's gap cell now names the landed test(s) and file (or records the re-disposition + reason, pointing to step 52's Disposition). The adjacent Go/Starlark coverage cells and the Outstanding sections were made coherent in the same pass -- no cell or section still frames a step-52-landed item as missing/absent/"none". Verified: zero step-52 staleness across all nine docs (3.5.2/3/4/7/9/10/12/13/14). Notable: powershell's doc had a hard contradiction (still called it "the only provider with zero coverage") -- now past-tense; file's WalkTree/Name/Parent/Root Go cells and function's Call cell were bare `—` against landed tests -- now populated. Step 52 flips to COMPLETE (Exit satisfied: rows delivered/re-dispositioned, test names greppable, make test green, status-doc gap cells swept). Phase-8 row updated.
MarshalYAML: invert the entry guard (skip-nil -> append-non-nil) plus a blank line; behavior-identical (same non-nil compensators collected). Doc-comment grammar only elsewhere (commas, "at resumption time", "reparsed", a sentence split). No behavior change; gofmt clean; make test green.
O1 was settled months ago and only left open in the ledger. The target-driven direction (one lookup, no source-first dispatch, no fallback) shipped as step 15's op.Convert cascade + the SourceConverter/TargetConverter opt-in interfaces (complete 2026-07-03), and O2 removed the old source-first path (done 2026-06-15). op.Convert (convert.go) is the single conversion path; there is no unmarshaler.go and no ToUnmarshaler. The five "open questions" are answered by that implemented design. - O1 section rewritten as a SETTLED record pointing at step 15 + O2 + op.Convert; the stale "current source-first pipeline" narrative and the five open questions removed. - O1 row + the exit-blocker line updated: O1 settled, O2 code-done, only O3 remains. - Corrected the eliminated term: this is CONVERSION (op.Convert), not marshaling (marshaling is reserved for JSON/YAML serialization).
…rected O2 was code-done 2026-06-15 and only left open in the ledger. The section rewritten as a CODE-DONE record: the ~900 lines of source-first dispatch (the Unmarshaler interface + ToUnmarshaler + 11 unmarshal_*.go files) are gone, replaced by the target-driven op.Convert (see O1); the surviving names are the starlarkbridge data types, adapters, and runtime. Dropped the stale exit-criterion, open-questions, and 19-file to-do inventory. - O2 row status corrected: "conversion design (O1) is settled" (was "the marshaling-design questions tied to O1 remain open"). - Fixed one conversion-context use of the eliminated term in step 25's row (the fn wrapper "converts the Go args -> starlark ... converts the result back to Go"). Remaining "marshal" hits are legitimate JSON/YAML serialization or historical file/symbol names.
…outer The package is the HTTP wire listener for op runs -- it serves control commands AND the SSE event stream AND status polling, so "controlhttp" undersold it; "server" is the accurate name (and it grows the eventing endpoints under step 50). The primary type is a stateless run-id -> plane router (the doc already said so) and it is the http.Handler served externally, so Server -> Router: accurate, no server.Server stutter, idiomatic (mux.Router / chi.Router). The receiver stays `s` because the http handlers already take `r *http.Request` / `w`. - package controlhttp -> package server; server.go -> router.go, server_test.go -> router_test.go. - Nothing outside the package imported it, so no consumer updates. - Verified: make build clean, make test green (0 FAIL).
…rected O2 was code-done 2026-06-15 and only left open in the ledger. The section rewritten as a CODE-DONE record: the ~900 lines of source-first dispatch (the Unmarshaler interface + ToUnmarshaler + 11 unmarshal_*.go files) are gone, replaced by the target-driven op.Convert (see O1); the surviving names are the starlarkbridge data types, adapters, and runtime. Dropped the stale exit-criterion, open-questions, and 19-file to-do inventory. - O2 row status corrected: "conversion design (O1) is settled" (was "the marshaling-design questions tied to O1 remain open"). - Fixed one conversion-context use of the eliminated term in step 25's row. - Rides along (same file): the phase-8 status-row reference pkg/op/controlhttp -> pkg/op/server (the rename below).
The doc references for the package rename: controlhttp -> server, Server -> Router, server.go -> router.go. Rosters (package-hierarchy, package-reference) + prose (step-36/50, 3-operation-namespaces + status, 2.7-control-plane status). Two prose knock-ons: "command server" -> "command surface" (avoid "server's server"; the doc's own vocabulary), and bare controlhttp -> backticked `pkg/op/server` (bare "server" is ambiguous). No controlhttp reference remains anywhere in the repo.
The install step piped the golangci-lint installer from the moving `master` ref, so a mismatched binary/checksum pair failed verification and aborted the whole quality-gate job before any lint or test ran. Pin the installer to the v2.10.1 release tag -- the tag's install.sh installs the matching v2.10.1 binary, so the checksum is self-consistent -- and wrap it in a 3x retry to ride out transient network blips. v2.10.1 is the version already in use locally; it lints the whole repo clean.
… methods
The knowledge-extract job crashed with "NoneType is not iterable". The goast
provider initialized its result slices with `var result []T`, so a no-match
query returned a nil Go slice. The Starlark bridge converts a nil slice to
None (not an empty list), and extract.star then iterated it -- e.g. a scope
with no matching calls, a zero-arg call's args, an empty struct's fields.
Initialize every Starlark-facing slice as `[]T{}` so "present but empty"
round-trips to an empty Starlark list. Covers the six exported result lists
(Calls, Composites, ConstGroups, Funcs, Methods, Structs) and the seven nested
lists that become list-valued fields (call args, callable params, composite
elems, const-group constants, struct fields, Deps.Files, Metrics.Files). The
internal accumulators that never reach Starlark are left as-is.
Verified: knowledge extract runs clean over the whole repo; make test-race
green (0 FAIL, 0 races); ./build/star lint go ./... clean; make vet clean.
In a linked git worktree, .git is a gitdir-pointer file, not a directory, so opening .git/info/exclude fails with ENOTDIR. loadPatterns treated only ErrNotExist as "no patterns", so NewTracker propagated the error and every file.find call (and with it star lint shell) died in a worktree checkout. Treat ENOTDIR like a missing file: the exclude layer is skipped, in-tree .gitignore files still apply. Resolving the gitdir indirection to load the main repository's info/exclude is possible follow-up work. Also in pkg/gitignore while touching it: a regression test for the worktree case, a Returns section on Tracker.Root, doc comments on the test functions, and removal of the unused assertContains/assertNotContains helpers.
…ries The phase-8 file provider returns []Entry (file.Regular values) from find; the lint-shell, lint-go-style, and lint-markdown extensions still called sorted() directly on the result, which dies with '"<" not supported between "Regular" values' -- the quality-gate Lint Shell failure on PR #288. Derive the path per entry with source_path.rel() before sorting, the idiom lint-copyright already uses. Also fix lint-go-style's line 47: ast.check_compliance is a method and was iterated without being called ("builtin_function_or_method value is not iterable"). The bug was unreachable until discovery worked again.
A compliant source file produced a nil []ComplianceViolation, which the Starlark bridge converts to None, so lint go-style crashed with "NoneType value is not iterable" on any clean package. Same class as c232a89; this method was missed because the lint-go-style call-site bug made the path unreachable. Initialize the accumulator as []ComplianceViolation{}.
… and APIs The knowledge-extract job failed at extract.star:927: the migrate source moved from internal/writ/migrate to cmd/writ/writ/migrate. Beyond the path, three more breaks were latent behind that first failure: - internal/execution is retired with the old engine. Drop the execution scan and engine-graph.json generation (graph.go, *Op Name() methods, NodeStatus/SlotValue/GraphState no longer exist anywhere), and reduce build_ops_knowledge to an explicit not-yet-implemented note (*Service structs exist nowhere in the tree). Regenerating an engine schema from the pkg/op graph model is separate design work. - find_files in both validate.star files: file.find rejects patterns outside the scoped root (the registry checkout is out-of-tree in CI) and now returns resource entries. Use file.glob -- every pattern is single-star, and glob is unconfined -- and derive source_path.abs(). - yaml.parse now fails the run on malformed input and its Resource has no .parsed attribute; drop the parsed-None guard and return result.valid/result.errors directly. Verified against a scratch copy of devlore-registry: extract --domain all, package validate, and knowledge validate all exit 0; the only content change is the systems-reference.yaml source-path metadata line. A deliberately corrupted lifecycle.yaml fails package validate with "1 of 10 files failed validation", proving discovery and validation are live.
David-Noble-at-work
deleted the
refactor/extract-starlark-from-op.phase-8
branch
July 25, 2026 06:59
This was referenced Sep 26, 2026
David-Noble-at-work
added a commit
that referenced
this pull request
Sep 30, 2026
…tc/os-release before it closes it (#986) * fix(writ): plan for #959, writ detects the Linux distribution detectDistro closes /etc/os-release before reading it, so DISTRO has been empty on every Linux host since 57a9fd2. The plan: readDistro(path) opens, defers the close, and scans; tests that go through the file and fail first; the adopt test that pinned the bug refuses a word no host detects; PR B's owed ticks. Lane 18 of #949; #944 follows as lane 19. Claude-Session: https://claude.ai/code/session_01DuNfZFyfxBoD9pdaiDEMrY * fix(writ): writ detects the Linux distribution; detectDistro reads /etc/os-release before it closes it detectDistro closed /etc/os-release between the open and the scan, so every read failed and the DISTRO segment has been empty on every Linux host since 57a9fd2 (#288): no directory with a distribution suffix matched anywhere. It now calls readDistro(path), which defers the close straight after the open. The path is the seam the tests use: readDistro runs against os-release fixtures (Ubuntu with its ID_LIKE, Debian, a quoted ID, an unknown ID, no ID, no file), and TestDetectSegments checks DISTRO on a Linux host whose os-release carries an ID. Both failed against the old close and pass now. The adopt test that pinned the bug refused "Ubuntu", which is matched on Ubuntu now; it refuses NoSuchPlatform instead. DISTRO stays the ID field, capitalized: `debian` matches `.Debian`. The lineage, so that Ubuntu also matches `.Debian`, is #944, the next PR. PR B's plan takes its two owed ticks, the installers jobs on #984 and the site's sync, and is complete. Resolves #959 Claude-Session: https://claude.ai/code/session_01DuNfZFyfxBoD9pdaiDEMrY
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.
Prototype → foundation. Before this branch,
pkg/opcould build and run a graph once. This branch makes it foundational infrastructure: a sealed, restartable, compensable, observable graph-execution engine with plan-time authoring in Go and Starlark, a typed provider ecosystem, content-addressed resources, and settled designs for the systems that drive real devlore operations. 59 of 63 tracked steps complete; the rest are deferred by design.What is now true
RuntimeEnvironment, per-subgraph child executors; graph + trace rehydrates and continues. (1-system-model.md,2.2-phase-execution.md)plan.*invocations, thesubgraph/choose/gather/wait_untilcombinators, orphan detection, topological type-check. (2.3-orchestration-primitives.md,3-operation-namespaces.md)compensation_failedwith a restart contract. (compensation-failure-contract.md,5.2-recovery-serialization.md)RunStatus{Phase, Condition, Reason},{completed, stopped} × Conditionterminals,TransitionPolicy. (2.2,2.6-execution-policies.md)4-resource-management.md,2.1-typed-slots.md)op.Convert+SourceConverter/TargetConverteropt-ins (the old source-first path retired). (pkg/op/convert.go)3.2-projected-provider-api.md,3.5-provider-catalog.md)writmigrate/deploy,lore, graph signing (pkg/signing), content identity in the trace. (graph-signing.md,2.4-hermeticity-guarantees.md)pkg/platform+ per-backendNeedsSudo(). (3.4-platform-package-managers.md)pkg/op/server— a run-id → control-planeRouterserving commands + SSE events + status. (2.7-control-plane.md)configuration.md), OpenTelemetry-native eventing (2.8-eventing-infrastructure.md), elevation problem space (6.1-privilege-elevation.md).Deferred by design (post-PR)
pkg/op→pkg/workflowrename (deferred pending closure).Readiness
Repository-wide
make testgreen (0 FAIL); all apps compile; the phase-8 PR gate (step 24) and test gate (step 18) MET. O1 (conversion design) + O2 (bind retired) settled; test backfill (step 52) complete.