From 650dcb4e5067fbac0d3fcf3f31949bf6fc4654a4 Mon Sep 17 00:00:00 2001 From: farchettiensis <110430578+farchettiensis@users.noreply.github.com> Date: Tue, 19 May 2026 09:37:04 -0300 Subject: [PATCH 1/4] feat: add artifact provenance reconciliation workshop --- .../js-projects/artifact-provenance/script.js | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 fullstack-cert/js-projects/artifact-provenance/script.js diff --git a/fullstack-cert/js-projects/artifact-provenance/script.js b/fullstack-cert/js-projects/artifact-provenance/script.js new file mode 100644 index 00000000..f41787d6 --- /dev/null +++ b/fullstack-cert/js-projects/artifact-provenance/script.js @@ -0,0 +1,206 @@ +// start with an empty records array +const provenanceRecords = []; + +// walk learners through the creation of the first record +const record1 = { + id: "REC-001", + artifactId: "ANT-001", + curatorId: "curator-sofia", + ownerChain: [ + { name: "Cairo Museum", from: 1923, to: 1965 }, + { name: "Hartley Collection", from: 1965, to: 2001 }, + { name: "Meridian Art Trust", from: 2001, to: null }, + ], + notes: [ + "Verified with 1923 acquisition documents", + "Export license confirmed in British Archives", + ], + priority: 1, +}; + +const record2 = { + id: "REC-002", + artifactId: "ANT-001", + curatorId: "curator-james", + ownerChain: [ + { name: "National Museum of Cairo", from: 1921, to: 1972 }, + { name: "Meridian Art Trust", from: 1972, to: null }, + ], + notes: ["Cross-referenced with British Museum archives"], + priority: 2, +}; + +const record3 = { + id: "REC-003", + artifactId: "ANT-002", + curatorId: "curator-mia", + ownerChain: [ + { name: "Ottoman Archive", from: 1890, to: 1952 }, + { name: "Kaplan Foundation", from: 1952, to: null }, + ], + notes: ["Documented in 1952 foundation records"], + priority: 1, +}; + +const record4 = { + id: "REC-004", + artifactId: "ANT-002", + curatorId: "curator-luca", + ownerChain: [ + { name: "Ottoman Archive", from: 1890, to: 1952 }, + { name: "Kaplan Foundation", from: 1952, to: null }, + ], + notes: ["Verified against Istanbul university thesis, 2003"], + priority: 2, +}; + +// provide the other records and have the learner push them into the array +provenanceRecords.push(record1, record2, record3, record4); + +// a helper to format an ownership period as a readable string +function formatPeriod(from, to) { + return `${from}–${to || "present"}`; +} + +// merge curator submissions for the same artifact +function mergeProvenance(records) { + const source1 = records[0].id; + const source2 = records[1].id; + + return { + artifactId: records[0].artifactId, + ownerChains: { + [source1]: JSON.parse(JSON.stringify(records[0].ownerChain)), + [source2]: JSON.parse(JSON.stringify(records[1].ownerChain)), + }, + notes: [...records[0].notes, ...records[1].notes], + sources: [source1, source2], + priorities: { + [source1]: records[0].priority, + [source2]: records[1].priority, + }, + }; +} + +// detect conflicts between two submissions +function detectConflicts(merged) { + const conflicts = []; + const chain1 = merged.ownerChains[merged.sources[0]]; + const chain2 = merged.ownerChains[merged.sources[1]]; + + // early quick check + if (JSON.stringify(chain1) === JSON.stringify(chain2)) { + return conflicts; + } + + // compare chain lengths + if (chain1.length !== chain2.length) { + conflicts.push({ + field: "ownerChain", + explanation: `Chain lengths differ: ${merged.sources[0]} has ${chain1.length} entries, ${merged.sources[1]} has ${chain2.length} entries`, + }); + } + + // check first entry name + if (chain1[0] && chain2[0] && chain1[0].name !== chain2[0].name) { + conflicts.push({ + field: "ownerChain[0].name", + explanation: `First owner name differs: "${chain1[0].name}" (${merged.sources[0]}) vs "${chain2[0].name}" (${merged.sources[1]})`, + }); + } + + // check first entry period + if (chain1[0] && chain2[0]) { + const period1 = formatPeriod(chain1[0].from, chain1[0].to); + const period2 = formatPeriod(chain2[0].from, chain2[0].to); + if (period1 !== period2) { + conflicts.push({ + field: "ownerChain[0].period", + explanation: `First owner period differs: ${period1} (${merged.sources[0]}) vs ${period2} (${merged.sources[1]})`, + }); + } + } + + return conflicts; +} + +// resolve conflicts by applying priority rules +function applyPriorityRules(merged) { + const source1 = merged.sources[0]; + const source2 = merged.sources[1]; + + const leadSource = + merged.priorities[source1] <= merged.priorities[source2] + ? source1 + : source2; + + return { + ...merged, + ownerChain: merged.ownerChains[leadSource], + resolvedBy: leadSource, + }; +} + +// store resolved records by artifactId, so we can use it as a lookup table +const resolvedReports = {}; + +// render an audit summary for a given artifact +function renderAuditReport(artifactId) { + if (!resolvedReports.hasOwnProperty(artifactId)) { + return `No report found for artifact ${artifactId}`; + } + + const record = resolvedReports[artifactId]; + + return `## Artifact Provenance Report: ${artifactId} + +**Resolved by:** ${record.resolvedBy} +**Sources:** ${record.sources[0]}, ${record.sources[1]} +**Conflicts detected:** ${record.conflicts.length} + +### Ownership Chain +\`\`\`json +${JSON.stringify(record.ownerChain, null, 2)} +\`\`\` + +### Conflicts +\`\`\`json +${JSON.stringify(record.conflicts, null, 2)} +\`\`\` + +### Notes +\`\`\`json +${JSON.stringify(record.notes, null, 2)} +\`\`\``; +} + +// demo +console.log("=".repeat(60)); +console.log("ARTIFACT PROVENANCE RECONCILIATION"); +console.log("=".repeat(60)); +console.log(); + +// main case, curators disagree on ownership history +console.log("Processing ANT-001..."); +const merged1 = mergeProvenance([record1, record2]); +merged1.conflicts = detectConflicts(merged1); +const resolved1 = applyPriorityRules(merged1); +resolvedReports[resolved1.artifactId] = resolved1; +console.log(`Conflicts found: ${resolved1.conflicts.length}`); +console.log(`Resolved by: ${resolved1.resolvedBy}`); +console.log(); + +// secondary case, curators are in full agreement +console.log("Processing ANT-002..."); +const merged2 = mergeProvenance([record3, record4]); +merged2.conflicts = detectConflicts(merged2); +const resolved2 = applyPriorityRules(merged2); +resolvedReports[resolved2.artifactId] = resolved2; +console.log(`Conflicts found: ${resolved2.conflicts.length}`); +console.log(`Resolved by: ${resolved2.resolvedBy}`); +console.log(); + +// render audit reports +console.log(renderAuditReport("ANT-001")); +console.log(); +console.log(renderAuditReport("ANT-002")); From 12c46587eef247ec75a55bfb9478c2fa9cb196ee Mon Sep 17 00:00:00 2001 From: farchettiensis <110430578+farchettiensis@users.noreply.github.com> Date: Mon, 25 May 2026 18:45:44 -0300 Subject: [PATCH 2/4] refactor(artifact-provenance): use structuredClone and demo missing chains --- .../js-projects/artifact-provenance/script.js | 243 +++++++++++------- 1 file changed, 147 insertions(+), 96 deletions(-) diff --git a/fullstack-cert/js-projects/artifact-provenance/script.js b/fullstack-cert/js-projects/artifact-provenance/script.js index f41787d6..9fe893aa 100644 --- a/fullstack-cert/js-projects/artifact-provenance/script.js +++ b/fullstack-cert/js-projects/artifact-provenance/script.js @@ -1,14 +1,12 @@ -// start with an empty records array const provenanceRecords = []; -// walk learners through the creation of the first record const record1 = { - id: "REC-001", - artifactId: "ANT-001", - curatorId: "curator-sofia", + id: 1, + artifactId: 101, + curatorId: 201, + curatorName: "Sofia Reyes", ownerChain: [ { name: "Cairo Museum", from: 1923, to: 1965 }, - { name: "Hartley Collection", from: 1965, to: 2001 }, { name: "Meridian Art Trust", from: 2001, to: null }, ], notes: [ @@ -19,9 +17,10 @@ const record1 = { }; const record2 = { - id: "REC-002", - artifactId: "ANT-001", - curatorId: "curator-james", + id: 2, + artifactId: 101, + curatorId: 202, + curatorName: "James Liu", ownerChain: [ { name: "National Museum of Cairo", from: 1921, to: 1972 }, { name: "Meridian Art Trust", from: 1972, to: null }, @@ -31,9 +30,10 @@ const record2 = { }; const record3 = { - id: "REC-003", - artifactId: "ANT-002", - curatorId: "curator-mia", + id: 3, + artifactId: 102, + curatorId: 203, + curatorName: "Mia Chen", ownerChain: [ { name: "Ottoman Archive", from: 1890, to: 1952 }, { name: "Kaplan Foundation", from: 1952, to: null }, @@ -43,9 +43,10 @@ const record3 = { }; const record4 = { - id: "REC-004", - artifactId: "ANT-002", - curatorId: "curator-luca", + id: 4, + artifactId: 102, + curatorId: 204, + curatorName: "Luca Ferrari", ownerChain: [ { name: "Ottoman Archive", from: 1890, to: 1952 }, { name: "Kaplan Foundation", from: 1952, to: null }, @@ -54,153 +55,203 @@ const record4 = { priority: 2, }; -// provide the other records and have the learner push them into the array -provenanceRecords.push(record1, record2, record3, record4); +const record5 = { + id: 5, + artifactId: 103, + curatorId: 205, + curatorName: "Amari Osei", + ownerChain: [ + { name: "Royal Persian Collection", from: 1875, to: 1923 }, + { name: "Reza Foundation", from: 1923, to: null }, + ], + notes: ["Catalogued in 1923 import records"], + priority: 1, +}; -// a helper to format an ownership period as a readable string -function formatPeriod(from, to) { - return `${from}–${to || "present"}`; +const record6 = { + id: 6, + artifactId: 103, + curatorId: 206, + curatorName: "Elena Vasquez", + ownerChain: [ + { name: "Reza Foundation", from: 1923, to: null }, + ], + notes: ["Provenance prior to 1923 undocumented"], + priority: 2, +}; + +provenanceRecords.push(record1, record2, record3, record4, record5, record6); + +function formatPeriod(period) { + return `${period.from}–${period.to || "present"}`; } -// merge curator submissions for the same artifact function mergeProvenance(records) { - const source1 = records[0].id; - const source2 = records[1].id; - + const a = records[0]; + const b = records[1]; return { - artifactId: records[0].artifactId, - ownerChains: { - [source1]: JSON.parse(JSON.stringify(records[0].ownerChain)), - [source2]: JSON.parse(JSON.stringify(records[1].ownerChain)), - }, - notes: [...records[0].notes, ...records[1].notes], - sources: [source1, source2], - priorities: { - [source1]: records[0].priority, - [source2]: records[1].priority, - }, + artifactId: a.artifactId, + submissions: [ + { + recordId: a.id, + curatorId: a.curatorId, + curatorName: a.curatorName, + priority: a.priority, + ownerChain: structuredClone(a.ownerChain), + notes: structuredClone(a.notes), + }, + { + recordId: b.id, + curatorId: b.curatorId, + curatorName: b.curatorName, + priority: b.priority, + ownerChain: structuredClone(b.ownerChain), + notes: structuredClone(b.notes), + }, + ], + notes: [...a.notes, ...b.notes], }; } -// detect conflicts between two submissions function detectConflicts(merged) { const conflicts = []; - const chain1 = merged.ownerChains[merged.sources[0]]; - const chain2 = merged.ownerChains[merged.sources[1]]; - - // early quick check - if (JSON.stringify(chain1) === JSON.stringify(chain2)) { - return conflicts; - } + const subA = merged.submissions[0]; + const subB = merged.submissions[1]; + const chainA = subA.ownerChain; + const chainB = subB.ownerChain; - // compare chain lengths - if (chain1.length !== chain2.length) { + if (chainA.length !== chainB.length) { conflicts.push({ - field: "ownerChain", - explanation: `Chain lengths differ: ${merged.sources[0]} has ${chain1.length} entries, ${merged.sources[1]} has ${chain2.length} entries`, + field: "ownerChain.length", + explanation: `${subA.curatorName} provided ${chainA.length} entries; ${subB.curatorName} provided ${chainB.length} entries`, }); } - // check first entry name - if (chain1[0] && chain2[0] && chain1[0].name !== chain2[0].name) { - conflicts.push({ - field: "ownerChain[0].name", - explanation: `First owner name differs: "${chain1[0].name}" (${merged.sources[0]}) vs "${chain2[0].name}" (${merged.sources[1]})`, - }); + if (chainA[0] && chainB[0]) { + const firstA = chainA[0]; + const firstB = chainB[0]; + if ( + firstA.name !== firstB.name || + firstA.from !== firstB.from || + firstA.to !== firstB.to + ) { + conflicts.push({ + field: "ownerChain[0]", + explanation: `${subA.curatorName} recorded ${firstA.name} (${formatPeriod(firstA)}); ${subB.curatorName} recorded ${firstB.name} (${formatPeriod(firstB)})`, + }); + } } - // check first entry period - if (chain1[0] && chain2[0]) { - const period1 = formatPeriod(chain1[0].from, chain1[0].to); - const period2 = formatPeriod(chain2[0].from, chain2[0].to); - if (period1 !== period2) { + if (chainA[1] && chainB[1]) { + const secondA = chainA[1]; + const secondB = chainB[1]; + if ( + secondA.name !== secondB.name || + secondA.from !== secondB.from || + secondA.to !== secondB.to + ) { conflicts.push({ - field: "ownerChain[0].period", - explanation: `First owner period differs: ${period1} (${merged.sources[0]}) vs ${period2} (${merged.sources[1]})`, + field: "ownerChain[1]", + explanation: `${subA.curatorName} recorded ${secondA.name} (${formatPeriod(secondA)}); ${subB.curatorName} recorded ${secondB.name} (${formatPeriod(secondB)})`, }); } } + if (subA.notes.length !== subB.notes.length) { + conflicts.push({ + field: "notes", + explanation: `${subA.curatorName} provided ${subA.notes.length} note(s); ${subB.curatorName} provided ${subB.notes.length} note(s)`, + }); + } + return conflicts; } -// resolve conflicts by applying priority rules -function applyPriorityRules(merged) { - const source1 = merged.sources[0]; - const source2 = merged.sources[1]; +function applyPriorityRules(merged, conflicts) { + const subA = merged.submissions[0]; + const subB = merged.submissions[1]; - const leadSource = - merged.priorities[source1] <= merged.priorities[source2] - ? source1 - : source2; + const lead = subA.priority <= subB.priority ? subA : subB; return { - ...merged, - ownerChain: merged.ownerChains[leadSource], - resolvedBy: leadSource, + ...structuredClone(merged), + resolvedBy: lead.curatorName, + ownerChain: structuredClone(lead.ownerChain), + conflicts, }; } -// store resolved records by artifactId, so we can use it as a lookup table const resolvedReports = {}; -// render an audit summary for a given artifact function renderAuditReport(artifactId) { if (!resolvedReports.hasOwnProperty(artifactId)) { return `No report found for artifact ${artifactId}`; } - const record = resolvedReports[artifactId]; + const report = resolvedReports[artifactId]; + const subA = report.submissions[0]; + const subB = report.submissions[1]; return `## Artifact Provenance Report: ${artifactId} -**Resolved by:** ${record.resolvedBy} -**Sources:** ${record.sources[0]}, ${record.sources[1]} -**Conflicts detected:** ${record.conflicts.length} +**Resolved by:** ${report.resolvedBy} +**Submissions:** ${subA.curatorName} (priority ${subA.priority}), ${subB.curatorName} (priority ${subB.priority}) +**Conflicts detected:** ${report.conflicts.length} ### Ownership Chain \`\`\`json -${JSON.stringify(record.ownerChain, null, 2)} +${JSON.stringify(report.ownerChain, null, 2)} \`\`\` ### Conflicts \`\`\`json -${JSON.stringify(record.conflicts, null, 2)} +${JSON.stringify(report.conflicts, null, 2)} \`\`\` ### Notes \`\`\`json -${JSON.stringify(record.notes, null, 2)} +${JSON.stringify(report.notes, null, 2)} \`\`\``; } -// demo -console.log("=".repeat(60)); console.log("ARTIFACT PROVENANCE RECONCILIATION"); -console.log("=".repeat(60)); console.log(); -// main case, curators disagree on ownership history -console.log("Processing ANT-001..."); -const merged1 = mergeProvenance([record1, record2]); -merged1.conflicts = detectConflicts(merged1); -const resolved1 = applyPriorityRules(merged1); +// curators disagree +console.log("Processing artifact 101..."); +const ant001Submissions = provenanceRecords.slice(0, 2); +const merged1 = mergeProvenance(ant001Submissions); +const conflicts1 = detectConflicts(merged1); +const resolved1 = applyPriorityRules(merged1, conflicts1); resolvedReports[resolved1.artifactId] = resolved1; console.log(`Conflicts found: ${resolved1.conflicts.length}`); console.log(`Resolved by: ${resolved1.resolvedBy}`); console.log(); -// secondary case, curators are in full agreement -console.log("Processing ANT-002..."); -const merged2 = mergeProvenance([record3, record4]); -merged2.conflicts = detectConflicts(merged2); -const resolved2 = applyPriorityRules(merged2); +// curators agree +console.log("Processing artifact 102..."); +const ant002Submissions = provenanceRecords.slice(2, 4); +const merged2 = mergeProvenance(ant002Submissions); +const conflicts2 = detectConflicts(merged2); +const resolved2 = applyPriorityRules(merged2, conflicts2); resolvedReports[resolved2.artifactId] = resolved2; console.log(`Conflicts found: ${resolved2.conflicts.length}`); console.log(`Resolved by: ${resolved2.resolvedBy}`); console.log(); -// render audit reports -console.log(renderAuditReport("ANT-001")); +// chains differ in length +console.log("Processing artifact 103..."); +const ant003Submissions = provenanceRecords.slice(4, 6); +const merged3 = mergeProvenance(ant003Submissions); +const conflicts3 = detectConflicts(merged3); +const resolved3 = applyPriorityRules(merged3, conflicts3); +resolvedReports[resolved3.artifactId] = resolved3; +console.log(`Conflicts found: ${resolved3.conflicts.length}`); +console.log(`Resolved by: ${resolved3.resolvedBy}`); +console.log(); + +console.log(renderAuditReport(101)); +console.log(); +console.log(renderAuditReport(102)); console.log(); -console.log(renderAuditReport("ANT-002")); +console.log(renderAuditReport(103)); From 94cfccd808a57f835007336f6ecbf2325683a71c Mon Sep 17 00:00:00 2001 From: farchettiensis <110430578+farchettiensis@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:12:38 -0300 Subject: [PATCH 3/4] refactor(artifact-provenance): simplify workshop for new-camper level --- .../js-projects/artifact-provenance/script.js | 199 ++++-------------- 1 file changed, 44 insertions(+), 155 deletions(-) diff --git a/fullstack-cert/js-projects/artifact-provenance/script.js b/fullstack-cert/js-projects/artifact-provenance/script.js index 9fe893aa..f322b256 100644 --- a/fullstack-cert/js-projects/artifact-provenance/script.js +++ b/fullstack-cert/js-projects/artifact-provenance/script.js @@ -1,39 +1,27 @@ const provenanceRecords = []; const record1 = { - id: 1, artifactId: 101, - curatorId: 201, - curatorName: "Sofia Reyes", + curatorName: "John Lennon", ownerChain: [ { name: "Cairo Museum", from: 1923, to: 1965 }, { name: "Meridian Art Trust", from: 2001, to: null }, ], - notes: [ - "Verified with 1923 acquisition documents", - "Export license confirmed in British Archives", - ], + notes: ["Verified with 1923 acquisition documents"], priority: 1, }; const record2 = { - id: 2, artifactId: 101, - curatorId: 202, - curatorName: "James Liu", - ownerChain: [ - { name: "National Museum of Cairo", from: 1921, to: 1972 }, - { name: "Meridian Art Trust", from: 1972, to: null }, - ], + curatorName: "Paul McCartney", + ownerChain: [{ name: "National Museum of Cairo", from: 1921, to: null }], notes: ["Cross-referenced with British Museum archives"], priority: 2, }; const record3 = { - id: 3, artifactId: 102, - curatorId: 203, - curatorName: "Mia Chen", + curatorName: "George Harrison", ownerChain: [ { name: "Ottoman Archive", from: 1890, to: 1952 }, { name: "Kaplan Foundation", from: 1952, to: null }, @@ -43,10 +31,8 @@ const record3 = { }; const record4 = { - id: 4, artifactId: 102, - curatorId: 204, - curatorName: "Luca Ferrari", + curatorName: "Ringo Starr", ownerChain: [ { name: "Ottoman Archive", from: 1890, to: 1952 }, { name: "Kaplan Foundation", from: 1952, to: null }, @@ -55,112 +41,47 @@ const record4 = { priority: 2, }; -const record5 = { - id: 5, - artifactId: 103, - curatorId: 205, - curatorName: "Amari Osei", - ownerChain: [ - { name: "Royal Persian Collection", from: 1875, to: 1923 }, - { name: "Reza Foundation", from: 1923, to: null }, - ], - notes: ["Catalogued in 1923 import records"], - priority: 1, -}; - -const record6 = { - id: 6, - artifactId: 103, - curatorId: 206, - curatorName: "Elena Vasquez", - ownerChain: [ - { name: "Reza Foundation", from: 1923, to: null }, - ], - notes: ["Provenance prior to 1923 undocumented"], - priority: 2, -}; - -provenanceRecords.push(record1, record2, record3, record4, record5, record6); +provenanceRecords.push(record1, record2, record3, record4); function formatPeriod(period) { return `${period.from}–${period.to || "present"}`; } +function cloneSubmission(record) { + return { + curatorName: record.curatorName, + priority: record.priority, + ownerChain: structuredClone(record.ownerChain), + notes: structuredClone(record.notes), + }; +} + function mergeProvenance(records) { - const a = records[0]; - const b = records[1]; + const [a, b] = records; return { artifactId: a.artifactId, - submissions: [ - { - recordId: a.id, - curatorId: a.curatorId, - curatorName: a.curatorName, - priority: a.priority, - ownerChain: structuredClone(a.ownerChain), - notes: structuredClone(a.notes), - }, - { - recordId: b.id, - curatorId: b.curatorId, - curatorName: b.curatorName, - priority: b.priority, - ownerChain: structuredClone(b.ownerChain), - notes: structuredClone(b.notes), - }, - ], + submissions: [cloneSubmission(a), cloneSubmission(b)], notes: [...a.notes, ...b.notes], }; } function detectConflicts(merged) { const conflicts = []; - const subA = merged.submissions[0]; - const subB = merged.submissions[1]; - const chainA = subA.ownerChain; - const chainB = subB.ownerChain; + const [a, b] = merged.submissions; - if (chainA.length !== chainB.length) { + if (a.ownerChain.length !== b.ownerChain.length) { conflicts.push({ field: "ownerChain.length", - explanation: `${subA.curatorName} provided ${chainA.length} entries; ${subB.curatorName} provided ${chainB.length} entries`, + explanation: `${a.curatorName} listed ${a.ownerChain.length} owners; ${b.curatorName} listed ${b.ownerChain.length}`, }); } - if (chainA[0] && chainB[0]) { - const firstA = chainA[0]; - const firstB = chainB[0]; - if ( - firstA.name !== firstB.name || - firstA.from !== firstB.from || - firstA.to !== firstB.to - ) { - conflicts.push({ - field: "ownerChain[0]", - explanation: `${subA.curatorName} recorded ${firstA.name} (${formatPeriod(firstA)}); ${subB.curatorName} recorded ${firstB.name} (${formatPeriod(firstB)})`, - }); - } - } - - if (chainA[1] && chainB[1]) { - const secondA = chainA[1]; - const secondB = chainB[1]; - if ( - secondA.name !== secondB.name || - secondA.from !== secondB.from || - secondA.to !== secondB.to - ) { - conflicts.push({ - field: "ownerChain[1]", - explanation: `${subA.curatorName} recorded ${secondA.name} (${formatPeriod(secondA)}); ${subB.curatorName} recorded ${secondB.name} (${formatPeriod(secondB)})`, - }); - } - } - - if (subA.notes.length !== subB.notes.length) { + const currentA = a.ownerChain[a.ownerChain.length - 1]; + const currentB = b.ownerChain[b.ownerChain.length - 1]; + if (currentA.name !== currentB.name) { conflicts.push({ - field: "notes", - explanation: `${subA.curatorName} provided ${subA.notes.length} note(s); ${subB.curatorName} provided ${subB.notes.length} note(s)`, + field: "currentOwner", + explanation: `${a.curatorName} ends at ${currentA.name}; ${b.curatorName} ends at ${currentB.name}`, }); } @@ -168,10 +89,9 @@ function detectConflicts(merged) { } function applyPriorityRules(merged, conflicts) { - const subA = merged.submissions[0]; - const subB = merged.submissions[1]; + const [a, b] = merged.submissions; - const lead = subA.priority <= subB.priority ? subA : subB; + const lead = a.priority <= b.priority ? a : b; return { ...structuredClone(merged), @@ -184,74 +104,43 @@ function applyPriorityRules(merged, conflicts) { const resolvedReports = {}; function renderAuditReport(artifactId) { - if (!resolvedReports.hasOwnProperty(artifactId)) { + const report = resolvedReports[artifactId]; + if (!report) { return `No report found for artifact ${artifactId}`; } - const report = resolvedReports[artifactId]; - const subA = report.submissions[0]; - const subB = report.submissions[1]; + const owners = report.ownerChain + .map((period) => `- ${period.name} (${formatPeriod(period)})`) + .join("\n"); + + const conflicts = report.conflicts.length + ? report.conflicts.map((c) => `- **${c.field}:** ${c.explanation}`).join("\n") + : "- None"; - return `## Artifact Provenance Report: ${artifactId} + return `## Provenance Report: ${artifactId} **Resolved by:** ${report.resolvedBy} -**Submissions:** ${subA.curatorName} (priority ${subA.priority}), ${subB.curatorName} (priority ${subB.priority}) **Conflicts detected:** ${report.conflicts.length} ### Ownership Chain -\`\`\`json -${JSON.stringify(report.ownerChain, null, 2)} -\`\`\` +${owners} ### Conflicts -\`\`\`json -${JSON.stringify(report.conflicts, null, 2)} -\`\`\` - -### Notes -\`\`\`json -${JSON.stringify(report.notes, null, 2)} -\`\`\``; +${conflicts}`; } -console.log("ARTIFACT PROVENANCE RECONCILIATION"); -console.log(); +console.log("ARTIFACT PROVENANCE RECONCILIATION\n"); -// curators disagree -console.log("Processing artifact 101..."); -const ant001Submissions = provenanceRecords.slice(0, 2); -const merged1 = mergeProvenance(ant001Submissions); +const merged1 = mergeProvenance(provenanceRecords.slice(0, 2)); const conflicts1 = detectConflicts(merged1); const resolved1 = applyPriorityRules(merged1, conflicts1); resolvedReports[resolved1.artifactId] = resolved1; -console.log(`Conflicts found: ${resolved1.conflicts.length}`); -console.log(`Resolved by: ${resolved1.resolvedBy}`); -console.log(); -// curators agree -console.log("Processing artifact 102..."); -const ant002Submissions = provenanceRecords.slice(2, 4); -const merged2 = mergeProvenance(ant002Submissions); +const merged2 = mergeProvenance(provenanceRecords.slice(2, 4)); const conflicts2 = detectConflicts(merged2); const resolved2 = applyPriorityRules(merged2, conflicts2); resolvedReports[resolved2.artifactId] = resolved2; -console.log(`Conflicts found: ${resolved2.conflicts.length}`); -console.log(`Resolved by: ${resolved2.resolvedBy}`); -console.log(); - -// chains differ in length -console.log("Processing artifact 103..."); -const ant003Submissions = provenanceRecords.slice(4, 6); -const merged3 = mergeProvenance(ant003Submissions); -const conflicts3 = detectConflicts(merged3); -const resolved3 = applyPriorityRules(merged3, conflicts3); -resolvedReports[resolved3.artifactId] = resolved3; -console.log(`Conflicts found: ${resolved3.conflicts.length}`); -console.log(`Resolved by: ${resolved3.resolvedBy}`); -console.log(); console.log(renderAuditReport(101)); console.log(); -console.log(renderAuditReport(102)); -console.log(); -console.log(renderAuditReport(103)); +console.log(renderAuditReport(102)); \ No newline at end of file From c0f1b0c9efb8d843559d047e40a3c4a3063c0a10 Mon Sep 17 00:00:00 2001 From: farchettiensis <110430578+farchettiensis@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:18:37 -0300 Subject: [PATCH 4/4] refactor(artifact-provenance): completely refactor prototype per the reviewer suggestion --- .../js-projects/artifact-provenance/script.js | 220 ++++++++---------- 1 file changed, 100 insertions(+), 120 deletions(-) diff --git a/fullstack-cert/js-projects/artifact-provenance/script.js b/fullstack-cert/js-projects/artifact-provenance/script.js index f322b256..1f765442 100644 --- a/fullstack-cert/js-projects/artifact-provenance/script.js +++ b/fullstack-cert/js-projects/artifact-provenance/script.js @@ -1,146 +1,126 @@ -const provenanceRecords = []; - -const record1 = { - artifactId: 101, - curatorName: "John Lennon", - ownerChain: [ - { name: "Cairo Museum", from: 1923, to: 1965 }, - { name: "Meridian Art Trust", from: 2001, to: null }, - ], - notes: ["Verified with 1923 acquisition documents"], - priority: 1, -}; - -const record2 = { - artifactId: 101, - curatorName: "Paul McCartney", - ownerChain: [{ name: "National Museum of Cairo", from: 1921, to: null }], - notes: ["Cross-referenced with British Museum archives"], - priority: 2, -}; - -const record3 = { - artifactId: 102, - curatorName: "George Harrison", - ownerChain: [ - { name: "Ottoman Archive", from: 1890, to: 1952 }, - { name: "Kaplan Foundation", from: 1952, to: null }, - ], - notes: ["Documented in 1952 foundation records"], - priority: 1, -}; - -const record4 = { - artifactId: 102, - curatorName: "Ringo Starr", - ownerChain: [ - { name: "Ottoman Archive", from: 1890, to: 1952 }, - { name: "Kaplan Foundation", from: 1952, to: null }, - ], - notes: ["Verified against Istanbul university thesis, 2003"], - priority: 2, -}; - -provenanceRecords.push(record1, record2, record3, record4); - -function formatPeriod(period) { - return `${period.from}–${period.to || "present"}`; +const collection = [ + { + id: 101, + title: "Golden Mask", + category: "Ceremonial", + curator: { + id: 201, + name: "Earl Sinclair", + }, + locations: [ + { gallery: "Hall A", year: 2020 }, + { gallery: "Hall C", year: 2024 }, + ], + tags: ["gold", "egypt"], + onDisplay: true, + }, + { + id: 102, + title: "Bronze Tablet", + category: "Inscription", + curator: { + id: 202, + name: "Robert Sinclair", + }, + locations: [{ gallery: "Archive Wing", year: 2019 }], + tags: ["bronze", "writing"], + onDisplay: false, + }, +]; + +console.log("FIRST ARTIFACT"); +console.log(collection[0].title); +console.log(collection[0].curator.name); + +console.log("---------"); + +function getArtifactTitle(id) { + const artifact = collection.find((item) => item.id === id); + return artifact ? artifact.title : "Artifact not found"; } -function cloneSubmission(record) { - return { - curatorName: record.curatorName, - priority: record.priority, - ownerChain: structuredClone(record.ownerChain), - notes: structuredClone(record.notes), - }; -} +console.log("LOOKUP BY ID"); +console.log(getArtifactTitle(102)); -function mergeProvenance(records) { - const [a, b] = records; - return { - artifactId: a.artifactId, - submissions: [cloneSubmission(a), cloneSubmission(b)], - notes: [...a.notes, ...b.notes], - }; -} +console.log("---------"); -function detectConflicts(merged) { - const conflicts = []; - const [a, b] = merged.submissions; +function addTag(id, tag) { + const artifact = collection.find((item) => item.id === id); - if (a.ownerChain.length !== b.ownerChain.length) { - conflicts.push({ - field: "ownerChain.length", - explanation: `${a.curatorName} listed ${a.ownerChain.length} owners; ${b.curatorName} listed ${b.ownerChain.length}`, - }); + if (artifact && !artifact.tags.includes(tag)) { + artifact.tags.push(tag); } +} - const currentA = a.ownerChain[a.ownerChain.length - 1]; - const currentB = b.ownerChain[b.ownerChain.length - 1]; - if (currentA.name !== currentB.name) { - conflicts.push({ - field: "currentOwner", - explanation: `${a.curatorName} ends at ${currentA.name}; ${b.curatorName} ends at ${currentB.name}`, - }); - } +addTag(101, "royal"); - return conflicts; -} +console.log("UPDATED TAGS"); +console.log(collection[0].tags); -function applyPriorityRules(merged, conflicts) { - const [a, b] = merged.submissions; +console.log("---------"); - const lead = a.priority <= b.priority ? a : b; +function moveArtifact(id, gallery, year) { + const artifact = collection.find((item) => item.id === id); - return { - ...structuredClone(merged), - resolvedBy: lead.curatorName, - ownerChain: structuredClone(lead.ownerChain), - conflicts, - }; + if (artifact) { + artifact.locations.push({ gallery, year }); + } } -const resolvedReports = {}; +moveArtifact(102, "Hall B", 2026); + +console.log("UPDATED LOCATIONS"); +console.log(collection[1].locations); + +console.log("---------"); + +function toggleDisplayStatus(id) { + const artifact = collection.find((item) => item.id === id); -function renderAuditReport(artifactId) { - const report = resolvedReports[artifactId]; - if (!report) { - return `No report found for artifact ${artifactId}`; + if (artifact) { + artifact.onDisplay = !artifact.onDisplay; } +} - const owners = report.ownerChain - .map((period) => `- ${period.name} (${formatPeriod(period)})`) - .join("\n"); +console.log("DISPLAY STATUS"); +console.log(collection[1].onDisplay); - const conflicts = report.conflicts.length - ? report.conflicts.map((c) => `- **${c.field}:** ${c.explanation}`).join("\n") - : "- None"; +toggleDisplayStatus(102); - return `## Provenance Report: ${artifactId} +console.log(collection[1].onDisplay); -**Resolved by:** ${report.resolvedBy} -**Conflicts detected:** ${report.conflicts.length} +console.log("---------"); -### Ownership Chain -${owners} +function updateCurator(id, name) { + const artifact = collection.find((item) => item.id === id); -### Conflicts -${conflicts}`; + if (artifact) { + artifact.curator.name = name; + } } -console.log("ARTIFACT PROVENANCE RECONCILIATION\n"); +updateCurator(101, "Fran Sinclair"); + +console.log("UPDATED CURATOR"); +console.log(collection[0].curator.name); + +console.log("---------"); -const merged1 = mergeProvenance(provenanceRecords.slice(0, 2)); -const conflicts1 = detectConflicts(merged1); -const resolved1 = applyPriorityRules(merged1, conflicts1); -resolvedReports[resolved1.artifactId] = resolved1; +function buildSummary(id) { + const artifact = collection.find((item) => item.id === id); -const merged2 = mergeProvenance(provenanceRecords.slice(2, 4)); -const conflicts2 = detectConflicts(merged2); -const resolved2 = applyPriorityRules(merged2, conflicts2); -resolvedReports[resolved2.artifactId] = resolved2; + if (!artifact) { + return "Artifact not found"; + } + + const currentLocation = artifact.locations[artifact.locations.length - 1]; + + return `${artifact.title} +Category: ${artifact.category} +Curator: ${artifact.curator.name} +Current Gallery: ${currentLocation.gallery} +On Display: ${artifact.onDisplay}`; +} -console.log(renderAuditReport(101)); -console.log(); -console.log(renderAuditReport(102)); \ No newline at end of file +console.log("SUMMARY"); +console.log(buildSummary(101)); \ No newline at end of file