feat: publish A1-A8 card viewer and FreeRTOS C generator
Check card layouts / check (push) Has been cancelled
Check card layouts / check (push) Has been cancelled
This commit is contained in:
Executable
+419
@@ -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()
|
||||
+881
-34
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user