From 15cd5eac5ea89ca9c1e2df32f6b64fa957a3e03b Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 16 May 2026 17:47:52 +0700 Subject: [PATCH] fix: patch generated web diagnostics --- CHANGELOG.md | 8 ++ Cargo.lock | 2 +- README.md | 10 +- package.json | 2 +- phonton-cli/Cargo.toml | 2 +- phonton-cli/src/focus.rs | 31 ++++++- phonton-cli/src/main.rs | 66 +++++++++++++ phonton-worker/src/lib.rs | 68 +++++++++++++- .../src/templates/chessRules.test.ts | 93 +++++++++---------- release-notes/v0.14.1.md | 31 +++++++ 10 files changed, 254 insertions(+), 59 deletions(-) create mode 100644 release-notes/v0.14.1.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 636c6a1..870b8ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable Phonton CLI release changes should be documented here. This project follows pre-1.0 SemVer: minor versions may still include breaking changes while the public API and CLI surface settle. +## 0.14.1 - Generated Web Failure Diagnostics + +### Fixed + +- Problems focus now shows the changed-file excerpt that matches the verifier diagnostic path. A failure such as `[typescript syntax] src/App.tsx:1:1 invalid syntax` now jumps to the `src/App.tsx` diff instead of showing the first unrelated changed file. +- Generated web syntax diagnostics that include line/column suffixes such as `src/App.tsx:1:1` are normalized back to artifact paths before retry policy runs. This keeps generic repair contexts on the generated-web fast-fail path instead of spending another broad provider repair. +- The existing Vite/React chess rules test seed no longer imports `vitest`. The seeded test file uses self-executing assertions, so it does not require Phonton to repair `package.json` before local rules verification can pass. + ## 0.14.0 - Non-Interactive Node Verification ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 2fa42b2..bd7e7d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2274,7 +2274,7 @@ dependencies = [ [[package]] name = "phonton-cli" -version = "0.14.0" +version = "0.14.1" dependencies = [ "anyhow", "async-trait", diff --git a/README.md b/README.md index 2fc8a09..31f68ad 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Phonton CLI logo

-

Phonton CLI · v0.13.5

+

Phonton CLI · v0.14.1

Verified code changes with repo memory.
@@ -12,7 +12,7 @@

CI GitHub stars - release + release license status

@@ -75,6 +75,8 @@ It walks through the evidence trail a real run should expose: GoalContract, plan - `/why-tokens` and `phonton why-tokens --by-source` explain the latest prompt manifest in plain language, including first-attempt, repair-attempt, context/artifact, system, goal, memory, attachment, repo-code, MCP/tool, retry, compaction, dedupe, and cached-token buckets. - v0.11 context planning builds a compact repo map, selects only the highest-value code slices under a target budget, exposes omitted code tokens, and labels target-exceeded prompts honestly when one required slice must go over budget. - v0.12 enforces lower spend before the provider call: generated app/game goals dispatch as acceptance-slice subtasks, simple/docs/test prompts use small task-class budgets, generated repairs use a sub-1k context target, semantic retrieval top-k and repo maps shrink by task class, MCP result context is capped, and provider output ceilings are lower. +- v0.14.1 fixes generated-web failure diagnostics: Problems focus now jumps to the changed file named by the verifier, `src/App.tsx:1:1` diagnostics are normalized for retry policy, and the local chess rules test seed no longer requires a Vitest import. +- v0.14.0 hardens Node verification so stock Vite/Vitest/Jest test scripts run in non-interactive CI mode instead of hanging in watch mode. - v0.13.5 seeds the existing Vite/React chess rules/test boundary with a locally verified template before provider UI slices, including recovery from partial invalid `src/chessRules.ts` artifacts without another provider call. - v0.13.4 detects existing Vite/React workspaces for chess benchmark prompts that say "use the existing project stack," then starts on source/test slices instead of fragile `package.json` or `index.html` scaffold edits. - v0.13.1 hardens generated web-app token behavior: Vite/React chess prompts stay on compact acceptance slices even in partial workspaces, and first-attempt TSX/HTML syntax failures stop before automatic repair. @@ -153,7 +155,7 @@ Windows PowerShell: Direct Cargo install: ```bash -cargo install --git https://github.com/phonton-dev/phonton-cli --tag v0.13.5 phonton-cli --locked --force +cargo install --git https://github.com/phonton-dev/phonton-cli --tag v0.14.1 phonton-cli --locked --force ``` Check the install: @@ -169,7 +171,7 @@ Phonton uses GitHub branches and releases as install channels: | Channel | Install | Use when | |---|---|---| -| Stable | `cargo install --git https://github.com/phonton-dev/phonton-cli --tag v0.13.5 phonton-cli --locked --force` | You want the best validated public alpha | +| Stable | `cargo install --git https://github.com/phonton-dev/phonton-cli --tag v0.14.1 phonton-cli --locked --force` | You want the best validated public alpha | | Dev | `cargo install --git https://github.com/phonton-dev/phonton-cli --branch dev phonton-cli --locked --force` | You want next-release integration changes | | Nightly | `cargo install --git https://github.com/phonton-dev/phonton-cli --branch nightly phonton-cli --locked --force` | You want daily snapshots and can tolerate breakage | | Main | `cargo install --git https://github.com/phonton-dev/phonton-cli --branch main phonton-cli --locked --force` | You want the current release branch tip | diff --git a/package.json b/package.json index cc180f8..8a7c1d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "phonton-cli", - "version": "0.14.0", + "version": "0.14.1", "description": "Local-first agentic development terminal with context packs, source handles, and verification gates.", "license": "MIT OR Apache-2.0", "homepage": "https://github.com/phonton-dev/phonton-cli#readme", diff --git a/phonton-cli/Cargo.toml b/phonton-cli/Cargo.toml index 4af320f..37a07d2 100644 --- a/phonton-cli/Cargo.toml +++ b/phonton-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "phonton-cli" -version = "0.14.0" +version = "0.14.1" edition.workspace = true license.workspace = true repository.workspace = true diff --git a/phonton-cli/src/focus.rs b/phonton-cli/src/focus.rs index 3715f7d..44b107e 100644 --- a/phonton-cli/src/focus.rs +++ b/phonton-cli/src/focus.rs @@ -275,10 +275,11 @@ pub(crate) fn problems_focus_text(goal: &GoalEntry, selected_file: usize) -> Str out.push_str("\nRepair\n- Press r or run /retry to queue a repair with compact diagnostics.\n- Use /why-tokens to inspect retry/context token buckets.\n"); let groups = diff_hunks_by_file(goal); - if let Some((path, hunks)) = groups.get(selected_file.min(groups.len().saturating_sub(1))) { + let selected_group = problem_excerpt_index(&diagnostics, &groups, selected_file); + if let Some((path, hunks)) = groups.get(selected_group) { out.push_str(&format!( "\nChanged excerpt {}/{}\nfile: {}\n", - selected_file.min(groups.len().saturating_sub(1)) + 1, + selected_group + 1, groups.len(), path.display() )); @@ -309,6 +310,32 @@ pub(crate) fn problems_focus_text(goal: &GoalEntry, selected_file: usize) -> Str out } +fn problem_excerpt_index( + diagnostics: &[String], + groups: &[(PathBuf, Vec)], + selected_file: usize, +) -> usize { + if groups.is_empty() { + return 0; + } + let diagnostic_text = diagnostics + .iter() + .map(|item| item.replace('\\', "/").to_ascii_lowercase()) + .collect::>() + .join("\n"); + if let Some((idx, _)) = groups.iter().enumerate().find(|(_, (path, _))| { + diagnostic_text.contains( + &path + .to_string_lossy() + .replace('\\', "/") + .to_ascii_lowercase(), + ) + }) { + return idx; + } + selected_file.min(groups.len().saturating_sub(1)) +} + pub(crate) fn problem_diagnostics(goal: &GoalEntry) -> Vec { let mut items = Vec::new(); for record in &goal.flight_log { diff --git a/phonton-cli/src/main.rs b/phonton-cli/src/main.rs index 34410f4..15c64ff 100644 --- a/phonton-cli/src/main.rs +++ b/phonton-cli/src/main.rs @@ -9575,6 +9575,72 @@ fn extract_id(line: &str) -> Option { assert!(app.focus_text().contains("Kimi was used")); } + #[test] + fn problems_focus_prioritizes_changed_excerpt_for_diagnostic_file() { + let mut app = App::default(); + app.goals.push(GoalEntry::new("make chess".into())); + app.apply_event( + 0, + EventRecord { + task_id: TaskId::new(), + timestamp_ms: 1, + event: OrchestratorEvent::SubtaskReviewReady { + subtask_id: SubtaskId::new(), + description: "generated chess slice".into(), + tier: ModelTier::Standard, + tokens_used: 10, + token_usage: TokenUsage::estimated(10), + cost: phonton_types::CostSummary::default(), + diff_hunks: vec![ + DiffHunk { + file_path: PathBuf::from("src/chessRules.ts"), + old_start: 0, + old_count: 0, + new_start: 1, + new_count: 1, + lines: vec![DiffLine::Added("export const rules = true".into())], + }, + DiffHunk { + file_path: PathBuf::from("src/App.tsx"), + old_start: 0, + old_count: 0, + new_start: 1, + new_count: 1, + lines: vec![DiffLine::Added("```tsx".into())], + }, + ], + verify_result: VerifyResult::Fail { + layer: VerifyLayer::Syntax, + errors: vec!["[typescript syntax] src/App.tsx:1:1 invalid syntax".into()], + attempt: 1, + }, + provider: ProviderKind::OpenAiCompatible, + model_name: "fixture".into(), + }, + }, + ); + app.apply_event( + 0, + EventRecord { + task_id: TaskId::new(), + timestamp_ms: 2, + event: OrchestratorEvent::VerifyFail { + subtask_id: SubtaskId::new(), + layer: VerifyLayer::Syntax, + errors: vec!["[typescript syntax] src/App.tsx:1:1 invalid syntax".into()], + attempt: 1, + }, + }, + ); + app.apply_state(0, failed_state("syntax verification failed")); + + let text = app.focus_text(); + + assert!(text.contains("file: src/App.tsx"), "{text}"); + assert!(!text.contains("file: src/chessRules.ts"), "{text}"); + assert!(text.contains("+```tsx"), "{text}"); + } + #[test] fn problems_shortcuts_open_and_retry_failed_goal() { let mut app = App::default(); diff --git a/phonton-worker/src/lib.rs b/phonton-worker/src/lib.rs index db65f06..ae9d656 100644 --- a/phonton-worker/src/lib.rs +++ b/phonton-worker/src/lib.rs @@ -1104,7 +1104,8 @@ fn should_stop_before_generated_app_syntax_repair( if !matches!( classify_intent(&subtask.description).task_class, TaskClass::GeneratedAppGame - ) { + ) && !diagnostics_name_generated_web_artifact(&subtask.description, errors) + { return false; } let diagnostics = errors.join("\n").to_ascii_lowercase(); @@ -1116,6 +1117,28 @@ fn should_stop_before_generated_app_syntax_repair( || diagnostics.contains("src/app") } +fn diagnostics_name_generated_web_artifact(description: &str, errors: &[String]) -> bool { + let mut paths = artifact_paths_from_subtask(description); + for path in artifact_paths_from_errors(errors) { + if !paths.iter().any(|existing| existing == &path) { + paths.push(path); + } + } + paths.iter().any(|path| { + let normalized = path + .to_string_lossy() + .replace('\\', "/") + .to_ascii_lowercase(); + normalized == "index.html" + || normalized == "src/app.tsx" + || normalized == "src/app.jsx" + || normalized == "src/main.tsx" + || normalized == "src/main.jsx" + || normalized.ends_with(".tsx") + || normalized.ends_with(".jsx") + }) +} + fn repair_guidance_for_errors(errors: &[String]) -> Vec { if !looks_like_stale_hunk_error(errors) { return Vec::new(); @@ -1336,6 +1359,7 @@ fn artifact_paths_from_errors(errors: &[String]) -> Vec { .trim_end_matches(':') .trim_end_matches(',') .trim_end_matches(';'); + let cleaned = strip_diagnostic_location_suffix(cleaned); if !looks_like_relative_artifact_path(cleaned) { continue; } @@ -1348,6 +1372,16 @@ fn artifact_paths_from_errors(errors: &[String]) -> Vec { paths } +fn strip_diagnostic_location_suffix(mut value: &str) -> &str { + while let Some((head, tail)) = value.rsplit_once(':') { + if head.is_empty() || tail.is_empty() || !tail.chars().all(|ch| ch.is_ascii_digit()) { + break; + } + value = head; + } + value +} + fn looks_like_relative_artifact_path(value: &str) -> bool { if value.is_empty() || value.contains("://") { return false; @@ -2772,6 +2806,20 @@ mod tests { let _ = std::fs::remove_dir_all(base); } + #[test] + fn local_chess_rules_test_seed_does_not_require_test_runner_dependency() { + let template = include_str!("templates/chessRules.test.ts"); + + assert!( + !template.contains("from 'vitest'") && !template.contains("from \"vitest\""), + "existing Vite seed must not require adding Vitest before package.json is repaired" + ); + assert!( + template.contains("runRulesSeedTests()"), + "template should still self-execute assertions when a runner discovers the file" + ); + } + #[derive(Clone)] struct BrokenTsxProvider { calls: Arc>, @@ -2855,6 +2903,24 @@ mod tests { let _ = std::fs::remove_dir_all(base); } + #[test] + fn generated_web_syntax_fast_fail_uses_artifact_diagnostics_when_description_is_generic() { + let task = subtask("Repair verifier failure from previous attempt"); + let errors = + vec!["Verifier Syntax: [typescript syntax] src/App.tsx:1:1 invalid syntax".into()]; + + assert!( + should_stop_before_generated_app_syntax_repair( + &task, + VerifyLayer::Syntax, + &errors, + 1, + false + ), + "web artifact syntax diagnostics should stop before another broad provider repair" + ); + } + #[test] fn repeated_diagnostic_signature_ignores_attempt_noise() { let first = diagnostic_signature( diff --git a/phonton-worker/src/templates/chessRules.test.ts b/phonton-worker/src/templates/chessRules.test.ts index 033f4c0..c407188 100644 --- a/phonton-worker/src/templates/chessRules.test.ts +++ b/phonton-worker/src/templates/chessRules.test.ts @@ -1,4 +1,3 @@ -import { describe, expect, it } from 'vitest' import { createGame, createInitialGame, @@ -9,62 +8,58 @@ import { movePiece, } from './chessRules' -describe('chess rules boundary', () => { - it('creates the standard starting position', () => { - const game = createInitialGame() - const pieceCount = Object.values(game.board).filter(Boolean).length +function assert(condition: unknown, message: string): asserts condition { + if (!condition) { + throw new Error(message) + } +} - expect(pieceCount).toBe(32) - expect(game.turn).toBe('white') - expect(game.board.e1?.kind).toBe('king') - expect(game.board.e8?.kind).toBe('king') - }) +function assertIncludes(values: string[], expected: string, message: string) { + assert(values.includes(expected), message) +} - it('allows normal pawn and knight moves and enforces turn order', () => { - const game = createInitialGame() +function assertExcludes(values: string[], expected: string, message: string) { + assert(!values.includes(expected), message) +} - expect(legalMovesFor(game, 'e2')).toContain('e4') - expect(legalMovesFor(game, 'g1')).toContain('f3') +export function runRulesSeedTests() { + const initial = createInitialGame() + const pieceCount = Object.values(initial.board).filter(Boolean).length - const moved = movePiece(game, 'e2', 'e4') - expect(moved.ok).toBe(true) - if (moved.ok) { - expect(moved.state.turn).toBe('black') - expect(movePiece(moved.state, 'e4', 'e5').ok).toBe(false) - } - }) + assert(pieceCount === 32, 'starting position has 32 pieces') + assert(initial.turn === 'white', 'white moves first') + assert(initial.board.e1?.kind === 'king', 'white king starts on e1') + assert(initial.board.e8?.kind === 'king', 'black king starts on e8') - it('rejects blocked and illegal moves', () => { - const game = createInitialGame() + assertIncludes(legalMovesFor(initial, 'e2'), 'e4', 'white pawn can move two squares') + assertIncludes(legalMovesFor(initial, 'g1'), 'f3', 'white knight can move from g1 to f3') - expect(legalMovesFor(game, 'a1')).not.toContain('a4') - expect(movePiece(game, 'e2', 'e5').ok).toBe(false) - }) + const moved = movePiece(initial, 'e2', 'e4') + assert(moved.ok, 'legal pawn move succeeds') + assert(moved.state.turn === 'black', 'turn switches after legal move') + assert(!movePiece(moved.state, 'e4', 'e5').ok, 'turn order is enforced') - it('detects check and filters king moves into attacked squares', () => { - const board = emptyBoard() - board.e1 = makePiece('white', 'king') - board.a8 = makePiece('black', 'king') - board.e8 = makePiece('black', 'rook') - const game = createGame(board, 'white') + assertExcludes(legalMovesFor(initial, 'a1'), 'a4', 'rook cannot jump blocked pawns') + assert(!movePiece(initial, 'e2', 'e5').ok, 'illegal pawn move is rejected') - expect(isInCheck(game.board, 'white')).toBe(true) - expect(legalMovesFor(game, 'e1')).not.toContain('e2') - }) + const checkBoard = emptyBoard() + checkBoard.e1 = makePiece('white', 'king') + checkBoard.a8 = makePiece('black', 'king') + checkBoard.e8 = makePiece('black', 'rook') + const checkGame = createGame(checkBoard, 'white') + assert(isInCheck(checkGame.board, 'white'), 'rook gives check on open file') + assertExcludes(legalMovesFor(checkGame, 'e1'), 'e2', 'king cannot move into check') - it('promotes pawns to queens', () => { - const board = emptyBoard() - board.e1 = makePiece('white', 'king') - board.e8 = makePiece('black', 'king') - board.a7 = makePiece('white', 'pawn') - const game = createGame(board, 'white') + const promotionBoard = emptyBoard() + promotionBoard.e1 = makePiece('white', 'king') + promotionBoard.e8 = makePiece('black', 'king') + promotionBoard.a7 = makePiece('white', 'pawn') + const promotionGame = createGame(promotionBoard, 'white') + const promotion = movePiece(promotionGame, 'a7', 'a8') - const result = movePiece(game, 'a7', 'a8') + assert(promotion.ok, 'promotion move succeeds') + assert(promotion.state.board.a8?.kind === 'queen', 'pawn promotes to queen') + assert(promotion.move.promotion === 'queen', 'promotion is recorded') +} - expect(result.ok).toBe(true) - if (result.ok) { - expect(result.state.board.a8?.kind).toBe('queen') - expect(result.move.promotion).toBe('queen') - } - }) -}) +runRulesSeedTests() diff --git a/release-notes/v0.14.1.md b/release-notes/v0.14.1.md new file mode 100644 index 0000000..50d578e --- /dev/null +++ b/release-notes/v0.14.1.md @@ -0,0 +1,31 @@ +# Phonton CLI v0.14.1 + +v0.14.1 is a patch release for generated-web failure diagnostics and the +existing Vite/React chess seed path. + +## Fixed + +- Problems focus now selects the changed-file excerpt named by verifier + diagnostics. If syntax verification fails on `src/App.tsx`, the TUI shows + the `src/App.tsx` excerpt instead of the first changed file. +- Worker retry policy now strips diagnostic line/column suffixes such as + `src/App.tsx:1:1` before extracting artifact paths. Generic repair contexts + still recognize generated web artifact failures and stop before another + broad provider repair. +- The local chess rules test seed no longer imports `vitest`. It uses + self-executing assertions so existing Vite/React workspaces do not need a + package repair before seeded rule verification can run. + +## Verification + +- `cargo test -p phonton-worker local_chess_rules_test_seed_does_not_require_test_runner_dependency` +- `cargo test -p phonton-worker generated_web_syntax_fast_fail_uses_artifact_diagnostics_when_description_is_generic` +- `cargo test -p phonton-cli problems_focus_prioritizes_changed_excerpt_for_diagnostic_file` +- `cargo fmt --all -- --check` +- `cargo test --locked --workspace` +- `cargo clippy --locked --workspace --all-targets -- -D warnings` +- `cargo build --release --locked -p phonton-cli` +- `target/release/phonton.exe version` +- `target/release/phonton.exe --help` +- `npm run test:npm-wrapper` +- `npm pack --dry-run`