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/.
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/.
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/.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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).
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.
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.
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.
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.
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.
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.