Skip to content

WING-164 — Credit-by-exam / competency degree-completion planner: feasibility + architecture

Spike + architecture memo. Produced by Clahadore (Claude, WT-a8dc), 2026-07-24. Research → 5-seat LLM council (opus tier) → this memo. Provisional id WING-164 — confirm the real next board number before filing; WorkWingman ticket numbering has known WW↔WING collisions (see WING-144's renumbering note).

Andrew's ask (verbatim): "it should support all schools that allow you to test out of classes like franklin western governor excelsior all of those." Guide he pointed to: YouTube @CollegeHacked (Dr. Clifford Stumme; collegehacked.substack.com).

Bottom line for Andrew

GO — buildable and genuinely WorkWingman-shaped, but ship a small, heavily-cited, ESTIMATE-ONLY engine. Anything that reads like a registrar guarantee is over-reach and repeats the GI-Bill trap. The honest product is: given a target degree at one of a handful of curated schools, show which CLEP/DSST/TECEP exams (and ACE-course credits) TYPICALLY count toward that program per the school's published policy as of [date] — and make the user verify with the registrar before spending a dollar. Not a promise; an organizer with citations.

The single biggest correction from the council: your "test out like WGU" mental model is half wrong, and the data model must not paper over it. There are TWO mechanisms, and the planner has to render them differently and honestly.

Research verdict (settled — do not re-derive)

Two mechanisms, not one

Mechanism Schools How credit lands
Credit-by-exam banking Thomas Edison (TESU), Excelsior, Franklin, Charter Oak exam → ACE credit-hours → maps to a specific program requirement
Competency-based WGU, UMPI YourPace, SNHU-competency flat-rate term, pass assessments at your own pace, front-loaded with transfer credit — NOT class-by-class exam-out

Both consume the same feeder layer: CLEP + DSST + TECEP exams and ACE-course providers (Sophia, Study.com, StraighterLine, ALEKS). The CollegeHacked channel's own current recommendation ("why UMPI is the best college to hack") has already moved past the classic "Big 3" toward competency schools fed by cheap ACE credit — which is exactly why WGU belongs, modeled as competency, not exam-bank.

Per-school feasibility

  • TESU — ✅ strongest. Credit-by-exam powerhouse; own TECEP + CLEP + DSST; transfer up to 90/120.
  • Excelsior — ✅, but UExcel exams RETIRED 21 Aug 2022 (dead) — must be tombstoned, never a live option. Now CLEP/DSST/transfer only.
  • Franklin — ✅. ACE-minimum standards for CLEP/DSST/Excelsior; 3+1 alliance; up to 94 transfer credits.
  • WGU — ⚠️ competency, accepts CLEP as transfer ≤75% of program. Model as CompetencyBased, gate the copy so it never renders an exam→course "test out" claim.
  • UMPI YourPace / SNHU-competency — ✅ secondary competency targets.

Data-source reality

No official API. College Board's CLEP per-institution policy pages are a JS SPA ("Loading Clep Policy Search App") — no clean fetch, and referrer-not-extractor doctrine forbids scrape+redisplay regardless. The stable citation anchors are the schools' own server-rendered catalog pages (TESU smartcatalogiq, explore.excelsior.edu, franklin.edu proficiency-testing). → Build pattern = curated, human-vetted snapshot + deep-link out to confirm, NOT a live scraper. Matches every WW honesty epic.

Existing primitives this plugs into (do NOT fork)

  • SourcedField<T> + SourceCitation{Kind,Title,UrlOrLocalRef,AsOf,RetrievedAt,CaptureMethod} — the honesty spine. CaptureMethod.MaintainerCurated already exists for exactly this (hand-transcribed bundled reference data).
  • AcademicRecord.RequirementRemaining{Category,CreditsNeeded,Status,CoursesSatisfying} + ClepEligibleCount + AsOfDate — the requirement-gap primitive. Extend it.
  • StudyPlan / StudyResourceKind (WW-106 adds a CLEP plan-item) — planner rows become study-plan items.
  • ClaimSource.StudyPlanCompletion + HonestyGuard.FilterMaterialSkills — already drop Claim-only/Aspire-only records, so a passed exam must be a Prove artifact, never a bare claim. Kerr-alignment is partly enforced by existing code.
  • IJobSource pluggable-adapter pattern — the shape to mirror (with the semantics fix below).
  • Guard family (RewardsHonestyGuard, TakeHomeHonestyGuard, BenefitsHonestyGuard) — the template for DegreeCompletionHonestyGuard.
  • Precedent: GI-Bill spike — public chips + feature flag + asOf cache; NO-GO on personalized award calculation. Same discipline applies here.

Architecture (post-council, hardened)

Naming honesty

Types and UI say Estimate / illustrative plan, never "your completion plan." A passed exam that posts to AcademicRecord is a Prove artifact with a source; planner rows are recommendations, never auto-credit posts.

Models (WorkWingman.Core)

ExamCredit {                      // feeder catalog row
  ExamProgram (CLEP|DSST|TECEP|AceCourse), Code, Title,
  AceRecommendedCredits, Level(Lower|Upper), Subject,
  SourcedField<...> per value
}

SchoolCreditPolicy {
  SchoolId, Name,
  DegreeModel (CreditByExam|CompetencyBased|Hybrid),
  MaxTransferCredits,
  TransferCapAccounting { global + NESTED caps: max-from-CLEP, max-in-major, ... },
  ResidencyConstraint { credits + which buckets are HARD non-transferable (upper-div/major/last-N) },
  AcceptanceRows: [ PolicyMapping ],       // grouped by source to avoid SourcedField payload-bloat (Gemini)
  PolicyUrl, AsOf, EffectiveTerm/ValidUntil, RetrievedAt, Reviewer, Checksum, TtlDays
}

PolicyMapping {                    // one exam↔this school, wrapped once w/ shared citation
  ExamCode, AcceptanceScope (ProgramSpecific|InstitutionWide),
  MinScore, ScoreValidity(examVersion), CreditsAwarded, Level,
  DuplicateCreditRule (CLEP+Sophia same ACE-ID = ONE award),
  CreditAwardMechanism (ByExam|AceCourse|Pla|MilitaryJst|Certification|ChallengeExam),
  PolicyCaveat[] (min-score, level-mismatch, program-exclusion, age-limit, dup-restriction),
  Status (Recorded|NotOffered|Conflicting)   // retired exams (UExcel) tombstoned NotOffered
}

DegreeCompletionEstimate {         // engine output — "estimate", not "plan"
  TargetProgram { school, degree, CatalogYear, RequirementRemaining[] },
  UserState { priorAceBank, examsTaken(pass/fail+score), residencyRemaining,
              SchoolEvaluationStatus(Unverified|SelfMapped|OfficialEvaluationOnFile) },
  RankedRecommendations[], EstimatedTimeToConferral, CautionFlags[]
}
New SourceKind values: SchoolCatalog, ClepInstitutionPolicy, DsstPolicy, AceCreditRecommendation (append after existing members — ordinals stable).

Adapter (semantics fixed — council Q2)

ICreditPolicySnapshotProvider (NOT ICreditPolicySource; NOT FetchAsync):

LoadAsync(schoolId, snapshotVersion) : SchoolCreditPolicy
SourceMode Mode { get; }   // enum SourceMode { BundledSnapshot }  — "live" is a deliberate FUTURE decision, type-level
Per-school implementations (TesuPolicyProvider, ExcelsiorPolicyProvider, FranklinPolicyProvider, WguPolicyProvider) each read a bundled curated JSON snapshot. Pluggable registration kept (different schools update on different cadences); the honesty boundary is enforced by the type, so a future maintainer cannot silently swap in a scraper. Feature flag education.degreeCompletionPlanner.

Engine — objective = TIME-TO-CONFERRAL (council Q5)

DegreeCompletionPlanner evaluates whole-degree state, not exams one at a time: 1. Map feeders → RequirementRemaining buckets under the user's school/degree/catalog-year. 2. Cap-clamp before ranking: global + nested transfer caps, residency hard buckets, dedup by requirement (a duplicate ACE-ID awards once). 3. Rank by estimated remaining time-to-conferral (cost secondary). Never reward "more exams" or "more ACE hours banked." 4. Every recommendation must produce a positive degree-audit delta — reduce the user's actual remaining deficit — or it is dropped.

DegreeCompletionHonestyGuard (mirrors the guard family)

  • ≥1 citation + AsOf per recommendation row (else NotOnRecord).
  • Anti-certainty regex: "will count"/"guaranteed"/"transfers" → "typically counts per [school] as of [year], confirm with school."
  • Staleness tiers: fresh → stale-visible-chip + confidence demote → expired-blocking (past TtlDays: show estimate w/ severe downgrade + "do not enroll/pay until verified"; do not silently hard-fail the feature).
  • Kerr invariant: plan can NEVER render >100% / "complete" while any residency/capstone/non-transferable bucket is unmet.
  • DegreeModel gates copy: CompetencyBased renders transfer+competency framing, never exam→course "test out."
  • VerifyWithRegistrar required plan item blocks any "buy this exam" / paid-feeder CTA (structural, beats disclaimer text).
  • Money-waste exposure: on any non-refundable, low-confidence or stale row, surface dollar + time cost + "may not apply if score/version/program differs."
  • Trademark/defamation: nominative school/exam names only, no logos, "Not affiliated with [school]; policy cited from public catalog as of [date]" footer; quote public policy only, no "diploma mill"/"fastest" league tables.
  • FERPA/PII: academic records stay local; no cloud "transcript optimization" without explicit consent design.

CI / snapshot governance (council Q1)

  • CI gate: no recommendation row ships without source + RetrievedAt + Reviewer review date.
  • CI HEAD-check job on every PolicyUrl: 404 / cross-host redirect → citation anchor moved → suppress the row (we can no longer stand behind the claim).
  • Remote kill-switch / denylist for known-bad snapshots, honored even in local-first mode.
  • Optional higher-confidence overlay: user-pasted official evaluation PDF (LlmExtractedFromUserDoc/UserTyped), surfaced as a conflict against the catalog, never a silent override.
  • v1 does NOT scale to "many schools" — cap to the research set so freshness stays human-governable.

MVP scope (council Q6)

SHIP (MVP): feature-flagged; curated snapshots for the research set only (~3–6 schools: TESU, Excelsior, Franklin + WGU/UMPI competency); feeder catalog (CLEP/DSST/TECEP + ACE providers) with citations + deep links; planner that fills user-declared remaining requirement buckets with visible freshness chips; DegreeCompletionHonestyGuard + DegreeModel copy gates; estimate-only UX; required VerifyWithRegistrar before any paid feeder.

DO NOT SHIP: broad school search, live scrapers, guaranteed graduation date or exact total cost, financial-aid / GI-Bill implications, PLA/portfolio "fast degree" paths, auto-enroll, "fastest degree in America" rankings, or posting credit without a Prove artifact.

Council record

5-seat opus council (Clahadore/Claude, Cedric/Codex, Jenny/Gemini, Gronktayvius/Grok, local). Consensus ranking Grok > Codex > Claude > Gemini > local; verdict = approve with mandatory hardening (all folded above). Full report: llm-council/reports/2026-07-24_105523_architecture-review-for-workwingmans-new-test.md. Rejected (local seat): overriding the ACE mapping with syllabus-intersection checks (honesty overreach — the ACE recommendation IS the sanctioned mapping); dropping residency from the MVP (recreates the exact false-progress Kerr failure this feature exists to prevent).

T6 verified data-availability findings (2026-07-24, ceiling-reviewed)

A Sonnet research pass read each school's OWN server-rendered pages (no aggregators, no CLEP SPA) and returned CANDIDATE data with per-row citations. Verified corrections:

  • ExamProgram.Tecep must NOT be a generic feeder. TECEP is a TESU trademark exam program; it appears nowhere on franklin.edu / excelsior.edu. Franklin's own equivalent is FUPE, which Franklin's page states is non-transferable to other institutions. → School-internal challenge exams (TECEP@TESU, FUPE@Franklin, retired UExcel@Excelsior) are a different category from portable feeders (CLEP/DSST/ACE-course). In MVP this is auto-respected because we plan per-target- school (a FUPE row only ever appears under Franklin and is only recommended when targeting Franklin — we never suggest "bank FUPE then transfer"). If a cross-school "bank cheap then transfer" flow is ever added (explicitly NOT MVP), a Portable flag becomes required.
  • Residency is not always a credit count. WGU measures residency as 25% of Degree Competency Units (competency schools use CUs, not credit hours); Excelsior residency is course-based (Cornerstone + Capstone + a 1-cr Information Literacy course). ResidencyConstraint.HardBuckets (list) already carries the named courses; the CU-percentage is surfaced as a caveat, not a fake credit number. Adequate for MVP; do not fabricate a credit-equivalent.
  • Data-source gating (affects T4 CI HEAD-check): WGU cm.wgu.edu returns HTTP 403 to a plain fetch (bot-gated; only a rendered browser got through) — a naive HEAD-check would misreport it as a dead/moved anchor on every run. UMPI's CLEP list sits behind the mycampus.maine.edu login portal (inaccessible). Excelsior's academic catalog is a non-text PDF (needs a pdftotext pass). → T4 must allowlist known-gated hosts and treat 403≠dead, and some data simply cannot be auto-revalidated — it needs periodic human recuration.

The honesty-critical finding — per-exam MIN-SCORE data is largely unpublished

Franklin publishes exam→course→credits but no minimum scores; WGU delegates scores to ACE per-exam by design; Excelsior and UMPI publish essentially no accessible per-exam table. So the honest MVP data per school is thinner than the original design assumed:

School Model Caps/residency Accepted-exam list Per-exam credits Per-exam MIN SCORE
Franklin CreditByExam ✅ (94 cap / 30 residency, published) ✅ (28 CLEP + 32 DSST rows, published) ✅ published ❌ not published
WGU CompetencyBased ✅ (75% transfer / 25% CU residency) ✅ (accepts CLEP/DSST via ACE) ~ per ACE ❌ delegated to ACE per-exam
Excelsior CreditByExam ⚠️ conflicting cap figures; course-based residency ✅ (codes 2899/9370) ❌ PDF-only ❌ not accessible
UMPI CompetencyBased ✅ (30/12/9 residency; 15-cr CPL cap on assoc.) ⚠️ gated portal ❌ gated ❌ gated

Consequence (honest scope tightening): the planner must NOT promise a scored plan. It can honestly organize model + caps + residency + accepted-exam list + published credit-hours (Franklin) + deep-link, and it must present min-score as "look up the ACE-recommended score / confirm with the registrar" — never a number we can't cite to the school. This makes VerifyWithRegistrar load-bearing, not optional, and confirms the estimate-only posture. Franklin is the strongest first real snapshot (richest publishable data); TESU seed exists as a fixture; Excelsior/WGU/UMPI ship as model+caps+residency+deep-link with min-scores marked Unknown until recurated.

T4 IMPLEMENTED — snapshot governance (CI gate + anchor HEAD-check + kill-switch)

Shipped 2026-07-24 (Clahadore/Claude, WT-5482). Three layers turn the honesty posture the models already carry (PolicyUrl, AsOf, RetrievedAt, Reviewer, Checksum, TtlDays; SchoolCreditPolicy.FreshnessOn; DegreeCompletionHonestyGuard) into an end-to-end enforced gate. Plain-language twin: ww-credit-by-exam-degree-completion-feasibility (plain).

1. Deterministic provenance gate (unit test, runs offline in the backend suite + churn x5). SnapshotGovernanceTests enumerates the actual embedded {schoolId}.json snapshots (so a new T6 school is governed automatically, with no test edit) and fails the build if any snapshot lacks a Reviewer, RetrievedAt (review date), AsOf (vintage), a positive TtlDays, or an absolute http(s) PolicyUrl — or if any acceptance row fails its honesty invariant (≥1 citation, status never NotOnRecord), has a source without RetrievedAt / UrlOrLocalRef, or a citation not tagged MaintainerCurated. This is the "no recommendation row ships without source + RetrievedAt + Reviewer review date" rule, mechanically enforced.

2. Citation-anchor HEAD-check (network → separate snapshot-anchors CI job). Kept out of the deterministic suite because it makes outbound HTTP. SnapshotAnchorChecker (in Infrastructure, so the unit suite still covers its logic) HEAD-checks every PolicyUrl and every per-row source URL, following redirects manually: - 404 / 410, or a redirect to a different host → build FAILURE. The anchor is dead or moved; the rows it cites can no longer be stood behind and must be suppressed/tombstoned or repointed. - 403 from a known bot-gated host (cm.wgu.edu) → PASS. Per the T6 findings, 403 ≠ dead — a naive check would false-positive WGU on every run. Known-gated hosts are allowlisted (AnchorCheckAllowlist.BotGatedHosts). - Login portals / non-text PDFs (UMPI mycampus.maine.edu, an Excelsior catalog PDF) → FLAGGED manual-recuration-required and skipped, never auto-checked (ManualRecurationHosts / ManualRecurationUrlPatterns). These need periodic human recuration, not a HEAD probe. - Transient / gated / unexpected (5xx, 429, DNS failure, non-allowlisted 401/403) → WARN only, so a flaky upstream never reds an unrelated PR. Only a definitive dead/moved anchor fails the build.

The job runs tools/WorkWingman.SnapshotAnchorCheck and uploads a JSON report artifact. T6 must add any Excelsior catalog-PDF URL to ManualRecurationUrlPatterns when that snapshot lands.

3. Remote kill-switch / denylist (runtime, honored local-first). A maintainer who finds a snapshot wrong after ship can withdraw it with no app redeploy. ISnapshotDenylistService (impl RemoteSnapshotDenylistService) merges a bundled denylist (denylist.json, baked in, always honored) with an optional remote denylist (SnapshotDenylist:RemoteUrl, TTL-cached). DegreeCompletionController.GetEstimate checks it right after loading the policy; a withdrawn snapshot returns 404 with the maintainer's reason instead of being served. Kill granularity: an entry with a null/* snapshotVersion denies the whole school; a specific version matches the served policy's Checksum (or its AsOf date for pre-checksum snapshots), so a re-curated fix keeps serving while the bad vintage stays denied. Fail-safe invariants (unit-tested): bundled entries always win over remote (a build-time kill can't be un-denied by an edited/emptied remote list); a remote outage never throws, never fabricates a denial, and never clears the last-good remote entries — so an outage can neither deny everything nor silently un-deny a killed snapshot.

How to invoke the kill-switch. Add an entry to the remote denylist document (same JSON shape as the bundled src/WorkWingman.Infrastructure/Reference/Data/degree-completion/denylist.json) served at SnapshotDenylist:RemoteUrl, e.g. to pull the whole TESU snapshot:

{ "schemaVersion": 1, "entries": [
  { "schoolId": "tesu", "snapshotVersion": null,
    "reason": "cap figure mis-transcribed; pending re-curation",
    "reviewer": "andrew", "deniedAt": "2026-07-24T00:00:00+00:00" } ] }
Within CacheTtlSeconds (default 900) every running client stops serving that snapshot. For a build-time-known-bad snapshot, add the same entry to the bundled denylist.json instead (ships in the build, no remote needed). Both paths are exercised by SnapshotDenylistTests.

Proposed ticket decomposition (WING-164 epic — numbers provisional)

  • T0 — Core models: ExamCredit, SchoolCreditPolicy, PolicyMapping, DegreeCompletionEstimate, enums (DegreeModel, SourceMode, CreditAwardMechanism, AcceptanceScope), new SourceKind values. Snapshot JSON schema + one seeded curated school (TESU) w/ citations. Foundation; ceiling/skip-list (core abstraction).
  • T1 — ICreditPolicySnapshotProvider + SourceMode{BundledSnapshot} + per-school providers reading bundled snapshots; feature flag wiring.
  • T2 — DegreeCompletionPlanner engine: bucket mapping, cap-clamp (nested caps + residency), dedup-by-requirement, time-to-conferral ranking, positive-audit-delta gate.
  • T3 — DegreeCompletionHonestyGuard: citation/AsOf enforcement, anti-certainty regex, staleness tiers, >100% invariant, DegreeModel copy gate, money-waste exposure, verify-before-paid-feeder, trademark footer.
  • T4 — Snapshot governance: CI schema gate + PolicyUrl HEAD-check + kill-switch/denylist; retired-exam tombstones (UExcel).
  • T5 — Angular surface: estimate-only UX, freshness chips, confidence pills, deep-link-out, VerifyWithRegistrar step, competency-vs-exam framing.
  • T6 — Curate remaining research-set snapshots (Excelsior, Franklin, WGU, UMPI) with per-row citations + as-of.

Gating: T0 → (T1, T2, T3) → T4/T5 → T6. T0/T2/T3 are ceiling/skip-list (core abstraction + honesty/anti-gaming). T1/T4/T6 are Sonnet-tier. T5 is Sonnet w/ interface-kit.