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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ walkdir = "2.5"
sha2 = "0.11"
ureq = { version = "3.3", features = ["json"] }
serde_yml = "0.0.12"
similar = "2"

[dev-dependencies]
tempfile = "3.27"
Expand Down
113 changes: 102 additions & 11 deletions src/reference/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,52 @@ pub fn compute_line_diff(before: &[String], after: &[String]) -> Vec<Hunk> {
if before == after {
return vec![];
}
vec![Hunk {
before: before.to_vec(),
after: after.to_vec(),
before_start: 1,
after_start: 1,
}]
let before_refs: Vec<&str> = before.iter().map(|s| s.as_str()).collect();
let after_refs: Vec<&str> = after.iter().map(|s| s.as_str()).collect();
let diff = similar::TextDiff::from_slices(&before_refs, &after_refs);
let mut hunks: Vec<Hunk> = Vec::new();
let mut current: Option<(Vec<String>, Vec<String>, u32, u32)> = None;
let mut before_line = 1u32;
let mut after_line = 1u32;

for change in diff.iter_all_changes() {
let value = change.value().to_string();
match change.tag() {
similar::ChangeTag::Equal => {
if let Some((b, a, bs, as_)) = current.take() {
hunks.push(Hunk {
before: b,
after: a,
before_start: bs,
after_start: as_,
});
}
before_line += 1;
after_line += 1;
}
similar::ChangeTag::Delete => {
let entry = current
.get_or_insert_with(|| (Vec::new(), Vec::new(), before_line, after_line));
entry.0.push(value);
before_line += 1;
}
similar::ChangeTag::Insert => {
let entry = current
.get_or_insert_with(|| (Vec::new(), Vec::new(), before_line, after_line));
entry.1.push(value);
after_line += 1;
}
}
}
if let Some((b, a, bs, as_)) = current.take() {
hunks.push(Hunk {
before: b,
after: a,
before_start: bs,
after_start: as_,
});
}
hunks
}

pub fn compute_symbol_diff(
Expand Down Expand Up @@ -282,15 +322,66 @@ mod tests {
}

#[test]
fn compute_line_diff_produces_single_hunk_for_changes() {
fn compute_line_diff_replacement_isolates_changed_lines() {
let before = vec!["a".to_string(), "b".to_string()];
let after = vec!["a".to_string(), "c".to_string()];
let hunks = compute_line_diff(&before, &after);
assert_eq!(hunks.len(), 1);
assert_eq!(hunks[0].before, before);
assert_eq!(hunks[0].after, after);
assert_eq!(hunks[0].before_start, 1);
assert_eq!(hunks[0].after_start, 1);
assert_eq!(hunks[0].before, vec!["b".to_string()]);
assert_eq!(hunks[0].after, vec!["c".to_string()]);
assert_eq!(hunks[0].before_start, 2);
assert_eq!(hunks[0].after_start, 2);
}

#[test]
fn compute_line_diff_for_inserts_only_returns_addition_hunk() {
let before = vec!["a".to_string(), "b".to_string()];
let after = vec!["a".to_string(), "b".to_string(), "c".to_string()];
let hunks = compute_line_diff(&before, &after);
assert_eq!(hunks.len(), 1);
assert!(hunks[0].before.is_empty());
assert_eq!(hunks[0].after, vec!["c".to_string()]);
assert_eq!(hunks[0].after_start, 3);
assert_eq!(hunks[0].before_start, 3);
}

#[test]
fn compute_line_diff_for_deletes_only_returns_removal_hunk() {
let before = vec!["a".to_string(), "b".to_string(), "c".to_string()];
let after = vec!["a".to_string(), "c".to_string()];
let hunks = compute_line_diff(&before, &after);
assert_eq!(hunks.len(), 1);
assert_eq!(hunks[0].before, vec!["b".to_string()]);
assert!(hunks[0].after.is_empty());
assert_eq!(hunks[0].before_start, 2);
assert_eq!(hunks[0].after_start, 2);
}

#[test]
fn compute_line_diff_for_separate_changes_produces_multiple_hunks() {
let before = vec![
"a".to_string(),
"b".to_string(),
"c".to_string(),
"d".to_string(),
"e".to_string(),
];
let after = vec![
"X".to_string(),
"b".to_string(),
"c".to_string(),
"d".to_string(),
"Y".to_string(),
];
let hunks = compute_line_diff(&before, &after);
assert!(
hunks.len() >= 2,
"expected at least 2 hunks for two separate changes: {hunks:?}"
);
// First hunk: a -> X at line 1
assert_eq!(hunks.first().unwrap().before_start, 1);
// Last hunk: e -> Y at line 5
assert_eq!(hunks.last().unwrap().before_start, 5);
}

#[test]
Expand Down
7 changes: 7 additions & 0 deletions tasks/lessons.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,10 @@
- Mistake: section 構造が必要であることを起票時に意識せず、`gwt-build-spec` の completion gate で tasks セクション更新ができなくなった。
- Rule: SPEC を起票するときは body 内に `## Spec` / `## Plan` / `## Tasks` / `## TDD` の見出しを揃え、必要なら `gwtd issue spec --edit <section>` を初回投入時から使う。`--edit` 経由なら gwtd 側が `<!-- sections: ... -->` コメントを管理してくれる。
- Checkpoint: 1. spec create 直後に `gwtd issue spec <n>` を実行して `<!-- sections: -->` が埋まっているか確認 2. 空ならその場で `--edit spec` / `--edit plan` / `--edit tasks` / `--edit tdd` で投入し直す

### 2026-05-12 (子 SPEC 増殖の防止)

- Context: Phase 4 umbrella SPEC #191 を起票した直後、最優先サブタスクを「子 SPEC #192」として切り出してしまい、user から「基本的には子 SPEC は作らないでください」と明示指示を受けた。
- Mistake: ralph loop / autonomous 進行時に、umbrella SPEC があってもサブタスクごとに新 SPEC を生やす癖が出た。Phase 1-3 の「Phase ごとに 1 SPEC」パターンを引きずって過剰に細分化した。
- Rule: umbrella SPEC が存在する Phase では、サブタスクで独立した SPEC を新設しない。実装は umbrella SPEC を `Refs` する commit / PR で進め、umbrella SPEC 本文の Tasks セクションをチェックボックスで更新する。新 SPEC を起こすのは umbrella の意図と明確に外れる別軸の作業に限定する。
- Checkpoint: 1. 新規 SPEC 起票前に「既存の umbrella SPEC で受け止められないか」を 1 度自問する 2. 起票する場合は umbrella との関係(吸収 / 並列 / 独立)を本文の `Related` で明示する
Loading