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
64 changes: 63 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,64 @@
# codeanalyzer-schema
Versioned schema

Versioned contracts for CodeLLM analyzers and SDK consumers.

## v1 snapshots

The `v1/` tree contains the published language-analysis and Neo4j contracts.
These files are compatibility snapshots: new schema-v2 work must not rewrite
them. A path selector can validate one snapshot independently, for example:

```bash
python3 scripts/check.py v1/json/java
```

## IaC schema v2 contract

The files under `v2/iac/` are the normative contract for
`codeanalyzer-iac`:

- `v2/iac/json/analysis.schema.json` is the normative JSON output schema.
Analyzer golden outputs must validate against it at their declared analysis
level and must also pass `scripts/check_iac.py` semantic validation.
- `v2/iac/json/analysis.l1.sample.json`, `analysis.l2.sample.json`, and
`analysis.l3.sample.json` demonstrate the additive level contract. Higher
levels preserve every lower-level fact; resolution fields may be added but
existing facts may not be replaced.
- `v2/iac/neo4j/contract.schema.json` describes the graph-catalog format.
The `codeanalyzer-iac` catalog must byte-match
`v2/iac/neo4j/schema.neo4j.sample.json`.

Draft 2020-12 validation covers document structure. The repository semantic
gate additionally enforces globally unique node IDs across all named
collections, real edge endpoints, source digests and span bounds, relative
artifact paths, contiguous render-profile layer ordinals, hash-only Secret
data, and `Package.id == Package.purl`. Every identity alias—including a Helm
Chart alias—must target a collected canonical node, must not self-target or
target another alias, and must have exactly one matching `iac_alias_of` edge.
The graph catalog admits only the shared neutral relationships
`HAS_ARTIFACT` and `DEFINES_CONFIG`; IaC-owned names use the `IAC_*` namespace,
relationship properties are empty, and all referenced labels must exist.

Install the checker dependency once, then run the repository gates:

```bash
python3 -m pip install jsonschema
python3 scripts/check.py
python3 -m unittest discover -s tests -v
```

The unit and repository check suites use only checked-in files and stay
network-free. These live repositories are downstream backend-consumer gates,
not inputs fetched by this repository:

- `sample-daytrader/sample.daytrader.microservices@8a68b59430a94a242c54384763da9eb7682728b4`:
each emitted document
must pass structural validation with `v2/iac/json/analysis.schema.json` and
semantic `scripts/check_iac.py` validation.
- `quarkuscoffeeshop/quarkuscoffeeshop-helm@aa3c842658e0fc7e44fa25132d8b817eab225cbe`:
each emitted document must pass
structural validation with `v2/iac/json/analysis.schema.json` and semantic
`scripts/check_iac.py` validation.

Preserve the emitted JSON from both pinned repositories and do not weaken
either contract to admit a consumer output.
127 changes: 127 additions & 0 deletions scripts/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""Check every schema in this repo is a valid JSON Schema, and validate the
documents it claims to cover.

A schema validates any *.sample.json sitting in its own directory, plus every
file matched by the globs in its x-cldk.validates list (relative to the schema).

python3 scripts/check.py # all versions
python3 scripts/check.py v1/java # one directory
"""
import json
import sys
from pathlib import Path

from jsonschema import Draft202012Validator

if __package__:
from .check_iac import assert_monotone, check_catalog, check_document
else:
from check_iac import assert_monotone, check_catalog, check_document

ROOT = Path(__file__).resolve().parent.parent


def check(schema_path: Path) -> int:
schema = json.loads(schema_path.read_text())
Draft202012Validator.check_schema(schema)
rel = schema_path.relative_to(ROOT)
print(f"ok {rel}")

covered = list(schema_path.parent.glob("*.sample.json"))
for pattern in schema.get("x-cldk", {}).get("validates", []):
covered += schema_path.parent.glob(pattern)

failures = 0
validator = Draft202012Validator(schema)
for sample in sorted(set(covered)):
errors = sorted(validator.iter_errors(json.loads(sample.read_text())),
key=lambda e: list(e.absolute_path))
if errors:
failures += 1
print(f"FAIL {sample.relative_to(ROOT)}: {len(errors)} error(s)")
for e in errors[:5]:
where = "/".join(str(p) for p in e.absolute_path) or "<root>"
print(f" {where}: {e.message[:160]}")
else:
print(f"ok {sample.relative_to(ROOT)}")
return failures


def selected_iac_root(where: Path) -> Path | None:
"""Return the IaC contract root only when the selected path includes it."""
iac_root = ROOT / "v2/iac"
if not iac_root.exists():
return None
try:
iac_root.relative_to(where)
return iac_root
except ValueError:
pass
try:
where.relative_to(iac_root)
return iac_root
except ValueError:
return None


def check_iac_contract(iac_root: Path) -> int:
"""Run semantic and level-monotonicity checks for the IaC contract."""
failures = 0
analysis_paths = sorted((iac_root / "json").glob("analysis.l*.sample.json"))
analyses: list[tuple[Path, dict]] = []

for path in analysis_paths:
document = json.loads(path.read_text())
analyses.append((path, document))
errors = check_document(document)
if errors:
failures += 1
print(f"FAIL {path.relative_to(ROOT)}: {len(errors)} semantic error(s)")
for error in errors:
print(f" {error}")
else:
print(f"ok {path.relative_to(ROOT)} (semantic)")

catalog_path = iac_root / "neo4j/schema.neo4j.sample.json"
if catalog_path.exists():
errors = check_catalog(json.loads(catalog_path.read_text()))
if errors:
failures += 1
print(f"FAIL {catalog_path.relative_to(ROOT)}: {len(errors)} semantic error(s)")
for error in errors:
print(f" {error}")
else:
print(f"ok {catalog_path.relative_to(ROOT)} (semantic)")

for (lower_path, lower), (higher_path, higher) in zip(analyses, analyses[1:]):
errors = assert_monotone(lower, higher)
label = f"{lower_path.name} <= {higher_path.name}"
if errors:
failures += 1
print(f"FAIL {label}: {len(errors)} monotonicity error(s)")
for error in errors:
print(f" {error}")
else:
print(f"ok {label} (monotone)")

return failures


def main() -> int:
where = ROOT / sys.argv[1] if len(sys.argv) > 1 else ROOT
schemas = sorted(where.rglob("*.schema.json"))
if not schemas:
print(f"no schemas under {where}")
return 1
failures = sum(check(schema) for schema in schemas)
if failures:
return 1
iac_root = selected_iac_root(where)
if iac_root is not None:
failures += check_iac_contract(iac_root)
return min(failures, 1)


if __name__ == "__main__":
raise SystemExit(main())
Loading