Skip to content
Open
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,21 @@ Format: [Semantic Versioning](https://semver.org). Schema versions and record se
new record.

### Added
- Optional, append-only `revisions` array on records (#317, design agreed
on #219). Each entry carries `date`, `field`, `from`, `to` and a closed
`reason_code`, with an optional free-text `reason`; `from` is null for an
added field. The four `reason_code` values are copied from
`revision_reason` in probityai/agent-evidence-vocabulary at commit
aff6e0187d05fbb01aa26bd4b4035aebcbc81be0, and the enum description cites
that commit; AVE's copy stands on its own. A record with no `revisions`
array makes no claim about its own history. `validate_records.py` also
refuses entries out of date order and a `last_updated` older than the
latest revision. First use: AVE-2026-00074, whose `evidence_vantage`
stamp in #299 moved its derived `verification_basis` from
`artifact_reconstructed` to `substrate_reconstructed`. Its `last_updated`
moves to the date of this edit, since #299 left it at 2026-08-07. The
other eight records stamped in #299 get no entry, because their derived
basis did not change.
- AVE-2026-00087: Agentjacking — a monitoring/diagnostic integration's
own publicly-writable ingest endpoint lets an attacker plant a
crafted event formatted to visually mimic the integration's
Expand Down
6 changes: 6 additions & 0 deletions docs/guides/confidence-baseline-consumer-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ a consumer is told which:

A record can carry both, one, or neither.

An `evidence_vantage` of `substrate` is the producer's declaration, not an
observation a consumer can check: AVE records are unsigned, so a reader with
no pinned substrate root treats it as unattested. AVE-2026-00074's
`substrate`, for example, rests on queries to external authorities rather
than on observation of an executed artifact.

These were one check until the two predicates were run against each other
over the same field. Engine-set cardinality was the check's original floor
test, chosen when no measure of vantage existed, and it turns out not to
Expand Down
4 changes: 4 additions & 0 deletions docs/terms.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ Name of the researcher or team who authored this record. REQUIRED once a record

URL for the researcher or team. Optional.

### `revisions`

Optional, append-only history of edits that changed a value a consumer may gate on, such as a derived verification_basis. Entries are appended in date order and never rewritten or removed. A record with no revisions array makes no claim about its own history; a consumer must not read its absence as never revised. Design: issue #317, agreed on #219.

### `schema_version`

AVE schema version this record was authored against, e.g. 1.1.0.
Expand Down
14 changes: 12 additions & 2 deletions records/AVE-2026-00074.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"researcher": "AIR Security",
"researcher_url": "https://www.air.security",
"published": "2026-08-07T00:00:00Z",
"last_updated": "2026-08-07T00:00:00Z",
"last_updated": "2026-10-13T00:00:00Z",
"references": [
{
"tag": "repo-forensics dead-anchor scanner",
Expand Down Expand Up @@ -115,5 +115,15 @@
"version": "2026",
"read_date": "2026-08-23"
}
}
},
"revisions": [
{
"date": "2026-09-28",
"field": "evidence_vantage",
"from": null,
"to": "substrate",
"reason_code": "declaration_added",
"reason": "Stamped in #299. The derived verification_basis moved from artifact_reconstructed to substrate_reconstructed as a result."
}
]
}
50 changes: 50 additions & 0 deletions schema/ave-record-1.1.0.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,56 @@
}
}
}
},
"revisions": {
"type": "array",
"description": "Optional, append-only history of edits that changed a value a consumer may gate on, such as a derived verification_basis. Entries are appended in date order and never rewritten or removed. A record with no revisions array makes no claim about its own history; a consumer must not read its absence as never revised. Design: issue #317, agreed on #219.",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"date",
"field",
"from",
"to",
"reason_code"
],
"properties": {
"date": {
"type": "string",
"format": "date",
"description": "ISO 8601 date the change landed in this record, e.g. the merge date of the pull request that made it."
},
"field": {
"type": "string",
"minLength": 1,
"description": "Name of the record field whose value changed, e.g. evidence_vantage."
},
"from": {
"description": "The field's value before the change, or null when the field was absent before (an added declaration has no prior value)."
},
"to": {
"not": {
"type": "null"
},
"description": "The field's value after the change."
},
"reason_code": {
"type": "string",
"enum": [
"declaration_added",
"declaration_corrected",
"vocabulary_member_added",
"derivation_rule_changed"
],
"description": "Closed code for why this entry exists, so a consumer can count and filter revisions by cause without parsing prose. Copied from revision_reason in probityai/agent-evidence-vocabulary at commit aff6e0187d05fbb01aa26bd4b4035aebcbc81be0 (https://github.com/probityai/agent-evidence-vocabulary/blob/aff6e0187d05fbb01aa26bd4b4035aebcbc81be0/vocabulary.yaml#L291-L315), where the codes are status proposed; this enum stands on its own and does not follow later changes there. declaration_added: a field the record omitted is now declared, and a value derived from it changed as a result. declaration_corrected: a field the record already declared was wrong and now carries a different value. vocabulary_member_added: the schema gained a closed-vocabulary member, and re-deriving the record under it changed a derived value; the record's own declarations did not change. derivation_rule_changed: the rule that computes a derived value changed; the record's own declarations did not change."
},
"reason": {
"type": "string",
"description": "Optional free text: what changed downstream and where the change was made."
}
}
}
}
}
}
50 changes: 50 additions & 0 deletions schema/ave-record.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,56 @@
}
}
}
},
"revisions": {
"type": "array",
"description": "Optional, append-only history of edits that changed a value a consumer may gate on, such as a derived verification_basis. Entries are appended in date order and never rewritten or removed. A record with no revisions array makes no claim about its own history; a consumer must not read its absence as never revised. Design: issue #317, agreed on #219.",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"date",
"field",
"from",
"to",
"reason_code"
],
"properties": {
"date": {
"type": "string",
"format": "date",
"description": "ISO 8601 date the change landed in this record, e.g. the merge date of the pull request that made it."
},
"field": {
"type": "string",
"minLength": 1,
"description": "Name of the record field whose value changed, e.g. evidence_vantage."
},
"from": {
"description": "The field's value before the change, or null when the field was absent before (an added declaration has no prior value)."
},
"to": {
"not": {
"type": "null"
},
"description": "The field's value after the change."
},
"reason_code": {
"type": "string",
"enum": [
"declaration_added",
"declaration_corrected",
"vocabulary_member_added",
"derivation_rule_changed"
],
"description": "Closed code for why this entry exists, so a consumer can count and filter revisions by cause without parsing prose. Copied from revision_reason in probityai/agent-evidence-vocabulary at commit aff6e0187d05fbb01aa26bd4b4035aebcbc81be0 (https://github.com/probityai/agent-evidence-vocabulary/blob/aff6e0187d05fbb01aa26bd4b4035aebcbc81be0/vocabulary.yaml#L291-L315), where the codes are status proposed; this enum stands on its own and does not follow later changes there. declaration_added: a field the record omitted is now declared, and a value derived from it changed as a result. declaration_corrected: a field the record already declared was wrong and now carries a different value. vocabulary_member_added: the schema gained a closed-vocabulary member, and re-deriving the record under it changed a derived value; the record's own declarations did not change. derivation_rule_changed: the rule that computes a derived value changed; the record's own declarations did not change."
},
"reason": {
"type": "string",
"description": "Optional free text: what changed downstream and where the change was made."
}
}
}
}
}
}
99 changes: 89 additions & 10 deletions scripts/validate_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,74 @@ def check_no_vendor_boilerplate(raw_text: str) -> list[str]:
]


def check_revision_order(record: dict) -> list[str]:
"""The schema checks each revisions entry's shape; it cannot check the
array's order or its agreement with last_updated. Entries are appended in
date order, so a date earlier than the one before it is a rewritten
history, and a revision dated after last_updated means last_updated no
longer names the record's latest change, which is the one signal a
consumer had before this field existed (#317). This checks one record
only: an earlier entry deleted or rewritten in place is visible only
against the previous version of the record, so the schema's append-only
rule is not fully enforced here.
"""
revisions = record.get("revisions")
if not isinstance(revisions, list):
return []
errors = []
dates = [entry.get("date", "") for entry in revisions if isinstance(entry, dict)]
for i in range(1, len(dates)):
if dates[i] < dates[i - 1]:
errors.append(
f"revisions[{i}].date {dates[i]} is earlier than revisions[{i - 1}].date "
f"{dates[i - 1]}: revisions are appended in date order"
)
last_updated = str(record.get("last_updated") or "")[:10]
if dates and last_updated and max(dates) > last_updated:
errors.append(
f"last_updated {last_updated} predates the latest revision {max(dates)}: "
f"last_updated must name the record's latest change"
)
return errors


def check_revision_values(record: dict, known_fields) -> list[str]:
"""Each revision must name a field the schema defines and record a real
change, and the revisions of one field must chain: every entry's from is
the previous entry's to, and the latest to is the value the record holds
now. That refuses a misspelt field, a no-op entry, a duplicated entry and
a history that contradicts the record. A deleted or rewritten earlier
entry can only be seen against the previous version of the record.
"""
revisions = record.get("revisions")
if not isinstance(revisions, list):
return []
errors = []
latest = {}
for i, entry in enumerate(revisions):
if not isinstance(entry, dict):
continue
field = entry.get("field")
if field not in known_fields:
errors.append(f"revisions[{i}].field {field!r} is not a property the schema defines")
continue
if entry.get("from") == entry.get("to"):
errors.append(f"revisions[{i}] has the same from and to: it records no change")
if field in latest and entry.get("from") != latest[field]:
errors.append(
f"revisions[{i}].from {entry.get('from')!r} is not the previous to "
f"{latest[field]!r} for {field}"
)
latest[field] = entry.get("to")
for field, value in latest.items():
if record.get(field) != value:
errors.append(
f"the latest revision of {field} sets it to {value!r}, "
f"but the record holds {record.get(field)!r}"
)
return errors


def check_researcher_matches_disclosure(record: dict) -> list[str]:
"""Soft warning only: flags records where `researcher` is an AVE
maintainer name while `references` contains something that reads
Expand Down Expand Up @@ -198,6 +266,26 @@ def check_researcher_matches_disclosure(record: dict) -> list[str]:
return []


def record_errors(record: dict, raw_text: str, validator) -> list[str]:
"""Every hard error for one record, in the order main() reports them.

main() and the tests both call this, so a check dropped from the
pipeline fails a test instead of passing silently.
"""
return (
check_schema(record, validator)
+ check_no_old_field_names(record)
+ check_no_nested_owasp_mcp_mapping(record)
+ check_behavioral_vector_or_example_patterns(record)
+ check_mitigation_enums_only(record)
+ check_aivss_arithmetic(record)
+ check_no_vendor_boilerplate(raw_text)
+ check_verification_basis(record)
+ check_revision_order(record)
+ check_revision_values(record, validator.schema.get("properties", {}))
)


def main() -> int:
schema = json.loads(SCHEMA_PATH.read_text())
jsonschema.Draft202012Validator.check_schema(schema)
Expand All @@ -213,16 +301,7 @@ def main() -> int:
raw_text = path.read_text()
record = json.loads(raw_text)
rid = record.get("ave_id", path.name)
errors = (
check_schema(record, validator)
+ check_no_old_field_names(record)
+ check_no_nested_owasp_mcp_mapping(record)
+ check_behavioral_vector_or_example_patterns(record)
+ check_mitigation_enums_only(record)
+ check_aivss_arithmetic(record)
+ check_no_vendor_boilerplate(raw_text)
+ check_verification_basis(record)
)
errors = record_errors(record, raw_text, validator)
for e in errors:
print(f"{rid}: {e}")
total_errors += len(errors)
Expand Down
Loading
Loading