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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to traust-contracts are documented here.

## [0.54.0]

### Added

- Storage revision 2: `portfolio_forge_facts` in SQLite and PostgreSQL, with
an explicit revision-1 migration that preserves existing records. Stores forge
observations and their audit-baseline context; derived repository state is not
stored. See [migration instructions](storage/v1/README.md#migrations).

## [0.53.0]

### Added
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.53.0
0.54.0
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "traust-contracts"
version = "0.53.0"
version = "0.54.0"
license = "Apache-2.0"
description = "Contract surface for the AI security ledger ecosystem: schemas and shared enums"
requires-python = ">=3.11"
Expand Down
3 changes: 2 additions & 1 deletion src/traust_contracts/v1/storage/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
#: and every earlier change are part of revision 1. Databases created under any
#: earlier schema are recreated, not migrated.
#: Each later step adds migrations/NNN_to_NNN+1.sql; Store.migrate() runs them.
REVISION = 1
#: 2: cached Portfolio forge observations (XWING-2214).
REVISION = 2


@cache
Expand Down
14 changes: 14 additions & 0 deletions storage/v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,17 @@ make test

PostgreSQL tests use the fixed local test DSN. They run when `psycopg` and the
database are available; otherwise they warn and skip.

## Portfolio forge observations

`portfolio_forge_facts` stores the latest forge observation, keyed by canonical URL
(`url:...`) or filing key (`key:...`) when no supported URL exists. Missing-URL
filings remain distinct; these lookup keys do not replace repository/CodeLine UUIDs.
The audited SHA, audit date and audit-presence flag identify the comparison
baseline; Core rejects reuse when it changes. Unknown measurements stay NULL.
Core derives age, churn, finding counts, risk and exposure on read; `RepoState`
is not stored.

Storage revision 2 adds this table to the configured database. For existing
revision-1 storage, back up and explicitly call `Store.migrate()`; `Store.init()`
refuses older revisions. See [Migrations](#migrations).
5 changes: 1 addition & 4 deletions storage/v1/postgres/migrations/001_to_002.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
-- Placeholder for storage v1 revision 1 -> 2.
--
-- Deltas only (ALTER / DROP / data UPDATE). New tables, indexes and views
-- belong in schema/ and views/; Store.migrate() re-runs those files.
-- Revision 2 adds portfolio_forge_facts through schema bootstrap; no existing rows change.
1 change: 1 addition & 0 deletions storage/v1/postgres/migrations/002_to_003.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-- Placeholder for revision 2 -> 3; deltas only.
27 changes: 27 additions & 0 deletions storage/v1/postgres/schema/portfolio_forge_facts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- Latest forge observation only; derived RepoState is never stored.
-- identity is a canonical URL key or a distinct missing-URL filing key, supplied by Core.
-- audited_sha, audit_date and has_audit identify the baseline used for the observation.
CREATE TABLE IF NOT EXISTS traust_storage.portfolio_forge_facts (
identity TEXT NOT NULL PRIMARY KEY,
repo_key TEXT NOT NULL UNIQUE,
repo_url TEXT,
source_url TEXT,
audited_sha TEXT,
audit_date TEXT,
has_audit BOOLEAN NOT NULL,
status TEXT NOT NULL,
pushed_at TEXT,
archived BOOLEAN,
default_branch TEXT,
visibility TEXT,
fork BOOLEAN,
parent TEXT,
ahead_by BIGINT,
changed_lines BIGINT,
sensitive_changed BOOLEAN,
sensitive_lines BIGINT,
deps_only BOOLEAN,
compare_truncated BOOLEAN,
observed_at TIMESTAMPTZ NOT NULL,
error TEXT
);
5 changes: 1 addition & 4 deletions storage/v1/sqlite/migrations/001_to_002.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
-- Placeholder for storage v1 revision 1 -> 2.
--
-- Deltas only (ALTER / DROP / data UPDATE). New tables, indexes and views
-- belong in schema/ and views/; Store.migrate() re-runs those files.
-- Revision 2 adds portfolio_forge_facts through schema bootstrap; no existing rows change.
1 change: 1 addition & 0 deletions storage/v1/sqlite/migrations/002_to_003.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-- Placeholder for revision 2 -> 3; deltas only.
27 changes: 27 additions & 0 deletions storage/v1/sqlite/schema/portfolio_forge_facts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- Latest forge observation only; derived RepoState is never stored.
-- identity is a canonical URL key or a distinct missing-URL filing key, supplied by Core.
-- audited_sha, audit_date and has_audit identify the baseline used for the observation.
CREATE TABLE IF NOT EXISTS portfolio_forge_facts (
identity TEXT NOT NULL PRIMARY KEY,
repo_key TEXT NOT NULL UNIQUE,
repo_url TEXT,
source_url TEXT,
audited_sha TEXT,
audit_date TEXT,
has_audit BOOLEAN NOT NULL,
status TEXT NOT NULL,
pushed_at TEXT,
archived BOOLEAN,
default_branch TEXT,
visibility TEXT,
fork BOOLEAN,
parent TEXT,
ahead_by BIGINT,
changed_lines BIGINT,
sensitive_changed BOOLEAN,
sensitive_lines BIGINT,
deps_only BOOLEAN,
compare_truncated BOOLEAN,
observed_at TEXT NOT NULL,
error TEXT
);
52 changes: 52 additions & 0 deletions tests/test_forge_facts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Upgrade safety and the persisted/derived boundary for XWING-2214."""

import sqlite3

import pytest

from traust_contracts.v1.storage import IngestError, Store
from traust_contracts.v1.storage.sql import REVISION


def test_revision_one_upgrade_adds_facts_without_changing_existing_rows():
conn = sqlite3.connect(":memory:")
store = Store(conn)
store.init()
product = store.register_product("example")
conn.execute("DROP TABLE portfolio_forge_facts")
conn.execute("UPDATE traust_storage_meta SET revision = 1")
conn.commit()
with pytest.raises(IngestError, match="migration"):
store.init()
assert store.migrate() == 1
assert conn.execute("SELECT revision FROM traust_storage_meta").fetchone() == (REVISION,)
assert store.register_product("example") == product
assert conn.execute("SELECT count(*) FROM portfolio_forge_facts").fetchone() == (0,)


def test_unknown_measurements_and_missing_url_filings_remain_distinct():
conn = sqlite3.connect(":memory:")
Store(conn).init()
for key in ("filing-a", "filing-b"):
conn.execute(
"""INSERT INTO portfolio_forge_facts
(identity, repo_key, has_audit, status, observed_at)
VALUES (?, ?, ?, ?, ?)""",
(f"key:{key}", key, True, "no-repo-url", "2026-10-01T00:00:00+00:00"),
)
assert conn.execute("SELECT repo_url, changed_lines FROM portfolio_forge_facts").fetchall() == [
(None, None),
(None, None),
]
columns = {row[1] for row in conn.execute("PRAGMA table_info(portfolio_forge_facts)")}
assert not columns & {
"risk_tier",
"exposure",
"audit_age_days",
"churn_ratio",
"live_crit_high",
}
assert {"audited_sha", "audit_date", "has_audit"} <= columns
assert not conn.execute(
"SELECT name FROM sqlite_master WHERE name='security_repo_state'"
).fetchall()
1 change: 1 addition & 0 deletions tests/test_storage_postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from traust_contracts.v1.storage.sql import REVISION

TABLES = {
"portfolio_forge_facts",
*REGISTRY_TABLES,
*INVENTORY_TABLES,
"artifact_binding",
Expand Down
6 changes: 3 additions & 3 deletions tests/test_storage_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,12 +331,12 @@ def test_migrate_runs_a_step_through_its_delta_file(monkeypatch: pytest.MonkeyPa
conn.execute("CREATE VIEW current_binding AS SELECT binding_id FROM artifact_binding")
conn.commit()
for module in ("traust_contracts.v1.storage.store", "traust_contracts.v1.storage.sql"):
monkeypatch.setattr(f"{module}.REVISION", 2)
monkeypatch.setattr(f"{module}.REVISION", REVISION + 1)
with pytest.raises(IngestError, match="explicit migration"):
store.init()
assert store.migrate() == 1
assert store.migrate() == REVISION
store.init()
assert conn.execute("SELECT revision FROM traust_storage_meta").fetchone() == (2,)
assert conn.execute("SELECT revision FROM traust_storage_meta").fetchone() == (REVISION + 1,)
assert _schema(conn) == _schema(_fresh())
assert store.find_product_repo("rhacm", ACM_CLI) == owner

Expand Down
3 changes: 2 additions & 1 deletion tests/test_storage_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"artifact_binding",
"artifact_location",
"traust_storage_meta",
"portfolio_forge_facts",
*PROJECTION_TABLES.values(),
*ALL_SECONDARY_PROJECTION_TABLES,
}
Expand All @@ -35,7 +36,7 @@ def test_storage_revision_matches_migration_placeholder() -> None:
# Revision 1 is the rebaselined schema (0.50.0). Every later step needs a
# delta file so Store.migrate() can build up to REVISION; the next step's
# file exists as a placeholder.
assert REVISION == 1
assert REVISION == 2
for dialect in ("sqlite", "postgres"):
migrations = storage_dir() / dialect / "migrations"
assert sorted(p.name for p in migrations.glob("*.sql")) == [
Expand Down
3 changes: 2 additions & 1 deletion tests/test_storage_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
TABLES = [
*REGISTRY_TABLES,
*INVENTORY_TABLES,
"portfolio_forge_facts",
"artifact_binding",
"artifact_evidence",
"artifact_location",
Expand Down Expand Up @@ -121,7 +122,7 @@ def test_init_revision_and_dependency_shape(store: Store) -> None:
store.init()
assert conn.execute("SELECT * FROM traust_storage_meta").fetchall() == original
conn.execute("ALTER TABLE report_finding DROP COLUMN blocked_external")
conn.execute("UPDATE traust_storage_meta SET revision=?", (2,))
conn.execute("UPDATE traust_storage_meta SET revision=?", (REVISION + 1,))
conn.commit()
legacy_dump = tuple(conn.iterdump())
with pytest.raises(IngestError, match=r"explicit migration"):
Expand Down
4 changes: 2 additions & 2 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.