Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions .scafld/receipts/verify-review-coverage-parity.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
{
"body": {
"schema_version": 1,
"task_id": "verify-review-coverage-parity",
"session_id": "verify-review-coverage-parity",
"verdict": "pass",
"snapshot_mode": "base_delta",
"base_ref": "3be719f89bf90c50e2242d108faf0ff3efb37e7f",
"base_commit": "3be719f89bf90c50e2242d108faf0ff3efb37e7f",
"head_commit": "3be719f89bf90c50e2242d108faf0ff3efb37e7f",
"scope": [
"internal/adapters/cli/finalize/run.go",
"internal/adapters/cli/finalize/run_test.go",
"internal/adapters/cli/verify/verify.go",
"internal/adapters/cli/verify/verify_test.go",
"internal/adapters/providers/evidence_sandbox.go",
"internal/core/reviewevidence/evidence.go"
],
"tree_sha": "8cf3965171bafd13311d1f0dd4e725ff0df7a197",
"file_digests": {
"internal/adapters/cli/finalize/run.go": "593e4edf77f171f86ef21d008b99b53b8ad5e7afbc8d7edbe089b1fafeea3858",
"internal/adapters/cli/finalize/run_test.go": "22423347234cc7b1e0a913b70eda40470d65d99ecfe5313cdf1830eb651323af",
"internal/adapters/cli/verify/verify.go": "211cb78da6c0657aef62847deb74875cb8980b56755f4e528862a1454a8dfff5",
"internal/adapters/cli/verify/verify_test.go": "cdb272c19fe5156afd5bb44ce4be00a5b331c95fff6422f9b44cf90a017b9c15",
"internal/adapters/providers/evidence_sandbox.go": "f4bff75e90928c0832ed9827ba83e91194a4817b0fa551a7d447f41b930b19f4",
"internal/core/reviewevidence/evidence.go": "6b6e9c31b49b2560c85ef84ba47edae2143a5604ad1ff698acb20f8fb90170ba"
},
"ignored_unreviewed": [],
"reviewed_context_provenance": [
{
"kind": "evidence_file",
"path": "internal/adapters/cli/finalize/run.go",
"sha256": "593e4edf77f171f86ef21d008b99b53b8ad5e7afbc8d7edbe089b1fafeea3858"
},
{
"kind": "evidence_file",
"path": "internal/adapters/cli/finalize/run_test.go",
"sha256": "22423347234cc7b1e0a913b70eda40470d65d99ecfe5313cdf1830eb651323af"
},
{
"kind": "evidence_file",
"path": "internal/adapters/cli/verify/verify.go",
"sha256": "211cb78da6c0657aef62847deb74875cb8980b56755f4e528862a1454a8dfff5"
},
{
"kind": "evidence_file",
"path": "internal/adapters/cli/verify/verify_test.go",
"sha256": "cdb272c19fe5156afd5bb44ce4be00a5b331c95fff6422f9b44cf90a017b9c15"
},
{
"kind": "evidence_file",
"path": "internal/adapters/providers/evidence_sandbox.go",
"sha256": "f4bff75e90928c0832ed9827ba83e91194a4817b0fa551a7d447f41b930b19f4"
},
{
"kind": "evidence_file",
"path": "internal/core/reviewevidence/evidence.go",
"sha256": "6b6e9c31b49b2560c85ef84ba47edae2143a5604ad1ff698acb20f8fb90170ba"
}
],
"reviewer": {
"provider": "claude",
"model": "claude-opus-5[1m]"
},
"host_under_review": {
"agent": "codex",
"session_id": "verify-review-coverage-parity"
},
"independence": {
"level": "cross_vendor",
"distinct": true,
"reason": "cross_vendor: reviewer \"claude\" and host \"codex\" are different model vendors; this is multi-model review that reduces correlated blind spots but remains single-party local tooling"
},
"spec_fingerprint": "7773fdbfc8fc7725a520d0a316fff287e9811f6699735b6c8ef9bacb534f91f9",
"acceptance_declared": true,
"acceptance": [
{
"id": "ac1",
"command": "make check",
"expected_kind": "exit_code_zero",
"status": "pass",
"reason": "exit code was 0",
"output_sha256": "492aa450ed14c030868b30f86d014d5396f32b039d18ca204533f7817d94b050",
"diagnostic": "/Users/kam/dev/0state/scafld/.scafld/runs/verify-review-coverage-parity/artifacts/commands/ac1-1788839146855016000.txt"
}
],
"open_blockers": [],
"mutation_guard": {
"status": "clean",
"scope": [
"internal/adapters/cli/finalize/run.go",
"internal/adapters/cli/finalize/run_test.go",
"internal/adapters/cli/verify/verify.go",
"internal/adapters/cli/verify/verify_test.go",
"internal/adapters/providers/evidence_sandbox.go",
"internal/core/reviewevidence/evidence.go"
]
},
"ledger_head": "7a619f6a55637676262ecb7c34ba0698f789fed416e3a82d5f4d67c44e4d7ac2",
"minted_at": "2026-09-08T03:45:44Z"
},
"signature": {
"alg": "ed25519",
"key_id": "ed25519:RGGe6gUQpsB6KGu0VcnMxGUL5IA1I1VDSKi3ecCzYOU",
"sig": "7eNBj6ErdMGxGy5/CyV1IEVzL0eS565zahnZ1EJBQf5GeXONgwhUdyp2f272T2qA3G8AOVU8GqNqoPsXWf+LDQ=="
}
}
12 changes: 2 additions & 10 deletions internal/adapters/cli/finalize/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,14 +319,14 @@ func snapshotReviewCoverage(snap appfinalize.Snapshot) ([]receipt.Provenance, []
provenance := make([]receipt.Provenance, 0, len(snap.Files)+len(snap.Deleted))
ignored := append([]string(nil), snap.IgnoredUnreviewed...)
for _, file := range snap.Files {
if file.Status == "gitlink" || blocklistedEvidence(file.Path) {
if file.Status == "gitlink" || reviewevidence.PinnedEvidenceExclusionReason(file.Path) != "" {
ignored = append(ignored, file.Path)
continue
}
provenance = append(provenance, receipt.Provenance{Kind: "evidence_file", Path: file.Path, SHA256: file.SHA256})
}
for _, path := range snap.Deleted {
if blocklistedEvidence(path) {
if reviewevidence.PinnedEvidenceExclusionReason(path) != "" {
ignored = append(ignored, path)
continue
}
Expand Down Expand Up @@ -655,14 +655,6 @@ func (a gateAcceptance) Evaluate(ctx context.Context, in appacceptance.EvaluateI
return appacceptance.Evaluate(ctx, a.runner, in), nil
}

func blocklistedEvidence(path string) bool {
switch filepath.Base(filepath.FromSlash(path)) {
case "CLAUDE.md", "AGENTS.md", "GEMINI.md":
return true
}
return strings.TrimSpace(path) == ".scafld/config.yaml"
}

func gateCriteria(model spec.Model, ledger session.Session) []appacceptance.Criterion {
criteria := model.AllCriteria()
out := make([]appacceptance.Criterion, 0, len(criteria))
Expand Down
109 changes: 31 additions & 78 deletions internal/adapters/cli/finalize/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"testing"
"time"

verifyadapter "github.com/nilstate/scafld/v2/internal/adapters/cli/verify"
"github.com/nilstate/scafld/v2/internal/adapters/git"
"github.com/nilstate/scafld/v2/internal/adapters/jsonstore"
"github.com/nilstate/scafld/v2/internal/adapters/markdown"
Expand Down Expand Up @@ -434,7 +435,12 @@ func TestCommittedBaseDeltaSealVerifiesAfterCommit(t *testing.T) {
t.Fatalf("default base_ref = %q, want parent HEAD %q", baseRef, parent)
}

out, trusted := mintTestReceipt(t, root, baseRef)
scope := []string{"file.txt", "AGENTS.md", "nested/CLAUDE.md", "GEMINI.md", ".scafld/config.yaml", "docs/AGENTS.md"}
if err := os.Remove(filepath.Join(root, "docs/AGENTS.md")); err != nil {
t.Fatal(err)
}
writeFinalizeFile(t, root, "AGENTS.md", "updated instructions\n")
out, trusted := mintTestReceipt(t, root, baseRef, scope)
if out.Receipt == nil {
t.Fatal("finalize did not mint a receipt")
}
Expand All @@ -451,13 +457,7 @@ func TestCommittedBaseDeltaSealVerifiesAfterCommit(t *testing.T) {
finalizeRunGit(t, root, "add", "-A")
finalizeRunGit(t, root, "commit", "-m", "seal")

ports := appverify.Ports{
Snapshotter: finalizeVerifySnapshotter{git: git.Adapter{Root: root}},
AcceptanceRunner: finalizeVerifyAcceptance{runner: process.Runner{}, root: root},
AncestryChecker: git.Adapter{Root: root},
SignatureVerifier: finalizeVerifySignature{},
}
res, err := appverify.Run(ctx, *out.Receipt, trusted, appverify.Policy{TargetCommit: parent}, ports)
res, err := verifyMintedReceipt(t, root, *out.Receipt, trusted, parent)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -520,13 +520,7 @@ func TestFinalizeIgnoresOutOfScopeMutationDuringAcceptance(t *testing.T) {
finalizeRunGit(t, root, "add", "-A")
finalizeRunGit(t, root, "commit", "-m", "task plus ambient")
head := strings.TrimSpace(finalizeGitOutput(t, root, "rev-parse", "HEAD"))
ports := appverify.Ports{
Snapshotter: finalizeVerifySnapshotter{git: git.Adapter{Root: root}},
AcceptanceRunner: finalizeVerifyAcceptance{runner: process.Runner{}, root: root},
AncestryChecker: git.Adapter{Root: root},
SignatureVerifier: finalizeVerifySignature{},
}
res, err := appverify.Run(ctx, *out.Receipt, trusted, appverify.Policy{TargetCommit: head}, ports)
res, err := verifyMintedReceipt(t, root, *out.Receipt, trusted, head)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -560,7 +554,7 @@ func buildEvidence(ctx context.Context, g git.Adapter, treeSHA string, scope []s
var ignored []string
reviewable := make([]appfinalize.FileFact, 0, len(facts))
for _, fact := range facts {
if fact.Status == "gitlink" || blocklistedEvidence(fact.Path) {
if fact.Status == "gitlink" || reviewevidence.PinnedEvidenceExclusionReason(fact.Path) != "" {
ignored = append(ignored, fact.Path)
continue
}
Expand All @@ -585,7 +579,7 @@ func buildEvidence(ctx context.Context, g git.Adapter, treeSHA string, scope []s
provenance = append(provenance, receipt.Provenance{Kind: "evidence_file", Path: fact.Path, SHA256: fact.SHA256, Bytes: len(data)})
}
for _, path := range deleted {
if blocklistedEvidence(path) {
if reviewevidence.PinnedEvidenceExclusionReason(path) != "" {
ignored = append(ignored, path)
continue
}
Expand Down Expand Up @@ -947,77 +941,32 @@ func passingReviewEvidence(t *testing.T, root string, scope []string, baseRef st
}
}

type finalizeVerifySnapshotter struct{ git git.Adapter }

func (s finalizeVerifySnapshotter) Snapshot(ctx context.Context, in appverify.SnapshotInput) (appverify.Snapshot, error) {
snap, err := s.git.Snapshot(ctx, git.SnapshotInput{Scope: in.Scope, BaseRef: in.BaseRef})
if err != nil {
return appverify.Snapshot{}, err
}
digests := make(map[string]string, len(snap.FileDigests))
for _, d := range snap.FileDigests {
digests[d.Path] = d.SHA256
}
ignored := make([]string, 0, len(snap.IgnoredUnreviewed))
for _, item := range snap.IgnoredUnreviewed {
ignored = append(ignored, item.Path)
}
return appverify.Snapshot{TreeSHA: snap.TreeSHA, BaseCommit: snap.BaseCommit, FileDigests: digests, Ignored: ignored}, nil
}

type finalizeVerifyAcceptance struct {
runner appacceptance.Runner
root string
}

type finalizeAcceptanceFunc func(context.Context, appacceptance.EvaluateInput) (appacceptance.EvaluateOutput, error)

func (f finalizeAcceptanceFunc) Evaluate(ctx context.Context, in appacceptance.EvaluateInput) (appacceptance.EvaluateOutput, error) {
return f(ctx, in)
}

func (a finalizeVerifyAcceptance) RunAcceptance(ctx context.Context, criteria []receipt.Acceptance) ([]appverify.AcceptanceResult, error) {
out := make([]appverify.AcceptanceResult, 0, len(criteria))
for _, c := range criteria {
evaluated := appacceptance.Evaluate(ctx, a.runner, appacceptance.EvaluateInput{
Criteria: []appacceptance.Criterion{{ID: c.ID, Command: c.Command, ExpectedKind: c.ExpectedKind}},
WorkDir: a.root,
})
if len(evaluated.Results) == 0 {
continue
}
result := evaluated.Results[0]
out = append(out, appverify.AcceptanceResult{ID: result.ID, Status: result.Status, ExitCode: result.ExitCode})
}
return out, nil
}

type finalizeVerifySignature struct{}

func (finalizeVerifySignature) Verify(envelope receipt.Envelope, trusted trust.TrustedKeys) error {
key, err := trusted.ActiveKey(envelope.Signature.KeyID)
if err != nil {
return err
}
pub, err := key.PublicKeyBytes()
if err != nil {
return err
}
sig, err := base64.StdEncoding.DecodeString(envelope.Signature.Sig)
func verifyMintedReceipt(t *testing.T, root string, envelope receipt.Envelope, trusted trust.TrustedKeys, target string) (appverify.Result, error) {
t.Helper()
data, err := json.Marshal(envelope)
if err != nil {
return err
t.Fatal(err)
}
canonical, err := receipt.CanonicalBody(envelope.Body)
keys, err := trust.MarshalTrustedKeys(trusted)
if err != nil {
return err
}
if !ed25519.Verify(ed25519.PublicKey(pub), canonical, sig) {
return errors.New("invalid signature")
t.Fatal(err)
}
return nil
receiptPath := filepath.Join(root, ".scafld/receipts/verify-test.json")
keysPath := filepath.Join(root, ".scafld/trusted-keys.json")
writeFinalizeFile(t, root, ".scafld/receipts/verify-test.json", string(data))
writeFinalizeFile(t, root, ".scafld/trusted-keys.json", string(keys))
return verifyadapter.Run(context.Background(), verifyadapter.Options{
Root: root, ReceiptPath: receiptPath, TrustedKeys: keysPath, Target: target,
})
}

func mintTestReceipt(t *testing.T, root string, baseRef string) (appfinalize.Output, trust.TrustedKeys) {
func mintTestReceipt(t *testing.T, root string, baseRef string, scope []string) (appfinalize.Output, trust.TrustedKeys) {
t.Helper()
keyPath, trusted := newFinalizeSigningKey(t)
out, err := appfinalize.Run(context.Background(),
Expand All @@ -1027,10 +976,10 @@ func mintTestReceipt(t *testing.T, root string, baseRef string) (appfinalize.Out
appfinalize.Input{
TaskID: "base-delta-seal",
SessionID: "base-delta-seal",
Scope: []string{"file.txt"},
Scope: scope,
BaseRef: baseRef,
SpecFingerprint: "spec",
Review: passingReviewEvidence(t, root, []string{"file.txt"}, baseRef),
Review: passingReviewEvidence(t, root, scope, baseRef),
HostUnderReview: receipt.HostUnderReview{Agent: "unknown"},
Criteria: []appacceptance.Criterion{{ID: "ac1", Command: "true", ExpectedKind: "exit_code_zero"}},
WorkDir: root,
Expand Down Expand Up @@ -1075,6 +1024,10 @@ func initFinalizeRepo(t *testing.T) string {
finalizeRunGit(t, root, "config", "user.name", "scafld")
finalizeRunGit(t, root, "config", "user.email", "scafld@example.invalid")
writeFinalizeFile(t, root, "file.txt", "before\n")
for _, name := range []string{"AGENTS.md", "nested/CLAUDE.md", "GEMINI.md", "docs/AGENTS.md"} {
writeFinalizeFile(t, root, name, "fixture instructions\n")
}
writeFinalizeFile(t, root, ".scafld/config.yaml", "version: \"1.0\"\n")
finalizeRunGit(t, root, "add", "-A")
finalizeRunGit(t, root, "commit", "-m", "base")
return root
Expand Down
8 changes: 7 additions & 1 deletion internal/adapters/cli/verify/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
appverify "github.com/nilstate/scafld/v2/internal/app/verify"
"github.com/nilstate/scafld/v2/internal/core/execution"
"github.com/nilstate/scafld/v2/internal/core/receipt"
"github.com/nilstate/scafld/v2/internal/core/reviewevidence"
"github.com/nilstate/scafld/v2/internal/core/runartifact"
"github.com/nilstate/scafld/v2/internal/core/trust"
"github.com/nilstate/scafld/v2/internal/platform/processguard"
Expand Down Expand Up @@ -200,7 +201,12 @@ func snapshotForVerify(snapshot git.Snapshot) appverify.Snapshot {
}
for _, item := range snapshot.FileDigests {
digests[item.Path] = item.SHA256
if item.Status == "gitlink" {
if item.Status == "gitlink" || reviewevidence.PinnedEvidenceExclusionReason(item.Path) != "" {
ignoredSet[item.Path] = struct{}{}
}
}
for _, item := range snapshot.DeletedPaths {
if reviewevidence.PinnedEvidenceExclusionReason(item.Path) != "" {
ignoredSet[item.Path] = struct{}{}
}
}
Expand Down
22 changes: 18 additions & 4 deletions internal/adapters/cli/verify/verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestParseTarget(t *testing.T) {
}
}

func TestSnapshotForVerifyMarksGitlinksIgnoredForFinalizeParity(t *testing.T) {
func TestSnapshotForVerifyPreservesDigestsAndFinalizeExclusionParity(t *testing.T) {
t.Parallel()

snapshot := snapshotForVerify(gitadapter.Snapshot{
Expand All @@ -42,15 +42,29 @@ func TestSnapshotForVerifyMarksGitlinksIgnoredForFinalizeParity(t *testing.T) {
FileDigests: []gitadapter.FileDigest{
{Path: "api", Status: "gitlink", SHA256: "api-sha"},
{Path: "README.md", Status: "modified", SHA256: "readme-sha"},
{Path: "AGENTS.md", Status: "modified", SHA256: "agents-sha"},
{Path: "nested/CLAUDE.md", Status: "modified", SHA256: "claude-sha"},
{Path: "GEMINI.md", Status: "added", SHA256: "gemini-sha"},
{Path: ".scafld/config.yaml", Status: "modified", SHA256: "config-sha"},
},
DeletedPaths: []gitadapter.DeletedPath{{Path: "docs/AGENTS.md"}, {Path: "docs/ordinary.md"}},
IgnoredUnreviewed: []gitadapter.IgnoredPath{{Path: "secret.env", Reason: "ignored"}},
})

if got, want := snapshot.Ignored, []string{"api", "secret.env"}; !equalStrings(got, want) {
if got, want := snapshot.Ignored, []string{".scafld/config.yaml", "AGENTS.md", "GEMINI.md", "api", "docs/AGENTS.md", "nested/CLAUDE.md", "secret.env"}; !equalStrings(got, want) {
t.Fatalf("ignored = %v, want %v", got, want)
}
if snapshot.FileDigests["api"] != "api-sha" || snapshot.FileDigests["README.md"] != "readme-sha" {
t.Fatalf("file digests = %v", snapshot.FileDigests)
wantDigests := map[string]string{
"api": "api-sha", "README.md": "readme-sha", "AGENTS.md": "agents-sha",
"nested/CLAUDE.md": "claude-sha", "GEMINI.md": "gemini-sha", ".scafld/config.yaml": "config-sha",
}
if len(snapshot.FileDigests) != len(wantDigests) {
t.Fatalf("file digests = %v, want all present files retained", snapshot.FileDigests)
}
for path, want := range wantDigests {
if snapshot.FileDigests[path] != want {
t.Fatalf("file digest %s = %q, want %q", path, snapshot.FileDigests[path], want)
}
}
}

Expand Down
Loading