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
2 changes: 1 addition & 1 deletion plugins/provenance/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "provenance",
"version": "0.5.6",
"version": "0.5.7",
"description": "Finds prose in tracked markdown that restates content an external source owns (vendor docs, blogs, articles) without adequate attribution, confirms the source, and refactors the copy into a pointer, a citation, or a dated stamped record. Documentation provenance, not software supply chain. Nomination and judgment are LLM work; the scripts do only reasoning-free work (corpus scoping, breadcrumb extraction, stamp expiry, fingerprint compare of two concrete texts). Read-only audit by default; explicit fix and sweep actions apply dispositions behind a semantic-diff guard and live pointer verification. Findings conform to the detector-findings convention.",
"author": {
"name": "Melodic Software",
Expand Down
30 changes: 30 additions & 0 deletions plugins/provenance/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# Changelog

## [0.5.7]

### Fixed

- **`audit`:** the skill published one spelling of the neutral tier and described another.
`SKILL.md`'s frontmatter listed the tier as `source-not-identified`; the rubric, the
dispositions and source-fetch references, `context/persist-findings.md`, the plugin README and
the script's own prose all say `not-found`. `SKILL.md` now says `not-found` too, so one name is
published. `emit-findings.sh` still recognizes both spellings, deliberately and permanently: a
sidecar is model-authored against whatever description was in context, and recognizing one name
too many can only withhold a record, while one too few walks a judgment verdict onto a relay
row. The comments beside the two predicates and the tests that exercise both spellings now say
the tolerance covers a retired name rather than a currently published one.
- **`audit`:** the comment introducing the stamp-rule relay exception in `emit-findings.sh`
claimed "ONE exception" where the code has two. `withheld_verdict` is evaluated before any rule
id is read, so `{"rule": "rule-stamp-expired", "verdict": "not-found"}` is withheld on the
judgment-verdict check and never reaches the unreadable-`tier` predicate the comment described.
The comment now names both conditions and states which runs first;
`context/persist-findings.md` gets the same correction, since it framed the same claim as a
single exception. No behavior changed: the classification chain is untouched, and the case the
comment miscounted was already correct, withholding a record that declared a judgment verdict.
`emit-findings.test.sh` gains a case pinning it, asserting zero relay rows, the withheld count
rather than the not-relay-eligible count, and no payload leak.

### Changed

- **`audit`:** `context/persist-findings.md` states the normalization mechanism before the motive
it approximates, rather than framing rendering-equivalence as a goal and then carving out the
homoglyph exception. The limit is unchanged and still stated.

## [0.5.6]

### Changed
Expand Down
2 changes: 1 addition & 1 deletion plugins/provenance/skills/audit/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: "Audit tracked markdown for prose restating content an external source owns without a pointer or a stamped record, and convert copies into links, quoted citations, or four-part stamped records. Breadcrumb-first: citations in or near a passage are the first confirm targets; budgeted search runs only when no breadcrumb exists. Findings carry evidence-gated tiers (fingerprint-confirmed, source-fetched-similar, llm-suspected, source-not-identified); only fingerprint-confirmed copies are fix-eligible. Also flags verification stamps past their expiry window. Use when: 'find copied content', 'is this copied from the docs', 'check our docs for copied text', 'replace copies with links', 'find stale verification stamps', 'audit provenance', 'where did this paragraph come from', or before publishing prose that restates an upstream page. Read-only by default; explicit 'fix' applies dispositions behind a semantic-diff guard and live pointer checks, and 'sweep' adds per-file closure. Empty target audits tracked markdown."
description: "Audit tracked markdown for prose restating content an external source owns without a pointer or a stamped record, and convert copies into links, quoted citations, or four-part stamped records. Breadcrumb-first: citations in or near a passage are the first confirm targets; budgeted search runs only when no breadcrumb exists. Findings carry evidence-gated tiers (fingerprint-confirmed, source-fetched-similar, llm-suspected, not-found); only fingerprint-confirmed copies are fix-eligible. Also flags verification stamps past their expiry window. Use when: 'find copied content', 'is this copied from the docs', 'check our docs for copied text', 'replace copies with links', 'find stale verification stamps', 'audit provenance', 'where did this paragraph come from', or before publishing prose that restates an upstream page. Read-only by default; explicit 'fix' applies dispositions behind a semantic-diff guard and live pointer checks, and 'sweep' adds per-file closure. Empty target audits tracked markdown."
argument-hint: "[audit|fix|sweep] [target]"
user-invocable: true
disable-model-invocation: false
Expand Down
32 changes: 22 additions & 10 deletions plugins/provenance/skills/audit/context/persist-findings.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,15 @@ says" below.

**Only fingerprint-confirmed copy findings and the two deterministic stamp rules enter the
file.** Judgment verdicts — `source-fetched-similar`, `llm-suspected`, and the neutral outcome
under both the names this skill uses for it, `not-found` and the `source-not-identified` that
`SKILL.md` publishes — go to the human report only. They have no crosswalk row to look a tier up
`not-found` — go to the human report only. They have no crosswalk row to look a tier up
from, and a relay row is an instruction to a remediation surface, not a place to record a
suspicion. Both spellings are recognized because the sidecar is model-authored against that
published description: recognizing one name too many can only withhold a record, and one too few
walks a judgment verdict onto a relay row.
suspicion.

`not-found` is the single name every prose surface of this skill publishes for the neutral
outcome. The reader also recognizes `source-not-identified`, the name `SKILL.md` published before
that reconciliation, and that tolerance is permanent: a sidecar is model-authored against
whatever description was in context, and recognizing one name too many can only withhold a
record, while one too few walks a judgment verdict onto a relay row.

The script applies this filter itself rather than trusting the sidecar to arrive pre-filtered,
and it counts what it withheld in `## Surfaces` rather than dropping it. Two consequences worth
Expand Down Expand Up @@ -156,8 +159,9 @@ it names a tier only when it EQUALS one — so `" not-found "`, `["not-found"]
unknown tier rather than the verdict it happens to start with. A valid rule id sitting beside a
verdict does not readmit it either.

A tier that RENDERS as a verdict name in the written file should BE a verdict name, and the
reader pursues that by Unicode CLASS rather than by a list of the code points someone thought of.
The reader strips and folds by Unicode CLASS rather than by a list of the code points someone
thought of. That approximates a motive no jq predicate reaches in general: a tier that RENDERS as
a verdict name in the written file should BE a verdict name.
Characters that render as nothing are stripped everywhere, by `Default_Ignorable_Code_Point` plus
the rest of `Cf`, and hyphen-like code points are folded to ASCII by the dash class, because
every one of these names is hyphenated. Anything narrower leaks: an enumeration of two
Expand All @@ -172,9 +176,17 @@ to find. Such a tier is an unknown tier, and the record takes the ordinary path
— never a relay row it could have reached by declaring a verdict this reader cannot read. That
holds for the stamp rules too: they fire on date arithmetic that owes the tier nothing and relay
whatever a record does or does not declare, but a record whose OWN `tier` field names no tier
this reader knows is not relayed on it. The exception stops at that field; a stamp finding
carrying a benign `verdict` sibling has declared no tier and still relays, because withholding it
would be this rule committing the over-capture the boundary exists to avoid.
this reader knows is not relayed on it.

**Two conditions stop a stamp record short of the relay, and the unreadable `tier` field is the
second of them.** The first is the judgment-verdict check, which runs before any rule id is read:
a stamp record naming a judgment verdict is withheld there, whether it names it in `tier` or only
in `verdict`, so `{"rule": "rule-stamp-expired", "verdict": "not-found"}` produces no relay row
and is counted as withheld even though it declares no `tier` at all. That is the boundary doing
its job on a record that declared a judgment verdict. Neither condition reaches a stamp finding
carrying a benign `verdict` sibling: it names no verdict and declares no tier, so it still
relays, because withholding it would be these rules committing the over-capture the boundary
exists to avoid.

Both directions matter. Separators and combining marks at large do render, so a separator is
trimmed at the ends only and a combining mark is not stripped at all: `"not found"` and
Expand Down
42 changes: 27 additions & 15 deletions plugins/provenance/skills/audit/scripts/emit-findings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -249,11 +249,13 @@ def norm:
# `## Unparsed`. "Every string anywhere inside" has to mean every string.
def names_in:
[ .[] | .. | (strings, (objects | keys[])) | norm ];
# `source-not-identified` is the spelling SKILL.md publishes for the neutral outcome
# this file elsewhere calls `not-found`. Both are recognized, because the sidecar is
# written against that description and a name this reader does not know is a verdict
# that walks into the relay. Recognizing one name too many can only withhold a
# record; recognizing one too few relays a judgment verdict.
# `not-found` is the one spelling every prose surface of this skill now publishes for
# the neutral outcome, SKILL.md included. `source-not-identified` is the spelling
# SKILL.md published before that reconciliation, and it stays recognized here: a
# sidecar is model-authored against whatever description was in context, and a name
# this reader does not know is a verdict that walks into the relay. Recognizing one
# name too many can only withhold a record; recognizing one too few relays a judgment
# verdict. Do not narrow this to one name.
def is_verdict_name:
. == "source-fetched-similar" or . == "llm-suspected"
or . == "not-found" or . == "source-not-identified";
Expand All @@ -280,17 +282,27 @@ def declares_not_found:
def declares_confirmed:
declares("fingerprint-confirmed");
# A stamp rule fires on a date arithmetic that owes the tier nothing, so it relays
# whatever the record does or does not declare — with ONE exception. When the record
# declares a tier in its OWN `tier` field and that field names no tier this reader
# knows, the declaration is uninterpretable, and relaying on it hands the apply relay
# a record whose own verdict this producer cannot read. `"tier": "nоt-found"` with a
# Cyrillic o is the case that matters: a homoglyph past the dash class, which the
# stated limit says takes the ordinary path rather than the relay.
# whatever the record does or does not declare — with TWO exceptions, and the one
# below is the second of them to be evaluated.
#
# Scoped to the `tier` field, NOT to the verdict fallback. A stamp finding carrying
# `"verdict": {"reviewed_by": "alice"}` has declared no tier and must still relay;
# withholding it would be this fix committing the over-capture the whole boundary
# exists to avoid.
# FIRST, in the classification chain: `withheld_verdict` is tested before any rule id
# is looked at, so a stamp record that declares a judgment verdict is withheld there
# and never reaches this predicate. `{"rule": "rule-stamp-expired", "verdict":
# "not-found"}` declares no `tier` at all, yet the verdict fallback reads that name
# and withholds the record. That is the boundary doing its job, not a hole: the record
# declared a judgment verdict.
#
# SECOND, here. When the record declares a tier in its OWN `tier` field and that field
# names no tier this reader knows, the declaration is uninterpretable, and relaying on
# it hands the apply relay a record whose own verdict this producer cannot read.
# `"tier": "nоt-found"` with a Cyrillic o is the case that matters: a homoglyph past
# the dash class, which the stated limit says takes the ordinary path rather than the
# relay.
#
# This second exception is scoped to the `tier` field, NOT to the verdict fallback. A
# stamp finding carrying `"verdict": {"reviewed_by": "alice"}` names no verdict and
# declares no tier, so it passes both exceptions and must still relay; withholding it
# would be this fix committing the over-capture the whole boundary exists to avoid.
def own_tier_unreadable:
([ tier_slot ] | names_in) as $own
| ($own | length) > 0 and ($own | any(is_tier_name) | not);
Expand Down
38 changes: 32 additions & 6 deletions plugins/provenance/skills/audit/scripts/emit-findings.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -883,13 +883,15 @@ for gs in verdict-searched verdict-cased-searched tier-object-searched \
"$(cat "$GO")" "1 judgment"
done

# --- The neutral outcome under the name SKILL.md publishes ------------------------
# --- The neutral outcome under the retired spelling -------------------------------
#
# `SKILL.md` publishes the neutral tier as `source-not-identified` while this file
# elsewhere calls it `not-found`. The sidecar is model-authored against that
# description, so a reader that knows one spelling relays the other. Recognizing one
# name too many can only withhold a record; one too few walks a judgment verdict
# onto a relay row.
# Every prose surface of this skill, `SKILL.md` included, now publishes the neutral
# tier as `not-found`. `source-not-identified` is the name `SKILL.md` published before
# that reconciliation, and the reader still recognizes it: a sidecar is model-authored
# against whatever description was in context, so a reader that knows one spelling
# relays the other. Recognizing one name too many can only withhold a record; one too
# few walks a judgment verdict onto a relay row. These cases pin the tolerance, which
# is permanent and must not be narrowed to a single name.
write_report neutral-published-name.json '{
"counts": {"files": 2},
"findings": [{"rule": "provenance/audit/rule-stamp-expired", "file": "sn.md",
Expand Down Expand Up @@ -1456,6 +1458,30 @@ assert_eq "a stamp rule with a benign verdict still relays" \
assert_not_contains "and is not counted as ineligible" \
"$(cat "$SBV")" "Not relay-eligible"

# The OTHER condition that stops a stamp record short of the relay, and the one
# evaluated FIRST. A stamp record with no `tier` field at all, whose `verdict` names a
# judgment verdict, is withheld on the judgment-verdict check before the unreadable-
# tier predicate is ever consulted. It is counted as WITHHELD, not as not-relay-
# eligible, which is how the two conditions are told apart from the outside. The
# behavior is correct and load-bearing: the record declared a judgment verdict.
write_report stamp-verdict-names-judgment.json '{
"counts": {"files": 2},
"findings": [{"rule": "provenance/audit/rule-stamp-expired", "file": "svj.md",
"line": 1, "verdict": "not-found", "excerpt": "SVJCANARY",
"stamp_date": "2020-01-01", "window_days": 90, "days_over": 5,
"searched": ["https://svj.example/u"]}]
}'
SVJ="$OUTDIR/stamp-verdict-names-judgment.md"
run --report "$REPORTS/stamp-verdict-names-judgment.json" --out "$SVJ" >/dev/null 2>&1
SVJ_BODY="$(cat "$SVJ")"
assert_eq "a stamp rule whose verdict names a judgment verdict emits no relay row" \
"$(grep -c '^| [0-9]' "$SVJ")" "0"
assert_contains "and it is counted as withheld, not as ineligible" \
"$SVJ_BODY" "Withheld from the relay: 1 judgment"
assert_not_contains "and it takes the withheld path, not the unreadable-tier path" \
"$SVJ_BODY" "Not relay-eligible"
assert_not_contains "and it leaks no payload" "$SVJ_BODY" "SVJCANARY"

# A stamp rule declaring no tier at all is the common case and relays.
write_report stamp-no-tier.json '{
"counts": {"files": 2},
Expand Down
Loading