The proposed finish is not supported by the current narrative.
Resolve the blockers below or record why the decision authority accepts them.
-Resolve the blockers below or record why the decision authority accepts them.
-ProjectLens saves the evidence version and any unresolved blockers with your decision. You make the decision; ProjectLens does not.
Use attaches the case and its sources to the decision record. Ignore records that you looked at it and set it aside. Mark every card before you save. The XER comparison runs in this browser with fixed rules. The optional local retrieval service sees only the narrative text and filters.
+Use attaches the case and its sources as context. Ignore records that you considered and dismissed it. Untouched cards stay not reviewed. Bundled records are illustrative context, not matched causal precedents. The XER comparison runs in this browser with fixed rules. Live retrieval, if configured, receives only narrative text and filters.
Open the full schedule evidence review ↗ How precedent retrieval works ↗No blockers found. Inspect the finish dates and narrative beside this list.
`; + if (review.blockers.length > 3) $("#blockerList").insertAdjacentHTML("beforeend", `${escapeHtml(change.code)} ${escapeHtml(change.name)}${escapeHtml(relation.predecessor)} → ${escapeHtml(relation.successor)}${escapeHtml(finding?.detail || "No blockers found by these fixed checks.")}
+Sources: ${escapeHtml(evidenceVersion(review))} · PROJECT scheduled finish; TASK activity dates, constraints and total float.
+${escapeHtml(($("#assuranceNarrative").value.split(/\n\s*\n/).find(paragraph => /unchanged|on track|stable|no impact/i.test(paragraph)) || $("#assuranceNarrative").value || "No narrative supplied.").trim())}
${escapeHtml($("#assuranceNarrative").value || "No narrative supplied.")}
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.
`; +} + +function saveRecordGroup(updates) { + const previous = {}; + try { + for (const key of Object.keys(updates)) previous[key] = localStorage.getItem(key); + for (const [key, value] of Object.entries(updates)) localStorage.setItem(key, JSON.stringify(value)); + return true; + } catch { + for (const [key, value] of Object.entries(previous)) { + try { if (value === null) localStorage.removeItem(key); else localStorage.setItem(key, value); } catch { /* Keep the open form available for recovery. */ } + } + showError("The decision could not be saved. Keep this tab open, allow site storage or free device space, then retry."); + return false; + } +} + +function saveDraft() { + const fields = Object.fromEntries(new FormData($("#decisionForm"))); + const draft = { inputIdentity: assuranceState.review?.inputIdentity, files: assuranceState.files, narrative: $("#assuranceNarrative").value, fields, precedents: assuranceState.precedents, selectedFinding: assuranceState.selectedFinding, decisionOpen: !$("#decisionPanel").hidden, savedAt: new Date().toISOString() }; + if (!saveStorage(storageKeys.draft, draft)) return; + $("#draftNotice").hidden = false; + $("#draftStatus").textContent = "Draft saved on this device, including XER contents and decision fields. Save again after further edits. Delete it when finished."; + showToast("Draft saved on this device. You can resume after reopening."); +} + +async function resumeDraft() { + try { + const draft = storage(storageKeys.draft); + if (!draft.files?.previous?.text || !draft.files?.current?.text) throw new Error("The saved draft is incomplete. Delete it and reload your two XER files."); + assuranceState.files = draft.files; + $("#assuranceNarrative").value = draft.narrative || ""; + const review = analyseReview(parseXer(draft.files.previous.text, draft.files.previous.name), parseXer(draft.files.current.text, draft.files.current.name), draft.narrative || ""); + review.inputIdentity = await inputIdentity(); + renderReview(review, draft.precedents); + assuranceState.precedents = draft.precedents || assuranceState.precedents; + renderPrecedentPanel({ offline: assuranceState.precedents.mode === "offline-fallback" }); + for (const [name, value] of Object.entries(draft.fields || {})) { + const input = $("#decisionForm").elements.namedItem(name); + if (input) input.value = value; + } + $("#decisionPanel").hidden = !draft.decisionOpen; + renderSelectedFinding(draft.selectedFinding || 0); + showToast("Saved draft restored. Recheck the evidence before deciding."); + } catch (error) { showError(error.message || "The saved draft could not be restored. Reload your source files."); } +} + bindEvents(); +$("#draftNotice").hidden = !storage(storageKeys.draft)?.files; renderConditionRegister(); showView(window.location.hash.slice(1) || "reviews"); const bootStatus = $("#precedentStatus"); diff --git a/requirements.txt b/requirements.txt index c7be335..0079193 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ pandas>=1.5,<2.2 numpy>=1.23,<1.27 +# PyArrow 26 requires NumPy 2; keep the existing NumPy 1 analytics stack compatible. +pyarrow>=14,<26 statsforecast>=1.5,<1.8 ruptures>=1.1,<1.2 openpyxl>=3.0,<3.2