Plan Go and process refactoring

This commit is contained in:
2026-08-31 23:40:42 +00:00
parent af5fbc1c4f
commit da74d7ad18
7 changed files with 2604 additions and 112 deletions

View File

@@ -33,23 +33,25 @@ Verdicts use these terms:
Narratio does not have a broad maintainability problem. Its package direction,
explicit contracts, documentation, tests, and small dependency surface are all
healthy. Most size is a consequence of real orchestration, durability, recovery,
and integration requirements.
healthy. Most size is a consequence of real orchestration, recovery, and
integration requirements. The exception is filesystem code, where mechanisms
originally chosen for adversarial races and power-loss durability have been
treated as durable product requirements without sufficient operational need.
The audit did identify one correctness remediation and several proportional
maintainability improvements:
1. Restore planning must inspect existing local targets without following
symlinks. `classifyRestoreAction` currently uses `os.Stat` and path-based
hashing, so a symlink to a regular file can be classified as `skip_same`
despite the documented restore and path-confinement contract.
2. `internal/fileops/directory.go` contains a second directory-promotion and
tree-copy implementation used only by tests. Delete that alternate
implementation and test the production `os.Root` path. Then remove
identity-race hooks and rechecks that exist solely for an adversarial
concurrent-filesystem threat model, while retaining static symlink checks,
root confinement, regular-file restrictions, atomic no-replace promotion,
durability, workspace modes, and cleanup guardrails.
1. Simplify `internal/fileops` around Narratio's actual trusted-workspace model.
Retain root-scoped destructive and restore operations, atomic visibility for
corruption-sensitive file replacement, bounded I/O, group-owned workspace
modes, and stricter handling of API-key material. Drop power-loss durability,
hostile-race defenses, universal symlink and regular-file restrictions, and
atomic no-replace directory promotion as application contracts.
2. Restore planning must still classify local targets safely within the session
root. `classifyRestoreAction` currently uses `os.Stat` and path-based hashing,
so a symlink to a regular file can be classified as `skip_same`; correct this
within the simpler root-scoped restore design rather than adding more
identity-checking machinery to the current abstraction.
3. Decompose `executePlan` around its existing lifecycle boundaries, and share
the pre-execution stage-decision model used by planning and execution. Do not
introduce a generic workflow engine or change transition ordering during the
@@ -68,8 +70,10 @@ maintainability improvements:
creating a general dependency-injection framework.
The audit does **not** recommend package-wide reorganization, a CLI framework,
an external flock library, generic configuration or atomic-file frameworks,
generic subprocess machinery, or new identifier/retry/storage abstractions.
an external flock library, generic configuration frameworks, generic subprocess
machinery, or new identifier/retry/storage abstractions. Atomic replacement is
a generic operation under the reduced contract, but a small local helper is
preferable to a dependency that does not support Narratio's full platform set.
## Findings In Source Order
@@ -87,16 +91,19 @@ than command wrapping, and the observed size maps to documented responsibilities
**Verdict:** Confirmed as a refactoring constraint.
**Recommended change:** Preserve accidental path-escape and destructive-action
guardrails, but remove tests and identity rechecks whose only purpose is to
defeat a concurrent adversary replacing directory entries after inspection.
Update internal file-operation documentation when that distinction changes.
**Recommended change:** Preserve root-scoped destructive and restore operations
and the checks needed to avoid accidental path escape. Remove tests and
identity rechecks whose only purpose is to defeat a concurrent adversary, and
stop applying universal no-follow and regular-file rules to ordinary reads,
writes, and copies. Update the owning architecture, operations, and internal
documents when the policy changes.
**Reasoning:** Narratio still needs safe scope, static symlink rejection,
regular-file checks, atomic updates, and crash durability. It does not need to
prove that every inspected inode remained at the same name in the presence of
an attacker racing each operation. `os.Root` already prevents rooted operations
from escaping their root.
**Reasoning:** Narratio operates on trusted D&D campaign workspaces under a
session lock; their contents are intended to be shared and are not sensitive.
The application still needs safe destructive scope and special protection for
API keys, but it does not need to prove inode identity against hostile races or
reject ordinary workspace symlinks by default. `os.Root` remains useful at the
few boundaries where containment is itself the requirement.
### H03 — Production-code concentration
@@ -138,36 +145,42 @@ The high interaction count reflects the application boundary's intended role.
### H06 — Confined-filesystem and directory-promotion complexity
**Verdict:** Confirmed, with a concrete duplication issue.
**Verdict:** Confirmed more broadly than the preliminary hypothesis.
**Recommended change:** Delete the test-only `promoteDirectory`,
`promoteDirectoryWithHooks`, `copyRegularTree`, `copyRegularDirectory`, and
`copyRegularFile` implementation family from production code. Exercise
`PromoteDirectory` and its actual handle-relative install path in tests. Remove
adversarial replacement hooks and redundant `SameFile` rechecks where `os.Root`
already supplies the required confinement, while retaining the application
semantics listed in H21.
**Recommended change:** Replace the general confined-filesystem and directory-
promotion subsystem with ordinary standard-library operations plus a few
focused helpers for the retained policies in H21. Delete both directory-copy
algorithms, their race hooks and identity rechecks, and the platform-specific
no-replace and directory-sync machinery. Let Notarius write a validated staging
bundle in the session workspace and rename that directory directly to its
unique durable destination.
**Reasoning:** The first half of `directory.go` is the real `os.Root`-based
implementation; the second half reimplements the same tree copy for injectable
tests. Tests of the alternate algorithm can pass while production changes, and
the duplication accounts for a substantial portion of the file's size and
complexity.
**Reasoning:** The duplicate implementation is real, but removing only the
test-only half would preserve a much larger abstraction whose guarantees are
not operational requirements. The session lock, trusted workspace, same-
workspace staging location, and unique run destination remove the practical
need for adversarial no-replace promotion. A direct rename also avoids the
second full-tree copy that the current implementation performs.
### H07 — Test code larger than production code
**Verdict:** Focused opportunity, not evidence of overtesting.
**Recommended change:** Preserve behavior and invariant coverage. Remove tests
of the alternate directory implementation, consolidate repeated fixture setup
within individual packages, and replace app-level mutable global function seams
with invocation-scoped dependencies as related code is touched.
**Recommended change:** Preserve behavior and retained-invariant coverage.
Delete tests for abandoned power-loss, hostile-race, universal no-follow, and
directory-promotion guarantees along with the implementation they constrain.
Add focused tests for the smaller atomic-file helper, bounded I/O, modes,
root-scoped cleanup and restore, validated Notarius rename, and API-key handling.
Also consolidate repeated fixture setup within individual packages and replace
app-level mutable global function seams with invocation-scoped dependencies as
related code is touched.
**Reasoning:** The suite makes extensive legitimate use of real temporary
filesystems, fake object stores, and subprocess helpers. The clearest hygiene
signals are a few very large test files, 11 mutable production function seams
that are reset repeatedly by tests, and duplicated filesystem test machinery;
the overall test-to-production ratio is not itself a defect.
that are reset repeatedly by tests, and a filesystem suite that treats
unneeded implementation guarantees as policy; the overall test-to-production
ratio is not itself a defect.
### H08 — Repository-wide tests and vet
@@ -281,28 +294,31 @@ not the stage/adapter boundary.
**Verdict:** Mostly healthy, with focused opportunities.
**Recommended change:** Retain behavior-level filesystem, manifest, subprocess,
and recovery tests. Remove tests tied to the alternate fileops algorithm; use a
small dependency struct instead of mutable app globals; use structural diffs for
large values where useful.
**Recommended change:** Retain behavior-level manifest, subprocess, recovery,
and filesystem tests for the reduced contract. Delete tests tied to abandoned
fileops mechanisms and guarantees; use a small dependency struct instead of
mutable app globals; use structural diffs for large values where useful.
**Reasoning:** Most tests assert outputs, persisted state, CLI behavior, or
external command contracts. The main incidental coupling comes from private
hook algorithms and package-global function substitution, not from the suite as
a whole.
hook algorithms, tests that elevate defensive implementation choices into
contracts, and package-global function substitution, not from the suite as a
whole.
### H18 — Invariant discoverability across packages
**Verdict:** Already satisfied, except for one implementation mismatch.
**Verdict:** Confirmed for filesystem policy; otherwise already satisfied.
**Recommended change:** Fix restore target inspection as described in H46.
Otherwise retain the current policy-to-internal-document structure and keep
transaction-order comments adjacent to runner and publish code.
**Recommended change:** Revise the architecture, operations, and internal
filesystem documents so they distinguish required outcomes from the current
implementation's power-loss, no-follow, identity-race, and no-replace
mechanisms. Fix restore target inspection as described in H46. Keep transaction-
order comments adjacent to runner and publish code.
**Reasoning:** Manifest durability, restore recovery, publish commit ordering,
workspace modes, lock ownership, and confined file operations are all described
in owner documents and protected by focused tests. Restore planning's use of
`os.Stat` is inconsistent with those otherwise clear rules.
**Reasoning:** The invariants are discoverable, but the filesystem documents
currently make several defensive mechanisms appear to be permanent product
contracts. Restore planning's use of `os.Stat` is also inconsistent with the
root-scoped restore rule that remains justified.
### H19 — Upgrade Go 1.25 to Go 1.27
@@ -310,8 +326,9 @@ in owner documents and protected by focused tests. Restore planning's use of
**Recommended change:** In one dedicated change, update `go.mod`, all Woodpecker
images including the pinned release image, release documentation, and generated
module state. Run the full test, race, vet, documentation, example, and six-target
release-build checks required by the release procedure.
module state. Use the Go 1.27 `os.Root.RemoveAll` operation where root-scoped
recursive removal remains required. Run the full test, race, vet, documentation,
example, and six-target release-build checks required by the release procedure.
**Reasoning:** [Go 1.27.0](https://go.dev/doc/go1.27) is now released. The full
suite and vet pass under it, and all supported release targets cross-compile
@@ -320,28 +337,40 @@ separate from refactors.
### H20 — Replace custom confinement with `os.Root`
**Verdict:** Already substantially satisfied.
**Verdict:** Focused opportunity, not a package-wide filesystem model.
**Recommended change:** Standardize remaining fileops paths on the existing
`os.Root` primitives and delete duplicate compatibility/test paths. Do not
rewrite already-rooted code merely to rename helpers.
**Recommended change:** Use `os.Root` only where a root-relative boundary is an
actual requirement: destructive cleanup, restore, and any other explicitly
scoped operation. Prefer direct `os` and `io` operations for ordinary trusted-
workspace reads, writes, copies, and renames. Do not preserve a universal
confined-filesystem facade merely because its current implementation uses
`os.Root`.
**Reasoning:** Current confined writes, reads, cleanup, download installation,
and production directory promotion already use `os.Root`. The remaining value
is simplification and consolidation, not an initial migration.
**Reasoning:** `os.Root` is an idiomatic way to express scope, particularly with
`RemoveAll` available in Go 1.27. Applying it to every filesystem operation,
however, preserves no-follow and handle-relative complexity that Narratio's
trusted workspace model does not require.
### H21 — Retain Narratio-owned durability and policy code
**Verdict:** Retain.
**Verdict:** Not substantiated as stated; retain only focused product policies.
**Recommended change:** Keep atomic replace and no-replace promotion ordering,
file and directory syncing, unsupported-platform errors, workspace modes,
bounded reads, regular-file restrictions, proper-descendant cleanup checks, and
application-owned destructive-operation gates.
**Recommended change:** Keep atomic visibility for corruption-sensitive file
replacement, centralized bounded-I/O limits and clear adapter errors, group-
owned workspace modes, validated downloads before installation, root-scoped
destructive and restore operations, and stricter treatment of API-key material.
Drop file and directory syncing, power-loss durability claims, platform-
unsupported directory-sync errors, atomic no-replace directory promotion,
universal regular-file/no-follow restrictions, and hostile-race identity checks.
**Reasoning:** These are observable Narratio contracts that `os.Root` does not
provide. Generic filesystem packages would either weaken them or require a
similar application-specific layer around the dependency.
**Reasoning:** Most Narratio data is intentionally shareable D&D material, and
the application runs under a session lock in a trusted workspace without heavy
write concurrency. Atomic replacement still prevents readers from observing a
partially written manifest or configuration, and bounded I/O still prevents a
runaway adapter from exhausting disk or memory. The stronger guarantees do not
address a realistic product risk proportionate to their implementation and test
cost. In this context, “durable” should mean managed persistent state, not a
promise of survival across every acknowledged power-loss window.
### H22 — Use `os.Process.WithHandle` on Windows
@@ -432,14 +461,20 @@ still requiring the existing semantic checks.
### H29 — Generic atomic-file package
**Verdict:** Not substantiated.
**Verdict:** Focused opportunity, but no new dependency is presently justified.
**Recommended change:** Retain and simplify `internal/fileops` rather than
replacing it.
**Recommended change:** Treat same-directory temporary-file replacement as a
generic operation and expose only a small local helper for it. Localize any
unavoidable platform-specific replace mechanic behind that helper. Use direct
standard-library operations for everything else instead of retaining a broad
`fileops` facade.
**Reasoning:** Narratio requires explicit modes, file and parent syncing,
pre-commit cancellation, confined parents, downloaded-temp installation, and
clear unsupported guarantees. Generic packages do not own that full contract.
**Reasoning:** Once file and directory syncing, hostile-race defenses, and
universal confinement are removed from the contract, atomic file replacement
is no longer Narratio-specific. A thin local implementation removes more
complexity than a general framework while keeping the supported-platform and
group-mode behavior explicit. Dependency research may revisit that choice if a
maintained package demonstrably removes more code than it adds.
### H30 — Provider-neutral object-storage abstraction
@@ -637,33 +672,42 @@ navigation without distributing transaction ownership.
### H45 — `internal/fileops/directory.go`
**Verdict:** Confirmed; see H06 and H20.
**Verdict:** Confirmed for removal; see H06 and H21.
**Recommended change:** Remove the alternate path-based implementation and its
test hooks, keep one `os.Root` implementation, and simplify adversarial
identity-race checks according to H02. Retain platform-specific handle-relative
no-replace rename and durability checks.
**Recommended change:** Delete both directory-promotion implementations, their
test hooks, copy helpers, identity checks, directory syncing, and platform-
specific no-replace rename code. Have extract validate the Notarius staging
bundle and use a direct same-workspace `os.Rename` to its absent, unique run
destination. Treat a destination collision as an ordinary error and clean up
only verified staging paths.
**Reasoning:** This is the clearest actual code duplication found. It also
causes tests to exercise a different install implementation from production.
**Reasoning:** The duplicate implementation is the clearest literal duplication
found, but the production implementation is also unnecessary under the revised
contract. Notarius already writes beneath the invocation output root, and the
destination is protected by the session lock and unique run ID. Direct rename
removes an entire tree copy and the need to preserve a second pre-promotion copy;
the receipt and diagnostic log remain the appropriate troubleshooting record.
### H46 — `classifyRestoreAction`
**Verdict:** Confirmed for correctness, not merely complexity.
**Recommended change:** Pass the session root into classification and inspect an
existing target with `os.Lstat` plus the shared confined regular-file opener.
Hash the already-opened handle rather than reopening a mutable absolute path.
Classify a symlink at the leaf or in an ancestor as
`RestoreConflictNonRegular`, even when its target bytes match. Add tests for
leaf and ancestor symlinks in force and non-force plans. Optionally extract the
legacy remote-download digest comparison after this behavior is fixed.
**Recommended change:** Fold classification into the simpler root-scoped
restore implementation. Resolve and inspect targets relative to an `os.Root`,
without layering inode-identity rechecks onto the operation. Classify a symlink
at the leaf or in an ancestor as `RestoreConflictNonRegular`, even when its
target bytes match. Hash a verified regular target without reopening an
unconfined absolute path. Add leaf- and ancestor-symlink tests for force and
non-force plans. Optionally extract the legacy remote-download digest comparison
after this behavior is fixed.
**Reasoning:** `os.Stat(localPath)` follows a leaf symlink, and
`artifacts.SHA256File(localPath)` follows it again. A matching target can
therefore produce `skip_same` and leave an unmanaged symlink in place, contrary
to the restore and confined-filesystem contracts. The rest of the function is
an explicit and readable state matrix whose branching is justified.
to the retained restore-scope contract. This is an accidental-scope issue, not
a reason to retain hostile-race defenses throughout ordinary file operations.
The rest of the function is an explicit and readable state matrix whose
branching is justified.
### H47 — `internal/manifest/analyze_state.go`
@@ -679,29 +723,31 @@ is easier to audit in one owner.
### H48 — Preliminary priority order
**Verdict:** Confirmed with one correction.
**Verdict:** Confirmed with a broader filesystem correction.
**Recommended change:** Address restore symlink classification first because it
is a correctness issue. Then remove duplicate fileops code and simplify the
threat-model-only machinery. Next decompose execution and share its narrow
planning decision model. Follow with analyze/config, publish/previous-cache, and
finally modernization/test ergonomics.
**Recommended change:** Define the reduced filesystem policy and replace the
current fileops subsystem, incorporating the restore classification correction
into the root-scoped restore design. Next decompose execution and share its
narrow planning decision model. Follow with analyze/config, publish/previous-
cache, and finally modernization/test ergonomics.
**Reasoning:** Execution remains the largest change-blast hotspot, and
configuration/analyze remain the next tier. The deeper review elevated the
restore mismatch and fileops duplication because they are concrete rather than
metric-only findings.
**Reasoning:** The restore mismatch remains a correctness issue, but preserving
the surrounding abstraction solely to fix it would reinforce the mistaken
contract. Execution remains the largest non-filesystem change-blast hotspot,
and configuration/analyze remain the next tier.
## Recommended Implementation Sequence
This is ordering guidance for a future decision-complete implementation plan,
not staged implementation instructions:
1. Correct restore target confinement and add regression tests.
1. Define the reduced filesystem policy in its canonical documents.
2. Upgrade the Go baseline to 1.27 in isolation and run the full release
candidate checks.
3. Consolidate fileops on the production `os.Root` path and revise its tests and
internal documentation to the trusted-environment threat model.
3. Replace the broad fileops facade with direct standard-library operations and
focused helpers, simplify extract to validated directory rename, incorporate
the root-scoped restore correction, and replace obsolete mechanism tests with
tests of the retained outcomes.
4. Adopt `os.Process.WithHandle` and simplify transcription with `errgroup` as
separate, bounded modernization changes.
5. Decompose `executePlan`, then introduce the shared narrow pre-execution
@@ -712,6 +758,7 @@ not staged implementation instructions:
9. Improve app test dependency injection and apply selective `slices`, `maps`,
and `go-cmp` cleanup only in code touched by the preceding work.
No open product or architecture decision is required before turning these
findings into an implementation plan. The implementation plan should preserve
the scope boundaries and rejected alternatives recorded here.
The filesystem decisions are specified in [`fileops.md`](fileops.md). No open
product or architecture decision is required before turning these findings into
an implementation plan. The implementation plan should preserve the scope
boundaries and rejected alternatives recorded here.