diff --git a/.gitignore b/.gitignore
index 6222b78..552c8ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,6 +53,7 @@ work/
Processing/precedent_rag/.cache/
# Local-only notes, agent prompts/logs and research (never published)
+.impeccable/
CLAUDE-*.md
.impeccable.md
DONE.md
diff --git a/DESIGN.md b/DESIGN.md
index a514b4c..8a5e8f2 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -1,11 +1,131 @@
-# Change assurance interface
+---
+name: ProjectLens
+description: Clear schedule evidence before a human decision
+colors:
+ primary: "#0857e8"
+ primary-hover: "#0645bd"
+ white: "#fff"
+ ground: "#fafbfc"
+ panel: "#f3f6fa"
+ ink: "#0b1933"
+ muted: "#4c5b70"
+ selection: "#e8f0ff"
+ line: "#d5dce6"
+ attention: "#805300"
+ attention-ground: "#fff6de"
+typography:
+ display:
+ fontFamily: '"Encode Sans", sans-serif'
+ fontSize: "61px"
+ fontWeight: 700
+ lineHeight: 1.02
+ letterSpacing: "-.035em"
+ title:
+ fontFamily: '"Encode Sans", sans-serif'
+ fontSize: "29px"
+ fontWeight: 600
+ lineHeight: 1.25
+ letterSpacing: "-.025em"
+ body:
+ fontFamily: '"Encode Sans", sans-serif'
+ fontSize: "15px"
+ fontWeight: 400
+ lineHeight: 1.6
+ label:
+ fontFamily: '"Encode Sans", sans-serif'
+ fontSize: "13px"
+ fontWeight: 500
+ lineHeight: 1.4
+rounded:
+ control: "7px"
+ field: "6px"
+ panel: "12px"
+ example: "9px"
+ status: "5px"
+spacing:
+ small: "12px"
+ medium: "16px"
+ gutter: "24px"
+ panel: "28px"
+ roomy: "32px"
+components:
+ button-primary:
+ backgroundColor: "{colors.primary}"
+ textColor: "{colors.white}"
+ rounded: "{rounded.control}"
+ padding: "12px 28px"
+ button-primary-hover:
+ backgroundColor: "{colors.primary-hover}"
+ evidence-panel:
+ backgroundColor: "{colors.panel}"
+ textColor: "{colors.ink}"
+ rounded: "{rounded.panel}"
+ padding: "24px"
+ status:
+ backgroundColor: "{colors.attention-ground}"
+ textColor: "{colors.attention}"
+ rounded: "{rounded.status}"
+ padding: "6px 10px"
+---
-Operate mode. Preserve ProjectLens's warm paper (#f1ecdf), near-black ink (#071a1b), off-white (#fffdf6), rust (#a94d35) and pale blue (#8eb0bd). Rust identifies unresolved evidence; green is reserved for the limited outcome of fixed checks. No causation, entitlement or approval claim follows from colour.
+## Overview
-The review uses Libre Franklin: title 24px, body 14px, compact evidence 13px, metadata at least 12px. Archivo Black stays on the landing page. JetBrains Mono identifies source activity codes. Use 8/16/24/32px spacing, 1px borders, flat rectangular surfaces and no decorative gradients or shadows.
+**Creative North Star: "Evidence in plain view"**
-The top navigation remains Reviews, Decisions and Follow-up, with aria-current. The active review hides the landing pitch. Findings and source evidence share a two-column desk; a selected finding has a labelled pressed state. Mobile uses one pane and a Back to findings control which returns focus. The primary action records a human decision; questions and optional public context are secondary.
+ProjectLens makes schedule evidence readable before a person records a decision. Cool white surfaces, blue-grey text and cobalt actions frame one continuous evidence workspace. The landing explains the value through a real-engine synthetic comparison; the review and walkthrough use the same visual language at task scale.
-Keep raw source facts and the original narrative visible. Untouched supplemental records are not reviewed; using one never makes it an authoritative precedent. Owner and rationale are required. Conditional approvals require condition, owner and due date. Errors persist with recovery guidance. A deliberate device draft includes XER contents and form fields; saving and deletion are explicit. No automatic upload or localhost probing.
+This contract covers docs/change-assurance.html, its product-landing.css/js layer, and the normal review, decision, follow-up and guided walkthrough surfaces reached there. GMPP and separate detailed schedule/board pages retain their existing identities. It replaces the former cream/rust guidance for this scoped experience only.
-Focus uses a 3px blue outline, controls have visible hover and disabled states, and navigation transfers focus. Motion only scrolls to revealed work and respects reduced motion. Do not add app heroes, metric cards, invented confidence scores, decorative icons or sidebars.
+**Key Characteristics:**
+- Precise sans typography and restrained cobalt emphasis.
+- Continuous evidence workspace with calm tonal panels.
+- Explicit human actions and bounded explanatory motion.
+
+## Colors
+
+Primary cobalt identifies actions, active navigation, selection and keyboard focus. The deeper primary is its hover state. Neutral ink carries claims and values; muted text carries supporting explanation. White and ground establish the canvas; panel and selection distinguish grouping and active evidence. Line separates content. Attention and its pale ground label unresolved review work, including the ready state: no green approval signal is implied.
+
+**The Evidence Rule.** Colour communicates interface state, never proof or permission to approve.
+
+## Typography
+
+Encode Sans is self-hosted in docs/assets/fonts at weights 400, 500, 600 and 700; the bundled OFL governs distribution. Display type is reserved for the landing headline. Review headings use the title scale; evidence copy uses compact body type, with metadata generally 12–13px. The landing introduction and claim/observation comparison use larger reading type. Source codes retain the JetBrains Mono/monospace stack. Dates and quantitative comparisons use tabular numerals.
+
+## Layout
+
+The shared maximum content width is 1380px. The header uses max(5vw, 24px) gutters, changing to 18px on phones. The approved landing uses a centered headline and actions above a wide comparison workspace. Its left selector is 280px on large screens and 210px at tablet width. Review findings and source evidence form a two-column desk; phone layouts use a single pane and an explicit return to findings.
+
+At 1101–1535px, selected landing dimensions scale through the existing --pl-u factor, with compact labels bounded at 12px. At 761–1100px, comparison type and selector width compress. At 760px and below, selectors become three horizontal tabs; dates sit side by side above their connection, and claim, observation and question stack. Review navigation occupies its own full-width header row. Forms, decision choices and register rows stack. Do not infer a universal spacing grid: use the recorded spacing tokens and existing component-specific measurements.
+
+## Elevation & Depth
+
+This layer uses flat white and blue-grey surfaces, thin separators and tonal selection rather than decorative gradients or added shadows. Preserve functional inherited overlays. The inactive toast must be hidden with visibility, opacity and pointer-event rules; only its visible status state may occupy visual attention.
+
+## Shapes
+
+Moderately rounded controls and fields sit within larger rounded evidence and decision panels. The comparison frame uses its own slightly tighter radius. Borders are thin and cool; structure remains rectangular rather than pill-shaped.
+
+## Components
+
+- **Actions:** cobalt primary buttons, white text, restrained background hover; secondary actions use white or transparent backgrounds and blue text. Keyboard focus uses a 3px cobalt outline with 4px offset. Disabled buttons retain their disabled behaviour and reduced opacity.
+- **Navigation:** native header links preserve real destinations. Active review links use cobalt text and a bottom rule with aria-current. Landing navigation switches to Reviews, Decisions and Follow-up within the operating journey.
+- **Evidence selectors:** selected state uses pale blue, cobalt text and a labelled selected/pressed state. Findings retain title, explanation and source context.
+- **Evidence panels:** tonal backgrounds contain dates, facts, quoted narrative, source files and a separately labelled review question. File names can wrap.
+- **Inputs and decisions:** white fields on tonal panels, visible labels and thin borders. Owner and rationale remain required; conditional decisions retain condition, owner and due date. Persistent errors include recovery guidance.
+- **Preview and walkthrough:** the clearly labelled synthetic preview reads the real comparison engine. Factual questions are fixed independently of narrative matching. Replay explicitly draws the finish relationship over 700ms; evidence changes use bounded 180ms transitions. No looping motion. Reduced motion removes animation and replay while retaining facts and relationships. Walkthrough records remain disposable and isolated from ordinary decisions.
+
+**The Human Decision Rule.** Interface hierarchy leads from fact to source to question before presenting the action that records a person's decision.
+
+## Do's and Don'ts
+
+- Do keep observations, narrative claims, factual questions and human decisions visually distinct.
+- Do preserve source facts and both narrative versions when text changes.
+- Do use the shared cool palette and self-hosted type across landing, review and walkthrough.
+- Do retain meaningful static dates and state when reduced motion is requested.
+- Don't use colour as evidence of causation, entitlement or approval.
+- Don't let narrative wording remove independent factual review questions.
+- Don't add fabricated metrics, customer proof or decorative feature-card marketing.
+- Don't extend this identity to separate routes without an explicit redesign.
+
+
+Implementation authority: docs/product-landing.css and docs/product-landing.js; product truth: PRODUCT.md; approved composition: .impeccable/surfaces/docs-change-assurance-html.md. Finished captures are ../../outputs/projectlens-{product,review,decision}-{desktop,phone}.png. The finish verdict at ../product-finish-verdict.txt resolves desktop clarity and the two phone fixes; it is not a full security or accessibility certification. Local full pytest collection lacked pandas; CI remains the stated verification boundary.
diff --git a/PRODUCT.md b/PRODUCT.md
new file mode 100644
index 0000000..b9b6e1b
--- /dev/null
+++ b/PRODUCT.md
@@ -0,0 +1,50 @@
+# ProjectLens
+
+
+
+## Platform
+
+web
+
+## Users
+
+Project controls reviewers compare a project update with two Primavera P6 schedule exports, inspect changes and record a human decision. Portfolio visitors also need to understand the problem, try the real workflow and judge the engineering evidence. Practitioner demand and measured benefits remain unvalidated.
+
+## Product Purpose
+
+Make a schedule update's changes, supporting source evidence and outstanding review questions understandable before a person records a decision. The user confirmed that ProjectLens should open with a proper product landing page. It must explain the value before offering the working sample and the visitor's own-file journey.
+
+## Positioning
+
+A browser-local evidence-to-decision workflow: compare two XER exports, inspect their facts alongside the narrative, record a decision, export the record and track conditions. Do not claim that schedule comparison itself is novel, that the tool establishes causation or entitlement, or that it replaces Primavera P6 or professional judgement.
+
+## Operating Context
+
+The existing static product ships from docs/ on GitHub Pages. The synthetic Northstar pack demonstrates a 73-day scheduled finish movement. The guided walkthrough uses the real browser engine with disposable in-tab records. The ordinary route supports explicit local drafts, saved decisions, exports and conditions.
+
+## Capabilities and Constraints
+
+- Two XER files and an optional narrative feed deterministic browser checks. Files and decision records stay in the browser during the standard workflow.
+- Factual schedule changes must remain visible independently of narrative wording. Narrative checks are review prompts, not proof that an issue is resolved or that a pack is safe to approve.
+- Preserve submitted and revised narrative text as distinct evidence when the narrative changes.
+- A person supplies the decision, owner, rationale and any conditions. Context records are supplementary and require explicit human handling before inclusion.
+- An optional, separately configured local research service can retrieve public records and produce a citation-checked AI brief. Its tracing support is optional; no live evaluation result or deployed AI capability may be inferred from configuration alone.
+- Keep existing uploads, downloads, validation, browser storage, walkthrough isolation and decision/follow-up journeys working through the redesign.
+
+## Brand Commitments
+
+Keep the ProjectLens name. The user asks for Apple-level design care, taste and clear information hierarchy. The user explicitly rejects generic AI-generated website styling. Those references set a craft standard; they do not authorise copying another brand's identity.
+
+## Evidence on Hand
+
+The runnable product, source comparison engine, synthetic XER pairs, real-engine walkthrough, exported decision records, browser tests and existing before-and-after captures are available. Northstar facts include a 73-day finish movement, 10 materially changed activities, 3 constraint changes, 9 activities with reduced reported float and 5 added or removed predecessor signatures. These are demonstration facts, not customer outcomes.
+
+No verified customer logos, testimonials, time savings, independent accuracy claim or production adoption evidence are available. Do not fabricate them.
+
+## Product Principles
+
+1. Explain what changed, show its source, state why it warrants review and identify the next question.
+2. Keep observations, narrative claims, unresolved questions and human decisions distinct.
+3. Demonstrate real behaviour with clearly labelled synthetic inputs.
+4. State practical limits at the point where they affect interpretation.
+5. Make product value understandable to a visitor without hiding the evidence from a practitioner.
diff --git a/Processing/tests/browser_change_assurance.py b/Processing/tests/browser_change_assurance.py
index b170cbd..2cd0f86 100644
--- a/Processing/tests/browser_change_assurance.py
+++ b/Processing/tests/browser_change_assurance.py
@@ -25,7 +25,7 @@ def run_desktop(browser):
page.wait_for_load_state("networkidle")
assert page.locator("[data-view-link]").count() == 3
- assert page.get_by_role("heading", name="Check the change pack before you decide.").is_visible()
+ assert page.get_by_role("heading", name="The schedule changed. Did the update explain it?").is_visible()
page.get_by_role("button", name="Try the Northstar example").click()
page.locator("#readinessWorkspace").wait_for(state="visible")
readiness_status = page.locator("#readinessStatus").inner_text()
@@ -165,7 +165,7 @@ def run_recovery(browser):
return results;
}""", [(ROOT / "docs/demo/northstar-previous.xer").read_text(), (ROOT / "docs/demo/northstar-current.xer").read_text()])
assert all(rejected.values()), rejected
- page.get_by_role("button", name="Use my own XER files").click()
+ page.get_by_role("button", name="Use your own XER files").click()
page.locator("#assurancePrevious").set_input_files(ROOT / "docs/demo/northstar-previous.xer")
page.locator("#assuranceCurrent").set_input_files(ROOT / "docs/demo/riverside-current.xer")
page.get_by_role("button", name="Check decision readiness").click()
diff --git a/Processing/tests/browser_product_landing.py b/Processing/tests/browser_product_landing.py
new file mode 100644
index 0000000..b14b331
--- /dev/null
+++ b/Processing/tests/browser_product_landing.py
@@ -0,0 +1,184 @@
+"""Product-led landing, adversarial narrative checks and complete own-file journey."""
+
+import json
+import os
+from pathlib import Path
+
+from playwright.sync_api import expect, sync_playwright
+
+
+ROOT = Path(__file__).resolve().parents[2]
+BASE = os.environ.get("PROJECTLENS_BROWSER_BASE_URL", "http://127.0.0.1:8765/change-assurance.html")
+CAPTURES = Path(os.environ.get("PROJECTLENS_SCREENSHOT_DIR", "/tmp/projectlens-product"))
+CAPTURES.mkdir(parents=True, exist_ok=True)
+
+
+def loaded(page):
+ page.goto(BASE)
+ expect(page.locator("#example-finish")).to_be_enabled()
+ page.evaluate("document.fonts.ready")
+
+
+def landing(browser):
+ page = browser.new_page(viewport={"width": 1440, "height": 1000})
+ errors, requests = [], []
+ page.on("pageerror", lambda error: errors.append(str(error)))
+ page.on("request", lambda request: requests.append((request.method, request.url)))
+ loaded(page)
+ expect(page.locator("#productTitle")).to_have_text("The schedule changed.Did the update explain it?")
+ expect(page.locator("#exampleTitle")).to_have_text("The finish moves by 73 days.")
+ expect(page.locator("#exampleComparison")).to_contain_text("15 Dec 2027")
+ expect(page.locator("#exampleComparison")).to_contain_text("26 Feb 2028")
+ assert page.evaluate("localStorage.getItem('projectlens:change-decisions:v1')") is None
+ page.locator("#exampleReplay").click()
+ assert page.locator(".example-line").evaluate("el => el.getAnimations().length") == 1
+ page.locator("#exampleReplay").click()
+ assert page.locator(".example-line").evaluate("el => el.getAnimations().length") == 1
+ page.locator("#example-finish").focus()
+ page.keyboard.press("ArrowDown")
+ expect(page.locator("#example-constraints")).to_be_focused()
+ expect(page.locator("#exampleTitle")).to_have_text("3 activities have changed constraints.")
+ page.locator("#exampleInspect").click()
+ expect(page.locator("#exampleSource tbody tr")).to_have_count(3)
+ expect(page.locator("#exampleSource")).to_contain_text("TASK")
+ page.locator("#example-relationships").click()
+ expect(page.locator("#exampleSource tbody tr")).to_have_count(5)
+ expect(page.locator("#exampleQuestion")).to_have_text("What changed in the delivery sequence, and why?")
+ page.locator("#exampleInspect").click()
+ page.locator("#example-finish").click()
+ page.locator("#exampleInspect").click()
+ expect(page.locator("#exampleSource")).to_contain_text("PROJECT.scd_end_date")
+ page.locator("#exampleInspect").click()
+ page.evaluate("window.scrollTo(0, 0)")
+ page.screenshot(path=str(CAPTURES / "projectlens-redesign-after-landing.png"))
+ page.screenshot(path=".impeccable/review/desktop.png", full_page=True)
+ assert all(method == "GET" for method, _ in requests), requests
+ assert not errors, errors
+ page.close()
+
+
+def semantic_regressions(browser):
+ page = browser.new_page()
+ loaded(page)
+ result = page.evaluate("""async () => {
+ const api = ProjectLensChangeAssurance;
+ const [a,b] = await Promise.all(['previous','current'].map(async kind => api.parseXer(await (await fetch(`demo/northstar-${kind}.xer`)).text(), `${kind}.xer`)));
+ return [
+ 'The finish has slipped by 73 days. Constraints remain unexplained. Logic is unverified. Governance action has no owner and no due date.',
+ 'This project is not on track. The finish moves 73 days. All constraint and logic changes are acknowledged. The action owner is the Planning Manager, due 1 December 2027.',
+ 'Everything is explained. Constraints and logic are covered. Owner and due date supplied.'
+ ].map(text => {
+ const review = api.analyseReview(a,b,text);
+ return { movement:review.finishMovement, findings:review.blockers, constraints:review.changes.constraintChanges, relationships:review.changes.relationChanges, float:review.changes.floatErosion };
+ });
+ }""")
+ for review in result:
+ assert (review["movement"], review["constraints"], review["relationships"], review["float"]) == (73, 3, 5, 9)
+ assert len(review["findings"]) >= 4
+ assert any("constraint changes" in row["title"] for row in review["findings"])
+ assert any("relationship signatures" in row["title"] for row in review["findings"])
+ assert any(row["type"] == "Float loss" for row in review["findings"])
+ assert any(row["type"] == "Ownership gap" for row in result[0]["findings"])
+ assert not any(row["type"] == "Contradiction" for row in result[1]["findings"])
+ page.close()
+
+
+def own_files(browser):
+ page = browser.new_page(viewport={"width": 1440, "height": 1000}, reduced_motion="reduce")
+ loaded(page)
+ page.locator("#showOwnPack").click()
+ expect(page.locator("#productLanding")).to_be_hidden()
+ expect(page.locator("#intakeTitle")).to_be_focused()
+ for kind in ("Previous", "Current"):
+ page.locator(f"#assurance{kind}").set_input_files(ROOT / "docs" / "demo" / f"northstar-{kind.lower()}.xer")
+ original = (ROOT / "docs/demo/northstar-narrative.txt").read_text()
+ page.locator("#assuranceNarrative").fill(original)
+ page.locator("#runAssuranceReview").click()
+ expect(page.locator("#readinessWorkspace")).to_be_visible()
+ expect(page.locator("#reviewFacts")).to_contain_text("Constraint changes")
+ page.screenshot(path=str(CAPTURES / "projectlens-redesign-after-review.png"))
+ revision = "This project is not on track. The finish moves 73 days. All constraint and logic changes are acknowledged. The action owner is the Planning Manager, due 1 December 2027."
+ page.locator("#narrativeRevision summary").click()
+ page.locator("#revisedNarrative").fill(revision)
+ page.locator("#recheckNarrative").click()
+ expect(page.locator("#narrativeRevision")).not_to_have_attribute("open", "")
+ expect(page.locator(".original-narrative")).to_contain_text(original)
+ expect(page.locator("#reviewFacts")).to_contain_text("Constraint changes")
+ page.locator("#showDecisionForm").click()
+ page.get_by_label("Approve with conditions", exact=True).check()
+ page.locator("#decisionOwner").fill("Review chair")
+ page.locator("#decisionRationale").fill("Proceed only after the stated evidence is supplied.")
+ page.locator("#decisionForm button[type=submit]").click()
+ expect(page.locator("#workspaceError")).to_contain_text("Conditional approval needs")
+ page.locator("#decisionCondition").fill("Supply the approved instruction for changed constraints")
+ page.locator("#conditionOwner").fill("Planning manager")
+ page.locator("#conditionDue").fill("2027-12-01")
+ page.locator("#saveDraft").click()
+ page.reload()
+ page.locator("#resumeDraft").click()
+ expect(page.locator("#decisionOwner")).to_have_value("Review chair")
+ page.locator("#decisionForm button[type=submit]").click()
+ page.wait_for_url("**#decisions")
+ record = page.evaluate("JSON.parse(localStorage.getItem('projectlens:change-decisions:v1'))[0]")
+ assert record["originalNarrative"] == original
+ assert record["narrative"] == revision
+ assert record["unresolved"] == 4
+ page.evaluate("Object.defineProperty(navigator,'clipboard',{configurable:true,value:{writeText:text=>{window.__export=text;return Promise.resolve()}}})")
+ page.locator("#exportDecisions").click()
+ page.wait_for_function("typeof window.__export === 'string'")
+ exported = page.evaluate("window.__export")
+ assert original in exported and revision in exported
+ page.locator('[data-view-link="follow-up"]').click()
+ page.locator("#conditionRegister button").click()
+ expect(page.locator("#conditionRegister")).to_contain_text("Closed")
+ assert page.evaluate("JSON.parse(localStorage.getItem('projectlens:change-conditions:v1'))[0].closed") is True
+ page.close()
+
+
+def responsive_and_recovery(browser):
+ for width in (320, 390, 768, 1280, 1920):
+ page = browser.new_page(viewport={"width": width, "height": 844}, reduced_motion="reduce")
+ loaded(page)
+ assert page.evaluate("document.documentElement.scrollWidth <= innerWidth + 1"), width
+ expect(page.locator('#assuranceToast')).to_be_hidden()
+ header = page.locator('.assurance-header').bounding_box()
+ navigation = page.locator('.product-navigation').bounding_box()
+ assert navigation['y'] >= header['y'] and navigation['y'] + navigation['height'] <= header['y'] + header['height'] + 1, (width, header, navigation)
+ assert page.locator("#loadAssuranceDemo").bounding_box()["y"] < 800, width
+ expect(page.locator("#exampleReplay")).to_be_hidden()
+ page.locator("#example-relationships").click()
+ page.locator("#exampleInspect").click()
+ assert page.evaluate("document.documentElement.scrollWidth <= innerWidth + 1"), width
+ if width == 390:
+ page.locator("#exampleInspect").click()
+ page.locator("#example-finish").click()
+ page.screenshot(path=".impeccable/review/mobile.png", full_page=True)
+ page.screenshot(path=str(CAPTURES / "projectlens-redesign-after-mobile.png"), full_page=True)
+ page.locator('#loadAssuranceDemo').click()
+ expect(page.locator('#readinessWorkspace')).to_be_visible()
+ expect(page.locator('#assuranceToast')).not_to_have_class('assurance-toast show')
+ expect(page.locator('#assuranceToast')).to_be_hidden()
+ header = page.locator('.assurance-header').bounding_box()
+ navigation = page.locator('.review-navigation').bounding_box()
+ assert navigation['y'] + navigation['height'] <= header['y'] + header['height'] + 1
+ page.close()
+ page = browser.new_page()
+ page.route("**/demo/northstar-current.xer", lambda route: route.fulfill(status=503, body="Unavailable"))
+ page.goto(BASE)
+ expect(page.locator("#exampleRetry")).to_be_visible()
+ expect(page.locator("#exampleInspect")).to_be_disabled()
+ page.unroute("**/demo/northstar-current.xer")
+ page.locator("#exampleRetry").click()
+ expect(page.locator("#exampleInspect")).to_be_enabled()
+ page.close()
+
+
+if __name__ == "__main__":
+ with sync_playwright() as p:
+ browser = p.chromium.launch()
+ landing(browser)
+ semantic_regressions(browser)
+ own_files(browser)
+ responsive_and_recovery(browser)
+ browser.close()
+ print("Product journeys passed: real evidence, keyboard, animation, negation, own files, draft, decision, condition, five widths and retry")
diff --git a/Processing/tests/browser_walkthrough.py b/Processing/tests/browser_walkthrough.py
index e647891..1d597cb 100644
--- a/Processing/tests/browser_walkthrough.py
+++ b/Processing/tests/browser_walkthrough.py
@@ -56,7 +56,7 @@ def run_journey(browser):
page.locator("#tourNarrative").fill("A new narrative awaiting checks.")
expect(page.locator("#tourNext")).to_be_disabled()
page.get_by_role("button", name="Try an acknowledged finish change", exact=True).click()
- expect(page.locator("#tourCheckStatus")).to_contain_text("4 findings in the original pack; 3 now")
+ expect(page.locator("#tourCheckStatus")).to_contain_text("5 findings in the original pack; 5 now")
changed = snapshot(page)
assert changed["review"]["inputIdentity"] != original["review"]["inputIdentity"]
assert changed["review"]["previous"] == original["review"]["previous"]
diff --git a/Processing/tests/test_xer_review_assets.py b/Processing/tests/test_xer_review_assets.py
index 1877617..1c6508b 100644
--- a/Processing/tests/test_xer_review_assets.py
+++ b/Processing/tests/test_xer_review_assets.py
@@ -86,17 +86,17 @@ def test_change_assurance_exposes_one_simple_decision_workflow():
script = (DOCS / "change-assurance.js").read_text(encoding="utf-8")
stylesheet = (DOCS / "change-assurance.css").read_text(encoding="utf-8")
- assert "Check the change pack" in page
- assert "Add the pack" in page
- assert "Review blockers" in page
- assert "Record decision" in page
- assert "Track conditions" in page
+ assert "The schedule changed." in page
+ assert "Bring the update" in page
+ assert "Inspect what changed" in page
+ assert "Keep the decision" in page
+ assert "track any conditions" in page
assert page.count('data-view-link=') == 3
assert "window.ProjectLensChangeAssurance" in script
assert "localStorage" in script
assert "fetch(\"demo/" in script
assert "prefers-reduced-motion" in stylesheet
- assert "Your files stay in this browser" in page
+ assert "Your schedule files stay with you" in page
def test_riverside_pair_is_a_second_comparable_project():
diff --git a/README.md b/README.md
index 4f75bb9..8703bd1 100644
--- a/README.md
+++ b/README.md
@@ -9,16 +9,16 @@ For the project controls reviewer who has to tell a board whether a change pack
**Live demo (no install, no account):** [interactive guided review](https://matthewpaver.github.io/ProjectLens/change-assurance.html?walkthrough=1) · [review a change pack](https://matthewpaver.github.io/ProjectLens/change-assurance.html) · [XER schedule review](https://matthewpaver.github.io/ProjectLens/schedule-review.html) · [board review](https://matthewpaver.github.io/ProjectLens/board-readiness.html) · [public GMPP evidence](https://matthewpaver.github.io/ProjectLens/) · [2-minute walkthrough (MP4)](docs/assets/projectlens-evidence-demo.mp4)
-On the change-assurance page, select **Try the Northstar example**. It is a synthetic pack, and the result is one readiness verdict with three blockers and the questions to send the team before the meeting.
+On the change-assurance page, select **Try the Northstar example**. The landing example lets you inspect real finish, constraint and relationship fields before opening a review. Northstar is synthetic; its review contains five questions, with three prioritised beside the evidence and two available under additional findings.
## The problem
-A board often decides on how polished the pack looks, because nobody has time to reconcile the narrative with the schedules, risks, actions and prior conditions submitted with it. The bundled Northstar example reproduces the failure. Its progress report says there is "no change to the finish date", but the current schedule has moved the finish by 73 days. The change-assurance page checks schedule fields and narrative keywords; the separate board-readiness page covers risks, actions and earlier conditions. If nobody checks the evidence against the story, the board approves the story.
+A board often decides on how polished the pack looks, because nobody has time to reconcile the narrative with the schedules, risks, actions and prior conditions submitted with it. The bundled Northstar example reproduces the failure. Its progress report says the "key completion date is unchanged", but the current schedule has moved the finish by 73 days. The change-assurance page checks schedule fields and narrative keywords; the separate board-readiness page covers risks, actions and earlier conditions. If nobody checks the evidence against the story, the board approves the story.
ProjectLens gives the reviewer:
- **Source-linked conflicts and gaps.** Each finding names the evidence that produced it: current or previous schedule fields and the narrative under review.
-- **A prepared decision, not a dashboard.** One readiness verdict, at most three blockers, and specific questions for the team.
+- **A review tied to source facts.** Three prioritised questions, additional findings retained, and an explicit human decision. Rewording a narrative never removes factual constraint, relationship or float changes.
- **A record of the decision.** The decision, its owner, rationale and conditions are kept; each condition stays open until it is marked closed.
## Quickstart
@@ -32,9 +32,9 @@ python3 -m http.server 8000 --bind 127.0.0.1 --directory docs
# open http://127.0.0.1:8000/change-assurance.html and select "Try the Northstar example"
```
-Expected result: the evidence desk shows three findings, including the 73-day finish movement, with source dates beside the selected finding. Stop the server with Ctrl+C.
+Expected result: the evidence desk shows three prioritised findings and five total questions, including the 73-day finish movement, with source dates beside the selected finding. Stop the server with Ctrl+C.
-**Your own schedules.** Export two single-project XER files from Primavera P6 (**File → Export → Primavera XER**): the comparison point and the latest submission. Select **Use my own XER files**, choose both files and paste the narrative. Files are parsed in the browser and never uploaded. If you have no XER to hand, use the synthetic [Northstar](https://matthewpaver.github.io/ProjectLens/demo/northstar-previous.xer) ([current](https://matthewpaver.github.io/ProjectLens/demo/northstar-current.xer)) or [Riverside](https://matthewpaver.github.io/ProjectLens/demo/riverside-previous.xer) ([current](https://matthewpaver.github.io/ProjectLens/demo/riverside-current.xer), [narrative](https://matthewpaver.github.io/ProjectLens/demo/riverside-narrative.txt)) pairs.
+**Your own schedules.** Export two single-project XER files from Primavera P6 (**File → Export → Primavera XER**): the comparison point and the latest submission. Select **Use your own XER files**, choose both files and paste the narrative. Files are parsed in the browser and never uploaded. If you have no XER to hand, use the synthetic [Northstar](https://matthewpaver.github.io/ProjectLens/demo/northstar-previous.xer) ([current](https://matthewpaver.github.io/ProjectLens/demo/northstar-current.xer)) or [Riverside](https://matthewpaver.github.io/ProjectLens/demo/riverside-previous.xer) ([current](https://matthewpaver.github.io/ProjectLens/demo/riverside-current.xer), [narrative](https://matthewpaver.github.io/ProjectLens/demo/riverside-narrative.txt)) pairs.
## Guided review
@@ -50,7 +50,7 @@ The walkthrough journey, isolation, playback, phone layout and failed-load recov
flowchart LR
X[(Two XER exports + narrative)] --> P[Parse XER in the browser]
P --> R[Deterministic checks finish movement vs narrative, constraints, logic, float, ownership]
- R --> V[Readiness verdict ≤3 blockers + questions]
+ R --> V[Source review 3 prioritised questions + additional findings]
V -. optional .-> S[Precedent sidecar local FastAPI]
S --> H[Hybrid retrieve over 189 public GMPP records]
H --> C{Citation and authority check}
@@ -69,11 +69,11 @@ To run the sidecar, copy `.env.example` to `.env`, set `GEMINI_API_KEY` (and `LA
## Review, resume and export
-The evidence desk puts three prioritised findings (with any additional findings disclosed and retained in the record) beside their source dates, activity movements and original narrative. The decision maker supplies a decision, owner and rationale. Conditional approval also needs a condition, owner and due date. Export preserves findings, the full SHA-256 input identity and optional context dispositions. Existing records without a hash remain readable and explicitly lack that identity.
+The evidence desk puts three prioritised findings (with any additional findings disclosed and retained in the record) beside their source dates, activity movements and original narrative. The decision maker supplies a decision, owner and rationale. Conditional approval also needs a condition, owner and due date. Export preserves findings, the full SHA-256 input identity, original and revised narratives, and optional context dispositions. Use **Check a revised narrative** to rerun a revision against the same exports; unchecked edits do not change decision evidence. Existing records without a hash remain readable and explicitly lack that identity.
**Save draft on this device** deliberately stores both XER contents, narrative and decision fields in browser storage. Save again after further edits; use **Resume saved draft** after reopening, and **Delete saved draft** when finished. Anyone with access to this browser profile may access that stored data. Normal review processing uploads nothing. Browser storage is not a shared or tamper-proof audit system.
-Project names and source IDs must match. Identical evidence and reversed data dates are rejected. Missing activity finish or float fields produce an evidence gap. Renamed or re-imported projects need exports with consistent identity before comparison. The narrative checks are keyword heuristics, not semantic understanding; this is not critical-path, causation, entitlement or contractual approval analysis. Validate on representative real exports before operational use.
+Project names and source IDs must match. Identical evidence and reversed data dates are rejected. Missing activity finish or float fields produce an evidence gap. Renamed or re-imported projects need exports with consistent identity before comparison. The narrative checks are bounded keyword heuristics with local negation handling, not semantic understanding; factual changes persist independently of narrative wording; this is not critical-path, causation, entitlement or contractual approval analysis. Validate on representative real exports before operational use.
## Results
@@ -89,7 +89,7 @@ For each of 8 labelled queries in [`eval_queries.json`](Processing/precedent_rag
What it does not show: 8 author-written queries are a smoke test, not a benchmark. The hashing embedder is a lexical baseline and does not measure the Gemini path (`make precedent-eval`); no Gemini hit rate is published. It scores retrieval only, not the quality of the generated brief.
-**Change-assurance behaviour** is checked in a real browser (Playwright). On Northstar the suite asserts the 73-day headline and exactly three blockers. It also exercises real file inputs, the decision and condition flow, exported packs, and mobile layouts. The XER parser is tested on the Northstar and Riverside fixtures plus synthetic format variants (BOM; LF, CRLF and CR line endings; reordered fields). These are not real client exports; see the [parser compatibility contract](docs/PARSER_COMPATIBILITY.md).
+**Change-assurance behaviour** is checked in a real browser (Playwright). On Northstar the suites assert the 73-day movement, five total questions and three prioritised findings. The landing suite also checks the real-engine preview, keyboard selections, explicit animation and reduced motion, own-file input, revised and original narratives, draft restoration, export, condition closure, five viewport widths and loading recovery. Adversarial narratives cannot erase schedule facts, and “not on track” must not become a stability claim. It also exercises real file inputs, the decision and condition flow, exported packs, and mobile layouts. The XER parser is tested on the Northstar and Riverside fixtures plus synthetic format variants (BOM; LF, CRLF and CR line endings; reordered fields). These are not real client exports; see the [parser compatibility contract](docs/PARSER_COMPATIBILITY.md).
## Design decisions and trade-offs
diff --git a/docs/assets/change-assurance-overview.png b/docs/assets/change-assurance-overview.png
index a8b7c0b..3020f3a 100644
Binary files a/docs/assets/change-assurance-overview.png and b/docs/assets/change-assurance-overview.png differ
diff --git a/docs/assets/fonts/OFL-Encode-Sans.txt b/docs/assets/fonts/OFL-Encode-Sans.txt
new file mode 100644
index 0000000..9dadca8
--- /dev/null
+++ b/docs/assets/fonts/OFL-Encode-Sans.txt
@@ -0,0 +1,93 @@
+Copyright 2020 The Encode Project Authors (https://github.com/thundernixon/Encode-Sans), with Reserved Font Name "Encode Sans”.
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/docs/assets/fonts/encode-sans-400.ttf b/docs/assets/fonts/encode-sans-400.ttf
new file mode 100644
index 0000000..1191602
Binary files /dev/null and b/docs/assets/fonts/encode-sans-400.ttf differ
diff --git a/docs/assets/fonts/encode-sans-500.ttf b/docs/assets/fonts/encode-sans-500.ttf
new file mode 100644
index 0000000..68a1d41
Binary files /dev/null and b/docs/assets/fonts/encode-sans-500.ttf differ
diff --git a/docs/assets/fonts/encode-sans-600.ttf b/docs/assets/fonts/encode-sans-600.ttf
new file mode 100644
index 0000000..9ca97a9
Binary files /dev/null and b/docs/assets/fonts/encode-sans-600.ttf differ
diff --git a/docs/assets/fonts/encode-sans-700.ttf b/docs/assets/fonts/encode-sans-700.ttf
new file mode 100644
index 0000000..a47fda0
Binary files /dev/null and b/docs/assets/fonts/encode-sans-700.ttf differ
diff --git a/docs/assets/product-landing-overview.png b/docs/assets/product-landing-overview.png
new file mode 100644
index 0000000..0a261cd
Binary files /dev/null and b/docs/assets/product-landing-overview.png differ
diff --git a/docs/change-assurance.html b/docs/change-assurance.html
index 835cf12..e0c8258 100644
--- a/docs/change-assurance.html
+++ b/docs/change-assurance.html
@@ -3,14 +3,14 @@
Load the previous and current Primavera P6 XER exports and the update narrative. ProjectLens compares the schedules with what the narrative claims, lists up to 3 blockers and keeps a record of what you decide.
-
-
-
+
+
+
The schedule changed. Did the update explain it?
+
Compare two Primavera P6 schedules with the project update. See the evidence behind the movement, then record a human decision.
The standard review reads your files locally. Saving a draft or decision stores it on this device. Downloads give you a copy to keep or share. Optional public-record research is separately configured and receives narrative text, never XER files.
+ Does a flagged change mean the project is delayed?
A later scheduled finish is an observed difference between exports. These checks do not recalculate the critical path or establish cause, contractual entitlement or the right decision.
+ What can I compare?
Two supported, single-project Primavera P6 XER exports from the same source project. ProjectLens checks their identity, dates and field coverage before comparing them. Missing fields and unsupported inputs are reported.
+
+
+
Bring a change pack. Leave with clearer questions.
Start with the example, or review your own two schedule exports.
+
+
+
Add your two XER files
Export both schedules from Primavera P6 (File → Export → Primavera XER): the previous submission and the current one. Your browser reads the files and uploads nothing. Add the narrative if you want ProjectLens to check what the update claims.
@@ -126,11 +152,19 @@
Check the change pack before you decide.
Needs evidence before decision
The proposed finish is not supported by the current narrative.
Resolve the blockers below or record why the decision authority accepts them.
+
+
+ Check a revised narrative
+
Recheck a revision against these same schedules. The first checked narrative stays in the record. Unchecked edits do not change the decision evidence.
+
+
+
+
Findings
@@ -224,5 +258,6 @@
Similar cases from public records
+
diff --git a/docs/change-assurance.js b/docs/change-assurance.js
index 2573633..a85f320 100644
--- a/docs/change-assurance.js
+++ b/docs/change-assurance.js
@@ -16,6 +16,7 @@ if (IS_WALKTHROUGH) {
const assuranceState = {
files: { previous: null, current: null },
review: null,
+ originalNarrative: null,
// Optional context stays human-controlled: use / ignore only, never auto-apply.
precedents: { items: [], used: {}, ignored: {}, summary: null, mode: null },
};
@@ -202,7 +203,10 @@ function analyseReview(previous, current, narrative) {
const finishMovement = dayDifference(current.project.scheduledFinish, previous.project.scheduledFinish);
const changes = taskChanges(previous, current);
const text = narrative.toLowerCase();
- const statedStable = /\b(unchanged|no material change|on track|as planned|remains stable|no impact)\b/.test(text);
+ // These are bounded text prompts, not a judgement that a claim is true.
+ // Exclude explicit local negations such as "not on track" before matching.
+ const stabilityText = text.replace(/\b(?:not|never|no longer)\s+(?:\w+\s+){0,3}(?:on track|unchanged|as planned|stable)\b/g, "");
+ const statedStable = /\b(unchanged|no material change|on track|as planned|remains stable|no impact)\b/.test(stabilityText);
const blockers = [];
const coverage = [previous, current].map(schedule => ({ file: schedule.fileName, tasks: schedule.tasks.length, finishDates: schedule.tasks.filter(task => task.finish !== null).length, floatValues: schedule.tasks.filter(task => task.float !== null).length }));
if (coverage.some(item => item.finishDates !== item.tasks || item.floatValues !== item.tasks)) blockers.push({ type: "Evidence gap", title: "Some activity finish dates or float values could not be compared", detail: coverage.map(item => `${item.file}: ${item.finishDates}/${item.tasks} finishes and ${item.floatValues}/${item.tasks} float values`).join("; ") });
@@ -220,29 +224,32 @@ function analyseReview(previous, current, narrative) {
title: `The finish moved ${finishMovement} days but the narrative says the schedule is stable`,
detail: `${formatDate(previous.project.scheduledFinish)} → ${formatDate(current.project.scheduledFinish)}.`,
});
+ } else if (finishMovement !== null && Math.abs(finishMovement) >= 7) {
+ blockers.push({ type: "Schedule movement", title: `The scheduled finish moved ${finishMovement > 0 ? "+" : ""}${finishMovement} days`, detail: "Check the reason and supporting evidence. Mentioning the movement does not verify its cause." });
}
- if (changes.constraintChanges && !/\bconstraint|mandatory|fixed date|must finish|must start\b/.test(text)) {
+ if (changes.constraintChanges) {
blockers.push({
type: "Unexplained change",
- title: `The narrative does not mention ${changes.constraintChanges} constraint changes`,
- detail: "Dated constraints can change float and which path drives the finish.",
+ title: `${changes.constraintChanges} constraint changes need review`,
+ detail: "Constraint type or date changed. Inspect the source fields and the instruction supporting each change; narrative wording does not close this review question.",
});
}
- if (changes.relationChanges && !/\b(logic|relationship|predecessor|successor|resequence|sequence)\b/.test(text)) {
+ if (changes.relationChanges) {
blockers.push({
type: "Unexplained change",
- title: `The narrative does not mention ${changes.relationChanges} relationship changes`,
- detail: "Changed logic can move the driving path even when the headline dates stay the same.",
+ title: `${changes.relationChanges} relationship signatures changed`,
+ detail: "Added or removed predecessor signatures can affect sequencing. Ask which instruction explains the differences; these exports do not establish causation.",
});
}
- if (changes.floatErosion && statedStable) {
+ if (changes.floatErosion) {
blockers.push({
type: "Float loss",
title: `Float reduced on ${changes.floatErosion} changed activities`,
- detail: "The narrative calls the schedule stable, but these activities now have less float.",
+ detail: "Reported total float is lower in the current export. Check the underlying fields and planning assumptions; this does not prove a delay or a critical path.",
});
}
- if (/\b(manage|mitigat|governance|action)\b/.test(text) && !/\b(owner|responsible|accountable|due|by \d{1,2})\b/.test(text)) {
+ const explicitMissingOwnership = /\b(?:no|without|missing|unassigned)\s+(?:named\s+)?(?:owner|due date)\b|\b(?:owner|due date)\s+(?:is\s+)?(?:missing|unassigned|unknown|not set)\b/.test(text);
+ if (explicitMissingOwnership || (/\b(manage\w*|mitigat\w*|governance|action)\b/.test(text) && !/\b(owner|responsible|accountable|due|by \d{1,2})\b/.test(text))) {
blockers.push({
type: "Ownership gap",
title: "The stated response has no named owner or due date",
@@ -305,16 +312,18 @@ async function inputIdentity() {
function renderReview(review, restoredContext = null) {
assuranceState.review = review;
+ $("#revisedNarrative").value = $("#assuranceNarrative").value;
+ $("#narrativeRevision").open = false;
const ready = review.blockers.length === 0;
const topBlockers = review.blockers.slice(0, 3);
$("#reviewName").textContent = review.current.project.name;
$("#reviewEvidenceVersion").textContent = `${evidenceVersion(review)} · checked ${new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }).format(new Date(review.generatedAt))}`;
$("#readinessCard").classList.toggle("ready", ready);
- $("#readinessStatus").textContent = ready ? "Ready for human decision" : "Needs evidence before decision";
+ $("#readinessStatus").textContent = ready ? "Human review still required" : "Needs evidence before decision";
document.body.classList.add("has-review");
$("#readinessHeadline").textContent = ready ? "Review the evidence and record a decision" : `${topBlockers.length} findings need your review${review.blockers.length > 3 ? ` · ${review.blockers.length} total` : ""}`;
$("#readinessExplanation").textContent = ready
- ? "No blockers found by these fixed checks. Professional and contractual review still applies."
+ ? "These fixed checks raised no additional questions. Inspect all source facts; this is not an approval or an assurance verdict."
: "Select a finding to check its source. Resolve it or record why the decision authority accepts it.";
$("#blockerList").innerHTML = topBlockers.length ? topBlockers.map((blocker, index) => `
@@ -341,6 +350,7 @@ function renderReview(review, restoredContext = null) {
];
$("#evidenceSummary").innerHTML = facts.map(([label, value, detail]) => `
${escapeHtml(label)}${escapeHtml(value)}${escapeHtml(detail)}`).join("");
+ $("#reviewFacts").innerHTML = facts.slice(1, 5).map(([label, value, detail]) => `
${escapeHtml(label)}
${escapeHtml(value)}
${escapeHtml(detail)}
`).join("");
// kick off cited precedent retrieval — XER never leaves the browser; only narrative + blockers
assuranceState.precedents = { items: [], used: {}, ignored: {}, summary: null, mode: null };
@@ -571,6 +581,9 @@ function setPrecedentGate(id, gate) {
function questionFor(blocker) {
if (blocker.type === "Contradiction") return "How should the narrative be corrected or evidenced against the finish movement?";
+ if (blocker.type === "Schedule movement") return "What source evidence explains the scheduled finish movement?";
+ if (blocker.title.includes("constraint")) return "Which instruction or approval explains each constraint change?";
+ if (blocker.title.includes("relationship")) return "What changed in the delivery sequence, and why?";
if (blocker.type === "Ownership gap") return "Who owns the stated response, and by when must it be complete?";
if (blocker.type === "Evidence gap") return "What narrative, reason and approval basis supports this submission?";
if (blocker.type === "Float loss") return "Which approved change explains the lost float, and does the narrative need correcting?";
@@ -584,6 +597,7 @@ async function loadFile(kind, file) {
return;
}
assuranceState.files[kind] = { name: file.name, text: await file.text() };
+ assuranceState.originalNarrative = null;
const label = kind === "previous" ? $("#assurancePreviousName") : $("#assuranceCurrentName");
label.textContent = file.name;
label.closest(".assurance-file").classList.add("loaded");
@@ -602,6 +616,7 @@ async function runReview() {
const previous = parseXer(assuranceState.files.previous.text, assuranceState.files.previous.name);
const current = parseXer(assuranceState.files.current.text, assuranceState.files.current.name);
const review = analyseReview(previous, current, $("#assuranceNarrative").value.trim());
+ assuranceState.originalNarrative ??= $("#assuranceNarrative").value;
review.inputIdentity = await inputIdentity();
renderReview(review);
showToast("Readiness check complete. Files remained in this browser.");
@@ -629,6 +644,7 @@ async function loadDemo() {
assuranceState.files.previous = { name: "northstar-previous.xer", text: previousText };
assuranceState.files.current = { name: "northstar-current.xer", text: currentText };
$("#assuranceNarrative").value = narrative;
+ assuranceState.originalNarrative = narrative;
$("#assurancePreviousName").textContent = "northstar-previous.xer";
$("#assuranceCurrentName").textContent = "northstar-current.xer";
$$(".assurance-file").forEach(card => card.classList.add("loaded"));
@@ -654,6 +670,7 @@ async function loadDemo() {
function showView(name) {
const safeName = ["reviews", "decisions", "follow-up"].includes(name) ? name : "reviews";
+ document.body.dataset.currentView = safeName;
$$("[data-view]").forEach(view => {
const active = view.dataset.view === safeName;
view.hidden = !active;
@@ -717,6 +734,7 @@ function decisionExportText() {
lines.push(`- Input SHA-256: ${item.inputIdentity || "Unavailable (legacy record)"}`);
if (item.sourceEvidence) lines.push(`- Scheduled finish: ${item.sourceEvidence.finishPrevious} → ${item.sourceEvidence.finishCurrent}`);
if (item.narrative) lines.push(`- Narrative under review: ${item.narrative}`);
+ if (item.originalNarrative !== undefined) lines.push(`- Original narrative at first check: ${item.originalNarrative}`);
(item.sourceEvidence?.activityEvidence || []).forEach(activity => lines.push(`- Activity ${activity.code} (${activity.name}): previous ${JSON.stringify(activity.before)}; current ${JSON.stringify(activity.after)}`));
(item.sourceEvidence?.relationEvidence || []).forEach(relation => lines.push(`- ${relation.change} relationship: ${relation.predecessor} → ${relation.successor}; type ${relation.type}; lag ${relation.lagHours === null ? "Not supplied" : `${relation.lagHours} hours`}`));
if (item.unresolved) lines.push(`- Unresolved blockers preserved: ${item.unresolved}`);
@@ -815,6 +833,7 @@ function saveDecision(event) {
precedentsNotReviewed: pending,
findings: review.blockers,
narrative: $("#assuranceNarrative").value,
+ originalNarrative: assuranceState.originalNarrative,
sourceEvidence: { finishPrevious: formatDate(review.previous.project.scheduledFinish), finishCurrent: formatDate(review.current.project.scheduledFinish), materialChanges: review.changes.changes, activityEvidence: review.changes.activityEvidence, relationEvidence: review.changes.relationEvidence },
contextSources: assuranceState.precedents.items.filter(item => usedIds.includes(item.id)),
precedentSummary: usedIds.length ? (assuranceState.precedents.summary?.text || null) : null,
@@ -850,11 +869,14 @@ function saveDecision(event) {
document.dispatchEvent(new Event("projectlens:decision-saved"));
}
-function resetReview() {
- if (assuranceState.review && !window.confirm("Start another review? Unsaved work in this tab will be cleared. A saved device draft is kept until you delete or replace it.")) return;
+function resetReview(options = {}) {
+ const landing = options.landing === true;
+ if ((assuranceState.review || assuranceState.files.previous || assuranceState.files.current) && !window.confirm("Start another review? Unsaved work in this tab will be cleared. A saved device draft is kept until you delete or replace it.")) return;
document.body.classList.remove("has-review");
+ document.body.classList.toggle("has-intake", !landing);
assuranceState.files = { previous: null, current: null };
assuranceState.review = null;
+ assuranceState.originalNarrative = null;
$("#assurancePrevious").value = "";
$("#assuranceCurrent").value = "";
$("#assuranceNarrative").value = "";
@@ -864,19 +886,25 @@ function resetReview() {
$("#runAssuranceReview").disabled = true;
$("#intakeMessage").textContent = "Choose 2 XER files to begin.";
$("#readinessWorkspace").hidden = true;
- $("#packIntake").hidden = false;
- $("#packIntake").scrollIntoView({ behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "instant" : "smooth" });
+ $("#packIntake").hidden = landing;
+ moveTo(landing ? $("#productTitle") : $("#intakeTitle"));
}
function bindEvents() {
$("#loadAssuranceDemo").addEventListener("click", loadDemo);
$("#showOwnPack").addEventListener("click", () => {
+ document.body.classList.add("has-intake");
$("#packIntake").hidden = false;
- $("#packIntake").scrollIntoView({ behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "instant" : "smooth" });
+ moveTo($("#intakeTitle"));
});
+ $("#backToProduct").addEventListener("click", () => resetReview({ landing: true }));
$("#assurancePrevious").addEventListener("change", event => loadFile("previous", event.target.files[0]));
$("#assuranceCurrent").addEventListener("change", event => loadFile("current", event.target.files[0]));
$("#runAssuranceReview").addEventListener("click", runReview);
+ $("#recheckNarrative").addEventListener("click", () => {
+ $("#assuranceNarrative").value = $("#revisedNarrative").value;
+ void runReview();
+ });
$("#startAnotherReview").addEventListener("click", resetReview);
$("#reviewPrecedents")?.addEventListener("click", () => openPrecedentSection({ scroll: true }));
$("#requestAnswers").addEventListener("click", () => {
@@ -955,12 +983,14 @@ function renderSelectedFinding(index) {
const relationshipRows = review.changes.relationEvidence.map(relation => `
${escapeHtml($("#assuranceNarrative").value || "No narrative supplied.")}
+ ${assuranceState.originalNarrative !== null && assuranceState.originalNarrative !== $("#assuranceNarrative").value ? `Read original narrative from the first check
${escapeHtml(assuranceState.originalNarrative || "No narrative was supplied at the first check.")}
` : ""}
Field coverage: ${review.coverage.map(item => `${escapeHtml(item.file)}: ${item.finishDates}/${item.tasks} finishes, ${item.floatValues}/${item.tasks} float values`).join("; ")}. Fixed checks flag conflicts. They do not establish causation, entitlement or contractual approval.