Skip to content
Closed
273 changes: 270 additions & 3 deletions .github/scripts/clean_wheel_agent_acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import argparse
import hashlib
import json
import os
import subprocess
Expand Down Expand Up @@ -197,6 +198,190 @@ def _dataset_query(
return scope


EVENT_FIELDS = (
"id",
"action",
"address",
"size",
"stream",
"allocated",
"active",
"reserved",
"callstack",
)
EVENT_SOURCE_FIELDS = (
"source_stack_id",
"stack_id",
"stack_kind",
"text_kind",
"stack_event_id",
"frames",
"frames_status",
)
FRAME_FIELDS = ("filename", "line", "name")


def _standalone_rows(
query: dict[str, Any], standalone: Path, device: int, template: str
) -> list[dict[str, Any]]:
label = f"Standalone {template} reference"
_require(_path_value(query, "db_path", label) == standalone, f"{label} changed target")
_require(query.get("focus_source") == "explicit", f"{label} did not use explicit target")
_require(
type(query.get("device_id")) is int and query["device_id"] == device,
f"{label} changed device",
)
_require(query.get("template") == template, f"{label} changed template")
rows = query.get("rows")
_require(
isinstance(rows, list) and all(isinstance(row, dict) for row in rows),
f"{label} has invalid rows",
)
_require(
type(query.get("returned")) is int
and query["returned"] == len(rows)
and type(query.get("total")) is int
and query["total"] == len(rows)
and query.get("total_is_exact") is True
and query.get("has_more") is False
and query.get("truncated") is False,
f"{label} is incomplete",
)
return rows


def _event_parity(
crossing: dict[str, Any],
reference: dict[str, Any],
frame_references: dict[int, dict[str, Any]],
*,
standalone: Path,
fingerprint: str,
device: int,
event_ids: list[int],
) -> int:
"""Check base events and ordered public frame fields, not local stack IDs.

Standalone event_frames exposes filename/line/name, not arbitrary raw extras.
The full raw-array digest below attests internal identity consistency only;
independent extra-field preservation remains covered by the core tests.
"""
expected = _standalone_rows(reference, standalone, device, "event")
rows = crossing.get("rows")
_require(
crossing.get("template") == "event"
and isinstance(rows, list)
and len(rows) == len(expected) == len(event_ids)
and set(frame_references) == set(event_ids),
"Cross-shard event/reference counts or template differ",
)
_require(
type(crossing.get("returned")) is int
and crossing["returned"] == len(rows)
and type(crossing.get("total")) is int
and crossing["total"] == len(rows)
and crossing.get("total_is_exact") is True
and crossing.get("has_more") is False
and crossing.get("truncated") is False,
"Cross-shard event rows are incomplete",
)
coverage = crossing.get("scope", {}).get("source_coverage", {})
_require(isinstance(coverage, dict), "Cross-shard source coverage is invalid")
for field, value in {
"range_complete": True,
"event_position": "after",
"coverage_scope": "returned_events",
"events_resolved": len(event_ids),
"ordered_frame_events": len(event_ids),
}.items():
_require(
type(coverage.get(field)) is type(value) and coverage[field] == value,
f"Cross-shard source coverage differs: {field}",
)
_require(
type(coverage.get("source_queries")) is int and coverage["source_queries"] > 0,
"Cross-shard source coverage differs: source_queries",
)
frame_count = 0
for row, original, event_id in zip(rows, expected, event_ids, strict=True):
label = f"Event {event_id}"
_require(set(original) == set(EVENT_FIELDS), f"{label} standalone base fields differ")
_require(
isinstance(row, dict) and set(row) == set(EVENT_FIELDS + EVENT_SOURCE_FIELDS),
f"{label} dataset event fields differ",
)
_require(
type(original["id"]) is int and original["id"] == event_id,
f"{label} standalone event order differs",
)
for field in EVENT_FIELDS:
_require(
type(row[field]) is type(original[field]) and row[field] == original[field],
f"{label} base field differs: {field}",
)
frames = row["frames"]
_require(
isinstance(frames, list)
and all(isinstance(frame, dict) for frame in frames)
and row["frames_status"] == "ordered"
and row["stack_kind"] == "captured",
f"{label} lost ordered frame evidence",
)
_require(
row["text_kind"] == ("captured" if original["callstack"] else "missing")
and type(row["stack_event_id"]) is int
and row["stack_event_id"] == event_id,
f"{label} source attribution differs",
)
try:
canonical = json.dumps(
frames, sort_keys=True, ensure_ascii=True, separators=(",", ":"), allow_nan=False
)
except (TypeError, ValueError, RecursionError, OverflowError) as exc:
raise AcceptanceError(f"{label} has invalid raw frame JSON") from exc
digest = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
identity = f"dataset:{fingerprint}:device:{device}:frames:sha256:{digest}"
_require(
row["source_stack_id"] == row["stack_id"] == identity,
f"{label} ordered frame identity/namespace differs",
)
frame_rows = _standalone_rows(
frame_references[event_id], standalone, device, "event_frames"
)
_require(len(frames) == len(frame_rows), f"{label} ordered frame count differs")
local_stack_id = None
for position, (frame, original_frame) in enumerate(zip(frames, frame_rows, strict=True)):
_require(
set(original_frame)
== set(FRAME_FIELDS + ("event_id", "stack_id", "position", "frame_id")),
f"{label} standalone frame fields differ",
)
_require(
type(original_frame["event_id"]) is int
and original_frame["event_id"] == event_id
and type(original_frame["position"]) is int
and original_frame["position"] == position,
f"{label} standalone frame event/order differs at position {position}",
)
_require(
all(
type(original_frame[key]) is int and original_frame[key] >= 0
for key in ("stack_id", "frame_id")
)
and local_stack_id in (None, original_frame["stack_id"]),
f"{label} standalone frame local identity differs at position {position}",
)
local_stack_id = original_frame["stack_id"]
for field, field_type in (("filename", str), ("line", int), ("name", str)):
_require(
type(frame.get(field)) is type(original_frame[field]) is field_type
and frame[field] == original_frame[field],
f"{label} ordered frame field differs at position {position}: {field}",
)
frame_count += len(frames)
return frame_count


def _sharded_acceptance(
pt_snap: Path,
fixture: Path,
Expand Down Expand Up @@ -358,10 +543,89 @@ def cli(label: str, *args: object) -> dict[str, Any]:
scope.get("slice_indices") == [slices[0]["index"], slices[1]["index"]],
"Event range did not cross shards",
)
_require(
crossing["rows"] == reference["rows"] and len(crossing["rows"]) == 2,
"Cross-shard real events differ from standalone",

def check_events(actual, original, event_ids, *, name=format_name, generation=fingerprint):
frame_references = {
event_id: cli(
f"{name} standalone frames {event_id}",
"query",
standalone,
"--device",
device,
"--template-use",
"event_frames",
"--params",
json.dumps({"event_id": event_id, "frame_order": "original"}),
"--exact-total",
)
for event_id in event_ids
}
return _event_parity(
actual,
original,
frame_references,
standalone=standalone,
fingerprint=generation,
device=device,
event_ids=event_ids,
)

frame_count = check_events(
crossing, reference, [slices[0]["last_event_id"], slices[1]["first_event_id"]]
)
if frame_count == 0:
# A shard boundary can legitimately fall between two empty stacks.
# Select one real captured event, then prove its frames independently;
# formatted text is only a locator, never reconstructed frame evidence.
candidate = cli(
f"{format_name} captured event locator",
"query",
standalone,
"--device",
device,
"--template-use",
"event",
"--params",
json.dumps({"min_id": 0, "order_by": "callstack", "order_dir": "DESC", "limit": 1}),
)
candidates = candidate.get("rows")
_require(
_path_value(candidate, "db_path", "captured event locator") == standalone
and candidate.get("focus_source") == "explicit"
and type(candidate.get("device_id")) is int
and candidate["device_id"] == device
and candidate.get("template") == "event"
and type(candidate.get("returned")) is int
and candidate["returned"] == 1
and isinstance(candidates, list)
and len(candidates) == 1
and isinstance(candidates[0], dict),
"Invalid captured event locator",
)
event_id = candidates[0].get("id")
_require(
type(event_id) is int
and slices[0]["first_event_id"] <= event_id <= slices[-1]["last_event_id"]
and isinstance(candidates[0].get("callstack"), str)
and bool(candidates[0]["callstack"]),
"Fixture has no real captured event for nonempty frame acceptance",
)
point_args = (
"--device",
device,
"--template-use",
"event",
"--params",
json.dumps({"id": event_id}),
"--exact-total",
)
captured = cli(f"{format_name} captured event", "query", dataset, *point_args)
original = cli(
f"{format_name} standalone captured event", "query", standalone, *point_args
)
_dataset_query(captured, dataset, dataset, fingerprint, device)
frame_count = check_events(captured, original, [event_id])
_require(frame_count > 0, "Ordered frame acceptance has no nonempty frame evidence")
_require(
crossing.get("total_is_exact") is True and crossing.get("total") == 2,
"Cross-shard exact total is invalid",
Expand All @@ -380,6 +644,9 @@ def cli(label: str, *args: object) -> dict[str, Any]:
"reused": True,
"standalone_peak_parity": True,
"cross_shard_event_parity": True,
"ordered_frame_field_parity": True,
"ordered_frame_identity_checked": True,
"nonempty_ordered_frames_checked": True,
"focus_unchanged": True,
}
)
Expand Down
11 changes: 5 additions & 6 deletions .github/workflows/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,11 @@ The required installed-wheel case creates a clean venv without system packages
and runs its actual `pt-snap` console script from isolated HOME/CWD. Its trusted,
test-owned input has exactly 1,000,000 events; the default exporter produces two
full 500,000-event shards. All installed catalog entries are classified: currently
11 dataset-supported templates succeed with installed CLI/API equivalence and
independent known-result checks, while `event_frames` and
`active_memory_frame_tree_at_event` return explicit unsupported-dataset errors.
Both report commands run: `peak-memory` succeeds and `memory-tree` preserves the
corresponding dataset rejection. The allocation/free-request/free-completion
boundary assertions remain covered, alongside static occupancy, offset pages,
12 dataset-supported templates succeed with installed CLI/API equivalence and
independent known-result checks; `event_frames` remains the explicit
unsupported-dataset query. Both `peak-memory` and `memory-tree` reports succeed.
The allocation/free-request/free-completion boundary assertions remain covered,
alongside static occupancy, offset pages,
terminal survivors and unchanged source/manifest/member hashes. A second required
nightly case separately imports 500,002 events and proves the original final
short shard has exactly two real events, with the crossing completed lifetime
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ byte units, allocated/active/reserved counters, expected output and partial repo
- [Replayable pickle splitting](docs/en/splitting.md): one split strategy and a new output directory; separate from dataset import.
- [Complete-dataset addressing](docs/en/sharded-snapshotdb.md#complete-dataset-focus-and-addressing-p1), [global built-in support](docs/en/querying.md#dataset-global-built-in-support-p3), and [recorded interoperability acceptance](docs/en/interop-acceptance.md): advanced contracts and evidence, not prerequisites for a first report.

Native reuse retains content verification and disposable validation receipts.
Compatible reuse checks source identity, complete member inventory, required
schema and event endpoints, not database bytes or arbitrary row values; queries
still perform independent admission. Parser-v2 caches require a new output
directory for parser-v3 export, never replacement. See the
[cache and preservation contract](docs/en/sharded-snapshotdb.md).

## Built-in Query Templates & Reports

Use `pt-snap query --list` or `pt-snap capabilities --json` to inspect all 13 built-in templates and their parameter/output schemas:
Expand Down Expand Up @@ -168,8 +175,8 @@ Peak reports expose attribution completeness and same-event active-byte coverage
group percentages use included bytes after filtering and ranking. See the
[report guide](docs/en/querying.md#report-command) before interpreting a capped breakdown.

Standalone imports preserve ordered structured callstack frames. Use `event_frames` to
inspect them, or `report memory-tree --event-id 100 --format html > memory-tree.html`
Standalone imports and new native/compatible datasets preserve ordered frames.
Use standalone `event_frames` (dataset `event` exposes `frames`), or `report memory-tree --event-id 100 --format html > memory-tree.html`
to explore event-time occupancy by caller path. See
[frame memory breakdown](docs/en/querying.md#frame-memory-breakdown); older
text-only databases require re-importing the original snapshot.
Expand All @@ -182,6 +189,9 @@ Defaults remain 100000 work rows / 64 MiB work bytes / 512 MiB scratch pages; se
[public dataset limits](docs/en/querying.md#public-dataset-limits) for precedence,
deep-pagination costs and all-or-error semantics.

Concurrent frame-tree queries on one Python analyzer use the latest request;
superseded queries raise `QueryCancelledError`. See [API cancellation semantics](docs/en/snapshot-analyzer-api.md#errors-and-scope).

## Agent Skills

Install bundled diagnostic workflows with `pt-snap skill install`. Agent integration uses skills plus the CLI; there is no MCP server. See the [Agent Skills guide](docs/en/skills.md).
Expand Down
11 changes: 10 additions & 1 deletion README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ allocated/active/reserved 指标、预期输出和报告不完整的情况。
- [可回放 pickle 拆分](docs/zh/splitting.md):只选一种拆分策略并使用新目录,与数据集导入不同。
- [完整数据集寻址](docs/zh/sharded-snapshotdb.md#完整数据集-focus-与寻址p1)、[全局内建支持](docs/zh/querying.md#数据集全局内建支持p3)和[已记录的互操作验收](docs/zh/interop-acceptance.md):按需查看高级合同与证据,不是第一份报告的前置条件。

原生复用保留内容校验与可丢弃的校验记录。兼容复用检查源身份、完整成员清单、必需
schema 和事件端点,不验证数据库字节或任意行值;查询仍独立进行准入校验。
parser-v2 缓存须换新输出目录导出 parser-v3,不会被替换。详见
[缓存及保留合同](docs/zh/sharded-snapshotdb.md)。

## 内置查询模板与报告

可执行 `pt-snap query --list` 或 `pt-snap capabilities --json` 查看全部 13 个内置查询模板及其参数与输出契约:
Expand Down Expand Up @@ -165,7 +170,8 @@ allocated/active/reserved 指标、预期输出和报告不完整的情况。
included bytes 为分母。解读有上限的分组结果前,请参阅
[报告指南](docs/zh/querying.md#report-命令)。

单库导入时保留结构化调用栈帧及原始顺序。用 `event_frames` 查看帧,或执行
单库及新导入的原生/兼容分片数据集均保留去重有序帧。单库用 `event_frames`、
数据集用 `event` 的 `frames` 查看帧,或执行
`report memory-tree --event-id 100 --format html > memory-tree.html`
按调用路径查看事件时刻的占用。详见[调用栈帧内存拆解](docs/zh/querying.md#调用栈帧内存拆解);
旧文本数据库需要从原始快照重新导入。
Expand All @@ -177,6 +183,9 @@ included bytes 为分母。解读有上限的分组结果前,请参阅
默认仍为 100000 工作行 / 64 MiB 工作字节 / 512 MiB scratch 页;优先级、
深分页成本和整次失败语义见[公开数据集预算](docs/zh/querying.md#公开数据集预算)。

同一 Python analyzer 的并发帧树查询以最新请求为准,旧请求抛出 `QueryCancelledError`。
详见 [API 取消语义](docs/zh/snapshot-analyzer-api.md#错误与范围)。

## Agent Skills

使用 `pt-snap skill install` 安装随包诊断工作流。Agent 集成入口是 skills 与 CLI,不再提供 MCP 服务器。详见 [Agent Skills 指南](docs/zh/skills.md)。
Expand Down
Loading
Loading