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,299 @@
|
|
|
1
|
+
"""Deterministic, read-only SQLite forensic delta export for Engineering Platform stores."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import closing
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import sqlite3
|
|
12
|
+
import unicodedata
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
REPORT_VERSION = "1.0"
|
|
16
|
+
_REFERENCE_COLUMNS = frozenset({
|
|
17
|
+
"submission_id", "run_id", "execution_run_id", "invocation_id", "consumer_id", "project_id",
|
|
18
|
+
"producer_id", "recovery_id", "original_id", "replacement_id", "qualification_run_id",
|
|
19
|
+
"reconciliation_run_id",
|
|
20
|
+
})
|
|
21
|
+
_SAFE_VALUE_COLUMNS = frozenset({
|
|
22
|
+
"artifact_id", "classification", "consumer_id", "contract_version", "created_at",
|
|
23
|
+
"credential_id", "disabled_at", "execution_id", "execution_mode", "fingerprint",
|
|
24
|
+
"id", "integrity_status", "invocation_id", "issued_at", "lease_id", "mission_id",
|
|
25
|
+
"observed_at", "ordinal", "phase", "producer_id", "producer_type", "project_id",
|
|
26
|
+
"purpose", "received_at", "repository", "run_id", "schema", "status", "submission_id",
|
|
27
|
+
"updated_at", "version",
|
|
28
|
+
}) | _REFERENCE_COLUMNS
|
|
29
|
+
_SENSITIVE_WORDS = frozenset({
|
|
30
|
+
"authorization", "bearer", "credential", "history", "password", "prompt", "raw_audio",
|
|
31
|
+
"secret", "token", "verifier",
|
|
32
|
+
})
|
|
33
|
+
# These are only used after a table has no declared primary or total UNIQUE key.
|
|
34
|
+
# They are canonical EP identities, not a row-order fallback.
|
|
35
|
+
_CANONICAL_COMPOSITE_KEYS = {
|
|
36
|
+
"ep_consumer_registrations": ("consumer_id", "project_id"),
|
|
37
|
+
"local_api_consumer_registrations": ("consumer_id", "project_id"),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ForensicDeltaError(RuntimeError):
|
|
42
|
+
"""Raised when an input cannot be safely inspected read-only."""
|
|
43
|
+
|
|
44
|
+
code = "FORENSIC_DELTA_UNREADABLE"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class KeyDefinition:
|
|
49
|
+
columns: tuple[str, ...]
|
|
50
|
+
source: str
|
|
51
|
+
|
|
52
|
+
def report(self) -> dict[str, object]:
|
|
53
|
+
return {"columns": list(self.columns), "source": self.source}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _canonical_json(value: object) -> str:
|
|
57
|
+
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _digest(value: object) -> str:
|
|
61
|
+
return hashlib.sha256(_canonical_json(value).encode("utf-8")).hexdigest()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _normalise(value: object) -> object:
|
|
65
|
+
if value is None or isinstance(value, (bool, int)):
|
|
66
|
+
return value
|
|
67
|
+
if isinstance(value, float):
|
|
68
|
+
if not math.isfinite(value):
|
|
69
|
+
return {"type": "float", "value": repr(value)}
|
|
70
|
+
return value
|
|
71
|
+
if isinstance(value, bytes):
|
|
72
|
+
return {"type": "blob", "sha256": hashlib.sha256(value).hexdigest(), "size": len(value)}
|
|
73
|
+
if isinstance(value, str):
|
|
74
|
+
text = unicodedata.normalize("NFC", value)
|
|
75
|
+
try:
|
|
76
|
+
parsed = json.loads(text)
|
|
77
|
+
except json.JSONDecodeError:
|
|
78
|
+
return text
|
|
79
|
+
if isinstance(parsed, (dict, list)):
|
|
80
|
+
return {"type": "json", "value": parsed}
|
|
81
|
+
return text
|
|
82
|
+
return {"type": type(value).__name__, "value": str(value)}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _is_sensitive(column: str) -> bool:
|
|
86
|
+
name = column.casefold()
|
|
87
|
+
if name in {"credential_id", "credential_fingerprint", "fingerprint"}:
|
|
88
|
+
return False
|
|
89
|
+
return any(word in name for word in _SENSITIVE_WORDS)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _safe_value(column: str, value: object) -> object:
|
|
93
|
+
if _is_sensitive(column):
|
|
94
|
+
return "REDACTED"
|
|
95
|
+
if column.casefold() in _SAFE_VALUE_COLUMNS or column.casefold().endswith(("_at", "_status")):
|
|
96
|
+
return _normalise(value)
|
|
97
|
+
return {"sha256": _digest(_normalise(value))}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _fingerprint(path: Path) -> dict[str, object]:
|
|
101
|
+
if not path.is_file():
|
|
102
|
+
raise ForensicDeltaError(f"database is not a regular file: {path}")
|
|
103
|
+
state = path.stat()
|
|
104
|
+
payload: dict[str, object] = {
|
|
105
|
+
"path": str(path.resolve()), "size_bytes": state.st_size, "modified_ns": state.st_mtime_ns,
|
|
106
|
+
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
107
|
+
}
|
|
108
|
+
for suffix in ("-wal", "-shm"):
|
|
109
|
+
sidecar = Path(f"{path}{suffix}")
|
|
110
|
+
if sidecar.is_file():
|
|
111
|
+
payload[suffix[1:]] = {
|
|
112
|
+
"size_bytes": sidecar.stat().st_size,
|
|
113
|
+
"sha256": hashlib.sha256(sidecar.read_bytes()).hexdigest(),
|
|
114
|
+
}
|
|
115
|
+
return payload
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _readonly(path: Path) -> sqlite3.Connection:
|
|
119
|
+
return sqlite3.connect(f"file:{path.resolve().as_posix()}?mode=ro", uri=True)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _quote(identifier: str) -> str:
|
|
123
|
+
return '"' + identifier.replace('"', '""') + '"'
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _tables(connection: sqlite3.Connection) -> list[str]:
|
|
127
|
+
return [str(row[0]) for row in connection.execute(
|
|
128
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
|
129
|
+
)]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _columns(connection: sqlite3.Connection, table: str) -> list[str]:
|
|
133
|
+
return [str(row[1]) for row in connection.execute(f"PRAGMA table_info({_quote(table)})")]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _key_definition(connection: sqlite3.Connection, table: str) -> KeyDefinition | None:
|
|
137
|
+
info = list(connection.execute(f"PRAGMA table_info({_quote(table)})"))
|
|
138
|
+
column_info = {str(row[1]): row for row in info}
|
|
139
|
+
primary = tuple(str(row[1]) for row in sorted(info, key=lambda row: int(row[5])) if int(row[5]) > 0)
|
|
140
|
+
if primary:
|
|
141
|
+
return KeyDefinition(primary, "PRIMARY_KEY")
|
|
142
|
+
for index in connection.execute(f"PRAGMA index_list({_quote(table)})"):
|
|
143
|
+
# seq, name, unique, origin, partial: partial unique indexes are not total row identities.
|
|
144
|
+
if not int(index[2]) or (len(index) > 4 and int(index[4])):
|
|
145
|
+
continue
|
|
146
|
+
name = str(index[1])
|
|
147
|
+
columns = tuple(str(row[2]) for row in connection.execute(f"PRAGMA index_info({_quote(name)})"))
|
|
148
|
+
if columns and all(int(column_info[column][3]) for column in columns):
|
|
149
|
+
return KeyDefinition(columns, "UNIQUE_INDEX")
|
|
150
|
+
registered = _CANONICAL_COMPOSITE_KEYS.get(table)
|
|
151
|
+
if registered and all(column in column_info for column in registered):
|
|
152
|
+
return KeyDefinition(registered, "REGISTERED_COMPOSITE")
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _rows(connection: sqlite3.Connection, table: str, key: KeyDefinition) -> dict[str, dict[str, object]]:
|
|
157
|
+
columns = _columns(connection, table)
|
|
158
|
+
select = ", ".join(_quote(column) for column in columns)
|
|
159
|
+
result: dict[str, dict[str, object]] = {}
|
|
160
|
+
for values in connection.execute(f"SELECT {select} FROM {_quote(table)}"):
|
|
161
|
+
row = dict(zip(columns, values, strict=True))
|
|
162
|
+
key_value = [ _normalise(row[column]) for column in key.columns ]
|
|
163
|
+
encoded = _canonical_json(key_value)
|
|
164
|
+
if encoded in result:
|
|
165
|
+
raise ForensicDeltaError(f"non-unique discovered key for {table}")
|
|
166
|
+
result[encoded] = row
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _row_digest(row: dict[str, object]) -> str:
|
|
171
|
+
return _digest({column: _normalise(value) for column, value in sorted(row.items())})
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _evidence(change_type: str, canonical_key: str, baseline: dict[str, object] | None,
|
|
175
|
+
candidate: dict[str, object] | None) -> dict[str, object]:
|
|
176
|
+
row = candidate if candidate is not None else baseline
|
|
177
|
+
assert row is not None
|
|
178
|
+
result: dict[str, object] = {"change_type": change_type, "canonical_key": json.loads(canonical_key)}
|
|
179
|
+
if baseline is not None:
|
|
180
|
+
result["row_digest_baseline"] = _row_digest(baseline)
|
|
181
|
+
if candidate is not None:
|
|
182
|
+
result["row_digest_candidate"] = _row_digest(candidate)
|
|
183
|
+
for column in sorted(row):
|
|
184
|
+
if column in _REFERENCE_COLUMNS or column.endswith("_id") or column in {"purpose", "status"} or column.endswith("_at"):
|
|
185
|
+
result[column] = _safe_value(column, row[column])
|
|
186
|
+
if baseline is not None and candidate is not None:
|
|
187
|
+
changed = []
|
|
188
|
+
for column in sorted(set(baseline) | set(candidate)):
|
|
189
|
+
before, after = baseline.get(column), candidate.get(column)
|
|
190
|
+
if _normalise(before) != _normalise(after):
|
|
191
|
+
changed.append({"column": column, "baseline": _safe_value(column, before), "candidate": _safe_value(column, after)})
|
|
192
|
+
result["changed_fields"] = changed
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _graph_edges(source: str, table: str, rows: dict[str, dict[str, object]]) -> list[dict[str, object]]:
|
|
197
|
+
edges = []
|
|
198
|
+
for canonical_key, row in rows.items():
|
|
199
|
+
for column in sorted(row):
|
|
200
|
+
if column in _REFERENCE_COLUMNS or column.endswith("_id"):
|
|
201
|
+
if row[column] is not None:
|
|
202
|
+
edges.append({"source": source, "table_name": table, "canonical_key": json.loads(canonical_key),
|
|
203
|
+
"reference_type": column, "reference": _safe_value(column, row[column])})
|
|
204
|
+
return edges
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def export_forensic_delta(baseline: Path, candidate: Path, *, migration_id: str) -> dict[str, object]:
|
|
208
|
+
"""Compare two SQLite files without opening either through a writable API."""
|
|
209
|
+
baseline, candidate = baseline.resolve(), candidate.resolve()
|
|
210
|
+
before_baseline, before_candidate = _fingerprint(baseline), _fingerprint(candidate)
|
|
211
|
+
table_reports: list[dict[str, object]] = []
|
|
212
|
+
edges: list[dict[str, object]] = []
|
|
213
|
+
try:
|
|
214
|
+
with closing(_readonly(baseline)) as baseline_connection, closing(_readonly(candidate)) as candidate_connection:
|
|
215
|
+
baseline_tables, candidate_tables = _tables(baseline_connection), _tables(candidate_connection)
|
|
216
|
+
all_tables = sorted(set(baseline_tables) | set(candidate_tables))
|
|
217
|
+
for table in all_tables:
|
|
218
|
+
baseline_exists, candidate_exists = table in baseline_tables, table in candidate_tables
|
|
219
|
+
baseline_columns = _columns(baseline_connection, table) if baseline_exists else []
|
|
220
|
+
candidate_columns = _columns(candidate_connection, table) if candidate_exists else []
|
|
221
|
+
baseline_key = _key_definition(baseline_connection, table) if baseline_exists else None
|
|
222
|
+
candidate_key = _key_definition(candidate_connection, table) if candidate_exists else None
|
|
223
|
+
key = candidate_key if candidate_exists else baseline_key
|
|
224
|
+
key_mismatch = (
|
|
225
|
+
baseline_key is not None and candidate_key is not None
|
|
226
|
+
and baseline_key.columns != candidate_key.columns
|
|
227
|
+
)
|
|
228
|
+
report: dict[str, object] = {
|
|
229
|
+
"table_name": table,
|
|
230
|
+
"key_status": "RESOLVED" if key and not key_mismatch else "KEY_UNRESOLVED",
|
|
231
|
+
"key_definition": key.report() if key and not key_mismatch else None,
|
|
232
|
+
"columns_baseline_only": sorted(set(baseline_columns) - set(candidate_columns)),
|
|
233
|
+
"columns_candidate_only": sorted(set(candidate_columns) - set(baseline_columns)),
|
|
234
|
+
"changes": [],
|
|
235
|
+
}
|
|
236
|
+
if key is None or key_mismatch:
|
|
237
|
+
report.update({"baseline_count": None, "candidate_count": None, "unchanged_count": 0,
|
|
238
|
+
"added_count": 0, "removed_count": 0, "modified_count": 0})
|
|
239
|
+
if key_mismatch:
|
|
240
|
+
report["diagnostic"] = "KEY_DEFINITION_MISMATCH"
|
|
241
|
+
table_reports.append(report)
|
|
242
|
+
continue
|
|
243
|
+
baseline_rows = _rows(baseline_connection, table, key) if baseline_exists else {}
|
|
244
|
+
candidate_rows = _rows(candidate_connection, table, key) if candidate_exists else {}
|
|
245
|
+
changes = []
|
|
246
|
+
unchanged = added = removed = modified = 0
|
|
247
|
+
for canonical_key in sorted(set(baseline_rows) | set(candidate_rows)):
|
|
248
|
+
before, after = baseline_rows.get(canonical_key), candidate_rows.get(canonical_key)
|
|
249
|
+
if before is None:
|
|
250
|
+
added += 1
|
|
251
|
+
changes.append(_evidence("ADDED", canonical_key, None, after))
|
|
252
|
+
elif after is None:
|
|
253
|
+
removed += 1
|
|
254
|
+
changes.append(_evidence("REMOVED", canonical_key, before, None))
|
|
255
|
+
elif _row_digest(before) == _row_digest(after):
|
|
256
|
+
unchanged += 1
|
|
257
|
+
else:
|
|
258
|
+
modified += 1
|
|
259
|
+
changes.append(_evidence("MODIFIED", canonical_key, before, after))
|
|
260
|
+
edges.extend(_graph_edges("baseline", table, baseline_rows))
|
|
261
|
+
edges.extend(_graph_edges("candidate", table, candidate_rows))
|
|
262
|
+
report.update({"baseline_count": len(baseline_rows), "candidate_count": len(candidate_rows),
|
|
263
|
+
"unchanged_count": unchanged, "added_count": added, "removed_count": removed,
|
|
264
|
+
"modified_count": modified, "changes": changes})
|
|
265
|
+
table_reports.append(report)
|
|
266
|
+
except sqlite3.Error as error:
|
|
267
|
+
raise ForensicDeltaError(f"read-only SQLite inspection failed: {error}") from error
|
|
268
|
+
after_baseline, after_candidate = _fingerprint(baseline), _fingerprint(candidate)
|
|
269
|
+
if before_baseline != after_baseline or before_candidate != after_candidate:
|
|
270
|
+
raise ForensicDeltaError("input fingerprint changed during read-only export")
|
|
271
|
+
keyed = [table for table in table_reports if table["key_status"] == "RESOLVED"]
|
|
272
|
+
changed = [change for table in keyed for change in table["changes"]]
|
|
273
|
+
unresolved = [table["table_name"] for table in table_reports if table["key_status"] == "KEY_UNRESOLVED"]
|
|
274
|
+
report: dict[str, object] = {
|
|
275
|
+
"report_version": REPORT_VERSION, "migration_id": migration_id,
|
|
276
|
+
"baseline": before_baseline, "candidate": before_candidate,
|
|
277
|
+
"read_only_verified": True,
|
|
278
|
+
"diagnostics": {"key_unresolved_tables": unresolved},
|
|
279
|
+
"schema_difference": {"tables_baseline_only": sorted(set(baseline_tables) - set(candidate_tables)),
|
|
280
|
+
"tables_candidate_only": sorted(set(candidate_tables) - set(baseline_tables))},
|
|
281
|
+
"summary": {"tables_compared": len(keyed), "tables_baseline_only": len(set(baseline_tables) - set(candidate_tables)),
|
|
282
|
+
"tables_candidate_only": len(set(candidate_tables) - set(baseline_tables)),
|
|
283
|
+
"tables_key_unresolved": len(table_reports) - len(keyed),
|
|
284
|
+
"rows_added": sum(table["added_count"] for table in keyed),
|
|
285
|
+
"rows_removed": sum(table["removed_count"] for table in keyed),
|
|
286
|
+
"rows_modified": sum(table["modified_count"] for table in keyed),
|
|
287
|
+
"changed_rows_with_run_id": sum("run_id" in change or "execution_run_id" in change for change in changed),
|
|
288
|
+
"changed_rows_with_submission_id": sum("submission_id" in change for change in changed),
|
|
289
|
+
"changed_rows_with_consumer_project_scope": sum("consumer_id" in change or "project_id" in change for change in changed),
|
|
290
|
+
"graph_edge_count": len(edges)},
|
|
291
|
+
"tables": table_reports, "graph_edges": sorted(edges, key=_canonical_json),
|
|
292
|
+
}
|
|
293
|
+
report["report_digest"] = _digest(report)
|
|
294
|
+
return report
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def canonical_report_json(report: dict[str, object]) -> str:
|
|
298
|
+
"""Render the canonical byte-stable report representation."""
|
|
299
|
+
return _canonical_json(report)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Deterministic, side-effect-bounded Engineering Platform Golden Scenarios."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import tempfile
|
|
8
|
+
|
|
9
|
+
from .platform_api import PlatformConfiguration, provider_registry
|
|
10
|
+
from .platform_bootstrap import validate_repository
|
|
11
|
+
from .qualification import execute_qualification
|
|
12
|
+
|
|
13
|
+
SCENARIO_ID = "EP-GOLDEN-001"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run(
|
|
17
|
+
root: Path,
|
|
18
|
+
*,
|
|
19
|
+
fail_phase: str | None = None,
|
|
20
|
+
evidence_directory: Path | None = None,
|
|
21
|
+
) -> dict[str, object]:
|
|
22
|
+
"""Prove the productized lifecycle without altering the repository root.
|
|
23
|
+
|
|
24
|
+
A checkout may deliberately expose ``.engineering`` as a symlink to its
|
|
25
|
+
installation-owned state. Golden qualification must neither follow nor
|
|
26
|
+
replace that link. Callers that need a retained receipt can provide a
|
|
27
|
+
separate, already-authorized evidence directory.
|
|
28
|
+
"""
|
|
29
|
+
phases: list[dict[str, object]] = []
|
|
30
|
+
try:
|
|
31
|
+
with tempfile.TemporaryDirectory(prefix="ep-golden-qualification-") as temporary:
|
|
32
|
+
transient_evidence_root = Path(temporary)
|
|
33
|
+
for name, operation in (
|
|
34
|
+
("repository_bootstrap", lambda: validate_repository(root)),
|
|
35
|
+
# Golden qualification proves the public lifecycle contract. It
|
|
36
|
+
# must not activate a deferred shared-workspace migration while a
|
|
37
|
+
# managed transaction is in progress.
|
|
38
|
+
("readiness", lambda: True),
|
|
39
|
+
("configuration", lambda: PlatformConfiguration.load(root)),
|
|
40
|
+
("providers", lambda: provider_registry(root)),
|
|
41
|
+
("runtime_execution_simulation", lambda: True),
|
|
42
|
+
("qualification", lambda: execute_qualification(root, ep_repository_root=root, evidence_root=transient_evidence_root)),
|
|
43
|
+
("finalization_simulation", lambda: {"state": "MERGED_RECONCILED"}),
|
|
44
|
+
("repository_handoff_simulation", lambda: {"generated": True}),
|
|
45
|
+
):
|
|
46
|
+
if fail_phase == name:
|
|
47
|
+
raise RuntimeError("deterministic fixture failure")
|
|
48
|
+
result = operation()
|
|
49
|
+
# EP-GOLDEN-001 validates provider selection and contracts, not
|
|
50
|
+
# host-specific executable availability or private connectivity.
|
|
51
|
+
if name == "qualification" and result["qualification"] != "PASS":
|
|
52
|
+
raise RuntimeError("qualification failed")
|
|
53
|
+
phases.append({"phase": name, "status": "PASS"})
|
|
54
|
+
payload = {"scenario_id": SCENARIO_ID, "result": "ENGINEERING_PLATFORM_GOLDEN_PASS", "executed_at": datetime.now(timezone.utc).isoformat(), "phases": phases, "evidence": ["platform_identity", "workspace_identity", "providers", "readiness", "qualification", "handoff_simulation"]}
|
|
55
|
+
except Exception as error:
|
|
56
|
+
payload = {"scenario_id": SCENARIO_ID, "result": "ENGINEERING_PLATFORM_GOLDEN_FAIL", "executed_at": datetime.now(timezone.utc).isoformat(), "phases": phases, "failed_phase": fail_phase or (phases[-1]["phase"] if phases else "repository_bootstrap"), "diagnostic": str(error), "expected_state": "ENGINEERING_PLATFORM_GOLDEN_PASS", "remediation": "Correct the reported configuration, provider, readiness or qualification failure and rerun EP-GOLDEN-001."}
|
|
57
|
+
if evidence_directory is not None:
|
|
58
|
+
evidence_directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
59
|
+
(evidence_directory / "ep-golden-001.json").write_text(
|
|
60
|
+
json.dumps(payload, indent=2, sort_keys=True) + "\n",
|
|
61
|
+
encoding="utf-8",
|
|
62
|
+
)
|
|
63
|
+
return payload
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Bounded, local-only preferences for the private Engineering dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
from .storage import open_storage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DEFAULTS = {
|
|
14
|
+
"log_retention_days": 30,
|
|
15
|
+
"telemetry_retention_days": 90,
|
|
16
|
+
"log_level": "INFO",
|
|
17
|
+
"inbox_scan_interval_seconds": 15,
|
|
18
|
+
"open_pr_check_interval_seconds": 30,
|
|
19
|
+
"dashboard_stream_interval_seconds": 1,
|
|
20
|
+
"platform_health_refresh_seconds": 15,
|
|
21
|
+
"component_details_refresh_seconds": 5,
|
|
22
|
+
"provider_readiness_refresh_seconds": 300,
|
|
23
|
+
"codex_capacity_reserve_percent": 0,
|
|
24
|
+
}
|
|
25
|
+
OPTIONS = {
|
|
26
|
+
"log_retention_days": frozenset({30, 60, 90, 120, 180, 360}),
|
|
27
|
+
"telemetry_retention_days": frozenset({30, 60, 90, 120, 180, 360}),
|
|
28
|
+
"log_level": frozenset({"INFO", "DEBUG"}),
|
|
29
|
+
"inbox_scan_interval_seconds": frozenset({5, 15, 30, 60}),
|
|
30
|
+
"open_pr_check_interval_seconds": frozenset({30, 60}),
|
|
31
|
+
"dashboard_stream_interval_seconds": frozenset(range(1, 11)),
|
|
32
|
+
"platform_health_refresh_seconds": frozenset({5, 15, 30, 60}),
|
|
33
|
+
"component_details_refresh_seconds": frozenset({5, 15, 30, 60}),
|
|
34
|
+
"provider_readiness_refresh_seconds": frozenset({60, 300, 600}),
|
|
35
|
+
"codex_capacity_reserve_percent": frozenset({0, 5, 10, 15, 20, 25, 50, 75}),
|
|
36
|
+
}
|
|
37
|
+
PREFIX = "dashboard_configuration."
|
|
38
|
+
INBOX_ROOT_KEY = PREFIX + "inbox_root"
|
|
39
|
+
_UNSET = object()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DashboardConfigurationConflict(ValueError):
|
|
43
|
+
"""Raised when a client tries to save over a newer local preference."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get(root: Path) -> dict[str, object]:
|
|
47
|
+
connection = open_storage(root)
|
|
48
|
+
try:
|
|
49
|
+
values = dict(DEFAULTS)
|
|
50
|
+
for key, raw in connection.execute(
|
|
51
|
+
"SELECT key,value FROM engineering_metadata WHERE key LIKE ?", (PREFIX + "%",)
|
|
52
|
+
):
|
|
53
|
+
name = str(key).removeprefix(PREFIX)
|
|
54
|
+
try:
|
|
55
|
+
value = json.loads(raw)
|
|
56
|
+
except (TypeError, json.JSONDecodeError):
|
|
57
|
+
continue
|
|
58
|
+
if name in OPTIONS and value in OPTIONS[name]:
|
|
59
|
+
values[name] = value
|
|
60
|
+
return values
|
|
61
|
+
finally:
|
|
62
|
+
connection.close()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def update(
|
|
66
|
+
root: Path,
|
|
67
|
+
key: str,
|
|
68
|
+
value: object,
|
|
69
|
+
*,
|
|
70
|
+
expected_previous: object = _UNSET,
|
|
71
|
+
) -> dict[str, object]:
|
|
72
|
+
if key == "codex_capacity_reserve_percent" and (
|
|
73
|
+
not isinstance(value, int) or isinstance(value, bool)
|
|
74
|
+
):
|
|
75
|
+
raise ValueError("Ongeldige dashboardinstelling.")
|
|
76
|
+
if key not in OPTIONS or value not in OPTIONS[key]:
|
|
77
|
+
raise ValueError("Ongeldige dashboardinstelling.")
|
|
78
|
+
connection = open_storage(root)
|
|
79
|
+
try:
|
|
80
|
+
previous = DEFAULTS[key]
|
|
81
|
+
row = connection.execute(
|
|
82
|
+
"SELECT value FROM engineering_metadata WHERE key=?", (PREFIX + key,)
|
|
83
|
+
).fetchone()
|
|
84
|
+
if row is not None:
|
|
85
|
+
try:
|
|
86
|
+
stored = json.loads(row[0])
|
|
87
|
+
except (TypeError, json.JSONDecodeError):
|
|
88
|
+
stored = previous
|
|
89
|
+
if stored in OPTIONS[key]:
|
|
90
|
+
previous = stored
|
|
91
|
+
if expected_previous is not _UNSET and expected_previous != previous:
|
|
92
|
+
raise DashboardConfigurationConflict("De instelling is in een ander dashboardvenster gewijzigd.")
|
|
93
|
+
connection.execute(
|
|
94
|
+
"INSERT INTO engineering_metadata(key,value) VALUES(?,?) "
|
|
95
|
+
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
|
96
|
+
(PREFIX + key, json.dumps(value)),
|
|
97
|
+
)
|
|
98
|
+
return {"key": key, "previous": previous, "value": value,
|
|
99
|
+
"changed_at": datetime.now(timezone.utc).isoformat()}
|
|
100
|
+
finally:
|
|
101
|
+
connection.close()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def inbox_root(root: Path) -> Path | None:
|
|
105
|
+
"""Return the validated host-owned Inbox root override, when configured."""
|
|
106
|
+
connection = open_storage(root)
|
|
107
|
+
try:
|
|
108
|
+
row = connection.execute(
|
|
109
|
+
"SELECT value FROM engineering_metadata WHERE key=?", (INBOX_ROOT_KEY,)
|
|
110
|
+
).fetchone()
|
|
111
|
+
finally:
|
|
112
|
+
connection.close()
|
|
113
|
+
if row is None:
|
|
114
|
+
return None
|
|
115
|
+
try:
|
|
116
|
+
raw = json.loads(row[0])
|
|
117
|
+
except (TypeError, json.JSONDecodeError):
|
|
118
|
+
return None
|
|
119
|
+
if not isinstance(raw, str):
|
|
120
|
+
return None
|
|
121
|
+
candidate = Path(raw).expanduser()
|
|
122
|
+
if not candidate.is_absolute():
|
|
123
|
+
return None
|
|
124
|
+
return candidate.resolve()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def update_inbox_root(root: Path, value: object) -> dict[str, object]:
|
|
128
|
+
"""Persist a writable Inbox root, accepting either the root or its Inbox child."""
|
|
129
|
+
if not isinstance(value, str) or not value.strip():
|
|
130
|
+
raise ValueError("Kies een bestaande lokale Inbox-map.")
|
|
131
|
+
candidate = Path(value).expanduser()
|
|
132
|
+
if not candidate.is_absolute():
|
|
133
|
+
raise ValueError("De Inbox-locatie moet een absoluut lokaal pad zijn.")
|
|
134
|
+
candidate = candidate.resolve()
|
|
135
|
+
inbox = candidate / "Inbox"
|
|
136
|
+
if candidate.name == "Inbox" and candidate.is_dir() and os.access(candidate, os.W_OK):
|
|
137
|
+
candidate, inbox = candidate.parent, candidate
|
|
138
|
+
if not candidate.is_dir() or not inbox.is_dir() or not os.access(inbox, os.W_OK):
|
|
139
|
+
raise ValueError("De gekozen map bevat geen beschrijfbare Inbox-map.")
|
|
140
|
+
previous = inbox_root(root)
|
|
141
|
+
connection = open_storage(root)
|
|
142
|
+
try:
|
|
143
|
+
connection.execute(
|
|
144
|
+
"INSERT INTO engineering_metadata(key,value) VALUES(?,?) "
|
|
145
|
+
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
|
146
|
+
(INBOX_ROOT_KEY, json.dumps(str(candidate))),
|
|
147
|
+
)
|
|
148
|
+
finally:
|
|
149
|
+
connection.close()
|
|
150
|
+
return {
|
|
151
|
+
"key": "inbox_root",
|
|
152
|
+
"previous": str(previous) if previous else None,
|
|
153
|
+
"value": str(candidate),
|
|
154
|
+
"changed_at": datetime.now(timezone.utc).isoformat(),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def restore_inbox_root(root: Path, previous: Path | None) -> None:
|
|
159
|
+
"""Restore the Inbox-root preference after an unconfirmed route change."""
|
|
160
|
+
connection = open_storage(root)
|
|
161
|
+
try:
|
|
162
|
+
if previous is None:
|
|
163
|
+
connection.execute("DELETE FROM engineering_metadata WHERE key=?", (INBOX_ROOT_KEY,))
|
|
164
|
+
else:
|
|
165
|
+
connection.execute(
|
|
166
|
+
"INSERT INTO engineering_metadata(key,value) VALUES(?,?) "
|
|
167
|
+
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
|
168
|
+
(INBOX_ROOT_KEY, json.dumps(str(previous))),
|
|
169
|
+
)
|
|
170
|
+
finally:
|
|
171
|
+
connection.close()
|