Skip to content

content: bump content.lock — C1 + EVM elective + reflection prompts + flip wave - #892

Merged
thomgabriel merged 2 commits into
mainfrom
content/lock-bump-c1-evm-flip-30-07-2026
Jul 30, 2026
Merged

content: bump content.lock — C1 + EVM elective + reflection prompts + flip wave#892
thomgabriel merged 2 commits into
mainfrom
content/lock-bump-c1-evm-flip-30-07-2026

Conversation

@thomgabriel

Copy link
Copy Markdown
Collaborator

Activates everything staged in solanabr/courses-academy since the last pin:
9b1f03e6c5c625e03b23f6ce527da884456f7b2d36c26bed.

Counts

before (9b1f03e6) after (c5c625e0) Δ
courses 4 6 +2
lessons 49 66 +17
learningPaths 8 8
achievements 10 10
quests 5 5
skills (taxonomy) 41 43 +2 (explorer-reading, api-currency)

6 courses = the 5-course ladder (C1→C2→C3→C4→C5) + 1 off-ladder elective.

Wave delta

Six upstream PRs land in this pin:

Doc-level classification of the recompiled bundle: 2 courses added, 17 lessons added, 6 lessons edited, 0 removed, no id churn, no slot remapping on any existing course (both new courses get fresh slots.json entries with empty retired).

Goldens methodology

Regenerated through the real projectors (src/lib/content/project.ts), never hand-written, per the house pattern:

  • courses.json, lessons.json — existing entries re-projected in place, existing order preserved, new docs appended.
  • paths.json — rebuilt wholesale from bundle membership (projectLearningPath(bundlePath, resolvedMembers, deps)), so zero-to-deployed is now C1-first and solana-core carries order: 8.
  • course-summaries.jsoncoursesByIds / recommended / lessonCounts rebuilt for all 6 courses (alphabetical by _id); learningPath resolved from real path membership, hence null for the EVM elective.
  • quests-raw.json — derived challengeLessonIds / moduleLessonMap recomputed via projectQuestData; raw quest docs untouched.
  • course-by-slug.jsonunchanged; its subject (course-building-first-program) is not in this wave.
  • achievements-raw.json — hand-patched for exactly two fields (course-completer.award.course, full-stack-solana.description) in both the all and deployed arrays; the rest stays the frozen prod-Sanity capture.

Before rewriting anything, every pre-existing golden doc was re-projected against the new bundle to find unexplained drift. The only docs that diverged were the 6 intentionally-edited lessons above — no silent absorption. The wave delta is documented in the project.golden.test.ts header, as with every prior wave.

E2E impact

e2e/harness/fixtures.mjs models the active catalog. C1 and the EVM elective have no public_onchain_deployments row, so they land in the existing "no row → ABSENT" bucket and the fixture needs no new entries. Verified, not asserted: the full Playwright suite was run against a production build — catalog.spec.ts, learn-loop.spec.ts and o4-journey.spec.ts all pass unchanged (unsubscribe.spec.ts stays skipped pending #769). The only edit to the file is a stale comment that still described the bundle as "the live 4-course ladder".

Verification

  • pnpm compile-contentEmitted 8 modules, counts as tabled above
  • pnpm typecheck → 6/6 tasks green
  • pnpm --filter web test244 files / 2217 tests passed
  • pnpm lint → green (pre-existing import/order warnings only)
  • pnpm --filter web e2e → 3 passed, 2 skipped
  • prettier --check on every touched file → clean

Visibility

C1 and the EVM elective stay INVISIBLE to learners on merge. The catalog gate (lib/content/deployments.ts, one isSynced chokepoint) shows a course only when it has a public_onchain_deployments row that is status === "synced" and is_active !== false. Neither course has been created on-chain, so neither has a row, and the gate is fail-closed. This bump only stages them; they surface when the owner runs the on-chain course create. The flip-wave edits that touch already-live content (path ordering, the course-completer retarget, the reflection prompts) DO take effect on deploy.

Do not merge — opened for review.

Surprises

  1. An extra upstream PR rode along. The brief described 4 upstream changes; the range actually contains 6. courses-academy kazachikapai #22 (C4 inverted deposit account order) and feat: Complete LMS Frontend — 10 Pages, Wallet Auth, Gamification, i18n, Code Editor #23 (path order-1 collision + achievement record alignment) also land here. Both are small, well-scoped content fixes and are folded into the wave note rather than dropped — but they were not in the brief.
  2. 6 lessons edited, not 4. Reflection block: comprehension reflection + one-round AI feedback — Earn submission does NOT belong in the reflection box (C5 finale) #848's reflection rewrites cover 4 lessons; kazachikapai #22's code fix covers 3, overlapping on lesson-dsk-first-signed-deposit.
  3. achievements-raw needed a second patch. The brief scoped it to the course-completer retarget only, but feat: Complete LMS Frontend — 10 Pages, Wallet Auth, Gamification, i18n, Code Editor #23 also reworded full-stack-solana's description, which projectAchievement compares — leaving it would have failed the golden. Patched and documented.
  4. A block _key changed (submitreflect on lesson-sap-submit-and-what-you-built). Grepped the app for hardcoded block keys: none — blocks are addressed structurally, so this is inert. Worth knowing if any learner-progress record ever keys on block _key.
  5. Only 2 new skills. The EVM course contributed no new taxonomy entries; explorer-reading and api-currency both come from C1.

🤖 Generated with Claude Code

… flip wave

Activates courses-academy @c5c625e0 (PRs #22/#23/#24/#25/#26/#27).

Bundle 4 courses / 49 lessons -> 6 courses / 66 lessons:
  + C1 course-solana-for-web-devs (8 lessons, trackLevel 1, #25)
  + elective course-solana-for-evm-devs (9 lessons, trackId 0, #24)

Flip wave (#27/#23): path-zero-to-deployed gains C1 in the FRONT slot,
path-solana-core order 1 -> 8 (order-1 collision), achievement
course-completer retargeted C2 -> C1 (closes the #673 placeholder),
full-stack-solana description reworded. Reflection rewrites (#848 /
courses-academy #26) touch 4 openEnded prompts across C4/C5. C4 code
stand-ins fixed for inverted deposit account order (#22).

Goldens regenerated from the bundle through the REAL projectors; e2e
catalog fixture unchanged (C1/EVM have no deployment row, so the gate
keeps them invisible).
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
superteam-academy-web Ready Ready Preview Jul 30, 2026 11:08pm

Request Review

@claude

claude Bot commented Jul 30, 2026

Copy link
Copy Markdown

Claude finished @thomgabriel's task in 5m 1s —— View job


Review: PR #892 — content.lock bump (C1 + EVM elective + reflections + flip wave)

  • Gather context (PR body, changed files, CLAUDE.md)
  • Read docs/TASK-CODES.md for conventions (n/a — no Closes #N in this PR)
  • Review golden fixtures / projector consistency
  • Review content.lock pin and e2e/harness/fixtures.mjs catalog-gate reasoning
  • Check visibility/fail-closed claim against lib/content/deployments.ts
  • Trace the flip-wave's effect on live gamification logic (not just content projection)
  • Post final review

BLOCKING

Adding C1 to path-zero-to-deployed permanently breaks the "Full Stack Solana" achievement until C1 ships on-chain — for users who already finished C2–C5.

paths.json (apps/web/src/content/generated/paths.json:90-95, mirrored in the golden fixture) puts course-solana-for-web-devs (C1) into the already-live, non-draft path-zero-to-deployed learning path. The catalog-visibility gate (isSynced, lib/content/deployments.ts:91) correctly keeps C1 out of the public course list and out of getAllLearningPaths() (which filters path membership through isSynced, queries.ts:299) — so the catalog story in the PR body is accurate.

But the achievement engine reads path membership through a different, unfiltered path:

  • getLearningPathsForAdmin() (apps/web/src/lib/content/queries.ts:696-701pathCourseRefIds, queries.ts:148-151) returns the path's raw courseIds with no isSynced filtering.
  • buildUserState() (apps/web/src/lib/gamification/achievements.ts:161-171) computes completedPathIds by requiring every courseId in path.courseIds to be in completedCourseIds.
  • completedCourseIds is built from getAllCourseLessonCounts(), which is synced+active-only (confirmed by the existing test name at apps/web/src/lib/content/__tests__/queries.test.ts:323, "getAllCourseTags / getAllCourseLessonCounts only synced+active") — so C1 can never appear in completedCourseIds until it has an on-chain deployment row.
  • achievement-full-stack-solana (achievements.json:103-111) is a path-completed award keyed on path-zero-to-deployed, checked via s.completedPathIds.has(a.path) (achievements.ts:66-67).

Net effect: path-zero-to-deployed can never satisfy .every(...) again — not until C1 is deployed on-chain — so no user, including ones who already completed the full C2→C5 ladder, can newly unlock "Full Stack Solana" from merge onward. This is a real functional regression on a live feature, silent because content-projection golden tests (project.golden.test.ts) and the gamification predicate tests (predicates.test.ts, which mocks getAllCourseLessonCounts) never exercise the two together against real bundle content — it only shows up when an unsynced course is added as a member of an already-live path, which is what this PR does for the first time.

This is distinct from — and not addressed by — the PR's own "Visibility" section, which only discusses catalog display and the (intentionally deferred) achievement-course-completer retarget. Nothing in the PR body or "Surprises" section flags this path-completion side effect.

Suggested fix (not applied — flagging per review-only instructions): either (a) don't add C1 to path-zero-to-deployed's bundle membership until C1 has an on-chain deployment, or (b) make buildUserState's path-completion check use the same isSynced-filtered course set the catalog uses (e.g. swap to a sync-aware path/course source, or filter path.courseIds to synced ones before the .every(...) check). Given path-zero-to-deployed is meant to gain C1 as a front slot member per the wave note, option (b) is likely the durable fix, since future waves will hit this same trap.

Fix this →


Everything else checks out

  • content.lock SHA, meta.json counts (6 courses / 66 lessons / +2 skills), and every generated/golden pair match the PR's own count table exactly.
  • Catalog fail-closed gate (isSynced) is correct and unmodified — C1/EVM elective are genuinely invisible in the public catalog and in getAllLearningPaths().
  • e2e/harness/fixtures.mjs diff is comment-only, consistent with "no row → ABSENT" needing no new fixture rows.
  • achievements-raw.json patch is minimal and matches exactly the two claimed fields (course-completer.award.course, full-stack-solana.description); achievement-course-completer's C1 retarget is explicitly and correctly called out as deferred until C1 ships (that one's fine — it's a course-completed predicate on a single course, not a path).
  • path-solana-core order 1→8 collision fix: harmless, that path is still draft: true.
  • C4 code-fixture account-order fix (courses-academy kazachikapai #22): hand-written client stand-ins now match [vault, owner, system], consistent throughout starter/solution/expectedOutput blocks I sampled — this is teaching-content only, not the real on-chain program, so no onchain-academy/** risk.
  • No supabase/schema.sql, onchain-academy/**, or secret-touching files in this diff — outside the blast radius this review's priorities scrutinize hardest, except for the gamification-logic interaction flagged above.

Not run: pnpm install / pnpm typecheck / pnpm test — this sandbox has no node_modules and pnpm requires interactive approval I can't grant here. The author's reported green run (244 files / 2217 tests, typecheck, lint, e2e) is plausible given everything I traced by hand, but it would not have caught the blocking issue above since neither the golden-projector tests nor predicates.test.ts exercise real path membership against the gamification layer.

This review is advisory — CI is the hard gate, and a human should confirm the path-completion interaction before merge given it touches live-user achievement state.

… on #892)

buildUserState() computed completedPathIds from getLearningPathsForAdmin()'s
UNFILTERED path.courseIds while completedCourseIds is synced+active-only, so
staging an unsynced course into an already-live path made that path's
path-completed achievement permanently unsatisfiable — this wave adding C1 to
path-zero-to-deployed would have retroactively broken achievement-full-stack-
solana for learners who already finished C2-C5, and every future wave that
adds a member to a live path re-armed the same trap.

Filter path.courseIds through the same isSynced chokepoint the catalog and
getAllLearningPaths use, before the .every(...) check. getLearningPathsForAdmin
keeps returning raw membership (the admin panel needs it). A path whose synced
membership is empty still does not count as completed — the length > 0 guard
keeps .every([]) from being vacuously true.

New regression suite path-completion-sync-gate.test.ts drives the REAL bundle
membership and REAL queries through a controlled deployment set. Red-proofed at
the previous head: cases 1 and 2 failed (expected false to be true / expected
true to be false); case 3 (a synced-but-unfinished member still blocks) passed
both before and after, as the control.
@thomgabriel

Copy link
Copy Markdown
Collaborator Author

Gate bounce addressed — option (b), the durable fix (head 1c431be)

The reviewer's BLOCKING finding is real and correctly diagnosed. Implemented option (b) in this PR, since the guard belongs with the wave that first introduces the condition.

The fix

apps/web/src/lib/gamification/achievements.tsbuildUserState() now filters path.courseIds through the same isSynced chokepoint (lib/content/deployments.ts) that the catalog and getAllLearningPaths (queries.ts:299) use, before the .every(...) completion check. getActiveDeployments() joins the existing Promise.all, so there's no added round-trip latency and no new data source.

getLearningPathsForAdmin() is left alone — the admin panel needs raw, ungated membership. The gate is applied at the consumption site that has the mismatched semantics.

Vacuous case preserved: the length > 0 guard now runs on the filtered list, so a path whose synced membership is empty does not count as completed — .every([]) can't be vacuously true.

The rationale is recorded in a comment at the site, naming the trap so future waves that add a member to a live path read it before re-arming it.

Regression test

New apps/web/src/lib/gamification/__tests__/path-completion-sync-gate.test.ts. It mocks exactly one seam — the deployment set — and drives real bundle path membership through the real getLearningPathsForAdmin / getAllCourseLessonCounts; isSynced itself stays real, since it is the gate under test. This is precisely the pairing the reviewer noted no existing test exercises (project.golden.test.ts never touches gamification; predicates.test.ts mocks getAllCourseLessonCounts away).

Three cases:

  1. C1 unsynced + learner finished all four live members → path-zero-to-deployed completes.
  2. Nothing synced → path does not complete, even for a learner credited with every course in it (vacuous-every guard).
  3. C1 synced but unfinished → path does not complete (control: the gate loosens membership, never the completion requirement).

Red-proof

Reverted achievements.ts to the previous head (9b0c828), leaving the new test in place:

× an UNSYNCED member does not block the path for a learner who finished every live member
    AssertionError: expected false to be true
× a path whose synced membership is EMPTY never counts as completed
    AssertionError: expected true to be false
Tests  2 failed | 1 passed (3)

Case 3 passes on both sides by design — it's the control that proves the fix doesn't over-loosen. Restored the fix: 3/3 green.

Re-run

  • pnpm typecheck → 6/6 green
  • pnpm --filter web test245 files / 2220 tests passed (was 244/2217; +1 file, +3 tests)
  • pnpm lint → green

Head: 1c431bec2ff1ac7da08b54555274ef779789d3cd. Still not merged.

@thomgabriel

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Jul 30, 2026

Copy link
Copy Markdown

Claude finished @thomgabriel's task in 2m 14s —— View job


Review: PR #892 follow-up — path-completion sync-gate fix (head 1c431be)

  • Gather context (PR body, prior review, new commits)
  • Review the achievements.ts fix (option b: sync-gate on path.courseIds)
  • Review the new regression test (path-completion-sync-gate.test.ts)
  • Check for related call sites / edge cases missed by the fix
  • Post final review

Verdict: BLOCKING issue from the previous review is resolved. No new blockers found.

The fix (apps/web/src/lib/gamification/achievements.ts:161-186) filters path.courseIds through isSynced(deployments.get(cid)) — sourced from getActiveDeployments(), joined into the existing Promise.all — before the .every(...) completion check. This is exactly the gate getAllLearningPaths already applies at queries.ts:292-299, so the semantics now match between the catalog path and the achievement path. getLearningPathsForAdmin() itself is correctly left ungated (the admin panel needs raw membership); the fix is applied at the one consumption site with the mismatched requirement.

I checked the two things most likely to go wrong with this kind of fix:

  1. Vacuous-every regression. The length > 0 guard now runs on the filtered (syncedCourseIds) list rather than the raw one — necessary, since a path could now have zero synced members. Test case 2 (sync() with nothing synced, learner credited with every course) confirms .every([]) doesn't sneak a false completion through.
  2. Other unfiltered consumers of getLearningPathsForAdmin. Grepped for it — achievements.ts is the only non-admin caller in the codebase, so there's no sibling call site carrying the same bug forward.

The regression test (path-completion-sync-gate.test.ts) is well-constructed: it mocks only the deployment-map seam and drives real bundle path membership (path-zero-to-deployed, real course ids) through the real getLearningPathsForAdmin/getAllCourseLessonCounts, with isSynced itself left real since it's the thing under test. Three cases cover exactly the fix's three failure modes (unsynced member doesn't block · empty-synced-membership doesn't vacuously complete · synced-but-unfinished member still blocks as a control), and the PR description documents a red-proof at the pre-fix commit showing cases 1 and 2 fail there. I traced the logic by hand rather than executing it — this sandbox has no node_modules, same limitation as the original review — but the assertions match the code path exactly.

One thing I confirmed is not a problem, in case it looks suspicious on a skim: achievements.ts now imports @/lib/content/deployments directly, and the older predicates.test.ts doesn't mock that module. That's fine — predicates.test.ts only exercises PREDICATES/checkNewAchievements against hand-built UserState fixtures, never calls buildUserState, and deployments.ts's top-level code has no side effects that fire on import (env access is all inside function bodies, called lazily). No adjustment needed there.

Not re-verified in this pass (same sandbox constraint as the original review)

pnpm typecheck / pnpm --filter web test / pnpm lint weren't re-run here (no node_modules, no interactive pnpm approval available). The author's reported second run (245 files / 2220 tests, +1 file/+3 tests over the pre-fix baseline, typecheck and lint green) lines up with what the diff should produce.
· branch content/lock-bump-c1-evm-flip-30-07-2026

@thomgabriel
thomgabriel merged commit 3b28a05 into main Jul 30, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant