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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ The GitHub Actions validation workflow passed on `main` after the audit and lear

## Unreleased

### Assessment progression

- removed premature use of Python's `is` identity operator from Module 2 exercises; Module 2 now assesses separate-instance reasoning without Module 11 syntax;
- clarified Module 5 mastery feedback so whole–part has-a is not automatically equated with strict composition/lifecycle ownership;
- aligned the Module 3 self-assessment checkpoint with the deliberate decision to postpone validation rules until Module 4;
- added a validator guard that prevents selected later-course mechanisms from becoming learner-facing assessment requirements before their introduction.

### Reference discipline

- added `SOURCE_BOUNDARY.md` to distinguish formal concepts, minimal Python implementation bridges, and out-of-scope additions;
Expand Down
4 changes: 3 additions & 1 deletion EXECUTION_AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ The audit checks the public course repository end-to-end:
- relative Markdown links;
- public learner-onboarding navigation from `README.md` and `START_HERE.md`;
- Previous / Course Map / Next navigation across Module 0–16;
- accidental publication of module quiz answer keys.
- accidental publication of module quiz answer keys;
- learner-facing assessment concept progression, so later syntax/mechanisms do not become early requirements.

## Result

Expand All @@ -32,6 +33,7 @@ Module 12 models.py/main.py PASS
Relative Markdown links PASS
Learner onboarding nav PASS
Module course navigation PASS
Assessment progression PASS
Public quiz-key guard PASS
```

Expand Down
2 changes: 1 addition & 1 deletion MASTERY_CHECKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ Classify each relationship: “Car–Engine” and “Doctor–Person.”
<details>
<summary>Feedback</summary>

Car **has an** Engine, so composition/reference is appropriate. Doctor **is a** Person, so inheritance may be appropriate if that specialization is meaningful in the model.
Car **has an** Engine, so a whole–part has-a relationship is appropriate; the Python implementation will typically hold an Engine reference, while exact lifecycle ownership depends on the model. Doctor **is a** Person, so inheritance may be appropriate if that specialization is meaningful in the model.

</details>

Expand Down
2 changes: 1 addition & 1 deletion SELF_ASSESSMENT_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ For each module, aim to reach all four levels:
| 0 | write small Python functions, conditions, loops, and collection operations without step-by-step help |
| 1 | identify meaningful objects and explain identity, state, behavior, and collaboration |
| 2 | define classes, create instances, and explain `__init__`, `self`, and instance state |
| 3 | assign responsibilities and implement behavior that changes valid object state |
| 3 | assign responsibilities and implement meaningful behavior that reads or changes object state, while recognizing that validation rules are introduced in Module 4 |
| 4 | protect state behind meaningful operations and distinguish a class invariant from a state-transition rule |
| 5 | distinguish general references/associations, whole–part has-a relationships, and is-a inheritance |
| 6 | use inheritance only when the subtype relationship is meaningful |
Expand Down
81 changes: 81 additions & 0 deletions scripts/validate_course.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from __future__ import annotations

import ast
import contextlib
import csv
import io
Expand Down Expand Up @@ -652,6 +653,84 @@ def validate_assessment_structure(errors: list[str]) -> None:
)
break


def validate_assessment_concept_progression(errors: list[str]) -> None:
"""Keep learner-facing assessments from requiring concepts before they are taught."""

rules = [
(4, "@property", "property-based encapsulation"),
(9, "@abstractmethod", "abstract/deferred class implementation"),
(9, "from abc import", "abstract/deferred class implementation"),
(10, "__mro__", "method resolution order"),
(13, "raise ValueError", "explicit exception raising"),
(13, "except ValueError", "exception handling"),
(13, "try:", "exception handling"),
(13, "assert ", "assertion-based internal checks"),
(15, "Strategy-style", "design-pattern implementation"),
(15, "Factory Method-style", "design-pattern implementation"),
]

implementation_patterns = [
(12, re.compile(r"def\s+__eq__\s*\("), "operator overloading/equality implementation"),
(12, re.compile(r"implement[^\\n]*`__eq__\\(\\)`", re.IGNORECASE), "operator overloading/equality implementation"),
(12, re.compile(r"def\s+__add__\s*\("), "operator overloading/addition implementation"),
(12, re.compile(r"implement[^\\n]*`__add__\\(\\)`", re.IGNORECASE), "operator overloading/addition implementation"),
]

def uses_identity_operator(source: str) -> bool:
try:
tree = ast.parse(source)
except SyntaxError:
try:
tree = ast.parse(source, mode="eval")
except SyntaxError:
return False

return any(
isinstance(node, ast.Compare)
and any(isinstance(op, (ast.Is, ast.IsNot)) for op in node.ops)
for node in ast.walk(tree)
)

for module in list(range(1, 8)) + list(range(9, 16)):
folder = ROOT / f"week-{module:02d}"
for name in ["exercises.md", "quiz.md", "assignment.md"]:
path = folder / name
if not path.exists():
continue

text = path.read_text(encoding="utf-8")
rel = path.relative_to(ROOT)

for introduced_in, fragment, label in rules:
if module < introduced_in and fragment in text:
fail(
errors,
f"{rel}: requires {label} before Module {introduced_in}",
)

for introduced_in, pattern, label in implementation_patterns:
if module < introduced_in and pattern.search(text):
fail(
errors,
f"{rel}: requires {label} before Module {introduced_in}",
)

if module < 11:
code_fragments = re.findall(r"`([^`\n]+)`", text)
python_blocks = re.findall(
r"```python\s*(.*?)```",
text,
flags=re.DOTALL,
)
for fragment in code_fragments + python_blocks:
if uses_identity_operator(fragment):
fail(
errors,
f"{rel}: uses Python identity operator before Module 11",
)
break

def validate_public_assessment_safety(errors: list[str]) -> None:
for path in sorted(ROOT.glob("week-*/instructor-notes.md")):
text = path.read_text(encoding="utf-8").lower()
Expand Down Expand Up @@ -686,6 +765,7 @@ def main() -> int:
validate_local_setup_guide(errors)
validate_publication_metadata(errors)
validate_assessment_structure(errors)
validate_assessment_concept_progression(errors)
validate_public_assessment_safety(errors)
validate_open_course_neutrality(errors)

Expand Down Expand Up @@ -718,6 +798,7 @@ def main() -> int:
print("- local reproducibility guide: OK")
print("- citation/accessibility metadata: OK")
print("- assignment rubric / quiz structure: OK")
print("- assessment concept progression: OK")
print("- learner-facing assessment neutrality: OK")
print("- public assessment-key guard: OK")
print("- gradebook calculator self-test: OK")
Expand Down
4 changes: 3 additions & 1 deletion week-02/exercises.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ Answer:

1. How many class definitions are there?
2. How many `Book` instances are there?
3. Is `book_1 is book_2` expected to be `True` or `False`?
3. Are `book_1` and `book_2` two separate instances created from the same class? Explain without using Python identity operators yet.
4. What is the conceptual relationship between `Book` and `book_1`?

> Python's `is` identity operator is introduced formally in Module 11. Module 2 only requires the concept that separate constructor calls create separate instances.

---

## Exercise 2 — Build a Class with `__init__()`
Expand Down
Loading