From c0bacf838b024e9e8e3ea8ff963b4fce1ff3eb57 Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Mon, 10 Aug 2026 07:44:49 -0400 Subject: [PATCH] refactor: clarify evaluation and reconciliation names --- internal/application/apply/decisions.go | 4 +- internal/application/apply/evaluate_test.go | 2 +- internal/application/apply/prepare.go | 8 +- internal/application/evaluation/semantics.go | 34 +++---- internal/application/initialize/service.go | 12 +-- internal/application/inspect/service_test.go | 4 +- internal/application/inspect/status.go | 4 +- internal/reconcile/classify_alias.go | 24 ++--- internal/reconcile/classify_alias_test.go | 64 ++++++------- internal/reconcile/classify_file.go | 22 ++--- internal/reconcile/classify_file_test.go | 56 +++++------ internal/reconcile/classify_retirement.go | 10 +- .../reconcile/classify_retirement_test.go | 26 ++--- internal/reconcile/decisions.go | 4 +- internal/reconcile/decisions_test.go | 26 ++--- internal/reconcile/snapshot.go | 16 ++-- internal/reconcile/types.go | 86 +++++++++-------- internal/reconcile/types_test.go | 4 +- internal/repository/compiler.go | 34 +++---- internal/repository/scan.go | 96 +++++++++---------- 20 files changed, 269 insertions(+), 267 deletions(-) diff --git a/internal/application/apply/decisions.go b/internal/application/apply/decisions.go index f0baef1..e2c3c18 100644 --- a/internal/application/apply/decisions.go +++ b/internal/application/apply/decisions.go @@ -94,14 +94,14 @@ func decisionSpecs(candidates Candidates) ([]reconcile.DecisionSpec, error) { // specFor freezes the decision spec of one candidate, or returns nil when // the candidate does not require a decision. func specFor(candidate Candidate) (*reconcile.DecisionSpec, error) { - if candidate.file.Convergence == reconcile.DecisionRequired { + if candidate.file.Convergence == reconcile.ConvergenceDecisionRequired { spec, err := reconcile.DecisionSpecForFile(candidate.file, candidate.record.File.Kind) if err != nil { return nil, failure.New(failure.InvalidInput, "apply: freeze file decision", err) } return &spec, nil } - if candidate.alias.Convergence == reconcile.DecisionRequired { + if candidate.alias.Convergence == reconcile.ConvergenceDecisionRequired { spec, err := reconcile.DecisionSpecForAlias(candidate.alias) if err != nil { return nil, failure.New(failure.InvalidInput, "apply: freeze alias decision", err) diff --git a/internal/application/apply/evaluate_test.go b/internal/application/apply/evaluate_test.go index b6c5f58..f2d1eaa 100644 --- a/internal/application/apply/evaluate_test.go +++ b/internal/application/apply/evaluate_test.go @@ -226,7 +226,7 @@ func testApplyEvaluationStateOnly(t *testing.T) { t.Fatalf("evaluate: %v", err) } requirePaths(t, candidates, "gone") - if candidates.All()[0].retirement.Action != reconcile.ActionRetireState { + if candidates.All()[0].retirement.Action != reconcile.ActionRetireFileState { t.Fatalf("retirement action = %v, want retire state", candidates.All()[0].retirement.Action) } } diff --git a/internal/application/apply/prepare.go b/internal/application/apply/prepare.go index b310cb4..2c96da8 100644 --- a/internal/application/apply/prepare.go +++ b/internal/application/apply/prepare.go @@ -143,7 +143,7 @@ func needsDecision(candidate Candidate) bool { } func candidateNeedsDecision(candidate Candidate) bool { - return candidate.file.Convergence == reconcile.DecisionRequired || candidate.alias.Convergence == reconcile.DecisionRequired + return candidate.file.Convergence == reconcile.ConvergenceDecisionRequired || candidate.alias.Convergence == reconcile.ConvergenceDecisionRequired } // plannedRecord builds the planned per-target record of one candidate. @@ -183,8 +183,8 @@ func classificationAction(candidate Candidate) (reconcile.Action, string, bool) return candidate.file.Action, candidate.record.File.SourceRepositoryPath, true case candidate.alias.Action == reconcile.ActionCreateAlias || candidate.alias.Action == reconcile.ActionReplaceAlias || candidate.alias.Action == reconcile.ActionVerifyAlias: return candidate.alias.Action, "", true - case candidate.retirement.Action == reconcile.ActionRetireState: - return reconcile.ActionRetireState, "", true + case candidate.retirement.Action == reconcile.ActionRetireFileState: + return reconcile.ActionRetireFileState, "", true case candidate.retirement.Action == reconcile.ActionRetireAliasState: return reconcile.ActionRetireAliasState, "", true } @@ -200,7 +200,7 @@ func mapKind(action reconcile.Action) (ActionKind, bool) { return ActionKindReplaceFile, true case reconcile.ActionCreateAlias, reconcile.ActionReplaceAlias, reconcile.ActionVerifyAlias: return ActionKindRealizeAlias, true - case reconcile.ActionRetireState: + case reconcile.ActionRetireFileState: return ActionKindRetireFile, true case reconcile.ActionRetireAliasState: return ActionKindRetireAlias, true diff --git a/internal/application/evaluation/semantics.go b/internal/application/evaluation/semantics.go index 86ec205..fae73f0 100644 --- a/internal/application/evaluation/semantics.go +++ b/internal/application/evaluation/semantics.go @@ -14,7 +14,7 @@ func (service *Service) classify(ctx context.Context, assembly reconcile.Evaluat records := assembly.All() evaluated := make([]Record, 0, len(records)) for _, record := range records { - fingerprints, err := semantics.fingerprints(fingerprintInput{ + semanticFingerprints, err := semantics.semanticFingerprints(fingerprintInput{ context: ctx, home: assembly.HomePath, record: record, includeUnmanagedTargetDigest: service.includeUnmanagedTargetDigest, }) @@ -23,10 +23,10 @@ func (service *Service) classify(ctx context.Context, assembly reconcile.Evaluat } evaluated = append(evaluated, Record{ Evaluation: record, - File: reconcile.ClassifyFile(record, fingerprints), - Alias: reconcile.ClassifyAlias(record, fingerprints), + File: reconcile.ClassifyFile(record, semanticFingerprints), + Alias: reconcile.ClassifyAlias(record, semanticFingerprints), Retirement: reconcile.ClassifyRetirement(record, assembly.Platform), - Semantics: fingerprints, + Semantics: semanticFingerprints, }) } return evaluated, nil @@ -48,7 +48,7 @@ type semanticState struct { haveKey bool } -func (state *semanticState) fingerprints(input fingerprintInput) (reconcile.FileSemantics, error) { +func (semanticState *semanticState) semanticFingerprints(input fingerprintInput) (reconcile.FileSemantics, error) { ctx, home, record := input.context, input.home, input.record if record.Entry != reconcile.PlanEntryFile { if input.includeUnmanagedTargetDigest && record.Target.Kind() == reconcile.KindFile { @@ -62,29 +62,29 @@ func (state *semanticState) fingerprints(input fingerprintInput) (reconcile.File Target: record.Target.Digest(), }, nil } - return state.secretFingerprints(ctx, home, record) + return semanticState.secretSemanticFingerprints(ctx, home, record) } -func (state *semanticState) secretFingerprints(ctx context.Context, home string, record reconcile.Evaluation) (reconcile.FileSemantics, error) { +func (semanticState *semanticState) secretSemanticFingerprints(ctx context.Context, home string, record reconcile.Evaluation) (reconcile.FileSemantics, error) { semantics := reconcile.FileSemantics{} targetFile := record.Target.Kind() == reconcile.KindFile if !targetFile && !SecretDecryptionNeeded(record) { return semantics, nil } - if err := state.recover(); err != nil { + if err := semanticState.loadHashKey(); err != nil { return semantics, err } if targetFile { - content, err := ReadTargetContent(home, record, state.commandLabel) + content, err := ReadTargetContent(home, record, semanticState.commandLabel) if err != nil { return semantics, err } - semantics.Target = deployment.SecretSemantic(content, state.key) + semantics.Target = deployment.SecretSemantic(content, semanticState.key) } if SecretDecryptionNeeded(record) { - source, err := record.Source.KeyedSemantic(ctx, state.key) + source, err := record.Source.KeyedSemantic(ctx, semanticState.key) if err != nil { - return semantics, categorized(err, state.commandLabel+": decrypt source "+record.File.SourceRepositoryPath) + return semantics, categorized(err, semanticState.commandLabel+": decrypt source "+record.File.SourceRepositoryPath) } semantics.Source = source } @@ -107,14 +107,14 @@ func SecretDecryptionNeeded(record reconcile.Evaluation) bool { return record.Source.Snapshot().Storage() != record.FileState.BaselineSource() } -func (state *semanticState) recover() error { - if state.haveKey { +func (semanticState *semanticState) loadHashKey() error { + if semanticState.haveKey { return nil } - key, err := state.reader.RecoverHashKey() + key, err := semanticState.reader.RecoverHashKey() if err != nil { - return failure.New(failure.Operational, state.commandLabel+": recover hash key", err) + return failure.New(failure.Operational, semanticState.commandLabel+": load hash key", err) } - state.key, state.haveKey = key, true + semanticState.key, semanticState.haveKey = key, true return nil } diff --git a/internal/application/initialize/service.go b/internal/application/initialize/service.go index 46e646f..2452843 100644 --- a/internal/application/initialize/service.go +++ b/internal/application/initialize/service.go @@ -52,7 +52,7 @@ func (service *Service) Initialize(ctx context.Context, request Request) (Result home: environment.home, stateDirectory: environment.stateDirectory, } - repository, err := resolveRoot(anchors) + repository, err := ensureRoot(anchors) if err != nil { return Result{}, err } @@ -96,10 +96,10 @@ type roots struct { stateDirectory string } -// resolveRoot returns the canonical repository root, creating it first when +// ensureRoot returns the canonical repository root, creating it first when // missing, after validating the Section 6.1 overlaps. -func resolveRoot(anchors roots) (string, error) { - existing, err := directoryKind(anchors.repository) +func ensureRoot(anchors roots) (string, error) { + existing, err := existingDirectory(anchors.repository) if err != nil { return "", err } @@ -194,10 +194,10 @@ func resolvePath(requested string) (string, error) { return working, nil } -// directoryKind reports whether path names an existing real directory. An +// existingDirectory reports whether path names an existing real directory. An // existing non-directory is rejected as InvalidInput; a missing path returns // false so the caller can create it. -func directoryKind(path string) (bool, error) { +func existingDirectory(path string) (bool, error) { info, err := os.Stat(path) if err == nil { if !info.IsDir() { diff --git a/internal/application/inspect/service_test.go b/internal/application/inspect/service_test.go index 0742bab..dde4968 100644 --- a/internal/application/inspect/service_test.go +++ b/internal/application/inspect/service_test.go @@ -231,7 +231,7 @@ func testEvaluationStateOnlySelection(t *testing.T) { func assertRetirement(t *testing.T, evaluation Result) { t.Helper() retirement := evaluation.records[0].retirement - if retirement.Action != reconcile.ActionRetireState || retirement.Reason != reconcile.ReasonSourceRemoved { + if retirement.Action != reconcile.ActionRetireFileState || retirement.Reason != reconcile.ReasonSourceRemoved { t.Fatalf("retirement = %+v, want pending retire-state", retirement) } } @@ -336,7 +336,7 @@ func testDecryptClassifies(t *testing.T) { }} evaluation := mustEvaluate(t, fx.service, Request{}) classification := evaluation.records[2].file - if classification.Action != reconcile.ActionNoOp || classification.Convergence != reconcile.Converged { + if classification.Action != reconcile.ActionNoOp || classification.Convergence != reconcile.ConvergenceConverged { t.Fatalf("classification = %+v, want converged no-op", classification) } } diff --git a/internal/application/inspect/status.go b/internal/application/inspect/status.go index 53a9dd8..42028e4 100644 --- a/internal/application/inspect/status.go +++ b/internal/application/inspect/status.go @@ -172,7 +172,7 @@ type classificationInput struct { // statusRecord projects one classification onto its immutable status row. func statusRecord(input classificationInput) StatusRecord { return StatusRecord{targetPath: input.path, kind: input.kind, action: actionName(input.action), - reason: reasonName(input.reason), converged: input.convergence == reconcile.Converged} + reason: reasonName(input.reason), converged: input.convergence == reconcile.ConvergenceConverged} } // statusCounts tallies the per-kind records of one status result. @@ -219,7 +219,7 @@ func actionName(action reconcile.Action) string { reconcile.ActionWriteSourceToTarget: "write-source-to-target", reconcile.ActionEstablishBaseline: "establish-baseline", reconcile.ActionNeedsDecision: "needs-decision", - reconcile.ActionRetireState: "retire-state", + reconcile.ActionRetireFileState: "retire-state", reconcile.ActionCreateAlias: "create-alias", reconcile.ActionReplaceAlias: "replace-alias", reconcile.ActionVerifyAlias: "verify-alias", diff --git a/internal/reconcile/classify_alias.go b/internal/reconcile/classify_alias.go index f50049f..0629481 100644 --- a/internal/reconcile/classify_alias.go +++ b/internal/reconcile/classify_alias.go @@ -45,19 +45,19 @@ func ClassifyAlias(record Evaluation, semantics FileSemantics) AliasClassificati func classifyAliasEntry(record Evaluation) AliasClassification { switch record.Target.Kind() { case KindDirectory, KindSpecial: - return aliasWithPath(aliasOutcome(ActionNeedsDecision, ReasonAliasOccupied, Rejected), record) + return aliasWithPath(aliasOutcome(ActionNeedsDecision, ReasonAliasOccupied, ConvergenceRejected), record) case KindAbsent: - return aliasWithPath(aliasOutcome(ActionCreateAlias, ReasonUnbaselinedAbsent, ActionPending), record) + return aliasWithPath(aliasOutcome(ActionCreateAlias, ReasonUnbaselinedAbsent, ConvergencePending), record) case KindSymlink: if record.Target.Payload() != payloadFor(record.Alias.CanonicalTargetRelativePath, record.Alias.AliasRelativePath) { - return aliasWithPath(aliasOutcome(ActionNeedsDecision, ReasonAliasWrong, DecisionRequired), record) + return aliasWithPath(aliasOutcome(ActionNeedsDecision, ReasonAliasWrong, ConvergenceDecisionRequired), record) } if record.AliasState == nil { - return aliasWithPath(aliasOutcome(ActionVerifyAlias, ReasonUnbaselinedEqual, Converged), record) + return aliasWithPath(aliasOutcome(ActionVerifyAlias, ReasonUnbaselinedEqual, ConvergenceConverged), record) } - return aliasWithPath(aliasOutcome(ActionNoOp, ReasonAliasExact, Converged), record) + return aliasWithPath(aliasOutcome(ActionNoOp, ReasonAliasExact, ConvergenceConverged), record) } - return aliasWithPath(aliasOutcome(ActionNeedsDecision, ReasonAliasOccupied, DecisionRequired), record) + return aliasWithPath(aliasOutcome(ActionNeedsDecision, ReasonAliasOccupied, ConvergenceDecisionRequired), record) } // classifyFileToAlias classifies a plan alias over an active file row: the @@ -67,13 +67,13 @@ func classifyAliasEntry(record Evaluation) AliasClassification { func classifyFileToAlias(record Evaluation, semantics FileSemantics) AliasClassification { switch record.Target.Kind() { case KindDirectory, KindSpecial: - return aliasWithPath(aliasOutcome(ActionNeedsDecision, ReasonRepresentationDrift, Rejected), record) + return aliasWithPath(aliasOutcome(ActionNeedsDecision, ReasonRepresentationDrift, ConvergenceRejected), record) case KindFile: if representationIntact(record, semantics) { - return aliasWithPath(aliasOutcome(ActionReplaceAlias, ReasonRepresentationIntact, ActionPending), record) + return aliasWithPath(aliasOutcome(ActionReplaceAlias, ReasonRepresentationIntact, ConvergencePending), record) } } - return aliasWithPath(aliasOutcome(ActionNeedsDecision, ReasonRepresentationDrift, DecisionRequired), record) + return aliasWithPath(aliasOutcome(ActionNeedsDecision, ReasonRepresentationDrift, ConvergenceDecisionRequired), record) } // classifyAliasToFile classifies a plan file over an active alias row: the @@ -83,13 +83,13 @@ func classifyFileToAlias(record Evaluation, semantics FileSemantics) AliasClassi func classifyAliasToFile(record Evaluation) AliasClassification { switch record.Target.Kind() { case KindDirectory, KindSpecial: - return aliasWithPath(aliasOutcome(ActionNeedsDecision, ReasonRepresentationDrift, Rejected), record) + return aliasWithPath(aliasOutcome(ActionNeedsDecision, ReasonRepresentationDrift, ConvergenceRejected), record) case KindSymlink: if record.Target.Payload() == payloadFor(record.AliasState.CanonicalTargetPath(), record.AliasState.AliasPath()) { - return aliasWithPath(aliasOutcome(ActionWriteSourceToTarget, ReasonRepresentationIntact, ActionPending), record) + return aliasWithPath(aliasOutcome(ActionWriteSourceToTarget, ReasonRepresentationIntact, ConvergencePending), record) } } - return aliasWithPath(aliasOutcome(ActionNeedsDecision, ReasonRepresentationDrift, DecisionRequired), record) + return aliasWithPath(aliasOutcome(ActionNeedsDecision, ReasonRepresentationDrift, ConvergenceDecisionRequired), record) } // representationIntact reports whether the current regular target provably diff --git a/internal/reconcile/classify_alias_test.go b/internal/reconcile/classify_alias_test.go index ae31942..5723126 100644 --- a/internal/reconcile/classify_alias_test.go +++ b/internal/reconcile/classify_alias_test.go @@ -49,69 +49,69 @@ type aliasCase struct { // representation-transition record, plus records the classifier must ignore. var aliasClassificationCases = []aliasCase{ {name: "alias create", kind: KindAbsent, - want: want(ActionCreateAlias, ReasonUnbaselinedAbsent, ActionPending)}, + want: want(ActionCreateAlias, ReasonUnbaselinedAbsent, ConvergencePending)}, {name: "alias exact unbaselined", kind: KindSymlink, payload: aliasPayload, - want: want(ActionVerifyAlias, ReasonUnbaselinedEqual, Converged)}, + want: want(ActionVerifyAlias, ReasonUnbaselinedEqual, ConvergenceConverged)}, {name: "alias wrong relative link", kind: KindSymlink, payload: "files/tool", - want: want(ActionNeedsDecision, ReasonAliasWrong, DecisionRequired)}, + want: want(ActionNeedsDecision, ReasonAliasWrong, ConvergenceDecisionRequired)}, {name: "alias absolute link", kind: KindSymlink, payload: "/home/user/files/tool", - want: want(ActionNeedsDecision, ReasonAliasWrong, DecisionRequired)}, + want: want(ActionNeedsDecision, ReasonAliasWrong, ConvergenceDecisionRequired)}, {name: "alias occupied file", kind: KindFile, content: targetX, mode: 0o644, - want: want(ActionNeedsDecision, ReasonAliasOccupied, DecisionRequired)}, + want: want(ActionNeedsDecision, ReasonAliasOccupied, ConvergenceDecisionRequired)}, {name: "alias occupied directory", kind: KindDirectory, - want: want(ActionNeedsDecision, ReasonAliasOccupied, Rejected)}, + want: want(ActionNeedsDecision, ReasonAliasOccupied, ConvergenceRejected)}, {name: "alias occupied special", kind: KindSpecial, - want: want(ActionNeedsDecision, ReasonAliasOccupied, Rejected)}, + want: want(ActionNeedsDecision, ReasonAliasOccupied, ConvergenceRejected)}, {name: "alias exact baselined", aliasRow: aliasStateActive(), kind: KindSymlink, payload: aliasPayload, - want: want(ActionNoOp, ReasonAliasExact, Converged)}, + want: want(ActionNoOp, ReasonAliasExact, ConvergenceConverged)}, {name: "alias missing recreated", aliasRow: aliasStateActive(), kind: KindAbsent, - want: want(ActionCreateAlias, ReasonUnbaselinedAbsent, ActionPending)}, + want: want(ActionCreateAlias, ReasonUnbaselinedAbsent, ConvergencePending)}, {name: "alias wrong baselined", aliasRow: aliasStateActive(), kind: KindSymlink, payload: "files/tool", - want: want(ActionNeedsDecision, ReasonAliasWrong, DecisionRequired)}, + want: want(ActionNeedsDecision, ReasonAliasWrong, ConvergenceDecisionRequired)}, {name: "alias occupied baselined", aliasRow: aliasStateActive(), kind: KindFile, content: targetX, mode: 0o644, - want: want(ActionNeedsDecision, ReasonAliasOccupied, DecisionRequired)}, + want: want(ActionNeedsDecision, ReasonAliasOccupied, ConvergenceDecisionRequired)}, {name: "alias directory baselined", aliasRow: aliasStateActive(), kind: KindDirectory, - want: want(ActionNeedsDecision, ReasonAliasOccupied, Rejected)}, + want: want(ActionNeedsDecision, ReasonAliasOccupied, ConvergenceRejected)}, {name: "alias retired exact", aliasRow: aliasStateRetired(), kind: KindSymlink, payload: aliasPayload, - want: want(ActionNoOp, ReasonAliasExact, Converged)}, + want: want(ActionNoOp, ReasonAliasExact, ConvergenceConverged)}, {name: "alias retired wrong", aliasRow: aliasStateRetired(), kind: KindSymlink, payload: "files/tool", - want: want(ActionNeedsDecision, ReasonAliasWrong, DecisionRequired)}, + want: want(ActionNeedsDecision, ReasonAliasWrong, ConvergenceDecisionRequired)}, {name: "alias over retired file", fileRow: retired(stateAt(sourceA, sourceA, 0)), kind: KindAbsent, - want: want(ActionCreateAlias, ReasonUnbaselinedAbsent, ActionPending)}, + want: want(ActionCreateAlias, ReasonUnbaselinedAbsent, ConvergencePending)}, {name: "file to alias intact", fileRow: stateAt(sourceA, sourceA, 0), kind: KindFile, content: sourceA, mode: 0o644, - want: want(ActionReplaceAlias, ReasonRepresentationIntact, ActionPending)}, + want: want(ActionReplaceAlias, ReasonRepresentationIntact, ConvergencePending)}, {name: "file to alias exec intact", fileRow: stateAt(sourceA, sourceA, 0o111), kind: KindFile, content: sourceA, mode: 0o755, - want: want(ActionReplaceAlias, ReasonRepresentationIntact, ActionPending)}, + want: want(ActionReplaceAlias, ReasonRepresentationIntact, ConvergencePending)}, {name: "file to alias mode drift", fileRow: stateAt(sourceA, sourceA, 0), kind: KindFile, content: sourceA, mode: 0o755, - want: want(ActionNeedsDecision, ReasonRepresentationDrift, DecisionRequired)}, + want: want(ActionNeedsDecision, ReasonRepresentationDrift, ConvergenceDecisionRequired)}, {name: "file to alias content drift", fileRow: stateAt(sourceA, sourceA, 0), kind: KindFile, content: sourceB, mode: 0o644, - want: want(ActionNeedsDecision, ReasonRepresentationDrift, DecisionRequired)}, + want: want(ActionNeedsDecision, ReasonRepresentationDrift, ConvergenceDecisionRequired)}, {name: "file to alias missing", fileRow: stateAt(sourceA, sourceA, 0), kind: KindAbsent, - want: want(ActionNeedsDecision, ReasonRepresentationDrift, DecisionRequired)}, + want: want(ActionNeedsDecision, ReasonRepresentationDrift, ConvergenceDecisionRequired)}, {name: "file to alias link", fileRow: stateAt(sourceA, sourceA, 0), kind: KindSymlink, payload: aliasPayload, - want: want(ActionNeedsDecision, ReasonRepresentationDrift, DecisionRequired)}, + want: want(ActionNeedsDecision, ReasonRepresentationDrift, ConvergenceDecisionRequired)}, {name: "file to alias directory", fileRow: stateAt(sourceA, sourceA, 0), kind: KindDirectory, - want: want(ActionNeedsDecision, ReasonRepresentationDrift, Rejected)}, + want: want(ActionNeedsDecision, ReasonRepresentationDrift, ConvergenceRejected)}, {name: "secret file to alias intact", secret: true, fileRow: secretStateAt(plainA, cipherA, 0), kind: KindFile, content: plainA, mode: 0o600, - want: want(ActionReplaceAlias, ReasonRepresentationIntact, ActionPending)}, + want: want(ActionReplaceAlias, ReasonRepresentationIntact, ConvergencePending)}, {name: "secret file to alias exec", secret: true, fileRow: secretStateAt(plainA, cipherA, 0o100), kind: KindFile, content: plainA, mode: 0o700, - want: want(ActionReplaceAlias, ReasonRepresentationIntact, ActionPending)}, + want: want(ActionReplaceAlias, ReasonRepresentationIntact, ConvergencePending)}, {name: "secret file to alias mode drift", secret: true, fileRow: secretStateAt(plainA, cipherA, 0), kind: KindFile, content: plainA, mode: 0o644, - want: want(ActionNeedsDecision, ReasonRepresentationDrift, DecisionRequired)}, + want: want(ActionNeedsDecision, ReasonRepresentationDrift, ConvergenceDecisionRequired)}, {name: "secret file to alias content drift", secret: true, fileRow: secretStateAt(plainA, cipherA, 0), kind: KindFile, content: plainB, mode: 0o600, - want: want(ActionNeedsDecision, ReasonRepresentationDrift, DecisionRequired)}, + want: want(ActionNeedsDecision, ReasonRepresentationDrift, ConvergenceDecisionRequired)}, {name: "alias to file intact", entry: PlanEntryFile, aliasRow: aliasStateActive(), kind: KindSymlink, payload: aliasPayload, - want: want(ActionWriteSourceToTarget, ReasonRepresentationIntact, ActionPending)}, + want: want(ActionWriteSourceToTarget, ReasonRepresentationIntact, ConvergencePending)}, {name: "alias to file wrong link", entry: PlanEntryFile, aliasRow: aliasStateActive(), kind: KindSymlink, payload: "files/tool", - want: want(ActionNeedsDecision, ReasonRepresentationDrift, DecisionRequired)}, + want: want(ActionNeedsDecision, ReasonRepresentationDrift, ConvergenceDecisionRequired)}, {name: "alias to file absolute", entry: PlanEntryFile, aliasRow: aliasStateActive(), kind: KindSymlink, payload: "/abs", - want: want(ActionNeedsDecision, ReasonRepresentationDrift, DecisionRequired)}, + want: want(ActionNeedsDecision, ReasonRepresentationDrift, ConvergenceDecisionRequired)}, {name: "alias to file missing", entry: PlanEntryFile, aliasRow: aliasStateActive(), kind: KindAbsent, - want: want(ActionNeedsDecision, ReasonRepresentationDrift, DecisionRequired)}, + want: want(ActionNeedsDecision, ReasonRepresentationDrift, ConvergenceDecisionRequired)}, {name: "alias to file file", entry: PlanEntryFile, aliasRow: aliasStateActive(), kind: KindFile, content: sourceA, mode: 0o644, - want: want(ActionNeedsDecision, ReasonRepresentationDrift, DecisionRequired)}, + want: want(ActionNeedsDecision, ReasonRepresentationDrift, ConvergenceDecisionRequired)}, {name: "alias to file directory", entry: PlanEntryFile, aliasRow: aliasStateActive(), kind: KindDirectory, - want: want(ActionNeedsDecision, ReasonRepresentationDrift, Rejected)}, + want: want(ActionNeedsDecision, ReasonRepresentationDrift, ConvergenceRejected)}, {name: "file entry untouched", entry: PlanEntryFile, kind: KindAbsent, want: AliasClassification{TargetPath: aliasPath}}, {name: "file with retired alias untouched", entry: PlanEntryFile, aliasRow: aliasStateRetired(), kind: KindFile, content: sourceA, mode: 0o644, diff --git a/internal/reconcile/classify_file.go b/internal/reconcile/classify_file.go index a65a080..fd783b2 100644 --- a/internal/reconcile/classify_file.go +++ b/internal/reconcile/classify_file.go @@ -49,9 +49,9 @@ func ClassifyFile(record Evaluation, semantics FileSemantics) FileClassification // explicit decision; directories and special entries require manual // intervention and are rejected. func classifyUnexpectedType(record Evaluation) FileClassification { - base := outcome(ActionNeedsDecision, ReasonUnexpectedTargetType, DecisionRequired) + base := outcome(ActionNeedsDecision, ReasonUnexpectedTargetType, ConvergenceDecisionRequired) if record.Target.Kind() == KindDirectory || record.Target.Kind() == KindSpecial { - base.Convergence = Rejected + base.Convergence = ConvergenceRejected } return withPath(base, record) } @@ -61,12 +61,12 @@ func classifyUnexpectedType(record Evaluation) FileClassification { // or require an explicit decision for differing content. func classifyUnbaselined(record Evaluation, semantics FileSemantics) FileClassification { if record.Target.Kind() != KindFile { - return withPath(outcome(ActionCreateTarget, ReasonUnbaselinedAbsent, ActionPending), record) + return withPath(outcome(ActionCreateTarget, ReasonUnbaselinedAbsent, ConvergencePending), record) } if semantics.Source != semantics.Target { - return withPath(outcome(ActionNeedsDecision, ReasonUnbaselinedDiffer, DecisionRequired), record) + return withPath(outcome(ActionNeedsDecision, ReasonUnbaselinedDiffer, ConvergenceDecisionRequired), record) } - return withPath(applyModeCorrection(outcome(ActionEstablishBaseline, ReasonUnbaselinedEqual, Converged), record), record) + return withPath(applyModeCorrection(outcome(ActionEstablishBaseline, ReasonUnbaselinedEqual, ConvergenceConverged), record), record) } // classifyBaselined maps the five core matrix rows of PLAN.md Section 9.2. @@ -83,15 +83,15 @@ func classifyBaselined(record Evaluation, row *FileState, semantics FileSemantic targetChanged := semantics.Target != row.BaselineContent() switch { case !sourceChanged && !targetChanged: - return withPath(applyModeCorrection(outcome(ActionNoOp, ReasonNoChange, Converged), record), record) + return withPath(applyModeCorrection(outcome(ActionNoOp, ReasonNoChange, ConvergenceConverged), record), record) case sourceChanged && !targetChanged: - return withPath(outcome(ActionWriteSourceToTarget, ReasonSourceChanged, ActionPending), record) + return withPath(outcome(ActionWriteSourceToTarget, ReasonSourceChanged, ConvergencePending), record) case !sourceChanged && targetChanged: - return withPath(outcome(ActionNeedsDecision, ReasonTargetDrift, DecisionRequired), record) + return withPath(outcome(ActionNeedsDecision, ReasonTargetDrift, ConvergenceDecisionRequired), record) case semantics.Source == semantics.Target: - return withPath(applyModeCorrection(outcome(ActionEstablishBaseline, ReasonAlreadyConverged, Converged), record), record) + return withPath(applyModeCorrection(outcome(ActionEstablishBaseline, ReasonAlreadyConverged, ConvergenceConverged), record), record) } - return withPath(outcome(ActionNeedsDecision, ReasonConflict, DecisionRequired), record) + return withPath(outcome(ActionNeedsDecision, ReasonConflict, ConvergenceDecisionRequired), record) } // applyModeCorrection upgrades a content-converged classification into a @@ -108,7 +108,7 @@ func applyModeCorrection(candidate FileClassification, record Evaluation) FileCl } candidate.Action = ActionCorrectMode candidate.Reason = ReasonModeCorrection - candidate.Convergence = ActionPending + candidate.Convergence = ConvergencePending return candidate } diff --git a/internal/reconcile/classify_file_test.go b/internal/reconcile/classify_file_test.go index f4fb52a..30933a0 100644 --- a/internal/reconcile/classify_file_test.go +++ b/internal/reconcile/classify_file_test.go @@ -44,80 +44,80 @@ type fileCase struct { var fileClassificationCases = []fileCase{ {name: "no-op", kind: KindFile, source: sourceA, target: sourceA, mode: 0o644, baseline: stateAt(sourceA, sourceA, 0), - want: FileClassification{TargetPath: "a.conf", Action: ActionNoOp, Reason: ReasonNoChange, Convergence: Converged}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionNoOp, Reason: ReasonNoChange, Convergence: ConvergenceConverged}}, {name: "mode correction", kind: KindFile, source: sourceA, target: sourceA, mode: 0o644, exec: 0o100, baseline: stateAt(sourceA, sourceA, 0), - want: FileClassification{TargetPath: "a.conf", Action: ActionCorrectMode, Reason: ReasonModeCorrection, Convergence: ActionPending}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionCorrectMode, Reason: ReasonModeCorrection, Convergence: ConvergencePending}}, {name: "rw bits preserved", kind: KindFile, source: sourceA, target: sourceA, mode: 0o600, baseline: stateAt(sourceA, sourceA, 0), - want: FileClassification{TargetPath: "a.conf", Action: ActionNoOp, Reason: ReasonNoChange, Convergence: Converged}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionNoOp, Reason: ReasonNoChange, Convergence: ConvergenceConverged}}, {name: "source-only change", kind: KindFile, source: sourceB, target: sourceA, mode: 0o644, baseline: stateAt(sourceA, sourceA, 0), - want: FileClassification{TargetPath: "a.conf", Action: ActionWriteSourceToTarget, Reason: ReasonSourceChanged, Convergence: ActionPending}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionWriteSourceToTarget, Reason: ReasonSourceChanged, Convergence: ConvergencePending}}, {name: "target-only drift", kind: KindFile, source: sourceA, target: targetX, mode: 0o644, baseline: stateAt(sourceA, sourceA, 0), - want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonTargetDrift, Convergence: DecisionRequired}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonTargetDrift, Convergence: ConvergenceDecisionRequired}}, {name: "both changed converged", kind: KindFile, source: sourceB, target: sourceB, mode: 0o644, baseline: stateAt(sourceA, sourceA, 0), - want: FileClassification{TargetPath: "a.conf", Action: ActionEstablishBaseline, Reason: ReasonAlreadyConverged, Convergence: Converged}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionEstablishBaseline, Reason: ReasonAlreadyConverged, Convergence: ConvergenceConverged}}, {name: "conflict", kind: KindFile, source: sourceB, target: targetX, mode: 0o644, baseline: stateAt(sourceA, sourceA, 0), - want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonConflict, Convergence: DecisionRequired}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonConflict, Convergence: ConvergenceDecisionRequired}}, {name: "absent target drift", source: sourceA, kind: KindAbsent, baseline: stateAt(sourceA, sourceA, 0), - want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonTargetDrift, Convergence: DecisionRequired}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonTargetDrift, Convergence: ConvergenceDecisionRequired}}, {name: "absent target conflict", source: sourceB, kind: KindAbsent, baseline: stateAt(sourceA, sourceA, 0), - want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonConflict, Convergence: DecisionRequired}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonConflict, Convergence: ConvergenceDecisionRequired}}, {name: "secret no-op", kind: KindFile, source: plainA, cipher: cipherA, target: plainA, secret: true, mode: 0o600, baseline: secretStateAt(plainA, cipherA, 0), - want: FileClassification{TargetPath: "a.conf", Action: ActionNoOp, Reason: ReasonNoChange, Convergence: Converged}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionNoOp, Reason: ReasonNoChange, Convergence: ConvergenceConverged}}, {name: "secret mode correction", kind: KindFile, source: plainA, cipher: cipherA, target: plainA, secret: true, mode: 0o600, exec: 0o100, baseline: secretStateAt(plainA, cipherA, 0o100), - want: FileClassification{TargetPath: "a.conf", Action: ActionCorrectMode, Reason: ReasonModeCorrection, Convergence: ActionPending}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionCorrectMode, Reason: ReasonModeCorrection, Convergence: ConvergencePending}}, {name: "secret re-encryption", kind: KindFile, source: plainA, cipher: cipherB, target: plainA, secret: true, mode: 0o600, baseline: secretStateAt(plainA, cipherA, 0), - want: FileClassification{TargetPath: "a.conf", Action: ActionNoOp, Reason: ReasonNoChange, Convergence: Converged}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionNoOp, Reason: ReasonNoChange, Convergence: ConvergenceConverged}}, {name: "secret source change", kind: KindFile, source: plainB, cipher: cipherB, target: plainA, secret: true, mode: 0o600, baseline: secretStateAt(plainA, cipherA, 0), - want: FileClassification{TargetPath: "a.conf", Action: ActionWriteSourceToTarget, Reason: ReasonSourceChanged, Convergence: ActionPending}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionWriteSourceToTarget, Reason: ReasonSourceChanged, Convergence: ConvergencePending}}, {name: "secret target drift", kind: KindFile, source: plainA, cipher: cipherA, target: plainB, secret: true, mode: 0o600, baseline: secretStateAt(plainA, cipherA, 0), - want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonTargetDrift, Convergence: DecisionRequired}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonTargetDrift, Convergence: ConvergenceDecisionRequired}}, {name: "secret converged", kind: KindFile, source: plainB, cipher: cipherB, target: plainB, secret: true, mode: 0o600, baseline: secretStateAt(plainA, cipherA, 0), - want: FileClassification{TargetPath: "a.conf", Action: ActionEstablishBaseline, Reason: ReasonAlreadyConverged, Convergence: Converged}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionEstablishBaseline, Reason: ReasonAlreadyConverged, Convergence: ConvergenceConverged}}, {name: "secret conflict", kind: KindFile, source: plainB, cipher: cipherB, target: plainC, secret: true, mode: 0o600, baseline: secretStateAt(plainA, cipherA, 0), - want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonConflict, Convergence: DecisionRequired}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonConflict, Convergence: ConvergenceDecisionRequired}}, {name: "database loss create", source: sourceA, kind: KindAbsent, - want: FileClassification{TargetPath: "a.conf", Action: ActionCreateTarget, Reason: ReasonUnbaselinedAbsent, Convergence: ActionPending}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionCreateTarget, Reason: ReasonUnbaselinedAbsent, Convergence: ConvergencePending}}, {name: "database loss adopt", kind: KindFile, source: sourceA, target: sourceA, mode: 0o644, - want: FileClassification{TargetPath: "a.conf", Action: ActionEstablishBaseline, Reason: ReasonUnbaselinedEqual, Convergence: Converged}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionEstablishBaseline, Reason: ReasonUnbaselinedEqual, Convergence: ConvergenceConverged}}, {name: "database loss mode", kind: KindFile, source: sourceA, target: sourceA, mode: 0o644, exec: 0o100, - want: FileClassification{TargetPath: "a.conf", Action: ActionCorrectMode, Reason: ReasonModeCorrection, Convergence: ActionPending}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionCorrectMode, Reason: ReasonModeCorrection, Convergence: ConvergencePending}}, {name: "database loss differ", kind: KindFile, source: sourceA, target: targetX, mode: 0o644, - want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonUnbaselinedDiffer, Convergence: DecisionRequired}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonUnbaselinedDiffer, Convergence: ConvergenceDecisionRequired}}, {name: "database loss secret adopt", kind: KindFile, source: plainA, cipher: cipherA, target: plainA, secret: true, mode: 0o600, - want: FileClassification{TargetPath: "a.conf", Action: ActionEstablishBaseline, Reason: ReasonUnbaselinedEqual, Convergence: Converged}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionEstablishBaseline, Reason: ReasonUnbaselinedEqual, Convergence: ConvergenceConverged}}, {name: "symlink drift", source: sourceA, kind: KindSymlink, baseline: stateAt(sourceA, sourceA, 0), - want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonUnexpectedTargetType, Convergence: DecisionRequired}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonUnexpectedTargetType, Convergence: ConvergenceDecisionRequired}}, {name: "symlink unbaselined", source: sourceA, kind: KindSymlink, - want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonUnexpectedTargetType, Convergence: DecisionRequired}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonUnexpectedTargetType, Convergence: ConvergenceDecisionRequired}}, {name: "directory target", source: sourceA, kind: KindDirectory, baseline: stateAt(sourceA, sourceA, 0), - want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonUnexpectedTargetType, Convergence: Rejected}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonUnexpectedTargetType, Convergence: ConvergenceRejected}}, {name: "directory unbaselined", source: sourceA, kind: KindDirectory, - want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonUnexpectedTargetType, Convergence: Rejected}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonUnexpectedTargetType, Convergence: ConvergenceRejected}}, {name: "special target", source: sourceA, kind: KindSpecial, - want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonUnexpectedTargetType, Convergence: Rejected}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionNeedsDecision, Reason: ReasonUnexpectedTargetType, Convergence: ConvergenceRejected}}, {name: "retired reactivation", kind: KindFile, source: sourceA, target: sourceA, mode: 0o644, baseline: retired(stateAt(sourceA, sourceA, 0)), - want: FileClassification{TargetPath: "a.conf", Action: ActionNoOp, Reason: ReasonNoChange, Convergence: Converged}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionNoOp, Reason: ReasonNoChange, Convergence: ConvergenceConverged}}, {name: "retired source change", kind: KindFile, source: sourceB, target: sourceA, mode: 0o644, baseline: retired(stateAt(sourceA, sourceA, 0)), - want: FileClassification{TargetPath: "a.conf", Action: ActionWriteSourceToTarget, Reason: ReasonSourceChanged, Convergence: ActionPending}}, + want: FileClassification{TargetPath: "a.conf", Action: ActionWriteSourceToTarget, Reason: ReasonSourceChanged, Convergence: ConvergencePending}}, } // fileRecordAt assembles one complete file evaluation from its table row. diff --git a/internal/reconcile/classify_retirement.go b/internal/reconcile/classify_retirement.go index 1256978..5ede6bd 100644 --- a/internal/reconcile/classify_retirement.go +++ b/internal/reconcile/classify_retirement.go @@ -32,7 +32,7 @@ func ClassifyRetirement(record Evaluation, platform string) RetirementClassifica case row == nil && alias == nil: return base } - return withRetirementPath(retirementOutcome(ActionNoOp, ReasonAlreadyRetired, Converged), record) + return withRetirementPath(retirementOutcome(ActionNoOp, ReasonAlreadyRetired, ConvergenceConverged), record) } // fileRetirement classifies one active file row without a producer: tracking @@ -40,17 +40,17 @@ func ClassifyRetirement(record Evaluation, platform string) RetirementClassifica // layer. func fileRetirement(record Evaluation, platform string) RetirementClassification { if record.FileState.Layer().InactiveOn(platform) { - return withRetirementPath(retirementOutcome(ActionNoOp, ReasonInactivePlatform, Converged), record) + return withRetirementPath(retirementOutcome(ActionNoOp, ReasonInactivePlatform, ConvergenceConverged), record) } - return withRetirementPath(retirementOutcome(ActionRetireState, ReasonSourceRemoved, ActionPending), record) + return withRetirementPath(retirementOutcome(ActionRetireFileState, ReasonSourceRemoved, ConvergencePending), record) } // aliasRetirement classifies one active alias row without a producer. func aliasRetirement(record Evaluation, platform string) RetirementClassification { if record.AliasState.Layer().InactiveOn(platform) { - return withRetirementPath(retirementOutcome(ActionNoOp, ReasonInactivePlatform, Converged), record) + return withRetirementPath(retirementOutcome(ActionNoOp, ReasonInactivePlatform, ConvergenceConverged), record) } - return withRetirementPath(retirementOutcome(ActionRetireAliasState, ReasonSourceRemoved, ActionPending), record) + return withRetirementPath(retirementOutcome(ActionRetireAliasState, ReasonSourceRemoved, ConvergencePending), record) } // retirementOutcome builds a bare classification from its three enum fields. diff --git a/internal/reconcile/classify_retirement_test.go b/internal/reconcile/classify_retirement_test.go index bf6ed9e..e7153ab 100644 --- a/internal/reconcile/classify_retirement_test.go +++ b/internal/reconcile/classify_retirement_test.go @@ -43,27 +43,27 @@ type retirementCase struct { // classifiers. var retirementClassificationCases = []retirementCase{ {name: "file source removed", fileRow: fileRowState(deployment.LayerBase), - want: retirementWant(ActionRetireState, ReasonSourceRemoved, ActionPending)}, + want: retirementWant(ActionRetireFileState, ReasonSourceRemoved, ConvergencePending)}, {name: "linux file source removed", fileRow: fileRowState(deployment.LayerLinux), - want: retirementWant(ActionRetireState, ReasonSourceRemoved, ActionPending)}, + want: retirementWant(ActionRetireFileState, ReasonSourceRemoved, ConvergencePending)}, {name: "base file row on darwin", platform: "darwin", fileRow: fileRowState(deployment.LayerBase), - want: retirementWant(ActionRetireState, ReasonSourceRemoved, ActionPending)}, + want: retirementWant(ActionRetireFileState, ReasonSourceRemoved, ConvergencePending)}, {name: "darwin file row on linux", platform: "linux", fileRow: fileRowState(deployment.LayerDarwin), - want: retirementWant(ActionNoOp, ReasonInactivePlatform, Converged)}, + want: retirementWant(ActionNoOp, ReasonInactivePlatform, ConvergenceConverged)}, {name: "linux file row on darwin", platform: "darwin", fileRow: fileRowState(deployment.LayerLinux), - want: retirementWant(ActionNoOp, ReasonInactivePlatform, Converged)}, + want: retirementWant(ActionNoOp, ReasonInactivePlatform, ConvergenceConverged)}, {name: "file already retired", fileRow: retiredFileRow(deployment.LayerBase), - want: retirementWant(ActionNoOp, ReasonAlreadyRetired, Converged)}, + want: retirementWant(ActionNoOp, ReasonAlreadyRetired, ConvergenceConverged)}, {name: "alias source removed", aliasRow: aliasRowState(state.LayerAll), - want: retirementWant(ActionRetireAliasState, ReasonSourceRemoved, ActionPending)}, + want: retirementWant(ActionRetireAliasState, ReasonSourceRemoved, ConvergencePending)}, {name: "linux alias source removed", aliasRow: aliasRowState(state.LayerLinux), - want: retirementWant(ActionRetireAliasState, ReasonSourceRemoved, ActionPending)}, + want: retirementWant(ActionRetireAliasState, ReasonSourceRemoved, ConvergencePending)}, {name: "darwin alias row on linux", platform: "linux", aliasRow: aliasRowState(state.LayerDarwin), - want: retirementWant(ActionNoOp, ReasonInactivePlatform, Converged)}, + want: retirementWant(ActionNoOp, ReasonInactivePlatform, ConvergenceConverged)}, {name: "linux alias row on darwin", platform: "darwin", aliasRow: aliasRowState(state.LayerLinux), - want: retirementWant(ActionNoOp, ReasonInactivePlatform, Converged)}, + want: retirementWant(ActionNoOp, ReasonInactivePlatform, ConvergenceConverged)}, {name: "alias already retired", aliasRow: retiredAliasRow(state.LayerAll), - want: retirementWant(ActionNoOp, ReasonAlreadyRetired, Converged)}, + want: retirementWant(ActionNoOp, ReasonAlreadyRetired, ConvergenceConverged)}, {name: "moved file ownership", entry: PlanEntryFile, fileRow: fileRowState(deployment.LayerBase), want: retirementUntouched}, {name: "moved alias ownership", entry: PlanEntryAlias, aliasRow: aliasRowState(state.LayerAll), @@ -185,12 +185,12 @@ func retirementWant(action Action, reason Reason, convergence Convergence) Retir // retiring is the source-removal classification of one file target. func retiring(path string) RetirementClassification { - return RetirementClassification{TargetPath: path, Action: ActionRetireState, Reason: ReasonSourceRemoved, Convergence: ActionPending} + return RetirementClassification{TargetPath: path, Action: ActionRetireFileState, Reason: ReasonSourceRemoved, Convergence: ConvergencePending} } // retiringAlias is the source-removal classification of one alias target. func retiringAlias(path string) RetirementClassification { - return RetirementClassification{TargetPath: path, Action: ActionRetireAliasState, Reason: ReasonSourceRemoved, Convergence: ActionPending} + return RetirementClassification{TargetPath: path, Action: ActionRetireAliasState, Reason: ReasonSourceRemoved, Convergence: ConvergencePending} } // retirementZero is the untouched classification of a record another diff --git a/internal/reconcile/decisions.go b/internal/reconcile/decisions.go index 7eddab8..9ebe44a 100644 --- a/internal/reconcile/decisions.go +++ b/internal/reconcile/decisions.go @@ -57,7 +57,7 @@ func AllowedChoices(action Action, reason Reason, kind deployment.FileKind) []De // a decision are rejected: converged, pending, and rejected outcomes never // prompt (PLAN.md Sections 9.2 and 9.3). func DecisionSpecForFile(classification FileClassification, kind deployment.FileKind) (DecisionSpec, error) { - if classification.Convergence != DecisionRequired { + if classification.Convergence != ConvergenceDecisionRequired { return DecisionSpec{}, fmt.Errorf("reconcile: file %q does not require a decision", classification.TargetPath) } return NewDecisionSpec(DecisionSpecInput{ @@ -73,7 +73,7 @@ func DecisionSpecForFile(classification FileClassification, kind deployment.File // (PLAN.md Sections 5.4 and 9.5). Alias reasons never qualify for diff, so // the kind argument is irrelevant to the eligibility call. func DecisionSpecForAlias(classification AliasClassification) (DecisionSpec, error) { - if classification.Convergence != DecisionRequired { + if classification.Convergence != ConvergenceDecisionRequired { return DecisionSpec{}, fmt.Errorf("reconcile: alias %q does not require a decision", classification.TargetPath) } return NewDecisionSpec(DecisionSpecInput{ diff --git a/internal/reconcile/decisions_test.go b/internal/reconcile/decisions_test.go index d0c2acf..9256af8 100644 --- a/internal/reconcile/decisions_test.go +++ b/internal/reconcile/decisions_test.go @@ -92,19 +92,19 @@ type specCase struct { // classification: only DecisionRequired outcomes produce a spec, carrying // exactly the allowed choices of their action, reason, and source kind. var decisionSpecCases = []specCase{ - {name: "file drift", path: decisionPath, action: ActionNeedsDecision, reason: ReasonTargetDrift, convergence: DecisionRequired, kind: deployment.FileOrdinary, want: diffOverwriteSkipAbort}, - {name: "file drift secret", path: decisionPath, action: ActionNeedsDecision, reason: ReasonTargetDrift, convergence: DecisionRequired, kind: deployment.FileSecret, want: overwriteSkipAbort}, - {name: "file conflict", path: decisionPath, action: ActionNeedsDecision, reason: ReasonConflict, convergence: DecisionRequired, kind: deployment.FileOrdinary, want: diffOverwriteSkipAbort}, - {name: "file unbaselined differ", path: decisionPath, action: ActionNeedsDecision, reason: ReasonUnbaselinedDiffer, convergence: DecisionRequired, kind: deployment.FileOrdinary, want: diffOverwriteSkipAbort}, - {name: "file unexpected type", path: decisionPath, action: ActionNeedsDecision, reason: ReasonUnexpectedTargetType, convergence: DecisionRequired, kind: deployment.FileOrdinary, want: overwriteSkipAbort}, - {name: "file converged rejected", path: decisionPath, action: ActionNoOp, reason: ReasonNoChange, convergence: Converged, kind: deployment.FileOrdinary, wantErr: true}, - {name: "file pending rejected", path: decisionPath, action: ActionWriteSourceToTarget, reason: ReasonSourceChanged, convergence: ActionPending, kind: deployment.FileOrdinary, wantErr: true}, - {name: "file rejected rejected", path: decisionPath, action: ActionNeedsDecision, reason: ReasonUnexpectedTargetType, convergence: Rejected, kind: deployment.FileOrdinary, wantErr: true}, - {name: "alias wrong", path: aliasDecisionPath, action: ActionNeedsDecision, reason: ReasonAliasWrong, convergence: DecisionRequired, kind: deployment.FileOrdinary, alias: true, want: overwriteSkipAbort}, - {name: "alias occupied", path: aliasDecisionPath, action: ActionNeedsDecision, reason: ReasonAliasOccupied, convergence: DecisionRequired, kind: deployment.FileSecret, alias: true, want: overwriteSkipAbort}, - {name: "alias representation drift", path: aliasDecisionPath, action: ActionNeedsDecision, reason: ReasonRepresentationDrift, convergence: DecisionRequired, kind: deployment.FileOrdinary, alias: true, want: overwriteSkipAbort}, - {name: "alias converged rejected", path: aliasDecisionPath, action: ActionNoOp, reason: ReasonAliasExact, convergence: Converged, alias: true, wantErr: true}, - {name: "alias rejected rejected", path: aliasDecisionPath, action: ActionNeedsDecision, reason: ReasonAliasOccupied, convergence: Rejected, alias: true, wantErr: true}, + {name: "file drift", path: decisionPath, action: ActionNeedsDecision, reason: ReasonTargetDrift, convergence: ConvergenceDecisionRequired, kind: deployment.FileOrdinary, want: diffOverwriteSkipAbort}, + {name: "file drift secret", path: decisionPath, action: ActionNeedsDecision, reason: ReasonTargetDrift, convergence: ConvergenceDecisionRequired, kind: deployment.FileSecret, want: overwriteSkipAbort}, + {name: "file conflict", path: decisionPath, action: ActionNeedsDecision, reason: ReasonConflict, convergence: ConvergenceDecisionRequired, kind: deployment.FileOrdinary, want: diffOverwriteSkipAbort}, + {name: "file unbaselined differ", path: decisionPath, action: ActionNeedsDecision, reason: ReasonUnbaselinedDiffer, convergence: ConvergenceDecisionRequired, kind: deployment.FileOrdinary, want: diffOverwriteSkipAbort}, + {name: "file unexpected type", path: decisionPath, action: ActionNeedsDecision, reason: ReasonUnexpectedTargetType, convergence: ConvergenceDecisionRequired, kind: deployment.FileOrdinary, want: overwriteSkipAbort}, + {name: "file converged rejected", path: decisionPath, action: ActionNoOp, reason: ReasonNoChange, convergence: ConvergenceConverged, kind: deployment.FileOrdinary, wantErr: true}, + {name: "file pending rejected", path: decisionPath, action: ActionWriteSourceToTarget, reason: ReasonSourceChanged, convergence: ConvergencePending, kind: deployment.FileOrdinary, wantErr: true}, + {name: "file rejected rejected", path: decisionPath, action: ActionNeedsDecision, reason: ReasonUnexpectedTargetType, convergence: ConvergenceRejected, kind: deployment.FileOrdinary, wantErr: true}, + {name: "alias wrong", path: aliasDecisionPath, action: ActionNeedsDecision, reason: ReasonAliasWrong, convergence: ConvergenceDecisionRequired, kind: deployment.FileOrdinary, alias: true, want: overwriteSkipAbort}, + {name: "alias occupied", path: aliasDecisionPath, action: ActionNeedsDecision, reason: ReasonAliasOccupied, convergence: ConvergenceDecisionRequired, kind: deployment.FileSecret, alias: true, want: overwriteSkipAbort}, + {name: "alias representation drift", path: aliasDecisionPath, action: ActionNeedsDecision, reason: ReasonRepresentationDrift, convergence: ConvergenceDecisionRequired, kind: deployment.FileOrdinary, alias: true, want: overwriteSkipAbort}, + {name: "alias converged rejected", path: aliasDecisionPath, action: ActionNoOp, reason: ReasonAliasExact, convergence: ConvergenceConverged, alias: true, wantErr: true}, + {name: "alias rejected rejected", path: aliasDecisionPath, action: ActionNeedsDecision, reason: ReasonAliasOccupied, convergence: ConvergenceRejected, alias: true, wantErr: true}, } // invalidCase is one row of the choice-validation matrix. diff --git a/internal/reconcile/snapshot.go b/internal/reconcile/snapshot.go index 24675ba..fbdafd6 100644 --- a/internal/reconcile/snapshot.go +++ b/internal/reconcile/snapshot.go @@ -53,16 +53,16 @@ type EvaluationSnapshot struct { // union path, and persisted state rows join their paths. Plan and state // inputs may arrive in any order; records always return sorted. Secret // sources keep ciphertext only; no decryption ever happens here. -func Assemble(plan deployment.Plan, state StateSnapshot, client *secrets.Client) (EvaluationSnapshot, error) { - if err := requireAssemblyPlan(plan, state); err != nil { +func Assemble(plan deployment.Plan, stateSnapshot StateSnapshot, client *secrets.Client) (EvaluationSnapshot, error) { + if err := requireAssemblyPlan(plan, stateSnapshot); err != nil { return EvaluationSnapshot{}, err } files, aliases, err := entryIndexes(plan) if err != nil { return EvaluationSnapshot{}, err } - input := joinInput{home: state.HomePath(), files: files, aliases: aliases, - fileRows: byFileState(state.AllFiles()), aliasRows: byAliasState(state.AllAliases()), client: client} + input := joinInput{home: stateSnapshot.HomePath(), files: files, aliases: aliases, + fileRows: byFileState(stateSnapshot.AllFiles()), aliasRows: byAliasState(stateSnapshot.AllAliases()), client: client} records, err := joinedRecords(input) if err != nil { return EvaluationSnapshot{}, err @@ -70,7 +70,7 @@ func Assemble(plan deployment.Plan, state StateSnapshot, client *secrets.Client) sort.SliceStable(records, func(first, second int) bool { return records[first].TargetPath < records[second].TargetPath }) - return EvaluationSnapshot{RepositoryRoot: plan.RepositoryRoot(), HomePath: state.HomePath(), + return EvaluationSnapshot{RepositoryRoot: plan.RepositoryRoot(), HomePath: stateSnapshot.HomePath(), Platform: plan.Platform(), records: records}, nil } @@ -224,11 +224,11 @@ func cloneAliasRecord(record *AliasState) *AliasState { // All returns a defensive copy of every joined record in bytewise path // order, cloning the persisted rows so callers cannot reach the snapshot. -func (s EvaluationSnapshot) All() []Evaluation { - if s.records == nil { +func (snapshot EvaluationSnapshot) All() []Evaluation { + if snapshot.records == nil { return nil } - records := append([]Evaluation(nil), s.records...) + records := append([]Evaluation(nil), snapshot.records...) for index := range records { records[index].FileState = cloneFileRecord(records[index].FileState) records[index].AliasState = cloneAliasRecord(records[index].AliasState) diff --git a/internal/reconcile/types.go b/internal/reconcile/types.go index ed5799c..8b2c287 100644 --- a/internal/reconcile/types.go +++ b/internal/reconcile/types.go @@ -49,7 +49,7 @@ const ( ActionWriteSourceToTarget ActionEstablishBaseline ActionNeedsDecision - ActionRetireState + ActionRetireFileState ActionCreateAlias ActionReplaceAlias ActionVerifyAlias @@ -88,13 +88,13 @@ func (r Reason) Valid() bool { return r >= ReasonNoChange && r <= ReasonAlreadyR type Convergence int const ( - Converged Convergence = iota - ActionPending - DecisionRequired - Rejected + ConvergenceConverged Convergence = iota + ConvergencePending + ConvergenceDecisionRequired + ConvergenceRejected ) -func (c Convergence) Valid() bool { return c >= Converged && c <= Rejected } +func (c Convergence) Valid() bool { return c >= ConvergenceConverged && c <= ConvergenceRejected } // DecisionChoice names one answer a user may give to a resolution prompt. type DecisionChoice int @@ -148,13 +148,13 @@ func NewDecisionSpec(input DecisionSpecInput) (DecisionSpec, error) { return DecisionSpec{targetPath: input.TargetPath, action: input.Action, reason: input.Reason, choices: append([]DecisionChoice(nil), input.Choices...)}, nil } -func (s DecisionSpec) TargetPath() string { return s.targetPath } -func (s DecisionSpec) Action() Action { return s.action } -func (s DecisionSpec) Reason() Reason { return s.reason } +func (spec DecisionSpec) TargetPath() string { return spec.targetPath } +func (spec DecisionSpec) Action() Action { return spec.action } +func (spec DecisionSpec) Reason() Reason { return spec.reason } // AllChoices returns a defensive copy of the resolution choices. -func (s DecisionSpec) AllChoices() []DecisionChoice { - return append([]DecisionChoice(nil), s.choices...) +func (spec DecisionSpec) AllChoices() []DecisionChoice { + return append([]DecisionChoice(nil), spec.choices...) } // SourceSnapshot freezes the immutable facts of one deployment source. @@ -168,13 +168,13 @@ type SourceSnapshot struct { executable fs.FileMode } -func (s SourceSnapshot) Path() string { return s.path } -func (s SourceSnapshot) Identity() pathsafe.Identity { return s.identity } -func (s SourceSnapshot) Kind() EntryKind { return s.kind } -func (s SourceSnapshot) Token() ContentToken { return s.token } -func (s SourceSnapshot) Semantic() deployment.Digest { return s.semantic } -func (s SourceSnapshot) Storage() deployment.Digest { return s.storage } -func (s SourceSnapshot) Executable() fs.FileMode { return s.executable } +func (snapshot SourceSnapshot) Path() string { return snapshot.path } +func (snapshot SourceSnapshot) Identity() pathsafe.Identity { return snapshot.identity } +func (snapshot SourceSnapshot) Kind() EntryKind { return snapshot.kind } +func (snapshot SourceSnapshot) Token() ContentToken { return snapshot.token } +func (snapshot SourceSnapshot) Semantic() deployment.Digest { return snapshot.semantic } +func (snapshot SourceSnapshot) Storage() deployment.Digest { return snapshot.storage } +func (snapshot SourceSnapshot) Executable() fs.FileMode { return snapshot.executable } // TargetSnapshot freezes the immutable facts of one destination observation // and doubles as the immutable target precondition (PLAN.md Section 12.4). @@ -189,14 +189,14 @@ type TargetSnapshot struct { payload string } -func (t TargetSnapshot) Destination() Destination { return t.destination } -func (t TargetSnapshot) Parent() pathsafe.Identity { return t.parent } -func (t TargetSnapshot) Kind() EntryKind { return t.kind } -func (t TargetSnapshot) Identity() pathsafe.Identity { return t.identity } -func (t TargetSnapshot) Token() ContentToken { return t.token } -func (t TargetSnapshot) Digest() deployment.Digest { return t.digest } -func (t TargetSnapshot) Mode() fs.FileMode { return t.mode } -func (t TargetSnapshot) Payload() string { return t.payload } +func (snapshot TargetSnapshot) Destination() Destination { return snapshot.destination } +func (snapshot TargetSnapshot) Parent() pathsafe.Identity { return snapshot.parent } +func (snapshot TargetSnapshot) Kind() EntryKind { return snapshot.kind } +func (snapshot TargetSnapshot) Identity() pathsafe.Identity { return snapshot.identity } +func (snapshot TargetSnapshot) Token() ContentToken { return snapshot.token } +func (snapshot TargetSnapshot) Digest() deployment.Digest { return snapshot.digest } +func (snapshot TargetSnapshot) Mode() fs.FileMode { return snapshot.mode } +func (snapshot TargetSnapshot) Payload() string { return snapshot.payload } // FileState is one immutable evaluation record of a persisted file row. type FileState struct { @@ -212,16 +212,16 @@ type FileState struct { retiredAt *time.Time } -func (s FileState) TargetPath() string { return s.targetPath } -func (s FileState) GroupName() string { return s.groupName } -func (s FileState) SourcePath() string { return s.sourcePath } -func (s FileState) SourceKind() deployment.FileKind { return s.sourceKind } -func (s FileState) Layer() deployment.Layer { return s.layer } -func (s FileState) BaselineContent() deployment.Digest { return s.baselineContent } -func (s FileState) BaselineSource() deployment.Digest { return s.baselineSource } -func (s FileState) ExecutableBits() fs.FileMode { return s.executableBits } -func (s FileState) Active() bool { return s.active } -func (s FileState) RetiredAt() *time.Time { return state.CloneTimestamp(s.retiredAt) } +func (fileState FileState) TargetPath() string { return fileState.targetPath } +func (fileState FileState) GroupName() string { return fileState.groupName } +func (fileState FileState) SourcePath() string { return fileState.sourcePath } +func (fileState FileState) SourceKind() deployment.FileKind { return fileState.sourceKind } +func (fileState FileState) Layer() deployment.Layer { return fileState.layer } +func (fileState FileState) BaselineContent() deployment.Digest { return fileState.baselineContent } +func (fileState FileState) BaselineSource() deployment.Digest { return fileState.baselineSource } +func (fileState FileState) ExecutableBits() fs.FileMode { return fileState.executableBits } +func (fileState FileState) Active() bool { return fileState.active } +func (fileState FileState) RetiredAt() *time.Time { return state.CloneTimestamp(fileState.retiredAt) } // AliasState is one immutable evaluation record of a persisted alias row. type AliasState struct { @@ -233,9 +233,11 @@ type AliasState struct { retiredAt *time.Time } -func (s AliasState) AliasPath() string { return s.aliasPath } -func (s AliasState) CanonicalTargetPath() string { return s.canonicalTargetPath } -func (s AliasState) GroupName() string { return s.groupName } -func (s AliasState) Layer() state.AliasLayer { return s.layer } -func (s AliasState) Active() bool { return s.active } -func (s AliasState) RetiredAt() *time.Time { return state.CloneTimestamp(s.retiredAt) } +func (aliasState AliasState) AliasPath() string { return aliasState.aliasPath } +func (aliasState AliasState) CanonicalTargetPath() string { return aliasState.canonicalTargetPath } +func (aliasState AliasState) GroupName() string { return aliasState.groupName } +func (aliasState AliasState) Layer() state.AliasLayer { return aliasState.layer } +func (aliasState AliasState) Active() bool { return aliasState.active } +func (aliasState AliasState) RetiredAt() *time.Time { + return state.CloneTimestamp(aliasState.retiredAt) +} diff --git a/internal/reconcile/types_test.go b/internal/reconcile/types_test.go index 520465d..963bc31 100644 --- a/internal/reconcile/types_test.go +++ b/internal/reconcile/types_test.go @@ -36,7 +36,7 @@ func TestReconciliationContract(t *testing.T) { func testEnumValidity(t *testing.T) { actions := []Action{ ActionNoOp, ActionCorrectMode, ActionCreateTarget, ActionWriteSourceToTarget, - ActionEstablishBaseline, ActionNeedsDecision, ActionRetireState, ActionCreateAlias, + ActionEstablishBaseline, ActionNeedsDecision, ActionRetireFileState, ActionCreateAlias, ActionReplaceAlias, ActionVerifyAlias, ActionRetireAliasState, } for _, action := range actions { @@ -66,7 +66,7 @@ func testSmallEnumValidity(t *testing.T) { t.Fatalf("known kind %d must be valid", kind) } } - convergences := []Convergence{Converged, ActionPending, DecisionRequired, Rejected} + convergences := []Convergence{ConvergenceConverged, ConvergencePending, ConvergenceDecisionRequired, ConvergenceRejected} for _, convergence := range convergences { if !convergence.Valid() { t.Fatalf("known convergence %d must be valid", convergence) diff --git a/internal/repository/compiler.go b/internal/repository/compiler.go index dfd9cf7..705ad26 100644 --- a/internal/repository/compiler.go +++ b/internal/repository/compiler.go @@ -24,7 +24,7 @@ type CompileInput struct { Selected []string } -type compiled struct { +type compiledRepository struct { groups []string files []deployment.ManagedFile aliases []deployment.Alias @@ -46,27 +46,27 @@ func Compile(input CompileInput) (deployment.Plan, error) { return finalize(input, records) } -func compileRepository(input CompileInput) (compiled, error) { +func compileRepository(input CompileInput) (compiledRepository, error) { base, err := scanAndSelect(input) if err != nil { - return compiled{}, err + return compiledRepository{}, err } files, err := ResolvePlatform(input.RepositoryRoot, base, input.Platform) if err != nil { - return compiled{}, err + return compiledRepository{}, err } - aliases, err := activateRoutes(input, compiled{groups: base.Groups, files: files}) + aliases, err := activateRoutes(input, compiledRepository{groups: base.Groups, files: files}) if err != nil { - return compiled{}, err + return compiledRepository{}, err } hookRecords, err := discoverHooks(input, base.Groups) if err != nil { - return compiled{}, err + return compiledRepository{}, err } if err := validateDestinations(files, aliases); err != nil { - return compiled{}, err + return compiledRepository{}, err } - return compiled{groups: base.Groups, files: files, aliases: aliases, hooks: hookRecords}, nil + return compiledRepository{groups: base.Groups, files: files, aliases: aliases, hooks: hookRecords}, nil } func scanAndSelect(input CompileInput) (ScanResult, error) { @@ -80,18 +80,18 @@ func scanAndSelect(input CompileInput) (ScanResult, error) { return base, nil } -func finalize(input CompileInput, records compiled) (deployment.Plan, error) { +func finalize(input CompileInput, compiled compiledRepository) (deployment.Plan, error) { selected := input.Selected - groups := records.groups + groups := compiled.groups if len(input.Selected) > 0 { groups = append([]string(nil), selected...) sort.Strings(groups) } - keptFiles := selectRecords(records.files, selected, + keptFiles := selectRecords(compiled.files, selected, func(file deployment.ManagedFile) bool { return scopeKept(file.Scope.Group, selected) }) - keptAliases := selectRecords(records.aliases, selected, + keptAliases := selectRecords(compiled.aliases, selected, func(alias deployment.Alias) bool { return scopeKept(alias.Scope.Group, selected) }) - keptHooks := selectRecords(records.hooks, selected, + keptHooks := selectRecords(compiled.hooks, selected, func(hook deployment.Hook) bool { return hookKept(hook.Scope, selected) }) deployment.SortFiles(keptFiles) deployment.SortAliases(keptAliases) @@ -141,10 +141,10 @@ func validateSelection(groups, selected []string) error { return nil } -func activateRoutes(input CompileInput, records compiled) ([]deployment.Alias, error) { +func activateRoutes(input CompileInput, compiled compiledRepository) ([]deployment.Alias, error) { var activated []deployment.Alias - for _, scope := range scopesOf(records.groups) { - scopeAliases, err := activateScope(input, scope, records.files) + for _, scope := range scopesOf(compiled.groups) { + scopeAliases, err := activateScope(input, scope, compiled.files) if err != nil { return nil, err } diff --git a/internal/repository/scan.go b/internal/repository/scan.go index 3fc3d51..e7bc6fe 100644 --- a/internal/repository/scan.go +++ b/internal/repository/scan.go @@ -64,90 +64,90 @@ type scopeScanner struct { hooks []HookCandidate } -func (s *scopeScanner) scanScopeRoot() error { - entries, err := os.ReadDir(filepath.Join(s.repoRoot, s.scopeRoot)) +func (scanner *scopeScanner) scanScopeRoot() error { + entries, err := os.ReadDir(filepath.Join(scanner.repoRoot, scanner.scopeRoot)) if err != nil { return err } for _, entry := range entries { - if err := s.scanEntry(entry); err != nil { + if err := scanner.scanEntry(entry); err != nil { return err } } return nil } -func (s *scopeScanner) scanEntry(entry os.DirEntry) error { +func (scanner *scopeScanner) scanEntry(entry os.DirEntry) error { control := ClassifyRoot(entry.Name()) switch { // Root dot-directories do not promote to groups: pathsafe.GroupName // rejects leading-"." names, so they must route to ordinary scanning // (e.g. an ungrouped HOME tree rooted at a dot-directory) rather than // being treated as a group. - case s.rootTree && control == ControlNone && entry.IsDir() && !strings.HasPrefix(entry.Name(), "."): - return s.beginGroup(entry) + case scanner.rootTree && control == ControlNone && entry.IsDir() && !strings.HasPrefix(entry.Name(), "."): + return scanner.beginGroup(entry) case control == ControlNone: - return s.scanOrdinary(entry) + return scanner.scanOrdinary(entry) case control == ControlSecrets: - return s.scanSecrets(entry) + return scanner.scanSecrets(entry) case control == ControlHooks: - return s.scanHooks(entry) + return scanner.scanHooks(entry) case control == ControlMetadata: - if s.rootTree { + if scanner.rootTree { return nil } - return s.scanOrdinary(entry) + return scanner.scanOrdinary(entry) default: return nil } } -func (s *scopeScanner) beginGroup(entry os.DirEntry) error { +func (scanner *scopeScanner) beginGroup(entry os.DirEntry) error { name := entry.Name() if err := pathsafe.GroupName(name); err != nil { return err } - s.groups = append(s.groups, name) - previousScopeRoot, previousScope := s.scopeRoot, s.scope - s.scopeRoot = filepath.Join(s.scopeRoot, name) - s.scope = deployment.NewScope(name) - s.rootTree = false - err := s.scanScopeRoot() - s.scopeRoot, s.scope, s.rootTree = previousScopeRoot, previousScope, true + scanner.groups = append(scanner.groups, name) + previousScopeRoot, previousScope := scanner.scopeRoot, scanner.scope + scanner.scopeRoot = filepath.Join(scanner.scopeRoot, name) + scanner.scope = deployment.NewScope(name) + scanner.rootTree = false + err := scanner.scanScopeRoot() + scanner.scopeRoot, scanner.scope, scanner.rootTree = previousScopeRoot, previousScope, true return err } -func (s *scopeScanner) scanOrdinary(entry os.DirEntry) error { +func (scanner *scopeScanner) scanOrdinary(entry os.DirEntry) error { if entry.IsDir() { - return s.walkTree(entry.Name(), deployment.FileOrdinary) + return scanner.walkTree(entry.Name(), deployment.FileOrdinary) } if !entry.Type().IsRegular() { - return s.nonRegular(entry.Name()) + return scanner.newNonRegularSourceError(entry.Name()) } - return s.addFileAt(entry.Name(), entry, deployment.FileOrdinary) + return scanner.addFileAt(entry.Name(), entry, deployment.FileOrdinary) } -func (s *scopeScanner) scanSecrets(entry os.DirEntry) error { +func (scanner *scopeScanner) scanSecrets(entry os.DirEntry) error { if !entry.IsDir() { return fmt.Errorf("repository: control %q is not a directory", entry.Name()) } - return s.walkTree(entry.Name(), deployment.FileSecret) + return scanner.walkTree(entry.Name(), deployment.FileSecret) } -func (s *scopeScanner) scanHooks(entry os.DirEntry) error { +func (scanner *scopeScanner) scanHooks(entry os.DirEntry) error { if !entry.IsDir() { return fmt.Errorf("repository: control %q is not a directory", entry.Name()) } for _, phase := range []deployment.HookPhase{deployment.HookBefore, deployment.HookAfter} { - if err := s.scanHookPhase(entry.Name(), phase); err != nil { + if err := scanner.scanHookPhase(entry.Name(), phase); err != nil { return err } } return nil } -func (s *scopeScanner) scanHookPhase(hooksDir string, phase deployment.HookPhase) error { - path := filepath.Join(s.repoRoot, s.scopeRoot, hooksDir, string(phase)) +func (scanner *scopeScanner) scanHookPhase(hooksDir string, phase deployment.HookPhase) error { + path := filepath.Join(scanner.repoRoot, scanner.scopeRoot, hooksDir, string(phase)) info, err := os.Lstat(path) if err != nil { if errors.Is(err, fs.ErrNotExist) { @@ -164,66 +164,66 @@ func (s *scopeScanner) scanHookPhase(hooksDir string, phase deployment.HookPhase } for _, entry := range entries { if entry.Type().IsRegular() { - s.hooks = append(s.hooks, s.hookCandidate(hooksDir, phase, entry.Name())) + scanner.hooks = append(scanner.hooks, scanner.hookCandidate(hooksDir, phase, entry.Name())) } } return nil } -func (s *scopeScanner) walkTree(relative string, kind deployment.FileKind) error { - entries, err := os.ReadDir(filepath.Join(s.repoRoot, s.scopeRoot, relative)) +func (scanner *scopeScanner) walkTree(relative string, kind deployment.FileKind) error { + entries, err := os.ReadDir(filepath.Join(scanner.repoRoot, scanner.scopeRoot, relative)) if err != nil { return err } for _, entry := range entries { - if err := s.walkEntry(relative, entry, kind); err != nil { + if err := scanner.walkEntry(relative, entry, kind); err != nil { return err } } return nil } -func (s *scopeScanner) walkEntry(parent string, entry os.DirEntry, kind deployment.FileKind) error { +func (scanner *scopeScanner) walkEntry(parent string, entry os.DirEntry, kind deployment.FileKind) error { path := filepath.Join(parent, entry.Name()) if entry.IsDir() { - return s.walkTree(path, kind) + return scanner.walkTree(path, kind) } if !entry.Type().IsRegular() { - return s.nonRegular(path) + return scanner.newNonRegularSourceError(path) } - return s.addFileAt(path, entry, kind) + return scanner.addFileAt(path, entry, kind) } -func (s *scopeScanner) addFileAt(relative string, entry os.DirEntry, kind deployment.FileKind) error { +func (scanner *scopeScanner) addFileAt(relative string, entry os.DirEntry, kind deployment.FileKind) error { info, err := entry.Info() if err != nil { return err } - path := filepath.Join(s.scopeRoot, relative) - s.files = append(s.files, Candidate{ - Scope: s.scope, + path := filepath.Join(scanner.scopeRoot, relative) + scanner.files = append(scanner.files, Candidate{ + Scope: scanner.scope, Layer: deployment.LayerBase, Kind: kind, SourceRepoPath: path, - SourceAbsPath: filepath.Join(s.repoRoot, path), + SourceAbsPath: filepath.Join(scanner.repoRoot, path), ExecutableBits: info.Mode() & deployment.ExecutableBitMask, }) return nil } -func (s *scopeScanner) hookCandidate(hooks string, phase deployment.HookPhase, name string) HookCandidate { - path := filepath.Join(s.scopeRoot, hooks, string(phase), name) +func (scanner *scopeScanner) hookCandidate(hooks string, phase deployment.HookPhase, name string) HookCandidate { + path := filepath.Join(scanner.scopeRoot, hooks, string(phase), name) return HookCandidate{ - Scope: s.scope, + Scope: scanner.scope, Phase: phase, Name: name, - AbsolutePath: filepath.Join(s.repoRoot, path), + AbsolutePath: filepath.Join(scanner.repoRoot, path), RepositoryPath: path, } } -func (s *scopeScanner) nonRegular(relative string) error { - return fmt.Errorf("repository: non-regular source entry %q", filepath.Join(s.scopeRoot, relative)) +func (scanner *scopeScanner) newNonRegularSourceError(relative string) error { + return fmt.Errorf("repository: non-regular source entry %q", filepath.Join(scanner.scopeRoot, relative)) } func checkGroupCollisions(groups []string) error { -- 2.51.2