208 Commits

Author SHA1 Message Date
fb293d229f refactor(dr): preserve recovery contracts in bounded owners 2026-10-24 19:08:27 +03:00
853eec182f fix(semantics): contract catalog and DR test definitions individually 2026-10-24 09:10:40 +03:00
ab3d2be44f fix(semantics): require fresh audit diagnostics and exact config identity 2026-10-23 22:56:28 +03:00
cbe1ba1264 test(bundle): separate output and wait deadlines 2026-10-23 19:57:26 +03:00
8170461af6 merge: integrate independently reviewed DR recovery checks 2026-10-23 17:15:37 +03:00
fef0a8a71e fix(bundle): bind deployment metadata to the selected release 2026-10-23 17:14:44 +03:00
e355dce56c feat(dr): verify exact recovery objects and retained evidence 2026-10-23 17:10:28 +03:00
885fe6b649 feat(bundle): verify archive identities before image import 2026-10-23 16:45:51 +03:00
c8b12f37e4 build: standardize runtime on Python 3.13 2026-10-23 15:57:53 +03:00
61c69dd343 merge: integrate accepted offline DR safeguards with authoring phase1 2026-10-23 11:28:54 +03:00
agent
898918c8a3 fix(dr): refuse absolute and ambiguous root tar paths before root-marker handling
Owner-scope continuation of TASK-DR-RUNBOOK-BOUNDED-FIX-20261023 after the
independent review of 86683500. Same DR session and branch; backend tree
unchanged; no extraction, no DB/PG, no n2/full/lab/backup/restore/push.

Reproduced blocker: tarfile strips trailing slashes from directory names, so raw
"/", "///", "//" (and "") arrive as an empty member name. The inventory skipped
an empty name as a root marker before _unsafe_name ran, so a full check() over a
hostile archive with consistent manifest digest/size and listing returned [].

Fix: the archive inventory now refuses a raw empty or "/"-prefixed member name
with BACKUP_UNSAFE_ENTRY before any "." / "./" root-marker normalization; only
an explicitly relative "." (from "." or "./") remains a valid root directory
marker. Recorded listing entries are checked for raw absolute paths the same
way before normalization. No extraction, no escape claim: this is read-only
preflight acceptance of forbidden absolute paths.

New full-preflight fixtures (existing cases retained unchanged): "/", "///",
"//" and "" directory members refused; absolute regular member refused;
absolute listing entries "/", "///", "/drafts", "/etc/synthetic" refused; and
"." / "./" directory markers still accepted. The review's authoritative
artifacts/.orchestrator/dr-review-86683500/test_absolute_root_gap.py now passes
3/3 against this source (previously 2 failed).

Offline evidence: focused run RC 0, 67 passed (36 preflight + 13 flow + 18
unchanged accepted helper fixtures), JUnit 0 failures/errors/skips;
artifacts/.orchestrator/dr-bounded-fix3-20261023/.
2026-10-23 10:12:12 +03:00
agent
8668350064 fix(dr): close three reproduced bounded-fix blockers on 242dd8fb
Owner-scope continuation of TASK-DR-RUNBOOK-BOUNDED-FIX-20261023 after the
independent review of 242dd8fb. Same DR branch; backend tree unchanged; no
PG/lab/real backup/restore/network/push and no n2 selector.

1. Resolver failure can no longer be masked and stale targets cannot survive.
   §0.1 uses the pinned "$PYTHON", captures the resolver output and checks its
   exit status (DR_RESOLVER_FAILED/DR_RESOLVER_EMPTY) BEFORE eval; stale
   PGHOST/PGPORT/PGUSER/PGDATABASE are unset before applying only the fully
   successful assignments. The §0.2 credential fence also uses the pinned
   interpreter instead of PATH python.
2. Destination mapping is enforced before any write boundary.
   New §2.0 runs the read-only preflight BEFORE createdb/restore/extract and
   passes the actually selected destination (host/port/database/user/schema from
   the pinned resolver plus DB_SCHEMA). The preflight now validates the
   destination against manifest source and the optional reviewed mapping:
   absent mapping for a foreign destination, a mapping for the same destination,
   or a mapping whose destination differs are typed refusals. §2.3 is restore
   only; §2.5 keeps the post-restore snapshot mapping re-check.
3. Root-like tar members cannot bypass the forbidden-type contract.
   The archive inventory now checks every member's type before a root marker
   (""/.) is skipped; only DIRTYPE is accepted as a root marker, while
   symlink/hardlink/FIFO/device/regular root-like members are refused with the
   typed member code. Normal root-directory markers remain supported.

Focused fixtures added (existing 49 retained, accepted helper and its 18 tests
untouched): destination guard positives/negatives (same, foreign without
mapping, reviewed mapping, destination mismatch, unexpected mapping, incomplete
destination, CLI), root-directory positive plus "./" link/device/regular
negatives with consistent digest/list, resolver pinned-interpreter success
without PATH python and stale env cleared, resolver failure stopping before any
write boundary, and destination-guard ordering before createdb/restore/extract/
start.

Offline evidence: focused run RC 0, 57 passed (26 preflight + 13 flow + 18
unchanged helper), JUnit 0 failures/errors/skips;
artifacts/.orchestrator/dr-bounded-fix2-20261023/.
2026-10-23 09:44:50 +03:00
agent
242dd8fbf4 fix(dr): bounded runbook hardening, preflight gate and focused offline tests
Implements the bounded scope of TASK-DR-RUNBOOK-BOUNDED-FIX-20261023 on top of
9e5ba2fd. No backend/model/migration change and no PG/lab/backup/restore/network
operation.

Runbook (specs/DR-ROLLBACK-RUNBOOK.md):
- §0.0a pins REPO_ROOT and one absolute PYTHON from the existing venv and refuses
  non-absolute BACKUP_DIR/STORAGE_ROOT_PATH before any operation; every
  executable fence starts at REPO_ROOT and local cd runs only inside a subshell,
  so a failing cd stops the procedure instead of being masked.
- §1.7 manifest now stores only immutable reference counts plus exact
  reference_ids (catalog revisions with digests, approval receipts, published
  publication operations); the manifest is never rewritten.
- §2.3 runs the new read-only preflight BEFORE pg_restore; §2.4 extracts the
  storage archive without starting writers; §2.5 verifies snapshot identity,
  revision, counts and exact reference IDs BEFORE upgrade/start and records
  restore.identity.json; §2.6 starts the backend as the single writer/upgrade
  boundary with a bounded readiness wait; §2.7 verifies an explicitly declared
  EXPECTED_TARGET_HEAD (no first/latest) and reference preservation, recording
  restore.after.json. A reviewed source→destination mapping is required for a
  foreign destination and the identity guard is never bypassed.
- §3.1 fixes numeric-zero wording for count queries; §3.3 refuses an undeclared or
  empty approved catalog fixture instead of accepting catalog OK 0; §3.4 uses the
  pinned interpreter.
- §5 states that RTO/RPO budgets are not set; §6 lists Git recovery, holds,
  destination mapping, credentials policy, release 0051 and the unified exercise
  as prerequisites / LIVE NOT PROVEN.

New scripts/dr_backup_preflight.py: read-only gate with exact manifest schema and
types, non-empty dump/archive/lists, sha256+bytes, archive listing from the same
archive, dump listing from the same dump, and type-aware tar inventory (regular
files/directories only; links/devices/FIFOs and unsafe/duplicate paths refused)
with typed codes and no raw values or tracebacks.

Focused offline tests: scripts/tests/test_dr_backup_preflight.py (23) and
scripts/tests/test_dr_runbook_flow.py (8). The flow tests execute the actual bash
fences with PATH-only fakes, assert cwd discipline and fail-fast ordering
(preflight/restore/extract/start), and execute the real §2.5/§2.7 comparison
bodies against a DB-boundary double with hardcoded positives and negatives
(foreign revision/database/schema/counts/reference, mapping required/reviewed,
missing target head, changed reference, modified manifest). The previous 18
fixtures are unchanged and pass.

Offline evidence: focused run RC 0, 49 passed, JUnit 0 failures/errors/skips;
artifacts/.orchestrator/dr-bounded-fix-20261023/.
2026-10-23 09:31:45 +03:00
7abbd500cc Merge commit '9e5ba2fd7fc3b5083e5dafd4ac53a86e91fe6771' into integration/chart-dr-20261023 2026-10-23 08:59:15 +03:00
agent
9e5ba2fd7f fix(dr): pin evidence traversal with dir_fd and require explicit handle prefix
Two reproduced blockers from the review of d9518168:

1. Parent-directory race in dr_evidence_check.py: the helper lstat-ed and
   resolved a full pathname and then opened that pathname again, so O_NOFOLLOW
   protected only the leaf. A probe replaced the verified run directory with a
   symlink immediately before os.open, which opened the external inode and
   _verify returned [] even though the ref digest matched.
   Fix: traverse pinned directory descriptors. The storage root is opened once
   (O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC) and closed in a finally block; every
   following component is opened relative to its parent fd with O_NOFOLLOW
   (O_DIRECTORY for directories) and the leaf descriptor is fstat-ed and read.
   No pathname is checked and then reopened. Component classification and a
   safe post-failure re-classification (without following) report the symlink
   even when the kernel surfaces ENOTDIR instead of ELOOP. External bytes are
   never read, including when their digest equals the ref digest.

2. handle: prefix bypass at :214: removeprefix("handle:") accepted a bare
   64-hex ref, so a file under drafts/handles/<digest> resolved and _verify
   returned [] although production HandleBlobStore.retrieve requires the
   explicit handle: prefix and 64 hex digits (scenario/handles.py:46-49).
   Fix: the resolver requires the explicit handle: prefix (the draft branch
   keeps DraftStorage.parse_content_ref as the only parser); a bare digest is a
   typed EVIDENCE_REF_MALFORMED refusal.

Retained offline proof added to scripts/tests/test_dr_evidence_check.py:
- test_parent_directory_swap_before_open_cannot_escape_storage_root: a
  deterministic swap of the checked run directory to a symlink right before
  its os.open (monkeypatched at the dir_fd boundary) must yield
  EVIDENCE_PATH_SYMLINK and no digest check, using identical inside/outside
  bytes so hashing alone could not catch it;
- test_bare_digest_is_not_a_handle_ref_even_when_handle_bytes_exist: a bare
  64-hex ref with existing handle bytes must be EVIDENCE_REF_MALFORMED, not a
  PASS.
All 16 previous fixtures are retained: 18 offline tests pass.

Verified offline only: ruff clean, 18 tests, runbook python blocks parse and
its backup sentinels/fail-fast gate are intact. No backup, restore, database,
lab or push was executed; no live DR claim is made. Backend tree is unchanged,
so n2 is not repeated and no selectors are created.
2026-10-23 00:20:19 +03:00
agent
d95181682d docs(dr): fix runbook blockers 1-5, tighten typed evidence scope, add offline regressions
Review blockers addressed (msg-dr-runbook-review-20261022 on 46233d9c):
1. Credentials are pinned to /home/busya/ss-tools-lab/config (owner/mode/symlink
   checks on root and subdir, /home/busya not group-writable). The passfile is
   created exclusively (O_EXCL, 0600, unique name) and holds exactly two entries:
   the target database and the maintenance database postgres, both with the same
   exact host/port/user. No wildcard entry, no URL or password in argv.
2. The helper refuses symlinked path components and out-of-root resolutions
   before opening anything: every component is lstat-ed, the resolved path must
   stay inside the resolved storage root, the file is opened with O_NOFOLLOW and
   must be a non-empty regular file. Diagnostics stay fail-closed.
3. Scope is no longer a prefix scan: it comes from the typed contract - catalog
   DraftRef fields validated by CatalogRevisionDocument (capture_content_ref,
   descriptor native_request content_ref, visual expected_image_content_ref),
   entry-authority protected evidence including the nested security authority
   evidence, active scenario artifacts without a terminal retention receipt, and
   draft artifacts referenced by those sets. Malformed or missing mandatory
   fields are typed refusals; duplicate refs merge only when digests agree.
4. Diagnostics print only code, controlled source, scheme and a 16-hex ref
   digest; raw untrusted ref text (and potential secrets in it) never reaches
   the report.
5. Fail-fast starts at the top of the procedure: set -euo pipefail in the
   preamble and in the single backup block, and a DR_BACKUP_INCOMPLETE gate over
   dump/archive/listing runs before any manifest write. A failed dump cannot
   produce a successful manifest.

Add retained offline regressions in scripts/tests/test_dr_evidence_check.py
(16 tests, no PostgreSQL, temp dirs only): symlink-file and symlink-directory
escape, relative storage root, malformed non-prefix ref next to a valid ref,
typed catalog/authority scope completeness, security-authority evidence
inclusion and legacy absence, malformed typed DraftRef refusal, missing entry
authority refusal, duplicate/conflicting digest handling, diagnostic redaction,
exit codes, and a backup-block run with a failing pg_dump stub proving no
manifest is written.

Verified offline only: ruff clean, 16 offline tests pass, all python blocks in
the runbook parse, extracted backup block aborts before manifest. No backup,
restore, database or lab was executed; no live DR claim is made. Backend tree is
unchanged, so no new n2 selectors are created.
2026-10-23 00:09:20 +03:00
agent
46233d9c18 docs(dr): fix backup/restore runbook review findings, add offline evidence check
Findings from msg-dr-runbook-review-20261022:
1. Password could reach argv: host/port/user/database are now resolved from
   DATABASE_URL once and passed as flags; the password lives only in a private
   0700/0600 PGPASSFILE with --no-password everywhere (PGPASSWORD forbidden).
2. Restore target is now exact and fail-fast: existence is checked through the
   maintenance database, createdb gets explicit host/port/user, the target must
   be empty before restore, pg_restore runs with --exit-on-error
   --single-transaction, and target/identity/counts are compared with the
   backup manifest (RESTORE_MISMATCH). No destructive reset anywhere.
3. Added the snapshot protocol: quiesce writers (backend owns scheduler and
   retention), prove no other client backends, T_snap = stop point, order is
   dump -> storage archive -> evidence verification; RPO is measured from
   T_snap, never from t_dump_end (hot mode is explicitly an estimate).
4. Trigger check now asserts the real five (table, trigger) pairs with schema
   scope, a reverse check and the shared function, instead of an unscoped
   count of four.
5. Evidence verification is fail-closed via scripts/dr_evidence_check.py:
   malformed refs, missing bytes, digest mismatch (file vs ref and vs row),
   and archive misses are typed refusals with exit 1; an empty scope exits 3
   and never counts as a PASS.

The helper is offline and read-only: it imports DraftStorage.parse_content_ref
as the single draft-ref authority, reads the database and the storage tree only
when no --refs-from fixture is given, and its ref/hash/archive logic was
smoke-tested offline (exit 0/1/3, all five codes). No backup, restore, database
or lab was executed for this change; the runbook states explicitly that it is
offline preparation and not a live DR proof.

Docs-only for the backend tree: no file under backend/ changes.
2026-10-22 23:54:24 +03:00
f097371284 feat(e2e): drive explicit target deploy and validation receipts
Send target_selection=environment and the exact target id for explicit deploy
and validation, require applied/recorded receipt with matching environment id
and exact deployment/commit/content fields, preserve typed server error codes,
and remove the obsolete pre-write guard. Add mock-only positive and refusal
fixtures for the CLI.
2026-10-22 23:20:53 +03:00
aa97fee14a fix(e2e): keep default existing environment URLs accepted
The explicit-target preflight was also applied to the default allow_create path,
where it compared an existing full-flow environment URL with the constant. The
legacy ensure_environment contract intentionally checks URL only for explicit
maps; existing default environments may have custom URLs.

Keep the explicit guards (exact id/stage/URL, unique canonical stage, pre-write
refusal, full receipt unavailable, explicit push-only, empty-arg refusal) and
the stage/ambiguity checks. For the default path only, validate stage and
uniqueness without imposing the constant URL. Add a full onboard fixture with
existing custom URLs and absent explicit args, plus the explicit negative that
the same custom URL mismatch refuses before writes.
2026-10-22 19:35:49 +03:00
003d82666c fix(e2e): pin onboarding target to explicit server environments
The explicit --env-ids/--stage-urls maps were not proof of the deployment target:
/deploy and /deployment-validation resolve the single server environment per
canonical stage, and ensure_environment did not check the stage of an existing
record. A request could write init/sync/commit/push before discovering a wrong
or ambiguous PREPROD target.

Preflight the server role resolution before any write: every explicit id must
exist, carry the expected canonical stage and URL, and be the only configured
environment for that stage. Full deploy/validate requests are refused before
writes when the final receipt cannot prove the expected environment id (the
current deployment response carries stage/status only); an explicitly requested
--stop-after push remains available. Empty explicit CLI values are rejected
instead of falling back to defaults; defaults apply only when both arguments are
truly absent. Existing guards and six original fixtures are preserved.
2026-10-22 19:35:49 +03:00
b2adeb76cb feat(e2e): parameterize onboard_twin_dashboard environment ids and URLs
The onboarding tool hardcoded full-flow-* environment ids and
http://superset-<stage>:8088 URLs, so a second Superset 6.1 candidate with
superset61-dev/preprod/prod could not reuse it.

Add --env-ids and --stage-urls (dev=...,preprod=...,prod=...). Defaults are the
previous constants, so the existing 5.0 pipeline is unchanged: missing default
environments are still created. When either flag is explicit, environments are
only verified: a missing id refuses ENVIRONMENT_MISSING:<id>, a URL mismatch
refuses ENVIRONMENT_URL_MISMATCH:<id>, and nothing is created. Repository
init/sync/commit/deploy parameters now use the resolved dev env id.

Tests: default creation path, explicit ids without creation, missing env, URL
mismatch, and init/sync params with superset61-* ids.
2026-10-22 19:35:49 +03:00
57473dc681 docs(e2e): record PARTIAL handlebars acceptance and track credential helpers
Add the one-page PARTIAL report: UI approval/consume/publication/resume/pin are proven live; runs, mutation
and audit are blocked by the missing standard authoring path for an executable chart check. Record the product
findings (binding restart, BA-UX label, self-approval, G-CHART-TYPES/G-AUTHORING-FROM-LINK) and append-only ids.
Move the artifact-only resume login and PREPROD credential lookup into the tracked instrument: explicit
BACKEND_USERNAME (no admin default), TARGET_SUPERSET_* for PREPROD, and a tested login helper.
2026-10-22 07:40:02 +03:00
7e65d55c42 fix(full-flow): compute effective RLS scope for a live binding
Read the execution principal's roles from Superset CurrentUserRestApi, apply Regular membership and Base
non-membership semantics, and hash the canonical scope (roles, applicable rules and considered ids). The
fixture guard now requires zero applicable rules rather than an empty global RLS table, which preserves its
meaning without rejecting unrelated fixture roles. Tests cover both filter types, unknown types, guard and
deterministic ordering.
2026-10-22 06:26:19 +03:00
2dc161ccd2 fix(e2e): resume chart 111 from recorded publication facts
Add verify_resume_capture: the consumed candidate must match the exact published catalog revision entry
(chart id, capture artifact and source hash), otherwise refuse. Tests cover a matching resume and both
candidate and entry mismatches. The acceptance artifact now resumes chart 111 from the recorded ids and starts
chart 114 from the revision produced by the 111 publication instead of the initial catalog pin.
2026-10-22 06:10:44 +03:00
de07c936e9 fix(e2e): pin the newly published catalog revision and register a capture scenario
The post-publication pin step used the numeric M01 registration, which requires numeric_verification absent
from the handlebars target. Build the pin target from the consume response's exact catalog_selection and
register a normal pinned capture scenario instead. Missing/invalid published selection refuses typed
TABLE_FAMILY_PIN_SELECTION_UNAVAILABLE without falling back to the starting empty revision.
2026-10-22 06:07:06 +03:00
3b843c1a88 fix(e2e): consume the approval gate as its confirming approver
The UI decision confirms the gate with the approver actor, while the instrument consumed it with the admin
session; the product correctly refuses consume by a non-confirming actor. Authenticate a second Backend client
as the approver for the consume call, then keep publication under the admin workflow. Add a test that the
consume call and approver credentials remain wired.
2026-10-22 06:03:23 +03:00
9bcb5bf7ca fix(e2e): wait for the approver session before opening the review page
The login click returns before the app stores auth_token and leaves /login, so the immediate goto to the
scenario page lost the race and never requested baseline-candidates. Wait for auth_token and for a
non-login URL, then wait for the candidates response and the exact data-candidate-id card. Tests assert
the session wait and the existing delayed/missing/duplicate card cases.
2026-10-22 04:40:02 +03:00
8fce397f91 fix(e2e): wait for the candidate list and card before selecting
The review page renders candidates asynchronously, so the exact data-candidate-id check raced the
baseline-candidates response and saw zero cards. Wait for that response, require it to be successful, then
wait for the exact card to be attached; 0 or 2 matches still refuses
TABLE_FAMILY_UI_CANDIDATE_CARD_NOT_UNIQUE. Tests cover delayed attachment, timeout and duplicate cards.
2026-10-22 04:35:40 +03:00
ed2ade2c52 fix(scenario-editor): expose the candidate id and select the exact review card
Three pending candidates share the same label, so ui_approve could approve a stale card by DOM position.
Render data-candidate-id on each BaselineReview card and resolve the card in the instrument by that exact
attribute; exactly one match is required or TABLE_FAMILY_UI_CANDIDATE_CARD_NOT_UNIQUE refuses. All controls
are card-scoped. After the UI decision the instrument reads the gate and requires status=confirmed with the
approver as actor, recording the approval receipt id. Tests cover non-first selection, duplicate cards and
one exact label per step.
2026-10-22 04:31:10 +03:00
e554074907 test(e2e): drive baseline approval with one exact UI label per step
The approver UI now exposes the candidate card; replace the alternative login regex with the exact Вход label,
scope every action to the candidate card, and refuse typed TABLE_FAMILY_UI_CONTROL_UNAVAILABLE when an exact
label is missing or ambiguous. The test covers found, missing and ambiguous controls and asserts one exact
label per step.
2026-10-22 04:28:01 +03:00
84c8778c54 fix(scenario-editor): bind baseline review to the candidate catalog revision
BaselineReview called baseline-overview with scenario_id only, while the no-fallback backend requires an
explicit catalog revision. Read every candidate's frozen catalog binding with readCatalogBinding, skip the
overview when there are no candidates, request it only when all admitted candidates share one catalog pair,
and show an explicit conflict when they differ. Approval gate create/decide/consume now carry each
candidate's own catalog pair; page URL parameters are never read. Tests cover single pair, no candidates,
mixed revisions, invalid binding kept visible, and planted URL parameters. Also fix acceptance UI login to
#username/#password and the exact Вход button.
2026-10-22 04:00:02 +03:00
40c08b7089 fix(e2e): distinguish an empty filter context from unavailable data
Removing bool(expected_ids) allowed zero filters but let an unparsed context or a missing
target expected_filter_context reach len(None). Require both sides to be lists before length/equality
checks; the existing context_valid gate then raises NATIVE_DISCOVERY_FILTER_CONTEXT_UNAVAILABLE.
Negative tests cover context=None, a wrong schema version and a missing target context.
2026-10-21 23:20:51 +03:00
c79cfa47c4 fix(e2e): require explicit PREPROD Superset credentials in the target builder
The target readback fell back to the DEV Superset password, so a missing PREPROD credential could silently
run against the wrong account. TARGET_SUPERSET_USERNAME/TARGET_SUPERSET_PASSWORD are now required in the
0600 private env (no DEV substitution), a missing pair refuses TABLE_TARGET_TARGET_SUPERSET_CREDENTIAL_UNAVAILABLE,
and the ambiguous target-dashboard code is TABLE_TARGET_DASHBOARD_UUID_AMBIGUOUS.
2026-10-21 23:20:51 +03:00
696392ace7 fix(e2e): accept a zero-filter native discovery context
select_native_chart_coordinate required bool(expected_ids), so a dashboard with no native filters could
never match even when the observed and expected filter sets were both empty. Zero filters is a valid input
class: compare lengths and equality directly, and pin it with a coordinate-selection test.
2026-10-21 23:20:51 +03:00
771d033ce5 fix(e2e): authenticate the native-capture adapter with the product token
prepare._native_http_call sends requests through a bare httpx client without headers, so the table-family
capture adapter sent reference-discovery unauthenticated (401 NATIVE_HTTP_RESPONSE_UNAVAILABLE). Expose the
authenticated headers from Backend and copy them onto the borrowed client.
2026-10-21 23:20:51 +03:00
167302e311 feat(e2e): build one consistent DEV->PREPROD table-family target
build_table_family_target.py used to read only the DEV Superset, so the target mixed source identities
with target environment fields: superset_url/reference_url/target_dashboard_id stayed DEV while the release
and capture run against PREPROD. The builder now reads the source dashboard/chart/dataset in DEV and resolves
the same dashboard UUID to its numeric PREPROD id, then emits preprod target_stage, superset_url, reference
origin/url and target ids alongside full-flow-dev source identities.

- capture_scenario is seed_empty-compatible (mcp_bootstrap) with explicit case ids and field_names parameters;
- expected_filters_hash/filter_context_path are materialized for the empty filter context;
- same source/target origin or environment, or a missing/ambiguous target UUID, refuses typed;
- fake-readback tests prove DEV fields stay DEV and PREPROD fields stay PREPROD.
2026-10-21 23:20:51 +03:00
7fedcac41d fix(e2e): send product login form data as httpx data for table-family acceptance
httpx.Client.post has no form keyword; the backend login form must be passed as data. The
table-family acceptance backend client is the only caller of this login path.
2026-10-21 21:39:52 +03:00
f2ed685b80 test(e2e): guard tooling against the lab credentials file reference
The lab rule is that ~/ss-tools-lab/config/credentials.env is never read by agent tooling; only
twin-onboard.env carries lab secrets. A guard test scans scripts/e2e-scripts/*.py and the
handlebars-acceptance orchestrator artifact for that filename. The pivot fixture that only used
the same string as a temporary file name now calls it private-env.conf.
2026-10-21 21:39:52 +03:00
6c81fa04f1 feat(e2e): add table-family target builder and three-run acceptance
Both tools are family-level, not handlebars-only: ag-grid and the virtual SQL/Jinja vector need the same
readback contract and the same three-run PASS -> named-cell FAIL -> restored PASS envelope.

build_table_family_target.py reads the dashboard, chart and dataset from the lab Superset (UUID only from
a list row addressed by id eq with count == 1), parses the aggregate form and writes an exact per-column
chart_contract (keyless or groupby keys), seed and prepare-compatible profile. Raw, dimension-only, foreign
viz types and missing identities refuse typed; nothing is synthesized.

table_family_acceptance.py runs the vector through the public Scenario Runs API: named cells are explicit
(query/key/metric/before/after/delta), the mutation is declarative ClickHouse with a validated predicate,
it refuses a busy run window before writing, restores in finally and requires a clean superset_write_audit
run phase. Capture/gate/consume/publication reuse native_chart8_prepare; the approval decision itself is
performed only in the product web UI under the named APPROVE_BASELINE account. Compensation restores
ClickHouse only and never deletes catalog rows.
2026-10-21 18:59:51 +03:00
b1bea5f510 feat(e2e): classify apply-window filter_state as ephemeral and data writes as fatal
Owner amendment 2026-10-21: the exact POST/PUT /api/v1/dashboard/<id>/filter_state
of the run dashboard inside a declared Apply window is temporary UI state (Redis in
the owner's production; key_value with a 90-day TTL in the lab), not a metadata
mutation. The Apply forwarding mechanism itself is unchanged.

superset_write_audit: repeatable --apply-window DASHBOARD_ID:START/FINISH; only the
exact scoped path for that dashboard inside that window becomes
CLASS_EPHEMERAL_FILTER_STATE, counted in by_class/ephemeral_filter_state and
excluded from state-changing. A foreign dashboard, unscoped or keyed filter_state
path, an outside-window request or any other write stay violations. The window must
lie inside a declared run window.

Data write: the owner's only real contour mutation is POST
/api/v1/datasource/update_comments (ClickHouse comment insert from the ag-grid
fork). The audit now classifies the exact deployment-relative path and its subpaths
as CLASS_DATA_WRITE for any method; any occurrence in any phase sets
data_write_attempted=true and fails the audit. The guard still aborts the request
with a fatal context block, but logs the distinct SUPERSET_DATA_WRITE_BLOCKED code so
the attempt is visible immediately; behaviour is otherwise unchanged.

Matrix G-SUPERSET-READONLY and the filters report record both dated owner
amendments without changing the gate status.
2026-10-21 17:10:46 +03:00
73e2692747 fix(e2e): read twin uuid from the dashboard list and compensate a failed create
The seed now paginates the dashboard list to its exact count and refuses FILTER_TWIN_DASHBOARD_LIST_INCOMPLETE; uuid and comparison identity come from the list (the Superset 5 show omits uuid), while show supplies position/metadata. Any post-create check failure deletes exactly the created dashboard and cloned chart ids (FILTER_TWIN_CREATE_COMPENSATED) and reports remaining ids if compensation fails. A create response without an id is adopted by the unique slug and completed through add-missing. Tests cover show-without-uuid, incomplete pagination, compensation and incomplete compensation.
2026-10-21 11:45:18 +03:00
3fedd51854 feat(e2e): add disposable date-filter twin create+clone and seed wrapper
build_twin_dashboard --create --clone-charts creates a container dashboard with the
source filters, then clones every source chart into it through the existing add-missing
clone path (compensating the new dashboard when the clone pass fails);
--plan --create --clone-charts prints the planned clones without writing.
seed_filter_twin.py wraps plan/create/verify/drop for the G-BROWSER-FILTERS twin
(source dashboard 1, dataset #1 read-only): it sets showSearch explicitly on exactly
the Region filter, writes a mode-0600 receipt and a before/after dashboard manifest
that allows exactly one new dashboard, and refuses drift on verify.

Verification: e2e-scripts suite 741 passed; twin builder tests 81 passed; seed tests
6 passed; ruff clean; semantic gate PASS.
2026-10-21 11:45:18 +03:00
915cc525a6 test(e2e): add the single isolated-stack runner script
Ports are checked before anything starts (exit 3 when busy), a trap always tears the
e2e-repair project down with down -v, readiness and both one-shot seeds are verified, and the
Playwright runner is executed for the isolated chromium project only.
2026-10-21 01:39:43 +03:00
6341a535bf feat(dashboard-testing): wire dashboard tabs and make the semantic gate deterministic
- check-options now serves tab_id, tab_path and tab_titles (2cad4418): charts group by tabs and
  the headings use the dashboard titles (path fallback when titles are typed-unavailable), so the
  "waits backend" item is closed.
- semantic_parse_gate explains and fixes the fresh-worktree first-run FAIL: a full rebuild leaves
  the server process diagnostics cache stale, so the first audit can report phantom parser
  warnings while the persisted index is fine. The gate now retries: it audits again after
  recycling the MCP server, accepts a result only when it is stable, returns exit 1 for stable
  non-zero diagnostics and exit 2 when diagnostics do not stabilize (environment, not FAIL).
  Unit tests cover stable failure, phantom recovery, unstable diagnostics and stale index.
- A fresh-index run now passes on the first invocation (verified after removing graph.duckdb:
  PASS with 2 audit attempts, and repeated runs PASS).
2026-10-21 00:37:19 +03:00
a033e2de3d chore(semantics): keep the parse gate OPEN and add a pre-merge check
Owner decision (2026-10-20): G-SEMANTIC-HEALTH and G-FULL-REGRESSION are closed last,
on the GO commit, because later lanes add markup and every intermediate measurement
goes stale. This commit therefore does not close the gate.

- specs/PRODUCTION-ACCEPTANCE-MATRIX-043-050.md: the G-SEMANTIC-HEALTH row stays OPEN and
  records the rehearsal on addb727f (parser/schema 0, no-new base c7cd4023, INV-7 size
  warnings and Doxygen noise as explicit exceptions, final measurement on the GO commit);
  the amendment section and the recalculated P0 line say the same (30 rows: 13 CLOSED,
  2 RETIRED, 15 open — 8 PARTIAL, 7 OPEN; product NO-GO).
- scripts/semantic_parse_gate.py: pre-merge check over AXIOM's audit_contracts. It refreshes
  the index (incremental, unless --no-rebuild), reads severity_breakdown and exits 1 when the
  parse/schema class is non-zero, 0 when it is zero, 2 on a tool/environment failure. INV-7
  size warnings and Doxygen noise never fail it. --json prints the machine-readable result.
- scripts/e2e-scripts/test_semantic_parse_gate.py: unit tests for classification, exclusions
  and the two environment-failure exit paths.

Verified on this tree: audit parser/schema 0 (contracts 17 031, edges 6 415, unresolved 417 —
base c7cd4023 is 421), gate script exit 0, doc-gen nav exit 0, ruff 0, pytest scripts/e2e-scripts
728 passed.
2026-10-20 23:25:52 +03:00
addb727faf chore(semantics): close the parse-warning backlog (G-SEMANTIC-HEALTH plan A)
Markup-only pass: comments and semantic anchors, no product code lines.

Closed anchors (24 files): the missing #endregion lines are appended before the anchor that
exposed the mismatch or at end of block; two translate test files also had their inline
"// #endregion" comments moved to their own lines and one close order fixed; two misnamed
closes in tests/api/test_assistant_tools.py now carry the anchor they really close.

Schema fixes: 3 tombstone contracts gained @STATUS DEPRECATED (code) and 7 spec tombstones the
same tag; @REASON -> @NOTE (2), @BINDS_TO -> @RELATION BINDS_TO (3), @DECISION -> @RATIONALE (1);
INVERSE_OF -> BINDS_TO (2, not an allowed predicate), OWNED_BY -> DEPENDS_ON (1); [C:6] -> [C:5];
@LAYER TestSupport -> Test and @LAYER Model -> Store.

Flat hierarchical ids: 18 spec contracts in specs/030 and specs/031 renamed under their module
scope (DatasetLifecycleWorkspace.*, MaintenanceBanner.*) with their closing anchors. No relation
target referenced these ids, so no unresolved relation was created; prose mentions in other
documents are untouched.

Indexing: the generated dump specs-036-050-20260907-111314.md is excluded by exact name in
.axiom/axiom_config.yaml (snapshot, not a contract source).

Measured on this tree: parser/schema warnings 90 -> 1 (remaining: @media CSS at-rule inside the
indexed prototype specs/039.../prototype/new-test-from-link.html — parser false positive, needs
an owner decision); unresolved relations 421 -> 417; unresolved C4-C5 195 -> 190; naked
definitions backend/src 429/27 files -> 414/25, frontend/src 6/3 unchanged.
2026-10-20 22:56:39 +03:00
lane38
5540c15e31 fix(e2e): keep extra unchanged totals levels valid on FAIL
The pivot FAIL branch still requires a one-cent moving cell at both declared render places, but a target that also declares an unchanged level of the same kind is not refused.
2026-10-20 20:35:55 +03:00
lane38
fcc4985883 fix(e2e): rebuild pivot named-cell key from product key columns
The changed-cell identity now follows the query descriptor key_columns order, taking the pivot row-group values from the declared totals cell and the row key from the target. Table keeps the single row-key part. A mismatch records expected_key/observed_key/column while keeping NAMED_CHANGED_CELL_IDENTITY_MISMATCH. The pivot FAIL branch now requires both the column total and the row-group subtotal to move by exactly one cent.
2026-10-20 20:34:58 +03:00
97ff9d8dfb fix(e2e): canonicalize chart totals cells and record pass mismatch facts
The chart-data wire carries JSON numbers for numeric metric cells while the
product client_totals_rows owner declares canonical string cells (its own
capture path passes decode_chart_response(normalized_value)). Reproducing the
pivot totals from the raw numbers made pivot_aggregators._sum treat each number
as NaN and keep the last group member, so a correct Sum became the smallest
member and TOTALS_PASS_VALUE_MISMATCH refused a valid run.

Canonicalize every wire cell with the product normalize_scalar before
client_totals_rows, exactly like normalize_table. A mismatch now raises
TotalsPassMismatch carrying expected/actual/delta/canonical_key/level_columns,
oracle_row_count and the wire completeness facts; verify_cycle records them
under totals.pass_mismatch. The refusal code, status and exit stay unchanged.
2026-10-20 20:10:00 +03:00
8ddd8fbd4d fix(dashboard-e2e): identify the chart by its raw Superset viz type
current_dashboard compared the coarse query-model taxonomy viz_type
(unknown_taxonomy for pivot_table_v2) with the authored Superset type, so
the PREPROD pivot acceptance failed chart8-current-query-model-identity
while a table chart matched only by accident. The identity now reads the
exact raw_viz_type; a missing, blank or non-string raw type is refused
with NATIVE_CURRENT_CHART_RAW_VIZ_TYPE_UNAVAILABLE instead of substituting
the taxonomy. Offline tests cover pivot/table positives, a foreign raw
type, an absent raw type and an unmatched chart.
2026-10-20 15:18:08 +03:00