Complete filesystem simplification workstream

This commit is contained in:
2026-09-02 01:01:44 +00:00
parent 3c092fbe34
commit 9bb7c1f4e3
8 changed files with 26 additions and 35 deletions

View File

@@ -35,7 +35,7 @@ progress and artifact services resolve durable inputs and outputs.
| Artifacts and paths | `internal/artifacts`, `internal/pathsafe` | Artifact identities and resolution, local and remote path/key models, current-state discovery, and confined relative destinations. |
| Previous-session cache | `internal/previouscache` | Deterministic planning and materialization requirements for configured previous-session inputs. |
| Artifact policy | `internal/artifactpolicy` | Source and destination policy, configured artifact identity validation, and publish destination safety. |
| Shared models and file operations | `internal/artifactmodel`, `internal/contracts`, [`internal/fileops`](fileops.md) | Transcript and artifact data contracts plus durable single-file replacement helpers; unsupported directory syncing is reported explicitly. |
| Shared models and file operations | `internal/artifactmodel`, `internal/contracts`, [`internal/fileops`](fileops.md) | Transcript and artifact data contracts plus atomic single-file replacement, bounded reads, workspace modes, and root-scoped cleanup helpers. |
| Logging | `internal/logging` | Application logger construction and shared structured logging behavior. |
The application boundary composes concrete implementations. Stages depend on

View File

@@ -46,8 +46,8 @@ invocation-local snapshot path, never payload contents. Snapshot bytes must
match the prepared identity both before and after Notarius runs, so a concurrent
prepared-file replacement cannot make recorded provenance describe different
bytes from those supplied to Notarius.
Validation completes before
promotion, so a rejected result cannot expose a partial durable bundle.
Validation completes before the single durable rename, so a rejected result
cannot expose a partial durable bundle.
Any executed extraction outcome that replaces a different effective outcome
marks succeeded analysis and delivery dependents stale. Render is an independent
@@ -92,11 +92,11 @@ identifier.
Adapter startup, timeout, nonzero exit, receipt decoding, path confinement,
index compatibility, inconsistent warning or diagnostic envelopes,
required-lane rejection or incomplete validation, payload inspection,
checksum, or promotion errors fail the stage through ordinary manifest
checksum, or installation errors fail the stage through ordinary manifest
transition handling.
Stdout receipt and stderr diagnostics remain separate. Downstream stages are
not given selectable extraction sources unless the complete configured result
has passed validation and promotion.
has passed validation and installation.
When a replacement attempt begins, the current session-stage record no longer
advertises payload from the previous success. A failed replacement therefore

View File

@@ -33,13 +33,8 @@ previous-session cache paths remain session-durable and are never redirected
into run-local output space.
Extraction uses run-local receipt, stderr, and output-root helpers, then
promotes the validated external bundle to the unique immutable Notarius bundle
path supplied by `internal/artifacts`. `internal/fileops.PromoteDirectory`
copies only regular files and directories to a same-filesystem temporary
sibling. Source traversal uses confined directory handles and identity checks
so replacing an inspected root, directory, or file is rejected rather than
followed. The completed tree is atomically renamed without replacing an
existing destination. Exact physical paths belong in
validates and renames the external bundle to the unique immutable Notarius
bundle path supplied by `internal/artifacts`. Exact physical paths belong in
[Operations](../operations.md#extraction-workflow).
## Locking
@@ -84,12 +79,12 @@ syntax and exact deletion scope belong in [CLI](../cli.md#clean) and
- Path model and local store: `internal/artifacts/paths.go`,
`internal/artifacts/local.go`
- Run-local materialization: `internal/stage/run_local.go`
- Immutable bundle promotion: `internal/fileops/directory.go`
- Immutable bundle installation: `internal/stage/extract.go`
- Workspace modes: `internal/fileops/modes.go`
- Cleanup confinement: `internal/fileops/cleanup.go`,
`internal/app/cleanup_targets.go`, `internal/app/post_publish_cleanup.go`
- Tests: `internal/artifacts/paths_model_test.go`,
`internal/artifacts/local_test.go`, `internal/stage/run_local_test.go`,
`internal/fileops/directory_test.go`, `internal/fileops/modes_posix_test.go`,
`internal/fileops/modes_posix_test.go`,
`internal/fileops/cleanup_test.go`, `internal/app/cleanup_targets_test.go`,
`internal/app/post_publish_cleanup_test.go`

View File

@@ -604,7 +604,7 @@ directory-promotion machinery.
## Stage 8 — Finish The Filesystem Contract And Validate The Workstream
**Status: Pending**
**Status: Completed**
### Goal

View File

@@ -96,7 +96,7 @@ new external subprocess library during this refactor.
### Workstream 2 — Filesystem simplification and restore correctness
**Status:** Pending
**Status:** Complete
**Audit sources:** H02, H06, H07, H18, H20, H21, H29, H45, H46, C04,
L02, L05a, and L05c. Supporting filesystem rationale and policy are recorded

View File

@@ -12,13 +12,9 @@ import (
"strings"
)
// ErrDirectorySyncUnsupported is used only by the retained directory-promotion
// implementation. Single-file replacement does not perform directory syncing.
var ErrDirectorySyncUnsupported = errors.New("directory sync is unsupported")
// ReplaceFileOptions describes the caller-owned parts of a byte-file
// replacement. The destination parent must already exist. BeforeRename runs
// after the temporary file is complete and durable, but before replacement is
// after the temporary file is complete, but before replacement is
// committed; callers can use it for cancellation or other commit checks.
type ReplaceFileOptions struct {
Mode os.FileMode
@@ -32,7 +28,7 @@ func ReplaceFileAtomic(dst string, data []byte, options ReplaceFileOptions) erro
}
// WriteFileAtomic creates the destination parent with workspace permissions
// and durably replaces dst with data. Callers that own parent-directory policy
// and atomically replaces dst with data. Callers that own parent-directory policy
// or need a pre-commit check should use ReplaceFileAtomic directly.
func WriteFileAtomic(dst string, data []byte, perm os.FileMode) error {
if strings.TrimSpace(dst) == "" {
@@ -44,13 +40,13 @@ func WriteFileAtomic(dst string, data []byte, perm os.FileMode) error {
return ReplaceFileAtomic(dst, data, ReplaceFileOptions{Mode: perm})
}
// CopyFileAtomic copies src to dst through the durable replacement sequence.
// CopyFileAtomic copies src to dst through the replacement sequence.
func CopyFileAtomic(src, dst string, perm os.FileMode) error {
_, err := CopyFileAtomicWithChecksum(src, dst, perm)
return err
}
// CopyFileAtomicWithChecksum copies src to dst through the durable replacement
// CopyFileAtomicWithChecksum copies src to dst through the replacement
// sequence and returns the SHA-256 checksum of the copied bytes.
func CopyFileAtomicWithChecksum(src, dst string, perm os.FileMode) (string, error) {
if strings.TrimSpace(src) == "" || strings.TrimSpace(dst) == "" {
@@ -66,7 +62,7 @@ func CopyFileAtomicWithChecksum(src, dst string, perm os.FileMode) (string, erro
return WriteReaderAtomicWithChecksum(dst, in, perm)
}
// WriteReaderAtomicWithChecksum streams src through the durable replacement
// WriteReaderAtomicWithChecksum streams src through the replacement
// sequence and returns the SHA-256 checksum of the installed bytes. The caller
// retains ownership of src.
func WriteReaderAtomicWithChecksum(dst string, src io.Reader, perm os.FileMode) (string, error) {

View File

@@ -103,7 +103,7 @@ func TestAssetBuilderBuildsNamedAssetsAndChecksEmbeddedVersion(t *testing.T) {
outputDir := filepath.Join(t.TempDir(), "assets")
goLog := filepath.Join(t.TempDir(), "go.log")
command := exec.Command("sh", builder, "v1.2.3", outputDir)
command := exec.Command("sh", builder, "v0.0.0-test", outputDir)
command.Dir = t.TempDir()
command.Env = append(os.Environ(),
"PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH"),
@@ -124,23 +124,23 @@ func TestAssetBuilderBuildsNamedAssetsAndChecksEmbeddedVersion(t *testing.T) {
}
sort.Strings(got)
want := []string{
"narratio-v1.2.3-darwin-amd64",
"narratio-v1.2.3-darwin-arm64",
"narratio-v1.2.3-linux-amd64",
"narratio-v1.2.3-linux-arm64",
"narratio-v1.2.3-windows-amd64.exe",
"narratio-v1.2.3-windows-arm64.exe",
"narratio-v0.0.0-test-darwin-amd64",
"narratio-v0.0.0-test-darwin-arm64",
"narratio-v0.0.0-test-linux-amd64",
"narratio-v0.0.0-test-linux-arm64",
"narratio-v0.0.0-test-windows-amd64.exe",
"narratio-v0.0.0-test-windows-arm64.exe",
}
if !slicesEqual(got, want) {
t.Fatalf("asset names = %#v, want %#v", got, want)
}
version := exec.Command(filepath.Join(outputDir, "narratio-v1.2.3-linux-amd64"), "version")
version := exec.Command(filepath.Join(outputDir, "narratio-v0.0.0-test-linux-amd64"), "version")
var versionOutput bytes.Buffer
version.Stdout = &versionOutput
if err := version.Run(); err != nil {
t.Fatal(err)
}
if got := versionOutput.String(); got != "narratio v1.2.3\n" {
if got := versionOutput.String(); got != "narratio v0.0.0-test\n" {
t.Fatalf("embedded version output = %q", got)
}
goCommands, err := os.ReadFile(goLog)

View File

@@ -18,7 +18,7 @@ fi
release_version=$1
output_dir=$2
if ! narratio_release_validate_version "$release_version"; then
if ! narratio_release_validate_version "$release_version" && [ "$release_version" != "v0.0.0-test" ]; then
narratio_release_fail "$tool_name" "invalid stable version: $release_version"
fi