Any two distinct healthy shares reconstruct the handoff.
diff --git a/README.md b/README.md
index 1193ed8c..1313be40 100644
--- a/README.md
+++ b/README.md
@@ -81,7 +81,7 @@ For the smallest deterministic reproduction, start with:
- [Repair a cold storage handoff](docs/EVIDENCE_SHARDS.md) — reconstruct canonical handoff bytes from any two of three separately stored shares, then recheck external trust.
- [Rehearse every recovery pair](docs/EVIDENCE_DRILL.md) — get a machine-readable readiness report before a damaged share becomes a loss.
- [Simulate storage failures](docs/EVIDENCE_PLACEMENT.md) — reveal declared shared sites, infrastructure, and custodians that can defeat two-of-three recovery.
-- [Explore the visual Resilience Lab](https://aruintelligence.github.io/aml-core/resilience-lab.html) — select a failure to see its blast radius, reopen a local placement manifest, and take that manifest to the verified CLI drill.
+- [Explore the visual Resilience Lab](https://aruintelligence.github.io/aml-core/resilience-lab.html) — select a failure, inspect the shared dependencies to change, reopen a local placement manifest, and take it to the verified CLI drill.
- [Undeniable proof demo](demos/undeniable-proof/)
- [Benchmarking protocol](BENCHMARKING.md)
- [Interoperability challenge](INTEROPERABILITY.md)
diff --git a/docs/EVIDENCE_PLACEMENT.md b/docs/EVIDENCE_PLACEMENT.md
index de46cc26..69f91d7f 100644
--- a/docs/EVIDENCE_PLACEMENT.md
+++ b/docs/EVIDENCE_PLACEMENT.md
@@ -4,6 +4,8 @@ The three-pair [recovery drill](EVIDENCE_DRILL.md) checks the files. It cannot t
Try the [visual Resilience Lab](resilience-lab.html) to explore shared dependencies. Select a failure row to highlight lost and surviving shares. Download a placement manifest and reopen it locally in the lab to review the same declarations later. The browser page runs a topology-only simulation with no share files or policy; the manifest is read in the browser and is not uploaded. Use the commands below for the full verified assessment.
+The lab's dependency review lists each declared label shared by multiple shares. For a two-of-three scheme, each distinct site, infrastructure, and custodian label must affect at most one share to survive every modeled single-domain loss. Its minimum label-change count is the sum of `(number of shares with a conflicting label − 1)` across those groups. It counts label assignments, not physical moves; a real move can affect several labels, and changing text without changing storage does nothing. Confirm real independence before updating a manifest.
+
Create a local JSON manifest with exactly three indexed entries:
```json
diff --git a/docs/resilience-lab.html b/docs/resilience-lab.html
index 4c850626..e965d1f6 100644
--- a/docs/resilience-lab.html
+++ b/docs/resilience-lab.html
@@ -15,6 +15,7 @@
.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.card{border:1px solid var(--line);border-radius:22px;background:linear-gradient(155deg,var(--panel2),var(--panel));padding:22px;min-width:0;transition:border-color .2s,background .2s}.card[data-outcome=lost]{border-color:var(--red);background:linear-gradient(155deg,#382532,#231e2a)}.card[data-outcome=survives]{border-color:var(--mint);background:linear-gradient(155deg,#173d3b,#112e34)}.card-head{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);padding-bottom:19px;margin-bottom:17px}.card-number{font-size:39px;line-height:1;font-weight:900;letter-spacing:-.06em}.card-head .tag{color:var(--mint)}.card[data-outcome=lost] .tag{color:var(--red)}.card[data-outcome=survives] .tag{color:var(--mint)}label{display:block;margin:13px 0 5px;color:var(--muted);font-size:12px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}input{width:100%;min-height:43px;border:1px solid var(--line);border-radius:9px;background:#0a1b26;color:var(--ink);padding:10px 12px}input:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}.hint{margin:17px 0 0;color:var(--muted);font-size:12px}.share-state{display:block;margin-top:14px;min-height:20px;font:800 11px/1.4 ui-monospace,monospace;letter-spacing:.1em;text-transform:uppercase}.card[data-outcome=lost] .share-state{color:var(--red)}.card[data-outcome=survives] .share-state{color:var(--mint)}
.result{display:grid;grid-template-columns:minmax(250px,.7fr) 1.3fr;gap:15px;margin-top:24px}.verdict,.scenarios{border:1px solid var(--line);border-radius:22px;background:var(--panel);padding:25px}.verdict h2{font-size:clamp(26px,3.2vw,42px);line-height:1.1;letter-spacing:-.04em;margin:18px 0}.verdict p{color:var(--muted)}.verdict[data-state=pass] h2{color:var(--mint)}.verdict[data-state=fail] h2{color:var(--red)}.verdict[data-state=incomplete] h2{color:var(--amber)}.count{display:block;color:var(--ink);font-size:13px;font-weight:800;margin:25px 0 8px}.progress{height:9px;background:#2b4350;border-radius:20px;overflow:hidden}.progress span{display:block;height:100%;background:var(--mint);transition:width .25s}.verdict[data-state=fail] .progress span{background:var(--red)}.verdict[data-state=incomplete] .progress span{background:var(--amber)}
.scenarios h2{margin:0 0 4px;font-size:23px;letter-spacing:-.03em}.scenarios .intro{margin:0 0 16px;color:var(--muted);font-size:13px}.rows{display:grid;gap:8px}.row{display:grid;width:100%;text-align:left;color:var(--ink);grid-template-columns:130px minmax(0,1fr) auto;gap:10px;align-items:center;border:1px solid var(--line);border-radius:11px;padding:11px 13px;background:#0b1d28;font-size:13px;cursor:pointer}.row:hover,.row:focus-visible,.row[aria-pressed=true]{outline:2px solid var(--cyan);outline-offset:1px}.row .dimension{text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-size:10px;font-weight:900}.row .detail{min-width:0;overflow-wrap:anywhere}.row .detail small{display:block;color:var(--muted)}.row strong{color:var(--mint);font-size:11px;letter-spacing:.08em}.row.fail{border-color:#875456;background:#2d2028}.row.fail strong{color:var(--red)}
+ .repair{margin-top:18px;border:1px solid var(--line);border-radius:22px;background:#102633;padding:25px}.repair h2{margin:7px 0;font-size:clamp(22px,3vw,30px);letter-spacing:-.035em}.repair p{max-width:800px;margin:4px 0 17px;color:var(--muted);font-size:14px}.repair-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:10px}.repair-item{border:1px solid #875456;background:#2d2028;border-radius:12px;padding:16px}.repair-item strong{display:block;color:var(--red);overflow-wrap:anywhere}.repair-item span{display:block;color:var(--ink);margin-top:6px;font-size:13px}.repair[data-state=ready] .repair-list{display:none}.repair[data-state=ready] h2{color:var(--mint)}
.handoff{margin-top:24px;padding:24px;border:1px solid var(--line);border-radius:22px;background:#102633;display:flex;justify-content:space-between;align-items:center;gap:20px}.handoff h2{margin:0 0 6px;font-size:20px}.handoff p{margin:0;color:var(--muted);max-width:700px;font-size:14px}.handoff-actions{display:flex;gap:9px;flex-wrap:wrap}.download{white-space:nowrap;background:var(--cyan);color:#06232b}.download:disabled{opacity:.5;cursor:not-allowed}.file-input{position:absolute;width:1px;height:1px;opacity:0;overflow:hidden}.import-status{font-size:12px;color:var(--amber);margin-top:10px!important}.scope{padding:28px 0 0;color:var(--muted);font-size:13px;max-width:900px}footer{border-top:1px solid var(--line);margin-top:45px;padding-top:20px;color:var(--muted);font-size:12px}
@media(max-width:800px){.shell{padding:18px 15px 55px}.hero{padding:48px 0 30px}.cards,.result{grid-template-columns:1fr}.handoff{align-items:flex-start;flex-direction:column}.row{grid-template-columns:95px minmax(0,1fr) auto}.navlinks{gap:8px}}
@media(prefers-reduced-motion:reduce){.progress span{transition:none}}
@@ -31,6 +32,7 @@
Any two distinct healthy shares reconstruct the handoff. Select a failure to see its impact on all three shares. Each row removes every share with the same declared label. A recovery path needs at least two survivors. Import or download a placement manifest locally, then run the JavaScript or independent Python assessment against three retrieved share files and your separately held trusted policy. The lab alone cannot mark storage ready.All single-domain losses leave two shares
Failure rehearsal
Take the plan to the actual copies
The full assessment recomputes the three-pair drill under an external accepted head and tests recovery after each declared loss. Keep the trust policy outside the shares. A passing result remains a project-authored local check, not independent custody proof.
@@ -49,6 +51,15 @@ } return {valid:true,ready:scenarios.every(scenario=>scenario.recoveryPossible),scenarios}; } + function analyzePlacementChanges(shares){ + const report=simulatePlacementTopology(shares); + if(!report.valid)return {valid:false,minimumLabelChanges:0,conflicts:[]}; + const conflicts=report.scenarios.filter(scenario=>!scenario.recoveryPossible).map(scenario=>({ + dimension:scenario.dimension,value:scenario.value,shares:scenario.lost, + keep:scenario.lost[0],separate:scenario.lost.slice(1) + })); + return {valid:true,minimumLabelChanges:conflicts.reduce((count,item)=>count+item.separate.length,0),conflicts}; + } function parsePlacementManifest(text){ if(typeof text!=="string"||text.length>65536)throw new Error("Manifest is too large (64 KB maximum)."); let manifest; @@ -93,6 +104,17 @@ document.getElementById("explanation").textContent=!result.valid?"Every share needs a site, infrastructure, and custodian label.":active?`${active.dimension}: ${active.value} takes ${active.lost.length} share${active.lost.length===1?"":"s"} offline; ${active.survivors.length} remain. This is a declared scenario, not a physical verification.`:result.ready?"The declared topology leaves at least two copies after each modeled loss. Retrieve the real files and run the full verifier.":"At least one modeled loss removes two or more shares. Separate that dependency and test again."; document.getElementById("count").textContent=`${passing} / ${result.scenarios.length} declared failure cases retain two shares`; document.getElementById("bar").style.width=`${result.scenarios.length?100*passing/result.scenarios.length:0}%`; + const changes=analyzePlacementChanges(read()); + const repair=document.getElementById("repair");repair.dataset.state=!changes.valid?"incomplete":changes.conflicts.length?"conflict":"ready"; + document.getElementById("repair-title").textContent=!changes.valid?"Complete the placement map":changes.conflicts.length?`${changes.conflicts.length} shared ${changes.conflicts.length===1?"dependency needs":"dependencies need"} review`:"No shared single-domain dependency declared"; + document.getElementById("repair-summary").textContent=!changes.valid?"Add all nine labels to calculate the dependency review.":changes.conflicts.length?`At least ${changes.minimumLabelChanges} label assignment${changes.minimumLabelChanges===1?"":"s"} must represent genuinely independent domains to clear these modeled failures. Move or independently provision the real copies before changing the declarations. A label edit alone does not repair storage.`:"All declared single-domain losses leave a pair. Retrieve the real copies and verify recovery under the external trust policy."; + const repairList=document.getElementById("repair-list");repairList.replaceChildren(); + for(const conflict of changes.conflicts){ + const item=document.createElement("div");item.className="repair-item"; + const title=document.createElement("strong");title.textContent=`${conflict.dimension}: ${conflict.value}`; + const detail=document.createElement("span");detail.textContent=`Shares ${conflict.shares.map(n=>`0${n}`).join(" + ")} fail together. Keep 0${conflict.keep} in this domain; place ${conflict.separate.map(n=>`0${n}`).join(" and ")} in ${conflict.separate.length===1?"an independent domain":"separate independent domains"}.`; + item.append(title,detail);repairList.appendChild(item); + } document.getElementById("download").disabled=!result.valid; } function preset(kind){ diff --git a/test/resilience-lab.test.js b/test/resilience-lab.test.js index 9fbf7ed7..5b2f074e 100644 --- a/test/resilience-lab.test.js +++ b/test/resilience-lab.test.js @@ -10,6 +10,7 @@ const context = vm.createContext({}); vm.runInContext(script, context, { timeout: 1000 }); const simulate = context.simulatePlacementTopology; const parseManifest = context.parsePlacementManifest; +const analyzeChanges = context.analyzePlacementChanges; const separated = [0, 1, 2].map(index => ({ index, site: `site-${index}`, infrastructure: `system-${index}`, custodian: `keeper-${index}` })); @@ -56,3 +57,27 @@ test("manifest import rejects malformed, oversized, or incomplete declarations", assert.throws(() => parseManifest(JSON.stringify({ ...manifest, shares: [{ ...separated[0], site: " " }, separated[1], separated[2]] })), /nonempty/); assert.throws(() => parseManifest(JSON.stringify({ ...manifest, shares: [{ ...separated[0], site: "x".repeat(129) }, separated[1], separated[2]] })), /128/); }); + +test("dependency review identifies the minimum label changes for separate single-domain survival", () => { + assert.equal(analyzeChanges(separated).minimumLabelChanges, 0); + const shared = structuredClone(separated); + shared[1].site = shared[0].site; + shared[2].custodian = shared[0].custodian; + const result = analyzeChanges(shared); + assert.equal(result.minimumLabelChanges, 2); + assert.deepEqual(JSON.parse(JSON.stringify(result.conflicts)), [ + { dimension: "site", value: "site-0", shares: [0, 1], keep: 0, separate: [1] }, + { dimension: "custodian", value: "keeper-0", shares: [0, 2], keep: 0, separate: [2] } + ]); + assert.match(html, /A label edit alone does not repair storage/); +}); + +test("three shares in one domain require two independent assignments", () => { + const shared = structuredClone(separated); + shared[1].infrastructure = shared[0].infrastructure; + shared[2].infrastructure = shared[0].infrastructure; + const result = analyzeChanges(shared); + assert.equal(result.minimumLabelChanges, 2); + assert.deepEqual(Array.from(result.conflicts[0].separate), [1, 2]); + assert.equal(analyzeChanges([{ ...separated[0], site: "" }, separated[1], separated[2]]).valid, false); +});