Measure keyboard continuation and navigation feedback

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0cffe-206f-7323-89d3-b20ca4cf4bf9
This commit is contained in:
tegwick 2026-09-27 00:22:14 +02:00
parent 2b1c9a4109
commit 26d7c4523b
17 changed files with 1773 additions and 13 deletions

View file

@ -351,6 +351,8 @@ research/evolution/EvolutionReport.md
research/evolution/README.md
research/evolution/context.json
research/evolution/cycles/2026-09-27-navigation-effort.json
research/evolution/cycles/2026-09-27-navigation-focus-effort.json
research/evolution/cycles/2026-09-27-navigation-focus.md
research/evolution/cycles/2026-09-27-navigation.md
research/evolution/ledger.json
research/evolution/profiles.json
@ -400,6 +402,9 @@ research/experiments/matrix-recovery/provenance.json
research/experiments/matrix-recovery/result.json
research/experiments/navigation-history/README.md
research/experiments/navigation-history/experiment.mjs
research/experiments/navigation-history/focus-policy.js
research/experiments/navigation-history/focus-result.json
research/experiments/navigation-history/focus.mjs
research/experiments/navigation-history/initial-partial.json
research/experiments/navigation-history/package-lock.json
research/experiments/navigation-history/package.json
@ -423,6 +428,7 @@ research/reviews/LocalFirstAdmissionScreen.json
research/reviews/LocalFirstEvidenceExpansion.md
research/reviews/MatrixRecoveryExperiment.md
research/reviews/NavigationCoverage.md
research/reviews/NavigationFocusExperiment.md
research/reviews/NavigationHistoryExperiment.md
research/reviews/NavigationRenderingEvidence.md
research/reviews/NestedDismissalIsolation.md

View file

@ -74,3 +74,14 @@ requires application policy. This supports separating input ownership from focus
restoration, not a general production-ready fix: animated exits, competing owners,
rapid input bursts and assistive technology remain untested. Sixteen hypotheses;
no new pattern or maturity promotion.
## Boundary with route continuation — 2026-09-27
[Navigation focus contrast](../reviews/NavigationFocusExperiment.md) NF02–NF06
provides a non-modal counter-context: a successful route may continue at a new heading,
while failed navigation keeps the invoker and background work leaves focus alone.
A held response must respect focus moved to a surviving search field. Raw htmx in
this fixture restores a same-ID link; that behavior alone does not choose the next
task. Preserve this candidate's modal scope instead of treating every route change
as a return-to-invoker problem. No general route-focus pattern or AT validation is
admitted; delayed history restoration and competing navigation remain open.

View file

@ -50,3 +50,18 @@ native recovery link navigates successfully. This does not establish automatic f
late-script safety, accessible focus/announcements or offline availability.
See [NH01–NH06](../reviews/NavigationHistoryExperiment.md). One authored application
with a real package, not independent deployed recurrence. Hypothesis retained.
## Keyboard continuation and feedback — 2026-09-27
[Navigation focus experiment](../reviews/NavigationFocusExperiment.md) NF01–NF06
distinguishes successful route continuation, failed navigation, history restoration
and background updates. In the raw fixture, a replacement link retains focus but
fragment navigation leaves the old title and failures produce no visible status.
The authored policy updates title/status, selects a heading for direct continuation
and preserves a surviving control when the user has moved focus during the request.
Choose a continuation policy for the task; do not universally focus a heading or
return to the invoker. Check ownership when the response is applied, not only when
it is requested. Failure feedback need not move focus. Same-ID restoration is not
proof of semantic continuity. DOM/ARIA checks here do not establish spoken feedback;
AT, delayed history and overlapping-request behavior remain unverified.

View file

@ -173,3 +173,19 @@ binaries. Authored HTTP application; 18 browser cases plus explicit HTTP validat
sequence repeated twice. [NH01–NH06 review](../reviews/NavigationHistoryExperiment.md),
preregistered prospective corroboration. This is one implementation family, not
an independent production deployment. Initial launch failure receipt preserved.
## Navigation focus and feedback — 2026-09-27
SRC-0038 follow-up: [NF01–NF06](../reviews/NavigationFocusExperiment.md), separate
focus runner and authored policy using the same pinned htmx fixture family. 22
registered keyboard/DOM rows; not assistive-technology validation or independent use.
SRC-0039: [W3C Understanding Focus Order](https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html)
and [Status Messages](https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html),
primary interpretive guidance accessed 2026-09-27; living revision unknown. One W3C
family, not two application uses. NF review limits the inference to meaningful focus
order and programmatically exposed feedback; no conformance claim from this fixture.
SRC-0040: [htmx events](https://htmx.org/events/), lifecycle-hook reference accessed
2026-09-27, living revision unknown. Same htmx family as SRC-0018/0019 and SRC-0038.
Runtime conclusions use pinned 2.0.11 code and hashed receipts.

View file

@ -22,7 +22,7 @@ Independent human profile reviews: 0/17. Full-contract independent-use counts un
## Change since baseline
Baseline baseline-2026-09-26; source revision `f273a190f0e08b8cfe0bb7260d0e7d889f1c090a`. Added: 0; removed: 0; shared candidates with changed content: 2; changed evidence/review assessments: 2; maturity transitions: 0.
Baseline baseline-2026-09-26; source revision `f273a190f0e08b8cfe0bb7260d0e7d889f1c090a`. Added: 0; removed: 0; shared candidates with changed content: 3; changed evidence/review assessments: 3; maturity transitions: 0.
The shared cohort is compared separately from additions. Changed text or evidence counts are not learning gains.
@ -51,13 +51,13 @@ Ten selected retrospective outcomes (not a historical census), followed by prosp
| Outcome | Entries (not independent discoveries) |
| --- | ---: |
| corroboration | 1 |
| corroboration | 2 |
| inconclusive | 0 |
| merge | 0 |
| rejection | 0 |
| revision | 10 |
Cycles with recorded research effort: 1/11. Unknown effort is neither zero nor estimated from commits, tokens or assertion counts.
Cycles with recorded research effort: 2/12. Unknown effort is neither zero nor estimated from commits, tokens or assertion counts.
### Stream B / B-navigation-mechanism-v1
@ -67,11 +67,19 @@ Cycles with recorded research effort: 1/11. Unknown effort is neither zero nor e
One bounded question counts at most one consequential outcome. Corroboration and inconclusive work remain visible separately. Fewer than three comparable cycles: no trend assessment.
### Stream F / F-navigation-focus-v1
| Cycle | Outcome | Research minutes | Consequential outcome/hour |
| --- | --- | ---: | ---: |
| LEARN-0012 | corroboration | 5.138 | 0.00 |
One bounded question counts at most one consequential outcome. Corroboration and inconclusive work remain visible separately. Fewer than three comparable cycles: no trend assessment.
## Coherence and maintainability
Profile hashes and local references passed validation when this report was generated. That checks traceability, not source truth or external URL freshness.
Profiles with unknown last substantive review date: 15. Assessment dates record this metadata review, not a new substantive validation. No age-based freshness claim is available for those profiles.
Profiles with unknown last substantive review date: 14. Assessment dates record this metadata review, not a new substantive validation. No age-based freshness claim is available for those profiles.
Coherence metrics (ambiguous selections, unjustified relations and overlap resolved): not measured. Conflict Copy / Explicit Reconciliation overlap remains an explicit open question.
@ -230,8 +238,9 @@ Keep essential actions usable when enhancement is unavailable.
| documentary | htmx | Boosted links and forms. | [NavigationRenderingEvidence](../../research/reviews/NavigationRenderingEvidence.md) |
| documentary | SvelteKit | Form actions and rendering/page options. | [NavigationRenderingEvidence](../../research/reviews/NavigationRenderingEvidence.md) |
| runtime | htmx in authored public-catalog fixture | NH01–NH06: native/enhanced routes, history hit/miss, actual browser cache ordering and missing-Vary negative control; explicit Node validator checks separate. | [NavigationHistoryExperiment](../../research/reviews/NavigationHistoryExperiment.md) |
| runtime | htmx in authored public-catalog fixture | NF01–NF06: Tab/Enter navigation, raw versus authored focus/status policy, held-response ownership and background update; DOM evidence only, not AT speech. | [NavigationFocusExperiment](../../research/reviews/NavigationFocusExperiment.md) |
Open limits: Independent deployed full-contract recurrence remains unverified. Keyboard/focus/announcements, late-script activation, interrupted transport, invalid/deleted routes, offline availability and real intermediary behavior remain open.
Open limits: Independent deployed full-contract recurrence and human/AT validation remain unverified. Focus visibility, delayed history, overlapping requests, late-script activation, interrupted transport, invalid/deleted routes, offline availability and real intermediaries remain open.
Superseded by: none recorded.
@ -318,8 +327,9 @@ Restore meaningful focus after a temporary interaction ends.
| documentary | WAI-ARIA APG | Dialog focus obligations. | [PatternBreadthExpansion](../../research/reviews/PatternBreadthExpansion.md) |
| static-code | Radix | Pinned layer/listener ownership implementation. | [NestedDismissalIsolation](../../research/reviews/NestedDismissalIsolation.md) |
| runtime | Radix | Authored fixture: raw 20/24 versus routed 24/24; browser/build limits retained. | [NestedOwnershipRouting](../../research/reviews/NestedOwnershipRouting.md) |
| runtime | htmx in authored public-catalog fixture | NF01–NF06: Tab/Enter navigation, raw versus authored focus/status policy, held-response ownership and background update; DOM evidence only, not AT speech. | [NavigationFocusExperiment](../../research/reviews/NavigationFocusExperiment.md) |
Open limits: One implementation family; repeated trials are not independent uses. Animated exits and competing owners remain untested; no production repair claim.
Open limits: One implementation family; repeated trials are not independent uses. Animated exits and competing owners remain untested; no production repair claim. Route continuation is a contrasting context, not independent modal-pattern recurrence; delayed history and AT speech unverified.
Superseded by: none recorded.
@ -495,3 +505,17 @@ Evidence: [NavigationHistoryExperiment](../../research/reviews/NavigationHistory
Remaining uncertainty: One authored application, no independent recurrence or usefulness gain. Accessibility, delayed scripts, interrupted transport, real intermediaries and offline availability remain open.
Supersedes: none recorded.
### LEARN-0012 — Does enhanced route replacement need explicit continuation and feedback policy that respects changed focus ownership?
2026-09-27 · stream F · prospective · corroboration
Before: Native controls alone do not establish enhanced focus/feedback semantics; delayed responses should respect focus moved to another surviving task. Modal return is not automatically route continuation.
Conclusion: Corroborated in 22 cases: raw same-ID link focus survives but this fragment contract leaves stale title and silent failure; authored heading/status policy works and preserves newer search focus. No AT speech or universal heading policy established.
Evidence: [NavigationFocusExperiment](../../research/reviews/NavigationFocusExperiment.md), [focus-result](../../research/experiments/navigation-history/focus-result.json), [2026-09-27-navigation-focus-effort](../../research/evolution/cycles/2026-09-27-navigation-focus-effort.json).
Remaining uncertainty: Author-model DOM evidence, no human/AT validation or independent recurrence. Delayed history focus, competing requests, delayed scripts and interrupted enhancement remain open.
Supersedes: none recorded.

View file

@ -0,0 +1,34 @@
{
"start": 198900.113504947,
"execution_seconds": 31.501304913981585,
"executions": [
{
"command": [
"node",
"research/experiments/navigation-history/focus.mjs"
],
"seconds": 17.86536105797859,
"exit_code": 0
},
{
"command": [
"make",
"evolution-check",
"evolution-test",
"schema-test",
"bench-test"
],
"seconds": 13.635943856002996,
"exit_code": 0
}
],
"status": "complete",
"start_utc_approx": "2026-09-26T22:16:12.694172+00:00",
"end_utc": "2026-09-26T22:21:52.471986+00:00",
"elapsed_seconds": 339.778,
"research_minutes": 5.138,
"execution_minutes": 0.525,
"method": "Single-agent monotonic elapsed session minus blocking experiment/check subprocess durations. Checkpoint inspection assigned to execution; no double count. Rounded minute fields are elapsed agent effort, not human labor or model compute.",
"excluded": "Orientation before clock; final receipt refresh, commit, snapshot and State Hub sync after cutoff. Tokens and cost unknown.",
"budget": "Within preregistered 45 active research / 10 execution minute budget."
}

View file

@ -373,6 +373,43 @@
"cost_usd": null
},
"supersedes": []
},
{
"id": "LEARN-0012",
"date": "2026-09-27",
"recording": "prospective",
"stream": "F",
"comparison_group": "F-navigation-focus-v1",
"protocol_ref": "research/evolution/cycles/2026-09-27-navigation-focus.md",
"question": "Does enhanced route replacement need explicit continuation and feedback policy that respects changed focus ownership?",
"previous_belief": "Native controls alone do not establish enhanced focus/feedback semantics; delayed responses should respect focus moved to another surviving task. Modal return is not automatically route continuation.",
"conclusion": "Corroborated in 22 cases: raw same-ID link focus survives but this fragment contract leaves stale title and silent failure; authored heading/status policy works and preserves newer search focus. No AT speech or universal heading policy established.",
"disposition": "corroboration",
"affected_patterns": [
"CAND-0010",
"CAND-0016"
],
"evidence": [
"research/reviews/NavigationFocusExperiment.md",
"research/experiments/navigation-history/focus-result.json",
"research/evolution/cycles/2026-09-27-navigation-focus-effort.json"
],
"remaining_uncertainty": [
"Author-model DOM evidence, no human/AT validation or independent recurrence.",
"Delayed history focus, competing requests, delayed scripts and interrupted enhancement remain open."
],
"reviewer": {
"name": "Codex",
"role": "author-model",
"independent_human": false
},
"effort": {
"research_minutes": 5.138,
"execution_minutes": 0.525,
"reported_tokens": null,
"cost_usd": null
},
"supersedes": []
}
]
}

View file

@ -359,7 +359,7 @@
"id": "CAND-0010",
"path": "research/candidates/ProgressiveEnhancement.md",
"title": "Progressive Enhancement",
"sha256": "e9ca2b046cb69fff16379e22e1a9d0d61d2a5801ff92bbfc1b56ae3e3be16c7c",
"sha256": "ec3ab16d680308593af4d22fbbcbea737fe46096c5f3e65968d0db3a8befec21",
"status": "draft",
"maturity": "hypothesis",
"decision": "Keep essential actions usable when enhancement is unavailable.",
@ -381,6 +381,12 @@
"family": "htmx in authored public-catalog fixture",
"ref": "research/reviews/NavigationHistoryExperiment.md",
"scope": "NH01–NH06: native/enhanced routes, history hit/miss, actual browser cache ordering and missing-Vary negative control; explicit Node validator checks separate."
},
{
"kind": "runtime",
"family": "htmx in authored public-catalog fixture",
"ref": "research/reviews/NavigationFocusExperiment.md",
"scope": "NF01–NF06: Tab/Enter navigation, raw versus authored focus/status policy, held-response ownership and background update; DOM evidence only, not AT speech."
}
],
"independent_full_contract_uses": null,
@ -392,8 +398,8 @@
"last_substantive_review_date": "2026-09-27"
},
"gaps": [
"Independent deployed full-contract recurrence remains unverified.",
"Keyboard/focus/announcements, late-script activation, interrupted transport, invalid/deleted routes, offline availability and real intermediary behavior remain open."
"Independent deployed full-contract recurrence and human/AT validation remain unverified.",
"Focus visibility, delayed history, overlapping requests, late-script activation, interrupted transport, invalid/deleted routes, offline availability and real intermediaries remain open."
],
"superseded_by": []
},
@ -560,7 +566,7 @@
"id": "CAND-0016",
"path": "research/candidates/FocusReturn.md",
"title": "Focus Return",
"sha256": "b44f976684d7f2a9186e09aaa6fd6fdb51dc5674a53bcc90afa08748a641f041",
"sha256": "129ef7623d11da91f3f5429686455be16180e5477d1706836f5c1c3617698eae",
"status": "draft",
"maturity": "hypothesis",
"decision": "Restore meaningful focus after a temporary interaction ends.",
@ -582,19 +588,26 @@
"family": "Radix",
"ref": "research/reviews/NestedOwnershipRouting.md",
"scope": "Authored fixture: raw 20/24 versus routed 24/24; browser/build limits retained."
},
{
"kind": "runtime",
"family": "htmx in authored public-catalog fixture",
"ref": "research/reviews/NavigationFocusExperiment.md",
"scope": "NF01–NF06: Tab/Enter navigation, raw versus authored focus/status policy, held-response ownership and background update; DOM evidence only, not AT speech."
}
],
"independent_full_contract_uses": null,
"review": {
"assessed_at": "2026-09-26",
"assessed_at": "2026-09-27",
"reviewer": "Codex",
"role": "author-model",
"independent_human": false,
"last_substantive_review_date": null
"last_substantive_review_date": "2026-09-27"
},
"gaps": [
"One implementation family; repeated trials are not independent uses.",
"Animated exits and competing owners remain untested; no production repair claim."
"Animated exits and competing owners remain untested; no production repair claim.",
"Route continuation is a contrasting context, not independent modal-pattern recurrence; delayed history and AT speech unverified."
],
"superseded_by": []
},

View file

@ -38,3 +38,18 @@ History eviction uses htmx 2.0.11's sessionStorage key, an experimental control
to the pinned implementation. No assertion of identical content freshness across
history, general offline availability, accessibility, script-late races, writes,
real intermediary correctness or independent full-pattern recurrence follows.
## Keyboard/focus follow-up
[Preregistered LEARN-0012](../../evolution/cycles/2026-09-27-navigation-focus.md),
[review](../../reviews/NavigationFocusExperiment.md), [receipt](focus-result.json).
Run the same browser environment with `node focus.mjs`. Output defaults to
`/tmp/fep-focus-result.json`; choose a fresh `FEP_RESULT_PATH` per execution.
The separate focus runner/policy preserve the earlier navigation/cache inputs and
receipt hashes. Same locked packages. 22 cases: native keyboard plus raw/managed
navigation, 503 feedback, held-response focus ownership, history and background work.
Raw rows observe behavior, so 22 passing rows do not mean raw and managed variants
meet identical focus/feedback obligations. See NF01–NF06. The policy is authored
experimental code; DOM focus and live-region text do not establish assistive-technology
announcements or conformance. History and request-race limits remain explicit.

View file

@ -0,0 +1,32 @@
// Authored experimental navigation policy; raw variant installs only diagnostics.
window.probe=[];
const managed=new URL(location.href).searchParams.get('mode')==='managed';
const pending=new WeakMap();
const notice=message=>{document.querySelector('#feedback').textContent=message;};
function complete(moveFocus){
const heading=document.querySelector('#heading');
document.title=`${heading.textContent} — Catalog`;
notice(`Loaded ${heading.textContent}`);
if(moveFocus)heading.focus();
}
document.addEventListener('htmx:beforeRequest',e=>{
if(managed&&e.detail.target.id==='content')pending.set(e.detail.xhr,{source:e.detail.elt,ownsFocus:false});
});
document.addEventListener('htmx:beforeSwap',e=>{
const state=pending.get(e.detail.xhr);
if(state)state.ownsFocus=document.activeElement===state.source;
});
document.addEventListener('htmx:afterSwap',e=>{
const state=pending.get(e.detail.xhr);
if(state){complete(state.ownsFocus);pending.delete(e.detail.xhr);}
});
document.addEventListener('htmx:responseError',e=>{
if(managed&&pending.has(e.detail.xhr)){
notice('Navigation failed. Current article retained. Use Native recovery to continue.');
pending.delete(e.detail.xhr);
}
});
document.addEventListener('htmx:historyRestore',()=>{if(managed)complete(true);});
for(const name of ['htmx:afterSettle','htmx:historyRestore','htmx:responseError']){
document.addEventListener(name,()=>window.probe.push({name,focus:document.activeElement.id||document.activeElement.tagName,title:document.title,url:location.href,feedback:document.querySelector('#feedback').textContent}));
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,74 @@
import {chromium,firefox} from 'playwright';
import {createServer} from 'node:http';
import {once} from 'node:events';
import {readFile,writeFile} from 'node:fs/promises';
import {createHash} from 'node:crypto';
import assert from 'node:assert/strict';
const dir=new URL('.',import.meta.url);
const htmx=await readFile(new URL('node_modules/htmx.org/dist/htmx.js',dir));
const policy=await readFile(new URL('focus-policy.js',dir));
let requests=[],release=null,heldResolve=null;
const server=createServer((req,res)=>{
const u=new URL(req.url,'http://local');res.setHeader('Cache-Control','no-store');
const hx=req.headers['hx-request']==='true';requests.push({path:req.url,hx,restore:req.headers['hx-history-restore-request']==='true'});
if(u.pathname==='/htmx.js'||u.pathname==='/policy.js'){res.setHeader('Content-Type','application/javascript');res.end(u.pathname==='/htmx.js'?htmx:policy);return;}
if(u.pathname==='/news'){res.end('<p>Background news updated</p>');return;}
if(u.pathname!=='/articles'){res.writeHead(404);res.end('Not found');return;}
const topic=u.searchParams.get('topic')==='beta'?'beta':'alpha',mode=u.searchParams.get('mode')==='managed'?'managed':'raw';
const target=`/articles?topic=${topic==='alpha'?'beta':'alpha'}&mode=${mode}`;
const boost='hx-boost="true" hx-target="#content" hx-select="#content" hx-swap="outerHTML"';
const content=`<main id="content" hx-history-elt><h1 id="heading" tabindex="-1">${topic}</h1><p>Article ${topic}</p><a id="next" href="${target}" ${boost}>Other topic</a><a id="broken" href="${target}&fail=1" ${boost}>Fail enhanced navigation</a><a id="recover" href="${target}&fail=1">Native recovery</a><a id="slow" href="${target}&slow=1" ${boost}>Delayed navigation</a></main>`;
const body=hx?content:`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${topic} — Catalog</title><meta name="htmx-config" content='{"historyRestoreAsHxRequest":false}'><script src="/policy.js" defer></script><script src="/htmx.js" defer></script></head><body>${content}<label>Persistent search <input id="search"></label><p id="feedback" role="status" aria-live="polite" aria-atomic="true"></p><aside id="news">Initial news</aside></body></html>`;
const send=()=>{res.writeHead(hx&&u.searchParams.has('fail')?503:200,{'Content-Type':'text/html'});res.end(hx&&u.searchParams.has('fail')?'Outage':body);};
if(hx&&u.searchParams.has('slow')){release=send;heldResolve?.();}else send();
});
server.listen(0,'127.0.0.1');await once(server,'listening');const base=`http://127.0.0.1:${server.address().port}`;
const results=[],browsers={};const output=process.env.FEP_RESULT_PATH||'/tmp/fep-focus-result.json';
async function inspect(p){return p.evaluate(()=>({focus:document.activeElement.id||document.activeElement.tagName,title:document.title,url:location.href,heading:document.querySelector('h1')?.textContent,feedback:document.querySelector('#feedback')?.textContent,statusRole:document.querySelector('#feedback')?.getAttribute('role'),live:document.querySelector('#feedback')?.getAttribute('aria-live')}));}
async function tabTo(p,id){for(let i=0;i<12;i++){if((await inspect(p)).focus===id)return;await p.keyboard.press('Tab');}throw Error(`Tab failed to reach ${id}`);}
async function selected(p,topic){await p.waitForFunction(t=>document.querySelector('h1')?.textContent===t,topic);assert.equal(new URL(p.url()).searchParams.get('topic'),topic);}
async function settled(p){await p.waitForFunction(()=>window.probe.some(e=>e.name==='htmx:afterSettle'));}
try{
for(const [engine,launcher]of [['chromium',chromium],['firefox',firefox]]){
const path=engine==='chromium'?process.env.FEP_CHROMIUM_PATH:undefined;
const browser=await launcher.launch({headless:true,...(path?{executablePath:path}:{})});
browsers[engine]={version:browser.version(),executable:path||launcher.executablePath(),sha256:createHash('sha256').update(await readFile(path||launcher.executablePath())).digest('hex')};
try{
async function scenario(id,mode,fn){
requests=[];release=null;const context=await browser.newContext({javaScriptEnabled:mode!=='native'});const p=await context.newPage();p.setDefaultTimeout(4000);
const r={id,engine,mode,status:'pass'};
try{await p.goto(base+`/articles?topic=alpha&mode=${mode}`);if(mode!=='native')await p.waitForFunction(()=>window.htmx?.version==='2.0.11');r.observation=await fn(p);}
catch(e){r.status='fail';r.error=e.message;}
r.final=await inspect(p).catch(()=>null);r.events=await p.evaluate(()=>window.probe||[]).catch(()=>[]);r.requests=[...requests];results.push(r);
if(release){release();release=null;}heldResolve=null;await context.close();await writeFile(output,JSON.stringify({status:'incomplete',results},null,2)+'\n');
}
await scenario('K01-native-keyboard','native',async p=>{await tabTo(p,'next');await p.keyboard.press('Enter');await selected(p,'beta');assert.equal(await p.title(),'beta — Catalog');return {keyboardLink:true};});
for(const mode of ['raw','managed']){
await scenario('K02-enhanced-keyboard',mode,async p=>{
await tabTo(p,'next');await p.keyboard.press('Enter');await selected(p,'beta');await settled(p);const o=await inspect(p);
o.headingContinuation=o.focus==='heading';o.updatedTitle=o.title==='beta — Catalog';o.completionFeedback=o.feedback==='Loaded beta';
if(mode==='managed')assert(o.headingContinuation&&o.updatedTitle&&o.completionFeedback);return o;
});
await scenario('K03-error-feedback-recovery',mode,async p=>{
await tabTo(p,'broken');await p.keyboard.press('Enter');await p.waitForFunction(()=>window.probe.some(e=>e.name==='htmx:responseError'));await selected(p,'alpha');const o=await inspect(p);assert.equal(o.focus,'broken');
o.failureFeedback=o.feedback.includes('Navigation failed');if(mode==='managed'){assert(o.failureFeedback);assert.equal(o.statusRole,'status');assert.equal(o.live,'polite');}
await tabTo(p,'recover');await p.keyboard.press('Enter');await selected(p,'beta');o.nativeRecovery=true;return o;
});
await scenario('K04-delayed-new-focus',mode,async p=>{
const held=new Promise(resolve=>{heldResolve=resolve;});await tabTo(p,'slow');await p.keyboard.press('Enter');await held;await tabTo(p,'search');
release();release=null;await selected(p,'beta');await settled(p);const o=await inspect(p);assert.equal(o.focus,'search');if(mode==='managed')assert.equal(o.feedback,'Loaded beta');return o;
});
await scenario('K05-history-continuation',mode,async p=>{
await tabTo(p,'next');await p.keyboard.press('Enter');await selected(p,'beta');await settled(p);await p.goBack();await selected(p,'alpha');await p.waitForFunction(()=>window.probe.some(e=>e.name==='htmx:historyRestore'));
const o=await inspect(p);if(mode==='managed'){assert.equal(o.focus,'heading');assert.equal(o.title,'alpha — Catalog');assert.equal(o.feedback,'Loaded alpha');}return o;
});
await scenario('K06-background-no-theft',mode,async p=>{
await tabTo(p,'search');await p.evaluate(()=>htmx.ajax('GET','/news',{target:'#news',swap:'innerHTML'}));await p.locator('#news').filter({hasText:'Background news updated'}).waitFor();
const o=await inspect(p);assert.equal(o.focus,'search');assert.equal(o.feedback,'');assert.equal(o.title,'alpha — Catalog');return o;
});
}
}finally{await browser.close();}
}
}finally{if(release)release();await new Promise(resolve=>server.close(resolve));}
const files={};for(const f of ['focus.mjs','focus-policy.js','package.json','package-lock.json','node_modules/htmx.org/dist/htmx.js'])files[f]=createHash('sha256').update(await readFile(new URL(f,dir))).digest('hex');
const r={date:new Date().toISOString(),node:process.version,kind:'keyboard-focus-dom-observation-not-assistive-technology-validation',files,browsers,results,passed:results.filter(x=>x.status==='pass').length,total:results.length};await writeFile(output,JSON.stringify(r,null,2)+'\n');console.log(JSON.stringify({output,passed:r.passed,total:r.total,failures:results.filter(x=>x.status==='fail')},null,2));if(r.passed!==r.total)process.exitCode=1;

View file

@ -33,3 +33,10 @@ corroboration to VC-03, URL-addressable State and Progressive Enhancement. Direc
native/enhanced/history and representation-cache subset exercised in two browsers;
missing-Vary negative control reproduced. VC-03 stays provisional and offline/
accessibility work remains open. No maturity or M1/M2 promotion.
## Keyboard continuation evidence — 2026-09-27
[NF01–NF06](NavigationFocusExperiment.md) adds 22 native/raw/managed observations to
the provisional VC-03 subset. Explicit focus/title/status policy and delayed-response
ownership exercised; AT speech and full accessibility validation remain pending.
This is a new stream-F comparison group, not another comparable cache cycle.

View file

@ -0,0 +1,85 @@
# Keyboard continuation and navigation feedback — 2026-09-27
LEARN-0012; Codex author-model review. [Preregistered protocol](../evolution/cycles/2026-09-27-navigation-focus.md)
committed as `2b1c9a4` before implementation/execution. New stream-F comparison group
F-navigation-focus-v1, not comparable to the preceding HTTP/cache question's group.
[Runner](../experiments/navigation-history/focus.mjs),
[authored policy](../experiments/navigation-history/focus-policy.js),
[receipt](../experiments/navigation-history/focus-result.json).
## Evidence and result
Pinned htmx 2.0.11, Playwright 1.63.0; same binary versions/hashes as the previous
navigation cycle: Chromium 151.0.7922.34 and Firefox 155.0. One run, no harness
corrections or discarded failures. 22/22 registered rows completed: two native cases,
ten raw observation cases, ten managed-policy cases. A passing raw observation row
means it was recorded successfully, not that it satisfies the managed obligations.
All browser actions initiating navigation use Tab/Enter; background update is
explicitly script-triggered and browser history uses Playwright's history command.
| Claim | Cases | Observations in both browsers |
| --- | --- | --- |
| NF01 | K01 | With scripts disabled, Tab reaches the native link and Enter loads beta with its full-document title |
| NF02 | K02 | Raw htmx retains focus on the new same-ID `next` link, but this fragment response leaves alpha's title over beta content and produces no completion status. Managed policy focuses beta's heading, updates title and writes `Loaded beta` |
| NF03 | K03 | Enhanced 503 preserves alpha URL/content and the invoking link's focus in both variants. Only managed policy writes the failure/recovery message; native recovery works by keyboard in both |
| NF04 | K04 | While a response is held at the server, the user tabs to persistent search. Both variants preserve search focus on release; managed policy reports completion without moving focus |
| NF05 | K05 | History restores alpha. Raw focus is the same-ID link; managed focus is the heading with matching title/status |
| NF06 | K06 | An unrelated background region update preserves search focus and does not emit navigation feedback in either variant |
The raw result narrows our anticipation of focus loss: stable IDs let htmx restore
focus to a replacement element. Here that link now leads back to the other article.
DOM identity retention alone is not proof of an appropriate next task, but neither is
this observation proof that retaining the link is always wrong. The server deliberately
returns a fragment without title metadata; this is an application contract gap, not
a claim that htmx cannot update document titles.
## Mechanism and interpretation
The authored policy remembers the request source, checks focus ownership immediately
before a zero-delay swap, and applies the continuation synchronously on afterSwap,
after the library's own focus restoration. It updates heading/title/status only for
registered content navigation. It leaves a surviving search control alone if focus
has moved there. Errors keep focus on the invoker and provide a visible polite status
message with a native recovery action already present. History uses a separate explicit
heading policy. It is a small experiment, not a production navigation library.
[W3C focus-order guidance](https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html)
allows more than one meaningful focus order and does not prescribe one universal target.
[Status-message guidance](https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html)
informs making feedback programmatically available without requiring focus movement.
[htmx events](https://htmx.org/events/) supplies the lifecycle hooks. Living primary
pages accessed 2026-09-27; exact documentation revisions unknown. Package implementation
is pinned and hashed. DOM status/ARIA observations are not evidence that a screen reader
spoke the message; no assistive technology or human evaluation was run.
## Disposition and limits
**Corroboration**, one bounded outcome. Existing Progressive Enhancement already calls
for focus, feedback and recovery semantics; Focus Return explicitly excludes automatic
generalization from modal closure to route transitions. The experiment makes that
boundary concrete and adds an ownership-sensitive candidate mechanism. No newly
invented pattern, canonical promotion, independent recurrence or usefulness gain.
Add scoped implementation guidance: distinguish successful route continuation, failed
navigation, history and background updates. Test whether a late response still owns
focus before moving it. Returning to a modal invoker and continuing into a new page
are different decisions. Keep both candidates as hypotheses.
Unverified: screen-reader speech (including possible heading/status duplication),
focus visibility, complete names/order audit, actual usability, pointer modality,
nonzero swap delays, overlapping/superseded route requests, delayed history responses
while the user moves focus, script-late activation, interrupted transport and offline
availability. The explicit history policy always focuses a heading in the tested
immediate restore; it is not proven safe for an asynchronous history miss. Public
single-response success does not settle these ownership races. No writes or production
application were exercised.
[Effort receipt](../evolution/cycles/2026-09-27-navigation-focus-effort.json) separates
monotonic active agent time from experiment/check subprocess time; final administrative
refresh/commit/snapshot/sync overhead excluded. No token/cost estimate. One cycle in
this new comparison group cannot establish a trend, even though the ledger now has
two prospective cycles in different groups.
WP-0003 T01 retains delayed history restoration/overlapping navigation ownership and
interrupted enhancement. WP-0003 T02 retains independent human/AT validation and the
offline branch. WP-0006 T03 retains comparable-window and remaining review-date work.

View file

@ -108,3 +108,12 @@ loss under missing Vary. Validator checks use explicit Node requests. This stren
the read-only trace; the case remains provisional. Keyboard/focus/announcements, late
scripts, interrupted transport, invalid/deleted routes and the offline branch remain
open. No performance, independent-human or decision-usefulness gate is completed.
## Keyboard/feedback subset — 2026-09-27
[NF01–NF06](../reviews/NavigationFocusExperiment.md) exercises keyboard links, request
failure/recovery, explicit route continuation and a held response after focus moves
to persistent search. The managed policy passes its DOM obligations; raw same-ID
focus retention does not itself update title or report errors in this fragment host.
VC-03 stays provisional. Spoken feedback, focus visibility, delayed history/request
races and offline availability remain open; no complete accessibility claim.

View file

@ -201,3 +201,17 @@ WP-0006 T03 remains progress for comparable-window review and remaining substant
review-date reconciliation. Choose a new comparison group for materially different
questions. M1/M2 and independent-human validation remain open. See
research/reviews/NavigationHistoryExperiment.md.
## Measured keyboard/focus cycle — 2026-09-27
LEARN-0012 registered at 2b1c9a4 in new comparison group F-navigation-focus-v1.
22/22 native/raw/managed rows complete; raw rows are observations, not fulfillment
of managed obligations. Same-ID focus retained by raw htmx, while authored policy
adds heading/title/status continuation and preserves newer search focus. Outcome
corroboration, hypotheses retained; no AT or human validation, no trend across the
two different prospective groups. See research/reviews/NavigationFocusExperiment.md.
WP-0003 T01 next: delayed history restoration and competing-navigation ownership;
interrupted/late enhancement remains open. T02 retains AT/independent review and
offline branch. WP-0006 T03 remains progress for comparable windows and remaining
review-date reconciliation. No M1/M2 completion.

View file

@ -80,3 +80,17 @@ WP-0006 T03 remains progress for comparable-window review and remaining substant
review-date reconciliation. Choose a new comparison group for materially different
questions. M1/M2 and independent-human validation remain open. See
research/reviews/NavigationHistoryExperiment.md.
## Measured keyboard/focus cycle — 2026-09-27
LEARN-0012 registered at 2b1c9a4 in new comparison group F-navigation-focus-v1.
22/22 native/raw/managed rows complete; raw rows are observations, not fulfillment
of managed obligations. Same-ID focus retained by raw htmx, while authored policy
adds heading/title/status continuation and preserves newer search focus. Outcome
corroboration, hypotheses retained; no AT or human validation, no trend across the
two different prospective groups. See research/reviews/NavigationFocusExperiment.md.
WP-0003 T01 next: delayed history restoration and competing-navigation ownership;
interrupted/late enhancement remains open. T02 retains AT/independent review and
offline branch. WP-0006 T03 remains progress for comparable windows and remaining
review-date reconciliation. No M1/M2 completion.