engineering-platform 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +32 -0
- engineering_platform/ENGINEERING_PLATFORM_VERSION.json +15 -0
- engineering_platform/__init__.py +1 -0
- engineering_platform/__main__.py +7 -0
- engineering_platform/agent_state.py +530 -0
- engineering_platform/agent_trust.py +174 -0
- engineering_platform/assets/dashboard.css +1317 -0
- engineering_platform/assets/dashboard.js +8534 -0
- engineering_platform/assets/dashboard_locales.mjs +4049 -0
- engineering_platform/assets/dashboard_status_store.mjs +41 -0
- engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
- engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-dark.png +0 -0
- engineering_platform/assets/operations-console/icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-transparent.png +0 -0
- engineering_platform/assets/operations-console/manifest.webmanifest +11 -0
- engineering_platform/capability_preflight.py +285 -0
- engineering_platform/capability_review.py +261 -0
- engineering_platform/central_data_transfer.py +195 -0
- engineering_platform/central_database.py +245 -0
- engineering_platform/central_store_migration.py +1672 -0
- engineering_platform/codex_capacity.py +81 -0
- engineering_platform/codex_chat.py +226 -0
- engineering_platform/codex_observability.py +153 -0
- engineering_platform/component_lock.py +40 -0
- engineering_platform/component_logging.py +420 -0
- engineering_platform/console_presentation.py +14 -0
- engineering_platform/console_route_ownership.py +83 -0
- engineering_platform/contracts/__init__.py +38 -0
- engineering_platform/contracts/ep_consumer.py +391 -0
- engineering_platform/contracts/models.py +105 -0
- engineering_platform/contracts/projection.py +401 -0
- engineering_platform/dashboard_browser_validation.py +206 -0
- engineering_platform/dashboard_state.py +630 -0
- engineering_platform/dashboard_supervisor.swift +105 -0
- engineering_platform/dashboard_translation.py +129 -0
- engineering_platform/dependabot_producer.py +349 -0
- engineering_platform/drift_diagnostics.py +144 -0
- engineering_platform/emergency_recovery.py +268 -0
- engineering_platform/engineering_memory.py +139 -0
- engineering_platform/ep_consumer_credentials.py +473 -0
- engineering_platform/evidence_projection.py +213 -0
- engineering_platform/execution_activity.py +218 -0
- engineering_platform/execution_context.py +132 -0
- engineering_platform/execution_errors.py +42 -0
- engineering_platform/execution_evidence.py +24 -0
- engineering_platform/execution_executor.py +730 -0
- engineering_platform/execution_finalization.py +44 -0
- engineering_platform/execution_host.py +3306 -0
- engineering_platform/execution_lease.py +365 -0
- engineering_platform/execution_lifecycle.py +447 -0
- engineering_platform/execution_models.py +43 -0
- engineering_platform/execution_readiness.py +166 -0
- engineering_platform/execution_reporting.py +1607 -0
- engineering_platform/execution_repository.py +253 -0
- engineering_platform/execution_timeout_policy.py +56 -0
- engineering_platform/execution_timing.py +440 -0
- engineering_platform/execution_transaction.py +28 -0
- engineering_platform/external_producer_binding.py +235 -0
- engineering_platform/file_inbox.py +249 -0
- engineering_platform/forensic_attribution.py +338 -0
- engineering_platform/forensic_attribution_v2.py +134 -0
- engineering_platform/forensic_delta.py +299 -0
- engineering_platform/golden_scenario.py +63 -0
- engineering_platform/historical_dashboard_configuration.py +171 -0
- engineering_platform/host_admin.py +199 -0
- engineering_platform/host_preflight.py +231 -0
- engineering_platform/installation_relocation.py +122 -0
- engineering_platform/investigation_ledger.py +89 -0
- engineering_platform/legacy_inbox_migration.py +79 -0
- engineering_platform/lifecycle_worker.py +223 -0
- engineering_platform/live_status.py +267 -0
- engineering_platform/local_api.py +209 -0
- engineering_platform/local_api_keychain.py +51 -0
- engineering_platform/local_repository_binding.py +138 -0
- engineering_platform/managed_autonomy.py +509 -0
- engineering_platform/managed_codex_runtime.py +105 -0
- engineering_platform/parity_context.py +203 -0
- engineering_platform/parity_lifecycle_dispatcher.py +488 -0
- engineering_platform/platform_admin.py +13 -0
- engineering_platform/platform_api.py +428 -0
- engineering_platform/platform_bootstrap.py +385 -0
- engineering_platform/platform_components.py +65 -0
- engineering_platform/platform_version.py +171 -0
- engineering_platform/pr_check_repair.py +276 -0
- engineering_platform/pr_evidence_backfill.py +278 -0
- engineering_platform/producer.py +209 -0
- engineering_platform/project_agent.py +366 -0
- engineering_platform/project_agent_service.py +244 -0
- engineering_platform/project_topology.py +126 -0
- engineering_platform/prompt_history.py +591 -0
- engineering_platform/provider_context.py +136 -0
- engineering_platform/provider_context_benchmark.py +41 -0
- engineering_platform/provider_context_scope.py +90 -0
- engineering_platform/provider_interruption.py +168 -0
- engineering_platform/provider_process_identity.py +80 -0
- engineering_platform/provider_readiness.py +138 -0
- engineering_platform/provider_recovery.py +647 -0
- engineering_platform/provider_usage.py +497 -0
- engineering_platform/providers.py +471 -0
- engineering_platform/qualification.py +220 -0
- engineering_platform/recommendation_handoff.py +238 -0
- engineering_platform/report_analysis.py +193 -0
- engineering_platform/repository_attachment.py +171 -0
- engineering_platform/repository_handoff.py +95 -0
- engineering_platform/resources.py +38 -0
- engineering_platform/reviewer_evidence.py +70 -0
- engineering_platform/schemas/repository-attachment.schema.json +61 -0
- engineering_platform/server.py +3679 -0
- engineering_platform/server_console_services.py +2024 -0
- engineering_platform/server_relay.py +172 -0
- engineering_platform/server_service.py +122 -0
- engineering_platform/status_model.py +135 -0
- engineering_platform/status_reconciliation.py +34 -0
- engineering_platform/storage.py +2440 -0
- engineering_platform/submission_cli.py +77 -0
- engineering_platform/submission_intake.py +45 -0
- engineering_platform/submission_service.py +317 -0
- engineering_platform/telemetry.py +951 -0
- engineering_platform/templates/workspace-config.json +25 -0
- engineering_platform/validation_identity.py +50 -0
- engineering_platform/validation_profile.py +211 -0
- engineering_platform/workspace_preflight.py +263 -0
- engineering_platform/worktree_provenance.py +147 -0
- engineering_platform/worktree_tooling.py +18 -0
- engineering_platform-2.2.0.dist-info/METADATA +18 -0
- engineering_platform-2.2.0.dist-info/RECORD +130 -0
- engineering_platform-2.2.0.dist-info/WHEEL +5 -0
- engineering_platform-2.2.0.dist-info/entry_points.txt +6 -0
- engineering_platform-2.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""Deterministic, evidence-only provenance attribution for forensic delta reports.
|
|
2
|
+
|
|
3
|
+
This module never opens a database. It consumes the persisted JSON output of
|
|
4
|
+
``forensic_delta`` together with repository source and test evidence.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections import Counter, defaultdict
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import re
|
|
15
|
+
import subprocess
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
ATTRIBUTION_VERSION = "1.0"
|
|
20
|
+
REPORT_VERSION = "1.0"
|
|
21
|
+
ANCESTRY_ORIGINS = frozenset({"PRODUCTION", "TEST_HARNESS", "OPERATOR", "FORGE", "UNKNOWN"})
|
|
22
|
+
WRITER_ORIGINS = frozenset({"PRODUCTION_RUNTIME", "TEST_HARNESS", "OPERATOR_CONTROL", "FORGE_CONTROL", "MAINTENANCE", "UNKNOWN"})
|
|
23
|
+
STATE_SEMANTICS = frozenset({
|
|
24
|
+
"IMMUTABLE_BUSINESS_STATE", "EXECUTION_EVIDENCE", "CONTROL_STATE", "CONFIGURATION",
|
|
25
|
+
"MUTABLE_PROJECTION", "COMPONENT_LOG", "RETENTION_STATE", "TEST_ONLY_STRUCTURE", "UNKNOWN",
|
|
26
|
+
})
|
|
27
|
+
EVIDENCE_STATUSES = frozenset({"PROVEN", "UNRESOLVED"})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ForensicAttributionError(RuntimeError):
|
|
31
|
+
"""Raised when a forensic report cannot be safely or deterministically used."""
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def code(self) -> str:
|
|
35
|
+
return str(self.args[0]) if self.args else "FORENSIC_ATTRIBUTION_FAILED"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _canonical_json(value: object) -> str:
|
|
39
|
+
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _digest(value: object) -> str:
|
|
43
|
+
return hashlib.sha256(_canonical_json(value).encode("utf-8")).hexdigest()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def canonical_attribution_json(report: dict[str, object]) -> str:
|
|
47
|
+
"""Render the stable on-disk JSON representation."""
|
|
48
|
+
return _canonical_json(report)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _source_path(root: Path, path: Path) -> str:
|
|
52
|
+
return path.relative_to(root).as_posix()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class WriterCandidate:
|
|
57
|
+
table_name: str
|
|
58
|
+
write_api: str
|
|
59
|
+
production_callers: tuple[str, ...] = ()
|
|
60
|
+
test_callers: tuple[str, ...] = ()
|
|
61
|
+
operator_callers: tuple[str, ...] = ()
|
|
62
|
+
maintenance_callers: tuple[str, ...] = ()
|
|
63
|
+
|
|
64
|
+
def report(self) -> dict[str, object]:
|
|
65
|
+
return {
|
|
66
|
+
"table_name": self.table_name,
|
|
67
|
+
"write_api": self.write_api,
|
|
68
|
+
"production_callers": list(self.production_callers),
|
|
69
|
+
"test_callers": list(self.test_callers),
|
|
70
|
+
"operator_callers": list(self.operator_callers),
|
|
71
|
+
"maintenance_callers": list(self.maintenance_callers),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# This is an index of APIs, not a classification rule: shared writers never
|
|
76
|
+
# establish a row origin without a separate deterministic signal.
|
|
77
|
+
_WRITER_CANDIDATES = (
|
|
78
|
+
WriterCandidate("execution_submissions", "storage.record_submission", ("src/engineering_platform/human_text_ingress.py",), ("tests/engineering",)),
|
|
79
|
+
WriterCandidate("execution_runs", "storage.record_execution_run", (), ("tests/engineering",)),
|
|
80
|
+
WriterCandidate("ep_consumer_credentials", "ep_consumer_credentials.issue_or_rotate", ("src/engineering_platform/ep_consumer_credentials.py",), ("tests/engineering",), ("src/engineering_platform/dashboard.py",)),
|
|
81
|
+
WriterCandidate("ep_consumer_registrations", "ep_consumer_credentials.register_consumer", ("src/engineering_platform/ep_consumer_credentials.py",), ("tests/engineering",), ("src/engineering_platform/dashboard.py",)),
|
|
82
|
+
WriterCandidate("engineering_component_logs", "component_logging.log_event", ("src/engineering_platform",), ("tests/engineering",), maintenance_callers=("src/engineering_platform/component_logging.py",)),
|
|
83
|
+
WriterCandidate("engineering_metadata", "dashboard_configuration.update", ("src/engineering_platform/dashboard.py",), ("tests/engineering",)),
|
|
84
|
+
WriterCandidate("execution_projections", "storage.store_projection", ("src/engineering_platform",), ("tests/engineering",)),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def build_writer_index(repository_root: Path) -> list[dict[str, object]]:
|
|
89
|
+
"""Return the fixed, source-auditable candidate-writer index.
|
|
90
|
+
|
|
91
|
+
Paths are retained only when present, so output remains useful after Phase
|
|
92
|
+
3 extraction and does not claim a missing source file as evidence.
|
|
93
|
+
"""
|
|
94
|
+
root = repository_root.resolve()
|
|
95
|
+
result = []
|
|
96
|
+
for candidate in _WRITER_CANDIDATES:
|
|
97
|
+
item = candidate.report()
|
|
98
|
+
for field in ("production_callers", "test_callers", "operator_callers", "maintenance_callers"):
|
|
99
|
+
item[field] = [path for path in item[field] if (root / path).exists()]
|
|
100
|
+
result.append(item)
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _test_literal_index(repository_root: Path) -> dict[str, list[str]]:
|
|
105
|
+
"""Index exact quoted test literals as deterministic fixture evidence."""
|
|
106
|
+
root = repository_root.resolve()
|
|
107
|
+
matches: dict[str, list[str]] = defaultdict(list)
|
|
108
|
+
for folder in (root / "tests/engineering",):
|
|
109
|
+
if not folder.is_dir():
|
|
110
|
+
continue
|
|
111
|
+
for path in sorted(folder.rglob("*.py")):
|
|
112
|
+
text = path.read_text(encoding="utf-8")
|
|
113
|
+
# Only quoted tokens are indexed. A test-like name in a report is
|
|
114
|
+
# not enough; a literal must appear in a committed test fixture.
|
|
115
|
+
for literal in re.findall(r"(?<![\\w-])['\"]([A-Za-z][A-Za-z0-9_.:-]{2,})['\"]", text):
|
|
116
|
+
matches[literal].append(_source_path(root, path))
|
|
117
|
+
return {literal: sorted(set(paths)) for literal, paths in sorted(matches.items())}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _safe_string(change: dict[str, Any], field: str) -> str | None:
|
|
121
|
+
value = change.get(field)
|
|
122
|
+
return value if isinstance(value, str) and value != "REDACTED" else None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _canonical_key(change: dict[str, Any]) -> list[object]:
|
|
126
|
+
key = change.get("canonical_key")
|
|
127
|
+
return key if isinstance(key, list) else []
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _component_key(change: dict[str, Any]) -> str:
|
|
131
|
+
for field, prefix in (("submission_id", "submission"), ("run_id", "run"), ("execution_run_id", "run")):
|
|
132
|
+
value = _safe_string(change, field)
|
|
133
|
+
if value:
|
|
134
|
+
return f"{prefix}:{value}"
|
|
135
|
+
consumer, project = _safe_string(change, "consumer_id"), _safe_string(change, "project_id")
|
|
136
|
+
if consumer and project:
|
|
137
|
+
return f"authority:{consumer}:{project}"
|
|
138
|
+
return f"row:{change['table_name']}:{_canonical_json(_canonical_key(change))}"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _evidence(rule_id: str, *items: dict[str, object]) -> list[dict[str, object]]:
|
|
142
|
+
return [{"rule_id": rule_id, **item} for item in items]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _unknown(change: dict[str, Any], *, semantics: str = "UNKNOWN", rule_id: str = "NO_POSITIVE_EVIDENCE") -> dict[str, object]:
|
|
146
|
+
return {
|
|
147
|
+
"ancestry_origin": "UNKNOWN", "writer_origin": "UNKNOWN", "state_semantics": semantics,
|
|
148
|
+
"evidence_status": "UNRESOLVED", "rule_id": rule_id,
|
|
149
|
+
"evidence": _evidence(rule_id, {"type": "canonical_delta", "table_name": change["table_name"]}),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _fixture_evidence(change: dict[str, Any], literals: dict[str, list[str]]) -> dict[str, object] | None:
|
|
154
|
+
"""Recognize fixture families only from exact repository literals plus shape."""
|
|
155
|
+
values = [value for value in (
|
|
156
|
+
_safe_string(change, "submission_id"), _safe_string(change, "consumer_id"),
|
|
157
|
+
_safe_string(change, "project_id"), _safe_string(change, "credential_id"),
|
|
158
|
+
) if value]
|
|
159
|
+
sources = sorted({source for value in values for source in literals.get(value, [])})
|
|
160
|
+
if change["table_name"] == "execution_submissions" and _safe_string(change, "submission_id") in literals:
|
|
161
|
+
return {"ancestry_origin": "TEST_HARNESS", "writer_origin": "TEST_HARNESS",
|
|
162
|
+
"state_semantics": "IMMUTABLE_BUSINESS_STATE", "evidence_status": "PROVEN",
|
|
163
|
+
"rule_id": "EXACT_TEST_SUBMISSION_FIXTURE",
|
|
164
|
+
"evidence": _evidence("EXACT_TEST_SUBMISSION_FIXTURE", {"type": "exact_fixture_literal", "sources": sources})}
|
|
165
|
+
if change["table_name"] in {
|
|
166
|
+
"ep_consumer_credentials", "ep_consumer_registrations",
|
|
167
|
+
"local_api_credentials", "local_api_consumer_registrations",
|
|
168
|
+
} and len(sources) and len(values) >= 2:
|
|
169
|
+
# Explicit test scopes prove fixed credentials/registrations. Generated
|
|
170
|
+
# IDs require the scope plus an established lifecycle signal, never ID
|
|
171
|
+
# shape alone.
|
|
172
|
+
generated = _safe_string(change, "credential_id")
|
|
173
|
+
lifecycle = bool(change.get("revoked_at")) or (generated or "").startswith("qualification-") or (generated or "").startswith("production-")
|
|
174
|
+
if generated is None or generated in literals or lifecycle:
|
|
175
|
+
return {"ancestry_origin": "TEST_HARNESS", "writer_origin": "TEST_HARNESS",
|
|
176
|
+
"state_semantics": "CONTROL_STATE", "evidence_status": "PROVEN",
|
|
177
|
+
"rule_id": "TEST_AUTHORITY_FIXTURE_FAMILY",
|
|
178
|
+
"evidence": _evidence("TEST_AUTHORITY_FIXTURE_FAMILY", {"type": "fixture_scope_and_lifecycle", "sources": sources, "signals": ["exact_fixture_scope", "deterministic_lifecycle"]})}
|
|
179
|
+
run_id = _safe_string(change, "run_id") or _safe_string(change, "execution_run_id")
|
|
180
|
+
if change["table_name"].startswith(("execution_", "provider_")) and run_id and run_id in literals:
|
|
181
|
+
return {"ancestry_origin": "TEST_HARNESS", "writer_origin": "TEST_HARNESS",
|
|
182
|
+
"state_semantics": "EXECUTION_EVIDENCE", "evidence_status": "PROVEN",
|
|
183
|
+
"rule_id": "EXACT_TEST_RUN_FIXTURE",
|
|
184
|
+
"evidence": _evidence("EXACT_TEST_RUN_FIXTURE", {"type": "exact_fixture_literal", "sources": literals[run_id], "signals": ["exact_run_fixture", "execution_evidence_row"]})}
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _classify(change: dict[str, Any], literals: dict[str, list[str]]) -> dict[str, object]:
|
|
189
|
+
table, change_type = str(change["table_name"]), str(change["change_type"])
|
|
190
|
+
submission_id = _safe_string(change, "submission_id")
|
|
191
|
+
if table == "engineering_component_logs" and change_type == "REMOVED":
|
|
192
|
+
return {"ancestry_origin": "UNKNOWN", "writer_origin": "MAINTENANCE", "state_semantics": "RETENTION_STATE",
|
|
193
|
+
"evidence_status": "PROVEN", "rule_id": "COMPONENT_LOG_RETENTION",
|
|
194
|
+
"evidence": _evidence("COMPONENT_LOG_RETENTION", {"type": "repository_semantics", "source": "src/engineering_platform/component_logging.py", "signals": ["bounded_log_pruning_api", "removed_component_log"]})}
|
|
195
|
+
if table == "engineering_metadata":
|
|
196
|
+
key = str(_canonical_key(change)[0]) if _canonical_key(change) else ""
|
|
197
|
+
if key.startswith("dashboard_configuration."):
|
|
198
|
+
return {"ancestry_origin": "UNKNOWN", "writer_origin": "UNKNOWN", "state_semantics": "CONFIGURATION",
|
|
199
|
+
"evidence_status": "UNRESOLVED", "rule_id": "SHARED_CONFIGURATION_WRITER",
|
|
200
|
+
"evidence": _evidence("SHARED_CONFIGURATION_WRITER", {"type": "writer_index", "source": "src/engineering_platform/historical_dashboard_configuration.py", "signals": ["configuration_key", "shared_api_no_caller_receipt"]})}
|
|
201
|
+
if table == "execution_projections":
|
|
202
|
+
return {"ancestry_origin": "UNKNOWN", "writer_origin": "UNKNOWN", "state_semantics": "MUTABLE_PROJECTION",
|
|
203
|
+
"evidence_status": "UNRESOLVED", "rule_id": "SHARED_PROJECTION_WRITER",
|
|
204
|
+
"evidence": _evidence("SHARED_PROJECTION_WRITER", {"type": "writer_index", "source": "src/engineering_platform/storage.py", "signals": ["projection_table", "shared_api_no_caller_receipt"]})}
|
|
205
|
+
fixture = _fixture_evidence(change, literals)
|
|
206
|
+
if fixture:
|
|
207
|
+
return fixture
|
|
208
|
+
if table == "execution_submissions" and submission_id and re.fullmatch(r"human-ingress-[0-9a-f]{32,64}", submission_id) and (_safe_string(change, "producer_id") or "").startswith("human:"):
|
|
209
|
+
return {"ancestry_origin": "PRODUCTION", "writer_origin": "PRODUCTION_RUNTIME", "state_semantics": "IMMUTABLE_BUSINESS_STATE",
|
|
210
|
+
"evidence_status": "PROVEN", "rule_id": "CANONICAL_HUMAN_INGRESS_ENVELOPE",
|
|
211
|
+
"evidence": _evidence("CANONICAL_HUMAN_INGRESS_ENVELOPE", {"type": "canonical_ingress_structure", "source": "src/engineering_platform/human_text_ingress.py", "signals": ["canonical_human_ingress_id", "human_producer_binding", "immutable_submission_row"]})}
|
|
212
|
+
if table == "backup_probe":
|
|
213
|
+
return {"ancestry_origin": "TEST_HARNESS", "writer_origin": "TEST_HARNESS", "state_semantics": "TEST_ONLY_STRUCTURE",
|
|
214
|
+
"evidence_status": "PROVEN", "rule_id": "TEST_ONLY_SCHEMA_STRUCTURE",
|
|
215
|
+
"evidence": _evidence("TEST_ONLY_SCHEMA_STRUCTURE", {"type": "schema_difference", "signals": ["candidate_only_test_structure"]})}
|
|
216
|
+
semantics = "EXECUTION_EVIDENCE" if table.startswith(("execution_", "provider_", "managed_")) else "UNKNOWN"
|
|
217
|
+
return _unknown(change, semantics=semantics)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _binding_for(component_id: str, evidence_bindings: dict[str, Any]) -> dict[str, Any] | None:
|
|
221
|
+
binding = evidence_bindings.get(component_id)
|
|
222
|
+
if not isinstance(binding, dict) or binding.get("ancestry_origin") not in ANCESTRY_ORIGINS:
|
|
223
|
+
return None
|
|
224
|
+
evidence = binding.get("evidence")
|
|
225
|
+
if not isinstance(evidence, list) or not evidence or not all(isinstance(item, dict) and item.get("source") for item in evidence):
|
|
226
|
+
return None
|
|
227
|
+
if "writer_origin" in binding and binding["writer_origin"] not in WRITER_ORIGINS:
|
|
228
|
+
return None
|
|
229
|
+
if "state_semantics" in binding and binding["state_semantics"] not in STATE_SEMANTICS:
|
|
230
|
+
return None
|
|
231
|
+
return binding
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _verify_input(report: dict[str, Any], expected_report_digest: str | None) -> str:
|
|
235
|
+
if report.get("report_version") != REPORT_VERSION or not isinstance(report.get("migration_id"), str):
|
|
236
|
+
raise ForensicAttributionError("FORENSIC_REPORT_CONTRACT_INVALID")
|
|
237
|
+
for side in ("baseline", "candidate"):
|
|
238
|
+
if not isinstance(report.get(side), dict) or not isinstance(report[side].get("sha256"), str):
|
|
239
|
+
raise ForensicAttributionError("FORENSIC_REPORT_FINGERPRINT_MISSING")
|
|
240
|
+
observed = report.get("report_digest")
|
|
241
|
+
unsigned = dict(report)
|
|
242
|
+
unsigned.pop("report_digest", None)
|
|
243
|
+
if not isinstance(observed, str) or _digest(unsigned) != observed:
|
|
244
|
+
raise ForensicAttributionError("FORENSIC_REPORT_DIGEST_INVALID")
|
|
245
|
+
if expected_report_digest and observed != expected_report_digest:
|
|
246
|
+
raise ForensicAttributionError("FORENSIC_REPORT_DIGEST_MISMATCH")
|
|
247
|
+
return observed
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _repository_revision(repository_root: Path) -> str:
|
|
251
|
+
completed = subprocess.run(["git", "-C", str(repository_root), "rev-parse", "HEAD"], text=True, capture_output=True, check=False)
|
|
252
|
+
if completed.returncode:
|
|
253
|
+
raise ForensicAttributionError("REPOSITORY_REVISION_UNAVAILABLE")
|
|
254
|
+
return completed.stdout.strip()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _counts(rows: list[dict[str, object]], field: str, vocabulary: frozenset[str]) -> dict[str, int]:
|
|
258
|
+
observed = Counter(str(row[field]) for row in rows)
|
|
259
|
+
return {value: observed[value] for value in sorted(vocabulary)}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _schema_findings(report: dict[str, Any]) -> list[dict[str, object]]:
|
|
263
|
+
"""Attribute candidate-only structures without pretending they are row deltas."""
|
|
264
|
+
difference = report.get("schema_difference")
|
|
265
|
+
candidates = difference.get("tables_candidate_only", []) if isinstance(difference, dict) else []
|
|
266
|
+
findings = []
|
|
267
|
+
for table_name in sorted(value for value in candidates if isinstance(value, str)):
|
|
268
|
+
if table_name == "backup_probe":
|
|
269
|
+
findings.append({"table_name": table_name, "ancestry_origin": "TEST_HARNESS", "writer_origin": "TEST_HARNESS", "state_semantics": "TEST_ONLY_STRUCTURE", "evidence_status": "PROVEN", "rule_id": "TEST_ONLY_SCHEMA_STRUCTURE", "evidence": _evidence("TEST_ONLY_SCHEMA_STRUCTURE", {"type": "schema_difference", "signals": ["candidate_only_test_structure"]})})
|
|
270
|
+
else:
|
|
271
|
+
findings.append({"table_name": table_name, "ancestry_origin": "UNKNOWN", "writer_origin": "UNKNOWN", "state_semantics": "UNKNOWN", "evidence_status": "UNRESOLVED", "rule_id": "CANDIDATE_ONLY_SCHEMA_UNRESOLVED", "evidence": _evidence("CANDIDATE_ONLY_SCHEMA_UNRESOLVED", {"type": "schema_difference"})})
|
|
272
|
+
return findings
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def attribute_forensic_delta(report: dict[str, Any], *, repository_root: Path, expected_report_digest: str | None = None, repository_revision: str | None = None, evidence_bindings: dict[str, Any] | None = None) -> dict[str, object]:
|
|
276
|
+
"""Attribute a verified forensic report without opening any storage input."""
|
|
277
|
+
input_digest = _verify_input(report, expected_report_digest)
|
|
278
|
+
root = repository_root.resolve()
|
|
279
|
+
literals = _test_literal_index(root)
|
|
280
|
+
bindings = evidence_bindings or {}
|
|
281
|
+
rows: list[dict[str, object]] = []
|
|
282
|
+
for table in sorted(report.get("tables", []), key=lambda item: str(item.get("table_name", ""))):
|
|
283
|
+
if not isinstance(table, dict):
|
|
284
|
+
continue
|
|
285
|
+
for change in sorted(table.get("changes", []), key=_canonical_json):
|
|
286
|
+
if not isinstance(change, dict) or change.get("change_type") not in {"ADDED", "REMOVED", "MODIFIED"}:
|
|
287
|
+
continue
|
|
288
|
+
material = dict(change)
|
|
289
|
+
material["table_name"] = str(table.get("table_name", ""))
|
|
290
|
+
attributed = _classify(material, literals)
|
|
291
|
+
component_id = _component_key(material)
|
|
292
|
+
binding = _binding_for(component_id, bindings)
|
|
293
|
+
if binding:
|
|
294
|
+
attributed = dict(attributed)
|
|
295
|
+
attributed["ancestry_origin"] = binding["ancestry_origin"]
|
|
296
|
+
if "writer_origin" in binding:
|
|
297
|
+
attributed["writer_origin"] = binding["writer_origin"]
|
|
298
|
+
attributed["evidence_status"] = "PROVEN"
|
|
299
|
+
if "state_semantics" in binding:
|
|
300
|
+
attributed["state_semantics"] = binding["state_semantics"]
|
|
301
|
+
attributed["evidence"] = [*attributed["evidence"], {"rule_id": "EXTERNAL_EVIDENCE_BINDING", "type": "immutable_evidence_binding", "source": binding["evidence"][0]["source"], "signals": binding["evidence"][0].get("signals", [])}]
|
|
302
|
+
rows.append({
|
|
303
|
+
"table_name": material["table_name"], "change_type": material["change_type"],
|
|
304
|
+
"canonical_key": _canonical_key(material), "component_id": component_id,
|
|
305
|
+
"ancestry_origin": attributed["ancestry_origin"], "writer_origin": attributed["writer_origin"],
|
|
306
|
+
"state_semantics": attributed["state_semantics"], "evidence_status": attributed["evidence_status"],
|
|
307
|
+
"rule_id": attributed["rule_id"], "evidence": attributed["evidence"],
|
|
308
|
+
})
|
|
309
|
+
rows.sort(key=_canonical_json)
|
|
310
|
+
components: dict[str, list[str]] = defaultdict(list)
|
|
311
|
+
for row in rows:
|
|
312
|
+
components[str(row["component_id"])].append(_digest(row))
|
|
313
|
+
component_reports = [{"component_id": key, "row_digests": sorted(values)} for key, values in sorted(components.items())]
|
|
314
|
+
schema_findings = _schema_findings(report)
|
|
315
|
+
summary = {
|
|
316
|
+
"changed_row_count": len(rows), "component_count": len(component_reports), "schema_finding_count": len(schema_findings),
|
|
317
|
+
"writer_origin": _counts(rows, "writer_origin", WRITER_ORIGINS),
|
|
318
|
+
"ancestry_origin": _counts(rows, "ancestry_origin", ANCESTRY_ORIGINS),
|
|
319
|
+
"state_semantics": _counts(rows, "state_semantics", STATE_SEMANTICS),
|
|
320
|
+
"evidence_status": _counts(rows, "evidence_status", EVIDENCE_STATUSES),
|
|
321
|
+
}
|
|
322
|
+
result: dict[str, object] = {
|
|
323
|
+
"attribution_version": ATTRIBUTION_VERSION, "input_report": {"report_digest": input_digest, "report_version": report["report_version"], "migration_id": report["migration_id"], "baseline_fingerprint": report["baseline"]["sha256"], "candidate_fingerprint": report["candidate"]["sha256"]},
|
|
324
|
+
"repository_revision": repository_revision or _repository_revision(root), "writer_index": build_writer_index(root),
|
|
325
|
+
"summary": summary, "components": component_reports, "schema_findings": schema_findings, "rows": rows,
|
|
326
|
+
}
|
|
327
|
+
result["report_digest"] = _digest(result)
|
|
328
|
+
return result
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def load_and_attribute(path: Path, *, repository_root: Path, expected_report_digest: str | None = None, evidence_bindings: dict[str, Any] | None = None) -> dict[str, object]:
|
|
332
|
+
try:
|
|
333
|
+
report = json.loads(path.read_text(encoding="utf-8"))
|
|
334
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
335
|
+
raise ForensicAttributionError("FORENSIC_REPORT_UNREADABLE") from error
|
|
336
|
+
if not isinstance(report, dict):
|
|
337
|
+
raise ForensicAttributionError("FORENSIC_REPORT_CONTRACT_INVALID")
|
|
338
|
+
return attribute_forensic_delta(report, repository_root=repository_root, expected_report_digest=expected_report_digest, evidence_bindings=evidence_bindings)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Deterministic V2 enrichment of an existing forensic attribution report.
|
|
2
|
+
|
|
3
|
+
V2 consumes only V1 JSON and committed repository evidence. It never opens a
|
|
4
|
+
database or replays the delta exporter.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections import Counter
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import subprocess
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .forensic_attribution import _test_literal_index, canonical_attribution_json
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
ATTRIBUTION_VERSION = "2.0"
|
|
20
|
+
RULES_VERSION = "2.0"
|
|
21
|
+
V1_VERSION = "1.0"
|
|
22
|
+
_ORIGINS = ("FORGE_CONTROL", "MAINTENANCE", "OPERATOR_CONTROL", "PRODUCTION_RUNTIME", "TEST_HARNESS", "UNKNOWN")
|
|
23
|
+
_ANCESTRIES = ("FORGE", "OPERATOR", "PRODUCTION", "TEST_HARNESS", "UNKNOWN")
|
|
24
|
+
_STATUSES = ("PROVEN", "UNRESOLVED")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ForensicAttributionV2Error(RuntimeError):
|
|
28
|
+
"""Raised when a V1 report cannot be safely enriched."""
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def code(self) -> str:
|
|
32
|
+
return str(self.args[0]) if self.args else "FORENSIC_ATTRIBUTION_V2_FAILED"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _canonical_json(value: object) -> str:
|
|
36
|
+
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _digest(value: object) -> str:
|
|
40
|
+
return hashlib.sha256(_canonical_json(value).encode("utf-8")).hexdigest()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _revision(root: Path) -> str:
|
|
44
|
+
completed = subprocess.run(["git", "-C", str(root), "rev-parse", "HEAD"], text=True, capture_output=True, check=False)
|
|
45
|
+
if completed.returncode:
|
|
46
|
+
raise ForensicAttributionV2Error("REPOSITORY_REVISION_UNAVAILABLE")
|
|
47
|
+
return completed.stdout.strip()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _verify_v1(report: dict[str, Any], expected_digest: str | None) -> str:
|
|
51
|
+
if report.get("attribution_version") != V1_VERSION:
|
|
52
|
+
raise ForensicAttributionV2Error("ATTRIBUTION_V1_VERSION_INVALID")
|
|
53
|
+
input_report = report.get("input_report")
|
|
54
|
+
if not isinstance(input_report, dict) or not isinstance(input_report.get("report_digest"), str):
|
|
55
|
+
raise ForensicAttributionV2Error("ATTRIBUTION_V1_INPUT_INVALID")
|
|
56
|
+
observed = report.get("report_digest")
|
|
57
|
+
unsigned = dict(report)
|
|
58
|
+
unsigned.pop("report_digest", None)
|
|
59
|
+
if not isinstance(observed, str) or _digest(unsigned) != observed:
|
|
60
|
+
raise ForensicAttributionV2Error("ATTRIBUTION_V1_DIGEST_INVALID")
|
|
61
|
+
if expected_digest and observed != expected_digest:
|
|
62
|
+
raise ForensicAttributionV2Error("ATTRIBUTION_V1_DIGEST_MISMATCH")
|
|
63
|
+
return observed
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _component_literal(component_id: object) -> str | None:
|
|
67
|
+
if not isinstance(component_id, str) or not component_id.startswith(("run:", "submission:")):
|
|
68
|
+
return None
|
|
69
|
+
value = component_id.split(":", 1)[1]
|
|
70
|
+
return value or None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _counts(rows: list[dict[str, Any]], key: str, vocabulary: tuple[str, ...]) -> dict[str, int]:
|
|
74
|
+
observed = Counter(str(row.get(key)) for row in rows)
|
|
75
|
+
return {value: observed[value] for value in vocabulary}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def enrich_attribution_v2(report: dict[str, Any], *, repository_root: Path, expected_attribution_digest: str | None = None, repository_revision: str | None = None) -> dict[str, object]:
|
|
79
|
+
"""Apply only direct, exact-test-component evidence to V1 unresolved rows."""
|
|
80
|
+
v1_digest = _verify_v1(report, expected_attribution_digest)
|
|
81
|
+
root = repository_root.resolve()
|
|
82
|
+
literals = _test_literal_index(root)
|
|
83
|
+
rows: list[dict[str, Any]] = []
|
|
84
|
+
for original in report.get("rows", []):
|
|
85
|
+
if not isinstance(original, dict):
|
|
86
|
+
continue
|
|
87
|
+
row = dict(original)
|
|
88
|
+
literal = _component_literal(row.get("component_id"))
|
|
89
|
+
sources = literals.get(literal or "", [])
|
|
90
|
+
if row.get("evidence_status") == "UNRESOLVED" and literal and sources:
|
|
91
|
+
# This applies only when the V1 row directly names the run or
|
|
92
|
+
# submission component; a test-like string or ancestry alone never
|
|
93
|
+
# qualifies. The pre-existing semantics remain unchanged.
|
|
94
|
+
row.update({
|
|
95
|
+
"ancestry_origin": "TEST_HARNESS", "writer_origin": "TEST_HARNESS",
|
|
96
|
+
"evidence_status": "PROVEN", "rule_id": "EXACT_TEST_COMPONENT_FIXTURE",
|
|
97
|
+
"evidence": [*list(row.get("evidence", [])), {
|
|
98
|
+
"rule_id": "EXACT_TEST_COMPONENT_FIXTURE", "type": "exact_fixture_component",
|
|
99
|
+
"sources": sources, "signals": ["direct_run_or_submission_component", "exact_committed_test_literal"],
|
|
100
|
+
}],
|
|
101
|
+
})
|
|
102
|
+
rows.append(row)
|
|
103
|
+
rows.sort(key=_canonical_json)
|
|
104
|
+
summary = {
|
|
105
|
+
"changed_row_count": len(rows),
|
|
106
|
+
"writer_origin": _counts(rows, "writer_origin", _ORIGINS),
|
|
107
|
+
"ancestry_origin": _counts(rows, "ancestry_origin", _ANCESTRIES),
|
|
108
|
+
"evidence_status": _counts(rows, "evidence_status", _STATUSES),
|
|
109
|
+
"v1_unresolved_count": sum(1 for row in report.get("rows", []) if isinstance(row, dict) and row.get("evidence_status") == "UNRESOLVED"),
|
|
110
|
+
"v2_resolved_count": sum(1 for row in rows if row.get("rule_id") == "EXACT_TEST_COMPONENT_FIXTURE"),
|
|
111
|
+
}
|
|
112
|
+
result: dict[str, object] = {
|
|
113
|
+
"attribution_version": ATTRIBUTION_VERSION, "attribution_rules_version": RULES_VERSION,
|
|
114
|
+
"input_attribution": {"report_digest": v1_digest, "attribution_version": report["attribution_version"], "forensic_report_digest": report["input_report"]["report_digest"]},
|
|
115
|
+
"repository_revision": repository_revision or _revision(root), "summary": summary,
|
|
116
|
+
"schema_findings": report.get("schema_findings", []), "rows": rows,
|
|
117
|
+
}
|
|
118
|
+
result["report_digest"] = _digest(result)
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def load_and_enrich_v2(path: Path, *, repository_root: Path, expected_attribution_digest: str | None = None) -> dict[str, object]:
|
|
123
|
+
try:
|
|
124
|
+
report = json.loads(path.read_text(encoding="utf-8"))
|
|
125
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
126
|
+
raise ForensicAttributionV2Error("ATTRIBUTION_V1_UNREADABLE") from error
|
|
127
|
+
if not isinstance(report, dict):
|
|
128
|
+
raise ForensicAttributionV2Error("ATTRIBUTION_V1_INPUT_INVALID")
|
|
129
|
+
return enrich_attribution_v2(report, repository_root=repository_root, expected_attribution_digest=expected_attribution_digest)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def canonical_attribution_v2_json(report: dict[str, object]) -> str:
|
|
133
|
+
"""Keep the public V2 JSON renderer explicit at the command boundary."""
|
|
134
|
+
return canonical_attribution_json(report)
|