diff --git a/CITATION.cff b/CITATION.cff
index 9a8990e..d9e0d87 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -2,7 +2,7 @@ cff-version: 1.2.0
title: Frontier Decision Engine
message: Please cite the software and the specific dataset or case version used.
type: software
-version: 0.5.15
+version: 0.5.16
authors:
- name: Bridge Node 7
license: Apache-2.0
diff --git a/README.md b/README.md
index 09ff4bd..de527c1 100644
--- a/README.md
+++ b/README.md
@@ -10,15 +10,13 @@
## Start with one input
-The first screen asks one question: **What are you considering?**
+The public root asks one question: **What are you considering?**
-Share the situation, decision, question, or context in your own words. **Natural-language intake currently supports English.** FDE then does one of three things:
+Type or paste relevant context in ordinary language and choose **Continue**. For ordinary in-scope material, FDE returns a working **Decision Brief** immediately rather than beginning a compulsory clarification sequence. The brief separates the decision or focus, what matters, what remains unclear, and the next useful move. Sparse or multi-decision context can still produce a partial brief; unsupported information remains explicit instead of being silently invented.
-- shows only decision structure that is supportable from the words provided;
-- asks exactly one useful clarification question when the decision is still too sparse; or
-- states an honest capability boundary and a useful next action when the request is outside the browser-local decision-support scope.
+The original source text remains preserved as working context. Candidate decisions, choices, criteria, and conditions may be broader than the formal model. When the person voluntarily opens **Compare options**, they select the bounded set for one deterministic comparison: 2–4 objectives, 2–3 strategies, and 2–4 scenarios. This bounds the computation without silently discarding the source context.
-When explicit language identifies a requirement, time gate, dependency, conditional, or blocker that may control the decision, FDE can surface it as a provisional **decision hinge** with source provenance. The person confirms, rejects, or marks the condition uncertain before it can affect bounded comparison inputs. When a consequential decision needs more structure first, **Guided framing** captures the decision, criteria, choices, and uncertainties before handing confirmed inputs into the same Decision Lab. Supportable first-run structure uses four human-facing concepts: **Decision, What matters, Choices, What may change.** Nothing inferred becomes a canonical model input until a person confirms it.
+When explicit language identifies a requirement, time gate, dependency, conditional, or blocker that may control the decision, FDE can surface a provisional **decision hinge** with source provenance. It does not affect formal comparison inputs until the person explicitly keeps it or leaves it uncertain during comparison setup.
The browser does not call a remote AI provider, retrieve outside facts, invent evidence, probabilities, scores, thresholds, scenario effects, or recommendations.
@@ -32,7 +30,7 @@ Accepted context stays in JavaScript page memory only and is discarded on refres
## Decision Lab
-FDE is the human-governed decision layer within Bridge Node 7's Frontier Mission Assurance architecture. Materials-to-Mission and other assurance profiles can contribute governed evidence and dependency context; Governed Context carries bounded preparation context into FDE; the accountable human records the consequential decision and reassessment. After confirmation, FDE asks only for the additional explicit inputs required for a deterministic comparison. The formal Decision Lab preserves the existing ranking semantics, published schemas, saved-work protections, and truthful alternate outcomes including ties, no acceptable option, and insufficient information.
+FDE is the human-governed decision layer within Bridge Node 7's Frontier Mission Assurance architecture. Materials-to-Mission and other assurance profiles can contribute governed evidence and dependency context; Governed Context carries bounded preparation context into FDE; the accountable human records the consequential decision and reassessment. Formal comparison is optional. A single comparison-setup surface prepares the bounded explicit inputs required by the existing deterministic Decision Lab. The Decision Lab preserves the existing ranking semantics, published schemas, saved-work protections, and truthful alternate outcomes including ties, no acceptable option, and insufficient information.
The comparison informs. A person decides. The result surface leads with what held up, why, what could change it, the next required proof, and the next action before exposing detailed calculations. A lightweight Decision Brief can be copied or downloaded before recording; when the working decision later changes, FDE shows a deterministic reassessment delta and preserves prior valid Receipt v2 records in a bounded **Local Decision Receipt Archive** before a new Receipt is recorded. The archive preserves independently verifiable local receipts; it is not a cryptographic continuity chain.
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 8d740e9..ec7667b 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -6,13 +6,11 @@ Frontier Decision Engine is the human-governed decision layer of Bridge Node 7's
The public root is a one-input decision experience for technical, organizational, mission, and strategic decisions. A person can start in ordinary language without learning decision-science vocabulary first.
-After Continue, the runtime must resolve to exactly one of three states:
+After **Continue**, ordinary in-scope text resolves to a working **Decision Brief**. The brief is a valid stopping point. It presents only supportable structure: the decision or focus, what matters, what remains unclear, and the next useful move. Sparse, ambiguous, or multi-decision input remains useful as a partial brief rather than forcing a question-by-question completion path.
-1. supportable decision structure;
-2. exactly one useful clarification question; or
-3. an honest capability boundary plus a useful next action.
+A pre-interpretation scope boundary still runs before brief generation. High-confidence personal safety language receives the established safety boundary; otherwise first-person personal-life decisions are redirected neutrally unless explicit organizational or decision-support context makes the work in scope. Information-only requests receive the established no-retrieval boundary. These are product-scope decisions, not clinical judgments.
-The first-run structure uses stable semantic concepts: `decision`, `what_matters`, `options`, `what_may_change`, `status`, and `next_required_input`. A provisional decision hinge may be derived only from explicit requirement, time-gate, dependency, conditional, or blocker language. It preserves the exact source span, begins with no formal influence, and requires human confirmation before it can populate a bounded formal input. Human-facing labels are **Decision, What matters, Choices, What may change**. Only fields actually supported by explicit input are rendered.
+The first-run layer preserves the original source text and candidate decisions, choices, criteria, conditions, and provisional decision hinge. Candidate capture can be broader than the formal comparison. The Decision Brief is working context, not a Decision Receipt and not evidence by itself.
A Decision Map remains an internal architecture/export concept where it improves technical precision. It is not the pre-input first-run surface.
@@ -20,15 +18,17 @@ A Decision Map remains an internal architecture/export concept where it improves
FDE follows this interaction rule:
-> Continue without bothering the person when the system can proceed safely; otherwise ask for the smallest human contribution that materially advances the decision.
+> Produce useful structure before asking the person to operate the formal model.
-No ordinary in-scope input is treated as an error-only dead end. Sparse or ambiguous input receives one clarification question. A hinge-derived question may preempt the ordinary schema order only when the answer can deterministically change a requirement, criterion, evidence need, choice, constraint, or modeled future; otherwise the deterministic choices → goals → futures fallback remains in force. A pre-interpretation scope boundary keeps FDE inside its technical, organizational, mission, and strategic product domain. High-confidence personal safety language receives the established safety boundary; otherwise first-person personal-life decisions are redirected neutrally unless explicit organizational or decision-support context makes the work in scope. The neutral redirect is a product-scope decision, not a clinical judgment.
+**Refine** returns to the same freeform source context. It does not open another structured questionnaire. Formal depth is voluntary through **Compare options**.
+
+The optional comparison setup shows the complete bounded formal setup on one surface. The person selects one decision when needed, 2–3 strategies, 2–4 objectives, and 2–4 scenarios. Additional source candidates remain preserved outside the active formal set. A provisional decision hinge may enter a formal criterion or modeled future only after explicit human handling.
## Formal Decision Lab
-Once enough explicit structure exists and the person confirms it, the information is handed into the existing deterministic Decision Lab. Guided comparison supports 2–4 objectives, 2–3 strategies, and 2–4 scenarios. The minimum comparison is a true 2 × 2 × 2 model.
+After the person deliberately opens a valid bounded comparison, FDE creates the existing guided Decision Case and hands it into the existing deterministic Decision Lab. Guided comparison remains 2–4 objectives, 2–3 strategies, and 2–4 scenarios. The minimum comparison remains a true 2 × 2 × 2 model.
-The comparison core is isolated from the first-run layer so UX changes do not silently alter ranking semantics. Missing analytical values are not fabricated.
+The comparison core is isolated from the first-run layer so UX changes do not silently alter ranking semantics. Missing analytical values are not fabricated. Saved formal work is not silently overwritten by a new first-run brief.
## Epistemic boundaries
diff --git a/docs/METHODOLOGY.md b/docs/METHODOLOGY.md
index d440fbe..402db1c 100644
--- a/docs/METHODOLOGY.md
+++ b/docs/METHODOLOGY.md
@@ -2,13 +2,13 @@
Frontier Decision Engine operationalizes the human decision layer of Frontier Mission Assurance: choices, criteria, evidence state, dependencies, uncertainty, thresholds, conditions, and reassessment remain inspectable. It uses a transparent scenario comparison rather than a hidden prediction model.
-1. State the decision or situation in ordinary language.
-2. When explicit language supports it, identify and confirm the condition most capable of changing the decision.
-3. Define what needs to go well and what counts as good enough.
-4. Name realistic choices.
-5. Name plausible futures that could change the answer.
-6. Enter explicit performance assumptions and future-specific changes.
-7. Compare threshold passes, critical failures, and vulnerabilities.
+1. State the decision, situation, question, or context in ordinary language.
+2. Review the supportable Decision Brief: decision or focus, what matters, what remains unclear, and the next useful move.
+3. Refine the same source context only when more framing would materially help.
+4. When formal comparison is warranted, select one bounded comparison set: realistic choices, criteria, and plausible futures or conditions.
+5. When explicit language supports it, explicitly keep, leave uncertain, or remove a provisional decision hinge before it can affect the formal model.
+6. Enter explicit performance assumptions, thresholds, evidence state, and future-specific changes in Decision Lab.
+7. Compare threshold passes, critical failures, vulnerabilities, ties, no-acceptable-choice states, and insufficient-information states.
8. Review the result-first decision value: what held up, why, what could change it, and any required next proof.
9. Re-evaluate scope policy on the operative decision/action fields immediately before accountable recording.
10. Record the human-owned decision, rationale, and next action.
diff --git a/docs/PILOT_READINESS.md b/docs/PILOT_READINESS.md
index 9baf4f5..6f52c70 100644
--- a/docs/PILOT_READINESS.md
+++ b/docs/PILOT_READINESS.md
@@ -11,7 +11,7 @@ FDE pilots test whether accountable decision-makers find the decision architectu
5. Proof request: required unresolved evidence can stop additional scoring and surface the human-declared next proof.
6. Privacy: public FDE is for public or sanitized information only; browser storage is not encrypted confidential storage.
7. Decision surface: the final view leads with the supportable prepared bottom line and controlling issue before deeper detail, while making clear that the accountable human still decides.
-8. Human burden: a participant can complete the workflow without BN7 narrating the product, and the pilot records where avoidable clarification or reconstruction burden remains.
+8. Human burden: the first Continue action produces a useful Decision Brief for ordinary in-scope text, the brief is a valid stopping point, and formal comparison is entered only when the participant chooses deeper analysis.
9. Reconstruction: an independent reviewer can understand the receipt from the artifact alone.
10. Provenance: application version, schema version, and receipt digests are preserved.
11. Reuse intent: the pilot asks whether the participant would voluntarily use FDE on another real decision.
diff --git a/docs/PRIVACY.md b/docs/PRIVACY.md
index fe09e7d..fae7780 100644
--- a/docs/PRIVACY.md
+++ b/docs/PRIVACY.md
@@ -2,7 +2,7 @@
Frontier Decision Engine is browser-local by default. The public application has no backend, account system, analytics, telemetry, cookies, remote AI provider, or default upload endpoint.
-Guided framing uses tab-scoped session storage to recover in-progress framing after an accidental refresh. Decision Lab uses bounded browser autosave for structured work and, after a re-record, a bounded **Local Decision Receipt Archive** for prior valid Decision Receipt v2 records. Clearing the current draft does not erase that archive. The archive is local convenience storage, not a cryptographic continuity chain, and browser/profile deletion can remove it. Browser-local storage is not encrypted confidential storage.
+The public first-run Decision Brief uses bounded tab-scoped session storage to recover in-progress source context after an accidental refresh. Guided framing remains available as a separate route. Decision Lab uses bounded browser autosave for structured work and, after a re-record, a bounded **Local Decision Receipt Archive** for prior valid Decision Receipt v2 records. Clearing the current draft does not erase that archive. The archive is local convenience storage, not a cryptographic continuity chain, and browser/profile deletion can remove it. Browser-local storage is not encrypted confidential storage.
Local decision files are processed in the browser. Do not enter information that requires an approved confidential, controlled, or otherwise restricted environment.
diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md
index 5227eb2..f47bda3 100644
--- a/docs/RELEASE_NOTES.md
+++ b/docs/RELEASE_NOTES.md
@@ -1,16 +1,20 @@
-# v0.5.15
+# v0.5.16
-Application version is 0.5.15.
+Application version is 0.5.16.
Compatible decision schema: 0.2.10. Semantic decision schema: 0.3.0. Mission Graph Decision Context Packet: 0.3.0. Decision Receipt format: 2. Pilot Proof Envelope: 1.
-This corrective UX release makes the supportable prepared bottom line the first substantive result on the final decision surface and in the downloadable Decision Brief.
+This release refines the public first-run experience while preserving the deterministic Decision Lab and accountable decision boundaries.
-- Required unresolved evidence produces a HOLD prepared bottom line even when another declared posture would otherwise permit advancement.
-- STOP, HOLD, REWORK, ADVANCE WITH CONDITIONS, and ADVANCE retain their existing semantic authority and are translated into plain-language prepared output.
-- An inactive assurance posture remains comparison-only rather than being promoted into an automated recommendation.
-- The visible prepared bottom line shows the controlling issue, the next required proof when one exists, and an explicit reminder that the accountable human still decides.
-- Browser UAT now requires the prepared bottom-line surface and its human-authority boundary.
-- The Decision Case schemas, Mission Graph context contract, Decision Receipt v2 format, ranking semantics, reassessment behavior, authority roles, and portable interfaces are unchanged. Accountable human judgment remains authoritative.
+- The public root keeps one freeform input: **What are you considering?**
+- **Continue** now produces a working Decision Brief for ordinary in-scope text rather than beginning a compulsory clarification sequence.
+- Sparse and multi-decision context can produce a partial brief while unresolved structure remains explicit.
+- Original source context and detected candidate decisions, choices, criteria, and conditions remain preserved before formal comparison.
+- Formal comparison is voluntary and uses one bounded setup surface before entering the existing Decision Lab.
+- The formal comparison contract remains 2–4 objectives, 2–3 strategies, and 2–4 scenarios.
+- Provisional decision hinges remain source-derived and require explicit human handling before they can affect formal comparison inputs.
+- Existing saved Decision Lab work is not silently overwritten by a new first-run brief.
+- Product-scope, safety, evidence-readiness, authority, Decision Receipt v2, governed-context, reassessment, and ranking semantics remain unchanged.
+- The Bridge Node 7 shell is quieter and more consistent with the institutional website while preserving product navigation.
-Browser-local storage is not encrypted confidential storage. Public deployment is for public or sanitized decision material only.
+Browser-local storage is not encrypted confidential storage. Public deployment is for public or sanitized decision material only. Human judgment remains authoritative. The comparison informs; a person decides.
diff --git a/package-lock.json b/package-lock.json
index 90b5fa7..b178ce5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "frontier-decision-engine",
- "version": "0.5.15",
+ "version": "0.5.16",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frontier-decision-engine",
- "version": "0.5.15",
+ "version": "0.5.16",
"license": "Apache-2.0",
"engines": {
"node": ">=22"
diff --git a/package.json b/package.json
index 5f351d1..4fc973a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "frontier-decision-engine",
- "version": "0.5.15",
+ "version": "0.5.16",
"private": true,
"type": "module",
"description": "Browser-local, human-governed decision infrastructure for Frontier Mission Assurance.",
diff --git a/project-facts.json b/project-facts.json
index 80d4966..6cc6735 100644
--- a/project-facts.json
+++ b/project-facts.json
@@ -1,5 +1,5 @@
{
- "applicationVersion": "0.5.15",
+ "applicationVersion": "0.5.16",
"schemaVersions": {
"decision": "0.2.10",
"semanticDecision": "0.3.0"
diff --git a/scripts/browser_e2e_release.py b/scripts/browser_e2e_release.py
index c38c359..2932c35 100644
--- a/scripts/browser_e2e_release.py
+++ b/scripts/browser_e2e_release.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-"""Release wrapper for the retained browser E2E suite with the v0.3.3 root-route contract."""
+"""Release wrapper for the retained browser E2E suite and current root-route contract."""
from __future__ import annotations
import browser_e2e as suite
@@ -32,13 +32,13 @@ def no_js_one_page(browser) -> None:
"How it works",
"Decision Lab",
"Appearance",
+ "Partner",
"What are you considering?",
- "Share the situation, decision, question, or context in your own words.",
- "Advanced paths",
- "Private by design. Your working decision stays in this browser unless you choose to export it.",
+ "JavaScript is required for the browser-local decision workspace.",
"Human authority",
- "FDE structures and calculates; it does not approve or authorize the decision.",
- "confidential or controlled information",
+ "FDE structures and calculates; it does not approve or authorize a decision.",
+ "public or sanitized material only",
+ "Privacy",
]:
assert text in body
for removed in ["Bring the whole mess", "Decision Map", "Find the decision"]:
diff --git a/scripts/browser_guided_framing_e2e.py b/scripts/browser_guided_framing_e2e.py
index 5488fcb..551b18a 100644
--- a/scripts/browser_guided_framing_e2e.py
+++ b/scripts/browser_guided_framing_e2e.py
@@ -1,274 +1,248 @@
#!/usr/bin/env python3
-"""Focused browser verification for the frictionless FDE first-run surface."""
+"""First-run browser verification for the Decision-Brief-first FDE experience."""
from __future__ import annotations
-import functools
-import http.server
import os
-import socketserver
-import threading
-from pathlib import Path
+from urllib.parse import urlparse
-from playwright.sync_api import sync_playwright
+try:
+ from playwright.sync_api import sync_playwright
+except ImportError as exc: # pragma: no cover
+ raise SystemExit(
+ "Browser E2E requires Playwright. Run: node scripts/run-python.mjs -m pip install "
+ "-r requirements-dev.txt and node scripts/run-python.mjs -m playwright install chromium"
+ ) from exc
-ROOT = Path(__file__).resolve().parents[1]
-SITE = ROOT / "site"
-SESSION_KEY = "fde.universal.session.v1"
-DECISION_KEY = "fde.decision.autosave.v0.2.11"
-GUIDED_SESSION_KEY = "fde.guided-framing.session.v1"
+from browser_e2e import browser_executable, install_static_route, native_http_available, start_static_server
+SESSION_KEY = "fde.universal.session.v2"
+OLD_SESSION_KEY = "fde.universal.session.v1"
+DECISION_STORAGE_KEY = "fde.decision.v1"
-class QuietHandler(http.server.SimpleHTTPRequestHandler):
- def log_message(self, *_args):
- pass
+def launch_browser(playwright):
+ executable = browser_executable()
+ kwargs = {"headless": True}
+ if executable:
+ kwargs["executable_path"] = executable
+ return playwright.chromium.launch(**kwargs)
-def browser_executable() -> str | None:
- return os.environ.get("CHROME_BIN") or None
+
+def reset(page, base: str) -> None:
+ page.goto(base, wait_until="networkidle")
+ page.evaluate("localStorage.clear(); sessionStorage.clear()")
+ page.goto(base, wait_until="networkidle")
+
+
+def uses_static_route(base: str) -> bool:
+ parsed = urlparse(base)
+ return parsed.scheme == "http" and parsed.hostname == "fde.test" and parsed.port is None
+
+
+def assert_first_view(page) -> None:
+ assert page.locator("#universal-title").inner_text() == "What are you considering?"
+ field = page.locator("#universal-input")
+ assert field.get_attribute("placeholder") == "Type or paste anything relevant…"
+ assert page.get_by_role("button", name="Continue").count() == 1
+ assert page.get_by_text("Advanced paths", exact=True).count() == 0
+ assert page.get_by_text("Share the situation, decision, question, or context in your own words.", exact=True).count() == 0
+
+
+def assert_brief(page) -> None:
+ assert page.locator("#universal-response-title").inner_text() == "Decision brief"
+ assert page.locator('[data-fde-field="decision"]').count() == 1
+ assert page.locator('[data-fde-field="what_matters"]').count() == 1
+ assert page.locator('[data-fde-field="uncertainty"]').count() == 1
+ assert page.locator('[data-fde-field="next_useful_move"]').count() == 1
+ assert page.locator(".universal-question").count() == 0
def run() -> None:
- handler = functools.partial(QuietHandler, directory=str(SITE))
- with socketserver.TCPServer(("127.0.0.1", 0), handler) as server:
- thread = threading.Thread(target=server.serve_forever, daemon=True)
- thread.start()
- base = f"http://127.0.0.1:{server.server_address[1]}/"
- try:
- with sync_playwright() as p:
- launch = {"headless": True}
- executable = browser_executable()
- if executable:
- launch["executable_path"] = executable
- browser = p.chromium.launch(**launch)
- try:
- context = browser.new_context(viewport={"width": 390, "height": 844})
- remote_requests: list[str] = []
- context.on("request", lambda request: remote_requests.append(request.url) if not request.url.startswith(base) else None)
- page = context.new_page()
- page.goto(base, wait_until="networkidle")
-
- assert page.title() == "Frontier Decision Engine"
- assert page.locator("h1").inner_text() == "What are you considering?"
- assert page.get_by_text("Share the situation, decision, question, or context in your own words.", exact=True).is_visible()
- assert page.get_by_role("button", name="Continue").is_visible()
- assert page.get_by_text("Advanced paths", exact=True).is_visible()
- page.get_by_text("Advanced paths", exact=True).click()
- assert page.get_by_role("link", name="Already know the decision and choices? Open Decision Lab →").is_visible()
- assert page.get_by_role("link", name="Need more help framing the decision? Use guided framing →").is_visible()
- assert page.get_by_text("Private by design. Your working decision stays in this browser unless you choose to export it.", exact=True).is_visible()
- assert page.locator("#universal-input").get_attribute("placeholder") == "Decision, choices, criteria, uncertainties, notes, or context…"
- assert page.locator(".universal-surface").count() == 0
- assert "Decision Map" not in page.locator("main").inner_text()
- assert "Bring the whole mess" not in page.locator("body").inner_text()
- assert page.locator("#theme-toggle").inner_text() == "Appearance"
- assert "Current:" in (page.locator("#theme-toggle").get_attribute("aria-label") or "")
- assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth + 1")
-
- # Long input stays visible and produces an explicit bounded-processing message.
- long_input = "x" * 12001
- page.locator("#universal-input").fill(long_input)
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-limit").is_visible()
- assert len(page.locator("#universal-input").input_value()) == len(long_input)
-
- # A grounded controlling condition is shown before schema completion.
- page.locator("#universal-input").fill("Should we qualify Supplier A or Supplier B? November is our integration window.")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("[data-fde-field='decision_hinge']").is_visible()
- assert page.get_by_text("November is our integration window", exact=True).is_visible()
- page.get_by_role("button", name="Yes").click()
- assert page.locator("#universal-response-title").inner_text() == "Does this condition need to be true for the decision?"
- page.get_by_role("button", name="Yes").click()
- assert "What else matters" in page.locator("#universal-response-title").inner_text()
- page.get_by_role("button", name="Adjust original input").click()
-
- # Explicit prohibitive hard requirements remain eligible hinge candidates.
- page.locator("#universal-input").fill("Should we proceed with this design? The system must not exceed 10 watts.")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("[data-fde-field='decision_hinge']").is_visible()
- assert page.get_by_text("must not exceed 10 watts", exact=True).is_visible()
- page.locator("#universal-adjust").click()
-
- # Sparse input yields one question, not an invalid state or empty structural cards.
- page.locator("#universal-input").fill("qualification evidence incomplete")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "Which decision or question should we focus on?"
- assert page.locator("[data-fde-field='next_required_input']").count() == 1
- assert page.locator("[data-fde-field='decision']").count() == 0
- assert "Invalid input" not in page.locator("body").inner_text()
-
- page.get_by_role("button", name="Adjust original input").click()
- clear_input = "Should we qualify an alternate source or redesign around the dependency? Schedule risk and resilience matter, but qualification may be late. "
- page.locator("#universal-input").fill(clear_input)
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "Decision structure"
- assert page.get_by_text("Needs confirmation", exact=True).is_visible()
- assert page.locator("[data-fde-field='decision']").is_visible()
- assert page.locator("[data-fde-field='what_matters']").is_visible()
- assert page.locator("[data-fde-field='options']").is_visible()
- assert page.locator("[data-fde-field='what_may_change']").is_visible()
- assert page.get_by_text("qualify an alternate source", exact=True).is_visible()
- assert page.get_by_text("redesign around the dependency", exact=True).is_visible()
- assert page.get_by_text("Schedule risk", exact=True).is_visible()
- assert page.get_by_text("Resilience", exact=True).is_visible()
- assert page.get_by_text("Timing gets worse", exact=True).is_visible()
- assert page.locator("script").filter(has_text="alert(1)").count() == 0
-
- # Confirmation requests only the next required input.
- page.get_by_role("button", name="Yes").click()
- assert page.locator("#universal-response-title").inner_text() == "What else could change the choice?"
- page.locator("#universal-input").fill("Requirements change")
-
- # Saved-work protection: never silently replace a Decision Lab draft.
- page.evaluate(f"localStorage.setItem('{DECISION_KEY}', JSON.stringify({{sentinel:'keep-me'}}))")
- page.get_by_role("button", name="Continue").click()
- assert page.evaluate(f"localStorage.getItem('{DECISION_KEY}')") == '{"sentinel":"keep-me"}'
- assert page.locator("#universal-response-title").inner_text() == "A saved FDE decision already exists."
- assert page.get_by_role("link", name="Open Decision Lab →").is_visible()
-
- # A completely new decision entered during a follow-up must not inherit
- # criteria or options from the abandoned decision.
- page.evaluate(f"localStorage.removeItem('{DECISION_KEY}')")
- page.evaluate(f"sessionStorage.removeItem('{SESSION_KEY}')")
- page.goto(base, wait_until="networkidle")
- diligence_input = "Should we qualify a second gallium nitride wafer supplier in Japan or keep our current Chinese supplier? We care about cost, schedule risk, and DFARS compliance."
- page.locator("#universal-input").fill(diligence_input)
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "Decision structure"
- assert page.get_by_text("Cost", exact=True).is_visible()
- assert page.get_by_text("Schedule risk", exact=True).is_visible()
- assert page.get_by_text("Compliance", exact=True).is_visible()
- page.get_by_role("button", name="Yes").click()
- assert page.locator("#universal-response-title").inner_text() == "What conditions or uncertainties could change the choice?"
- page.locator("#universal-input").fill("Should we retire the legacy test stand?")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "Decision structure"
- assert page.get_by_text("Should we retire the legacy test stand", exact=True).is_visible()
- current_text = page.locator("main").inner_text()
- assert "Cost" not in current_text
- assert "Schedule risk" not in current_text
- assert "Compliance" not in current_text
- assert "Japanese supplier" not in current_text
- assert "Chinese supplier" not in current_text
-
- # Information-request UAT is a separate first-run scenario. Clear only the
- # tab-scoped intake session; the persistence behavior itself is tested below.
- page.evaluate(f"localStorage.removeItem('{DECISION_KEY}')")
- page.evaluate(f"sessionStorage.removeItem('{SESSION_KEY}')")
- page.goto(base, wait_until="networkidle")
- page.locator("#universal-input").fill("What is the current spot price of gallium?")
- page.get_by_role("button", name="Continue").click()
- assert "does not retrieve outside facts" in page.locator("#universal-response-title").inner_text().lower()
- assert "Gather the fact first" in page.locator("main").inner_text()
-
- # Self-directed crisis language fails closed before ordinary decision structuring.
- page.evaluate(f"localStorage.removeItem('{DECISION_KEY}')")
- page.evaluate(f"sessionStorage.removeItem('{SESSION_KEY}')")
- page.goto(base, wait_until="networkidle")
- page.locator("#universal-input").fill("I want to die.")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "This decision is outside FDE’s comparison scope."
- assert "does not compare or optimize self-harm" in page.locator("main").inner_text()
- assert "988" in page.locator("main").inner_text()
-
- # Organizational prevention decisions remain valid decision inputs.
- page.get_by_role("button", name="Adjust").click()
- page.locator("#universal-input").fill("Should the ministry fund suicide prevention hotlines or school counselors?")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "Decision structure"
-
- # Oversized choice sets get an explicit bounded-selection prompt.
- page.get_by_role("button", name="Adjust").click()
- page.locator("#universal-input").fill("Choose between Supplier Alpha, Supplier Bravo, an alternate material, a reserve, or subsystem redesign.")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "I found 5 possible choices. Choose up to 3 to compare."
-
- # Mission-focused criteria remain choices/criteria, not an ambiguous option list.
- page.evaluate(f"sessionStorage.removeItem('{SESSION_KEY}')")
- page.goto(base, wait_until="networkidle")
- page.locator("#universal-input").fill("Should we impose export controls or negotiate supply agreements with allies? National security, cost, and time matter.")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "Decision structure"
- assert page.get_by_text("National security", exact=True).is_visible()
- assert page.get_by_text("Cost", exact=True).is_visible()
- assert page.get_by_text("Time", exact=True).is_visible()
- assert page.get_by_text("impose export controls", exact=True).is_visible()
- assert page.get_by_text("negotiate supply agreements with allies", exact=True).is_visible()
-
- # Explicit criteria outside the built-in vocabulary stay visible for human confirmation.
- page.get_by_role("button", name="Adjust").click()
- page.locator("#universal-input").fill("Should we qualify Supplier Alpha or Supplier Bravo? Data residency, capex, measurement traceability, and corrosion resistance matter.")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "Decision structure"
- for criterion in ("Data residency", "Capex", "Measurement traceability", "Corrosion resistance"):
- assert page.get_by_text(criterion, exact=True).is_visible()
-
- # More criteria than the bounded draft can hold must never be silently truncated.
- page.get_by_role("button", name="Adjust").click()
- page.locator("#universal-input").fill("Should we qualify Supplier Alpha or Supplier Bravo? Data residency, capex, measurement traceability, corrosion resistance, and repairability matter.")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "I found 5 possible criteria. Choose up to 4 to keep."
-
- # Informational comparison language remains informational rather than inventing a choice set.
- page.get_by_role("button", name="Adjust original input").click()
- page.locator("#universal-input").fill("Explain qualification versus redesign for a new engineer.")
- page.get_by_role("button", name="Continue").click()
- assert "does not retrieve outside facts" in page.locator("#universal-response-title").inner_text().lower()
-
- # Ctrl/Cmd + Enter activates Continue.
- page.get_by_role("button", name="Adjust").click()
- page.locator("#universal-input").fill("Should we qualify the alternate source or hold for evidence? Mission safety and cost exposure matter. Schedule slips and demand changes are possible.")
- page.locator("#universal-input").press("Control+Enter")
- assert page.locator("#universal-response-title").inner_text() == "Decision structure"
-
- # Refresh preserves in-progress first-run work.
- page.get_by_role("button", name="Adjust").click()
- page.locator("#universal-input").fill("Refresh should not erase this qualification context.")
- assert page.evaluate(f"Boolean(sessionStorage.getItem('{SESSION_KEY}'))")
- page.reload(wait_until="networkidle")
- assert page.locator("#universal-input").input_value() == "Refresh should not erase this qualification context."
-
- # Guided framing enforces the same input boundary.
- page.evaluate(f"sessionStorage.removeItem('{GUIDED_SESSION_KEY}')")
- page.goto(f"{base}#/framing", wait_until="networkidle")
- page.locator("#guided-question").wait_for(state="visible")
- page.locator("#guided-intake").fill("Should I cut myself or call someone?")
- page.locator("#guided-start").click()
- assert "This decision is outside FDE’s comparison scope." in page.locator("#guided-status").inner_text()
- assert "988" in page.locator("#guided-status").inner_text()
- assert page.locator("#guided-question").inner_text() == "What decision context are you facing?"
-
- # Decision Lab enforces the same boundary before stage advancement.
- page.goto(f"{base}#/decision/new", wait_until="networkidle")
- page.locator("#decision-question").fill("I am suicidal and deciding what to do.")
- page.locator('[data-decision-stage="0"] [data-stage-next]').click()
- validation = page.locator('[data-stage-validation="0"]')
- assert "This decision is outside FDE’s comparison scope." in validation.inner_text()
- assert "988" in validation.inner_text()
- assert page.locator("#decision-question").get_attribute("aria-invalid") == "true"
-
- assert not remote_requests, f"FDE made unexpected remote requests: {remote_requests}"
- context.close()
-
- # Appearance follows system, then remains operable in dark mode.
- theme = browser.new_context(viewport={"width": 1280, "height": 900}, color_scheme="light")
- theme_page = theme.new_page()
- theme_page.goto(base, wait_until="networkidle")
- assert theme_page.locator("html").get_attribute("data-theme") == "light"
- assert theme_page.locator("html").get_attribute("data-theme-preference") == "system"
- assert theme_page.locator("#theme-toggle").inner_text() == "Appearance"
- theme_page.emulate_media(color_scheme="dark")
- theme_page.locator('html[data-theme="dark"]').wait_for(state="attached")
- assert theme_page.locator("html").get_attribute("data-theme-preference") == "system"
- assert theme_page.evaluate("document.documentElement.scrollWidth <= window.innerWidth + 1")
- theme.close()
- finally:
- browser.close()
- finally:
- server.shutdown()
- thread.join(timeout=5)
+ server, native_base = start_static_server()
+ try:
+ with sync_playwright() as playwright:
+ browser = launch_browser(playwright)
+ try:
+ base = native_base
+ if not native_http_available(browser, base, attempts=2):
+ base = "http://fde.test/"
+
+ context = browser.new_context(viewport={"width": 1280, "height": 900}, color_scheme="dark")
+ if uses_static_route(base):
+ install_static_route(context)
+
+ remote_requests: list[str] = []
+ page = context.new_page()
+ allowed_host = urlparse(base).netloc
+
+ def observe_request(request) -> None:
+ parsed = urlparse(request.url)
+ if parsed.scheme in {"http", "https"} and parsed.netloc != allowed_host:
+ remote_requests.append(request.url)
+
+ page.on("request", observe_request)
+
+ reset(page, base)
+ assert_first_view(page)
+ assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth + 1")
+
+ # Clear, supportable input returns value immediately instead of starting a questionnaire.
+ clear_input = (
+ "Should we qualify the alternate source or retain the incumbent? "
+ "Schedule risk, resilience, and cost matter. Delay and shortage are possible."
+ )
+ page.locator("#universal-input").fill(clear_input)
+ page.get_by_role("button", name="Continue").click()
+ assert_brief(page)
+ assert "qualify the alternate source" in page.locator('[data-fde-field="decision"]').inner_text().lower()
+ assert page.get_by_role("button", name="Compare options").count() == 1
+ assert page.get_by_role("button", name="Refine").count() == 1
+ assert page.get_by_role("button", name="Start another").count() == 1
+
+ # Refine returns to the same freeform source context with no new form.
+ page.get_by_role("button", name="Refine").click()
+ assert_first_view(page)
+ assert page.locator("#universal-input").input_value() == clear_input
+ page.locator("#universal-input").fill(clear_input + " Technical performance also matters.")
+ page.get_by_role("button", name="Continue").click()
+ assert_brief(page)
+ assert "Technical performance" in page.locator('[data-fde-field="what_matters"]').inner_text()
+
+ # Sparse input still yields a partial brief. No compulsory clarification screen appears.
+ page.get_by_role("button", name="Start another").click()
+ page.locator("#universal-input").fill("Qualification evidence is incomplete and the mission dependency is unclear.")
+ page.get_by_role("button", name="Continue").click()
+ assert_brief(page)
+ assert "not explicit" in page.locator('[data-fde-field="decision"]').inner_text().lower()
+ assert "decision itself" in page.locator('[data-fde-field="uncertainty"]').inner_text().lower()
+
+ # Multiple decisions are preserved rather than forcing a choose-one page.
+ page.get_by_role("button", name="Start another").click()
+ page.locator("#universal-input").fill(
+ "Should we qualify a second source? Should we redesign around the dependency? "
+ "Should we build strategic inventory?"
+ )
+ page.get_by_role("button", name="Continue").click()
+ assert_brief(page)
+ decision_text = page.locator('[data-fde-field="decision"]').inner_text()
+ assert "qualify a second source" in decision_text.lower()
+ assert "redesign around the dependency" in decision_text.lower()
+ assert "build strategic inventory" in decision_text.lower()
+ assert "3 decision questions preserved" in page.locator(".universal-preserved").inner_text().lower()
+
+ # Large candidate sets remain preserved before bounded formal comparison.
+ page.get_by_role("button", name="Start another").click()
+ page.locator("#universal-input").fill(
+ "Choose between Supplier Alpha, Supplier Bravo, an alternate material, a reserve, or subsystem redesign. "
+ "Cost, schedule, resilience, qualification, and compliance matter. Delay and shortage are possible."
+ )
+ page.get_by_role("button", name="Continue").click()
+ assert_brief(page)
+ preserved = page.locator(".universal-preserved").inner_text().lower()
+ assert "5 possible choices preserved" in preserved
+ assert "5 criteria preserved" in preserved
+
+ # Formal depth is voluntary and visible on one surface.
+ page.get_by_role("button", name="Compare options").click()
+ assert page.locator("#universal-response-title").inner_text() == "Compare options"
+ assert page.locator('legend').filter(has_text="Options").count() >= 1
+ assert page.locator('legend').filter(has_text="What matters").count() >= 1
+ assert page.locator('legend').filter(has_text="What may change").count() >= 1
+ assert page.get_by_role("button", name="Open Decision Lab").count() == 1
+ # Reduce the preselected choices/criteria to formal limits and provide a second scenario if needed.
+ choice_boxes = page.locator('input[name="formal-choice"]')
+ for index in range(choice_boxes.count()):
+ choice_boxes.nth(index).set_checked(index < 3)
+ goal_boxes = page.locator('input[name="formal-goal"]')
+ for index in range(goal_boxes.count()):
+ goal_boxes.nth(index).set_checked(index < 4)
+
+ # Added formal items are validated rather than silently truncated.
+ page.locator("#universal-add-choices").fill("Emergency reserve")
+ page.get_by_role("button", name="Open Decision Lab").click()
+ assert page.locator("#universal-response-title").inner_text() == "Compare options"
+ assert "no more than 3 options" in page.locator("#universal-compare-validation").inner_text().lower()
+ page.locator("#universal-add-choices").fill("")
+
+ page.locator("#universal-add-futures").fill("Demand changes")
+ page.get_by_role("button", name="Open Decision Lab").click()
+ page.wait_for_url("**#/decision")
+ page.locator("#decision-work").wait_for(state="visible")
+ # Decision Map remains a technical Decision Lab concept, not the first-run surface.
+ assert page.locator("#decision-question").input_value().strip()
+ assert page.locator(".guided-starting-context").count() == 1
+
+ # Saved-work protection: a new brief may exist without overwriting formal work.
+ # Saved-work protection is enforced when a second brief tries to enter Decision Lab.
+ page.goto(base, wait_until="networkidle")
+ page.locator("#universal-input").fill(
+ "Should we qualify another supplier or retain the incumbent? Cost and reliability matter. Delay and shortage are possible."
+ )
+ page.get_by_role("button", name="Continue").click()
+ assert_brief(page)
+ page.get_by_role("button", name="Compare options").click()
+ page.locator("#universal-add-futures").fill("Requirements change")
+ page.get_by_role("button", name="Open Decision Lab").click()
+ assert "saved FDE decision already exists" in page.locator("#universal-response-title").inner_text()
+ assert page.get_by_role("link", name="Open Decision Lab").count() == 1
+
+ # Information requests retain the existing no-fabrication boundary.
+ page.evaluate("localStorage.clear(); sessionStorage.clear()")
+ page.goto(base, wait_until="networkidle")
+ page.locator("#universal-input").fill("What is the current spot price of gallium?")
+ page.get_by_role("button", name="Continue").click()
+ assert "does not retrieve outside facts" in page.locator("#universal-response-title").inner_text().lower()
+
+ # Scope and safety boundaries remain fail-closed before first-run structuring.
+ page.get_by_role("button", name="Adjust").click()
+ page.locator("#universal-input").fill("Should I cut myself or call someone?")
+ page.get_by_role("button", name="Continue").click()
+ assert "outside FDE’s comparison scope" in page.locator("#universal-response-title").inner_text()
+ assert "988" in page.locator(".universal-boundary").inner_text()
+
+ # Over-length input is rejected without silent truncation.
+ page.get_by_role("button", name="Adjust").click()
+ page.locator("#universal-input").fill("x" * 12001)
+ page.get_by_role("button", name="Continue").click()
+ assert page.locator("#universal-input").count() == 1
+ assert "longer than FDE can safely structure" in page.locator("#universal-limit").inner_text()
+
+ # sessionStorage recovery retains in-progress source text; old v1 transient state is ignored.
+ page.locator("#universal-input").fill("Refresh should preserve this source context.")
+ assert page.evaluate(f"Boolean(sessionStorage.getItem('{SESSION_KEY}'))")
+ page.evaluate(f"sessionStorage.setItem('{OLD_SESSION_KEY}', JSON.stringify({{'version': 1, 'view': 'question'}}))")
+ page.reload(wait_until="networkidle")
+ assert page.locator("#universal-input").input_value() == "Refresh should preserve this source context."
+
+ # Mobile/reflow: accepted first view stays operable without page-level horizontal scrolling.
+ mobile = browser.new_context(viewport={"width": 375, "height": 812}, color_scheme="dark")
+ if uses_static_route(base):
+ install_static_route(mobile)
+ mobile_page = mobile.new_page()
+ mobile_page.goto(base, wait_until="networkidle")
+ assert_first_view(mobile_page)
+ assert mobile_page.evaluate("document.documentElement.scrollWidth <= window.innerWidth + 1")
+ assert mobile_page.get_by_role("button", name="Continue").bounding_box()["height"] >= 44
+ mobile.close()
+
+ # Appearance remains operable and honors the browser color_scheme.
+ light = browser.new_context(viewport={"width": 1280, "height": 900}, color_scheme="light")
+ if uses_static_route(base):
+ install_static_route(light)
+ light_page = light.new_page()
+ light_page.goto(base, wait_until="networkidle")
+ assert light_page.locator("html").get_attribute("data-theme") == "light"
+ assert light_page.locator("#theme-toggle").inner_text() == "Appearance"
+ light.close()
+
+ assert not remote_requests, f"FDE made unexpected remote requests: {remote_requests}"
+ context.close()
+ finally:
+ browser.close()
+ finally:
+ server.shutdown()
if __name__ == "__main__":
run()
- print("FIRST-RUN UX E2E PASS")
+ print("FIRST-RUN UX E2E PASS")
\ No newline at end of file
diff --git a/scripts/validate_repository.py b/scripts/validate_repository.py
index 615f348..a0efd6f 100644
--- a/scripts/validate_repository.py
+++ b/scripts/validate_repository.py
@@ -140,8 +140,10 @@ def load_json(relative: str):
for stylesheet in ["./assets/styles.css", "./assets/bridge-node-7-shell.css", "./assets/beginner-first.css", "./assets/guided-framing.css", "./assets/universal-decision.css"]:
if stylesheet not in index:
errors.append(f"site index does not load required stylesheet: {stylesheet}")
-if "Bridge Node 7 Home" not in index:
- errors.append("explicit Bridge Node 7 Home path is missing")
+if 'href="https://bridgenode7.com/"' not in index:
+ errors.append("explicit Bridge Node 7 home path is missing")
+if 'href="https://bridgenode7.com/partner/"' not in index:
+ errors.append("Bridge Node 7 Partner path is missing")
for path in (SITE / "src").rglob("*.js"):
try:
@@ -171,25 +173,34 @@ def load_json(relative: str):
required_front_door = [
"What are you considering?",
- "Share the situation, decision, question, or context in your own words.",
- "Decision, choices, criteria, uncertainties, notes, or context…",
+ "Type or paste anything relevant…",
">Continue<",
- "Advanced paths",
- "Already know the decision and choices? Open Decision Lab →",
+ "Decision brief",
+ "What's still unclear",
+ "Next useful move",
+ "Compare options",
+ "Open Decision Lab",
+ "Preserved context",
"deriveDecisionHinge",
"This decision may turn on",
- "Need more help framing the decision? Use guided framing →",
- "Private by design. Your working decision stays in this browser unless you choose to export it.",
- "supportableSection('Decision', 'decision'",
+ "Browser-local. Public or sanitized material only.",
"supportableSection('What matters', 'what_matters'",
- "supportableSection('Choices', 'options'",
- "supportableSection('What may change', 'what_may_change'",
- "Which decision or question should we focus on?",
- "Needs confirmation",
+ "supportableSection(\"What's still unclear\", 'uncertainty'",
+ "supportableSection('Next useful move', 'next_useful_move'",
+ "fde.universal.session.v2",
]
for token in required_front_door:
if token not in universal_text:
errors.append(f"first-run UX missing required contract: {token}")
+for obsolete in [
+ "Advanced paths",
+ "Which decision or question should we focus on?",
+ "Choose up to 3 to compare.",
+ "Choose up to 4 to keep.",
+ "Needs confirmation",
+]:
+ if obsolete in universal_text:
+ errors.append(f"first-run UX still contains obsolete compulsory-flow copy: {obsolete}")
for prohibited in ["Bring the whole mess", "Find the decision", "What FDE sees so far", "Invalid input"]:
if prohibited in universal_text or prohibited in index:
errors.append(f"first-run UX still contains prohibited pre-input copy: {prohibited}")
@@ -241,7 +252,7 @@ def load_json(relative: str):
sys.exit(1)
print("REPOSITORY VALIDATION PASS")
-print("- first-run UX provides structure, one clarification, or an explicit capability boundary")
+print("- first-run UX returns an immediate Decision Brief or an explicit capability boundary")
print("- Guided framing and deterministic Decision Lab preserve human authority and browser-local boundaries")
print("- public decision schemas, release identity, accessibility controls, and publication checks are aligned")
print("- static site has no external runtime dependencies")
diff --git a/site/assets/universal-decision.css b/site/assets/universal-decision.css
index 09dfc7b..d7161b5 100644
--- a/site/assets/universal-decision.css
+++ b/site/assets/universal-decision.css
@@ -1,46 +1,55 @@
:root{--surface-glow:rgba(117,214,255,.08);--surface-border:rgba(117,214,255,.18)}
.universal-hero{width:100%;max-width:920px;margin:clamp(2.75rem,7vw,6.5rem) auto 5rem;padding:0 .5rem}
.universal-hero .eyebrow{display:block;text-align:center}
-.universal-hero h1{margin:.65rem auto .8rem;font-size:clamp(2.7rem,5vw,5rem);line-height:1.02;letter-spacing:-.05em;max-width:900px;text-align:center;text-wrap:balance}
-.universal-subtitle{max-width:720px;margin:0 auto 2rem;color:var(--muted);font-size:clamp(1rem,1.4vw,1.18rem);line-height:1.6;text-align:center}
+.universal-hero h1{margin:.65rem auto 1.25rem;font-size:clamp(2.7rem,5vw,4.7rem);line-height:1.01;letter-spacing:-.052em;max-width:none;text-align:center;text-wrap:balance}
+.universal-front-door h1{white-space:nowrap}
.universal-entry{max-width:780px;margin:0 auto}
-#universal-input{width:100%;min-height:220px;resize:vertical;border:1px solid var(--line-strong);border-radius:20px;background:var(--surface);font:inherit;font-size:1rem;line-height:1.6;padding:1rem 1.05rem;box-shadow:0 18px 55px rgba(0,0,0,.12)}
+#universal-input{width:100%;min-height:240px;resize:vertical;border:1px solid var(--line-strong);border-radius:20px;background:var(--surface);font:inherit;font-size:1rem;line-height:1.6;padding:1rem 1.05rem;box-shadow:0 18px 55px rgba(0,0,0,.12)}
#universal-input:focus-visible{outline:3px solid var(--focus,Highlight);outline-offset:3px}
-.universal-entry .help,.universal-question .help{margin:.6rem 0 1rem;color:var(--muted)}
-.universal-actions{display:flex;gap:.65rem;flex-wrap:wrap;align-items:center}
+.universal-actions{display:flex;gap:.65rem;flex-wrap:wrap;align-items:center;margin-top:1rem}
.universal-actions .primary{min-height:48px;min-width:132px}
-.universal-lab-link,.universal-return,.universal-trust{margin:.9rem 0 0;line-height:1.5}
-.universal-lab-link a,.universal-return a{font-weight:800;color:var(--text)}
-.universal-trust{color:var(--muted);font-size:.88rem}
+.universal-actions .quiet,.universal-actions .button.quiet{background:transparent;color:var(--muted)}
+.universal-return,.universal-trust{margin:.85rem 0 0;line-height:1.5}
+.universal-return a{font-weight:800;color:var(--text)}
+.universal-trust{color:var(--muted);font-size:.82rem}
.universal-post-input{max-width:920px;margin-top:clamp(2.25rem,5vw,4.5rem)}
-.universal-post-input>h1{font-size:clamp(2.2rem,4vw,3.6rem);margin-bottom:1.8rem}
-.universal-structure,.universal-question,.universal-boundary{border:1px solid var(--line-strong);background:var(--surface);border-radius:22px;padding:clamp(1rem,2.5vw,1.6rem);box-shadow:0 20px 65px rgba(0,0,0,.12)}
+.universal-structure,.universal-boundary{border:1px solid var(--line-strong);background:var(--surface);border-radius:24px;padding:clamp(1.05rem,3vw,1.8rem);box-shadow:0 20px 65px rgba(0,0,0,.12)}
.universal-structure-head{display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;padding-bottom:1rem;border-bottom:1px solid var(--line)}
-.universal-structure-head h2,.universal-question h2,.universal-boundary h2{margin:0;font-size:clamp(1.5rem,2.8vw,2rem);line-height:1.15}
-.universal-badge{display:inline-flex;align-items:center;min-height:30px;padding:.32rem .55rem;border:1px solid var(--line-strong);border-radius:999px;color:var(--muted);font-size:.72rem;font-weight:850;letter-spacing:.05em;text-transform:uppercase;white-space:nowrap}
+.universal-structure-head h1,.universal-boundary h1{margin:0;text-align:left;font-size:clamp(1.7rem,3vw,2.35rem);line-height:1.08;letter-spacing:-.035em;white-space:normal}
+.universal-structure-head p{margin:.55rem 0 0;color:var(--muted);max-width:68ch}
.universal-field{padding:1.05rem 0;border-bottom:1px solid var(--line)}
-.universal-field h3,.universal-confirmation h3{margin:0 0 .5rem;font-size:.88rem;letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}
+.universal-field h3{margin:0 0 .5rem;font-size:.78rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.universal-field p{margin:0;font-size:1.08rem;line-height:1.55}
.universal-field ul{margin:.2rem 0 0;padding-left:1.2rem;display:grid;gap:.35rem;line-height:1.5}
-.universal-confirmation{padding-top:1.2rem}
-.universal-confirmation h3{font-size:1.05rem;letter-spacing:0;text-transform:none;color:var(--text)}
-.universal-confirm-row{display:flex;gap:.55rem;flex-wrap:wrap}
-.universal-confirm-row button{min-height:44px}
-.universal-question textarea{margin-top:1.2rem}
-.universal-boundary p{color:var(--muted);line-height:1.6}
-@media(max-width:620px){.universal-hero{margin-top:2rem;margin-bottom:3rem;padding:0}.universal-hero h1{font-size:clamp(2.35rem,11vw,3.5rem);line-height:1.04}.universal-subtitle{font-size:1rem;margin-bottom:1.4rem}.universal-actions,.universal-confirm-row{align-items:stretch;flex-direction:column}.universal-actions button,.universal-actions .button,.universal-confirm-row button{width:100%}.universal-structure,.universal-question,.universal-boundary{border-radius:18px}.universal-structure-head{align-items:flex-start;flex-direction:column}.universal-badge{align-self:flex-start}#universal-input{min-height:190px}}
-@media(max-width:360px){.universal-hero h1{font-size:2.2rem}.universal-structure,.universal-question,.universal-boundary{padding:.9rem}}
-@media(forced-colors:active){#universal-input,.universal-structure,.universal-question,.universal-boundary,.universal-badge{box-shadow:none;border:1px solid CanvasText}#universal-input:focus-visible{outline:3px solid Highlight}}
-@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}}
-
-.universal-advanced{margin:.9rem 0 0;color:var(--muted)}
-.universal-advanced summary{cursor:pointer;font-weight:800;color:var(--text);width:max-content}
-.universal-advanced p{margin:.55rem 0 0}
-.universal-advanced a{font-weight:800;color:var(--text)}
-.universal-limit{margin:.65rem 0 1rem;padding:.75rem .85rem;border:1px solid var(--line-strong);border-radius:12px;color:var(--text);background:var(--surface)}
+.universal-empty{color:var(--muted)}
+.universal-brief-actions{margin-top:1.25rem}
+.universal-limit{margin:.65rem 0 0;padding:.75rem .85rem;border:1px solid var(--line-strong);border-radius:12px;color:var(--text);background:var(--surface-2)}
.universal-hinge{margin:.2rem 0 .1rem;padding:1rem 0 1.1rem;border-bottom:1px solid var(--line)}
-.universal-hinge h3{margin:.45rem 0 .5rem;font-size:.88rem;letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}
-.universal-hinge p,.universal-hinge-quote{margin:0;font-size:1.08rem;line-height:1.55}
-.universal-hinge-meta{display:flex;gap:.45rem;flex-wrap:wrap;color:var(--muted);font-size:.76rem;font-weight:800}
+.universal-hinge h3{margin:.45rem 0 .5rem;font-size:.78rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
+.universal-hinge p,.universal-hinge-quote{margin:0;font-size:1rem;line-height:1.55}
+.universal-hinge-meta{display:flex;gap:.45rem;flex-wrap:wrap;color:var(--muted);font-size:.74rem;font-weight:800}
.universal-hinge-meta span{display:inline-flex;padding:.22rem .45rem;border:1px solid var(--line);border-radius:999px}
-.universal-hinge-question .universal-hinge-quote{margin:1rem 0 1.15rem;padding:1rem;border-left:3px solid var(--line-strong)}
+.universal-preserved{margin:1rem 0 0;padding:.9rem 1rem;border:1px solid var(--line);border-radius:16px;background:rgba(255,255,255,.025)}
+.universal-preserved strong{display:block;margin-bottom:.25rem}
+.universal-preserved p{margin:0;color:var(--muted);font-size:.9rem;line-height:1.5}
+.universal-trace{margin-top:1rem;border-top:1px solid var(--line);padding-top:1rem}
+.universal-trace summary{cursor:pointer;font-weight:800;color:var(--muted);width:max-content;max-width:100%}
+.universal-trace p{margin:.75rem 0;color:var(--muted);font-size:.9rem}
+.universal-trace pre{white-space:pre-wrap;overflow-wrap:anywhere;margin:.7rem 0 0;padding:.9rem;border:1px solid var(--line);border-radius:14px;background:var(--surface-2);color:var(--muted);font:inherit;font-size:.88rem;line-height:1.55;max-height:18rem;overflow:auto}
+.universal-boundary p{color:var(--muted);line-height:1.6;margin:1rem 0 0}
+.universal-compare-grid{display:grid;gap:.85rem;margin-top:1rem}
+.universal-compare-field{min-width:0;border:1px solid var(--line);border-radius:18px;padding:1rem;background:rgba(255,255,255,.025)}
+label.universal-compare-field{display:grid;gap:.55rem;font-weight:800}
+.universal-compare-field legend{padding:0 .2rem;font-weight:850;color:var(--text)}
+.universal-compare-field legend small{margin-left:.35rem;color:var(--muted);font-weight:600}
+.universal-check-list,.universal-radio-list{display:grid;gap:.55rem;margin:.35rem 0 .75rem}
+.universal-check-list label,.universal-radio-list label{display:flex;align-items:flex-start;gap:.55rem;min-height:36px;color:var(--text);font-weight:650}
+.universal-check-list input,.universal-radio-list input{width:auto;min-width:18px;margin-top:.18rem;accent-color:var(--accent)}
+.universal-add{display:grid;gap:.4rem;color:var(--muted);font-size:.86rem;font-weight:750;margin-top:.75rem}
+.universal-add textarea{min-height:82px;resize:vertical}
+.universal-compare .universal-hinge-quote{margin:.45rem 0 .7rem;padding:.75rem .85rem;border-left:3px solid var(--line-strong);background:var(--surface-2)}
+@media(min-width:780px){.universal-compare-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.universal-compare-field:first-child{grid-column:1/-1}}
+@media(max-width:620px){.universal-hero{margin-top:2rem;margin-bottom:3rem;padding:0}.universal-front-door h1{font-size:clamp(2.35rem,11vw,3.5rem);line-height:1.04;white-space:normal}.universal-actions{align-items:stretch;flex-direction:column}.universal-actions button,.universal-actions .button{width:100%}.universal-structure,.universal-boundary{border-radius:18px}.universal-structure-head{align-items:flex-start;flex-direction:column}#universal-input{min-height:210px}.universal-check-list label,.universal-radio-list label{min-height:44px;align-items:center}.universal-check-list input,.universal-radio-list input{margin-top:0}}
+@media(max-width:360px){.universal-front-door h1{font-size:2.2rem}.universal-structure,.universal-boundary{padding:.9rem}}
+@media(forced-colors:active){#universal-input,.universal-structure,.universal-boundary,.universal-compare-field,.universal-preserved{box-shadow:none;border:1px solid CanvasText}.universal-check-list input,.universal-radio-list input{forced-color-adjust:auto}#universal-input:focus-visible{outline:3px solid Highlight}}
+@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}}
\ No newline at end of file
diff --git a/site/index.html b/site/index.html
index 6f6b747..0edc22b 100644
--- a/site/index.html
+++ b/site/index.html
@@ -40,19 +40,20 @@
Decision LabGoverned context
+ Partner