PageSourceSearch

https://paloframework.org/assets/palo-case-file.js?v=3.1.0-8843ecb7b89e81c9

js paloframework.org collected 2026-10-02 01:07:12 UTC 20,326 bytes, 324 lines download raw bytes

1(function (global) {
2  "use strict";
3
4  var CASE_FORMAT = "palo-case-file";
5  var BUNDLE_FORMAT = "palo-evidence-bundle";
6  var VERSION = "1.0.0";
7  var CASE_KEY = "palo.case-file.v1";
8  var HANDOFF_KEY = "palo.handoff.v1";
9  var DISCLAIMER = "PALO provides educational governance support, not certification or legal advice. Verify decisions against current official sources and qualified human review.";
10  var ARRAY_IDS = { assessments: "assessmentId", evidence: "evidenceId", sources: "sourceId", incidents: "incidentId", handoffs: "handoffId" };
11
12  function clone(value) {
13    return value == null ? value : JSON.parse(JSON.stringify(value));
14  }
15
16  function slug(value) {
17    return String(value || "case").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 42) || "case";
18  }
19
20  function token() {
21    if (global.crypto && global.crypto.getRandomValues) {
22      var values = new Uint32Array(2);
23      global.crypto.getRandomValues(values);
24      return Array.prototype.map.call(values, function (value) { return value.toString(36); }).join("").slice(0, 10);
25    }
26    return Math.random().toString(36).slice(2, 12);
27  }
28
29  function id(prefix, label) {
30    return prefix + "-" + slug(label) + "-" + token();
31  }
32
33  function now() { return new Date().toISOString(); }
34  function isObject(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }
35  function isDate(value) {
36    return typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && !Number.isNaN(Date.parse(value));
37  }
38  function error(path, message) { return { path: path, message: message }; }
39
40  function validateSource(source, path, errors) {
41    if (!isObject(source)) { errors.push(error(path, "must be an object")); return; }
42    ["sourceId", "title", "url", "sourceType", "publisher"].forEach(function (key) {
43      if (typeof source[key] !== "string" || !source[key]) errors.push(error(path + "." + key, "must be a non-empty string"));
44    });
45    if (typeof source.url === "string") {
46      try { new URL(source.url); } catch (urlError) { errors.push(error(path + ".url", "must be an absolute URI")); }
47    }
48    if (["official", "standard", "organizational", "research", "monitoring-signal"].indexOf(source.sourceType) === -1) errors.push(error(path + ".sourceType", "is not supported"));
49    if (!isDate(source.checkedAt)) errors.push(error(path + ".checkedAt", "must be an ISO date-time"));
50    if (!isObject(source.freshness)) errors.push(error(path + ".freshness", "must be an object"));
51    else {
52      if (["current", "review-due", "stale", "superseded", "unknown"].indexOf(source.freshness.status) === -1) errors.push(error(path + ".freshness.status", "is not supported"));
53      if (!Number.isInteger(source.freshness.reviewIntervalDays) || source.freshness.reviewIntervalDays < 1) errors.push(error(path + ".freshness.reviewIntervalDays", "must be a positive integer"));
54      if (!isDate(source.freshness.nextReviewAt)) errors.push(error(path + ".freshness.nextReviewAt", "must be an ISO date-time"));
55    }
56  }
57
58  function validateCase(value) {
59    var errors = [];
60    if (!isObject(value)) return { valid: false, type: null, errors: [error("$", "must be an object")] };
61    if (value.format !== CASE_FORMAT) errors.push(error("$.format", "must equal " + CASE_FORMAT));
62    if (value.schemaVersion !== VERSION) errors.push(error("$.schemaVersion", "must equal " + VERSION));
63    if (!/^case-[a-z0-9][a-z0-9-]{5,63}$/.test(value.caseId || "")) errors.push(error("$.caseId", "must be a valid case id"));
64    if (typeof value.title !== "string" || !value.title.trim()) errors.push(error("$.title", "must be a non-empty string"));
65    if (["draft", "active", "review", "closed", "reopened"].indexOf(value.status) === -1) errors.push(error("$.status", "is not supported"));
66    ["createdAt", "updatedAt"].forEach(function (key) { if (!isDate(value[key])) errors.push(error("$." + key, "must be an ISO date-time")); });
67    if (!isObject(value.context)) errors.push(error("$.context", "must be an object"));
68    ["assessments", "evidence", "sources", "incidents", "handoffs"].forEach(function (key) { if (!Array.isArray(value[key])) errors.push(error("$." + key, "must be an array")); });
69    (value.assessments || []).forEach(function (item, index) {
70      if (!isObject(item) || !item.assessmentId || !item.module || !isDate(item.recordedAt) || !isObject(item.data)) errors.push(error("$.assessments[" + index + "]", "requires assessmentId, module, recordedAt, and object data"));
71    });
72    (value.evidence || []).forEach(function (item, index) {
73      if (!isObject(item) || !item.evidenceId || !item.title || !item.kind || ["open", "draft", "ready", "verified", "superseded"].indexOf(item.status) === -1 || !isDate(item.recordedAt)) errors.push(error("$.evidence[" + index + "]", "requires evidenceId, title, kind, supported status, and recordedAt"));
74    });
75    (value.sources || []).forEach(function (item, index) { validateSource(item, "$.sources[" + index + "]", errors); });
76    (value.incidents || []).forEach(function (item, index) {
77      if (!isObject(item) || !item.incidentId || !item.triggerId || !isDate(item.occurredAt) || !item.summary || !Array.isArray(item.reopenGates) || !item.reopenGates.length) errors.push(error("$.incidents[" + index + "]", "requires incidentId, triggerId, occurredAt, summary, and reopenGates"));
78    });
79    (value.handoffs || []).forEach(function (item, index) {
80      if (!isObject(item) || !item.handoffId || !item.from || !item.to || !isDate(item.createdAt)) errors.push(error("$.handoffs[" + index + "]", "requires handoffId, from, to, and createdAt"));
81    });
82    return { valid: errors.length === 0, type: CASE_FORMAT, errors: errors };
83  }
84
85  function validateBundle(value) {
86    var errors = [];
87    if (!isObject(value)) return { valid: false, type: null, errors: [error("$", "must be an object")] };
88    if (value.format !== BUNDLE_FORMAT) errors.push(error("$.format", "must equal " + BUNDLE_FORMAT));
89    if (value.schemaVersion !== VERSION) errors.push(error("$.schemaVersion", "must equal " + VERSION));
90    if (!/^bundle-[a-z0-9][a-z0-9-]{5,63}$/.test(value.bundleId || "")) errors.push(error("$.bundleId", "must be a valid bundle id"));
91    if (!isDate(value.generatedAt)) errors.push(error("$.generatedAt", "must be an ISO date-time"));
92    if (!Array.isArray(value.artifacts)) errors.push(error("$.artifacts", "must be an array"));
93    (value.artifacts || []).forEach(function (item, index) {
94      if (!isObject(item) || !item.artifactId || !item.title || !item.kind || ["open", "draft", "ready", "verified", "superseded"].indexOf(item.status) === -1) errors.push(error("$.artifacts[" + index + "]", "requires artifactId, title, kind, and a supported status"));
95    });
96    if (!Array.isArray(value.sourceRegistry)) errors.push(error("$.sourceRegistry", "must be an array"));
97    (value.sourceRegistry || []).forEach(function (item, index) { validateSource(item, "$.sourceRegistry[" + index + "]", errors); });
98    if (!isObject(value.freshness) || !isDate(value.freshness.evaluatedAt) || ["current", "review-due", "stale", "unknown"].indexOf(value.freshness.status) === -1) errors.push(error("$.freshness", "requires evaluatedAt and a supported status"));
99    return { valid: errors.length === 0, type: BUNDLE_FORMAT, errors: errors };
100  }
101
102  function validate(value) {
103    if (value && value.format === CASE_FORMAT) return validateCase(value);
104    if (value && value.format === BUNDLE_FORMAT) return validateBundle(value);
105    return { valid: false, type: null, errors: [error("$.format", "unsupported or missing format")] };
106  }
107
108  function createCase(options) {
109    options = options || {};
110    var stamp = options.createdAt || now();
111    return Object.assign({}, clone(options.extensions || {}), {
112      format: CASE_FORMAT,
113      schemaVersion: VERSION,
114      caseId: options.caseId || id("case", options.title),
115      title: options.title || "Untitled PALO case",
116      status: options.status || "draft",
117      createdAt: stamp,
118      updatedAt: options.updatedAt || stamp,
119      owner: options.owner || "",
120      context: clone(options.context || {}),
121      assessments: clone(options.assessments || []),
122      evidence: clone(options.evidence || []),
123      sources: clone(options.sources || []),
124      incidents: clone(options.incidents || []),
125      handoffs: clone(options.handoffs || [])
126    });
127  }
128
129  function sourceFromUrl(url, index, stamp) {
130    var host = "Imported source";
131    try { host = new URL(url).hostname; } catch (urlError) { /* validation will flag the URL */ }
132    var checked = stamp || now();
133    var next = new Date(Date.parse(checked) + 90 * 86400000).toISOString();
134    return { sourceId: "legacy-source-" + (index + 1), title: host, url: url, sourceType: "official", publisher: host, checkedAt: checked, freshness: { status: "unknown", reviewIntervalDays: 90, nextReviewAt: next } };
135  }
136
137  function migrateLegacyBundle(value) {
138    var stamp = isDate(value.generatedAt) ? value.generatedAt : now();
139    var title = value.assessment && value.assessment.systemName ? value.assessment.systemName : "Imported assessment";
140    return Object.assign({}, clone(value), {
141      format: BUNDLE_FORMAT,
142      schemaVersion: VERSION,
143      bundleId: value.bundleId || id("bundle", title),
144      generatedAt: stamp,
145      artifacts: Array.isArray(value.artifacts) ? clone(value.artifacts) : [{ artifactId: "legacy-assessment-route", title: "Imported assessment route", kind: "assessment-route", status: "ready", content: { route: clone(value.route || []), evidenceReadiness: clone(value.evidenceReadiness || {}) } }],
146      sourceRegistry: Array.isArray(value.sourceRegistry) ? clone(value.sourceRegistry) : (value.sources || []).map(function (url, index) { return sourceFromUrl(url, index, stamp); }),
147      freshness: isObject(value.freshness) ? clone(value.freshness) : { evaluatedAt: stamp, status: "unknown" },
148      disclaimer: value.disclaimer || DISCLAIMER
149    });
150  }
151
152  function normalize(value) {
153    if (value && value.formatVersion === "PALO Evidence Bundle 0.1") return migrateLegacyBundle(value);
154    return clone(value);
155  }
156
157  function importJSON(input) {
158    function finish(value) {
159      var normalized = normalize(value);
160      var result = validate(normalized);
161      if (!result.valid) {
162        var importError = new Error("Invalid PALO file: " + result.errors.map(function (item) { return item.path + " " + item.message; }).join("; "));
163        importError.validation = result;
164        throw importError;
165      }
166      return { document: normalized, type: result.type, migrated: Boolean(value && value.formatVersion === "PALO Evidence Bundle 0.1"), validation: result };
167    }
168    if (typeof input === "string") return Promise.resolve(finish(JSON.parse(input)));
169    if (input && typeof input.text === "function") return input.text().then(function (text) { return finish(JSON.parse(text)); });
170    return Promise.resolve(finish(input));
171  }
172
173  function mergeValue(base, incoming, key) {
174    if (Array.isArray(base) && Array.isArray(incoming)) {
175      var identity = ARRAY_IDS[key];
176      if (!identity) return clone(base).concat(clone(incoming));
177      var output = clone(base);
178      var positions = new Map();
179      output.forEach(function (item, index) { if (item && item[identity] != null) positions.set(item[identity], index); });
180      incoming.forEach(function (item) {
181        var index = item && positions.has(item[identity]) ? positions.get(item[identity]) : -1;
182        if (index === -1) output.push(clone(item));
183        else output[index] = mergeValue(output[index], item, "");
184        if (index === -1 && item && item[identity] != null) positions.set(item[identity], output.length - 1);
185      });
186      return output;
187    }
188    if (isObject(base) && isObject(incoming)) {
189      var object = clone(base);
190      Object.keys(incoming).forEach(function (childKey) { object[childKey] = childKey in object ? mergeValue(object[childKey], incoming[childKey], childKey) : clone(incoming[childKey]); });
191      return object;
192    }
193    return clone(incoming);
194  }
195
196  function merge(base, incoming) {
197    var left = clone(base);
198    var right = clone(incoming);
199    if (right && right.format === BUNDLE_FORMAT) right = bundleToCase(right);
200    if (!left) left = createCase({ title: right && right.title });
201    if (right && right.caseId && left.caseId && right.caseId !== left.caseId) throw new Error("Cannot merge different PALO cases (" + left.caseId + " and " + right.caseId + ")");
202    var result = mergeValue(left, right, "");
203    delete result.mobileIntegrity; // Any edit invalidates the imported transport digest.
204    result.updatedAt = now();
205    var validation = validateCase(result);
206    if (!validation.valid) throw new Error("Merged case is invalid: " + validation.errors.map(function (item) { return item.path + " " + item.message; }).join("; "));
207    return result;
208  }
209
210  function bundleToCase(bundle) {
211    var stamp = bundle.generatedAt || now();
212    var assessment = clone(bundle.assessment || {});
213    var title = assessment.systemName || "Imported PALO assessment";
214    return createCase({
215      caseId: bundle.caseId || undefined,
216      title: title,
217      createdAt: stamp,
218      updatedAt: stamp,
219      context: { organization: assessment.organization || "", sector: assessment.sector || "", intendedUse: assessment.useCase || "" },
220      assessments: [{ assessmentId: "assessment-path-" + slug(bundle.bundleId), module: "assessment-path", recordedAt: stamp, data: { assessment: assessment, route: clone(bundle.route || []), evidenceReadiness: clone(bundle.evidenceReadiness || {}) } }],
221      evidence: (bundle.artifacts || []).map(function (artifact) { return Object.assign({}, clone(artifact), { evidenceId: artifact.evidenceId || artifact.artifactId, recordedAt: artifact.recordedAt || stamp }); }),
222      sources: clone(bundle.sourceRegistry || []),
223      extensions: { importedBundle: clone(bundle) }
224    });
225  }
226
227  function save(caseFile) {
228    var result = validateCase(caseFile);
229    if (!result.valid) return { ok: false, errors: result.errors };
230    try { global.localStorage.setItem(CASE_KEY, JSON.stringify(caseFile)); return { ok: true, caseFile: clone(caseFile) }; }
231    catch (storageError) { return { ok: false, errors: [error("storage", storageError.message)] }; }
232  }
233
234  function load() {
235    try {
236      var stored = global.localStorage.getItem(CASE_KEY);
237      if (!stored) return null;
238      var parsed = JSON.parse(stored);
239      return validateCase(parsed).valid ? parsed : null;
240    } catch (storageError) { return null; }
241  }
242
243  function clear() {
244    try { global.localStorage.removeItem(CASE_KEY); global.sessionStorage.removeItem(HANDOFF_KEY); return true; }
245    catch (storageError) { return false; }
246  }
247
248  function handoff(caseFile, from, to, detail) {
249    var record = { handoffId: id("handoff", from + "-" + to), from: from, to: to, createdAt: now(), detail: clone(detail || {}) };
250    var updated = merge(caseFile, { handoffs: [record] });
251    var payload = { contractVersion: VERSION, from: from, to: to, createdAt: record.createdAt, 
251caseFile: updated };
252    var saved = save(updated);
253    if (!saved.ok) return { ok: false, caseFile: updated, errors: saved.errors };
254    try { global.sessionStorage.setItem(HANDOFF_KEY, JSON.stringify(payload)); }
255    catch (storageError) { return { ok: false, caseFile: updated, errors: [error("storage", storageError.message)] }; }
256    return { ok: true, caseFile: updated, payload: payload };
257  }
258
259  function consumeHandoff(target) {
260    try {
261      var raw = global.sessionStorage.getItem(HANDOFF_KEY);
262      if (!raw) return null;
263      var payload = JSON.parse(raw);
264      if (payload.contractVersion !== VERSION || (target && payload.to !== target) || !validateCase(payload.caseFile).valid) return null;
265      var saved = save(payload.caseFile);
266      if (!saved.ok) return null;
267      global.sessionStorage.removeItem(HANDOFF_KEY);
268      return payload;
269    } catch (storageError) { return null; }
270  }
271
272  function markdownValue(value) { return String(value == null || value === "" ? "Not provided" : value).replace(/\r?\n/g, " "); }
273
274  function caseMarkdown(caseFile) {
275    var lines = ["# PALO case file", "", "- Case ID: " + caseFile.caseId, "- Title: " + caseFile.title, "- Status: " + caseFile.status, "- Updated: " + caseFile.updatedAt, "- Owner: " + markdownValue(caseFile.owner), "", "## Context", ""];
276    Object.keys(caseFile.context || {}).sort().forEach(function (key) { lines.push("- " + key + ": " + markdownValue(caseFile.context[key])); });
277    lines.push("", "## Assessments", "");
278    caseFile.assessments.forEach(function (item) { lines.push("- " + item.module + " (" + item.recordedAt + ")"); });
279    lines.push("", "## Evidence", "");
280    caseFile.evidence.forEach(function (item) { lines.push("- " + item.title + " | " + item.kind + " | " + item.status); });
281    lines.push("", "## Sources and freshness", "");
282    caseFile.sources.forEach(function (item) { lines.push("- [" + item.title + "](" + item.url + ") | " + item.freshness.status + " | checked " + item.checkedAt + " | next review " + item.freshness.nextReviewAt); });
283    lines.push("", "## Incidents and reopened gates", "");
284    caseFile.incidents.forEach(function (item) { lines.push("- " + item.summary + " | " + item.reopenGates.join(", ")); });
285    lines.push("", "## Disclaimer", "", DISCLAIMER, "");
286    return lines.join("\n");
287  }
288
289  function boardPack(caseFile) {
290    var latest = caseFile.assessments[caseFile.assessments.length - 1];
291    var openEvidence = caseFile.evidence.filter(function (item) { return ["ready", "verified"].indexOf(item.status) === -1; });
292    var dueSources = caseFile.sources.filter(function (item) { return item.freshness.status !== "current"; });
293    var lines = ["# PALO board review pack", "", "## Decision header", "", "- Case: " + caseFile.title + " (" + caseFile.caseId + ")", "- Status: " + caseFile.status, "- Accountable owner: " + markdownValue(caseFile.owner), "- Last updated: " + caseFile.updatedAt, "", "## Decision requested", "", markdownValue(caseFile.context.decisionRequested || "Record a proceed, conditional-proceed, pause, or stop decision with rationale and named owner."), "", "## Operating context", ""];
294    ["organization", "sector", "intendedUse", "affectedPeople"].forEach(function (key) { if (caseFile.context[key]) lines.push("- " + key + ": " + markdownValue(caseFile.context[key])); });
295    lines.push("", "## Latest assessment", "", latest ? "- " + latest.module + " recorded " + latest.recordedAt : "- No assessment recorded", "", "## Evidence readiness", "", "- Total artifacts: " + caseFile.evidence.length, "- Open or draft artifacts: " + openEvidence.length, "", "## Source freshness", "", "- Registered sources: " + caseFile.sources.length, "- Sources requiring review: " + dueSources.length);
296    dueSources.forEach(function (item) { lines.push("- Review " + item.title + " (" + item.freshness.status + ")"); });
297    lines.push("", "## Incidents and material changes", "", "- Recorded triggers: " + caseFile.incidents.length);
298    caseFile.incidents.forEach(function (item) { lines.push("- " + item.summary + "; reopen " + item.reopenGates.join(", ")); });
299    lines.push("", "## Board record", "", "- Decision: [record decision]", "- Conditions: [record conditions]", "- Residual risk owner: [name role]", "- Next review: [date or trigger]", "", "## Disclaimer", "", DISCLAIMER, "");
300    return lines.join("\n");
301  }
302
303  function exportJSON(document) { var copy = clone(document); delete copy.mobileIntegrity; return JSON.stringify(copy, null, 2) + "\n"; }
304
305  global.PALOCaseFile = Object.freeze({
306    version: VERSION,
307    formats: Object.freeze({ caseFile: CASE_FORMAT, evidenceBundle: BUNDLE_FORMAT }),
308    storageKeys: Object.freeze({ caseFile: CASE_KEY, handoff: HANDOFF_KEY }),
309    create: createCase,
310    validate: validate,
311    import: importJSON,
312    exportJSON: exportJSON,
313    exportMarkdown: caseMarkdown,
314    merge: merge,
315    bundleToCase: bundleToCase,
316    save: save,
317    load: load,
318    clear: clear,
319    handoff: handoff,
320    consumeHandoff: consumeHandoff,
321    boardPack: boardPack,
322    disclaimer: DISCLAIMER
323  });
324}(window));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.