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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ accepting either tree as authoritative:
python scripts/artifact_contract_diff.py --expected path/to/committed --actual path/to/regenerated
```

Add `--json-out path/to/artifact-diff.json` for a deterministic
[`artifact-contract-diff/v1`](schemas/artifact_contract_diff.schema.json)
report. Keep that report path outside both compared trees.

See [`docs/reviewer-artifact-diff.md`](docs/reviewer-artifact-diff.md#executable-human-triage)
for comparison semantics, limits, and exit codes.

Expand Down
31 changes: 31 additions & 0 deletions docs/reviewer-artifact-diff.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ python scripts/artifact_contract_diff.py \
--actual path/to/regenerated-artifacts
```

Add a strict machine-readable projection when automation or an attached review
artifact needs the same semantics:

```bash
python scripts/artifact_contract_diff.py \
--expected path/to/committed-artifacts \
--actual path/to/regenerated-artifacts \
--json-out path/outside-both-trees/artifact-diff.json
```

The output lists missing, extra, and changed relative paths in stable order.
CSV, Markdown, text, JSON, and JSONL use strict UTF-8 and normalize CRLF and
lone CR to LF before comparison. A binary path that exists in both trees is
Expand All @@ -34,6 +44,14 @@ plain content changes from structure, schema-version, and provenance-digest
changes. Identical JSON and JSONL are not parsed after their normalized bytes
match. Artifact bodies are never printed.

The optional JSON output conforms to
[`artifact-contract-diff/v1`](../schemas/artifact_contract_diff.schema.json).
It contains no timestamp, checkout root, or artifact body, so repeated runs on
the same trees are byte-identical. The destination must resolve outside both
input roots. A new report is written in the destination directory and atomically
replaces an older report only after serialization and file synchronization
succeed; a comparison or write failure leaves an existing report unchanged.

Exit status is `0` when comparable artifacts are unchanged, `1` when the tool
finds contract differences, and `2` when an input cannot be compared safely.
The output contains no artifact bodies, timestamps, or absolute checkout paths.
Expand All @@ -48,6 +66,19 @@ linked entries, special files, unsafe relative paths or metadata, malformed
digest fields, unreadable files, invalid JSON/JSONL, exceeded limits, and
invalid UTF-8 text fail closed with exit `2`.

### JSON Report Semantics

| Field | Contract |
| --- | --- |
| `status` | `unchanged` requires no differences; `changed` requires at least one. |
| `summary` | Counts expected, actual, unchanged, missing, extra, changed, and presence-only files. Core report invariants reconcile these counts. |
| `differences[].status` | `missing` has only an expected snapshot, `extra` has only an actual snapshot, and `changed` has both. |
| `change_reasons` | Missing and extra use one path reason. Changed comparable artifacts begin with `content-changed` and may add structure, schema-version, or run-manifest-digest reasons. |
| `comparison_digest` | SHA-256 of comparison bytes; text-like artifacts use normalized strict UTF-8 bytes. |
| `comparison_size_bytes` | Length of the comparison bytes, which may differ from the on-disk size after newline normalization. |
| `structure` | Present only for summarized JSON/JSONL snapshots. |
| `presence_only_paths` | Sorted binary paths present in both trees; their bytes are not compared or reported. |

## Required Release Diff Sections

Each release artifact diff must include:
Expand Down
257 changes: 257 additions & 0 deletions schemas/artifact_contract_diff.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/stacknil/telemetry-lab/schemas/artifact_contract_diff.schema.json",
"title": "Telemetry lab artifact contract diff v1",
"description": "Deterministic bounded comparison of two local reviewer artifact trees.",
"type": "object",
"additionalProperties": false,
"required": [
"report_schema_version",
"status",
"summary",
"differences",
"presence_only_paths"
],
"properties": {
"report_schema_version": {"const": "artifact-contract-diff/v1"},
"status": {"enum": ["unchanged", "changed"]},
"summary": {"$ref": "#/$defs/summary"},
"differences": {
"type": "array",
"maxItems": 20000,
"uniqueItems": true,
"items": {"$ref": "#/$defs/difference"}
},
"presence_only_paths": {
"type": "array",
"maxItems": 10000,
"uniqueItems": true,
"items": {"$ref": "#/$defs/relativePath"}
}
},
"allOf": [
{
"if": {"properties": {"status": {"const": "unchanged"}}},
"then": {"properties": {"differences": {"maxItems": 0}}}
},
{
"if": {"properties": {"status": {"const": "changed"}}},
"then": {"properties": {"differences": {"minItems": 1}}}
}
],
"$defs": {
"relativePath": {
"type": "string",
"maxLength": 1024,
"pattern": "^(?![A-Za-z]:)(?!.*\\\\)(?!.*(?:^|/)\\.\\.?(?:/|$))[^/]+(?:/[^/]+)*$"
},
"digest": {
"type": "string",
"pattern": "^sha256:[0-9a-f]{64}$"
},
"metadataString": {"type": "string", "maxLength": 1024},
"count": {
"type": "integer",
"minimum": 0,
"maximum": 10000
},
"summary": {
"type": "object",
"additionalProperties": false,
"required": [
"expected_files",
"actual_files",
"unchanged_files",
"missing_files",
"extra_files",
"changed_files",
"presence_only_files"
],
"properties": {
"expected_files": {"$ref": "#/$defs/count"},
"actual_files": {"$ref": "#/$defs/count"},
"unchanged_files": {"$ref": "#/$defs/count"},
"missing_files": {"$ref": "#/$defs/count"},
"extra_files": {"$ref": "#/$defs/count"},
"changed_files": {"$ref": "#/$defs/count"},
"presence_only_files": {"$ref": "#/$defs/count"}
}
},
"changeReason": {
"enum": [
"missing-from-actual",
"extra-in-actual",
"content-changed",
"structure-changed",
"schema-version-changed",
"run-manifest-digest-changed"
]
},
"difference": {
"type": "object",
"additionalProperties": false,
"required": ["path", "status", "artifact_kind", "change_reasons"],
"properties": {
"path": {"$ref": "#/$defs/relativePath"},
"status": {"enum": ["missing", "extra", "changed"]},
"artifact_kind": {"enum": ["json", "jsonl", "text", "binary"]},
"change_reasons": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {"$ref": "#/$defs/changeReason"}
},
"expected": {"$ref": "#/$defs/snapshot"},
"actual": {"$ref": "#/$defs/snapshot"}
},
"allOf": [
{
"if": {"properties": {"status": {"const": "missing"}}},
"then": {
"required": ["expected"],
"properties": {
"change_reasons": {"const": ["missing-from-actual"]}
},
"not": {"required": ["actual"]}
}
},
{
"if": {"properties": {"status": {"const": "extra"}}},
"then": {
"required": ["actual"],
"properties": {
"change_reasons": {"const": ["extra-in-actual"]}
},
"not": {"required": ["expected"]}
}
},
{
"if": {"properties": {"status": {"const": "changed"}}},
"then": {
"required": ["expected", "actual"],
"properties": {
"artifact_kind": {"enum": ["json", "jsonl", "text"]},
"change_reasons": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"prefixItems": [{"const": "content-changed"}],
"items": {
"enum": [
"structure-changed",
"schema-version-changed",
"run-manifest-digest-changed"
]
}
}
}
}
},
{
"if": {
"properties": {"artifact_kind": {"enum": ["json", "jsonl"]}}
},
"then": {
"properties": {
"expected": {"$ref": "#/$defs/structuredSnapshot"},
"actual": {"$ref": "#/$defs/structuredSnapshot"}
}
}
},
{
"if": {
"properties": {"artifact_kind": {"enum": ["text", "binary"]}}
},
"then": {
"properties": {
"expected": {"$ref": "#/$defs/plainSnapshot"},
"actual": {"$ref": "#/$defs/plainSnapshot"}
}
}
}
]
},
"snapshot": {
"type": "object",
"additionalProperties": false,
"required": ["comparison_digest", "comparison_size_bytes"],
"properties": {
"comparison_digest": {"$ref": "#/$defs/digest"},
"comparison_size_bytes": {"type": "integer", "minimum": 0},
"structure": {"$ref": "#/$defs/structure"}
}
},
"structuredSnapshot": {
"allOf": [
{"$ref": "#/$defs/snapshot"},
{"required": ["structure"]}
]
},
"plainSnapshot": {
"allOf": [
{"$ref": "#/$defs/snapshot"},
{"not": {"required": ["structure"]}}
]
},
"structure": {
"type": "object",
"additionalProperties": false,
"required": ["container", "record_count", "top_level_keys"],
"properties": {
"container": {
"enum": [
"object",
"array",
"jsonl",
"string",
"number",
"boolean",
"null"
]
},
"record_count": {
"type": "integer",
"minimum": 0,
"maximum": 67108864
},
"top_level_keys": {
"type": "array",
"maxItems": 4096,
"uniqueItems": true,
"items": {"$ref": "#/$defs/metadataString"}
},
"schema_versions": {
"type": "object",
"minProperties": 1,
"maxProperties": 4096,
"propertyNames": {"$ref": "#/$defs/metadataString"},
"additionalProperties": {
"type": "array",
"minItems": 1,
"maxItems": 4096,
"uniqueItems": true,
"items": {"$ref": "#/$defs/metadataString"}
}
},
"run_manifest_digests": {"$ref": "#/$defs/runManifestDigests"}
}
},
"runManifestDigests": {
"type": "object",
"minProperties": 1,
"additionalProperties": false,
"properties": {
"input_digest": {"$ref": "#/$defs/digest"},
"config_digest": {"$ref": "#/$defs/digest"},
"input_file_digests": {"$ref": "#/$defs/fileDigestMap"},
"config_file_digests": {"$ref": "#/$defs/fileDigestMap"}
}
},
"fileDigestMap": {
"type": "object",
"maxProperties": 10000,
"propertyNames": {"$ref": "#/$defs/relativePath"},
"additionalProperties": {"$ref": "#/$defs/digest"}
}
}
}
Loading
Loading