Skip to content

Fix YAML block scalar indentation in docs-publish workflow - #9

Merged
David-Noble-at-work merged 1 commit into
developfrom
fix/docs-publish-yaml-indent
Jan 24, 2026
Merged

David-Noble-at-work merged 1 commit into
developfrom
fix/docs-publish-yaml-indent

Conversation

@David-Noble-at-work

Copy link
Copy Markdown
Contributor

Summary

  • Fix the actual cause of docs-publish workflow failure: unindented lines in the YAML literal block scalar broke GitHub's YAML parser
  • Extract multi-line PR body into a shell variable with proper indentation
  • Validated with actionlint

Test plan

  • actionlint passes
  • docs-publish workflow runs after merge to develop

The multi-line --body string had lines at column 0 which broke the
YAML literal block scalar parsing. Extract body into a shell variable
with proper indentation.
@David-Noble-at-work
David-Noble-at-work merged commit 621aacf into develop Jan 24, 2026
1 check passed
@David-Noble-at-work
David-Noble-at-work deleted the fix/docs-publish-yaml-indent branch January 31, 2026 03:57
David-Noble-at-work pushed a commit that referenced this pull request Mar 3, 2026
Append-only resource ledger with monotonic ID assignment, URI→ResourceID
namespace with Resolve/Shadow semantics, and scheme-aware URI builder
with file path canonicalization. Pure additions — no existing code changed.
Decision #9: the ledger is the sole source of truth for resource identity
and lineage. Node annotations (resource.input/resource.output) eliminated
from Phase 2 — they would duplicate ledger data and cannot represent
resources produced at runtime.
David-Noble-at-work pushed a commit that referenced this pull request Mar 3, 2026
Append-only resource ledger with monotonic ID assignment, URI→ResourceID namespace with Resolve/Shadow semantics,
and scheme-aware URI builder with file path canonicalization. Pure additions — no existing code changed. Decision #9:
the ledger is the sole source of truth for resource identity and lineage. Node annotations (resource.input/resource.output)
eliminated from Phase 2 — they would duplicate ledger data and cannot represent resources produced at runtime. Legacy
backup annotation flagged for removal in Phase 2.
David-Noble-at-work added a commit that referenced this pull request Mar 3, 2026
…#176)

* feature(e2e): devlore-test binary + graph test harness — Phase 0

Separate binary (cmd/devlore-test/) with process-level isolation for
end-to-end testing of Starlark plan → graph → execute → verify.

- internal/e2e/testrunner/: Runner, TestContext (t namespace), Tracer
- 4 baseline test scripts: write_text, copy, write_and_read, compensation
- Star extension: com.noblefactor.devlore.Test (host.exec pending)
- Makefile: add devlore-test build target, fix stale STAR_REPO path
- Restore t.Skip for #170, #171 (gen template projection gaps)

* feature(op): ResourceManager, NamespaceMap, and URI helpers — Phase 1
Append-only resource ledger with monotonic ID assignment, URI→ResourceID
namespace with Resolve/Shadow semantics, and scheme-aware URI builder
with file path canonicalization. Pure additions — no existing code changed.

* feature(op): ResourceManager, NamespaceMap, and URI helpers — Phase 1
Append-only resource ledger with monotonic ID assignment, URI→ResourceID
namespace with Resolve/Shadow semantics, and scheme-aware URI builder
with file path canonicalization. Pure additions — no existing code changed.
Decision #9: the ledger is the sole source of truth for resource identity
and lineage. Node annotations (resource.input/resource.output) eliminated
from Phase 2 — they would duplicate ledger data and cannot represent
resources produced at runtime.

* feature(op): ResourceManager, NamespaceMap, and URI helpers — Phase 1

Append-only resource ledger with monotonic ID assignment, URI→ResourceID namespace with Resolve/Shadow semantics,
and scheme-aware URI builder with file path canonicalization. Pure additions — no existing code changed. Decision #9:
the ledger is the sole source of truth for resource identity and lineage. Node annotations (resource.input/resource.output)
eliminated from Phase 2 — they would duplicate ledger data and cannot represent resources produced at runtime. Legacy
backup annotation flagged for removal in Phase 2.

---------

Co-authored-by: David Noble <[email protected]>
David-Noble-at-work pushed a commit that referenced this pull request Mar 3, 2026
Graph Integration: wire ResourceManager and NamespaceMap into Graph lifecycle. FillSlot creates implicit edges from
resource identity (OriginNodeID). extractResource reflection helper handles direct Resource, embedded struct, and
map[string]any forms. Removed dead BackedUp annotation (Decision #9: ledger is sole source of truth).
David-Noble-at-work added a commit that referenced this pull request Mar 3, 2026
* feature(e2e): devlore-test binary + graph test harness — Phase 0

Separate binary (cmd/devlore-test/) with process-level isolation for
end-to-end testing of Starlark plan → graph → execute → verify.

- internal/e2e/testrunner/: Runner, TestContext (t namespace), Tracer
- 4 baseline test scripts: write_text, copy, write_and_read, compensation
- Star extension: com.noblefactor.devlore.Test (host.exec pending)
- Makefile: add devlore-test build target, fix stale STAR_REPO path
- Restore t.Skip for #170, #171 (gen template projection gaps)

* feature(op): ResourceManager, NamespaceMap, and URI helpers — Phase 1
Append-only resource ledger with monotonic ID assignment, URI→ResourceID
namespace with Resolve/Shadow semantics, and scheme-aware URI builder
with file path canonicalization. Pure additions — no existing code changed.

* feature(op): ResourceManager, NamespaceMap, and URI helpers — Phase 1
Append-only resource ledger with monotonic ID assignment, URI→ResourceID
namespace with Resolve/Shadow semantics, and scheme-aware URI builder
with file path canonicalization. Pure additions — no existing code changed.
Decision #9: the ledger is the sole source of truth for resource identity
and lineage. Node annotations (resource.input/resource.output) eliminated
from Phase 2 — they would duplicate ledger data and cannot represent
resources produced at runtime.

* feature(op): ResourceManager, NamespaceMap, and URI helpers — Phase 1

Append-only resource ledger with monotonic ID assignment, URI→ResourceID namespace with Resolve/Shadow semantics,
and scheme-aware URI builder with file path canonicalization. Pure additions — no existing code changed. Decision #9:
the ledger is the sole source of truth for resource identity and lineage. Node annotations (resource.input/resource.output)
eliminated from Phase 2 — they would duplicate ledger data and cannot represent resources produced at runtime. Legacy
backup annotation flagged for removal in Phase 2.

* chore: add SPDX header to resource.go and test-race Makefile target

Add missing SPDX license header to resource.go for consistency with
namespace.go and test files. Add make test-race target so race
detection runs through make, not bare go commands.

* feature(op): ResourceManager, NamespaceMap, and URI helpers — Phase 2

Graph Integration: wire ResourceManager and NamespaceMap into Graph lifecycle. FillSlot creates implicit edges from
resource identity (OriginNodeID). extractResource reflection helper handles direct Resource, embedded struct, and
map[string]any forms. Removed dead BackedUp annotation (Decision #9: ledger is sole source of truth).

---------

Co-authored-by: David Noble <[email protected]>
David-Noble-at-work pushed a commit that referenced this pull request Jun 6, 2026
…sign (#6)

Rewrite the step-21.4/#6 plan to the Composite reality. pkg.Provider becomes a thin veneer:
Resource -> []PURL -> router.{Install,Remove,Upgrade}(kwargs) -> adapt []platform.Receipt;
convergence and verification live in the leaf. A materialized []PURL (not an iter.Seq) feeds
the batch-by-type router, which returns one receipt per package in input order. cask becomes a
kwarg (drops the cask positional and runBrewCask); no manager kwarg (routing is by PURL.Type).

Update is retained, two flavors: an automatic, staleness-gated per-leaf refresh (six rules), and
a manual force-refresh -- PackageManager.Update() fans out to every leaf, and the provider keeps
Update / pkg.update -> router.Update(). Neither is built yet (the manager.go "no public Update"
comment was aspirational); tracked as task #9, the prerequisite for the provider's Update verb.

Implementation order: #9 (platform Update + auto-refresh) then the #6 veneer migration, ending at
make check green for pkg/op/... and cmd/lore/...
David-Noble-at-work pushed a commit that referenced this pull request Jun 6, 2026
…part A)

Add the manual force-refresh half of the index-update feature (#9) and harden the shell-out
against interactive hangs. NOT a green tree -- the #6 consumer migration and cmd/writ stale code
remain; pkg/platform itself compiles.

- manager.go: PackageManager gains Update() error -- the manual force-refresh (the automatic
  staleness gate is part B, still pending).
- composite.go: the router fans Update() out to every leaf concurrently and errors.Join's failures.
- driver.go: driver.Update() calls the optional refresher; refresher is the new optional interface
  (apt/dnf/pacman/brew/port implement it; live-store managers snap/flatpak/winget no-op).
- refresh() primitives: apt-get update, dnf makecache, pacman -Sy, brew update, port -N selfupdate
  (real *_os + fail-loud *_other stubs). Elevation follows each manager's ownership model: brew
  none; apt/dnf/pacman/port require sudo.
- helpers.go: runShellCommand made hang-proof -- a 30m timeout, sudo -n (fail fast, never prompt),
  an endless "y\n" stdin backstop for prompts the per-tool flags miss, and DEBIAN_FRONTEND=
  noninteractive. The scattered sudo is a stopgap; a TODO marks where it centralizes into the
  ElevationOffer/Elevator (one credential-cached, policy-governed, audited session).

Empirically: brew update run on a live mac (exit 0); the elevated commands are CI/box-verified.
Deferred: part B (the automatic staleness gate), and the pkg/platform test rewrites -- the test
package does not compile yet, so Update/refresh have no runnable coverage. That is next.
David-Noble-at-work pushed a commit that referenced this pull request Jun 6, 2026
… the new API

Make the pkg/platform tests genuine -- compile and pass -- covering the #9 Part A index-update work.

- helpers.go: runShellCommand becomes an injectable package var (test seam), so tests assert the
  (command, sudo) each leaf's refresh issues without shelling out or needing root.
- update_test.go: composite.Update fan-out + failure aggregation (fake leaves).
- update_darwin_test.go / update_linux_test.go: per-leaf refresh command+sudo -- brew ("brew update",
  no sudo) and port ("port -N selfupdate", sudo) run on macOS; apt/dnf/pacman (sudo) are //go:build
  linux, CI-verified.
- Rewrote 12 stale test files (constructors / detect_* / platform / spec / defaults and the four
  *_managers_other) from the removed per-manager string API to the new factories + Composite router +
  PURL/Receipt contract; platform_test.go retargeted to ResolvePurlType / DefaultPurlType / fail-closed
  routing. pkg/platform tests: 52+ green (re-verified via make).

Docs: the 3.4 verb-surface section now documents Update (manual force-refresh + the in-progress
automatic staleness gate); 3.4 status + pkg-install-reconciler.md record Part A landed and the test
rewrite done.

Tree still red on out-of-scope consumers (#6 pkg/op/provider/pkg, cmd/writ); pkg/platform is green.
David-Noble-at-work pushed a commit that referenced this pull request Jun 6, 2026
…queued plan)

Complete #9 with the automatic half of index update.

- driver.go: stalenessAware interface + driver.ensureFresh(), gating Install/Upgrade/Search/Available
  (never Remove/Installed/Version). A leaf is gated only when it is both a refresher and stalenessAware
  and its index age exceeds refreshTTL; refresh is best-effort (failure non-fatal).
- helpers.go: refreshTTL (24h knob), unknownIndexAge, indexAgeOf(path).
- linux_managers_linux.go: apt -> /var/lib/apt/lists mtime, pacman -> /var/lib/pacman/sync mtime. brew
  and dnf self-manage their freshness and port defers, so they are not stalenessAware (manual Update
  still force-refreshes all five). A successful refresh bumps the mtime, so a batch triggers one refresh.
- update_test.go: cross-platform gate tests (stale -> refresh, fresh -> skip, local ops -> no gate,
  index-consuming ops -> gate) via a fake rawDriver. pkg/platform green (gate mechanism + helpers run on
  macOS; apt/pacman indexAge bodies are _linux, CI-verified).

Docs: 3.4 verb-surface + status + pkg-install-reconciler now record #9 (manual force-refresh + the
automatic gate) implemented and tested.

Also queues the subgraph-result-flow concern as a plan: subgraph-result-flow.md (#10 investigate whether
subgraph executors toss results; #11 two verification tests, Go + Starlark, via flow.Provider.Completed),
referenced from 21-lore-migration.md.

Tree still red on out-of-scope consumers (#6 pkg/op/provider/pkg, cmd/writ); pkg/platform is green.
David-Noble-at-work pushed a commit that referenced this pull request Jun 7, 2026
Both were at status: draft despite shipped work; neither is complete (the #6 veneer remains):
- platform-unification.md (step 21.4): the op-free pkg/platform Composite reshape (#1-4) is committed;
  remaining are #5 (delete the redundant op managers) and #6 (the pkg.Provider veneer + consumer
  migration), both still scoped in its Work breakdown -> in-progress.
- pkg-install-reconciler.md (#9 + #6): #9 Update is complete (Part A manual + Part B auto-gate,
  committed cd92631); the stale "Remaining: #9 Part B" status line is corrected; #6 veneer remains
  -> in-progress.
David-Noble-at-work pushed a commit that referenced this pull request Jun 29, 2026
Config-section naming: a section is named <TypeName>Config, paired with the
type it configures -- RuntimeEnvironment -> RuntimeEnvironmentConfig, Provider
-> ProviderConfig, Broker -> BrokerConfig. Interfaces are exported,
implementations are not. op.BrokerBase is kept as an optional convenience base.

Provider name: elevator -> elevation, following the codebase's domain-noun
convention (encryption.Provider, not encryptor; elevator was the lone
agent-noun among 21 providers). The op-free elevation-contract vs
elevator-provider split is collapsed into one elevation package
(elevation.Provider, elevation.Broker, elevation.BrokerConfig); that package is
no longer op-free.

- 3.2: SectionOf[*Config] -> SectionOf[*ProviderConfig]
- configuration.md: elevation.ProviderConfig / elevation.BrokerConfig in the
  case study; concrete brokers and leaf sub-brokers shown unexported
- 6.1: title, settled-names admonition, the op-free collapse, the
  ElevationProvider collision resolved by package qualification, open items 1
  and 3 marked settled
- phase-8 plan: elevator -> elevation throughout

Deferred to the prose sweep (#9): 6.1's flat per-environment ElevatorConfig
sections now carry the new name but still describe the superseded flat model.
SigningConfig keeps its name pending its paired-type decision.
David-Noble-at-work added a commit that referenced this pull request Jul 25, 2026
* docs(phase-8): chained recovery stacks (option C) + control plane + save/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.

* feat(op): per-subgraph executor + chained recovery stacks (option C); 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.

* fix(op): gate compensation on the complement, route through the Undo 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.

* style(op/provider): bring powershell, regexp, service packages to go-style compliance

* docs(config): settle the broker seam (resource-mirrored); sync plan and 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

* style(op/provider): bring shell, template, ui, yaml packages to go-style compliance

* docs(config): settle Starlark projection of typed sections

- 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

* docs(config): settle set-by / History; retire "provenance"

- 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

* fix(op): carry the complement through dispatch failure so rollback runs

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.

* docs(phase-8): record the pseudo-replay resume design for step 28(b)

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.

* docs(phase-8): step-28 resume design (pseudo replay) + chained-stacks 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.

* feat(op): step 28(b) in-process resume re-entry (pseudo replay)

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.

* feat(op): step 28(b) save->load->resume serialize round-trip

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.

* docs(phase-8): settle Option B -- save the resource ledger in Trace, 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.

* docs(phase-8): correct Option B -- ledger keyed by resource id, references 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.

* feat(op): Option B B1+B2 -- resource ledger save + id-preserving rehydrate

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.

* feat(op): Option B B3 -- compensation-after-resume via Receipt.RestoreEncoded

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.

* docs(phase-8): record e2e lifecycle coverage; reframe step-28 remaining 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.

* docs(phase-8): sub-step 10 redesign -- format-neutral trace reconstruction

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.

* feat(op): format-neutral trace reconstruction -- RestoreEncoded consumes 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.

* feat(op): cross-pause promise fidelity -- resource case + produced-type-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).

* feat(op): Convert handles struct<->map and text-unmarshal (sub-step 11 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).

* feat(op): produced-type-id -- restore retypes a result to its produced 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).

* docs(phase-8): defer re-observe-and-verify to reconciliation drift detection

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.

* docs(phase-8): unified file-mutation receipts design + settle complement-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.

* feat(file): streaming write core + receipt kind (file-mutation mechanism, 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.

* docs(phase-8): re-slice file-mutation surface — drop +devlore:internal, 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.

* docs(phase-8): slice-2 compensation seam — constructor-stamped compensationAction

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.

* refactor(op): rename receipt action fields -> forwardAction / compensatingAction

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.

* feat(op): split receipt Commit stamping; serialize compensatingAction 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).

* docs(broker): settle provider-owned router model

The broker model is a pattern, not framework machinery. A broker is any type
that fulfills the provider's broker interface -- no base to embed, no
registration, no required home for the type. Each provider builds a router --
itself a broker -- that allocates and configures its sub-brokers from the
config passed to its constructor and delegates; the recursion bottoms out at
leaf sub-brokers (the former "services"). op supplies only the interface and
the typed config tree -- no op.AnnounceBroker, no op.WireBrokers, no global
registry, no op.BrokerRegistry trait. pkg/platform's compositeManager is the
worked router precedent.

- 3.2: rewrite "Pluggable brokers" off the self-announce / resource-mirror
  model onto the provider-owned router pattern (interface + router + config
  tree); drop the resource<->broker table, op.AnnounceBroker/op.WireBrokers, and
  the BrokerRegistry trait; dispatch note recast as the router's job
- 6.1: re-align the broker-registry admonition and open-work item to the router
  model (interface-only brokers, provider-built router, no announcements)
- configuration.md: drop op.WireBrokers from the elevator case study; brokers
  typed via the child-type schema against the Brokers parent handle; services
  are leaf sub-brokers
- phase-8 plan: item 4 onto the router model

* feat(op): resolve a receipt's compensatingAction through a compensator-name index

Slice-2 step 3 of the file-mutation compensation seam. Behavior-preserving: it adds the resolution path the
receipt-names-its-own-undo model needs, but no receipt names a compensator until slice 4, so existing
receipts still route via the forward path.
See docs/plans/extract-starlark-from-op/phase-8/file-mutation-receipts.md.

- The registry builds a lazy compensator-name index over every action provider's exported Compensate*
  methods, keyed by dotted name (provider name + snake method, e.g. "file.compensate_write_text").
  CompensatorByName resolves it; the index mirrors the existing lazy productTypeIndex pattern.
- invokeCompensateForReceipt and receiptTypeForAction resolve the receipt's compensatingAction through the
  index first, falling back to the existing forward->.undo path for a compensatingAction that is still a
  dispatch action (a not-yet-migrated provider). New helpers invokeCompensator (mirrors Method.Undo's
  reflection call) and ignoreNotCompensable (shared ErrNotCompensable handling).
- Relaxed the registration check: a compensable forward MAY declare a Compensate<Name> companion (attached
  as its .undo) but no longer must -- a receipt can name its own compensator instead. This sets up the file
  companion collapse (slice 4). The safety the hard error gave now lives at the receipt level.

Verified: a new index test (pkg/op/inventory/compensator_test.go, where every provider is blank-imported)
confirms CompensatorByName resolves real compensators and rejects forward-action names. make build clean
(cmd/writ standing break only); make test shows no new failures.

* docs(archive): design of record, phase-8 plan, and provider catalog

Add the archive-provider design of record (architecture 3.5.1) covering
content-based format detection, the decompressor-to-container pipeline, and
extraction as unified file-mutation receipts; the phase-8 implementation plan,
which absorbs file-mutation-receipts slice 3; and a new provider catalog (3.5)
indexing every provider. Wire both new docs into the architecture index and
repoint file-mutation-receipts slice 3 at the archive plan.

* docs(naming): settle config-section + elevation provider names

Config-section naming: a section is named <TypeName>Config, paired with the
type it configures -- RuntimeEnvironment -> RuntimeEnvironmentConfig, Provider
-> ProviderConfig, Broker -> BrokerConfig. Interfaces are exported,
implementations are not. op.BrokerBase is kept as an optional convenience base.

Provider name: elevator -> elevation, following the codebase's domain-noun
convention (encryption.Provider, not encryptor; elevator was the lone
agent-noun among 21 providers). The op-free elevation-contract vs
elevator-provider split is collapsed into one elevation package
(elevation.Provider, elevation.Broker, elevation.BrokerConfig); that package is
no longer op-free.

- 3.2: SectionOf[*Config] -> SectionOf[*ProviderConfig]
- configuration.md: elevation.ProviderConfig / elevation.BrokerConfig in the
  case study; concrete brokers and leaf sub-brokers shown unexported
- 6.1: title, settled-names admonition, the op-free collapse, the
  ElevationProvider collision resolved by package qualification, open items 1
  and 3 marked settled
- phase-8 plan: elevator -> elevation throughout

Deferred to the prose sweep (#9): 6.1's flat per-environment ElevatorConfig
sections now carry the new name but still describe the superseded flat model.
SigningConfig keeps its name pending its paired-type decision.

* docs(phase-8): collapse the file-mutation surface to WriteFile; drop DeleteFile/MakeDir/RemoveDir

WriteFile is the only mutation worth a dedicated method: it factors out non-trivial shared logic (streaming io.Copy +
displacement-to-recovery, reused by WriteText/WriteBytes and archive.extract) and adds a capability the existing surface
lacks (streaming from a first-class stream.Resource), so it is added and exposed as a public action.

DeleteFile/MakeDir/RemoveDir are dropped, not built: each is an os-call-plus-receipt wrapper identical to the existing
Remove/RemoveAll/Mkdir, which already perform the operation and (post-slice-2) return self-describing receipts naming
CompensateFileMutation -- and none has a caller (extract only creates; compensation does its os ops directly; the public
deletes are file.remove/file.remove_all). archive.extract loops WriteFile (files) + the existing Mkdir (dirs); deletion
stays file.remove/file.remove_all.

Consequence: slice 3 is one new method (WriteFile) + the archive rewrite, with no announce-exclusion marker and no
step-24 gate -- WriteFile announces fine, and op.Convert already projects io.Reader sources (the json/yaml resource
constructors do this today). The earlier +devlore:internal vs. step-24-discriminator analysis is superseded; that
directive remains a sound general capability but is no longer a prerequisite here. See file-mutation-receipts decision 8
(and the rewritten decisions 2, 3, 5).

Touches the primary design/plan (file-mutation-receipts.md), the archive plan (archive-provider.md), the master plan
step-28 cell (phase-8.md), the step-28 step doc, and the archive architecture doc + status.

* feat(op,file): collapse file compensation into CompensateFileMutation via a constructor-stamped ReceiptSpec

Slice-2 of the file-mutation receipts mechanism -- the file seam.
See docs/plans/extract-starlark-from-op/phase-8/file-mutation-receipts.md.

- op.NewReceiptBaseWithCompensator(resource, compensatingAction): a receipt's compensator identity is fixed at
  construction, never via a setter. NewMethod's "compensable forward requires a Compensate<Name> companion"
  enforcement is relaxed (method.go) to match the earlier receiver_type relaxation -- a receipt names its own undo.
- file.Receipt is built through the fluent ReceiptSpec (NewReceiptSpec(resource, kind) +
  WithBoundary/WithRecovery/WithSource -> NewReceipt(spec)), mirroring op.NodeSpec / op.NewNode. kind and the
  compensator identity are set at construction; SetKind/SetRecoveryID/SetRecoveryDigest/SetSource are removed.
- The ten per-action file companions collapse into one CompensateFileMutation, dispatching on the receipt's Kind:
  file create/update/delete -> compensateWrite; a recorded source -> compensateMove; dir create -> compensateMakeDir;
  dir delete -> compensateRemoveDir. Every file.Receipt routes there via its constructor-stamped compensatingAction.
  CompensateWalkTree (a *RecoveryStack complement on a base receipt) stays.
- Compensator-name index: registration indexes every Compensate* method by its dotted name;
  invokeCompensateForReceipt / receiptTypeForAction resolve compensatingAction through it, falling back to the
  forward->.undo path for not-yet-migrated providers. The inventory index test is updated to the collapsed name.
- File tests migrated to CompensateFileMutation + ReceiptSpec.
- archive/provider.go: one-call-site adaptation to the new file API to keep the build green (it also carries the
  concurrent archive session's interim WIP). Slice 3 rewrites archive onto file.WriteFile + the existing Mkdir.

Verified in the worktree: make build clean (only the standing cmd/writ break); make test introduces no new failures --
only the standing reds remain (TestBackup_DefaultSuffix, TestWalkTree_Planned, TestShellCompletionPath, and the
cmd/writ / docgen / lore / internal/e2e / archive-test build breaks).

* docs(phase-8): renumber file-mutation-receipts slices to 1-5; refresh implementation status

Renumbers the slices into a clean linear sequence and brings the implementation status in line with the committed
reality (slice 2 has landed):

- Slice 3 = the seam tests (was the informal "2.5" test debt of slice 2), promoted to a numbered slice.
- Slice 4 = WriteFile + the archive rewrite (was slice 3; lives in the archive-provider plan).
- Slice 5 = the cross-provider seam migration + dropping the Commit fallback (was 2b), moved to last because the
  fallback can only go once every provider and consumer is off it.

Staleness fixes folded in:
- The archive adaptation (archive/provider.go's one call site) shipped *with* slice 2, not "as part of slice 3" -- the
  inventory/devlore-test/star cascade is already resolved. Only archive's broken provider_test.go is still pending
  (the archive-provider plan, slice 4).
- The archive rewrite depends on slices 1-2, not 1-2(b): it builds file receipts, constructor-stamped as of slice 2, so
  it does not wait on the cross-provider migration. The archive plan's S1/S2 gates and the architecture status updated
  to match.
- WriteFile is a public action with no step-24 gate and no exclusion marker (decisions 5, 8); the archive plan's
  step-24 "dependency" and S1 gating corrected.
- Status header redated 2026-06-29.

Touches file-mutation-receipts.md (slice headings + every cross-reference + the rewritten status block), the archive
plan (archive-provider.md), the master plan step-28 cell (phase-8.md), and the archive architecture status.

* test(op,file): slice 3 -- the file-mutation compensation seam tests

Prove the compensation seam end-to-end. Every existing file compensation test calls CompensateFileMutation directly,
and the inventory test only checks CompensatorByName resolves -- nothing exercised the real path,
Push(receipt) -> Unwind() resolving the constructor-stamped compensatingAction through the compensator-name index to
CompensateFileMutation.

- pkg/op/inventory/seam_test.go:
  - TestRecoveryStackUnwind_FileReceiptCreate_RemovesViaIndex -- create -> Unwind removes the file; also asserts
    ForwardAction is empty while CompensatingAction is file.compensate_file_mutation, so compensation follows the
    receipt's constructor-stamped compensator, not the dispatcher (archive's case).
  - TestRecoveryStackUnwind_FileReceiptUpdate_RestoresViaIndex -- overwrite -> Unwind restores the displaced prior
    content from recovery (the path archive relies on for files it overwrites).
- pkg/op/provider/file/seam_test.go:
  - TestCompensateFileMutation_UnknownKind_Errors -- the default dispatch arm.
  - TestCompensateFileMutation_DeleteDir_RecreatesDir -- the compensateRemoveDir arm (no forward producer after
    decision 8 dropped RemoveDir, so this is its only coverage).

Group B (cross-dispatcher save/load/unwind) is not a standalone test: rearm is unexported on op.RecoveryStack and op
cannot import file (cycle), so the round-trip can only run through the lifecycle harness. The file-receipt resume path
is already covered by plan's TestGraphResumeThenFail_RollsBack; the cross-dispatcher serialization rides slice 4's
archive round-trip.

Plan/design docs: slices numbered 1-5, slice 3 detailed to match these tests and marked implemented, status refreshed
(the archive adaptation shipped with slice 2). Touches file-mutation-receipts.md, archive-provider.md, phase-8.md, and
3.5.1-archive-provider.status.md.

Verified: make test -- pkg/op/inventory green; file's only failure is the standing TestBackup_DefaultSuffix; no new
failures; gofmt clean.

* docs(phase-8): plan slice 4 -- WriteFile + the archive.extract rewrite (#277)

Details slice 4: the one public mutation method and the archive rewrite that consumes it.

file-mutation-receipts.md (slice 4):
- Part 1 -- WriteFile is a thin exported wrapper over the slice-1 streaming write core (p.write already streams an
  io.Reader and builds the self-describing receipt via prepareWrite). Resource-typed, so no activation record;
  WriteText/WriteBytes rewrap onto it. Announced as file.write_file (public, decisions 5/8): Go callers (archive) use it
  directly, and the Starlark path becomes callable once stream.Resource supplies an io.Reader source -- until then a
  file.write_file Starlark call errors cleanly at op.Convert, it does not panic. Tests: create/update round-trips
  through CompensateFileMutation; WriteText/WriteBytes still pass via delegation.
- Part 2 -- archive.extract loops the existing Mkdir (dirs) + WriteFile (files). Because WriteFile archives any
  displaced content and stamps the MutationUpdateFile kind + recovery onto the receipt, #277 closes for free:
  overwrite-compensation now restores prior content (today a no-op -- archive records PriorArchiveID but never threads
  it onto the receipt). Extract returns one *op.RecoveryStack; a mid-extract failure returns the partial stack;
  CompensateExtract is stack.Unwind(). archive's writeExtractedFile/extractedEntry/per-format write logic are deleted.

archive-provider.md (S2): the #277 fix, the deletion of archive's own write/displacement logic, the
partial-stack-on-failure return, and the rewritten + extended provider_test.go (new / displaced / directory
round-trips + a mid-extract-failure unwind).

The design of record (3.5.1-archive-provider.md) already describes this compensation (displaced restore, partial-stack
unwind, resume), so it is unchanged -- the plan schedules the implementation to match it. Prerequisites: slices 1-2
(done). Part 2 lands in the archive plan's S1/S2, ahead of the concurrent session's content-detection slices (S3-S6).

* docs(phase-8): split slice 4 into WriteFile (4) + the archive rewrite (5); renumber to 1-6

The old slice 4 carried two parts; flatten them into top-level slices, no sub-parts:

- Slice 4 -- WriteFile, the exported streaming-write method (file-provider work; needs only slices 1-2).
- Slice 5 -- the archive.extract rewrite onto WriteFile + Mkdir (fixes #277); absorbed by the archive-provider plan;
  depends on slice 4.
- Slice 6 -- finish the cross-provider seam (was slice 5).

Updated every cross-reference: archive-provider.md absorbs slice 5 (WriteFile is the slice-4 prerequisite),
3.5.1-archive-provider.status.md's mutation-core row, and the in-doc decision/intro references.

* feat(file): WriteFile -- the exported streaming-write method (slice 4)

WriteFile(target *Resource, src io.Reader, mode os.FileMode) (*Resource, *Receipt, error) is a thin exported wrapper
over the slice-1 streaming write core: p.write already streams via io.Copy and builds the self-describing receipt
through prepareWrite, so WriteFile just exposes that core on a resource-typed signature. It takes an already-interned
target (env + producer ride the resource), so no activation record and no ownership change -- WriteText/WriteBytes keep
their chown via the shared write core (rewrapping onto WriteFile would drop it; WriteFile is the no-chown streaming
export archive needs). Announced as file.write_file (public, decisions 5/8): Go callers use it directly, and the
Starlark path becomes callable once stream.Resource supplies an io.Reader source.

- pkg/op/provider/file/seam_test.go: TestWriteFile_Create_RemovedOnCompensate and TestWriteFile_Update_RestoredOnCompensate
  -- forward write + CompensateFileMutation round-trips (create -> removed; overwrite -> prior restored).
- gen regenerated: file.write_file is in the announce map; its generated DryRun test passes (dry-run short-circuits
  before arg conversion, so the io.Reader param is never converted).
- file-mutation-receipts.md: slice 4 marked implemented; the "WriteText/WriteBytes rewrap onto WriteFile" claim
  corrected (it would drop chown).

Verified: make test -- the WriteFile tests pass; file's only failure is the standing TestBackup_DefaultSuffix; no new
failures; gofmt clean.

* feat(archive): rewrite extract onto file.WriteFile + Mkdir; fix #277 (slice 5)

archive.extract no longer hand-writes files and hand-builds receipts; it loops the file provider's unified mutation
surface -- a directory entry -> file.Mkdir, a regular file -> file.WriteFile (streamed via io.Copy). Each returned
receipt is committed (self-complement; forwardAction=archive.extract, compensatingAction=file.compensate_file_mutation)
and pushed onto one *op.RecoveryStack; CompensateExtract is stack.Unwind().

#277 closes: WriteFile archives displaced content and stamps the recovery onto the receipt, so compensating an
extraction that overwrote existing files restores them -- previously a no-op (archive recorded PriorArchiveID but never
threaded it onto the receipt). The per-format write logic (extractTarGz/extractZip/writeExtractedFile/extractedEntry) is
replaced by an openArchive entry iterator (extension-based selection; content detection is a later slice).

- archive/provider_test.go rewritten to the *op.RecoveryStack signature; adds
  TestCompensateExtract_RoundTrip_DisplacedFiles (the #277 proof) and asserts created subdirectories are pruned on
  compensation. Clears the standing archive [build failed].
- file-mutation-receipts.md: slices 4 and 5 marked implemented.

Verified: make test -- pkg/op/provider/archive ok (all archive tests pass); no new f…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant