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
35 changes: 34 additions & 1 deletion _shared/provenance-schema.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: provenance-schema
version: 0.1.0
version: 0.2.0
---
# Provenance Schema

Expand Down Expand Up @@ -31,6 +31,19 @@ supports same-session drift analysis only.
- The machine-readable `<!-- ai-workflow-provenance:{...} -->` comment is the stable hook
for future metrics pipelines. Human-readable lines above it are
for reviewer triage, not programmatic parsing.
- A session that starts at `/respond`, `/revise`, or a manual-edit record — with no prior
`/draft` or `commit` snapshot — sets `origin_untracked: true` and renders a disclaimer
line, since the document's structure was never verified against the template from
origin. This is independent of `provenance_kind` (see below).
- `origin_untracked` reflects only the *local* session log, not absolute ground truth.
Because `provenance.json` is gitignored (see "Published vs session-local" above), a
fresh clone, a new git worktree, a cleaned `.artifacts/`, or a CI runner all start that
log from empty. If the next captured event on that empty log is `respond`, `revise`, or
`manual-edit`, `origin_untracked` becomes `true` even when the document was genuinely
drafted in an earlier session whose log no longer exists. It also does not self-heal:
`capture_event` only appends, so a later `/draft` run lands past `events[0]` and leaves
the flag set. Recovery requires deleting the stale `provenance.json` before the next
`/draft` run so the log restarts cleanly with `draft` as its first event.

## `provenance.json` (schema_version 1)

Expand Down Expand Up @@ -93,6 +106,15 @@ Recomputed after every capture. Compares the first and last events on:
| `commit_only` | Log has only `commit` events (auto-captured at publish) | Committed line + disclaimer |
| `declined` | User bypassed via `--allow-missing` | None — machine comment only |

`origin_untracked` is a separate, orthogonal boolean (not a `provenance_kind` value):
always `false` for a `commit_only` log (a bare commit-time snapshot with no
authoring history yet, already covered by its own disclaimer); otherwise `true`
unless the log's very first-ever event was `draft`. This covers both a session
that starts at `/respond`, `/revise`, or a manual-edit record, and a log that
started as a `commit_only` snapshot and later picked up authoring events (e.g.
`commit` followed by `revise`) — in both cases the document's structure was
never checked against the template from origin.

**Session — single context** (no drift):

```markdown
Expand Down Expand Up @@ -121,6 +143,17 @@ Final: revise @ prd 0.5.0 - adfad68, workspace main @ 00e78b8f
> Context changed between draft and revise.
```

**Session — untracked origin** (first-ever event is `respond`, `revise`, or
`manual-edit` — no prior `/draft` or commit snapshot):

```markdown
Authored: respond @ prd 0.5.0 - adfad68, workspace main @ 00e78b8f

> This document's phase history does not include an initial /draft — structure was not verified against the template from origin.

<!-- ai-workflow-provenance:{"schema_version":1,"provenance_kind":"session","phases":["respond"],"origin_untracked":true,...} -->
```

**Commit-only** (no authoring phases recorded this session):

```markdown
Expand Down
17 changes: 17 additions & 0 deletions _shared/scripts/provenance.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
COMMIT_ONLY_NOTE = (
"> Authoring phases not recorded this session (commit-time snapshot only)."
)
ORIGIN_UNTRACKED_NOTE = (
"> This document's phase history does not include an initial /draft — "
"structure was not verified against the template from origin."
)


def repo_root(start: Path) -> Path | None:
Expand Down Expand Up @@ -256,6 +260,14 @@ def provenance_kind(events: list[dict[str, Any]]) -> str:
return "session"


def origin_untracked(events: list[dict[str, Any]]) -> bool:
if not events:
return False
if provenance_kind(events) == "commit_only":
return False
return events[0].get("phase") != "draft"


def capture_event(
workflow: str,
issue: str,
Expand Down Expand Up @@ -356,6 +368,7 @@ def build_metrics_payload(data: dict[str, Any]) -> dict[str, Any]:
{event.get("authoring_mode", "skill") for event in events}
),
"context_changed": drift.get("context_changed", False),
"origin_untracked": origin_untracked(events),
}


Expand Down Expand Up @@ -390,6 +403,10 @@ def build_footer(data: dict[str, Any]) -> str:
if len(phases) > 1:
lines.append(f"Phases: {', '.join(phases)}")

if origin_untracked(events):
lines.append("")
lines.append(ORIGIN_UNTRACKED_NOTE)

lines.append("")
lines.append(metrics_comment)
return "\n".join(lines) + "\n"
Expand Down
Loading
Loading