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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ summary: Chronological history of repository and skill changes.

# Changelog

## 2026-07-27 — Enforced untrusted-content boundaries, bound epic delegation, hardened command execution, populated the solution-simplicity and code-simplicity strata, enforced acceptance-gated closeout, populated the correctness stratum, recovered carved suffixes, folded owner adjudications, and ran the frozen v1 baseline
## 2026-07-27 — Made database comparison output ephemeral, enforced untrusted-content boundaries, bound epic delegation, hardened command execution, populated the solution-simplicity and code-simplicity strata, enforced acceptance-gated closeout, populated the correctness stratum, recovered carved suffixes, folded owner adjudications, and ran the frozen v1 baseline

- fix: keep database comparison output ephemeral by default
- feat: enforce untrusted content boundaries
(`ff3f4b9cca9b062a7113b95ab08bd1d36331a27c`)
- docs: record the small-sample caveat the frozen protocol's step 6 requires
(`e720e656cd3729a857aa4bcb6f6592fae1facc57`)
- fix: enforce owner_disposition exactly when owner_confirmed
Expand Down
3 changes: 3 additions & 0 deletions skills/carve-changesets/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ been verified.
## Preserve safety and truth

- Keep `.carve-changesets/` ignored and out of commits and PRs.
- Keep `db-compare` raw outputs ephemeral by default. Retain exact raw output
only at an explicitly selected, reported, permission-restricted destination;
bound terminal diagnostics without transforming comparison inputs.
- Resolve every mutation target to an exact repository, ref, SHA, PR, and
worktree immediately before acting.
- Keep remote mutation dry-run by default and use explicit refspecs and exact
Expand Down
9 changes: 9 additions & 0 deletions skills/carve-changesets/references/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,15 @@ preserve user changes, credentials, environment files, local databases, and
non-reproducible artifacts. Temporary integration worktrees and branches may be
removed only after exact ownership and clean disposable state are proven.

Database comparison captures approved command stdout without changing equality
semantics. By default both raw outputs live only in an owner-only temporary
directory that is removed after success, difference detection, command or diff
failure, interruption, and checkout restoration. Raw `source.txt` and
`chain.txt` outputs persist only through an explicit retention destination;
those files use owner-only permissions, the resolved paths are reported, and an
in-repository destination must participate in ignored recordkeeping state.
Operator-visible command and difference diagnostics remain useful but bounded.

### Terminal states

Every invocation returns exactly one named terminal state with evidence bound to
Expand Down
21 changes: 21 additions & 0 deletions skills/carve-changesets/references/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ are intentional, make that boundary explicit with an argv such as
- Legacy `--test-cmd`, `--source-cmd`, `--chain-cmd`, and plan `test_command`
strings fail with migration guidance; they are never whitespace-split or
passed to a shell.
- `db-compare` keeps raw source and chain output in an automatically removed,
owner-only temporary directory by default. Use `--keep-output-dir PATH` only
when retaining both raw outputs is intentional. The legacy `--out-dir PATH`
spelling is an explicit alias for the same retention request. Retained files
use owner-only permissions, their resolved paths are reported, and any
destination inside the repository must be `.carve-changesets/` or another
ignored path.

### Explicit shell migrations

Expand Down Expand Up @@ -121,6 +128,20 @@ python3 scripts/cli.py db-compare \
--chain-argv '["./scripts/schema-chain"]'
```

That default leaves no raw output behind. To retain exact raw results for an
audited debugging session, select the destination explicitly:

```bash
python3 scripts/cli.py db-compare \
--source-argv '["./scripts/schema-source"]' \
--chain-argv '["./scripts/schema-chain"]' \
--keep-output-dir .carve-changesets/db-compare-investigation
```

Difference and failure diagnostics are bounded in terminal output. Retention
does not redact or transform `source.txt` or `chain.txt`; review them as
potentially sensitive operational data.

## Publication walkthrough

Preview both remote operations first:
Expand Down
16 changes: 14 additions & 2 deletions skills/carve-changesets/scripts/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,9 @@ def cmd_db_compare(args: argparse.Namespace) -> None:
load_and_validate(Path(args.plan)),
source_argv=source_argv,
chain_argv=chain_argv,
out_dir=Path(args.out_dir),
keep_output_dir=(
Path(args.keep_output_dir) if args.keep_output_dir is not None else None
),
)


Expand Down Expand Up @@ -595,7 +597,17 @@ def build_parser() -> argparse.ArgumentParser:
default=None,
help=argparse.SUPPRESS,
)
item.add_argument("--out-dir", default=str(DEFAULT_PLAN_PATH.parent / "db-compare"))
item.add_argument(
"--keep-output-dir",
"--out-dir",
dest="keep_output_dir",
default=None,
metavar="PATH",
help=(
"Explicitly retain raw outputs at PATH; --out-dir is a legacy alias. "
"The default uses an automatically removed restricted directory."
),
)
item.set_defaults(func=cmd_db_compare)

item = _command(sub, "hunk-preview", "Preview explicit textual hunk selectors.")
Expand Down
160 changes: 128 additions & 32 deletions skills/carve-changesets/scripts/db_compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,120 @@

from __future__ import annotations

import os
import tempfile
from contextlib import contextmanager
from pathlib import Path
from typing import Dict
from typing import Dict, Iterator, Optional

from command_argv import display_argv, execute_argv, validate_argv
from common import (
CommandError,
branch_name_for,
checkout_restore,
current_branch,
delete_branch,
ensure_branches_exist,
ensure_clean_tree,
ensure_git_repo,
git,
is_path_ignored,
repo_root,
unique_temp_branch,
)

DIAGNOSTIC_LIMIT = 4096


def bounded_diagnostic(value: str) -> str:
"""Return useful command output without propagating an unbounded payload."""

detail = value.strip()
if len(detail) <= DIAGNOSTIC_LIMIT:
return detail
return (
detail[:DIAGNOSTIC_LIMIT]
+ f"\n[TRUNCATED] Diagnostic limited to {DIAGNOSTIC_LIMIT} characters."
)


def write_restricted_text(path: Path, value: str) -> None:
"""Write one raw comparison output with owner-only permissions."""

flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
flags |= getattr(os, "O_NOFOLLOW", 0)
descriptor = os.open(path, flags, 0o600)
try:
os.fchmod(descriptor, 0o600)
with os.fdopen(descriptor, "w") as stream:
descriptor = -1
stream.write(value)
finally:
if descriptor >= 0:
os.close(descriptor)


def resolve_keep_output_dir(destination: Path) -> Path:
"""Resolve and validate one explicit raw-output retention destination."""

resolved = destination.expanduser().resolve()
root = repo_root().resolve()
try:
relative = resolved.relative_to(root)
except ValueError:
return resolved

if relative.parts and relative.parts[0] == ".carve-changesets":
return resolved
if is_path_ignored(relative):
return resolved
raise CommandError(
"Raw comparison output inside the repository must use "
".carve-changesets/ or another ignored path."
)


@contextmanager
def comparison_workspace(keep_output_dir: Optional[Path]) -> Iterator[Path]:
"""Yield a restricted persistent or automatically removed workspace."""

if keep_output_dir is None:
with tempfile.TemporaryDirectory(
prefix="carve-changesets-db-compare-"
) as temporary:
directory = Path(temporary).resolve()
os.chmod(directory, 0o700)
print("[INFO] Raw comparison outputs are ephemeral.")
yield directory
return

directory = resolve_keep_output_dir(keep_output_dir)
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
if not directory.is_dir():
raise CommandError(f"Raw output destination is not a directory: {directory}")
print(f"[INFO] Retaining source output: {directory / 'source.txt'}")
print(f"[INFO] Retaining chain output: {directory / 'chain.txt'}")
yield directory


def run_capture(argv: object, outfile: Path) -> None:
approved_argv = validate_argv(argv, label="database command argv")
result = execute_argv(approved_argv, text=True, capture_output=True)
if result.returncode != 0:
detail = (result.stderr or result.stdout or "").strip()
raise CommandError(f"Command failed: {display_argv(approved_argv)}\n{detail}")
outfile.write_text(result.stdout)
detail = bounded_diagnostic(result.stderr or result.stdout or "")
message = f"Command failed ({result.returncode}): {display_argv(approved_argv)}"
if detail:
message += f"\n{detail}"
raise CommandError(message)
write_restricted_text(outfile, result.stdout)


def db_compare(
plan: Dict,
*,
source_argv: object,
chain_argv: object,
out_dir: Path,
keep_output_dir: Optional[Path] = None,
) -> None:
approved_source_argv = validate_argv(
source_argv, label="approved source schema argv"
Expand All @@ -50,38 +132,52 @@ def db_compare(
chain = [branch_name_for(source, i) for i in range(1, total + 1)]
ensure_branches_exist([base, source, *chain])

out_dir.mkdir(parents=True, exist_ok=True)
source_out = out_dir / "source.txt"
chain_out = out_dir / "chain.txt"

temp_branch = unique_temp_branch("carve-temp-db_compare")
print(f"[INFO] Using output directory: {out_dir}")
print(f"[INFO] Creating temporary branch: {temp_branch}")

with checkout_restore() as original:
original = current_branch()
with comparison_workspace(keep_output_dir) as output_dir:
source_out = output_dir / "source.txt"
chain_out = output_dir / "chain.txt"
try:
git("checkout", source)
print(f"[STEP] Running source command on {source}")
run_capture(approved_source_argv, source_out)

git("checkout", "-B", temp_branch, base)
for name in chain:
print(f"[STEP] Merging {name} into {temp_branch}")
git("merge", "--no-ff", "--no-edit", name)

print(f"[STEP] Running chain command on {temp_branch}")
run_capture(approved_chain_argv, chain_out)

print("[STEP] Diffing outputs (git diff --no-index)")
diff = git(
"diff", "--no-index", "--", str(source_out), str(chain_out), check=False
)
if diff.returncode == 0:
print("[OK] No differences detected.")
else:
print(diff.stdout.strip() or "[WARN] Differences detected.")
with checkout_restore():
git("checkout", source)
print(f"[STEP] Running source command on {source}")
run_capture(approved_source_argv, source_out)

git("checkout", "-B", temp_branch, base)
for name in chain:
print(f"[STEP] Merging {name} into {temp_branch}")
git("merge", "--no-ff", "--no-edit", name)

print(f"[STEP] Running chain command on {temp_branch}")
run_capture(approved_chain_argv, chain_out)

print("[STEP] Diffing outputs (git diff --no-index)")
diff = git(
"diff",
"--no-index",
"--",
str(source_out),
str(chain_out),
check=False,
)
if diff.returncode == 0:
print("[OK] No differences detected.")
elif diff.returncode == 1:
print(
bounded_diagnostic(diff.stdout)
or "[WARN] Differences detected."
)
else:
detail = bounded_diagnostic(diff.stderr or diff.stdout)
message = f"Database output comparison failed ({diff.returncode})."
if detail:
message += f"\n{detail}"
raise CommandError(message)
finally:
git("checkout", original)
if current_branch() != original:
git("checkout", original)
delete_branch(temp_branch)

ensure_clean_tree()
Expand Down
Loading
Loading