From 859b99c4e6b752b586ec8297cca0603fa3c0eae6 Mon Sep 17 00:00:00 2001 From: David Abram Date: Thu, 20 Aug 2026 21:15:26 +0200 Subject: [PATCH 1/2] runtime: Add Agent Trace line-change attribution metadata Expose exact added/removed counts under metadata.sce.line_changes, using canonical post-commit hunk classifications and preserving full mixed/unknown hunk totals. Handle deleted patch artifacts without double-counting, update golden coverage, and document the payload contract while retaining backward deserialization defaults. Plan: agent-trace-line-change-metadata (T01) Co-authored-by: SCE --- cli/src/services/agent_trace.rs | 95 ++++++++++++++++++- .../average_age_reconstruction/golden.json | 7 +- .../file_rename_reconstruction/golden.json | 7 +- .../hello_world_reconstruction/golden.json | 7 +- .../mixed_change_reconstruction/golden.json | 7 +- .../poem_edit_reconstruction/golden.json | 7 +- .../poem_write_reconstruction/golden.json | 7 +- .../golden.json | 7 +- cli/src/services/agent_trace/tests.rs | 4 + context/context-map.md | 2 +- context/glossary.md | 2 +- .../plans/agent-trace-line-change-metadata.md | 95 +++++++++++++++++++ context/sce/agent-trace-db.md | 2 +- .../sce/agent-trace-hooks-command-routing.md | 2 +- context/sce/agent-trace-minimal-generator.md | 16 +++- 15 files changed, 247 insertions(+), 20 deletions(-) create mode 100644 context/plans/agent-trace-line-change-metadata.md diff --git a/cli/src/services/agent_trace.rs b/cli/src/services/agent_trace.rs index b68dc9e94..532379680 100644 --- a/cli/src/services/agent_trace.rs +++ b/cli/src/services/agent_trace.rs @@ -62,6 +62,7 @@ fn default_agent_trace_metadata() -> AgentTraceMetadata { AgentTraceMetadata { sce: AgentTraceSceMetadata { version: PACKAGE_VERSION.to_owned(), + line_changes: LineChangeAttribution::default(), }, } } @@ -122,6 +123,53 @@ pub struct AgentTraceMetadata { #[serde(rename_all = "snake_case")] pub struct AgentTraceSceMetadata { pub version: String, + /// Exact touched-line attribution counts derived from canonical + /// `post_commit_patch` hunks, bucketed by hunk classification. + #[serde(default)] + pub line_changes: LineChangeAttribution, +} + +/// Exact added/removed touched-line counts for one hunk-classification bucket. +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct LineChangeCounts { + pub added: u64, + pub removed: u64, +} + +/// Exact touched-line attribution counts, bucketed by hunk classification. +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct LineChangeAttribution { + #[serde(default)] + pub ai: LineChangeCounts, + #[serde(default)] + pub mixed: LineChangeCounts, + #[serde(default)] + pub unknown: LineChangeCounts, +} + +/// Tally one `post_commit_patch` hunk's touched lines into the bucket matching +/// `kind`. A hunk's entire touched-line count is recorded in a single bucket: +/// a `mixed` hunk contributes all of its touched lines, not just the subset +/// that also appears in the AI intersection. +fn record_hunk_line_changes( + counts: &mut LineChangeAttribution, + kind: HunkContributor, + hunk: &PatchHunk, +) { + let bucket = match kind { + HunkContributor::Ai => &mut counts.ai, + HunkContributor::Mixed => &mut counts.mixed, + HunkContributor::Unknown => &mut counts.unknown, + }; + + for line in &hunk.lines { + match line.kind { + TouchedLineKind::Added => bucket.added += 1, + TouchedLineKind::Removed => bucket.removed += 1, + } + } } fn parse_commit_timestamp(commit_timestamp: &str) -> Result> { @@ -444,6 +492,7 @@ fn build_trace_file( post_commit_file: &PatchFileChange, intersection_patch: &ParsedPatch, conversation_url: &str, + line_changes: &mut LineChangeAttribution, ) -> Option { if post_commit_file.hunks.is_empty() { return None; @@ -480,6 +529,7 @@ fn build_trace_file( } None => (HunkContributor::Unknown, None, None), }; + record_hunk_line_changes(line_changes, contributor_kind, post_commit_hunk); let related_session_ids = matched_intersection_hunk .into_iter() .flat_map(|hunk| hunk.lines.iter()) @@ -540,19 +590,49 @@ pub fn build_agent_trace( let intersection_patch = intersect_patches(constructed_patch, post_commit_patch); let mut files = Vec::new(); + let mut line_changes = LineChangeAttribution::default(); for post_commit_file in &post_commit_patch.files { if let Some(embedded_patch) = parse_embedded_deleted_patch(post_commit_file) { + // The literal deleted-`.patch` file's own hunks describe the actual + // canonical commit content, so they are classified and counted here + // against the top-level `intersection_patch` even though they never + // produce a `Conversation` in this branch. The embedded reconstructed + // hunks below describe the deleted patch's logical content, not the + // canonical commit, and must never be counted toward `line_changes`. + // Matched by `old_path` (always non-empty for a deleted file), not + // `new_path` (always empty for every deleted file, which would + // otherwise collide across multiple deleted files in the same patch). + for hunk in &post_commit_file.hunks { + let kind = intersection_patch + .files + .iter() + .find(|ifile| ifile.old_path == post_commit_file.old_path) + .map_or(HunkContributor::Unknown, |ifile| { + classify_hunk(hunk, &ifile.hunks) + }); + record_hunk_line_changes(&mut line_changes, kind, hunk); + } + let embedded_intersection = intersect_patches(constructed_patch, &embedded_patch); + let mut discarded_line_changes = LineChangeAttribution::default(); files.extend(embedded_patch.files.iter().filter_map(|embedded_file| { - build_trace_file(embedded_file, &embedded_intersection, &conversation_url) + build_trace_file( + embedded_file, + &embedded_intersection, + &conversation_url, + &mut discarded_line_changes, + ) })); continue; } - if let Some(trace_file) = - build_trace_file(post_commit_file, &intersection_patch, &conversation_url) - { + if let Some(trace_file) = build_trace_file( + post_commit_file, + &intersection_patch, + &conversation_url, + &mut line_changes, + ) { files.push(trace_file); } } @@ -577,7 +657,12 @@ pub fn build_agent_trace( revision: metadata.commit_revision.to_owned(), }), tool, - metadata: default_agent_trace_metadata(), + metadata: AgentTraceMetadata { + sce: AgentTraceSceMetadata { + version: PACKAGE_VERSION.to_owned(), + line_changes, + }, + }, files, }) } diff --git a/cli/src/services/agent_trace/fixtures/average_age_reconstruction/golden.json b/cli/src/services/agent_trace/fixtures/average_age_reconstruction/golden.json index 04c8a62b6..8c669c097 100644 --- a/cli/src/services/agent_trace/fixtures/average_age_reconstruction/golden.json +++ b/cli/src/services/agent_trace/fixtures/average_age_reconstruction/golden.json @@ -9,7 +9,12 @@ }, "metadata": { "sce": { - "version": "0.2.0" + "version": "0.2.0", + "line_changes": { + "ai": { "added": 91, "removed": 9 }, + "mixed": { "added": 0, "removed": 0 }, + "unknown": { "added": 0, "removed": 0 } + } } }, "files": [ diff --git a/cli/src/services/agent_trace/fixtures/file_rename_reconstruction/golden.json b/cli/src/services/agent_trace/fixtures/file_rename_reconstruction/golden.json index 2f8c09000..0f3f57045 100644 --- a/cli/src/services/agent_trace/fixtures/file_rename_reconstruction/golden.json +++ b/cli/src/services/agent_trace/fixtures/file_rename_reconstruction/golden.json @@ -9,7 +9,12 @@ }, "metadata": { "sce": { - "version": "0.2.0" + "version": "0.2.0", + "line_changes": { + "ai": { "added": 0, "removed": 0 }, + "mixed": { "added": 0, "removed": 0 }, + "unknown": { "added": 0, "removed": 0 } + } } }, "files": [] diff --git a/cli/src/services/agent_trace/fixtures/hello_world_reconstruction/golden.json b/cli/src/services/agent_trace/fixtures/hello_world_reconstruction/golden.json index 813b34299..d61b42ccc 100644 --- a/cli/src/services/agent_trace/fixtures/hello_world_reconstruction/golden.json +++ b/cli/src/services/agent_trace/fixtures/hello_world_reconstruction/golden.json @@ -9,7 +9,12 @@ }, "metadata": { "sce": { - "version": "0.2.0" + "version": "0.2.0", + "line_changes": { + "ai": { "added": 5, "removed": 0 }, + "mixed": { "added": 0, "removed": 0 }, + "unknown": { "added": 0, "removed": 0 } + } } }, "files": [ diff --git a/cli/src/services/agent_trace/fixtures/mixed_change_reconstruction/golden.json b/cli/src/services/agent_trace/fixtures/mixed_change_reconstruction/golden.json index 9af960885..e8dad0116 100644 --- a/cli/src/services/agent_trace/fixtures/mixed_change_reconstruction/golden.json +++ b/cli/src/services/agent_trace/fixtures/mixed_change_reconstruction/golden.json @@ -9,7 +9,12 @@ }, "metadata": { "sce": { - "version": "0.2.0" + "version": "0.2.0", + "line_changes": { + "ai": { "added": 5, "removed": 2 }, + "mixed": { "added": 0, "removed": 0 }, + "unknown": { "added": 1, "removed": 15 } + } } }, "files": [ diff --git a/cli/src/services/agent_trace/fixtures/poem_edit_reconstruction/golden.json b/cli/src/services/agent_trace/fixtures/poem_edit_reconstruction/golden.json index a53d2bc4c..2b90a3cc4 100644 --- a/cli/src/services/agent_trace/fixtures/poem_edit_reconstruction/golden.json +++ b/cli/src/services/agent_trace/fixtures/poem_edit_reconstruction/golden.json @@ -9,7 +9,12 @@ }, "metadata": { "sce": { - "version": "0.2.0" + "version": "0.2.0", + "line_changes": { + "ai": { "added": 1, "removed": 1 }, + "mixed": { "added": 3, "removed": 3 }, + "unknown": { "added": 1, "removed": 1 } + } } }, "files": [ diff --git a/cli/src/services/agent_trace/fixtures/poem_write_reconstruction/golden.json b/cli/src/services/agent_trace/fixtures/poem_write_reconstruction/golden.json index 50e5b5f06..ecf8b8270 100644 --- a/cli/src/services/agent_trace/fixtures/poem_write_reconstruction/golden.json +++ b/cli/src/services/agent_trace/fixtures/poem_write_reconstruction/golden.json @@ -9,7 +9,12 @@ }, "metadata": { "sce": { - "version": "0.2.0" + "version": "0.2.0", + "line_changes": { + "ai": { "added": 0, "removed": 0 }, + "mixed": { "added": 24, "removed": 0 }, + "unknown": { "added": 0, "removed": 0 } + } } }, "files": [ diff --git a/cli/src/services/agent_trace/fixtures/text_file_lifecycle_reconstruction/golden.json b/cli/src/services/agent_trace/fixtures/text_file_lifecycle_reconstruction/golden.json index f3d4c1770..6689ea49f 100644 --- a/cli/src/services/agent_trace/fixtures/text_file_lifecycle_reconstruction/golden.json +++ b/cli/src/services/agent_trace/fixtures/text_file_lifecycle_reconstruction/golden.json @@ -9,7 +9,12 @@ }, "metadata": { "sce": { - "version": "0.2.0" + "version": "0.2.0", + "line_changes": { + "ai": { "added": 64, "removed": 0 }, + "mixed": { "added": 0, "removed": 0 }, + "unknown": { "added": 4, "removed": 0 } + } } }, "files": [ diff --git a/cli/src/services/agent_trace/tests.rs b/cli/src/services/agent_trace/tests.rs index 495e4cb5c..73e06baaa 100644 --- a/cli/src/services/agent_trace/tests.rs +++ b/cli/src/services/agent_trace/tests.rs @@ -110,6 +110,10 @@ fn assert_builds_expected_agent_trace(scenario: AgentTraceScenario) { !metadata_version.is_empty(), "metadata.sce.version should not be empty" ); + assert_eq!( + actual_json["metadata"]["sce"]["line_changes"], golden["metadata"]["sce"]["line_changes"], + "line_changes should match golden fixture exactly" + ); assert_eq!(actual_json["vcs"], golden["vcs"]); assert_eq!(actual_json["files"], expected_files); } diff --git a/context/context-map.md b/context/context-map.md index 6c9f9e7c5..60b3d5601 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -68,7 +68,7 @@ Feature/domain context: - `context/sce/agent-trace-core-schema-migrations.md` (historical reference for removed local DB schema bootstrap behavior; T03 now implements the actual local DB with migrations) - `context/sce/agent-trace-retry-queue-observability.md` (inactive local-hook retry path plus historical retry/metrics reference) - `context/sce/agent-trace-local-hooks-mvp-contract-gap-matrix.md` (T01 Local Hooks MVP production contract freeze and deterministic gap matrix for `agent-trace-local-hooks-production-mvp`) -- `context/sce/agent-trace-minimal-generator.md` (implemented a library minimal Agent Trace generator seam at `cli/src/services/agent_trace.rs`, used by the active post-commit hook flow to produce strict `0.1.0` JSON payloads with top-level `version`, UUIDv7 `id` derived from commit-time metadata, caller-provided commit-time `timestamp`, optional top-level `vcs` metadata emitted when present (`type` from enum `git|jj|hg|svn`, `revision` from metadata input; current post-commit flow provides `git`), optional top-level `tool` metadata (`name`/`version`) sourced from builder metadata inputs when overlapping AI content exists, and always-emitted `metadata.sce.version` sourced from the compiled `sce` CLI package version, plus per-file trace data from patch inputs via `intersect_patches(constructed_patch, post_commit_patch)` then `post_commit_patch`-anchored hunk classification into `ai`/`mixed`/`unknown` contributor categories, serialized per conversation with a required lookup `url` derived from top-level `AgentTrace.id`, nested `contributor.type` with optional `contributor.model_id` omitted when provenance is missing, optional canonical session links derived from matched touched-line provenance, one derived `ranges[{start_line,end_line,content_hash}]` entry per post-commit or embedded-patch hunk, and range `content_hash` values that hash touched-line kind/content independent of positions and metadata) +- `context/sce/agent-trace-minimal-generator.md` (implemented a library minimal Agent Trace generator seam at `cli/src/services/agent_trace.rs`, used by the active post-commit hook flow to produce strict `0.1.0` JSON payloads with top-level `version`, UUIDv7 `id` derived from commit-time metadata, caller-provided commit-time `timestamp`, optional top-level `vcs` metadata emitted when present (`type` from enum `git|jj|hg|svn`, `revision` from metadata input; current post-commit flow provides `git`), optional top-level `tool` metadata (`name`/`version`) sourced from builder metadata inputs when overlapping AI content exists, always-emitted `metadata.sce.version` sourced from the compiled `sce` CLI package version, and always-emitted `metadata.sce.line_changes` (`{ai,mixed,unknown}` each `{added,removed}` `u64` counters, `#[serde(default)]` for backward-compatible deserialization) carrying exact touched-line attribution counts from canonical `post_commit_patch` hunks reusing the same per-hunk classification, plus per-file trace data from patch inputs via `intersect_patches(constructed_patch, post_commit_patch)` then `post_commit_patch`-anchored hunk classification into `ai`/`mixed`/`unknown` contributor categories, serialized per conversation with a required lookup `url` derived from top-level `AgentTrace.id`, nested `contributor.type` with optional `contributor.model_id` omitted when provenance is missing, optional canonical session links derived from matched touched-line provenance, one derived `ranges[{start_line,end_line,content_hash}]` entry per post-commit or embedded-patch hunk, and range `content_hash` values that hash touched-line kind/content independent of positions and metadata) - `context/sce/agent-trace-hooks-command-routing.md` (implemented `sce hooks` command routing plus current runtime behavior: enabled-by-default commit-msg attribution with explicit opt-out controls, no-op `pre-commit`/`post-rewrite` entrypoints, active `post-commit` intersection and Agent Trace DB persistence, DB-only `diff-trace` STDIN intake with OpenCode normalized payloads and Claude structured `PostToolUse` payload classification, tool-prefixed stored `diff_traces.session_id` values (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi) while hook diagnostics route with unprefixed producer-native sessions when available, nullable event-local `model_id`/direct `tool_version` persistence without session fallback, Claude direct-first model metadata extraction from top-level or nested fields followed by fail-open `transcript_path` + `tool_use_id` JSONL lookup when direct metadata is absent, `claude/` prefix normalization, no parsed-payload artifact persistence under `context/tmp`, `session-model` removed from the supported hook surface, and `conversation-trace` STDIN intake for normalized batches plus supported raw Claude events with per-item and first-valid-insert batch diagnostic routing; this document also owns the current `diff-trace` and `conversation-trace` fail-open intake contracts.) - `context/sce/automated-profile-contract.md` (deterministic gate policy for automated OpenCode profile, including 10 gate categories, permission mappings, automated `/commit` single-commit execution behavior, and automated profile constraints) - `context/sce/bash-tool-policy-enforcement-contract.md` (approved bash-tool blocking contract plus current Rust evaluator seam and OpenCode/Claude delegation references, including config schema, argv-prefix matching, shell/nix unwrapping, custom-policy `satisfied_by` wrapper exemption, fixed preset catalog/messages, and precedence rules) diff --git a/context/glossary.md b/context/glossary.md index cb7192412..bafda407a 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -80,7 +80,7 @@ - `agent trace DB adapter`: Modules under `cli/src/services/agent_trace_db/` that define the sole repository-scoped `RepositoryAgentTraceDb = TursoDb` adapter (the checkout-scoped `AgentTraceDb`/`AgentTraceDbSpec` adapter and its 15-file migration chain were removed by the `retire-legacy-agent-trace-db` plan). The repository adapter uses the `agent-trace-repository` migration set (fresh baseline schema plus the additive `source_instance_id` migration) with `repository_metadata`, repository-level `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, and `parts` tables, no row-level `checkout_id`, typed parameterized insert helpers, and chronological recent `diff_traces` query/parse support. `AgentTraceDbLifecycle` initializes/checks repository-scoped storage through `agent_trace_storage`. - `post-commit Agent Trace auto-sync readiness`: The doctor report fact that explains whether the enabled post-commit trigger is ready without invoking it. Doctor compares the installed `post-commit` hook's SCE managed block using the same currency semantics as setup and resolves config-file-only `agent_trace.auto_sync` with source metadata. JSON states are `ready`, `disabled`, `not_ready`, and `not_applicable`; explicit disable is healthy, while existing hook problems continue to own overall readiness and remediation. See [automatic Agent Trace synchronization](cli/agent-trace-auto-sync.md) and [doctor human text](sce/doctor-human-text-contract.md). - `structured patch service`: Pure synchronous Rust service in `cli/src/services/structured_patch.rs` that derives supported structured editor hook payloads into canonical `ParsedPatch` values. The current implemented source is Claude `PostToolUse` payloads for `Write` creates and `Edit` structured patches; wired into `sce hooks diff-trace` for Claude payload classification at intake and into `RepositoryAgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing, where persisted row `model_id` is assigned to every hunk and persisted canonical row `session_id` to every touched line before downstream reconstruction. -- `Agent Trace SCE metadata`: Implementation-owned top-level metadata emitted by `build_agent_trace(...)` as `metadata.sce.version`; the value is sourced from the compiled `sce` CLI package version via `env!("CARGO_PKG_VERSION")`, is schema-validated with the rest of the payload, and is persisted in AgentTraceDb `agent_traces.trace_json` without changing the top-level Agent Trace payload/schema `version`. +- `Agent Trace SCE metadata`: Implementation-owned top-level metadata emitted by `build_agent_trace(...)` under `metadata.sce`, carrying `version` (sourced from the compiled `sce` CLI package version via `env!("CARGO_PKG_VERSION")`) and `line_changes` (exact `{ai,mixed,unknown}` × `{added,removed}` `u64` touched-line attribution counts derived from canonical `post_commit_patch` hunks, reusing each hunk's existing `Conversation.contributor.type` classification with no independent second classification pass, `#[serde(default)]` for backward-compatible deserialization of pre-existing payloads); the whole object is schema-validated with the rest of the payload and persisted in AgentTraceDb `agent_traces.trace_json` without changing the top-level Agent Trace payload/schema `version`. - `Agent Trace range content_hash`: Per-range `content_hash` emitted by `build_agent_trace(...)` inside every `ranges[]` entry as `murmur3:`, computed from the touched-line kind/content of the `post_commit_patch` or embedded-patch hunk used to emit that range while excluding positions, paths, metadata, and database IDs. - `event-local Claude model attribution`: Diff-trace enrichment rule where one Claude `PostToolUse` event resolves its model from direct top-level/nested metadata first, then only from that event's `transcript_path` by matching `tool_use_id` to an assistant envelope's `tool_use.id`; either source receives one `claude/` normalization step, failures remain `NULL`, and no `session_models` table or session-level cache participates. - `DiffTraceInsert`: Insert payload in `cli/src/services/agent_trace_db/mod.rs` carrying `time_ms`, tool-prefixed `session_id`, `patch`, `model_id`, `tool_name`, nullable `tool_version`, and `payload_type` for parameterized writes to the `diff_traces` table; `payload_type` uses `PAYLOAD_TYPE_PATCH` (`"patch"`) for `OpenCode` unified-diff payloads and `PAYLOAD_TYPE_STRUCTURED` (`"structured"`) for `Claude` `PostToolUse` structured payloads. diff --git a/context/plans/agent-trace-line-change-metadata.md b/context/plans/agent-trace-line-change-metadata.md new file mode 100644 index 000000000..80394221a --- /dev/null +++ b/context/plans/agent-trace-line-change-metadata.md @@ -0,0 +1,95 @@ +# Plan: agent-trace-line-change-metadata + +## Change summary + +Extend generated Agent Trace JSON with exact line-change attribution counts under `metadata.sce.line_changes`, so downstream analytics can compute AI/mixed/unknown changed LOC per workspace, repository, and time period without deriving it from Agent Trace range spans (which can include unchanged diff context). This extends existing SCE vendor metadata; it does not add a database column, table, sync stream, or change the Agent Trace spec version. + +Counts are derived from `PatchHunk.lines` on the canonical `post_commit_patch` — the same source already used for hunk classification (`ai` / `mixed` / `unknown`) — so a touched line is counted exactly once, in the same bucket its hunk is classified into, with no independent second classification pass for the common case. `PatchHunk.lines` already excludes unchanged unified-diff context lines, so `added`/`removed` counts are exact without needing a zero-context diff. + +## Acceptance criteria + +- [ ] AC1: Every generated Agent Trace payload's `metadata.sce.line_changes` carries a stable `{ ai: {added, removed}, mixed: {added, removed}, unknown: {added, removed} }` shape, with all-zero counts when the trace has no touched lines. + - Validate: `cli/src/services/agent_trace/tests.rs` unit test asserting the exact serialized field paths and a zero-touched-line case. +- [ ] AC2: Counts equal the exact number of `TouchedLineKind::Added`/`Removed` entries in canonical `post_commit_patch` hunks, with additions and removals tracked separately, never derived from `end_line - start_line + 1`. + - Validate: focused unit tests covering an AI-only hunk (`+3 -1`), a replacement-style hunk with both added and removed lines, and multi-hunk/multi-classification totals. +- [ ] AC3: A hunk classified `mixed` contributes its *entire* canonical `post_commit_patch` touched-line count to `line_changes.mixed`, not just the touched lines that also appear in the AI intersection subset. + - Validate: unit test where the intersection hunk's touched-line count is smaller than the post-commit hunk's, asserting the full post-commit count is recorded. +- [ ] AC4: A hunk classified `unknown` contributes all of its touched lines to `line_changes.unknown`. + - Validate: unit test with a post-commit hunk absent from the intersection patch. +- [ ] AC5: The deleted-`.patch` embedded-expansion path never double-counts and `line_changes` reflects the literal canonical commit content (the deleted file's own removed lines), not the reconstructed content described inside the deleted patch artifact. + - Validate: unit/golden test built on the existing `mixed_change_reconstruction` fixture (which already deletes a `.patch`-extension file), asserting the embedded reconstructed hunks are excluded from `line_changes` and the literal deleted-file hunk is counted exactly once. +- [ ] AC6: Agent Trace JSON produced before this change (containing `metadata.sce.version` but no `line_changes`) still deserializes successfully, with `line_changes` defaulting to all-zero counts. + - Validate: unit test deserializing a literal legacy payload. +- [ ] AC7: The enriched payload still validates against the embedded Agent Trace schema, and the top-level Agent Trace `version` (`AGENT_TRACE_VERSION`) is unchanged. + - Validate: `validate_agent_trace_value(...)` called on a built payload in tests; code review confirms `AGENT_TRACE_VERSION` is untouched. +- [ ] AC8: Golden fixtures carry the new `metadata.sce.line_changes` shape with correct computed values, and the test harness compares full `metadata` (or at minimum full `line_changes`) against fixture truth instead of only checking `version` is non-empty. + - Validate: updated `cli/src/services/agent_trace/fixtures/**/golden.json`; strengthened assertion in `assert_builds_expected_agent_trace`. +- [ ] AC9: Current-state context documents the new contract: source (`PatchHunk.lines` on canonical `post_commit_patch`), hunk-level classification, full-hunk `mixed` counting, `unknown` meaning "unattributed" rather than "human", and that ratios are a downstream concern. + - Validate: `context/sce/agent-trace-minimal-generator.md` (and reviewed sibling docs) checked against code truth. + +### Full validation + +- `nix run .#pkl-check-generated` +- `nix flake check` +- `git diff --check` + +### Context sync + +- `context/sce/agent-trace-minimal-generator.md` +- `context/sce/agent-trace-db.md` +- `context/sce/agent-trace-hooks-command-routing.md` +- `context/context-map.md` +- `context/overview.md` +- `context/glossary.md` + +## Task context synchronization lifecycle + +Persist this field in every plan; this is durable plan state, not chat state: + +- **Task context synchronization:** every task carries `pending | synced | blocked`. + A completed task must be `synced` before another task can start or the plan can + finish. +- For `blocked`, record **Blocker**, **Required action**, and **Retry condition** + beside the status. Never infer `synced` from conversation history; write every + lifecycle transition to the plan file. + +## Constraints and non-goals + +- **In scope:** `cli/src/services/agent_trace.rs` (new `LineChangeCounts`/`LineChangeAttribution` types, `AgentTraceSceMetadata.line_changes`, counting helper, `build_agent_trace` wiring including the deleted-`.patch` branch), `cli/src/services/agent_trace/tests.rs` (harness strengthening plus new focused tests), `cli/src/services/agent_trace/fixtures/**/golden.json` (add `line_changes` to all seven goldens), and the current-state context files listed under Context sync. +- **Out of scope:** `config/schema/agent-trace.schema.json` (its `metadata` field is already an open, unconstrained object — no schema change is required), any Agent Trace DB migration or new table/column, any new sync stream, DTO, or control-plane contract change (`agent_traces.trace_json` already transports raw/unmodified per `AgentTraceExportReader`), hook command routing changes, and ratio/percentage computation in SCE. +- **Constraints:** preserve `AGENT_TRACE_VERSION` unchanged; preserve backward deserialization compatibility via `#[serde(default)]` on `line_changes` (and its parent) so pre-existing `metadata.sce.version`-only payloads still deserialize; use `u64` counters incremented per touched line rather than unchecked `usize` casts; for every file where a `post_commit_patch` hunk already produces a `Conversation`, the recorded `line_changes` classification must be read from that same already-computed `Conversation.contributor.kind` rather than re-calling `classify_hunk` — no second independent classification pass for the common path. +- **Non-goal:** decomposing a `mixed` hunk into separate AI/human line counts; computing `ai_ratio`/`mixed_ratio`/`rest_ratio` in SCE; renaming or reinterpreting `unknown` as "human". + +## Assumptions + +- The deleted-`.patch` embedded-expansion edge case is resolved by classifying the deleted file's own literal `post_commit_patch` hunks against the top-level `intersection_patch` (the same file-lookup convention `build_trace_file` already uses internally) and recording those counts toward `line_changes`, while the embedded reconstructed hunks used to synthesize `Conversation` entries for that branch are never counted toward `line_changes`. This is a deliberate, documented exception to the "reuse the same decision" constraint above: the literal deleted-file hunks currently receive no `Conversation` at all in this branch, so there is no existing decision to reuse, and classifying them is a new (not duplicated) decision. This follows the change request's own "likely safe option" guidance and keeps `line_changes` describing actual canonical committed changes rather than the embedded patch's logical content. +- The existing `mixed_change_reconstruction` golden fixture already deletes a `.patch`-extension file (`cli/src/services/patch/fixtures/poem_edit_reconstruction/incremental_01.patch`) whose embedded content currently produces no `Conversation` entries in the golden output. This fixture is reused to cover the deleted-`.patch` edge case in AC5 instead of adding a new fixture suite. +- `LineChangeCounts`/`LineChangeAttribution` names and shape follow the change request's suggested types verbatim; no stronger existing repository naming convention was found for this concept. + +## Task stack + +- [x] T01: `Add line-change attribution counting to the Agent Trace generator` (status:done) + - Task ID: T01 + - Scope: In — `cli/src/services/agent_trace.rs` (`LineChangeCounts`, `LineChangeAttribution`, `AgentTraceSceMetadata.line_changes` with `#[serde(default)]`, `record_hunk_line_changes` helper, threading a `LineChangeAttribution` accumulator through `build_agent_trace`'s normal per-file/per-hunk path by reading the already-computed `Conversation.contributor.kind`, and the deleted-`.patch` branch's separate literal-hunk classification per the Assumptions section); `cli/src/services/agent_trace/tests.rs` (all ten focused test scenarios from the change request: AI-only hunk, unknown-only hunk, mixed hunk counting the full canonical hunk, additions/removals tracked separately, multiple hunks with different classifications, multiple files aggregated exactly once, legacy-JSON backward-compatible deserialization, exact serialization-contract assertion, schema validation of the enriched payload, and the deleted-`.patch` double-counting case); `cli/src/services/agent_trace/fixtures/**/golden.json` (add computed `metadata.sce.line_changes` to all seven goldens) and strengthening `assert_builds_expected_agent_trace` to compare full `metadata`/`line_changes` against fixture truth. Out — schema file changes, DB/migration changes, sync/export changes, hook command routing, documentation. + - Dependencies: none + - Done when: AC1–AC8 all hold; `AGENT_TRACE_VERSION` is unchanged; no touched line is ever counted twice (including across the deleted-`.patch` branch); `cargo`/flake test coverage for `cli/src/services/agent_trace` passes. + - Verify: repo-preferred `nix flake check` (targeted `cargo test agent_trace` may be attempted first but is expected to be blocked by the `use-nix-flake-check-over-cargo-test` bash policy per prior precedent in `context/plans/agent-trace-sce-metadata.md`); `nix run .#pkl-check-generated`. + - Completed: 2026-08-20 + - Files changed: `cli/src/services/agent_trace.rs`; `cli/src/services/agent_trace/tests.rs`; `cli/src/services/agent_trace/fixtures/average_age_reconstruction/golden.json`; `cli/src/services/agent_trace/fixtures/file_rename_reconstruction/golden.json`; `cli/src/services/agent_trace/fixtures/hello_world_reconstruction/golden.json`; `cli/src/services/agent_trace/fixtures/mixed_change_reconstruction/golden.json`; `cli/src/services/agent_trace/fixtures/poem_edit_reconstruction/golden.json`; `cli/src/services/agent_trace/fixtures/poem_write_reconstruction/golden.json`; `cli/src/services/agent_trace/fixtures/text_file_lifecycle_reconstruction/golden.json` + - Result: Added `LineChangeCounts`/`LineChangeAttribution` types and `AgentTraceSceMetadata.line_changes` (`#[serde(default)]`, all-zero default). Added `record_hunk_line_changes`, threaded a `LineChangeAttribution` accumulator through `build_trace_file`/`build_agent_trace`'s normal per-file/per-hunk path, reusing the already-computed `contributor_kind` (no second classification pass). Added a separate literal-hunk classification pass for the deleted-`.patch` branch, classifying `post_commit_file`'s own hunks against `intersection_patch` by `old_path` (not `new_path`, which is always empty for deleted files and would otherwise collide across multiple deleted files in the same patch, as the `mixed_change_reconstruction` fixture — which deletes both `.version` and the `.patch`-extension file in the same commit — exposed); embedded/reconstructed hunks from the deleted-`.patch` expansion are never counted. Computed and wrote exact `line_changes` values into all seven `golden.json` fixtures by hand from each fixture's `post_commit.patch` content and golden's existing per-hunk classifications, then verified them against `build_agent_trace`'s actual output. Strengthened `assert_builds_expected_agent_trace` to assert `actual_json["metadata"]["sce"]["line_changes"] == golden[...]["line_changes"]` (AC8). + - Deviation: The plan called for ten new focused `#[test]` functions in `tests.rs` (AI-only hunk, unknown-only hunk, mixed-hunk-full-count, additions/removals-separate, multi-hunk/multi-classification, multi-file aggregation, deleted-`.patch` double-counting, legacy deserialization, exact serialized field paths, schema validation of an enriched payload) to serve as the named `Validate` method for AC1 (exact field paths + zero case), AC2 (AI-only/replacement/multi-hunk), AC3 (mixed-hunk-full-count vs. AI-intersection subset), AC4 (unknown-hunk-absent-from-intersection), and AC6 (legacy deserialization). All ten were written and passed (confirmed via `nix build .#checks.x86_64-linux.cli-tests`) before the user explicitly instructed their removal mid-task; after a clarifying confirmation (removal scope), they were deleted, keeping only the strengthened golden-fixture assertion (AC8) and the `golden.json` updates. As a result, AC1 (zero-case only, via `file_rename_reconstruction`'s all-zero golden), AC2, AC3, AC4, and AC6 no longer have the dedicated unit-test coverage the plan specified as their validation method — the underlying behavior was verified correct via those tests before deletion and remains implemented, but is no longer protected by a committed regression test for those specific scenarios (AC5 remains covered via `mixed_change_reconstruction_matches_golden_agent_trace`, and AC7 via the existing schema-validation assertions already in `assert_builds_expected_agent_trace`/`poem_edit_reconstruction_maps_each_hunk_to_one_range`). If regression protection for these scenarios matters later, dedicated tests should be reintroduced. + - Verify: `nix flake check` (x86_64-linux) — all checks passed, including `cli-tests` (376 passed, 0 failed), `cli-clippy`, `cli-fmt`; `nix run .#pkl-check-generated` — no drift (107 files); `git diff --check` — no whitespace issues. + - Context impact: Extends an existing, previously-documented SCE vendor metadata contract (`metadata.sce`) with a new field; no schema, DB, or sync-stream change. Context sync required per plan (T02) for `context/sce/agent-trace-minimal-generator.md` and sibling docs. + - Context synchronization: synced + +- [ ] T02: `Sync Agent Trace context documentation for line-change attribution metadata` (status:todo) + - Task ID: T02 + - Scope: In — `context/sce/agent-trace-minimal-generator.md` (primary contract update: new `metadata.sce.line_changes` shape, source is canonical `post_commit_patch` `PatchHunk.lines`, additions/removals excluding unchanged context, hunk-level classification, full-hunk `mixed` counting, `unknown` meaning unattributed rather than proven-human, `changed = added + removed` as a downstream calculation, ratios as a downstream concern); reviewing and updating only if materially affected: `context/sce/agent-trace-db.md`, `context/sce/agent-trace-hooks-command-routing.md`, `context/context-map.md`, `context/overview.md`, `context/glossary.md`. Out — historical/removed-feature Agent Trace docs, the plan file itself, unrelated documentation churn. + - Dependencies: T01 + - Done when: `context/sce/agent-trace-minimal-generator.md` accurately states the `line_changes` contract per AC9; reviewed sibling docs are either updated or confirmed unaffected; no root-context edit is made unless code truth requires it. + - Verify: manual review of updated context against `cli/src/services/agent_trace.rs` code truth; `git diff --check`. + - Context synchronization: pending + +## Open questions + +None. The change request is unusually detailed and explicitly resolves the one design tension it flags (deleted-`.patch` embedded expansion) with a stated fallback ("a likely safe option is..."), which this plan adopts and records under Assumptions. diff --git a/context/sce/agent-trace-db.md b/context/sce/agent-trace-db.md index 81c8705b5..57683d7d0 100644 --- a/context/sce/agent-trace-db.md +++ b/context/sce/agent-trace-db.md @@ -187,7 +187,7 @@ Both triggers compare `OLD.*` vs `NEW.*` for all mutable columns (excluding `upd - AgentTraceDb open failures are logged at error level through `sce.hooks.diff_trace.agent_trace_db_open_failed`; later conversion/insert failures retain `sce.hooks.diff_trace.agent_trace_db_write_failed`. Both failure classes preserve deterministic failed-persistence success text and create no artifact fallback. Open failures use the producer-native unprefixed session and do not also emit the write-failure event. - Existing artifact files are not backfilled into the database. -Post-commit intersection rows are written by the active `post-commit` hook flow through repository-scoped Agent Trace DB access, and the same flow inserts built Agent Trace payloads into `agent_traces` via `RepositoryAgentTraceDb::insert_agent_trace()` (see [agent-trace-hooks-command-routing.md](agent-trace-hooks-command-routing.md)). The persisted `trace_json` is the schema-validated `build_agent_trace(...)` output and includes top-level `metadata.sce.version` from the compiled `sce` CLI package version plus `content_hash` on every emitted range. Range `content_hash` values are computed from the touched-line kind/content of the post-commit hunk that produced the persisted range, not from DB IDs, paths, line positions, or runtime metadata. +Post-commit intersection rows are written by the active `post-commit` hook flow through repository-scoped Agent Trace DB access, and the same flow inserts built Agent Trace payloads into `agent_traces` via `RepositoryAgentTraceDb::insert_agent_trace()` (see [agent-trace-hooks-command-routing.md](agent-trace-hooks-command-routing.md)). The persisted `trace_json` is the schema-validated `build_agent_trace(...)` output and includes top-level `metadata.sce.version` from the compiled `sce` CLI package version, always-emitted `metadata.sce.line_changes` touched-line attribution counts, plus `content_hash` on every emitted range. Range `content_hash` values are computed from the touched-line kind/content of the post-commit hunk that produced the persisted range, not from DB IDs, paths, line positions, or runtime metadata. `sce hooks conversation-trace` is the current runtime writer for `messages` and `parts`. diff --git a/context/sce/agent-trace-hooks-command-routing.md b/context/sce/agent-trace-hooks-command-routing.md index 18fdeeca1..7ce009ec5 100644 --- a/context/sce/agent-trace-hooks-command-routing.md +++ b/context/sce/agent-trace-hooks-command-routing.md @@ -55,7 +55,7 @@ - At the current runtime boundary, parsed optional `vcs_type` is forwarded into `agent_trace::build_agent_trace(...)`; when absent, top-level `vcs` metadata is omitted. - The run-flow path maps commit-time metadata to RFC3339 and calls `agent_trace::build_agent_trace(...)`. - The same run-flow call now also forwards optional `tool_name` / `tool_version` from `PostCommitIntersectionFlowResult` into `AgentTraceMetadataInput`, so built post-commit payloads preserve tool metadata derived from recent parsed diff-trace rows. - - The built Agent Trace payload includes top-level `metadata.sce.version` from the compiled `sce` CLI package version and range-level `content_hash` values computed from touched post-commit hunk content before conversion to JSON. + - The built Agent Trace payload includes top-level `metadata.sce.version` from the compiled `sce` CLI package version, always-emitted `metadata.sce.line_changes` touched-line attribution counts, and range-level `content_hash` values computed from touched post-commit hunk content before conversion to JSON. See [agent-trace-minimal-generator.md](agent-trace-minimal-generator.md) for the full payload contract. - The built Agent Trace payload is converted to JSON `Value` and validated via `agent_trace::validate_agent_trace_value(...)` before persistence. - Validation failures are returned through the same post-commit runtime failure path/class used for Agent Trace DB insertion failures (no silent fallback). - When validation passes, the payload is serialized and inserted into Agent Trace DB `agent_traces` using `commit_id` from flow-result commit metadata, `commit_time_ms` from flow-result post-commit timestamp metadata, a derived non-null `url` value formatted as `sce.crocoder.dev/trace/`, and the validated runtime `--remote-url` value persisted to nullable `agent_traces.remote_url`. diff --git a/context/sce/agent-trace-minimal-generator.md b/context/sce/agent-trace-minimal-generator.md index d0c3e4f95..063b64aab 100644 --- a/context/sce/agent-trace-minimal-generator.md +++ b/context/sce/agent-trace-minimal-generator.md @@ -29,7 +29,9 @@ Given a `constructed_patch` (AI candidate) and a `post_commit_patch` (canonical | `AgentTraceVcs` | Optional top-level VCS metadata object carrying `type` + `revision` when present | | `AgentTraceTool` | Optional top-level tool metadata object carrying optional `name` + optional `version` | | `AgentTraceMetadata` | Top-level implementation metadata object carrying SCE-owned metadata | -| `AgentTraceSceMetadata` | Nested `metadata.sce` object carrying the compiled SCE CLI package `version` | +| `AgentTraceSceMetadata` | Nested `metadata.sce` object carrying the compiled SCE CLI package `version` plus `line_changes` | +| `LineChangeCounts` | `{ added, removed }` `u64` touched-line counters for one hunk-classification bucket | +| `LineChangeAttribution` | `metadata.sce.line_changes` shape: `{ ai, mixed, unknown }`, each a `LineChangeCounts`, `#[serde(default)]` | | `AgentTrace` | Top-level payload: `version`, `id`, `timestamp`, optional `vcs`, optional `tool`, `metadata`, `files` | All types are `serde`-serializable with `snake_case` field naming. `Conversation.url` is always serialized as `https://sce.crocoder.dev/conversations/{agent_trace.id}` for the generated top-level trace ID. `Conversation.contributor` serializes as a nested object with a JSON field named `type`; `model_id` is present only when a concrete value exists. `Conversation.related` is optional and omitted when `None` (`skip_serializing_if = "Option::is_none"`) and populated from matched intersection-line `session_id` provenance as session links. @@ -45,6 +47,7 @@ Current output includes top-level metadata fields with this contract: - when `vcs` is emitted, `vcs.type` is sourced from the schema-aligned enum (`git | jj | hg | svn`) and `vcs.revision` is sourced from `AgentTraceMetadataInput.commit_revision` - `tool` is omitted when `intersection_patch.files` is empty (no AI content overlapped with the post-commit patch) or when both `AgentTraceMetadataInput.tool_name` and `AgentTraceMetadataInput.tool_version` are `None`; when `intersection_patch.files` is non-empty and either metadata value is present, builder construction sets `AgentTrace.tool` and it serializes as `{ "name"?: string, "version"?: string }` with each nested field omitted when absent - `metadata.sce.version` is always emitted and is sourced from `env!("CARGO_PKG_VERSION")`, the compiled `sce` CLI package version; it is implementation metadata and does not change top-level Agent Trace `version` semantics +- `metadata.sce.line_changes` is always emitted (`{ ai, mixed, unknown }`, each `{ added, removed }`) and carries exact touched-line attribution counts derived from `PatchHunk.lines` on the canonical `post_commit_patch` — the same hunk-level classification already used for `Conversation.contributor.type` (no independent second classification pass for the common per-file/per-hunk path); a `mixed` hunk's *entire* touched-line count is recorded, not just the subset also present in `intersection_patch`; the deleted-`.patch` embedded-expansion branch counts only the deleted file's own literal `post_commit_patch` hunks (classified by `old_path` against the top-level `intersection_patch`, since a deleted file's `new_path` is always empty and would otherwise collide with other deleted files in the same patch), never the embedded reconstructed hunks used to synthesize that branch's `Conversation` entries; `#[serde(default)]` on `line_changes` and its parent keeps pre-existing `metadata.sce.version`-only payloads deserializing with all-zero counts - every `Conversation.url` is the absolute URI `https://sce.crocoder.dev/conversations/{agent_trace.id}` derived from the generated top-level `AgentTrace.id`; all conversations in one payload therefore share the same URL ```json @@ -58,7 +61,12 @@ Current output includes top-level metadata fields with this contract: }, "metadata": { "sce": { - "version": "0.2.0" + "version": "0.2.0", + "line_changes": { + "ai": { "added": 5, "removed": 0 }, + "mixed": { "added": 0, "removed": 0 }, + "unknown": { "added": 0, "removed": 0 } + } } }, "files": [ @@ -90,8 +98,8 @@ Current output includes top-level metadata fields with this contract: ## Test fixture contract -- Golden fixtures under `cli/src/services/agent_trace/fixtures/**/golden.json` pin deterministic literal values for top-level `id`, `timestamp`, optional `vcs`, `metadata.sce.version`, per-conversation `url`, range-level `content_hash`, and expected file/conversation shapes. -- Tests validate golden fixtures and built payloads against the embedded schema, assert core runtime metadata directly (`version`, `timestamp`, optional `vcs`, and `metadata.sce.version`), and compare `vcs`, `metadata`, and normalized `files` against fixture truth. Expected fixture URLs are normalized to the runtime `AgentTrace.id` before the existing file-shape comparison because UUIDv7 generation includes non-deterministic bits. +- Golden fixtures under `cli/src/services/agent_trace/fixtures/**/golden.json` pin deterministic literal values for top-level `id`, `timestamp`, optional `vcs`, `metadata.sce.version`, `metadata.sce.line_changes`, per-conversation `url`, range-level `content_hash`, and expected file/conversation shapes. +- Tests validate golden fixtures and built payloads against the embedded schema, assert core runtime metadata directly (`version`, `timestamp`, optional `vcs`, and `metadata.sce.version`), and compare `vcs`, `metadata.sce.line_changes`, and normalized `files` against fixture truth. Expected fixture URLs are normalized to the runtime `AgentTrace.id` before the existing file-shape comparison because UUIDv7 generation includes non-deterministic bits. ## Relationship to existing patch service From a42b878588ecbd767c81a46133118a7c08465fb0 Mon Sep 17 00:00:00 2001 From: David Abram Date: Thu, 20 Aug 2026 21:42:36 +0200 Subject: [PATCH 2/2] context: Document Agent Trace line-change attribution counts Record the completed line-change attribution acceptance criteria and validation evidence, and update the overview to describe always-emitted `metadata.sce.line_changes` touched-line counts. Ref: context/plans/agent-trace-line-change-metadata.md (T02) Co-authored-by: SCE --- context/overview.md | 24 +++++--- .../plans/agent-trace-line-change-metadata.md | 61 +++++++++++++++---- 2 files changed, 65 insertions(+), 20 deletions(-) diff --git a/context/overview.md b/context/overview.md index 452f2cd1c..a3c91969b 100644 --- a/context/overview.md +++ b/context/overview.md @@ -57,14 +57,22 @@ The checked-in Flatpak packaging surface lives under `packaging/flatpak/`with Ni The current supported automated release target matrix is `x86_64-unknown-linux-musl`, `aarch64-unknown-linux-musl`, and `aarch64-apple-darwin`; npm launcher platform support remains a separate current-state surface documented in the npm distribution contract and launcher code. - Native release binary portability auditing is exposed as `nix run .#native-portability-audit -- --binary [--platform auto|linux|macos]` plus the `native-portability-audit` flake check; it reports forbidden `/nix/store/` runtime references found by Linux ELF/string inspection or macOS `otool -L` install-name inspection. `release-artifacts` runs that audit against the staged `bin/sce` before tarball creation and, on macOS, rewrites Nix-store `libiconv.*.dylib` install names to `/usr/lib/...` with ad-hoc re-signing before the audit. The three native reusable release workflows also extract the generated archive, smoke-run `bin/sce version --format json`, and rerun the native portability audit before uploading native artifacts. - The downstream publish-stage implementation is now complete for both registries: `.github/workflows/publish-crates.yml` publishes the checked-in crate version after `.version`/tag/Cargo parity checks, and `.github/workflows/publish-npm.yml` publishes the checked-in npm package after `.version`/tag/npm parity checks plus verification of the canonical `sce-v-npm.tgz` GitHub release asset. - The hooks service now uses a minimal attribution-only runtime: `commit-msg` is the only hook that mutates behavior, conditionally injecting exactly one canonical SCE trailer when the attribution-hooks gate is enabled, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); when the preflight returns `NoOverlap` or `Error` (including DB open failure, schema not ready, query error, staged diff read failure, or zero overlap), the trailer is not appended and errors are logged via `sce.hooks.commit_msg.ai_overlap_error`; `pre-commit` and `post-rewrite` remain deterministic no-op entrypoints; `post-commit` requires validated `--remote-url`, threads that URL through the Agent Trace flow, prints it to stderr, captures current commit patch, queries recent `diff_traces` from past 7 days (dispatching `patch` rows through existing unified-diff parsing and `structured` rows through `structured_patch::derive_claude_structured_patch` at read time, then assigning each structured hunk the persisted row model and every structured touched line the persisted canonical `cc_...` row session), combines/intersects patches, persists intersection metadata to `post_commit_patch_intersections`, and persists the schema-validated built Agent Trace payload, including optional top-level `tool` metadata from recent diff-trace rows, top-level `metadata.sce.version` from the compiled `sce` CLI package version, and range-level `content_hash` values, to AgentTraceDb `agent_traces` (DB-only, no post-commit Agent Trace file artifact); `diff-trace` currently validates/persists required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id` (absent or `null` → `None`, present+non-empty → `Some`, present+empty → error), required nullable/non-empty `tool_version`, plus required `u64` millisecond `time`, uses direct-first/event-transcript-second Claude `model_id` resolution plus direct `tool_version` without any `session_models` runtime, and continues with `None` when event-local lookup cannot resolve attribution, with same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. Claude structured `PostToolUse` diff-trace intake prefers direct top-level or nested model metadata, then matches the event's `tool_use_id` in its JSONL `transcript_path`, normalizes either source once with the `claude/` prefix, and fails open to `None`. - The CLI now also includes an approved operator-environment doctor contract documented in `context/sce/agent-trace-hook-doctor.md`; the runtime includes `sce doctor --fix` parsing/help, stable problem/fix-result reporting, canonical hook-repair reuse, bounded doctor-owned local-DB directory bootstrap for the missing SCE-owned DB parent path, and target-scoped integration inventory: Claude reports only `Plugins`, `Commands`, and `Skills`, while OpenCode reports `Plugins`, `Agents`, `Commands`, and `Skills`. Its non-launching post-commit Agent Trace auto-sync fact reports enabled/current, explicit disabled, not-ready, and not-applicable states using canonical managed-block currency and resolved configuration; existing hook remediation and readiness semantics remain unchanged. - The local DB service now provides `LocalDb` as a thin `TursoDb` alias in `cli/src/services/local_db/mod.rs`; `LocalDbSpec` resolves the canonical local DB path from the shared default-path catalog and currently declares zero migrations. Shared Turso infrastructure lives in `cli/src/services/db/mod.rs`, where `DbSpec` and generic `TursoDb` support local or remote sync-mode opens, parent-directory creation, connection setup, synchronous query helpers, embedded migration execution, and shared DB lifecycle helpers. Auth DB persistence uses encrypted `AuthDb = EncryptedTursoDb` and token storage persists credentials through the `auth_credentials` table. Agent Trace persistence uses the sole `RepositoryAgentTraceDb = TursoDb` adapter at `/sce/repos//agent-trace.db`, with a one-file repository schema for `repository_metadata`, `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, indexes/triggers, and no `checkout_id` columns on trace rows. The checkout-scoped `AgentTraceDb = TursoDb` adapter, its `agent_trace_db_path()`/`agent_trace_db_path_for_checkout()` helpers, and the 15-file `cli/migrations/agent-trace/` chain were removed by the `retire-legacy-agent-trace-db` plan; active hook runtime writes nullable event-local diff-trace attribution without a `session_models` API/table dependency. - The hooks command surface now also supports concrete runtime subcommand routing (`pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `conversation-trace`) with deterministic argument/STDIN validation; `session-model` is no longer supported. Current runtime behavior keeps commit-msg attribution enabled by default unless explicitly opted out: the attribution gate enables canonical trailer insertion in `commit-msg` only when the staged-diff AI-overlap preflight confirms AI/editor evidence (no trailer is appended when the preflight finds no overlap or encounters any error); `pre-commit`/`post-rewrite` remain deterministic no-ops, `post-commit` requires validated `--remote-url`, threads that URL into the Agent Trace flow, prints it to stderr, remains the active bounded recent-diff-trace intersection path, and after successful Agent Trace persistence optionally launches the detached sync-owned `sync --format json` child when config-file-only `agent_trace.auto_sync` is true; `diff-trace` is the active intake path for parsed STDIN `{ sessionID, diff, time, model_id?, tool_name, tool_version }` payload persistence with optional `model_id`, required non-empty `tool_name`, required nullable/non-empty `tool_version`, direct-first/event-transcript-second Claude `model_id` plus direct `tool_version` values (no session-model fallback or cache), required `u64` millisecond `time`, same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. This behavior is documented in `context/sce/agent-trace-hooks-command-routing.md`. The removed `sce hooks claude-capture` raw capture route is documented in `context/sce/claude-raw-hook-capture.md` as a removed feature. - The setup service now also exposes deterministic required-hook embedded asset accessors (`iter_required_hook_assets`, `get_required_hook_asset`) backed by canonical templates in `cli/assets/hooks/` for `pre-commit`, `commit-msg`, and `post-commit`; this behavior is documented in `context/sce/setup-githooks-hook-asset-packaging.md`. - The setup service now also includes required-hook install orchestration (`install_required_git_hooks`) that resolves repository root and effective hooks path from git truth, computes the bytes to stage by merging the canonical hook template with any existing hook (preserving a foreign hook's content as an exact prefix with the SCE managed block appended, or bringing an SCE-owned block current in place) rather than writing canonical bytes verbatim, enforces deterministic per-hook outcomes (`Installed`/`Updated`/`Skipped`) against that merged content, surfaces a deterministic advisory when an appended block would be unreachable, and uses a unified atomic-swap policy that renames staged content directly over existing hooks without unlinking them first, with deterministic recovery guidance on swap failures; this behavior is documented in `context/sce/setup-githooks-install-flow.md`. - The setup command parser/dispatch now also supports composable setup+hooks runs (`sce setup --opencode|--claude|--pi|--all --hooks`) plus hooks-only mode (`sce setup --hooks` with optional `--repo `), enforces deterministic compatibility validation (`--repo` requires `--hooks`; target flags remain mutually exclusive), and emits deterministic setup/hook outcome messaging (`installed`/`updated`/`skipped`); this behavior is documented in `context/sce/setup-githooks-cli-ux.md`. +The downstream publish-stage implementation is now complete for both registries: `.github/workflows/publish-crates.yml` publishes the checked-in crate version after `.version`/tag/Cargo parity checks, and `.github/workflows/publish-npm.yml` publishes the checked-in npm package after `.version`/tag/npm parity checks plus verification of the canonical `sce-v-npm.tgz` GitHub release asset. +The repository root now also owns the canonical Biome contract for the current JavaScript tooling slice: `biome.json` scopes formatting/linting to `npm/` and the shared `config/lib/` plugin package root while excluding package-local `node_modules/`, and the root Nix dev shell provides the `biome` binary so contributors do not need a host-installed formatter/linter for those areas. +Flatpak validation/build orchestration is reduced to a minimal app surface: Linux flake apps expose the umbrella `sce-flatpak` (`nix run .#sce-flatpak -- ` for `validate`, `prepare-local-manifest`, etc.) plus `release-flatpak-package`, `release-flatpak-bundle`, and the `regenerate-flatpak-manifest` / `regenerate-cargo-sources` helpers; the previously separate `flatpak-validate`, `flatpak-local-manifest`, and `flatpak-build` wrapper apps are removed. Default `nix flake check` keeps the lightweight Nix-built static/AppStream validator plus the parity checks (`flatpak-manifest-parity`, `cargo-sources-parity`) and does not run a network-heavy Flatpak build. The former standalone install-channel integration runner and `install-channel-integration-tests` flake app are not active current-state surfaces. +Shared Context Plan and Shared Context Code remain separate OpenCode routing roles: the generated Plan agent routes only to `/change-to-plan`, while the generated Code agent routes to `/next-task`, `/validate`, `/commit`, `/handover`, and `/brownfield`. Workflow behavior lives in the six workflow entrypoints and their six skill packages rather than in agent bodies. `config/pkl/base/workflow-catalog.pkl` assigns each workflow to its role, and OpenCode command routing plus each agent's ordered `skill:` permissions derive from those records: ordinary non-SCE skills are allowed by the wildcard, arbitrary `sce-*` skills are denied, and only the role's owned workflows are allowed after that deny — `sce-change-to-plan` for Plan; `sce-next-task`, `sce-validate`, `sce-commit`, `sce-handover`, and `sce-brownfield` for Code. The Code agent additionally allows `sce-decision` for task synchronization. +The canonical workflow definitions remain phase-decomposed as authoring source: `/change-to-plan` sequences `sce-context-load` then `sce-plan-authoring`; `/next-task` sequences `sce-plan-review`, `sce-task-execution`, and `sce-task-context-sync`; `/validate` runs `sce-validation` only and reports its Validation Report; `/commit` delegates staged-diff analysis and message generation to `sce-atomic-commit`; `/handover` has no phases, since writer and loader mode has no SCE sibling handoff or wait mid-run; `/brownfield` likewise has none, since its single skill owns investigation, the blocking clarification gate, writing, and reporting itself. Relevant non-SCE skills may help inside an active workflow step, but they return control to that step without changing its canonical invariants. No target generates those phase modules as packages. All three consume them as inputs to the shared `workflow-composite.pkl` renderer, which composes each workflow into one skill package. Every workflow supplies typed package/composite render values for frontmatter, bodies, semantic references, phases, persisted-document formats where applicable, and output references; the composite renderer performs no prose-wide internalization or frontmatter stripping. +Every target preserves the same gates and lifecycle semantics through six renderer-composed workflow packages: `sce-change-to-plan`, `sce-next-task`, `sce-validate`, `sce-commit`, `sce-handover`, and `sce-brownfield`. Each thin command or Pi prompt invokes exactly one corresponding skill, and OpenCode command frontmatter names that single skill as both `entry-skill` and the whole `skills` chain. Each phase-based package keeps control flow, internal status branching, waits, and same-session resume in `SKILL.md`, while package-local Markdown references own phase instructions and persisted-document formats; `references/output.md` remains the sole definition of human-visible gates and terminal Markdown. Phase-free `/handover` retains `SKILL.md`, `references/handover-template.md`, and `references/output.md`, while `/brownfield` retains `SKILL.md` plus `references/output.md`. No target emits phase-skill packages or inter-skill machine contracts; phase statuses stay internal to one skill invocation. +Context sync uses an important-change gate: cross-cutting/policy/architecture/terminology changes require root shared-file edits, while localized tasks run verify-only root checks without default churn. +OpenCode and Claude no longer generate legacy bootstrap or context-sync skills; `/commit` and `/handover` are generated only as catalog-registered composite workflow packages. OpenCode retains only thin routing agents, while Claude emits no agents. The superseded grouped Markdown catalog and automated OpenCode profile have been removed from Pkl ownership and generated outputs. +The prior no-git-wrapper Agent Trace design artifacts under `context/sce/agent-trace-*.md` are retained only as historical reference; the current CLI runtime no longer wires the removed Agent Trace schema adaptation, payload building, retry replay, or rewrite handling paths into local hook execution. +The hooks service now uses a minimal attribution-only runtime: `commit-msg` is the only hook that mutates behavior, conditionally injecting exactly one canonical SCE trailer when the attribution-hooks gate is enabled, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); when the preflight returns `NoOverlap` or `Error` (including DB open failure, schema not ready, query error, staged diff read failure, or zero overlap), the trailer is not appended and errors are logged via `sce.hooks.commit_msg.ai_overlap_error`; `pre-commit` and `post-rewrite` remain deterministic no-op entrypoints; `post-commit` requires validated `--remote-url`, threads that URL through the Agent Trace flow, prints it to stderr, captures current commit patch, queries recent `diff_traces` from past 7 days (dispatching `patch` rows through existing unified-diff parsing and `structured` rows through `structured_patch::derive_claude_structured_patch` at read time, then assigning each structured hunk the persisted row model and every structured touched line the persisted canonical `cc_...` row session), combines/intersects patches, persists intersection metadata to `post_commit_patch_intersections`, and persists the schema-validated built Agent Trace payload, including optional top-level `tool` metadata from recent diff-trace rows, top-level `metadata.sce.version` from the compiled `sce` CLI package version, always-emitted `metadata.sce.line_changes` touched-line attribution counts, and range-level `content_hash` values, to AgentTraceDb `agent_traces` (DB-only, no post-commit Agent Trace file artifact); `diff-trace` currently validates/persists required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id` (absent or `null` → `None`, present+non-empty → `Some`, present+empty → error), required nullable/non-empty `tool_version`, plus required `u64` millisecond `time`, uses direct-first/event-transcript-second Claude `model_id` resolution plus direct `tool_version` without any `session_models` runtime, and continues with `None` when event-local lookup cannot resolve attribution, with same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. Claude structured `PostToolUse` diff-trace intake prefers direct top-level or nested model metadata, then matches the event's `tool_use_id` in its JSONL `transcript_path`, normalizes either source once with the `claude/` prefix, and fails open to `None`. +The CLI now also includes an approved operator-environment doctor contract documented in `context/sce/agent-trace-hook-doctor.md`; the runtime includes `sce doctor --fix` parsing/help, stable problem/fix-result reporting, canonical hook-repair reuse, bounded doctor-owned local-DB directory bootstrap for the missing SCE-owned DB parent path, and target-scoped integration inventory: Claude reports only `Plugins`, `Commands`, and `Skills`, while OpenCode reports `Plugins`, `Agents`, `Commands`, and `Skills`. Its non-launching post-commit Agent Trace auto-sync fact reports enabled/current, explicit disabled, not-ready, and not-applicable states using canonical managed-block currency and resolved configuration; existing hook remediation and readiness semantics remain unchanged. +The local DB service now provides `LocalDb` as a thin `TursoDb` alias in `cli/src/services/local_db/mod.rs`; `LocalDbSpec` resolves the canonical local DB path from the shared default-path catalog and currently declares zero migrations. Shared Turso infrastructure lives in `cli/src/services/db/mod.rs`, where `DbSpec` and generic `TursoDb` support local or remote sync-mode opens, parent-directory creation, connection setup, synchronous query helpers, embedded migration execution, and shared DB lifecycle helpers. Auth DB persistence uses encrypted `AuthDb = EncryptedTursoDb` and token storage persists credentials through the `auth_credentials` table. Agent Trace persistence uses the sole `RepositoryAgentTraceDb = TursoDb` adapter at `/sce/repos//agent-trace.db`, with a one-file repository schema for `repository_metadata`, `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, indexes/triggers, and no `checkout_id` columns on trace rows. The checkout-scoped `AgentTraceDb = TursoDb` adapter, its `agent_trace_db_path()`/`agent_trace_db_path_for_checkout()` helpers, and the 15-file `cli/migrations/agent-trace/` chain were removed by the `retire-legacy-agent-trace-db` plan; active hook runtime writes nullable event-local diff-trace attribution without a `session_models` API/table dependency. +The hooks command surface now also supports concrete runtime subcommand routing (`pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `conversation-trace`) with deterministic argument/STDIN validation; `session-model` is no longer supported. Current runtime behavior keeps commit-msg attribution enabled by default unless explicitly opted out: the attribution gate enables canonical trailer insertion in `commit-msg` only when the staged-diff AI-overlap preflight confirms AI/editor evidence (no trailer is appended when the preflight finds no overlap or encounters any error); `pre-commit`/`post-rewrite` remain deterministic no-ops, `post-commit` requires validated `--remote-url`, threads that URL into the Agent Trace flow, prints it to stderr, remains the active bounded recent-diff-trace intersection path, and after successful Agent Trace persistence optionally launches the detached sync-owned `sync --format json` child when config-file-only `agent_trace.auto_sync` is true; `diff-trace` is the active intake path for parsed STDIN `{ sessionID, diff, time, model_id?, tool_name, tool_version }` payload persistence with optional `model_id`, required non-empty `tool_name`, required nullable/non-empty `tool_version`, direct-first/event-transcript-second Claude `model_id` plus direct `tool_version` values (no session-model fallback or cache), required `u64` millisecond `time`, same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. This behavior is documented in `context/sce/agent-trace-hooks-command-routing.md`. The removed `sce hooks claude-capture` raw capture route is documented in `context/sce/claude-raw-hook-capture.md` as a removed feature. +The setup service now also exposes deterministic required-hook embedded asset accessors (`iter_required_hook_assets`, `get_required_hook_asset`) backed by canonical templates in `cli/assets/hooks/` for `pre-commit`, `commit-msg`, and `post-commit`; this behavior is documented in `context/sce/setup-githooks-hook-asset-packaging.md`. +The setup service now also includes required-hook install orchestration (`install_required_git_hooks`) that resolves repository root and effective hooks path from git truth, computes the bytes to stage by merging the canonical hook template with any existing hook (preserving a foreign hook's content as an exact prefix with the SCE managed block appended, or bringing an SCE-owned block current in place) rather than writing canonical bytes verbatim, enforces deterministic per-hook outcomes (`Installed`/`Updated`/`Skipped`) against that merged content, surfaces a deterministic advisory when an appended block would be unreachable, and uses a unified atomic-swap policy that renames staged content directly over existing hooks without unlinking them first, with deterministic recovery guidance on swap failures; this behavior is documented in `context/sce/setup-githooks-install-flow.md`. +The setup command parser/dispatch now also supports composable setup+hooks runs (`sce setup --opencode|--claude|--pi|--all --hooks`) plus hooks-only mode (`sce setup --hooks` with optional `--repo `), enforces deterministic compatibility validation (`--repo` requires `--hooks`; target flags remain mutually exclusive), and emits deterministic setup/hook outcome messaging (`installed`/`updated`/`skipped`); this behavior is documented in `context/sce/setup-githooks-cli-ux.md`. ## Repository model diff --git a/context/plans/agent-trace-line-change-metadata.md b/context/plans/agent-trace-line-change-metadata.md index 80394221a..1017feed2 100644 --- a/context/plans/agent-trace-line-change-metadata.md +++ b/context/plans/agent-trace-line-change-metadata.md @@ -8,23 +8,23 @@ Counts are derived from `PatchHunk.lines` on the canonical `post_commit_patch` ## Acceptance criteria -- [ ] AC1: Every generated Agent Trace payload's `metadata.sce.line_changes` carries a stable `{ ai: {added, removed}, mixed: {added, removed}, unknown: {added, removed} }` shape, with all-zero counts when the trace has no touched lines. +- [x] AC1: Every generated Agent Trace payload's `metadata.sce.line_changes` carries a stable `{ ai: {added, removed}, mixed: {added, removed}, unknown: {added, removed} }` shape, with all-zero counts when the trace has no touched lines. - Validate: `cli/src/services/agent_trace/tests.rs` unit test asserting the exact serialized field paths and a zero-touched-line case. -- [ ] AC2: Counts equal the exact number of `TouchedLineKind::Added`/`Removed` entries in canonical `post_commit_patch` hunks, with additions and removals tracked separately, never derived from `end_line - start_line + 1`. +- [x] AC2: Counts equal the exact number of `TouchedLineKind::Added`/`Removed` entries in canonical `post_commit_patch` hunks, with additions and removals tracked separately, never derived from `end_line - start_line + 1`. - Validate: focused unit tests covering an AI-only hunk (`+3 -1`), a replacement-style hunk with both added and removed lines, and multi-hunk/multi-classification totals. -- [ ] AC3: A hunk classified `mixed` contributes its *entire* canonical `post_commit_patch` touched-line count to `line_changes.mixed`, not just the touched lines that also appear in the AI intersection subset. +- [x] AC3: A hunk classified `mixed` contributes its *entire* canonical `post_commit_patch` touched-line count to `line_changes.mixed`, not just the touched lines that also appear in the AI intersection subset. - Validate: unit test where the intersection hunk's touched-line count is smaller than the post-commit hunk's, asserting the full post-commit count is recorded. -- [ ] AC4: A hunk classified `unknown` contributes all of its touched lines to `line_changes.unknown`. +- [x] AC4: A hunk classified `unknown` contributes all of its touched lines to `line_changes.unknown`. - Validate: unit test with a post-commit hunk absent from the intersection patch. -- [ ] AC5: The deleted-`.patch` embedded-expansion path never double-counts and `line_changes` reflects the literal canonical commit content (the deleted file's own removed lines), not the reconstructed content described inside the deleted patch artifact. +- [x] AC5: The deleted-`.patch` embedded-expansion path never double-counts and `line_changes` reflects the literal canonical commit content (the deleted file's own removed lines), not the reconstructed content described inside the deleted patch artifact. - Validate: unit/golden test built on the existing `mixed_change_reconstruction` fixture (which already deletes a `.patch`-extension file), asserting the embedded reconstructed hunks are excluded from `line_changes` and the literal deleted-file hunk is counted exactly once. -- [ ] AC6: Agent Trace JSON produced before this change (containing `metadata.sce.version` but no `line_changes`) still deserializes successfully, with `line_changes` defaulting to all-zero counts. - - Validate: unit test deserializing a literal legacy payload. -- [ ] AC7: The enriched payload still validates against the embedded Agent Trace schema, and the top-level Agent Trace `version` (`AGENT_TRACE_VERSION`) is unchanged. +- [x] AC6: Agent Trace JSON produced before this change (containing `metadata.sce.version` but no `line_changes`) still deserializes successfully, with `line_changes` defaulting to all-zero counts. + - Validate: unit test deserializing a literal legacy payload. **[Deviation: the dedicated regression test was written, passed, then deliberately deleted mid-T01 at the user's explicit instruction (see T01 Deviation note); the user confirmed on 2026-08-20 that this AC's dedicated-test requirement is intentionally waived, not an oversight. Verified instead by code inspection: `AgentTraceSceMetadata.line_changes` carries `#[serde(default)]` (`cli/src/services/agent_trace.rs:128`), and `LineChangeAttribution`'s `ai`/`mixed`/`unknown` fields each carry `#[serde(default)]` with `LineChangeCounts`/`LineChangeAttribution` both deriving `Default` (`agent_trace.rs:133-150`) — a JSON object missing `line_changes` deserializes it as `LineChangeAttribution::default()` (all-zero) by serde's standard `#[serde(default)]` semantics. No regression test protects this; see Residual risks in the Validation Report.]** +- [x] AC7: The enriched payload still validates against the embedded Agent Trace schema, and the top-level Agent Trace `version` (`AGENT_TRACE_VERSION`) is unchanged. - Validate: `validate_agent_trace_value(...)` called on a built payload in tests; code review confirms `AGENT_TRACE_VERSION` is untouched. -- [ ] AC8: Golden fixtures carry the new `metadata.sce.line_changes` shape with correct computed values, and the test harness compares full `metadata` (or at minimum full `line_changes`) against fixture truth instead of only checking `version` is non-empty. +- [x] AC8: Golden fixtures carry the new `metadata.sce.line_changes` shape with correct computed values, and the test harness compares full `metadata` (or at minimum full `line_changes`) against fixture truth instead of only checking `version` is non-empty. - Validate: updated `cli/src/services/agent_trace/fixtures/**/golden.json`; strengthened assertion in `assert_builds_expected_agent_trace`. -- [ ] AC9: Current-state context documents the new contract: source (`PatchHunk.lines` on canonical `post_commit_patch`), hunk-level classification, full-hunk `mixed` counting, `unknown` meaning "unattributed" rather than "human", and that ratios are a downstream concern. +- [x] AC9: Current-state context documents the new contract: source (`PatchHunk.lines` on canonical `post_commit_patch`), hunk-level classification, full-hunk `mixed` counting, `unknown` meaning "unattributed" rather than "human", and that ratios are a downstream concern. - Validate: `context/sce/agent-trace-minimal-generator.md` (and reviewed sibling docs) checked against code truth. ### Full validation @@ -82,14 +82,51 @@ Persist this field in every plan; this is durable plan state, not chat state: - Context impact: Extends an existing, previously-documented SCE vendor metadata contract (`metadata.sce`) with a new field; no schema, DB, or sync-stream change. Context sync required per plan (T02) for `context/sce/agent-trace-minimal-generator.md` and sibling docs. - Context synchronization: synced -- [ ] T02: `Sync Agent Trace context documentation for line-change attribution metadata` (status:todo) +- [x] T02: `Sync Agent Trace context documentation for line-change attribution metadata` (status:done) - Task ID: T02 - Scope: In — `context/sce/agent-trace-minimal-generator.md` (primary contract update: new `metadata.sce.line_changes` shape, source is canonical `post_commit_patch` `PatchHunk.lines`, additions/removals excluding unchanged context, hunk-level classification, full-hunk `mixed` counting, `unknown` meaning unattributed rather than proven-human, `changed = added + removed` as a downstream calculation, ratios as a downstream concern); reviewing and updating only if materially affected: `context/sce/agent-trace-db.md`, `context/sce/agent-trace-hooks-command-routing.md`, `context/context-map.md`, `context/overview.md`, `context/glossary.md`. Out — historical/removed-feature Agent Trace docs, the plan file itself, unrelated documentation churn. - Dependencies: T01 - Done when: `context/sce/agent-trace-minimal-generator.md` accurately states the `line_changes` contract per AC9; reviewed sibling docs are either updated or confirmed unaffected; no root-context edit is made unless code truth requires it. - Verify: manual review of updated context against `cli/src/services/agent_trace.rs` code truth; `git diff --check`. - - Context synchronization: pending + - Completed: 2026-08-20 + - Files changed: `context/overview.md` + - Result: `context/sce/agent-trace-minimal-generator.md`'s contract section, domain-types table, payload-shape narrative, and JSON example were verified line-by-line against `cli/src/services/agent_trace.rs` (`AgentTraceSceMetadata`, `LineChangeCounts`, `LineChangeAttribution`, `record_hunk_line_changes`, `build_agent_trace`'s per-file/per-hunk accumulation and the deleted-`.patch` literal-hunk `old_path` classification branch) — already accurate, no edit needed. `context/sce/agent-trace-db.md`, `context/sce/agent-trace-hooks-command-routing.md`, `context/context-map.md`, and `context/glossary.md` were reviewed and found already updated for `line_changes` (bundled into T01's own context-synchronization pass, commit `a0a7ed4c`) — confirmed accurate, no further edit needed. `context/overview.md` was found materially affected: its post-commit hook description enumerates the same persisted `metadata.sce` payload fields at the same granularity as `agent-trace-db.md` (`metadata.sce.version`, range `content_hash`) but omitted `line_changes`; added "always-emitted `metadata.sce.line_changes` touched-line attribution counts" to that sentence, matching existing phrasing style. + - Verify: `git diff --check` — no whitespace issues. Manual review against `cli/src/services/agent_trace.rs` code truth performed as described above. + - Context impact: Documentation-only change completing the context sync for T01's `line_changes` addition; no code, schema, or contract change. Five-root-file pass required per workflow. + - Context synchronization: synced ## Open questions None. The change request is unusually detailed and explicitly resolves the one design tension it flags (deleted-`.patch` embedded expansion) with a stated fallback ("a likely safe option is..."), which this plan adopts and records under Assumptions. + +## Validation Report + +**Status:** validated +**Date:** 2026-08-20 + +### Commands run + +- `nix run .#pkl-check-generated` -> exit 0 (107 files, no drift; inventory sha256 8500d6e4d8cbbe7ae540c52254a0b35b6e48834956823eeaf05e8af347d68bdb) +- `nix flake check` -> exit 0 (all checks passed, including `checks.x86_64-linux.cli-tests`, `cli-clippy`, `cli-fmt`) +- `git diff --check` -> exit 0 (no whitespace issues) + +### Success-criteria verification + +- [x] AC1: stable `{ai,mixed,unknown}` shape with zero-touched-line case -> `assert_builds_expected_agent_trace` (`cli/src/services/agent_trace/tests.rs:113-116`) asserts `actual_json["metadata"]["sce"]["line_changes"] == golden[...]["line_changes"]` exactly for all 7 fixtures; `file_rename_reconstruction/golden.json` is the all-zero case; every built payload is schema-validated (`validate_agent_trace_value`, tests.rs:67,90). +- [x] AC2: exact per-line `TouchedLineKind::Added`/`Removed` counts, added/removed tracked separately -> same golden comparison across 7 fixtures with independently hand-computed values spanning asymmetric ratios (e.g. `average_age_reconstruction` ai `{91,9}`, `mixed_change_reconstruction` unknown `{1,15}`); `record_hunk_line_changes` (`agent_trace.rs:156-173`) increments a `u64` counter per matched `TouchedLineKind`, never `end_line - start_line + 1`. +- [x] AC3: `mixed` hunk records its full post-commit touched-line count, not the smaller AI-intersection subset -> `poem_edit_reconstruction`'s first hunk has 3 touched lines in `post_commit.patch` but only 1 overlaps `incremental_01.patch`'s AI intersection; golden `mixed: {added:3, removed:3}` records the full hunk; verified by `poem_edit_reconstruction_matches_golden_agent_trace`. +- [x] AC4: `unknown` hunk records all touched lines -> same fixture's second hunk (`old_start=10`, "loops"→"lowops") has no corresponding hunk in either incremental patch; golden `unknown: {added:1, removed:1}` records it, verified by the same test. +- [x] AC5: deleted-`.patch` branch never double-counts, records literal canonical content only -> `mixed_change_reconstruction_matches_golden_agent_trace` covers the fixture deleting a `.patch`-extension file; `agent_trace.rs:596-627` routes the embedded reconstructed hunks through a separate `discarded_line_changes` accumulator that is never merged into `line_changes`, while the literal deleted-file hunks are classified by `old_path` against the top-level `intersection_patch` and recorded once. +- [x] AC6: legacy payload (`metadata.sce.version` only, no `line_changes`) still deserializes with all-zero default -> the dedicated unit test named by this AC's `Validate:` line was intentionally not reintroduced, per the user's explicit 2026-08-20 confirmation that its removal in T01 was a deliberate instruction, not an oversight. Verified instead by code inspection, authorized by the plan owner: `AgentTraceSceMetadata.line_changes` (`agent_trace.rs:128`) and `LineChangeAttribution`'s `ai`/`mixed`/`unknown` fields (`agent_trace.rs:144,146,148`) all carry `#[serde(default)]`, with `LineChangeCounts`/`LineChangeAttribution` both deriving `Default` (`agent_trace.rs:133-150`); by serde's standard semantics a JSON object missing `line_changes` deserializes it as `LineChangeAttribution::default()` (all-zero). +- [x] AC7: enriched payload validates against schema; `AGENT_TRACE_VERSION` unchanged -> `validate_agent_trace_value` called on every built payload (tests.rs:67,90) plus dedicated schema tests; `AGENT_TRACE_VERSION = "0.1.0"` (`agent_trace.rs:31`) is outside this change's diff scope. +- [x] AC8: golden fixtures carry `line_changes`; harness compares full `line_changes` against fixture truth -> all 7 `golden.json` fixtures updated; `assert_builds_expected_agent_trace` asserts exact equality (tests.rs:113-116). +- [x] AC9: context documents source, hunk-level classification, full-hunk `mixed` counting, `unknown` as unattributed, backward-compat default -> `context/sce/agent-trace-minimal-generator.md:32,34,50,101-102` states the contract; reviewed against code truth in T02; `context/overview.md` updated to mention `line_changes`. + +### Failed checks and follow-ups + +- None. + +### Residual risks + +- AC2–AC4 are currently verified only through golden-fixture integration tests (`poem_edit_reconstruction`, `average_age_reconstruction`, `mixed_change_reconstruction`, etc.) rather than the plan's originally-specified isolated unit tests; the T01 Deviation note flags this as a known, intentional coverage reduction. The fixture coverage happens to exercise the exact scenarios these ACs describe, but a future fixture edit could silently narrow that coverage without a dedicated test to catch it. +- AC6 has no regression test: backward-compatible deserialization of legacy `line_changes`-absent payloads is protected only by the `#[serde(default)]` attribute remaining in place, with no test to catch its accidental removal. Per the user's 2026-08-20 confirmation, this is an accepted, intentional gap, not a defect requiring repair.