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
+56 -4
View File
@@ -45,6 +45,17 @@ której powstaje `https://<url_host_uuid>/<doc_uuid>`.
`show_repository_qr: true` wymaga `repository_url`. Adres podglądu `localhost` `show_repository_qr: true` wymaga `repository_url`. Adres podglądu `localhost`
nie powinien być publikowany na karcie. nie powinien być publikowany na karcie.
### Publikacja lokalna i globalna
UUID hosta/workspace oraz UUID karty są stabilną tożsamością zasobu, a nie
tożsamością konkretnego serwera. Viewer podczas zajęć otwiera kartę przez
względny adres lokalnego ingressu K3s; kanoniczny adres globalny pozostaje w
metadanych, QR i udostępnianych odsyłaczach. Utrata Internetu nie może blokować
katalogu, przełączania kart, API sterowania, symulatora, dowodów ani raportu.
Pełny kontrakt topologii, routingu, magazynowania i przyszłej synchronizacji
opisuje [OFFLINE-FIRST-DEPLOYMENT.md](OFFLINE-FIRST-DEPLOYMENT.md).
### Status i wersja tasków w zakresie karty ### Status i wersja tasków w zakresie karty
Wiersze `front_page_scope.scope_table.rows[]` mogą opisywać stan opracowania Wiersze `front_page_scope.scope_table.rows[]` mogą opisywać stan opracowania
@@ -180,10 +191,51 @@ rozmiaru:
Jeśli każda figura ma odpowiadać osobnej stronie wydruku, sekcja deklaruje Jeśli każda figura ma odpowiadać osobnej stronie wydruku, sekcja deklaruje
`"asset_page_mode": "one-per-page"`. Generator wtedy wstawia jawne podziały `"asset_page_mode": "one-per-page"`. Generator wtedy wstawia jawne podziały
stron w TeX i buduje osobne płótna A4 w React/HTML. Pierwsza strona zachowuje stron w TeX i buduje osobne płótna A4 w React/HTML. Diagram interaktywny sam
nagłówek sekcji, a kolejne są stronami kontynuacji z powtórzoną tabliczką renderuje zwarty, jednakowy nagłówek na każdej części; zewnętrzny nagłówek
zasobu. Widok ekranowy rozdziela płótna odstępem `5cm`; reguła nie wpływa na sekcji nie przesuwa pierwszego kafla względem pozostałych.
wydruk.
Orientację wybiera sekcja przez `"page_orientation": "portrait"` albo
`"landscape"`. Jest to własność fizycznej strony, a nie zamiennik dla
`page_grid`: pojedynczy szeroki diagram może dostać landscape, pionowa
hierarchia portrait, a dopiero model nieczytelny po właściwym dobraniu
orientacji otrzymuje jawne złożenie kilku stron.
Diagram większy od bezpiecznego pola A4 deklaruje `page_grid` przy assetcie:
```json
"page_grid": {
"columns": 2,
"rows": 2,
"cuts_x": [660],
"cuts_y": [440],
"page_width": 660,
"page_height": 760,
"step_tiles": {"class-base": 1, "runtime-proof": 4}
}
```
`cuts_x` i `cuts_y` są współrzędnymi SVG, a `step_tiles` przypisuje każdy krok
interaktywny dokładnie do jednego fizycznego kafla (numeracja wierszowa od 1).
Generator odrzuca kafel większy od pola A4 oraz szew przecinający zamknięty
blok UML. React może złożyć strony w `spread_group`, zachowując jeden kursor i
jedną numerację rysunku; wydruk nadal zawiera niezależne kartki A4.
Włączony spread jest automatycznie dopasowywany do szerokości widoku, tak aby
wszystkie strony jednego wiersza i ich łączenia były widoczne równocześnie.
Nagłówki stron pokazują pulsujące kierunki kontynuacji `← ↑ ↓ →`; środkowa
strona szeregu może wskazywać obie strony, a złożenie wielowierszowe także górę
i dół. Wskaźniki są wyłącznie nawigacją ekranową i znikają w druku.
### Boczne szyny paneli
Narzędzia pomocnicze HTML używają wspólnego systemu paneli bocznych zamiast
modalnej zasłony. Cienka kolorowa szyna otwiera panel w trybie `Auto` po
najechaniu; kliknięcie przełącza go w `Fixed`. Kolejny klik, przycisk zamknięcia
albo `Esc` odpina panel. Renderer może wystawić kilka niezależnych szyn po obu
stronach (obecnie: katalog i spis treści po lewej, przebieg i raport po prawej).
Panele nakładają się na widok tylko podczas pracy i są zawsze usuwane z
wydruku. Nie zmieniają geometrii stron A4 ani kafelków diagramu.
Końcowy słownik jest domyślnie obecny w React/HTML dla zgodności. Karta, która Końcowy słownik jest domyślnie obecny w React/HTML dla zgodności. Karta, która
ma zawierać wyłącznie jawnie wymienione strony, ustawia na poziomie głównym ma zawierać wyłącznie jawnie wymienione strony, ustawia na poziomie głównym
+164
View File
@@ -0,0 +1,164 @@
# Offline-first local and global deployment
## Architecture decision
The teaching application must remain fully operational when the Internet
connection is unavailable. The local K3s cluster is therefore the primary
runtime during a lesson. The global installation is a replica, publication
endpoint and later synchronization peer; it is not a runtime dependency of a
lesson.
The development server on `http://localhost:8080` represents the local ingress
contract. Browser code must use relative, same-origin application and API URLs
so that the same generated bundle can run behind the development server, local
K3s ingress or the global ingress without rebuilding the card.
## Stable identity is not a transport address
A resource is identified by stable values:
```text
workspace/site UUID + card UUID + version + content digest
```
These values must not change when the resource moves between local and global
deployments. A URL is only one endpoint resolving that identity.
The target catalogue contract is:
```json
{
"resource": {
"site_uuid": "<uuid-1>",
"card_uuid": "<uuid-2>",
"version": "v00.04",
"digest": "sha256:..."
},
"endpoints": {
"local": "/cards/<uuid-1>/<uuid-2>/",
"canonical": "https://<public-origin>/<uuid-1>/<uuid-2>/"
}
}
```
The internal card tree navigates to `endpoints.local`. The canonical endpoint
is retained for publication metadata, QR codes, external sharing and global
synchronization. It must not be the only way to open another card from a local
viewer.
An installation may additionally use split-horizon DNS so the canonical host
resolves to the local ingress on the school network and to the global ingress
outside it. Correctness must not depend on split DNS, however: the relative
local route remains the explicit offline path.
## Local K3s runtime
The local cluster owns every service required during a lesson:
```text
browser
-> local DNS / K3s ingress
-> card catalogue and resource resolver
-> generated HTML, SVG, TeX and downloadable artefacts
-> card control API and navigation state
-> Neovim / simulator bridge
-> progress, event outbox and evidence storage
-> local Gitea and container registry where the lesson needs them
```
No lesson-critical asset may be loaded at runtime from a CDN, public font
service, GitHub, a public package registry or the global STEM instance.
Dependencies, container images and generated card releases are synchronized to
the local cluster before the lesson.
Recommended K3s responsibilities are:
- Traefik or an equivalent ingress as the single browser entry point;
- a catalogue/resolver mapping resource UUIDs to local immutable releases;
- local object storage for generated artefacts, screenshots and reports;
- a durable database for catalogue, progress and synchronization metadata;
- persistent volumes for card state, evidence and the event outbox;
- local DNS and pre-provisioned TLS material;
- a local OCI registry and Gitea mirror for reproducible laboratory work.
A browser service worker may cache the application shell and the last opened
cards, but it is an additional resilience layer. It does not replace the local
server, control API or durable K3s storage.
## Global runtime
The global deployment exposes the same resource identity and compatible API
schemas. It provides:
- externally reachable published cards and QR targets;
- long-term storage of accepted reports and evidence;
- upstream card releases and catalogue metadata;
- cross-site administration and backup;
- the synchronization peer for one or more local teaching sites.
Local and global installations may have different origins. Generated cards
must therefore contain no environment-specific API origin in JavaScript.
## Navigation contract
Clicking a card in the tree follows this policy:
1. resolve its stable resource identity in the local catalogue;
2. open the relative local route on the current origin;
3. use a locally cached immutable release when the active release is already
present but the catalogue backend is temporarily restarting;
4. offer the canonical global endpoint only as an explicit fallback when a
network is available;
5. show a clear `PLAN / no release` state when no local or global release
exists—never silently open an unrelated card.
Changing cards must not require starting another development HTTP server or
changing ports. One ingress serves the catalogue and routes all card releases.
## Synchronization boundary
Synchronization is deliberately separate from lesson execution. Its data model
must be designed now even if the transport is implemented later.
Card releases are immutable and content-addressed. They normally synchronize
from global to local. Classroom work synchronizes from local to global through
an append-only outbox containing globally unique, idempotent events:
```text
event_id, site_id, actor_id, resource identity, local sequence,
recorded_at, payload schema, payload digest
```
Progress changes, navigation checkpoints, screenshots, report annotations and
final report submissions are separate event types. Retrying a transfer cannot
duplicate an event. Binary evidence is uploaded by digest and referenced from
events.
Conflict rules are explicit:
- immutable card releases never merge; a new digest is a new release;
- append-only evidence never overwrites another site event;
- mutable progress uses its event history and actor/site identity rather than
blind last-write-wins;
- catalogue activation is an administrative decision with a recorded base
revision;
- rejected or incompatible events remain in a visible quarantine queue.
The local installation displays synchronization health but never blocks a
lesson because the global peer is offline.
## Offline acceptance test
A release is ready for classroom use only when the following test passes:
1. preload the selected card series, images and containers into local K3s;
2. disconnect the Internet uplink while preserving the classroom LAN;
3. open the catalogue through the local ingress;
4. switch between all released cards without leaving the local origin;
5. run Neovim, simulator checkpoints and diagram navigation;
6. record progress, timestamps, screenshots and a report;
7. restart the browser and one application pod and recover the saved state;
8. reconnect the uplink and verify idempotent synchronization without data
loss or duplicated evidence.
Passing only an online browser test is not sufficient.
+725
View File
@@ -0,0 +1,725 @@
# A1A8 Interactive Architecture Viewpoints
Status: design contract, version 0.2, 2026-07-18.
This document defines how STEM cards describe architecture, code and runtime
evidence. It is a house convention, not a new UML standard. The convention is
organized in the spirit of ISO/IEC/IEEE 42010:2022, uses UML 2.5.1 notation,
adds selected MARTE 1.3 concepts for real-time systems, and uses PlantUML as
the textual source of rendered diagrams.
## Scope and rollout
A1A8 is runtime-independent. The fixed viewpoints are intended to support
embedded RTOS software, ordinary C++ object models, POSIX or standard threads,
and asynchronous Boost.Asio applications without changing the navigation
grammar.
The rollout is deliberately staged:
1. **current application:** FreeRTOS C++ cards on RV32I/Hazard3;
2. **later application:** `std::thread`/`std::jthread` and Boost.Asio;
3. **not yet scheduled:** concrete Asio/thread card repositories and lesson
numbering.
The later profile is specified below so current diagrams do not bake in RTOS-
only assumptions. It is a compatibility target, not a claim that the current
FreeRTOS card already implements or teaches Asio.
## Standards boundary
- **ISO/IEC/IEEE 42010:2022** supplies the concepts of architecture
descriptions, viewpoints, views and model kinds. A1A8 is our concrete set
of viewpoints. We do not claim full ISO conformance merely because a card
uses these labels.
- **UML 2.5.1** supplies the notation for classes, objects, components,
interactions, activities and state machines. It does not prescribe A1A8.
- **MARTE 1.3** may annotate schedulable resources, time, hardware and
allocation in RTOS cards. A MARTE property must come from configuration,
code or measurement; authors must not invent period, deadline or execution
time merely to fill a box.
- **arc42** is a useful wider documentation template. A1A8 is the smaller,
interactive teaching slice used inside a card, not a replacement for an
architecture description of the whole product.
- **PlantUML** is the text-first renderer. The `.puml` source, not a manually
edited SVG, is authoritative.
Primary references:
- <https://www.iso.org/standard/74393.html>
- <https://www.omg.org/spec/UML/2.5.1>
- <https://www.omg.org/spec/MARTE/1.3>
- <https://arc42.org/overview>
- <https://plantuml.com/class-diagram>
- <https://plantuml.com/sequence-diagram>
- <https://plantuml.com/ascii-art>
## The fixed navigation model
The tab names remain stable across cards. A subtitle identifies the actual
mechanism being taught.
```text
A1 CONTEXT
A2 STRUCTURE
A3 DISPATCH
A4 APPLICATION
A5 FLOW
A6 STATE
A7 RUNTIME
A8 PATTERNS
```
These eight tabs ask eight different questions:
| ID | Question | Typical model kind |
|---|---|---|
| A1 CONTEXT | Where is the subject and what lies outside its boundary? | component, package or deployment view |
| A2 STRUCTURE | What static elements and relationships construct it? | class, component or object structure |
| A3 DISPATCH | How is an abstract call bound to the actual implementation? | class plus sequence/call-path view |
| A4 APPLICATION | How does application code specialize or compose it? | application class/object view |
| A5 FLOW | What happens in what order? | sequence or activity view |
| A6 STATE | Which lifecycles and state transitions exist? | state machine, never an unlabeled flowchart |
| A7 RUNTIME | What actually exists in memory and in the debugger now? | object diagram plus evidence overlay |
| A8 PATTERNS | Which design roles and trade-offs explain the construction? | participant map plus decision record |
Not every card needs all eight views. The top-level navigation keeps all eight
positions stable; an omitted view is visibly unavailable and states why. A
small card should not create decorative diagrams to fill empty tabs.
## Viewpoint contracts
### A1 CONTEXT
**Purpose:** establish system boundary and dependency direction before class
details appear.
Required content:
- subject under study;
- upstream application actor or component;
- wrapped platform/API;
- runtime platform, hardware or simulator when relevant;
- one verb on every boundary-crossing relation.
For an RTOS wrapper this normally reads:
```text
Application task
│ specializes / uses
C++ wrapper
│ calls
FreeRTOS C API
│ schedules on
Hazard3 simulator
```
A1 must not list every method. It is a map, not a class reference.
### A2 STRUCTURE
**Purpose:** show the static construction that the compiler sees.
Required content:
- class/interface/component names;
- relevant fields and operations only;
- visibility where it is part of the lesson;
- precise relationship type: generalization, realization, composition,
aggregation or dependency;
- storage/ownership role when the structure owns a resource.
Class rows receive semantic anchors only when a card can connect them to a
source line or a compile-time proof. Inheritance, `friend`, templates and
visibility are CODE facts; they are never presented as runtime events.
### A3 DISPATCH
**Purpose:** explain the binding mechanism between the generic boundary and
the concrete implementation.
The title is fixed; the subtitle is mechanism-specific. Examples:
```text
A3 DISPATCH · CRTP static dispatch
A3 DISPATCH · virtual dispatch through vtable
A3 DISPATCH · C callback and function pointer
A3 DISPATCH · event to subscriber
A3 DISPATCH · executor and completion handler
```
The diagram must identify:
1. the call origin;
2. the erased or abstract boundary;
3. the cast, lookup or binding operation;
4. the concrete target;
5. whether the decision is compile-time or runtime;
6. the ABI proof that distinguishes the mechanism.
This viewpoint prevents a wrapper from being mislabeled. A CRTP wrapper must
show `static_cast<Derived*>` and the absence of virtual dispatch. A dynamic
wrapper must show object address, `vptr`, vtable entry and indirect call.
### A4 APPLICATION
**Purpose:** show one concrete use without repeating the complete library.
The generic mechanism is collapsed to its public contract. The view maps every
pattern or framework role to the names used in the exercise, for example
`Strategy -> PaymentStrategy` and `Context -> ShoppingCart`.
At least one application invariant is printed next to the model. Example:
"`ShoppingCart` depends on the strategy interface, not a payment variant."
### A5 FLOW
**Purpose:** show causal order for one named scenario.
Rules:
- state the precondition and observable postcondition;
- use sequence diagrams for collaborating participants and activity diagrams
for algorithmic branching;
- number messages in the same order as interactive steps;
- show returns only when they carry teaching information;
- split construction, scheduling and callback dispatch into phases when one
page would hide the boundary.
A5 is the primary home of RUN steps and deterministic checkpoints.
### A6 STATE
**Purpose:** separate lifecycle from execution flow.
When two state domains exist, draw two explicit lanes rather than merging them.
For a FreeRTOS C++ task these are commonly:
```text
C++ object lifetime: absent -> constructed -> alive -> destroyed
Kernel task state: Ready <-> Running <-> Blocked; Suspended; Deleted
```
Every transition has a trigger, guard or event. Values returned by platform
APIs are platform states, not invented wrapper states. A wrapper-specific state
machine is allowed only when the code really stores and enforces it.
### A7 RUNTIME
**Purpose:** connect the model to a reproducible observation.
A7 is deliberately a hybrid, not pure UML. It combines an object diagram with
selected debugger facts:
- object and subobject addresses;
- `this`, callback context and handles;
- fields, ownership pointers, TCB and stack interval;
- PC, SP and relevant argument registers;
- vptr/vtable target for dynamic dispatch;
- symbol/absence-of-symbol proof for static dispatch;
- timestamp, target, ELF revision and checkpoint identity.
Every value is either captured from a deterministic run or marked as a
symbolic example. A screenshot without target, source revision and timestamp is
illustration, not evidence.
### A8 PATTERNS
**Purpose:** explain why the design has its shape and what it costs.
Each claimed pattern records:
- intent and problem;
- forces/constraints;
- canonical participant roles;
- exact mapping from role to project class/function;
- collaboration that makes the pattern real;
- invariant or test;
- benefit and cost;
- rejected alternative;
- conditions in which the pattern should not be used.
Pattern names are not decorative stereotypes. `Facade` is valid only when a
simpler boundary to a subsystem can be identified. `RAII` is valid only when
acquisition and release are safely paired for every legal lifetime. A callback
bridge may be an Adapter without making the whole wrapper an Adapter.
## Authored viewpoint record
The future machine-readable form should preserve this conceptual record. This
is a design target, not yet a required `card_source.json` schema extension:
```json
{
"id": "a3-dispatch",
"number": "A3",
"label": "DISPATCH",
"subtitle": "Dynamic polymorphism through vtable",
"purpose": "Trace the C callback to the concrete C++ override.",
"stakeholders": ["student", "teacher", "embedded developer"],
"concerns": ["C-to-C++ bridge", "void* context", "vtable", "ABI"],
"model_kind": "uml-sequence",
"source": "doc/assets/a3-dispatch.puml",
"fallback_text": "doc/assets/a3-dispatch.txt",
"spread_group": "task-dispatch",
"steps": [
{
"id": "dispatch-vptr",
"number": 4,
"mode": "RUN",
"svg_target": "dispatch-vptr",
"code_ref": "include/FreeRTOS/Task.hpp:110",
"snapshot_ref": "task.dispatch.vptr",
"evidence": "a0, object address, vptr, vtable entry and jalr target"
}
]
}
```
Required semantics:
- `id`, `number` and `label` are stable teaching identity;
- `subtitle` names the actual mechanism and may differ between cards;
- `source` is the text source of the rendered asset;
- `fallback_text` is a compact readable map, not generated alt text;
- `code_ref` is repository-relative and validated;
- CODE steps may have no snapshot and never reset the program;
- RUN steps require a deterministic replay target;
- `evidence` says what proves the claim, not merely what should be visible;
- `spread_group` joins logical continuations without changing A4 print pages.
## Plain technical rendering profile
"Plain text UML" means two related things in this project:
1. diagrams are authored as reviewable text;
2. their rendered form resembles a restrained technical drawing rather than a
presentation slide.
The default profile therefore uses:
- white background, black/charcoal strokes and no shadows;
- square or nearly square corners;
- one monospaced family for identifiers, fields, methods and evidence;
- one sans-serif family only for explanatory prose;
- orthogonal relations where the renderer can keep labels legible;
- left-aligned members and short, explicit verbs on relations;
- no gradients, icons, ornamental colors or oversized titles;
- line weight and spacing, not color, as the primary print hierarchy;
- visible step anchors such as `01``08` that remain useful in SVG and print;
- two muted selectable tones in HTML and one subtle active outline;
- non-selectable content in neutral gray, never confused with a selectable
inactive element.
PlantUML can generate ASCII/Unicode text output for sequence diagrams. We may
ship that output as the fallback for sequences. Other diagram kinds receive a
small hand-maintained text map because PlantUML's ASCII renderer does not cover
all UML kinds. The fallback communicates topology and reading order; SVG and
TeX remain the visual outputs.
## Orientation, type scale and page visibility
The default reading direction is vertical. A horizontal layout is reserved for
a genuine sequence or timeline whose primary fact is execution order over
time. A binding chain is vertical unless time or participant lifelines are
being modelled explicitly.
| Viewpoint | Default orientation | Reason |
|---|---|---|
| A1 CONTEXT | vertical | responsibility descends from application to platform |
| A2 STRUCTURE | vertical | base types, specializations and dependencies form tiers |
| A3 DISPATCH | vertical | binding stages form a top-to-bottom chain; use horizontal only for a genuine sequence |
| A4 APPLICATION | vertical | application objects lead to observers and invariants |
| A5 FLOW | horizontal | messages and time advance in reading order |
| A6 STATE | vertical | lifecycle transitions form explicit top-to-bottom paths |
| A7 RUNTIME | geometry-dependent | use vertical for an ownership descent, horizontal for a memory/CPU evidence pipeline |
| A8 PATTERNS | geometry-dependent | use vertical for tiers and horizontal for a short chain of roles and consequences |
Use `top to bottom direction` unless the diagram is a genuine sequence or
timeline (normally A5). A diagram must not be made horizontal merely to fill
page width.
Physical page orientation follows the rendered model rather than the viewpoint
name: use one portrait page, one landscape page, or an explicit semantic page
composition. Prefer a single page after a deliberate layout change; introduce
tiles only when readable labels and complete semantic blocks still cannot fit.
The default PlantUML type scale is `14` for ordinary labels, `15` for class
names and `13` for class attributes. A smaller value is allowed only when the
diagram remains readable at the normal A4 viewer scale. Prefer fewer words,
shorter relation labels and a second logical page over shrinking text.
All diagrams belonging to one card use one shared PlantUML style include. The
style is print-first and keeps the following invariants:
- one monospaced family for nodes, relations and sequence messages;
- a `190 px` minimum class width, while content may make a node wider;
- `1.5 px` node borders and `1.3 px` arrows;
- a near-white node fill, blue-grey borders and dark text with sufficient
monochrome-print contrast;
- the same base styling for class, state and sequence diagrams;
- no shadows, decorative gradients or per-diagram colour inventions.
Interactive selection is an overlay owned by the viewer. The source diagram
must remain fully legible with no selection, while selectable nodes receive a
clearer idle treatment than non-interactive context nodes. Selection changes
emphasis, not geometry: it must not resize a node, move a label or alter the
diagram layout.
At `content scale = 100%`, the HTML viewer preserves the intrinsic PlantUML
scale. A diagram that exceeds the declared A4-safe viewport is split into
semantic tiles; it is not silently compressed to `width: 100%`. The joined
HTML spread reconstructs the logical page in a CSS grid, while ordinary view
and print retain separate physical A4 sheets. Canvas zoom remains an explicit
viewer action and may be used to inspect the joined overview.
Every diagram starts on a new A4 page in HTML and TeX. Its SVG must fit within
the page-safe margins with all labels, arrowheads and numbered anchors visible.
Long diagrams continue on a new page with stable global step numbers and may be
joined by the HTML spread view. Every physical tile remains legible on its own,
but its `part`, row and column identify its exact position in the logical model.
## Large diagrams and page continuity
A viewpoint is one logical model even when it needs several A4 pages.
- `page_grid` declares columns, rows and explicit semantic `cuts_x`/`cuts_y`;
- a cut may cross whitespace or a connector, but never a class, object, state,
participant, note, numbered anchor or other closed semantic block;
- the generator validates the rendered SVG geometry and rejects a colliding
seam instead of shrinking the diagram or accepting a broken print;
- every interactive step belongs to exactly one tile through `step_tiles`;
- all tiles use one logical figure number and one global navigation cursor;
- each page carries `A<n>`, the same viewpoint title, `part x/y`, and a shared
`spread_group`;
- duplicated participants at a page boundary are marked `continued`, not
treated as new objects;
- step numbers remain global across parts;
- connectors leaving a page use a named continuation token, for example
`D3 -> A5.2`;
- HTML can join the parts side by side; print keeps independent A4 pages;
- the joined HTML view fits the complete logical row into the viewport, keeps
page tops aligned and centers the semantic join rather than either sheet;
- a screen-only continuation cue in every part header indicates available
neighbours to the left, right, above and below;
- selecting a step scrolls to the correct part and preserves one cursor;
- no information is encoded only in the physical proximity of two pages.
The source diagram may use a modest, explicit PlantUML `scale` when that is
necessary to place a semantic seam inside the A4-safe viewport. This is a
design decision, not automatic fit-to-width. The resulting normal print must
still meet the shared type-scale readability gate; HTML hover/canvas zoom is a
convenience and never a substitute for readable print.
## Interaction and correspondence rules
The diagram, source tree and debugger share one navigation identity:
```text
SERIES -> CARD -> TASK -> BLOCK -> VIEWPOINT/PHASE -> STEP -> SNAPSHOT
```
For every selectable anchor:
- its visible step number occurs exactly once inside that diagram;
- click and keyboard navigation select the same step;
- CODE opens the exact code or binary proof and does not mutate runtime;
- RUN opens code and may replay only after explicit activation;
- compile-time relations use code, `nm`, `readelf` or disassembly evidence;
- runtime messages use checkpoint evidence;
- focus is a subtle overlay; unselected selectable targets stay visible;
- a missing `snapshot_ref` is valid for CODE and invalid for RUN;
- the authored SVG target is validated after PlantUML rendering.
## Diagram-first teaching contract
The diagram is the primary entry point into an example. A printed card must
teach the algorithm, responsibility boundary, state transition or dispatch
mechanism before it exposes implementation syntax. Complete source listings
do not belong on the card. The card carries the repository URL/QR code, while
an interactive CODE anchor opens only the relevant symbol or source range in
Neovim.
The intended learning path is:
```text
goal and scope
-> relevant A1-A8 viewpoint
-> numbered diagram element
-> CODE source anchor or RUN checkpoint
-> Neovim / Termdebug / Hazard3 evidence
-> timestamped snapshot
```
Consequently:
- a card uses only the A1-A8 viewpoints which materially explain the subject;
- the diagram explains the mechanism, source confirms its implementation and
the debugger supplies evidence;
- CODE navigation does not restart or mutate the program;
- RUN navigation may replay only to a declared deterministic checkpoint;
- source excerpts may appear temporarily in the interactive Neovim view, but
are not duplicated as full static listings in HTML, TeX or print;
- every source jump is attached to a concrete diagram element, relation,
message or state rather than to an unrelated code catalogue;
- the repository link remains the canonical route to the complete source.
This order is deliberate: students first reason about algorithms, states and
responsibilities, then inspect the C or C++ syntax that implements them.
## Editable UML layout and JSON persistence
Interactive UML views may expose an explicit `EDIT` mode. It applies only to
UML/model assets; it must not turn the whole card page into a free-form editor
or interfere with normal CODE/RUN navigation.
PlantUML remains the authoritative semantic source for participants,
relationships and diagram kind. Manual visual decisions are stored in a
sidecar JSON document named after the source, for example:
```text
doc/assets/a2-structure.puml
doc/assets/a2-structure.layout.json
```
Saving a layout persists the complete authored presentation state, not only a
temporary browser transform. At minimum the sidecar records:
- diagram identity, source revision and layout schema version;
- canvas size, page orientation, scale and optional page-grid metadata;
- stable element identity (`id`/`svg_target`), position and stacking order;
- width, height, local scale and rotation when the model kind allows it;
- displayed name, stereotype, short description and member text overrides;
- font family, font size, weight, style, line height and text alignment;
- fill, stroke, border width, corner treatment and semantic style role;
- group membership, collapsed state and explicit grouping/ungrouping;
- relation endpoints, labels and manually placed routing points;
- numbered CODE/RUN anchor placement without changing its teaching identity;
- editor metadata such as update timestamp and optional author.
A representative record is:
```json
{
"schema": "stem.uml-layout/v1",
"diagram_id": "task01.a2-structure",
"source": "a2-structure.puml",
"source_digest": "sha256:...",
"canvas": {
"width": 1120,
"height": 760,
"orientation": "landscape",
"scale": 1
},
"elements": {
"task-base": {
"x": 310,
"y": 72,
"width": 360,
"height": 290,
"z": 2,
"text": {
"title": "Task<Derived>",
"description": "Static task wrapper",
"font_family": "IBM Plex Mono",
"font_size": 14,
"font_weight": 500,
"align": "left"
},
"style": {
"role": "wrapper",
"fill": "#eef5f8",
"stroke": "#397c99",
"stroke_width": 1.5
}
}
},
"relations": {
"task-to-api": {
"from": "task-base",
"to": "freertos-api",
"label": "calls",
"points": [[490, 362], [490, 430], [690, 430]]
}
},
"updated_at": "2026-07-19T12:00:00+02:00"
}
```
Editor requirements:
- dragging, resizing, text editing, grouping and relation routing update an
in-memory draft first;
- `Save layout` validates stable IDs and writes the sidecar atomically;
- `Cancel` restores the last saved sidecar without changing PlantUML;
- `Reset layout` removes visual overrides and returns to the generated
PlantUML geometry only after explicit confirmation;
- rerendering PlantUML reapplies compatible overrides by stable ID and reports
orphaned overrides instead of silently discarding them;
- a source digest mismatch is visible to the editor but does not destroy the
saved layout;
- normal view and print consume the same saved JSON, so the authored geometry
is reproducible outside the editing session;
- CODE/RUN step IDs, source references and snapshot identities cannot be
renamed accidentally by a visual edit.
Exact manual coordinates cannot be represented reliably through PlantUML
layout hints alone. The sidecar is therefore the canonical manual presentation
layer, while `.puml` remains the canonical semantic layer.
## Two valid A3 profiles for FreeRTOS wrappers
The viewpoint set intentionally supports both of these designs.
### Current teaching wrapper in K02
```text
A2 STRUCTURE Task<Derived>, SumTask, SupervisorTask
A3 DISPATCH CRTP static dispatch
void* -> Task<Derived>* -> Derived* -> run()
A7 RUNTIME same object address; direct/inlined target; no vtable symbol
```
This matches the current K02 acceptance gate: no virtual dispatch, vtable,
RTTI or hosted runtime.
### `jonenz/FreeRTOS-Cpp` reference design
```text
A2 STRUCTURE TaskBase <- Task / StaticTask<N> <- application task
A3 DISPATCH dynamic dispatch
void* -> TaskBase* -> taskEntry() -> virtual taskFunction()
A7 RUNTIME object address, vptr, vtable entry and indirect target
```
The upstream `Task.hpp` creates dynamic/static kernel tasks in constructors,
passes `this` to `callTaskFunction(void*)`, and uses a virtual
`taskFunction()`. Its `TaskBase` destructor may call `vTaskDelete(handle)` when
configured. These are reference facts, not automatic design choices for our
teaching wrapper. Adopting that design would require changing K02's lifecycle,
ABI and no-vtable acceptance contracts explicitly.
Reference:
<https://github.com/jonenz/FreeRTOS-Cpp/blob/develop/FreeRTOS-Cpp/include/FreeRTOS/Task.hpp>
## Target profile: Boost.Asio and standard threads
This is the required future compatibility profile. It does not rename the
fixed tabs. Mechanism-specific wording belongs in subtitles:
```text
A1 CONTEXT · application, Asio, io_context, worker threads, OS
A2 STRUCTURE · Server, Session, socket, strand and ThreadPool
A3 DISPATCH · executor and completion-handler dispatch
A4 APPLICATION · one concrete Session or Client
A5 FLOW · asynchronous initiation and completion
A6 STATE · operation, session and thread lifecycles
A7 RUNTIME · threads, executors, handlers and owned buffers
A8 PATTERNS · Proactor and verified collaboration roles
```
The fixed label remains `A5 FLOW`, not `A5 ASYNC FLOW`; the UI subtitle may be
`Async completion flow`. Likewise the fixed label remains `A7 RUNTIME`, with
`Threads / executors` as a subtitle. This keeps cards comparable while still
naming the actual mechanism.
### A1A8 mapping
**A1 CONTEXT** shows this dependency/execution stack:
```text
Application Session / Server
│ initiates
Boost.Asio async operations, executors and strands
│ queue completions in
io_context
│ run() is called by
std::thread / std::jthread workers
│ use
operating-system sockets and scheduling
```
**A2 STRUCTURE** shows the static ownership and dependency graph: `Server`,
`Session`, `tcp::socket`, executor/strand, buffers, `io_context` and the worker
collection. It must make lifetime ownership explicit, especially when a
session uses `shared_from_this()` to outlive an initiating call.
**A3 DISPATCH** traces an initiating function through the outstanding
operation, associated executor, completion queue and selected handler. It must
not draw `async_read` as if it created a user thread. Completion handlers are
invoked only by threads currently running `io_context::run()`; several such
threads form a pool from which `io_context` may choose. A strand adds strictly
sequential handler invocation, not physical affinity to one thread.
**A4 APPLICATION** collapses Asio infrastructure and shows one concrete
protocol/session contract, for example `EchoSession::start/read/onRead/write`.
**A5 FLOW** uses the explicit sequence:
```text
initiate -> outstanding -> operation completes -> handler queued
-> one run() thread invokes handler -> next operation or completion
```
**A6 STATE** separates at least three domains when present:
```text
jthread object: absent -> joinable -> stop requested -> joined
async operation: initiated -> outstanding -> queued -> running -> done
application Session: Created -> Reading -> Processing -> Writing -> Closed
```
The `std::jthread` destructor requests stop and joins when the object is still
joinable. A session state is application data; it must not be inferred merely
from a handler being present in a queue.
**A7 RUNTIME** records `io_context`, worker IDs, current handler and thread,
executor/strand identity, socket descriptor, operation kind, session address,
buffer lifetime and relevant smart-pointer ownership. The evidence view uses
`info threads`, backtraces and object inspection rather than RTOS TCB fields.
**A8 PATTERNS** may identify Proactor, callback/continuation, serialized
execution through a strand, thread pool, RAII thread ownership and lifetime
management. These are classified carefully: `std::jthread` ownership is a C++
idiom, a thread pool is a concurrency architecture, and not every completion
handler constitutes a GoF Command. Boost.Asio documents its asynchronous model
as Proactor; the card must still map each claimed role to project code.
The central invariant for every future Asio card is:
```text
asynchronous operation != thread
operation completion -> associated executor -> io_context
-> one thread currently executing run()
```
Primary references for this profile:
- <https://www.boost.org/doc/libs/latest/doc/html/boost_asio/overview/core/threads.html>
- <https://www.boost.org/doc/libs/latest/doc/html/boost_asio/overview/core/strands.html>
- <https://www.boost.org/doc/libs/latest/doc/html/boost_asio/overview/core/async.html>
- <https://eel.is/c++draft/thread.thread.class>
- <https://eel.is/c++draft/thread.jthread.class>
## Definition of done for a card view
A viewpoint is ready when:
1. its purpose and question are stated;
2. the chosen UML/model kind fits that question;
3. every relation has one unambiguous meaning;
4. every selectable item has a stable anchor and code reference;
5. runtime claims have reproducible evidence;
6. compile-time claims use compiler/binary evidence rather than fake runtime;
7. the text fallback remains understandable without SVG;
8. the view prints in monochrome and works as an interactive HTML asset;
9. continuation across pages preserves identity and step order;
10. A8 names costs and rejected alternatives, not only benefits.
+2402 -70
View File
File diff suppressed because it is too large Load Diff
+1449 -165
View File
File diff suppressed because it is too large Load Diff
+193 -2
View File
@@ -31,6 +31,23 @@
"debug_checkpoints": { "debug_checkpoints": {
"$ref": "#/$defs/debugCheckpoints" "$ref": "#/$defs/debugCheckpoints"
}, },
"viewpoints": {
"type": "array",
"minItems": 1,
"maxItems": 8,
"items": {
"$ref": "#/$defs/viewpoint"
}
},
"debug_strategies": {
"type": "object",
"propertyNames": {
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"
},
"additionalProperties": {
"$ref": "#/$defs/debugStrategy"
}
},
"render_dictionary": { "render_dictionary": {
"type": "boolean", "type": "boolean",
"description": "Czy React/HTML ma dodać końcową stronę słownika WE/EN/EK/KW." "description": "Czy React/HTML ma dodać końcową stronę słownika WE/EN/EK/KW."
@@ -309,6 +326,11 @@
"type": "string", "type": "string",
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
}, },
"url_domain": {
"type": "string",
"pattern": "^[A-Za-z0-9.-]+$",
"description": "Opcjonalna domena dopinana do subdomeny UUID, np. mpabi.pl."
},
"url_port": { "type": ["string", "integer"] }, "url_port": { "type": ["string", "integer"] },
"doc_uuid": { "doc_uuid": {
"type": "string", "type": "string",
@@ -605,6 +627,27 @@
"enum": ["flow", "one-per-page"], "enum": ["flow", "one-per-page"],
"description": "Sposób paginacji assetów. one-per-page wymusza osobną kartkę A4 w HTML i PDF." "description": "Sposób paginacji assetów. one-per-page wymusza osobną kartkę A4 w HTML i PDF."
}, },
"page_orientation": {
"enum": ["portrait", "landscape"],
"default": "portrait",
"description": "Orientacja fizycznych stron tej sekcji w HTML i TeX. Dobierana do geometrii diagramu, niezależnie od page_grid."
},
"spread_group": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$",
"description": "Łączy kolejne strony assetów w opcjonalny ekranowy spread; druk nadal zachowuje osobne kartki A4."
},
"spread_label": {
"type": "string",
"minLength": 1,
"description": "Krótka nazwa logicznego diagramu pokazywana przy przełączniku spreadu."
},
"spread_columns": {
"type": "integer",
"minimum": 1,
"maximum": 12,
"description": "Liczba fizycznych stron A4 w jednym wierszu logicznego spreadu."
},
"area_tree_refs": { "area_tree_refs": {
"$ref": "#/$defs/stringList" "$ref": "#/$defs/stringList"
}, },
@@ -674,6 +717,72 @@
}, },
"interactive": { "interactive": {
"$ref": "#/$defs/interactiveAsset" "$ref": "#/$defs/interactiveAsset"
},
"page_grid": {
"$ref": "#/$defs/diagramPageGrid"
}
},
"additionalProperties": false
},
"diagramPageGrid": {
"type": "object",
"required": ["columns", "rows"],
"properties": {
"columns": {
"type": "integer",
"minimum": 1,
"maximum": 12
},
"rows": {
"type": "integer",
"minimum": 1,
"maximum": 12
},
"page_width": {
"type": "number",
"exclusiveMinimum": 0,
"default": 660,
"description": "Referencyjna szerokość bezpiecznego pola diagramu A4 w jednostkach PlantUML."
},
"page_height": {
"type": "number",
"exclusiveMinimum": 0,
"default": 760,
"description": "Referencyjna wysokość bezpiecznego pola diagramu A4 w jednostkach PlantUML."
},
"step_tiles": {
"type": "object",
"propertyNames": {
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"
},
"additionalProperties": {
"type": "integer",
"minimum": 1
},
"description": "Jawne przypisanie id kroku do numeru kafla w porządku wierszowym, liczonego od 1."
},
"cuts_x": {
"type": "array",
"items": {
"type": "number",
"exclusiveMinimum": 0
},
"uniqueItems": true,
"description": "Semantyczne pionowe linie cięcia w układzie współrzędnych SVG; nie mogą przecinać bloków ani etykiet."
},
"cuts_y": {
"type": "array",
"items": {
"type": "number",
"exclusiveMinimum": 0
},
"uniqueItems": true,
"description": "Semantyczne poziome linie cięcia w układzie współrzędnych SVG; nie mogą przecinać bloków ani etykiet."
},
"overview": {
"type": "boolean",
"default": true,
"description": "W HTML udostępnia pomniejszony podgląd całej logicznej planszy obok widoku kafli 1:1."
} }
}, },
"additionalProperties": false "additionalProperties": false
@@ -883,6 +992,11 @@
"type": "object", "type": "object",
"required": ["stop"], "required": ["stop"],
"properties": { "properties": {
"event_id": {
"type": "string",
"pattern": "^E[0-9]{2}$",
"description": "Semantic event shared by comparable cards; the snapshot id remains card-local."
},
"stop": { "stop": {
"type": "object", "type": "object",
"required": ["symbol", "offset"], "required": ["symbol", "offset"],
@@ -962,12 +1076,30 @@
}, },
"umlSequenceStep": { "umlSequenceStep": {
"type": "object", "type": "object",
"required": ["id", "number", "label", "description", "snapshot"], "required": ["id", "number", "label", "description"],
"properties": { "properties": {
"id": { "type": "string", "minLength": 1 }, "id": { "type": "string", "minLength": 1 },
"number": { "type": "integer", "minimum": 1 }, "number": { "type": "integer", "minimum": 1 },
"label": { "type": "string", "minLength": 1 }, "label": { "type": "string", "minLength": 1 },
"mode": {
"enum": ["CODE", "RUN"],
"description": "CODE opens source only; RUN opens source and may replay snapshot_ref. When omitted, steps with snapshot_ref infer RUN and code-only steps infer CODE."
},
"event_id": {
"type": "string",
"pattern": "^E[0-9]{2}$"
},
"strategy_ref": {
"type": "string",
"minLength": 1,
"description": "Reference to a structured entry in debug_strategies."
},
"description": { "type": "string", "minLength": 1 }, "description": { "type": "string", "minLength": 1 },
"evidence": {
"type": "string",
"minLength": 1,
"description": "Expected source, ELF, disassembly, or runtime proof for the teaching step."
},
"code_ref": { "type": "string" }, "code_ref": { "type": "string" },
"progress_id": { "type": "string", "minLength": 1 }, "progress_id": { "type": "string", "minLength": 1 },
"snapshot_ref": { "snapshot_ref": {
@@ -981,8 +1113,67 @@
"minLength": 1, "minLength": 1,
"description": "Dokładny numer lub etykieta wiadomości PlantUML wybieranej w inline SVG." "description": "Dokładny numer lub etykieta wiadomości PlantUML wybieranej w inline SVG."
}, },
"svg_target": {
"type": "string",
"minLength": 1,
"description": "Stable PlantUML SVG element id. If absent, the renderer focuses the svg_label row."
},
"snapshot": { "$ref": "#/$defs/umlSnapshot" } "snapshot": { "$ref": "#/$defs/umlSnapshot" }
}, },
"allOf": [
{
"if": {
"properties": { "mode": { "const": "CODE" } },
"required": ["mode"]
},
"then": { "not": { "required": ["snapshot_ref"] } }
}
],
"additionalProperties": false
},
"viewpoint": {
"type": "object",
"required": ["id", "label", "status"],
"properties": {
"id": {
"enum": ["A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8"]
},
"label": { "type": "string", "minLength": 1 },
"subtitle": { "type": "string" },
"status": { "enum": ["enabled", "unavailable"] },
"target_label": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9:.-]*$"
},
"reason": { "type": "string", "minLength": 1 }
},
"allOf": [
{
"if": { "properties": { "status": { "const": "enabled" } }, "required": ["status"] },
"then": { "required": ["target_label"] }
},
{
"if": { "properties": { "status": { "const": "unavailable" } }, "required": ["status"] },
"then": { "required": ["reason"] }
}
],
"additionalProperties": false
},
"debugStrategy": {
"type": "object",
"required": ["id", "kind", "action", "expected_observations", "assertions", "evidence_fields"],
"properties": {
"id": { "type": "string", "minLength": 1 },
"kind": { "enum": ["code", "run"] },
"prerequisites": { "$ref": "#/$defs/stringList" },
"code_ref": { "type": "string", "minLength": 1 },
"action": { "enum": ["open", "replay"] },
"layout": { "$ref": "#/$defs/stringList" },
"commands": { "$ref": "#/$defs/stringList" },
"expected_observations": { "$ref": "#/$defs/stringList" },
"assertions": { "$ref": "#/$defs/stringList" },
"evidence_fields": { "$ref": "#/$defs/stringList" }
},
"additionalProperties": false "additionalProperties": false
}, },
"umlSequencePhase": { "umlSequencePhase": {
@@ -1006,7 +1197,7 @@
"required": ["kind"], "required": ["kind"],
"properties": { "properties": {
"kind": { "kind": {
"enum": ["allocator-memory-flow", "uml-sequence"] "enum": ["allocator-memory-flow", "uml-sequence", "uml-class"]
}, },
"storage_key": { "storage_key": {
"type": "string", "type": "string",
+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()
+868 -21
View File
File diff suppressed because it is too large Load Diff
+1
View File
File diff suppressed because one or more lines are too long
+61
View File
File diff suppressed because one or more lines are too long