Isolate nested dismissal ownership across browsers and builds
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0cffe-206f-7323-89d3-b20ca4cf4bf9
This commit is contained in:
parent
3eecd74b0d
commit
85ae4f62af
12 changed files with 7090 additions and 0 deletions
4
TREE.md
4
TREE.md
|
|
@ -352,6 +352,9 @@ research/experiments/boundary-recovery/README.md
|
|||
research/experiments/boundary-recovery/development-results.json
|
||||
research/experiments/boundary-recovery/experiment.mjs
|
||||
research/experiments/boundary-recovery/fixture.jsx
|
||||
research/experiments/boundary-recovery/isolation-provenance.json
|
||||
research/experiments/boundary-recovery/isolation-results.json
|
||||
research/experiments/boundary-recovery/isolation.mjs
|
||||
research/experiments/boundary-recovery/package-lock.json
|
||||
research/experiments/boundary-recovery/package.json
|
||||
research/experiments/boundary-recovery/provenance.json
|
||||
|
|
@ -379,6 +382,7 @@ research/reviews/LocalFirstEvidenceExpansion.md
|
|||
research/reviews/MatrixRecoveryExperiment.md
|
||||
research/reviews/NavigationCoverage.md
|
||||
research/reviews/NavigationRenderingEvidence.md
|
||||
research/reviews/NestedDismissalIsolation.md
|
||||
research/reviews/ObsidianConflictScreen.json
|
||||
research/reviews/PatternBreadthExpansion.md
|
||||
research/reviews/RecoveryReviewMetrics.json
|
||||
|
|
|
|||
|
|
@ -52,3 +52,7 @@ previous counts above describe earlier slices. No additional runtime or model ru
|
|||
Latest execution: [rendering/focus boundary experiment](reviews/BoundaryRecoveryExperiment.md)
|
||||
uses pinned React/Radix packages in Chromium. It records seven stable case observations
|
||||
and a timing-sensitive nested-dialog failure; no production or accessibility certification.
|
||||
|
||||
Nested-dialog follow-up: [ownership isolation](reviews/NestedDismissalIsolation.md)
|
||||
captures a stale Escape-listener interval in a 48-trial browser/build comparison.
|
||||
A partial guard loses input; a complete remedy remains open. Original results retained.
|
||||
|
|
|
|||
|
|
@ -51,3 +51,14 @@ run, while a 100 ms settling variant passed. Neither focus arrival nor dialog vi
|
|||
alone proves dismissal ownership has stabilized. The delay is an experimental variable,
|
||||
not a recommended fix. Root cause and accessibility conformance remain unverified;
|
||||
retain hypothesis maturity and test lifecycle ownership before a stronger claim.
|
||||
|
||||
## Nested ownership isolation — 2026-09-26
|
||||
|
||||
[Follow-up matrix](../reviews/NestedDismissalIsolation.md) captures an interval where
|
||||
the inner layer is registered and focused but the outer Escape listener still handles
|
||||
input. This explains the recorded wrong-owner dismissal in the instrumented fixture.
|
||||
Development/production and Chromium/Firefox comparisons retain all 48 trial results.
|
||||
A current-owner guard can lose the keypress; private listener-readiness waiting passes
|
||||
its trials but is not a product solution. Test ownership of dismissal as well as return
|
||||
focus, including activation/teardown input. No arbitrary-delay fix, production repair
|
||||
or full-pattern promotion is established.
|
||||
|
|
|
|||
|
|
@ -122,3 +122,8 @@ Content API and keyboard interaction; living page accessed 2026-09-26, revision
|
|||
unknown. Displayed documentation version differs from executed package; runtime
|
||||
claims use the pinned archive, not an assumption that the page exactly matches it.
|
||||
[Results and limits](../reviews/BoundaryRecoveryExperiment.md).
|
||||
|
||||
SRC-0029 follow-up: [nested dismissal isolation](../reviews/NestedDismissalIsolation.md)
|
||||
reuses the same pinned packages across two browsers/build modes. Instrumented and
|
||||
experimentally modified bundles are explicitly separated from the unmodified control;
|
||||
this is additional evidence from the same family, not a new independent use.
|
||||
|
|
|
|||
|
|
@ -61,3 +61,28 @@ or full accessibility conformance. X03 handles a response error explicitly; it d
|
|||
not test unhandled promises, timeouts, mutation idempotency or remote rollback.
|
||||
Nested dismissal remains unresolved. Dependency/version compatibility and fixture
|
||||
lifecycle effects must be separated before attributing a library defect.
|
||||
|
||||
## Follow-up: nested ownership isolation
|
||||
|
||||
[Review](../../reviews/NestedDismissalIsolation.md) and [48-trial receipt](isolation-results.json)
|
||||
compare development/production, Chromium/Firefox and four explicit variants. The
|
||||
original experiment and receipts above remain unchanged. Follow-up reports 43/48
|
||||
obligations met, including failures; it is a research runner, not a green CI gate.
|
||||
|
||||
```bash
|
||||
PLAYWRIGHT_BROWSERS_PATH=/tmp/fep-playwright-browsers node node_modules/playwright/cli.js install firefox
|
||||
PLAYWRIGHT_BROWSERS_PATH=/tmp/fep-playwright-browsers FEP_CHROMIUM_PATH=/path/to/chrome node isolation.mjs
|
||||
```
|
||||
|
||||
Runs three trials per browser/build/variant, without automatic retries. Default
|
||||
output is `/tmp/fep-isolation.json`; `FEP_RESULT_PATH` overrides it. A separate
|
||||
`/tmp/fep-isolation-checkpoint.json` preserves completed trials if a later launch
|
||||
fails. Launch failures abort; completed matrices retain assertion failures in JSON.
|
||||
A sandbox prevented Firefox accessing its temporary profile here; the recorded
|
||||
completed matrix ran outside that sandbox. Firefox build and limitations are in
|
||||
[isolation-provenance.json](isolation-provenance.json).
|
||||
|
||||
Instrumentation and an experimental guard are applied in memory during bundling;
|
||||
installed vendor files are not modified. The readiness variant observes private
|
||||
listener state and is not a proposed public readiness API. The guard can lose input
|
||||
and is not a validated fix. Do not replace raw failures with readiness passes.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"date": "2026-09-26",
|
||||
"firefox": {
|
||||
"playwright_build": "1543",
|
||||
"executable_sha256": "062272ac4d851eade5b75db2dc7c14569d96cb3ca38f949337cdc1b74367e35b",
|
||||
"version": "155.0"
|
||||
},
|
||||
"chromium": "Same executable as provenance.json; explicit override",
|
||||
"instrumentation": "In-memory esbuild transformation; installed package files unmodified",
|
||||
"initial_attempt": "Sandboxed run reached Firefox launch, which failed to access its temporary profile; no completed matrix receipt. Re-executed outside sandbox.",
|
||||
"limitations": [
|
||||
"Fixed condition order, three trials each, no population failure-rate inference",
|
||||
"Tracing and before-event snapshot can change scheduling; raw is an unmodified-library control",
|
||||
"ready uses private listener instrumentation, not a public product readiness API",
|
||||
"guarded is an experimental mutation, not vendor behavior or a validated fix"
|
||||
]
|
||||
}
|
||||
6807
research/experiments/boundary-recovery/isolation-results.json
Normal file
6807
research/experiments/boundary-recovery/isolation-results.json
Normal file
File diff suppressed because it is too large
Load diff
91
research/experiments/boundary-recovery/isolation.mjs
Normal file
91
research/experiments/boundary-recovery/isolation.mjs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
// Follow-up only: original fixture, runner and result receipts remain unchanged.
|
||||
import {build} from 'esbuild';
|
||||
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 {fileURLToPath} from 'node:url';
|
||||
const dir = fileURLToPath(new URL('.', import.meta.url));
|
||||
const hash = data => createHash('sha256').update(data).digest('hex');
|
||||
const repetitions = 3;
|
||||
const layerPath = dir + 'node_modules/@radix-ui/react-dismissable-layer/dist/index.mjs';
|
||||
const layerSource = await readFile(layerPath, 'utf8');
|
||||
const expectedLayerHash = JSON.parse(await readFile(dir + 'provenance.json')).inspected_files['@radix-ui/react-dismissable-layer/dist/index.mjs'].sha256;
|
||||
if (hash(layerSource) !== expectedLayerHash) throw new Error('Inspected dependency changed');
|
||||
const bundles = new Map();
|
||||
function instrument(source, guard) {
|
||||
const replace = (from, to) => {
|
||||
if (source.split(from).length !== 2) throw new Error('Ambiguous instrumentation locator: ' + from);
|
||||
source = source.replace(from, to);
|
||||
};
|
||||
replace('const isHighestLayer = node ? index === layers.length - 1 : false;', `const isHighestLayer = node ? index === layers.length - 1 : false;
|
||||
const trace = (kind) => window.layerTrace.push({kind, t: performance.now(), node: node?.getAttribute('aria-labelledby'), active: document.activeElement?.id, layers: [...context.layers].map(n => n.getAttribute('aria-labelledby'))});`);
|
||||
replace(' onEscapeKeyDown?.(event);', ` trace('escape');
|
||||
${guard ? "if ([...context.layers].at(-1) !== node) { trace('guard-rejected'); return; }" : ''}
|
||||
onEscapeKeyDown?.(event);`);
|
||||
replace(' ownerDocument.addEventListener("keydown", handleKeyDown, { capture: true });', ' trace("listen");\n ownerDocument.addEventListener("keydown", handleKeyDown, { capture: true });');
|
||||
replace(' return () => ownerDocument.removeEventListener("keydown", handleKeyDown, { capture: true });', ' return () => { trace("unlisten"); ownerDocument.removeEventListener("keydown", handleKeyDown, { capture: true }); };');
|
||||
replace(' context.layers.add(node);', ' context.layers.add(node); trace("layer-add");');
|
||||
replace(' context.layers.delete(node);', ' trace("layer-remove"); context.layers.delete(node);');
|
||||
return source;
|
||||
}
|
||||
for (const mode of ['development', 'production']) {
|
||||
for (const variant of ['raw', 'traced', 'ready', 'guarded']) {
|
||||
const b = await build({entryPoints: [dir + 'fixture.jsx'], bundle: true, write: false,
|
||||
define: {'process.env.NODE_ENV': JSON.stringify(mode)}, format: 'iife',
|
||||
plugins: variant === 'raw' ? [] : [{name: 'layer-observation', setup(build) {
|
||||
build.onLoad({filter: /@radix-ui\/react-dismissable-layer\/dist\/index\.mjs$/}, () => ({contents: instrument(layerSource, variant === 'guarded'), loader: 'js', resolveDir: dir + 'node_modules/@radix-ui/react-dismissable-layer/dist'}));
|
||||
}}]});
|
||||
bundles.set(mode + '-' + variant, b.outputFiles[0].contents);
|
||||
}
|
||||
}
|
||||
const server = createServer((req, res) => {
|
||||
const u = new URL(req.url, 'http://localhost');
|
||||
if (u.pathname === '/bundle.js') {res.setHeader('Content-Type','application/javascript'); res.end(bundles.get(u.searchParams.get('key')));}
|
||||
else {res.setHeader('Content-Type', 'text/html');res.end(`<!doctype html><html lang="en"><title>Nested isolation</title><body><div id="root"></div><script>window.layerTrace=[];</script><script src="/bundle.js?key=${u.searchParams.get('key')}"></script></body></html>`);}
|
||||
});
|
||||
server.listen(0, '127.0.0.1');await once(server, 'listening');
|
||||
const results = [];
|
||||
try {
|
||||
for (const [engine, launcher] of [['chromium', chromium], ['firefox', firefox]]) {
|
||||
const browser = await launcher.launch({headless: true, ...(engine === 'chromium' && process.env.FEP_CHROMIUM_PATH ? {executablePath: process.env.FEP_CHROMIUM_PATH} : {})});
|
||||
try {
|
||||
for (const mode of ['development', 'production']) for (const variant of ['raw', 'traced', 'ready', 'guarded']) for (let trial = 1; trial <= repetitions; trial++) {
|
||||
const page = await browser.newPage();page.setDefaultTimeout(2000);
|
||||
const r = {engine, browser: browser.version(), mode, variant, trial, status: 'pass'};
|
||||
try {
|
||||
await page.goto(`http://127.0.0.1:${server.address().port}/?mode=focus&key=${mode}-${variant}`);
|
||||
await page.locator('#invoker').focus();await page.keyboard.press('Enter');
|
||||
await page.waitForFunction(() => document.activeElement?.id === 'remove');
|
||||
await page.keyboard.press('Tab');await page.waitForFunction(() => document.activeElement?.id === 'nested-invoker');
|
||||
await page.keyboard.press('Enter');await page.waitForFunction(() => document.activeElement?.id === 'nested-close');
|
||||
if (variant === 'ready') await page.waitForFunction(() => {
|
||||
const inner = document.getElementById('nested-close').closest('[role=dialog]').getAttribute('aria-labelledby');
|
||||
const listeners = new Set();for (const e of window.layerTrace) {if (e.kind === 'listen') listeners.add(e.node);if(e.kind === 'unlisten') listeners.delete(e.node);}
|
||||
return listeners.size === 1 && listeners.has(inner);
|
||||
});
|
||||
r.before = await page.evaluate(() => ({active: document.activeElement.id, dialogs: [...document.querySelectorAll('[role=dialog]')].map(n => ({id: n.getAttribute('aria-labelledby'), title: document.getElementById(n.getAttribute('aria-labelledby'))?.textContent}))}));
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForFunction(() => document.activeElement?.id === 'nested-invoker' && document.querySelectorAll('[role=dialog]').length === 1, null, {timeout: 1500});
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForFunction(() => document.activeElement?.id === 'invoker', null, {timeout: 1500});
|
||||
} catch (e) {r.status = 'fail';r.error = e.message;}
|
||||
r.after = await page.evaluate(() => ({active: document.activeElement?.id, dialogs: document.querySelectorAll('[role=dialog]').length, transitions: window.observed.dialogTransitions, trace: window.layerTrace}));
|
||||
results.push(r); await page.close();
|
||||
await writeFile('/tmp/fep-isolation-checkpoint.json', JSON.stringify({status: 'incomplete', results}, null, 2) + '\n');
|
||||
}
|
||||
} finally {await browser.close();}
|
||||
}
|
||||
} finally {await new Promise(resolve => server.close(resolve));}
|
||||
const files = {};
|
||||
for (const name of ['isolation.mjs','fixture.jsx','package.json','package-lock.json']) files[name] = hash(await readFile(dir + name));
|
||||
const summary = [];
|
||||
for (const engine of ['chromium','firefox']) for (const mode of ['development','production']) for (const variant of ['raw','traced','ready','guarded']) {
|
||||
const group=results.filter(r => r.engine===engine && r.mode===mode && r.variant===variant);
|
||||
summary.push({engine,mode,variant,passed:group.filter(r=>r.status==='pass').length,total:group.length});
|
||||
}
|
||||
const output = process.env.FEP_RESULT_PATH || '/tmp/fep-isolation.json';
|
||||
await writeFile(output, JSON.stringify({date:new Date().toISOString(), node:process.version, repetitions, files, layerSourceSha256:hash(layerSource), bundleHashes:Object.fromEntries([...bundles].map(([k,v])=>[k,hash(v)])), summary,results},null,2)+'\n');
|
||||
console.log(JSON.stringify({output,summary},null,2));
|
||||
// Findings are retained, including failed obligations; not a CI acceptance gate.
|
||||
|
|
@ -79,3 +79,11 @@ Q06 separates module interfaces, client activation, fault containment and indepe
|
|||
release contracts. Q05 asks when modal focus/lifecycle requirements must shape
|
||||
composition. Q03 still lacks verified recurrence for these complete contracts;
|
||||
Q04 still lacks demonstrated decision benefit. Synthetic probes do not close cases.
|
||||
|
||||
## Q05 temporal interaction ownership — 2026-09-26
|
||||
|
||||
[Nested dismissal isolation](../reviews/NestedDismissalIsolation.md) distinguishes
|
||||
visible/focused interaction from ownership of its dismissal handler. A guard can
|
||||
prevent the wrong close while losing valid input. Q05 remains open: lifecycle and
|
||||
input-routing contracts may need to shape composition before focus-return rules.
|
||||
No new pattern or canonical dimension follows from one synthetic family.
|
||||
|
|
|
|||
|
|
@ -58,3 +58,10 @@ build plus another browser before assigning cause. Preserve the immediate path a
|
|||
a regression probe; test explicit readiness/ownership rather than adding a delay.
|
||||
Screen-reader behavior remains untested. Feature/release coupling inspection and
|
||||
navigation/history/cache checks remain separate pending work.
|
||||
|
||||
## Follow-up disposition
|
||||
|
||||
[Nested isolation](NestedDismissalIsolation.md) now captures the stale-listener
|
||||
interval and compares builds/browsers. The mechanism is located in an instrumented
|
||||
fixture; a complete product remedy remains open. This supersedes the earlier lack
|
||||
of timing diagnosis, while preserving the original receipts and their limitations.
|
||||
|
|
|
|||
95
research/reviews/NestedDismissalIsolation.md
Normal file
95
research/reviews/NestedDismissalIsolation.md
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# Nested dismissal isolation — 2026-09-26
|
||||
|
||||
Status: controlled follow-up to [boundary recovery](BoundaryRecoveryExperiment.md),
|
||||
Codex editorial analysis. Same pinned React/Radix fixture, now compared across
|
||||
Chromium 151.0.7922.34 and Firefox 155.0, development and production builds.
|
||||
No package upgrade, deployment, model call or accessibility certification.
|
||||
|
||||
## Design and provenance
|
||||
|
||||
[isolation.mjs](../experiments/boundary-recovery/isolation.mjs) bundles the unchanged
|
||||
fixture with four variants, three fresh-page trials each, in fixed order. The
|
||||
[48-trial receipt](../experiments/boundary-recovery/isolation-results.json) retains
|
||||
successes, failures, focus snapshots, open/close transitions and layer event traces.
|
||||
It hashes runner, fixture, lockfile, inspected dependency source and every bundle.
|
||||
[Additional provenance](../experiments/boundary-recovery/isolation-provenance.json)
|
||||
records Firefox build/executable hash and the initial sandbox launch failure.
|
||||
|
||||
- **raw:** unmodified dependencies, no layer tracing, immediate browser Escape
|
||||
after focus arrives and a before-event snapshot is captured.
|
||||
- **traced:** in-memory instrumentation logs layer registration, listener changes
|
||||
and Escape handling; no intended change to vendor control flow.
|
||||
- **ready:** same instrumentation; additionally waits until only the inner layer's
|
||||
Escape listener is installed. This uses private instrumentation, not a public API.
|
||||
- **guarded:** traced build adds a current-top-layer check at event handling time;
|
||||
stale listeners return without dismissing. This is experimental modified behavior.
|
||||
|
||||
All variants retain keyboard Enter/Tab/Escape interaction and assert inner return
|
||||
followed by outer return. No fixed settling sleep or hidden retry. Extra snapshots,
|
||||
tracing and readiness observation can change scheduling. Results are exploratory
|
||||
counts, not failure rates. Fixed ordering and three trials do not establish browser
|
||||
or build superiority. Production mode changes React build selection, not the fixture
|
||||
into a production application. The historical eight-case runner/results are unchanged.
|
||||
|
||||
## Results
|
||||
|
||||
Pass means both expected return targets were reached in order.
|
||||
|
||||
| Browser | Build | raw | traced | ready | guarded |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Chromium | development | 2/3 | 2/3 | 3/3 | 3/3 |
|
||||
| Chromium | production | 3/3 | 1/3 | 3/3 | 2/3 |
|
||||
| Firefox | development | 3/3 | 3/3 | 3/3 | 3/3 |
|
||||
| Firefox | production | 3/3 | 3/3 | 3/3 | 3/3 |
|
||||
|
||||
43/48 obligations met. Keep conditions separate: a readiness wait changes the
|
||||
precondition, and the guard changes the implementation. An unmodified production
|
||||
failure was not observed in these three Chromium trials; failures with tracing
|
||||
show the timing problem is not restricted to a development bundle, but instrumentation
|
||||
may influence whether its window is hit. Firefox's passes do not prove immunity.
|
||||
|
||||
## Captured mechanism
|
||||
|
||||
Chromium/development/traced trial 1 records this order:
|
||||
|
||||
1. Outer Escape listener is installed.
|
||||
2. Inner layer is registered; focus reaches its close button.
|
||||
3. Escape invokes the **outer** listener while the registered layer stack already
|
||||
ends in the inner layer.
|
||||
4. The outer listener is removed and the inner listener installed afterward.
|
||||
5. Outer-close transition removes both dialogs; focus returns to the outer invoker.
|
||||
|
||||
This directly locates a stale Escape-owner interval in the instrumented fixture.
|
||||
The shipped dismissable-layer module selects its listener using render-time
|
||||
`isHighestLayer` and effect registration; event-time handling does not recheck the
|
||||
current layer stack. Source hash matches the earlier inspected package. This
|
||||
explains the captured wrong-owner dismissal; it is not a universal diagnosis of
|
||||
all focus bugs or a claim about every Radix version/application.
|
||||
|
||||
Chromium/production/guarded trial 1 rejects the stale outer handler while focus is
|
||||
inside the inner dialog. The inner listener is installed later. Both dialogs remain
|
||||
open: the guard preserves the outer task but loses this Escape action. A check that
|
||||
prevents a wrong transition is insufficient when it cannot deliver the valid action.
|
||||
|
||||
The readiness variant passes 12/12 by waiting for observed ownership before Escape.
|
||||
It supports distinguishing focus arrival from event-owner readiness. It does not
|
||||
establish a product solution: users cannot query this private trace, and a product
|
||||
must define how input during the ownership transition is accepted, routed or deferred.
|
||||
|
||||
## Pattern disposition and next work
|
||||
|
||||
Refine [Focus Return](../candidates/FocusReturn.md): test both **which task may close**
|
||||
and **where focus returns**, including input during activation and teardown. Neither
|
||||
visibility nor focus alone proves the dismissal handler belongs to the active task.
|
||||
Avoid arbitrary sleeps and avoid treating ignored input as successful recovery.
|
||||
Keep hypothesis maturity; this is still one synthetic implementation family.
|
||||
|
||||
No new pattern is needed yet. The broader question is whether temporal ownership
|
||||
belongs in the containing modal/interaction contract. A production repair would need
|
||||
stable ownership/routing plus tests for opening, nesting, removal and teardown;
|
||||
this study has not supplied or deployed such a repair. No upstream issue was sent.
|
||||
|
||||
WP-0003 T01 retains a bounded residual: evaluate a lifecycle-safe ownership strategy
|
||||
without arbitrary delay and verify ordinary/nested dismissal in both browsers before
|
||||
recommending it. Feature/import versus release-boundary research and navigation
|
||||
checks remain open; this focused investigation does not complete those tasks or M2.
|
||||
|
|
@ -97,3 +97,19 @@ browser, verify readiness/ownership without arbitrary delay. Do not mark its ass
|
|||
resolved from the settled run. Feature/release inspection and navigation checks remain
|
||||
pending. T02/T03 unchanged, sixteen hypotheses, no independent human validation or
|
||||
M1/M2 completion. See research/reviews/BoundaryRecoveryExperiment.md; no model calls.
|
||||
|
||||
## Nested dismissal isolated across browsers/builds — 2026-09-26
|
||||
|
||||
T01 progress: unchanged pinned fixture compared across Chromium 151.0.7922.34 and
|
||||
Firefox 155.0, development/production, raw/traced/readiness/experimental-guard variants,
|
||||
three trials each. 43/48 obligations met; all outcomes retained in isolation-results.json.
|
||||
Trace captures inner layer registration/focus before Escape listener ownership transfers:
|
||||
stale outer handler can close both tasks. Production traced failures reproduce the
|
||||
mechanism; unmodified production trials passed. Firefox passed 24/24, not immunity.
|
||||
|
||||
Private listener-readiness condition passes 12/12; guard loses input in one production
|
||||
trial and is not a fix. Original fixture/receipts unchanged. See
|
||||
research/reviews/NestedDismissalIsolation.md. Residual under T01: assess lifecycle-safe
|
||||
ownership/routing during activation and teardown before recommending a product repair.
|
||||
No upstream message, deployment, new inference, canonical promotion or M2 completion.
|
||||
Feature/release coupling and navigation evidence remain pending; T02/T03 unchanged.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue