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:
+56
-4
@@ -45,6 +45,17 @@ której powstaje `https://<url_host_uuid>/<doc_uuid>`.
|
||||
`show_repository_qr: true` wymaga `repository_url`. Adres podglądu `localhost`
|
||||
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
|
||||
|
||||
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
|
||||
`"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
|
||||
nagłówek sekcji, a kolejne są stronami kontynuacji z powtórzoną tabliczką
|
||||
zasobu. Widok ekranowy rozdziela płótna odstępem `5cm`; reguła nie wpływa na
|
||||
wydruk.
|
||||
stron w TeX i buduje osobne płótna A4 w React/HTML. Diagram interaktywny sam
|
||||
renderuje zwarty, jednakowy nagłówek na każdej części; zewnętrzny nagłówek
|
||||
sekcji nie przesuwa pierwszego kafla względem pozostałych.
|
||||
|
||||
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
|
||||
ma zawierać wyłącznie jawnie wymienione strony, ustawia na poziomie głównym
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,725 @@
|
||||
# A1–A8 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
|
||||
|
||||
A1–A8 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. A1–A8 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 A1–A8.
|
||||
- **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. A1–A8 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.
|
||||
|
||||
### A1–A8 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
File diff suppressed because it is too large
Load Diff
+1449
-165
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,23 @@
|
||||
"debug_checkpoints": {
|
||||
"$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": {
|
||||
"type": "boolean",
|
||||
"description": "Czy React/HTML ma dodać końcową stronę słownika WE/EN/EK/KW."
|
||||
@@ -309,6 +326,11 @@
|
||||
"type": "string",
|
||||
"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"] },
|
||||
"doc_uuid": {
|
||||
"type": "string",
|
||||
@@ -605,6 +627,27 @@
|
||||
"enum": ["flow", "one-per-page"],
|
||||
"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": {
|
||||
"$ref": "#/$defs/stringList"
|
||||
},
|
||||
@@ -674,6 +717,72 @@
|
||||
},
|
||||
"interactive": {
|
||||
"$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
|
||||
@@ -883,6 +992,11 @@
|
||||
"type": "object",
|
||||
"required": ["stop"],
|
||||
"properties": {
|
||||
"event_id": {
|
||||
"type": "string",
|
||||
"pattern": "^E[0-9]{2}$",
|
||||
"description": "Semantic event shared by comparable cards; the snapshot id remains card-local."
|
||||
},
|
||||
"stop": {
|
||||
"type": "object",
|
||||
"required": ["symbol", "offset"],
|
||||
@@ -962,12 +1076,30 @@
|
||||
},
|
||||
"umlSequenceStep": {
|
||||
"type": "object",
|
||||
"required": ["id", "number", "label", "description", "snapshot"],
|
||||
"required": ["id", "number", "label", "description"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "minLength": 1 },
|
||||
"number": { "type": "integer", "minimum": 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 },
|
||||
"evidence": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Expected source, ELF, disassembly, or runtime proof for the teaching step."
|
||||
},
|
||||
"code_ref": { "type": "string" },
|
||||
"progress_id": { "type": "string", "minLength": 1 },
|
||||
"snapshot_ref": {
|
||||
@@ -981,8 +1113,67 @@
|
||||
"minLength": 1,
|
||||
"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" }
|
||||
},
|
||||
"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
|
||||
},
|
||||
"umlSequencePhase": {
|
||||
@@ -1006,7 +1197,7 @@
|
||||
"required": ["kind"],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": ["allocator-memory-flow", "uml-sequence"]
|
||||
"enum": ["allocator-memory-flow", "uml-sequence", "uml-class"]
|
||||
},
|
||||
"storage_key": {
|
||||
"type": "string",
|
||||
|
||||
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()
|
||||
+868
-21
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+61
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user