diff --git a/CHANGELOG.md b/CHANGELOG.md index 35613431a..b75e2edb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +- [Docs] πŸ“ **product-technical-gap-baseline.md 톡합**: μƒμš©ν™”κΉŒμ§€μ˜ 격차λ₯Ό 단일 좔적 λ¬Έμ„œλ‘œ μ •λ¦¬ν–ˆμŠ΅λ‹ˆλ‹€. μƒμš©ν™” blocker(org CI μΈμ‹œλ˜νŠΈ `.github#1531`둜 2026-08-20 이후 pg-erd-cloud main 병합 0, merge-ready 증뢄 PR 19건 + λ³Έ λ¬Έμ„œ λŒ€κΈ°)λ₯Ό λͺ…μ‹œν•˜κ³ , 이슈 #946–#953λ³„λ‘œ κΈ°λŠ₯ λͺ…μ„ΈΒ·ν˜„ν–‰Β·Gap·이번 루프 증뢄 PR(#1024/#1025/#1031/#1032/#1033/#1035/#1048/#1060/#1036/#1041/#1045/#1056/#1037/#1051/#1038/#1050/#1039/#1049/#1057/#1063)Β·μž”μ—¬ 증뢄을 μ •λ¦¬ν–ˆμœΌλ©°, cross-repo(keyverse/contextual-orchestrator/wardnet/.github) 연계와 μƒνƒœ λ²”λ‘€, ν‘œμ€€ 인용(APA 7th)을 ν¬ν•¨ν–ˆμŠ΅λ‹ˆλ‹€. #949Β·#952Β·#953 μ ˆμ€ 이슈 본문으둜 채웠고, #952λŠ” κΈ°μ‘΄ configuration-only `/chat/completions` 톡합(`docs/llm-orchestrator-integration.md`)κ³Ό μ •ν•©ν•˜λ„λ‘ 남은 μž‘μ—…(자격 증λͺ… 경계·tenant contextΒ·discovery/routing μœ„μž„)만 κΈ°μˆ ν•©λ‹ˆλ‹€. PR #942의 μ΄ˆμ•ˆ 버전을 λŒ€μ²΄Β·ν™•μž₯ν•˜λ©° 게이트 볡ꡬ μ‹œ μ •ν•©ν™”ν•©λ‹ˆλ‹€. - [BE] πŸ”’ **Cryptography 50+ λ³΄μ•ˆ 경계 κ°±μ‹ **: `pyproject.toml`κ³Ό 두 hash-locked μš”κ΅¬μ‚¬ν•­ νŒŒμΌμ„ λ™μΌν•œ Cryptography 50+ ν•΄μ„μœΌλ‘œ μ •ν•©ν™”ν•˜μ—¬ PKCS#7 였λ₯˜Β·νƒ€μ΄λ° κ΅¬λΆ„μœΌλ‘œ μΈν•œ CVE-2026-69247 μ™„ν™”λ₯Ό μ‹€μ œ μ„€μΉ˜Β·κ²€μ¦ κ²½λ‘œμ— λ°˜μ˜ν–ˆμŠ΅λ‹ˆλ‹€. - [FE] ⚑ **검색 λ…Έλ“œ μ°Έμ‘° μ•ˆμ •ν™” 및 순차 μŠ€λƒ…μƒ· 폴링**: 같은 μ •κ·œν™” 검색어와 원본 ν…Œμ΄λΈ” λ°μ΄ν„°μ—λŠ” μž₯μ‹λœ `node.data` μ°Έμ‘°λ₯Ό μž¬μ‚¬μš©ν•˜μ—¬ λ“œλž˜κ·Έ 쀑 λΆˆν•„μš”ν•œ ν•˜μœ„ λ Œλ”λ§κ³Ό 할당을 μ€„μž…λ‹ˆλ‹€. μŠ€λƒ…μƒ· 폴링은 이전 μš”μ²­μ΄ λλ‚œ λ’€μ—λ§Œ λ‹€μŒ μš”μ²­μ„ μ˜ˆμ•½ν•˜λ©°, 선택 λ³€κ²½Β·μ–Έλ§ˆμš΄νŠΈ ν›„ λ„μ°©ν•œ 였래된 성곡 λ˜λŠ” μ‹€νŒ¨ 응닡을 λ¬΄μ‹œν•©λ‹ˆλ‹€. - [BE] πŸ”’ **곡유 export μ „ 경둜 redaction**: 곡개 share의 SQL / index-design / reversing-spec exportμ—μ„œ μ½”λ©˜νŠΈΒ·`example_value`λ₯Ό μ œκ±°ν•©λ‹ˆλ‹€. λ‹¨μœ„ ν…ŒμŠ€νŠΈλ‘œ λˆ„μΆœμ„ μ°¨λ‹¨ν•©λ‹ˆλ‹€. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md new file mode 100644 index 000000000..b09ad4abd --- /dev/null +++ b/docs/product-technical-gap-baseline.md @@ -0,0 +1,691 @@ +# Product & technical gap baseline + +**Last consolidated:** 2026-09-02 UTC (autonomous review/merge loop, iter28). + +This document is the single tracker for the distance between what +pg-erd-cloud does today and a defensible first commercial release. It is +derived from the open `[Product Gap]` / `[Enterprise Gap]` / `[Performance +Gap]` / `[Security Gap]` / `[Epic]` issues (#946–#953), the code on +`main@8dc74692`, and the in-flight PRs that close pieces of each gap. + +It supersedes the draft in PR #942 (which first introduces this file and is +currently blocked β€” see the commercial-readiness blocker below); the two +should be reconciled when either merges. + +## What pg-erd-cloud is + +A PostgreSQL-focused cloud ERD collaboration service. It reverse-engineers a +target database into immutable JSON schema snapshots, renders them as an +interactive ERD (React Flow), and forward-engineers snapshots into DDL +exports, schema diffs / migration SQL, DBML / Mermaid exports, and reversing +spec documents. Project owners can create read-only share links. + +## Commercial-readiness bar + +A buyer must be able to verify, from product-owned evidence, that: (1) schema +quality claims are backed by catalog or measured evidence, not heuristics; +(2) the deployment's isolation boundary is described accurately and enforced; +(3) large-schema behaviour has a published capacity envelope; (4) credential +lifecycle is auditable; (5) forward-engineering apply is governed with +rollback; (6) every release carries exact-head, migration, operability, and +supply-chain evidence. + +## Commercial-readiness blocker (active incident) + +**`ContextualWisdomLab/pg-erd-cloud` `main` has merged nothing since +2026-08-20.** Root cause: `ContextualWisdomLab/.github` GitHub Actions queue +saturation (`.github#1531`). The central required check `opencode-review` +dispatches a review to `.github` and polls ~90 min for a current-head +verdict; with ~800–900 runs queued org-wide the verdict does not arrive and +the check fails closed on every PR. A parallel remediation effort owns +`.github#1531`; a related `head_sha` TOCTOU in `opencode-review-dispatch.yml` +was found and is being addressed in that repo. + +**Consequence for this baseline:** every gap increment below is shipped as a +small, tested, mypy-clean, 100%-docstring PR and **held merge-ready** until +the gate clears. As of iter40 the loop is holding **19 stacked increment +PRs** plus **this document** (PR #1040). Separately, PR #942 (the original +baseline draft) is green on every required check and waits only on one +non-author approval; it is not one of the 19 increments. + +The 19 increments and the merge-wave order once the gate clears: + +```text +#942 (approval-pending, not an increment) + -> #1024 -> #1025 + -> #1031 -> #1032 -> #1033 -> #1035 -> #1048 -> #1060 (#947 chain) + -> #1036 -> #1041 -> #1045 -> #1056 (#951 chain) + -> #1037 (#946) -> #1051 + -> #1038 (#948) -> #1050 + -> #1039 (#950) -> #1049 + -> #1057 -> #1063 (#953) + -> #1040 (this document) +``` + +## Status legend + +| status | meaning | +| --- | --- | +| `spec'd` | issue defines the contract; no code yet | +| `in-progress` | β‰₯1 increment shipped as a merge-ready PR; more increments remain | +| `merge-ready-blocked` | code complete for this increment; waiting on the gate | +| `not-started` | no work this loop | + +--- + +## #946 β€” Auditable credential-provider contract (`[Security/Product Gap]`) + +**Feature spec (summary).** Replace unaudited runtime env/`.env` secret +transport with a provider-neutral `CredentialProvider` / `SecretReference` +boundary: bootstrap transport only, no plaintext in ORM rows / logs / traces / +metrics / repr; local mounted-file + org-registry + deterministic-test +providers; dual-read / single-write `APP_SECRET` rotation; fail-closed on +missing / revoked / expired / symlinked / oversized / malformed material; +recovery runbook proving key-unavailable recovery cannot expose DSN plaintext. + +**Current state.** `Settings` builds directly from env / `.env`. +`APP_SECRET_FILE` is a fail-closed `/run/secrets` seam but every other +credential (DB, OIDC, LLM, Clearfolio HMAC, metrics, Valkey) is unmanaged +runtime config. + +**Gap.** No single auditable credential lifecycle; no rotation; no access +attribution. + +**This loop's increment PRs.** +- **#1037** β€” `app/secret_provider/`: typed `CredentialProvider` Protocol, + `SecretReference` (no value), `ResolvedSecret` (value only via `reveal()`; + `str`/`repr`/`format`/logs redact), fail-closed `SecretResolutionError`, + `LocalMountedFileProvider` (fail-closed on missing/empty/oversized/non-UTF8/ + symlink/path-escape/non-file), `DeterministicTestProvider`. 14 tests incl. + "value never appears in str/repr/format/logs". Rotation design documented. +- **#1051** β€” `app/secret_provider/rotation.py` (stacked on #1037): the pure + core of the `APP_SECRET` dual-read / single-write rotation. `dual_read_decrypt` + tries each candidate `APP_SECRET` in order (HKDF key, then the legacy + raw-SHA-256 key, matching `app/security`); `plan_key_rotation` re-encrypts + rows that decrypt with a previous key under the active key, leaves + active-key rows alone, and surfaces undecryptable / malformed rows as + `needs_key_recovery` β€” never dropped, never re-encrypted from a guess. No + DB, no `Settings`, no plaintext in the result. A round-trip test through + `app.security.encrypt_text` / `decrypt_text` guards the key derivation. 10 tests. + +**Remaining increments.** `Settings` integration behind a `local_secret_file` +profile; the `SecretReference`-backed key set wired into `app/security`; org +credential-registry provider with cache-TTL + fail-closed +timeout/permission/revoked/stale; the resumable re-encryption *migration job* +over `db_connection` rows (needs a DB + PG fixture); key-recovery runbook; +move LLM credentials to `contextual-orchestrator`; persisted non-secret +credential metadata. + +**Status:** `in-progress`. + +--- + +## #947 β€” Evidence-backed 3NF, FD, and hot-partition assessment (`[Product Gap]`) + +**Feature spec (summary).** A versioned Schema Quality & Operability +Assessment: normalization / functional-dependency findings from catalog + +profiling + declared-rule evidence (never a theorem from column names), with +`observed` / `declared` / `inferred` / `proposed` / `waived` evidence classes, +source refs, caveats, next actions, and signed waivers; hot-partition & growth +findings from workload evidence or an explicit capacity profile, with +`EXPLAIN` pruning fixtures; report as JSON + accessible HTML table + buyer +summary. + +**Current state.** `app.spec` has naming lint, wide-table, constraint, index, +FK-cycle analyzers. No normalization assessment; JSONB payloads are stored but +never described as 3NF proof. + +**Gap.** No defensible normalization / hot-partition answer for a buyer. + +**This loop's increment PRs.** +- **#1031** β€” `app/spec/normalization_assessment.py`: catalog-evidence + analyzer. Findings: `non_atomic_column` (1NF), `missing_candidate_key` + (BCNF β†’ insufficient_evidence), `nullable_unique_determinant` (BCNF), + `partial_dependency_precondition` (2NF). Evidence classes; waivers by scope. + 14 golden fixtures. +- **#1032** β€” `app/spec/normalization_report.py` + `GET + /api/snapshots/{uuid}/normalization-assessment`: versioned report envelope + (stable SHA-256 fingerprint, generated_at, summary headline). IDOR-safe. +- **#1033** β€” `app/spec/hot_partition_assessment.py` + `GET + /api/snapshots/{uuid}/hot-partition-assessment`: catalog + optional explicit + capacity profile. Findings: `append_heavy_table`, `unbounded_retention`, + `monotonic_key_hot_page`, `partition_semantics_review`, `skew_candidate`. + Concrete remediations only when a capacity profile supplies the quantity or + the signal is catalog-declared. 10 fixtures. +- **#1035** β€” `app/spec/assessment_html.py` + `?format=html` on both + endpoints: accessible exact-value HTML (every cell `html.escape`d; + text-label state, not colour; `` per finding kind). Completes the + "JSON + HTML + summary" contract line. +- **#1048** β€” `app/spec/transitive_dependency_assessment.py` + (`assess_transitive_dependencies`): the third-normal-form layer. From the + catalog alone it can only flag `non_key_reference_cluster` β€” more than + one non-candidate-key foreign-key column beside non-prime descriptive + columns β€” as a *structural precondition* (evidence class `inferred`, + with a caveat that profiling or a declared FD is needed). Given a + caller-supplied `declared_functional_dependencies` list it asserts a real + 3NF violation as `transitive_dependency_via_declared_fd` (evidence class + `declared`) and pairs it with a `candidate_3nf_split` proposal (evidence + class `proposed`, never applied). It never infers a dependency from + column names; unresolvable declared FDs are returned, not dropped. + 13 golden fixtures. +- **#1060** β€” `app/spec/waiver_record.py` (`sign_waiver` / + `verify_waiver_signature`): a pure, IO-free HMAC-SHA256 tamper-evidence + pair for assessment waivers. The canonical JSON folds the + signer / signed-at / key-id metadata in, so altering the metadata + invalidates the signature exactly as altering the waiver body does; + verification is constant-time. The caller supplies the secret key and it + is never stored, logged, or echoed into the record. Cites NIST FIPS + 198-1 and RFC 8785. + +**Remaining increments.** Row-level functional-dependency *discovery* from +table data (a profiling service, out of scope for the pure analyzer); +persistence of the signed waiver records (the signing / verification core +landed in #1060); the `EXPLAIN` pruning fixtures against a +real PostgreSQL; the versioned `assessment_run` persistence; a Rust core +once the #951 profile shows a measured hotspot. + +**Status:** `in-progress`. + +--- + +## #948 β€” Snapshot promotion, bitemporal lineage, retention, recovery (`[Product Gap]`) + +**Feature spec (summary).** A first-class immutable lineage & promotion model: +separate `captured_at` / `available_at` / `valid_from` / `valid_to` / +`recorded_at` / `superseded_at` / `knowledge_cutoff`; typed parentβ†’child +derivations (`captured_from` / `imported_from` / `normalized_from` / +`compared_with` / `exported_from` / `planned_from`); optimistic-concurrency +promotion that closes intervals rather than rewriting; `development` / +`staging` / `production` environments; retention & legal hold as policy +records, not background deletes; recovery restores metadata + a diagram state +only (never a live DB). + +**Current state.** Immutable `schema_snapshot` / `schema_snapshot_data` + +`diff_snapshots`. A timestamped list, no lifecycle. + +**Gap.** No approved-baseline record, no derivation typing, no retention +policy, no recovery checkpoint. + +**This loop's increment PRs.** +- **#1038** β€” `app/lineage/`: pure model + algorithms (no DB). `lineage_model` + bitemporal TypedDicts; `build_lineage_graph` (typed-edge DAG, cycle / + self-loop / unknown-kind rejection, topo order, orphans / dangling + reported); `apply_promotion` (append-only optimistic concurrency, closes + prior interval, `PromotionConflictError`); `decide_retention` (disposition + record, never deletes; promoted + legal-hold protected). 9 tests. +- **#1050** β€” `app/lineage/prov_projection.py` (`to_prov_document`): a pure + W3C **PROV-JSON** projection of the `build_lineage_graph` result. One + `prov:Entity` per snapshot id (including ids referenced only as a dangling + parent); one `wasDerivedFrom` per typed edge, carrying `pg:derivationKind` + so the "by kind" information survives. PROV-JSON is plain JSON, so no new + dependency; deterministic and `json.dumps`-serializable. 9 tests. + +**Remaining increments.** Normalized tables + Alembic migration; repositories ++ a `Settings`-gated HTTP surface (history / compare / promote / supersede / +archive / recover); a `wasGeneratedBy` / `activity` layer on the PROV +projection once the persisted model records the tool / commit / policy per +snapshot; embed exact references into every export. + +**Status:** `in-progress`. + +--- + +## #949 β€” Governed forward-engineering apply, rollback, recovery (`[Product Epic]`) + +**Feature spec (summary).** The snapshot β†’ DDL path must become a +protected, versioned vertical workflow: base snapshot β†’ proposed target β†’ +deterministic migration plan β†’ risk/precondition review β†’ isolated dry run +β†’ live read-only preflight β†’ human approval β†’ bounded apply β†’ convergence +capture β†’ success or recovery β†’ immutable evidence bundle. The issue +mandates a **bounded-PR decomposition** rather than one growing branch: + +1. **Plan authority & compiler** β€” immutable source/target snapshot IDs + + hashes; deterministic typed operations with dependency order; a + dialect/version capability matrix; reversible / conditionally reversible + / irreversible classification; fixed resource limits; no free-form SQL + authority. +2. **Sandbox runtime** β€” ephemeral isolated PostgreSQL 14–18 with no + production credentials or customer network; CPU/memory/storage/ + wall-clock/statement/output bounds; cleanup + orphan reaper; a + convergence report. +3. **Stored-target live preflight provider** β€” exact project / connection / + base-snapshot / attempt-lease binding; post-connect revalidation; + read-only catalog capture + precondition checks; DNS/SSRF/TLS + least + privilege; secret-safe errors; cancellation. +4. **Approval & authorization** β€” deployer role + maker-checker for + high-risk plans; exact plan digest / target fingerprint / environment / + expiry / scope; approval invalidated on any plan/target/state change; an + accessible review UI that explains risk and the next action. +5. **Apply worker** β€” production consumer registration; one active attempt + per run with fenced leases + heartbeats; statement-level timeouts + + cancellation checkpoints; a transaction boundary declared per operation + class; retry only where idempotency is proved; no generic replay of + partially committed DDL. +6. **Convergence & recovery** β€” recapture target state through the same + guarded connection; compare actual vs planned; distinguish success / + partial / divergent / unknown; generate recovery guidance from known + committed operations; integrate approved backup/PITR evidence; never + claim automatic rollback for irreversible or non-transactional DDL. +7. **Operations & evidence** β€” durable event/outbox/inbox; OpenTelemetry + traces + metrics with no DSN/schema-value leakage; incident + + cancellation runbooks; downloadable signed execution evidence + + machine-readable provenance; recovery from restart / worker crash / + lease loss / queue duplication / provider timeout. + +**Safety invariants (must hold).** `dry_run=false` stays default-deny +until deployment policy explicitly enables the final apply capability; a +legacy free-form SQL route can never silently become structured apply +authority; the worker never accepts plaintext DSNs, connection overrides, +or plan SQL from queue payloads; every external identifier is re-resolved +and re-authorized at execution time; the target is sticky to the approved +provider/connection lineage; no status is `successful` before post-apply +convergence evidence is committed transactionally with the outbox event. + +**Current state.** `app/ddl/apply_postgres_ddl` runs a validated +`ForwardDdlBatch` inside one transaction, `dry_run` default, SSRF-guarded. +`migration_safety.analyze_migration_safety` classifies risk. PR #834 built +an execution-neutral foundation (structured plans, dry-run attempts, +cancellation, leases, live preflight, audit evidence) but deliberately +registers no production consumer, provisions no sandbox, grants no live +apply authority, and proves no process recovery. + +**Gap.** No production apply consumer; no sandbox runtime; no approval +record bound to an exact plan digest; no convergence/recovery step; no +immutable evidence bundle; #834's useful commits are not yet decomposed +onto protected `main`. + +**This loop's increment PRs.** _none yet_ β€” deferred behind the #948 +lineage model (parts 1 and 6 reuse `planned_from` / `exported_from` edges +and audit records) and the #946 credential boundary (part 3 preflight +provider). Sequencing #948 β†’ #946 integration β†’ #949 part 1 avoids +building the plan model twice. + +**Remaining increments.** All seven parts above, each as a bounded PR from +protected `main` with exact-head evidence; realistic acceptance tests on +PostgreSQL 14–18 (additive column/index/FK, rename, type conversion, +partition op, extension-owned index AM, quoted multilingual identifiers) +and failure injection (lock contention, statement timeout, deadlock, +connection loss, worker `SIGKILL`, lease expiry, duplicate signal, restart; +plan/target changed after approval; partial-commit recovery without +replay). + +**Status:** `spec'd` (foundations forming in #948 / #946; adjacent to #948 +lineage). + +--- + +## #950 β€” GA deployment profiles, tenant isolation, SSO, provisioning (`[Enterprise Gap]`) + +**Feature spec (summary).** Two explicit, published profiles behind the same +contracts: `single_tenant_managed` (one org per deployment/database, external +OIDC / Keyverse with org binding, customer-owned backup/secret/network +policy, no cross-customer claim) and `multi_tenant_saas` (normalized tenant +authority tables; every persisted & cached object carries or derives an +immutable `tenant_account_uuid`; provisioning lifecycle with receipts; +per-tenant data-residency). Never imply multi-tenancy because projects have +members. + +**Current state.** Project membership, OIDC verification, API keys, encrypted +DSNs, share links. No tenant authority; no isolation-mode contract. + +**Gap.** No truthful deployment claim; no enforced tenant ownership. + +**This loop's increment PRs.** +- **#1039** β€” `app/deploy/profile.py`: typed `DeploymentProfile` + + `validate_profile()` honesty validator (rejects a dishonest GA claim for + either profile), `AUTHORITY_BEARING_OBJECTS` enumeration, + `PROFILE_A_TEMPLATE` / `PROFILE_B_TEMPLATE`. 9 tests. +- **#1049** β€” `app/deploy/tenant_authority_check.py` (`check_tenant_authority`): + the concrete check behind the `all_authority_objects_tenant_scoped` bool. + Given `{name, columns, derives_tenant_from}` table descriptions it + partitions every `AUTHORITY_BEARING_OBJECTS` entry into `carrying` (has + `tenant_account_uuid`) / `derived` (scoped through a named parent) / + `missing_scoping` / `missing_definition`, and is `compliant` only when + neither missing-list has an entry. `single_org_per_database` returns + `applicable=False` / `compliant=True` with a reason. Pure; 11 tests. + +**Remaining increments.** `tenant_account` authority tables + migration; a +repository layer deriving `tenant_account_uuid` on every authority-bearing +read/write, feeding the real ORM metadata to `check_tenant_authority`; SSO / +SCIM identity-link + provisioning flows; data-residency enforcement; a +`Settings`-selected active profile with a fail-closed startup self-check +running `validate_profile` and `check_tenant_authority`. + +**Status:** `in-progress`. + +--- + +## #951 β€” Large-schema SLOs, workload benchmarks, measured Rust boundary (`[Performance Gap]`) + +**Feature spec (summary).** A versioned Performance & Capacity Profile: +deterministic anonymized `small` / `medium` / `large` workload generators plus +skew cases; measured paths (capture, hashing, JSON encode/decode + persist, +diff, export, API, queue, browser); p50/p95/p99 + RSS + allocations + query +count + lock wait + queue lag + artifact size + cancellation time; SLOs +separated from benchmark targets, **no SLA until production evidence**; a Rust +decision gate ADR per measured hotspot. + +**Current state.** Focused perf work (background introspection, indexed queue +claims, bounded parsing, memoized search). No published capacity envelope. + +**Gap.** No reproducible workload model, no measured baseline, no Rust +decision evidence. + +**This loop's increment PRs.** +- **#1036** β€” `app/perf/workload_profiles.py`: deterministic anonymized + generators hitting #951's exact counts for `small` / `medium` / `large`; + skew builders (5,000-col relation, dense FK cluster, deep chain, + disconnected components, multilingual/quoted identifiers + large comments, + partition hierarchy). Seeded β†’ byte-identical. **No invented threshold** + (meta-test enforced). 12 tests; `large` in ~0.4s. +- **#1041** β€” `app/perf/baseline.py` (stacked on #1036): + `run_baseline(profile_name, *, seed=None) -> dict` times the pure + side-effect-free paths β€” canonical hash, JSON round-trip, self-diff, + PostgreSQL + Snowflake DDL export, data-dictionary Markdown β€” and records + only `wall_seconds`, `tracemalloc` `peak_bytes`, and `result_size_bytes` + per path. `python -m app.perf.baseline --profile small [--seed N] + [--json]` CLI. `tracemalloc` torn down in `finally`; a cancelled run + returns no partial report. **No threshold or verdict** (meta-test + enforced). 9 tests. +- **#1045** β€” `app/perf/baseline_stats.py` (stacked on #1041): + `aggregate_baseline(profile_name, *, repeat, seed=None) -> dict` runs + `run_baseline` `repeat` times over the *same* seeded workload (snapshot + fixed; only timing varies) and reduces each path's `wall_seconds` and + `peak_bytes` sample lists to `{samples, min, max, mean, p50, p95, p99}` + via `statistics.quantiles` (standard library only). `result_size_bytes` + is deterministic, so it stays a scalar. `repeat < 1` β†’ `ValueError`; + `repeat == 1` β†’ degenerate summary; a cancelled run returns no partial + aggregate. `python -m app.perf.baseline_stats --profile small --repeat 5 + [--seed N] [--json]` CLI. **No threshold or verdict** (meta-test + enforced). 9 tests. +- **#1056** β€” `app/perf/baseline_report.py` (stacked on #1045): + `build_baseline_report(profile_name, *, repeat, seed=None) -> dict` wraps + `aggregate_baseline` in a versioned buyer-facing envelope β€” `report_version`, + `generated_at` (UTC ISO-8601), a `schema_fingerprint` (`"sha256:"` digest + of the exact workload snapshot that was measured), and a `summary` + (`{headline, path_count, slowest_path_by_wall_p95}` β€” names and counts + only, never a duration value). The full statistics block is preserved + verbatim under `statistics`. Mirrors the #1032 normalization-report + envelope pattern. **No threshold or verdict** (meta-test enforced). 8 tests. + +**Remaining increments.** The DB / event-loop measured paths (API +list/detail/pagination/search, queue claim/retry/lease/cleanup/fairness) in +the benchmark workflow; `docs/PERFORMANCE.md`; the release-candidate +benchmark CI workflow with a reproducibility receipt; frontend traces; +per-hotspot Rust decision-gate ADRs. + +**Status:** `in-progress`. + +--- + +## #952 β€” Tenant-scoped document & LLM workflows without weakening standalone (`[Ecosystem Gap]`) + +**Feature spec (summary).** Turn the currently-optional connector calls +into three complete, governed vertical workflows while keeping standalone +operation fully functional: + +1. **Reference-document attachment** β€” project/snapshot/table β†’ authorized + attachment intent β†’ signed tenant/purpose request β†’ Clearfolio + conversion job β†’ durable connector receipt β†’ viewer artifact reference β†’ + project evidence drawer. Needs normalized `connector_account` / + `connector_grant` / `document_reference` / `attachment_binding` / + `connector_job` / `connector_receipt` metadata; opaque external IDs only; + short-lived signed tenant/project/purpose claims; an allowlisted + endpoint with exact host/port/method/MIME/timeout/size/redirect/retry + policy; consent + data-classification review; status/retry/cancel/ + revoke/expiry; immutable source hash; no document contents in any log, + metric, billing record, or LLM trace. +2. **Grounded reversing specification** β€” exact snapshot + authorized + references β†’ evidence bundle β†’ orchestrator operation (e.g. + `draft_database_reversing_spec`) β†’ schema-bound draft β†’ independent + grounding verification β†’ reviewed revision. Must send bounded semantic + evidence units (never whole documents or DSNs); record snapshot hash, + evidence IDs, model/provider IDs, prompt hash, orchestration mode, + reasoning effort, knowledge cutoff, and verification result; distinguish + local deterministic draft / LLM draft / verified draft / human-approved + revision; detect unsupported claims, wrong object names / cardinality, + inverted relationships, fabricated rationale, and prompt injection from + comments or documents; no automatic publication or migration approval. +3. **Naruon / context-fabric projection** β€” a read-only versioned evidence + contract for authorized consumers: canonical references, truth status + (`observed` / `declared` / `inferred` / `proposed`), valid/system time + + knowledge cutoff, provenance + source hashes, policy-filtered metadata + with no DSN/secret, an idempotent event/receipt contract, and **no + requirement that naruon be present** for standalone operation. + +**Product boundary (explicit in the issue).** pg-erd-cloud stays the +authority for ERD projects, connections, snapshots, views, annotations, +sharing, migration plans/runs, and connector *references*. It does not +become a document viewer, object store, PIM/knowledge graph, or LLM +gateway. Clearfolio owns document conversion/viewer jobs; contextual- +orchestrator owns provider/model discovery, routing, fallback, +orchestration, evaluation, and cost/quality telemetry; naruon may consume +pg-erd-cloud evidence through an explicit connector but never owns project +state. Every integration is optional and fails as an *unavailable +capability*, not a broken core product. + +**Current state.** `app/spec/llm.py` already performs a **configuration- +only** OpenAI-compatible integration: it calls a `/chat/completions` +endpoint via `LLM_API_BASE_URL` / `LLM_API_KEY` / `LLM_MODEL` (see +`docs/llm-orchestrator-integration.md`) and the local deterministic +reversing spec / data dictionary work with no LLM configured. The +transport works; what is missing is governance, not a connection. + +**Gap.** The LLM credentials are unmanaged runtime config (ties to #946); +model discovery / capability routing / fallback / evaluation are not +delegated to `contextual-orchestrator` (the issue requires replacing +direct per-provider runtime authority with a versioned orchestrator +operation, and supporting non-chat-completions model classes such as +NVIDIA NIM via discovery + fallback); no tenant/purpose scoping or +evidence lineage on document artifacts (ties to #950); connector +failure / retry / revocation is not one coherent UI + audit contract; no +`standalone`-mode conformance test proving all connectors can be disabled. + +**This loop's increment PRs.** _none yet_. Foundations are in PR #1037 (the +credential-provider boundary the orchestrator client sits behind) and in +PR #1039 (the tenant authority model the artifact scoping needs). + +**Remaining increments.** A versioned `contextual-orchestrator` client +behind the #946 provider boundary that replaces the direct +`LLM_API_*` runtime authority and adds discovery / capability routing / +fallback / grounding verification; the normalized connector-reference +metadata + Clearfolio attachment workflow; tenant-scoping of +reversing-spec / data-dictionary / connector artifacts per #950; the +naruon read-only evidence contract; a `standalone` profile conformance +test (no network, no Keyverse, all connectors disabled) plus adversarial +tests (cross-tenant attachment, credential revocation mid-job, webhook +replay/reorder, DNS-rebind / oversized body / wrong MIME, document + schema +comment prompt injection, fabricated table/column/FK in LLM output, +knowledge-cutoff leakage). + +**Status:** `spec'd` (transport exists; governance blocked on #946 + #950 +foundations). + +--- + +## #953 β€” First commercial release with exact-head, migration, operability, supply-chain evidence (`[Release Epic]`) + +**Feature spec (summary).** Produce the first truthful, installable, +supportable **single-tenant managed / self-hosted GA candidate**. +Multi-tenant SaaS stays non-GA until #950 is complete; the release must +work standalone with optional CWL connectors as capability additions, not +hidden prerequisites. The epic **owns release integration only** and must +not duplicate implementation bodies. Its work: + +- **PR-queue shaping.** Capture the exact protected-`main` SHA, ruleset, + required checks, and every open PR's exact head. Classify each PR: + unique in-scope change / stack dependency / superseded-duplicate / + contaminated aggregate needing reconstruction / experiment-or-post-GA / + blocked by the org control-plane incident. Close duplicates with a link + to the canonical issue; never transfer stale-head review evidence. + Rebase bounded stacks in dependency order without force-pushing over + concurrent agent work. Refresh **this document** after each integration + wave. A release-cut branch/tag comes only from protected `main`. +- **Dependency backlog** (each gets an explicit `release_blocker` / + `post_ga_committed` / `experimental` / `not_planned` decision + rationale + before release): #946, #947, #948, #949, #950, #951, #952, #865 + (orphaned Actions identities), #899 / #928 / PR #944 (design-system / + Storybook contract), PR #936 / #838 (ORM ↔ Alembic exact-head drift). + Core security, data integrity, migration safety, standalone deployment, + backup/restore, operability, licensing, and supported-database + truthfulness cannot be deferred silently. +- **Required release evidence.** A clean-environment browser/API rehearsal + of the full product journey (login β†’ project β†’ encrypted connection β†’ + async snapshot β†’ ERD search/layout/annotation/saved view β†’ diff/exports β†’ + share + revocation β†’ approved snapshot/history β†’ backup and restore); + clean install on PostgreSQL 18 + the compatibility matrix; upgrade from + the oldest supported `0.1.x` through every Alembic revision + + downgrade/rollback policy; ORM↔migration drift producing no unreviewed + DDL; backup + PITR/logical restore + queue/job recovery after restart; + 100% production statement + branch + public-API docstring coverage with + real PostgreSQL fixtures; fuzz/property tests at the DSN / identifier / + snapshot / DBML-DDL / import-export / connector boundaries; threat model + + secure deployment guide + a CSAP / SOC 2 control **crosswalk** (an + engineering evidence map, not a certification claim); reproducible + backend/frontend/container builds; an SPDX or CycloneDX **SBOM** per + artifact; **SLSA v1.2**-compatible build provenance; container + FS vuln + results with reviewed exceptions; a signed tag/release + documented + rollback; an **immutable release manifest** (source commit, migrations, + dependency locks, workflow provenance, SBOM, image digest, test/benchmark + receipts, Figma file ID, known limitations); liveness/readiness split; + OpenTelemetry traces/metrics/logs with stable cardinality and no + secrets/customer values; SLI/SLO + capacity profile linked to #951; + dashboards/alerts + runbooks for secret loss/rotation, target outage, + queue backlog, failed migration, backup restore, dependency incident, + compromised share/API key. + +**Current state.** At `main@8dc74692` backend + frontend versions are +`0.1.0`; the repo has 60+ open PRs. Supply-chain pinning is already +enforced (hash-locked pip, digest-pinned Docker, SHA-pinned Actions, +OpenSSF Scorecard). CI runs mypy + pytest + typecheck + vitest + production +build + CodeQL + Scorecard + dependency-review. No tagged, +provenance-backed release candidate proves the complete buyer journey; no +consolidated release-evidence manifest exists. + +**Gap.** No single release-evidence manifest; no PR-queue classification of +record; the operability baseline (#951), tenant claim (#950), credential +lifecycle (#946), lineage (#948), and governed apply (#949) are all +incomplete; PR #834's useful commits are not yet decomposed onto `main`. + +**This loop's increment PRs.** +- **#1040** β€” this document: the first artifact toward the #953 evidence + manifest. It currently classifies only **this loop's own increment + PRs** (each mapped to its issue, with the org-incident blocker named and + the merge-wave dependency order stated). The full #953 PR-queue shaping + step β€” every one of the ~60 open PRs captured at its exact head and + classified as unique / stack-dependency / superseded-duplicate / + contaminated-aggregate / experiment-or-post-GA / blocked-by-incident β€” + is **not yet done** and remains a tracked #953 deliverable (see + "Remaining increments"). +- **#1024** β€” `.Jules` ↔ `.jules` case-collision fix that unblocks + CI-clean git operations on case-insensitive filesystems (release-hygiene + prerequisite for any rebase wave). +- **#1025** β€” local Playwright E2E harness + `nanoid` pin (closes #1014); + the harness the product-journey rehearsal will extend. +- **#1057** β€” `app/release/manifest.py` `build_release_manifest(...)`, a + pure assembler (no git / network / filesystem) that validates and + normalizes the supplied release facts (source commit, backend / + frontend versions, migration revisions, dependency-lock digests, + included PRs, known limitations, generated-at) into one immutable + JSON-serializable manifest; `is_ga_candidate` is `True` only when the + known-limitations list is empty (honesty rule); a `ValueError` names + the first field that fails validation. New `app/release/` package. + Cites NIST SP 800-218 and SLSA v1.2. +- **#1063** β€” `app/release/sbom.py` (`parse_pip_lock` / `parse_npm_lock` + / `build_sbom`): a pure text/JSON parser that turns the lockfiles the + repo already commits into a **CycloneDX 1.6** `bom` β€” no `pip`/`npm` + run, no dependency resolution, no network. Components are de-duplicated + by purl and sorted by `(type, name, version)`; blank envelope metadata + raises a `ValueError` naming the field. Cites OWASP CycloneDX 1.6 and + NTIA (2021) SBOM minimum elements. + +**Remaining increments.** The pure manifest assembler landed (#1057) and +the CycloneDX SBOM generator landed (#1063); what remains to make this a +release-evidence artifact of record: signed build provenance / +attestation (SLSA v1.2); signing the SBOM and referencing it from the +manifest by digest; the operability baseline (SLI / SLO + dashboards ++ runbooks, linked to #951); migration rehearsal automation; the +per-dependency release-decision table; and the full open-PR classification +of record (every open PR at its exact head, with a `release_blocker` / +`post_ga_committed` / `experimental` / `not_planned` decision + +rationale) β€” deferred until the incident clears and the merge wave drains +the loop's own stack, since classifying ~60 PRs that cannot merge yet +would go stale immediately. Then a synchronized version bump + `CHANGELOG` +release section + `RELEASE_NOTES.md` once #946–#952 reach `merge-ready` on +their MVP increments. + +**Status:** `spec'd` (this document + #1024 / #1025 are the first +release-hygiene increments). + +--- + +## Cross-repo / ecosystem + +| repo | relationship to pg-erd-cloud | status | +| --- | --- | --- | +| `ContextualWisdomLab/.github` | Central required-workflow authority (`opencode-review`, `strix`, coverage, scorecard). **Currently the commercial-readiness blocker** (`#1531`). | incident, owned by a parallel effort | +| `ContextualWisdomLab/keyverse` | Central Identity Provider β€” the `keyverse` identity mode in #950; org binding for the single-tenant GA profile. | not yet integrated | +| `ContextualWisdomLab/contextual-orchestrator` | The LLM access contract #946 Β§9 and #952 require pg-erd-cloud to adopt instead of a per-provider key. | not yet integrated | +| `ContextualWisdomLab/wardnet` | Rust-first gateway / SOC control-plane baseline; relevant to #950 ingress + #953 operability. | not yet integrated | +| `ContextualWisdomLab/TEPP`, `fast-mlsirm` | Psychometrics platforms β€” **not consumed by pg-erd-cloud**; listed for ecosystem completeness only. | n/a | +| `ContextualWisdomLab/RankWeave`, `ThreadWeave`, `LineageWeave`, `disksage` | Independent libraries; `LineageWeave`'s DAG-reconstruction idea informed the #948 lineage model shape but the code is not imported. | n/a | + +## References (APA 7th) + +The contracts summarized above lean on these external standards; each gap's +own doctoring note under `docs/doctoring/` carries the domain-specific +citations for its increment. + +- American Educational Research Association, American Psychological + Association, & National Council on Measurement in Education. (2014). + *Standards for educational and psychological testing*. American + Educational Research Association. + https://www.aera.net/Publications/Books/Standards-for-Educational-Psychological-Testing-2014-Edition + β€” evidence-class framing (`observed` / `declared` / `inferred` / + `proposed`) in #947, #948, #952. +- Codd, E. F. (1970). A relational model of data for large shared data + banks. *Communications of the ACM, 13*(6), 377–387. + https://doi.org/10.1145/362384.362685 β€” the relational-normalization + basis (further normal forms follow in Codd, 1971/1972); normalization + assessment in #947. +- International Organization for Standardization. (2017). *Health + informatics β€” Pseudonymization* (ISO/TS 25237:2017). + https://www.iso.org/standard/63553.html β€” cited for the principle that + protection is access control, encryption, purpose limitation, and audit + rather than blanket masking; the non-masking protection stance in #946, + #949, #953. +- National Institute of Standards and Technology. (2022). *Secure software + development framework (SSDF) version 1.1* (NIST Special Publication + 800-218). https://doi.org/10.6028/NIST.SP.800-218 β€” the release evidence + and governed-apply controls in #949 and #953. +- PostgreSQL Global Development Group. (2026). *PostgreSQL 18 documentation: + Data definition*. https://www.postgresql.org/docs/18/ddl.html β€” the + supported-operation matrix in #949 and the compatibility matrix in #953. +- SLSA Community. (2025). *Supply-chain levels for software artifacts + specification, version 1.2*. https://slsa.dev/spec/v1.2/ β€” the build + provenance and attestation requirements in #953. + +## How this document is maintained + +The autonomous review/merge loop updates this file each time a gap increment +ships or the incident status changes. This revision (iter40) added PR #1063 +(the CycloneDX SBOM generator, `app/release/sbom.py`) to the #953 list, +taking the stacked-PR count to 19. iter36 added PR #1060 (the signed-waiver +helper, `app/spec/waiver_record.py`) to the #947 list. iter32 added PR #1057 +(the pure release-manifest assembler, a new `app/release/` package) as the +release-evidence increment for issue #953. iter28 added PR #1056 (the +versioned baseline-report envelope) to the #951 list. iter22 added PR #1049 +(the tenant-authority +column-presence check) to the #950 list and PR #1050 (the lineage PROV-JSON +projection) to the #948 list. iter20 added PR #1048 (transitive-dependency / +3NF assessment) to the #947 list. iter19 fixed the markdownlint MD018 line-start warnings +and the reference links flagged on PR #1040. iter18 added PR #1045 +(repeat-run percentile aggregation) to the performance-gap increment list. +iter16 filled the three epic sections (#949, #952, #953) from their issue +bodies and aligned the LLM-orchestrator wording with the existing +configuration-only `/chat/completions` integration in +`docs/llm-orchestrator-integration.md`. +When the gate clears and PR #942 merges, reconcile its +`docs/product-technical-gap-baseline.md`, +`docs/doctoring/product-technical-gap-baseline.md`, and +`docs/adr/0002-product-technical-gap-baseline.md` with this consolidation.