You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Bynk's zero-install playground already does the hard thing an interactive course
needs: it compiles Bynk in the browser (bynk-wasm) and executes the result in a
sandboxed, separate-origin Worker (ADR 0140), with a CodeMirror editor and
on-type diagnostics. That machinery was built as a REPL. This track turns it into
a teaching substrate — hardened, tested, and reusable by more than one app —
and then builds an interactive course on top of it: sequenced lessons whose
exercises the toolchain grades, rather than prose the reader is trusted to try.
The course is explicitly not a product: no monetisation, no accounts, no
enrolment, no certificates. It is a second mode of learning alongside the Book,
for developers who learn by doing rather than by reading.
When the track retires: the playground's internals are a tested, shared package
with a deterministic execution contract; a graded-exercise course is live and
every exercise is compile-verified in CI; and the decision on whether
suite-based grading is worth its compiler changes has been taken on evidence.
Realises:design/bynk-design-notes.md's framing of Bynk as an explicitly educational language, and design/bynk-positioning.md's justification of the
design partly by "novices, students and course design" (§§21, 29, 55, 60, 89).
Gives the shipped playground (ADR 0140) a second consumer, which is what forces
it to become a substrate rather than an app.
Track doc (added by the settling PR):design/tracks/interactive-learning.md
Relation to the documentation track ([#557](Track: documentation & web presence #557)):
adjacent, not overlapping — and the seam needs stating. Track: documentation & web presence #557 owns the Book, By
Example, the site shell, and the snippet-verification harness; it declares
itself not a language-surface change. This track owns the playground's internals and the course surface, and includes work Track: documentation & web presence #557 excludes by
construction (a new diagnostic, an emitter change to needs_compose). The
course consumesTrack: documentation & web presence #557's verification harness rather than building a second
one; slice 4 below depends on it. site/src/components/Example.astro already
implements the inline embed, so the site→playground seam exists and this track
must not fork it.
Why a track (the ADR 0076 trigger)
All three hold.
Multi-increment — roughly a dozen slices across two phases: making the
playground testable and shareable, changing the execution contract, then the
course shell, the exercise corpus, and a conditional grading tier. The
connective decisions (the entry contract, the corpus format, the origin model)
constrain every later slice and cannot live in a delete-on-merge proposal.
Surface not yet settled — the exercise data format does not exist; where
the grading boundary sits is genuinely open (see Q2); the entry-discovery
contract needs designing rather than implementing; and whether the course
eventually leaves this repo is undecided by intent (Q5).
Security/safety boundary — slice 0 moves the code that decides what
executes in the sandbox (WORKER_SRC, linkGraph), and a second app surface
means APP_ORIGIN stops being a single constant. Both touch the boundary
ADR 0140 defines. Threat model below.
Open design questions
Q1 — The exercise corpus format. The highest-leverage artefact in the track,
because the corpus is the asset and the shell is replaceable: a discarded spike,
a rewritten shell, even a repo move all leave the corpus valid, but thirty
exercises against a bad schema is thirty rewrites. Needs: what an exercise
declares (starter source, solution, objective, hints, prerequisites, difficulty),
how it declares its grading tier and success criterion, and how the CI gate
reads it. Prior art to review: Exercism's per-exercise config, Rustlings' exercises/ + info.toml, the Rust Book's listings/ extraction, and playground/src/examples.ts (the closest in-repo precedent, and deliberately
thin — { id, title, source } only).
Q2 — Where the grading boundary sits. Three tiers with very different costs:
(1) assert on bynk_analyze diagnostic categories — "make this program produce bynk.refine.literal_violates" — needs no compiler change; (2) assert on the
sandbox's { logs, value } — also none; (3) a hidden suite grades the
learner's code, needing ~6 changes including a multi-file wasm entry. Tiers 1–2
plausibly cover two-thirds of a spine, and tier 1 is the most thesis-aligned
grading available. The settling question is whether tier 3 is in scope for this
track at all, or a follow-on justified by evidence from tiers 1–2. Tier 1 also
has an under-exploited asset behind it: reference/diagnostics.md is ~74 KB of
stable category codes and currently backs zero exercises.
Q3 — The browser entry contract. Today sandbox.ts requires composeApp,
then takes the first surface function with fn.length === 0, so a program with
two zero-arg handlers resolves by object key enumeration order, and a program
with none returns an opaque noEntry flag. Acceptable for a REPL; a correctness
bug for a grader. Needs settling: whether the contract is exactly one
zero-arg on call per browser run; the diagnostic identities (candidates bynk.target.no_browser_entry / bynk.target.ambiguous_browser_entry, beside
the three existing bynk.target.*); and whether needs_compose widens to fire
for a context holding a zero-arg on call — which is what makes the seven site/src/snippets/*.bynkrun rather than merely type-check, since all seven
are bare commons today and the five playground examples all carry a contrived consumes bynk { Logger } to force a composition root. Compatibility check
required: does widening needs_compose alter emission for any existing
program?
Closed already, recorded so it is not re-litigated. A dedicated service
kind — service … from playground with on run () — was considered and
rejected. All six ServiceProtocol variants name a real trigger (Http, Cron, Queue, WebSocket, Events, Call); "playground" would name a
person clicking a button, i.e. a development affordance in architecture's
costume. ServiceProtocol::Call is already documented as "no from clause:
the service holds on call handlers only" — on callis the
non-triggered entry kind, so the gap is that the sandbox does not respect it,
not that the language lacks it. There is also a pedagogy cost specific to a
course: teaching from playground teaches a construct present in no real
program, which every learner then unlearns at the install boundary. If a
marker is ever wanted, an attribute (@entry) is the proportionate shape, not
a protocol variant.
Q4 — The sandbox origin model. A course is a third surface. Either it gets
its own sandbox origin (a fourth Pages project) or APP_ORIGIN becomes an
allow-list. The allow-list is cheaper and probably better, but it amends the
boundary ADR 0140 defines, and playground/README.md is emphatic that the two
origins must never collapse. Widening an allow-list is not collapsing — but the
distinction deserves an ADR, not a changed constant.
Q5 — Does the course stay in this repo? Intent is to build in-tree and keep
extraction possible. The tension to settle: the monorepo is what makes a language
change that breaks an exercise break the build in the same PR, and that is
the single highest-leverage anti-rot mitigation available. Extracted, it degrades
to a nightly against a published release — structurally always behind. The
README already documents this failure mode for accuser/architecture-that-compiles: "a diagnostic rename here can invalidate a
listing there — that repository's nightly gate is what reports it." Correct for
a manuscript pinning an edition; a downgrade for a course claiming to teach what
compiles today. Extraction would also need three pipelines that do not exist: a
published bynk-web-core, a published wasm artefact (src/vendor/ is gitignored
and built from in-tree crates), and a gate against a released toolchain. Settle
the extraction contract now (own directory, own package.json, own test
script, dependencies by workspace name, exercises as data behind a manifest,
nothing imported from site/) and leave the extraction trigger as a named
signal rather than a date.
Q6 — How the boundary half of the language gets taught. The browser can teach
values, types, refinement, capabilities and agents. It cannot teach the edge: from http, cron, queue and websocket all compile for Platform::Browser
but are structurally unreachable, because the composed surface only ever carries on call handlers. So a browser-only course stops before the learner has built a
service — which is what the language is for. Options: an install capstone; a bynk learn CLI over bynkc test (the Rustlings/Ziglings pattern, no browser
constraints at all, reuses the native runner as-is); or the larger idea of
letting an in-process entry drive the generated router — synthesising a
request, essentially what as system suites already do under bynkc test —
which would largely dissolve the break and deserves its own proposal on its own
merits.
Q7 — Page weight. Debug wasm is 17.7 MiB and the release size is unmeasured — ADR 0139 says so directly ("the wasm payload is real and
unmeasured here — the REPL slice owns the size budget"), and deploy-playground.yml mentions a budget while enforcing no number. Tolerable for
a page visited once; a design input for a course where every lesson wants the
compiler. Needs a measured number and a decision on load strategy (once per
shell, lazy on first Run).
Candidate slice decomposition
Provisional until the doc settles. Phase A is worth shipping on its own merits —
every slice improves the playground for its existing users, so a decision not to
proceed to Phase B strands nothing.
Phase A — the playground as a substrate
Slice 0 — Testability. Move WORKER_SRC out of the template literal into a
real typechecked module; give playground/ a test script (it has none — CI
runs build:wasm then tsc --noEmit, so the comment-enforced "every curated
example is runnable" invariant is unchecked); unit-test entry selection against
hand-built fake surfaces. No wasm needed for those tests — the logic is
plain JS over a composeApp() result. This slice is a prerequisite for slice 1:
there is currently nowhere to write the test proving an ambiguous entry is
rejected rather than guessed.
Slice 1 — The entry contract (Q3). Deterministic selection, real
diagnostics, needs_compose widening. Carries a front-loaded ADR.
Slice 2 — Shared seams. Export linkGraph; make the wasm URLs configurable
rather than location.href-relative (they break on any nested route, which a
lessons app has); per-app build output (dist/ is rm'd at build start, so two
apps collide); measure the release wasm and put a number in CI (Q7). Defer the
full bynk-web-core package with an exports field until there are two real
consumers — abstracting from one caller's shape is how the seams come out wrong.
Slice 3 — The origin model (Q4). ADR plus implementation.
Phase B — the course
Slice 4 — The corpus format (Q1) plus the CI gate reading it, plus the first
handful of exercises expressed in it — the format is only validated by real
content. Consumes Track: documentation & web presence #557's verification harness.
Slice 5 — The course shell and modules 1–5. Tier-1/2 grading only. Progress
in localStorage: no accounts, no backend, no personal data. Reuse deeplink.ts unchanged so a learner can share an attempt.
Slice 6 — (conditional) Tier-3 grading (Q2). Multi-file wasm entry, test
mode in the sandbox, RunReply widened to a suite/case/outcome shape reusing bynk-driver/src/test_json.rs's document rather than inventing one, and contracts: true on the in-memory path.
Slice 7 — (conditional) The boundary half (Q6).
Slice status
Slice 0 — Playground testability: WORKER_SRC as a module, test script, entry-selection unit tests
Slice 4 — The exercise corpus format and its CI gate
Slice 5 — Course shell and modules 1–5, tier-1/2 grading
Slice 6 — (conditional) Tier-3 suite grading
Slice 7 — (conditional) The boundary half: install capstone or bynk learn
Front-loaded ADR candidates
Numbers taken at merge, not pre-allocated.
The browser entry contract. Load-bearing and expensive to reverse: every
exercise, every runnable Book snippet and every embedded example depends on
which programs are runnable and how the entry is found. Changing it after a
corpus exists invalidates content. Covers the exactly-one rule, the diagnostic
identities, and the needs_compose widening.
The sandbox origin model. Amends ADR 0140's safety boundary; security-
relevant; hard to reverse once a second surface is deployed.
The exercise corpus format. The corpus is the asset. Hard to reverse once
N exercises exist, and it is the contract between the content, the CI gate and
any future shell — including one in a different repository.
Explicitly not an ADR: a from playground service kind. Rejected during
scoping; reasoning recorded under Q3 so it is not re-opened by default.
Threat model
Assets. The app origin's storage and session on playground.bynk-lang.org
(and any future course origin); the learner's browser and anything else it holds.
Adversary. A malicious shared program. Deep links (#base64url(deflate-raw(…)))
and ?s=<id> share ids are attacker-controllable, so anyone can hand a victim a
URL that executes code in their browser. This exposure exists today and the track
must not widen it. A course adds a second vector of the same kind — a shared
exercise attempt.
Where verification happens. Four properties carry the boundary, and each is
load-bearing rather than defence-in-depth garnish: the sandbox runs on a distinct origin in an <iframe sandbox="allow-scripts">without allow-same-origin, so it is an opaque origin with no access to app-origin
storage; both directions of postMessage validate (sandbox checks e.origin === APP_ORIGIN; the app checks e.source === iframe.contentWindow);
execution is killed on a 3 s wall clock via worker.terminate(); and the
capability surface is withheld in the browser binding — Fetch and Secrets
throw by construction rather than being merely unbound.
What this track changes, and the invariants it must preserve. Slice 0 moves WORKER_SRC and slice 2 moves linkGraph — i.e. the code deciding what
executes — and slice 3 widens who may ask the sandbox to execute. Preserve:
distinct origin, no allow-same-origin, per-message origin validation on both
hops, and the wall clock. Widening APP_ORIGIN to an allow-list must be an
explicit finite list of first-party origins, never a wildcard or a suffix match.
One new constraint this track should adopt. Today the sandbox has no
affirmative network block — no CSP file exists under playground/, and dist/
ships no _headers. Outbound fetch is unreachable only because Bynk emits no
JavaScript escape hatch, so everything running in the sandbox is compiler
output. A course is exactly the thing likely to break that assumption, by
wanting a hand-written JS test harness or scaffolding. So: everything executed
in the sandbox must remain compiler output; a graded exercise's assertions are
written in Bynk (suite/case/expect) or evaluated outside the sandbox from { logs, value } and the diagnostic set — never injected as hand-authored JS.
Adding a CSP to both origins is cheap belt-and-braces and worth doing in slice 0
regardless, so the property stops resting on a language accident.
Scoping notes, the reusability audit, the third-party build-vs-buy
comparison, and a verification checklist of claims that were read but not
executed are in the feasibility assessment this issue was cut from; it becomes
the basis of design/tracks/interactive-learning.md in the settling PR.
The theme
Bynk's zero-install playground already does the hard thing an interactive course
needs: it compiles Bynk in the browser (
bynk-wasm) and executes the result in asandboxed, separate-origin Worker (ADR 0140), with a CodeMirror editor and
on-type diagnostics. That machinery was built as a REPL. This track turns it into
a teaching substrate — hardened, tested, and reusable by more than one app —
and then builds an interactive course on top of it: sequenced lessons whose
exercises the toolchain grades, rather than prose the reader is trusted to try.
The course is explicitly not a product: no monetisation, no accounts, no
enrolment, no certificates. It is a second mode of learning alongside the Book,
for developers who learn by doing rather than by reading.
When the track retires: the playground's internals are a tested, shared package
with a deterministic execution contract; a graded-exercise course is live and
every exercise is compile-verified in CI; and the decision on whether
suite-based grading is worth its compiler changes has been taken on evidence.
design/bynk-design-notes.md's framing of Bynk as an explicitlyeducational language, and
design/bynk-positioning.md's justification of thedesign partly by "novices, students and course design" (§§21, 29, 55, 60, 89).
Gives the shipped playground (ADR 0140) a second consumer, which is what forces
it to become a substrate rather than an app.
design/tracks/interactive-learning.mdadjacent, not overlapping — and the seam needs stating. Track: documentation & web presence #557 owns the Book, By
Example, the site shell, and the snippet-verification harness; it declares
itself not a language-surface change. This track owns the playground's
internals and the course surface, and includes work Track: documentation & web presence #557 excludes by
construction (a new diagnostic, an emitter change to
needs_compose). Thecourse consumes Track: documentation & web presence #557's verification harness rather than building a second
one; slice 4 below depends on it.
site/src/components/Example.astroalreadyimplements the inline embed, so the site→playground seam exists and this track
must not fork it.
Why a track (the ADR 0076 trigger)
All three hold.
playground testable and shareable, changing the execution contract, then the
course shell, the exercise corpus, and a conditional grading tier. The
connective decisions (the entry contract, the corpus format, the origin model)
constrain every later slice and cannot live in a delete-on-merge proposal.
the grading boundary sits is genuinely open (see Q2); the entry-discovery
contract needs designing rather than implementing; and whether the course
eventually leaves this repo is undecided by intent (Q5).
executes in the sandbox (
WORKER_SRC,linkGraph), and a second app surfacemeans
APP_ORIGINstops being a single constant. Both touch the boundaryADR 0140 defines. Threat model below.
Open design questions
Q1 — The exercise corpus format. The highest-leverage artefact in the track,
because the corpus is the asset and the shell is replaceable: a discarded spike,
a rewritten shell, even a repo move all leave the corpus valid, but thirty
exercises against a bad schema is thirty rewrites. Needs: what an exercise
declares (starter source, solution, objective, hints, prerequisites, difficulty),
how it declares its grading tier and success criterion, and how the CI gate
reads it. Prior art to review: Exercism's per-exercise config, Rustlings'
exercises/+info.toml, the Rust Book'slistings/extraction, andplayground/src/examples.ts(the closest in-repo precedent, and deliberatelythin —
{ id, title, source }only).Q2 — Where the grading boundary sits. Three tiers with very different costs:
(1) assert on
bynk_analyzediagnostic categories — "make this program producebynk.refine.literal_violates" — needs no compiler change; (2) assert on thesandbox's
{ logs, value }— also none; (3) a hiddensuitegrades thelearner's code, needing ~6 changes including a multi-file wasm entry. Tiers 1–2
plausibly cover two-thirds of a spine, and tier 1 is the most thesis-aligned
grading available. The settling question is whether tier 3 is in scope for this
track at all, or a follow-on justified by evidence from tiers 1–2. Tier 1 also
has an under-exploited asset behind it:
reference/diagnostics.mdis ~74 KB ofstable category codes and currently backs zero exercises.
Q3 — The browser entry contract. Today
sandbox.tsrequirescomposeApp,then takes the first surface function with
fn.length === 0, so a program withtwo zero-arg handlers resolves by object key enumeration order, and a program
with none returns an opaque
noEntryflag. Acceptable for a REPL; a correctnessbug for a grader. Needs settling: whether the contract is exactly one
zero-arg
on callper browser run; the diagnostic identities (candidatesbynk.target.no_browser_entry/bynk.target.ambiguous_browser_entry, besidethe three existing
bynk.target.*); and whetherneeds_composewidens to firefor a context holding a zero-arg
on call— which is what makes the sevensite/src/snippets/*.bynkrun rather than merely type-check, since all sevenare bare
commonstoday and the five playground examples all carry a contrivedconsumes bynk { Logger }to force a composition root. Compatibility checkrequired: does widening
needs_composealter emission for any existingprogram?
Q4 — The sandbox origin model. A course is a third surface. Either it gets
its own sandbox origin (a fourth Pages project) or
APP_ORIGINbecomes anallow-list. The allow-list is cheaper and probably better, but it amends the
boundary ADR 0140 defines, and
playground/README.mdis emphatic that the twoorigins must never collapse. Widening an allow-list is not collapsing — but the
distinction deserves an ADR, not a changed constant.
Q5 — Does the course stay in this repo? Intent is to build in-tree and keep
extraction possible. The tension to settle: the monorepo is what makes a language
change that breaks an exercise break the build in the same PR, and that is
the single highest-leverage anti-rot mitigation available. Extracted, it degrades
to a nightly against a published release — structurally always behind. The
README already documents this failure mode for
accuser/architecture-that-compiles: "a diagnostic rename here can invalidate alisting there — that repository's nightly gate is what reports it." Correct for
a manuscript pinning an edition; a downgrade for a course claiming to teach what
compiles today. Extraction would also need three pipelines that do not exist: a
published
bynk-web-core, a published wasm artefact (src/vendor/is gitignoredand built from in-tree crates), and a gate against a released toolchain. Settle
the extraction contract now (own directory, own
package.json, own testscript, dependencies by workspace name, exercises as data behind a manifest,
nothing imported from
site/) and leave the extraction trigger as a namedsignal rather than a date.
Q6 — How the boundary half of the language gets taught. The browser can teach
values, types, refinement, capabilities and agents. It cannot teach the edge:
from http,cron,queueandwebsocketall compile forPlatform::Browserbut are structurally unreachable, because the composed surface only ever carries
on callhandlers. So a browser-only course stops before the learner has built aservice — which is what the language is for. Options: an install capstone; a
bynk learnCLI overbynkc test(the Rustlings/Ziglings pattern, no browserconstraints at all, reuses the native runner as-is); or the larger idea of
letting an in-process entry drive the generated router — synthesising a
request, essentially what
as systemsuites already do underbynkc test—which would largely dissolve the break and deserves its own proposal on its own
merits.
Q7 — Page weight. Debug wasm is 17.7 MiB and the release size is
unmeasured — ADR 0139 says so directly ("the wasm payload is real and
unmeasured here — the REPL slice owns the size budget"), and
deploy-playground.ymlmentions a budget while enforcing no number. Tolerable fora page visited once; a design input for a course where every lesson wants the
compiler. Needs a measured number and a decision on load strategy (once per
shell, lazy on first Run).
Candidate slice decomposition
Provisional until the doc settles. Phase A is worth shipping on its own merits —
every slice improves the playground for its existing users, so a decision not to
proceed to Phase B strands nothing.
Phase A — the playground as a substrate
WORKER_SRCout of the template literal into areal typechecked module; give
playground/atestscript (it has none — CIruns
build:wasmthentsc --noEmit, so the comment-enforced "every curatedexample is runnable" invariant is unchecked); unit-test entry selection against
hand-built fake surfaces. No wasm needed for those tests — the logic is
plain JS over a
composeApp()result. This slice is a prerequisite for slice 1:there is currently nowhere to write the test proving an ambiguous entry is
rejected rather than guessed.
diagnostics,
needs_composewidening. Carries a front-loaded ADR.linkGraph; make the wasm URLs configurablerather than
location.href-relative (they break on any nested route, which alessons app has); per-app build output (
dist/isrm'd at build start, so twoapps collide); measure the release wasm and put a number in CI (Q7). Defer the
full
bynk-web-corepackage with anexportsfield until there are two realconsumers — abstracting from one caller's shape is how the seams come out wrong.
Phase B — the course
handful of exercises expressed in it — the format is only validated by real
content. Consumes Track: documentation & web presence #557's verification harness.
in
localStorage: no accounts, no backend, no personal data. Reusedeeplink.tsunchanged so a learner can share an attempt.mode in the sandbox,
RunReplywidened to a suite/case/outcome shape reusingbynk-driver/src/test_json.rs's document rather than inventing one, andcontracts: trueon the in-memory path.Slice status
WORKER_SRCas a module,testscript, entry-selection unit testsneeds_composelinkGraphexport, configurable wasm URLs, per-app build output, wasm size budgetbynk learnFront-loaded ADR candidates
Numbers taken at merge, not pre-allocated.
exercise, every runnable Book snippet and every embedded example depends on
which programs are runnable and how the entry is found. Changing it after a
corpus exists invalidates content. Covers the exactly-one rule, the diagnostic
identities, and the
needs_composewidening.relevant; hard to reverse once a second surface is deployed.
N exercises exist, and it is the contract between the content, the CI gate and
any future shell — including one in a different repository.
Explicitly not an ADR: a
from playgroundservice kind. Rejected duringscoping; reasoning recorded under Q3 so it is not re-opened by default.
Threat model
Assets. The app origin's storage and session on
playground.bynk-lang.org(and any future course origin); the learner's browser and anything else it holds.
Adversary. A malicious shared program. Deep links (
#base64url(deflate-raw(…)))and
?s=<id>share ids are attacker-controllable, so anyone can hand a victim aURL that executes code in their browser. This exposure exists today and the track
must not widen it. A course adds a second vector of the same kind — a shared
exercise attempt.
Where verification happens. Four properties carry the boundary, and each is
load-bearing rather than defence-in-depth garnish: the sandbox runs on a
distinct origin in an
<iframe sandbox="allow-scripts">withoutallow-same-origin, so it is an opaque origin with no access to app-originstorage; both directions of
postMessagevalidate (sandboxcheckse.origin === APP_ORIGIN; the app checkse.source === iframe.contentWindow);execution is killed on a 3 s wall clock via
worker.terminate(); and thecapability surface is withheld in the browser binding —
FetchandSecretsthrow by construction rather than being merely unbound.
What this track changes, and the invariants it must preserve. Slice 0 moves
WORKER_SRCand slice 2 moveslinkGraph— i.e. the code deciding whatexecutes — and slice 3 widens who may ask the sandbox to execute. Preserve:
distinct origin, no
allow-same-origin, per-message origin validation on bothhops, and the wall clock. Widening
APP_ORIGINto an allow-list must be anexplicit finite list of first-party origins, never a wildcard or a suffix match.
One new constraint this track should adopt. Today the sandbox has no
affirmative network block — no CSP file exists under
playground/, anddist/ships no
_headers. Outboundfetchis unreachable only because Bynk emits noJavaScript escape hatch, so everything running in the sandbox is compiler
output. A course is exactly the thing likely to break that assumption, by
wanting a hand-written JS test harness or scaffolding. So: everything executed
in the sandbox must remain compiler output; a graded exercise's assertions are
written in Bynk (
suite/case/expect) or evaluated outside the sandbox from{ logs, value }and the diagnostic set — never injected as hand-authored JS.Adding a CSP to both origins is cheap belt-and-braces and worth doing in slice 0
regardless, so the property stops resting on a language accident.
Scoping notes, the reusability audit, the third-party build-vs-buy
comparison, and a verification checklist of claims that were read but not
executed are in the feasibility assessment this issue was cut from; it becomes
the basis of
design/tracks/interactive-learning.mdin the settling PR.