Add COPEN transition plans, with COPEN to CTFYS as the first - #68
Merged
Conversation
Starts #66. A COPEN student takes one common year and then transfers into a five-year programme, so what they actually study is published nowhere: COPEN's plan stops after year 1, and the target's plan assumes its own year 1. Picking COPEN and then a continuation programme now composes the real thing - COPEN year 1 followed by years 2-3 of the target, with the transition plan applied. THE PLAN RECORDS THE DIFFERENCE, NOT A COURSE LIST src/data/transitions.json holds one entry per (from, to) pair with three shapes of difference: exempt a target course the student does not take because a source course credits it - CTFYS drops SF1544, credited by COPEN's SF1546 added a course from neither published plan - SF1920 in P3 of year 2, taken with CELTE's second year moved a target course shifted to a later year, keeping its periods; unused today but the natural shape for "the target's own course, later" A hand-written combined course list would go stale the moment either programme is re-extracted, and silently - nothing would report that the composition no longer matched. Recording only the difference makes composeTransition a pure function of both programmes' current data. Year numbering needs no adjustment: the source contributes year 1 and the target years 2-3, so the composed years already read 1/2/3. WHY SF1920 AND NOT SF1922 A COPEN student has no probability course, and CTFYS teaches SF1922 in its year 1, which the transfer student was never present for. Issue #66 suggested taking SF1922 in P4 of year 2 with CTFYS's year-1 students. Taking SF1920 in P3 with CELTE's second year is better on both counts - the student does not wait until P4, and the credits land in the period the exemption emptied: SF1922 moved to y2 P4 15 / 14 / 10 / 21 SF1920 from CELTE in P3 15 / 14 / 16 / 15 Both total 60 hp, which is exactly why the load check has to be per period. The residual +-1 is structural: SF1544 carried 1 hp in P2 and 5 in P3, while SF1920 is 6 hp all in P3. An added course needs its own data, since CELTE is not a programme this app models. It is embedded in the plan in the same raw shape a data file uses and parsed with parseCourseEntries - the loader's own parser, split out for this purpose so the period/credit normalisation is not reimplemented. SF1920's values come from the live sources the extractor uses: 6 hp and P3 from CELTE's year-2 study plan, TEN1 and the SF1625 prerequisite from its course page. That prerequisite resolves inside the composed plan, because SF1625 is one of the nine COPEN courses, and the arrow is drawn. cosmeticsGroup is needed in practice: without it an added course falls to the default colour, which beside the light-tone palette reads as a bug rather than as "this came from elsewhere". SF1920 declares Matematik and renders identically to CTFYS's other maths courses. PREREQUISITE ARROWS FOLLOW THE COURSE ACTUALLY TAKEN A target course in years 2-3 states its prerequisites in the target's own terms: CTFYS's SF1683 and SI1146 both require SF1674. A transfer student never took SF1674 - they took SF1626, the same subject - so an arrow drawn to the letter starts from a course that is not in their plan and simply vanishes, leaving those courses looking as though they had no prerequisites at all. credited[] is therefore a list of objects, each able to say what it replaces, and redirectPrerequisites rewrites every reference through those equivalences. It is a general rule over the plan's data rather than a list of special cases. Exactly five CTFYS year-1 courses are referenced from years 2-3, and all five resolve: SF1674 -> SF1626 arrows into SF1683, SI1146 SF1672 -> SF1624 arrow into SF1681 DD1331 -> DD1310 arrow into DD1327 SG1112 -> SG1133 arrows into SE1055, SG1113 SK1104 -> SK1115 arrow into SH1014 Three fields feed the same map, so an equivalence is stated once: credited[].replaces, exempt[].creditedBy (SF1546 -> SF1544, which is why SF1546 carries no 'replaces'), and added[].substitutesFor. A reference that survives the rewrite but names a course outside the composed plan is reported - that means an equivalence is missing, and the symptom would otherwise be a silently absent arrow. validate-data checks the same invariant statically, so a missing equivalence fails in CI rather than only in the browser. COSMETICS HAVE TO BE MERGED The two files share no course codes, so rendering a composed chart from the target's cosmetics alone drew all nine COPEN courses in the default colour. mergeCosmetics merges by group name, so Matematik from both programmes becomes one legend row, with the target's colour winning on conflict: CTFYS has Ingenjörsämnen brick where COPEN has it turquoise. A group only the source has takes the first unused family rather than its own, because COPEN's Programmering is brick, which CTFYS already spends on Ingenjörsämnen. COPEN+CTFYS lands on exactly five families, which is the documented hard cap; an overflow is reported and those courses fall back to the default colour. FULL-TIME LOAD IS CHECKED PER PERIOD The same signal validate-data applies to the programme files, applied to the composition, because a swap can balance across a year while leaving individual periods lopsided and the year total hides it. Composed year 2 is 15/14/16/15; the ±1 is reported in the UI rather than corrected, since where the plan puts a course is the program director's call. THE CHART IS TITLED FOR BOTH PROGRAMMES "Civilingenjörsutbildning Öppen ingång -> Teknisk fysik (COPEN -> CTFYS)". The target's name is shortened because the qualification is identical on both sides and pure noise the second time. shortProgramName strips a fixed set of openers rather than the longest common word prefix, which looks more general but is wrong on the English names: "Degree Program in Engineering - Open Entrance" and "Degree Program in Engineering Physics" share "Degree Program in Engineering", which would reduce CTFYS to "Physics". An unrecognised name falls through unchanged. The composed code also reaches the export audit stamp, which is wanted; export filenames are fixed strings and unaffected. VALIDATION validate-data cross-checks each plan against both programmes in both directions. `credited` must match what the source actually teaches in those years - a course added to COPEN that nobody added to the plan would otherwise be dropped silently - and every exempt/moved code must exist in the target with the year the plan claims. verified: false warns, exactly as in programs.json. Verified in headless Chrome, COPEN + CTFYS: three years render; SF1544 and SF1922 both absent; SF1920 drawn at year 2 P3 in the Matematik colour, with the SF1625 prerequisite arrow; all nine COPEN courses present and coloured; every CTFYS year-1-only course absent; the notice line naming what was credited, dropped and added; and the load line showing 15/14/16/15. No console or page errors. COPEN alone is unchanged. Arrows verified against CTFYS alone: every arrow that started from one of the five year-1 courses now starts from its COPEN equivalent, and no arrow in the composed view starts from a CTFYS year-1 course. lint, tsc --noEmit, validate-data and the build are clean. WHAT IS NOT DONE Only COPEN -> CTFYS exists, and it is marked verified: false. The other targets (CINEK, CTMAT, ...) need their plans from @sellberg in whatever format they arrive; the schema above is the target to map them onto, and adding one is a data-only change.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Starts #66. First slice: the plumbing, plus COPEN → CTFYS as the worked example.
A COPEN student takes one common year and then transfers into a five-year programme, so what they actually study is published nowhere — COPEN's plan stops after year 1, and the target's plan assumes its own year 1. Picking COPEN and then a continuation programme now composes the real thing.
The plan records the difference, not a course list
src/data/transitions.json, one entry per (from, to) pair:{ "from": "COPEN", "to": "CTFYS", "sourceYears": [1], "credited": [{ "code": "SF1626", "replaces": ["SF1674"] }, { "code": "SF1624", "replaces": ["SF1672"] }, { "code": "SA1007" }], "exempt": [{ "code": "SF1544", "creditedBy": "SF1546" }], "added": [{ "code": "SF1920", "year": 2, "periodCredits": { "P3": 6 }, "substitutesFor": "SF1922", "fromProgram": "CELTE", "cosmeticsGroup": "Matematik" }], "verified": false }(Abridged — there are nine
creditedentries, six of which name areplaces.) Four kinds of statement:credited(a source course counting toward the target degree, optionally naming the target course itreplaces),exempt(a target course credited away),added(a course from neither published plan), andmoved(a target course shifted to a later year — unused today, kept because it's the natural shape for "the target's own course, later").Declarative on purpose: a hand-written combined course list would go stale the moment either programme is re-extracted, and silently — nothing would report that the composition no longer matched. Recording only the difference makes
composeTransitiona pure function of both programmes' current data. Year numbering needs no adjustment: source year 1 + target years 2–3 already reads 1/2/3.SF1920 from CELTE, not SF1922 from CTFYS
A COPEN student has no probability course, and CTFYS teaches SF1922 in its year 1, which the transfer student was never present for. #66 suggested taking SF1922 in P4 of year 2 with CTFYS's year-1 students. Taking SF1920 in P3 with CELTE's second year is better on both counts — no waiting until P4, and the credits land in the period the exemption emptied:
Both total 60 hp — which is exactly why the load check has to be per period. The residual ±1 is structural: SF1544 carried 1 hp in P2 and 5 in P3, while SF1920 is 6 hp all in P3.
An added course needs its own data, since CELTE isn't in
programs.json. It's embedded in the plan in the same raw shape a data file uses and parsed withparseCourseEntries— the loader's own parser, split out for this so the period/credit normalisation isn't reimplemented. SF1920's values were read from the live sources the extractor uses: 6 hp and P3 from CELTE's year-2 plan,TEN1and theSF1625prerequisite from its course page. That prerequisite resolves inside the composed plan, since SF1625 is one of the nine COPEN courses — and the arrow is drawn.cosmeticsGroupturned out to be necessary rather than decorative: without it the course falls to the default colour, which beside the light-tone palette reads as a bug rather than as "this came from elsewhere". SF1920 declaresMatematikand now renders identically to CTFYS's other maths courses (verified: same computed fill as SF1681 and SF1683).Prerequisite arrows follow the course actually taken
A target course in years 2–3 states its prerequisites in the target's own terms — CTFYS's SF1683 and SI1146 both require SF1674. A transfer student never took SF1674; they took SF1626, the same subject. Drawn to the letter, the arrow starts from a course that isn't in their plan and simply vanishes, leaving those courses looking as though they had no prerequisites at all.
So
creditedis a list of objects that can say what each course replaces, andredirectPrerequisitesrewrites every reference through those equivalences — a general rule over the plan's data, not a list of special cases. Exactly five CTFYS year-1 courses are referenced from years 2–3, and all five resolve:Three fields feed the same map, so an equivalence is stated once:
credited[].replaces,exempt[].creditedBy(SF1546 → SF1544 — which is why SF1546 carries noreplaces; the validator caught that redundancy), andadded[].substitutesFor.A reference that survives the rewrite but names a course outside the composed plan is reported: that means an equivalence is missing, and the symptom would otherwise be a silently absent arrow.
validate-datachecks the same invariant statically, so a missing equivalence fails in CI rather than only in the browser.Cosmetics have to be merged
The two files share no course codes, so rendering from the target's cosmetics alone drew all nine COPEN courses in the default colour.
mergeCosmeticsmerges by group name — "Matematik" from both becomes one legend row — with the target's colour winning on conflict:A source-only group takes the first unused family rather than its own, because COPEN's
Programmeringis brick, which CTFYS already spends on Ingenjörsämnen. COPEN+CTFYS lands on exactly five families — the documented hard cap. An overflow is reported and those courses fall back to the default colour.Full-time load, checked per period
A swap can balance across a year while leaving individual periods lopsided, and the year total hides it entirely — so the check is per period, the same signal
validate-dataapplies to the programme files. The ±1 is shown in the UI rather than corrected: where the plan puts a course is the program director's call.The chart is titled for both programmes
The target's name is shortened, because the qualification is identical on both sides and pure noise the second time.
shortProgramNamestrips a fixed set of openers rather than the longest common word prefix — the latter looks more general but is wrong on the English names: "Degree Program in Engineering - Open Entrance" and "Degree Program in Engineering Physics" share "Degree Program in Engineering", which would reduce CTFYS to "Physics". An unrecognised name falls through unchanged.Verified in both languages, and that a plain programme view is untouched:
Civilingenjörsutbildning Öppen ingång (COPEN)Civilingenjörsutbildning Öppen ingång → Teknisk fysik (COPEN → CTFYS)Degree Program in Engineering - Open Entrance → Engineering Physics (COPEN → CTFYS)Civilingenjörsutbildning i Teknisk fysik (CTFYS)The SVG
<title>used by screen readers picks it up too, with the right year range. The composed code also reaches the export audit stamp (COPEN → CTFYS · build … · date) — wanted; export filenames are fixed strings and unaffected.Validation
validate-datacross-checks each plan against both programmes, in both directions:creditedmust match what the source actually teaches in those years — a course added to COPEN that nobody added to the plan would otherwise be dropped silently — and everyexempt/movedcode must exist in the target with the year the plan claims.verified: falsewarns, exactly as inprograms.json.Verification
Headless Chrome,
?program=COPEN&continuation=CTFYS:lint,tsc --noEmit,validate-data --cohorts(0 errors) and the build all clean.Not done
verified: false. The other targets (CINEK, CTMAT, …) need their plans in whatever format they arrive; the schema above is what to map them onto, and adding one is a data-only change.movedis implemented and validated but unused — CTFYS needs onlyexempt+added. It stays because "take the target's own course a year later" is a shape other programmes may well need.