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 @@
02Parity share

Any two distinct healthy shares reconstruct the handoff.

Declared topology

All single-domain losses leave two shares

Failure rehearsal

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.

+
Dependency review

Take the plan to the actual copies

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.

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); +});