10 KiB
Capability Spec Delta: form-detection (form-detection)
Package: @suaveplan/form-detection
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time. No exceptions. (Seeopenspec/AGENTS.mdSection 13.)
ADDED Requirements
Requirement: DOM Traversal and Form Candidate Discovery
The package SHALL traverse the DOM to discover candidate form containers. A candidate is any <form> element or a loose grouping of related input fields that lack an explicit <form> ancestor (e.g., a <div> containing username and password inputs). The traversal SHALL respect the same-origin iframe boundary and SHALL descend into open shadow roots. The traversal SHALL stop at <template> elements and document fragments that are not attached to the active document.
Scenario: Login form inside <form> is discovered
- WHEN the document contains a
<form>with username and password inputs - THEN
detectFormsreturns aFormCandidatewithtype: 'login'and both fields listed
Scenario: Registration form without <form> ancestor is discovered
- WHEN the document contains a
<div>with email, password, and confirm-password inputs but no wrapping<form> - THEN
detectFormsreturns aFormCandidatewithtype: 'registration'and all three fields listed
Scenario: Same-origin iframe forms are included
- WHEN the document contains a same-origin
<iframe>with a login form - THEN
detectFormscalled on the top-level document also returns the iframe's form candidate
Scenario: Cross-origin iframe forms are excluded
- WHEN the document contains a cross-origin
<iframe>with a login form - THEN
detectFormsdoes not return the iframe's form candidate
Scenario: Open shadow root inputs are discovered
- WHEN a custom element hosts an open shadow root containing a password input
- THEN
detectFormsdescends into the shadow root and discovers the input
Verified by: src/detect/detect.test.ts::"discovers login form inside explicit form element" AND src/detect/detect.test.ts::"discovers registration fields without form ancestor" AND src/detect/detect.test.ts::"includes same-origin iframe forms" AND src/detect/detect.test.ts::"excludes cross-origin iframe forms" AND src/detect/detect.spec-conformance.test.ts::"descends into open shadow roots"
Requirement: Field Classification and Label Extraction
The package SHALL classify each discovered field into username, password, new-password, email, otp, credit-card, or unknown. Classification SHALL use a weighted combination of HTML attributes (type, name, id, autocomplete), label keywords, and surrounding text context. The package SHALL also extract the human-readable label for each field from, in order of precedence: associated <label for>, aria-label, aria-labelledby, placeholder, and surrounding text heuristics.
Scenario: Password input classified by type and autocomplete
- WHEN an
<input type="password" autocomplete="current-password">is encountered - THEN
classifyFieldreturnspasswordwith high confidence
Scenario: Username input classified by name heuristic
- WHEN an
<input type="text" name="user">is encountered with no autocomplete attribute - THEN
classifyFieldreturnsusernamebased on name-keyword matching
Scenario: OTP input classified by pattern
- WHEN an
<input inputmode="numeric" autocomplete="one-time-code" maxlength="6">is encountered - THEN
classifyFieldreturnsotp
Scenario: Label extracted from explicit label element
- WHEN a field has
<label for="field-id">Email address</label> - THEN
extractLabelreturns"Email address"with sourcelabel-element
Scenario: Label falls back to placeholder
- WHEN a field has no
<label>,aria-label, oraria-labelledby, but hasplaceholder="Enter your name" - THEN
extractLabelreturns"Enter your name"with sourceplaceholder
Scenario: Ambiguous field returns unknown
- WHEN an
<input type="text">has no distinguishing attributes, labels, or context - THEN
classifyFieldreturnsunknownandextractLabelreturnsnull
Verified by: src/classify/classify.test.ts::"classifies password input by type and autocomplete" AND src/classify/classify.test.ts::"classifies username input by name heuristic" AND src/classify/classify.test.ts::"classifies OTP input by pattern" AND src/label/label.test.ts::"extracts label from explicit label element" AND src/label/label.test.ts::"falls back to placeholder when no label exists" AND src/classify/classify.test.ts::"returns unknown for ambiguous field"
Requirement: Confidence Scoring
The package SHALL assign every FormCandidate a confidence score in the range [0.0, 1.0]. The score SHALL be a weighted aggregation of structural signals: field-type diversity, presence of expected field combinations, label-keyword matches, and attribute specificity. A candidate with all expected fields for its type and strong label matches SHALL score above 0.8. A candidate with only one weak signal SHALL score below 0.3.
Scenario: Perfect login form scores above 0.8
- WHEN a form contains a username field, a password field, and a submit button with clear labels
- THEN the candidate's
confidenceis greater than or equal to 0.8
Scenario: Weak candidate scores below 0.3
- WHEN a lone password field exists with no username field and no supporting labels
- THEN the candidate's
confidenceis less than or equal to 0.3
Scenario: Score is monotonic with added signals
- WHEN a second confirming signal (e.g., matching label keyword) is added to a candidate
- THEN the new confidence score is greater than or equal to the previous score
Scenario: Registration form with confirm password scores higher than without
- WHEN a registration form contains email, password, and confirm-password fields versus email and password only
- THEN the three-field candidate scores higher than the two-field candidate
Verified by: src/score/score.test.ts::"perfect login form scores at least 0.8" AND src/score/score.test.ts::"weak candidate scores at most 0.3" AND src/score/score.spec-conformance.test.ts::"confidence is monotonic with added signals" AND src/score/score.test.ts::"registration with confirm password scores higher than without"
Requirement: Mutation Observer Integration
The package SHALL expose FormObserver, a MutationObserver wrapper that re-evaluates form candidates when the DOM changes. The observer SHALL debounce re-evaluation by a configurable interval (default 150 ms) to batch rapid mutations. It SHALL emit onFormsDetected with the full set of current candidates after each debounced pass. Calling disconnect() SHALL stop observation and release the underlying MutationObserver.
Scenario: Observer detects dynamically injected login form
- WHEN a login form is injected into the DOM after
FormObserver.start() - THEN
onFormsDetectedeventually emits a candidate array containing the new login form
Scenario: Observer debounces rapid mutations
- WHEN ten DOM mutations occur within 50 ms
- THEN
detectFormsis called at most twice within that window due to debouncing
Scenario: Observer stops after disconnect
- WHEN
observer.disconnect()is called and then a new form is injected - THEN
onFormsDetecteddoes not emit again
Scenario: Observer re-evaluates removed forms
- WHEN a previously detected form is removed from the DOM
- THEN the next
onFormsDetectedemission no longer includes that candidate
Verified by: src/observer/observer.test.ts::"detects dynamically injected login form" AND src/observer/observer.test.ts::"debounces rapid mutations" AND src/observer/observer.test.ts::"stops emission after disconnect" AND src/observer/observer.test.ts::"removes candidate when form is removed from DOM"
Requirement: Tree-Shakeable Public API
The package SHALL be fully tree-shakeable. Importing a single function (e.g., detectForms) SHALL not pull in FormObserver or label-extraction utilities unless they are also imported. The package SHALL ship ES modules only and SHALL declare sideEffects: false in package.json. The bundled core (without observer) SHALL remain under 10 KB.
Scenario: Importing detectForms alone does not include observer
- WHEN a consumer imports only
detectFormsand bundles with Rollup/Vite - THEN the output bundle does not contain
FormObserverorMutationObserverreferences
Scenario: Bundle size under 10 KB
- WHEN the core entry point (
detectForms,classifyField,scoreConfidence) is bundled and minified - THEN the resulting gzip size is less than 10 KB
Verified by: src/spec-conformance.test.ts::"tree-shaking excludes unused modules" AND src/spec-conformance.test.ts::"core bundle gzip size is under 10KB"
Requirement: Performance Budget
The package SHALL complete a full detectForms pass on a document with 1,000 DOM nodes in under 16 ms on a modern laptop. The FormObserver debounce timer SHALL use an injectable Clock interface so tests can run without real timers. No synchronous operation within the package SHALL trigger forced synchronous layout (no offsetHeight, getComputedStyle, or similar reads during the detection pass).
Scenario: Detection completes within 16 ms
- WHEN
detectFormsis called on a synthetic document with 1,000 nodes - THEN the elapsed wall time is less than 16 ms
Scenario: No forced synchronous layout
- WHEN
detectFormsruns on a document with nested flex containers - THEN no
offsetHeight,offsetWidth,getComputedStyle, orgetBoundingClientRectcalls are made
Scenario: Observer uses injectable clock
- WHEN
FormObserveris instantiated with a customClockthat advances time programmatically - THEN debounced re-evaluation fires exactly when the clock reaches the debounce threshold
Verified by: src/detect/detect.spec-conformance.test.ts::"detects 1000 nodes in under 16ms" AND src/detect/detect.spec-conformance.test.ts::"does not trigger forced synchronous layout" AND src/observer/observer.test.ts::"debounce respects injectable clock"