Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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).
57 changes: 57 additions & 0 deletions dokimi/docs/reference/cli-interface.json
Original file line number Diff line number Diff line change
Expand Up @@ -13050,6 +13050,7 @@
"'repo_environment'",
"'repo_readiness'",
"'result'",
"'skill_evidence'",
"'subject_scorecard'",
"'task_inspect'",
"'task_runs'",
Expand Down Expand Up @@ -13170,6 +13171,62 @@
"path": "dokimi subjects",
"subcommand_required": true,
"subcommands": {
"evidence": {
"allow_abbrev": true,
"params": [
{
"action": "_StoreAction",
"choices": null,
"const": null,
"default": null,
"group_key": null,
"group_required": null,
"hidden": false,
"is_flag": false,
"key": "scorecard",
"kind": "argument",
"nargs": null,
"position": 0,
"required": true,
"type": null
},
{
"action": "_HelpAction",
"choices": null,
"const": null,
"default": "==SUPPRESS==",
"group_key": null,
"group_required": null,
"hidden": false,
"is_flag": true,
"key": "--help/-h",
"kind": "option",
"nargs": 0,
"position": null,
"required": false,
"type": null
},
{
"action": "_StoreAction",
"choices": null,
"const": null,
"default": null,
"group_key": null,
"group_required": null,
"hidden": false,
"is_flag": false,
"key": "--out",
"kind": "option",
"nargs": null,
"position": null,
"required": false,
"type": null
}
],
"path": "dokimi subjects evidence",
"subcommand_required": null,
"subcommands": {}
},
"list": {
"allow_abbrev": true,
"params": [
Expand Down
1 change: 1 addition & 0 deletions dokimi/dokimi/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"matrix_rollup": "matrix_rollup.v1",
"plugin_delta": "plugin_delta.v1",
"subject_scorecard": "subject_scorecard.v2",
"skill_evidence": "skill_evidence.v1",
"novelty": "novelty.v1",
"task_runs": "task_runs.v1",
"explore_runs": "explore_runs.v1",
Expand Down
1 change: 1 addition & 0 deletions dokimi/dokimi/cli_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class Tombstone:
Leaf(("subjects", "list"), FREE),
Leaf(("subjects", "show"), FREE),
Leaf(("subjects", "run"), PAID),
Leaf(("subjects", "evidence"), FREE),
Leaf(("tasks", "list"), FREE),
Leaf(("tasks", "inspect"), FREE),
Leaf(("tasks", "harnesses"), FREE),
Expand Down
48 changes: 48 additions & 0 deletions dokimi/dokimi/commands/subjects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
Must-fix: with a valid scorecard, dokimi subjects evidence sc.json --out . (or --out / on POSIX) reaches out.with_name(...) with an empty out.name, which raises ValueError. This handler catches only OSError and RuntimeError, and the CLI does not catch this ValueError, 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 catch ValueError here, and add regression tests for current-directory and root output paths.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e5fa6df: the --out guard now catches ValueError too. with_name on . or / (no file name) exits 2 with a usage diagnostic, no traceback. Tests --out . and --out / (red before).

Comment thread
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
Must-fix: the rejected-sibling protection still misses hard links. Take the schema-valid scorecard used by the string-n rejection test, run ln sc.json evidence.json.rejected, then dokimi subjects evidence sc.json --out evidence.json. The resolved paths differ, so this passes the guard; validation fails and save_artifact writes the rejected payload with rejected.write_text(...), truncating the shared inode and replacing the input scorecard with evidence. Check existing rejected siblings with samefile() as well as resolved-path equality, and add a regression asserting the scorecard bytes remain unchanged. The main output write uses atomic replacement, so an ordinary hard-linked --out does not have this problem.

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(
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions dokimi/dokimi/eval/matrix_rollup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def build_matrix_rollup(
"harness": data.get("harness"),
"plugin_set": data.get("plugin_set", "none"),
"plugin_set_id": data.get("plugin_set_id"),
"baseline_arm": data.get("baseline_arm"),
"summary": data.get("summary", {}),
"cost_summary": data.get("cost_summary", {}),
}
Expand Down
2 changes: 2 additions & 0 deletions dokimi/dokimi/runs/kinds.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ class KindFields:
"plugin_delta": KindFields(subject=("library_name",), model=("model",), counts=("arms",)),
"subject_scorecard": KindFields(subject=("subject.id",), time=("generated_at",),
counts=("cells",)),
"skill_evidence": KindFields(subject=("subject.id",), time=("generated_at",),
counts=("results", "warnings")),
"novelty": KindFields(subject=("artifact.ref",), model=("model",), time=("generated_at",),
counts=("conflicts", "warnings")),
"task_runs": KindFields(model=("model",), time=("generated_at",),
Expand Down
6 changes: 6 additions & 0 deletions dokimi/dokimi/schemas/matrix_rollup.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@
},
"cost_summary": {
"type": "object"
},
"baseline_arm": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": true
Expand Down
Loading
Loading