diff --git a/bench/repair-loop-test.sh b/bench/repair-loop-test.sh index 65dbcf0e..5b475275 100755 --- a/bench/repair-loop-test.sh +++ b/bench/repair-loop-test.sh @@ -389,5 +389,30 @@ check "round 1 scores in REPAIR_TARGET_DIR" grep -qx "$S/kept-target" "$B-round1 check "REPAIR_TARGET_DIR is made" test -d "$S/kept-target" check "no -target is made" test ! -e "$B-target" +echo "inside a job, a pass below the bar writes the stage's findings file" +batch "c1=6 drop" "c2=4 5 3" +mkdir -p "$S/state" +JOB_STATE_DIR="$S/state" JOB_STAGE=repair loop --rounds 2 +check "loop exits 1" test "$CODE" = 1 +check "the findings file names the survivors" \ + jq -e '(.findings | any(test("2 survivor"))) and (has("rework") | not)' "$S/state/findings/repair.json" +check "it says nothing clears the bar" \ + jq -e '.findings | any(test("^nothing clears the landing bar: best .*c1.rs 6 of 10"))' "$S/state/findings/repair.json" + +echo "inside a job, a pass that left no survivors says no round can follow" +batch +rm -rf "$S/state"; mkdir -p "$S/state" +JOB_STATE_DIR="$S/state" JOB_STAGE=repair loop --rounds 2 +check "loop exits 1" test "$CODE" = 1 +check "the findings file says rework false" \ + jq -e '.rework == false and (.rework_reason | test("no survivors"))' "$S/state/findings/repair.json" + +echo "inside a job, a pass that clears the bar writes no findings file" +batch "c1=6 drop" "c2=4 10" +rm -rf "$S/state"; mkdir -p "$S/state" +JOB_STATE_DIR="$S/state" JOB_STAGE=repair loop --rounds 3 +check "loop exits 0" test "$CODE" = 0 +check "no findings file" test ! -e "$S/state/findings/repair.json" + echo "$PASS passed, $FAIL failed" [ "$FAIL" = 0 ] diff --git a/bench/repair-loop.sh b/bench/repair-loop.sh index 0d298eea..5263e0da 100755 --- a/bench/repair-loop.sh +++ b/bench/repair-loop.sh @@ -59,7 +59,10 @@ # round beats it. Every run ends by writing -survivors/ (replies/, sources/, # requests.json): the final candidate, and the other candidates as the last round carried # them forward. Inside a feature job, a round after the first (JOB_ROUND above 1, set by the job -# runner when a failed review sends the job back) continues from there instead of from the +# runner when a failed review sends the job back, or when this loop's pass ended below the +# bar and the `repair` stage's `on_fail: "rework"` starts the next round here; the loop then +# writes $JOB_STATE_DIR/findings/$JOB_STAGE.json, with `"rework": false` when it left no +# survivors, so no round starts from nothing) continues from there instead of from the # batch: on its first start in that round the loop moves -survivors/ to # -seed/, records the round in -seed/job-round, removes the earlier round's # round folders and final folder, and scores the seed as its round 0 against the cases it @@ -503,4 +506,17 @@ else status=0 fi fi +# Inside a job, a pass below the bar sends the job back for another round at this stage +# (its `on_fail: "rework"`), seeded with the survivors above; with none there is nothing to +# repair again, and the findings file says so (`"rework": false`, jobs/src/runner.rs rule 14). +if [ "$status" != 0 ] && [ -n "${JOB_STATE_DIR:-}" ] && [ -n "${JOB_STAGE:-}" ]; then + kept_count="$(find "$survivors/sources" -name '*.rs' | wc -l | tr -d ' ')" + mkdir -p "$JOB_STATE_DIR/findings" + jq -n --arg best "${final:-none} ${final_passed} of ${final_run:-?} train cases" \ + --arg bar "$base_label" --argjson kept "$kept_count" \ + '{findings: ["nothing clears the landing bar: best \($best), \($bar)", + "\($kept) survivor(s) for the next job round"]} + + (if $kept == 0 then {rework: false, rework_reason: "the repair left no survivors to repair again"} else {} end)' \ + >"$JOB_STATE_DIR/findings/$JOB_STAGE.json" +fi exit "$status" diff --git a/jobs/src/manifest.rs b/jobs/src/manifest.rs index 5de570b0..d0820c32 100644 --- a/jobs/src/manifest.rs +++ b/jobs/src/manifest.rs @@ -279,14 +279,18 @@ impl Manifest { /// The index of the stage a round started by a failure at stage `failed` begins at: /// for a review stage, [`Self::rework_from_index`]; for a script stage marked - /// `on_fail: "rework"`, the nearest writer stage before it. None for any other stage, - /// whose failure starts no round. + /// `on_fail: "rework"`, the nearest writer stage before it, or, in a manifest with no + /// writer stage, that stage itself (it comes after `rework_from`, `validate`). None for + /// any other stage, whose failure starts no round. pub fn round_start_for(&self, failed: usize) -> Option { let stage = self.stages.get(failed)?; match stage.kind.as_str() { "review" => self.rework_from_index(), "script" if stage.on_fail.as_deref() == Some(ON_FAIL_REWORK) => { - self.stages[..failed].iter().rposition(|s| s.kind == "writer") + self.stages[..failed].iter().rposition(|s| s.kind == "writer").or_else(|| { + let no_writer = !self.stages.iter().any(|s| s.kind == "writer"); + (no_writer && self.rework_from_index().is_some_and(|r| r < failed)).then_some(failed) + }) } _ => None, } @@ -928,10 +932,26 @@ fn rework_violations(obj: &Map, stages: &[Value], v: &mut Vec/findings/.json`) saying `"rework": false` (no survivors) starts none. +//! The round's start is kept as state.json's `round_start`. //! The round is handed `/rework.json` ([`REWORK_FILE`]), also kept as //! `/rounds//rework.json`; every stage gets `JOB_ROUND`, and every stage of a //! round after the first `JOB_REWORK_FILE`. A round resets every stage from its @@ -947,20 +952,32 @@ fn run_inner(opts: &Options) -> anyhow::Result { }); state::save(&state_dir, &st)?; } - // A review in a manifest without a writer stage that says no round could add a case. - let no_rework = (stage.kind == "review" - && end == StageEnd::CodeFailed - && !manifest.stages.iter().any(|s| s.kind == "writer")) - .then(|| no_rework_reason(st.review.as_ref())) - .flatten(); + // A review in a manifest without a writer stage that says no round could add a case, + // or an `on_fail` stage whose findings file says another round would have nothing to + // work from (a repair that left no survivors). + let no_writer = !manifest.stages.iter().any(|s| s.kind == "writer"); + let no_rework = if end != StageEnd::CodeFailed || !no_writer { + None + } else if stage.kind == "review" { + no_rework_reason(st.review.as_ref()) + } else if stage.on_fail.is_some() { + no_rework_reason(stage_findings_file(&state_dir, stage).as_ref()) + } else { + None + }; let (findings, notes) = round_findings(&state_dir, stage, &st, i); let start = match manifest.round_start_for(i) { - Some(r) if end == StageEnd::CodeFailed && i > r && no_rework.is_none() => r, + // `i == r`: an `on_fail` stage of a manifest without a writer stage starts its + // round at itself. + Some(r) if end == StageEnd::CodeFailed && i >= r && no_rework.is_none() => r, // Every writer of a fan_out stage failed: the round starts at that stage again. _ if end == StageEnd::CodeFailed && stage.fan_out.is_some() && stage.kind != "review" && !st.stages[i].writers.is_empty() => i, _ => { let why = match &no_rework { - Some(reason) => format!("no round: the review names nothing a round could turn into a case ({reason})"), + Some(reason) if stage.kind == "review" => { + format!("no round: the review names nothing a round could turn into a case ({reason})") + } + Some(reason) => format!("no round: {} left nothing a round could work from ({reason})", stage.name), None => format!("no round follows a failure of {}", stage.name), }; if land_earlier(&ctx, &mut st, i, rework_from, &tip_now, &why, findings)? { @@ -1166,11 +1183,8 @@ fn round_findings(state_dir: &Path, stage: &Stage, st: &State, i: usize) -> (Vec findings.push(Value::String(message.clone())); } } - let file = state_dir.join(FINDINGS_DIR).join(format!("{}.json", stage.name)); - let written: Option> = std::fs::read_to_string(&file) - .ok() - .and_then(|text| serde_json::from_str::(&text).ok()) - .and_then(|doc| doc.get("findings").and_then(Value::as_array).cloned()); + let written: Option> = + stage_findings_file(state_dir, stage).and_then(|doc| doc.get("findings").and_then(Value::as_array).cloned()); match written { Some(lines) => findings.extend(lines.into_iter().filter(Value::is_string)), None => { @@ -1181,6 +1195,12 @@ fn round_findings(state_dir: &Path, stage: &Stage, st: &State, i: usize) -> (Vec (findings, None) } +/// `/findings/.json` as this run of the stage wrote it, when it is JSON. +fn stage_findings_file(state_dir: &Path, stage: &Stage) -> Option { + let text = std::fs::read_to_string(state_dir.join(FINDINGS_DIR).join(format!("{}.json", stage.name))).ok()?; + serde_json::from_str(&text).ok() +} + /// The last [`LOG_TAIL_LINES`] lines of `log` once compiler warning blocks are dropped, so /// the lines that name a failure (errors, `FAILED`, panics, assertions, case ids) are not /// crowded out by warnings. When nothing survives the filter, the unfiltered tail. @@ -1224,7 +1244,9 @@ fn log_tail_findings(log: &str) -> Vec { /// Why `review.json` says no round could add a case: its `rework` is `false` /// (`jobs/scripts/pi-review.sh` writes that when the review's fail names no wrong input a -/// findings round could use), with its `rework_reason` or a stand-in; None otherwise. +/// findings round could use), with its `rework_reason` or a stand-in; None otherwise. The +/// same fields in an `on_fail` stage's findings file say the same of that stage +/// (`bench/repair-loop.sh` writes them when it left no survivors to repair again). fn no_rework_reason(review: Option<&Value>) -> Option { let review = review?; (review.get("rework") == Some(&Value::Bool(false))).then(|| { diff --git a/jobs/tests/manifest.rs b/jobs/tests/manifest.rs index 6692c7ad..0cafed4d 100644 --- a/jobs/tests/manifest.rs +++ b/jobs/tests/manifest.rs @@ -260,7 +260,7 @@ fn no_writer_stage_means_no_rounds() { no_writer(m); m["stages"][1]["on_fail"] = json!("rework"); }, - "on_fail is set but the manifest has no writer stage", + "on_fail is set but the manifest has no writer stage, and the stage does not come after rework_from", ); one( |m| { @@ -343,13 +343,25 @@ fn rework_from_a_script_stage_without_a_writer_stage() { }, "rework_from \"nothing\" names no stage", ); - // on_fail still needs a writer stage before it, rework_from or not. + // on_fail on a script stage after rework_from: its failure starts a round at that stage + // again (a feature job's repair below the bar, repaired again from its survivors). + let repair_rounds = |m: &mut Value| { + script_rounds(m); + m["stages"][2]["on_fail"] = json!("rework"); + }; + assert!(violations(repair_rounds).is_empty()); + let mut value = good(); + repair_rounds(&mut value); + let m: manifest::Manifest = serde_json::from_value(value).unwrap(); + assert_eq!(m.round_start_for(2), Some(2), "an on_fail stage after rework_from starts its round at itself"); + assert_eq!(m.round_start_for(3), Some(1), "a review still goes to rework_from"); + // Not on rework_from itself or before it. one( |m| { script_rounds(m); - m["stages"][2]["on_fail"] = json!("rework"); + m["stages"][1]["on_fail"] = json!("rework"); }, - "on_fail is set but the manifest has no writer stage", + "the stage does not come after rework_from", ); // With a writer stage, rework_from still names a writer. one(|m| m["rework_from"] = json!("batch"), "must name a stage of kind writer"); diff --git a/jobs/tests/runner_rounds.rs b/jobs/tests/runner_rounds.rs index 858adefa..3d134fae 100644 --- a/jobs/tests/runner_rounds.rs +++ b/jobs/tests/runner_rounds.rs @@ -949,3 +949,95 @@ fn a_later_round_failing_after_a_new_commit_does_not_land_the_earlier_candidate( assert_eq!(r["status"], "failed"); assert_eq!(r["failed_stage"], "repair"); } + +// A feature job's `repair` marked `on_fail: "rework"`: a pass below the bar starts another +// round at `repair` itself, as `bench/repair-loop.sh` seeds it from its survivors. + +/// A repair that appends its `JOB_ROUND` to `/r` and fails (below the bar) while the +/// round is below `pass_from`, writing its findings file, with `"rework": false` when +/// `/no-survivors` exists. +fn repair_on_fail(pass_from: u64) -> Value { + let script = format!( + r#"echo "$JOB_ROUND" >> "$JOB_STATE_DIR/r"; if [ "$JOB_ROUND" -lt {pass_from} ]; then if [ -f "$JOB_STATE_DIR/no-survivors" ]; then printf '%s' '{{"findings": ["nothing clears the landing bar"], "rework": false, "rework_reason": "the repair left no survivors to repair again"}}' > "$JOB_STATE_DIR/findings/$JOB_STAGE.json"; else printf '%s' '{{"findings": ["nothing clears the landing bar", "3 survivor(s) for the next job round"]}}' > "$JOB_STATE_DIR/findings/$JOB_STAGE.json"; fi; exit 1; fi"# + ); + json!({"name": "repair", "kind": "script", "on_fail": "rework", "command": ["sh", "-c", script]}) +} + +/// `prep`, `findings` (named by `rework_from`), [`repair_on_fail`], `review`. +fn repair_rounds(job: &Job, repair: Value, review: Value, rounds: u64) { + job.manifest(json!([counting("prep", "script", "p"), findings_stage(), repair, review]), move |m| { + failed_rounds(Some(rounds))(m); + m["rework_from"] = json!("findings"); + }); +} + +#[test] +fn a_repair_below_the_bar_starts_a_round_at_the_repair() { + let job = Job::new("rounds-repair-on-fail"); + repair_rounds(&job, repair_on_fail(2), review("pass"), 3); + let out = job.run(); + assert_eq!(out.status.code(), Some(0), "{}", stderr(&out)); + assert_eq!(job.count("p"), 1); + assert_eq!(lines(&job, "f"), ["1"], "the findings stages do not run again: the repair names no wrong input"); + assert_eq!(lines(&job, "r"), ["1", "2"]); + let st = job.state_json(); + assert_eq!(st["round"], 2); + assert_eq!(st["round_start"], 2, "{st}"); + assert_eq!(st["failed_rounds_used"], 1); + let rounds = st["rounds"].as_array().unwrap(); + assert_eq!(rounds[0]["sent_back_by"], "repair"); + assert_eq!(rounds[0]["findings"], json!(["nothing clears the landing bar", "3 survivor(s) for the next job round"])); + let rework = state_file_json(&job, "rounds/2/rework.json"); + assert_eq!(rework["sent_back_by"], "repair"); + let r = job.result().unwrap(); + assert_eq!(r["status"], "passed"); + assert_eq!(r["round"], 2); +} + +#[test] +fn a_repair_that_left_no_survivors_starts_no_round() { + let job = Job::new("rounds-repair-no-survivors"); + repair_rounds(&job, repair_on_fail(2), review("pass"), 3); + std::fs::create_dir_all(job.state()).unwrap(); + std::fs::write(job.state().join("no-survivors"), "").unwrap(); + let out = job.run(); + assert_eq!(out.status.code(), Some(1), "{}", stderr(&out)); + assert_eq!(lines(&job, "r"), ["1"]); + let r = job.result().unwrap(); + assert_eq!(r["status"], "failed"); + assert_eq!(r["failed_stage"], "repair"); + assert_eq!(r["round"], 1); + let st = job.state_json(); + let messages = stage(&st, "repair")["messages"].as_array().unwrap(); + assert!(messages.iter().any(|m| m.as_str().unwrap().contains("no survivors")), "{st}"); +} + +#[test] +fn repair_rounds_count_against_the_allowance() { + let job = Job::new("rounds-repair-limit"); + repair_rounds(&job, repair_on_fail(99), review("pass"), 2); + let out = job.run(); + assert_eq!(out.status.code(), Some(11), "{}", stderr(&out)); + assert_eq!(lines(&job, "r"), ["1", "2"]); + let r = job.result().unwrap(); + assert_eq!(r["status"], "needs-person"); + assert_eq!(r["failed_stage"], "repair"); +} + +#[test] +fn a_later_repair_below_the_bar_repairs_again_then_lands_the_earlier_candidate() { + // Round 1 stores a candidate and its advisory review fails; round 2 (from findings) + // and round 3 (from repair) fall below the bar; the allowance then lands round 1's. + let script = r#"echo "$JOB_ROUND" >> "$JOB_STATE_DIR/r"; if [ "$JOB_ROUND" = 1 ]; then echo "round 1" > stored.txt; git add stored.txt; git -c user.name=t -c user.email=t@localhost commit -q -m 'store round 1'; else printf '%s' '{"findings": ["below the bar"]}' > "$JOB_STATE_DIR/findings/$JOB_STAGE.json"; exit 1; fi"#; + let repair = json!({"name": "repair", "kind": "script", "on_fail": "rework", "command": ["sh", "-c", script]}); + let job = Job::new("rounds-repair-earlier"); + repair_rounds(&job, repair, advisory(review_always_failing()), 3); + let out = job.run(); + assert_eq!(out.status.code(), Some(0), "{}", stderr(&out)); + assert_eq!(lines(&job, "f"), ["1", "2"]); + assert_eq!(lines(&job, "r"), ["1", "2", "3"]); + let r = job.result().unwrap(); + assert_landed_advisory(&job, &r); + assert_eq!(r["review"]["findings"], json!(["finding in round 1"])); + assert_eq!(sh(&job.workdir(), "git log -1 --format=%s"), "store round 1"); +} diff --git a/specs/aspect-ratio.job.json b/specs/aspect-ratio.job.json index 56d92d68..1c0fed44 100644 --- a/specs/aspect-ratio.job.json +++ b/specs/aspect-ratio.job.json @@ -459,6 +459,7 @@ "name": "repair", "kind": "script", "agent": "candidates", + "on_fail": "rework", "inputs": [ "bench/repair-loop.sh", "corpus/aspect-ratio-prompt", "corpus/aspect-ratio-batch", diff --git a/specs/aspect-ratio.md b/specs/aspect-ratio.md index 844bde6d..7fe19b4a 100644 --- a/specs/aspect-ratio.md +++ b/specs/aspect-ratio.md @@ -134,7 +134,13 @@ so a stage runs alone on the development box with the same command line. round; box run `034eWRT4E70xnVxCtwTumo` reached 60 of 62 at round 8 and rounds 9 to 12 added nothing). It exits 0 when its final candidate clears the bar, preferring one that clears it over a higher score that does not, and 1 when none does. The held-out score - is recorded in `-final/held-out-report.json` and does not gate. + is recorded in `-final/held-out-report.json` and does not gate. The stage is + marked `on_fail: "rework"`: a pass below the bar starts another job round at `repair` + itself, seeded with that pass's survivors, while rounds and time remain, unless it left + no survivors (its findings file then says `"rework": false`). Folknet runs + `034f8b5uiuxtBVe7KoHglD` and `034f9KQfZJpAI8wr8ytaeY` ended failed after one pass + with 7 survivors each; `034f3V1yKaIHGKT8RHonSV` went from a best of 73 to 75 of 78 + only in a second round seeded that way. 14. `store`: `jobs/scripts/store-candidate.sh --types` copies the loop's final candidate into `crates/generation/tests/fixtures/aspect-ratio-repaired/`, named after it and the prompt's hash, with the types file it was written against under `types/`, appends its @@ -171,8 +177,9 @@ so a stage runs alone on the development box with the same command line. review passes, and also when it fails and no round follows (the three rounds of `limits.failed_rounds` used, the time left too short, or `rework: false`); the result's review then says `verdict_was` `fail` and `advisory` true, and the known-failures file goes -with the function. A round after the first whose repair or store falls below the bar lands -the earlier round's candidate instead of ending failed. Folknet run `034f3V1yKaIHGKT8RHonSV`, +with the function. A round after the first whose repair falls below the bar starts another +round at `repair` while rounds and time remain; when none follows, or when its store falls +below the bar, the job lands the earlier round's candidate instead of ending failed. Folknet run `034f3V1yKaIHGKT8RHonSV`, which stored 75 of 78 train and 15 of 16 held-out cases against the current function's 66 and ended failed on one review finding the inputs struct could not express, lands under this rule. @@ -182,8 +189,9 @@ review reruns stages 12 to 15 only: the first round's prompt and candidates stay repair continues from where the earlier round's loop ended (`-survivors`, the candidates of its last round and its final candidate), scored against the enlarged cases (`bench/repair-loop.sh` header), which hold the findings pages of every round so -far. The runner's check that the time left can hold -another round measures the earlier round from `findings-pages-prompt` on +far. A round started by a `repair` below the bar reruns stages 13 to 15 only, on the same +cases, from that pass's survivors. The runner's check that the time left can hold +another round measures the earlier round from where it started on (`jobs/src/runner.rs` rule 14). **Models.** The job names folknet's self-hosted gateway model, `qwen3.8-27b-fast-2`: the diff --git a/specs/job-review-rework.md b/specs/job-review-rework.md index dbaf8962..e3a426a7 100644 --- a/specs/job-review-rework.md +++ b/specs/job-review-rework.md @@ -42,7 +42,8 @@ violations: writer (a `substrate` writer, then a check of its work, then an `admit` writer named by `rework_from`) sends a failed check to the writer that can fix it, not to a later one and not to the start of the job. No other value is accepted, a script stage with no writer - stage before it may not carry it (there is nothing to send it back to), and neither a + stage before it may not carry it (there is nothing to send it back to) except after + `rework_from` in a manifest with no writer stage (below), and neither a writer stage (it has `writer_relaunches`) nor the review stage (it always starts a round while rounds remain) may carry it. @@ -75,8 +76,15 @@ inputs are all ones the candidate's failing train cases hand the function become with `verdict_was` `fail` (`specs/aspect-ratio.md`, the `review` stage): no round is needed, since a findings round would only add a page for what the train cases already hold. Without `rework_from`, such a manifest has nothing to send a job back to, and naming `failed_rounds`, -or `review_rounds` above 1, is a violation. `on_fail` is a violation in it either way, since -there is no writer stage before the stage. +or `review_rounds` above 1, is a violation, and so is `on_fail`. With `rework_from`, a script +stage after it may carry `on_fail: "rework"`: its failure starts a round at that stage itself, +not at `rework_from`, since it names no wrong input the findings stages could turn into a +case. A feature job's `repair` carries it: a pass below the score bar is repaired again from +its survivors (`bench/repair-loop.sh` seeds a round after the first from them), under the +same allowance and time rule. The stage's findings file (`/findings/.json`) +may say `"rework": false` with a `rework_reason`, as `review.json` may, and then no round +starts (`repair-loop.sh` writes it when the pass left no survivors). `on_fail` on +`rework_from` or a stage before it remains a violation. The review of such a manifest may be advisory (`"advisory": true` on the review stage, `jobs/src/runner.rs` rule 22; a violation on any other stage, and in a manifest with a writer @@ -298,7 +306,8 @@ stages are `sh -c` one-liners (each writing a counter file), with no lane, model stage run. - manifest: `rework_from` naming no stage, or a non-writer stage while a writer stage exists, or a non-script stage while none does; `on_fail` with another - value, on a writer or review stage, or with no writer stage before it; `failed_rounds` zero or not + value, on a writer or review stage, or with no writer stage before it (and, with no writer + stage at all, not after `rework_from`); `failed_rounds` zero or not an integer; `failed_rounds` and `review_rounds` both set; `failed_rounds` or `review_rounds` above 1 with neither a writer stage nor `rework_from`: each its own violation; a manifest without `review_rounds` passes; a manifest without the new fields is @@ -308,7 +317,11 @@ stages are `sh -c` one-liners (each writing a counter file), with no lane, model the script stages from `rework_from` with the findings in `JOB_REWORK_FILE`, the stages before it once, and passes in round 2; a script stage there that fails in round 2 ends the job failed at it; a review that always fails ends `needs-person` after the allowance; a - review saying `rework: false` starts no round and ends the job failed at review. + review saying `rework: false` starts no round and ends the job failed at review; an + `on_fail` repair that fails once starts round 2 at itself, the findings stages not rerun; + one whose findings file says `rework: false` ends the job failed at it in round 1; one that + always fails ends `needs-person` after the allowance, or, behind an advisory review that + failed a stored candidate, repairs again and then lands that candidate. - runner, advisory review (`jobs/tests/runner_rounds.rs`): a review failing every round lands after the allowance, with every stage passed, `review.json` and the result's `review` saying `verdict` `pass`, `verdict_was` `fail` and `advisory` true, and `job-check result` passing; so