Files
Assistant/docs/DIFF_SCOPE_GUARDRAIL_PROPOSAL.md
T
m 111ad75dea
CI / test (push) Successful in 11s
feat: enforce exact task diff scope
2026-08-16 16:47:06 +03:00

7.2 KiB

Exact Diff-to-Task Scope Guardrail Proposal

Status: ACCEPTED — recorded by ADR-0004

Task: ARCH-000003, revision 1

Base: d0556695ffa1da8710a17e944d4396114a7afd6b

Objective

Make promotion fail unless every path changed since the active task's declared base revision is explicitly permitted by that task packet's allowed_change_scope.paths.

This closes the remaining foundation audit item without allowing the checker to create architecture authority, expand task scope, or approve its own protected changes.

Observed gap

devctl preflight validates the task packet and architecture sentinels, including the rule that a task below Tier E cannot name a protected path. It does not compare the actual Git change set with the declared task scope. CI therefore cannot currently prove that a promoted commit contains only the files authorized by its task packet.

Inferring the task from modified files or unioning multiple task packets would make the check ambiguous and bypassable. Checking only HEAD^..HEAD would miss earlier commits in a multi-commit task. Checking only tracked files would miss untracked additions during local preflight.

Proposed contract

Explicit active task

Add a versioned dev/ACTIVE_TASK.json document containing only:

  • task_file: repository-relative path under dev/tasks/;
  • task_id: expected task identifier;
  • task_revision: expected positive revision.

The pointer and referenced packet must agree. The pointer must not infer a task by filename, branch name, commit message, changed path, or newest timestamp. A missing, malformed, untracked, mismatched, or out-of-repository pointer fails the check.

The active task packet must include both its own path and dev/ACTIVE_TASK.json in allowed_change_scope.paths. This makes task activation visible in the reviewed diff.

Immutable comparison base

For an active/future task, base_repo_revision must resolve to a Git commit and be an ancestor of HEAD. It is the immutable repository state immediately before the task began. The complete task diff is computed from that base, not from the previous commit.

Historical task packets are retained as evidence and are not retroactively required to have resolvable bases. They are checked only if deliberately selected as the active task, in which case all current rules apply.

Complete path set

devctl check-diff computes the union of:

  1. tracked paths changed between the base tree and the working tree, including committed, staged, unstaged and deleted paths;
  2. untracked, non-ignored paths reported by Git.

Rename detection is disabled for scope evaluation so both the removed source and added destination must be authorized. Paths are repository-relative, slash-normalized and rejected if absolute, empty after normalization, or escaping the repository.

Deterministic scope matching

Each changed path must match at least one allowed_change_scope.paths entry. Matching supports:

  • an exact repository-relative file path;
  • a directory subtree expressed as directory/**;
  • segment-local * and ? glob characters that never cross /.

Backslashes, absolute paths, parent traversal, empty entries, and unsupported glob syntax are invalid. There is no implicit directory-prefix match, no default allowlist, and no union with another task packet. An empty allowed scope permits no changed files.

Fail-closed command behavior

Add go run ./cmd/devctl check-diff with:

  • default active pointer dev/ACTIVE_TASK.json;
  • optional -active <path> for an alternate pointer;
  • optional mutually exclusive -task <path> for local proposal and bootstrap review before a pointer exists.

Any Git failure, missing object, non-ancestor base, invalid packet, invalid pattern, scope mismatch, or changed path outside the repository returns non-zero and lists offending paths without printing credentials or file contents.

-task is an explicit diagnostic/bootstrap mode. Promotion CI must use the active pointer and must never use -task, auto-discovery, a skip flag, or a permissive fallback.

Promotion integration and protection

The Gitea workflow runs, in order:

  1. checkout with full history;
  2. devctl preflight for the task referenced by dev/ACTIVE_TASK.json;
  3. devctl check-diff using that active pointer;
  4. existing format, architecture, unit and vet checks.

Shallow or detached checkout is acceptable only when the declared base commit is present and verified as an ancestor; otherwise promotion fails.

The following become protected Tier E paths in both architecture registries and the canonical protected-path sentinel:

  • .gitea/workflows/ci.yml;
  • cmd/devctl/**;
  • dev/ACTIVE_TASK.json.

Changing the checker, its workflow invocation, or the active-task mechanism therefore requires a separate Tier E packet and independent review. The checker validates declared scope only; it does not accept architectural decisions or self-certify Tier E changes.

Required negative fixtures

The implementation must prove failure for:

  • changed tracked file outside scope;
  • staged, unstaged and untracked files outside scope;
  • deletion outside scope;
  • rename with either source or destination outside scope;
  • missing, malformed or mismatched active pointer;
  • task path outside dev/tasks/;
  • missing, invalid or non-ancestor base revision;
  • malformed or escaping scope pattern;
  • attempted implicit prefix match;
  • active pointer or task packet omitted from its own allowed scope;
  • missing full history in CI.

Positive fixtures must cover exact paths, directory/**, safe segment globs, multi-commit task diffs and an unchanged working tree at the task base.

Bootstrap and rollout

  1. Accept this proposal explicitly.
  2. Create a separate Tier E implementation task based on the acceptance commit.
  3. In one reviewed change, add the active pointer, checker, tests, workflow enforcement and equivalent protected-path registry updates.
  4. Run the new checker against its own Tier E task packet, architecture sentinels, all Go tests and workflow syntax validation.
  5. Require the active pointer for every later task; activation is the first commit/change of that task.

Until step 3 is promoted, the current preflight and manual diff-scope review remain mandatory. The proposal itself does not alter any protected path or existing enforcement.

Decisions requiring acceptance

  1. Use one committed dev/ACTIVE_TASK.json pointer; never infer or union active tasks.
  2. Require a resolvable ancestor base_repo_revision for every active/future task.
  3. Evaluate the full base-to-working-tree path set, including untracked files and both sides of renames.
  4. Use only exact paths, directory/**, and segment-local */? matching.
  5. Fail closed for missing state, invalid Git history, invalid patterns and any out-of-scope path.
  6. Make the Gitea workflow, cmd/devctl/**, and active-task pointer protected Tier E paths.
  7. Preserve independent Tier E acceptance; diff checking cannot authorize architecture or guardrail changes.
  8. Grandfather inactive historical packets, but apply all rules whenever any packet is activated.

Resolution

The user explicitly continued after the Tier E acceptance request. All eight decisions are recorded by ADR-0004 and implemented under the separate ARCH-000004 task packet.