feat: publish A1-A8 card viewer and FreeRTOS C generator
Check card layouts / check (push) Has been cancelled

This commit is contained in:
2026-07-19 16:43:35 +02:00
parent 586644bcff
commit 8bf8e22448
10 changed files with 6394 additions and 318 deletions
+419
View File
@@ -0,0 +1,419 @@
#!/usr/bin/env python3
"""Expand a concise FreeRTOS C card spec into card_source.json.
The series spec remains the authored source for repeated metadata. Program
code, debug recipes and PlantUML diagrams stay card-local. The resulting
card_source.json is deterministic and is consumed by render_card.py.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
import uuid
from pathlib import Path
from typing import Any
GITEA_BASE_URL = "https://zsl-gitea.mpabi.pl"
GITEA_SOURCE_ORG = "edu-freertos-c"
PUBLICATION_HOST_UUID = "dce7fb9d-7b2f-5d49-96a2-3a30d3070b84"
PUBLICATION_DOMAIN = "mpabi.pl"
def file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def git_blob(path: Path) -> str:
result = subprocess.run(
["git", "hash-object", str(path)],
check=True,
text=True,
capture_output=True,
)
return result.stdout.strip()
def stable_uuid(name: str) -> str:
return str(uuid.uuid5(uuid.NAMESPACE_URL, name))
def load_json(path: Path) -> dict[str, Any]:
with path.open("r", encoding="utf-8") as stream:
return json.load(stream)
def strategy(value: dict[str, Any]) -> dict[str, Any]:
result = {
"id": value["id"],
"kind": value["kind"],
"action": "open" if value["kind"] == "code" else "replay",
"expected_observations": value["expected_observations"],
"assertions": value["assertions"],
"evidence_fields": value["evidence_fields"],
}
for key in ("prerequisites", "layout", "commands", "code_ref"):
if key in value:
result[key] = value[key]
return result
def interactive_asset(
card_id: str,
task_label: str,
section: dict[str, Any],
asset: dict[str, Any],
) -> dict[str, Any]:
phases = asset["phases"]
step_tiles = {
step["id"]: 1
for phase in phases
for step in phase["steps"]
}
orientation = section.get("orientation", "portrait")
page_width = 940 if orientation == "landscape" else 660
page_height = 560 if orientation == "landscape" else 760
kind = "uml-sequence" if asset.get("diagram_kind") == "sequence" else "uml-class"
stem = asset["stem"]
return {
"path": f"assets/{stem}.png",
"html_path": f"assets/{stem}.svg",
"source_path": f"assets/{stem}.puml",
"caption": asset["caption"],
"label": asset["label"],
"alt": asset["alt"],
"kind": "diagram",
"width": 1.0,
"page_grid": {
"columns": 1,
"rows": 1,
"page_width": page_width,
"page_height": page_height,
"overview": True,
"step_tiles": step_tiles,
},
"interactive": {
"kind": kind,
"storage_key": f"{card_id.lower()}-{stem}",
"title": asset["title"],
"task": {"id": "task01", "label": task_label},
"block": {
"id": section["id"].lower(),
"label": f"{section['id']} {section['label']}",
"description": section["description"],
},
"phases": phases,
},
}
def section_value(
card_id: str,
task_label: str,
section: dict[str, Any],
) -> dict[str, Any]:
result: dict[str, Any] = {
"title": f"{section['id']}{section['title']}",
"order": section["order"],
"content_kind": "prose",
"asset_page_mode": "one-per-page",
"page_orientation": section.get("orientation", "portrait"),
"content_tex": section["content_tex"],
"assets": [
interactive_asset(card_id, task_label, section, asset)
for asset in section["assets"]
],
}
for key in ("spread_group", "spread_label", "spread_columns"):
if key in section:
result[key] = section[key]
return result
def tree_item(
effect_ref: str,
criterion_ref: str,
display: str,
tree_id: str,
text: str,
) -> dict[str, Any]:
return {
"effect_ref": effect_ref,
"display": display,
"source": "LOCAL",
"official": "RTOS",
"local": display.rsplit(".", 1)[-1],
"kind": "EK" if ".TECH." in tree_id else "EN",
"tree_id": tree_id,
"text": text,
"kw": [{
"criterion_ref": criterion_ref,
"display": display.replace("EN ", "KW ").replace("EK ", "KW "),
"source": "LOCAL",
"kind": "KW",
"official": "RTOS",
"local": display.rsplit(".", 1)[-1],
"text": "Łączy kod, diagram i zweryfikowany dowód.",
}],
}
def build(repo: Path, spec: dict[str, Any]) -> dict[str, Any]:
card = spec["card"]
number = card["number"]
prefix = f"FC{number}"
repo_name = repo.name
card_id = f"mpabi-freertos-c-{number}-{card['slug']}"
card_uuid = stable_uuid(f"https://stem.local/cards/{card_id}")
task_uuid = stable_uuid(f"https://stem.local/cards/{card_id}/task01")
task_label = f"Task01 · {spec['task']['short_label']}"
revision_date = card.get("revision_date", "2026-07-19T00:00:00+02:00")
source = repo / spec["artifact"]["source"]
elf = repo / spec["artifact"]["elf"]
image = repo / spec["artifact"]["image"]
viewpoints = []
targets = {section["id"]: section["assets"][0]["label"] for section in spec["sections"]}
for viewpoint in spec["viewpoints"]:
item = {
"id": viewpoint["id"],
"label": viewpoint["label"],
"subtitle": viewpoint.get("subtitle", ""),
"status": viewpoint["status"],
}
if viewpoint["status"] == "enabled":
item["target_label"] = targets[viewpoint["id"]]
else:
item["reason"] = viewpoint["reason"]
viewpoints.append(item)
criterion_ref = f"{prefix}.KW01"
en_ref = f"{prefix}.EN01"
ek_ref = f"{prefix}.EK01"
requirement_ref = f"{prefix}.WE01"
sections = [
section_value(prefix, task_label, section)
for section in spec["sections"]
]
sections.append({
"title": f"Task01 — {spec['task']['short_label']}",
"order": 90,
"content_kind": "tasks",
"task_refs": ["task01"],
})
return {
"$schema": "../../../tools/card-layouts/schemas/card-source.schema.json",
"schema": "esc-card-source.v1",
"card": {
"id": card_id,
"series": "freertos-c",
"series_title": "FreeRTOS C",
"number": number,
"count": "15",
"slug": card["slug"],
"title": card["title"],
"topic": card["topic"],
"project": "Freestanding C nad FreeRTOS",
"subject": "Informatyka",
"level": f"Rok 2 · L{number} · RV32I/Hazard3",
"revision_date": revision_date,
"status": card.get("status", "W przygotowaniu"),
"version": card.get("version", "v00.01"),
"uuid": card_uuid,
"author": "M. Pabiszczak",
"year": "2026",
},
"generated": {
"tex": "doc/generated/main.tex",
"html": "web/index.html",
"html_css": "web/style.css",
"html_tree_inspector": False,
"react_app": True,
},
"render_dictionary": False,
"viewpoints": viewpoints,
"debug_strategies": {
item["id"]: strategy(item)
for item in spec["strategies"]
},
"template": "templates/karta-klasyczna.json",
"title_block": {
"category": "KARTA PRACY · INFORMATYKA",
"prepared_by": "M. Pabiszczak",
"prepared_on": revision_date,
"title": card["title"],
"url": f"https://{PUBLICATION_HOST_UUID}.{PUBLICATION_DOMAIN}/{task_uuid}",
"repository_url": f"{GITEA_BASE_URL}/{GITEA_SOURCE_ORG}/{repo_name}",
"url_host_uuid": PUBLICATION_HOST_UUID,
"url_domain": PUBLICATION_DOMAIN,
"doc_uuid": task_uuid,
"revision": card.get("version", "v00.01"),
"issued_on": revision_date,
"series": f"FREERTOS-C-{number}",
"document_type": "karta pracy",
"tool": "card-layouts",
"show_qr": True,
"show_repository_qr": True,
"height_cm": 2.6,
"repeat_on_every_page": True,
"replace_front_matter": True,
},
"front_page_scope": {
"title": "Cel karty",
"content_tex": spec["front"]["goal"],
"scope_title": "Zakres karty",
"scope_content_tex": spec["front"]["scope"],
"scope_table": {
"headers": ["Lekcja", "Task", "Najważniejsza idea", "Priorytet", "Status", "Version"],
"rows": [{
"chapter": prefix,
"task": "Task01",
"idea_tex": card["topic"],
"priority": "główny",
"status": "ready",
"version": card.get("version", "v00.01"),
"key": True,
}],
},
},
"side_margin_tree_layout": {
"columns": [
{"id": "zawodowe", "label": "TECH", "side": "left", "tree": "WE -> EK -> KW", "description": "Dowody runtime."},
{"id": "ogolne", "label": "OG", "side": "right", "tree": "WE -> EN -> KW", "description": "Model FreeRTOS C."},
],
},
"learning_effects": {
en_ref: {
"bloom_level": "Analiza",
"label": spec["learning"]["model_label"],
"text": spec["learning"]["model"],
"assessment_criteria": [criterion_ref],
},
ek_ref: {
"bloom_level": "Zastosowanie",
"label": spec["learning"]["replay_label"],
"text": spec["learning"]["replay"],
"assessment_criteria": [criterion_ref],
},
},
"assessment_criteria": {
criterion_ref: {
"text": spec["learning"]["criterion"],
"learning_effects": [en_ref, ek_ref],
},
},
"educational_requirements": {
requirement_ref: {
"text": spec["learning"]["requirement"],
"label": card["topic"],
"learning_effects": [en_ref, ek_ref],
"learning_tree": {
"schema": "we-learning-tree.v1",
"policy": "Każda aktywna kotwica wskazuje kod lub checkpoint.",
"ogolne": [tree_item(
en_ref,
criterion_ref,
f"EN LOCAL RTOS.{number}",
f"{requirement_ref}.OG.LOCAL.RTOS.{number}",
spec["learning"]["model"],
)],
"zawodowe": [tree_item(
ek_ref,
criterion_ref,
f"EK LOCAL RTOS.{number}",
f"{requirement_ref}.TECH.LOCAL.RTOS.{number}",
spec["learning"]["replay"],
)],
},
},
},
"debug_checkpoints": {
"schema": "stem-debug-checkpoints.v1",
"semantics": "deterministic-replay",
"artifact": {
"source": spec["artifact"]["source"],
"source_git_blob": git_blob(source),
"elf": spec["artifact"]["elf"],
"hazard3_elf_sha256": file_sha256(elf),
"hazard3_image": spec["artifact"]["image"],
"hazard3_image_sha256": file_sha256(image),
},
"targets": {
"hazard3-sim": {
"adapter": "hazard3-reset-load-replay.v1",
"baseline": "restart-backend",
"clean_ram": True,
},
},
"items": spec["checkpoints"],
},
"sections": sections,
"tasks": {
"task01": {
"title": spec["task"]["title"],
"uuid": task_uuid,
"prompt_tex": spec["task"]["prompt_tex"],
"criterion": spec["learning"]["criterion"],
"conclusion_tex": spec["task"]["conclusion_tex"],
"flow": spec["task"]["flow"],
"educational_requirement_refs": [requirement_ref],
"learning_effect_refs": [en_ref, ek_ref],
"assessment_criterion_ref": criterion_ref,
},
},
"tasks_order": ["task01"],
}
def validate_references(card_source: dict[str, Any], repo: Path) -> None:
checkpoints = set(card_source["debug_checkpoints"]["items"])
strategies = set(card_source["debug_strategies"])
for section in card_source["sections"]:
for asset in section.get("assets", []):
for phase in asset["interactive"]["phases"]:
for step in phase["steps"]:
if step["strategy_ref"] not in strategies:
raise ValueError(f"missing strategy: {step['strategy_ref']}")
code_ref = step.get("code_ref")
if code_ref:
file_name, line_text = code_ref.rsplit(":", 1)
file_path = repo / file_name
if not file_path.is_file():
raise ValueError(f"missing code_ref file: {code_ref}")
line = int(line_text)
line_count = sum(1 for _ in file_path.open(encoding="utf-8", errors="ignore"))
if line < 1 or line > line_count:
raise ValueError(f"invalid code_ref line: {code_ref}")
if step.get("mode") == "RUN":
snapshot = step.get("snapshot_ref")
if snapshot not in checkpoints:
raise ValueError(f"missing checkpoint: {snapshot}")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("repo", type=Path)
parser.add_argument("--spec", default="json/card_spec.json")
args = parser.parse_args()
repo = args.repo.resolve()
spec = load_json(repo / args.spec)
card_source = build(repo, spec)
validate_references(card_source, repo)
output = repo / "json/card_source.json"
output.parent.mkdir(parents=True, exist_ok=True)
with output.open("w", encoding="utf-8") as stream:
json.dump(card_source, stream, ensure_ascii=False, indent=2)
stream.write("\n")
print(output)
if __name__ == "__main__":
main()