Skip to content

refactor(ios): drop the unused modal-scope input and document corpus recording - #4147

Merged
thymikee merged 5 commits into
mainfrom
refactor/modal-scope-trim-and-corpus-docs
Nov 1, 2026
Merged

thymikee merged 5 commits into
mainfrom
refactor/modal-scope-trim-and-corpus-docs

Conversation

@thymikee

@thymikee thymikee commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Summary

  • SnapshotPresentedSurface.classify drops its modalScope input. Its only caller always passed nil. The modalScopeExpected column, its fixture text, and its Swift test go too. The table rows and the TS rule stay as they are.
  • ModalSurface (follow-up to fix(ios): reject ambiguous modal-surface pairings #4128): a note that an empty honor-modal read reports rootMismatch only as a label, the duplicate-occurrence rule in the walk comment, and each parent's children counted once. No behavior change.
  • Corpus README: prerequisites, a worked expect + expectModalScope example, the closing swift test step, and a screenshot-first rule: expectations come from what the screen shows, never from the rule's output. An ambiguous duplicate may be expected as unknown(duplicatedChild).
  • pnpm print:alert-surface: prints a recording's honor-modal tree with indexes and kept-children markers, plus ModalSurface's reading labelled as the rule's answer, not ground truth. It rejects malformed or non-format-2 recordings.
  • Test-app README: a fresh-simulator recipe for the SpringBoard prompt (create/boot, build and install, open a session, raise it).

Tree printer on a real recording

$ pnpm print:alert-surface apple/snapshot-presentation/Tests/AlertSurfaceCorpus/iphone-duo-folded-springboard-open-in-app.json
ModalSurface reads (the rule's answer, not ground truth): modal
   0 Application ""  [kept 1 of 40 children]
   1   Window ""  [kept 1 of 3 children]
   2     Other ""  [kept 1 of 3 children]
   3       Alert "Open in “Agent Device Tester”?"
   ...

Verification

  • swift test --package-path apple/snapshot-presentation: 50 passed
  • geometry-policy-presented-surface.test.ts: passed
  • build:xcuitest:ios and build:xcuitest:macos: succeeded
  • pnpm check:affected --run --head 4b6ccdf7cc: passed

🤖 Generated with Claude Code

View guided diff Turn on auto-fix

…recording

SnapshotPresentedSurface.classify loses its modalScope parameter, which its
one caller always passed as nil, along with the modalScopeExpected column and
its tests. ModalSurface counts each parent's children once and notes the
rootMismatch label and the duplicate-occurrence rule.

The corpus README now lists prerequisites, a worked expect and
expectModalScope example, and pnpm print:alert-surface, which prints a
recording's honor-modal tree with indexes, types and labels. The test-app
README gains a full fresh-simulator recipe for the SpringBoard prompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agent-device-website Ready Ready Preview Nov 1, 2026 8:19pm UTC

Request Review

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.65 MB 5.65 MB +378 B
Package (unpacked) 5.65 MB 5.65 MB +378 B
Package (download) 1.72 MB 1.72 MB +86 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 41.0 ms 40.6 ms -0.5 ms
CLI --help 87.8 ms 87.9 ms +0.1 ms

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 9 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread scripts/print-alert-surface-tree.ts Resolved
Comment thread scripts/print-alert-surface-tree.ts Resolved
print:alert-surface now checks preorder like the Swift loader and pairs each
honor-modal node with one plain occurrence, the way ModalSurface walks the
pair. It marks a node whose duplicated child was kept only in part as
duplicatedChild instead of merging occurrences. A recording can now state
that reading as {"reading": "unknown", "reason": "duplicatedChild"}, and the
ModalSurface comment says what the walk does when every occurrence is
removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

The classify change at d59a304 looks right, but two open threads below (the README modal guidance and the repeated-key counts) still apply and need a fix before merge. I did not run swift test, so the Swift lane on this head is the proof that the classify change and the unchanged ModalSurface tests compile and pass. I ran the head's print script on the seven committed recordings only, and I did not run the simulator recipe or the capture script on a device.

Not blocking, and you can take or leave these: print-alert-surface-tree.ts:25 counts children by parentElement>element, so repeated elements under same-element parents share one counter and a lost child can be hidden or marked twice (the recordings repeat these keys 8 to 70 times per tree); count per parent node, or say in the header comment and README that markers are per element identity. The comment at ModalSurface.swift:69 is only true when the honor-modal read keeps some but not all occurrences, because removing all of them reports nothing. Step 5 in the corpus README says to record reading: modal, but a marked tree can also read unknown, so please add that such a screen should not be recorded until ModalSurface is fixed. The PR also carries the fresh-simulator recipe, the new print:alert-surface script and a ModalSurface refactor, so a line about that scope in the PR body would help.

The open inline threads on the README modal guidance (#4147 (comment)), the repeated-key counts (#4147 (comment)) and the overstated comment (#4147 (comment)) still apply. The thread on validating input in print-alert-surface-tree.ts:17 does not apply, so please resolve it: the file reads recordings written by the capture script, and all seven committed ones have parentIndex below index (#4147 (comment)).

Two Smoke Tests runs and Coverage were still pending when I looked, and Repo Guards & Compatibility and the other Smoke Tests run have passed. The diff touches no packages/ or src/ code, so a Coverage or Smoke failure that does not name the Swift changes or the new script is likely unrelated. There are no conflicts. Once the pending runs finish green, this is ready to merge.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 5 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread scripts/print-alert-surface-tree.ts Resolved
@apex-sentinel-staging

apex-sentinel-staging Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Apex Sentinel

Verdict: approve

Since the last review: 6 new · 0 still open · 6 no longer raised

6 finding(s): 2 minor, 4 nit. Verdict: approve. reached max rounds (1).

🟡 Minor (2)

Where Finding
scripts/print-alert-surface-tree.ts:51 🆕 print:alert-surface accepts non-integer node indexes the Swift replay rejects
scripts/print-alert-surface-tree.ts:179 🆕 Tree printer marks nodes the rule does not treat as hosts when modals nest
🔵 Nit (4)
Where Finding
scripts/print-alert-surface-tree.ts:77 🆕 frameKey collapses infinite and nonFinite rects differently than the Swift CGRect equality it ports
scripts/print-alert-surface-tree.ts:80 🆕 frameKey builds an origin array it only spreads again
scripts/print-alert-surface-tree.ts:136 🆕 surfaces() re-asserts its two child tables on every line
scripts/print-alert-surface-tree.ts:163 🆕 Pairing instability messages can name the wrong node index when the printed tree is plain

Reply under an inline comment with @apex-sentinel-staging note ... or @apex-sentinel-staging pivot ... to re-check that finding.
Comment @apex-sentinel-staging review on this conversation to run a full review.

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apex Sentinel: 6 new findings on 07dd8c8. The verdict and the full list are in the summary comment.

Comment thread scripts/print-alert-surface-tree.ts Outdated

// Pairs each honor-modal node with one plain occurrence, the way ModalSurface walks the pair. An
// element listed more than once under one parent pairs in order when every occurrence is kept.
// When only some are kept, ModalSurface reads `unknown(duplicatedChild)`, so this stops there.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor · print-alert-surface-tree.ts reports nothing for a root mismatch or an empty honor-modal read, so those recordings look like noExclusion

The marker walk starts pairing only when the two roots carry the same element (line 63), and ModalSurface.classify-style pairing returns .unknown(.rootMismatch) in exactly that case (and for an empty honor-modal read, per the note this PR adds at ModalSurface.swift:61-62). When the roots differ, pairedPlain stays empty, every node hits continue at line 66, markers stays empty and the script prints a clean tree with no marker and no message. The corpus README this PR adds then instructs the opposite reading ("If no node is marked, write {"reading": "noExclusion"}"), so the contributor records noExclusion for a screen whose expected reading is unknown/rootMismatch, and ModalSurfaceTests fails on a fresh recording with no hint why. Fail or print an explicit [rootMismatch] marker instead of printing a marker-free tree. scripts/print-alert-surface-tree.ts:63.

Suggested change
// When only some are kept, ModalSurface reads `unknown(duplicatedChild)`, so this stops there.
const markers = new Map<number, string>();
if (treeName === 'honorModal') {
const plainChildren = childrenOf(plain);
const keptChildren = childrenOf(nodes);
const pairedPlain = new Map<number, number>();
if (nodes[0].element !== plain[0].element) {
fail(`${file}: the roots differ (${plain[0].element} vs ${nodes[0].element}), so the pair reads unknown(rootMismatch).`);
}
pairedPlain.set(0, 0);

Suggested change

When nodes[0].element !== plain[0].element or the honor-modal tree is empty, fail the command (or add a root-level marker) saying the pair reads unknown(rootMismatch), so a marker-free tree only ever means noExclusion.

Details

Also raised here

Also raised at this location by swift-ios: Printer reports a tree as unmarked when the two reads have different roots, so the README's rule yields a wrong noExclusion — scripts/print-alert-surface-tree.ts:66 seeds the pairing only when nodes[0].element === plain[0].element. When the honor-modal read starts at a different element, pairedPlain stays empty, so the loop at line 67 continues on every node and the printer prints the whole tree with no markers and no warning — yet ModalSurface.read answers .unknown(.rootMismatch) for exactly that pair (ModalSurface.swift:63). The new README instructs the contributor to translate "no node is marked" into {"reading": "noExclusion"} (README.md:48), which is the opposite of what the rule reads. The author's own testAnHonorModalReadFromAnotherRootIsUnknown (ModalSurfaceTests.swift:179-182) covers that reading, so the pair is reachable in the rule the printer exists to serve. A pair whose two roots differ prints identically to a screen with no modal. Print the reason when the roots differ instead of staying silent.

persona: maintainability, swift-ios

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

Comment thread scripts/print-alert-surface-tree.ts Outdated
);
continue;
}
const removed = [...plainChildren[plainNode]];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor · Tree printer marks only partial exclusions, so the README's marker-only procedure mislabels noSurvivingSurface and root-mismatch recordings

The new printer marks a node only when it removed children AND kept at least one: if (removed.length > 0 && keptChildren[node.index].length > 0) (scripts/print-alert-surface-tree.ts:91). A node whose children were all removed is printed with no marker, and the script never reports the other readings ModalSurface.read can return — noSurvivingSurface (removal with no kept sibling), rootMismatch (the script pairs only when nodes[0].element === plain[0].element, line 66, and prints an unmarked tree otherwise), and the unstable(...) reasons.

Suggested change

Either mark the other unknown readings in the printer output (root mismatch, kept-nothing / noSurvivingSurface), or extend the README step 5 with the case where a node loses all of its children and the case where the honor-modal root differs, so a contributor does not write noExclusion for a pair ModalSurface reads as unknown.

Details

The rewritten README decides the expectation from the markers alone: "If no node is marked, write {\"reading\": \"noExclusion\"}" (apple/snapshot-presentation/Tests/AlertSurfaceCorpus/README.md, step 5). So for a recording whose honor-modal read empties a container without keeping a sibling — the shape the existing unit test testRemovedContentWithoutAKeptSiblingIsUnknown pins as .unknown(.noSurvivingSurface) — the printer prints no marker and the documented procedure tells the contributor to write noExclusion, which then fails swift test with no explanation of why. The same happens for a recording whose honor-modal read starts at a different root, which ModalSurface.read answers .unknown(.rootMismatch).

The printer already prints the reason for one unknown (duplicatedChild); it should do the same for the other readings it can infer (root mismatch, no kept sibling), or the README should say what to write when a node loses all its children instead of falling back to "no node is marked".

persona: javascript

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

// write its `expectModalScope` by hand. See apple/snapshot-presentation/Tests/AlertSurfaceCorpus/README.md.
import fs from 'node:fs';

type RecordedNode = {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Nit · New printer script is outside the typechecked program, so its RecordedNode shape drifts from the decoder silently

scripts/print-alert-surface-tree.ts is added, but tsconfig.json include lists only src, test and a few named scripts (scripts/help-conformance-command-validator.ts, scripts/clean-daemon.ts, scripts/coverage-*/**, the vitest reporters), so pnpm typecheck (tsc -b tsconfig.typecheck.json) never compiles it — it runs only through node --experimental-strip-types. Its RecordedNode type (index, parentIndex?, element, type, label) is a hand-maintained mirror of RecordedAlertSurface.Node in apple/snapshot-presentation/Tests/AgentDeviceSnapshotPresentationTests/RecordedAlertSurface.swift, and the corpus node shape already carries identifier and rect fields the TS type omits. Nothing fails today because the script only reads index, parentIndex, element, type, label, and the as Record<string, unknown> / tree as RecordedNode[] casts (lines 22, 40) assert the shape instead of checking it, so a future field rename in the capture format reaches the script as undefined with no typecheck error and no runtime validation.

Suggested change

Add scripts/print-alert-surface-tree.ts (and scripts/capture-alert-surface.ts, which has the same gap) to the tsconfig include list so the corpus-node mirror is at least typechecked.

Details

Also raised here

Also raised at this location by swift-ios: New script's typecheck coverage and duplicated preorder rule vs the corpus loader — scripts/print-alert-surface-tree.ts re-implements the corpus preorder rule that RecordedAlertSurface.isPreorder already owns (RecordedAlertSurface.swift:59-66), and unlike the neighbouring entries in tsconfig.json:28-32 (scripts/help-conformance-command-validator.ts, scripts/clean-daemon.ts, scripts/vitest-coverage-run-stamp-reporter.ts) it is not listed there, so its types are not checked by tsc — the tree.forEach((node: RecordedNode, position) => …) cast at line 30/40 is exactly the unverified assertion a checked build would look at (recording[name] is unknown[], so node.element is unchecked). Nothing fails today on the seven recordings, which all pass the printer.

persona: javascript, swift-ios

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

Comment thread scripts/print-alert-surface-tree.ts Outdated
if (!file || (treeName !== 'honorModal' && treeName !== 'plain')) {
fail('Usage: pnpm print:alert-surface <recording.json> [honorModal|plain]');
}
const recording = JSON.parse(fs.readFileSync(file, 'utf8')) as Record<string, unknown>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Nit · Printer's preorder check accepts Number.isInteger floats the corpus reader rejects, and prints an ENOENT stack for a mistyped path

Two smaller gaps on the new script. (1) preorderTree accepts a parent written as 1.0 because Number.isInteger(1.0) is true, while it then indexes children[node.parentIndex] at line 46 with that raw value; the Swift reader it claims to mirror (RecordedAlertSurface.isPreorder, RecordedAlertSurface.swift:59-66) decodes Int, so the same recording fails RecordedAlertSurface.load after the printer calls it well-formed. The comment on line 26 says "The same check as RecordedAlertSurface.load", which is then not quite true. (2) A mistyped recording path bypasses fail() and dumps a Node stack trace (Error: ENOENT: no such file or directory, open 'does-not-exist.json') instead of the script's own message, because fs.readFileSync on line 22 is not guarded. Both are contributor-facing only, on a helper script.

Suggested change
const recording = JSON.parse(fs.readFileSync(file, 'utf8')) as Record<string, unknown>;
let raw: string;
try {
raw = fs.readFileSync(file, 'utf8');
} catch (error) {
fail(`${file}: ${error instanceof Error ? error.message : String(error)}`);
}
const recording = JSON.parse(raw) as Record<string, unknown>;

Suggested change

Wrap the read in the script's own failure path, and require an integer-valued parentIndex the way the Swift loader does (e.g. Number.isInteger(parent) && parent >= 0 && parent < position && String(parent) === String(node.parentIndex) or a plain typeof parent === 'number' && Number.isInteger(parent) plus rejecting a non-array/NaN index).

Details

persona: swift-ios

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

Comment thread scripts/print-alert-surface-tree.ts Outdated
return tree as RecordedNode[];
}

function childrenOf(tree: RecordedNode[]): number[][] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Nit · print-alert-surface-tree.ts rebuilds the tree from parentIndex while claiming to pair "the way ModalSurface walks the pair"

The new printer derives both trees' children with childrenOf, which keys on node.parentIndex (lines 43-48), and pairs nodes with that structure (lines 62-97). The Swift rule it documents instead rebuilds each read's tree from parentElement: ModalSurface.children(of:) (ModalSurface.swift:122-140) assigns every node to its nearest listed ancestor whose element equals the node's parent element, and the README tells contributors to read host/roots off the markers this script prints (AlertSurfaceCorpus README step 3 and 5, "the innermost marked node of the branch"). Those two walks agree on the seven recorded corpus files (I compared both walks on all of them: 0 differing nodes), so the agreement is coincidental, not enforced: it holds only while no node's nearest matching-element ancestor differs from the node parentIndex names — e.g. a node whose parent element equals its own element, or an element listed under an ancestor chain that repeats a parent element. The corpus already contains elements listed twice under one parent, so a recapture can break the invariant. When it does, the printer marks a different node than ModalSurface reads as host: the contributor writes the wrong host/roots, ModalSurfaceTests fails, and the README's advice is "print the tree again", which prints the same misleading marker — the marker and the rule disagree with no hint which one is wrong. The script's own comment (line 58: "the way ModalSurface walks the pair") and its preorder comment (line 26: "The same check as RecordedAlertSurface.load") claim an equivalence neither type nor test enforces. Two concrete divergences in the preorder comment alone: the recording's root node carries no parentIndex key and the corpus records parentIndex: null style nodes only by absence, so parent === undefined and Swift's decoded Int? nil coincide only because the capture never writes an explicit null; and a parentIndex: null on node 0 would pass Swift's check but fail the printer's Number.isInteger(parent) as "out of preorder".

Suggested change
function childrenOf(tree: RecordedNode[]): number[][] {
function childrenOf(tree: RecordedNode[]): number[][] {
// Mirrors ModalSurface.children(of:): a node belongs to its nearest listed ancestor whose element
// equals its parent element, not to the node its parentIndex names. Keep both walks in step.
const children = tree.map((): number[] => []);
const ancestors: number[] = [];
for (const node of tree) {
const parentElement =
node.parentIndex === undefined || node.parentIndex === null
? undefined
: tree[node.parentIndex].element;
if (parentElement !== undefined) {
while (ancestors.length > 0 && tree[ancestors[ancestors.length - 1]].element !== parentElement)
ancestors.pop();
if (ancestors.length === 0) fail(`${file}: "${node.index}" names no listed parent element.`);
children[ancestors[ancestors.length - 1]].push(node.index);
} else if (node.index !== 0) {
fail(`${file}: node ${node.index} is out of preorder. Capture it again.`);
}
ancestors.push(node.index);
}
return children;
}

Suggested change

Either port the Swift nearest-ancestor-by-parentElement walk into the printer (as in the patch) or have the printer assert at startup that parentIndex-based and parentElement-based children agree, and fail with a message naming the node where they diverge. Do not claim in the comment and in the README that the printer pairs "the way ModalSurface walks the pair" while it reads a different parent field.

Details

persona: senior-developer

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

Comment thread scripts/print-alert-surface-tree.ts Outdated
const listedCount = listed.get(element) ?? 0;
return listedCount > 1 && count < listedCount;
});
if (ambiguous) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Nit · Printer's duplicatedChild marker prints the raw AX element id, which the README's example presents as an element name

The README tells the contributor to look for [duplicatedChild: kept 1 of 2 occurrences of …] (AlertSurfaceCorpus/README.md step 3). The script fills that slot with element (print-alert-surface-tree.ts:80), and in real recordings element is an opaque accessibility id — for example 99943:4829729792.1219 in iphone-duo-folded-springboard-open-in-app.json — not a type or label, so the marker the contributor is told to recognise never resembles the documented one and names no node they can see on screen. The tree lines print type and label, so the same node is identifiable there by two other fields the marker ignores.

Suggested change
if (ambiguous) {
if (ambiguous) {
const [element, count] = ambiguous;
const name = nodes[node.index].type || nodes[node.index].label || element;
markers.set(
node.index,
`[duplicatedChild: kept ${count} of ${listed.get(element)} occurrences of ${name} (${element})]`,
);
continue;
}

Suggested change

Print the node's type and label alongside the raw element id in the marker, or show the README a real marker line so the documented example matches the command output.

Details

persona: senior-developer

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

thymikee and others added 2 commits November 1, 2026 19:50
print:alert-surface now ports ModalScopePair.pair and ModalSurface.read step
for step. It marks kept children only when the rule reads a modal surface.
Otherwise it prints the rule's reason on the first line, such as
`unknown: unstable(reparentedElement) at node 3`, and marks no node. The
seven corpus recordings print unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
print:alert-surface now prints `reading: <x>` first for every recording:
modal, noExclusion, or unknown(<reason>) with the failing node. It ports
children(of:) with its ancestor walk and CGRect equality, so the reading
matches ModalSurface.read in every case. It rejects a recording the corpus
loader rejects, such as a non-integer index, and reports a missing file in one
line. The corpus README procedure now starts from the reading line, names
the raw AX element id in duplicatedChild, and asks contributors to check the
reading against the screenshot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread scripts/print-alert-surface-tree.ts Resolved
print:alert-surface rejects a recording whose format is not 2, and labels its
first line as ModalSurface's answer, not ground truth. The corpus README
now says, before the steps, that expect and expectModalScope come from the
screenshot. The printer only helps find node indexes. Step 5 derives the
surface from the prompt on the screen, then compares it with the rule's
reading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

I reviewed c7a395e. The open thread on the missing format check (item 1 below) still applies and should be fixed before merge. Most of the earlier findings are fixed: the printer now prints the reading first, validates the recording shape, counts markers per parent node, and the README and ModalSurfaceTests cover unknown(duplicatedChild).

Smoke Tests is still running and is the only check not passing. This head changes no packages/ or src/ code, and the Swift change at the daemon call site is only the classify signature, so I expect the failure, if any, to be unrelated. Merge needs Smoke Tests to finish green.

Item (1) is the blocking one; the others you can take or leave: (1) The README says the printer stops on a recording the replay would not load, and the script says it makes the loader's checks, but recordedTree() skips two of them. It accepts format 1 (I got reading: modal on an edited copy of the springboard recording), and it prints unknown(rootMismatch) for an empty honorModal tree where the loader rejects it. Please check format === 2 and reject the empty tree, or drop those two claims. (2) README step 5 says to record unknown(duplicatedChild), while the paragraph at line 69 says a screen with a prompt must read modal and a disagreement must not be recorded. No committed recording uses unknown. Please say that it is recorded only for a screen without a prompt, or drop the unknown expectation and the reason field. (3) The PR body still shows the printer example without the reading: line, cites swift test and check:affected at 07dd8c8, and does not mention the TypeScript port.

The two open inline threads on the missing format check (discussion) still stand, as covered by item (1). One thread does not apply and can be resolved: the circularity point (discussion), because README lines 69-73 now state it and require a screenshot check. These are fixed at this head and can be resolved too: reading line and empty tree, partial exclusion markers, ancestor walk, raw element id in README, ENOENT stack, duplicatedChild docs, recording validation, per-parent markers, ModalSurface comment and reparented child. The tsconfig include thread does not apply, since the sibling capture-alert-surface.ts is also left out and the script runs through node strip-types. The float-rect remark in this thread does not apply either, because capture-alert-surface writes integers.

A question on scope: does the printer need to port the rule into TypeScript? The delta replaced a ~25-line marker counter with ~150 lines that re-implement pair, read and children(of:), and each review round has pushed the port to be more faithful. A smaller option is to keep a plain tree printer (index, type, label, and per-parent "kept N of M children" counts) as a drawing aid and let the Swift replay own the reading, since the existing XCTAssertEqual messages already print the actual hosts, roots and actions to compare with the screenshot. If a pre-computed reading is wanted, it could live in the SnapshotPresentationConformance target so there is one implementation, which also removes the hand-synced frameKey, isRect and loader checks. If you keep the port, please add a drift check, because today the only one is the seven committed recordings. Nothing has to change first.

I did not run swift test, so the classify change, the new ModalSurfaceTests case and the reason decode are covered only if the Swift CI job is green. I ran the printer on the seven committed recordings and two edited copies (format 1 and an empty honorModal). I did not exercise duplicatedChild, noSurvivingSurface or unorderedRead, because no recording reaches them; I compared those branches by reading. No live device run was needed, since nothing device-facing changed.

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apex Sentinel: 6 new findings on 4b6ccdf. The verdict and the full list are in the summary comment.

if (!Array.isArray(tree) || (tree.length === 0 && name === 'plain')) {
fail(`${file}: "${name}" is not a tree`);
}
tree.forEach((node, position) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🆕 🟡 Minor · print:alert-surface accepts non-integer node indexes the Swift replay rejects

recordedTree validates node.index === position without checking that the index is an integer, while the loader it claims to mirror (RecordedAlertSurface.isPreorder) decodes index as Int, so any non-integer index makes the recording fail to load there. JSON.stringify/JSON.parse keeps a fractional literal distinct (1.5 !== 1, and 1.0 is stored as the double 1, not 1.5), so a recording whose node 1 carries "index": 1.5 passes the printer and fails RecordedAlertSurface.load with a corrupt-decode error.

Suggested change
tree.forEach((node, position) => {
tree.forEach((node, position) => {
if (node.parentIndex === null) delete node.parentIndex;
const parent = node.parentIndex;
const isValid =
Number.isInteger(node.index) &&
node.index === position &&
(parent === undefined
? position === 0
: Number.isInteger(parent) && parent >= 0 && parent < position) &&
['element', 'type', 'identifier', 'label'].every((key) => typeof node[key] === 'string') &&
isRect(node.rect);

Suggested change

Require Number.isInteger(node.index) in recordedTree, so the printer rejects exactly the recordings RecordedAlertSurface.load rejects.

Details

Result: the printer's promise in the comment on lines 44-45 ("The checks RecordedAlertSurface.load makes") and in the README ("If the recording does not load in the replay, the command stops with one line that names the problem") is false for this case. The contributor gets a printed reading and node indexes for a recording that the corpus replay can never load, and the real failure only appears later as CocoaError(.coderReadCorrupt) with no node named.

The same gap applies to parentIndex: only Number.isInteger(parent) is checked (line 58), never that node.index is an integer.

persona: senior-developer

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

if (found === undefined) return undefined;
nested += found;
}
if (removed.length === 0 || kept.length === 0) return nested;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🆕 🟡 Minor · Tree printer marks nodes the rule does not treat as hosts when modals nest

surfaces() writes a [kept N of M children] marker on every node that removed some and kept some children, including a node that has a nested surface under it (lines 179-184). Swift ModalSurface.read records a host only when the kept children hold no surface of their own (guard nested.isEmpty, !removed.isEmpty, !kept.isEmpty, ModalSurface.swift:93) — the inner surface replaces the outer one.

Suggested change

Mark a node only when it is a host in the rule's sense (no nested surface under it), matching guard nested.isEmpty, !removed.isEmpty, !kept.isEmpty, or state in the README and the printer output that outer marked nodes are not hosts and the innermost mark is the host.

Details

I ran the printer's own logic on a nested pair (window keeps a sheet and loses its content; the sheet keeps an alert and loses its sheetContent). It printed markers on BOTH the window and the sheet, while the rule answers ModalSurface(host: 3, roots: [4]) — the sheet only. The README tells the contributor that the host is "the node where the honor-modal tree removed the rest of the screen", and the PR's own worked example quietly relies on picking "the innermost marked node", but neither the README nor the printer says that several nodes can be marked, or that the outer ones are not hosts. A contributor who reads host off the first/outer marker writes an expectModalScope the replay then rejects for a recording whose tree is fine.

persona: senior-developer

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

}

// CGRect equality compares rects after it makes their sizes positive.
function frameKey({ infinite, nonFinite, x, y, width, height }: Rect): string {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🆕 🔵 Nit · frameKey collapses infinite and nonFinite rects differently than the Swift CGRect equality it ports

The printer ports ModalScopePair.pair and claims (lines 84-86) that pairInstability returns the reading the replay checks. For the frames it does not, in one case. FixtureRect decodes {"infinite":true} to CGRect.infinite (= x:-inf, y:-inf, w:inf, h:inf) and {"nonFinite":true} to the identical rect. So in Swift, a plain node flagged infinite and its honor-modal counterpart flagged nonFinite compare equal via match.frame == node.frame, and the pair stays .paired. In the TS port, frameKey returns 'infinite' for one and 'nonFinite' for the other (lines 78-79), so pairInstability prints unknown(unstable(movedElement)) where the replay reads a normal result. Reachable only when the AX server flags the same node's rect with different flags across the two reads of one screen, so the impact is a misleading label in the printed reading, not a wrong test result; the comment claiming parity should then hold only with that caveat, or the two flags should collapse to one key like Swift does.

Suggested change
function frameKey({ infinite, nonFinite, x, y, width, height }: Rect): string {
function frameKey({ infinite, nonFinite, x, y, width, height }: Rect): string {
if (infinite || nonFinite) return 'nonFinite';
const origin = [width < 0 ? x + width : x, height < 0 ? y + height : y];
return [...origin, Math.abs(width), Math.abs(height)].join();
}
Details

persona: javascript

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

function frameKey({ infinite, nonFinite, x, y, width, height }: Rect): string {
if (infinite) return 'infinite';
if (nonFinite) return 'nonFinite';
const origin = [width < 0 ? x + width : x, height < 0 ? y + height : y];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🆕 🔵 Nit · frameKey builds an origin array it only spreads again

origin holds two of the four numbers that join() then receives, so a reader has to follow the spread across two statements to see the key is origin, width, height. Join the four values in one call.

Suggested change
const origin = [width < 0 ? x + width : x, height < 0 ? y + height : y];
return [width < 0 ? x + width : x, height < 0 ? y + height : y, Math.abs(width), Math.abs(height)].join();
Details

persona: maintainability

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

const counts = new Map<string, number>();
for (const position of positions) {
counts.set(tree[position].element, (counts.get(tree[position].element) ?? 0) + 1);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🆕 🔵 Nit · surfaces() re-asserts its two child tables on every line

readModalSurface returns early when plainChildren or keptChildren is undefined, yet the nested surfaces still writes plainChildren! and keptChildren! five times, so each use has to be checked against that guard by hand. Bind them once after the guard (const plainChildren = tables.plainChildren; const keptChildren = tables.keptChildren;) and drop the assertions.

Suggested change
}
const plainChildren = plainTables ?? fail(`${file}: plain children are not readable`);
const keptChildren = keptTables ?? fail(`${file}: honor-modal children are not readable`);
Details

persona: maintainability

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

const listedCount = listed.get(element) ?? 0;
const keptCount = keptListed.get(element) ?? 0;
if (listedCount >= 2 && keptCount < listedCount) {
failure = `unknown(duplicatedChild): node ${honorModalNode} kept ${keptCount} of ${listedCount} children with element id ${element}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🆕 🔵 Nit · Pairing instability messages can name the wrong node index when the printed tree is plain

pairInstability and surfaces build their failure strings with node.index from whichever tree the failing node came from — the honor-modal tree for movedElement/unmatchedElement/reparentedElement, and the honor-modal parent for duplicatedChild (lines 103, 106, 163, 168). Line 197 then optionally prints the plain tree instead. Run as pnpm print:alert-surface <recording> plain, the first line says e.g. unknown(unstable(movedElement)): node 12 while the listed node 12 is the plain tree's node 12, an unrelated node; the plain counterpart is the removed match the walk already has at line 166. The README instructs contributors to search the recording for the reported id (which is correct for duplicatedChild), so a numeric index that points at the wrong tree of the same file is exactly the confusion the tool should avoid.

Suggested change

Either suppress the : node N suffix when treeName is 'plain', or print the node's element id (which is shared by both trees) alongside the honor-modal index.

Details

persona: javascript

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

The code looks good at 4b6ccdf. The format check and the screenshot-first rule from the earlier review are now in place, and the two open inline threads on them (#4147 (comment) and #4147 (comment)) are fixed at this head, so the author can resolve them.

Not blocking, and you can take or leave these: recordedTree() in scripts/print-alert-surface-tree.ts:48 rejects an empty tree only for plain, so an empty honor-modal tree prints unknown(rootMismatch) with exit 0 while RecordedAlertSurface.isPreorder rejects it, which means requiring both trees to be non-empty (and dropping the honorModal.length === 0 branch) would match the README claim that a bad recording stops the command; and in the README step 5, bullet 3 says to skip a recording whose reading differs from the screenshot while bullet 4 records unknown(duplicatedChild) from the reading, so stating once that duplicatedChild is the one case where the expectation follows the rule (as in ModalSurfaceTests.swift:35-37) would remove the conflict.

Of the other open threads, the P2 notes on the marker for removed children (#4147 (comment)) and the instability index in plain mode (#4147 (comment)) still apply, as do three lower-priority style or cosmetic threads. These threads no longer apply and can be resolved: #4147 (comment) (the reading now prints first), #4147 (comment) (the README says nodes are marked only for a modal reading), #4147 (comment) (the sibling script is also outside the program and runs through strip-types), #4147 (comment) (a mistyped path gives one line and node.index === position forces an integer), #4147 (comment) (childrenOf() ports the nearest-listed-ancestor walk), #4147 (comment) (the README says to search the recording for the raw element id), and #4147 (comment) (node.index === position already rejects non-integers).

Smoke Tests and Coverage are still running, so there is no result to attribute yet. This delta changes only the README and the printer script, and neither job runs that script. I did not run swift test or the repository gates. I ran the printer on the springboard recording and two edited copies (format 1 and an empty honor-modal tree), and I read the Swift loader and rule without running them. Before merge, Smoke Tests and Coverage must finish green.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Nov 1, 2026
@thymikee
thymikee merged commit 343b7fc into main Nov 1, 2026
25 checks passed
@thymikee
thymikee deleted the refactor/modal-scope-trim-and-corpus-docs branch November 1, 2026 21:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant