@@ -212,7 +212,7 @@ jobs:
212212 -OutputJson phase2-results/clean/blockUpdateMs.analysis.json -Overwrite
213213 fi
214214
215- python3 - "$MANIFEST" "$SCENARIO" "$RUNS" \
215+ python3 - "$MANIFEST" "$SCENARIO" "$RUNS" "$ITEMS" \
216216 phase2-results/clean/blockUpdateChecks.evidence.json <<'PY'
217217 from pathlib import Path
218218 import csv
@@ -225,7 +225,8 @@ jobs:
225225 manifest_path = Path(sys.argv[1])
226226 scenario = sys.argv[2]
227227 expected_runs = int(sys.argv[3])
228- output_path = Path(sys.argv[4])
228+ expected_items = int(sys.argv[4])
229+ output_path = Path(sys.argv[5])
229230 records = []
230231 with manifest_path.open(encoding="utf-8", newline="") as stream:
231232 rows = list(csv.DictReader(stream))
@@ -249,6 +250,9 @@ jobs:
249250 raise SystemExit(f"invalid blockUpdateMs for {run_id}: {elapsed_ms!r}")
250251 if checks > 0 and elapsed_ms <= 0:
251252 raise SystemExit(f"{run_id} recorded {checks} checks but no elapsed time")
253+ tick_samples = metrics.get("tickSamples")
254+ if isinstance(tick_samples, bool) or not isinstance(tick_samples, int) or tick_samples <= 0:
255+ raise SystemExit(f"invalid tickSamples for {run_id}: {tick_samples!r}")
252256 expected_event_driven = row["Variant"] == "B"
253257 if metrics.get("eventDrivenBlockUpdates") is not expected_event_driven:
254258 raise SystemExit(f"event-driven config mismatch for {run_id}")
@@ -258,6 +262,8 @@ jobs:
258262 "variant": row["Variant"],
259263 "runId": run_id,
260264 "blockUpdateChecks": checks,
265+ "tickSamples": tick_samples,
266+ "checksPerTick": checks / tick_samples,
261267 "blockUpdateMs": elapsed_ms,
262268 "msPerCheck": elapsed_ms / checks if checks else None,
263269 "sourcePath": source_path.as_posix(),
@@ -266,6 +272,7 @@ jobs:
266272 def summarize(variant):
267273 selected = [record for record in records if record["variant"] == variant]
268274 checks = [record["blockUpdateChecks"] for record in selected]
275+ checks_per_tick = [record["checksPerTick"] for record in selected]
269276 times = [record["blockUpdateMs"] for record in selected]
270277 return {
271278 "runCount": len(selected),
@@ -274,18 +281,48 @@ jobs:
274281 "median": statistics.median(checks),
275282 "mean": statistics.fmean(checks),
276283 },
284+ "checksPerTick": {
285+ "values": checks_per_tick,
286+ "median": statistics.median(checks_per_tick),
287+ "mean": statistics.fmean(checks_per_tick),
288+ },
277289 "blockUpdateMs": {
278290 "values": times,
279291 "median": statistics.median(times),
280292 "mean": statistics.fmean(times),
281293 },
282294 }
283295
296+ by_variant = {"A": summarize("A"), "B": summarize("B")}
297+ cadence_guard = {
298+ "evaluated": False,
299+ "reason": "only block-active workloads with at least 100 blocks have a stable mixed-type cadence",
300+ }
301+ if scenario == "block-active" and expected_items >= 100:
302+ baseline = by_variant["A"]["checksPerTick"]["median"]
303+ candidate = by_variant["B"]["checksPerTick"]["median"]
304+ if baseline <= 0:
305+ raise SystemExit("block-active baseline recorded no checks")
306+ ratio = candidate / baseline
307+ cadence_guard = {
308+ "evaluated": True,
309+ "minimumCandidateToBaselineRatio": 0.60,
310+ "maximumCandidateToBaselineRatio": 0.85,
311+ "candidateToBaselineRatio": ratio,
312+ "passed": 0.60 <= ratio <= 0.85,
313+ "interpretation": (
314+ "The active candidate aggregate rate must remain consistent with all three furnace "
315+ "cadences while eliminating "
316+ "idle bee polling and noisy level-event invalidations."
317+ ),
318+ }
319+
284320 evidence = {
285321 "schemaVersion": 1,
286322 "analysisType": "diagnostic-work-evidence",
287323 "scenario": scenario,
288324 "requestedRuns": expected_runs,
325+ "expectedItems": expected_items,
289326 "formalComplete": expected_runs == 12,
290327 "blockUpdateChecksDirection": None,
291328 "blockUpdateMsDirection": None if scenario == "block-idle" else "LowerIsBetter",
@@ -296,7 +333,8 @@ jobs:
296333 "has no ratio analysis. Interpret all scenarios with machine-validated scene actions, "
297334 "blockUpdateMs, MSPT and TPS."
298335 ),
299- "byVariant": {"A": summarize("A"), "B": summarize("B")},
336+ "activeCadenceGuard": cadence_guard,
337+ "byVariant": by_variant,
300338 "runs": records,
301339 }
302340 output_path.write_text(
@@ -309,16 +347,30 @@ jobs:
309347 with open(step_summary, "a", encoding="utf-8", newline="\n") as stream:
310348 stream.write(f"### {scenario} block update evidence\n\n")
311349 stream.write("`blockUpdateChecks` is diagnostic work evidence (no universal direction).\n\n")
312- stream.write("| Run | Variant | Checks | Update ms | ms/check |\n")
313- stream.write("|---|---:|---:|---:|---:|\n")
350+ stream.write("| Run | Variant | Checks | Checks/tick | Update ms | ms/check |\n")
351+ stream.write("|---|---:|---:|---:|---:|---:| \n")
314352 for record in records:
315353 per_check = record["msPerCheck"]
316354 per_check_text = "n/a" if per_check is None else f"{per_check:.9f}"
317355 stream.write(
318356 f"| {record['runId']} | {record['variant']} | "
319- f"{record['blockUpdateChecks']} | {record['blockUpdateMs']:.6f} | "
357+ f"{record['blockUpdateChecks']} | {record['checksPerTick']:.6f} | "
358+ f"{record['blockUpdateMs']:.6f} | "
320359 f"{per_check_text} |\n"
321360 )
361+ if cadence_guard["evaluated"]:
362+ stream.write(
363+ "\nActive cadence guard: "
364+ f"`{'pass' if cadence_guard['passed'] else 'fail'}`; "
365+ f"candidate/baseline=`{cadence_guard['candidateToBaselineRatio']:.6f}` "
366+ "(required `0.60..0.85`).\n"
367+ )
368+
369+ if cadence_guard["evaluated"] and not cadence_guard["passed"]:
370+ raise SystemExit(
371+ "block-active candidate checks/tick escaped the expected mixed-type cadence: "
372+ f"ratio={cadence_guard['candidateToBaselineRatio']:.6f}"
373+ )
322374 PY
323375 fi
324376
0 commit comments