From 20fffbe418376506d7696c7fe201efadc5b0da22 Mon Sep 17 00:00:00 2001 From: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:51:01 -0400 Subject: [PATCH 1/7] feat: add an optional revisions array with a closed reason_code (#317) Records can now carry an append-only history of edits that changed a value a consumer may gate on. Each entry has date, field, from, to and reason_code, with an optional reason; from is null for an added field and to is never null. The reason_code enum copies the four revision_reason codes from probityai/agent-evidence-vocabulary at aff6e0187d05fbb01aa26bd4b4035aebcbc81be0 and cites that commit. validate_records.py refuses entries out of date order and a last_updated older than the latest revision, which the schema cannot express. docs/terms.md is regenerated. --- docs/terms.md | 4 ++ schema/ave-record-1.1.0.schema.json | 50 +++++++++++++++++ schema/ave-record.schema.json | 50 +++++++++++++++++ scripts/validate_records.py | 29 ++++++++++ tests/test_revisions.py | 85 +++++++++++++++++++++++++++++ 5 files changed, 218 insertions(+) create mode 100644 tests/test_revisions.py diff --git a/docs/terms.md b/docs/terms.md index 8ba23ca..13e56c6 100644 --- a/docs/terms.md +++ b/docs/terms.md @@ -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. diff --git a/schema/ave-record-1.1.0.schema.json b/schema/ave-record-1.1.0.schema.json index b842e1f..9c1434d 100644 --- a/schema/ave-record-1.1.0.schema.json +++ b/schema/ave-record-1.1.0.schema.json @@ -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." + } + } + } } } } diff --git a/schema/ave-record.schema.json b/schema/ave-record.schema.json index b842e1f..9c1434d 100644 --- a/schema/ave-record.schema.json +++ b/schema/ave-record.schema.json @@ -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." + } + } + } } } } diff --git a/scripts/validate_records.py b/scripts/validate_records.py index 01a47f9..2c86d03 100644 --- a/scripts/validate_records.py +++ b/scripts/validate_records.py @@ -168,6 +168,34 @@ def check_no_vendor_boilerplate(raw_text: str) -> list[str]: ] +def check_revisions_append_only(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). + """ + 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_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 @@ -222,6 +250,7 @@ def main() -> int: + check_aivss_arithmetic(record) + check_no_vendor_boilerplate(raw_text) + check_verification_basis(record) + + check_revisions_append_only(record) ) for e in errors: print(f"{rid}: {e}") diff --git a/tests/test_revisions.py b/tests/test_revisions.py new file mode 100644 index 0000000..739c7d8 --- /dev/null +++ b/tests/test_revisions.py @@ -0,0 +1,85 @@ +import copy +import json + +import pytest + +from scripts import validate_records + + +def _validator(): + schema = json.loads(validate_records.SCHEMA_PATH.read_text(encoding="utf-8")) + return validate_records.build_validator(schema) + + +BASE = { + "ave_id": "AVE-2026-99999", + "schema_version": "1.1.0", + "status": "draft", + "title": "Revisions fixture", + "description": "A minimal draft record carrying a revisions array.", + "attack_class": "test", + "behavioral_fingerprint": "test", + "references": ["https://example.com"], + "last_updated": "2026-09-28T23:51:58Z", + "revisions": [ + { + "date": "2026-09-28", + "field": "evidence_vantage", + "from": None, + "to": "substrate", + "reason_code": "declaration_added", + } + ], +} + + +def _record(**entry_changes): + record = copy.deepcopy(BASE) + record["revisions"][0].update(entry_changes) + return record + + +def test_an_added_field_entry_with_null_from_is_valid(): + assert validate_records.check_schema(_record(), _validator()) == [] + assert validate_records.check_revisions_append_only(_record()) == [] + + +def test_an_unregistered_reason_code_is_refused(): + errors = validate_records.check_schema(_record(reason_code="value_tweaked"), _validator()) + assert any("value_tweaked" in error for error in errors) + + +@pytest.mark.parametrize("missing", ["date", "field", "from", "to", "reason_code"]) +def test_every_required_entry_member_is_required(missing): + record = _record() + del record["revisions"][0][missing] + assert validate_records.check_schema(record, _validator()) + + +def test_a_null_to_is_refused(): + assert validate_records.check_schema(_record(to=None), _validator()) + + +def test_an_unknown_entry_member_is_refused(): + assert validate_records.check_schema(_record(note="x"), _validator()) + + +def test_revisions_out_of_date_order_are_refused(): + record = _record() + record["revisions"].insert(0, dict(record["revisions"][0], date="2026-09-29")) + record["last_updated"] = "2026-09-29T00:00:00Z" + errors = validate_records.check_revisions_append_only(record) + assert any("date order" in error for error in errors) + + +def test_a_revision_after_last_updated_is_refused(): + record = _record() + record["last_updated"] = "2026-08-07T00:00:00Z" + errors = validate_records.check_revisions_append_only(record) + assert any("predates the latest revision" in error for error in errors) + + +def test_a_record_without_revisions_makes_no_claim(): + record = copy.deepcopy(BASE) + del record["revisions"] + assert validate_records.check_revisions_append_only(record) == [] From 509b00affaf569af5242d5c1c27c9d86248ec203 Mon Sep 17 00:00:00 2001 From: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:51:01 -0400 Subject: [PATCH 2/7] data: record the evidence_vantage stamp on AVE-2026-00074 (#317) The #299 stamp moved 00074's derived verification_basis from artifact_reconstructed to substrate_reconstructed, so it carries the first revisions entry, declaration_added, dated to the #299 merge, and last_updated now names that change. The other eight records stamped in #299 derive the same basis either way and get no entry. --- CHANGELOG.md | 14 ++++++++++++++ records/AVE-2026-00074.json | 14 ++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb5bd7b..263e63f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,6 +106,20 @@ 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` + now names that change. 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 diff --git a/records/AVE-2026-00074.json b/records/AVE-2026-00074.json index ed515e0..3d6019e 100644 --- a/records/AVE-2026-00074.json +++ b/records/AVE-2026-00074.json @@ -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-09-28T23:51:58Z", "references": [ { "tag": "repo-forensics dead-anchor scanner", @@ -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." + } + ] } From b68488cd2041e9555cdc557acaa6f408fc6106ab Mon Sep 17 00:00:00 2001 From: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:51:01 -0400 Subject: [PATCH 3/7] docs: note that a substrate evidence_vantage is a producer declaration AVE records are unsigned, so a reader with no pinned substrate root treats a substrate vantage as unattested. Agreed on #219 as guide text with no schema field. --- docs/guides/confidence-baseline-consumer-guide.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/guides/confidence-baseline-consumer-guide.md b/docs/guides/confidence-baseline-consumer-guide.md index 5ab692a..0fc1657 100644 --- a/docs/guides/confidence-baseline-consumer-guide.md +++ b/docs/guides/confidence-baseline-consumer-guide.md @@ -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 From 12ac775034268e65cffe7fe27dfcec16cd5fe823 Mon Sep 17 00:00:00 2001 From: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:58:19 -0400 Subject: [PATCH 4/7] data: set AVE-2026-00074 last_updated to the date of this edit The pull request template asks an update to an existing record to set last_updated to the day of the edit. #299 had left it at 2026-08-07. --- CHANGELOG.md | 7 ++++--- records/AVE-2026-00074.json | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 263e63f..ff5395f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,9 +117,10 @@ Format: [Semantic Versioning](https://semver.org). Schema versions and record se 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` - now names that change. The other eight records stamped in #299 get no - entry, because their derived basis did not change. + `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 diff --git a/records/AVE-2026-00074.json b/records/AVE-2026-00074.json index 3d6019e..5c7898e 100644 --- a/records/AVE-2026-00074.json +++ b/records/AVE-2026-00074.json @@ -61,7 +61,7 @@ "researcher": "AIR Security", "researcher_url": "https://www.air.security", "published": "2026-08-07T00:00:00Z", - "last_updated": "2026-09-28T23:51:58Z", + "last_updated": "2026-10-13T00:00:00Z", "references": [ { "tag": "repo-forensics dead-anchor scanner", From ab8466a4ca92a603587e13946a83dfc490caeb4f Mon Sep 17 00:00:00 2001 From: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:50:08 -0400 Subject: [PATCH 5/7] test: prove the revisions check runs in the validator pipeline Move the per-record error list out of main() into record_errors(), which main() and the tests both call, so a check dropped from the pipeline now fails a test. Add a test that out-of-order revisions are refused through record_errors(), and one that two distinct revisions on the same date are accepted. They kill the two mutations that survived review: removing the call from the pipeline, and < becoming <= in the date-order comparison. --- scripts/validate_records.py | 31 ++++++++++++++++++++----------- tests/test_revisions.py | 23 +++++++++++++++++++++++ 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/scripts/validate_records.py b/scripts/validate_records.py index 2c86d03..6c0aa88 100644 --- a/scripts/validate_records.py +++ b/scripts/validate_records.py @@ -226,6 +226,25 @@ 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_revisions_append_only(record) + ) + + def main() -> int: schema = json.loads(SCHEMA_PATH.read_text()) jsonschema.Draft202012Validator.check_schema(schema) @@ -241,17 +260,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) - + check_revisions_append_only(record) - ) + errors = record_errors(record, raw_text, validator) for e in errors: print(f"{rid}: {e}") total_errors += len(errors) diff --git a/tests/test_revisions.py b/tests/test_revisions.py index 739c7d8..d70e3fe 100644 --- a/tests/test_revisions.py +++ b/tests/test_revisions.py @@ -21,6 +21,7 @@ def _validator(): "behavioral_fingerprint": "test", "references": ["https://example.com"], "last_updated": "2026-09-28T23:51:58Z", + "evidence_vantage": "substrate", "revisions": [ { "date": "2026-09-28", @@ -83,3 +84,25 @@ def test_a_record_without_revisions_makes_no_claim(): record = copy.deepcopy(BASE) del record["revisions"] assert validate_records.check_revisions_append_only(record) == [] + + +def test_two_revisions_on_the_same_date_are_accepted(): + record = _record(to="artifact") + record["revisions"].append( + { + "date": "2026-09-28", + "field": "evidence_vantage", + "from": "artifact", + "to": "substrate", + "reason_code": "declaration_added", + } + ) + assert validate_records.check_revisions_append_only(record) == [] + + +def test_the_validator_pipeline_refuses_revisions_out_of_date_order(): + record = _record() + record["revisions"].insert(0, dict(record["revisions"][0], date="2026-09-29")) + record["last_updated"] = "2026-09-29T00:00:00Z" + errors = validate_records.record_errors(record, json.dumps(record), _validator()) + assert any("date order" in error for error in errors) From f14c3007e78bfd584722b13bcb1e8e1433bcb428 Mon Sep 17 00:00:00 2001 From: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:50:45 -0400 Subject: [PATCH 6/7] feat: refuse revisions that name no field or contradict the record Add check_revision_values() to the validator pipeline. A revision must name a property the schema defines and record a real change, and the revisions of one field must chain: each from is the previous to, and the latest to is the value the record holds now. This refuses a misspelt field, a no-op entry, a duplicated entry and a history that contradicts the record, which the review found accepted today. A deleted or rewritten earlier entry is still only visible against the base version. --- scripts/validate_records.py | 38 +++++++++++++++++++++++++++++++++++++ tests/test_revisions.py | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/scripts/validate_records.py b/scripts/validate_records.py index 6c0aa88..b9b46a9 100644 --- a/scripts/validate_records.py +++ b/scripts/validate_records.py @@ -196,6 +196,43 @@ def check_revisions_append_only(record: dict) -> list[str]: 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 @@ -242,6 +279,7 @@ def record_errors(record: dict, raw_text: str, validator) -> list[str]: + check_no_vendor_boilerplate(raw_text) + check_verification_basis(record) + check_revisions_append_only(record) + + check_revision_values(record, validator.schema.get("properties", {})) ) diff --git a/tests/test_revisions.py b/tests/test_revisions.py index d70e3fe..d3d9e3c 100644 --- a/tests/test_revisions.py +++ b/tests/test_revisions.py @@ -106,3 +106,38 @@ def test_the_validator_pipeline_refuses_revisions_out_of_date_order(): record["last_updated"] = "2026-09-29T00:00:00Z" errors = validate_records.record_errors(record, json.dumps(record), _validator()) assert any("date order" in error for error in errors) + + +KNOWN_FIELDS = json.loads(validate_records.SCHEMA_PATH.read_text(encoding="utf-8"))["properties"] + + +def test_a_consistent_history_passes_the_value_check(): + assert validate_records.check_revision_values(_record(), KNOWN_FIELDS) == [] + + +def test_a_field_the_schema_does_not_define_is_refused(): + errors = validate_records.check_revision_values(_record(field="evidence_vantge"), KNOWN_FIELDS) + assert any("not a property the schema defines" in error for error in errors) + + +def test_a_revision_with_the_same_from_and_to_is_refused(): + errors = validate_records.check_revision_values(_record(**{"from": "substrate"}), KNOWN_FIELDS) + assert any("records no change" in error for error in errors) + + +def test_a_duplicated_revision_breaks_the_chain(): + record = _record() + record["revisions"].append(copy.deepcopy(record["revisions"][0])) + errors = validate_records.check_revision_values(record, KNOWN_FIELDS) + assert any("is not the previous to" in error for error in errors) + + +def test_a_latest_to_that_contradicts_the_record_is_refused(): + errors = validate_records.check_revision_values(_record(to="artifact"), KNOWN_FIELDS) + assert any("but the record holds 'substrate'" in error for error in errors) + + +def test_the_validator_pipeline_refuses_a_contradicting_latest_to(): + record = _record(to="artifact") + errors = validate_records.record_errors(record, json.dumps(record), _validator()) + assert any("but the record holds" in error for error in errors) From c9c5e92ef17cdcd4ea02a6869fbc4aa882e3f02d Mon Sep 17 00:00:00 2001 From: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:51:21 -0400 Subject: [PATCH 7/7] refactor: name the revisions order check for what it checks check_revisions_append_only checked date order and last_updated agreement, not that the array was only appended to, which one record cannot show. Rename it check_revision_order and say in its docstring that a deleted or rewritten earlier entry is visible only against the previous version of the record. --- scripts/validate_records.py | 9 ++++++--- tests/test_revisions.py | 10 +++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/scripts/validate_records.py b/scripts/validate_records.py index b9b46a9..0286d05 100644 --- a/scripts/validate_records.py +++ b/scripts/validate_records.py @@ -168,13 +168,16 @@ def check_no_vendor_boilerplate(raw_text: str) -> list[str]: ] -def check_revisions_append_only(record: dict) -> 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). + 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): @@ -278,7 +281,7 @@ def record_errors(record: dict, raw_text: str, validator) -> list[str]: + check_aivss_arithmetic(record) + check_no_vendor_boilerplate(raw_text) + check_verification_basis(record) - + check_revisions_append_only(record) + + check_revision_order(record) + check_revision_values(record, validator.schema.get("properties", {})) ) diff --git a/tests/test_revisions.py b/tests/test_revisions.py index d3d9e3c..a7568ac 100644 --- a/tests/test_revisions.py +++ b/tests/test_revisions.py @@ -42,7 +42,7 @@ def _record(**entry_changes): def test_an_added_field_entry_with_null_from_is_valid(): assert validate_records.check_schema(_record(), _validator()) == [] - assert validate_records.check_revisions_append_only(_record()) == [] + assert validate_records.check_revision_order(_record()) == [] def test_an_unregistered_reason_code_is_refused(): @@ -69,21 +69,21 @@ def test_revisions_out_of_date_order_are_refused(): record = _record() record["revisions"].insert(0, dict(record["revisions"][0], date="2026-09-29")) record["last_updated"] = "2026-09-29T00:00:00Z" - errors = validate_records.check_revisions_append_only(record) + errors = validate_records.check_revision_order(record) assert any("date order" in error for error in errors) def test_a_revision_after_last_updated_is_refused(): record = _record() record["last_updated"] = "2026-08-07T00:00:00Z" - errors = validate_records.check_revisions_append_only(record) + errors = validate_records.check_revision_order(record) assert any("predates the latest revision" in error for error in errors) def test_a_record_without_revisions_makes_no_claim(): record = copy.deepcopy(BASE) del record["revisions"] - assert validate_records.check_revisions_append_only(record) == [] + assert validate_records.check_revision_order(record) == [] def test_two_revisions_on_the_same_date_are_accepted(): @@ -97,7 +97,7 @@ def test_two_revisions_on_the_same_date_are_accepted(): "reason_code": "declaration_added", } ) - assert validate_records.check_revisions_append_only(record) == [] + assert validate_records.check_revision_order(record) == [] def test_the_validator_pipeline_refuses_revisions_out_of_date_order():