From c2ce0cf1e1a485932179779b145be75fee784224 Mon Sep 17 00:00:00 2001 From: Tjakoen Stolk Date: Sun, 4 Oct 2026 17:56:16 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Resolve=20renamed=20workspace=20?= =?UTF-8?q?aliases=20by=20repository=20identity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- console/docs/usage.md | 4 ++++ console/site/lib/gradebook.mjs | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/console/docs/usage.md b/console/docs/usage.md index e7bed7b..71296d6 100644 --- a/console/docs/usage.md +++ b/console/docs/usage.md @@ -337,3 +337,7 @@ student PII and must never be committed. A private teacher repository can provide gradebook/workspaces.json when a submission account does not identify the student's actual workspace name. The optional file has schemaVersion 1, the section code, and a workspaces array of repo and studentNumber bindings. These bindings must be corroborated by enrollment and repository ownership before publication. They are private identity data and must never be included in the public Console site. The Console treats each binding as a locator. It still reads the workspace's current student.json and grade receipt before reporting delivery. Conflicting CSV identities, invalid sections and malformed repository names stop loading explicitly. Duplicate workspace identities remain held. Missing locator files preserve the existing lookup path. A locator does not approve an AI score, change a grade or establish Canvas delivery. + +### Renamed workspace repositories + +An old GitHub repository URL can redirect to a renamed workspace. When several names share a student number, the Console accepts them as aliases only after current repository metadata confirms the same repository ID and canonical class workspace, and readable receipt text matches across every name. Separate repositories, unreadable metadata and missing or differing receipts remain held for review. diff --git a/console/site/lib/gradebook.mjs b/console/site/lib/gradebook.mjs index 1104f00..68ff82f 100644 --- a/console/site/lib/gradebook.mjs +++ b/console/site/lib/gradebook.mjs @@ -126,7 +126,24 @@ export async function loadSection(sc) { const key = normNum(identity.studentNumber); identityGroups.set(key, [...(identityGroups.get(key) || []), repo]); } + const canonicalWorkspaces = new Map(); for (const repos of identityGroups.values()) if (repos.length > 1) { + // Renamed repository URLs redirect. Confirm physical identity before treating + // multiple names as separate workspaces; failed reads remain a hold. + let physical = []; + try { physical = await Promise.all(repos.map(repo => ghJSON(`/repos/${sc.org}/${repo}`))); } catch {} + const prefix = family ? `student-${family[1]}-${family[2]}-`.toLowerCase() : ""; + const first = physical[0]; + const canonical = first?.name?.toLowerCase(); + const sameRepository = physical.length === repos.length && Number.isSafeInteger(first?.id) && first.id > 0 && + canonical?.startsWith(prefix) && prefix && physical.every(info => info?.id === first.id && + info?.full_name?.toLowerCase() === `${sc.org}/${canonical}`.toLowerCase()) && + repos.every(repo => typeof receipts.get(repo) === "string" && receipts.get(repo) === receipts.get(repos[0])); + if (sameRepository) { + receipts.set(canonical, receipts.get(repos[0])); + for (const repo of repos) canonicalWorkspaces.set(repo, canonical); + continue; + } reviewWarnings.push("Multiple workspaces share one student number; their finals rows remain held."); for (const repo of repos) identities.set(repo, null); } @@ -138,7 +155,7 @@ export async function loadSection(sc) { }); const csvRows = csv.slice(1).map(parse); const rowKeys = new Set(csvRows.map(f => f[gi("assignment")] + "/" + f[gi("repo")])); - const workspaceByNumber = new Map([...identities].filter(([, identity]) => identity).map(([repo, identity]) => [normNum(identity.studentNumber), repo])); + const workspaceByNumber = new Map([...identities].filter(([, identity]) => identity).map(([repo, identity]) => [normNum(identity.studentNumber), canonicalWorkspaces.get(repo) || repo])); for (const x of pending.values()) { if (rowKeys.has(x.id + "/" + x.repo)) continue; const identity = identities.get(x.repo.toLowerCase());