From a2d4816bff697fa330c6c328d47d1f3c8febb55d Mon Sep 17 00:00:00 2001 From: Rahul Tomar Date: Fri, 6 Nov 2026 21:12:01 +0100 Subject: [PATCH] Find examples for everything: one search for every gap and slipped mistake Health gets a card that searches for examples for every untried case and every planted mistake that slipped through at once (search::find_all). Each example found is run against the targets still open and covers every one it also reaches, and later searches start from the examples found, so a few examples cover many. The results list each example as a sentence with what it covers, to untick or add together; the rest say why they weren't found. POST /api/projects/{id}/spec/find-examples. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 + apps/web/src/api.ts | 12 ++ apps/web/src/components/Design.tsx | 174 +++++++++++++++++++++++++-- apps/web/src/styles.css | 24 ++++ crates/wafer-api/src/design.rs | 41 +++++++ crates/wafer-api/src/lib.rs | 1 + crates/wafer-api/src/search.rs | 186 +++++++++++++++++++++++++++-- docs/api.md | 1 + docs/user-guide/design-view.md | 9 ++ 9 files changed, 437 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43d9557..b78c005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ All notable changes to Wafersmith are recorded here. The format follows ### Added +- **Find examples for everything**: Health searches for examples for every untried case and every + planted mistake that slipped through at once. Each example found is checked against the targets + still open, so one example covers several (on the up/down counter with one example, 2 examples + covered all 6; in the test, 3 covered 12). The examples found are listed as sentences with what + each covers, to untick or add together; those not found say why. No AI and no cost. + `POST /api/projects/{id}/spec/find-examples`. - **Why? through memories**: in "Why is it this value?", a value read from a memory (`dout = mem[rptr]`) now names the word it was (`mem[0] = 17`), and that word can be clicked like any value, back to the line that stored it (`mem[wptr] <= din`) and why it ran. The simulator records diff --git a/apps/web/src/api.ts b/apps/web/src/api.ts index edee20f..4afb925 100644 --- a/apps/web/src/api.ts +++ b/apps/web/src/api.ts @@ -289,6 +289,16 @@ export interface FoundExample { message: string | null; } +/** "Find examples for everything" (`POST .../spec/find-examples`): draft examples that between + * them cover the targets asked for, each with the targets it covers (indexes into the request), + * and why the rest weren't found. */ +export interface FoundExamples { + drafts: { example: SpecExample; covers: number[] }[]; + left: { target: number; message: string }[]; + tried: number; + elapsed_ms: number; +} + /** "Try a small fix": small repairs after which every example passes (`POST .../spec/small-fix`). */ export interface SmallFixes { fixes: SmallFix[]; @@ -671,6 +681,8 @@ export const api = { smallFix: (id: string, focus: { file: string; line: number }[]) => request("POST", `/projects/${enc(id)}/spec/small-fix`, { focus }), findExample: (id: string, target: FindTarget) => request("POST", `/projects/${enc(id)}/spec/find-example`, target), + findExamples: (id: string, targets: FindTarget[]) => + request("POST", `/projects/${enc(id)}/spec/find-examples`, { targets }), exampleTestbench: (id: string, body: { spec: Spec; example: SpecExample }) => request<{ path: string; top: string; source: string }>("POST", `/projects/${enc(id)}/spec/testbench`, body), draftSpec: (id: string, body: { instruction: string; spec?: Spec | null }) => diff --git a/apps/web/src/components/Design.tsx b/apps/web/src/components/Design.tsx index cb89252..08596ed 100644 --- a/apps/web/src/components/Design.tsx +++ b/apps/web/src/components/Design.tsx @@ -5,6 +5,7 @@ import { type Blocks, type ExampleResult, type FindTarget, + type FoundExamples, type Job, type MistakesReport, type SmallFix, @@ -24,6 +25,7 @@ import { TryIt } from "./TryIt"; import { SimClient } from "../sim/client"; import { compareExamples } from "../lib/compare"; import { BlocksStep } from "./Blocks"; +import { narrate } from "../lib/narrate"; type Step = "describe" | "examples" | "blocks" | "health"; @@ -297,14 +299,19 @@ export function DesignFlow({ projectId, startIntent, onWide, head, canEdit, onOp setExampleSeed({ text }); setStep("examples"); }} - onFoundExample={(ex) => { - if (!draft) return; - // Unsaved found examples may already use the id the server picked. + spec={draft} + onFoundExamples={(found) => { + if (!draft || !found.length) return; + // Unsaved found examples may already use the ids the server picked. const ids = new Set(draft.examples.map((e) => e.id)); - let id = ex.id; - for (let n = 1; ids.has(id); n++) id = `found_${n}`; - update({ examples: [...draft.examples, { ...ex, id }] }); - setExampleSeed({ select: id }); + const added = found.map((ex) => { + let id = ex.id; + for (let n = 1; ids.has(id); n++) id = `found_${n}`; + ids.add(id); + return { ...ex, id }; + }); + update({ examples: [...draft.examples, ...added] }); + setExampleSeed({ select: added[0].id }); setStep("examples"); }} /> @@ -1573,13 +1580,16 @@ function Health({ onShowRun, onOpenFile, onAddExample, - onFoundExample, + onFoundExamples, + spec, }: { onOpenFile: (path: string) => void; /** Opens Examples with "Add an example in words" filled in. */ onAddExample: (text: string) => void; - /** Adds an example "Find an example" found to the spec (unsaved) and opens it. */ - onFoundExample: (ex: SpecExample) => void; + /** Adds examples "Find an example" found to the spec (unsaved) and opens the first. */ + onFoundExamples: (exs: SpecExample[]) => void; + /** The spec as being edited, to describe found examples in words. */ + spec: Spec | null; projectId: string; head: string | null; /** From the spec, else the flow config's CLOCK_PERIOD. */ @@ -1609,6 +1619,7 @@ function Health({ } const [error, setError] = useState(null); const [tick, setTick] = useState(0); + const onFoundExample = (ex: SpecExample) => onFoundExamples([ex]); useEffect(() => { api.listJobs(projectId).then(setJobs, (e: Error) => setError(e.message)); @@ -1784,6 +1795,31 @@ function Health({ ), }); } + // Every case no example tries and every planted mistake that slipped through, for one search. + const report = mistakes && mistakes.head === head ? mistakes.report : null; + const findable: { target: FindTarget; text: string }[] = [ + ...(cov?.gaps ?? []).filter((g) => g.kind !== "default").map((g) => ({ target: { for: "gap" as const, file: g.file, line: g.line, text: g.text }, text: g.text })), + ...(report?.slipped ?? []).map((g) => ({ + target: { for: "mistake" as const, file: g.file, line: g.line, kind: g.kind, original: g.original, mistake: g.mistake }, + text: g.text, + })), + ].slice(0, 60); + if (canEdit && spec && findable.length > 1) { + const gapsN = findable.filter((f) => f.target.for === "gap").length; + const slippedN = findable.length - gapsN; + cards.push({ + id: "find-all", + ok: null, + title: "Find examples for everything", + plain: + `The simulator can search for examples for ${[gapsN && `the ${gapsN} untried ${gapsN === 1 ? "case" : "cases"}`, slippedN && `the ${slippedN} slipped ${slippedN === 1 ? "mistake" : "mistakes"}`].filter(Boolean).join(" and ")} above at once, using one example for several where it can. You check each one before it is added.` + + (!report && exTotal > 0 ? " Plant mistakes first to include those too." : ""), + explain: + "It searches for one target at a time, starting from your examples. Each example it finds is run against the targets still open, and every one it also covers is ticked off, so the set stays small. The checks at the end of each example are what the design did: if one looks wrong, you have found a bug.", + expert: `search per target ≤ 10 s, all ≤ 90 s; inputs from the examples, changed and fresh; cut to the shortest; covers = takes the branch / kills the mutant`, + extra: , + }); + } for (const [i, m] of (run?.machines ?? []).entries()) { const untested = m.transitions.filter((t) => t.covered === false).length; cards.push({ @@ -1911,6 +1947,124 @@ function withCode(text: string) { return text.split("`").map((part, i) => (i % 2 ? {part} : {part})); } +/** What an example covers, in a few words: "tries `up` = 0 while …", "catches `count + 1` as + * `count - 1` (line 14)". */ +function covers(item: { target: FindTarget; text: string }): string { + const t = item.target; + if (t.for === "gap") return item.text.replace(/^Never tried: /, "tries "); + if (t.kind === "forget") return `catches \`${t.original}\` left out (line ${t.line})`; + if (t.kind === "reverse") return `catches \`${t.original}\` the other way round (line ${t.line})`; + return `catches \`${t.original}\` as \`${t.mistake}\` (line ${t.line})`; +} + +/** "Find examples for everything": one search for every untried case and slipped mistake, then a + * list of the examples found to check and add together. */ +function FindAll({ + projectId, + spec, + items, + onAdd, +}: { + projectId: string; + spec: Spec; + items: { target: FindTarget; text: string }[]; + onAdd: (exs: SpecExample[]) => void; +}) { + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const [found, setFound] = useState(null); + const [skip, setSkip] = useState>(new Set()); + async function search() { + setBusy(true); + setError(null); + try { + setFound(await api.findExamples(projectId, items.map((i) => i.target))); + setSkip(new Set()); + } catch (e) { + setError((e as Error).message); + } finally { + setBusy(false); + } + } + const covered = found ? found.drafts.reduce((n, d) => n + d.covers.length, 0) : 0; + const chosen = found ? found.drafts.filter((_, i) => !skip.has(i)) : []; + return ( +
+
+ + {error && {error}} +
+ {found && ( + <> +

+ {found.drafts.length + ? `${found.drafts.length} ${found.drafts.length === 1 ? "example covers" : "examples cover"} ${covered} of ${items.length}. Read each one: the checks at the end are what the design did, so make sure that is what it should do.` + : `No example found for any of the ${items.length}.`}{" "} + + ({found.tried} input sequences tried in {found.elapsed_ms < 100 ? "under 0.1" : (found.elapsed_ms / 1000).toFixed(1)} s) + +

+ {found.drafts.length > 0 && ( +
    + {found.drafts.map((d, i) => ( +
  • + + {narrate(spec, d.example)} + + Covers: {d.covers.map((c, k) => ( + + {k > 0 && "; "} + {withCode(covers(items[c]))} + + ))} + +
  • + ))} +
+ )} + {found.left.length > 0 && ( +
+ + {found.left.length} not found + +
    + {found.left.map((u) => ( +
  • + {withCode(items[u.target].text)} + {u.message} +
  • + ))} +
+
+ )} + {found.drafts.length > 0 && ( +
+ + They open in Examples, unsaved, to save with the rest. +
+ )} + + )} +
+ ); +} + /** "Find an example": the simulator searches the design's inputs for an example that reaches the * target, which then opens in Examples as an unsaved draft. */ function FindExample({ projectId, target, onFound }: { projectId: string; target: FindTarget; onFound: (ex: SpecExample) => void }) { diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index b81558b..f01d148 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -2316,6 +2316,30 @@ select.small { text-align: right; } +/* "Find examples for everything": the examples found, to check and add. */ +.find-all-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +.find-all-list li { + display: flex; + flex-direction: column; + gap: 2px; + padding: 6px 0; + border-top: 1px solid var(--border); +} + +.find-all-pick { + display: flex; + align-items: center; + gap: 6px; +} + /* Try it: the design as a small board. */ .tryit { display: flex; diff --git a/crates/wafer-api/src/design.rs b/crates/wafer-api/src/design.rs index c228047..b7c8831 100644 --- a/crates/wafer-api/src/design.rs +++ b/crates/wafer-api/src/design.rs @@ -315,6 +315,47 @@ pub async fn find_example( found.map(Json).map_err(ApiError::Conflict) } +/// "Find examples for everything": the most targets one request may ask for, how long the search +/// for each may take, and how long all of them may. +const FIND_ALL_MAX: usize = 60; +const FIND_ALL_EACH: std::time::Duration = std::time::Duration::from_secs(10); +const FIND_ALL_BUDGET: std::time::Duration = std::time::Duration::from_secs(90); + +#[derive(Debug, Deserialize)] +pub struct FindAllReq { + pub targets: Vec, +} + +/// "Find examples for everything" (`crate::search::find_all`) at the branch tip: draft examples +/// that between them cover the coverage gaps and slipped mistakes asked for, and why the rest +/// weren't found. Nothing is saved. Editors: it costs server time. +pub async fn find_examples( + State(state): State, + user: AuthUser, + Path(id): Path, + Json(req): Json, +) -> ApiResult> { + authorize(&state, &user, id, Role::Editor).await?; + if req.targets.is_empty() || req.targets.len() > FIND_ALL_MAX { + return Err(ApiError::BadRequest(format!("ask for 1 to {FIND_ALL_MAX} targets"))); + } + let commit = head(&state, id).await?; + let files = files_at(&state, id, &commit).await?; + let spec = match files.get(SPEC_FILE) { + Some(t) => parse_spec(t)?, + None => return Err(ApiError::NotFound("this project has no spec yet".into())), + }; + let found = tokio::task::spawn_blocking(move || { + gate::with_slot(|| { + wafer_sim::on_big_stack(|| { + crate::search::find_all(&spec, &files, &req.targets, FIND_ALL_EACH, FIND_ALL_BUDGET) + }) + }) + }) + .await?; + found.map(Json).map_err(ApiError::Conflict) +} + /// How long "Try a small fix" may search. const FIX_BUDGET: std::time::Duration = std::time::Duration::from_secs(20); diff --git a/crates/wafer-api/src/lib.rs b/crates/wafer-api/src/lib.rs index 40834ef..0bd7590 100644 --- a/crates/wafer-api/src/lib.rs +++ b/crates/wafer-api/src/lib.rs @@ -76,6 +76,7 @@ pub fn router(state: AppState) -> Router { .route("/projects/{id}/spec/testbench", post(design::testbench)) .route("/projects/{id}/spec/mistakes", post(design::mistakes)) .route("/projects/{id}/spec/find-example", post(design::find_example)) + .route("/projects/{id}/spec/find-examples", post(design::find_examples)) .route("/projects/{id}/spec/small-fix", post(design::small_fix)) .route("/projects/{id}/spec/draft", post(design::draft)) .route("/starters", get(starters::list)) diff --git a/crates/wafer-api/src/search.rs b/crates/wafer-api/src/search.rs index c841750..b3b19df 100644 --- a/crates/wafer-api/src/search.rs +++ b/crates/wafer-api/src/search.rs @@ -143,6 +143,128 @@ pub fn find(spec: &Spec, files: &BTreeMap, target: &Target, budg Ok(Found { example, tried: s.tried, elapsed_ms: started.elapsed().as_millis() as u64, message }) } +/// A draft example "Find examples for everything" found, and the targets it tries or catches. +#[derive(Debug, Clone, Serialize)] +pub struct Draft { + pub example: Example, + /// Indexes into the targets asked for: the one it was searched for first, then the others it + /// covers too. + pub covers: Vec, +} + +/// A target no example was found for, and why. +#[derive(Debug, Clone, Serialize)] +pub struct Unfound { + pub target: usize, + pub message: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct FoundAll { + pub drafts: Vec, + /// In the order asked for. + pub left: Vec, + pub tried: usize, + pub elapsed_ms: u64, +} + +/// "Find examples for everything": searches for each target in turn (at most `each`, all of them +/// within `total`), and keeps each example found as a draft. An example found for one target is +/// checked against the targets still open, so one that also tries another case or catches another +/// mistake covers it, and later searches start from the examples found so far. Fails (in words) +/// only if the design doesn't parse. Call on a big stack. +pub fn find_all( + spec: &Spec, + files: &BTreeMap, + targets: &[Target], + each: Duration, + total: Duration, +) -> Result { + let started = Instant::now(); + let hdl: Vec<(&str, &str)> = files + .iter() + .filter(|(p, _)| crate::gate::is_hdl(p) && !p.starts_with("tb/")) + .map(|(p, c)| (p.as_str(), c.as_str())) + .collect(); + let (design, _) = wafer_sim::parse(&hdl).map_err(|e| format!("the design doesn't parse: {e}"))?; + let mutants: Vec>> = targets + .iter() + .map(|t| match t { + Target::Gap { .. } => None, + Target::Mistake { file, line, original, mistake, .. } => { + crate::mistakes::planted(&design, files, file, *line, original, mistake) + } + }) + .collect(); + let mut spec = spec.clone(); + let mut open: Vec = (0..targets.len()).collect(); + let (mut drafts, mut left, mut tried) = (Vec::new(), Vec::new(), 0); + while !open.is_empty() { + let i = open.remove(0); + let spent = started.elapsed(); + if spent >= total { + left.push(Unfound { + target: i, + message: "Not searched: the time for one search ran out. Add the examples found, then search again." + .into(), + }); + continue; + } + let found = match find(&spec, files, &targets[i], each.min(total - spent)) { + Ok(f) => f, + Err(message) => { + left.push(Unfound { target: i, message }); + continue; + } + }; + tried += found.tried; + let Some(ex) = found.example else { + left.push(Unfound { target: i, message: found.message.unwrap_or_default() }); + continue; + }; + let bare = Spec { examples: Vec::new(), ..spec.clone() }; + let mut covers = vec![i]; + open.retain(|&j| { + let hit = covered(&bare, &design, mutants[j].as_deref(), &ex, &targets[j]); + if hit { + covers.push(j); + } + !hit + }); + spec.examples.push(ex.clone()); + drafts.push(Draft { example: ex, covers }); + } + left.sort_by_key(|u| u.target); + Ok(FoundAll { drafts, left, tried, elapsed_ms: started.elapsed().as_millis() as u64 }) +} + +/// Whether `ex`, which passes on the design, also covers `target`: takes the gap's branch, or +/// fails with the mistake planted (`mutant`). +fn covered(spec: &Spec, design: &[Module], mutant: Option<&[Module]>, ex: &Example, target: &Target) -> bool { + match target { + Target::Gap { file, line, text } => run_one(spec, design, ex).is_some_and(|r| { + r.coverage + .iter() + .any(|(p, n)| *n > 0 && p.file == *file && p.line == *line && crate::coverage::text_of(p) == *text) + }), + Target::Mistake { .. } => match mutant { + Some(m) => run_one(spec, m, ex).is_none_or(|r| crate::gate::verdict(&r.log).0 != "pass"), + None => false, + }, + } +} + +/// Runs `ex` alone on `modules` with `spec`'s settings; None if it doesn't build or finish. +fn run_one(spec: &Spec, modules: &[Module], ex: &Example) -> Option { + let one = Spec { examples: vec![ex.clone()], ..spec.clone() }; + let tb = one.testbench(ex); + let (tbm, ts) = wafer_sim::parse(&[("tb/examples/search.v", tb.as_str())]).ok()?; + let mut all = modules.to_vec(); + all.extend(tbm); + let r = wafer_sim::run_parsed(&all, ts, &crate::spec::tb_name(&ex.id), crate::gate::limits()).ok()?; + r.finished.then_some(r) +} + struct Search<'a> { /// The spec without its examples: the search's examples are run one at a time. spec: Spec, @@ -165,13 +287,7 @@ impl Search<'_> { /// Runs `ex` on `modules`; None if it doesn't build or finish. fn run(&mut self, modules: &[Module], ex: &Example) -> Option { self.tried += 1; - let one = Spec { examples: vec![ex.clone()], ..self.spec.clone() }; - let tb = one.testbench(ex); - let (tbm, ts) = wafer_sim::parse(&[("tb/examples/search.v", tb.as_str())]).ok()?; - let mut all = modules.to_vec(); - all.extend(tbm); - let r = wafer_sim::run_parsed(&all, ts, &crate::spec::tb_name(&ex.id), crate::gate::limits()).ok()?; - r.finished.then_some(r) + run_one(&self.spec, modules, ex) } /// The example made of `segs`, then `tail`. @@ -618,6 +734,62 @@ mod tests { assert!(strong.slipped.len() < weak.slipped.len()); } + #[test] + fn finds_examples_for_everything_at_once() { + let (mut spec, f) = project(&[1]); + let tops: Vec = spec.examples.iter().map(|e| crate::spec::tb_name(&e.id)).collect(); + let check = |f: &BTreeMap, tops: &[String]| { + wafer_sim::on_big_stack(|| crate::mistakes::check(f, tops, 40, Duration::from_secs(60))).expect("checks") + }; + let weak = check(&f, &tops); + let mut targets: Vec = gaps(&spec) + .iter() + .filter(|g| g.kind != "default") + .map(|g| Target::Gap { file: g.file.clone(), line: g.line, text: g.text.clone() }) + .collect(); + let gap_count = targets.len(); + targets.extend(weak.slipped.iter().map(|s| Target::Mistake { + file: s.file.clone(), + line: s.line, + kind: s.kind.clone(), + original: s.original.clone(), + mistake: s.mistake.clone(), + })); + assert!(gap_count > 0 && targets.len() > gap_count, "the weak examples leave gaps and slipped mistakes"); + // A target that can't be found is reported, not fatal. + targets.push(Target::Gap { file: "src/updown.v".into(), line: 1, text: "Never tried: nothing".into() }); + + let all = wafer_sim::on_big_stack(|| { + find_all(&spec, &f, &targets, Duration::from_secs(10), Duration::from_secs(120)) + }) + .expect("searches"); + let mut seen: Vec = + all.drafts.iter().flat_map(|d| d.covers.clone()).chain(all.left.iter().map(|u| u.target)).collect(); + seen.sort(); + assert_eq!(seen, (0..targets.len()).collect::>(), "every target is covered once or left with a reason"); + assert!(all.drafts.len() < targets.len() - all.left.len(), "examples are shared: {:#?}", all.drafts); + assert!(all.left.iter().any(|u| u.target == targets.len() - 1 && u.message.contains("isn't in the design"))); + let ids: BTreeSet<&str> = all.drafts.iter().map(|d| d.example.id.as_str()).collect(); + assert_eq!(ids.len(), all.drafts.len(), "each draft has its own id"); + + // With the drafts added, the gaps they cover are tried and the mistakes caught. + spec.examples.extend(all.drafts.iter().map(|d| d.example.clone())); + let after = gaps(&spec); + let tops: Vec = spec.examples.iter().map(|e| crate::spec::tb_name(&e.id)).collect(); + let strong = check(&files(&spec), &tops); + for i in all.drafts.iter().flat_map(|d| d.covers.iter()) { + match &targets[*i] { + Target::Gap { text, line, .. } => { + assert!(!after.iter().any(|g| g.text == *text && g.line == *line), "{text} is still a gap") + } + Target::Mistake { original, mistake, line, .. } => assert!( + !strong.slipped.iter().any(|x| x.original == *original && x.mistake == *mistake && x.line == *line), + "{original} -> {mistake} still slips through" + ), + } + } + } + #[test] fn unreachable_targets_say_why() { let (spec, f) = project(&[1]); diff --git a/docs/api.md b/docs/api.md index 49f14ab..8862b8d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -50,6 +50,7 @@ shows how to get one locally). `GET /healthz` (outside `/api`) needs no token. | POST | `/projects/{id}/spec/run` | editor | Runs every testbench at `main`; results per example (and the other testbenches), and `coverage`: `{covered, total, gaps: [{file, line, kind, what, labels, text, suggestion}]}`, the design's branches the examples take (`null` without examples or branches), and `machines`: the design's state machines `{module, file, state, states, values, transitions: [{from, to, when, line, covered}], reset}`. | | POST | `/projects/{id}/spec/mistakes` | editor | Plants up to 40 small mistakes in the design, one at a time, and runs the examples against each (at most 30 s): `{caught, tried, sites, slipped: [{file, line, kind, original, mistake, text, suggestion}], elapsed_ms}`. `409` if an example already fails. | | POST | `/projects/{id}/spec/find-example` | editor | Searches the design's inputs for an example that reaches a target: `{"for": "gap", file, line, text}` (a coverage gap) or `{"for": "mistake", file, line, kind, original, mistake}` (a planted mistake that slipped through), as `spec/run` and `spec/mistakes` report them. Returns `{example, tried, elapsed_ms, message}`: a draft example (not saved) whose final checks hold what the design did, or `null` and why. At most about 30 s. `409` if the design doesn't parse, has no inputs, or no longer has the target. | +| POST | `/projects/{id}/spec/find-examples` | editor | "Find examples for everything": `{targets: [...]}` (1 to 60 targets, each as for `find-example`). Searches for each in turn, checks every example found against the targets still open, and returns `{drafts: [{example, covers}], left: [{target, message}], tried, elapsed_ms}`: `covers` and `target` index the request. Nothing is saved. At most about 10 s a target and 90 s in all. `409` if the design doesn't parse. | | POST | `/projects/{id}/spec/small-fix` | editor | "Try a small fix": with `{focus: [{file, line}]}` (lines to try first, optional), tries small repairs of the design at the branch tip one at a time and keeps those after which every example passes. Returns `{fixes: [{file, line, kind, original, replacement, text, edit: {before, after, content} \| null}], tried, sites, failing, elapsed_ms, message}`; `edit.content` is the whole file changed, for a commit. Nothing is saved. At most about 20 s. `409` if the design doesn't parse or every example passes already. | | POST | `/projects/{id}/spec/testbench` | viewer | `{spec, example}`: the testbench the example compiles to (`{path, top, source}`), without saving; `400` lists the problems. Used by the Try it playground. | | POST | `/projects/{id}/spec/draft` | editor | `{instruction, spec?}`: the assistant writes or revises the spec; nothing is saved. Returns `{spec, note, model, usage, usd, elapsed_ms}`; `usd` is priced for the model that served it and counts against the AI budget. | diff --git a/docs/user-guide/design-view.md b/docs/user-guide/design-view.md index 1d7d84c..a81a7b2 100644 --- a/docs/user-guide/design-view.md +++ b/docs/user-guide/design-view.md @@ -174,6 +174,15 @@ mean?** and the expert numbers underneath: The search takes at most about 30 seconds, and the same design and spec always give the same example. If nothing is found, it says why: the case may be impossible by design, or the mistake may change nothing that can be seen. +- **Find examples for everything**: when two or more cases are untried or mistakes slipped + through, one button searches for all of them at once (planted mistakes are included once you + have checked them). Each example found is run against the cases and mistakes still open, and + every one it also covers is ticked off, so a few examples usually cover many. The results list + each example as a sentence ("After reset: `down` is set to 1; 2 clock cycles pass; `count` must + be 14"), with what it covers ("tries `down && !up` is true; catches `count - 1` as `count + 1` + (line 14)"), and the ones not found with the reason. Untick any you don't want, then **Add**: + they open in Examples, unsaved, to check and save with the rest. About 10 seconds a search, 90 + at most; no AI and nothing to pay. - **How it works**: each state machine found in the Verilog, drawn as states (boxes, with the one after reset marked) and numbered moves between them, listed with when they happen ("state 0 → state 1 when `rx` = 0"). Moves an example takes are solid; dashed red ones no example takes yet;