Repository navigation
feat(subjects): subjects evidence renders a scorecard as skill_evidence.v1 #344
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
099c2b8
005c296
d0f1704
778a1ac
4eefca5
ac2b33c
842720f
541b928
e5fa6df
581ed93
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <!-- | ||
| Copyright (C) 2026 Intel Corporation | ||
| SPDX-License-Identifier: Apache-2.0 | ||
| --> | ||
|
|
||
| ### Added | ||
|
|
||
| - **`subjects evidence SCORECARD`: a subject scorecard as a `skill_evidence.v1` document** — the file a publisher puts next to a skill so a client can read what was measured: one result per (matrix cell, non-baseline arm) with model, provider, harness, effort, and the paired test of the arm against the cell's recorded baseline arm on `avg_aggregate` (`n` pairs, delta, confidence interval, corrected p-value) and a verdict (`improved` or `regressed` only on a delta that is significant after correction, `inconclusive` otherwise). It is keyed by `subject.digest`, the scorecard's `distribution_digest`, which is what a `.well-known/agent-skills` index entry carries for the skill. Free: it reads the saved scorecard and makes no model call. Every value comes from the scorecard; one it does not hold, such as `effort` before runs record it, is `null`. Writes `skill-evidence.json` next to the scorecard unless `--out` names a file; exits 2 when the scorecard has nothing to publish or `--out` is the scorecard itself. The schema is experimental until the agentskills evidence proposal settles. | ||
| - **Matrix rollup and subject scorecard cells record `baseline_arm`**, so a reader no longer has to guess the baseline from the arm that has no delta (an arm whose every answer failed has none either). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -166,6 +166,45 @@ def cmd_subjects_run(args: argparse.Namespace) -> None: | |
| print(f"OK Saved subject report: {out_md}") | ||
|
|
||
|
|
||
| def cmd_subjects_evidence(args: argparse.Namespace) -> None: | ||
| """Render a saved subject scorecard as a skill_evidence.v1 document; no model calls.""" | ||
| from dokimi.artifacts import ArtifactValidationError, load_artifact, save_artifact | ||
| from dokimi.subjects.evidence import build_skill_evidence | ||
|
|
||
| try: | ||
| scorecard = load_artifact("subject_scorecard", Path(args.scorecard)) | ||
| except (OSError, ValueError, ArtifactValidationError) as exc: | ||
| usage_exit(f"cannot read subject scorecard {args.scorecard}: {exc}") | ||
| try: | ||
| evidence = build_skill_evidence(scorecard, source=str(args.scorecard)) | ||
| except (AttributeError, TypeError) as exc: | ||
| usage_exit(f"{args.scorecard} is not a well-formed subject scorecard: {exc}") | ||
| if not evidence["results"]: | ||
| usage_exit(f"{args.scorecard} holds no awareness arm with a baseline to compare against: " | ||
| "nothing to publish") | ||
| out = Path(args.out) if args.out else Path(args.scorecard).with_name("skill-evidence.json") | ||
| try: | ||
| out_real = out.resolve() | ||
| # save_artifact keeps and removes "<out>.rejected"; that sibling must not be the scorecard. | ||
| stale_real = out.with_name(out.name + ".rejected").resolve() | ||
|
antonwolfy marked this conversation as resolved.
Resolved
|
||
| card_real = Path(args.scorecard).resolve() | ||
| # A symlink loop raises RuntimeError before Python 3.13; "." or "/" has no name, so with_name raises ValueError. | ||
| except (OSError, RuntimeError, ValueError) as exc: | ||
| usage_exit(f"cannot resolve --out {out}: {exc}") | ||
| if out_real == card_real: | ||
| usage_exit("--out names the scorecard itself; pick another file") | ||
| if stale_real == card_real: | ||
| usage_exit(f"{out}.rejected is the scorecard; a save to {out} would delete or overwrite it; pick another --out") | ||
|
Comment on lines
+196
to
+197
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| try: | ||
| with exit_on_unwritable(out): | ||
| save_artifact("skill_evidence", evidence, out) | ||
| except ArtifactValidationError as exc: | ||
| usage_exit(f"{args.scorecard} holds a value the evidence schema rejects: {exc}") | ||
| for warning in evidence["warnings"]: | ||
| print(f"WARN {warning}", file=sys.stderr) | ||
| print(f"OK Saved skill evidence: {out} ({len(evidence['results'])} results)") | ||
|
|
||
|
|
||
| def register(sub, positive_int) -> None: | ||
| """Add the `subjects` subcommand group.""" | ||
| subjects_p = sub.add_parser( | ||
|
|
@@ -264,6 +303,15 @@ def register(sub, positive_int) -> None: | |
| ) | ||
| run_p.set_defaults(func=cmd_subjects_run) | ||
|
|
||
| evidence_p = subjects_sub.add_parser( | ||
| "evidence", | ||
| help="Render a subject scorecard as a skill_evidence.v1 document (no model calls)", | ||
| ) | ||
| evidence_p.add_argument("scorecard", help="subject_scorecard JSON written by `subjects run`") | ||
| evidence_p.add_argument("--out", default=None, | ||
| help="Output file (default: skill-evidence.json next to the scorecard)") | ||
| evidence_p.set_defaults(func=cmd_subjects_evidence) | ||
|
|
||
|
|
||
| def _run_plan(descriptor, args: argparse.Namespace) -> tuple[Path, str, list[str]]: | ||
| """The output dir, baseline arm name and per-product questions, or exit 2 when the descriptor cannot run. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Must-fix: with a valid scorecard,
dokimi subjects evidence sc.json --out .(or--out /on POSIX) reachesout.with_name(...)with an emptyout.name, which raisesValueError. This handler catches onlyOSErrorandRuntimeError, and the CLI does not catch thisValueError, so an invalid output path produces a traceback instead of a usage diagnostic (exit 2). Reject paths without a filename before constructing the sibling, or catchValueErrorhere, and add regression tests for current-directory and root output paths.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in e5fa6df: the
--outguard now catches ValueError too.with_nameon.or/(no file name) exits 2 with a usage diagnostic, no traceback. Tests--out .and--out /(red before).