diff --git a/fixtures/contracts/evidence-freshness-conformance-v1/manifest.json b/fixtures/contracts/evidence-freshness-conformance-v1/manifest.json new file mode 100644 index 0000000..541d547 --- /dev/null +++ b/fixtures/contracts/evidence-freshness-conformance-v1/manifest.json @@ -0,0 +1,56 @@ +{ + "consumer_owned": [ + "threshold-values", + "ui-language", + "alert-policy", + "build-versus-reader-behavior", + "production-calculator" + ], + "consumer_profiles": { + "desktop-command-center": { + "state_mapping": { + "aging": "aging", + "fresh": "fresh", + "stale": "stale", + "unavailable": "unknown", + "unknown": "unknown" + } + }, + "operator-control-plane": { + "state_mapping": { + "aging": "aging", + "fresh": "fresh", + "stale": "stale", + "unavailable": "unavailable", + "unknown": "unverified" + } + }, + "public-site-projection": { + "state_mapping": { + "aging": "resting", + "fresh": "fresh", + "stale": "cold", + "unavailable": "unknown", + "unknown": "unknown" + } + } + }, + "contract_version": "1.0.0", + "non_goals": [ + "shared-runtime-library", + "shared-alert-policy", + "shared-ui-copy" + ], + "owner": { + "artifact_path": "fixtures/contracts/evidence-freshness-conformance-v1/vectors.json", + "artifact_sha256": "3111e2dd43b12f74481450b4ad07e7b8f285ff9b5e6931fd1ea3ded521fc0e7c", + "generator": "src.evidence_freshness_conformance:build_vectors", + "manifest_path": "fixtures/contracts/evidence-freshness-conformance-v1/manifest.json", + "repository": "saagpatel/GithubRepoAuditor" + }, + "schema": "EvidenceFreshnessConformanceManifestV1", + "versioning": { + "compatibility": "consumers-must-ignore-unknown-top-level-fields-and-new-cases", + "policy": "additive-minor-breaking-major" + } +} diff --git a/fixtures/contracts/evidence-freshness-conformance-v1/vectors.json b/fixtures/contracts/evidence-freshness-conformance-v1/vectors.json new file mode 100644 index 0000000..9ae62cb --- /dev/null +++ b/fixtures/contracts/evidence-freshness-conformance-v1/vectors.json @@ -0,0 +1,235 @@ +{ + "additive_canary": { + "consumer_behavior": "ignore-compatible-addition" + }, + "canonical_reasons": [ + "within_fresh_window", + "within_aging_window", + "at_or_beyond_stale_boundary", + "timestamp_missing", + "timestamp_unparseable", + "timestamp_timezone_missing", + "timestamp_future_within_tolerance", + "timestamp_future_beyond_tolerance", + "read_unavailable", + "policy_not_applicable" + ], + "canonical_states": [ + "fresh", + "aging", + "stale", + "unknown", + "unavailable" + ], + "cases": [ + { + "expected": { + "age_ms": 0, + "reason": "within_fresh_window", + "state": "fresh" + }, + "id": "fresh_at_reader_clock", + "input": { + "generated_at": "2026-08-01T12:00:00.000Z", + "policy_state": "configured", + "read_state": "available" + } + }, + { + "expected": { + "age_ms": 604799999, + "reason": "within_fresh_window", + "state": "fresh" + }, + "id": "fresh_one_ms_before_aging_boundary", + "input": { + "generated_at": "2026-07-25T12:00:00.001Z", + "policy_state": "configured", + "read_state": "available" + } + }, + { + "expected": { + "age_ms": 604800000, + "reason": "within_aging_window", + "state": "aging" + }, + "id": "aging_at_exact_boundary", + "input": { + "generated_at": "2026-07-25T12:00:00.000Z", + "policy_state": "configured", + "read_state": "available" + } + }, + { + "expected": { + "age_ms": 1209599999, + "reason": "within_aging_window", + "state": "aging" + }, + "id": "aging_one_ms_before_stale_boundary", + "input": { + "generated_at": "2026-07-18T12:00:00.001Z", + "policy_state": "configured", + "read_state": "available" + } + }, + { + "expected": { + "age_ms": 1209600000, + "reason": "at_or_beyond_stale_boundary", + "state": "stale" + }, + "id": "stale_at_exact_boundary", + "input": { + "generated_at": "2026-07-18T12:00:00.000Z", + "policy_state": "configured", + "read_state": "available" + } + }, + { + "expected": { + "age_ms": 1209600001, + "reason": "at_or_beyond_stale_boundary", + "state": "stale" + }, + "id": "stale_beyond_boundary", + "input": { + "generated_at": "2026-07-18T11:59:59.999Z", + "policy_state": "configured", + "read_state": "available" + } + }, + { + "expected": { + "age_ms": null, + "reason": "timestamp_missing", + "state": "unknown" + }, + "id": "missing_timestamp", + "input": { + "generated_at": null, + "policy_state": "configured", + "read_state": "available" + } + }, + { + "expected": { + "age_ms": null, + "reason": "timestamp_unparseable", + "state": "unknown" + }, + "id": "empty_timestamp", + "input": { + "generated_at": "", + "policy_state": "configured", + "read_state": "available" + } + }, + { + "expected": { + "age_ms": null, + "reason": "timestamp_unparseable", + "state": "unknown" + }, + "id": "unparseable_timestamp", + "input": { + "generated_at": "not-a-timestamp", + "policy_state": "configured", + "read_state": "available" + } + }, + { + "expected": { + "age_ms": null, + "reason": "timestamp_timezone_missing", + "state": "unknown" + }, + "id": "timezone_missing", + "input": { + "generated_at": "2026-08-01T12:00:00.000", + "policy_state": "configured", + "read_state": "available" + } + }, + { + "expected": { + "age_ms": 0, + "reason": "within_fresh_window", + "state": "fresh" + }, + "id": "offset_normalizes_to_reader_clock", + "input": { + "generated_at": "2026-08-01T07:00:00.000-05:00", + "policy_state": "configured", + "read_state": "available" + } + }, + { + "expected": { + "age_ms": 0, + "reason": "timestamp_future_within_tolerance", + "state": "fresh" + }, + "id": "future_at_skew_tolerance", + "input": { + "generated_at": "2026-08-01T12:01:00.000Z", + "policy_state": "configured", + "read_state": "available" + } + }, + { + "expected": { + "age_ms": null, + "reason": "timestamp_future_beyond_tolerance", + "state": "unknown" + }, + "id": "future_one_ms_beyond_skew_tolerance", + "input": { + "generated_at": "2026-08-01T12:01:00.001Z", + "policy_state": "configured", + "read_state": "available" + } + }, + { + "expected": { + "age_ms": null, + "reason": "read_unavailable", + "state": "unavailable" + }, + "id": "unavailable_read_wins", + "input": { + "generated_at": "2026-08-01T12:00:00.000Z", + "policy_state": "configured", + "read_state": "unavailable" + } + }, + { + "expected": { + "age_ms": 60000, + "reason": "policy_not_applicable", + "state": "unknown" + }, + "id": "policy_not_applicable_preserves_age", + "input": { + "generated_at": "2026-08-01T11:59:00.000Z", + "policy_state": "not_applicable", + "read_state": "available" + } + } + ], + "contract_version": "1.0.0", + "evaluation_clock": "2026-08-01T12:00:00.000Z", + "policy": { + "aging_after_ms": 604800000, + "boundary_semantics": "fresh_when_age_lt_aging;aging_when_age_lt_stale;stale_when_age_gte_stale", + "skew_tolerance_ms": 60000, + "stale_after_ms": 1209600000 + }, + "schema": "EvidenceFreshnessConformanceV1", + "timestamp_contract": { + "normalization": "UTC", + "reader_clock": "injected-never-wall-clock", + "timezone": "explicit-Z-or-offset-required" + } +} diff --git a/scripts/generate_evidence_freshness_conformance.py b/scripts/generate_evidence_freshness_conformance.py new file mode 100644 index 0000000..2e51c6b --- /dev/null +++ b/scripts/generate_evidence_freshness_conformance.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Write or verify the EvidenceFreshnessConformanceV1 contract artifacts.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from src.evidence_freshness_conformance import ( # noqa: E402 + MANIFEST_RELATIVE_PATH, + VECTORS_RELATIVE_PATH, + manifest_bytes, + vectors_bytes, +) + + +def _artifacts() -> tuple[tuple[Path, bytes], ...]: + return ( + (REPO_ROOT / VECTORS_RELATIVE_PATH, vectors_bytes()), + (REPO_ROOT / MANIFEST_RELATIVE_PATH, manifest_bytes()), + ) + + +def _check() -> int: + drifted = [ + path.relative_to(REPO_ROOT) + for path, expected in _artifacts() + if not path.is_file() or path.read_bytes() != expected + ] + if drifted: + print( + "Evidence freshness conformance artifacts drifted: " + + ", ".join(str(path) for path in drifted), + file=sys.stderr, + ) + print( + "Regenerate with: python scripts/generate_evidence_freshness_conformance.py", + file=sys.stderr, + ) + return 1 + print("EvidenceFreshnessConformanceV1 artifacts are current.") + return 0 + + +def _write() -> int: + for path, content in _artifacts(): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(content) + print(f"Wrote {path.relative_to(REPO_ROOT)}") + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "--check", + action="store_true", + help="Fail when committed artifacts differ from deterministic output.", + ) + args = parser.parse_args() + return _check() if args.check else _write() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/evidence_freshness_conformance.py b/src/evidence_freshness_conformance.py new file mode 100644 index 0000000..da727c4 --- /dev/null +++ b/src/evidence_freshness_conformance.py @@ -0,0 +1,315 @@ +"""Deterministic, language-neutral evidence freshness conformance vectors. + +The contract standardizes only the edge semantics that must not drift between +consumers: timestamp parsing, an injected reader clock, skew handling, exact +threshold boundaries, unavailable reads, and canonical reason codes. Threshold +values, product labels, alert policy, and build-versus-reader behavior remain +consumer owned. +""" + +from __future__ import annotations + +import hashlib +import json +import re +from datetime import datetime, timedelta, timezone +from typing import Any + +CONTRACT_SCHEMA = "EvidenceFreshnessConformanceV1" +MANIFEST_SCHEMA = "EvidenceFreshnessConformanceManifestV1" +CONTRACT_VERSION = "1.0.0" +OWNER_REPOSITORY = "saagpatel/GithubRepoAuditor" +VECTORS_RELATIVE_PATH = ( + "fixtures/contracts/evidence-freshness-conformance-v1/vectors.json" +) +MANIFEST_RELATIVE_PATH = ( + "fixtures/contracts/evidence-freshness-conformance-v1/manifest.json" +) + +EVALUATION_CLOCK = datetime(2026, 8, 1, 12, 0, tzinfo=timezone.utc) +AGING_AFTER_MS = 7 * 24 * 60 * 60 * 1000 +STALE_AFTER_MS = 14 * 24 * 60 * 60 * 1000 +SKEW_TOLERANCE_MS = 60_000 + +CANONICAL_STATES = ("fresh", "aging", "stale", "unknown", "unavailable") +CANONICAL_REASONS = ( + "within_fresh_window", + "within_aging_window", + "at_or_beyond_stale_boundary", + "timestamp_missing", + "timestamp_unparseable", + "timestamp_timezone_missing", + "timestamp_future_within_tolerance", + "timestamp_future_beyond_tolerance", + "read_unavailable", + "policy_not_applicable", +) + +_EXPLICIT_TIMEZONE = re.compile(r"(?:[zZ]|[+-]\d{2}:\d{2})$") + + +def canonical_json_bytes(payload: dict[str, Any]) -> bytes: + rendered = json.dumps(payload, indent=2, sort_keys=True, ensure_ascii=False) + return f"{rendered}\n".encode() + + +def _iso(value: datetime) -> str: + return value.isoformat(timespec="milliseconds").replace("+00:00", "Z") + + +def _parse_timestamp(value: object) -> tuple[datetime | None, str | None]: + if value is None: + return None, "timestamp_missing" + if not isinstance(value, str) or not value.strip(): + return None, "timestamp_unparseable" + raw = value.strip() + try: + parsed = datetime.fromisoformat(raw.replace("Z", "+00:00").replace("z", "+00:00")) + except ValueError: + return None, "timestamp_unparseable" + if _EXPLICIT_TIMEZONE.search(raw) is None or parsed.tzinfo is None: + return None, "timestamp_timezone_missing" + return parsed.astimezone(timezone.utc), None + + +def evaluate_freshness( + *, + generated_at: object, + reader_clock: datetime, + read_state: str = "available", + policy_state: str = "configured", + aging_after_ms: int = AGING_AFTER_MS, + stale_after_ms: int = STALE_AFTER_MS, + skew_tolerance_ms: int = SKEW_TOLERANCE_MS, +) -> dict[str, Any]: + """Evaluate the canonical state and reason without reading wall time.""" + if reader_clock.tzinfo is None: + raise ValueError("reader_clock must include an explicit timezone") + if read_state not in {"available", "unavailable"}: + raise ValueError("read_state must be available or unavailable") + if policy_state not in {"configured", "not_applicable"}: + raise ValueError("policy_state must be configured or not_applicable") + if aging_after_ms <= 0 or stale_after_ms <= aging_after_ms: + raise ValueError("freshness thresholds must be positive and strictly ordered") + if skew_tolerance_ms < 0: + raise ValueError("skew_tolerance_ms must be non-negative") + + if read_state == "unavailable": + return {"state": "unavailable", "reason": "read_unavailable", "age_ms": None} + + generated, parse_reason = _parse_timestamp(generated_at) + if parse_reason is not None: + return {"state": "unknown", "reason": parse_reason, "age_ms": None} + assert generated is not None + + raw_age_ms = round( + (reader_clock.astimezone(timezone.utc) - generated).total_seconds() * 1000 + ) + if raw_age_ms < -skew_tolerance_ms: + return { + "state": "unknown", + "reason": "timestamp_future_beyond_tolerance", + "age_ms": None, + } + age_ms = max(0, raw_age_ms) + if policy_state == "not_applicable": + return { + "state": "unknown", + "reason": "policy_not_applicable", + "age_ms": age_ms, + } + if raw_age_ms < 0: + return { + "state": "fresh", + "reason": "timestamp_future_within_tolerance", + "age_ms": 0, + } + if age_ms < aging_after_ms: + return { + "state": "fresh", + "reason": "within_fresh_window", + "age_ms": age_ms, + } + if age_ms < stale_after_ms: + return { + "state": "aging", + "reason": "within_aging_window", + "age_ms": age_ms, + } + return { + "state": "stale", + "reason": "at_or_beyond_stale_boundary", + "age_ms": age_ms, + } + + +def _case( + case_id: str, + *, + generated_at: object, + read_state: str = "available", + policy_state: str = "configured", +) -> dict[str, Any]: + expected = evaluate_freshness( + generated_at=generated_at, + reader_clock=EVALUATION_CLOCK, + read_state=read_state, + policy_state=policy_state, + ) + return { + "id": case_id, + "input": { + "generated_at": generated_at, + "policy_state": policy_state, + "read_state": read_state, + }, + "expected": expected, + } + + +def build_vectors() -> dict[str, Any]: + clock = EVALUATION_CLOCK + return { + "schema": CONTRACT_SCHEMA, + "contract_version": CONTRACT_VERSION, + "evaluation_clock": _iso(clock), + "policy": { + "aging_after_ms": AGING_AFTER_MS, + "stale_after_ms": STALE_AFTER_MS, + "skew_tolerance_ms": SKEW_TOLERANCE_MS, + "boundary_semantics": ( + "fresh_when_age_lt_aging;aging_when_age_lt_stale;" + "stale_when_age_gte_stale" + ), + }, + "canonical_states": list(CANONICAL_STATES), + "canonical_reasons": list(CANONICAL_REASONS), + "timestamp_contract": { + "timezone": "explicit-Z-or-offset-required", + "reader_clock": "injected-never-wall-clock", + "normalization": "UTC", + }, + "cases": [ + _case("fresh_at_reader_clock", generated_at=_iso(clock)), + _case( + "fresh_one_ms_before_aging_boundary", + generated_at=_iso(clock - timedelta(milliseconds=AGING_AFTER_MS - 1)), + ), + _case( + "aging_at_exact_boundary", + generated_at=_iso(clock - timedelta(milliseconds=AGING_AFTER_MS)), + ), + _case( + "aging_one_ms_before_stale_boundary", + generated_at=_iso(clock - timedelta(milliseconds=STALE_AFTER_MS - 1)), + ), + _case( + "stale_at_exact_boundary", + generated_at=_iso(clock - timedelta(milliseconds=STALE_AFTER_MS)), + ), + _case( + "stale_beyond_boundary", + generated_at=_iso(clock - timedelta(milliseconds=STALE_AFTER_MS + 1)), + ), + _case("missing_timestamp", generated_at=None), + _case("empty_timestamp", generated_at=""), + _case("unparseable_timestamp", generated_at="not-a-timestamp"), + _case("timezone_missing", generated_at="2026-08-01T12:00:00.000"), + _case( + "offset_normalizes_to_reader_clock", + generated_at="2026-08-01T07:00:00.000-05:00", + ), + _case( + "future_at_skew_tolerance", + generated_at=_iso(clock + timedelta(milliseconds=SKEW_TOLERANCE_MS)), + ), + _case( + "future_one_ms_beyond_skew_tolerance", + generated_at=_iso(clock + timedelta(milliseconds=SKEW_TOLERANCE_MS + 1)), + ), + _case( + "unavailable_read_wins", + generated_at=_iso(clock), + read_state="unavailable", + ), + _case( + "policy_not_applicable_preserves_age", + generated_at=_iso(clock - timedelta(minutes=1)), + policy_state="not_applicable", + ), + ], + "additive_canary": {"consumer_behavior": "ignore-compatible-addition"}, + } + + +def vectors_bytes() -> bytes: + return canonical_json_bytes(build_vectors()) + + +def vectors_sha256() -> str: + return hashlib.sha256(vectors_bytes()).hexdigest() + + +def build_manifest() -> dict[str, Any]: + return { + "schema": MANIFEST_SCHEMA, + "contract_version": CONTRACT_VERSION, + "owner": { + "repository": OWNER_REPOSITORY, + "artifact_path": VECTORS_RELATIVE_PATH, + "artifact_sha256": vectors_sha256(), + "generator": "src.evidence_freshness_conformance:build_vectors", + "manifest_path": MANIFEST_RELATIVE_PATH, + }, + "versioning": { + "policy": "additive-minor-breaking-major", + "compatibility": ( + "consumers-must-ignore-unknown-top-level-fields-and-new-cases" + ), + }, + "consumer_profiles": { + "operator-control-plane": { + "state_mapping": { + "fresh": "fresh", + "aging": "aging", + "stale": "stale", + "unknown": "unverified", + "unavailable": "unavailable", + } + }, + "desktop-command-center": { + "state_mapping": { + "fresh": "fresh", + "aging": "aging", + "stale": "stale", + "unknown": "unknown", + "unavailable": "unknown", + } + }, + "public-site-projection": { + "state_mapping": { + "fresh": "fresh", + "aging": "resting", + "stale": "cold", + "unknown": "unknown", + "unavailable": "unknown", + } + }, + }, + "consumer_owned": [ + "threshold-values", + "ui-language", + "alert-policy", + "build-versus-reader-behavior", + "production-calculator", + ], + "non_goals": [ + "shared-runtime-library", + "shared-alert-policy", + "shared-ui-copy", + ], + } + + +def manifest_bytes() -> bytes: + return canonical_json_bytes(build_manifest()) diff --git a/tests/test_evidence_freshness_conformance.py b/tests/test_evidence_freshness_conformance.py new file mode 100644 index 0000000..b1d322f --- /dev/null +++ b/tests/test_evidence_freshness_conformance.py @@ -0,0 +1,133 @@ +"""Producer gate for EvidenceFreshnessConformanceV1.""" + +from __future__ import annotations + +import hashlib +import json +from datetime import datetime +from pathlib import Path + +import pytest + +from src.evidence_freshness_conformance import ( + AGING_AFTER_MS, + CANONICAL_REASONS, + CANONICAL_STATES, + CONTRACT_SCHEMA, + MANIFEST_RELATIVE_PATH, + OWNER_REPOSITORY, + SKEW_TOLERANCE_MS, + STALE_AFTER_MS, + VECTORS_RELATIVE_PATH, + build_manifest, + build_vectors, + evaluate_freshness, + manifest_bytes, + vectors_bytes, +) + + +def test_committed_artifacts_match_deterministic_generator() -> None: + assert Path(VECTORS_RELATIVE_PATH).read_bytes() == vectors_bytes() + assert Path(MANIFEST_RELATIVE_PATH).read_bytes() == manifest_bytes() + + +def test_manifest_binds_owner_and_vector_digest() -> None: + manifest = json.loads(Path(MANIFEST_RELATIVE_PATH).read_text()) + vector_raw = Path(VECTORS_RELATIVE_PATH).read_bytes() + + assert manifest == build_manifest() + assert manifest["owner"]["repository"] == OWNER_REPOSITORY + assert manifest["owner"]["artifact_sha256"] == hashlib.sha256(vector_raw).hexdigest() + assert manifest["versioning"]["policy"] == "additive-minor-breaking-major" + + +def test_vectors_cover_every_canonical_state_and_reason() -> None: + vectors = build_vectors() + states = {case["expected"]["state"] for case in vectors["cases"]} + reasons = {case["expected"]["reason"] for case in vectors["cases"]} + + assert vectors["schema"] == CONTRACT_SCHEMA + assert states == set(CANONICAL_STATES) + assert reasons == set(CANONICAL_REASONS) + + +def test_every_case_is_recomputed_by_the_reference_evaluator() -> None: + vectors = build_vectors() + clock = datetime.fromisoformat(vectors["evaluation_clock"].replace("Z", "+00:00")) + policy = vectors["policy"] + + for case in vectors["cases"]: + assert evaluate_freshness( + generated_at=case["input"]["generated_at"], + reader_clock=clock, + read_state=case["input"]["read_state"], + policy_state=case["input"]["policy_state"], + aging_after_ms=policy["aging_after_ms"], + stale_after_ms=policy["stale_after_ms"], + skew_tolerance_ms=policy["skew_tolerance_ms"], + ) == case["expected"], case["id"] + + +def test_exact_boundaries_and_future_tolerance_fail_closed() -> None: + by_id = {case["id"]: case["expected"] for case in build_vectors()["cases"]} + + assert by_id["fresh_one_ms_before_aging_boundary"] == { + "state": "fresh", + "reason": "within_fresh_window", + "age_ms": AGING_AFTER_MS - 1, + } + assert by_id["aging_at_exact_boundary"] == { + "state": "aging", + "reason": "within_aging_window", + "age_ms": AGING_AFTER_MS, + } + assert by_id["aging_one_ms_before_stale_boundary"]["age_ms"] == STALE_AFTER_MS - 1 + assert by_id["stale_at_exact_boundary"] == { + "state": "stale", + "reason": "at_or_beyond_stale_boundary", + "age_ms": STALE_AFTER_MS, + } + assert by_id["future_at_skew_tolerance"] == { + "state": "fresh", + "reason": "timestamp_future_within_tolerance", + "age_ms": 0, + } + assert by_id["future_one_ms_beyond_skew_tolerance"] == { + "state": "unknown", + "reason": "timestamp_future_beyond_tolerance", + "age_ms": None, + } + assert SKEW_TOLERANCE_MS == 60_000 + + +def test_reader_clock_must_be_injected_with_timezone() -> None: + with pytest.raises(ValueError, match="reader_clock"): + evaluate_freshness( + generated_at="2026-08-01T12:00:00Z", + reader_clock=datetime(2026, 8, 1, 12, 0), + ) + + +def test_public_contract_does_not_name_private_repositories_or_paths() -> None: + rendered = (vectors_bytes() + manifest_bytes()).decode().lower() + + assert "/users/" not in rendered + + +def test_consumer_profiles_map_reasons_without_branching_the_vectors() -> None: + manifest = build_manifest() + profiles = manifest["consumer_profiles"] + + assert set(profiles) == { + "operator-control-plane", + "desktop-command-center", + "public-site-projection", + } + for profile in profiles.values(): + assert set(profile["state_mapping"]) == set(CANONICAL_STATES) + assert manifest["non_goals"] == [ + "shared-runtime-library", + "shared-alert-policy", + "shared-ui-copy", + ]