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.
Files changed (130) hide show
  1. engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +32 -0
  2. engineering_platform/ENGINEERING_PLATFORM_VERSION.json +15 -0
  3. engineering_platform/__init__.py +1 -0
  4. engineering_platform/__main__.py +7 -0
  5. engineering_platform/agent_state.py +530 -0
  6. engineering_platform/agent_trust.py +174 -0
  7. engineering_platform/assets/dashboard.css +1317 -0
  8. engineering_platform/assets/dashboard.js +8534 -0
  9. engineering_platform/assets/dashboard_locales.mjs +4049 -0
  10. engineering_platform/assets/dashboard_status_store.mjs +41 -0
  11. engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
  12. engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
  13. engineering_platform/assets/operations-console/icon-dark.png +0 -0
  14. engineering_platform/assets/operations-console/icon-light.png +0 -0
  15. engineering_platform/assets/operations-console/icon-transparent.png +0 -0
  16. engineering_platform/assets/operations-console/manifest.webmanifest +11 -0
  17. engineering_platform/capability_preflight.py +285 -0
  18. engineering_platform/capability_review.py +261 -0
  19. engineering_platform/central_data_transfer.py +195 -0
  20. engineering_platform/central_database.py +245 -0
  21. engineering_platform/central_store_migration.py +1672 -0
  22. engineering_platform/codex_capacity.py +81 -0
  23. engineering_platform/codex_chat.py +226 -0
  24. engineering_platform/codex_observability.py +153 -0
  25. engineering_platform/component_lock.py +40 -0
  26. engineering_platform/component_logging.py +420 -0
  27. engineering_platform/console_presentation.py +14 -0
  28. engineering_platform/console_route_ownership.py +83 -0
  29. engineering_platform/contracts/__init__.py +38 -0
  30. engineering_platform/contracts/ep_consumer.py +391 -0
  31. engineering_platform/contracts/models.py +105 -0
  32. engineering_platform/contracts/projection.py +401 -0
  33. engineering_platform/dashboard_browser_validation.py +206 -0
  34. engineering_platform/dashboard_state.py +630 -0
  35. engineering_platform/dashboard_supervisor.swift +105 -0
  36. engineering_platform/dashboard_translation.py +129 -0
  37. engineering_platform/dependabot_producer.py +349 -0
  38. engineering_platform/drift_diagnostics.py +144 -0
  39. engineering_platform/emergency_recovery.py +268 -0
  40. engineering_platform/engineering_memory.py +139 -0
  41. engineering_platform/ep_consumer_credentials.py +473 -0
  42. engineering_platform/evidence_projection.py +213 -0
  43. engineering_platform/execution_activity.py +218 -0
  44. engineering_platform/execution_context.py +132 -0
  45. engineering_platform/execution_errors.py +42 -0
  46. engineering_platform/execution_evidence.py +24 -0
  47. engineering_platform/execution_executor.py +730 -0
  48. engineering_platform/execution_finalization.py +44 -0
  49. engineering_platform/execution_host.py +3306 -0
  50. engineering_platform/execution_lease.py +365 -0
  51. engineering_platform/execution_lifecycle.py +447 -0
  52. engineering_platform/execution_models.py +43 -0
  53. engineering_platform/execution_readiness.py +166 -0
  54. engineering_platform/execution_reporting.py +1607 -0
  55. engineering_platform/execution_repository.py +253 -0
  56. engineering_platform/execution_timeout_policy.py +56 -0
  57. engineering_platform/execution_timing.py +440 -0
  58. engineering_platform/execution_transaction.py +28 -0
  59. engineering_platform/external_producer_binding.py +235 -0
  60. engineering_platform/file_inbox.py +249 -0
  61. engineering_platform/forensic_attribution.py +338 -0
  62. engineering_platform/forensic_attribution_v2.py +134 -0
  63. engineering_platform/forensic_delta.py +299 -0
  64. engineering_platform/golden_scenario.py +63 -0
  65. engineering_platform/historical_dashboard_configuration.py +171 -0
  66. engineering_platform/host_admin.py +199 -0
  67. engineering_platform/host_preflight.py +231 -0
  68. engineering_platform/installation_relocation.py +122 -0
  69. engineering_platform/investigation_ledger.py +89 -0
  70. engineering_platform/legacy_inbox_migration.py +79 -0
  71. engineering_platform/lifecycle_worker.py +223 -0
  72. engineering_platform/live_status.py +267 -0
  73. engineering_platform/local_api.py +209 -0
  74. engineering_platform/local_api_keychain.py +51 -0
  75. engineering_platform/local_repository_binding.py +138 -0
  76. engineering_platform/managed_autonomy.py +509 -0
  77. engineering_platform/managed_codex_runtime.py +105 -0
  78. engineering_platform/parity_context.py +203 -0
  79. engineering_platform/parity_lifecycle_dispatcher.py +488 -0
  80. engineering_platform/platform_admin.py +13 -0
  81. engineering_platform/platform_api.py +428 -0
  82. engineering_platform/platform_bootstrap.py +385 -0
  83. engineering_platform/platform_components.py +65 -0
  84. engineering_platform/platform_version.py +171 -0
  85. engineering_platform/pr_check_repair.py +276 -0
  86. engineering_platform/pr_evidence_backfill.py +278 -0
  87. engineering_platform/producer.py +209 -0
  88. engineering_platform/project_agent.py +366 -0
  89. engineering_platform/project_agent_service.py +244 -0
  90. engineering_platform/project_topology.py +126 -0
  91. engineering_platform/prompt_history.py +591 -0
  92. engineering_platform/provider_context.py +136 -0
  93. engineering_platform/provider_context_benchmark.py +41 -0
  94. engineering_platform/provider_context_scope.py +90 -0
  95. engineering_platform/provider_interruption.py +168 -0
  96. engineering_platform/provider_process_identity.py +80 -0
  97. engineering_platform/provider_readiness.py +138 -0
  98. engineering_platform/provider_recovery.py +647 -0
  99. engineering_platform/provider_usage.py +497 -0
  100. engineering_platform/providers.py +471 -0
  101. engineering_platform/qualification.py +220 -0
  102. engineering_platform/recommendation_handoff.py +238 -0
  103. engineering_platform/report_analysis.py +193 -0
  104. engineering_platform/repository_attachment.py +171 -0
  105. engineering_platform/repository_handoff.py +95 -0
  106. engineering_platform/resources.py +38 -0
  107. engineering_platform/reviewer_evidence.py +70 -0
  108. engineering_platform/schemas/repository-attachment.schema.json +61 -0
  109. engineering_platform/server.py +3679 -0
  110. engineering_platform/server_console_services.py +2024 -0
  111. engineering_platform/server_relay.py +172 -0
  112. engineering_platform/server_service.py +122 -0
  113. engineering_platform/status_model.py +135 -0
  114. engineering_platform/status_reconciliation.py +34 -0
  115. engineering_platform/storage.py +2440 -0
  116. engineering_platform/submission_cli.py +77 -0
  117. engineering_platform/submission_intake.py +45 -0
  118. engineering_platform/submission_service.py +317 -0
  119. engineering_platform/telemetry.py +951 -0
  120. engineering_platform/templates/workspace-config.json +25 -0
  121. engineering_platform/validation_identity.py +50 -0
  122. engineering_platform/validation_profile.py +211 -0
  123. engineering_platform/workspace_preflight.py +263 -0
  124. engineering_platform/worktree_provenance.py +147 -0
  125. engineering_platform/worktree_tooling.py +18 -0
  126. engineering_platform-2.2.0.dist-info/METADATA +18 -0
  127. engineering_platform-2.2.0.dist-info/RECORD +130 -0
  128. engineering_platform-2.2.0.dist-info/WHEEL +5 -0
  129. engineering_platform-2.2.0.dist-info/entry_points.txt +6 -0
  130. 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)