From a7458265c0d1e769c7fd67a593acdb039947b937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E4=B9=89=E4=BB=91?= Date: Tue, 16 Jun 2026 21:20:35 +0800 Subject: [PATCH] docs: document topology graph v0.2 contract --- README.md | 33 +++++-- docs/design/action-topology-engine-v0.2.md | 56 +++++++++--- schemas/topology-graph.schema.md | 101 ++++++++++++++++----- tests/integration.test.ts | 67 ++++++++++++++ 4 files changed, 213 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 7a3a6ac..e173573 100644 --- a/README.md +++ b/README.md @@ -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). - diff --git a/docs/design/action-topology-engine-v0.2.md b/docs/design/action-topology-engine-v0.2.md index 4bf8cb5..7cab3d1 100644 --- a/docs/design/action-topology-engine-v0.2.md +++ b/docs/design/action-topology-engine-v0.2.md @@ -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 质量指标 diff --git a/schemas/topology-graph.schema.md b/schemas/topology-graph.schema.md index d510261..af21108 100644 --- a/schemas/topology-graph.schema.md +++ b/schemas/topology-graph.schema.md @@ -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`. diff --git a/tests/integration.test.ts b/tests/integration.test.ts index 1b470f5..b93c1e0 100644 --- a/tests/integration.test.ts +++ b/tests/integration.test.ts @@ -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(name: string, callback: (out: string) => Promise): Promise { 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(); + 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(); + + 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];