docs: document topology graph v0.2 contract

This commit is contained in:
赵义仑
2026-06-16 21:20:35 +08:00
parent 1ea4c52ad1
commit a7458265c0
4 changed files with 213 additions and 44 deletions

View File

@@ -7,18 +7,24 @@ The project intentionally does not include an LLM, a generic browser agent, or a
## Current Scope
- Open a URL or local HTML file with Playwright.
- Capture reproducibility metadata: browser, viewport, locale, auth mode, crawl session id, input fingerprint, and policy/scoring versions.
- Capture one page state with DOM and visible-text fingerprints.
- Extract actionable elements: buttons, links, inputs, selects, textareas, roles, tab stops, and click handlers.
- Generate locator candidates with scores.
- Optionally explore low-risk click actions from the entry state.
- Record state nodes, elements, locators, action edges, and basic effects in JSON.
- Generate locator candidates with scores, context scopes, identity checks, and verification evidence.
- Optionally run low-risk one-hop click exploration from a clean entry state.
- Replay actions from the entry state and record observed state transitions with snapshot-diff effects.
- Record state-local skipped default-exploration actions when risk policy blocks them.
- Record failed observations instead of turning failed locators or failed actions into successful edges.
- Emit state nodes, elements, locators, action edges, skipped actions, failed observations, and metadata in JSON.
## Non-Goals
- No LLM decision making.
- No natural-language task planner.
- No high-risk automatic actions.
- No attempt to fully recover frontend business functions in the first version.
- No full crawler or breadth-first site exploration.
- No general browser agent or workflow orchestration platform.
- No attempt to fully recover frontend business functions in this engine.
## Install
@@ -34,6 +40,12 @@ Static scan:
npm run scan -- --url ./examples/simple.html --out artifacts/simple-scan.json
```
Tests:
```bash
npm run test
```
Entry-state low-risk exploration:
```bash
@@ -52,12 +64,15 @@ The output JSON follows the schema described in [schemas/topology-graph.schema.m
Key objects:
- `StateNode`: URL, route, title, state hashes, modal stack, and entry metadata.
- `ActionableElement`: element semantics, visibility, bounding box, and locator candidates.
- `LocatorCandidate`: Playwright-friendly locator descriptors plus score and verification status.
- `ActionEdge`: action from one state to another through an element, with effects and risk.
- `TopologyGraph.metadata`: artifact version, input URL, entry URL, generated timestamp, engine version, mode, non-deterministic crawl session id, deterministic input fingerprint, runtime context, site, and policy/scoring versions.
- `StateNode`: URL, route, title, canonical key, language, viewport, state hashes, modal stack, lifecycle, evidence, and first observed `openedByEdgeId`.
- `ActionableElement`: state-local element semantics, visibility/interactability, bounding box, context anchors, parameter surface, evidence, and locator candidates.
- `LocatorCandidate`: Playwright-friendly locator descriptors plus score, verification status, context `scopes`, identity evidence, and failure count.
- `ActionEdge`: low-risk replayed action from one state to another through an element, with `riskCategory`, `riskLevel`, and snapshot-diff `effects`.
- `EffectRecord`: snapshot-diff record for `url`, `dom`, `visibleText`, or `actionableInventory`, with `before` and `after` values.
- `SkippedAction`: risk-policy default exploration refusal with risk category, reason, state id, element id, and evidence.
- `FailedObservation`: locator, actionability, action, or effect failure with machine-readable reason and diagnostic evidence.
## Source Notes
The originating ChatGPT share conversation is preserved in [docs/source/chatgpt-share-6a30e583.md](docs/source/chatgpt-share-6a30e583.md).

View File

@@ -1,5 +1,14 @@
# 网站可操作拓扑引擎设计文档 v0.2
## 0. 文档读法
本文同时保留两类内容:
- **v0.2.1 当前输出契约**:已经由当前代码、`README.md``schemas/topology-graph.schema.md``src/types.ts` 支持的 artifact 形态。
- **未来设计目标**:拓扑引擎后续可以演进的方向,但不代表 v0.2.1 已经发出或支持。
没有明确标注为 v0.2.1 当前输出契约的扩展能力,都应按未来设计目标理解,不能作为当前 artifact 消费合同。
## 1. 项目定位
本项目是一个确定性的 website action topology engine。它的职责不是自动完成任务也不是理解自然语言指令而是为一个网站建立可被机器消费的“可操作拓扑图”。
@@ -40,6 +49,21 @@ State 不是截图,也不是完整 DOM 快照。State 是在特定环境上下
拓扑关心的是:哪些动作可见、可定位、可执行;执行后世界如何变化。像素差异、随机节点、埋点、广告、时间戳,不应轻易制造新状态。
### 3.3 v0.2.1 当前输出契约
v0.2.1 当前 artifact 输出以下事实:
- `TopologyGraph` metadata、state、element、locator、edge、skipped action、failed observation。
- state 的 URL、route、title、language、viewport、DOM hash、visible text hash、actionable signature hash、state hash、modal stack 和 lifecycle。
- element 的语义字段、bbox、interactability、context anchors、parameter surface、locator candidates 和 evidence。
- locator candidate 的 Playwright/CSS descriptor、score、verification metadata、identity evidence、failure count 和 `scopes`
- 默认探索只从干净 entry-state context 执行低风险 one-hop click replay。
- risk policy 拒绝的默认探索动作记录为 `SkippedAction`
- locator、actionability、action 或 effect 失败记录为 `FailedObservation`
- effect 只发出 snapshot-diff 类型:`url``dom``visibleText``actionableInventory`;这些 v0.2.1 effect 会填充 `before``after`
v0.2.1 当前不发出 network、download、dialog、storage effect buffer不捕获真实 frame/shadow path不发出可执行的 row/card action template`frameContext``framePath``shadowPath` 字段存在但当前实现为空路径。locator `scopes` 只是证据和排序/消歧线索,不是可执行 scoped descriptor。
## 4. 设计目标
拓扑图面向机器消费,目标是稳定、可复验、可解释。
@@ -48,7 +72,7 @@ State 不是截图,也不是完整 DOM 快照。State 是在特定环境上下
## 5. 核心概念
`State`页面的可操作上下文。URL 只是其中一部分弹窗、菜单、抽屉、tab、筛选条件、frame、shadow DOM、可见文本、可操作项集合都会影响状态。
`State`页面的可操作上下文。URL 只是其中一部分弹窗、菜单、抽屉、tab、筛选条件、可见文本、可操作项集合都会影响状态。frame 和 shadow DOM 是未来设计目标中的状态边界v0.2.1 当前只保留空路径字段,不捕获真实 frame/shadow 层级
`Actionable Element`:某个状态下可由浏览器执行动作的对象。它不能脱离状态全局存在。同样一个“保存”按钮,在不同弹窗或表格行中是不同元素。
@@ -56,7 +80,7 @@ State 不是截图,也不是完整 DOM 快照。State 是在特定环境上下
`Action Edge`:从一个状态经过一个动作到另一个状态的边。边的价值在于记录动作和结果,而不是只记录“点击过”。
`Effect`:动作后的可观察变化,包括 URL、DOM、可见文本、网络请求、弹窗、下载、storage、history、focus、validation 等。没有明显视觉变化,也可能有真实 effect
`Effect`:动作后的可观察变化。v0.2.1 当前只发出 URL、DOM hash、visible text hash 和 actionable inventory hash 的 snapshot diff。网络请求、弹窗事件、下载、storage、history、focus、validation 等属于未来设计目标;没有对应代码前,不能作为当前输出契约
`Parameter Surface`:动作或元素可接受的参数形态。拓扑引擎不负责生成业务参数,但应记录输入槽位、约束和来源证据。
@@ -68,7 +92,7 @@ State 不是截图,也不是完整 DOM 快照。State 是在特定环境上下
拓扑 artifact 中的信息必须区分证据等级。
`Observed Fact` 是直接来自浏览器的事实,例如 URL、DOM 属性、AX role、network request、bounding box、visibility、enabled 状态。
`Observed Fact` 是直接来自浏览器的事实,例如 URL、DOM 属性、AX role、bounding box、visibility、enabled 状态。network request 观察属于未来设计目标,不是 v0.2.1 当前 artifact 字段。
`Derived Fact` 是由确定性规则推导出的信息例如状态签名、元素上下文、locator 候选评分、重复元素分组。
@@ -92,7 +116,7 @@ State 不是截图,也不是完整 DOM 快照。State 是在特定环境上下
很多动作不是简单 click而是填写、选择、上传、筛选、提交、选择表格行或对业务对象执行动作。
拓扑不需要知道“应该填什么”,但需要知道参数面:输入框的 type、name、label、placeholder、required、pattern、maxlength、validation选择控件的 options、selected value、disabled options日期控件的格式、范围和边界表单的字段、必填项、提交动作校验行为网络 body 线索。
拓扑不需要知道“应该填什么”,但需要知道参数面:输入框的 type、name、label、placeholder、required、pattern、maxlength、validation选择控件的 options、selected value、disabled options日期控件的格式、范围和边界表单的字段、必填项、提交动作校验行为网络 body 线索属于未来设计目标,不是 v0.2.1 当前输出字段
它的目标是让下游知道:这个动作需要哪些输入,哪些是必填,约束是什么,输入和后续 effect 是否有关联。
@@ -100,7 +124,7 @@ State 不是截图,也不是完整 DOM 快照。State 是在特定环境上下
状态必须从机器能复验的事实出发,而不是从页面名称出发。
同一个 URL 可以有多个状态,不同 URL 也可能落到相似状态。状态判断不能只靠路由必须结合可见内容、可操作项签名、模态层、frame/shadow 边界和关键上下文
同一个 URL 可以有多个状态,不同 URL 也可能落到相似状态。状态判断不能只靠路由。v0.2.1 当前状态签名由 URL、title、DOM hash、visible text hash 和 actionable signature hash 派生,并记录 modal stack。frame/shadow 边界和更细的业务上下文归并是未来设计目标
### 6.1 State Lifecycle
@@ -116,9 +140,9 @@ State 在拓扑中应有生命周期。
时间戳、随机 id、埋点节点、广告、非目标列表数据轻微变化、loading 到 loaded 的瞬态变化,通常不应单独构成新状态。
可操作项集合变化、modal/dialog/drawer/menu 打开或关闭、tab/stepper 改变、form schema 改变、frame 上下文改变、权限导致动作可见性改变、关键筛选条件改变、目标业务对象上下文改变,通常应构成新状态。
可操作项集合变化、modal/dialog/drawer/menu 打开或关闭、tab/stepper 改变、form schema 改变、权限导致动作可见性改变、关键筛选条件改变、目标业务对象上下文改变,通常应构成新状态。frame 上下文改变应构成新状态是未来设计目标v0.2.1 当前不捕获真实 frame context。
状态签名应优先基于 URL canonical form、可见可操作项签名、关键 heading/landmark、modal stack、selected tab、form signature、frame path、shadow boundary 和业务对象上下文,而不是完整 DOM hash。
未来状态签名应优先基于 URL canonical form、可见可操作项签名、关键 heading/landmark、modal stack、selected tab、form signature、frame path、shadow boundary 和业务对象上下文,而不是完整 DOM hash。v0.2.1 当前状态 hash 仍包含 DOM hash、visible text hash 和 actionable signature hash这不是 frame/shadow/state-signature 完整捕获能力。
## 7. 元素建模原则
@@ -138,17 +162,19 @@ locator 是否可信,不能只看它是否匹配一个节点,还要看它是
## 9. 上下文消歧
真实页面里“编辑”“查看”“删除”“保存”“确定”会大量重复。拓扑必须记录弹窗标题、表单标题、section heading、表格行文本、卡片标题、导航区域、列表项、父级 landmark
真实页面里“编辑”“查看”“删除”“保存”“确定”会大量重复。v0.2.1 当前记录 dialog、form、section、row、landmark、listitem 等 context anchors。card 专用 anchor 属于未来设计目标
机器需要的是“客户 A 这一行的编辑按钮”,不是“第 3 个编辑按钮”。
### 9.1 重复结构与动作模板
重复元素不应全部平铺为互不相关的独立元素
v0.2.1 当前不会发出可执行的 row/item/card action template也不会把 locator `scopes` 编译成 scoped execution descriptor。当前 artifact 只记录 context anchors 和 locator scopes 作为证据、排序和消歧线索
表格每行的“编辑”、订单卡片的“查看详情”、文件列表的“下载”,应尽量表达为 row/item/card action template
未来设计目标中,重复元素不应全部平铺为互不相关的独立元素
模板记录重复容器类型、候选 identity 字段、行或卡片上下文、动作元素相对位置、动作语义、风险等级和 locator 消歧方式。下游执行时,应先定位目标 row/card再在该上下文内定位动作
表格每行的“编辑”、订单卡片的“查看详情”、文件列表的“下载”,未来应尽量表达为 row/item/card action template
模板会记录重复容器类型、候选 identity 字段、行或卡片上下文、动作元素相对位置、动作语义、风险等级和 locator 消歧方式。下游执行时,应先定位目标 row/card再在该上下文内定位动作。只有在新增可执行 scoped descriptor 支持后,这一段才可作为当前执行合同。
## 10. 动作边原则
@@ -158,7 +184,7 @@ locator 是否可信,不能只看它是否匹配一个节点,还要看它是
### 10.1 Handler Trace 不是核心事实
如果运行环境允许引擎可以记录事件监听器、调用栈、source map 映射和框架组件线索。
未来如果运行环境允许引擎可以记录事件监听器、调用栈、source map 映射和框架组件线索。
但 Handler Trace 只能作为辅助证据,不应成为动作边成立的前提。动作边的核心事实来自动作前状态、目标元素、动作类型、可观察 effect、结果状态、定位复验和风险分类。
@@ -190,7 +216,7 @@ locator 是否可信,不能只看它是否匹配一个节点,还要看它是
### 12.2 拓扑事实不等于任务语义
拓扑引擎可以记录某按钮文本是“导出”,某请求路径包含 `/export`,某动作产生下载事件。
拓扑引擎未来可以记录某按钮文本是“导出”,某请求路径包含 `/export`,某动作产生下载事件。v0.2.1 当前不记录网络请求路径,也不发出下载事件 effect。
但它不应在内部上升为“完成导出销售报表任务”的 skill。
@@ -200,7 +226,9 @@ locator 是否可信,不能只看它是否匹配一个节点,还要看它是
## 13. 成功标准
好的拓扑图应该满足:状态不是靠 URL 粗暴判断元素绑定到具体状态locator 有候选和可信理由;重复元素可以通过上下文区分;动作边有可观察 effect高风险动作不被默认执行frame/shadow/modal 边界不被忽略;失败原因能被机器理解;下游系统不需要重新猜测页面结构。
好的拓扑图应该满足:状态不是靠 URL 粗暴判断元素绑定到具体状态locator 有候选和可信理由;重复元素可以通过上下文区分;动作边有可观察 effect高风险动作不被默认执行失败原因能被机器理解下游系统不需要重新猜测页面结构。
frame/shadow/modal 边界都不应被忽略是未来质量目标。v0.2.1 当前只实现 modal/context 相关记录frame/shadow 字段为空路径,不能视为已实现捕获能力。
### 13.1 质量指标

View File

@@ -4,48 +4,107 @@ This document describes the JSON shape emitted by the engine. It is a practical
## `TopologyGraph`
- `schemaVersion`: currently `0.1`.
- `schemaVersion`: currently `0.2`.
- `metadata.artifactVersion`: currently `topology-graph-v0.2`.
- `metadata.inputUrl`: raw URL or file path supplied by the caller.
- `metadata.entryUrl`: entry URL after Playwright navigation.
- `metadata.generatedAt`: ISO timestamp for this artifact generation.
- `metadata.engineVersion`: engine implementation version that emitted the artifact.
- `metadata.mode`: `scan` or `explore`.
- `metadata.crawlSessionId`: unique non-deterministic identifier for this graph generation session. It is intentionally different across runs.
- `metadata.inputSeed`: caller-supplied input seed. In v0.2 this is the raw `inputUrl`.
- `metadata.inputFingerprint`: deterministic hash of input URL, mode, and policy/scoring versions.
- `metadata.runtimeContext`: browser engine, viewport, locale, timezone, and auth context.
- `metadata.site`: base URL and entry URL.
- `metadata.riskPolicyVersion`: currently `risk-policy-v0.2`.
- `metadata.normalizationRuleVersion`: currently `normalization-v0.2`.
- `metadata.locatorScoringVersion`: currently `locator-scoring-v0.2`.
- `states`: page states.
- `elements`: actionable elements bound to a state.
- `edges`: observed actions from one state to another.
- `skippedActions`: state-local default-explorer actions that risk policy refused to execute.
- `failedObservations`: failed locator, actionability, action, or effect observations recorded as evidence.
## Evidence Levels
- `observed`: directly captured browser fact.
- `derived`: deterministic rule output from observed facts.
- `verified`: re-resolved or replay-verified claim.
- `heuristic`: conservative inference such as risk classification.
- `external`: human or downstream annotation.
## `StateNode`
- `stateId`: stable id derived from URL, DOM hash, visible text hash, and actionable signature hash.
- `url`, `route`, `title`: browser-visible state identity.
- `domHash`: hash of full `document.documentElement.outerHTML`.
- `visibleTextHash`: hash of normalized visible body text.
- `actionableSignatureHash`: hash of actionable element signatures.
- `modalStack`: visible dialogs or modal-like surfaces.
- `openedByEdgeId`: edge that opened this state, if known.
- `stateId`: stable id derived from URL, normalized state hashes, and actionable signatures.
- `lifecycle`: `observed`, `fingerprinted`, `verified`, `stale`, `deprecated`, or `conflicted`.
- `url`, `route`, `urlCanonicalKey`, `title`, `language`, `viewport`: browser-visible and normalized state identity.
- `stateHash`, `domHash`, `visibleTextHash`, `actionableSignatureHash`: deterministic state fingerprints.
- `modalStack`: visible modal context.
- `frameContext`: reserved frame context path. In v0.2.1 this is emitted as an empty array.
- `openedByEdgeId`: first edge that observed this state in the current run. It is not the only possible predecessor.
- `createdAt`: ISO timestamp for state capture.
- `evidence`: observed and derived state facts.
## `ActionableElement`
- `elementId`: stable id within the captured state.
- `elementId`: state-local element id. It encodes state identity and must not be treated as a global DOM node id.
- `stateId`: owning state.
- `tag`, `role`, `accessibleName`, `text`, `label`, `placeholder`: semantic identity.
- `attributes`: selected stable attributes such as `data-testid`, `id`, `name`, `type`, `href`, and ARIA fields.
- `bbox`: page-space bounding box.
- `attributes`: selected stable attributes such as test id, id, name, type, href, and ARIA fields.
- `bbox`: geometry.
- `framePath`, `shadowPath`: reserved containment paths. In v0.2.1 these are emitted as empty arrays.
- `interactability`: visibility, enabled/editable status, and whether the element receives events.
- `context`: nearest dialog, form, section, row, landmark, and list item anchors.
- `parameterSurface`: input/select/textarea constraints, options, required state, pattern, min/max, and maxLength.
- `locators`: scored locator candidates.
- `evidence`: observed and derived element facts.
## `LocatorCandidate`
- `descriptor`: structured Playwright locator descriptor.
- `locatorId`: locator candidate id.
- `elementId`: parent element id. This must equal the containing `ActionableElement.elementId`.
- `framework`: `playwright` or `css`.
- `strategy`: locator strategy such as role, label, placeholder, text, test id, or CSS.
- `descriptor`: structured locator descriptor.
- `expression`: human-readable locator expression.
- `score`: ranking score.
- `verified`: whether it resolved to exactly one element during capture.
- `matchCount`: observed locator match count.
- `score`, `reason`, `verified`, `matchCount`, `failureCount`, `lastVerifiedAt`: ranking and verification metadata.
- `scopes`: context-scope evidence such as row, dialog, form, or section anchors. In v0.2.1 scopes are ranking and disambiguation evidence only; they are not executable scoping unless descriptor support is added.
- `identity`: semantic identity checks for tag, role, accessible name, text, and visibility.
- `evidence`: generation and verification facts.
## `ActionEdge`
- `fromStateId`, `toStateId`: source and resulting states.
- `elementId`: action target.
- `actionType`: currently focused on `click`, with type support for future `fill`, `select`, and navigation actions.
- `preconditions`: reserved for stored open paths.
- `effects`: URL, visible text, network, download, dialog, storage, or DOM effects.
- `riskLevel`: `low`, `medium`, or `high`.
- `edgeId`: action edge id.
- `fromStateId`, `toStateId`, `elementId`, `actionType`: source state, resulting state if observed, target element, and action kind.
- `actionParams`, `preconditions`, `parameterSpecs`: action payload and future replay metadata.
- `effects`: snapshot-diff effects observed after replay.
- `riskLevel`, `riskCategory`: risk classification used by default exploration.
- `confidence`: confidence in observed edge replayability.
- `createdAt`: ISO timestamp for edge capture.
- In v0.2.1 default exploration replays each edge from a clean entry-state context.
## `EffectRecord`
- `type`: exactly one of `url`, `dom`, `visibleText`, or `actionableInventory`.
- `before`, `after`: populated for v0.2.1 snapshot-diff effects.
- `description`: human-readable effect summary.
- v0.2.1 emits snapshot-diff effects only: URL, DOM hash, visible text hash, and actionable inventory hash.
- Network, storage, download, and dialog event buffers are not emitted in v0.2.1 and are future work, not current contract.
## `SkippedAction`
- `skippedActionId`, `stateId`, `elementId`, `actionType`: state-local skipped action identity.
- `riskLevel`, `riskCategory`: risk-policy classification.
- `reason`: policy decision string such as `blocked:destructive`.
- `evidence`: risk-policy evidence explaining why the action was skipped.
- `createdAt`: ISO timestamp for the skip record.
- `SkippedAction` is only for risk-policy default-explorer refusals. Hidden, disabled, obscured, duplicate-locator, locator-not-found, action, and effect failures are `FailedObservation`.
## `FailedObservation`
- `failedObservationId`, `stateId`, optional `elementId`, optional `actionType`: failed observation identity.
- `reason`: machine-readable failure code.
- `message`: diagnostic message.
- `evidence`: observed facts explaining the failure.
- `createdAt`: ISO timestamp for the failure record.
- `FailedObservation` covers hidden, disabled, obscured, locator, action, and effect failures. Risk-policy default-explorer refusals belong in `SkippedAction`.

View File

@@ -10,6 +10,7 @@ import { shortHash } from "../src/hash.js";
const fixturePath = path.resolve("tests/fixtures/context-risk.html");
const fixtureFileUrl = pathToFileURL(fixturePath).href;
const examplePath = path.resolve("examples/simple.html");
const snapshotDiffEffectTypes = ["actionableInventory", "dom", "url", "visibleText"];
async function withTempOutput<T>(name: string, callback: (out: string) => Promise<T>): Promise<T> {
const dir = await mkdtemp(path.join(tmpdir(), "action-topology-engine-test-"));
@@ -238,6 +239,72 @@ test("explore records edge effects and opened-by state relationships", async ()
assert.equal(openSettingsEdge.effects.some((effect) => effect.description === "Actionable inventory changed after action" && effect.type === "dom"), false);
});
test("explore artifact preserves referential integrity and snapshot-diff effect records", async () => {
const graph = await withTempOutput("explore-integrity", (out) => exploreUrl({ url: fixturePath, maxActions: 12, out }));
const stateIds = new Set(graph.states.map((state) => state.stateId));
const elementIds = new Set(graph.elements.map((element) => element.elementId));
const edgeIds = new Set(graph.edges.map((edge) => edge.edgeId));
const locatorIds = new Set<string>();
const skippedActionIds = new Set(graph.skippedActions.map((skip) => skip.skippedActionId));
const failedObservationIds = new Set(graph.failedObservations.map((failure) => failure.failedObservationId));
const elementById = new Map(graph.elements.map((element) => [element.elementId, element]));
const observedEffectTypes = new Set<string>();
assert.equal(stateIds.size, graph.states.length);
assert.equal(elementIds.size, graph.elements.length);
assert.equal(edgeIds.size, graph.edges.length);
assert.equal(skippedActionIds.size, graph.skippedActions.length);
assert.equal(failedObservationIds.size, graph.failedObservations.length);
for (const element of graph.elements) {
assert.equal(stateIds.has(element.stateId), true);
for (const locator of element.locators) {
assert.equal(locator.elementId, element.elementId);
assert.equal(locatorIds.has(locator.locatorId), false);
locatorIds.add(locator.locatorId);
}
}
for (const edge of graph.edges) {
const targetElement = elementById.get(edge.elementId);
assert.equal(stateIds.has(edge.fromStateId), true);
if (edge.toStateId !== null) assert.equal(stateIds.has(edge.toStateId), true);
assert.ok(targetElement);
assert.equal(targetElement.stateId, edge.fromStateId);
for (const effect of edge.effects) {
assert.equal(snapshotDiffEffectTypes.includes(effect.type), true);
assert.equal(typeof effect.before, "string");
assert.equal(typeof effect.after, "string");
observedEffectTypes.add(effect.type);
}
}
assert.deepEqual([...observedEffectTypes].sort(), snapshotDiffEffectTypes);
for (const state of graph.states) {
if (state.openedByEdgeId === null) continue;
const openingEdge = graph.edges.find((edge) => edge.edgeId === state.openedByEdgeId);
assert.ok(openingEdge);
assert.equal(edgeIds.has(state.openedByEdgeId), true);
assert.equal(openingEdge.toStateId, state.stateId);
}
for (const skip of graph.skippedActions) {
const element = graph.elements.find((item) => item.elementId === skip.elementId);
assert.equal(stateIds.has(skip.stateId), true);
assert.ok(element);
assert.equal(skip.stateId, element.stateId);
}
for (const failure of graph.failedObservations) {
assert.equal(stateIds.has(failure.stateId), true);
if (failure.elementId === null) continue;
const element = graph.elements.find((item) => item.elementId === failure.elementId);
assert.ok(element);
assert.equal(failure.stateId, element.stateId);
}
});
test("explore replays each v0.2.1 edge from the entry state", async () => {
const graph = await withTempOutput("explore-entry-replay", (out) => exploreUrl({ url: fixturePath, maxActions: 8, out }));
const entryState = graph.states[0];