Skip to content

The classroom's rules, in one file — and a table that checks them - #720

Open
mircealungu wants to merge 3 commits into
masterfrom
wt/visibility-rules
Open

The classroom's rules, in one file — and a table that checks them#720
mircealungu wants to merge 3 commits into
masterfrom
wt/visibility-rules

Conversation

@mircealungu

@mircealungu mircealungu commented Aug 31, 2026

Copy link
Copy Markdown
Member

Two commits: a table that checks the rules, and a file that states them.

The problem

The rules for what a student sees were spread across User.cohort_articles_for_user, _classroom_only and the article filters. No file stated any of them. That is the common root of the last fortnight's bugs — the Greek learner staring at a blank app, the 54 students with texts in their class they cannot open, the teacher exemption that made the first bug report misleading.

1. zeeguu/core/classroom.py — the rules, readable

The whole of the classroom's behaviour in the order someone would ask about it. Each rule is one function and the sentence that justifies it:

def student_can_read(student, text):
    """A student reads only in the language they are currently learning.

    The student's own setting decides, not the class's -- which is why a
    Greek learner in an English class sees an empty classroom, and why a class
    can hold texts that nobody in it can open.
    """

def classroom_of(student):
    """The texts a student sees, each with the classes it reached them through.

    One merged list: a student in several classes has one classroom, not one
    per class. A text shared with two of their classes appears once, naming both.
    """

def sees_only_class_texts(user):
    """Whether the app is reduced to the classroom: no feed, no search, no inbox.

    ... the strictest class wins ... Teachers are never restricted, even in a
    class that asks for it: they need the feed and the search to find the texts
    they are going to share.
    """

Both former homes become adapters: cohort_articles_for_user turns the answer into article infos and tags them; _classroom_only forwards.

2. test_classroom_visibility_rules.py — the same rules as a table

#        learning  classes                                sees  mode
("a class in another language is invisible, not empty",
     "da",     {"English Class": "en en"},              0,   FULL_APP),

("a class may hold texts the student cannot see",
     "da",     {"Mixed Class": "da en en"},             1,   FULL_APP),

("Jack's bug: restricted, and every text is in another language",
     "de",     {"CUT!": "en en en en en"},              0,   CLASSROOM_ONLY),

A row is one world. ! on a class name means "students see only the texts I share". Each row builds that world and asks the real code.

No behaviour change, and the table is what says so

298 tests and 20 subtests pass unchanged. The refactor is exactly the kind that is nerve-racking without a spec and dull with one.

The table also found a live fragility on its first run: _tiago_exercises dereferenced user.learned_language.code with no guard, and features_for_user runs on every /user_details — an account with no learned language would have taken down the whole call. Zero such accounts exist today; cohort_articles_for_user already handled the case, so the two halves disagreed. Guarded.

The rules for what a student may see are spread across
cohort_articles_for_user, _classroom_only and the article filters. No file
states them, which is why they keep being got wrong: the language filter that
emptied Jack's classroom, the off-language texts 54 students cannot open, and
his question about whether two classes would collide were all the same gap in
one form or another.

Eight rows, each one world and what a student in it sees. Readable in a
minute, and executed against the real model rather than a description of it.

Writing them down immediately found one: _tiago_exercises dereferenced
user.learned_language.code with no guard, and features_for_user runs on every
/user_details -- so an account with no learned language would have taken down
the whole call. No such account exists in production, and the model allows one;
the classroom code already handles it, so the two disagreed. Guarded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown

ArchLens detected architectural changes in the following views:
diff

The table in the previous commit checks that the rules hold. This makes the
code that implements them say the same thing.

zeeguu/core/classroom.py is the whole of the classroom's behaviour, in the
order someone would ask about it: which classes is this student in, which
texts are in them, which of those can the student read, and is the classroom
the whole app for them. Each rule is one function and the sentence that
justifies it:

    student_can_read     the student's own language decides, not the class's
    classroom_of         one merged list; a text in two classes appears once
    hidden_from          the texts a class holds that nobody in it can open
    sees_only_class_texts  strictest class wins; teachers are never restricted

They used to be spread across User.cohort_articles_for_user, _classroom_only
and the article filters, with no file stating any of them -- which is how a
Greek learner in an English class ended up staring at a blank app, and how 54
students came to have texts in their class they cannot open.

Both call sites are now adapters. cohort_articles_for_user turns the answer
into article infos; _classroom_only forwards. No behaviour change, and the
table is what says so: 298 tests and 20 subtests pass unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mircealungu mircealungu changed the title State the classroom visibility rules as a table The classroom's rules, in one file — and a table that checks them Sep 2, 2026
hidden_from was in the same file as student_can_read, and it is not a rule --
it walks every class and every text and applies one. Mixing the two means the
file is no longer readable in a minute, because you have to sort the policy
from the traversal yourself.

classroom/rules.py now holds only policies: functions that answer "is this
allowed?" from values they are given, with no traversal, no query and no I/O.
Two of them, 44 lines including the prose:

    student_can_read(student, text)
    app_is_reduced_to_classroom(user, classes)

Note the second takes the classes rather than fetching them -- that is what
keeps it a rule.

classroom/queries.py finds things and applies those: classes_of, texts_of,
classroom_of, hidden_from, sees_only_class_texts. Queries call rules; rules
never call queries.

No behaviour change: 298 tests and 20 subtests pass unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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