plm-shared 0.1.0__tar.gz
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.
- plm_shared-0.1.0/PKG-INFO +264 -0
- plm_shared-0.1.0/README.md +241 -0
- plm_shared-0.1.0/plm_shared/__init__.py +11 -0
- plm_shared-0.1.0/plm_shared/_w1_13_backfill.py +159 -0
- plm_shared-0.1.0/plm_shared/agents/__init__.py +14 -0
- plm_shared-0.1.0/plm_shared/agents/base_agent.py +110 -0
- plm_shared-0.1.0/plm_shared/agents/domain_classifier.py +86 -0
- plm_shared-0.1.0/plm_shared/agents/domain_prompts.py +99 -0
- plm_shared-0.1.0/plm_shared/agents/raci_extractor.py +80 -0
- plm_shared-0.1.0/plm_shared/artifact_publisher.py +342 -0
- plm_shared-0.1.0/plm_shared/artifact_store.py +244 -0
- plm_shared-0.1.0/plm_shared/autonomy.py +188 -0
- plm_shared-0.1.0/plm_shared/autonomy_gating.yaml +73 -0
- plm_shared-0.1.0/plm_shared/capabilities.py +64 -0
- plm_shared-0.1.0/plm_shared/capabilities.yaml +29 -0
- plm_shared-0.1.0/plm_shared/capability_registry/__init__.py +49 -0
- plm_shared-0.1.0/plm_shared/capability_registry/types.py +261 -0
- plm_shared-0.1.0/plm_shared/capability_registry.yaml +355 -0
- plm_shared-0.1.0/plm_shared/cors.py +223 -0
- plm_shared-0.1.0/plm_shared/db.py +599 -0
- plm_shared-0.1.0/plm_shared/errors/__init__.py +29 -0
- plm_shared-0.1.0/plm_shared/errors/envelope.py +32 -0
- plm_shared-0.1.0/plm_shared/governance/__init__.py +142 -0
- plm_shared-0.1.0/plm_shared/governance/bus.py +155 -0
- plm_shared-0.1.0/plm_shared/governance/envelope.py +168 -0
- plm_shared-0.1.0/plm_shared/governance/events.py +300 -0
- plm_shared-0.1.0/plm_shared/governance/hitl.py +198 -0
- plm_shared-0.1.0/plm_shared/governance/invariants.py +224 -0
- plm_shared-0.1.0/plm_shared/governance/middleware.py +249 -0
- plm_shared-0.1.0/plm_shared/governance/signed_conformance.py +187 -0
- plm_shared-0.1.0/plm_shared/idempotency.py +67 -0
- plm_shared-0.1.0/plm_shared/identity.py +313 -0
- plm_shared-0.1.0/plm_shared/invocation_kinds.py +64 -0
- plm_shared-0.1.0/plm_shared/kernel_api.py +169 -0
- plm_shared-0.1.0/plm_shared/knowledge/__init__.py +66 -0
- plm_shared-0.1.0/plm_shared/knowledge/primitives_v1.py +287 -0
- plm_shared-0.1.0/plm_shared/knowledge_envelope.py +45 -0
- plm_shared-0.1.0/plm_shared/llm_errors.py +329 -0
- plm_shared-0.1.0/plm_shared/mcp/__init__.py +13 -0
- plm_shared-0.1.0/plm_shared/mcp/federation/__init__.py +44 -0
- plm_shared-0.1.0/plm_shared/mcp/federation/builder.py +416 -0
- plm_shared-0.1.0/plm_shared/mcp/federation/manifest_v1.py +134 -0
- plm_shared-0.1.0/plm_shared/middleware.py +201 -0
- plm_shared-0.1.0/plm_shared/models/__init__.py +24 -0
- plm_shared-0.1.0/plm_shared/models/cleansing.py +284 -0
- plm_shared-0.1.0/plm_shared/models/db.py +292 -0
- plm_shared-0.1.0/plm_shared/models/document_schemas.py +94 -0
- plm_shared-0.1.0/plm_shared/models/issue_schemas.py +104 -0
- plm_shared-0.1.0/plm_shared/models/parse_schemas.py +76 -0
- plm_shared-0.1.0/plm_shared/parsers/__init__.py +15 -0
- plm_shared-0.1.0/plm_shared/parsers/bpmn_text.py +152 -0
- plm_shared-0.1.0/plm_shared/plm_schemas.py +243 -0
- plm_shared-0.1.0/plm_shared/pricing.py +196 -0
- plm_shared-0.1.0/plm_shared/protocols/__init__.py +52 -0
- plm_shared-0.1.0/plm_shared/protocols/_registry.py +130 -0
- plm_shared-0.1.0/plm_shared/protocols/services/__init__.py +8 -0
- plm_shared-0.1.0/plm_shared/protocols/services/agent.py +55 -0
- plm_shared-0.1.0/plm_shared/protocols/services/database.py +83 -0
- plm_shared-0.1.0/plm_shared/protocols/services/feature_flags.py +47 -0
- plm_shared-0.1.0/plm_shared/protocols/services/llm.py +142 -0
- plm_shared-0.1.0/plm_shared/protocols/services/plan_analysis.py +69 -0
- plm_shared-0.1.0/plm_shared/protocols/services/rag.py +67 -0
- plm_shared-0.1.0/plm_shared/protocols/services/retry.py +109 -0
- plm_shared-0.1.0/plm_shared/protocols/services/settings.py +69 -0
- plm_shared-0.1.0/plm_shared/protocols/services/telemetry.py +50 -0
- plm_shared-0.1.0/plm_shared/skill_auth.py +161 -0
- plm_shared-0.1.0/plm_shared/skill_auth.yaml +97 -0
- plm_shared-0.1.0/plm_shared/sqlalchemy_kernel_emitter.py +522 -0
- plm_shared-0.1.0/plm_shared/system_context.py +59 -0
- plm_shared-0.1.0/plm_shared/telemetry/__init__.py +16 -0
- plm_shared-0.1.0/plm_shared/trace/__init__.py +55 -0
- plm_shared-0.1.0/plm_shared/trace/conformance_gate.py +145 -0
- plm_shared-0.1.0/plm_shared/trace/envelope.py +130 -0
- plm_shared-0.1.0/plm_shared/trace/manifest.py +75 -0
- plm_shared-0.1.0/plm_shared/trace/promotion_rule.py +92 -0
- plm_shared-0.1.0/plm_shared/trace/quality_levels.py +85 -0
- plm_shared-0.1.0/plm_shared/trace_context.py +205 -0
- plm_shared-0.1.0/plm_shared/utils/__init__.py +7 -0
- plm_shared-0.1.0/plm_shared/utils/plm_tools.py +531 -0
- plm_shared-0.1.0/plm_shared.egg-info/PKG-INFO +264 -0
- plm_shared-0.1.0/plm_shared.egg-info/SOURCES.txt +115 -0
- plm_shared-0.1.0/plm_shared.egg-info/dependency_links.txt +1 -0
- plm_shared-0.1.0/plm_shared.egg-info/requires.txt +17 -0
- plm_shared-0.1.0/plm_shared.egg-info/top_level.txt +1 -0
- plm_shared-0.1.0/pyproject.toml +129 -0
- plm_shared-0.1.0/setup.cfg +4 -0
- plm_shared-0.1.0/tests/test_artifact_publisher.py +239 -0
- plm_shared-0.1.0/tests/test_artifact_store.py +90 -0
- plm_shared-0.1.0/tests/test_capabilities.py +61 -0
- plm_shared-0.1.0/tests/test_capability_registry_types.py +264 -0
- plm_shared-0.1.0/tests/test_conformance_gate.py +275 -0
- plm_shared-0.1.0/tests/test_cors_helper.py +109 -0
- plm_shared-0.1.0/tests/test_db_models.py +176 -0
- plm_shared-0.1.0/tests/test_external_trace_envelope.py +182 -0
- plm_shared-0.1.0/tests/test_federation_manifest.py +248 -0
- plm_shared-0.1.0/tests/test_governance_events.py +361 -0
- plm_shared-0.1.0/tests/test_hitl_types.py +175 -0
- plm_shared-0.1.0/tests/test_idempotency.py +73 -0
- plm_shared-0.1.0/tests/test_identity.py +251 -0
- plm_shared-0.1.0/tests/test_kernel_api_models.py +231 -0
- plm_shared-0.1.0/tests/test_knowledge_non_mutation.py +91 -0
- plm_shared-0.1.0/tests/test_knowledge_primitives_v1.py +215 -0
- plm_shared-0.1.0/tests/test_mge_v1.py +236 -0
- plm_shared-0.1.0/tests/test_mge_validator.py +271 -0
- plm_shared-0.1.0/tests/test_middleware.py +254 -0
- plm_shared-0.1.0/tests/test_object_store_backend.py +294 -0
- plm_shared-0.1.0/tests/test_pricing.py +186 -0
- plm_shared-0.1.0/tests/test_protocols_lifted_modules.py +193 -0
- plm_shared-0.1.0/tests/test_protocols_registry.py +99 -0
- plm_shared-0.1.0/tests/test_protocols_services_proxies.py +163 -0
- plm_shared-0.1.0/tests/test_skill_auth.py +133 -0
- plm_shared-0.1.0/tests/test_sqlalchemy_kernel_emitter.py +233 -0
- plm_shared-0.1.0/tests/test_trace_context.py +159 -0
- plm_shared-0.1.0/tests/test_w1_10_signed_conformance.py +389 -0
- plm_shared-0.1.0/tests/test_w1_13_typed_columns.py +247 -0
- plm_shared-0.1.0/tests/test_w1_1_skeleton.py +242 -0
- plm_shared-0.1.0/tests/test_w1_9_promotion_rule.py +499 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plm-shared
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TracePulse PLM shared contracts: Kernel API surface, idempotency, capabilities, knowledge envelope, pricing, artifact store, trace context. Frozen in US-W1.0; consumed by US-W1.1+ and US-CR.0+.
|
|
5
|
+
Author: TracePulse
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: pydantic>=2.5.0
|
|
10
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
11
|
+
Requires-Dist: starlette<1.0,>=0.27
|
|
12
|
+
Requires-Dist: PyYAML>=6.0
|
|
13
|
+
Provides-Extra: db
|
|
14
|
+
Requires-Dist: SQLAlchemy>=2.0; extra == "db"
|
|
15
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == "db"
|
|
16
|
+
Requires-Dist: alembic>=1.13; extra == "db"
|
|
17
|
+
Provides-Extra: s3
|
|
18
|
+
Requires-Dist: boto3>=1.34; extra == "s3"
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest; extra == "test"
|
|
21
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
22
|
+
Requires-Dist: httpx<1.0,>=0.27; extra == "test"
|
|
23
|
+
|
|
24
|
+
# plm-shared
|
|
25
|
+
|
|
26
|
+
Frozen contracts shared across TracePulse PLM product lines (Core,
|
|
27
|
+
Skill Kernel, Skill Packages, Knowledge, Workbench, Studio,
|
|
28
|
+
Connectors). Owned by US-W1.0; consumed by US-W1.1 → US-W1.13 and
|
|
29
|
+
US-CR.0 → US-CR.13 without modification.
|
|
30
|
+
|
|
31
|
+
US-W1.0 ships in 7 PRs:
|
|
32
|
+
|
|
33
|
+
| PR | Scope |
|
|
34
|
+
|-----|-------------------------------------------------------------------|
|
|
35
|
+
| PR-1 | package skeleton + Pydantic models + IdempotencyKey + Capability |
|
|
36
|
+
| PR-2 | Alembic migrations 0001-0007 + SQLAlchemy ORM + `db.py` |
|
|
37
|
+
| PR-3 | migration 0008 (Postgres roles) + append-only audit suite |
|
|
38
|
+
| PR-4 | migration 0009 (RLS) + `middleware.py` + tenant-isolation audit |
|
|
39
|
+
| PR-5 | `ObjectStoreBackend` (S3 wire protocol) + `[s3]` extra + tests |
|
|
40
|
+
| PR-6 | `SqlAlchemyKernelEmitter` + migration 0011 (pricing dedup) + integ |
|
|
41
|
+
| PR-7 | `import-linter` contracts + BL5 + CI workflow + AC + QA + close |
|
|
42
|
+
|
|
43
|
+
## Surface
|
|
44
|
+
|
|
45
|
+
| Module | Purpose |
|
|
46
|
+
|-----------------------------|-------------------------------------------------------------|
|
|
47
|
+
| `kernel_api` | Pydantic args + `KernelEmitter` Protocol (frozen) |
|
|
48
|
+
| `sqlalchemy_kernel_emitter` | Concrete `SqlAlchemyKernelEmitter` impl (PR-6) |
|
|
49
|
+
| `idempotency` | `IdempotencyKey` + `compute_content_hash` |
|
|
50
|
+
| `capabilities` | `Capability` enum + YAML loader |
|
|
51
|
+
| `knowledge_envelope` | `KnowledgeEnvelopeV1` frozen schema |
|
|
52
|
+
| `trace_context` | `TraceContext` ContextVar + W0.6 bridge |
|
|
53
|
+
| `pricing` | `compute_cost` + `CostStatus` + `PricingSnapshot` dataclass |
|
|
54
|
+
| `artifact_store` | `ArtifactStorageBackend` Protocol + `LocalFilesystemBackend` + `ObjectStoreBackend` |
|
|
55
|
+
| `middleware` | `PlmTenantMiddleware` (ASGI) + `parse_core_caller` + `cors_allowed_headers` |
|
|
56
|
+
| `db` | SQLAlchemy 2.0 ORM models + `tenant_scoped_session` |
|
|
57
|
+
|
|
58
|
+
## Kernel API (US-W1.0 / AC-2)
|
|
59
|
+
|
|
60
|
+
`plm_shared.kernel_api.KernelEmitter` is the **frozen** 4-method
|
|
61
|
+
Protocol that every product line uses to record events. The
|
|
62
|
+
authorised emitter boundary is Core: product lines submit decisions
|
|
63
|
+
to Core (e.g. Workbench routes HITL approvals), and Core is the
|
|
64
|
+
authorised emitter through `plm-shared`.
|
|
65
|
+
|
|
66
|
+
### Protocol
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from plm_shared.kernel_api import KernelEmitter
|
|
70
|
+
from plm_shared.kernel_api import (
|
|
71
|
+
EmitTelemetryArgs,
|
|
72
|
+
EmitGovernanceArgs,
|
|
73
|
+
EmitLlmCallArgs,
|
|
74
|
+
EmitConnectorCallArgs,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
class KernelEmitter(Protocol):
|
|
78
|
+
def emit_telemetry(self, args: EmitTelemetryArgs) -> None: ...
|
|
79
|
+
def emit_governance(self, args: EmitGovernanceArgs) -> None: ...
|
|
80
|
+
def record_llm_call(self, args: EmitLlmCallArgs) -> None: ...
|
|
81
|
+
def record_connector_call(self, args: EmitConnectorCallArgs) -> None: ...
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Every method returns `None` on success — including the **silent replay**
|
|
85
|
+
path (200 OK: same key + same content). A logical-key collision
|
|
86
|
+
(same identity, different content) raises
|
|
87
|
+
`plm_shared.sqlalchemy_kernel_emitter.IdempotencyConflict`.
|
|
88
|
+
|
|
89
|
+
### Args
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
EmitTelemetryArgs(
|
|
93
|
+
event_type: str, # e.g. "knowledge_call"
|
|
94
|
+
span_id: str,
|
|
95
|
+
capability: str, # capability tag for the event
|
|
96
|
+
payload: Dict[str, Any] = {}, # event-specific shape
|
|
97
|
+
idempotency_key: IdempotencyKey,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
EmitGovernanceArgs(
|
|
101
|
+
governance_event_type: str, # e.g. "hitl_resolved"
|
|
102
|
+
span_id: str,
|
|
103
|
+
payload: Dict[str, Any] = {},
|
|
104
|
+
idempotency_key: IdempotencyKey,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
EmitLlmCallArgs(
|
|
108
|
+
span_id: str,
|
|
109
|
+
model: str,
|
|
110
|
+
prompt_tokens: int,
|
|
111
|
+
completion_tokens: int,
|
|
112
|
+
pricing_version: str, # "litellm-live" | "overrides-static" | "unavailable"
|
|
113
|
+
cost_eur: Optional[float] = None,
|
|
114
|
+
cost_status: str, # "pending" | "calculated" | "unavailable"
|
|
115
|
+
idempotency_key: IdempotencyKey,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
EmitConnectorCallArgs(
|
|
119
|
+
span_id: str,
|
|
120
|
+
connector_id: str, # e.g. "3dx-rest"
|
|
121
|
+
capability: str, # e.g. "read.parts"
|
|
122
|
+
envelope: Dict[str, Any] = {},
|
|
123
|
+
pricing_version: str,
|
|
124
|
+
cost_eur: Optional[float] = None,
|
|
125
|
+
cost_status: str,
|
|
126
|
+
idempotency_key: IdempotencyKey,
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Concrete emitter — `SqlAlchemyKernelEmitter`
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from plm_shared.sqlalchemy_kernel_emitter import (
|
|
134
|
+
IdempotencyConflict,
|
|
135
|
+
SqlAlchemyKernelEmitter,
|
|
136
|
+
)
|
|
137
|
+
from plm_shared.idempotency import IdempotencyKey, compute_content_hash
|
|
138
|
+
from plm_shared.kernel_api import EmitLlmCallArgs
|
|
139
|
+
|
|
140
|
+
tenant_id = "00000000-0000-0000-0000-000000000000"
|
|
141
|
+
emitter = SqlAlchemyKernelEmitter(tenant_id=tenant_id)
|
|
142
|
+
|
|
143
|
+
payload = {"model": "gpt-4o", "prompt_tokens": 100}
|
|
144
|
+
key = IdempotencyKey(
|
|
145
|
+
tenant_id=tenant_id,
|
|
146
|
+
trace_id="trace-X",
|
|
147
|
+
span_id="span-1",
|
|
148
|
+
event_type="llm_call",
|
|
149
|
+
content_hash=compute_content_hash(payload),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
emitter.record_llm_call(EmitLlmCallArgs(
|
|
154
|
+
span_id="span-1",
|
|
155
|
+
model="gpt-4o",
|
|
156
|
+
prompt_tokens=100,
|
|
157
|
+
completion_tokens=50,
|
|
158
|
+
pricing_version="litellm-live",
|
|
159
|
+
cost_eur=0.0042,
|
|
160
|
+
cost_status="calculated",
|
|
161
|
+
idempotency_key=key,
|
|
162
|
+
))
|
|
163
|
+
# 200 — inserted, OR 200 — silent replay (same key + same content)
|
|
164
|
+
except IdempotencyConflict:
|
|
165
|
+
# 409 — same logical identity, different content
|
|
166
|
+
raise
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The emitter:
|
|
170
|
+
|
|
171
|
+
- Opens a `tenant_scoped_session(tenant_id)` so RLS engages on INSERT
|
|
172
|
+
(migration 0009 — fail-closed for an unset GUC).
|
|
173
|
+
- Stamps the `idempotency_key` UNIQUE; on collision, dispatches by
|
|
174
|
+
constraint name (`<table>_idempotency_key_key` → 200 silent replay,
|
|
175
|
+
`uq_<table>_logical` → 409 raise).
|
|
176
|
+
- For LLM and connector calls, upserts a `pricing_snapshots` row keyed
|
|
177
|
+
on `(tenant_id, model, snapshot_date)` (migration 0011) and stores
|
|
178
|
+
the FK on the call row. `pricing_version="unavailable"` skips the
|
|
179
|
+
snapshot — the `cost_status` column carries the authoritative signal.
|
|
180
|
+
|
|
181
|
+
### Backend wiring (deferred to US-CR.0)
|
|
182
|
+
|
|
183
|
+
`PlmTenantMiddleware` is published in `plm_shared.middleware` but is
|
|
184
|
+
NOT yet installed in `02_App/backend/main.py`. US-CR.0 owns the
|
|
185
|
+
ordering — it adds `IdentityMiddleware` first, then plumbs
|
|
186
|
+
`PlmTenantMiddleware` behind it, then wires CORS so that
|
|
187
|
+
`CORSMiddleware` wraps both (failure responses still carry CORS
|
|
188
|
+
headers). Conv D leaves the middleware available-but-unwired so
|
|
189
|
+
US-CR.0 lands the full ordering in one place.
|
|
190
|
+
|
|
191
|
+
## Migration chain
|
|
192
|
+
|
|
193
|
+
| Rev | Subject |
|
|
194
|
+
|-----|---------------------------------------------------------------|
|
|
195
|
+
| 0001 | foundational tables `tenants`, `runs` + `quality_level` ENUM + `actor_kind` ENUM |
|
|
196
|
+
| 0002 | `telemetry_events` + composite index + UNIQUE `idempotency_key` |
|
|
197
|
+
| 0003 | `llm_calls` + `cost_*` columns + UNIQUE `idempotency_key` |
|
|
198
|
+
| 0004 | `connector_calls` + connector-specific fields |
|
|
199
|
+
| 0005 | `artifacts` + storage_uri + retention_until |
|
|
200
|
+
| 0006 | quality_level + cost_status transition triggers |
|
|
201
|
+
| 0007 | `mcrc_v1_view` (§1-§8 projection) |
|
|
202
|
+
| 0008 | Postgres roles `plm_kernel_writer` + `plm_migrator` |
|
|
203
|
+
| 0009 | Row-Level Security on the 5 tenant-scoped tables + `plm_migrator BYPASSRLS` |
|
|
204
|
+
| 0010 | logical UNIQUE constraints (backs the 200/409 IdempotencyKey contract) |
|
|
205
|
+
| 0011 | `pricing_snapshots` dedup table + nullable FKs |
|
|
206
|
+
| 0012 | `plm_migrator` table privileges (SELECT/INSERT/UPDATE/DELETE on the 7 tenant-scoped tables — closes the privilege gap surfaced by the live-Postgres validation event; BYPASSRLS alone does not grant table access) |
|
|
207
|
+
|
|
208
|
+
Run the chain with `alembic upgrade head` from `02_App/plm-shared/`
|
|
209
|
+
(needs `PG_DSN` exported).
|
|
210
|
+
|
|
211
|
+
## Install (developer)
|
|
212
|
+
|
|
213
|
+
From the `02_App/backend/` virtualenv:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
cd 02_App/backend
|
|
217
|
+
pip install -e ../plm-shared # base install
|
|
218
|
+
pip install -e "../plm-shared[db]" # + SQLAlchemy / psycopg / alembic
|
|
219
|
+
pip install -e "../plm-shared[s3]" # + boto3 (ObjectStoreBackend)
|
|
220
|
+
pip install -e "../plm-shared[db,s3]" # combined
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The `02_App/backend/requirements.txt` already lists `-e ../plm-shared`
|
|
224
|
+
so a clean `pip install -r requirements.txt` from `02_App/backend/`
|
|
225
|
+
picks it up. **`pip install` MUST run from `02_App/backend/`** because
|
|
226
|
+
pip resolves the editable path relative to the invocation CWD.
|
|
227
|
+
|
|
228
|
+
## Tests
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
cd 02_App/plm-shared
|
|
232
|
+
python -m pytest tests/ -v
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Audit + migration suites under `tests/audit/` and `tests/migrations/`
|
|
236
|
+
are gated on `PG_DSN`. Bring up the dev compose first:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
docker compose -f dev/postgres.yml up -d
|
|
240
|
+
export PG_DSN=postgresql+psycopg://tracepulse:tracepulse@localhost:5432/tracepulse_dev
|
|
241
|
+
cd 02_App/plm-shared
|
|
242
|
+
python -m pytest tests/audit/ tests/migrations/ -v
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
S3-protocol live tests under `tests/test_object_store_backend.py` gate
|
|
246
|
+
on `S3_ENDPOINT_URL` (skip-when-unset). Bring up minio (or any
|
|
247
|
+
S3-protocol endpoint) and export `S3_ENDPOINT_URL`, `S3_ACCESS_KEY`,
|
|
248
|
+
`S3_SECRET_KEY`, `S3_BUCKET`.
|
|
249
|
+
|
|
250
|
+
## Architecture conformance gate (US-W1.0 / AC-4)
|
|
251
|
+
|
|
252
|
+
`pyproject.toml` declares one `[tool.importlinter]` Forbidden contract
|
|
253
|
+
banning `sqlalchemy` from every plm-shared module except `db` and
|
|
254
|
+
`sqlalchemy_kernel_emitter`. Run it from the package root:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
cd 02_App/plm-shared
|
|
258
|
+
lint-imports --config pyproject.toml
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
The companion **BL5** rule in
|
|
262
|
+
`02_App/backend/scripts/architecture_guardrails.py` (warn-only at
|
|
263
|
+
launch) regex-scans for `INSERT INTO {telemetry_events|llm_calls|connector_calls|pricing_snapshots}`
|
|
264
|
+
outside `kernel_api` / `sqlalchemy_kernel_emitter` / migrations.
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# plm-shared
|
|
2
|
+
|
|
3
|
+
Frozen contracts shared across TracePulse PLM product lines (Core,
|
|
4
|
+
Skill Kernel, Skill Packages, Knowledge, Workbench, Studio,
|
|
5
|
+
Connectors). Owned by US-W1.0; consumed by US-W1.1 → US-W1.13 and
|
|
6
|
+
US-CR.0 → US-CR.13 without modification.
|
|
7
|
+
|
|
8
|
+
US-W1.0 ships in 7 PRs:
|
|
9
|
+
|
|
10
|
+
| PR | Scope |
|
|
11
|
+
|-----|-------------------------------------------------------------------|
|
|
12
|
+
| PR-1 | package skeleton + Pydantic models + IdempotencyKey + Capability |
|
|
13
|
+
| PR-2 | Alembic migrations 0001-0007 + SQLAlchemy ORM + `db.py` |
|
|
14
|
+
| PR-3 | migration 0008 (Postgres roles) + append-only audit suite |
|
|
15
|
+
| PR-4 | migration 0009 (RLS) + `middleware.py` + tenant-isolation audit |
|
|
16
|
+
| PR-5 | `ObjectStoreBackend` (S3 wire protocol) + `[s3]` extra + tests |
|
|
17
|
+
| PR-6 | `SqlAlchemyKernelEmitter` + migration 0011 (pricing dedup) + integ |
|
|
18
|
+
| PR-7 | `import-linter` contracts + BL5 + CI workflow + AC + QA + close |
|
|
19
|
+
|
|
20
|
+
## Surface
|
|
21
|
+
|
|
22
|
+
| Module | Purpose |
|
|
23
|
+
|-----------------------------|-------------------------------------------------------------|
|
|
24
|
+
| `kernel_api` | Pydantic args + `KernelEmitter` Protocol (frozen) |
|
|
25
|
+
| `sqlalchemy_kernel_emitter` | Concrete `SqlAlchemyKernelEmitter` impl (PR-6) |
|
|
26
|
+
| `idempotency` | `IdempotencyKey` + `compute_content_hash` |
|
|
27
|
+
| `capabilities` | `Capability` enum + YAML loader |
|
|
28
|
+
| `knowledge_envelope` | `KnowledgeEnvelopeV1` frozen schema |
|
|
29
|
+
| `trace_context` | `TraceContext` ContextVar + W0.6 bridge |
|
|
30
|
+
| `pricing` | `compute_cost` + `CostStatus` + `PricingSnapshot` dataclass |
|
|
31
|
+
| `artifact_store` | `ArtifactStorageBackend` Protocol + `LocalFilesystemBackend` + `ObjectStoreBackend` |
|
|
32
|
+
| `middleware` | `PlmTenantMiddleware` (ASGI) + `parse_core_caller` + `cors_allowed_headers` |
|
|
33
|
+
| `db` | SQLAlchemy 2.0 ORM models + `tenant_scoped_session` |
|
|
34
|
+
|
|
35
|
+
## Kernel API (US-W1.0 / AC-2)
|
|
36
|
+
|
|
37
|
+
`plm_shared.kernel_api.KernelEmitter` is the **frozen** 4-method
|
|
38
|
+
Protocol that every product line uses to record events. The
|
|
39
|
+
authorised emitter boundary is Core: product lines submit decisions
|
|
40
|
+
to Core (e.g. Workbench routes HITL approvals), and Core is the
|
|
41
|
+
authorised emitter through `plm-shared`.
|
|
42
|
+
|
|
43
|
+
### Protocol
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from plm_shared.kernel_api import KernelEmitter
|
|
47
|
+
from plm_shared.kernel_api import (
|
|
48
|
+
EmitTelemetryArgs,
|
|
49
|
+
EmitGovernanceArgs,
|
|
50
|
+
EmitLlmCallArgs,
|
|
51
|
+
EmitConnectorCallArgs,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
class KernelEmitter(Protocol):
|
|
55
|
+
def emit_telemetry(self, args: EmitTelemetryArgs) -> None: ...
|
|
56
|
+
def emit_governance(self, args: EmitGovernanceArgs) -> None: ...
|
|
57
|
+
def record_llm_call(self, args: EmitLlmCallArgs) -> None: ...
|
|
58
|
+
def record_connector_call(self, args: EmitConnectorCallArgs) -> None: ...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Every method returns `None` on success — including the **silent replay**
|
|
62
|
+
path (200 OK: same key + same content). A logical-key collision
|
|
63
|
+
(same identity, different content) raises
|
|
64
|
+
`plm_shared.sqlalchemy_kernel_emitter.IdempotencyConflict`.
|
|
65
|
+
|
|
66
|
+
### Args
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
EmitTelemetryArgs(
|
|
70
|
+
event_type: str, # e.g. "knowledge_call"
|
|
71
|
+
span_id: str,
|
|
72
|
+
capability: str, # capability tag for the event
|
|
73
|
+
payload: Dict[str, Any] = {}, # event-specific shape
|
|
74
|
+
idempotency_key: IdempotencyKey,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
EmitGovernanceArgs(
|
|
78
|
+
governance_event_type: str, # e.g. "hitl_resolved"
|
|
79
|
+
span_id: str,
|
|
80
|
+
payload: Dict[str, Any] = {},
|
|
81
|
+
idempotency_key: IdempotencyKey,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
EmitLlmCallArgs(
|
|
85
|
+
span_id: str,
|
|
86
|
+
model: str,
|
|
87
|
+
prompt_tokens: int,
|
|
88
|
+
completion_tokens: int,
|
|
89
|
+
pricing_version: str, # "litellm-live" | "overrides-static" | "unavailable"
|
|
90
|
+
cost_eur: Optional[float] = None,
|
|
91
|
+
cost_status: str, # "pending" | "calculated" | "unavailable"
|
|
92
|
+
idempotency_key: IdempotencyKey,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
EmitConnectorCallArgs(
|
|
96
|
+
span_id: str,
|
|
97
|
+
connector_id: str, # e.g. "3dx-rest"
|
|
98
|
+
capability: str, # e.g. "read.parts"
|
|
99
|
+
envelope: Dict[str, Any] = {},
|
|
100
|
+
pricing_version: str,
|
|
101
|
+
cost_eur: Optional[float] = None,
|
|
102
|
+
cost_status: str,
|
|
103
|
+
idempotency_key: IdempotencyKey,
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Concrete emitter — `SqlAlchemyKernelEmitter`
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from plm_shared.sqlalchemy_kernel_emitter import (
|
|
111
|
+
IdempotencyConflict,
|
|
112
|
+
SqlAlchemyKernelEmitter,
|
|
113
|
+
)
|
|
114
|
+
from plm_shared.idempotency import IdempotencyKey, compute_content_hash
|
|
115
|
+
from plm_shared.kernel_api import EmitLlmCallArgs
|
|
116
|
+
|
|
117
|
+
tenant_id = "00000000-0000-0000-0000-000000000000"
|
|
118
|
+
emitter = SqlAlchemyKernelEmitter(tenant_id=tenant_id)
|
|
119
|
+
|
|
120
|
+
payload = {"model": "gpt-4o", "prompt_tokens": 100}
|
|
121
|
+
key = IdempotencyKey(
|
|
122
|
+
tenant_id=tenant_id,
|
|
123
|
+
trace_id="trace-X",
|
|
124
|
+
span_id="span-1",
|
|
125
|
+
event_type="llm_call",
|
|
126
|
+
content_hash=compute_content_hash(payload),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
emitter.record_llm_call(EmitLlmCallArgs(
|
|
131
|
+
span_id="span-1",
|
|
132
|
+
model="gpt-4o",
|
|
133
|
+
prompt_tokens=100,
|
|
134
|
+
completion_tokens=50,
|
|
135
|
+
pricing_version="litellm-live",
|
|
136
|
+
cost_eur=0.0042,
|
|
137
|
+
cost_status="calculated",
|
|
138
|
+
idempotency_key=key,
|
|
139
|
+
))
|
|
140
|
+
# 200 — inserted, OR 200 — silent replay (same key + same content)
|
|
141
|
+
except IdempotencyConflict:
|
|
142
|
+
# 409 — same logical identity, different content
|
|
143
|
+
raise
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The emitter:
|
|
147
|
+
|
|
148
|
+
- Opens a `tenant_scoped_session(tenant_id)` so RLS engages on INSERT
|
|
149
|
+
(migration 0009 — fail-closed for an unset GUC).
|
|
150
|
+
- Stamps the `idempotency_key` UNIQUE; on collision, dispatches by
|
|
151
|
+
constraint name (`<table>_idempotency_key_key` → 200 silent replay,
|
|
152
|
+
`uq_<table>_logical` → 409 raise).
|
|
153
|
+
- For LLM and connector calls, upserts a `pricing_snapshots` row keyed
|
|
154
|
+
on `(tenant_id, model, snapshot_date)` (migration 0011) and stores
|
|
155
|
+
the FK on the call row. `pricing_version="unavailable"` skips the
|
|
156
|
+
snapshot — the `cost_status` column carries the authoritative signal.
|
|
157
|
+
|
|
158
|
+
### Backend wiring (deferred to US-CR.0)
|
|
159
|
+
|
|
160
|
+
`PlmTenantMiddleware` is published in `plm_shared.middleware` but is
|
|
161
|
+
NOT yet installed in `02_App/backend/main.py`. US-CR.0 owns the
|
|
162
|
+
ordering — it adds `IdentityMiddleware` first, then plumbs
|
|
163
|
+
`PlmTenantMiddleware` behind it, then wires CORS so that
|
|
164
|
+
`CORSMiddleware` wraps both (failure responses still carry CORS
|
|
165
|
+
headers). Conv D leaves the middleware available-but-unwired so
|
|
166
|
+
US-CR.0 lands the full ordering in one place.
|
|
167
|
+
|
|
168
|
+
## Migration chain
|
|
169
|
+
|
|
170
|
+
| Rev | Subject |
|
|
171
|
+
|-----|---------------------------------------------------------------|
|
|
172
|
+
| 0001 | foundational tables `tenants`, `runs` + `quality_level` ENUM + `actor_kind` ENUM |
|
|
173
|
+
| 0002 | `telemetry_events` + composite index + UNIQUE `idempotency_key` |
|
|
174
|
+
| 0003 | `llm_calls` + `cost_*` columns + UNIQUE `idempotency_key` |
|
|
175
|
+
| 0004 | `connector_calls` + connector-specific fields |
|
|
176
|
+
| 0005 | `artifacts` + storage_uri + retention_until |
|
|
177
|
+
| 0006 | quality_level + cost_status transition triggers |
|
|
178
|
+
| 0007 | `mcrc_v1_view` (§1-§8 projection) |
|
|
179
|
+
| 0008 | Postgres roles `plm_kernel_writer` + `plm_migrator` |
|
|
180
|
+
| 0009 | Row-Level Security on the 5 tenant-scoped tables + `plm_migrator BYPASSRLS` |
|
|
181
|
+
| 0010 | logical UNIQUE constraints (backs the 200/409 IdempotencyKey contract) |
|
|
182
|
+
| 0011 | `pricing_snapshots` dedup table + nullable FKs |
|
|
183
|
+
| 0012 | `plm_migrator` table privileges (SELECT/INSERT/UPDATE/DELETE on the 7 tenant-scoped tables — closes the privilege gap surfaced by the live-Postgres validation event; BYPASSRLS alone does not grant table access) |
|
|
184
|
+
|
|
185
|
+
Run the chain with `alembic upgrade head` from `02_App/plm-shared/`
|
|
186
|
+
(needs `PG_DSN` exported).
|
|
187
|
+
|
|
188
|
+
## Install (developer)
|
|
189
|
+
|
|
190
|
+
From the `02_App/backend/` virtualenv:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
cd 02_App/backend
|
|
194
|
+
pip install -e ../plm-shared # base install
|
|
195
|
+
pip install -e "../plm-shared[db]" # + SQLAlchemy / psycopg / alembic
|
|
196
|
+
pip install -e "../plm-shared[s3]" # + boto3 (ObjectStoreBackend)
|
|
197
|
+
pip install -e "../plm-shared[db,s3]" # combined
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The `02_App/backend/requirements.txt` already lists `-e ../plm-shared`
|
|
201
|
+
so a clean `pip install -r requirements.txt` from `02_App/backend/`
|
|
202
|
+
picks it up. **`pip install` MUST run from `02_App/backend/`** because
|
|
203
|
+
pip resolves the editable path relative to the invocation CWD.
|
|
204
|
+
|
|
205
|
+
## Tests
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
cd 02_App/plm-shared
|
|
209
|
+
python -m pytest tests/ -v
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Audit + migration suites under `tests/audit/` and `tests/migrations/`
|
|
213
|
+
are gated on `PG_DSN`. Bring up the dev compose first:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
docker compose -f dev/postgres.yml up -d
|
|
217
|
+
export PG_DSN=postgresql+psycopg://tracepulse:tracepulse@localhost:5432/tracepulse_dev
|
|
218
|
+
cd 02_App/plm-shared
|
|
219
|
+
python -m pytest tests/audit/ tests/migrations/ -v
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
S3-protocol live tests under `tests/test_object_store_backend.py` gate
|
|
223
|
+
on `S3_ENDPOINT_URL` (skip-when-unset). Bring up minio (or any
|
|
224
|
+
S3-protocol endpoint) and export `S3_ENDPOINT_URL`, `S3_ACCESS_KEY`,
|
|
225
|
+
`S3_SECRET_KEY`, `S3_BUCKET`.
|
|
226
|
+
|
|
227
|
+
## Architecture conformance gate (US-W1.0 / AC-4)
|
|
228
|
+
|
|
229
|
+
`pyproject.toml` declares one `[tool.importlinter]` Forbidden contract
|
|
230
|
+
banning `sqlalchemy` from every plm-shared module except `db` and
|
|
231
|
+
`sqlalchemy_kernel_emitter`. Run it from the package root:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
cd 02_App/plm-shared
|
|
235
|
+
lint-imports --config pyproject.toml
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The companion **BL5** rule in
|
|
239
|
+
`02_App/backend/scripts/architecture_guardrails.py` (warn-only at
|
|
240
|
+
launch) regex-scans for `INSERT INTO {telemetry_events|llm_calls|connector_calls|pricing_snapshots}`
|
|
241
|
+
outside `kernel_api` / `sqlalchemy_kernel_emitter` / migrations.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""plm-shared — frozen contracts shared across TracePulse PLM product lines.
|
|
2
|
+
|
|
3
|
+
US-W1.0 (PR-1): the modules in this package publish the Kernel emission
|
|
4
|
+
API surface, idempotency key, capability registry, knowledge envelope,
|
|
5
|
+
trace context, pricing helpers, and artifact-store protocol consumed by
|
|
6
|
+
the rest of Wave 1. Models in `kernel_api`, `idempotency`,
|
|
7
|
+
`knowledge_envelope`, and `capabilities` are frozen — never modified by
|
|
8
|
+
downstream stories — so the rest of the wave can be implemented in
|
|
9
|
+
parallel without contract drift.
|
|
10
|
+
"""
|
|
11
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""US-W1.13 / Conv I — idempotent backfill of Wave 0 metadata to typed columns.
|
|
2
|
+
|
|
3
|
+
Private module implementing the actual UPDATE statements + summary
|
|
4
|
+
shape used by the W1.13 backfill. Surfaced at the CLI level by
|
|
5
|
+
``02_App/plm-shared/scripts/backfill_w1_13_typed_columns.py``; the
|
|
6
|
+
audit test (`tests/audit/test_w1_13_backfill.py`) imports
|
|
7
|
+
``run_backfill`` from this module directly.
|
|
8
|
+
|
|
9
|
+
Walks every `telemetry_events` row whose typed column is NULL,
|
|
10
|
+
reads the equivalent value from `payload` JSONB, writes it into
|
|
11
|
+
the new column. Re-running on a fully-backfilled DB yields zero
|
|
12
|
+
further updates (W1.13 §AC-7 — idempotency).
|
|
13
|
+
|
|
14
|
+
Runs as `plm_migrator` per migration 0008 + Decision #16:
|
|
15
|
+
`plm_kernel_writer` is REVOKED UPDATE on `telemetry_events` per
|
|
16
|
+
the append-only rule, and a backfill IS an UPDATE. The script
|
|
17
|
+
issues `SET LOCAL ROLE plm_migrator` inside each transaction so
|
|
18
|
+
even a superuser-mode invocation stays faithful to the production
|
|
19
|
+
privilege contract.
|
|
20
|
+
|
|
21
|
+
llm_calls.pricing_source backfill is NOT covered here — the column
|
|
22
|
+
lives in PG, but pre-0016 telemetry-side rows that carried
|
|
23
|
+
`pricing_source` rode in `telemetry_events.payload`, not
|
|
24
|
+
`llm_calls.payload` (`llm_calls` has no payload JSONB column).
|
|
25
|
+
The legacy backend's SQLite `agent_invocations.metadata.pricing_source`
|
|
26
|
+
column is handled by `02_App/backend/database.py`'s startup
|
|
27
|
+
ALTER-TABLE pattern, not by this PG backfill.
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import os
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from typing import Optional
|
|
34
|
+
|
|
35
|
+
from sqlalchemy import create_engine, text
|
|
36
|
+
from sqlalchemy.engine import Engine
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class BackfillSummary:
|
|
41
|
+
"""Per-column rowcounts from a single backfill invocation.
|
|
42
|
+
|
|
43
|
+
A second invocation against a fully-backfilled DB returns
|
|
44
|
+
all-zeros — that's the AC-7 idempotency contract.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
updated_invocation_kind: int
|
|
48
|
+
updated_is_eval: int
|
|
49
|
+
updated_parent_invocation_id: int
|
|
50
|
+
updated_root_invocation_id: int
|
|
51
|
+
updated_system_context: int
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def total(self) -> int:
|
|
55
|
+
return (
|
|
56
|
+
self.updated_invocation_kind
|
|
57
|
+
+ self.updated_is_eval
|
|
58
|
+
+ self.updated_parent_invocation_id
|
|
59
|
+
+ self.updated_root_invocation_id
|
|
60
|
+
+ self.updated_system_context
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_INVOCATION_KIND_VALUES = ("user", "system", "hidden_retry", "background")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_BACKFILL_INVOCATION_KIND = text("""
|
|
68
|
+
UPDATE telemetry_events
|
|
69
|
+
SET invocation_kind = (payload->>'invocation_kind')::invocation_kind_v1
|
|
70
|
+
WHERE invocation_kind IS NULL
|
|
71
|
+
AND payload ? 'invocation_kind'
|
|
72
|
+
AND payload->>'invocation_kind' = ANY(:valid_kinds)
|
|
73
|
+
""")
|
|
74
|
+
|
|
75
|
+
# `is_eval` is non-nullable with default `false`. The migration
|
|
76
|
+
# already populated every legacy row to `false` via the server
|
|
77
|
+
# default; the only useful UPDATE is for rows whose payload
|
|
78
|
+
# explicitly carries `is_eval=true`. Idempotent: re-run on a
|
|
79
|
+
# flipped row no-ops because the WHERE filters it out.
|
|
80
|
+
_BACKFILL_IS_EVAL = text("""
|
|
81
|
+
UPDATE telemetry_events
|
|
82
|
+
SET is_eval = true
|
|
83
|
+
WHERE is_eval = false
|
|
84
|
+
AND payload ? 'is_eval'
|
|
85
|
+
AND lower(payload->>'is_eval') IN ('true', '1')
|
|
86
|
+
""")
|
|
87
|
+
|
|
88
|
+
_BACKFILL_PARENT_INVOCATION_ID = text("""
|
|
89
|
+
UPDATE telemetry_events
|
|
90
|
+
SET parent_invocation_id = (payload->>'parent_invocation_id')::uuid
|
|
91
|
+
WHERE parent_invocation_id IS NULL
|
|
92
|
+
AND payload ? 'parent_invocation_id'
|
|
93
|
+
AND payload->>'parent_invocation_id' ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
94
|
+
""")
|
|
95
|
+
|
|
96
|
+
_BACKFILL_ROOT_INVOCATION_ID = text("""
|
|
97
|
+
UPDATE telemetry_events
|
|
98
|
+
SET root_invocation_id = (payload->>'root_invocation_id')::uuid
|
|
99
|
+
WHERE root_invocation_id IS NULL
|
|
100
|
+
AND payload ? 'root_invocation_id'
|
|
101
|
+
AND payload->>'root_invocation_id' ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
102
|
+
""")
|
|
103
|
+
|
|
104
|
+
_BACKFILL_SYSTEM_CONTEXT = text("""
|
|
105
|
+
UPDATE telemetry_events
|
|
106
|
+
SET system_context = payload->>'system_context'
|
|
107
|
+
WHERE system_context IS NULL
|
|
108
|
+
AND payload ? 'system_context'
|
|
109
|
+
""")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def run_backfill(engine: Engine) -> BackfillSummary:
|
|
113
|
+
"""Apply every per-column backfill UPDATE; return a counts summary.
|
|
114
|
+
|
|
115
|
+
Each UPDATE runs in its own transaction so a partial failure
|
|
116
|
+
on one column doesn't roll back successful work on previous
|
|
117
|
+
columns. `SET LOCAL ROLE plm_migrator` is issued inside each
|
|
118
|
+
transaction so the privilege check happens even when the
|
|
119
|
+
connecting user is the dev superuser. Idempotent — every
|
|
120
|
+
WHERE clause filters rows that already have the typed column
|
|
121
|
+
populated.
|
|
122
|
+
"""
|
|
123
|
+
counts = {}
|
|
124
|
+
for label, statement in (
|
|
125
|
+
("invocation_kind", _BACKFILL_INVOCATION_KIND),
|
|
126
|
+
("is_eval", _BACKFILL_IS_EVAL),
|
|
127
|
+
("parent_invocation_id", _BACKFILL_PARENT_INVOCATION_ID),
|
|
128
|
+
("root_invocation_id", _BACKFILL_ROOT_INVOCATION_ID),
|
|
129
|
+
("system_context", _BACKFILL_SYSTEM_CONTEXT),
|
|
130
|
+
):
|
|
131
|
+
with engine.begin() as conn:
|
|
132
|
+
conn.execute(text("SET LOCAL ROLE plm_migrator"))
|
|
133
|
+
params = (
|
|
134
|
+
{"valid_kinds": list(_INVOCATION_KIND_VALUES)}
|
|
135
|
+
if label == "invocation_kind"
|
|
136
|
+
else {}
|
|
137
|
+
)
|
|
138
|
+
result = conn.execute(statement, params)
|
|
139
|
+
counts[label] = result.rowcount or 0
|
|
140
|
+
|
|
141
|
+
return BackfillSummary(
|
|
142
|
+
updated_invocation_kind=counts["invocation_kind"],
|
|
143
|
+
updated_is_eval=counts["is_eval"],
|
|
144
|
+
updated_parent_invocation_id=counts["parent_invocation_id"],
|
|
145
|
+
updated_root_invocation_id=counts["root_invocation_id"],
|
|
146
|
+
updated_system_context=counts["system_context"],
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def build_engine_from_env(dsn: Optional[str] = None) -> Engine:
|
|
151
|
+
"""Resolve `PG_DSN` from env (or the explicit override) and bind."""
|
|
152
|
+
url = (dsn or os.environ.get("PG_DSN", "")).strip()
|
|
153
|
+
if not url:
|
|
154
|
+
raise RuntimeError(
|
|
155
|
+
"PG_DSN is not set. Export it before running the W1.13 "
|
|
156
|
+
"backfill (e.g. "
|
|
157
|
+
"`postgresql+psycopg://tracepulse:tracepulse@localhost:5432/tracepulse_dev`)."
|
|
158
|
+
)
|
|
159
|
+
return create_engine(url, future=True)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Lifted ``agents/`` modules (Wave 6.7 Conv A).
|
|
2
|
+
|
|
3
|
+
Per Decision #177 — these are not Protocol indirections; they are the
|
|
4
|
+
**canonical** homes of ``BaseAgent`` (abstract base class for the multi-
|
|
5
|
+
agent PDF→BPMN chain), the domain classifier / RACI extractor, and the
|
|
6
|
+
domain-specific prompt prefixes. Lifted from
|
|
7
|
+
``02_App/backend/agents/`` via ``git mv`` so sibling packages
|
|
8
|
+
(:mod:`plm_skill_packages`) can import them without depending on
|
|
9
|
+
``02_App/backend/``.
|
|
10
|
+
|
|
11
|
+
The legacy paths under ``02_App/backend/agents/`` remain as thin
|
|
12
|
+
re-export shims for the duration of the DC-1 90-day soak, matching the
|
|
13
|
+
W1.4 RunTaskTracker + Wave 6.5 Conv A/B precedent.
|
|
14
|
+
"""
|