ordel-cli 0.4.0__tar.gz → 0.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/PKG-INFO +3 -3
  2. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/SKILL.md +20 -7
  3. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/adopt.py +23 -8
  4. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/cli.py +84 -7
  5. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/ingest.py +12 -2
  6. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/mcp_server.py +171 -38
  7. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/qa_plan.py +1 -1
  8. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/record.py +106 -15
  9. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/references/scenarios-and-oracles.md +18 -3
  10. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/references/tools.md +14 -6
  11. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/runner.py +9 -0
  12. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/scenarios.py +36 -3
  13. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/store.py +35 -2
  14. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/pyproject.toml +3 -3
  15. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/.gitignore +0 -0
  16. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/CATALOG.md +0 -0
  17. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/LICENSE +0 -0
  18. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/PRIVACY.md +0 -0
  19. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/README.md +0 -0
  20. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/__init__.py +0 -0
  21. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/branding.py +0 -0
  22. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/capture.cjs +0 -0
  23. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/capture_extract.cjs +0 -0
  24. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/catalog.py +0 -0
  25. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/context.py +0 -0
  26. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/coverage.py +0 -0
  27. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/doctor.py +0 -0
  28. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/explore.py +0 -0
  29. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/heal_service.py +0 -0
  30. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/human_record.cjs +0 -0
  31. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/mcp_supervisor.cjs +0 -0
  32. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/ordel_config.cjs +0 -0
  33. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/ordel_evidence.cjs +0 -0
  34. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/pom.py +0 -0
  35. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/py.typed +0 -0
  36. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/record.cjs +0 -0
  37. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/references/communication-and-harnesses.md +0 -0
  38. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/references/execution-and-triage.md +0 -0
  39. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/references/intake-and-readiness.md +0 -0
  40. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/references/maintenance.md +0 -0
  41. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/session.py +0 -0
  42. {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/session_driver.cjs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ordel-cli
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Ordel free CLI — local QA-automation for individual devs. Drives the deterministic ordel-engine and exposes it to a BYO coding agent over MCP. No account, no DB, no Ordel LLM.
5
5
  Project-URL: Homepage, https://ordel.io
6
6
  Author: Ordel
@@ -14,8 +14,8 @@ Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Topic :: Software Development :: Testing
15
15
  Requires-Python: >=3.11
16
16
  Requires-Dist: mcp<2.0,>=1.0
17
- Requires-Dist: ordel-engine==0.4.0
18
- Requires-Dist: ordel-tools==0.4.0
17
+ Requires-Dist: ordel-engine==0.4.2
18
+ Requires-Dist: ordel-tools==0.4.2
19
19
  Requires-Dist: typer>=0.12
20
20
  Provides-Extra: dev
21
21
  Requires-Dist: hatchling; extra == 'dev'
@@ -68,7 +68,7 @@ failure means. Three rules hold throughout:
68
68
  | `pass` | ran, and its assertions (listed in `evidence.checks`) held | passed those checks on that target |
69
69
  | `fail` | ran and failed; `message` says why | a failure to triage: app, expectation, test, data or environment |
70
70
  | `flaky` | failed, then passed on retry | unstable; never a stable pass |
71
- | `unjudged` | green in Playwright but NOT proven: no assertion ran, no evidence was captured, or it depends on a heal no human has reviewed (`reasons` says which) | **not proven**: never "passed" |
71
+ | `unjudged` | green in Playwright but NOT proven: no assertion ran, no evidence was captured, it depends on a heal no human has reviewed, or what it found after a sign-in a visitor who never signed in sees too (`reasons` says which) | **not proven**: never "passed" |
72
72
  | `skipped` | did not run | skipped, and why |
73
73
 
74
74
  A run whose `status` is `no_tests` checked nothing, and `error` means the runner or its setup
@@ -80,15 +80,21 @@ failed: neither is "all green". Before you call a new test done, run it and conf
80
80
  These are where agents most often go wrong. The references expand on them.
81
81
 
82
82
  - **Identity is an expectation, not an observation.** For a flow that signs in,
83
- `generate_scenarios` adds an identity check (the signed-in page names the account used) and a
83
+ `generate_scenarios` adds an identity check: the sign-in took effect (its form is gone) and
84
+ the signed-in page names the account used. A name that a visitor who never signed in also
85
+ sees at that address proves no session, so the check reports `unjudged`: assert an element
86
+ only a signed-in page shows. When sign-in lands somewhere other than the site root, it adds a
84
87
  signed-out check (a visitor who never signed in is not served that page). If identity fails
85
88
  because the app shows a display name, set THIS account's name from the user or the
86
89
  requirements; never copy whatever name the page shows ("Welcome back, Dana Smith" for user
87
90
  `agent` is a finding, not the expected value).
88
- - **A rejection needs the app's own rejection.** A negative test passes on an error message, a
89
- field marked invalid, a 4xx, or staying on the form; a 5xx or an error page is a crash, which
90
- is a finding, never a correct rejection. The browser blocking a form is not the server
91
- validating it.
91
+ - **A rejection needs the app's own rejection, and its effect.** A negative test passes on an
92
+ error message, a field marked invalid, a 4xx, or staying on the form; a 5xx or an error page
93
+ is a crash, which is a finding, never a correct rejection. Staying on the form is not enough
94
+ for a sign-in: rejected means no session, so the signed-in page must stay closed, or, when
95
+ any visitor may open it, must not name the account (the generated sign-in negatives check
96
+ this). A denial that crashes (5xx) is not a denial. The
97
+ browser blocking a form is not the server validating it.
92
98
  - **Only confirmed rules are defects.** A failure against a rule you inferred (a format you
93
99
  expected, a limit you guessed) is a question for the user, listed separately from defects.
94
100
  - **Never silence a failure.** Do not `test.skip`, delete an assertion or widen a pattern to get
@@ -98,7 +104,14 @@ These are where agents most often go wrong. The references expand on them.
98
104
  add it to `.ordel/secrets.env` (gitignored; `run_test` loads it). If the user is not
99
105
  available and the app itself shows the credential publicly (a demo account printed on its
100
106
  sign-in page), you may add it there yourself and say so. Never put a secret in a spec,
101
- another file or the chat, and never print it in a report.
107
+ another file or the chat, and never print it in a report. `run_test` lists what it finds:
108
+ `secret_leaks` (a secrets-file value in a file) and `hardcoded_secrets` (a spec line that
109
+ types a literal into a password field); move those to `.ordel/secrets.env`.
110
+ For interactive fills, pass `fill(env="ORDEL_SECRET_AGENT_PASSWORD", testid="password-input")`
111
+ instead of a literal `value`. The server resolves the variable from its environment or
112
+ `.ordel/secrets.env`; never read the value into an agent-visible command or tool argument.
113
+ For recording, an env-only fill step (`{"op": "fill", "testid": "password-input",
114
+ "env": "ORDEL_SECRET_AGENT_PASSWORD"}`) resolves on the server too. Leave out `value`.
102
115
  - **Coverage is not correctness.** `coverage_report` counts which mapped elements some spec
103
116
  touches; it says nothing about whether those checks pass.
104
117
 
@@ -180,7 +180,17 @@ def adopt(store: OrdelStore, path: str = "", runs: int = 3,
180
180
  report["ingest"] = {k: v for k, v in ingest(store, path).items() if k != "ascii"}
181
181
 
182
182
  playwright = find_playwright(store.root) is not None
183
- history = [r for r in _judged_runs(store) if r.get("schema") == RUN_SCHEMA]
183
+ # The selection bounds both what runs and which runs count as its history: a run of the
184
+ # whole project (or of another path) says nothing about this suite's flakiness, and running
185
+ # everything executed tests the user deliberately left out (the 0.4.0 audit's G2).
186
+ selection = Path(path).as_posix() if path else ""
187
+ report["selection"] = selection or "(whole project)"
188
+
189
+ def _selected(runs: list[dict[str, Any]]) -> list[dict[str, Any]]:
190
+ return [r for r in runs if r.get("schema") == RUN_SCHEMA
191
+ and str(r.get("spec") or "") == selection]
192
+
193
+ history = _selected(_judged_runs(store))
184
194
  executed: list[str] = []
185
195
  if not playwright:
186
196
  report["notes"].append("no @playwright/test in this project: runs, flake and heal "
@@ -189,20 +199,22 @@ def adopt(store: OrdelStore, path: str = "", runs: int = 3,
189
199
  else:
190
200
  if runs and len(history) >= runs:
191
201
  report["notes"].append(
192
- f".ordel/ already holds {len(history)} judged run(s), so nothing was run: --runs "
193
- f"builds history up to that many. Pass --runs {len(history) + 1} to run once more")
202
+ f".ordel/ already holds {len(history)} judged run(s) of {report['selection']}, "
203
+ f"so nothing was run: --runs builds that selection's history up to that many. "
204
+ f"Pass --runs {len(history) + 1} to run once more")
194
205
  for _ in range(max(0, runs - len(history))):
195
- doc = run_and_record(store, base_url=base_url)
206
+ doc = run_and_record(store, spec=selection or None, base_url=base_url)
196
207
  if doc["status"] == "error":
197
208
  report["notes"].append(f"run failed: {doc.get('error')}")
198
209
  break
199
210
  executed.append(str(doc["run_id"]))
200
211
  report["runs_executed"] = executed
201
212
 
202
- all_runs = _judged_runs(store)
213
+ all_runs = _selected(_judged_runs(store))
214
+ report["history"] = [str(r.get("run_id")) for r in all_runs]
203
215
  report["flake"] = build_flake_report(all_runs)
204
216
 
205
- latest = next((r for r in reversed(all_runs) if r.get("schema") == RUN_SCHEMA), None)
217
+ latest = next(reversed(all_runs), None)
206
218
  heal_items: list[dict[str, Any]] = []
207
219
  failures = [v for v in (latest or {}).get("verdicts", []) or [] if v.get("status") == "fail"]
208
220
  if latest is not None:
@@ -224,7 +236,8 @@ def adopt(store: OrdelStore, path: str = "", runs: int = 3,
224
236
 
225
237
  def render_adopt(report: dict[str, Any]) -> str:
226
238
  ing = report["ingest"]
227
- lines = [f"Adopted: {ing.get('specs_ingested', 0)} functional spec(s) -> "
239
+ lines = [f"Adopted {report.get('selection', '(whole project)')}: "
240
+ f"{ing.get('specs_ingested', 0)} functional spec(s) -> "
228
241
  f"{ing.get('flows_created', 0)} flow(s)"
229
242
  + (f" ({ing['cypress_specs']} Cypress)" if ing.get("cypress_specs") else "")]
230
243
  if ing.get("unparsed_actions"):
@@ -235,7 +248,9 @@ def render_adopt(report: dict[str, Any]) -> str:
235
248
  c = fl["counts"]
236
249
  just_ran = (f", {len(report['runs_executed'])} just executed" if report["runs_executed"]
237
250
  else "")
238
- lines += ["", f"Flake report ({fl['runs_considered']} judged run(s){just_ran}):",
251
+ runs_used = ", ".join(report.get("history") or []) or "none"
252
+ lines += ["", (f"Flake report ({fl['runs_considered']} judged run(s) of this selection"
253
+ f"{just_ran}: {runs_used}):"),
239
254
  (f" flaky {c['flaky']} regressed {c['regressed']} failing {c['failing']} "
240
255
  f"recovered {c['recovered']} stable {c['stable']} "
241
256
  f"too few runs {c['insufficient']}")]
@@ -15,6 +15,7 @@ import json
15
15
  import os
16
16
  import shutil
17
17
  import sys
18
+ from datetime import UTC, datetime
18
19
  from importlib.metadata import version as _dist_version
19
20
  from pathlib import Path
20
21
  from typing import Annotated, Any
@@ -321,11 +322,14 @@ def coverage(fmt: str = typer.Option("ascii", help="ascii | markdown | mermaid")
321
322
 
322
323
 
323
324
  @app.command()
324
- def scenarios(flow_name: str, show: bool = typer.Option(False, help="print the spec source")) -> None:
325
+ def scenarios(flow_name: str, show: bool = typer.Option(False, help="print the spec source"),
326
+ repeat_submit: str | None = typer.Option(
327
+ None, help="what a second submit of the same record must do: refused | "
328
+ "same-record | new-record (kept on the flow)")) -> None:
325
329
  """From a recorded flow, emit happy+NEGATIVE+boundary+replay tests that hunt bugs."""
326
330
  from ordel_cli.scenarios import generate_scenarios
327
331
  store = _store()
328
- res = generate_scenarios(store, flow_name)
332
+ res = generate_scenarios(store, flow_name, repeat_submit=repeat_submit)
329
333
  if res["status"] != "generated":
330
334
  typer.echo(res.get("note", res["status"]))
331
335
  raise typer.Exit(1)
@@ -404,7 +408,7 @@ def plan() -> None:
404
408
  def regression(
405
409
  pages: Annotated[list[str] | None, typer.Argument(help="changed page_ids (omit=all)")] = None,
406
410
  ) -> None:
407
- """The exact specs to re-run after a change (impact selection)."""
411
+ """Candidate specs to re-run after a change: those touching the changed pages."""
408
412
  from ordel_cli.qa_plan import regression_set
409
413
  res = regression_set(_store(), list(pages) if pages else None)
410
414
  typer.echo(res.get("ascii", res.get("note", res["status"])))
@@ -416,8 +420,10 @@ def report(
416
420
  ) -> None:
417
421
  """Emit a copy-pasteable Markdown report (Slack/Teams/Jira) of what `.ordel/` can prove.
418
422
 
419
- Every line is a number read from the local store: the map, the last recorded run's
420
- verdicts and the heals still waiting for review. It makes no claim the store can't back."""
423
+ Every line is read from the local store: the map, the last recorded run's verdicts with
424
+ links to its evidence, the heals still waiting for review, the mapped pages no spec
425
+ touches, and a pointer to the QA context your agent keeps (.ordel/qa-context.md), which
426
+ Ordel shows but does not verify. It makes no claim the store can't back."""
421
427
  store = _store()
422
428
  if not store.exists:
423
429
  typer.echo("no .ordel/ here — run `ordel init`")
@@ -441,9 +447,49 @@ def report(
441
447
  if last is not None:
442
448
  doc["last_run"] = {k: last.get(k) for k in (
443
449
  "run_id", "at", "status", "passed", "failed", "flaky", "unjudged", "skipped")}
450
+ doc["last_run"]["record"] = f".ordel/runs/{runs[-1]}.json"
451
+ verdicts = last.get("verdicts", []) or []
452
+
453
+ def _where(v: dict[str, Any]) -> str:
454
+ # Playwright names files relative to its testDir; the report links project paths
455
+ f = str(v.get("file") or "")
456
+ if f and not (store.root / f).exists() and (store.root / "tests" / f).exists():
457
+ f = f"tests/{f}"
458
+ return f"{f}:{v.get('line')}"
459
+
444
460
  doc["last_run"]["unjudged_tests"] = [
445
461
  {"file": v.get("file"), "title": v.get("title"), "reasons": v.get("reasons", [])}
446
- for v in last.get("verdicts", []) or [] if v.get("status") == "unjudged"]
462
+ for v in verdicts if v.get("status") == "unjudged"]
463
+ doc["last_run"]["failed_tests"] = [
464
+ {"where": _where(v), "title": v.get("title"),
465
+ "message": str(v.get("message") or "").splitlines()[0][:160] if v.get("message") else ""}
466
+ for v in verdicts if v.get("status") == "fail"]
467
+ doc["last_run"]["skipped_tests"] = [
468
+ {"where": _where(v), "title": v.get("title"),
469
+ "reasons": v.get("reasons", [])}
470
+ for v in verdicts if v.get("status") == "skipped"]
471
+ doc["pending_heals"] = [
472
+ {"id": h.get("id"), "spec": h.get("spec"),
473
+ "change": f"{h.get('element_id')} -> {h.get('healed_to')}", "exact": bool(h.get("exact"))}
474
+ for h in pending]
475
+ from ordel_cli.coverage import coverage_report as _cov
476
+ cov = _cov(store, force=True)
477
+ doc["untested_pages"] = list((cov.get("summary") or {}).get("untested_pages") or [])
478
+ ctx = store.dir / "qa-context.md"
479
+ if ctx.is_file():
480
+ text = ctx.read_text(encoding="utf-8", errors="replace")
481
+ doc["qa_context"] = {
482
+ "path": ".ordel/qa-context.md",
483
+ "updated": datetime.fromtimestamp(ctx.stat().st_mtime, UTC).isoformat(
484
+ timespec="seconds"),
485
+ "sections": [ln.lstrip("# ").strip() for ln in text.splitlines()
486
+ if ln.startswith("## ")]}
487
+ else:
488
+ doc["qa_context"] = None
489
+ doc["limits"] = ("a pass means its listed checks held on that run's target and build; "
490
+ "mapped pages are what was captured, not the whole app; expectations, "
491
+ "readiness and selections live in the QA context, which Ordel does not "
492
+ "verify")
447
493
  if as_json:
448
494
  typer.echo(json.dumps(doc, indent=2))
449
495
  return
@@ -470,10 +516,35 @@ def report(
470
516
  lines.append(f"| Tests flaky | {lr['flaky']} |")
471
517
  lines.append(f"| Tests unjudged | {lr['unjudged']} |")
472
518
  lines.append(f"| Heals pending human review | {len(pending)} of {len(heals)} |")
519
+ if lr and lr.get("at"):
520
+ lines.append(f"| Last run at | {lr['at']} (evidence: `{lr['record']}`) |")
521
+ qc = doc["qa_context"]
522
+ lines.append(f"| QA context | `{qc['path']}`, updated {qc['updated']} |" if qc else
523
+ "| QA context | none saved: expectations, readiness and selections are not "
524
+ "recorded |")
525
+ failed = (lr or {}).get("failed_tests") or []
526
+ if failed:
527
+ lines += ["", "Failed (triage before calling any of these a defect):"]
528
+ lines += [f"- `{f['where']}` {f['title']}: {f['message']}" for f in failed]
473
529
  unjudged = (lr or {}).get("unjudged_tests") or []
474
530
  if unjudged:
475
531
  lines += ["", "Unjudged (green in Playwright, not proven by Ordel):"]
476
532
  lines += [f"- `{u['file']}` {u['title']}: {'; '.join(u['reasons'])}" for u in unjudged]
533
+ skipped = (lr or {}).get("skipped_tests") or []
534
+ if skipped:
535
+ lines += ["", "Skipped (not run):"]
536
+ lines += [f"- `{k['where']}` {k['title']}"
537
+ + (f": {'; '.join(k['reasons'])}" if k["reasons"] else "") for k in skipped]
538
+ if doc["pending_heals"]:
539
+ lines += ["", "Heals waiting for review (`ordel adopt --apply|--reject <id>`):"]
540
+ lines += [f"- {h['id']} `{h['spec']}`: {h['change']}"
541
+ + (" (exact match)" if h["exact"] else "") for h in doc["pending_heals"]]
542
+ if doc["untested_pages"]:
543
+ lines += ["", f"Mapped pages no spec touches: {', '.join(doc['untested_pages'])}"]
544
+ if qc and qc["sections"]:
545
+ lines += ["", (f"QA context sections (written by your agent, not verified by Ordel): "
546
+ f"{', '.join(qc['sections'])}")]
547
+ lines += ["", f"_Limits: {doc['limits']}._"]
477
548
  typer.echo("\n".join(lines))
478
549
 
479
550
 
@@ -693,7 +764,8 @@ def record(
693
764
  typer.echo(" no assertion recorded: `ordel run` will report this test unjudged; "
694
765
  "Alt+click the result you expect next time")
695
766
  for var in res["needs_env"]:
696
- typer.echo(f" set {var} to run it: a password was typed and is never stored")
767
+ typer.echo(f" add {var}=<value> to .ordel/secrets.env to run it: a password was "
768
+ "typed and is never stored")
697
769
  raise typer.Exit(0 if res["status"] == "recorded" else 1)
698
770
 
699
771
 
@@ -811,6 +883,8 @@ def _render_run(doc: dict[str, Any]) -> str:
811
883
  f"skipped={doc.get('skipped', 0)}")]
812
884
  if status == "no_tests":
813
885
  lines.append(" no test executed - nothing was checked, so nothing passed")
886
+ for hit in doc.get("hardcoded_secrets") or []:
887
+ lines.append(f" SECRET IN A SPEC: {hit}: read it from .ordel/secrets.env instead")
814
888
  if doc.get("secret_leaks"):
815
889
  lines.append(f" SECRET IN A FILE: a value from .ordel/secrets.env is written in "
816
890
  f"{', '.join(doc['secret_leaks'])}: read it from the environment instead")
@@ -824,6 +898,9 @@ def _render_run(doc: dict[str, Any]) -> str:
824
898
  where = f"{v.get('file')}:{v.get('line')} › {v.get('title')}"
825
899
  detail = v.get("message") if v.get("status") == "fail" else "; ".join(v.get("reasons", []))
826
900
  lines.append(f" {mark} {where}" + (f": {detail}" if detail else ""))
901
+ if v.get("status") == "fail" and v.get("unresolved_locator"):
902
+ lines.append(f" locator never found: {v['unresolved_locator']} (heal_selector can "
903
+ "look for its replacement; a person reviews the heal)")
827
904
  checked = sum((v.get("evidence") or {}).get("assertions") or 0
828
905
  for v in verdicts if v.get("status") == "pass")
829
906
  if doc.get("passed"):
@@ -47,6 +47,7 @@ def ingest(store: OrdelStore, path: str = "") -> dict[str, Any]:
47
47
  if fn.lower().endswith(_SPEC_SUFFIXES)]
48
48
 
49
49
  ingested = flows_created = unparsed = skipped_unit = skipped_ordel = cypress_specs = 0
50
+ kept_asserts = dropped_asserts = 0
50
51
  created: list[str] = []
51
52
  capped = False
52
53
  for f in sorted(targets):
@@ -79,10 +80,15 @@ def ingest(store: OrdelStore, path: str = "") -> dict[str, Any]:
79
80
  n += 1
80
81
  uniq = f"{name[:60]}-{n}"
81
82
  start = next((s["url"] for s in t["steps"] if s.get("op") == "goto"), "")
83
+ kept = int(t.get("assertions_kept", 0))
84
+ dropped = int(t.get("assertions_dropped", 0))
85
+ kept_asserts += kept
86
+ dropped_asserts += dropped
82
87
  store.save_flow(uniq, {"name": uniq, "source": "ingested",
83
88
  "framework": "cypress" if cypress else "playwright",
84
89
  "spec": f.name, "spec_path": f.relative_to(root).as_posix(),
85
- "start": start, "final_url": "", "steps": t["steps"]})
90
+ "start": start, "final_url": "", "steps": t["steps"],
91
+ "assertions_kept": kept, "assertions_dropped": dropped})
86
92
  created.append(uniq)
87
93
  flows_created += 1
88
94
  if capped:
@@ -98,6 +104,9 @@ def ingest(store: OrdelStore, path: str = "") -> dict[str, Any]:
98
104
  lines.append(f" skipped {skipped_ordel} Ordel-generated spec(s) (already ours)")
99
105
  if unparsed:
100
106
  lines.append(f" {unparsed} action line(s) used idioms we couldn't parse — re-record those flows")
107
+ lines.append(f" assertions carried into the flows: {kept_asserts}"
108
+ + (f"; NOT carried: {dropped_asserts} (the original specs still check them, "
109
+ "the generated scenarios do not)" if dropped_asserts else ""))
101
110
  if capped:
102
111
  lines.append(f" stopped at the {_MAX_FLOWS}-flow cap — ingest a subdir at a time for the rest")
103
112
  if flows_created:
@@ -106,4 +115,5 @@ def ingest(store: OrdelStore, path: str = "") -> dict[str, Any]:
106
115
  return {"status": "ok", "specs_ingested": ingested, "flows_created": flows_created,
107
116
  "flows": created, "cypress_specs": cypress_specs,
108
117
  "skipped_unit": skipped_unit, "skipped_ordel": skipped_ordel,
109
- "unparsed_actions": unparsed, "ascii": "\n".join(lines)}
118
+ "unparsed_actions": unparsed, "assertions_kept": kept_asserts,
119
+ "assertions_dropped": dropped_asserts, "ascii": "\n".join(lines)}