Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .claude/skills/glossary/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
name: glossary
description: Look up or update specs/glossary.md, the registry of Mathiz's first-class concepts. Use before naming a new type, table, event, API resource or UI term, and whenever a PR adds, renames, splits or removes a concept.
---

# Updating the glossary

`specs/glossary.md` exists so that each concept has **one name and one
implementation**. Look things up there before writing code; edit it when a
concept is added, renamed, split or retired.

## Is it a first-class concept?

Add an entry only if at least one is true:
- It has its own ent schema, event type, or public Go type that other
packages use.
- It appears in an API path or response, a spec, or `docs/personas.md`.
- Parents or kids see it as a named thing in the UI.

Not glossary material: private helpers, generic programming terms, env
vars (CLAUDE.md), one-off config knobs.

## Do

- **Search first**, including synonyms and the "Homonyms & look-alikes"
section. `grep -in "<word>" specs/glossary.md` plus a code grep for the
idea, not just the word.
- **Reuse the existing name** in new code, API fields and specs, even if
you'd have picked a different word.
- **Use the exact entry format** in the file: `### Term`, a 1–3 sentence
definition (what it is plus its key invariant), `**Code:**` (1–3
authoritative paths), `**Spec:**` link, and an optional `**Aka / don't call
it:**` line.
- **Put it in the right domain section, alphabetically.**
- **Verify every `Code:` path exists** (`grep`/`ls`) before committing. Fix
stale paths when you move code.
- **Update the glossary in the same PR** as the code or spec that changes the
concept.
- **Add a Homonyms line** when your term means something else elsewhere
(e.g. billing *plan* vs session *plan*).
- **Map kid/parent-facing copy** to its internal concept on the `Aka` line
(e.g. "clue" → Question) instead of renaming internals to match the UI.
- **Mark draft-spec concepts** `(proposed, specs/NN)` and drop the marker
when they ship.

## Don't

- **Don't add a near-duplicate.** If your idea is a variant of an existing
concept, extend that entry (and the code) or explain the difference in
both entries. Never create two entries for one idea.
- **Don't write essays.** Design rationale belongs in the spec; the glossary
links to it.
- **Don't invent identifiers.** Every name in `Code:` must exist in the
repo.
- **Don't delete an entry silently** when a concept is retired. Remove it
in the PR that removes the code, and note the replacement on any entry
that referenced it.
- **Don't rename a concept in the glossary alone.** A rename touches code,
specs, API and docs together, or not at all.
- **Don't record volatile details** (counts, defaults, timeouts) unless
they're an invariant. They go stale; point to the code instead.

## Checklist for a PR that adds a concept

1. The grep found no existing concept that already covers it.
2. An entry is added in the right section, alphabetically, in the house
format.
3. The Homonyms section is updated if the word is overloaded.
4. The `Code:` paths and `Spec:` link resolve.
5. The spec and `docs/personas.md` (for a user-facing concept) use the same
name.
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
golang 1.25.6
golang 1.27.1
15 changes: 14 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ needed), `frontend` (web/ patterns — read it BEFORE touching web/),
tracking/telemetry change), `add-event-type`, `add-math-skill`,
`add-billing-provider`, `verify`,
`db-backup` (pg_dump backups via `scripts/db-backup.sh`),
`user-feedback` (intake protocol when the owner pastes user reports).
`user-feedback` (intake protocol when the owner pastes user reports),
`glossary` (dos and don'ts for editing `specs/glossary.md`).
If a PR changes a frontend pattern, update `.claude/skills/frontend/SKILL.md`
in the same PR (same rule as `docs/personas.md` for user-facing flows).

Expand Down Expand Up @@ -83,6 +84,18 @@ significant feature. `docs/personas.md` is the source of truth for
user-facing flows — **update it when you add or change one**.
`docs/development.md` is the human dev-setup guide.

### Glossary — check before you name anything

`specs/glossary.md` lists every first-class concept (skill, tier,
expedition, family space, credit grant…) with its code location and spec.
**Before introducing a new type, table, event, API resource, or UI
term, look it up there.** If the concept already exists, reuse its name
and code, even when another word feels more natural. A second name for the
same idea is a bug. If it is genuinely new, add an entry in the same PR,
following the `glossary` skill. Kid-facing copy ("clue", "dig spot") is
mapped to internal names there too, so don't rename internals to match UI
copy.

## Invariants (do not break)

**Tenant isolation**
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ COPY web/ ./
RUN mkdir -p /src/internal/saas/webui && npm run build

# --- Stage 2: build the Go binary with the SPA embedded ---
FROM golang:1.25-alpine AS build
FROM golang:1.27-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ But the system is designed to support multiple standards and curriculum.

## Build from source

Requires Go 1.25+ (and Node 20+ for the hosted-mode web app).
Requires Go 1.27+ (and Node 20+ for the hosted-mode web app).

```sh
git clone https://github.com/abhisek/mathiz.git
Expand Down
2 changes: 1 addition & 1 deletion ent/creditentry.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ent/llmrequestevent.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ent/schema/credit_entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (CreditEntry) Fields() []ent.Field {
field.String("source").
Unique().
Immutable().
Comment("Idempotency key, e.g. starter:<space>, sub:<eventID>, expedition:<sessionID>"),
Comment("Idempotency key, e.g. starter:<space>, sub:<eventID>, topup:<eventID>, session:<sessionID>"),
field.Time("expires_at").
Optional().
Nillable().
Expand Down
2 changes: 1 addition & 1 deletion ent/schema/llm_request_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func (LLMRequestEvent) Fields() []ent.Field {
field.String("model").
Comment("Actual model ID used"),
field.String("purpose").
Comment("Consumer-provided label: question-gen, hint, lesson, diagnosis"),
Comment("Consumer-provided label (llm.Purpose*): question-gen, quest-gen, lesson, error-diagnosis, session-compress, profile"),
field.Int("input_tokens").
Default(0).
Comment("Tokens in the request"),
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/abhisek/mathiz

go 1.25.6
go 1.27.1

require (
charm.land/bubbles/v2 v2.0.0-rc.1
Expand Down
2 changes: 1 addition & 1 deletion internal/skillgraph/seed.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package skillgraph

import "fmt"

// skills contains all 54 skill definitions for the MVP.
// skills contains all 56 skill definitions for the MVP.
var skills = []Skill{

// ── Number & Place Value (8 nodes) ────────────────────────
Expand Down
5 changes: 3 additions & 2 deletions internal/store/ownerguard.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ const ownerIDColumn = "owner_id"

// ownerScopedTypes lists every ent type whose rows belong to a single
// learner: all event schemas embedding EventMixin, plus Snapshot.
// Family-scoped control-plane types (Account, FamilySpace, ChildProfile,
// Invite, DeviceToken, CreditEntry, BillingState) are intentionally NOT
// Family-scoped control-plane types (Account, FamilySpace, FamilyMember,
// ChildProfile, Invite, ParentInvite, DeviceToken, CreditEntry,
// BillingState, Quest, QuestQuestion, QuestProgress) are intentionally NOT
// here — they have no owner_id column and are scoped by the authz layer.
var ownerScopedTypes = map[string]bool{
ent.TypeAnswerEvent: true,
Expand Down
24 changes: 15 additions & 9 deletions specs/02-persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,17 +99,23 @@ The `sequence` field is a global counter across all event types, providing a tot

Each event type is a **separate ent schema** with its own table. This gives each type its own typed fields and indexes while keeping the base fields consistent via the mixin.

Initial event types (fields beyond the base are illustrative — domain specs finalize them):
Event types as shipped (`ent/schema/*_event.go`; the schema files are
authoritative for fields). The names in the original design
(`AnswerAttemptEvent`, `HintRequestEvent`, `LessonViewEvent`,
`SkillStateChangeEvent`, `SessionStart/EndEvent`, `GemAwardEvent`) were
superseded by these:

| Event Type | Key Fields | Defined By |
|------------|-----------|------------|
| `AnswerAttemptEvent` | skill_id, question_hash, given_answer, correct, latency_ms | Session spec |
| `HintRequestEvent` | skill_id, question_hash, hint_level | AI Lessons spec |
| `LessonViewEvent` | skill_id, lesson_id | AI Lessons spec |
| `SkillStateChangeEvent` | skill_id, from_state, to_state | Mastery spec |
| `SessionStartEvent` | session_id, target_skill_id | Session spec |
| `SessionEndEvent` | session_id, questions_answered, accuracy | Session spec |
| `GemAwardEvent` | gem_type, rarity, reason_skill_id | Rewards spec |
| `AnswerEvent` | session_id, skill_id, tier, category, question_text, correct_answer, learner_answer, correct, time_ms, answer_format | Session spec |
| `SessionEvent` | session_id, action (start/end), questions_served, correct_answers, duration_secs, plan_summary, quest_uid, quest_name | Session spec |
| `MasteryEvent` | skill_id, from_state, to_state, trigger, fluency_score, session_id | Mastery spec |
| `DiagnosisEvent` | session_id, skill_id, category, misconception_id, confidence, classifier_name, reasoning | Diagnosis spec |
| `HintEvent` | session_id, skill_id, question_text, hint_text | AI Lessons spec |
| `LessonEvent` | session_id, skill_id, lesson_title, practice_* | AI Lessons spec |
| `LearnerProfileEvent` | summary, strengths, weaknesses, patterns, generated_at | AI Lessons spec |
| `GemEvent` | gem_type, rarity, skill_id, skill_name, session_id, reason | Rewards spec |
| `LLMRequestEvent` | provider, model, purpose, input/output_tokens, success, error_message, request/response body | LLM spec |

### Extensibility

Expand All @@ -129,7 +135,7 @@ Every event table has:
- Index on `sequence` (for ordered replay)
- Index on `timestamp` (for time-range queries)

Type-specific indexes (e.g., `skill_id` on `AnswerAttemptEvent`) are defined by the owning spec.
Type-specific indexes (e.g., `skill_id` on `AnswerEvent`) are defined by the owning spec.

## 5. Snapshots

Expand Down
4 changes: 4 additions & 0 deletions specs/03-skill-graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ func Validate() error

## Diagnostic Placement

> **Not implemented.** Only the `skillgraph.DiagnosticResult` stub exists
> (`internal/skillgraph/diagnostic.go`). New learners start from the root
> skill. The design below is kept for reference.

On first launch (no learner data exists), Mathiz runs a **top-down probing** diagnostic quiz to skip the learner past already-known skills.

### Algorithm
Expand Down
22 changes: 13 additions & 9 deletions specs/04-llm-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ type LLMRequestEvent struct {

Provider string // "anthropic", "openai", "gemini"
Model string // Actual model ID used
Purpose string // Consumer-provided label: "question-gen", "hint", "lesson", "diagnosis"
Purpose string // Consumer-provided label, e.g. "question-gen", "lesson", "error-diagnosis" (see Purpose Labels)
InputTokens int // Tokens in the request
OutputTokens int // Tokens in the response
LatencyMs int64 // Wall-clock time for the request
Expand Down Expand Up @@ -556,14 +556,18 @@ func PurposeFrom(ctx context.Context) string {

Standard purpose labels:

| Label | Consumer |
|-------|----------|
| `"question-gen"` | Problem Generation |
| `"hint"` | AI Lessons |
| `"lesson"` | AI Lessons |
| `"diagnosis"` | Error Diagnosis |
| `"compression"` | Context Compression |
| `"diagnostic-placement"` | Diagnostic Placement |
| Label | Constant | Consumer |
|-------|----------|----------|
| `"question-gen"` | `llm.PurposeQuestionGen` | Problem Generation (hints are part of the question, so there is no separate hint purpose) |
| `"quest-gen"` | `llm.PurposeQuestGen` | Quest AI authoring (specs/15) |
| `"lesson"` | `llm.PurposeLesson` | AI Lessons |
| `"error-diagnosis"` | `llm.PurposeDiagnosis` | Error Diagnosis (LLM fallback) |
| `"session-compress"` | `llm.PurposeSessionCompress` | Context Compression |
| `"profile"` | `llm.PurposeProfile` | Learner profile generation |

The constants live in `internal/llm/timeout.go`, which also sets a
per-attempt timeout for each purpose. Diagnostic placement was never built,
so it has no label.

---

Expand Down
4 changes: 2 additions & 2 deletions specs/11-rewards.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Five gem types, each tied to a distinct learning achievement:
| **Mastery** | `mastery` | Skill transitions Learning → Mastered (trigger: `prove-complete`) | Awarded when a skill is mastered for the first time |
| **Recovery** | `recovery` | Skill transitions Rusty → Mastered (trigger: `recovery-complete`) | Awarded when a rusty skill is recovered |
| **Retention** | `retention` | Spaced-rep graduation (`ConsecutiveHits >= 6`) | Awarded when a skill graduates from the review schedule |
| **Streak** | `streak` | 5 consecutive correct answers within a session | Awarded each time a 5-answer streak is achieved |
| **Streak** | `streak` | 5 consecutive correct answers within a session | Awarded at streak lengths 5, 10, 15, 20, then every further 5 (25, 30, …) |
| **Session** | `session` | Session timer expires naturally (no early quit) | Awarded for completing a full session |

```go
Expand Down Expand Up @@ -247,7 +247,7 @@ Streak gems are awarded each time the learner achieves a consecutive correct-ans
| 15 | Epic |
| 20+ | Legendary |

Gems are awarded **at each threshold**, not retroactively. Reaching 10 earns two gems: a Common at 5 and a Rare at 10.
Gems are awarded **at each threshold**, not retroactively. Reaching 10 earns two gems: a Common at 5 and a Rare at 10. Past 20, a Legendary is awarded at every further multiple of 5 (`gems.NextStreakThreshold`).

```go
// StreakThresholds defines the streak lengths that award gems.
Expand Down
15 changes: 10 additions & 5 deletions specs/12-saas.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,19 @@ type Principal struct {
}
```

Policy (v1):
- A parent may manage (read/write) a family space iff they are its `owner_account_id`.
- A parent may manage children, invites, devices, and read stats of spaces they own.
Policy (current: the membership model; see "Co-parents" below):
- A parent may manage (read/write) a family space iff they are a member of it
(`family_member` role `owner` or `parent`). The v1 rule "iff
`owner_account_id`" was replaced when co-parents shipped.
- Members may manage children, invites, devices, quests, and read stats of
their space. Billing and parent management are owner-only
(`CanManageBilling`, `CanManageParents`).
- A child may access exactly one thing: **their own** learning session and profile info.
- Children can never read other profiles, stats, or control-plane objects.

Every check returns `authz.ErrDenied` which the API maps to 403 (404 for existence
probes on objects the caller can't see).
Every check returns `authz.ErrDenied`, which the API **always** maps to 404
(`writeServiceError` in `internal/saas/server/middleware.go`). It never
returns 403, so a denial can't confirm that an object exists.

## 6. Terminal Bridge — `internal/saas/termbridge`

Expand Down
3 changes: 2 additions & 1 deletion specs/14-monetisation.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ Event-sourced entries on the **FamilySpace** (`credit_entries`):
- Debits (`debit`): negative `amount`, consumed FIFO from unexpired grants
ordered by soonest expiry (use-it-or-lose-it credits burn first).
- Every entry has a unique `source` (e.g. `starter:<spaceID>`,
`sub:<providerEventID>`, `expedition:<sessionID>`) — **idempotency is
`sub:<providerEventID>`, `topup:<providerEventID>`, `session:<sessionID>`
for an expedition debit) — **idempotency is
structural**: replayed webhooks and retried requests cannot double-grant
or double-charge.

Expand Down
Loading
Loading